OctoPrint support for Python 2/3 versus the plugin space

It sounds like OctoPrint v1.4.0 will be the first to support by Python 2 & 3 at the same time. At some date in the future, OctoPrint with v2.0.0 will only support Python 3.

So... during this transition, it sounds like plugin authors will then need to support both Python 2 & 3 for some period of time and then after v2.0.0, wait a bit until themselves dropping Python 2 support to allow the stragglers who haven't updated.

I guess I'm wondering about the timing of all of this as well as what sorts of things a plugin author would need to do to support both versions.

I suppose you would need to create two virtual environments, do the source path/activate command and a python setup.py develop from each from within your project directory.

Are there any OctoPrint-specific information that could be queried to find out which virtual environment the user is currently in? Or do we just do a...

import sys
if sys.version_info > (2, 8):
    # assume v3

Here on the forum, we have a standard request "does this still break running in Safe Mode?" I wonder if we should have a similar thing like "does this still break running in Python 2.7?" that we could ask the user to do. It would be good to identify if this is going to be one of many plugin problems that are related to the switchover. Minimally, it would be nice if the Get Help template asks for the Python version.

I'd lobby for some "I explicitly support 1.4.0" flag. Feature flags are what all the cool kids do, and it would help cull some of the plugins that haven't accepted a PR in 2+ years.

Relevant ticket on Github were we are also discussing some possible approaches for this:

and more specifically this comment.

1 Like

To quote myself:

and also this:

1 Like

Excellent. So I guess this is really happening. And it feels like the timing is certainly within the first two quarters of 2019.

That's fantastic. I like having a 'reset' of the plugins, as I mentioned.

@foosel Your thread references Python 3.6 in two places but mostly 3.x. Is this the target?

I just ran:

$ virtualenv -p python3 venv3
Running virtualenv with interpreter /usr/local/bin/python3
Using base prefix '/usr/local/Cellar/python/3.7.1/Frameworks/Python.framework/Versions/3.7'
/usr/local/lib/python2.7/site-packages/virtualenv.py:1041: DeprecationWarning: the imp module is deprecated in favour of importlib; see the module's documentation for alternative uses
  import imp
New python executable in /Users/me/sites/OctoPrint/venv3/bin/python3.7
Also creating executable in /Users/me/sites/OctoPrint/venv3/bin/python
Installing setuptools, pip, wheel...done.

So now I have 3.7 rather than 3.6. I need some advice here. Do we tell people to adjust the venv3 or adjust globally (probably not recommended) or is there some other cleverness in the Python/pip space that I don't know?

Do we install pyenv & pyenv-virtualenv and then run pyenv virtualenv 3.6 venv3? My brain hurts.

Python 3.7 seemed to be working fine for me so far on the my test windows dev environment.

True, but...

If I continue like this and assert that my plugin(s) work find with Python 3.x then that's not the same for every minor version of Python. It would be nice to know what we're shooting for if we're trying to establish some sort of "testing trust".

Then my assumption would be that if your plugin only works with a specific version of python that the above could be assigned as such....

__plugin_pythoncompat__ = ">=3.6,<=3.6"

or even

__plugin_pythoncompat__ = "==3.6"

If my rig has 3.7 and I don't test it with 3.6 then (noting that 3.6 is technically 3.x) then I see this as a problem.

I'm asking for the target: what is the minimum level of 3.x compatibility?

Granted, I wouldn't call myself an expert in 3.x Python and what was introduced at the 3.0 level. I have to assume that the "breaking changes" took place there, the ones which were completely different from 2.x.

As a plugin author I could shoot low, test with 3.0 and indicate that it works with both 2.7 and 3.0. What does this mean for the receiving end-user who has 3.7? Will it work for them (noting that I haven't tested with their version)? On the other end of the scale I could install and test with 3.7 but what does this mean for the receiving end-user who has something earlier?

There are approximately 150 published OctoPrint plugins represented by perhaps a hundred different authors, I'd guess. Collectively we could take an organic approach and just let each author do their own thing. Or to minimize the amount of uncertainty in the upcoming months here on the forum we could identify a single 3.x version and suggest this as a baseline.

macOS: 10.14.2
Safari: 12.0.2

rm -Rf ~/Library/Application Support/OctoPrint  # Nuke the existing location (.octoprint)
mkdir ~/sites && cd ~/sites                     # This is my projects folder
rm -Rf OctoPrint                                # Nuke the existing OctoPrint repository entirely
git clone https://github.com/foosel/OctoPrint.git
cd OctoPrint
git checkout devel                              # This appears to be the only branch that's Python 3.x compatible
virtualenv -p python3 venv3                     # Create a Python 3β€”based virtual environment
source venv3/bin/activate
python --version                                # Note the version
# Python 3.7.1
pip install -e .[develop,plugins]               # Get this ready
octoprint serve                                 # Start up the service

So this should be a clean install with no plugins other than those which are bundled.

Output of pip install:

Obtaining file:///Users/me/sites/OctoPrint
Collecting flask<0.13,>=0.12 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/2e/48/f1936dadac2326b3d73f2fe0a964a87d16be16eb9d7fc56f09c1bea3d17c/Flask-0.12.4-py2.py3-none-any.whl
Collecting Jinja2<2.9,>=2.8.1 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/67/ea/92b1d9d8f2dc43302df7f5271b9500bbfc237386782343561a5f62beb306/Jinja2-2.8.1-py2.py3-none-any.whl
Collecting tornado==4.5.3 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting regex!=2018.11.6 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting Flask-Login<0.5,>=0.4.1 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting Flask-Principal<0.5,>=0.4 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting Flask-Babel<0.13,>=0.12 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting Flask-Assets<0.13,>=0.12 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting werkzeug<0.15,>=0.14.1 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/20/c4/12e3e56473e52375aa29c4764e70d1b8f3efa6682bef8d0aae04fe335243/Werkzeug-0.14.1-py2.py3-none-any.whl
Collecting PyYAML<4,>=3.13 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting markdown<3.1,>=3.0 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/7a/6b/5600647404ba15545ec37d2f7f58844d690baf2f81f3a60b862e48f29287/Markdown-3.0.1-py2.py3-none-any.whl
Collecting pyserial<3.5,>=3.4 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/0d/e4/2a744dd9e3be04a0c0907414e2a01a7c88bb3915cbe3c8cc06e209f59c30/pyserial-3.4-py2.py3-none-any.whl
Collecting netaddr<0.8,>=0.7.19 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/ba/97/ce14451a9fd7bdb5a397abf99b24a1a6bb7a1a440b019bebd2e9a0dbec74/netaddr-0.7.19-py2.py3-none-any.whl
Collecting watchdog<0.10,>=0.9.0 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting sarge==0.1.5post0 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting netifaces<0.11,>=0.10.7 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/7f/ac/fab3113cbe9c42bdd06735b6b7e0d37816f97eb3d744049d16f3a3b3d9b7/netifaces-0.10.9-cp37-cp37m-macosx_10_14_x86_64.whl
Collecting pylru<1.2,>=1.1 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting rsa<5,>=4.0 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/02/e5/38518af393f7c214357079ce67a317307936896e961e35450b70fad2a9cf/rsa-4.0-py2.py3-none-any.whl
Collecting pkginfo<1.5,>=1.4.2 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/a3/fe/f32a48d48f40a7209be9825fba2566cab92364787cf37de2e08300dd6ce7/pkginfo-1.4.2-py2.py3-none-any.whl
Collecting requests<3,>=2.20.0 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/7d/e3/20f3d364d6c8e5d2353c72a67778eb189176f08e873c9900e10c0287b84b/requests-2.21.0-py2.py3-none-any.whl
Collecting semantic_version<2.7,>=2.6 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/28/be/3a7241d731ba89063780279a5433f5971c1cf41735b64a9f874b7c3ff995/semantic_version-2.6.0-py3-none-any.whl
Collecting psutil<5.5,>=5.4.8 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting Click<8,>=7 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/fa/37/45185cb5abbc30d7257104c434fe0b07e5a195a6847506c074527aa599ec/Click-7.0-py2.py3-none-any.whl
Collecting awesome-slugify<1.7,>=1.6.5 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting feedparser<5.3,>=5.2.1 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting future<0.18,>=0.17 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting websocket-client<0.54,>=0.53 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/14/d4/6a8cd4e7f67da465108c7cc0a307a1c5da7e2cdf497330b682069b1d4758/websocket_client-0.53.0-py2.py3-none-any.whl
Collecting python-dateutil<2.8,>=2.7.5 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/74/68/d87d9b36af36f44254a8d512cbfc48369103a3b9e474be9bdfe536abfc45/python_dateutil-2.7.5-py2.py3-none-any.whl
Collecting wrapt<1.11,>=1.10.11 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting emoji<0.6,>=0.5.1 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/9c/fb/7586e11ff9205c9be9d11d376fcb6990ec4bdfae0a35663fb1ada7e3c10f/emoji-0.5.1-py3-none-any.whl
Collecting frozendict<1.3,>=1.2 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting sentry-sdk==0.6.6 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/da/85/cc138ab711abf72d5de93da441b6bcc5c454f9a3c7be6ba69c6384024725/sentry_sdk-0.6.6-py2.py3-none-any.whl
Collecting appdirs>=1.4.0 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/56/eb/810e700ed1349edde4cbdc1b2a21e28cdf115f9faf263f6bbf8447c1abf3/appdirs-1.4.3-py2.py3-none-any.whl
Collecting mock<3,>=2.0.0 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/e6/35/f187bdf23be87092bd0f1200d43d23076cee4d0dec109f195173fd3ebc79/mock-2.0.0-py2.py3-none-any.whl
Collecting pytest<5.0,>=4.2.1 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/51/b2/2fa8e8b179c792c457c2f7800f1313bfbd34f515e3a833e6083121844c14/pytest-4.3.0-py2.py3-none-any.whl
Collecting pytest-doctest-custom<1.1,>=1.0.0 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/c5/58/34391a13faa8f671ec372175989a5ccff98cbd1aa106b0be05fec8b9db60/pytest_doctest_custom-1.0.0-py2.py3-none-any.whl
Collecting ddt (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/cf/f5/f83dea32dc3fb3be1e5afab8438dce73ed587740a2a061ae2ea56e04a36d/ddt-1.2.1-py2.py3-none-any.whl
Collecting flake8 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/e9/76/b915bd28976068a9843bf836b789794aa4a8eb13338b23581005cd9177c0/flake8-3.7.7-py2.py3-none-any.whl
Collecting sphinx<1.7,>=1.6 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/62/e7/5bda3672131458d7eb0c182754cf24cec13409bc322191be04d3156c25ac/Sphinx-1.6.7-py2.py3-none-any.whl
Collecting sphinxcontrib-httpdomain (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/79/35/6f43bde0c4ead866c349a1fa0ff732a31613b3f462ad9b9630c03a958f41/sphinxcontrib_httpdomain-1.7.0-py2.py3-none-any.whl
Collecting sphinxcontrib-mermaid>=0.3 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/b8/8d/3b3eaf71c03bea7308de8e646616e9536a44280d1250c642cbb87c118aba/sphinxcontrib_mermaid-0.3.1-py2.py3-none-any.whl
Collecting sphinx_rtd_theme (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/60/b4/4df37087a1d36755e3a3bfd2a30263f358d2dea21938240fa02313d45f51/sphinx_rtd_theme-0.4.3-py2.py3-none-any.whl
Collecting cookiecutter<1.7,>=1.6 (from OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/16/99/1ca3a75978270288354f419e9166666801cf7e7d8df984de44a7d5d8b8d0/cookiecutter-1.6.0-py2.py3-none-any.whl
Collecting itsdangerous>=0.21 (from flask<0.13,>=0.12->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/76/ae/44b03b253d6fade317f32c24d100b3b35c2239807046a4c953c7b89fa49e/itsdangerous-1.1.0-py2.py3-none-any.whl
Collecting MarkupSafe (from Jinja2<2.9,>=2.8.1->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/ce/c6/f000f1af136ef74e4a95e33785921c73595c5390403f102e9b231b065b7a/MarkupSafe-1.1.1-cp37-cp37m-macosx_10_6_intel.whl
Collecting blinker (from Flask-Principal<0.5,>=0.4->OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting Babel>=2.3 (from Flask-Babel<0.13,>=0.12->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/b8/ad/c6f60602d3ee3d92fbed87675b6fb6a6f9a38c223343ababdb44ba201f10/Babel-2.6.0-py2.py3-none-any.whl
Collecting webassets>=0.11.1 (from Flask-Assets<0.13,>=0.12->OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting argh>=0.24.1 (from watchdog<0.10,>=0.9.0->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/06/1c/e667a7126f0b84aaa1c56844337bf0ac12445d1beb9c8a6199a7314944bf/argh-0.26.2-py2.py3-none-any.whl
Collecting pathtools>=0.1.1 (from watchdog<0.10,>=0.9.0->OctoPrint==1.4.0.dev2071+g058dbd0c)
Collecting pyasn1>=0.1.3 (from rsa<5,>=4.0->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/7b/7c/c9386b82a25115cccf1903441bba3cbadcfae7b678a20167347fa8ded34c/pyasn1-0.4.5-py2.py3-none-any.whl
Collecting idna<2.9,>=2.5 (from requests<3,>=2.20.0->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/14/2c/cd551d81dbe15200be1cf41cd03869a46fe7226e7450af7a6545bfc474c9/idna-2.8-py2.py3-none-any.whl
Collecting chardet<3.1.0,>=3.0.2 (from requests<3,>=2.20.0->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/bc/a9/01ffebfb562e4274b6487b4bb1ddec7ca55ec7510b22e4c51f14098443b8/chardet-3.0.4-py2.py3-none-any.whl
Collecting urllib3<1.25,>=1.21.1 (from requests<3,>=2.20.0->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/62/00/ee1d7de624db8ba7090d1226aebefab96a2c71cd5cfa7629d6ad3f61b79e/urllib3-1.24.1-py2.py3-none-any.whl
Collecting certifi>=2017.4.17 (from requests<3,>=2.20.0->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/9f/e0/accfc1b56b57e9750eba272e24c4dddeac86852c2bebd1236674d7887e8a/certifi-2018.11.29-py2.py3-none-any.whl
Collecting Unidecode<0.05,>=0.04.14 (from awesome-slugify<1.7,>=1.6.5->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/01/a1/9d7f3138ee3d79a1ab865a2cb38200ca778d85121db19fe264c76c981184/Unidecode-0.04.21-py2.py3-none-any.whl
Collecting six (from websocket-client<0.54,>=0.53->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/73/fb/00a976f728d0d1fecfe898238ce23f502a721c0ac0ecfedb80e0d88c64e9/six-1.12.0-py2.py3-none-any.whl
Collecting pbr>=0.11 (from mock<3,>=2.0.0->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/8c/7f/fed53b379500fd889707d1f6e61c2a35e12f2de87396894aff89b017d1d6/pbr-5.1.2-py2.py3-none-any.whl
Requirement already satisfied: setuptools in ./venv3/lib/python3.7/site-packages (from pytest<5.0,>=4.2.1->OctoPrint==1.4.0.dev2071+g058dbd0c) (40.8.0)
Collecting py>=1.5.0 (from pytest<5.0,>=4.2.1->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/76/bc/394ad449851729244a97857ee14d7cba61ddb268dce3db538ba2f2ba1f0f/py-1.8.0-py2.py3-none-any.whl
Collecting pluggy>=0.7 (from pytest<5.0,>=4.2.1->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/84/e8/4ddac125b5a0e84ea6ffc93cfccf1e7ee1924e88f53c64e98227f0af2a5f/pluggy-0.9.0-py2.py3-none-any.whl
Collecting attrs>=17.4.0 (from pytest<5.0,>=4.2.1->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/3a/e1/5f9023cc983f1a628a8c2fd051ad19e76ff7b142a0faf329336f9a62a514/attrs-18.2.0-py2.py3-none-any.whl
Collecting atomicwrites>=1.0 (from pytest<5.0,>=4.2.1->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/52/90/6155aa926f43f2b2a22b01be7241be3bfd1ceaf7d0b3267213e8127d41f4/atomicwrites-1.3.0-py2.py3-none-any.whl
Collecting more-itertools>=4.0.0; python_version > "2.7" (from pytest<5.0,>=4.2.1->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/ae/d4/d6bad4844831943dd667510947712750004525c5807711982f4ec390da2b/more_itertools-6.0.0-py3-none-any.whl
Collecting pycodestyle<2.6.0,>=2.5.0 (from flake8->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/0e/0c/04a353e104d2f324f8ee5f4b32012618c1c86dd79e52a433b64fceed511b/pycodestyle-2.5.0-py2.py3-none-any.whl
Collecting mccabe<0.7.0,>=0.6.0 (from flake8->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/87/89/479dc97e18549e21354893e4ee4ef36db1d237534982482c3681ee6e7b57/mccabe-0.6.1-py2.py3-none-any.whl
Collecting pyflakes<2.2.0,>=2.1.0 (from flake8->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/16/3b/b6a508ad148ce1ef50bd7a9a783afbb8d775616fc4ae5e3007c8815a3c85/pyflakes-2.1.0-py2.py3-none-any.whl
Collecting entrypoints<0.4.0,>=0.3.0 (from flake8->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/ac/c6/44694103f8c221443ee6b0041f69e2740d89a25641e62fb4f2ee568f2f9c/entrypoints-0.3-py2.py3-none-any.whl
Collecting Pygments>=2.0 (from sphinx<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/13/e5/6d710c9cf96c31ac82657bcfb441df328b22df8564d58d0c4cd62612674c/Pygments-2.3.1-py2.py3-none-any.whl
Collecting alabaster<0.8,>=0.7 (from sphinx<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/10/ad/00b090d23a222943eb0eda509720a404f531a439e803f6538f35136cae9e/alabaster-0.7.12-py2.py3-none-any.whl
Collecting snowballstemmer>=1.1 (from sphinx<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/d4/6c/8a935e2c7b54a37714656d753e4187ee0631988184ed50c0cf6476858566/snowballstemmer-1.2.1-py2.py3-none-any.whl
Collecting sphinxcontrib-websupport (from sphinx<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/52/69/3c2fbdc3702358c5b34ee25e387b24838597ef099761fc9a42c166796e8f/sphinxcontrib_websupport-1.1.0-py2.py3-none-any.whl
Collecting docutils>=0.11 (from sphinx<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/36/fa/08e9e6e0e3cbd1d362c3bbee8d01d0aedb2155c4ac112b19ef3cae8eed8d/docutils-0.14-py3-none-any.whl
Collecting imagesize (from sphinx<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/fc/b6/aef66b4c52a6ad6ac18cf6ebc5731ed06d8c9ae4d3b2d9951f261150be67/imagesize-1.1.0-py2.py3-none-any.whl
Collecting whichcraft>=0.4.0 (from cookiecutter<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/ab/c6/eb4d1dfbb68168bb01c4394420e5e71d5851e64b910838aa0f14ebd5c7a0/whichcraft-0.5.2-py2.py3-none-any.whl
Collecting binaryornot>=0.2.0 (from cookiecutter<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/24/7e/f7b6f453e6481d1e233540262ccbfcf89adcd43606f44a028d7f5fae5eb2/binaryornot-0.4.4-py2.py3-none-any.whl
Collecting poyo>=0.1.0 (from cookiecutter<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/e0/16/e00e3001007a5e416ca6a51def6f9e4be6a774bf1c8486d20466f834d113/poyo-0.4.2-py2.py3-none-any.whl
Collecting jinja2-time>=0.1.0 (from cookiecutter<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/6a/a1/d44fa38306ffa34a7e1af09632b158e13ec89670ce491f8a15af3ebcb4e4/jinja2_time-0.2.0-py2.py3-none-any.whl
Collecting pytz>=0a (from Babel>=2.3->Flask-Babel<0.13,>=0.12->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/61/28/1d3920e4d1d50b19bc5d24398a7cd85cc7b9a75a490570d5a30c57622d34/pytz-2018.9-py2.py3-none-any.whl
Collecting arrow (from jinja2-time>=0.1.0->cookiecutter<1.7,>=1.6->OctoPrint==1.4.0.dev2071+g058dbd0c)
  Using cached https://files.pythonhosted.org/packages/f4/7f/0360628ba40bb93c10cd89cd289b6a8e9ea87b2db884b8edf32c80ee1c73/arrow-0.13.1-py2.py3-none-any.whl
Installing collected packages: werkzeug, MarkupSafe, Jinja2, Click, itsdangerous, flask, tornado, regex, Flask-Login, blinker, Flask-Principal, pytz, Babel, Flask-Babel, webassets, Flask-Assets, PyYAML, markdown, pyserial, netaddr, argh, pathtools, watchdog, sarge, netifaces, pylru, pyasn1, rsa, pkginfo, idna, chardet, urllib3, certifi, requests, semantic-version, psutil, Unidecode, awesome-slugify, feedparser, future, six, websocket-client, python-dateutil, wrapt, emoji, frozendict, sentry-sdk, appdirs, pbr, mock, py, pluggy, attrs, atomicwrites, more-itertools, pytest, pytest-doctest-custom, ddt, pycodestyle, mccabe, pyflakes, entrypoints, flake8, Pygments, alabaster, snowballstemmer, sphinxcontrib-websupport, docutils, imagesize, sphinx, sphinxcontrib-httpdomain, sphinxcontrib-mermaid, sphinx-rtd-theme, whichcraft, binaryornot, poyo, arrow, jinja2-time, cookiecutter, OctoPrint
  Running setup.py develop for OctoPrint
Successfully installed Babel-2.6.0 Click-7.0 Flask-Assets-0.12 Flask-Babel-0.12.2 Flask-Login-0.4.1 Flask-Principal-0.4.0 Jinja2-2.8.1 MarkupSafe-1.1.1 OctoPrint PyYAML-3.13 Pygments-2.3.1 Unidecode-0.4.21 alabaster-0.7.12 appdirs-1.4.3 argh-0.26.2 arrow-0.13.1 atomicwrites-1.3.0 attrs-18.2.0 awesome-slugify-1.6.5 binaryornot-0.4.4 blinker-1.4 certifi-2018.11.29 chardet-3.0.4 cookiecutter-1.6.0 ddt-1.2.1 docutils-0.14 emoji-0.5.1 entrypoints-0.3 feedparser-5.2.1 flake8-3.7.7 flask-0.12.4 frozendict-1.2 future-0.17.1 idna-2.8 imagesize-1.1.0 itsdangerous-1.1.0 jinja2-time-0.2.0 markdown-3.0.1 mccabe-0.6.1 mock-2.0.0 more-itertools-6.0.0 netaddr-0.7.19 netifaces-0.10.9 pathtools-0.1.2 pbr-5.1.2 pkginfo-1.4.2 pluggy-0.9.0 poyo-0.4.2 psutil-5.4.8 py-1.8.0 pyasn1-0.4.5 pycodestyle-2.5.0 pyflakes-2.1.0 pylru-1.1.0 pyserial-3.4 pytest-4.3.0 pytest-doctest-custom-1.0.0 python-dateutil-2.7.5 pytz-2018.9 regex-2019.2.21 requests-2.21.0 rsa-4.0 sarge-0.1.5.post0 semantic-version-2.6.0 sentry-sdk-0.6.6 six-1.12.0 snowballstemmer-1.2.1 sphinx-1.6.7 sphinx-rtd-theme-0.4.3 sphinxcontrib-httpdomain-1.7.0 sphinxcontrib-mermaid-0.3.1 sphinxcontrib-websupport-1.1.0 tornado-4.5.3 urllib3-1.24.1 watchdog-0.9.0 webassets-0.12.1 websocket-client-0.53.0 werkzeug-0.14.1 whichcraft-0.5.2 wrapt-1.10.11

So that seemed happy.

Output of "octoprint serve":

2019-02-25 18:45:16,190 - octoprint.startup - INFO - ******************************************************************************
2019-02-25 18:45:16,191 - octoprint.startup - INFO - Starting OctoPrint 1.4.0.dev2071+g058dbd0c
2019-02-25 18:45:16,191 - octoprint.startup - INFO - ******************************************************************************
2019-02-25 18:45:16,214 - octoprint.plugin.core - INFO - Loading plugins from /Users/me/sites/OctoPrint/src/octoprint/plugins, /Users/me/Library/Application Support/OctoPrint/plugins and installed plugin packages...
2019-02-25 18:45:17,184 - octoprint.plugin.core - INFO - Plugin Pi Support Plugin did not pass check, not loading.
2019-02-25 18:45:17,248 - octoprint.plugins.discovery - INFO - pybonjour is not installed, Zeroconf Discovery won't be available
2019-02-25 18:45:17,295 - octoprint.plugin.core - INFO - Found 13 plugin(s) providing 13 mixin implementations, 25 hook handlers
2019-02-25 18:45:17,373 - octoprint.server.heartbeat - INFO - Starting server heartbeat, 900.0s interval
2019-02-25 18:45:17,425 - octoprint.server - INFO - Intermediary server started
2019-02-25 18:45:17,425 - octoprint.plugin.core - INFO - Loading plugins from /Users/me/sites/OctoPrint/src/octoprint/plugins, /Users/me/Library/Application Support/OctoPrint/plugins and installed plugin packages...
2019-02-25 18:45:17,429 - octoprint.plugin.core - INFO - Plugin Pi Support Plugin did not pass check, not loading.
2019-02-25 18:45:17,454 - octoprint.plugin.core - INFO - Found 13 plugin(s) providing 13 mixin implementations, 25 hook handlers
2019-02-25 18:45:17,455 - octoprint.printer.profile - ERROR - Profile _default does not exist, creating _default again and setting it as default
2019-02-25 18:45:17,509 - octoprint.filemanager.storage - INFO - Initializing the file metadata for /Users/me/Library/Application Support/OctoPrint/uploads...
2019-02-25 18:45:17,510 - octoprint.filemanager.storage - INFO - ... file metadata for /Users/me/Library/Application Support/OctoPrint/uploads initialized successfully.
2019-02-25 18:45:17,511 - octoprint.server - INFO - Added new permission from plugin action_command_prompt: PLUGIN_ACTION_COMMAND_PROMPT_INTERACT (needs: "Need(method='role', value='plugin_action_command_prompt_interact')")
2019-02-25 18:45:17,511 - octoprint.server - INFO - Added new permission from plugin announcements: PLUGIN_ANNOUNCEMENTS_READ (needs: "Need(method='role', value='plugin_announcements_read')")
2019-02-25 18:45:17,511 - octoprint.server - INFO - Added new permission from plugin announcements: PLUGIN_ANNOUNCEMENTS_MANAGE (needs: "Need(method='role', value='plugin_announcements_manage'), Need(method='role', value='plugin_announcements_read')")
2019-02-25 18:45:17,511 - octoprint.server - INFO - Added new permission from plugin appkeys: PLUGIN_APPKEYS_ADMIN (needs: "Need(method='role', value='plugin_appkeys_admin')")
2019-02-25 18:45:17,511 - octoprint.server - INFO - Added new permission from plugin backup: PLUGIN_BACKUP_ACCESS (needs: "Need(method='role', value='plugin_backup_access')")
2019-02-25 18:45:17,511 - octoprint.server - INFO - Added new permission from plugin logging: PLUGIN_LOGGING_MANAGE (needs: "Need(method='role', value='plugin_logging_manage')")
2019-02-25 18:45:17,512 - octoprint.server - INFO - Added new permission from plugin pluginmanager: PLUGIN_PLUGINMANAGER_MANAGE (needs: "Need(method='role', value='plugin_pluginmanager_manage')")
2019-02-25 18:45:17,512 - octoprint.server - INFO - Added new permission from plugin pluginmanager: PLUGIN_PLUGINMANAGER_INSTALL (needs: "Need(method='role', value='plugin_pluginmanager_manage'), Need(method='role', value='plugin_pluginmanager_install')")
2019-02-25 18:45:17,512 - octoprint.server - INFO - Added new permission from plugin printer_safety_check: PLUGIN_PRINTER_SAFETY_CHECK_DISPLAY (needs: "Need(method='role', value='plugin_printer_safety_check_display')")
2019-02-25 18:45:17,512 - octoprint.server - INFO - Added new permission from plugin softwareupdate: PLUGIN_SOFTWAREUPDATE_CHECK (needs: "Need(method='role', value='plugin_softwareupdate_check')")
2019-02-25 18:45:17,512 - octoprint.server - INFO - Added new permission from plugin softwareupdate: PLUGIN_SOFTWAREUPDATE_UPDATE (needs: "Need(method='role', value='plugin_softwareupdate_update')")
2019-02-25 18:45:18,509 - octoprint.util.pip - INFO - Using "/Users/me/sites/OctoPrint/venv3/bin/python3.7 -m pip" as command to invoke pip
2019-02-25 18:45:19,222 - octoprint.util.pip - INFO - Version of pip is 19.0.3
2019-02-25 18:45:19,223 - octoprint.util.pip - INFO - pip installs to /Users/me/sites/OctoPrint/venv3/lib/python3.7/site-packages (writable -> yes), --user flag needed -> no, virtual env -> yes
2019-02-25 18:45:19,223 - octoprint.util.pip - INFO - ==> pip ok -> yes
2019-02-25 18:45:19,293 - octoprint.plugin.core - INFO - Initialized 13 plugin implementation(s)
2019-02-25 18:45:19,408 - octoprint.plugin.core - INFO - 13 plugin(s) registered with the system:
|  Action Command Prompt Support (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/action_command_prompt
|  Announcement Plugin (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/announcements
|  Anonymous Usage Tracking (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/tracking
|  Application Keys Plugin (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/appkeys
|  Backup & Restore (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/backup
|  Core Wizard (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/corewizard
|  Discovery (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/discovery
|  Error Tracking (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/errortracking
|  Logging (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/logging
|  Plugin Manager (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/pluginmanager
|  Printer Safety Check (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/printer_safety_check
|  Software Update (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/softwareupdate
|  Virtual Printer (bundled) = /Users/me/sites/OctoPrint/src/octoprint/plugins/virtual_printer
Prefix legend: ! = disabled, # = blacklisted, * = incompatible
2019-02-25 18:45:19,410 - octoprint.environment - INFO - Detected environment is Python 3.7.1 under Macos (darwin). Details:
|  hardware:
|      cores: 4
|      freq: 1800
|      ram: 8589934592
|  os:
|      id: macos
|      platform: darwin
|  python:
|      pip: 19.0.3
|      version: 3.7.1
|      virtualenv: /Users/me/sites/OctoPrint/venv3/bin/..
2019-02-25 18:45:19,413 - octoprint.server - INFO - Reset webasset folder /Users/me/Library/Application Support/OctoPrint/generated/webassets...
2019-02-25 18:45:19,413 - octoprint.server - INFO - Reset webasset folder /Users/me/Library/Application Support/OctoPrint/generated/.webassets-cache...
2019-02-25 18:45:19,524 - octoprint.server - INFO - Shutting down intermediary server...
2019-02-25 18:45:19,937 - octoprint.server - INFO - Intermediary server shut down
2019-02-25 18:45:19,939 - octoprint.events - INFO - Processing startup event, this is our first event
2019-02-25 18:45:19,939 - octoprint.events - INFO - Adding 8 events to queue that were held back before startup event
2019-02-25 18:45:19,940 - octoprint.filemanager - INFO - Adding backlog items from all storage types to analysis queue...
2019-02-25 18:45:19,947 - octoprint.filemanager - INFO - Added 0 items from storage type "local" to analysis queue
2019-02-25 18:45:19,951 - octoprint.plugins.discovery - INFO - Registered OctoPrint instance on michael-macbook-air.local for SSDP
2019-02-25 18:45:19,953 - octoprint.server - INFO - Listening on http://[::]:5000
2019-02-25 18:45:20,431 - octoprint.util.pip - INFO - Using "/Users/me/sites/OctoPrint/venv3/bin/python3.7 -m pip" as command to invoke pip
2019-02-25 18:45:20,431 - octoprint.util.pip - INFO - pip installs to /Users/me/sites/OctoPrint/venv3/lib/python3.7/site-packages (writable -> yes), --user flag needed -> no, virtual env -> yes
2019-02-25 18:45:20,432 - octoprint.util.pip - INFO - ==> pip ok -> yes
2019-02-25 18:45:20,816 - octoprint.plugins.announcements - INFO - Loaded channel _important from https://octoprint.org/feeds/important.xml in 0.79s
2019-02-25 18:45:20,988 - octoprint.plugins.pluginmanager - INFO - Loaded plugin repository data from https://plugins.octoprint.org/plugins.json
2019-02-25 18:45:21,582 - octoprint.plugins.announcements - INFO - Loaded channel _releases from https://octoprint.org/feeds/releases.xml in 0.7s
2019-02-25 18:45:21,625 - octoprint.util.pip - INFO - Using "/Users/me/sites/OctoPrint/venv3/bin/python3.7 -m pip" as command to invoke pip
2019-02-25 18:45:21,885 - octoprint.plugins.pluginmanager - INFO - Loaded plugin notices data from https://plugins.octoprint.org/notices.json
2019-02-25 18:45:22,284 - octoprint.plugins.announcements - INFO - Loaded channel _blog from https://octoprint.org/feeds/octoblog.xml in 0.69s
2019-02-25 18:45:22,504 - octoprint.util.pip - INFO - pip installs to /Users/me/sites/OctoPrint/venv3/lib/python3.7/site-packages/ (writable -> yes), --user flag needed -> no, virtual env -> yes
2019-02-25 18:45:22,505 - octoprint.util.pip - INFO - ==> pip ok -> yes
2019-02-25 18:45:22,650 - octoprint.plugins.softwareupdate - INFO - Saved version cache to disk
2019-02-25 18:45:23,025 - octoprint.plugins.announcements - INFO - Loaded channel _plugins from https://plugins.octoprint.org/feed.xml in 0.73s
2019-02-25 18:45:23,773 - octoprint.plugins.announcements - INFO - Loaded channel _octopi from https://octoprint.org/feeds/octopi.xml in 0.7s
2019-02-25 18:45:50,748 - tornado.access - WARNING - 404 GET /static/webassets/packed_libs.css?ec88168c (::1) 0.98ms
2019-02-25 18:45:50,751 - tornado.access - WARNING - 404 GET /static/webassets/packed_core.css?71b48105 (::1) 1.55ms
2019-02-25 18:45:50,753 - tornado.access - WARNING - 404 GET /static/webassets/packed_libs.js?d2f8f676 (::1) 1.09ms
2019-02-25 18:45:50,759 - tornado.access - WARNING - 404 GET /static/webassets/packed_client.js?0e7bdfba (::1) 0.96ms
2019-02-25 18:45:50,762 - tornado.access - WARNING - 404 GET /static/webassets/packed_core.js?cd151986 (::1) 0.80ms

What it looks like on http://localhost:5000/:

JavaScript console:

Grasping at straws:

pip install -e .
octoprint serve


Taking a look into OctoPrint's own setup.py on devel:

>=2.7,<4 for plugins is shorter though and considering that you shouldn't be able to install OctoPrint on anything but 2.7 and 3.6+ we shouldn't really need the rat's tail to exclude the untested intermediary versions.

Personally I'm almost tempted to limit it further to 3.7+ to be honest, considering that once things are ready for a stable release that will already have aged a bit. The compatibility affecting changes between 3.6 and 3.7 are minimal however (the only one that could bite you if you test against 3.6 but not 3.7 is the new async and await keywords, if you use those as variable names somewhere), and if you need to run under 2.7 as well shouldn't affect one in any way.

I currently only have 2.7 and 3.7 installed concurrently and it will probably stay this way until 3.8 is released.

Browser cache issues maybe? Remember that settings & such under ~/.octoprint persist, so something might be interfering here, though right now I don't know what. I certainly cannot reproduce this issue locally.

1 Like

That's why I nuked it (see above). I thought about caching being the cause but noted the pile of 404s. I'm used to handing my students a bullet-proof recipe of what commands they're supposed to run to minimize their pain and focus on their work. I'd like to feel like I understand this better by the time this big version rolls out.

For this "wagon train" of trying to get a hundred plugin authors to test their software, I'd encourage you to set a mandate for exactly one version (3.7) which will take all end-users into the land of Python 3 and then relax the standard (3.7+) after we get there. The support forum will likely blow up with upset users who would just like their plugins back. This will mean that many of us will be trying to recreate bugs; the fewer variables there are in this then the better.

If there's even a single difference in 3.7 with keywords then for sanity's sake, that needs to be a mandate or guidance in a case like this in my humble opinion.

The complication

So now, imagine that you've got a printer setup with OctoPrint and you've got a non-bundled plugin like GPX which you must have in order to print. It's not really optional for you. You're at the mercy of the plugin author to have made the Python3-compatible version available by the time you've upgraded to 1.4.0. A fair number of us here on the support forum don't have this and couldn't really participate in fixing that or adopting it.

Er, no? Only if you also switch to Python 3 at this point, which no one forces you to. 1.4.0 will not require Py3, it will support 3. If you fire up 1.4.0 under Python 2.7 you'll be able to install the full zoo of plugins that exists today. You'll only run into compatibility issues when you switch to Py3.

The majority of users will probably even stay on Py2.7 for a long time to come I guess, considering that I still see OctoPi 0.12 instances in the stats...

The problem here is that I can already see a ton of additional problems caused by "but you said it was Python 3 compatible and my distribution only has 3.6 so far" kinda situations. Far more actually than problems that should be expected based on 3.6 vs 3.7.

The biggest issues in Python 3 compatibility aren't in the point releases. If your plugin works with 3.7, you can be very sure it will also work in 3.6 or 3.8 unless you do some really convoluted stuff. The challenge is getting it to work with Python 2 and 3 in the first place, not getting it to work with 2.7, 3.6 and 3.7.

I guess I'm being thick then. I blame it on insufficient coffee but you'd think that I'd know better.

True, people will find many reasons to complain. Education, though, can alert people that there actually is a difference in a minor version like this. I wish I had a good way of searching across all the source code of all the plugins in order to determine whether or not people are using async/await.

Anyway, I'll continue to try to figure out what things I need to do to test my plugins. I'm usually more confident when I code. I'm finding this a little frustrating, to be honest.

Edited __init__.py to include: __plugin_pythoncompat__ = ">=2.7,<4"
Deleted Build folder and both previous *.egg-info* folders and Dist.

cd ~/sites/OctoPrint
git checkout devel
source ./venv3/bin/activate
cd ~/sites/OctoPrint-GitFiles/
python setup.py develop
octoprint serve
2019-02-26 20:14:57,616 - octoprint.plugin.core - WARNING - Plugin GitFiles (1.1.5) is not compatible to Python 3.7.1 (compatibility string: >=2.7,<3).
2019-02-26 20:21:28,620 - octoprint.util.pip - INFO - Using "/Users/justincase/sites/OctoPrint/venv3/bin/python -m pip" as command to invoke pip
2019-02-26 20:21:29,072 - octoprint.util.pip - INFO - Version of pip is 19.0.3
2019-02-26 20:21:29,073 - octoprint.util.pip - INFO - pip installs to /Users/justincase/sites/OctoPrint/venv3/lib/python3.7/site-packages (writable -> yes), --user flag needed -> no, virtual env -> yes
2019-02-26 20:21:29,073 - octoprint.util.pip - INFO - ==> pip ok -> yes
| *GitFiles (1.1.5) = /Users/justincase/sites/OctoPrint-GitFiles/octoprint_gitfiles
pip list
 GitFiles   1.1.5    /Users/me/sites/OctoPrint-GitFiles

Unfortunately, it doesn't show in the Settings list.

It feels like all this isn't working yet.

To be quite honest, I find it a bit frustrating that you expect full blown documentation and working ecosystem right after this huge amount of PRs was merged. There are a ton of issues probably still to iron out because this was a huge invasive change that I've frankly been dreading for a while now, and it will take some time for things to settle. When they do, I'll push out documentation on what to do and how to test but at this point you are really on a ridiculously bleeding edge here.

I'm literally in the process of debugging things while working on the code for other 1.4.0 related changes. Don't expect stability.

Quoting myself again from the ticket re not working __plugin_pythoncompat__:


tldr: if the pycompat flag isn't being respected/reflected it's most likely because your plugin isn't even parsable under Python 3.

And now I'll go back to trying to work around a deprecated standard library module needed by the plugin subsystem...

Apologies, of course. I know you're working hard over there. I get that it's a lot of work and I'm impressed that we're at this point.

On this side, I have the paradox: I'd like to install my plugin in a Python 3.x context in order to determine that it's compatible/happy... only OctoPrint doesn't recognize that it's compatible so that I could potentially test it.

I'll put this on a backburner for a while and let things settle down. (Thanks for putting up with me.)