first comit

This commit is contained in:
2024-02-23 10:30:02 +00:00
commit ddeb07d0ba
12482 changed files with 1857507 additions and 0 deletions

View File

@@ -0,0 +1,3 @@
from __future__ import annotations
from .version import __version__, version_info # noqa: F401

View File

@@ -0,0 +1,6 @@
"""Launch the root jupyter command"""
from __future__ import annotations
from .command import main
main()

View File

@@ -0,0 +1,321 @@
"""
A base Application class for Jupyter applications.
All Jupyter applications should inherit from this.
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations
import logging
import os
import sys
import typing as t
from copy import deepcopy
from pathlib import Path
from shutil import which
from traitlets import Bool, List, Unicode, observe
from traitlets.config.application import Application, catch_config_error
from traitlets.config.loader import ConfigFileNotFound
from .paths import (
allow_insecure_writes,
issue_insecure_write_warning,
jupyter_config_dir,
jupyter_config_path,
jupyter_data_dir,
jupyter_path,
jupyter_runtime_dir,
)
from .utils import ensure_dir_exists, ensure_event_loop
# mypy: disable-error-code="no-untyped-call"
# aliases and flags
base_aliases: dict[str, t.Any] = {}
if isinstance(Application.aliases, dict):
# traitlets 5
base_aliases.update(Application.aliases)
_jupyter_aliases = {
"log-level": "Application.log_level",
"config": "JupyterApp.config_file",
}
base_aliases.update(_jupyter_aliases)
base_flags: dict[str, t.Any] = {}
if isinstance(Application.flags, dict):
# traitlets 5
base_flags.update(Application.flags)
_jupyter_flags: dict[str, t.Any] = {
"debug": (
{"Application": {"log_level": logging.DEBUG}},
"set log level to logging.DEBUG (maximize logging output)",
),
"generate-config": ({"JupyterApp": {"generate_config": True}}, "generate default config file"),
"y": (
{"JupyterApp": {"answer_yes": True}},
"Answer yes to any questions instead of prompting.",
),
}
base_flags.update(_jupyter_flags)
class NoStart(Exception):
"""Exception to raise when an application shouldn't start"""
class JupyterApp(Application):
"""Base class for Jupyter applications"""
name = "jupyter" # override in subclasses
description = "A Jupyter Application"
aliases = base_aliases
flags = base_flags
def _log_level_default(self) -> int:
return logging.INFO
jupyter_path = List(Unicode())
def _jupyter_path_default(self) -> list[str]:
return jupyter_path()
config_dir = Unicode()
def _config_dir_default(self) -> str:
return jupyter_config_dir()
@property
def config_file_paths(self) -> list[str]:
path = jupyter_config_path()
if self.config_dir not in path:
# Insert config dir as first item.
path.insert(0, self.config_dir)
return path
data_dir = Unicode()
def _data_dir_default(self) -> str:
d = jupyter_data_dir()
ensure_dir_exists(d, mode=0o700)
return d
runtime_dir = Unicode()
def _runtime_dir_default(self) -> str:
rd = jupyter_runtime_dir()
ensure_dir_exists(rd, mode=0o700)
return rd
@observe("runtime_dir")
def _runtime_dir_changed(self, change: t.Any) -> None:
ensure_dir_exists(change["new"], mode=0o700)
generate_config = Bool(False, config=True, help="""Generate default config file.""")
config_file_name = Unicode(config=True, help="Specify a config file to load.")
def _config_file_name_default(self) -> str:
if not self.name:
return ""
return self.name.replace("-", "_") + "_config"
config_file = Unicode(
config=True,
help="""Full path of a config file.""",
)
answer_yes = Bool(False, config=True, help="""Answer yes to any prompts.""")
def write_default_config(self) -> None:
"""Write our default config to a .py config file"""
if self.config_file:
config_file = self.config_file
else:
config_file = str(Path(self.config_dir, self.config_file_name + ".py"))
if Path(config_file).exists() and not self.answer_yes:
answer = ""
def ask() -> str:
prompt = "Overwrite %s with default config? [y/N]" % config_file
try:
return input(prompt).lower() or "n"
except KeyboardInterrupt:
print("") # empty line
return "n"
answer = ask()
while not answer.startswith(("y", "n")):
print("Please answer 'yes' or 'no'")
answer = ask()
if answer.startswith("n"):
return
config_text = self.generate_config_file()
print("Writing default config to: %s" % config_file)
ensure_dir_exists(Path(config_file).parent.resolve(), 0o700)
with Path.open(Path(config_file), mode="w", encoding="utf-8") as f:
f.write(config_text)
def migrate_config(self) -> None:
"""Migrate config/data from IPython 3"""
try: # let's see if we can open the marker file
# for reading and updating (writing)
f_marker = Path.open(Path(self.config_dir, "migrated"), "r+")
except FileNotFoundError: # cannot find the marker file
pass # that means we have not migrated yet, so continue
except OSError: # not readable and/or writable
return # so let's give up migration in such an environment
else: # if we got here without raising anything,
# that means the file exists
f_marker.close()
return # so we must have already migrated -> bail out
from .migrate import get_ipython_dir, migrate
# No IPython dir, nothing to migrate
if not Path(get_ipython_dir()).exists():
return
migrate()
def load_config_file(self, suppress_errors: bool = True) -> None: # type:ignore[override]
"""Load the config file.
By default, errors in loading config are handled, and a warning
printed on screen. For testing, the suppress_errors option is set
to False, so errors will make tests fail.
"""
self.log.debug("Searching %s for config files", self.config_file_paths)
base_config = "jupyter_config"
try:
super().load_config_file(
base_config,
path=self.config_file_paths,
)
except ConfigFileNotFound:
# ignore errors loading parent
self.log.debug("Config file %s not found", base_config)
if self.config_file:
path, config_file_name = os.path.split(self.config_file)
else:
path = self.config_file_paths # type:ignore[assignment]
config_file_name = self.config_file_name
if not config_file_name or (config_file_name == base_config):
return
try:
super().load_config_file(config_file_name, path=path)
except ConfigFileNotFound:
self.log.debug("Config file not found, skipping: %s", config_file_name)
except Exception:
# Reraise errors for testing purposes, or if set in
# self.raise_config_file_errors
if (not suppress_errors) or self.raise_config_file_errors:
raise
self.log.warning("Error loading config file: %s", config_file_name, exc_info=True)
# subcommand-related
def _find_subcommand(self, name: str) -> str:
name = f"{self.name}-{name}"
return which(name) or ""
@property
def _dispatching(self) -> bool:
"""Return whether we are dispatching to another command
or running ourselves.
"""
return bool(self.generate_config or self.subapp or self.subcommand)
subcommand = Unicode()
@catch_config_error
def initialize(self, argv: t.Any = None) -> None:
"""Initialize the application."""
# don't hook up crash handler before parsing command-line
if argv is None:
argv = sys.argv[1:]
if argv:
subc = self._find_subcommand(argv[0])
if subc:
self.argv = argv
self.subcommand = subc
return
self.parse_command_line(argv)
cl_config = deepcopy(self.config)
if self._dispatching:
return
self.migrate_config()
self.load_config_file()
# enforce cl-opts override configfile opts:
self.update_config(cl_config)
if allow_insecure_writes:
issue_insecure_write_warning()
def start(self) -> None:
"""Start the whole thing"""
if self.subcommand:
os.execv(self.subcommand, [self.subcommand] + self.argv[1:]) # noqa: S606
raise NoStart()
if self.subapp:
self.subapp.start()
raise NoStart()
if self.generate_config:
self.write_default_config()
raise NoStart()
@classmethod
def launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None:
"""Launch an instance of a Jupyter Application"""
# Ensure an event loop is set before any other code runs.
loop = ensure_event_loop()
try:
super().launch_instance(argv=argv, **kwargs)
except NoStart:
return
loop.close()
class JupyterAsyncApp(JupyterApp):
"""A Jupyter application that runs on an asyncio loop."""
name = "jupyter_async" # override in subclasses
description = "An Async Jupyter Application"
# Set to True for tornado-based apps.
_prefer_selector_loop = False
async def initialize_async(self, argv: t.Any = None) -> None:
"""Initialize the application asynchronoously."""
async def start_async(self) -> None:
"""Run the application in an event loop."""
@classmethod
async def _launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None:
app = cls.instance(**kwargs)
app.initialize(argv)
await app.initialize_async(argv)
await app.start_async()
@classmethod
def launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None:
"""Launch an instance of an async Jupyter Application"""
loop = ensure_event_loop(cls._prefer_selector_loop)
coro = cls._launch_instance(argv, **kwargs)
loop.run_until_complete(coro)
loop.close()
if __name__ == "__main__":
JupyterApp.launch_instance()

View File

@@ -0,0 +1,405 @@
# 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()

View File

@@ -0,0 +1,279 @@
# PYTHON_ARGCOMPLETE_OK
"""Migrating IPython < 4.0 to Jupyter
This *copies* configuration and resources to their new locations in Jupyter
Migrations:
- .ipython/
- nbextensions -> JUPYTER_DATA_DIR/nbextensions
- kernels -> JUPYTER_DATA_DIR/kernels
- .ipython/profile_default/
- static/custom -> .jupyter/custom
- nbconfig -> .jupyter/nbconfig
- security/
- notebook_secret, notebook_cookie_secret, nbsignatures.db -> JUPYTER_DATA_DIR
- ipython_{notebook,nbconvert,qtconsole}_config.py -> .jupyter/jupyter_{name}_config.py
"""
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations
import os
import re
import shutil
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
from traitlets.config.loader import JSONFileConfigLoader, PyFileConfigLoader
from traitlets.log import get_logger
from .application import JupyterApp
from .paths import jupyter_config_dir, jupyter_data_dir
from .utils import ensure_dir_exists
# mypy: disable-error-code="no-untyped-call"
migrations = {
str(Path("{ipython_dir}", "nbextensions")): str(Path("{jupyter_data}", "nbextensions")),
str(Path("{ipython_dir}", "kernels")): str(Path("{jupyter_data}", "kernels")),
str(Path("{profile}", "nbconfig")): str(Path("{jupyter_config}", "nbconfig")),
}
custom_src_t = str(Path("{profile}", "static", "custom"))
custom_dst_t = str(Path("{jupyter_config}", "custom"))
for security_file in ("notebook_secret", "notebook_cookie_secret", "nbsignatures.db"):
src = str(Path("{profile}", "security", security_file))
dst = str(Path("{jupyter_data}", security_file))
migrations[src] = dst
config_migrations = ["notebook", "nbconvert", "qtconsole"]
regex = re.compile
config_substitutions = {
regex(r"\bIPythonQtConsoleApp\b"): "JupyterQtConsoleApp",
regex(r"\bIPythonWidget\b"): "JupyterWidget",
regex(r"\bRichIPythonWidget\b"): "RichJupyterWidget",
regex(r"\bIPython\.html\b"): "notebook",
regex(r"\bIPython\.nbconvert\b"): "nbconvert",
}
def get_ipython_dir() -> str:
"""Return the IPython directory location.
Not imported from IPython because the IPython implementation
ensures that a writable directory exists,
creating a temporary directory if not.
We don't want to trigger that when checking if migration should happen.
We only need to support the IPython < 4 behavior for migration,
so importing for forward-compatibility and edge cases is not important.
"""
return os.environ.get("IPYTHONDIR", str(Path("~/.ipython").expanduser()))
def migrate_dir(src: str, dst: str) -> bool:
"""Migrate a directory from src to dst"""
log = get_logger()
if not os.listdir(src):
log.debug("No files in %s", src)
return False
if Path(dst).exists():
if os.listdir(dst):
# already exists, non-empty
log.debug("%s already exists", dst)
return False
Path(dst).rmdir()
log.info("Copying %s -> %s", src, dst)
ensure_dir_exists(Path(dst).parent)
shutil.copytree(src, dst, symlinks=True)
return True
def migrate_file(src: str | Path, dst: str | Path, substitutions: Any = None) -> bool:
"""Migrate a single file from src to dst
substitutions is an optional dict of {regex: replacement} for performing replacements on the file.
"""
log = get_logger()
if Path(dst).exists():
# already exists
log.debug("%s already exists", dst)
return False
log.info("Copying %s -> %s", src, dst)
ensure_dir_exists(Path(dst).parent)
shutil.copy(src, dst)
if substitutions:
with Path.open(Path(dst), encoding="utf-8") as f:
text = f.read()
for pat, replacement in substitutions.items():
text = pat.sub(replacement, text)
with Path.open(Path(dst), "w", encoding="utf-8") as f:
f.write(text)
return True
def migrate_one(src: str, dst: str) -> bool:
"""Migrate one item
dispatches to migrate_dir/_file
"""
log = get_logger()
if Path(src).is_file():
return migrate_file(src, dst)
if Path(src).is_dir():
return migrate_dir(src, dst)
log.debug("Nothing to migrate for %s", src)
return False
def migrate_static_custom(src: str, dst: str) -> bool:
"""Migrate non-empty custom.js,css from src to dst
src, dst are 'custom' directories containing custom.{js,css}
"""
log = get_logger()
migrated = False
custom_js = Path(src, "custom.js")
custom_css = Path(src, "custom.css")
# check if custom_js is empty:
custom_js_empty = True
if Path(custom_js).is_file():
with Path.open(custom_js, encoding="utf-8") as f:
js = f.read().strip()
for line in js.splitlines():
if not (line.isspace() or line.strip().startswith(("/*", "*", "//"))):
custom_js_empty = False
break
# check if custom_css is empty:
custom_css_empty = True
if Path(custom_css).is_file():
with Path.open(custom_css, encoding="utf-8") as f:
css = f.read().strip()
custom_css_empty = css.startswith("/*") and css.endswith("*/")
if custom_js_empty:
log.debug("Ignoring empty %s", custom_js)
if custom_css_empty:
log.debug("Ignoring empty %s", custom_css)
if custom_js_empty and custom_css_empty:
# nothing to migrate
return False
ensure_dir_exists(dst)
if not custom_js_empty or not custom_css_empty:
ensure_dir_exists(dst)
if not custom_js_empty and migrate_file(custom_js, Path(dst, "custom.js")):
migrated = True
if not custom_css_empty and migrate_file(custom_css, Path(dst, "custom.css")):
migrated = True
return migrated
def migrate_config(name: str, env: Any) -> list[Any]:
"""Migrate a config file.
Includes substitutions for updated configurable names.
"""
log = get_logger()
src_base = str(Path(f"{env['profile']}", f"ipython_{name}_config"))
dst_base = str(Path(f"{env['jupyter_config']}", f"jupyter_{name}_config"))
loaders = {
".py": PyFileConfigLoader,
".json": JSONFileConfigLoader,
}
migrated = []
for ext in (".py", ".json"):
src = src_base + ext
dst = dst_base + ext
if Path(src).exists():
cfg = loaders[ext](src).load_config()
if cfg:
if migrate_file(src, dst, substitutions=config_substitutions):
migrated.append(src)
else:
# don't migrate empty config files
log.debug("Not migrating empty config file: %s", src)
return migrated
def migrate() -> bool:
"""Migrate IPython configuration to Jupyter"""
env = {
"jupyter_data": jupyter_data_dir(),
"jupyter_config": jupyter_config_dir(),
"ipython_dir": get_ipython_dir(),
"profile": str(Path(get_ipython_dir(), "profile_default")),
}
migrated = False
for src_t, dst_t in migrations.items():
src = src_t.format(**env)
dst = dst_t.format(**env)
if Path(src).exists() and migrate_one(src, dst):
migrated = True
for name in config_migrations:
if migrate_config(name, env):
migrated = True
custom_src = custom_src_t.format(**env)
custom_dst = custom_dst_t.format(**env)
if Path(custom_src).exists() and migrate_static_custom(custom_src, custom_dst):
migrated = True
# write a marker to avoid re-running migration checks
ensure_dir_exists(env["jupyter_config"])
with Path.open(Path(env["jupyter_config"], "migrated"), "w", encoding="utf-8") as f:
f.write(datetime.now(tz=timezone.utc).isoformat())
return migrated
class JupyterMigrate(JupyterApp):
"""A Jupyter Migration App."""
name = "jupyter-migrate"
description = """
Migrate configuration and data from .ipython prior to 4.0 to Jupyter locations.
This migrates:
- config files in the default profile
- kernels in ~/.ipython/kernels
- notebook javascript extensions in ~/.ipython/extensions
- custom.js/css to .jupyter/custom
to their new Jupyter locations.
All files are copied, not moved.
If the destinations already exist, nothing will be done.
"""
def start(self) -> None:
"""Start the application."""
if not migrate():
self.log.info("Found nothing to migrate.")
main = JupyterMigrate.launch_instance
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,110 @@
#!/usr/bin/env python
"""
display environment information that is frequently
used to troubleshoot installations of Jupyter or IPython
"""
from __future__ import annotations
import os
import platform
import subprocess
import sys
from typing import Any, Optional, Union
def subs(cmd: Union[list[str], str]) -> Optional[str]:
"""
get data from commands that we need to run outside of python
"""
try:
stdout = subprocess.check_output(cmd) # noqa: S603
return stdout.decode("utf-8", "replace").strip()
except (OSError, subprocess.CalledProcessError):
return None
def get_data() -> dict[str, Any]:
"""
returns a dict of various user environment data
"""
env: dict[str, Any] = {}
env["path"] = os.environ.get("PATH")
env["sys_path"] = sys.path
env["sys_exe"] = sys.executable
env["sys_version"] = sys.version
env["platform"] = platform.platform()
# FIXME: which on Windows?
if sys.platform == "win32":
env["where"] = subs(["where", "jupyter"])
env["which"] = None
else:
env["which"] = subs(["which", "-a", "jupyter"])
env["where"] = None
env["pip"] = subs([sys.executable, "-m", "pip", "list"])
env["conda"] = subs(["conda", "list"])
env["conda-env"] = subs(["conda", "env", "export"])
return env
def main() -> None:
"""
print out useful info
"""
# pylint: disable=superfluous-parens
# args = get_args()
if "_ARGCOMPLETE" in os.environ:
# No arguments to complete, the script can be slow to run to completion,
# so in case someone tries to complete jupyter troubleshoot just exit early
return
environment_data = get_data()
print("$PATH:")
for directory in environment_data["path"].split(os.pathsep):
print(f"\t{directory}")
print("\nsys.path:")
for directory in environment_data["sys_path"]:
print(f"\t{directory}")
print("\nsys.executable:")
print(f'\t{environment_data["sys_exe"]}')
print("\nsys.version:")
if "\n" in environment_data["sys_version"]:
for data in environment_data["sys_version"].split("\n"):
print(f"\t{data}")
else:
print(f'\t{environment_data["sys_version"]}')
print("\nplatform.platform():")
print(f'\t{environment_data["platform"]}')
if environment_data["which"]:
print("\nwhich -a jupyter:")
for line in environment_data["which"].split("\n"):
print(f"\t{line}")
if environment_data["where"]:
print("\nwhere jupyter:")
for line in environment_data["where"].split("\n"):
print(f"\t{line}")
if environment_data["pip"]:
print("\npip list:")
for package in environment_data["pip"].split("\n"):
print(f"\t{package}")
if environment_data["conda"]:
print("\nconda list:")
for package in environment_data["conda"].split("\n"):
print(f"\t{package}")
if environment_data["conda-env"]:
print("\nconda env:")
for package in environment_data["conda-env"].split("\n"):
print(f"\t{package}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,206 @@
# Copyright (c) Jupyter Development Team.
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations
import asyncio
import atexit
import errno
import inspect
import sys
import threading
import warnings
from contextvars import ContextVar
from pathlib import Path
from types import FrameType
from typing import Any, Awaitable, Callable, TypeVar, cast
def ensure_dir_exists(path: str | Path, mode: int = 0o777) -> None:
"""Ensure that a directory exists
If it doesn't exist, try to create it, protecting against a race condition
if another process is doing the same.
The default permissions are determined by the current umask.
"""
try:
Path(path).mkdir(parents=True, mode=mode)
except OSError as e:
if e.errno != errno.EEXIST:
raise
if not Path(path).is_dir():
raise OSError("%r exists but is not a directory" % path)
def _get_frame(level: int) -> FrameType | None:
"""Get the frame at the given stack level."""
# sys._getframe is much faster than inspect.stack, but isn't guaranteed to
# exist in all python implementations, so we fall back to inspect.stack()
# We need to add one to level to account for this get_frame call.
if hasattr(sys, "_getframe"):
frame = sys._getframe(level + 1)
else:
frame = inspect.stack(context=0)[level + 1].frame
return frame
# This function is from https://github.com/python/cpython/issues/67998
# (https://bugs.python.org/file39550/deprecated_module_stacklevel.diff) and
# calculates the appropriate stacklevel for deprecations to target the
# deprecation for the caller, no matter how many internal stack frames we have
# added in the process. For example, with the deprecation warning in the
# __init__ below, the appropriate stacklevel will change depending on how deep
# the inheritance hierarchy is.
def _external_stacklevel(internal: list[str]) -> int:
"""Find the stacklevel of the first frame that doesn't contain any of the given internal strings
The depth will be 1 at minimum in order to start checking at the caller of
the function that called this utility method.
"""
# Get the level of my caller's caller
level = 2
frame = _get_frame(level)
# Normalize the path separators:
normalized_internal = [str(Path(s)) for s in internal]
# climb the stack frames while we see internal frames
while frame and any(s in str(Path(frame.f_code.co_filename)) for s in normalized_internal):
level += 1
frame = frame.f_back
# Return the stack level from the perspective of whoever called us (i.e., one level up)
return level - 1
def deprecation(message: str, internal: str | list[str] = "jupyter_core/") -> None:
"""Generate a deprecation warning targeting the first frame that is not 'internal'
internal is a string or list of strings, which if they appear in filenames in the
frames, the frames will be considered internal. Changing this can be useful if, for example,
we know that our internal code is calling out to another library.
"""
_internal: list[str]
_internal = [internal] if isinstance(internal, str) else internal
# stack level of the first external frame from here
stacklevel = _external_stacklevel(_internal)
# The call to .warn adds one frame, so bump the stacklevel up by one
warnings.warn(message, DeprecationWarning, stacklevel=stacklevel + 1)
T = TypeVar("T")
class _TaskRunner:
"""A task runner that runs an asyncio event loop on a background thread."""
def __init__(self) -> None:
self.__io_loop: asyncio.AbstractEventLoop | None = None
self.__runner_thread: threading.Thread | None = None
self.__lock = threading.Lock()
atexit.register(self._close)
def _close(self) -> None:
if self.__io_loop:
self.__io_loop.stop()
def _runner(self) -> None:
loop = self.__io_loop
assert loop is not None
try:
loop.run_forever()
finally:
loop.close()
def run(self, coro: Any) -> Any:
"""Synchronously run a coroutine on a background thread."""
with self.__lock:
name = f"{threading.current_thread().name} - runner"
if self.__io_loop is None:
self.__io_loop = asyncio.new_event_loop()
self.__runner_thread = threading.Thread(target=self._runner, daemon=True, name=name)
self.__runner_thread.start()
fut = asyncio.run_coroutine_threadsafe(coro, self.__io_loop)
return fut.result(None)
_runner_map: dict[str, _TaskRunner] = {}
_loop: ContextVar[asyncio.AbstractEventLoop | None] = ContextVar("_loop", default=None)
def run_sync(coro: Callable[..., Awaitable[T]]) -> Callable[..., T]:
"""Wraps coroutine in a function that blocks until it has executed.
Parameters
----------
coro : coroutine-function
The coroutine-function to be executed.
Returns
-------
result :
Whatever the coroutine-function returns.
"""
if not inspect.iscoroutinefunction(coro):
raise AssertionError
def wrapped(*args: Any, **kwargs: Any) -> Any:
name = threading.current_thread().name
inner = coro(*args, **kwargs)
try:
# If a loop is currently running in this thread,
# use a task runner.
asyncio.get_running_loop()
if name not in _runner_map:
_runner_map[name] = _TaskRunner()
return _runner_map[name].run(inner)
except RuntimeError:
pass
# Run the loop for this thread.
loop = ensure_event_loop()
return loop.run_until_complete(inner)
wrapped.__doc__ = coro.__doc__
return wrapped
def ensure_event_loop(prefer_selector_loop: bool = False) -> asyncio.AbstractEventLoop:
# Get the loop for this thread, or create a new one.
loop = _loop.get()
if loop is not None and not loop.is_closed():
return loop
try:
loop = asyncio.get_running_loop()
except RuntimeError:
if sys.platform == "win32" and prefer_selector_loop:
loop = asyncio.WindowsSelectorEventLoopPolicy().new_event_loop()
else:
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
_loop.set(loop)
return loop
async def ensure_async(obj: Awaitable[T] | T) -> T:
"""Convert a non-awaitable object to a coroutine if needed,
and await it if it was not already awaited.
This function is meant to be called on the result of calling a function,
when that function could either be asynchronous or not.
"""
if inspect.isawaitable(obj):
obj = cast(Awaitable[T], obj)
try:
result = await obj
except RuntimeError as e:
if str(e) == "cannot reuse already awaited coroutine":
# obj is already the coroutine's result
return cast(T, obj)
raise
return result
# obj doesn't need to be awaited
return cast(T, obj)

View File

@@ -0,0 +1,18 @@
"""
store the current version info of the jupyter_core.
"""
from __future__ import annotations
import re
# Version string must appear intact for hatch versioning
__version__ = "5.7.1"
# Build up version_info tuple for backwards compatibility
pattern = r"(?P<major>\d+).(?P<minor>\d+).(?P<patch>\d+)(?P<rest>.*)"
match = re.match(pattern, __version__)
assert match is not None
parts: list[object] = [int(match[part]) for part in ["major", "minor", "patch"]]
if match["rest"]:
parts.append(match["rest"])
version_info = tuple(parts)