From cceac684ddbd9b90de20595951f593a669fd66e5 Mon Sep 17 00:00:00 2001 From: telamonian Date: Tue, 3 Dec 2019 17:26:39 -0600 Subject: [PATCH 1/3] --extensions, --settings, --static, --all flags for `jupyter lab clean` --- jupyterlab/commands.py | 47 +++++++++++++++++++++++++++++++++++------- jupyterlab/labapp.py | 44 ++++++++++++++++++++++++++++++++------- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/jupyterlab/commands.py b/jupyterlab/commands.py index 8bd606337fc4..662cf809f04a 100644 --- a/jupyterlab/commands.py +++ b/jupyterlab/commands.py @@ -346,6 +346,8 @@ def _default_registry(self): def _ensure_options(options, **kwargs): """Helper to use deprecated kwargs for AppOption""" + optClass = options.__class__ if issubclass(options.__class__, AppOptions) else AppOptions + # Filter out default-value kwargs kwargs = dict(filter(lambda item: item[1] is not None, kwargs.items())) # Warn for deprecated kwargs usage @@ -355,11 +357,11 @@ def _ensure_options(options, **kwargs): "deprecated, use the options argument instead: %r" % (kwargs,), DeprecationWarning) if options is None: - return AppOptions(**kwargs) + return optClass(**kwargs) # Also support mixed use of options and kwargs: opt_args = {name: getattr(options, name) for name in options.trait_names()} kwargs.update(**opt_args) - return AppOptions(**kwargs) + return optClass(**kwargs) def watch(app_dir=None, logger=None, core_config=None, app_options=None): @@ -434,15 +436,28 @@ def clean(app_dir=None, logger=None, app_options=None): handler = _AppHandler(app_options) logger = app_options.logger app_dir = app_options.app_dir + logger.info('Cleaning %s...', app_dir) if app_dir == pjoin(HERE, 'dev'): raise ValueError('Cannot clean the dev app') if app_dir == pjoin(HERE, 'core'): raise ValueError('Cannot clean the core app') - for name in ['staging']: - target = pjoin(app_dir, name) - if osp.exists(target): - _rmtree(target, logger) + + if app_options.all: + logger.info('Removing everything in %s...', app_dir) + _rmtree_star(app_dir, logger) + else: + possibleTargets = ['extensions', 'settings', 'staging', 'static'] + targets = [t for t in possibleTargets if getattr(app_options, t)] + + for name in targets: + target = pjoin(app_dir, name) + if osp.exists(target): + logger.info('Removing %s...', name) + _rmtree(target, logger) + else: + logger.info('%s not present, skipping...', name) + logger.info('Success!') @@ -1809,11 +1824,29 @@ def _normalize_path(extension): def _rmtree(path, logger): """Remove a tree, logging errors""" def onerror(*exc_info): - logger.debug('Error in rmtree', exc_info=exc_info) + logger.debug('Error in shutil.rmtree', exc_info=exc_info) shutil.rmtree(path, onerror=onerror) +def _unlink(path, logger): + """Remove a file, logging errors""" + try: + os.unlink(path) + except Exception: + logger.debug('Error in os.unlink', exc_info=sys.exc_info()) + + +def _rmtree_star(path, logger): + """Remove all files/trees within a dir, logging errors""" + for filename in os.listdir(path): + file_path = os.path.join(path, filename) + if os.path.isfile(file_path) or os.path.islink(file_path): + _unlink(file_path, logger) + elif os.path.isdir(file_path): + _rmtree(file_path, logger) + + def _validate_extension(data): """Detect if a package is an extension using its metadata. diff --git a/jupyterlab/labapp.py b/jupyterlab/labapp.py index 7ce213abfa37..d3050214d638 100644 --- a/jupyterlab/labapp.py +++ b/jupyterlab/labapp.py @@ -10,7 +10,7 @@ from os.path import join as pjoin import sys -from jupyter_core.application import JupyterApp, base_aliases +from jupyter_core.application import JupyterApp, base_aliases, base_flags from jupyterlab_server import slugify, WORKSPACE_EXTENSION from notebook.notebookapp import NotebookApp, aliases, flags from notebook.utils import url_path_join as ujoin @@ -101,26 +101,57 @@ def start(self): clean_aliases = dict(base_aliases) clean_aliases['app-dir'] = 'LabCleanApp.app_dir' +clean_flags = dict(base_flags) +clean_flags['extensions'] = ({'LabCleanApp': {'extensions': True}}, 'Also delete /extensions') +clean_flags['settings'] = ({'LabCleanApp': {'settings': True}}, 'Also delete /settings') +clean_flags['static'] = ({'LabCleanApp': {'static': True}}, 'Also delete /static') +clean_flags['all'] = ({'LabCleanApp': {'all': True}}, 'Delete the entire contents of the app directory') + + +class LabCleanAppOptions(AppOptions): + extensions = Bool(False) + settings = Bool(False) + staging = Bool(True) + static = Bool(False) + all = Bool(False) + class LabCleanApp(JupyterApp): version = version description = """ Clean the JupyterLab application - This will clean the app directory by removing the `staging` and `static` - directories. + This will clean the app directory by removing the `staging` directories. + Optionally, the `extensions`, `settings`, and/or `static` directories, + or the entire contents of the app directory, can also be removed. """ aliases = clean_aliases + flags = clean_flags # Not configurable! core_config = Instance(CoreConfig, allow_none=True) app_dir = Unicode('', config=True, help='The app directory to clean') + extensions = Bool(False, config=True, help="Also delete /extensions") + + settings = Bool(False, config=True, help="Also delete /settings") + + static = Bool(False, config=True, help="Also delete /static") + + all = Bool(False, config=True, help="Delete the entire contents of the app directory") + def start(self): - clean(app_options=AppOptions( - app_dir=self.app_dir, logger=self.log, - core_config=self.core_config)) + app_options = LabCleanAppOptions( + logger=self.log, + core_config=self.core_config, + app_dir=self.app_dir, + extensions=self.extensions, + settings=self.settings, + static=self.static, + all=self.all + ) + clean(app_options=app_options) class LabPathApp(JupyterApp): @@ -137,7 +168,6 @@ class LabPathApp(JupyterApp): environment variable or it will fall back to '/lab/workspaces' in the default Jupyter configuration directory. """ - def start(self): print('Application directory: %s' % get_app_dir()) print('User Settings directory: %s' % get_user_settings_dir()) From bff4f75f406887e72f3c5d8b2ea14e40fdd50f3f Mon Sep 17 00:00:00 2001 From: telamonian Date: Wed, 4 Dec 2019 19:22:21 -0600 Subject: [PATCH 2/3] added `jupyter lab clean --flag` commands to usage test, as requested --- scripts/ci_script.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/ci_script.sh b/scripts/ci_script.sh index 89bd038977d2..b951fb5ae555 100644 --- a/scripts/ci_script.sh +++ b/scripts/ci_script.sh @@ -230,6 +230,13 @@ if [[ $GROUP == usage ]]; then sleep 5 kill $TASK_PID wait $TASK_PID + + # Make sure we can clean various bits of the app dir + jupyter lab clean + jupyter lab clean --extensions + jupyter lab clean --settings + jupyter lab clean --static + jupyter lab clean --all fi if [[ $GROUP == nonode ]]; then From 8cc3dab32e545a4a3014e2ae0864ee67972058c3 Mon Sep 17 00:00:00 2001 From: telamonian Date: Thu, 5 Dec 2019 10:22:11 -0600 Subject: [PATCH 3/3] added warning about reinstalling extensions to help and `lab clean` output --- jupyterlab/commands.py | 2 ++ jupyterlab/labapp.py | 14 ++++++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/jupyterlab/commands.py b/jupyterlab/commands.py index 662cf809f04a..0de688edaac8 100644 --- a/jupyterlab/commands.py +++ b/jupyterlab/commands.py @@ -459,6 +459,8 @@ def clean(app_dir=None, logger=None, app_options=None): logger.info('%s not present, skipping...', name) logger.info('Success!') + if app_options.all or app_options.extensions: + logger.info('All of your extensions have been removed, and will need to be reinstalled') def build(app_dir=None, name=None, version=None, static_url=None, diff --git a/jupyterlab/labapp.py b/jupyterlab/labapp.py index d3050214d638..9dd57d2e7052 100644 --- a/jupyterlab/labapp.py +++ b/jupyterlab/labapp.py @@ -101,11 +101,15 @@ def start(self): clean_aliases = dict(base_aliases) clean_aliases['app-dir'] = 'LabCleanApp.app_dir' +ext_warn_msg = "WARNING: this will delete all of your extensions, which will need to be reinstalled" + clean_flags = dict(base_flags) -clean_flags['extensions'] = ({'LabCleanApp': {'extensions': True}}, 'Also delete /extensions') +clean_flags['extensions'] = ({'LabCleanApp': {'extensions': True}}, + 'Also delete /extensions.\n%s' % ext_warn_msg) clean_flags['settings'] = ({'LabCleanApp': {'settings': True}}, 'Also delete /settings') clean_flags['static'] = ({'LabCleanApp': {'static': True}}, 'Also delete /static') -clean_flags['all'] = ({'LabCleanApp': {'all': True}}, 'Delete the entire contents of the app directory') +clean_flags['all'] = ({'LabCleanApp': {'all': True}}, + 'Delete the entire contents of the app directory.\n%s' % ext_warn_msg) class LabCleanAppOptions(AppOptions): @@ -133,13 +137,15 @@ class LabCleanApp(JupyterApp): app_dir = Unicode('', config=True, help='The app directory to clean') - extensions = Bool(False, config=True, help="Also delete /extensions") + extensions = Bool(False, config=True, + help="Also delete /extensions.\n%s" % ext_warn_msg) settings = Bool(False, config=True, help="Also delete /settings") static = Bool(False, config=True, help="Also delete /static") - all = Bool(False, config=True, help="Delete the entire contents of the app directory") + all = Bool(False, config=True, + help="Delete the entire contents of the app directory.\n%s" % ext_warn_msg) def start(self): app_options = LabCleanAppOptions(