Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Install multiple versions of same extension #6857

Merged
merged 14 commits into from Aug 15, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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
saulshanabrook marked this conversation as resolved.
Show resolved Hide resolved
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