浏览代码

Use extension validation info and set up logging

Steven Silvester 8 年之前
父节点
当前提交
55916cb4cb
共有 1 个文件被更改,包括 232 次插入150 次删除
  1. 232 150
      jupyterlab/commands.py

+ 232 - 150
jupyterlab/commands.py

@@ -7,6 +7,7 @@ from __future__ import print_function
 from distutils.version import LooseVersion
 import errno
 import json
+import logging
 import pipes
 import os
 import glob
@@ -17,7 +18,9 @@ import shutil
 import sys
 import tarfile
 from jupyter_core.paths import ENV_JUPYTER_PATH
-from notebook.extensions import GREEN_ENABLED, RED_DISABLED
+from notebook.extensions import (
+    GREEN_ENABLED, GREEN_OK, RED_DISABLED, RED_X
+)
 
 from .semver import satisfies
 from ._version import __version__
@@ -31,6 +34,7 @@ else:
 
 
 here = osp.dirname(osp.abspath(__file__))
+logging.basicConfig(format='%(message)s', level=logging.INFO)
 
 
 def get_app_dir(app_dir=None):
@@ -44,18 +48,19 @@ def get_app_dir(app_dir=None):
 def run(cmd, **kwargs):
     """Run a command in the given working directory.
     """
-    print('> ' + list2cmdline(cmd))
+    logger = kwargs.pop('logger', logging) or logging
+    logger.info('> ' + list2cmdline(cmd))
     kwargs.setdefault('shell', sys.platform == 'win32')
     kwargs.setdefault('env', os.environ)
     kwargs.setdefault('stderr', STDOUT)
     try:
         return check_output(cmd, **kwargs)
     except CalledProcessError as error:
-        print(error.output)
+        logger.info(error.output)
         raise error
 
 
-def install_extension(extension, app_dir=None):
+def install_extension(extension, app_dir=None, logger=None):
     """Install an extension package into JupyterLab.
 
     Follows the semantics of https://docs.npmjs.com/cli/install.
@@ -68,14 +73,14 @@ def install_extension(extension, app_dir=None):
     if app_dir == here:
         raise ValueError('Cannot install extensions in core app')
     extension = _normalize_path(extension)
-    _ensure_package(app_dir)
+    _ensure_package(app_dir, logger=logger)
     target = pjoin(app_dir, 'extensions', 'temp')
     if os.path.exists(target):
         shutil.rmtree(target)
     os.makedirs(target)
 
     # npm pack the extension
-    run(['npm', 'pack', extension], cwd=target)
+    run(['npm', 'pack', extension], cwd=target, logger=logger)
 
     fname = os.path.basename(glob.glob(pjoin(target, '*.*'))[0])
     data = _read_package(pjoin(target, fname))
@@ -86,6 +91,18 @@ def install_extension(extension, app_dir=None):
         msg = '%s is not a valid JupyterLab extension' % extension
         raise ValueError(msg)
 
+    # Remove the tarball if the package is not compatible.
+    core_data = _get_core_data()
+    deps = data['dependencies']
+    errors = _validate_compatibility(extension, deps, core_data)
+    if errors:
+        shutil.rmtree(target)
+        msg = _format_compatibility_errors(
+            data['name'], data['version'], errors
+        )
+        raise ValueError(msg)
+
+    # Remove an existing extension tarball.
     ext_path = pjoin(app_dir, 'extensions', fname)
     if os.path.exists(ext_path):
         os.remove(ext_path)
@@ -94,10 +111,11 @@ def install_extension(extension, app_dir=None):
     shutil.rmtree(target)
 
     staging = pjoin(app_dir, 'staging')
-    run(['npm', 'install', pjoin(app_dir, 'extensions', fname)], cwd=staging)
+    run(['npm', 'install', pjoin(app_dir, 'extensions', fname)],
+        cwd=staging, logger=logger)
 
 
-def link_package(path, app_dir=None):
+def link_package(path, app_dir=None, logger=None):
     """Link a package against the JupyterLab build.
     """
     app_dir = get_app_dir(app_dir)
@@ -105,7 +123,7 @@ def link_package(path, app_dir=None):
         raise ValueError('Cannot link packages in core app')
 
     path = _normalize_path(path)
-    _ensure_package(app_dir)
+    _ensure_package(app_dir, logger=logger)
 
     # Verify the package.json data.
     pkg_path = osp.join(path, 'package.json')
@@ -122,16 +140,26 @@ def link_package(path, app_dir=None):
     else:
         msg = ('*** Note: Linking non-extension package "%s" (lacks ' +
                '`jupyterlab.extension` metadata)')
-        print(msg % data['name'])
+        logger.info(msg % data['name'])
+
+    core_data = _get_core_data()
+    deps = data['dependencies']
+    name = data['name']
+    errors = _validate_compatibility(name, deps, core_data)
+    if errors:
+        msg = _format_compatibility_errors(name, data['version'], errors)
+        raise ValueError(msg)
+
     config = _get_build_config(app_dir)
     config.setdefault('linked_packages', dict())
     config['linked_packages'][data['name']] = path
-    _write_build_config(config, app_dir)
+    _write_build_config(config, app_dir, logger=logger)
 
 
-def unlink_package(package, app_dir=None):
+def unlink_package(package, app_dir=None, logger=None):
     """Unlink a package from JupyterLab by path or name.
     """
+    logger = logger or logging
     package = _normalize_path(package)
     name = None
     app_dir = get_app_dir(app_dir)
@@ -146,12 +174,12 @@ def unlink_package(package, app_dir=None):
             break
 
     if not name:
-        print('No package matching "%s" is linked' % package)
+        logger.warn('No package matching "%s" is linked' % package)
         return False
 
     del linked[name]
     config['linked_packages'] = linked
-    _write_build_config(config, app_dir)
+    _write_build_config(config, app_dir, logger=logger)
 
     extensions = _get_extensions(app_dir)
     if name in extensions:
@@ -160,19 +188,19 @@ def unlink_package(package, app_dir=None):
     return True
 
 
-def enable_extension(extension, app_dir=None):
+def enable_extension(extension, app_dir=None, logger=None):
     """Enable a JupyterLab extension.
     """
-    _toggle_extension(extension, False, app_dir)
+    _toggle_extension(extension, False, app_dir, logger)
 
 
-def disable_extension(extension, app_dir=None):
+def disable_extension(extension, app_dir=None, logger=None):
     """Disable a JupyterLab package.
     """
-    _toggle_extension(extension, True, app_dir)
+    _toggle_extension(extension, True, app_dir, logger)
 
 
-def should_build(app_dir=None):
+def should_build(app_dir=None, logger=None):
     """Determine whether JupyterLab should be built.
 
     Note: Linked packages should be updated by manually building.
@@ -182,7 +210,7 @@ def should_build(app_dir=None):
     app_dir = get_app_dir(app_dir)
 
     # Check for installed extensions
-    extensions = _get_extensions(app_dir)
+    extensions = _get_extensions(app_dir, logger)
 
     # No linked and no extensions and no built version.
     if not extensions and not os.path.exists(pjoin(app_dir, 'static')):
@@ -202,7 +230,7 @@ def should_build(app_dir=None):
         return True, msg % (version, __version__)
 
     # Look for mismatched extensions.
-    _ensure_package(app_dir)
+    _ensure_package(app_dir, logger=logger)
 
     staging_path = pjoin(app_dir, 'staging', 'package.json')
     with open(staging_path) as fid:
@@ -215,6 +243,10 @@ def should_build(app_dir=None):
 
     # Look for mismatched extension paths.
     for name in extensions:
+        # Check for dependencies that were rejected as incompatible.
+        if name not in staging_data['dependencies']:
+            continue
+
         path = data['dependencies'][name]
         if path.startswith('file:'):
             path = path.replace('file:', '')
@@ -226,137 +258,71 @@ def should_build(app_dir=None):
     return False, ''
 
 
-def validate_compatibility(extension, app_dir=None):
+def validate_compatibility(extension, app_dir=None, logger=None):
+    """Validate the compatibility of an extension.
+    """
     app_dir = get_app_dir(app_dir)
     extensions = _get_extensions(app_dir)
 
     if extension not in extensions:
         raise ValueError('%s is not an installed extension')
 
-    with open(pjoin(here, 'package.app.json')) as fid:
-        core_data = json.load(fid)
-
-    core_deps = core_data['dependencies']
-    singletons = core_data['jupyterlab']['singletonPackages']
     deps = extensions[extension].get('dependencies', dict())
+    core_data = _get_core_data()
+    return _validate_compatibility(extension, deps, core_data)
 
-    errors = []
-
-    for (key, value) in deps.items():
-        if key in singletons:
-            if not satisfies(core_deps[key], value, True):
-                errors.push((key, core_deps[key], value))
-
-    return errors
-
-
-def _get_build_config(app_dir):
-    """Get the build config data for the given app dir
-    """
-    target = pjoin(app_dir, 'settings', 'build_config.json')
-    if not os.path.exists(target):
-        return {}
-    else:
-        with open(target) as fid:
-            return json.load(fid)
-
-
-def _get_page_config(app_dir):
-    """Get the page config data for the given app dir
-    """
-    target = pjoin(app_dir, 'settings', 'page_config.json')
-    if not os.path.exists(target):
-        return {}
-    else:
-        with open(target) as fid:
-            return json.load(fid)
-
-
-def _toggle_extension(extension, value, app_dir=None):
-    """Enable or disable a lab extension.
-    """
-    app_dir = get_app_dir(app_dir)
-    config = _get_page_config(app_dir)
-    extensions = _get_extensions(app_dir)
-    core_extensions = _get_core_extensions()
-
-    if extension not in extensions and extension not in core_extensions:
-        raise ValueError('Extension %s is not installed' % extension)
-    disabled = config.get('disabledExtensions', [])
-    if value and extension not in disabled:
-        disabled.append(extension)
-    if not value and extension in disabled:
-        disabled.remove(extension)
-
-    # Prune extensions that are not installed.
-    disabled = [ext for ext in disabled
-                if (ext in extensions or ext in core_extensions)]
-    config['disabledExtensions'] = disabled
-    _write_page_config(config, app_dir)
-
-
-def _write_build_config(config, app_dir):
-    """Write the build config to the app dir.
-    """
-    _ensure_package(app_dir)
-    target = pjoin(app_dir, 'settings', 'build_config.json')
-    with open(target, 'w') as fid:
-        json.dump(config, fid, indent=4)
-
-
-def _write_page_config(config, app_dir):
-    """Write the build config to the app dir.
-    """
-    _ensure_package(app_dir)
-    target = pjoin(app_dir, 'settings', 'page_config.json')
-    with open(target, 'w') as fid:
-        json.dump(config, fid, indent=4)
 
-
-def uninstall_extension(name, app_dir=None):
+def uninstall_extension(name, app_dir=None, logger=None):
     """Uninstall an extension by name.
     """
+    logger = logger or logging
     app_dir = get_app_dir(app_dir)
     if app_dir == here:
         raise ValueError('Cannot install packages in core app')
     # Allow for uninstalled core extensions here.
-    with open(pjoin(here, 'package.app.json')) as fid:
-        data = json.load(fid)
-        if name in data['jupyterlab']['extensions']:
-            print('Uninstalling core extension %s' % name)
-            config = _get_build_config(app_dir)
-            uninstalled = config.get('uninstalled_core_extensions', [])
-            if name not in uninstalled:
-                uninstalled.append(name)
-                config['uninstalled_core_extensions'] = uninstalled
-                _write_build_config(config, app_dir)
-            return True
+    data = _get_core_data()
+    if name in data['jupyterlab']['extensions']:
+        logger.info('Uninstalling core extension %s' % name)
+        config = _get_build_config(app_dir)
+        uninstalled = config.get('uninstalled_core_extensions', [])
+        if name not in uninstalled:
+            uninstalled.append(name)
+            config['uninstalled_core_extensions'] = uninstalled
+            _write_build_config(config, app_dir, logger=logger)
+        return True
 
     for (extname, data) in _get_extensions(app_dir).items():
         path = data['path']
         if extname == name:
-            print('Uninstalling %s from %s' % (name, os.path.dirname(path)))
+            msg = 'Uninstalling %s from %s' % (name, os.path.dirname(path))
+            logger.info(msg)
             os.remove(path)
             return True
 
-    print('No labextension named "%s" installed' % name)
+    logger.warn('No labextension named "%s" installed' % name)
     return False
 
 
-def list_extensions(app_dir=None):
+def list_extensions(app_dir=None, logger=None):
     """List the extensions.
     """
+    logger = logger or logging
     app_dir = get_app_dir(app_dir)
     extensions = _get_extensions(app_dir)
     disabled = _get_disabled(app_dir)
-    linked = _get_linked_packages(app_dir)
+    linked = _get_linked_packages(app_dir, logger=logger)
     app = []
     sys = []
     linked = []
+    errors = dict()
+
+    core_data = _get_core_data()
 
     # We want to organize by dir.
     sys_path = pjoin(get_app_dir(), 'extensions')
     for (key, value) in extensions.items():
+        deps = extensions[key].get('dependencies', dict())
+        errors[key] = _validate_compatibility(key, deps, core_data)
         if key in linked:
             linked.append(key)
         if value['path'] == sys_path and sys_path != app_dir:
@@ -364,40 +330,57 @@ def list_extensions(app_dir=None):
             continue
         app.append(key)
 
-    print('JupyterLab v%s' % __version__)
-    print('Known labextensions:')
+    logger.info('JupyterLab v%s' % __version__)
+    logger.info('Known labextensions:')
     if app:
-        print('   app dir: %s' % app_dir)
+        logger.info('   app dir: %s' % app_dir)
         for item in sorted(app):
             extra = ''
-            if item in linked:
-                extra += '*'
             if item in disabled:
                 extra += ' %s' % RED_DISABLED
             else:
                 extra += ' %s' % GREEN_ENABLED
-            print('        %s%s' % (item, extra))
+            if errors[item]:
+                extra += ' %s' % RED_X
+            else:
+                extra += ' %s' % GREEN_OK
+            if item in linked:
+                extra += '*'
+            logger.info('        %s%s' % (item, extra))
+            version = extensions[item]['version']
+            if errors[item]:
+                msg = _format_compatibility_errors(item, version, errors[item])
+                logger.warn(msg + '\n')
 
     if sys:
-        print('   sys dir: %s' % sys_path)
+        logger.info('   sys dir: %s' % sys_path)
         for item in sorted(sys):
             extra = ''
-            if item in linked:
-                extra += '*'
             if item in disabled:
                 extra += ' %s' % RED_DISABLED
             else:
                 extra += ' %s' % GREEN_ENABLED
-            print('        %s%s' % (item, extra))
+            logger.info('        %s%s' % (item, extra))
+            if errors[item]:
+                extra += ' %s' % RED_X
+            else:
+                extra += ' %s' % GREEN_OK
+            if item in linked:
+                extra += '*'
+            logger.info('        %s%s' % (item, extra))
+            version = extensions[item]['version']
+            if errors[item]:
+                msg = _format_compatibility_errors(item, version, errors[item])
+                logger.warn(msg + '\n')
 
     if linked:
-        print('* Denotes linked packages')
+        logger.info('* Denotes linked extensions. Use `jupyter labextension listlinked` to see details')
 
     # Handle uninstalled and disabled core packages
     uninstalled_core = _get_uinstalled_core_extensions(app_dir)
     if uninstalled_core:
-        print('\nUninstalled core extensiosn:')
-        [print('    %s' % item) for item in sorted(uninstalled_core)]
+        logger.info('\nUninstalled core extensiosn:')
+        [logger.info('    %s' % item) for item in sorted(uninstalled_core)]
 
     core_extensions = _get_core_extensions()
 
@@ -407,8 +390,8 @@ def list_extensions(app_dir=None):
             disabled_core.append(key)
 
     if disabled_core:
-        print('\nDisabled core extensions:')
-        [print('    %s' % item) for item in sorted(disabled_core)]
+        logger.info('\nDisabled core extensions:')
+        [logger.info('    %s' % item) for item in sorted(disabled_core)]
 
 
 def clean(app_dir=None):
@@ -422,25 +405,26 @@ def clean(app_dir=None):
             shutil.rmtree(target)
 
 
-def build(app_dir=None, name=None, version=None):
+def build(app_dir=None, name=None, version=None, logger=None):
     """Build the JupyterLab application."""
     # Set up the build directory.
+    logger = logger or logging
     app_dir = get_app_dir(app_dir)
     if app_dir == here:
         raise ValueError('Cannot build extensions in the core app')
 
-    _ensure_package(app_dir, name, version)
+    _ensure_package(app_dir, name=name, version=version, logger=logger)
     staging = pjoin(app_dir, 'staging')
 
     # Make sure packages are installed.
-    run(['npm', 'install'], cwd=staging)
+    run(['npm', 'install'], cwd=staging, logger=logger)
 
     # Install the linked extensions.
-    for path in _get_linked_packages(app_dir).values():
+    for path in _get_linked_packages(app_dir, logger=logger).values():
         install_extension(path, app_dir)
 
     # Build the app.
-    run(['npm', 'run', 'build'], cwd=staging)
+    run(['npm', 'run', 'build'], cwd=staging, logger=logger)
 
     # Move the app to the static dir.
     static = pjoin(app_dir, 'static')
@@ -449,9 +433,108 @@ def build(app_dir=None, name=None, version=None):
     shutil.copytree(pjoin(staging, 'build'), static)
 
 
-def _ensure_package(app_dir, name=None, version=None):
+def _get_build_config(app_dir):
+    """Get the build config data for the given app dir
+    """
+    target = pjoin(app_dir, 'settings', 'build_config.json')
+    if not os.path.exists(target):
+        return {}
+    else:
+        with open(target) as fid:
+            return json.load(fid)
+
+
+def _get_page_config(app_dir):
+    """Get the page config data for the given app dir
+    """
+    target = pjoin(app_dir, 'settings', 'page_config.json')
+    if not os.path.exists(target):
+        return {}
+    else:
+        with open(target) as fid:
+            return json.load(fid)
+
+
+def _validate_compatibility(extension, deps, core_data):
+    """Validate the compatibility of an extension.
+    """
+    core_deps = core_data['dependencies']
+    singletons = core_data['jupyterlab']['singletonPackages']
+
+    errors = []
+
+    for (key, value) in deps.items():
+        if key in singletons:
+            if not satisfies(core_deps[key], value, True):
+                errors.append((key, core_deps[key], value))
+
+    return errors
+
+
+def _get_core_data():
+    """Get the data for the app template.
+    """
+    with open(pjoin(here, 'package.app.json')) as fid:
+        return json.load(fid)
+
+
+def _format_compatibility_errors(name, version, errors):
+    """Format a message for compatibility errors.
+    """
+    msg = '\n"%s@%s" is not compatible with the current JupyterLab'
+    msg = msg % (name, version)
+    msg += '\nConflicting Dependencies:'
+    msg += '\nRequired\tActual\tPackage'
+    for error in errors:
+        msg += '\n%s  \t%s\t%s' % (error[1], error[2], error[0])
+    return msg
+
+
+def _toggle_extension(extension, value, app_dir=None, logger=None):
+    """Enable or disable a lab extension.
+    """
+    app_dir = get_app_dir(app_dir)
+    config = _get_page_config(app_dir)
+    extensions = _get_extensions(app_dir)
+    core_extensions = _get_core_extensions()
+
+    if extension not in extensions and extension not in core_extensions:
+        raise ValueError('Extension %s is not installed' % extension)
+    disabled = config.get('disabledExtensions', [])
+    if value and extension not in disabled:
+        disabled.append(extension)
+    if not value and extension in disabled:
+        disabled.remove(extension)
+
+    # Prune extensions that are not installed.
+    disabled = [ext for ext in disabled
+                if (ext in extensions or ext in core_extensions)]
+    config['disabledExtensions'] = disabled
+    _write_page_config(config, app_dir, logger=logger)
+
+
+def _write_build_config(config, app_dir, logger):
+    """Write the build config to the app dir.
+    """
+    _ensure_package(app_dir, logger=logger)
+    target = pjoin(app_dir, 'settings', 'build_config.json')
+    with open(target, 'w') as fid:
+        json.dump(config, fid, indent=4)
+
+
+def _write_page_config(config, app_dir, logger):
+    """Write the build config to the app dir.
+    """
+    _ensure_package(app_dir, logger=logger)
+    target = pjoin(app_dir, 'settings', 'page_config.json')
+    with open(target, 'w') as fid:
+        json.dump(config, fid, indent=4)
+
+
+def _ensure_package(app_dir, name=None, version=None, logger=None):
     """Make sure the build dir is set up.
     """
+    logger = logger or logging
     if not os.path.exists(pjoin(app_dir, 'extensions')):
         try:
             os.makedirs(pjoin(app_dir, 'extensions'))
@@ -485,13 +568,16 @@ def _ensure_package(app_dir, name=None, version=None):
         shutil.copy(pjoin(here, fname), dest)
 
     # Template the package.json file.
-    pkg_path = pjoin(here, 'package.app.json')
-    with open(pkg_path) as fid:
-        data = json.load(fid)
-
+    data = _get_core_data()
     extensions = _get_extensions(app_dir)
 
     for (key, value) in extensions.items():
+        # Reject incompatible extensions with a message.
+        errors = _validate_compatibility(key, value['dependencies'], data)
+        if errors:
+            msg = _format_compatibility_errors(key, value['version'], errors)
+            logger.warn(msg + '\n')
+            continue
         data['dependencies'][key] = value['path']
         data['jupyterlab']['extensions'].append(key)
 
@@ -544,8 +630,7 @@ def _get_disabled(app_dir):
 def _get_core_extensions():
     """Get the core extensions.
     """
-    with open(pjoin(here, 'package.app.json')) as fid:
-        return json.load(fid)['jupyterlab']['extensions']
+    return _get_core_data()['jupyterlab']['extensions']
 
 
 def _get_extensions(app_dir):
@@ -575,9 +660,10 @@ def _get_extensions(app_dir):
     return extensions
 
 
-def _get_linked_packages(app_dir=None):
+def _get_linked_packages(app_dir=None, logger=None):
     """Get the linked packages metadata.
     """
+    logger = logger or logging
     app_dir = get_app_dir(app_dir)
     config = _get_build_config(app_dir)
     linked = config.get('linked_packages', dict())
@@ -593,14 +679,14 @@ def _get_linked_packages(app_dir=None):
         path = linked[name]
         if name in extensions:
             uninstall_extension(name)
-            print('**Note: Removing dead linked extension "%s"' % name)
+            logger.warn('**Note: Removing dead linked extension "%s"' % name)
         else:
-            print('**Note: Removing dead linked package "%s"' % name)
+            logger.warn('**Note: Removing dead linked package "%s"' % name)
         del linked[name]
 
     if dead:
         config['linked_packages'] = linked
-        _write_build_config(config, app_dir)
+        _write_build_config(config, app_dir, logger=logger)
 
     return config.get('linked_packages', dict())
 
@@ -620,7 +706,3 @@ def _normalize_path(extension):
     if osp.exists(extension):
         extension = osp.abspath(extension)
     return extension
-
-
-if __name__ == '__main__':
-    print(validate_compatibility('jupyterlab-hub'))