Pyto and Shortcuts again

So, it turns out™︎ yesterday’s widget/shortcut/script for generating passwords needed more testing. Every individual component worked, and they worked together well on a single device, but as I learned last night, they didn’t work when shifting from one device to another.

Shortcuts widget

Here’s the problem. Let’s say I’m on my iPhone and I generate a password by tapping the button in the Shortcuts widget. It works fine. Later, I’m on my iPad and I try to generate a password the same way. I tap the button, and it gives me this error:

Error message for widget

To fix this error and get the shortcut to run on the iPad, I have to open the New Password shortcut,

New Password shortcut

and reselect the script that Pyto runs. Yes, I know it says it’s going to run the script pw.py, and that’s the script I want it to run, but for some reason Pyto doesn’t believe it, and I have to select pw.py again as the script to run. Now the shortcut works perfectly on my iPad and will continue to do so.

But—and you know where this is going, right?—now the shortcut won’t run on my iPhone until I go into Shortcuts and reselect pw.py on that device. And after I’ve done that, the shortcut won’t run on the iPad. And so on, ping-ponging back and forth. For some reason, the Run Script action for Pyto doesn’t know that the pw.py on the iPhone is the same pw.py that’s on the iPad. Even though they’re the same file, synced through iCloud.

I made a note to send a bug report to Pyto about this, but in the meantime, I wanted a shortcut that works.

The first step was to change the Run Script action to Run Code. This allowed me to keep the Python source code in the shortcut itself, which eliminated the pw.py confusion.

New New Password shortcut with Run Code action

Then I needed to change how the script accessed the list of words. Recall that originally, pw.py read a file called words.py that was saved in the same directory as pw.py itself. The code for that was simple:

python:
with open('words.py') as f:
  words = f.readlines()

But apparently the change from Run Script to Run Code changed the environment under which the code runs, so open('words.py') didn’t work anymore. I needed a more robust way to refer to the file of words.

The solution was bookmarks, which work in Pyto more or less the same way they work in Scriptable: you establish a bookmark to a file saved anywhere in iCloud Drive, and from that point on you can refer to that file through the bookmark. Establishing the bookmark is done through user interaction, which satisfies iOS’s privacy concerns, and Pyto then keeps track of the file and handles the permissions.

By switching to bookmarks, I eliminated the need to keep words.py in the same directory as the pw.py script. Thinking that I could use bookmarks for other scripts that access data files, I created a common-data folder in my iCloud Drive root directory and put the word.py file there. And I changed its name to words.txt, which is what it should have been in the first place.

So pw.py is now this:

python:
 1:  from secrets import choice, randbelow
 2:  import pasteboard
 3:  import bookmarks
 4:  
 5:  # Get the list of words 4-6 characters long.
 6:  wordPath = bookmarks.FileBookmark('words46').path
 7:  with open(wordPath) as f:
 8:    words = f.readlines()
 9:  
10:  # Select 4 words from the list.
11:  pwords = [ choice(words).strip() for i in range(4) ]
12:  
13:  # Add a numeral and a "special" character.
14:  # Capitalize one of the words.
15:  numeral = str(randbelow(10))
16:  special = choice('!@#$%^&*')
17:  pNum = randbelow(4)
18:  pSpec = randbelow(4)
19:  pCap = randbelow(4)
20:  pwords[pNum] = pwords[pNum] + numeral
21:  pwords[pSpec] = pwords[pSpec] + special
22:  pwords[pCap] = pwords[pCap].capitalize()
23:  
24:  # Assemble the password and put it on the clipboard.
25:  pw = '-'.join(pwords)
26:  pasteboard.set_string(pw)

The only difference is the bookmarks stuff in Lines 6–8.

I had to first run this from within Pyto to create the bookmark. When it hit Line 6, the file picker appeared, and I was asked to select the file that will be given the words46 bookmark. I navigated through iCloud Drive to select the words.txt file. From that point on, Pyto knows what words46 is and won’t ask me again. Well, it won’t ask me on that device. Apparently bookmarks don’t sync—which is probably right from a security point of view—so I had run pw.py on each device to establish the bookmark everywhere.

With that done, I copied the new source code for pw.py and pasted it into the Run Code action of the shortcut. Now I have a shortcut and a widget button that works on all my devices and doesn’t get confused anymore.