From 2b64353632a6f70638843c52e8a14fd105f9a79a Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 12 Aug 2019 15:31:55 +0100 Subject: [PATCH 01/10] Fix deprecation warning --- jupyterlab/commands.py | 6 +++--- jupyterlab/handlers/build_handler.py | 2 +- jupyterlab/labapp.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/jupyterlab/commands.py b/jupyterlab/commands.py index da2ecfc009c0..42cb8412f57f 100644 --- a/jupyterlab/commands.py +++ b/jupyterlab/commands.py @@ -775,7 +775,7 @@ def update_extension(self, name): Returns `True` if a rebuild is recommended, `False` otherwise. """ if name not in self.info['extensions']: - self.logger.warn('No labextension named "%s" installed' % name) + self.logger.warning('No labextension named "%s" installed' % name) return False return self._update_extension(name) @@ -815,8 +815,8 @@ def link_package(self, path): return self.install_extension(path) # Warn that it is a linked package. - self.logger.warn('Installing %s as a linked package:', path) - [self.logger.warn(m) for m in messages] + self.logger.warning('Installing %s as a linked package:', path) + [self.logger.warning(m) for m in messages] # Add to metadata. config = self._read_build_config() diff --git a/jupyterlab/handlers/build_handler.py b/jupyterlab/handlers/build_handler.py index 80cf27e5fa89..912840c9be18 100644 --- a/jupyterlab/handlers/build_handler.py +++ b/jupyterlab/handlers/build_handler.py @@ -114,7 +114,7 @@ def get(self): @web.authenticated @gen.coroutine def delete(self): - self.log.warn('Canceling build') + self.log.warning('Canceling build') try: yield self.builder.cancel() except Exception as e: diff --git a/jupyterlab/labapp.py b/jupyterlab/labapp.py index a79cf8ff7e7b..2c54e934981a 100644 --- a/jupyterlab/labapp.py +++ b/jupyterlab/labapp.py @@ -411,7 +411,7 @@ def init_server_extensions(self): super(LabApp, self).init_server_extensions() msg = 'JupyterLab server extension not enabled, manually loading...' if not self.nbserver_extensions.get('jupyterlab', False): - self.log.warn(msg) + self.log.warning(msg) load_jupyter_server_extension(self) From 6a3c03451478b1804fa0a68963f3173208297a17 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 12 Aug 2019 15:33:31 +0100 Subject: [PATCH 02/10] Add a CoreConfig object to python --- jupyterlab/commands.py | 86 +++++------ jupyterlab/coreconfig.py | 139 ++++++++++++++++++ jupyterlab/handlers/build_handler.py | 20 ++- .../handlers/extension_manager_handler.py | 20 ++- jupyterlab/labapp.py | 14 +- jupyterlab/labextensions.py | 45 ++++-- jupyterlab/tests/test_jupyterlab.py | 6 +- 7 files changed, 254 insertions(+), 76 deletions(-) create mode 100644 jupyterlab/coreconfig.py diff --git a/jupyterlab/commands.py b/jupyterlab/commands.py index 42cb8412f57f..0db625380bdb 100644 --- a/jupyterlab/commands.py +++ b/jupyterlab/commands.py @@ -29,6 +29,7 @@ from .semver import Range, gte, lt, lte, gt, make_semver from .jlpmapp import YARN_PATH, HERE +from .coreconfig import CoreConfig # The regex for expecting the webpack output. @@ -283,7 +284,7 @@ def watch_dev(logger=None): return package_procs + [wp_proc] -def watch(app_dir=None, logger=None): +def watch(app_dir=None, logger=None, core_config=None): """Watch the application. Parameters @@ -299,11 +300,11 @@ def watch(app_dir=None, logger=None): """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.watch() -def install_extension(extension, app_dir=None, logger=None): +def install_extension(extension, app_dir=None, logger=None, core_config=None): """Install an extension package into JupyterLab. The extension is first validated. @@ -312,24 +313,24 @@ def install_extension(extension, app_dir=None, logger=None): """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.install_extension(extension) -def uninstall_extension(name=None, app_dir=None, logger=None, all_=False): +def uninstall_extension(name=None, app_dir=None, logger=None, all_=False, core_config=None): """Uninstall an extension by name or path. Returns `True` if a rebuild is recommended, `False` otherwise. """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) if all_ is True: return handler.uninstall_all_extensions() return handler.uninstall_extension(name) -def update_extension(name=None, all_=False, app_dir=None, logger=None): +def update_extension(name=None, all_=False, app_dir=None, logger=None, core_config=None): """Update an extension by name, or all extensions. Either `name` must be given as a string, or `all_` must be `True`. @@ -339,7 +340,7 @@ def update_extension(name=None, all_=False, app_dir=None, logger=None): """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) if all_ is True: return handler.update_all_extensions() return handler.update_extension(name) @@ -363,96 +364,96 @@ def clean(app_dir=None, logger=None): def build(app_dir=None, name=None, version=None, static_url=None, logger=None, command='build:prod', kill_event=None, - clean_staging=False): + clean_staging=False, core_config=None): """Build the JupyterLab application. """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger, kill_event=kill_event) + handler = _AppHandler(app_dir, logger, kill_event=kill_event, core_config=core_config) return handler.build(name=name, version=version, static_url=static_url, command=command, clean_staging=clean_staging) -def get_app_info(app_dir=None, logger=None): +def get_app_info(app_dir=None, logger=None, core_config=None): """Get a dictionary of information about the app. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.info -def enable_extension(extension, app_dir=None, logger=None): +def enable_extension(extension, app_dir=None, logger=None, core_config=None): """Enable a JupyterLab extension. Returns `True` if a rebuild is recommended, `False` otherwise. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.toggle_extension(extension, False) -def disable_extension(extension, app_dir=None, logger=None): +def disable_extension(extension, app_dir=None, logger=None, core_config=None): """Disable a JupyterLab package. Returns `True` if a rebuild is recommended, `False` otherwise. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.toggle_extension(extension, True) -def check_extension(extension, app_dir=None, installed=False, logger=None): +def check_extension(extension, app_dir=None, installed=False, logger=None, core_config=None): """Check if a JupyterLab extension is enabled or disabled. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.check_extension(extension, installed) -def build_check(app_dir=None, logger=None): +def build_check(app_dir=None, logger=None, core_config=None): """Determine whether JupyterLab should be built. Returns a list of messages. """ logger = _ensure_logger(logger) _node_check(logger) - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.build_check() -def list_extensions(app_dir=None, logger=None): +def list_extensions(app_dir=None, logger=None, core_config=None): """List the extensions. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.list_extensions() -def link_package(path, app_dir=None, logger=None): +def link_package(path, app_dir=None, logger=None, core_config=None): """Link a package against the JupyterLab build. Returns `True` if a rebuild is recommended, `False` otherwise. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.link_package(path) -def unlink_package(package, app_dir=None, logger=None): +def unlink_package(package, app_dir=None, logger=None, core_config=None): """Unlink a package from JupyterLab by path or name. Returns `True` if a rebuild is recommended, `False` otherwise. """ - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.unlink_package(package) -def get_app_version(app_dir=None): +def get_app_version(app_dir=None, core_config=None): """Get the application version.""" app_dir = app_dir or get_app_dir() - handler = _AppHandler(app_dir) + handler = _AppHandler(app_dir, core_config=core_config) return handler.info['version'] -def get_latest_compatible_package_versions(names, app_dir=None, logger=None): +def get_latest_compatible_package_versions(names, app_dir=None, logger=None, core_config=None): """Get the latest compatible version of a list of packages. """ app_dir = app_dir or get_app_dir() - handler = _AppHandler(app_dir, logger) + handler = _AppHandler(app_dir, logger, core_config=core_config) return handler.latest_compatible_package_versions(names) @@ -476,12 +477,13 @@ def read_package(target): class _AppHandler(object): - def __init__(self, app_dir, logger=None, kill_event=None): + def __init__(self, app_dir, logger=None, kill_event=None, core_config=None): """Create a new _AppHandler object """ self.app_dir = app_dir or get_app_dir() self.sys_dir = get_app_dir() self.logger = _ensure_logger(logger) + self.core_data = (core_config or CoreConfig()).data self.info = self._get_app_info() self.kill_event = kill_event or Event() # TODO: Make this configurable @@ -713,7 +715,6 @@ def uninstall_extension(self, name): Returns `True` if a rebuild is recommended, `False` otherwise. """ # Allow for uninstalled core extensions. - data = self.info['core_data'] if name in self.info['core_extensions']: config = self._read_build_config() uninstalled = config.get('uninstalled_core_extensions', []) @@ -938,7 +939,7 @@ def _get_app_info(self): """ info = dict() - info['core_data'] = core_data = _get_core_data() + info['core_data'] = core_data = self.core_data info['extensions'] = extensions = self._get_extensions(core_data) page_config = self._read_page_config() info['disabled'] = page_config.get('disabledExtensions', []) @@ -963,7 +964,8 @@ def _get_app_info(self): info['sys_dir'] = self.sys_dir info['app_dir'] = self.app_dir - info['core_extensions'] = core_extensions = _get_core_extensions() + info['core_extensions'] = core_extensions = _get_core_extensions( + self.core_data) disabled_core = [] for key in core_extensions: @@ -1390,9 +1392,8 @@ def _install_extension(self, extension, tempdir): raise ValueError(msg % (extension, '\n'.join(messages))) # Verify package compatibility. - core_data = _get_core_data() deps = data.get('dependencies', dict()) - errors = _validate_compatibility(extension, deps, core_data) + errors = _validate_compatibility(extension, deps, self.core_data) if errors: msg = _format_compatibility_errors( data['name'], data['version'], errors @@ -1628,7 +1629,7 @@ def _node_check(logger): output = subprocess.check_output([node, 'node-version-check.js'], cwd=HERE) logger.debug(output.decode('utf-8')) except Exception: - data = _get_core_data() + data = CoreConfig().data ver = data['engines']['node'] msg = 'Please install nodejs %s before continuing. nodejs may be installed using conda or directly from the nodejs website.' % ver raise ValueError(msg) @@ -1728,13 +1729,6 @@ def _tarsum(input_file): return h.hexdigest() -def _get_core_data(): - """Get the data for the app template. - """ - with open(pjoin(HERE, 'staging', 'package.json')) as fid: - return json.load(fid) - - def _get_static_data(app_dir): """Get the data for the app static dir. """ @@ -1931,10 +1925,10 @@ def _compat_error_age(errors): return 0 -def _get_core_extensions(): +def _get_core_extensions(core_data): """Get the core extensions. """ - data = _get_core_data()['jupyterlab'] + data = core_data['jupyterlab'] return list(data['extensions']) + list(data['mimeExtensions']) diff --git a/jupyterlab/coreconfig.py b/jupyterlab/coreconfig.py new file mode 100644 index 000000000000..1b35d145ba57 --- /dev/null +++ b/jupyterlab/coreconfig.py @@ -0,0 +1,139 @@ +# coding: utf-8 +# Copyright (c) Jupyter Development Team. +# Distributed under the terms of the Modified BSD License. + +from itertools import filterfalse +import json +import os.path as osp + +from .jlpmapp import HERE + + +def pjoin(*args): + """Join paths to create a real path. + """ + return osp.abspath(osp.join(*args)) + + +def _get_core_data(): + """Get the data for the app template. + """ + with open(pjoin(HERE, 'staging', 'package.json')) as fid: + return json.load(fid) + + +def _is_lab_package(name): + """Whether a package name is in the lab namespace""" + return name.startswith('@jupyterlab/') + + +def _only_nonlab(collection): + """Filter a dict/sequence to remove all lab packages""" + if isinstance(collection, dict): + return dict( + (k, v) for (k, v) in collection.items() + if not _is_lab_package(k) + ) + elif isinstance(collection, (list, tuple)): + return list(filterfalse(_is_lab_package, collection)) + raise TypeError('collection arg should be either dict or list/tuple') + + +class CoreConfig: + """An object representing a core-mode package/extension configuration. + + This enables custom lab application to change + """ + + def __init__(self): + self._data = _get_core_data() + + def clear_defaults(self, lab_only=True): + """Clear the default packages/extensions. + + lab_only: string + Whether to remove all packages, or only those from + JupyterLab. Defaults to True (only lab packages). + This will leave dependencies like phosphor, react + etc untouched. + """ + data = self._data + if lab_only: + # Clear all "@jupyterlab/" dependencies + data['dependencies'] = _only_nonlab(data['dependencies']) + data['resolutions'] = _only_nonlab(data['resolutions']) + data['jupyterlab']['extensions'] = _only_nonlab( + data['jupyterlab']['extensions']) + data['jupyterlab']['mimeExtensions'] = _only_nonlab( + data['jupyterlab']['mimeExtensions']) + data['jupyterlab']['singletonPackages'] = _only_nonlab( + data['jupyterlab']['singletonPackages']) + else: + # Clear all dependencies + data['dependencies'] = {} + data['resolutions'] = {} + data['jupyterlab']['extensions'] = {} + data['jupyterlab']['mimeExtensions'] = {} + data['jupyterlab']['singletonPackages'] = [] + + def add(self, name, semver, extension=False, mimeExtension=False): + """Remove a package/extension. + + name: string + The npm package name + semver: string + The semver range for the package + extension: bool + Whether the package is an extension + mimeExtension: bool + Whether the package is a MIME extension + """ + data = self._data + if name in self._data['resolutions']: + raise ValueError('Package already present: %r' % (name,)) + data['resolutions'][name] = semver + + # If both mimeExtension and extensions are True, treat + # as mime extension + if mimeExtension: + data['jupyterlab']['mimeExtensions'][name] = "" + data['dependencies'][name] = semver + elif extension: + data['jupyterlab']['extensions'][name] = "" + data['dependencies'][name] = semver + else: + data['singletonPackages'].append(name) + + def remove(self, name): + """Remove a package/extension. + + name: string + The npm package name + """ + data = self._data + maps = ( + data['dependencies'], + data['resolutions'], + data['jupyterlab']['extensions'], + data['jupyterlab']['mimeExtensions'], + ) + for m in maps: + try: + del m[name] + except KeyError: + pass + + data['jupyterlab']['singletonPackages'].remove(name) + + + def set_static_dir(self, static_dir): + self._data['jupyterlab']['staticDir'] = static_dir + + @property + def data(self): + """Returns the raw core data. + + Its content should be considered an internal implementation + detail of lab, and should not be relied upon outide of lab. + """ + return self._data diff --git a/jupyterlab/handlers/build_handler.py b/jupyterlab/handlers/build_handler.py index 912840c9be18..c8bfbfec15db 100644 --- a/jupyterlab/handlers/build_handler.py +++ b/jupyterlab/handlers/build_handler.py @@ -21,10 +21,11 @@ class Builder(object): _kill_event = None _future = None - def __init__(self, log, core_mode, app_dir): + def __init__(self, log, core_mode, app_dir, core_config=None): self.log = log self.core_mode = core_mode self.app_dir = app_dir + self.core_config = core_config @gen.coroutine def get_status(self): @@ -34,7 +35,8 @@ def get_status(self): raise gen.Return(dict(status='building', message='')) try: - messages = yield self._run_build_check(self.app_dir, self.log) + messages = yield self._run_build_check( + self.app_dir, self.log, self.core_config) status = 'needed' if messages else 'stable' if messages: self.log.warn('Build recommended') @@ -60,7 +62,8 @@ def build(self): self.building = True self._kill_event = evt = Event() try: - yield self._run_build(self.app_dir, self.log, evt) + yield self._run_build( + self.app_dir, self.log, evt, self.core_config) future.set_result(True) except Exception as e: if str(e) == 'Aborted': @@ -84,12 +87,15 @@ def cancel(self): self.canceled = True @run_on_executor - def _run_build_check(self, app_dir, logger): - return build_check(app_dir=app_dir, logger=logger) + def _run_build_check(self, app_dir, logger, core_config): + return build_check( + app_dir=app_dir, logger=logger, core_config=core_config) @run_on_executor - def _run_build(self, app_dir, logger, kill_event): - kwargs = dict(app_dir=app_dir, logger=logger, kill_event=kill_event, command='build') + def _run_build(self, app_dir, logger, kill_event, core_config): + kwargs = dict( + app_dir=app_dir, logger=logger, kill_event=kill_event, + core_config=core_config, command='build') try: return build(**kwargs) except Exception as e: diff --git a/jupyterlab/handlers/extension_manager_handler.py b/jupyterlab/handlers/extension_manager_handler.py index 3ef873e357ba..df02348df367 100644 --- a/jupyterlab/handlers/extension_manager_handler.py +++ b/jupyterlab/handlers/extension_manager_handler.py @@ -66,9 +66,10 @@ def _build_check_info(app_dir, logger): class ExtensionManager(object): executor = ThreadPoolExecutor(max_workers=1) - def __init__(self, log, app_dir): + def __init__(self, log, app_dir, core_config=None): self.log = log self.app_dir = app_dir + self.core_config = core_config self._outdated = None # To start fetching data on outdated extensions immediately, uncomment: # IOLoop.current().spawn_callback(self._get_outdated) @@ -122,7 +123,9 @@ def list_extensions(self): def install(self, extension): """Handle an install/update request""" try: - install_extension(extension, app_dir=self.app_dir, logger=self.log) + install_extension( + extension, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) except ValueError as e: raise gen.Return(dict(status='error', message=str(e))) raise gen.Return(dict(status='ok',)) @@ -130,19 +133,25 @@ def install(self, extension): @gen.coroutine def uninstall(self, extension): """Handle an uninstall request""" - did_uninstall = uninstall_extension(extension, app_dir=self.app_dir, logger=self.log) + did_uninstall = uninstall_extension( + extension, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) raise gen.Return(dict(status='ok' if did_uninstall else 'error',)) @gen.coroutine def enable(self, extension): """Handle an enable request""" - enable_extension(extension, app_dir=self.app_dir, logger=self.log) + enable_extension( + extension, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) raise gen.Return(dict(status='ok',)) @gen.coroutine def disable(self, extension): """Handle a disable request""" - disable_extension(extension, app_dir=self.app_dir, logger=self.log) + disable_extension( + extension, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) raise gen.Return(dict(status='ok',)) @gen.coroutine @@ -187,6 +196,7 @@ def _load_outdated(self): names, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config, ) raise gen.Return(data) diff --git a/jupyterlab/labapp.py b/jupyterlab/labapp.py index 2c54e934981a..288b6a0afc86 100644 --- a/jupyterlab/labapp.py +++ b/jupyterlab/labapp.py @@ -14,7 +14,7 @@ from jupyterlab_server import slugify, WORKSPACE_EXTENSION from notebook.notebookapp import NotebookApp, aliases, flags from notebook.utils import url_path_join as ujoin -from traitlets import Bool, Unicode +from traitlets import Bool, Instance, Unicode from ._version import __version__ from .debuglog import DebugLogFileMixin @@ -23,6 +23,7 @@ build, clean, get_app_dir, get_app_version, get_user_settings_dir, get_workspaces_dir ) +from .coreconfig import CoreConfig build_aliases = dict(base_aliases) @@ -53,6 +54,9 @@ class LabBuildApp(JupyterApp, DebugLogFileMixin): aliases = build_aliases flags = build_flags + # Not configurable! + core_config = Instance(CoreConfig, allow_none=True) + app_dir = Unicode('', config=True, help="The app directory to build in") @@ -88,7 +92,8 @@ def start(self): clean(self.app_dir) self.log.info('Building in %s', app_dir) build(app_dir=app_dir, name=self.name, version=self.version, - command=command, logger=self.log) + command=command, logger=self.log, + core_config=self.core_config) clean_aliases = dict(base_aliases) @@ -105,10 +110,13 @@ class LabCleanApp(JupyterApp): """ aliases = clean_aliases + # Not configurable! + core_config = Instance(CoreConfig, allow_none=True) + app_dir = Unicode('', config=True, help='The app directory to clean') def start(self): - clean(self.app_dir, logger=self.log) + clean(self.app_dir, logger=self.log, core_config=self.core_config) class LabPathApp(JupyterApp): diff --git a/jupyterlab/labextensions.py b/jupyterlab/labextensions.py index bc5071a2c030..6d1984737eb4 100644 --- a/jupyterlab/labextensions.py +++ b/jupyterlab/labextensions.py @@ -11,7 +11,7 @@ from jupyter_core.application import JupyterApp, base_flags, base_aliases -from traitlets import Bool, Unicode +from traitlets import Bool, Instance, Unicode from .commands import ( install_extension, uninstall_extension, list_extensions, @@ -19,6 +19,7 @@ link_package, unlink_package, build, get_app_version, HERE, update_extension, ) +from .coreconfig import CoreConfig from .debuglog import DebugLogFileMixin @@ -65,6 +66,9 @@ class BaseExtensionApp(JupyterApp, DebugLogFileMixin): flags = flags aliases = aliases + # Not configurable! + core_config = Instance(CoreConfig, allow_none=True) + app_dir = Unicode('', config=True, help="The app directory to target") @@ -95,7 +99,8 @@ def start(self): command = ':'.join(parts) build(app_dir=self.app_dir, clean_staging=self.should_clean, - logger=self.log, command=command) + logger=self.log, command=command, + core_config=self.core_config) def run_task(self): pass @@ -111,7 +116,9 @@ class InstallLabExtensionApp(BaseExtensionApp): def run_task(self): self.extra_args = self.extra_args or [os.getcwd()] return any([ - install_extension(arg, self.app_dir, logger=self.log) + install_extension( + arg, self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -128,9 +135,13 @@ def run_task(self): self.log.warn('Specify an extension to update, or use --all to update all extensions') return False if self.all: - return update_extension(all_=True, app_dir=self.app_dir, logger=self.log) + return update_extension( + all_=True, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) return any([ - update_extension(name=arg, app_dir=self.app_dir, logger=self.log) + update_extension( + name=arg, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -149,7 +160,9 @@ class LinkLabExtensionApp(BaseExtensionApp): def run_task(self): self.extra_args = self.extra_args or [os.getcwd()] return any([ - link_package(arg, self.app_dir, logger=self.log) + link_package( + arg, self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -160,7 +173,9 @@ class UnlinkLabExtensionApp(BaseExtensionApp): def run_task(self): self.extra_args = self.extra_args or [os.getcwd()] return any([ - unlink_package(arg, self.app_dir, logger=self.log) + unlink_package( + arg, self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -175,7 +190,9 @@ class UninstallLabExtensionApp(BaseExtensionApp): def run_task(self): self.extra_args = self.extra_args or [os.getcwd()] return any([ - uninstall_extension(arg, all_=self.all, app_dir=self.app_dir, logger=self.log) + uninstall_extension( + arg, all_=self.all, app_dir=self.app_dir, logger=self.log, + core_config=self.core_config) for arg in self.extra_args ]) @@ -184,14 +201,16 @@ class ListLabExtensionsApp(BaseExtensionApp): description = "List the installed labextensions" def run_task(self): - list_extensions(self.app_dir, logger=self.log) + list_extensions( + self.app_dir, logger=self.log, core_config=self.core_config) class EnableLabExtensionsApp(BaseExtensionApp): description = "Enable labextension(s) by name" def run_task(self): - [enable_extension(arg, self.app_dir, logger=self.log) + [enable_extension( + arg, self.app_dir, logger=self.log, core_config=self.core_config) for arg in self.extra_args] @@ -199,7 +218,8 @@ class DisableLabExtensionsApp(BaseExtensionApp): description = "Disable labextension(s) by name" def run_task(self): - [disable_extension(arg, self.app_dir, logger=self.log) + [disable_extension( + arg, self.app_dir, logger=self.log, core_config=self.core_config) for arg in self.extra_args] @@ -215,7 +235,8 @@ def run_task(self): check_extension( arg, self.app_dir, self.should_check_installed_only, - logger=self.log) + logger=self.log, + core_config=self.core_config) for arg in self.extra_args) if not all_enabled: self.exit(1) diff --git a/jupyterlab/tests/test_jupyterlab.py b/jupyterlab/tests/test_jupyterlab.py index 415b19c23fd7..518b95b6d361 100644 --- a/jupyterlab/tests/test_jupyterlab.py +++ b/jupyterlab/tests/test_jupyterlab.py @@ -23,9 +23,9 @@ install_extension, uninstall_extension, list_extensions, build, link_package, unlink_package, build_check, disable_extension, enable_extension, get_app_info, - check_extension, _test_overlap, _get_core_data, - update_extension + check_extension, _test_overlap, update_extension ) +from jupyterlab.coreconfig import CoreConfig here = os.path.dirname(os.path.abspath(__file__)) @@ -477,7 +477,7 @@ def test_compatibility(self): assert _test_overlap('<0.6', '0.1') is None def test_install_compatible(self): - core_data = _get_core_data() + core_data = CoreConfig().data current_app_dep = core_data['dependencies']['@jupyterlab/application'] def _gen_dep(ver): return { "dependencies": { From 29a40e01ed6c58ff29158207d309156399e478ae Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 12 Aug 2019 16:41:00 +0100 Subject: [PATCH 03/10] fix clean --- jupyterlab/labapp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jupyterlab/labapp.py b/jupyterlab/labapp.py index 288b6a0afc86..992784d02560 100644 --- a/jupyterlab/labapp.py +++ b/jupyterlab/labapp.py @@ -116,7 +116,7 @@ class LabCleanApp(JupyterApp): app_dir = Unicode('', config=True, help='The app directory to clean') def start(self): - clean(self.app_dir, logger=self.log, core_config=self.core_config) + clean(self.app_dir, logger=self.log) class LabPathApp(JupyterApp): From 849244d282fd154fa492d241ebe8bede33009dd2 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 12 Aug 2019 19:12:15 +0100 Subject: [PATCH 04/10] Expose current info on coreconfig --- jupyterlab/coreconfig.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/jupyterlab/coreconfig.py b/jupyterlab/coreconfig.py index 1b35d145ba57..deeb81b1d304 100644 --- a/jupyterlab/coreconfig.py +++ b/jupyterlab/coreconfig.py @@ -77,7 +77,10 @@ def clear_defaults(self, lab_only=True): data['jupyterlab']['singletonPackages'] = [] def add(self, name, semver, extension=False, mimeExtension=False): - """Remove a package/extension. + """Remove an extension/singleton. + + If neither extension or mimeExtension is True (the default) + the package is added as a singleton dependency. name: string The npm package name @@ -104,6 +107,24 @@ def add(self, name, semver, extension=False, mimeExtension=False): else: data['singletonPackages'].append(name) + @property + def extensions(self): + """A dict mapping all extension names to their semver""" + return dict(self._data['jupyterlab']['extensions']) + + @property + def mimeExtensions(self): + """A dict mapping all MIME extension names to their semver""" + return dict(self._data['jupyterlab']['mimeExtensions']) + + @property + def singletons(self): + """A dict mapping all singleton names to their semver""" + return dict( + (k, self._data['resolutions'][k]) + for k in self._data['singletonPackages'] + ) + def remove(self, name): """Remove a package/extension. From 64ba1ca1d50a0bac043e74c3774014fa222495e3 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 12 Aug 2019 19:19:34 +0100 Subject: [PATCH 05/10] Avoid camelCase in Python --- jupyterlab/coreconfig.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jupyterlab/coreconfig.py b/jupyterlab/coreconfig.py index deeb81b1d304..3e07f3bcba17 100644 --- a/jupyterlab/coreconfig.py +++ b/jupyterlab/coreconfig.py @@ -76,7 +76,7 @@ def clear_defaults(self, lab_only=True): data['jupyterlab']['mimeExtensions'] = {} data['jupyterlab']['singletonPackages'] = [] - def add(self, name, semver, extension=False, mimeExtension=False): + def add(self, name, semver, extension=False, mime_extension=False): """Remove an extension/singleton. If neither extension or mimeExtension is True (the default) @@ -88,7 +88,7 @@ def add(self, name, semver, extension=False, mimeExtension=False): The semver range for the package extension: bool Whether the package is an extension - mimeExtension: bool + mime_extension: bool Whether the package is a MIME extension """ data = self._data @@ -98,7 +98,7 @@ def add(self, name, semver, extension=False, mimeExtension=False): # If both mimeExtension and extensions are True, treat # as mime extension - if mimeExtension: + if mime_extension: data['jupyterlab']['mimeExtensions'][name] = "" data['dependencies'][name] = semver elif extension: @@ -113,7 +113,7 @@ def extensions(self): return dict(self._data['jupyterlab']['extensions']) @property - def mimeExtensions(self): + def mime_extensions(self): """A dict mapping all MIME extension names to their semver""" return dict(self._data['jupyterlab']['mimeExtensions']) From b937f18f161053751095e10c253bca9614656144 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 12 Aug 2019 19:27:59 +0100 Subject: [PATCH 06/10] Fix CoreConfig signleton location --- jupyterlab/coreconfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyterlab/coreconfig.py b/jupyterlab/coreconfig.py index 3e07f3bcba17..74b4ca32630a 100644 --- a/jupyterlab/coreconfig.py +++ b/jupyterlab/coreconfig.py @@ -105,7 +105,7 @@ def add(self, name, semver, extension=False, mime_extension=False): data['jupyterlab']['extensions'][name] = "" data['dependencies'][name] = semver else: - data['singletonPackages'].append(name) + data['jupyterlab']['singletonPackages'].append(name) @property def extensions(self): @@ -122,7 +122,7 @@ def singletons(self): """A dict mapping all singleton names to their semver""" return dict( (k, self._data['resolutions'][k]) - for k in self._data['singletonPackages'] + for k in self._data['jupyterlab']['singletonPackages'] ) def remove(self, name): From 3c6a864e0ee1cd7e56951ff8a8f40f91d98dacb9 Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 12 Aug 2019 20:29:33 +0100 Subject: [PATCH 07/10] More fixes for CoreConfig --- jupyterlab/coreconfig.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/jupyterlab/coreconfig.py b/jupyterlab/coreconfig.py index 74b4ca32630a..8ce0d2cdf1df 100644 --- a/jupyterlab/coreconfig.py +++ b/jupyterlab/coreconfig.py @@ -92,6 +92,10 @@ def add(self, name, semver, extension=False, mime_extension=False): Whether the package is a MIME extension """ data = self._data + if not name: + raise ValueError('Missing package name') + if not semver: + raise ValueError('Missing package semver') if name in self._data['resolutions']: raise ValueError('Package already present: %r' % (name,)) data['resolutions'][name] = semver @@ -110,12 +114,16 @@ def add(self, name, semver, extension=False, mime_extension=False): @property def extensions(self): """A dict mapping all extension names to their semver""" - return dict(self._data['jupyterlab']['extensions']) + return dict( + (k, self._data['resolutions'][k]) + for k in self._data['jupyterlab']['extensions'].keys()) @property def mime_extensions(self): """A dict mapping all MIME extension names to their semver""" - return dict(self._data['jupyterlab']['mimeExtensions']) + return dict( + (k, self._data['resolutions'][k]) + for k in self._data['jupyterlab']['mimeExtensions'].keys()) @property def singletons(self): From a99899e4976d4cf19e3abc62e4ff11691c612d3f Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 12 Aug 2019 21:32:21 +0100 Subject: [PATCH 08/10] Lint COTRIBUTING.md --- CONTRIBUTING.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d3d3e772a5f4..6a8048827258 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,13 +44,14 @@ a keyboard shortcut or automatically on save. ## Submitting a Pull Request Contribution Generally, an issue should be opened describing a piece of proposed work and the -issues it solves before a pull request is opened. +issues it solves before a pull request is opened. ### Issue Management -Opening an issue lets community members participate in the design discussion, -makes others aware of work being done, and sets the stage for a fruitful community -interaction. A pull request should reference the issue it is addressing. Once the -pull request is merged, the issue related to it will also be closed. If there is + +Opening an issue lets community members participate in the design discussion, +makes others aware of work being done, and sets the stage for a fruitful community +interaction. A pull request should reference the issue it is addressing. Once the +pull request is merged, the issue related to it will also be closed. If there is additional discussion around implemementation the issue may be re-opened. Once 30 days have passed with no additional discussion, the [lock bot](https://github.com/apps/lock) will lock the issue. If additional discussion is desired, or if the pull request doesn't fully address the From 900b84750ff84c040cf91da276e6a1b6e1e1bfdb Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Mon, 12 Aug 2019 21:36:46 +0100 Subject: [PATCH 09/10] Update coreconfig docstrings --- jupyterlab/coreconfig.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/jupyterlab/coreconfig.py b/jupyterlab/coreconfig.py index 8ce0d2cdf1df..bdeeffe0aae2 100644 --- a/jupyterlab/coreconfig.py +++ b/jupyterlab/coreconfig.py @@ -42,7 +42,9 @@ def _only_nonlab(collection): class CoreConfig: """An object representing a core-mode package/extension configuration. - This enables custom lab application to change + This enables custom lab application to change the core configuration + of the various build system commands. See e.g. commands.py and + any apps that use these functions. """ def __init__(self): @@ -51,7 +53,7 @@ def __init__(self): def clear_defaults(self, lab_only=True): """Clear the default packages/extensions. - lab_only: string + lab_only: bool Whether to remove all packages, or only those from JupyterLab. Defaults to True (only lab packages). This will leave dependencies like phosphor, react From 853a5f5775edf9966efd73d27e58450e366e01fd Mon Sep 17 00:00:00 2001 From: Vidar Tonaas Fauske Date: Wed, 14 Aug 2019 16:35:41 +0100 Subject: [PATCH 10/10] Tweak and test --- jupyterlab/commands.py | 8 +- jupyterlab/coreconfig.py | 129 +++++++++++++--------------- jupyterlab/tests/test_jupyterlab.py | 50 ++++++++++- 3 files changed, 115 insertions(+), 72 deletions(-) diff --git a/jupyterlab/commands.py b/jupyterlab/commands.py index 0db625380bdb..64c781f5383c 100644 --- a/jupyterlab/commands.py +++ b/jupyterlab/commands.py @@ -29,7 +29,7 @@ from .semver import Range, gte, lt, lte, gt, make_semver from .jlpmapp import YARN_PATH, HERE -from .coreconfig import CoreConfig +from .coreconfig import _get_default_core_data # The regex for expecting the webpack output. @@ -483,7 +483,9 @@ def __init__(self, app_dir, logger=None, kill_event=None, core_config=None): self.app_dir = app_dir or get_app_dir() self.sys_dir = get_app_dir() self.logger = _ensure_logger(logger) - self.core_data = (core_config or CoreConfig()).data + self.core_data = ( + core_config._data if core_config else _get_default_core_data() + ) self.info = self._get_app_info() self.kill_event = kill_event or Event() # TODO: Make this configurable @@ -1629,7 +1631,7 @@ def _node_check(logger): output = subprocess.check_output([node, 'node-version-check.js'], cwd=HERE) logger.debug(output.decode('utf-8')) except Exception: - data = CoreConfig().data + data = CoreConfig()._data ver = data['engines']['node'] msg = 'Please install nodejs %s before continuing. nodejs may be installed using conda or directly from the nodejs website.' % ver raise ValueError(msg) diff --git a/jupyterlab/coreconfig.py b/jupyterlab/coreconfig.py index bdeeffe0aae2..af08f6cdd45b 100644 --- a/jupyterlab/coreconfig.py +++ b/jupyterlab/coreconfig.py @@ -2,6 +2,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from collections import defaultdict from itertools import filterfalse import json import os.path as osp @@ -15,7 +16,7 @@ def pjoin(*args): return osp.abspath(osp.join(*args)) -def _get_core_data(): +def _get_default_core_data(): """Get the data for the app template. """ with open(pjoin(HERE, 'staging', 'package.json')) as fid: @@ -28,7 +29,12 @@ def _is_lab_package(name): def _only_nonlab(collection): - """Filter a dict/sequence to remove all lab packages""" + """Filter a dict/sequence to remove all lab packages + + This is useful to take the default values of e.g. singletons and filter + away the '@jupyterlab/' namespace packages, but leave any others (e.g. + phosphor and react). + """ if isinstance(collection, dict): return dict( (k, v) for (k, v) in collection.items() @@ -40,43 +46,13 @@ def _only_nonlab(collection): class CoreConfig: - """An object representing a core-mode package/extension configuration. + """An object representing a core config. - This enables custom lab application to change the core configuration - of the various build system commands. See e.g. commands.py and - any apps that use these functions. + This enables custom lab application to override some parts of the core + configuration of the build system. """ - def __init__(self): - self._data = _get_core_data() - - def clear_defaults(self, lab_only=True): - """Clear the default packages/extensions. - - lab_only: bool - Whether to remove all packages, or only those from - JupyterLab. Defaults to True (only lab packages). - This will leave dependencies like phosphor, react - etc untouched. - """ - data = self._data - if lab_only: - # Clear all "@jupyterlab/" dependencies - data['dependencies'] = _only_nonlab(data['dependencies']) - data['resolutions'] = _only_nonlab(data['resolutions']) - data['jupyterlab']['extensions'] = _only_nonlab( - data['jupyterlab']['extensions']) - data['jupyterlab']['mimeExtensions'] = _only_nonlab( - data['jupyterlab']['mimeExtensions']) - data['jupyterlab']['singletonPackages'] = _only_nonlab( - data['jupyterlab']['singletonPackages']) - else: - # Clear all dependencies - data['dependencies'] = {} - data['resolutions'] = {} - data['jupyterlab']['extensions'] = {} - data['jupyterlab']['mimeExtensions'] = {} - data['jupyterlab']['singletonPackages'] = [] + self._data = _get_default_core_data() def add(self, name, semver, extension=False, mime_extension=False): """Remove an extension/singleton. @@ -98,7 +74,7 @@ def add(self, name, semver, extension=False, mime_extension=False): raise ValueError('Missing package name') if not semver: raise ValueError('Missing package semver') - if name in self._data['resolutions']: + if name in data['resolutions']: raise ValueError('Package already present: %r' % (name,)) data['resolutions'][name] = semver @@ -113,28 +89,6 @@ def add(self, name, semver, extension=False, mime_extension=False): else: data['jupyterlab']['singletonPackages'].append(name) - @property - def extensions(self): - """A dict mapping all extension names to their semver""" - return dict( - (k, self._data['resolutions'][k]) - for k in self._data['jupyterlab']['extensions'].keys()) - - @property - def mime_extensions(self): - """A dict mapping all MIME extension names to their semver""" - return dict( - (k, self._data['resolutions'][k]) - for k in self._data['jupyterlab']['mimeExtensions'].keys()) - - @property - def singletons(self): - """A dict mapping all singleton names to their semver""" - return dict( - (k, self._data['resolutions'][k]) - for k in self._data['jupyterlab']['singletonPackages'] - ) - def remove(self, name): """Remove a package/extension. @@ -156,15 +110,56 @@ def remove(self, name): data['jupyterlab']['singletonPackages'].remove(name) + def clear_packages(self, lab_only=True): + """Clear the packages/extensions. + """ + data = self._data + # Clear all dependencies + if lab_only: + # Clear all "@jupyterlab/" dependencies + data['dependencies'] = _only_nonlab(data['dependencies']) + data['resolutions'] = _only_nonlab(data['resolutions']) + data['jupyterlab']['extensions'] = _only_nonlab( + data['jupyterlab']['extensions']) + data['jupyterlab']['mimeExtensions'] = _only_nonlab( + data['jupyterlab']['mimeExtensions']) + data['jupyterlab']['singletonPackages'] = _only_nonlab( + data['jupyterlab']['singletonPackages']) + else: + data['dependencies'] = {} + data['resolutions'] = {} + data['jupyterlab']['extensions'] = {} + data['jupyterlab']['mimeExtensions'] = {} + data['jupyterlab']['singletonPackages'] = [] - def set_static_dir(self, static_dir): - self._data['jupyterlab']['staticDir'] = static_dir + @property + def extensions(self): + """A dict mapping all extension names to their semver""" + data = self._data + return dict( + (k, data['resolutions'][k]) + for k in data['jupyterlab']['extensions'].keys()) @property - def data(self): - """Returns the raw core data. + def mime_extensions(self): + """A dict mapping all MIME extension names to their semver""" + data = self._data + return dict( + (k, data['resolutions'][k]) + for k in data['jupyterlab']['mimeExtensions'].keys()) - Its content should be considered an internal implementation - detail of lab, and should not be relied upon outide of lab. - """ - return self._data + @property + def singletons(self): + """A dict mapping all singleton names to their semver""" + data = self._data + return dict( + (k, data['resolutions'].get(k, None)) + for k in data['jupyterlab']['singletonPackages']) + + @property + def static_dir(self): + return self._data['jupyterlab']['staticDir'] + + @static_dir.setter + def static_dir(self, static_dir): + self._data['jupyterlab']['staticDir'] = static_dir diff --git a/jupyterlab/tests/test_jupyterlab.py b/jupyterlab/tests/test_jupyterlab.py index 518b95b6d361..73c95f6173a7 100644 --- a/jupyterlab/tests/test_jupyterlab.py +++ b/jupyterlab/tests/test_jupyterlab.py @@ -5,6 +5,7 @@ # Distributed under the terms of the Modified BSD License. import glob import json +import logging import os import shutil import sys @@ -25,7 +26,7 @@ disable_extension, enable_extension, get_app_info, check_extension, _test_overlap, update_extension ) -from jupyterlab.coreconfig import CoreConfig +from jupyterlab.coreconfig import CoreConfig, _get_default_core_data here = os.path.dirname(os.path.abspath(__file__)) @@ -379,6 +380,51 @@ def test_build_custom(self): assert data['jupyterlab']['version'] == '1.0' assert data['jupyterlab']['staticUrl'] == 'bar' + def test_build_custom_minimal_core_config(self): + default_config = CoreConfig() + core_config = CoreConfig() + core_config.clear_packages() + logger = logging.getLogger('jupyterlab_test_logger') + logger.setLevel('DEBUG') + extensions = ( + '@jupyterlab/application-extension', + '@jupyterlab/apputils-extension', + ) + singletons = ( + "@jupyterlab/application", + "@jupyterlab/apputils", + "@jupyterlab/coreutils", + "@jupyterlab/services", + ) + for name in extensions: + semver = default_config.extensions[name] + core_config.add(name, semver, extension=True) + for name in singletons: + semver = default_config.singletons[name] + core_config.add(name, semver) + + assert install_extension(self.mock_extension) is True + build(core_config=core_config, logger=logger) + + # check static directory. + entry = pjoin(self.app_dir, 'static', 'index.out.js') + with open(entry) as fid: + data = fid.read() + assert self.pkg_names['extension'] in data + + pkg = pjoin(self.app_dir, 'static', 'package.json') + with open(pkg) as fid: + data = json.load(fid) + assert list(data['jupyterlab']['extensions'].keys()) == [ + '@jupyterlab/application-extension', + '@jupyterlab/apputils-extension', + self.pkg_names['extension'], + ] + assert data['jupyterlab']['mimeExtensions'] == {} + for pkg in data['jupyterlab']['singletonPackages']: + if pkg.startswith('@jupyterlab/'): + assert pkg in singletons + def test_load_extension(self): app = NotebookApp() stderr = sys.stderr @@ -477,7 +523,7 @@ def test_compatibility(self): assert _test_overlap('<0.6', '0.1') is None def test_install_compatible(self): - core_data = CoreConfig().data + core_data = _get_default_core_data() current_app_dep = core_data['dependencies']['@jupyterlab/application'] def _gen_dep(ver): return { "dependencies": {