2to3

From PyMOLWiki
Revision as of 04:02, 19 February 2019 by Speleo3 (talk | contribs) (created)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

This page is about porting your existing scripts and plugins to Python 3, while keeping Python 2 compatibility. The strategies described here have also been used to port most of the scripts in Pymol-script-repo.

Recipe overview:

  1. Make a backup of your scripts (of course)
  2. 2to3 -w ...
  3. fix imports
  4. fix str/bytes mixing
  5. ...

2to3

2to3 is an automated conversion tool and part of every Python installation. It will for example replace print "Hello" with print("Hello"). We'll exclude all import fixes here, because we want to use a Python 2 compatible fix instead.

Run in a terminal:

2to3 -w --nofix=imports --nofix=urllib --nofix=future myscript.py

Imports

Place (the relevant subset of) this block at the beginning of each of your script files:

from __future__ import division
from __future__ import print_function
from __future__ import absolute_import

import sys
if sys.version_info[0] < 3:
    import urllib2
    from urllib2 import URLError, HTTPError
    from urllib import quote
    from idlelib.TreeWidget import TreeItem, TreeNode
    from Tkinter import *
    import Tkinter
    import tkSimpleDialog
    import tkFileDialog
    import tkMessageBox
    import tkColorChooser
    import Queue
    import ttk
    from StringIO import StringIO
else:
    import urllib.request as urllib2
    from urllib.error import URLError, HTTPError
    from urllib.parse import quote
    from idlelib.tree import TreeItem, TreeNode
    from tkinter import *
    import tkinter as Tkinter
    from tkinter import simpledialog as tkSimpleDialog
    from tkinter import filedialog as tkFileDialog
    import tkinter.messagebox as tkMessageBox
    import tkinter.colorchooser as tkColorChooser
    import queue as Queue
    import tkinter.ttk as ttk
    from io import StringIO

Some important notes:

  • from __future__ import division will change the behavior of integer division, 3/2 == 1 becomes 3/2 == 1.5. Use // if you need floor division (3//2 == 1 in both Python 2 and 3).
  • io.StringIO is not an exact replacement for StringIO.StringIO, it doesn't accept bytes input. You might need io.BytesIO instead.

str vs. bytes

This can be the most tricky part. In Python 3, all strings are unicode, and unicode was renamed to str and str to bytes. Also, Python 2 allowed mixing of unicode and bytes and automatically encoded/decoded between them when needed. Python 3 doesn't do that, you have to encode and decode yourself, and pay attention to binary vs. text when writing files.

Python 2 Python 3 Literals (2 and 3 compatible)
Unicode type unicode str u"..."
Binary type str (alias bytes) bytes b"..."

Code examples that works in Python 2 and 3:

text   = u"Greek letter alpha: \u03B1"
binary = u"Greek letter alpha: \u03B1".encode("utf-8")
binary = b"Greek letter alpha: \xce\xb1"
text   = b"Greek letter alpha: \xce\xb1".decode("utf-8")

Use binary mode when reading or writing bytes from/to files:

# text mode
with open("file.txt", "w") as f:
    f.write(u"Hello World\n")
    f.write(b"Hello World\n".decode("utf-8"))

# binary mode
with open("file.bin", "wb") as f:
    f.write(b"Hello World\n")
    f.write(u"Hello World\n".encode("utf-8"))

The subprocess modules opens streams in binary mode, unless you pass universal_newlines=True:

# text mode
p = subprocess.Popen(['echo', 'Hello World'],
        universal_newlines=True,
        stdout=subprocess.PIPE)
text = p.stdout.read()

# binary mode
p = subprocess.Popen(['echo', 'Hello World'],
        stdout=subprocess.PIPE)
binary = p.stdout.read()

string module

Most string module functions are gone, use str methods instead.

Examples:

Python 2 only Python 2 and 3
string.strip(" Hello ") " Hello ".strip()
string.split("A B C") "A B C".split()
string.join(["A", "B", "C"], " ") " ".join(["A", "B", "C"])

Note: PyMOL has an interim solution for this, which will be removed in a future version:

if sys.version_info[0] == 3:
    import string
    for attr in ['capitalize', 'count', 'find', 'index', 'lower',
            'replace', 'rstrip', 'split', 'strip', 'upper', 'zfill']:
        setattr(string, attr, getattr(str, attr))
    string.letters      = string.ascii_letters
    string.lowercase    = string.ascii_lowercase
    string.join = lambda words, sep=' ': sep.join(words)
    string.atoi = int

Hex formatting of floats

This works in Python 2, but is an error in Python 3:

>>> '%x' % 12.34
TypeError: %x format: an integer is required, not float

Solution: Convert floats to int:

>>> '%x' % int(12.34)