drop support for old Python, IPython < 3

Require IPython >= 3.0, Python >= 3.3
This commit is contained in:
Min RK
2014-10-31 13:03:50 -07:00
parent 83569221b9
commit 40a99e61ac
20 changed files with 110 additions and 173 deletions

View File

@@ -2,7 +2,7 @@
language: python
python:
- 3.4
- 2.7
- 3.3
before_install:
# workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
- sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm

View File

@@ -18,6 +18,8 @@ Basic principles:
## Dependencies
JupyterHub requires IPython >= 3.0 (current master) and Python >= 3.3.
You will need nodejs/npm, which you can get from your package manager:
sudo apt-get install npm nodejs-legacy

View File

@@ -1,2 +1,2 @@
mock
-r requirements.txt
pytest

View File

@@ -1 +1,2 @@
from .version import *

View File

@@ -4,11 +4,7 @@
import json
try:
# py3
from http.client import responses
except ImportError:
from httplib import responses
from tornado import web
@@ -19,7 +15,7 @@ class APIHandler(BaseHandler):
"""Return the body of the request as JSON data."""
if not self.request.body:
return None
body = self.request.body.strip().decode(u'utf-8')
body = self.request.body.strip().decode('utf-8')
try:
model = json.loads(body)
except Exception:

View File

@@ -11,11 +11,6 @@ from .. import orm
from ..utils import admin_only, authenticated_403
from .base import APIHandler
try:
basestring
except NameError:
basestring = str # py3
class BaseUserHandler(APIHandler):
def user_model(self, user):
@@ -26,7 +21,7 @@ class BaseUserHandler(APIHandler):
}
_model_types = {
'name': basestring,
'name': str,
'admin': bool,
}

View File

@@ -5,20 +5,18 @@
# Distributed under the terms of the Modified BSD License.
import binascii
import io
import logging
import os
import socket
import sys
from datetime import datetime
from distutils.version import LooseVersion as V
from getpass import getuser
from subprocess import Popen
try:
raw_input
except NameError:
# py3
raw_input = input
if sys.version_info[:2] < (3,3):
raise ValueError("Python < 3.3 not supported: %s" % sys.version)
from six import text_type
from jinja2 import Environment, FileSystemLoader
from sqlalchemy.exc import OperationalError
@@ -30,6 +28,10 @@ from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.log import LogFormatter, app_log, access_log, gen_log
from tornado import gen, web
import IPython
if V(IPython.__version__) < V('3.0'):
raise ImportError("JupyterHub Requires IPython >= 3.0, found %s" % IPython.__version__)
from IPython.utils.traitlets import (
Unicode, Integer, Dict, TraitError, List, Bool, Any,
Type, Set, Instance, Bytes,
@@ -43,8 +45,8 @@ from . import handlers, apihandlers
from . import orm
from ._data import DATA_FILES_PATH
from .utils import (
url_path_join, TimeoutError,
ISO8601_ms, ISO8601_s, getuser_unicode,
url_path_join,
ISO8601_ms, ISO8601_s,
)
# classes for config
from .auth import Authenticator, PAMAuthenticator
@@ -302,14 +304,7 @@ class JupyterHubApp(Application):
def _log_format_default(self):
"""override default log format to include time"""
return u"%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
def _log_format_changed(self, name, old, new):
"""Change the log formatter when log_format is set."""
# FIXME: IPython < 3 compat
_log_handler = self.log.handlers[0]
_log_formatter = self._log_formatter_cls(fmt=new, datefmt=self.log_datefmt)
_log_handler.setFormatter(_log_formatter)
return "%(color)s[%(levelname)1.1s %(asctime)s.%(msecs).03d %(name)s]%(end_color)s %(message)s"
def init_logging(self):
# This prevents double log messages because tornado use a root logger that
@@ -325,8 +320,6 @@ class JupyterHubApp(Application):
logger.propagate = True
logger.parent = self.log
logger.setLevel(self.log.level)
# FIXME: IPython < 3 compat
self._log_format_changed('', '', self.log_format)
def init_ports(self):
if self.hub_port == self.port:
@@ -370,7 +363,7 @@ class JupyterHubApp(Application):
"""More informative log messages for failed filesystem access"""
path = os.path.abspath(path)
parent, fname = os.path.split(path)
user = getuser_unicode()
user = getuser()
if not os.path.isdir(parent):
self.log.error("Directory %s does not exist", parent)
if os.path.exists(parent) and not os.access(parent, os.W_OK):
@@ -399,7 +392,7 @@ class JupyterHubApp(Application):
self.log.error("Bad permissions on %s", secret_file)
else:
self.log.info("Loading %s from %s", trait_name, secret_file)
with io.open(secret_file) as f:
with open(secret_file) as f:
b64_secret = f.read()
try:
secret = binascii.a2b_base64(b64_secret)
@@ -413,8 +406,8 @@ class JupyterHubApp(Application):
if secret_file and secret_from == 'new':
# if we generated a new secret, store it in the secret_file
self.log.info("Writing %s to %s", trait_name, secret_file)
b64_secret = text_type(binascii.b2a_base64(secret))
with io.open(secret_file, 'w', encoding='utf8') as f:
b64_secret = binascii.b2a_base64(secret).decode('ascii')
with open(secret_file, 'w') as f:
f.write(b64_secret)
try:
os.chmod(secret_file, 0o600)
@@ -450,7 +443,7 @@ class JupyterHubApp(Application):
ip=self.hub_ip,
port=self.hub_port,
base_url=self.hub_prefix,
cookie_name=u'jupyter-hub-token',
cookie_name='jupyter-hub-token',
)
)
self.db.add(self.hub)
@@ -470,7 +463,7 @@ class JupyterHubApp(Application):
# add current user as admin if there aren't any others
admins = db.query(orm.User).filter(orm.User.admin==True)
if admins.first() is None:
self.admin_users.add(getuser_unicode())
self.admin_users.add(getuser())
for name in self.admin_users:
# ensure anyone specified as admin in config is admin in db
@@ -576,7 +569,7 @@ class JupyterHubApp(Application):
self.proxy.public_server.port = self.port
self.proxy.api_server.ip = self.proxy_api_ip
self.proxy.api_server.port = self.proxy_api_port
self.proxy.api_server.base_url = u'/api/routes/'
self.proxy.api_server.base_url = '/api/routes/'
self.db.commit()
@gen.coroutine
@@ -694,8 +687,8 @@ class JupyterHubApp(Application):
pid = os.getpid()
if self.pid_file:
self.log.debug("Writing PID %i to %s", pid, self.pid_file)
with io.open(self.pid_file, 'w') as f:
f.write(u'%i' % pid)
with open(self.pid_file, 'w') as f:
f.write('%i' % pid)
@catch_config_error
def initialize(self, *args, **kwargs):
@@ -756,7 +749,7 @@ class JupyterHubApp(Application):
def ask():
prompt = "Overwrite %s with default config? [y/N]" % self.config_file
try:
return raw_input(prompt).lower() or 'n'
return input(prompt).lower() or 'n'
except KeyboardInterrupt:
print('') # empty line
return 'n'
@@ -771,7 +764,7 @@ class JupyterHubApp(Application):
if isinstance(config_text, bytes):
config_text = config_text.decode('utf8')
print("Writing default config to: %s" % self.config_file)
with io.open(self.config_file, encoding='utf8', mode='w') as f:
with open(self.config_file, mode='w') as f:
f.write(config_text)
@gen.coroutine

View File

@@ -153,5 +153,5 @@ class PAMAuthenticator(LocalAuthenticator):
busername = username.encode(self.encoding)
bpassword = data['password'].encode(self.encoding)
if simplepam.authenticate(busername, bpassword, service=self.service):
raise gen.Return(username)
return username

View File

@@ -5,11 +5,7 @@
import re
from datetime import datetime
try:
# py3
from http.client import responses
except ImportError:
from httplib import responses
from jinja2 import TemplateNotFound
@@ -156,7 +152,7 @@ class BaseHandler(RequestHandler):
auth = self.authenticator
if auth is not None:
result = yield auth.authenticate(self, data)
raise gen.Return(result)
return result
else:
self.log.error("No authentication function, login is impossible!")
@@ -179,7 +175,7 @@ class BaseHandler(RequestHandler):
)
yield self.proxy.add_user(user)
user.spawner.add_poll_callback(self.user_stopped, user)
raise gen.Return(user)
return user
@gen.coroutine
def user_stopped(self, user):

View File

@@ -63,11 +63,11 @@ class Server(Base):
"""
__tablename__ = 'servers'
id = Column(Integer, primary_key=True)
proto = Column(Unicode, default=u'http')
ip = Column(Unicode, default=u'localhost')
proto = Column(Unicode, default='http')
ip = Column(Unicode, default='localhost')
port = Column(Integer, default=random_port)
base_url = Column(Unicode, default=u'/')
cookie_name = Column(Unicode, default=u'cookie')
base_url = Column(Unicode, default='/')
cookie_name = Column(Unicode, default='cookie')
def __repr__(self):
return "<Server(%s:%s)>" % (self.ip, self.port)

View File

@@ -9,7 +9,6 @@ import os
import requests
from tornado import ioloop
from tornado import web
from IPython.utils.traitlets import Unicode
@@ -21,8 +20,8 @@ from IPython.html.utils import url_path_join
from distutils.version import LooseVersion as V
import IPython
if V(IPython.__version__) < V('2.2'):
raise ImportError("JupyterHub Requires IPython >= 2.2, found %s" % IPython.__version__)
if V(IPython.__version__) < V('3.0'):
raise ImportError("JupyterHub Requires IPython >= 3.0, found %s" % IPython.__version__)
# Define two methods to attach to AuthenticatedHandler,
# which authenticate via the central auth server.
@@ -97,11 +96,6 @@ class SingleUserNotebookApp(NotebookApp):
# disable the exit confirmation for background notebook processes
ioloop.IOLoop.instance().stop()
def init_kernel_argv(self):
"""construct the kernel arguments"""
# FIXME: This is 2.x-compat, remove when 3.x is requred
self.kernel_argv = ["--profile-dir", self.profile_dir.location]
def init_webapp(self):
# monkeypatch authentication to use the hub
from IPython.html.base.handlers import AuthenticatedHandler
@@ -120,10 +114,7 @@ class SingleUserNotebookApp(NotebookApp):
# load the hub related settings into the tornado settings dict
env = os.environ
# FIXME: IPython < 3 compat
s = getattr(self, 'tornado_settings',
getattr(self, 'webapp_settings')
)
s = self.webapp_settings
s['cookie_cache'] = {}
s['user'] = self.user
s['hub_api_key'] = env.pop('JPY_API_TOKEN')

View File

@@ -114,7 +114,7 @@ class Spawner(LoggingConfigurable):
Subclasses should call super, to ensure that state is properly cleared.
"""
self.api_token = u''
self.api_token = ''
def get_args(self):
"""Return the arguments to be passed after self.cmd"""
@@ -339,13 +339,13 @@ class LocalProcessSpawner(Spawner):
break
pids = self._get_pg_pids(ppid)
if pids:
raise gen.Return(pids[0])
return pids[0]
else:
yield gen.Task(loop.add_timeout, loop.time() + 0.1)
self.log.error("Failed to get single-user PID")
# return sudo pid if we can't get the real PID
# this shouldn't happen
raise gen.Return(ppid)
return ppid
@gen.coroutine
def start(self):
@@ -378,7 +378,7 @@ class LocalProcessSpawner(Spawner):
if status is not None:
# clear state if the process is done
self.clear_state()
raise gen.Return(status)
return status
# if we resumed from stored state,
# we don't have the Popen handle anymore, so rely on self.pid
@@ -386,16 +386,16 @@ class LocalProcessSpawner(Spawner):
if not self.pid:
# no pid, not running
self.clear_state()
raise gen.Return(0)
return 0
# send signal 0 to check if PID exists
# this doesn't work on Windows, but that's okay because we don't support Windows.
alive = yield self._signal(0)
if not alive:
self.clear_state()
raise gen.Return(0)
return 0
else:
raise gen.Return(None)
return None
@gen.coroutine
def _signal(self, sig):
@@ -405,9 +405,9 @@ class LocalProcessSpawner(Spawner):
"""
if self.set_user == 'sudo':
rc = yield self._signal_sudo(sig)
raise gen.Return(rc)
return rc
else:
raise gen.Return(self._signal_setuid(sig))
return self._signal_setuid(sig)
def _signal_setuid(self, sig):
"""simple implementation of signal, which we can use when we are using setuid (we are root)"""
@@ -427,10 +427,10 @@ class LocalProcessSpawner(Spawner):
try:
check_output(['ps', '-p', str(self.pid)], stderr=PIPE)
except CalledProcessError:
raise gen.Return(False) # process is gone
return False # process is gone
else:
if sig == 0:
raise gen.Return(True) # process exists
return True # process exists
# build sudo -u user kill -SIG PID
cmd = self.sudo_cmd(self.user)
@@ -441,7 +441,7 @@ class LocalProcessSpawner(Spawner):
check_output(cmd,
preexec_fn=self.make_preexec_fn(self.user.name),
)
raise gen.Return(True) # process exists
return True # process exists
@gen.coroutine
def stop(self, now=False):

View File

@@ -4,12 +4,12 @@
# Distributed under the terms of the Modified BSD License.
import logging
from getpass import getuser
from pytest import fixture
from tornado import ioloop
from .. import orm
from ..utils import getuser_unicode
from .mocking import MockHubApp
@@ -24,7 +24,7 @@ def db():
if _db is None:
_db = orm.new_session_factory('sqlite:///:memory:', echo=True)()
user = orm.User(
name=getuser_unicode(),
name=getuser(),
server=orm.Server(),
)
hub = orm.Hub(

View File

@@ -1,29 +1,23 @@
"""mock utilities for testing"""
import os
import sys
from tempfile import NamedTemporaryFile
import threading
try:
from unittest import mock
except ImportError:
import mock
from tornado.ioloop import IOLoop
from six import text_type
from ..spawner import LocalProcessSpawner
from ..app import JupyterHubApp
from ..auth import PAMAuthenticator, Authenticator
from ..auth import PAMAuthenticator
from .. import orm
def mock_authenticate(username, password, service='login'):
# mimic simplepam's failure to handle unicode
if isinstance(username, text_type):
if isinstance(username, str):
return False
if isinstance(password, text_type):
if isinstance(password, str):
return False
# just use equality for testing

View File

@@ -1,6 +1,5 @@
"""Test the JupyterHubApp entry point"""
import io
import os
import sys
from subprocess import check_output
@@ -8,8 +7,8 @@ from tempfile import NamedTemporaryFile
def test_help_all():
out = check_output([sys.executable, '-m', 'jupyterhub', '--help-all']).decode('utf8', 'replace')
assert u'--ip' in out
assert u'--JupyterHubApp.ip' in out
assert '--ip' in out
assert '--JupyterHubApp.ip' in out
def test_generate_config():
with NamedTemporaryFile(prefix='jupyter_hub_config', suffix='.py') as tf:
@@ -19,7 +18,7 @@ def test_generate_config():
'--generate-config', '-f', cfg_file]
).decode('utf8', 'replace')
assert os.path.exists(cfg_file)
with io.open(cfg_file) as f:
with open(cfg_file) as f:
cfg_text = f.read()
os.remove(cfg_file)
assert cfg_file in out

View File

@@ -9,33 +9,33 @@ from .mocking import MockPAMAuthenticator
def test_pam_auth(io_loop):
authenticator = MockPAMAuthenticator()
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
u'username': u'match',
u'password': u'match',
'username': 'match',
'password': 'match',
}))
assert authorized == u'match'
assert authorized == 'match'
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
u'username': u'match',
u'password': u'nomatch',
'username': 'match',
'password': 'nomatch',
}))
assert authorized is None
def test_pam_auth_whitelist(io_loop):
authenticator = MockPAMAuthenticator(whitelist={'wash', 'kaylee'})
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
u'username': u'kaylee',
u'password': u'kaylee',
'username': 'kaylee',
'password': 'kaylee',
}))
assert authorized == u'kaylee'
assert authorized == 'kaylee'
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
u'username': u'wash',
u'password': u'nomatch',
'username': 'wash',
'password': 'nomatch',
}))
assert authorized is None
authorized = io_loop.run_sync(lambda : authenticator.authenticate(None, {
u'username': u'mal',
u'password': u'mal',
'username': 'mal',
'password': 'mal',
}))
assert authorized is None

View File

@@ -5,48 +5,42 @@
from .. import orm
try:
unicode
except NameError:
# py3
unicode = str
def test_server(db):
server = orm.Server()
db.add(server)
db.commit()
assert server.ip == u'localhost'
assert server.ip == 'localhost'
assert server.base_url == '/'
assert server.proto == 'http'
assert isinstance(server.port, int)
assert isinstance(server.cookie_name, unicode)
assert isinstance(server.cookie_name, str)
assert server.url == 'http://localhost:%i/' % server.port
def test_proxy(db):
proxy = orm.Proxy(
auth_token=u'abc-123',
auth_token='abc-123',
public_server=orm.Server(
ip=u'192.168.1.1',
ip='192.168.1.1',
port=8000,
),
api_server=orm.Server(
ip=u'127.0.0.1',
ip='127.0.0.1',
port=8001,
),
)
db.add(proxy)
db.commit()
assert proxy.public_server.ip == u'192.168.1.1'
assert proxy.api_server.ip == u'127.0.0.1'
assert proxy.auth_token == u'abc-123'
assert proxy.public_server.ip == '192.168.1.1'
assert proxy.api_server.ip == '127.0.0.1'
assert proxy.auth_token == 'abc-123'
def test_hub(db):
hub = orm.Hub(
server=orm.Server(
ip = u'1.2.3.4',
ip = '1.2.3.4',
port = 1234,
base_url='/hubtest/',
),
@@ -54,30 +48,30 @@ def test_hub(db):
)
db.add(hub)
db.commit()
assert hub.server.ip == u'1.2.3.4'
assert hub.server.ip == '1.2.3.4'
hub.server.port == 1234
assert hub.api_url == u'http://1.2.3.4:1234/hubtest/api'
assert hub.api_url == 'http://1.2.3.4:1234/hubtest/api'
def test_user(db):
user = orm.User(name=u'kaylee',
user = orm.User(name='kaylee',
server=orm.Server(),
state={'pid': 4234},
)
db.add(user)
db.commit()
assert user.name == u'kaylee'
assert user.server.ip == u'localhost'
assert user.name == 'kaylee'
assert user.server.ip == 'localhost'
assert user.state == {'pid': 4234}
found = orm.User.find(db, u'kaylee')
found = orm.User.find(db, 'kaylee')
assert found.name == user.name
found = orm.User.find(db, u'badger')
found = orm.User.find(db, 'badger')
assert found is None
def test_tokens(db):
user = orm.User(name=u'inara')
user = orm.User(name='inara')
db.add(user)
db.commit()
token = user.new_api_token()

View File

@@ -5,34 +5,17 @@
from binascii import b2a_hex
import errno
import getpass
import hashlib
import os
import socket
import uuid
from six import text_type
from tornado import web, gen, ioloop
from tornado.httpclient import AsyncHTTPClient, HTTPError
from tornado.log import app_log
from IPython.html.utils import url_path_join
try:
# make TimeoutError importable on Python >= 3.3
TimeoutError = TimeoutError
except NameError:
# python < 3.3
class TimeoutError(Exception):
pass
def getuser_unicode():
"""
Call getpass.getuser, ensuring that the output is returned as unicode.
"""
return text_type(getpass.getuser())
def random_port():
"""get a single random port"""
@@ -147,7 +130,7 @@ def new_token(*args, **kwargs):
For now, just UUIDs.
"""
return text_type(uuid.uuid4().hex)
return uuid.uuid4().hex
def hash_token(token, salt=8, rounds=16384, algorithm='sha512'):
@@ -169,7 +152,7 @@ def hash_token(token, salt=8, rounds=16384, algorithm='sha512'):
h.update(btoken)
digest = h.hexdigest()
return u"{algorithm}:{rounds}:{salt}:{digest}".format(**locals())
return "{algorithm}:{rounds}:{salt}:{digest}".format(**locals())
def compare_token(compare, token):

View File

@@ -1,7 +1,6 @@
ipython[notebook]>=2.2
tornado>=3.2
-e git+https://github.com/ipython/ipython.git#egg=ipython[notebook]
tornado>=4
jinja2
simplepam
sqlalchemy
requests
six

View File

@@ -14,12 +14,11 @@ import os
import sys
v = sys.version_info
if v[:2] < (2,7) or (v[0] >= 3 and v[:2] < (3,3)):
error = "ERROR: IPython requires Python version 2.7 or 3.3 or above."
if v[:2] < (3,3):
error = "ERROR: Jupyter Hub requires Python version 3.3 or above."
print(error, file=sys.stderr)
sys.exit(1)
PY3 = (sys.version_info[0] >= 3)
if os.name in ('nt', 'dos'):
error = "ERROR: Windows is not supported"
@@ -34,14 +33,6 @@ from glob import glob
from distutils.core import setup
from subprocess import check_call
try:
execfile
except NameError:
# py3
def execfile(fname, globs, locs=None):
locs = locs or globs
exec(compile(open(fname).read(), fname, "exec"), globs, locs)
pjoin = os.path.join
here = os.path.abspath(os.path.dirname(__file__))
@@ -67,7 +58,9 @@ def get_data_files():
ns = {}
execfile(pjoin(here, 'jupyterhub', 'version.py'), ns)
with open(pjoin(here, 'jupyterhub', 'version.py')) as f:
exec(f.read(), {}, ns)
packages = []
for d, _, _ in os.walk('jupyterhub'):
@@ -92,13 +85,11 @@ setup_args = dict(
keywords = ['Interactive', 'Interpreter', 'Shell', 'Web'],
classifiers = [
'Intended Audience :: Developers',
'Intended Audience :: System Administrators',
'Intended Audience :: Science/Research',
'License :: OSI Approved :: BSD License',
'Programming Language :: Python',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3',
'Topic :: System :: Shells',
],
)
@@ -194,11 +185,14 @@ if 'setuptools' in sys.modules:
self.distribution.run_command('css')
develop.run(self)
setup_args['cmdclass']['develop'] = develop_js_css
setup_args['install_requires'] = install_requires = []
with open('requirements.txt') as f:
install_requires = [ line.strip() for line in f.readlines() ]
setup_args['install_requires'] = install_requires
for line in f.readlines():
req = line.strip()
if req.startswith('-e'):
req = line.split('#egg=', 1)[1]
install_requires.append(req)
#---------------------------------------------------------------------------
# setup