diff --git a/.travis.yml b/.travis.yml index 4ff5f87d..7d579cf9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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 diff --git a/README.md b/README.md index fbe3a7ba..35ae63a7 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/dev-requirements.txt b/dev-requirements.txt index 93253de9..26b77f68 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,2 @@ -mock +-r requirements.txt pytest diff --git a/jupyterhub/__init__.py b/jupyterhub/__init__.py index 0eea61cc..48af2d06 100644 --- a/jupyterhub/__init__.py +++ b/jupyterhub/__init__.py @@ -1 +1,2 @@ from .version import * + diff --git a/jupyterhub/apihandlers/base.py b/jupyterhub/apihandlers/base.py index 8aa0046a..c55e2e8a 100644 --- a/jupyterhub/apihandlers/base.py +++ b/jupyterhub/apihandlers/base.py @@ -4,11 +4,7 @@ import json -try: - # py3 - from http.client import responses -except ImportError: - from httplib import responses +from http.client 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: diff --git a/jupyterhub/apihandlers/users.py b/jupyterhub/apihandlers/users.py index 2e65bb75..0464474a 100644 --- a/jupyterhub/apihandlers/users.py +++ b/jupyterhub/apihandlers/users.py @@ -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, } diff --git a/jupyterhub/app.py b/jupyterhub/app.py index f331d0d3..74ce26a8 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -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 @@ -299,18 +301,11 @@ class JupyterHubApp(Application): def _log_datefmt_default(self): """Exclude date from default date format""" return "%H:%M:%S" - + 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 # 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.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 diff --git a/jupyterhub/auth.py b/jupyterhub/auth.py index 5556f2e5..5461e8b0 100644 --- a/jupyterhub/auth.py +++ b/jupyterhub/auth.py @@ -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 \ No newline at end of file diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index ef308b96..8215563b 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -5,11 +5,7 @@ import re from datetime import datetime -try: - # py3 - from http.client import responses -except ImportError: - from httplib import responses +from http.client 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): diff --git a/jupyterhub/orm.py b/jupyterhub/orm.py index 69a183a2..5abbc04c 100644 --- a/jupyterhub/orm.py +++ b/jupyterhub/orm.py @@ -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 "" % (self.ip, self.port) diff --git a/jupyterhub/singleuser.py b/jupyterhub/singleuser.py index 1ac4c58b..e99c5a52 100644 --- a/jupyterhub/singleuser.py +++ b/jupyterhub/singleuser.py @@ -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') diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index c55872e0..faec7034 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -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): diff --git a/jupyterhub/tests/conftest.py b/jupyterhub/tests/conftest.py index 948b36f8..26270702 100644 --- a/jupyterhub/tests/conftest.py +++ b/jupyterhub/tests/conftest.py @@ -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( diff --git a/jupyterhub/tests/mocking.py b/jupyterhub/tests/mocking.py index 21b42483..bb56df53 100644 --- a/jupyterhub/tests/mocking.py +++ b/jupyterhub/tests/mocking.py @@ -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 unittest 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 diff --git a/jupyterhub/tests/test_app.py b/jupyterhub/tests/test_app.py index ed31b701..a8894e14 100644 --- a/jupyterhub/tests/test_app.py +++ b/jupyterhub/tests/test_app.py @@ -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 diff --git a/jupyterhub/tests/test_auth.py b/jupyterhub/tests/test_auth.py index a4d4d459..8e5103c7 100644 --- a/jupyterhub/tests/test_auth.py +++ b/jupyterhub/tests/test_auth.py @@ -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 diff --git a/jupyterhub/tests/test_orm.py b/jupyterhub/tests/test_orm.py index 738406aa..bb789baa 100644 --- a/jupyterhub/tests/test_orm.py +++ b/jupyterhub/tests/test_orm.py @@ -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() diff --git a/jupyterhub/utils.py b/jupyterhub/utils.py index 3a8750d0..a0aab211 100644 --- a/jupyterhub/utils.py +++ b/jupyterhub/utils.py @@ -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): diff --git a/requirements.txt b/requirements.txt index 063f082d..19e55f0b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/setup.py b/setup.py index 0a61fe64..14b5f364 100755 --- a/setup.py +++ b/setup.py @@ -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