From b8f4803ef4d4d254d7d817c130470dd5a5ac9126 Mon Sep 17 00:00:00 2001 From: Min RK Date: Thu, 15 Sep 2016 12:05:46 +0200 Subject: [PATCH] Deprecate `%U` username substitution use Python format-strings instead. --- docs/source/api/spawner.rst | 2 +- jupyterhub/spawner.py | 65 ++++++++++++++++++++++++++++---- jupyterhub/tests/test_spawner.py | 14 +++++++ 3 files changed, 73 insertions(+), 8 deletions(-) diff --git a/docs/source/api/spawner.rst b/docs/source/api/spawner.rst index 15cb847d68..8721280794 100644 --- a/docs/source/api/spawner.rst +++ b/docs/source/api/spawner.rst @@ -13,6 +13,6 @@ Module: :mod:`jupyterhub.spawner` ---------------- .. autoclass:: Spawner - :members: options_from_form, poll, start, stop, get_args, get_env, get_state + :members: options_from_form, poll, start, stop, get_args, get_env, get_state, template_namespace, format_string .. autoclass:: LocalProcessSpawner diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 65d0dd3145..8fea138407 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -21,6 +21,7 @@ from traitlets.config import LoggingConfigurable from traitlets import ( Any, Bool, Dict, Instance, Integer, Float, List, Unicode, + validate, ) from .traitlets import Command @@ -147,7 +148,7 @@ def options_from_form(self, form_data): help="""The notebook directory for the single-user server `~` will be expanded to the user's home directory - `%U` will be expanded to the user's username + `{username}` will be expanded to the user's username """ ).tag(config=True) @@ -158,10 +159,22 @@ def options_from_form(self, form_data): full filesystem traversal, while preserving user's homedir as landing page for notebook - `%U` will be expanded to the user's username + `{username}` will be expanded to the user's username """ ).tag(config=True) + @validate('notebook_dir', 'default_url') + def _deprecate_percent_u(self, proposal): + print(proposal) + v = proposal['value'] + if '%U' in v: + self.log.warn("%%U for username in %s is deprecated in JupyterHub 0.7, use {username}", + proposal['trait'].name, + ) + v = v.replace('%U', '{username}') + self.log.warn("Converting %r to %r", proposal['value'], v) + return v + disable_user_config = Bool(False, help="""Disable per-user configuration of single-user servers. @@ -243,7 +256,45 @@ def get_env(self): env['JPY_API_TOKEN'] = self.api_token return env - + + def template_namespace(self): + """Return the template namespace for format-string formatting. + + Currently used on default_url and notebook_dir. + + Subclasses may add items to the available namespace. + + The default implementation includes:: + + { + 'username': user.name, + 'base_url': users_base_url, + } + + Returns: + + ns (dict): namespace for string formatting. + """ + d = {'username': self.user.name} + if self.user.server: + d['base_url'] = self.user.server.base_url + return d + + def format_string(self, s): + """Render a Python format string + + Uses :meth:`Spawner.template_namespace` to populate format namespace. + + Args: + + s (str): Python format-string to be formatted. + + Returns: + + str: Formatted string, rendered + """ + return s.format(**self.template_namespace()) + def get_args(self): """Return the arguments to be passed after self.cmd""" args = [ @@ -264,11 +315,11 @@ def get_args(self): args.append('--port=%i' % self.user.server.port) if self.notebook_dir: - self.notebook_dir = self.notebook_dir.replace("%U",self.user.name) - args.append('--notebook-dir=%s' % self.notebook_dir) + notebook_dir = self.format_string(self.notebook_dir) + args.append('--notebook-dir=%s' % notebook_dir) if self.default_url: - self.default_url = self.default_url.replace("%U",self.user.name) - args.append('--NotebookApp.default_url=%s' % self.default_url) + default_url = self.format_string(self.default_url) + args.append('--NotebookApp.default_url=%s' % default_url) if self.debug: args.append('--debug') diff --git a/jupyterhub/tests/test_spawner.py b/jupyterhub/tests/test_spawner.py index 342167b974..68dfcf4343 100644 --- a/jupyterhub/tests/test_spawner.py +++ b/jupyterhub/tests/test_spawner.py @@ -41,6 +41,8 @@ def new_spawner(db, **kwargs): kwargs.setdefault('cmd', [sys.executable, '-c', _echo_sleep]) kwargs.setdefault('user', db.query(orm.User).first()) kwargs.setdefault('hub', db.query(orm.Hub).first()) + kwargs.setdefault('notebook_dir', os.getcwd()) + kwargs.setdefault('default_url', '/user/{username}/lab') kwargs.setdefault('INTERRUPT_TIMEOUT', 1) kwargs.setdefault('TERM_TIMEOUT', 1) kwargs.setdefault('KILL_TIMEOUT', 1) @@ -128,6 +130,7 @@ def test_stop_spawner_stop_now(db, io_loop): status = io_loop.run_sync(spawner.poll) assert status == -signal.SIGTERM + def test_spawner_poll(db, io_loop): first_spawner = new_spawner(db) user = first_spawner.user @@ -160,6 +163,7 @@ def test_spawner_poll(db, io_loop): status = io_loop.run_sync(spawner.poll) assert status is not None + def test_setcwd(): cwd = os.getcwd() with tempfile.TemporaryDirectory() as td: @@ -178,3 +182,13 @@ def raiser(path): spawnermod._try_setcwd(cwd) assert os.getcwd().startswith(temp_root) os.chdir(cwd) + + +def test_string_formatting(db): + s = new_spawner(db, notebook_dir='user/%U/', default_url='/base/{username}') + name = s.user.name + assert s.notebook_dir == 'user/{username}/' + assert s.default_url == '/base/{username}' + assert s.format_string(s.notebook_dir) == 'user/%s/' % name + assert s.format_string(s.default_url) == '/base/%s' % name +