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 language: python
python: python:
- 3.4 - 3.4
- 2.7 - 3.3
before_install: before_install:
# workaround for https://github.com/travis-ci/travis-cookbooks/issues/155 # workaround for https://github.com/travis-ci/travis-cookbooks/issues/155
- sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm - sudo rm -rf /dev/shm && sudo ln -s /run/shm /dev/shm

View File

@@ -18,6 +18,8 @@ Basic principles:
## Dependencies ## 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: You will need nodejs/npm, which you can get from your package manager:
sudo apt-get install npm nodejs-legacy sudo apt-get install npm nodejs-legacy

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,7 +9,6 @@ import os
import requests import requests
from tornado import ioloop from tornado import ioloop
from tornado import web
from IPython.utils.traitlets import Unicode 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 from distutils.version import LooseVersion as V
import IPython import IPython
if V(IPython.__version__) < V('2.2'): if V(IPython.__version__) < V('3.0'):
raise ImportError("JupyterHub Requires IPython >= 2.2, found %s" % IPython.__version__) raise ImportError("JupyterHub Requires IPython >= 3.0, found %s" % IPython.__version__)
# Define two methods to attach to AuthenticatedHandler, # Define two methods to attach to AuthenticatedHandler,
# which authenticate via the central auth server. # which authenticate via the central auth server.
@@ -97,11 +96,6 @@ class SingleUserNotebookApp(NotebookApp):
# disable the exit confirmation for background notebook processes # disable the exit confirmation for background notebook processes
ioloop.IOLoop.instance().stop() 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): def init_webapp(self):
# monkeypatch authentication to use the hub # monkeypatch authentication to use the hub
from IPython.html.base.handlers import AuthenticatedHandler from IPython.html.base.handlers import AuthenticatedHandler
@@ -120,10 +114,7 @@ class SingleUserNotebookApp(NotebookApp):
# load the hub related settings into the tornado settings dict # load the hub related settings into the tornado settings dict
env = os.environ env = os.environ
# FIXME: IPython < 3 compat s = self.webapp_settings
s = getattr(self, 'tornado_settings',
getattr(self, 'webapp_settings')
)
s['cookie_cache'] = {} s['cookie_cache'] = {}
s['user'] = self.user s['user'] = self.user
s['hub_api_key'] = env.pop('JPY_API_TOKEN') 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. Subclasses should call super, to ensure that state is properly cleared.
""" """
self.api_token = u'' self.api_token = ''
def get_args(self): def get_args(self):
"""Return the arguments to be passed after self.cmd""" """Return the arguments to be passed after self.cmd"""
@@ -339,13 +339,13 @@ class LocalProcessSpawner(Spawner):
break break
pids = self._get_pg_pids(ppid) pids = self._get_pg_pids(ppid)
if pids: if pids:
raise gen.Return(pids[0]) return pids[0]
else: else:
yield gen.Task(loop.add_timeout, loop.time() + 0.1) yield gen.Task(loop.add_timeout, loop.time() + 0.1)
self.log.error("Failed to get single-user PID") self.log.error("Failed to get single-user PID")
# return sudo pid if we can't get the real PID # return sudo pid if we can't get the real PID
# this shouldn't happen # this shouldn't happen
raise gen.Return(ppid) return ppid
@gen.coroutine @gen.coroutine
def start(self): def start(self):
@@ -378,7 +378,7 @@ class LocalProcessSpawner(Spawner):
if status is not None: if status is not None:
# clear state if the process is done # clear state if the process is done
self.clear_state() self.clear_state()
raise gen.Return(status) return status
# if we resumed from stored state, # if we resumed from stored state,
# we don't have the Popen handle anymore, so rely on self.pid # we don't have the Popen handle anymore, so rely on self.pid
@@ -386,16 +386,16 @@ class LocalProcessSpawner(Spawner):
if not self.pid: if not self.pid:
# no pid, not running # no pid, not running
self.clear_state() self.clear_state()
raise gen.Return(0) return 0
# send signal 0 to check if PID exists # send signal 0 to check if PID exists
# this doesn't work on Windows, but that's okay because we don't support Windows. # this doesn't work on Windows, but that's okay because we don't support Windows.
alive = yield self._signal(0) alive = yield self._signal(0)
if not alive: if not alive:
self.clear_state() self.clear_state()
raise gen.Return(0) return 0
else: else:
raise gen.Return(None) return None
@gen.coroutine @gen.coroutine
def _signal(self, sig): def _signal(self, sig):
@@ -405,9 +405,9 @@ class LocalProcessSpawner(Spawner):
""" """
if self.set_user == 'sudo': if self.set_user == 'sudo':
rc = yield self._signal_sudo(sig) rc = yield self._signal_sudo(sig)
raise gen.Return(rc) return rc
else: else:
raise gen.Return(self._signal_setuid(sig)) return self._signal_setuid(sig)
def _signal_setuid(self, sig): def _signal_setuid(self, sig):
"""simple implementation of signal, which we can use when we are using setuid (we are root)""" """simple implementation of signal, which we can use when we are using setuid (we are root)"""
@@ -427,10 +427,10 @@ class LocalProcessSpawner(Spawner):
try: try:
check_output(['ps', '-p', str(self.pid)], stderr=PIPE) check_output(['ps', '-p', str(self.pid)], stderr=PIPE)
except CalledProcessError: except CalledProcessError:
raise gen.Return(False) # process is gone return False # process is gone
else: else:
if sig == 0: if sig == 0:
raise gen.Return(True) # process exists return True # process exists
# build sudo -u user kill -SIG PID # build sudo -u user kill -SIG PID
cmd = self.sudo_cmd(self.user) cmd = self.sudo_cmd(self.user)
@@ -441,7 +441,7 @@ class LocalProcessSpawner(Spawner):
check_output(cmd, check_output(cmd,
preexec_fn=self.make_preexec_fn(self.user.name), preexec_fn=self.make_preexec_fn(self.user.name),
) )
raise gen.Return(True) # process exists return True # process exists
@gen.coroutine @gen.coroutine
def stop(self, now=False): def stop(self, now=False):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,34 +5,17 @@
from binascii import b2a_hex from binascii import b2a_hex
import errno import errno
import getpass
import hashlib import hashlib
import os import os
import socket import socket
import uuid import uuid
from six import text_type
from tornado import web, gen, ioloop from tornado import web, gen, ioloop
from tornado.httpclient import AsyncHTTPClient, HTTPError from tornado.httpclient import AsyncHTTPClient, HTTPError
from tornado.log import app_log from tornado.log import app_log
from IPython.html.utils import url_path_join 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(): def random_port():
"""get a single random port""" """get a single random port"""
@@ -147,7 +130,7 @@ def new_token(*args, **kwargs):
For now, just UUIDs. For now, just UUIDs.
""" """
return text_type(uuid.uuid4().hex) return uuid.uuid4().hex
def hash_token(token, salt=8, rounds=16384, algorithm='sha512'): 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) h.update(btoken)
digest = h.hexdigest() digest = h.hexdigest()
return u"{algorithm}:{rounds}:{salt}:{digest}".format(**locals()) return "{algorithm}:{rounds}:{salt}:{digest}".format(**locals())
def compare_token(compare, token): def compare_token(compare, token):

View File

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

View File

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