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": {