Skip to content
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

Python 3 support #3

Merged
merged 39 commits into from
Oct 26, 2017
Merged

Python 3 support #3

merged 39 commits into from
Oct 26, 2017

Conversation

mgedmin
Copy link
Member

@mgedmin mgedmin commented Oct 21, 2017

Why am I doing this? I want zodbbrowser to run on Python 3, just to fill in a grid cell in the matrix at http://projects.gedmin.as/#python-versions.

Status: all tests pass. (Including zodbbrowser's functional tests that run an actual HTTP server and talk to it over TCP sockets.)

Remaining work:

  • changelog
  • py3 in travis, tox, setup.py
  • manual testing?
  • 100% test coverage would be nice but not today

Footnote: pipelining is broken
The Python 3.6 documentation for HTTPConnection.get_response() has this
note:

    Note that you must have read the whole response before you can send
    a new request to the server.

As far as I understand, the problem is that HTTPResponse() wraps the raw
socket object in a buffered reader that reads ahead, so the first
response object gets all of the pipelined responses into the buffer of
the socket wrapper that is discarded and not passed over to subsequent
responses.
It's very hard to debug these because exceptions in the thread are
swallowed, not printed.
No Python 3.4 because it doesn't support b'...' % (...).
@mgedmin mgedmin changed the title WIP: Python 3 support Python 3 support Oct 22, 2017
@mgedmin mgedmin requested a review from jamadden October 24, 2017 15:11
@jamadden
Copy link
Member

I don’t use zope.server, but I’m happy (and honored) to review. It may be a day or so.

Copy link
Member

@jamadden jamadden left a comment

Choose a reason for hiding this comment

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

Great work! This looks pretty good to me.

I left some comments, mostly stylistic or potential ways to "tighten" the port for closer equivalence between the versions.

setup.py Outdated
'Programming Language :: Python :: Implementation :: CPython',
'Natural Language :: English',
'Operating System :: OS Independent',
'Topic :: Internet :: WWW/HTTP',
'Framework :: Zope3'],
'Framework :: Zope3',
],
url='http://pypi.python.org/pypi/zope.server',
Copy link
Member

Choose a reason for hiding this comment

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

Could this be changed to https://github.com/zopefoundation/zope.server/?

except ImportError:
from StringIO import StringIO
from io import BytesIO
Copy link
Member

Choose a reason for hiding this comment

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

Is there a reason not to use io.BytesIO on both platforms (so that we're guaranteed consistent with the use of bytes/text)? (Related: zopefoundation/ZServer#7 (comment))

Copy link
Member Author

Choose a reason for hiding this comment

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

Probably not! I'll try it and see how it goes.

Copy link
Member Author

@mgedmin mgedmin Oct 26, 2017

Choose a reason for hiding this comment

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

Here's how it goes: all tests pass. But since test coverage is 78%, ¯\_(ツ)_/¯

(That's partially why I preferred sticking with cStringIO on Python 2.7: it might not work on Python 3, but at least it wouldn't introduce regressions on Python 2.)

Copy link
Member

Choose a reason for hiding this comment

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

We're pretty close to 100% coverage. I can work on a followup PR to try to get us there.

try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
Copy link
Member

Choose a reason for hiding this comment

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

Same as above, can we use the io class on both platforms?. (Although io.StringIO looks odd to me; is this file really dealing in the text domain?)

Copy link
Member Author

Choose a reason for hiding this comment

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

The annoying thing with io.StringIO on Python 2 is that it doesn't accept native ASCII-only strings. This makes it painful when you're, e.g., mocking out sys.stdout. As a result I got into the habit of staying with cStringIO on Python 2 when I'm porting something.

I don't remember where this particular StringIO instance is used; perhaps unicode would work after all?

Copy link
Member

Choose a reason for hiding this comment

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

It's used just once: request = self.request_factory(StringIO(''), env). Probably the main reason that works is that it's an empty string and there's nothing to iterate or parse. I'm guessing other layers would have issues with unicode text there, and so this should really be ByteIO.

@@ -194,7 +194,7 @@ def cmd_dele(self, args):
def cmd_help(self, args):
'See IFTPCommandHandler'
self.reply('HELP_START', flush=0)
Copy link
Member

Choose a reason for hiding this comment

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

I see that LineServerChannel.reply handles encoding, whereas write does not (expects bytes). I found that slightly surprising and had to look it up to verify that a prefix wasn't missing.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is mostly an accident of implementation: I was trying to get the tests to pass, instead of stopping and thinking about what makes sense in the API.

Thank you for paying attention to such details. I'll think about this some more.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, so LineServerChannel.reply() doesn't take a reply string, it takes a reply code, which is used to look up the reply string in the status_messages dict.

TBH I initially assumed those codes would be integers (because why would I familiarize myself with code I'm trying to modify? there are tests so I wouldn't have to think /s).

Now about the encoding -- perhaps it would be cleaner if the status_messages dict contained bytestrings as values, and if the args passed to reply() also were bytes. I'll think about this some more.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ok, given that I want Python 3.4 support after all, it'd be difficult to have reply implement %-formatting I require args to be bytes. I'm going to keep this as is.

Besides, I don't expect zope.server FTP support to have many users.

self.reply('ERR_NO_LIST', str(err))
return
ok_reply = ('OPEN_DATA_CONN', self.type_map[self.transfer_mode])
cdc = RETRChannel(self, ok_reply)
try:
cdc.write(s)
cdc.write(s.encode('utf-8'))
Copy link
Member

Choose a reason for hiding this comment

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

I looked this up too 😄 . RFC2640 does extend the FTP character set from ASCII to UTF-8.

Copy link
Member Author

Choose a reason for hiding this comment

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

What a lucky guess! :)

start_new_thread(self.handlerThread, (thread_no,))
t = threading.Thread(target=self.handlerThread,
args=(thread_no,),
name='zope.server-%d' % thread_no)
Copy link
Member

Choose a reason for hiding this comment

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

Ahh, it looks like this is the thread being left behind by testServerAsProxy.

try:
from cStringIO import StringIO
except ImportError:
from io import StringIO
Copy link
Member

Choose a reason for hiding this comment

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

This seems to be used as the underlying buffer of a logging.StreamHandler. Similar comments as in test_wsgiserver.py (when it's used to replace sys.stdXXX) apply.

tox.ini Outdated
coverage
commands =
coverage run -m zope.testrunner --test-path=src {posargs:-pvc}
coverage report -m
Copy link
Member

Choose a reason for hiding this comment

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

A --fail-under=XX might be helpful. Even if XX is not 100% yet it at least captures the current state and prevents regressions.

.travis.yml Outdated
- 2.7
- 3.5
- 3.6
Copy link
Member

Choose a reason for hiding this comment

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

Since we have a coverage tox environment, can we get coveralls added here?

Can we enable pip caching?

(And maybe ditch tox-travis/tox)?

Copy link
Member Author

Choose a reason for hiding this comment

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

What's a good package to cargo-cult the current .travis-ci.yml best practices from?

Copy link
Member

Choose a reason for hiding this comment

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

I often copy zope.viewlet:

language: python
sudo: false
python:
    - 2.7
    - 3.4
    - 3.5
    - 3.6
    - pypy
    - pypy3
install:
    - pip install -U pip setuptools
    - pip install -U coverage coveralls
    - pip install -U -e .[test]
script:
    - coverage run -m zope.testrunner --test-path=src
after_success:
    - coveralls
notifications:
    email: false
cache: pip

(Of course that leaves off --auto-color.)

Copy link
Member Author

Choose a reason for hiding this comment

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

--auto-... is overkill for Travis, IMHO, we know they give us a pty. I like zope-testrunner ... -pvc.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you know if the before_cache: rm -f $HOME/.cache/pip/log/debug.log I snarfed from zope.vocabularyregistry is no longer necessary?

Copy link
Member

Choose a reason for hiding this comment

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

pip now seems to touch a bunch of files in its cache directory when it runs (probably something to do with the up-to-date check?) so removing that file no longer prevents a new cache archive from being created.

Copy link
Member

Choose a reason for hiding this comment

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

I say that, but when I re-ran this build, it didn't write a new archive:

$ rm -f $HOME/.cache/pip/log/debug.log
store build cache
    nothing changed, not updating cache

Looking at some other projects, it's hard to tell exactly when pip will touch files. Here's the output for a different projects where only six was downloaded and updated.

0.01s$ rm -f $HOME/.cache/pip/log/debug.log
cache.2
store build cache
0.01s
4.53schange detected (content changed, file is created, or file is deleted):
/home/travis/.cache/pip/http/0/5/1/d/d/051ddbcfada687fb27df4b43966167a67f6706da37bcbb89d2838836
/home/travis/.cache/pip/http/0/a/8/f/a/0a8faabd212d81beff3ad0e11f3e4746188c0ad05c9190218de2e48a
/home/travis/.cache/pip/http/0/e/7/b/9/0e7b99547a0247a24df2175f35e166cf96f0194417e99b4851dbf306
/home/travis/.cache/pip/http/1/1/4/a/4/114a41049557a84c55958c18d02d44eb147fadc5d401e269e3d7ea8e
/home/travis/.cache/pip/http/1/c/f/2/e/1cf2ec08b116d0e8a4bd0c1c6441595628f3d6f6275c7d9cb54fb0c2
/home/travis/.cache/pip/http/2/6/6/b/d/266bdf220752466a969e134263f71564e77e2b9b41ad250878d8f5d8
/home/travis/.cache/pip/http/2/b/9/3/c/2b93c2d17b7b3f0ea9392df02b2eaa5b41f0eefacc87dc903a847b70
/home/travis/.cache/pip/http/2/f/1/0/6/2f10635579eda75bec25d08978dec5575ccd2fa532b1f347d21ec0a1
/home/travis/.cache/pip/http/3/7/2/f/2/372f280a7b4ee088e9327ee15a8c8095522419e73ce895ede6016b18
/home/travis/.cache/pip/http/3/a/f/3/a/3af3addf06e983a6c02f46e7bea70c221d3ff95bf1418fa6da354e14
/home/travis/.cache/pip/http/4/a/1/1/8/4a
...

tox.ini Outdated
py26,py27
py27,
py35,
py36,

[testenv]
deps =
zope.testrunner
Copy link
Member

Choose a reason for hiding this comment

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

I like to list zope.testrunner in the [test] extra. It simplifies both tox.ini and .travis.yml, and (to me at least) it helps make it more clear that we're unlikely to run with 'python setup.py test'.

@mgedmin
Copy link
Member Author

mgedmin commented Oct 26, 2017

Any further comments before I hit the big green Merge button?

@jamadden
Copy link
Member

LGTM! Thanks for your work on this.

@mgedmin mgedmin merged commit 2271aa5 into master Oct 26, 2017
@mgedmin mgedmin deleted the py3 branch October 26, 2017 17:22
@mgedmin
Copy link
Member Author

mgedmin commented Oct 26, 2017

The next item on my list is zopefoundation/zope.app.server#1, which is waiting for a zope.server PyPI release.

I think it makes sense to wait for your promised 100% test coverage PR and outdated logging infrastructure removal PRs before I make one. (Actually it doesn't have to be me -- I gave you the PyPI bit just now.)

@jamadden
Copy link
Member

Coverage in the 100_coverage branch is at 86.4% and rising. There are only ~20 files that have less than 100% right now.

@mgedmin
Copy link
Member Author

mgedmin commented Oct 26, 2017

BTW I am curious -- do you have permissions to edit the Coveralls.io settings at https://coveralls.io/github/zopefoundation/zope.server/settings? I've set the COVERAGE THRESHOLD FOR FAILURE (why is coveralls shouting at me?) to 78%, and it'll likely need to be updated once we get to 100%.

(I've also set COVERAGE DECREASE THRESHOLD FOR FAILURE to 1%, so any backslides would have to be gradual.)

@jamadden
Copy link
Member

I do, yes. I was able to change it to 77% just now.

(I don't see coveralls shouting here? It shows green checkmarks.)

@mgedmin
Copy link
Member Author

mgedmin commented Oct 26, 2017

(I was referring to the pervasive ALL UPPERCASE TEXT in coveralls's UI.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants