406 lines
15 KiB
Python
406 lines
15 KiB
Python
# PYTHON_ARGCOMPLETE_OK
|
|
"""The root `jupyter` command.
|
|
|
|
This does nothing other than dispatch to subcommands or output path info.
|
|
"""
|
|
|
|
# Copyright (c) Jupyter Development Team.
|
|
# Distributed under the terms of the Modified BSD License.
|
|
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 # type: ignore[import-not-found]
|
|
|
|
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)
|
|
# don't use argparse's version action because it prints to stderr on py2
|
|
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"
|
|
)
|
|
# For argcomplete, supply all known subcommands
|
|
subcommand_action.completer = lambda *args, **kwargs: list_subcommands() # type: ignore[attr-defined] # noqa: ARG005
|
|
|
|
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()
|
|
# construct a set of `('foo', 'bar') from `jupyter-foo-bar`
|
|
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"):
|
|
# remove file-extension on Windows
|
|
name = os.path.splitext(name)[0] # noqa: PTH122, PLW2901
|
|
subcommand_tuples.add(tuple(name.split("-")[1:]))
|
|
# build a set of subcommand strings, excluding subcommands whose parents are defined
|
|
subcommands = set()
|
|
# Only include `jupyter-foo-bar` if `jupyter-foo` is not already present
|
|
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"):
|
|
# PATH is ignored when shell=False,
|
|
# so rely on shutil.which
|
|
cmd_path = which(cmd)
|
|
if cmd_path is None:
|
|
raise OSError("%r not found" % cmd, errno.ENOENT)
|
|
p = Popen([cmd_path] + argv[1:]) # noqa: S603
|
|
# Don't raise KeyboardInterrupt in the parent process.
|
|
# Set this after spawning, to avoid subprocess inheriting handler.
|
|
import signal
|
|
|
|
signal.signal(signal.SIGINT, signal.SIG_IGN)
|
|
p.wait()
|
|
sys.exit(p.returncode)
|
|
else:
|
|
os.execvp(cmd, argv) # noqa: S606
|
|
|
|
|
|
def _jupyter_abspath(subcommand: str) -> str:
|
|
"""This method get the abspath of a specified jupyter-subcommand with no
|
|
changes on ENV.
|
|
"""
|
|
# get env PATH with self
|
|
search_path = os.pathsep.join(_path_with_self())
|
|
# get the abs path for the jupyter-<subcommand>
|
|
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)
|
|
|
|
# Insert the "scripts" directory for this Python installation
|
|
# This allows the "jupyter" command to be relocated, while still
|
|
# finding subcommands that have been installed in the default
|
|
# location.
|
|
# We put the scripts directory at the *end* of PATH, so that
|
|
# if the user explicitly overrides a subcommand, that override
|
|
# still takes effect.
|
|
try:
|
|
bindir = sysconfig.get_path("scripts")
|
|
except KeyError:
|
|
# The Python environment does not specify a "scripts" location
|
|
pass
|
|
else:
|
|
path_list.append(bindir)
|
|
|
|
scripts = [sys.argv[0]]
|
|
if Path(scripts[0]).is_symlink():
|
|
# include realpath, if `jupyter` is a 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): # only if it's a script
|
|
# ensure executable's dir is on PATH
|
|
# avoids missing subcommands when jupyter is run via absolute path
|
|
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:
|
|
# traitlets >= 5.8 provides some argcomplete support,
|
|
# use helper methods to jump to argcomplete
|
|
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("-"):
|
|
# If first completion word looks like a subcommand,
|
|
# increment word from which to start handling arguments
|
|
increment_argcomplete_index()
|
|
return cwords
|
|
# Otherwise no subcommand, directly autocomplete and exit
|
|
parser.argcomplete()
|
|
except ImportError:
|
|
# traitlets >= 5.8 not available, just try to complete this without
|
|
# worrying about subcommands
|
|
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("-"):
|
|
# Don't parse if a subcommand is given
|
|
# Avoids argparse gobbling up args passed to subcommand, such as `-h`.
|
|
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": # We're already here
|
|
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"
|
|
)
|
|
|
|
# config path list
|
|
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()}'"
|
|
)
|
|
|
|
# data path list
|
|
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"
|
|
)
|
|
|
|
# runtime 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)
|
|
# special-case alias of "jupyter help" to "jupyter --help"
|
|
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()
|