From a73a11d66893bc6ec6a907bfed3214720b89da6f Mon Sep 17 00:00:00 2001 From: Zsailer Date: Sat, 18 May 2019 17:07:25 -0600 Subject: [PATCH 01/12] add server extension submodule to jupyter_server --- jupyter_server/extension/__init__.py | 0 jupyter_server/extension/application.py | 159 ++++++++++++++++++++++++ jupyter_server/extension/handler.py | 67 ++++++++++ 3 files changed, 226 insertions(+) create mode 100644 jupyter_server/extension/__init__.py create mode 100644 jupyter_server/extension/application.py create mode 100644 jupyter_server/extension/handler.py diff --git a/jupyter_server/extension/__init__.py b/jupyter_server/extension/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py new file mode 100644 index 0000000000..120a0611f0 --- /dev/null +++ b/jupyter_server/extension/application.py @@ -0,0 +1,159 @@ +import sys + +from traitlets.config.application import catch_config_error +from traitlets.config.configurable import Configurable +from traitlets import ( + Unicode, + List, + Dict, + default, + validate +) + +from jupyter_core.application import JupyterApp + +from jupyter_server.serverapp import ServerApp +from jupyter_server.transutils import trans, _ +from jupyter_server.utils import url_path_join +from jupyter_server.base.handlers import FileFindHandler + + +class ExtensionApp(JupyterApp): + """A base class for writing configurable Jupyter Server extension applications. + + These applications can be loaded using jupyter_server's + extension load mechanism or launched using Jupyter's command line interface. + """ + # Name of the extension + extension_name = Unicode( + "", + help="Name of extension." + ) + + @default("extension_name") + def _default_extension_name(self): + raise Exception("The extension must be given a `name`.") + + @validate("extension_name") + def _default_extension_name(self, obj, value): + if isinstance(name, str): + # Validate that extension_name doesn't contain any invalid characters. + for char in [' ', '.', '+', '/']: + self.error(obj, value) + return value + self.error(obj, value) + + # Extension can configure the ServerApp from the command-line + classes = [ + ServerApp + ] + + static_file_path = List(Unicode(), + help="""paths to search for serving static files. + + This allows adding javascript/css to be available from the notebook server machine, + or overriding individual files in the IPython + """ + ).tag(config=True) + + template_path = List(Unicode(), + help=_("""Paths to search for serving jinja templates. + + Can be used to override templates from notebook.templates.""") + ).tag(config=True) + + settings = Dict( + help=_("""Settings that will passed to the server.""") + ) + + handlers = List( + help=_("""Handlers appended to the server.""") + ) + + default_url = Unicode('/', config=True, + help=_("The default URL to redirect to from `/`") + ) + + def initialize_static_handler(self): + # Check to see if + if len(self.static_file_path) > 0: + # Append the extension's static directory to server handlers. + static_url = url_path_join("/static", self.extension_name, "(.*)") + + # Construct handler. + handler = (static_url, FileFindHandler, {'path': self.static_file_path}) + self.handlers.append(handler) + + # Add the file paths to webapp settings. + self.settings.update({ + "{}_static_path".format(self.extension_name): self.static_file_path, + "{}_template_path".format(self.extension_name): self.template_path + }) + + def initialize_handlers(self): + """Override this method to append handlers to a Jupyter Server.""" + pass + + def initialize_templates(self): + """""" + pass + + def initialize_settings(self): + """""" + pass + + @staticmethod + def initialize_server(): + """Add handlers to server.""" + serverapp = ServerApp() + serverapp.initialize() + return serverapp + + def initialize(self, serverapp, argv=None): + super(ExtensionApp, self).initialize(argv=argv) + self.serverapp = serverapp + + def start(self, **kwargs): + # Start the server. + self.serverapp.start() + + @classmethod + def launch_instance(cls, argv=None, **kwargs): + """Lśunch the ServerApp and Server Extension Application. + + Properly orders the steps to initialize and start the server and extension. + """ + # Initialize the server + serverapp = cls.initialize_server() + + # Load the extension + extension = cls.load_jupyter_server_extension(serverapp, argv=argv, **kwargs) + + # Start the browser at this extensions default_url. + serverapp.default_url = extension.default_url + + # Start the application. + extension.start() + + @classmethod + def load_jupyter_server_extension(cls, serverapp, argv=None, **kwargs): + """Load this extension following the server extension loading mechanism.""" + # Get webapp from the server. + webapp = serverapp.web_app + + # Create an instance and initialize extension. + extension = cls() + extension.initialize(serverapp, argv=argv) + extension.initialize_settings() + extension.initialize_handlers() + extension.initialize_static_handler() + extension.initialize_templates() + + # Make extension settings accessible to handlers inside webapp settings. + webapp.settings.update(**extension.settings) + webapp.settings.update(**) + + # Add handlers to serverapp. + webapp.add_handlers('.*$', extension.handlers) + + return extension \ No newline at end of file diff --git a/jupyter_server/extension/handler.py b/jupyter_server/extension/handler.py new file mode 100644 index 0000000000..4fc4bd8599 --- /dev/null +++ b/jupyter_server/extension/handler.py @@ -0,0 +1,67 @@ +from jupyter_server.base.handlers import JupyterHandler, FileFindHandler +from traitlets import Unicode, default + + +class ExtensionHandler(JupyterHandler): + """Base class for Jupyter server extension handlers. + + Subclasses can serve static files behind a namespaced + endpoint: "/static//" + + This allows multiple extensions to serve static files under + their own namespace and avoid intercepting requests for + other extensions. + """ + extension_name = Unicode(help="Name of the extenxsion") + + @default('extension_name') + def _default_extension_name(self): + raise Exception("extension_name must be set in {}.".format(self.__class__)) + + @property + def static_url_prefix(self): + return "/static/{extension_name}/".format( + extension_name=self.extension_name) + + @property + def static_path(self): + return self.settings['{}_static_path'.format(self.extension_name)] + + def static_url(self, path, include_host=None, **kwargs): + """Returns a static URL for the given relative static file path. + This method requires you set the ``{extension_name}_static_path`` + setting in your extension (which specifies the root directory + of your static files). + This method returns a versioned url (by default appending + ``?v=``), which allows the static files to be + cached indefinitely. This can be disabled by passing + ``include_version=False`` (in the default implementation; + other static file implementations are not required to support + this, but they may support other options). + By default this method returns URLs relative to the current + host, but if ``include_host`` is true the URL returned will be + absolute. If this handler has an ``include_host`` attribute, + that value will be used as the default for all `static_url` + calls that do not pass ``include_host`` as a keyword argument. + """ + self.require_setting("{}_static_path".format(self.extension_name), "static_url") + + get_url = self.settings.get( + "static_handler_class", FileFindHandler + ).make_static_url + + if include_host is None: + include_host = getattr(self, "include_host", False) + + if include_host: + base = self.request.protocol + "://" + self.request.host + else: + base = "" + + # Hijack settings dict to send extension templates to extension + # static directory. + settings = { + "static_path": self.static_path, + "static_url_prefix": self.static_url_prefix + } + return base + get_url(settings, path, **kwargs) From 78134bdc13a9275738552f1dcb9f74b89bfd2e0f Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sun, 19 May 2019 03:20:42 -0500 Subject: [PATCH 02/12] clean up docs --- jupyter_server/extension/application.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index 120a0611f0..7402d5347f 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -49,17 +49,11 @@ def _default_extension_name(self, obj, value): ] static_file_path = List(Unicode(), - help="""paths to search for serving static files. - - This allows adding javascript/css to be available from the notebook server machine, - or overriding individual files in the IPython - """ + help="""paths to search for serving static files for the extension.""" ).tag(config=True) template_path = List(Unicode(), - help=_("""Paths to search for serving jinja templates. - - Can be used to override templates from notebook.templates.""") + help=_("""Paths to search for serving jinja templates for the extension.""") ).tag(config=True) settings = Dict( @@ -95,11 +89,11 @@ def initialize_handlers(self): pass def initialize_templates(self): - """""" + """Override this method to add handling of template files.""" pass def initialize_settings(self): - """""" + """Override this method to add handling of settings.""" pass @staticmethod @@ -110,16 +104,18 @@ def initialize_server(): return serverapp def initialize(self, serverapp, argv=None): + """Initialize the extension app.""" super(ExtensionApp, self).initialize(argv=argv) self.serverapp = serverapp def start(self, **kwargs): + """Start the extension app.""" # Start the server. self.serverapp.start() @classmethod def launch_instance(cls, argv=None, **kwargs): - """Lśunch the ServerApp and Server Extension Application. + """Launch the ServerApp and Server Extension Application. Properly orders the steps to initialize and start the server and extension. """ From 344e566108c1f376bb5b1c46b718f032433adee8 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Mon, 20 May 2019 12:58:08 -0700 Subject: [PATCH 03/12] rename static_paths and template_paths --- jupyter_server/extension/application.py | 23 ++++++++++++++--------- jupyter_server/extension/handler.py | 14 ++++++++++++-- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index 7402d5347f..9de5dc456d 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -48,12 +48,18 @@ def _default_extension_name(self, obj, value): ServerApp ] - static_file_path = List(Unicode(), - help="""paths to search for serving static files for the extension.""" + static_paths = List(Unicode(), + help="""paths to search for serving static files. + + This allows adding javascript/css to be available from the notebook server machine, + or overriding individual files in the IPython + """ ).tag(config=True) - template_path = List(Unicode(), - help=_("""Paths to search for serving jinja templates for the extension.""") + template_paths = List(Unicode(), + help=_("""Paths to search for serving jinja templates. + + Can be used to override templates from notebook.templates.""") ).tag(config=True) settings = Dict( @@ -70,18 +76,18 @@ def _default_extension_name(self, obj, value): def initialize_static_handler(self): # Check to see if - if len(self.static_file_path) > 0: + if len(self.static_paths) > 0: # Append the extension's static directory to server handlers. static_url = url_path_join("/static", self.extension_name, "(.*)") # Construct handler. - handler = (static_url, FileFindHandler, {'path': self.static_file_path}) + handler = (static_url, FileFindHandler, {'path': self.static_paths}) self.handlers.append(handler) # Add the file paths to webapp settings. self.settings.update({ - "{}_static_path".format(self.extension_name): self.static_file_path, - "{}_template_path".format(self.extension_name): self.template_path + "{}_static_paths".format(self.extension_name): self.static_paths, + "{}_template_paths".format(self.extension_name): self.template_paths }) def initialize_handlers(self): @@ -147,7 +153,6 @@ def load_jupyter_server_extension(cls, serverapp, argv=None, **kwargs): # Make extension settings accessible to handlers inside webapp settings. webapp.settings.update(**extension.settings) - webapp.settings.update(**) # Add handlers to serverapp. webapp.add_handlers('.*$', extension.handlers) diff --git a/jupyter_server/extension/handler.py b/jupyter_server/extension/handler.py index 4fc4bd8599..6b0895a19f 100644 --- a/jupyter_server/extension/handler.py +++ b/jupyter_server/extension/handler.py @@ -25,7 +25,7 @@ def static_url_prefix(self): @property def static_path(self): - return self.settings['{}_static_path'.format(self.extension_name)] + return self.settings['{}_static_paths'.format(self.extension_name)] def static_url(self, path, include_host=None, **kwargs): """Returns a static URL for the given relative static file path. @@ -44,7 +44,17 @@ def static_url(self, path, include_host=None, **kwargs): that value will be used as the default for all `static_url` calls that do not pass ``include_host`` as a keyword argument. """ - self.require_setting("{}_static_path".format(self.extension_name), "static_url") + key = "{}_static_paths".format(self.extension_name) + try: + self.require_setting(key, "static_url") + except e: + if key in self.settings: + raise Exception( + "This extension doesn't have any static paths listed. Check that the " + "extension's `static_paths` trait is set." + ) + else: + raise e get_url = self.settings.get( "static_handler_class", FileFindHandler From 0357d2ac8d7df44828aa251dccad8c50e4ace668 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Tue, 21 May 2019 13:32:21 -0700 Subject: [PATCH 04/12] rename extension classes and add _prepare calls --- jupyter_server/extension/application.py | 69 ++++++++++++++++--------- jupyter_server/extension/handler.py | 2 +- 2 files changed, 45 insertions(+), 26 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index 9de5dc456d..bf8124c88f 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -18,7 +18,7 @@ from jupyter_server.base.handlers import FileFindHandler -class ExtensionApp(JupyterApp): +class ServerAppExtensionBase(JupyterApp): """A base class for writing configurable Jupyter Server extension applications. These applications can be loaded using jupyter_server's @@ -48,6 +48,11 @@ def _default_extension_name(self, obj, value): ServerApp ] + @property + def static_url_prefix(self): + return "/static/{extension_name}/".format( + extension_name=self.extension_name) + static_paths = List(Unicode(), help="""paths to search for serving static files. @@ -74,8 +79,34 @@ def _default_extension_name(self, obj, value): help=_("The default URL to redirect to from `/`") ) - def initialize_static_handler(self): - # Check to see if + def initialize_settings(self): + """Override this method to add handling of settings.""" + pass + + def initialize_handlers(self): + """Override this method to append handlers to a Jupyter Server.""" + pass + + def initialize_templates(self): + """Override this method to add handling of template files.""" + pass + + def _prepare_settings(self): + webapp = self.serverapp.web_app + + # Get setting defined by subclass. + self.initialize_settings() + + # Update with server settings so that they are available to handlers. + self.settings.update(**webapp.settings) + + def _prepare_handlers(self): + webapp = self.serverapp.web_app + + # Get handlers defined by subclass + self.initialize_handlers() + + # Add static endpoint for this extension, if static paths are given. if len(self.static_paths) > 0: # Append the extension's static directory to server handlers. static_url = url_path_join("/static", self.extension_name, "(.*)") @@ -90,17 +121,11 @@ def initialize_static_handler(self): "{}_template_paths".format(self.extension_name): self.template_paths }) - def initialize_handlers(self): - """Override this method to append handlers to a Jupyter Server.""" - pass - - def initialize_templates(self): - """Override this method to add handling of template files.""" - pass + # Add handlers to serverapp. + webapp.add_handlers('.*$', self.handlers) - def initialize_settings(self): - """Override this method to add handling of settings.""" - pass + def _prepare_templates(self): + self.initialize_templates() @staticmethod def initialize_server(): @@ -111,7 +136,7 @@ def initialize_server(): def initialize(self, serverapp, argv=None): """Initialize the extension app.""" - super(ExtensionApp, self).initialize(argv=argv) + super(ServerAppExtensionBase, self).initialize(argv=argv) self.serverapp = serverapp def start(self, **kwargs): @@ -128,7 +153,7 @@ def launch_instance(cls, argv=None, **kwargs): # Initialize the server serverapp = cls.initialize_server() - # Load the extension + # Load the extension nk extension = cls.load_jupyter_server_extension(serverapp, argv=argv, **kwargs) # Start the browser at this extensions default_url. @@ -146,15 +171,9 @@ def load_jupyter_server_extension(cls, serverapp, argv=None, **kwargs): # Create an instance and initialize extension. extension = cls() extension.initialize(serverapp, argv=argv) - extension.initialize_settings() - extension.initialize_handlers() - extension.initialize_static_handler() - extension.initialize_templates() - - # Make extension settings accessible to handlers inside webapp settings. - webapp.settings.update(**extension.settings) - - # Add handlers to serverapp. - webapp.add_handlers('.*$', extension.handlers) + # Initialize settings + extension._prepare_settings() + extension._prepare_handlers() + extension._prepare_templates() return extension \ No newline at end of file diff --git a/jupyter_server/extension/handler.py b/jupyter_server/extension/handler.py index 6b0895a19f..33aa5fc286 100644 --- a/jupyter_server/extension/handler.py +++ b/jupyter_server/extension/handler.py @@ -2,7 +2,7 @@ from traitlets import Unicode, default -class ExtensionHandler(JupyterHandler): +class ServerHandlerExtensionBase(JupyterHandler): """Base class for Jupyter server extension handlers. Subclasses can serve static files behind a namespaced From 91b2886a8de3b118ca524584e88f2c532568e84f Mon Sep 17 00:00:00 2001 From: Zsailer Date: Mon, 3 Jun 2019 12:53:03 -0700 Subject: [PATCH 05/12] reverting class names --- jupyter_server/extension/application.py | 65 +++++++++++++++++-------- jupyter_server/extension/handler.py | 7 +-- 2 files changed, 48 insertions(+), 24 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index bf8124c88f..b88964d460 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -18,7 +18,7 @@ from jupyter_server.base.handlers import FileFindHandler -class ServerAppExtensionBase(JupyterApp): +class ExtensionApp(JupyterApp): """A base class for writing configurable Jupyter Server extension applications. These applications can be loaded using jupyter_server's @@ -92,55 +92,78 @@ def initialize_templates(self): pass def _prepare_settings(self): + # Make webapp settings accessible to initialize_settings method webapp = self.serverapp.web_app + self.settings.update(**webapp.settings) + + # Add static and template paths to settings. + self.settings.update({ + "{}_static_paths".format(self.extension_name): self.static_paths, + }) + - # Get setting defined by subclass. + # Get setting defined by subclass using initialize_settings method. self.initialize_settings() - - # Update with server settings so that they are available to handlers. - self.settings.update(**webapp.settings) + + # Update server settings with extension settings. + webapp.settings.update(**self.settings) def _prepare_handlers(self): webapp = self.serverapp.web_app + print(webapp.default_router.rules) - # Get handlers defined by subclass + # Get handlers defined by extension subclass. self.initialize_handlers() - + + # prepend base_url onto the patterns that we match + new_handlers = [] + for handler in self.handlers: + pattern = url_path_join(webapp.settings['base_url'], handler[0]) + new_handler = tuple([pattern] + list(handler[1:])) + new_handlers.append(new_handler) + # Add static endpoint for this extension, if static paths are given. if len(self.static_paths) > 0: # Append the extension's static directory to server handlers. static_url = url_path_join("/static", self.extension_name, "(.*)") - + # Construct handler. - handler = (static_url, FileFindHandler, {'path': self.static_paths}) - self.handlers.append(handler) + handler = ( + static_url, + webapp.settings['static_handler_class'], + {'path': self.static_paths} + ) - # Add the file paths to webapp settings. - self.settings.update({ - "{}_static_paths".format(self.extension_name): self.static_paths, - "{}_template_paths".format(self.extension_name): self.template_paths - }) + new_handlers.append(handler) - # Add handlers to serverapp. - webapp.add_handlers('.*$', self.handlers) + webapp.add_handlers('.*$', new_handlers) def _prepare_templates(self): + + if len(self.template_paths) > 0: + self.settings.update({ + "{}_template_paths".format(self.extension_name): self.template_paths + }) self.initialize_templates() @staticmethod def initialize_server(): """Add handlers to server.""" - serverapp = ServerApp() + serverapp = ServerApp.instance() serverapp.initialize() return serverapp def initialize(self, serverapp, argv=None): """Initialize the extension app.""" - super(ServerAppExtensionBase, self).initialize(argv=argv) + super(ExtensionApp, self).initialize(argv=argv) self.serverapp = serverapp def start(self, **kwargs): - """Start the extension app.""" + """Start the extension app. + + Also starts the server. This allows extensions to add settings to the + server before it starts. + """ # Start the server. self.serverapp.start() @@ -173,7 +196,7 @@ def load_jupyter_server_extension(cls, serverapp, argv=None, **kwargs): extension.initialize(serverapp, argv=argv) # Initialize settings + extension._prepare_templates() extension._prepare_settings() extension._prepare_handlers() - extension._prepare_templates() return extension \ No newline at end of file diff --git a/jupyter_server/extension/handler.py b/jupyter_server/extension/handler.py index 33aa5fc286..a4fd83d923 100644 --- a/jupyter_server/extension/handler.py +++ b/jupyter_server/extension/handler.py @@ -2,7 +2,7 @@ from traitlets import Unicode, default -class ServerHandlerExtensionBase(JupyterHandler): +class ExtensionHandler(JupyterHandler): """Base class for Jupyter server extension handlers. Subclasses can serve static files behind a namespaced @@ -12,7 +12,7 @@ class ServerHandlerExtensionBase(JupyterHandler): their own namespace and avoid intercepting requests for other extensions. """ - extension_name = Unicode(help="Name of the extenxsion") + extension_name = Unicode("",help="Name of the extenxsion") @default('extension_name') def _default_extension_name(self): @@ -47,7 +47,7 @@ def static_url(self, path, include_host=None, **kwargs): key = "{}_static_paths".format(self.extension_name) try: self.require_setting(key, "static_url") - except e: + except Exception as e: if key in self.settings: raise Exception( "This extension doesn't have any static paths listed. Check that the " @@ -74,4 +74,5 @@ def static_url(self, path, include_host=None, **kwargs): "static_path": self.static_path, "static_url_prefix": self.static_url_prefix } + return base + get_url(settings, path, **kwargs) From 90d2784a98e40767a6da234bb46f881fcea637a0 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Tue, 4 Jun 2019 09:47:00 -0700 Subject: [PATCH 06/12] remove old print statement --- jupyter_server/extension/application.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index b88964d460..3ae35a2d5b 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -101,7 +101,6 @@ def _prepare_settings(self): "{}_static_paths".format(self.extension_name): self.static_paths, }) - # Get setting defined by subclass using initialize_settings method. self.initialize_settings() @@ -110,7 +109,6 @@ def _prepare_settings(self): def _prepare_handlers(self): webapp = self.serverapp.web_app - print(webapp.default_router.rules) # Get handlers defined by extension subclass. self.initialize_handlers() @@ -139,7 +137,7 @@ def _prepare_handlers(self): webapp.add_handlers('.*$', new_handlers) def _prepare_templates(self): - + # Add templates to web app settings if extension has templates. if len(self.template_paths) > 0: self.settings.update({ "{}_template_paths".format(self.extension_name): self.template_paths @@ -147,10 +145,10 @@ def _prepare_templates(self): self.initialize_templates() @staticmethod - def initialize_server(): + def initialize_server(argv=None): """Add handlers to server.""" - serverapp = ServerApp.instance() - serverapp.initialize() + serverapp = ServerApp() + serverapp.initialize(argv=argv) return serverapp def initialize(self, serverapp, argv=None): @@ -174,7 +172,7 @@ def launch_instance(cls, argv=None, **kwargs): Properly orders the steps to initialize and start the server and extension. """ # Initialize the server - serverapp = cls.initialize_server() + serverapp = cls.initialize_server(argv=argv) # Load the extension nk extension = cls.load_jupyter_server_extension(serverapp, argv=argv, **kwargs) @@ -195,7 +193,7 @@ def load_jupyter_server_extension(cls, serverapp, argv=None, **kwargs): extension = cls() extension.initialize(serverapp, argv=argv) - # Initialize settings + # Initialize extension template, settings, and handlers. extension._prepare_templates() extension._prepare_settings() extension._prepare_handlers() From 5b57e150ed2cdcd05a3ea75f6d3a0344065516ae Mon Sep 17 00:00:00 2001 From: Zsailer Date: Wed, 5 Jun 2019 11:49:29 -0700 Subject: [PATCH 07/12] working command line configuration, help, version, etc. --- jupyter_server/extension/application.py | 93 +++++++++++++++++++++---- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index 3ae35a2d5b..10d66963d3 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -12,17 +12,58 @@ from jupyter_core.application import JupyterApp -from jupyter_server.serverapp import ServerApp +from jupyter_server.serverapp import ServerApp, aliases, flags from jupyter_server.transutils import trans, _ from jupyter_server.utils import url_path_join from jupyter_server.base.handlers import FileFindHandler -class ExtensionApp(JupyterApp): - """A base class for writing configurable Jupyter Server extension applications. +# Remove alias for nested classes in ServerApp. +# Nested classes are not allowed in ExtensionApp. +try: + aliases.pop('transport') +except KeyError: + pass + + +def _preparse_command_line(Application): + """Looks for 'help' or 'version' commands in command line. If found, + raises the help and version of current Application. + + This is useful for traitlets applications that have to parse + the command line multiple times, but want to control when + when 'help' and 'version' is raised. + """ + # Arguments after a '--' argument are for the script IPython may be + # about to run, not IPython iteslf. For arguments parsed here (help and + # version), we want to only search the arguments up to the first + # occurrence of '--', which we're calling interpreted_argv. + try: + interpreted_argv = sys.argv[:sys.argv.index('--')] + except ValueError: + interpreted_argv = sys.argv + + if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')): + app = Application() + app.print_help('--help-all' in interpreted_argv) + app.exit(0) + + if '--version' in interpreted_argv or '-V' in interpreted_argv: + app = Application() + app.print_version() + app.exit(0) + - These applications can be loaded using jupyter_server's - extension load mechanism or launched using Jupyter's command line interface. +class ExtensionApp(JupyterApp): + """Base class for configurable Jupyter Server Extension Applications. + + ExtensionApp subclasses can be initialized two ways: + 1. Extension is listed as a jpserver_extension, and ServerApp calls + its load_jupyter_server_extension classmethod. This is the + classic way of loading a server extension. + 2. Extension is launched directly by calling its `launch_instance` + class method. This method can be set as a entry_point in + the extensions setup.py """ # Name of the extension extension_name = Unicode( @@ -45,9 +86,13 @@ def _default_extension_name(self, obj, value): # Extension can configure the ServerApp from the command-line classes = [ - ServerApp + ServerApp, ] + aliases = aliases + flags = flags + + @property def static_url_prefix(self): return "/static/{extension_name}/".format( @@ -131,7 +176,6 @@ def _prepare_handlers(self): webapp.settings['static_handler_class'], {'path': self.static_paths} ) - new_handlers.append(handler) webapp.add_handlers('.*$', new_handlers) @@ -145,10 +189,14 @@ def _prepare_templates(self): self.initialize_templates() @staticmethod - def initialize_server(argv=None): - """Add handlers to server.""" + def initialize_server(): + """Get an instance of the Jupyter Server.""" + # Get a jupyter server instance serverapp = ServerApp() - serverapp.initialize(argv=argv) + # Initialize ServerApp config. + # Parses the command line looking for + # ServerApp configuration. + serverapp.initialize() return serverapp def initialize(self, serverapp, argv=None): @@ -171,21 +219,36 @@ def launch_instance(cls, argv=None, **kwargs): Properly orders the steps to initialize and start the server and extension. """ + # Check for help or version arguments. + _preparse_command_line(cls) + # Initialize the server - serverapp = cls.initialize_server(argv=argv) + serverapp = cls.initialize_server() - # Load the extension nk - extension = cls.load_jupyter_server_extension(serverapp, argv=argv, **kwargs) + # Load the extension + args = sys.argv[1:] # slice out extension config. + extension = cls.load_jupyter_server_extension(serverapp, argv=args, **kwargs) # Start the browser at this extensions default_url. - serverapp.default_url = extension.default_url + try: + server_config = extension.config['ServerApp'] + if 'default_url' not in server_config: + serverapp.default_url = extension.default_url + except KeyError: + pass # Start the application. extension.start() @classmethod def load_jupyter_server_extension(cls, serverapp, argv=None, **kwargs): - """Load this extension following the server extension loading mechanism.""" + """Enables loading this extension application using the typical + `load_jupyter_server_extension` mechanism. + + - Initializes the ExtensionApp + - Loads configuration from file. + - + oad this extension following the server extension loading mechanism.""" # Get webapp from the server. webapp = serverapp.web_app From 953603ce41ad4ce400f096522ebc05454041f646 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Wed, 5 Jun 2019 14:57:52 -0700 Subject: [PATCH 08/12] properly handle generate-config command --- jupyter_server/extension/application.py | 28 ++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index 10d66963d3..6225e77c29 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -43,16 +43,24 @@ def _preparse_command_line(Application): except ValueError: interpreted_argv = sys.argv + # Catch any help calls. if any(x in interpreted_argv for x in ('-h', '--help-all', '--help')): app = Application() app.print_help('--help-all' in interpreted_argv) app.exit(0) + # Catch version commands if '--version' in interpreted_argv or '-V' in interpreted_argv: app = Application() app.print_version() app.exit(0) + # Catch generate-config commands. + if '--generate-config' in interpreted_argv: + app = Application() + app.write_default_config() + app.exit(0) + class ExtensionApp(JupyterApp): """Base class for configurable Jupyter Server Extension Applications. @@ -92,7 +100,6 @@ def _default_extension_name(self, obj, value): aliases = aliases flags = flags - @property def static_url_prefix(self): return "/static/{extension_name}/".format( @@ -219,7 +226,9 @@ def launch_instance(cls, argv=None, **kwargs): Properly orders the steps to initialize and start the server and extension. """ - # Check for help or version arguments. + # Check for help, version, and generate-config arguments + # before initializing server to make sure these + # arguments trigger actions from the extension not the server. _preparse_command_line(cls) # Initialize the server @@ -229,7 +238,8 @@ def launch_instance(cls, argv=None, **kwargs): args = sys.argv[1:] # slice out extension config. extension = cls.load_jupyter_server_extension(serverapp, argv=args, **kwargs) - # Start the browser at this extensions default_url. + # Start the browser at this extensions default_url, unless user + # configures ServerApp.default_url on command line. try: server_config = extension.config['ServerApp'] if 'default_url' not in server_config: @@ -242,13 +252,17 @@ def launch_instance(cls, argv=None, **kwargs): @classmethod def load_jupyter_server_extension(cls, serverapp, argv=None, **kwargs): - """Enables loading this extension application using the typical + """Enables loading this extension application via the documented `load_jupyter_server_extension` mechanism. + This method: - Initializes the ExtensionApp - - Loads configuration from file. - - - oad this extension following the server extension loading mechanism.""" + - Loads the extension's config from file + - Loads the extension's config from argv + - Initializes templates environment + - Passes settings to webapp + - Appends handlers to webapp. + """ # Get webapp from the server. webapp = serverapp.web_app From dc29a230ecede17b2dfe2b7445dd59f78defb164 Mon Sep 17 00:00:00 2001 From: Zsailer Date: Wed, 5 Jun 2019 15:04:56 -0700 Subject: [PATCH 09/12] remove unnecessary imports --- jupyter_server/extension/application.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index 6225e77c29..4540950d6c 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -1,7 +1,5 @@ import sys -from traitlets.config.application import catch_config_error -from traitlets.config.configurable import Configurable from traitlets import ( Unicode, List, @@ -13,9 +11,8 @@ from jupyter_core.application import JupyterApp from jupyter_server.serverapp import ServerApp, aliases, flags -from jupyter_server.transutils import trans, _ +from jupyter_server.transutils import _ from jupyter_server.utils import url_path_join -from jupyter_server.base.handlers import FileFindHandler # Remove alias for nested classes in ServerApp. From 9915490ebd517a2988f16e4ac7d9d3f7cf4120fc Mon Sep 17 00:00:00 2001 From: Zsailer Date: Thu, 6 Jun 2019 11:16:57 -0700 Subject: [PATCH 10/12] make extension handlers and settings configurable --- jupyter_server/extension/application.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index 4540950d6c..6614c0d6a5 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -118,11 +118,11 @@ def static_url_prefix(self): settings = Dict( help=_("""Settings that will passed to the server.""") - ) + ).tag(config=True) handlers = List( help=_("""Handlers appended to the server.""") - ) + ).tag(config=True) default_url = Unicode('/', config=True, help=_("The default URL to redirect to from `/`") From 8ef52dd2ae272d52e71d6d46d509a0e7e8c3820b Mon Sep 17 00:00:00 2001 From: Zsailer Date: Tue, 25 Jun 2019 16:17:23 -0700 Subject: [PATCH 11/12] allow application to generate config --- jupyter_server/extension/application.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/jupyter_server/extension/application.py b/jupyter_server/extension/application.py index 6614c0d6a5..0b3985a77d 100644 --- a/jupyter_server/extension/application.py +++ b/jupyter_server/extension/application.py @@ -24,8 +24,9 @@ def _preparse_command_line(Application): - """Looks for 'help' or 'version' commands in command line. If found, - raises the help and version of current Application. + """Looks for 'help', 'version', and 'generate-config; commands + in command line. If found, raises the help and version of + current Application. This is useful for traitlets applications that have to parse the command line multiple times, but want to control when @@ -164,9 +165,20 @@ def _prepare_handlers(self): # prepend base_url onto the patterns that we match new_handlers = [] - for handler in self.handlers: - pattern = url_path_join(webapp.settings['base_url'], handler[0]) - new_handler = tuple([pattern] + list(handler[1:])) + for handler_items in self.handlers: + # Build url pattern including base_url + pattern = url_path_join(webapp.settings['base_url'], handler_items[0]) + handler = handler_items[1] + + # Get handler kwargs, if given + kwargs = {} + try: + kwargs.update(handler_items[2]) + except IndexError: + pass + kwargs['extension_name'] = self.extension_name + + new_handler = (pattern, handler, kwargs) new_handlers.append(new_handler) # Add static endpoint for this extension, if static paths are given. From 34fc710f07b231eeb9a0f72ddc81b5f0dfc8635b Mon Sep 17 00:00:00 2001 From: Zsailer Date: Tue, 25 Jun 2019 16:18:09 -0700 Subject: [PATCH 12/12] handler sources extension name from extension app object --- jupyter_server/extension/handler.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/jupyter_server/extension/handler.py b/jupyter_server/extension/handler.py index a4fd83d923..e1f5e32413 100644 --- a/jupyter_server/extension/handler.py +++ b/jupyter_server/extension/handler.py @@ -12,11 +12,9 @@ class ExtensionHandler(JupyterHandler): their own namespace and avoid intercepting requests for other extensions. """ - extension_name = Unicode("",help="Name of the extenxsion") - - @default('extension_name') - def _default_extension_name(self): - raise Exception("extension_name must be set in {}.".format(self.__class__)) + def initialize(self, extension_name, **kwargs): + self.extension_name = extension_name + super(ExtensionHandler, self).initialize(**kwargs) @property def static_url_prefix(self):