| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- # 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:
- """Add subcommands to epilog on request
- Avoids searching PATH for subcommands unless help output is requested.
- """
- subcommands: str = " ".join(list_subcommands())
- return f"Available subcommands: {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 # noqa: PLC0415
- 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:
- bin_paths = list(Path(d).iterdir())
- except OSError:
- continue
- for path in bin_paths:
- name = path.name
- if name.startswith("jupyter-"):
- if sys.platform.startswith("win"):
- # remove file-extension on Windows
- name = path.stem
- 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:
- msg = f"{cmd!r} not found"
- raise OSError(msg, 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 # noqa: PLC0415
- 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 ( # noqa: PLC0415
- 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(f"{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()
|