Skip to content

Commit

Permalink
Merge pull request #6857 from jupyterlab/extension-alias
Browse files Browse the repository at this point in the history
Install multiple versions of same extension
  • Loading branch information
blink1073 committed Aug 15, 2019
2 parents 860877e + 5edaa3f commit 0acc990
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 16 deletions.
46 changes: 36 additions & 10 deletions jupyterlab/commands.py
Expand Up @@ -39,6 +39,9 @@
DEV_DIR = osp.abspath(os.path.join(HERE, '..', 'dev_mode'))


# If we are pinning the package, rename it `pin@<alias>`
PIN_PREFIX = 'pin@'

class ProgressProcess(Process):

def __init__(self, cmd, logger=None, cwd=None, kill_event=None,
Expand Down Expand Up @@ -304,7 +307,8 @@ def watch(app_dir=None, logger=None, core_config=None):
return handler.watch()


def install_extension(extension, app_dir=None, logger=None, core_config=None):

def install_extension(extension, app_dir=None, logger=None, core_config=None, pin=None):
"""Install an extension package into JupyterLab.
The extension is first validated.
Expand All @@ -314,7 +318,7 @@ def install_extension(extension, app_dir=None, logger=None, core_config=None):
logger = _ensure_logger(logger)
_node_check(logger)
handler = _AppHandler(app_dir, logger, core_config=core_config)
return handler.install_extension(extension)
return handler.install_extension(extension, pin=pin)


def uninstall_extension(name=None, app_dir=None, logger=None, all_=False, core_config=None):
Expand Down Expand Up @@ -491,7 +495,7 @@ def __init__(self, app_dir, logger=None, kill_event=None, core_config=None):
# TODO: Make this configurable
self.registry = 'https://registry.npmjs.org'

def install_extension(self, extension, existing=None):
def install_extension(self, extension, existing=None, pin=None):
"""Install an extension package into JupyterLab.
The extension is first validated.
Expand All @@ -518,7 +522,7 @@ def install_extension(self, extension, existing=None):

# Install the package using a temporary directory.
with TemporaryDirectory() as tempdir:
info = self._install_extension(extension, tempdir)
info = self._install_extension(extension, tempdir, pin=pin)

name = info['name']

Expand Down Expand Up @@ -787,14 +791,18 @@ def _update_extension(self, name):
Returns `True` if a rebuild is recommended, `False` otherwise.
"""
data = self.info['extensions'][name]
if data["alias_package_source"]:
self.logger.warn("Skipping updating pinned extension '%s'." % name)
return False
try:
latest = self._latest_compatible_package_version(name)
except URLError:
return False
if latest is None:
self.logger.warn('No compatible version found for %s!' % (name,))
return False
if latest == self.info['extensions'][name]['version']:
if latest == data['version']:
self.logger.info('Extension %r already up to date' % name)
return False
self.logger.info('Updating %s to version %s' % (name, latest))
Expand Down Expand Up @@ -1212,17 +1220,25 @@ def _get_extensions_in_dir(self, dname, core_data):
name = data['name']
jlab = data.get('jupyterlab', dict())
path = osp.abspath(target)

filename = osp.basename(target)
if filename.startswith(PIN_PREFIX):
alias = filename[len(PIN_PREFIX):-len(".tgz")]
else:
alias = None
# homepage, repository are optional
if 'homepage' in data:
url = data['homepage']
elif 'repository' in data and isinstance(data['repository'], dict):
url = data['repository'].get('url', '')
else:
url = ''
extensions[name] = dict(path=path,
extensions[alias or name] = dict(path=path,
filename=osp.basename(path),
url=url,
version=data['version'],
# Only save the package name if the extension name is an alias
alias_package_source=name if alias else None,
jupyterlab=jlab,
dependencies=deps,
tar_dir=osp.dirname(path),
Expand Down Expand Up @@ -1316,7 +1332,12 @@ def _list_extensions(self, info, ext_type):
extra += ' %s' % GREEN_OK
if data['is_local']:
extra += '*'
logger.info(' %s v%s%s' % (name, version, extra))
# If we have the package name in the data, this means this extension's name is the alias name
alias_package_source = data['alias_package_source']
if alias_package_source:
logger.info(' %s %s v%s%s' % (name, alias_package_source, version, extra))
else:
logger.info(' %s v%s%s' % (name, version, extra))
if errors:
error_accumulator[name] = (version, errors)

Expand Down Expand Up @@ -1381,10 +1402,10 @@ def _get_local_data(self, source):

return data

def _install_extension(self, extension, tempdir):
def _install_extension(self, extension, tempdir, pin=None):
"""Install an extension with validation and return the name and path.
"""
info = self._extract_package(extension, tempdir)
info = self._extract_package(extension, tempdir, pin=pin)
data = info['data']

# Verify that the package is an extension.
Expand Down Expand Up @@ -1438,7 +1459,7 @@ def _install_extension(self, extension, tempdir):
info['path'] = target
return info

def _extract_package(self, source, tempdir):
def _extract_package(self, source, tempdir, pin=None):
"""Call `npm pack` for an extension.
The pack command will download the package tar if `source` is
Expand All @@ -1465,6 +1486,11 @@ def _extract_package(self, source, tempdir):
info['path'] = target
else:
info['path'] = path
if pin:
old_path = info['path']
new_path = pjoin(osp.dirname(old_path), '{}{}.tgz'.format(PIN_PREFIX, pin))
shutil.move(old_path, new_path)
info['path'] = new_path

info['filename'] = osp.basename(info['path'])
info['name'] = info['data']['name']
Expand Down
33 changes: 29 additions & 4 deletions jupyterlab/labextensions.py
Expand Up @@ -57,6 +57,8 @@
aliases['minimize'] = 'BaseExtensionApp.minimize'
aliases['debug-log-path'] = 'DebugLogFileMixin.debug_log_path'

install_aliases = copy(aliases)
install_aliases['pin-version-as'] = 'InstallLabExtensionApp.pin'

VERSION = get_app_version()

Expand Down Expand Up @@ -111,15 +113,38 @@ def _log_format_default(self):


class InstallLabExtensionApp(BaseExtensionApp):
description = "Install labextension(s)"
description = """Install labextension(s)
Usage
jupyter labextension install [--pin-version-as <alias,...>] <package...>
This installs JupyterLab extensions similar to yarn add or npm install.
Pass a list of comma seperate names to the --pin-version-as flag
to use as alises for the packages providers. This is useful to
install multiple versions of the same extension.
These can be uninstalled with the alias you provided
to the flag, similar to the "alias" feature of yarn add.
"""
aliases = install_aliases

pin = Unicode('', config=True,
help="Pin this version with a certain alias")

def run_task(self):
pinned_versions = self.pin.split(',')
self.extra_args = self.extra_args or [os.getcwd()]
return any([
install_extension(
arg, self.app_dir, logger=self.log,
core_config=self.core_config)
for arg in self.extra_args
arg,
self.app_dir,
logger=self.log,
core_config=self.core_config,
# Pass in pinned alias if we have it
pin=pinned_versions[i] if i < len(pinned_versions) else None
)
for i, arg in enumerate(self.extra_args)
])


Expand Down
61 changes: 60 additions & 1 deletion jupyterlab/tests/test_jupyterlab.py
Expand Up @@ -9,6 +9,10 @@
import os
import shutil
import sys
import subprocess
import shutil
import pathlib
import platform
from os.path import join as pjoin
from tempfile import TemporaryDirectory
from unittest import TestCase
Expand Down Expand Up @@ -121,12 +125,15 @@ def ignore(dname, files):
self.assertEqual(paths.ENV_CONFIG_PATH, [self.config_dir])
self.assertEqual(paths.ENV_JUPYTER_PATH, [self.data_dir])
self.assertEqual(
commands.get_app_dir(),
os.path.realpath(commands.get_app_dir()),
os.path.realpath(pjoin(self.data_dir, 'lab'))
)

self.app_dir = commands.get_app_dir()

# Set pinned extension names
self.pinned_packages = ['jupyterlab-test-extension@1.0', 'jupyterlab-test-extension@2.0']

def test_install_extension(self):
assert install_extension(self.mock_extension) is True
path = pjoin(self.app_dir, 'extensions', '*.tgz')
Expand Down Expand Up @@ -228,6 +235,58 @@ def test_uninstall_core_extension(self):
assert '@jupyterlab/console-extension' in extensions
assert check_extension('@jupyterlab/console-extension')

def test_install_and_uninstall_pinned(self):
"""
You should be able to install different versions of the same extension with different
pinned names and uninstall them with those names.
"""
NAMES = ['test-1', 'test-2']
assert install_extension(self.pinned_packages[0], pin=NAMES[0])
assert install_extension(self.pinned_packages[1], pin=NAMES[1])

extensions = get_app_info(self.app_dir)['extensions']
assert NAMES[0] in extensions
assert NAMES[1] in extensions
assert check_extension(NAMES[0])
assert check_extension(NAMES[1])

# Uninstall
assert uninstall_extension(NAMES[0])
assert uninstall_extension(NAMES[1])

extensions = get_app_info(self.app_dir)['extensions']
assert NAMES[0] not in extensions
assert NAMES[1] not in extensions
assert not check_extension(NAMES[0])
assert not check_extension(NAMES[1])

@pytest.mark.skipif(platform.system() == 'Windows', reason='running npm pack fails on windows CI')
def test_install_and_uninstall_pinned_folder(self):
"""
Same as above test, but installs from a local folder instead of from npm.
"""
# Download each version of the package from NPM:
base_dir = pathlib.Path(self.tempdir())

# The archive file names are printed to stdout when run `npm pack`
packages = [
subprocess.run(
['npm', 'pack', name],
stdout=subprocess.PIPE,
universal_newlines=True,
check=True,
cwd=base_dir
).stdout.strip()
for name in self.pinned_packages
]

shutil.unpack_archive(str(base_dir / packages[0]), str(base_dir / '1'))
shutil.unpack_archive(str(base_dir / packages[1]), str(base_dir / '2'))
# Change pinned packages to be these directories now, so we install from these folders
self.pinned_packages = [str(base_dir / '1' / 'package'), str(base_dir / '2' / 'package')]
self.test_install_and_uninstall_pinned()


def test_link_extension(self):
path = self.mock_extension
name = self.pkg_names['extension']
Expand Down
1 change: 0 additions & 1 deletion scripts/travis_script.sh
Expand Up @@ -142,7 +142,6 @@ if [[ $GROUP == usage ]]; then
jupyter lab workspaces export newspace > newspace.json
rm workspace.json newspace.json


# Make sure we can call help on all the cli apps.
jupyter lab -h
jupyter lab build -h
Expand Down

0 comments on commit 0acc990

Please sign in to comment.