.. _sec-plugins-python3: Migrating to Python 3 ===================== Python 2 is now EOL as of January 1st 2020. With the release of 1.4.0 OctoPrint will be compatible to both Python 2 and Python 3. However, the same doesn't automatically hold true for all of the third party plugins for OctoPrint out there - it will fall to their authors to ensure compatibility to both Python versions. This guide is supposed to help plugin authors in making sure their plugins run under Python 2 as well as Python 3, which for now is the goal for OctoPrint's ecosystem, as we'll have to live with existing legacy Python 2 installations for a while to come (the plan is to stay Python 2 compatible until roughly a year after the release of 1.4.0). .. contents:: :local: .. _sec-plugins-python3-venv: How to get a Python 3 virtual environment with OctoPrint -------------------------------------------------------- In order to test your plugins for Python 3 compatibility and also to allow for ongoing maintenance against both Python versions, you should create a Python 3 virtual environment next to your Python 2 one. You can then quickly switch between Python 2 and Python 3 simply by ``activate``-ing whichever one you need. You can create a Python 3 virtualenv next to your (existing) Python 2 virtualenv and then just activate which one you currently want to use. After installing Python 3 on your development system it's as easy as supplying ``--python=/path/to/python3executable`` to ``virtualenv``, e.g.: .. code-block:: none virtualenv --python=/usr/bin/python3 venv3 That will have the virtualenv be created based on Python 3, regardless of whether it's currently running under Python 2 or 3. The same works for Python 2 btw: .. code-block:: none virtualenv --python=/usr/bin/python2 venv2 After creating the virtual environment, make sure to activate & install OctoPrint into it: .. code-block:: none source venv3/bin/activate pip install "OctoPrint>=1.4.0rc1" Then create an editable install of your plugin, start the server and start testing: .. code-block:: none pip install -e path/to/your/plugin octoprint serve --debug .. note:: On Windows that will probably look something like this instead: .. code-block:: none virtualenv --python=C:/Python37/python.exe venv37 venv3/Script/activate.bat pip install "OctoPrint>=1.4.0rc1" pip install -e path/to/your/plugin octoprint serve --debug .. note:: If you want to migrate your existing OctoPrint install *on OctoPi 0.17.0* to Python 3, I suggest to first make a :ref:`backup `, then move the existing venv ``/home/pi/oprint`` out of the way and create a new one based on Python 3 (which should already be present on current OctoPi images): .. code-block:: none mv ~/oprint ~/oprint.py2 virtualenv --python=/usr/bin/python3 oprint source ~/oprint/bin/activate pip install "OctoPrint>=1.4.0" sudo service octoprint restart .. _sec-plugins-python3-markup: Telling OctoPrint your plugin is Python 3 ready ----------------------------------------------- In order for OctoPrint to even load your plugin when it's running under Python 3, it first needs to know your plugin is compatible to a Python 3 environment. By default OctoPrint will assume your plugin isn't and refuse to load it when running under Python 3 itself. To tell OctoPrint about this, all you need is to set the ``__plugin_pythoncompat__`` property in your plugins's ``__init__.py`` accordingly, e.g. .. code-block:: python __plugin_pythoncompat__ = ">=2.7,<4" This would tell OctoPrint that your plugin is compatible to all Python versions between 2.7 and 3.x. This should be your target compatibility range for now. If at a later date you want to go all-in on Python 3 and mark your plugin as no longer supporting Python 2, tell OctoPrint about this as well: .. code-block:: python __plugin_pythoncompat__ = ">=3,<4" .. note:: You can also tell OctoPrint to ignore the Python compatibility flags for a specific plugin via `config.yaml`: .. code-block:: yaml plugins: _forcedCompatible: - "myplugin" - "anotherplugin" Note that this should only be used temporarily during testing and migration, or to mark an important plugin not under your own control that actually works fine under Python 3 out of the box as compatible while waiting until the plugin author has pushed an update including the needed flags. Do not just blindly mark third party plugins as compatible and then open support requests if that causes issues in your setup. Once your plugin is ensured to be compatible and you've released a new version that includes the necessary compatibility flag and changes, is done you also need to mark up your plugin in the Official Plugin Repository (if it's registered therein) so that OctoPrint's built-in Plugin Manager will see that your plugin is compatible as well and allow users to install it through it. In order to do that, you need to add a new flag compatibility.python to the front matter in your plugin registration file and file a pull request for that. Adjust the markdown file so that it contains this: .. code-block:: yaml compatibility: python: ">=2.7,<3" The value here follows the same mechanism as the ``__plugin_pythoncompat__`` property, so ``>=2.7,<3`` for 2 and 3 support and ``>=3,<4`` for 3+ support. .. warning:: Do **not** just mark your plugin as compatible without diligent testing that it actually does work as expected and without flooding ``octoprint.log`` with warnings and errors! .. _sec-plugins-python3-pitfalls: Common pitfalls during migration -------------------------------- Some of the changes in Python 3 compared to Python 2 are sadly backwards incompatible and usually cause a number of common issues in code written for Python 2 when run under Python 3. By now they are pretty well documented and there exist a number of helpful and comprehensive migration guides, three of which I want to mention here. One is the official Python 3 porting guide `Porting Python 2 Code to Python 3 `__ which sums up all the important changes and also gives hints on how best to go about running a project which supports both versions for now. The second is the `Writing Python 2-3 compatible code `__ cheat sheet from the Python-Future project, which is a comprehensive list of idioms that are compatible to both Python 2 and 3 and will make your code run under both, utilizing `future `__ and `six `__. I can strongly recommend this cheat sheet, it's what primarily guided me during the migration phase as well. The third one is the free online book `Support Python 3: An in-depth guide `__, and especially its chapter on `Common migration problems `__ in which you'll find extensive descriptions of the most troublesome changes in Python 3 and how to overcome them. Please note that with regards to the contents of this book, we are aiming for the "Python 2 and Python 3 without conversion" strategy, so code that runs in both environments. Sadly this book is a bit outdated by now and still references some long-out versions as "upcoming", so with regards to compatible idioms to use, best stick to the Python-Future cheat sheet. Looking at the issues encountered by some plugin authors and also my own experiences during the Python 3 migration of OctoPrint's code, the most common problems for these scenarios seem to be byte vs unicode issues, trouble with absolute imports, changes in integer division behaviour and the switch of map, filter and zip to return iterators instead of lists and causing issues in the following code due to that. .. _sec-plugins-python3-pitfalls-strings: Bytes vs unicode ................ One of if not the most problematic change between Python 2 and 3 surely must be the change in string handling. Under Python 2 your basic string was a byte string, but it could also magically turn into a unicode string depending on what you wrote into it. That did cause some confusion, especially in APIs, and caused quite a mess, which is why the decision was made to go for distinct text and binary types instead, and making the string literal always be a (unicode) text. .. note:: Please note that these changes in string handling also affect several Python APIs that operate on files and streams and thus might also affect parts of OctoPrint's plugin interface that inherit from these APIs. Currently only one such case has been reported, as OctoPrint's :py:class:`~octoprint.filemanager.util.LineProcessorStream` will return bytes instead of str on its ``process_line`` function under Python 3 - so here's a heads-up if your plugin happens to utilize that. Obviously, that will lead to issues in code using "just strings" when run under Python 2 vs 3. The first step to solve these problems would be to make your scripts behave the same under Python 2 and 3 by putting this right at the top of all your plugin's python files: .. code-block:: python from __future__ import unicode_literals That will make your files behave as if they were running under Python 3, even when run under Python 2, and your string literals will now be the text data type, which - annoyingly - is a different one under Python 2 vs 3, ``unicode`` vs ``str`` to be exact. Heads-up here - under Python 2 there's also a ``str`` type, but that one is for binary data. Yes, I know, this ain't fun. In any case, once you've done this, make sure that everything in your code that should be text is text (``unicode`` under Python 2, ``str`` under Python 3), and everything that should be binary is binary (``str`` under Python 2, ``bytes`` under Python 3). A good rule of thumb is that you usually want to use text as much as possible within your application and only convert to/from bytes at the outskirts, e.g. when writing to a file, a socket or something else machine like. Note that you do NOT need to convert to bytes when implementing API endpoints that return JSON, as that should use text with unicode anyhow. OctoPrint includes two utility methods you should use to ensure your strings enter/exit your code in the right format, under both Python versions: :py:func:`octoprint.util.to_bytes` and :py:func:`octoprint.util.to_unicode`. Use them to ensure the correct data types and to avoid weird conversion and encoding issues during runtime. You can read more about this specific issue in the corresponding section of the `Python porting guide `__ and also in the `cheat sheet `__. .. _sec-plugins-python3-pitfalls-absolute-imports: Absolute imports ................ Python 3 now defaults to absolute imports, meaning that trying to import a sub package with a .. code-block:: python import my_sub_package will now fail with an error. You'll need to explicitly make the import a relative one: .. code-block:: python from . import my_sub_package To make your code behave the same in that regard in both Python 2 and Python 3, you should add the corresponding future import: .. code-block:: python from __future__ import absolute_imports You can read more about this specific issue in the `cheat sheet `__ and also in `the book `__. .. _sec-plugins-python3-pitfalls-version-specific-imports: Version specific imports ........................ Sometimes it is necessary to use an import statement that is explicitly related to a specific Python version, e.g. due to a package change between Python 2 and 3. You can do this by first trying the Python 3 import and if that doesn't work out trying the Python 2 import instead: .. code-block:: python try: import queue except ImportError: import Queue as queue This should be the preferred method of handling situations like this. If you actually do need to do explicit version specific imports that cannot be handled this way, you can check for the Python version like this: .. code-block:: python import sys if sys.version[0] == '2': # Python 2 specific imports else: # Python 3 specific imports .. _sec-plugins-python3-pitfalls-intdiv: Integer division ................ When you divide two integers in Python 2 you'll get back an integer, rounded down. In Python 3 however you'll now get a float. That means you might have to revisit some places where you do integer divisions and might rely on the result to be an integer as well (e.g. when using a calculation result as an index in an array or something like that). Yet again there's a future-import to apply to your files in order to at least have them behave the same in that regard under both Python 2 and Python 3: .. code-block:: python from __future__ import division You can read more about this specific issue in the `Python porting guide `__ and in the `cheat sheet `__. .. _sec-plugins-python3-pitfalls-iterators: Iterators instead of list from map, filter, zip ............................................... The built-in functions ``map``, ``filter`` and ``zip`` return a ``list`` with their result in Python 2. In Python 3 they have been switched to returning iterators. That can cause trouble with code handling the result (e.g. if you try to return it as part of a JSON response on an API endpoint). The easiest way to solve this is to make sure to wrap any ``map``/``filter``/``zip`` calls into a ``list`` constructor if the result is to be used outside of the calling code (even though that comes with a small performance penalty under Python 2): .. code-block:: python result1 = filter(lambda x: x is not None, my_collection) result2 = list(filter(lambda x: x is not None, my_collection)) assert(isinstance(result1, list)) # Python 2 passes, Python 3 fails assert(isinstance(result2, list)) # Python 2 and 3 pass There also exist further options, take a look at the `cheat sheet `__. .. _sec-plugins-python3-checklist: Checklist --------- As a summary, follow this checklist to migrate your plugin to be compatible to both Python 2 and 3: * Create a Python 3 virtualenv and install OctoPrint and your plugin into it for testing. * Tell OctoPrint your plugin is Python 2 and 3 compatible by adding a new property ``__plugin_pycompat__`` to its ``__init__.py``: .. code-block:: python __plugin_pythoncompat__ = ">=2.7,<4" * Add a compatibility header to all `py` files to ensure similar basic behaviour under Python 2 and Python 3: .. code-block:: python # -*- coding: utf-8 -*- from __future__ import absolute_import, division, print_function, unicode_literals * Thoroughly test your plugin under Python 3. Pay special attention to any kind of string handling issues, integer division, relative imports from your plugin package and how the results of ``map``, ``filter`` and ``zip`` are used in your code, as those have proven to be the biggest issues during past migrations. * Once everything works under both Python versions and you've prepared a new release of your plugin (don't forget to increment the version!), update your registration file in the Official Plugin Repository to include the correct Python compatibility information as well: .. code-block:: yaml compatibility: python: ">=2.7,<4" .. _sec-plugins-python3-furtherreading: Further reading --------------- .. seealso:: `Porting Python 2 Code to Python 3 `__ The official Python 3 porting guide which sums up all the important changes and also gives hints on how best to go about running a project which supports both versions for now. `Cheat Sheet: Writing Python 2-3 compatible code `__ A comprehensive list of idioms that are compatible to both Python 2 and 3 and will make your code run under both, utilizing `future `__ and `six `__. Strongly recommended. `Supporting Python 3: An in-depth guide `__ A free online book on the switch to Python 3. Sadly seems a bit outdated by now, so with regards to compatible idioms to use, best stick to the cheat sheet. Gives some interesting background however. `Towards Python 3 and OctoPrint 1.4.0 `__ Forum topic discussing OctoPrint 1.4.0's roadmap including Python 3 compatibility and time frame. `Migrating plugins to Python 2 & 3 compatibility - experiences? `__ Forum topic collecting experiences by plugin developers in migrating their plugins to achieve Python 2 & 3 compatibility.