Pythonista 1.6 Beta
So, with Apple's new TestFlight, I can finally have a lot more beta testers, and won't have to worry about device limits so much.
If you'd like to play with the next version of Pythonista before it hits the App Store, please send me an email with your Apple ID, and I'll see what I can do. I don't plan to use up all the slots yet (I might need some later), but if you've been posting here, there's a very good chance that I can give you access to the beta.
As for what to expect, here's a list of the new features in the current beta:
- Improved support for the iPhone 6 and 6 Plus screen sizes
- Removing files from the library moves them to a trash folder instead of deleting them immediately.
- URLs in console output are 'linkified' automatically
remindersmodule for accessing the iOS Reminders database (read/write)
cbmodule for connecting to Bluetooth LE peripherals (experimental)
- New dialogs module as an easy-to-use alternative to the ui module when you just need simple data entry. This also contains functions to import files from the iCloud Drive document picker (iOS 8 only). – note: this is broken in the current beta, but will be fixed in the next few days
A possible backward compatibility issue. A number of existing scripts that have been written assume that the default bound/frame of ui.View is something large. It now defaults to something relatively small like 100x100. Net result is that the ui of some existing scripts comes up in a tiny box and unusable. This change in default may be a fix to some other problem so this is just an FYI. I have had to fix several scripts to set a proper frame in the init section of their derived view class.
It might be a good idea to add a sample that shows best practice for setting sizes of views based on the devices orientation and ui.get_screen_size() and uses flex attributes.
Also - this is for apps that generate their ui programmatically, not via pui files.
UPDATE: The 'sheet' presentation option has been changed in 1.6 to allow its frame to be user controlled. The default seems to be 100x100 which is different then what it was in past releases.
Email export indeed seems to be broken on iPad. Should be relatively easy to fix.
A possible backward compatibility issue. A number of existing scripts that have been written assume that the default bound/frame of ui.View is something large. It now defaults to something relatively small like 100x100.
The default frame/size of
ui.Viewactually hasn't changed, but the 'sheet' presentation mode now uses the current size of a view instead of a fixed (larger) size. This is basically a new feature of iOS 8, and while I could effectively disable this, it can be quite useful. The alternative would be to actually change the default frame of new views (which has always been 100x100 for most view types), but this could break other things...
@omz As for the ui.View thing, putting this in the release notes would help a lot of people, even though it is not a change in the actual app.
Having the cb module for BLE is very cool, but I'd like to get the manufacturer specific data while scanning. And the RSSI, and set the scan option to YES. This is for generic advertising BLE peripherals. Also, support for iBeacons would be great -- it is a separate CoreLocation API on IOS.
I second @bvwelch's requests. The support for RSSI and a look at iBeacons is going to be required for many appplications. This allows you to get data without having to connect. The other item I would like is providing the peripheral to all the callbacks. This becomes important as soon as you need to support more then one peripheral. It also becomes a performance issue when you have to somehow match data coming out of a characteristic with the peripheral sending the data. It is tricky to do this on the python side since the callbacks are running in multiple threads.
Also - the CentralManager supports state preservation and restoration but you have to ask for it and supply a key to the init procedure. This would be extremely useful for writing a real world app that can be terminated and restarted in a variety of ways. Right now you have to do a complete rescan every time you run your script which can take some time.
Is it too late to request TouchID support? At the minimum to replace
keychain.master_password, but ideally to be able to use within a script.
Is it too late to request TouchID support? At the minimum to replace keychain.master_password, but ideally to be able to use within a script.
Not sure yet, it's definitely on the roadmap, but might not make it into 1.6.
Found a bug that will cause the console to become completely useless.
I have a program that redirects the
sys.stdoutstream to a
ui.TextFieldvia the following kind of class:
class OutputField (object): def __init__(self, textfield): self.out = textfield def write(self, s): self.out.text += s
sys.stdout = OutputField(v['textfield1'])and now the console will not do anything; not even autocomplete code. I found a quick fix on my iPhone 6 by simply uninstalling and reinstalling the beta, but I have enough projects on my iPad 3 to make that too impractical at the moment.
If anyone has an idea to fix this, that would be greatly appreciated. I gave tried
sys.stdout = sys.__stdout__, which is supposed to contain the origin system output, but it did nothing.
This also effects other projects as well; as nothing will print to the console.
After searching frantically around the web, I finally tried restarting Pythonista (I didn't immediately do this at first because it didn't work the first time I tried on my iPhone). Anyways, I found out that saving a reference to stdout is always a good idea.
It's late, I hope this edit helps people in the future.
Just wondering, what happens when the beta 'expires'? Does it revert back to 1.5?
@blmacbeth, this is due to the unusual way that Pythonista uses to print output to the console. What is stored in
sys.__stdout__is the default Python output stream (a file object pointing to file descriptor 1, which is Unix
stdout) but (presumably) because of iOS limitations that goes into nowhere.
Instead Pythonista has a "secret" module named
_outputcapturewith a few built-in functions to handle text I/O through the Pythonista console.
sys.stdoutis also replaced with a
writemethod encodes the string and shows it using
_outputcapture. Because the
StdoutCatcherclass isn't stored anywhere by default, it is lost when you change
sys.stdoutand it is hard to restore afterwards.
The reason why the interactive code completion stops working as well is simple - whenever Pythonista needs a list of possible completions, it creates and runs a function named
_pythonista_complete_line, which uses some standard Python module to do the completion and prints all possible results to
sys.stdout, where they are caught by Pythonista and displayed in the completion list. (Yes, it is possible to monkey-patch
sys.stdoutand hack the code completion mechanism.)
Here's an emergency script that you can run in case the in/out/err streams get lost. It will replace them with objects practically identical to Pythonista's.
import importcompletion as _importcompletion import _outputcapture if ( sys.stdin.__class__.__name__ != "StdinCatcher" or sys.stdout.__class__.__name__ != "StdoutCatcher" or sys.stderr.__class__.__name__ != "StderrCatcher" ): _outputcapture.CaptureStdout(b"I'm alive.\n") class StdinCatcher(object): def __init__(self): self.encoding = "utf8" def read(self, limit=-1): return _outputcapture.ReadStdin(limit) def readline(self): return _outputcapture.ReadStdin() sys.stdin = StdinCatcher() _outputcapture.CaptureStdout(b"Rebuilt StdinCatcher and sys.stdin...\n") class StdoutCatcher(object): def __init__(self): self.encoding = "utf8" def flush(self): pass def write(self, s): if isinstance(s, str): _outputcapture.CaptureStdout(s) elif isinstance(s, unicode): _outputcapture.CaptureStdout(s.encode("utf8")) def writelines(self, lines): for line in lines: self.write(line + "\n") sys.stdout = StdoutCatcher() _outputcapture.CaptureStdout(b"Rebuilt StdoutCatcher and sys.stdout...\n") class StderrCatcher(object): def __init__(self): self.encoding = "utf8" def flush(self): pass def write(self, s): if isinstance(s, str): _outputcapture.CaptureStderr(s) elif isinstance(s, unicode): _outputcapture.CaptureStderr(s.encode("utf8")) def writelines(self, lines): for line in lines: self.write(line + "\n") sys.stderr = StderrCatcher() _outputcapture.CaptureStdout(b"Rebuilt StderrCatcher and sys.stderr...\n")
@dgelessus Thank you for that excellent explanation. Where did you find all of that out? Is it hidden deep within the documentation, or have you looked through some source code? Just wondering.
As far as I know none of this is documented anywhere, but Python has great introspection capabilites and an interactive prompt. With a little brute-forcing I managed to figure out much of the iOS/Python interaction internals.
_outputcapturemodule is in the list of
sys.builtin_module_namesand the syntax of its functions is (relatively) easy to guess. It is always imported (because it is necessary for console IO), but it is hidden from the autocompletion list. If you want to experiment with it, use
import _outputcapture as outputcaptureto import it under a name that isn't hidden from autocompletion.
Pythonista's custom in/out/err stream replacements are normal Python classes/objects, and although there is no source code for them, they can be decompiled using the
dismodule. Python bytecode is not too hard to read in its decompiled form, so it was relatively easy to translate it back into Python source code.
Figuring out the interactive autocompletion was a little more challenging. At first I only noticed that when typing a single
_into the console without hitting Return, one of the completions is
_pythonista_complete_line. However when trying to use that function it isn't there - it is only created when Pythonista needs a line completion, and once the results are returned it is immediately deleted. It still lists itself in the completion list though, because it exists in the main namespace until it is fully executed once.
Later I found out how I could get a reference to the actual function object - I (accidentally) shadowed the built-in
settype with another function, which caused the line completion function to raise exceptions every time it ran. (This made the interactive prompt completely unusable, so from then on I had to use short Python scripts as a console replacement.) Because the exceptions occurred inside of
_pythonista_complete_line, their tracebacks had a reference to the function, which I could
dis-assemble to reconstruct the code:
def pythonista_complete_line(): import string import sys from rlcompleter import Completer completions = _importcompletion.complete("current input text", "text") if completions is not None and len(completions) > 0 and completions != "importcompletion": sys.stdout.write(string.join(completions, "|")) else: completer = Completer() completions =  exclusions = set(["___omz_complete___(", "_importcompletion", "_outputcapture"]) for i in range(1000): c = completer.complete("current input text") if not c: break if c not in exclusions: completions.append(c) if len(completions) > 0: sys.stdout.write(string.join(completions, "|")) else: sys.stdout.write("|")
Nice detective work there, @dgelessus! :)
Omg, thank you! I had discovered the function, but was never able to figure out the interface, or hoe it got access to the current input text line. Will this work for allowing completions within Cmd loops? Or ... Ok, your
current input textIs really a placeholder for some inaccessible string?
I would LOVE to implement a history system usable within pdb.pm() for example.
Right, I forgot to explain the placeholder strings I used. For whatever reason
_pythonista_complete_lineis constructed with the current input string in it as literals. (Who needs function arguments anyway?) What is
"current input text"in my example is the full input line, and
"text"is the last word of the input (i. e.
"current input text".rsplit(" ", 1)) because
@JonB, the line completion is only called when in interactive Python mode. Pythonista doesn't attempt any completion during
raw_inputand such, so there's nowhere to inject your own code into. I don't think there would be any way to get the input before Pythonista executes it, so there isn't much use in hacking the line completion at the moment. In any case you'd need some way to differentiate normal output and completion lists -
sys.stdoutand there's no way to change that.
As far as I can tell the input history is separate from line completion, the two last matching history items are always added under the result list no matter what the code completer returns.
PS: Found something. This code should work as it is with no preparations necessary.
def write(self, s): ##if s == "|": inside_pcl = False try: raise Exception() except Exception: if sys.exc_info().tb_frame.f_back.f_code.co_name == "_pythonista_complete_line": inside_pcl = True if inside_pcl: self.real_write("A suggestion" if s == "|" else (s + "|A suggestion")) else: self.real_write(s) write_im = type(sys.stdout.write)(write, sys.stdout, type(sys.stdout)) if not hasattr(sys.stdout, "real_write"): sys.stdout.real_write = sys.stdout.write sys.stdout.write = write_im
I have recently installed the latest 1.6 beta over an existing 1.5 on both an iPad and an iPhone. On both devices I lost all keychain data (dropbox credentials) during the update, which has never happened before. As described by others, I also once got a file blanked out. It was a file that I just had left open when switching to another app and at some later time Pythonista got unloaded.
I've mainly been testing the cb module, successfully connecting to a WowWee MiP self-balancing (segway like) toy robot. The robot uses a single characteristic to send commands, and another characteristic with notifications for responses. Though I am new to BLE, everything seems to work. At least when I'm just sending a few commands and printing responses to the console.
However, when I tried to make a simple driving app using the ui module, I started to get a lot of crashes. I suspect these are due to threading issues, but I'm not proficient enough in Python yet to understand how to avoid them. The robot can drive continously at a given speed, but the command needs to be repeated every 50 ms or the robot will stop as a safety measure. I tried to accomplish this using ui.delay(), which crashed immediately when used by itself. But in combination with ui.in_background it sometimes worked for a while before crashing.
But I assume the crashes due to threading problems aren't specific to the beta or the cb module, and should be discussed elsewhere. Unless Ole wants to share any special threading considerations that only apply to the cb module.
The MiP sounds like a fun thing to hack with. Have you tried using the
yaw, pitch, and rollof your iOS devise to control the MiP?
motion.get_attitude()gives you access to these values.
I believe that there are issues with threads but have not been able to pin anything down enough to give omz anything to work with. I spent a great deal of time working on a way of logging that did not interfere with the cb modules natural functionality. I kept having the issue that a crashing problem (crash or weird behavior) would go away when I got the logging going. I can say for sure that each callback is run in a new thread each time and it looks a lot like IOS is using some kind of thread pool. The thread names seem to count up and then start over. I have been able to get dozens of different threads going using several peripherals. What all this means is that any code you write in a callback needs to be thread safe and some elements of the python libraries may not be thread safe in these callbacks. What omz needs is a sample app that clearly shows if one of the callbacks can be running simultaneously in multiple threads. Perhaps your app is just doing something that takes a long time in a callback and somehow gets python to context switch and allow the callback to get re-enterred? I am drawing at straws here. I would be willing to get a WowWee MIP in order to help repo this. This would be a good one to use as the sample app for cb in the future IMHO. Anything with a fairly high data transfer rate would be the best. I have been using a BLE MIDI interface and these tend to generate lots of data.
So, I gather the history is handled through some builtin that we cannot access? (Builtins can't be disassembled, right?)
I was hoping for the ability to add to history programatically, oh well. That leads to a feature request: @omz Add console.history command to get / set history. And/or some way to enable completion on raw_input ( which we could finesse into our own history system :)
Reported elsewhere but repeated here for completeness:
import photos # When you tap 'Cancel' in the image picker... x = photos.pick_image() # x = None which is the correct behavior. y = photos.pick_image(include_metadata=True) # Exception thrown which is a bug.