wiki:Python3Support

Version 11 (modified by neteler, 6 years ago) ( diff )

+

Table of Contents

    Python 3 support in GRASS

    Python versions

    • keep compatibility with 2.7 (may still work with 2.6, but we don't care)
    • port to work with 3.5

    Python components include:

    • Python Scripting Library
    • PyGRASS
    • Temporal Library
    • ctypes
    • wxGUI

    Python Scripting Library

    What to consider:

    • The API is used not only by the GRASS Development Team (core devs) but in general, e.g. by writing addons or custom user scripts.
      • Maybe the core devs can be convinced to follow certain special practices for the core modules, but it doesn't seem realistic that addon contributors will follow them if there are too distant from what is standard for the language (less serious example is requiring PEP8 conventions versus some custom ones).
      • The purpose of the API is to make it simple for people to use and extend GRASS GIS.
    • Trained (and even the non-trained) Python 3 programmers will expect API to behave in the same way as the standard library and language in general.
      • One writes os.environ['PATH'], not os.environ[b'PATH'] nor os.environ[u'PATH'].
      • GUI needs Unicode at the end.

    Possible approach:

    • functions need to accept unicode and return unicode
    • functions wrapping Python Popen class (read_command, run_command, ...) will have parameter encoding
      • encoding=None means expects and returns bytes (the current state)
      • encoding='default' means it takes current encoding using utils._get_encoding()
      • encoding='utf-8' takes whatever encoding user specifies, e.g., utf-8 in this case
      • this is similar to Popen class in Python3.6
      • by default encoding='default' to enable expected behavior by users, the following example shows Python3 behavior if we keep using bytes instead of unicode:
    # return bytes
    ret = read_command('r.what', encoding=None, ...
    
    for item in ret.splitlines():
        line = item.split('|')[3:]
    
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: a bytes-like object is required, not 'str'
    
    # we would have to use:
    for item in ret.splitlines():
        line = item.split(b'|')[3:]
    

    Unicode as the default type in the API, e.g. for keys, but also for many values, is supported by Unicode being the default string literal type in Python 3. API users will expect that expressions such as hypothetical computation_region['north'] will work. Unlike in Python 2, there is a difference in Python 3 between computation_region[u'north'] and computation_region[b'north']. See comparison of dictionary behavior in 2 and 3:

    # Python 2
    >>> d = {'a': 1, b'b': 2}
    >>> d['b']
    2
    >>> d[u'b']
    2
    >>> # i.e. no difference between u'' and b'' keys
    >>> and that applies for creating also:
    >>> d = {u'a': 1, b'a': 2}
    >>> d['a']
    2
    >>> # because
    >>> d
    {u'a': 2}
    # Python 3
    >>> # unlike in 2, we get now two entries:
    >>> d = {'a': 1, b'a': 2}
    >>> d
    {b'a': 2, 'a': 1}
    >>> d['a']
    1
    >>> d[b'a']
    2
    >>> # it becomes little confusing when we combine unicode and byte keys
    >>> d = {'a': 1, b'b': 2}
    >>> d['a']
    1
    >>> d['b']
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    KeyError: 'b'
    >>> d[b'b']
    2
    >>> # in other words, user needs to know and specify the key as bytes
    

    Python 2 and Python 3 differences

    The most important change between these two versions is dealing with strings.

    • In Python 2:
      • Bytes == Strings
      • Unicodes != Strings
    • In Python 3:
      • Bytes != Strings
      • Unicodes == Strings

    Label

    Python 2

    Python 3

    String:

    >>> 'sample'
    'sample'
    
    >>> 'sample'
    'sample'
    

    Unicode:

    >>> 'sample'
    u'sample'
    
    >>> 'sample'
    'sample'
    

    Bytes:

    >>> b'sample'
    'sample'
    
    >>> b'sample'
    b'sample'
    

    Types:

    >>> type('xx'), type(u'xx'), type(b'xx')
    (<type 'str'>, <type 'unicode'>, <type 'str'>)
    
    >>> type('xx'), type(u'xx'), type(b'xx')
    (<class 'str'>, <class 'str'>, <class 'bytes'>)
    

    When special characters are involved:

    Label

    Python 2

    Python 3

    String:

    >>> 'Příšerný kůň'
    'P\xc5\x99\xc3\xad\xc5\xa1ern\xc3\xbd k\xc5\xaf\xc5\x88'
    
    >>> 'Příšerný kůň'
    'Příšerný kůň'
    

    Unicode:

    >>> u'Příšerný kůň'
    u'P\u0159\xed\u0161ern\xfd k\u016f\u0148'
    
    >>> u'Příšerný kůň'
    'Příšerný kůň'
    

    Bytes:

    >>> b'Příšerný kůň'
    'P\xc5\x99\xc3\xad\xc5\xa1ern\xc3\xbd k\xc5\xaf\xc5\x88'
    
    >>> b'Příšerný kůň'
    SyntaxError: bytes can only contain ASCII literal characters.
    

    For Python 3, bytes objects can not contain character literals other than ASCII, therefore, we use bytes() to convert from unicode/string to byte object.

    >>> bytes('Příšerný kůň', encoding='utf-8')
    b'P\xc5\x99\xc3\xad\xc5\xa1ern\xc3\xbd k\xc5\xaf\xc5\x88'
    

    To decode, use decode():

    >>>b'P\xc5\x99\xc3\xad\xc5\xa1ern\xc3\xbd k\xc5\xaf\xc5\x88'.decode()
    'Příšerný kůň'
    

    We already have encode and decode functions available in (from grass.script.utils import encode, decode) lib/python/script/utils.py that makes it easy for us to convert back and forth. To make it work with Python3, made changes in those functions to avoid syntax errors and exceptions.

    Dictionary Differences

    Python 2

    Python 3

    >>> d = {'a': 1, b'b': 2, u'c':3}
    
    >>>d['a']
    1
    >>>d[b'a']
    1
    >>>d[u'a']
    1
    
    >>>d['b']
    2
    >>>d[b'b']
    2
    >>>d[u'b']
    2
    
    >>>d['c']
    3
    >>>d[b'c']
    3
    >>>d[u'c']
    3
    
    >>> d = {'a': 1, b'b': 2, u'c':3}
    
    >>>d['a']
    1
    >>>d[b'a']
    KeyError: b'a'
    >>>d[u'a']
    1
    
    >>>d['b']
    KeyError: 'b'
    >>>d[b'b']
    2
    >>>d[u'b']
    KeyError: 'b'
    
    >>>d['c']
    3
    >>>d[b'c']
    KeyError: b'c'
    >>>d[u'c']
    3
    

    When special characters are involved:

    >>> d = {'ř': 1, b'š': 2, u'ý':3}
    
    >>>d['ř']
    1
    >>>d[b'ř']
    1
    >>>d[u'ř']
    KeyError: u '\u0159'
    
    >>>d['š']
    1
    >>>d[b'š']
    1
    >>>d[u'š']
    KeyError: u '\u0161'
    
    >>>d['ý']
    KeyError: '\xc3\xbd'
    >>>d[b'ý']
    KeyError: '\xc3\xbd'
    >>>d[u'ý']
    3
    
    >>> d = {'ř': 1, b's': 2, u'ý':3}
    
    >>>d['ř']
    1
    >>>d[b'ř']
    SyntaxError
    >>>d[u'ř']
    1
    
    >>>d['s']
    KeyError: 's'
    >>>d[b's']
    2
    >>>d[u's']
    KeyError: 's'
    
    >>>d['ý']
    3
    >>>d[b'ý']
    SyntaxError
    >>>d[u'ý']
    3
    

    How to write Python 3 compatible code

    To check which Python version is being used, use sys.verson_info like:

    import sys
    if sys.version_info.major >= 3:
        //…
    else:
        //...
    

    Label

    Python 2

    Python 3

    Python 2 and 3 compatible solution

    Strings - bytes/str/unicode

    Use decode and encode functions from grass.script.utils
    

    String functions

    String.split
    String.join
    
    "".split(), "".join(" ")
    

    Use functions as in Python 3

    String types

    Types.StringTypes
    Types.Stringtype
    
    str or unicode
    str
    

    Use direct types like str, unicode, bytes as:

    if sys.version_info.major == 3:
        unicode = str
    else:
        bytes = str
    

    ps.communicate(): stdout, stderror

    Use decode:

    from grass.script.utils import decode
    
    stdout = decode(stdout)
    stderr = decode(stdout)
    

    Opening files

    fileName = file(“example.txt”, w)
    
    fileName = open(“example.txt”, w)
    

    Replace file() with open() to open files

    Filter function

    filter(a_function, a_sequence)
    

    Filter returns a list

    filter(a_function, a_sequence)
    

    Filter returns an iterator, not a list

    Explicit conversion to list using:

    list( filter(a_function, a_sequence) )
    

    Urlopen proxies

    Create proxy handler: https://docs.python.org/3.0/library/urllib.request.html#urllib.request.ProxyHandler

    Read more: http://www.diveintopython3.net/porting-code-to-python-3-with-2to3.html#urllib

    Import errors

    Use try/except to catch Import errors and deal accordingly

    Relative Imports

    import example
    from x import y
    
    from . import example
    from .x import y
    

    Use period (.) in import calls

    Dictionary methods

    a_dictionary.keys()
    a_dictionary.items()
    a_dictionary.iterkeys()
    
    list(a_dictionary.keys())
    list(a_dictionary.items())
    iter(a_dictionary.keys())
    

    Read more: http://www.diveintopython3.net/porting-code-to-python-3-with-2to3.html#dict

    Other dictionary iterators

    a_dictionary.iterkeys()
    a_dictionary.iteritems()
    a_dictionary.itervalues()
    
    six.iterkeys(a_dictionary)
    six.iteritems(a_dictionary)
    six.itervalues(a_dictionary)
    

    Use function from six library

    Map list

    map(x, y)
    

    Returns list

    map(x,y)
    

    Returns iterator

    list(map(x,y))
    

    Use list to wrap around map

    Other recommendations:

    Use .format specifier for the strings and parameters. For example instead of using:

    '%s %s' % ('one', 'two')
    

    Use:

    '{} {}'.format('one', 'two')
    

    .format is compatible with both Python 2 and Python 3.

    Read more at: https://pyformat.info/

    wxPython GUI

    There are a lot of changes found in wxPython Phoenix version. It is recommended to follow the MIgration guide (https://docs.wxpython.org/MigrationGuide.html) to properly migrate from the Classic version of wxPython. To support both the versions. The wrap.py includes a lot of new classes that work as a wrapper to accommodate both the versions of wxPython and Python itself.

    All the changes for Classic vs Phoenix can be found here: https://wxpython.org/Phoenix/docs/html/classic_vs_phoenix.html

    We have created a wrap.py class that contains overloaded classes for wx classes to support both versions. Example:

    from gui_core.wrap TextCtrl, StaticText
    

    Deprecated warnings can be removed by appropriately using the wx classes. Refer to the changes in both versions and see if the wrapper class is already created for the wx class; if not, create a new class in a similar manner as other wrapper classes in the wrap.py file.

    cmp function is not available in Python3, it has been custom created and included in gui/wxpython/core/utils.py file. Be sure to include it where cmp() is used.

    How to test

    We can rather easily create an isolated environment with Python3 using virtualenv which lets you test GRASS GIS with Python3 support while not cluttering your standard system.

    Installation of virtualenv-3:

    # 1) generic, using pip (WHICH SYSTEM?)
    pip install virtualenv
    
    # 2) on Fedora
    sudo dnf install python3-virtualenv gtk3-devel gstreamer-devel gstreamer-plugins-base-devel
    
    # 3) on Debian
    apt-get install -y libgtk2.0-dev libgtk-3-dev \
    	libjpeg-dev libtiff-dev \
    	libsdl1.2-dev libgstreamer-plugins-base0.10-dev \
    	libnotify-dev freeglut3 freeglut3-dev libsm-dev \
    	libwebkitgtk-dev libwebkitgtk-3.0-dev
    

    Preparation of the virtual Python3 environment:

    # create environment
    
    # 1) on Ubuntu
    virtualenv -p python3 grasspy3
    # 2) on Fedora
    virtualenv-3 -p python3 grasspy3
    

    Activating the virtual Python3 environment for testing:

    # activate it (this will change the terminal prompt so that you know where you are...)
    source grasspy3/bin/activate
    
    # now, within this environment
    # install required Python3 packages for GRASS GIS
    pip install wxpython
    pip install numpy
    

    We are now settled with the dependencies.

    Test GRASS GIS with Python3:

    # just run it in the virtualenv-3 session
    grass76 --gui
    

    In order to deactivate the virtualenv-3 environment, run

    deactivate
    

    References

    http://sebastianraschka.com/Articles/2014_python_2_3_key_diff.html
    http://osgeo-org.1560.x6.nabble.com/Python-3-porting-and-unicode-td5344215.html
    http://python3porting.com/toc.html
    https://docs.python.org/3.7/howto/pyporting.html
    http://python-future.org/index.html
    http://python-future.org/compatible_idioms.html
    http://lucumr.pocoo.org/2014/5/12/everything-about-unicode/
    https://pypi.python.org/pypi/six
    https://pypi.python.org/pypi/autopep8
    https://docs.python.org/3.0/library/urllib.request.html#urllib.request.ProxyHandler
    http://www.diveintopython3.net/porting-code-to-python-3-with-2to3.html#urllib
    http://www.diveintopython3.net/porting-code-to-python-3-with-2to3.html#dict
    https://pyformat.info/
    https://docs.wxpython.org/MigrationGuide.html
    https://wxpython.org/Phoenix/docs/html/classic_vs_phoenix.html

    Attachments (1)

    Download all attachments as: .zip

    Note: See TracWiki for help on using the wiki.