diff --git a/jupyterhub/app.py b/jupyterhub/app.py index 5544a8de84..ca0e6f3ff9 100644 --- a/jupyterhub/app.py +++ b/jupyterhub/app.py @@ -50,7 +50,7 @@ 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 @@ def _template_paths_default(self): 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 @@ def start_proxy(self): 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 46be3a2a1d..bdf4f35100 100644 --- a/jupyterhub/spawner.py +++ b/jupyterhub/spawner.py @@ -21,6 +21,7 @@ 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 @@ def _env_default(self): 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_proxy.py b/jupyterhub/tests/test_proxy.py index 55be87c08f..e55a57a562 100644 --- a/jupyterhub/tests/test_proxy.py +++ b/jupyterhub/tests/test_proxy.py @@ -26,7 +26,7 @@ def fin(): request.addfinalizer(fin) env = os.environ.copy() env['CONFIGPROXY_AUTH_TOKEN'] = auth_token - cmd = [app.proxy_cmd, + cmd = app.proxy_cmd + [ '--ip', app.ip, '--port', str(app.port), '--api-ip', proxy_ip, @@ -82,7 +82,7 @@ def wait_for_proxy(): new_auth_token = 'different!' env['CONFIGPROXY_AUTH_TOKEN'] = new_auth_token proxy_port = 55432 - cmd = [app.proxy_cmd, + cmd = app.proxy_cmd + [ '--ip', app.ip, '--port', str(app.port), '--api-ip', app.proxy_api_ip, diff --git a/jupyterhub/tests/test_traitlets.py b/jupyterhub/tests/test_traitlets.py new file mode 100644 index 0000000000..3ede4ea9bb --- /dev/null +++ b/jupyterhub/tests/test_traitlets.py @@ -0,0 +1,30 @@ +try: + from traitlets import HasTraits +except ImportError: + from IPython.utils.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 065ac1d5fa..d838895ca3 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 @@ def validate(self, obj, value): 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)