|
|
|
"""The root `jupyter` command. |
|
|
|
This does nothing other than dispatch to subcommands or output path info. |
|
""" |
|
|
|
|
|
|
|
from __future__ import annotations |
|
|
|
import argparse |
|
import errno |
|
import json |
|
import os |
|
import site |
|
import sys |
|
import sysconfig |
|
from pathlib import Path |
|
from shutil import which |
|
from subprocess import Popen |
|
from typing import Any |
|
|
|
from . import paths |
|
from .version import __version__ |
|
|
|
|
|
class JupyterParser(argparse.ArgumentParser): |
|
"""A Jupyter argument parser.""" |
|
|
|
@property |
|
def epilog(self) -> str | None: |
|
"""Add subcommands to epilog on request |
|
|
|
Avoids searching PATH for subcommands unless help output is requested. |
|
""" |
|
return "Available subcommands: %s" % " ".join(list_subcommands()) |
|
|
|
@epilog.setter |
|
def epilog(self, x: Any) -> None: |
|
"""Ignore epilog set in Parser.__init__""" |
|
|
|
def argcomplete(self) -> None: |
|
"""Trigger auto-completion, if enabled""" |
|
try: |
|
import argcomplete |
|
|
|
argcomplete.autocomplete(self) |
|
except ImportError: |
|
pass |
|
|
|
|
|
def jupyter_parser() -> JupyterParser: |
|
"""Create a jupyter parser object.""" |
|
parser = JupyterParser( |
|
description="Jupyter: Interactive Computing", |
|
) |
|
group = parser.add_mutually_exclusive_group(required=False) |
|
|
|
group.add_argument( |
|
"--version", action="store_true", help="show the versions of core jupyter packages and exit" |
|
) |
|
subcommand_action = group.add_argument( |
|
"subcommand", type=str, nargs="?", help="the subcommand to launch" |
|
) |
|
|
|
subcommand_action.completer = lambda *args, **kwargs: list_subcommands() |
|
|
|
group.add_argument("--config-dir", action="store_true", help="show Jupyter config dir") |
|
group.add_argument("--data-dir", action="store_true", help="show Jupyter data dir") |
|
group.add_argument("--runtime-dir", action="store_true", help="show Jupyter runtime dir") |
|
group.add_argument( |
|
"--paths", |
|
action="store_true", |
|
help="show all Jupyter paths. Add --json for machine-readable format.", |
|
) |
|
parser.add_argument("--json", action="store_true", help="output paths as machine-readable json") |
|
parser.add_argument("--debug", action="store_true", help="output debug information about paths") |
|
|
|
return parser |
|
|
|
|
|
def list_subcommands() -> list[str]: |
|
"""List all jupyter subcommands |
|
|
|
searches PATH for `jupyter-name` |
|
|
|
Returns a list of jupyter's subcommand names, without the `jupyter-` prefix. |
|
Nested children (e.g. jupyter-sub-subsub) are not included. |
|
""" |
|
subcommand_tuples = set() |
|
|
|
for d in _path_with_self(): |
|
try: |
|
names = os.listdir(d) |
|
except OSError: |
|
continue |
|
for name in names: |
|
if name.startswith("jupyter-"): |
|
if sys.platform.startswith("win"): |
|
|
|
name = os.path.splitext(name)[0] |
|
subcommand_tuples.add(tuple(name.split("-")[1:])) |
|
|
|
subcommands = set() |
|
|
|
for sub_tup in subcommand_tuples: |
|
if not any(sub_tup[:i] in subcommand_tuples for i in range(1, len(sub_tup))): |
|
subcommands.add("-".join(sub_tup)) |
|
return sorted(subcommands) |
|
|
|
|
|
def _execvp(cmd: str, argv: list[str]) -> None: |
|
"""execvp, except on Windows where it uses Popen |
|
|
|
Python provides execvp on Windows, but its behavior is problematic (Python bug#9148). |
|
""" |
|
if sys.platform.startswith("win"): |
|
|
|
|
|
cmd_path = which(cmd) |
|
if cmd_path is None: |
|
raise OSError("%r not found" % cmd, errno.ENOENT) |
|
p = Popen([cmd_path] + argv[1:]) |
|
|
|
|
|
import signal |
|
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN) |
|
p.wait() |
|
sys.exit(p.returncode) |
|
else: |
|
os.execvp(cmd, argv) |
|
|
|
|
|
def _jupyter_abspath(subcommand: str) -> str: |
|
"""This method get the abspath of a specified jupyter-subcommand with no |
|
changes on ENV. |
|
""" |
|
|
|
search_path = os.pathsep.join(_path_with_self()) |
|
|
|
jupyter_subcommand = f"jupyter-{subcommand}" |
|
abs_path = which(jupyter_subcommand, path=search_path) |
|
if abs_path is None: |
|
msg = f"\nJupyter command `{jupyter_subcommand}` not found." |
|
raise Exception(msg) |
|
|
|
if not os.access(abs_path, os.X_OK): |
|
msg = f"\nJupyter command `{jupyter_subcommand}` is not executable." |
|
raise Exception(msg) |
|
|
|
return abs_path |
|
|
|
|
|
def _path_with_self() -> list[str]: |
|
"""Put `jupyter`'s dir at the front of PATH |
|
|
|
Ensures that /path/to/jupyter subcommand |
|
will do /path/to/jupyter-subcommand |
|
even if /other/jupyter-subcommand is ahead of it on PATH |
|
""" |
|
path_list = (os.environ.get("PATH") or os.defpath).split(os.pathsep) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
try: |
|
bindir = sysconfig.get_path("scripts") |
|
except KeyError: |
|
|
|
pass |
|
else: |
|
path_list.append(bindir) |
|
|
|
scripts = [sys.argv[0]] |
|
if Path(scripts[0]).is_symlink(): |
|
|
|
scripts.append(os.path.realpath(scripts[0])) |
|
|
|
for script in scripts: |
|
bindir = str(Path(script).parent) |
|
if Path(bindir).is_dir() and os.access(script, os.X_OK): |
|
|
|
|
|
path_list.insert(0, bindir) |
|
return path_list |
|
|
|
|
|
def _evaluate_argcomplete(parser: JupyterParser) -> list[str]: |
|
"""If argcomplete is enabled, trigger autocomplete or return current words |
|
|
|
If the first word looks like a subcommand, return the current command |
|
that is attempting to be completed so that the subcommand can evaluate it; |
|
otherwise auto-complete using the main parser. |
|
""" |
|
try: |
|
|
|
|
|
from traitlets.config.argcomplete_config import ( |
|
get_argcomplete_cwords, |
|
increment_argcomplete_index, |
|
) |
|
|
|
cwords = get_argcomplete_cwords() |
|
if cwords and len(cwords) > 1 and not cwords[1].startswith("-"): |
|
|
|
|
|
increment_argcomplete_index() |
|
return cwords |
|
|
|
parser.argcomplete() |
|
except ImportError: |
|
|
|
|
|
parser.argcomplete() |
|
msg = "Control flow should not reach end of autocomplete()" |
|
raise AssertionError(msg) |
|
|
|
|
|
def main() -> None: |
|
"""The command entry point.""" |
|
parser = jupyter_parser() |
|
argv = sys.argv |
|
subcommand = None |
|
if "_ARGCOMPLETE" in os.environ: |
|
argv = _evaluate_argcomplete(parser) |
|
subcommand = argv[1] |
|
elif len(argv) > 1 and not argv[1].startswith("-"): |
|
|
|
|
|
subcommand = argv[1] |
|
else: |
|
args, opts = parser.parse_known_args() |
|
subcommand = args.subcommand |
|
if args.version: |
|
print("Selected Jupyter core packages...") |
|
for package in [ |
|
"IPython", |
|
"ipykernel", |
|
"ipywidgets", |
|
"jupyter_client", |
|
"jupyter_core", |
|
"jupyter_server", |
|
"jupyterlab", |
|
"nbclient", |
|
"nbconvert", |
|
"nbformat", |
|
"notebook", |
|
"qtconsole", |
|
"traitlets", |
|
]: |
|
try: |
|
if package == "jupyter_core": |
|
version = __version__ |
|
else: |
|
mod = __import__(package) |
|
version = mod.__version__ |
|
except ImportError: |
|
version = "not installed" |
|
print(f"{package:<17}:", version) |
|
return |
|
if args.json and not args.paths: |
|
sys.exit("--json is only used with --paths") |
|
if args.debug and not args.paths: |
|
sys.exit("--debug is only used with --paths") |
|
if args.debug and args.json: |
|
sys.exit("--debug cannot be used with --json") |
|
if args.config_dir: |
|
print(paths.jupyter_config_dir()) |
|
return |
|
if args.data_dir: |
|
print(paths.jupyter_data_dir()) |
|
return |
|
if args.runtime_dir: |
|
print(paths.jupyter_runtime_dir()) |
|
return |
|
if args.paths: |
|
data = {} |
|
data["runtime"] = [paths.jupyter_runtime_dir()] |
|
data["config"] = paths.jupyter_config_path() |
|
data["data"] = paths.jupyter_path() |
|
if args.json: |
|
print(json.dumps(data)) |
|
else: |
|
if args.debug: |
|
env = os.environ |
|
|
|
if paths.use_platform_dirs(): |
|
print( |
|
"JUPYTER_PLATFORM_DIRS is set to a true value, so we use platformdirs to find platform-specific directories" |
|
) |
|
else: |
|
print( |
|
"JUPYTER_PLATFORM_DIRS is set to a false value, or is not set, so we use hardcoded legacy paths for platform-specific directories" |
|
) |
|
|
|
if paths.prefer_environment_over_user(): |
|
print( |
|
"JUPYTER_PREFER_ENV_PATH is set to a true value, or JUPYTER_PREFER_ENV_PATH is not set and we detected a virtual environment, making the environment-level path preferred over the user-level path for data and config" |
|
) |
|
else: |
|
print( |
|
"JUPYTER_PREFER_ENV_PATH is set to a false value, or JUPYTER_PREFER_ENV_PATH is not set and we did not detect a virtual environment, making the user-level path preferred over the environment-level path for data and config" |
|
) |
|
|
|
|
|
if env.get("JUPYTER_NO_CONFIG"): |
|
print( |
|
"JUPYTER_NO_CONFIG is set, making the config path list only a single temporary directory" |
|
) |
|
else: |
|
print( |
|
"JUPYTER_NO_CONFIG is not set, so we use the full path list for config" |
|
) |
|
|
|
if env.get("JUPYTER_CONFIG_PATH"): |
|
print( |
|
f"JUPYTER_CONFIG_PATH is set to '{env.get('JUPYTER_CONFIG_PATH')}', which is prepended to the config path list (unless JUPYTER_NO_CONFIG is set)" |
|
) |
|
else: |
|
print( |
|
"JUPYTER_CONFIG_PATH is not set, so we do not prepend anything to the config paths" |
|
) |
|
|
|
if env.get("JUPYTER_CONFIG_DIR"): |
|
print( |
|
f"JUPYTER_CONFIG_DIR is set to '{env.get('JUPYTER_CONFIG_DIR')}', overriding the default user-level config directory" |
|
) |
|
else: |
|
print( |
|
"JUPYTER_CONFIG_DIR is not set, so we use the default user-level config directory" |
|
) |
|
|
|
if site.ENABLE_USER_SITE: |
|
print( |
|
f"Python's site.ENABLE_USER_SITE is True, so we add the user site directory '{site.getuserbase()}'" |
|
) |
|
else: |
|
print( |
|
f"Python's site.ENABLE_USER_SITE is not True, so we do not add the Python site user directory '{site.getuserbase()}'" |
|
) |
|
|
|
|
|
if env.get("JUPYTER_PATH"): |
|
print( |
|
f"JUPYTER_PATH is set to '{env.get('JUPYTER_PATH')}', which is prepended to the data paths" |
|
) |
|
else: |
|
print( |
|
"JUPYTER_PATH is not set, so we do not prepend anything to the data paths" |
|
) |
|
|
|
if env.get("JUPYTER_DATA_DIR"): |
|
print( |
|
f"JUPYTER_DATA_DIR is set to '{env.get('JUPYTER_DATA_DIR')}', overriding the default user-level data directory" |
|
) |
|
else: |
|
print( |
|
"JUPYTER_DATA_DIR is not set, so we use the default user-level data directory" |
|
) |
|
|
|
|
|
if env.get("JUPYTER_RUNTIME_DIR"): |
|
print( |
|
f"JUPYTER_RUNTIME_DIR is set to '{env.get('JUPYTER_RUNTIME_DIR')}', overriding the default runtime directory" |
|
) |
|
else: |
|
print( |
|
"JUPYTER_RUNTIME_DIR is not set, so we use the default runtime directory" |
|
) |
|
|
|
print() |
|
|
|
for name in sorted(data): |
|
path = data[name] |
|
print("%s:" % name) |
|
for p in path: |
|
print(" " + p) |
|
return |
|
|
|
if not subcommand: |
|
parser.print_help(file=sys.stderr) |
|
sys.exit("\nPlease specify a subcommand or one of the optional arguments.") |
|
|
|
try: |
|
command = _jupyter_abspath(subcommand) |
|
except Exception as e: |
|
parser.print_help(file=sys.stderr) |
|
|
|
if subcommand == "help": |
|
return |
|
sys.exit(str(e)) |
|
|
|
try: |
|
_execvp(command, [command] + argv[2:]) |
|
except OSError as e: |
|
sys.exit(f"Error executing Jupyter command {subcommand!r}: {e}") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |
|
|