diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 5544a8de..ca0e6f3f 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -50,7 +50,7 @@ from .handlers.static import CacheControlStaticFilesHandler from . import orm from ._data import DATA_FILES_PATH from .log import CoroutineLogFormatter, log_request -from .traitlets import URLPrefix +from .traitlets import URLPrefix, Command from .utils import ( url_path_join, ISO8601_ms, ISO8601_s, @@ -237,7 +237,7 @@ class JupyterHub(Application): help="Supply extra arguments that will be passed to Jinja environment." ) - proxy_cmd = Unicode('configurable-http-proxy', config=True, + proxy_cmd = Command('configurable-http-proxy', config=True, help="""The command to start the http proxy. Only override if configurable-http-proxy is not on your PATH @@ -742,7 +742,7 @@ class JupyterHub(Application): env = os.environ.copy() env['CONFIGPROXY_AUTH_TOKEN'] = self.proxy.auth_token - cmd = [self.proxy_cmd, + cmd = self.proxy_cmd + [ '--ip', self.proxy.public_server.ip, '--port', str(self.proxy.public_server.port), '--api-ip', self.proxy.api_server.ip, diff --git a/jupyterhub/spawner.py b/jupyterhub/spawner.py index 46be3a2a..bdf4f351 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -21,6 +21,7 @@ from IPython.utils.traitlets import ( Any, Bool, Dict, Enum, Instance, Integer, Float, List, Unicode, ) +from .traitlets import Command from .utils import random_port NUM_PAT = re.compile(r'\d+') @@ -93,7 +94,7 @@ class Spawner(LoggingConfigurable): env['JPY_API_TOKEN'] = self.api_token return env - cmd = List(Unicode, default_value=['jupyterhub-singleuser'], config=True, + cmd = Command(['jupyterhub-singleuser'], config=True, help="""The command used for starting notebooks.""" ) args = List(Unicode, config=True, diff --git a/jupyterhub/tests/test_traitlets.py b/jupyterhub/tests/test_traitlets.py new file mode 100644 index 00000000..5c1fa895 --- /dev/null +++ b/jupyterhub/tests/test_traitlets.py @@ -0,0 +1,26 @@ +from traitlets import HasTraits +from jupyterhub.traitlets import URLPrefix, Command + +def test_url_prefix(): + class C(HasTraits): + url = URLPrefix() + + c = C() + c.url = '/a/b/c/' + assert c.url == '/a/b/c/' + c.url = '/a/b' + assert c.url == '/a/b/' + c.url = 'a/b/c/d' + assert c.url == '/a/b/c/d/' + +def test_command(): + class C(HasTraits): + cmd = Command('default command') + cmd2 = Command(['default_cmd']) + + c = C() + assert c.cmd == ['default command'] + assert c.cmd2 == ['default_cmd'] + c.cmd = 'foo bar' + assert c.cmd == ['foo bar'] + diff --git a/jupyterhub/traitlets.py b/jupyterhub/traitlets.py index 065ac1d5..d838895c 100644 --- a/jupyterhub/traitlets.py +++ b/jupyterhub/traitlets.py @@ -2,7 +2,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. -from IPython.utils.traitlets import Unicode +from IPython.utils.traitlets import List, Unicode class URLPrefix(Unicode): def validate(self, obj, value): @@ -12,3 +12,18 @@ class URLPrefix(Unicode): if not u.endswith('/'): u = u + '/' return u + +class Command(List): + """Traitlet for a command that should be a list of strings, + but allows it to be specified as a single string. + """ + def __init__(self, default_value=None, **kwargs): + kwargs.setdefault('minlen', 1) + if isinstance(default_value, str): + default_value = [default_value] + super().__init__(Unicode, default_value, **kwargs) + + def validate(self, obj, value): + if isinstance(value, str): + value = [value] + return super().validate(obj, value)