Skip to content

Stop relying on 2to3 and use six.py for compatibility instead #2226

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Sep 3, 2013
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions boilerplate.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
# For some later history, see
# http://thread.gmane.org/gmane.comp.python.matplotlib.devel/7068

from __future__ import absolute_import, division, print_function, unicode_literals
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be successful with using six, this __future__ import should be boilerplate at the top of every file.


import six
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking here is to always include this in every file to make moving code around easier. The things it has are "core" enough that they should probably be considered "built-ins".


import os
import inspect
import random
Expand Down
4 changes: 3 additions & 1 deletion doc/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@
'sphinxext.github',
'numpydoc']


try:
import numpydoc
except ImportError:
Expand All @@ -53,6 +52,9 @@
# The suffix of source filenames.
source_suffix = '.rst'

# This is the default encoding, but it doesn't hurt to be explicit
source_encoding = "utf-8"

# The master toctree document.
master_doc = 'contents'

Expand Down
1 change: 1 addition & 0 deletions doc/devel/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
:maxdepth: 2

coding_guide.rst
portable_code.rst
license.rst
gitwash/index.rst
testing.rst
Expand Down
119 changes: 119 additions & 0 deletions doc/devel/portable_code.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
Writing code for Python 2 and 3
-------------------------------

As of matplotlib 1.4, the `six <http://pythonhosted.org/six/>`_
library is used to support Python 2 and 3 from a single code base.
The `2to3` tool is no longer used.

This document describes some of the issues with that approach and some
recommended solutions. It is not a complete guide to Python 2 and 3
compatibility.

Welcome to the ``__future__``
-----------------------------

The top of every `.py` file should include the following::

from __future__ import absolute_import, division, print_function, unicode_literals

This will make the Python 2 interpreter behave as close to Python 3 as
possible.

All matplotlib files should also import `six`, whether they are using
it or not, just to make moving code between modules easier, as `six`
gets used *a lot*::

import six

Finding places to use six
-------------------------

The only way to make sure code works on both Python 2 and 3 is to make sure it
is covered by unit tests.

However, the `2to3` commandline tool can also be used to locate places
that require special handling with `six`.

(The `modernize <https://pypi.python.org/pypi/modernize>`_ tool may
also be handy, though I've never used it personally).

The `six <http://pythonhosted.org/six/>`_ documentation serves as a
good reference for the sorts of things that need to be updated.

The dreaded ``\u`` escapes
--------------------------

When `from __future__ import unicode_literals` is used, all string
literals (not preceded with a `b`) will become unicode literals.

Normally, one would use "raw" string literals to encode strings that
contain a lot of slashes that we don't want Python to interpret as
special characters. A common example in matplotlib is when it deals
with TeX and has to represent things like ``r"\usepackage{foo}"``.
Unfortunately, on Python 2there is no way to represent `\u` in a raw
unicode string literal, since it will always be interpreted as the
start of a unicode character escape, such as `\u20af`. The only
solution is to use a regular (non-raw) string literal and repeat all
slashes, e.g. ``"\\usepackage{foo}"``.

The following shows the problem on Python 2::

>>> ur'\u'
File "<stdin>", line 1
SyntaxError: (unicode error) 'rawunicodeescape' codec can't decode bytes in
position 0-1: truncated \uXXXX
>>> ur'\\u'
u'\\\\u'
>>> u'\u'
File "<stdin>", line 1
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in
position 0-1: truncated \uXXXX escape
>>> u'\\u'
u'\\u'

This bug has been fixed in Python 3, however, we can't take advantage
of that and still support Python 2::

>>> r'\u'
'\\u'
>>> r'\\u'
'\\\\u'
>>> '\u'
File "<stdin>", line 1
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in
position 0-1: truncated \uXXXX escape
>>> '\\u'
'\\u'

Iteration
---------

The behavior of the methods for iterating over the items, values and
keys of a dictionary has changed in Python 3. Additionally, other
built-in functions such as `zip`, `range` and `map` have changed to
return iterators rather than temporary lists.

In many cases, the performance implications of iterating vs. creating
a temporary list won't matter, so it's tempting to use the form that
is simplest to read. However, that results in code that behaves
differently on Python 2 and 3, leading to subtle bugs that may not be
detected by the regression tests. Therefore, unless the loop in
question is provably simple and doesn't call into other code, the
`six` versions that ensure the same behavior on both Python 2 and 3
should be used. The following table shows the mapping of equivalent
semantics between Python 2, 3 and six for `dict.items()`:

============================== ============================== ==============================
Python 2 Python 3 six
============================== ============================== ==============================
``d.items()`` ``list(d.items())`` ``list(six.iteritems(d))``
``d.iteritems()`` ``d.items()`` ``six.iteritems(d)``
============================== ============================== ==============================

Numpy-specific things
---------------------

When specifying dtypes, all strings must be byte strings on Python 2
and unicode strings on Python 3. The best way to handle this is to
force cast them using `str()`. The same is true of structure
specifiers in the `struct` built-in module.
15 changes: 9 additions & 6 deletions doc/users/plotting/examples/pgf_preamble.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function, unicode_literals

import six

import matplotlib as mpl
mpl.use("pgf")
Expand All @@ -7,9 +10,9 @@
"text.usetex": True, # use inline math for ticks
"pgf.rcfonts": False, # don't setup fonts from rc parameters
"pgf.preamble": [
r"\usepackage{units}", # load additional packages
r"\usepackage{metalogo}",
r"\usepackage{unicode-math}", # unicode math setup
"\\usepackage{units}", # load additional packages
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On Python 2, there is no way to use a raw unicode string and represent \u, which is always interpreted as a unicode character, so we can't use raw strings here. We could use byte strings, but then we'd have to have a mechanism to not do that on Python 3.

Python 2

>>> ur'\u'
  File "<stdin>", line 1
SyntaxError: (unicode error) 'rawunicodeescape' codec can't decode bytes in position 0-1: truncated \uXXXX
>>> ur'\\u'
u'\\\\u'
>>> u'\u'
  File "<stdin>", line 1
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 0-1: truncated \uXXXX escape
>>> u'\\u'
u'\\u'

Python 3:

>>> r'\u'
'\\u'
>>> r'\\u'
'\\\\u'
>>> '\u'
  File "<stdin>", line 1
SyntaxError: (unicode error) 'unicodeescape' codec can't decode bytes in position 0-1: truncated \uXXXX escape
>>> '\\u'
'\\u'

"\\usepackage{metalogo}",
"\\usepackage{unicode-math}", # unicode math setup
r"\setmathfont{xits-math.otf}",
r"\setmainfont{DejaVu Serif}", # serif font via preamble
]
Expand All @@ -19,9 +22,9 @@
import matplotlib.pyplot as plt
plt.figure(figsize=(4.5,2.5))
plt.plot(range(5))
plt.xlabel(u"unicode text: я, ψ, €, ü, \\unitfrac[10]{°}{µm}")
plt.ylabel(u"\\XeLaTeX")
plt.legend([u"unicode math: $λ=∑_i^∞ μ_i^2$"])
plt.xlabel("unicode text: я, ψ, €, ü, \\unitfrac[10]{°}{µm}")
plt.ylabel("\\XeLaTeX")
plt.legend(["unicode math: $λ=∑_i^∞ μ_i^2$"])
plt.tight_layout(.5)

plt.savefig("pgf_preamble.pdf")
Expand Down
46 changes: 18 additions & 28 deletions lib/matplotlib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,9 @@
to MATLAB&reg;, a registered trademark of The MathWorks, Inc.

"""
from __future__ import print_function, absolute_import
from __future__ import absolute_import, division, print_function, unicode_literals

import six
import sys
import distutils.version

Expand Down Expand Up @@ -166,17 +167,6 @@ def _forward_ilshift(self, other):

import sys, os, tempfile

if sys.version_info[0] >= 3:
def ascii(s): return bytes(s, 'ascii')

def byte2str(b): return b.decode('ascii')

else:
ascii = str

def byte2str(b): return b


from matplotlib.rcsetup import (defaultParams,
validate_backend,
validate_toolbar)
Expand Down Expand Up @@ -224,7 +214,7 @@ def _is_writable_dir(p):
try:
t = tempfile.TemporaryFile(dir=p)
try:
t.write(ascii('1'))
t.write(b'1')
finally:
t.close()
except OSError:
Expand Down Expand Up @@ -304,7 +294,7 @@ def wrap(self, fmt, func, level='helpful', always=True):
if always is True, the report will occur on every function
call; otherwise only on the first time the function is called
"""
assert callable(func)
assert six.callable(func)
def wrapper(*args, **kwargs):
ret = func(*args, **kwargs)

Expand All @@ -330,7 +320,7 @@ def checkdep_dvipng():
s = subprocess.Popen(['dvipng','-version'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
line = s.stdout.readlines()[1]
v = byte2str(line.split()[-1])
v = line.split()[-1].decode('ascii')
return v
except (IndexError, ValueError, OSError):
return None
Expand All @@ -347,7 +337,7 @@ def checkdep_ghostscript():
stderr=subprocess.PIPE)
stdout, stderr = s.communicate()
if s.returncode == 0:
v = byte2str(stdout[:-1])
v = stdout[:-1]
return gs_exec, v

return None, None
Expand All @@ -358,7 +348,7 @@ def checkdep_tex():
try:
s = subprocess.Popen(['tex','-version'], stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
line = byte2str(s.stdout.readlines()[0])
line = s.stdout.readlines()[0].decode('ascii')
pattern = '3\.1\d+'
match = re.search(pattern, line)
v = match.group(0)
Expand All @@ -372,7 +362,7 @@ def checkdep_pdftops():
stderr=subprocess.PIPE)
for line in s.stderr:
if b'version' in line:
v = byte2str(line.split()[-1])
v = line.split()[-1].decode('ascii')
return v
except (IndexError, ValueError, UnboundLocalError, OSError):
return None
Expand All @@ -383,7 +373,7 @@ def checkdep_inkscape():
stderr=subprocess.PIPE)
for line in s.stdout:
if b'Inkscape' in line:
v = byte2str(line.split()[1])
v = line.split()[1].decode('ascii')
break
return v
except (IndexError, ValueError, UnboundLocalError, OSError):
Expand All @@ -395,7 +385,7 @@ def checkdep_xmllint():
stderr=subprocess.PIPE)
for line in s.stderr:
if b'version' in line:
v = byte2str(line.split()[-1])
v = line.split()[-1].decode('ascii')
break
return v
except (IndexError, ValueError, UnboundLocalError, OSError):
Expand Down Expand Up @@ -771,7 +761,7 @@ class RcParams(dict):
"""

validate = dict((key, converter) for key, (default, converter) in
defaultParams.iteritems())
six.iteritems(defaultParams))
msg_depr = "%s is deprecated and replaced with %s; please use the latter."
msg_depr_ignore = "%s is deprecated and ignored. Use %s"

Expand Down Expand Up @@ -856,7 +846,7 @@ def rc_params(fail_on_error=False):
# this should never happen, default in mpl-data should always be found
message = 'could not find rc file; returning defaults'
ret = RcParams([(key, default) for key, (default, _) in \
defaultParams.iteritems() ])
six.iteritems(defaultParams)])
warnings.warn(message)
return ret

Expand Down Expand Up @@ -888,7 +878,7 @@ def rc_params_from_file(fname, fail_on_error=False):
rc_temp[key] = (val, line, cnt)

ret = RcParams([(key, default) for key, (default, _) in \
defaultParams.iteritems()])
six.iteritems(defaultParams)])

for key in ('verbose.level', 'verbose.fileo'):
if key in rc_temp:
Expand All @@ -904,7 +894,7 @@ def rc_params_from_file(fname, fail_on_error=False):
verbose.set_level(ret['verbose.level'])
verbose.set_fileo(ret['verbose.fileo'])

for key, (val, line, cnt) in rc_temp.iteritems():
for key, (val, line, cnt) in six.iteritems(rc_temp):
if key in defaultParams:
if fail_on_error:
ret[key] = val # try to convert to proper type or raise
Expand Down Expand Up @@ -960,8 +950,8 @@ def rc_params_from_file(fname, fail_on_error=False):

rcParamsOrig = rcParams.copy()

rcParamsDefault = RcParams([ (key, default) for key, (default, converter) in \
defaultParams.iteritems() ])
rcParamsDefault = RcParams([(key, default) for key, (default, converter) in \
six.iteritems(defaultParams)])

rcParams['ps.usedistiller'] = checkdep_ps_distiller(rcParams['ps.usedistiller'])
rcParams['text.usetex'] = checkdep_usetex(rcParams['text.usetex'])
Expand Down Expand Up @@ -1033,7 +1023,7 @@ def rc(group, **kwargs):
if is_string_like(group):
group = (group,)
for g in group:
for k,v in kwargs.iteritems():
for k, v in six.iteritems(kwargs):
name = aliases.get(k) or k
key = '%s.%s' % (g, name)
try:
Expand Down Expand Up @@ -1289,4 +1279,4 @@ def test(verbosity=1):
verbose.report('verbose.level %s'%verbose.level)
verbose.report('interactive is %s'%rcParams['interactive'])
verbose.report('platform is %s'%sys.platform)
verbose.report('loaded modules: %s'%sys.modules.iterkeys(), 'debug')
verbose.report('loaded modules: %s'%six.iterkeys(sys.modules), 'debug')
2 changes: 1 addition & 1 deletion lib/matplotlib/_cm.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
Documentation for each is in pyplot.colormaps()
"""

from __future__ import print_function, division
from __future__ import absolute_import, division, print_function, unicode_literals
import numpy as np

_binary_data = {
Expand Down
Loading