I had this on the someday section of my todo list, but was prompted into action by a pull request. In the end, getting Python 2 and 3 compatible code was easier than I thought.
I started with the pull request code and got it so the tests were mostly passing in 3 and then worked to make it compatible with 2 again. I guess this is all old news now for most (then again, maybe not as Python 2 shows no signs of dying), but if you’ve only got something small I wouldn’t bother with things like 2to3 (which produced worse results than the pull request) and instead just port things manually. Then make everything work for both by simply using
sys.version conditionals instead of using compatibility libraries like six or future.
Import conditionally and use Python 2 names
So I could continue to use
urllib2 throughout the code, for Python 3,
urllib.request gets imported as
import sys if sys.version_info > (3, 0): import urllib.request as urllib2 import urllib.error from urllib.error import HTTPError import urllib.parse as urllib else: import urllib2 from urllib2 import HTTPError import urllib
Dummy Monkey Patching / Subclassing
Python 2 still requires (will always) monkey patching to add HTTP DELETE to urllib2. But Python 3 doesn’t. How to do for one, but not the other? Simply pass through. This might be obvious to some, but, for me, I thought this was a great little trick.
class Request(urllib2.Request): if sys.version_info < (3, 0): #Code here return urllib2.Request.get_method(self) else: pass
Yes, there are better HTTP libraries that would make all this easier on Python 2 and 3, but simplenote.py needs to work with the base Python install.
Use Exceptions as an alternative to version checking
Yeah, as I write this, I guess I should have just done this one way (
sys.version), but you can do things like this:
try: #For Python 2 values = base64.b64encode(bytes(auth_params,'utf-8')) except TypeError: #For Python 3 values = base64.encodestring(auth_params)
try: #For Python 2 return str(self.token,'utf-8') except TypeError: #For Python 3 return self.token
One really odd thing I found was that under Python 3.4 expectedFailures passing would lead to the test run failing.
Don’t Expect Failures if you don’t expect failures?
This has surely got to be one of those programming oddities as opposed to a proper bug* in Python 3, but I had to remove the use of
@unittest.expectedFailure as when the test passed it caused the whole test run to fail in Python 3 whereas it would work as intended in Python 2. Fortunately another pull request had much improved the testing making this particular test much more stable.
* - Sometimes you just need to get something working and move on, rather than try to get to the bottom of a rabbit hole that’s probably still being dug by a hundred rabbits in all directions.