Supporting Multiple Python Versions In Your Code? - python

Today I tried using pyPdf 1.12 in a script I was writing that targets Python 2.6. When running my script, and even importing pyPdf, I get complaints about deprecated functionality (md5->hashsum, sets). I'd like to contribute a patch to make this work cleanly in 2.6, but I imagine the author does not want to break compatibility for older versions (2.5 and earlier).
Searching Google and Stack Overflow have so far turned up nothing. I feel like I have seen try/except blocks around import statements before that accomplish something similar, but can't find any examples. Is there a generally accepted best practice for supporting multiple Python versions?

There are two ways to do this:
(1) Just like you described: Try something and work around the exception for old versions. For example, you could try to import the json module and import a userland implementation if this fails:
try:
import json
except ImportError:
import myutils.myjson as json
This is an example from Django (they use this technique often):
try:
reversed
except NameError:
from django.utils.itercompat import reversed # Python 2.3 fallback
If the iterator reversed is available, they use it. Otherwise, they import their own implementation from the utils package.
(2) Explicitely compare the version of the Python interpreter:
import sys
if sys.version_info < (2, 6, 0):
# Do stuff for old version...
else:
# Do 2.6+ stuff
sys.version_info is a tuple that can easily be compared with similar version tuples.

You can certainly do
try:
import v26
except ImportError:
import v25
Dive Into Python—Using Exceptions for Other Purposes

Multiple versions of Python are supported here. You can a) conditionally use the newer version, which takes a little work, or b) turn off the warnings, which should really be the default (and is on newer Pythons).

Related

What's the Pythonic way to write conditional statements based on installed modules?

Coming from a C++ world I got used to write conditional compilation based on flags that are determined at compilation time with tools like CMake and the like. I wonder what's the most Pythonic way to mimic this functionality. For instance, this is what I currently set depending on whether a module is found or not:
import imp
try:
imp.find_module('petsc4py')
HAVE_PETSC=True
except ImportError:
HAVE_PETSC=False
Then I can use HAVE_PETSC throughout the rest of my Python code. This works, but I wonder if it's the right way to do it in Python.
Yes, it is ok. You can even issue an import directly, and
use the modulename itself as the flag - like in:
try:
import petsc4py
except ImportError
petsc4py = None
And before any use, just test for the truthfulness of petsc4py itself.
Actually, checking if it exists, and only then trying to import it, feels unpythonic due to the redundancy, as both actions trigger an ImportError all the same. But having a HAVE_PETSC variable for the checkings is ok - it can be created after the try/except above with HAVE_PETSC = bool(petsc4py)
The way you're doing it is more-or-less fine. In fact, The python standard library uses a similar paradigm of "try to import something and if it's not valid for some reason then set a variable somehow" in multiple places. Checking if a boolean is set later in the program is going to be faster than doing a separate try/except block every single time.
In your case it would probably just be better to do this, though:
try:
import petsc4py
HAVE_PETSC = True
except ImportError:
HAVE_PETSC = False
What you have works on a paradigm level, but there's no real reason to go through importlib in this case (and you probably shouldn't use imp anyway, as it's deprecated in recent versions of python).

PEP 8 and deferred import

I am working on a large Python program which makes use of a multitude of modules depending on command-line options, in particular, numpy. We have recently found a need to run this on a small embedded module which precludes the use of numpy. From our perspective, this is easy enough (just don't use the problematic command line options.)
However, following PEP 8, our import numpy is at the beginning of each module that might need it, and the program will crash due to numpy not being installed. The straightforward solution is to move import numpy from the top of the file to the functions that need it. The question is, "How bad is this"?
(An alternative solution is to wrap import numpy in a try .. except. Is this better?)
Here is a best practice pattern to check if a module is installed and make code branch depending on it.
# GOOD
import pkg_resources
try:
pkg_resources.get_distribution('numpy')
except pkg_resources.DistributionNotFound:
HAS_NUMPY = False
else:
HAS_NUMPY = True
# You can also import numpy here unless you want to import it inside the function
Do this in every module imports having soft dependency to numpy. More information in Plone CMS coding conventions.
Another idiom which I've seen is to import the module as None if unavailable:
try:
import numpy as np
except ImportError:
np = None
Or, as in the other answer, you can use the pkg_resources.get_distribution above, rather than try/except (see the blog post linked to from the plone docs).
In that way, before using numpy you can hide numpy's use in an if block:
if np:
# do something with numpy
else:
# do something in vanilla python
The key is to ensure your CI tests have both environments - with and without numpy (and if you are testing coverage this should count both block as covered).

python _2or3 module?

I am writing a module to let me write code in python 3, but still run it in 2. It looks surprisingly easy actually... anything else I should add? From my (limited) flailing on the interactive interpreter, the future imports do not affect python 3 and are viewed as redundant.
# _2or3.py
'''
Common usage:
from __future__ import print_function, nested_scopes, division, absolute_import, unicode_literals
from _2or3 import *
'''
import sys
if sys.version[0] == '2':
range = xrange
input = raw_input
Obviously there are some things you cannot do that you would normally be able to do in 3 (like dictionary compressions), and there are a few gotchas between the languages (like bytecodes. It looks like you should NEVER use bytes)
Any comments would be appreciated.
Check out six, that already does this, and loads more. It also has methods that helps you do binary and Unicode in both versions. Not all techniques you need to do can be done this way, though, especially if you need to support Python 2.5 or earlier. I tried to cover most of them in the book, but I'm sure I've missed out on some.

Check version of Python library that doesn't define __version__

I'm dealing with a Python library that does not define the __version__ variable (sqlalchemy-migrate), and I want to have different behavior in my code based on what version of the library I have installed.
Is there a way to check at runtime what version of the library is installed (other than, say, checking the output of pip freeze)?
This being Python, the accepted way of doing this is generally to call something in the library that behaves differently depending on the version you have installed, something like:
import somelibrary
try:
somelibrary.this_only_exists_in_11()
SOME_LIBRARY_VERSION = 1.1
except AttributeError:
SOME_LIBRARY_VERSION = 1.0
A more elegant way might be to create wrapper functions.
def call_11_feature():
try:
somelibrary.this_only_exists_in_11()
except AttributeError:
somelibrary.some_convoluted_methods()
somelibrary.which_mimic()
somelibrary.the_11_feature()
pkg_resources may help, but you'll need to use the package name:
>>> import pkg_resources
>>> env = pkg_resources.Environment()
>>> env['sqlalchemy-migrate'][0].version
'0.6.2.dev'
If the library doesn't know its own version, then you are basically SOL. However, if one of the versions you want to support would raise an exception if the code went down the "wrong" path, you could use a try/except block.
Occasionally you can evaluate the path of the library and it will be in there somewhere... /usr/lib/python2.6/site-packages/XlsXcessive-0.1.6-py2.6.egg

How to gracefully deal with failed future feature (__future__) imports due to old interpreter version?

How do you gracefully handle failed future feature imports? If a user is running using Python 2.5 and the first statement in my module is:
from __future__ import print_function
Compiling this module for Python 2.5 will fail with a:
File "__init__.py", line 1
from __future__ import print_function
SyntaxError: future feature print_function is not defined
I'd like to inform the user that they need to rerun the program with Python >= 2.6 and maybe provide some instructions on how to do so. However, to quote PEP 236:
The only lines that can appear before
a future_statement are:
The module docstring (if any).
Comments.
Blank lines.
Other future_statements.
So I can't do something like:
import __future__
if hasattr(__future__, 'print_function'):
from __future__ import print_function
else:
raise ImportError('Python >= 2.6 is required')
Because it yields:
File "__init__.py", line 4
from __future__ import print_function
SyntaxError: from __future__ imports must occur at the beginning of the file
This snippet from the PEP seems to give hope of doing it inline:
Q: I want to wrap future_statements
in try/except blocks, so I can use
different code depending on which
version of Python I'm running. Why
can't I?
A: Sorry! try/except is a runtime
feature; future_statements are
primarily compile-time gimmicks, and
your try/except happens long after the
compiler is done. That is, by the
time you do try/except, the semantics
in effect for the module are already a
done deal. Since the try/except
wouldn't accomplish what it looks
like it should accomplish, it's simply
not allowed. We also want to keep
these special statements very easy to
find and to recognize.
Note that you can import __future__
directly, and use the information in
it, along with sys.version_info, to
figure out where the release you're
running under stands in relation to a
given feature's status.
Ideas?
"I'd like to inform the user that they need to rerun the program with Python >= 2.6 and maybe provide some instructions on how to do so."
Isn't that what a README file is for?
Here's your alternative. A "wrapper": a little blob of Python that checks the environment before running your target aop.
File: appwrapper.py
import sys
major, minor, micro, releaselevel, serial = sys.version_info
if (major,minor) <= (2,5):
# provide advice on getting version 2.6 or higher.
sys.exit(2)
import app
app.main()
What "direct import" means. You can examine the contents of __future__. You're still bound by the fact the a from __future__ import print_function is information to the compiler, but you can poke around before importing the module that does the real work.
import __future__, sys
if hasattr(__future__, 'print_function'):
# Could also check sys.version_info >= __future__. print_function.optional
import app
app.main()
else:
print "instructions for upgrading"
A rather hacky but simple method I've used before is to exploit the fact that byte literals were introduced in Python 2.6 and use something like this near the start of the file:
b'This module needs Python 2.6 or later. Please do xxx.'
This is harmless in Python 2.6 or later, but a SyntaxError in any earlier versions. Anyone trying to compile your file will still get an error, but they also get whatever message you want to give.
You might think that as you will have to have this line after your from __future__ import print_function then it will be the import that generates the SyntaxError and you won't get to see the useful error message, but strangely enough the later error takes precedence. I suspect that as the error from the import isn't really a syntax error in itself it isn't raised on the first compilation pass, and so real syntax errors get raised first (but I'm guessing).
This might not meet you criteria for being 'graceful', and it is very Python 2.6 specific, but it is quick and easy to do.
Just put a comment on the same line with the "from __future__ import ...", like this:
from __future__ import print_function, division # We require Python 2.6 or later
Since Python displays the line containing the error, if you try to run the module with Python 2.5 you'll get a nice, descriptive error:
from __future__ import print_function, division # We require Python 2.6 or later
SyntaxError: future feature print_function is not defined

Categories