diff --git a/jupyterhub/app.py b/jupyterhub/app.py index e1f7a0c5..3b06b18f 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -352,7 +352,8 @@ class JupyterHub(Application): users = Instance(UserDict) def _users_default(self): - return UserDict(db_factory=lambda : self.db) + assert self.tornado_settings + return UserDict(db_factory=lambda : self.db, settings=self.tornado_settings) admin_access = Bool(False, config=True, help="""Grant admin users permission to access single-user servers. @@ -705,17 +706,14 @@ class JupyterHub(Application): yield user.stop() for orm_user in db.query(orm.User): - self.users[orm_user.id] = user = User(orm_user) + self.users[orm_user.id] = user = User(orm_user, self.tornado_settings) if not user.state: # without spawner state, server isn't valid user.server = None user_summaries.append(_user_summary(user)) continue self.log.debug("Loading state for %s from db", user.name) - user.spawner = spawner = self.spawner_class( - user=user, hub=self.hub, config=self.config, db=self.db, - authenticator=self.authenticator, - ) + spawner = user.spawner status = yield spawner.poll() if status is None: self.log.info("%s still running", user.name) @@ -860,7 +858,6 @@ class JupyterHub(Application): proxy=self.proxy, hub=self.hub, admin_users=self.authenticator.admin_users, - users=self.users, admin_access=self.admin_access, authenticator=self.authenticator, spawner_class=self.spawner_class, @@ -879,6 +876,8 @@ class JupyterHub(Application): # allow configured settings to have priority settings.update(self.tornado_settings) self.tornado_settings = settings + # constructing users requires access to tornado_settings + self.tornado_settings['users'] = self.users def init_tornado_application(self): """Instantiate the tornado Application object""" @@ -915,9 +914,9 @@ class JupyterHub(Application): self.init_hub() self.init_proxy() yield self.init_users() + self.init_tornado_settings() yield self.init_spawners() self.init_handlers() - self.init_tornado_settings() self.init_tornado_application() @gen.coroutine diff --git a/jupyterhub/handlers/base.py b/jupyterhub/handlers/base.py index 4465337d..11b5f17c 100644 --- a/jupyterhub/handlers/base.py +++ b/jupyterhub/handlers/base.py @@ -275,14 +275,8 @@ class BaseHandler(RequestHandler): raise RuntimeError("Spawn already pending for: %s" % user.name) tic = IOLoop.current().time() - f = user.spawn( - spawner_class=self.spawner_class, - base_url=self.base_url, - hub=self.hub, - config=self.config, - authenticator=self.authenticator, - options=options, - ) + f = user.spawn(options) + @gen.coroutine def finish_user_spawn(f=None): """Finish the user spawn by registering listeners and notifying the proxy. diff --git a/jupyterhub/tests/test_api.py b/jupyterhub/tests/test_api.py index c498a991..7e598aea 100644 --- a/jupyterhub/tests/test_api.py +++ b/jupyterhub/tests/test_api.py @@ -47,7 +47,7 @@ def add_user(db, app=None, **kwargs): db.add(orm_user) db.commit() if app: - user = app.users[orm_user.id] = User(orm_user) + user = app.users[orm_user.id] = User(orm_user, app.tornado_settings) return user else: return orm_user @@ -409,7 +409,7 @@ def test_slow_spawn(app, io_loop): def test_never_spawn(app, io_loop): - app.tornado_application.settings['spawner_class'] = mocking.NeverSpawner + app.tornado_settings['spawner_class'] = mocking.NeverSpawner app.tornado_application.settings['slow_spawn_timeout'] = 0 db = app.db diff --git a/jupyterhub/tests/test_orm.py b/jupyterhub/tests/test_orm.py index 8d4f5bc2..1e296d55 100644 --- a/jupyterhub/tests/test_orm.py +++ b/jupyterhub/tests/test_orm.py @@ -98,15 +98,19 @@ def test_spawn_fails(db, io_loop): orm_user = orm.User(name='aeofel') db.add(orm_user) db.commit() - user = User(orm_user) class BadSpawner(MockSpawner): @gen.coroutine def start(self): raise RuntimeError("Split the party") + user = User(orm_user, { + 'spawner_class': BadSpawner, + 'config': None, + }) + with pytest.raises(Exception) as exc: - io_loop.run_sync(lambda : user.spawn(BadSpawner)) + io_loop.run_sync(user.spawn) assert user.server is None assert not user.running diff --git a/jupyterhub/user.py b/jupyterhub/user.py index 0d5699b1..10446144 100644 --- a/jupyterhub/user.py +++ b/jupyterhub/user.py @@ -12,15 +12,18 @@ from sqlalchemy import inspect from .utils import url_path_join from . import orm -from traitlets import HasTraits, Any +from traitlets import HasTraits, Any, Dict +from .spawner import LocalProcessSpawner + class UserDict(dict): """Like defaultdict, but for users Getting by a user id OR an orm.User instance returns a User wrapper around the orm user. """ - def __init__(self, db_factory): + def __init__(self, db_factory, settings): self.db_factory = db_factory + self.settings = settings super().__init__() @property @@ -32,7 +35,7 @@ class UserDict(dict): # users[orm_user] returns User(orm_user) orm_user = key if orm_user.id not in self: - user = self[orm_user.id] = User(orm_user) + user = self[orm_user.id] = User(orm_user, self.settings) return user user = dict.__getitem__(self, orm_user.id) user.db = self.db @@ -43,7 +46,7 @@ class UserDict(dict): orm_user = self.db.query(orm.User).filter(orm.User.id==id).first() if orm_user is None: raise KeyError("No such user: %s" % id) - user = self[id] = User(orm_user) + user = self[id] = User(orm_user, self.settings) return dict.__getitem__(self, id) else: raise KeyError(repr(key)) @@ -54,6 +57,8 @@ class User(HasTraits): def _log_default(self): return app_log + settings = Dict() + db = Any(allow_none=True) def _db_default(self): if self.orm_user: @@ -71,9 +76,32 @@ class User(HasTraits): spawn_pending = False stop_pending = False - def __init__(self, orm_user, **kwargs): + @property + def authenticator(self): + return self.settings.get('authenticator', None) + + @property + def spawner_class(self): + return self.settings.get('spawner_class', LocalProcessSpawner) + + def __init__(self, orm_user, settings, **kwargs): self.orm_user = orm_user + self.settings = settings super().__init__(**kwargs) + + hub = self.db.query(orm.Hub).first() + + self.cookie_name = '%s-%s' % (hub.server.cookie_name, quote(self.name, safe='')) + self.base_url = url_path_join( + self.settings.get('base_url', '/'), 'user', self.escaped_name) + + self.spawner = self.spawner_class( + user=self, + db=self.db, + hub=hub, + authenticator=self.authenticator, + config=self.settings.get('config'), + ) # pass get/setattr to ORM user @@ -95,8 +123,6 @@ class User(HasTraits): @property def running(self): """property for whether a user has a running server""" - if self.spawner is None: - return False if self.server is None: return False return True @@ -107,16 +133,13 @@ class User(HasTraits): return quote(self.name, safe='@') @gen.coroutine - def spawn(self, spawner_class, base_url='/', hub=None, config=None, - authenticator=None, options=None): + def spawn(self, options=None): """Start the user's spawner""" db = self.db - if hub is None: - hub = db.query(orm.Hub).first() self.server = orm.Server( - cookie_name='%s-%s' % (hub.server.cookie_name, quote(self.name, safe='')), - base_url=url_path_join(base_url, 'user', self.escaped_name), + cookie_name=self.cookie_name, + base_url=self.base_url, ) db.add(self.server) db.commit() @@ -124,19 +147,14 @@ class User(HasTraits): api_token = self.new_api_token() db.commit() - spawner = self.spawner = spawner_class( - config=config, - user=self, - hub=hub, - db=db, - authenticator=authenticator, - user_options=options or {}, - ) + spawner = self.spawner + spawner.user_options = options or {} # we are starting a new server, make sure it doesn't restore state spawner.clear_state() spawner.api_token = api_token # trigger pre-spawn hook on authenticator + authenticator = self.authenticator if (authenticator): yield gen.maybe_future(authenticator.pre_spawn_start(self, spawner)) @@ -207,8 +225,6 @@ class User(HasTraits): """ self.spawn_pending = False spawner = self.spawner - if spawner is None: - return self.spawner.stop_polling() self.stop_pending = True try: