diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6cf2307c..8da5bd87 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -33,7 +33,7 @@ repos: [mdformat-gfm, mdformat-frontmatter, mdformat-footnote] - repo: https://github.com/pre-commit/mirrors-prettier - rev: "v3.0.3" + rev: "v3.1.0" hooks: - id: prettier types_or: [yaml, html, json] @@ -45,7 +45,7 @@ repos: additional_dependencies: [black==23.7.0] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.6.1" + rev: "v1.7.1" hooks: - id: mypy files: jupyter_core @@ -67,7 +67,7 @@ repos: - id: rst-inline-touching-normal - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.5 + rev: v0.1.6 hooks: - id: ruff types_or: [python, jupyter] @@ -76,7 +76,7 @@ repos: types_or: [python, jupyter] - repo: https://github.com/scientific-python/cookie - rev: "2023.10.27" + rev: "2023.11.17" hooks: - id: sp-repo-review additional_dependencies: ["repo-review[cli]"] diff --git a/docs/conf.py b/docs/conf.py index 32b90eba..ae64ee5b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 -# # jupyter_core documentation build configuration file, created by # sphinx-quickstart on Wed Jun 24 11:51:36 2015. # @@ -11,9 +9,10 @@ # # All configuration values have a default; values that are commented out # serve to show the default. +from __future__ import annotations -import os import shutil +from pathlib import Path from jupyter_core.version import __version__, version_info @@ -39,7 +38,7 @@ ] try: - import enchant # type:ignore # noqa + import enchant # noqa: F401 extensions += ["sphinxcontrib.spelling"] except ImportError: @@ -63,7 +62,7 @@ # General information about the project. project = "jupyter_core" -copyright = "2015, Jupyter Development Team" # noqa +copyright = "2015, Jupyter Development Team" author = "Jupyter Development Team" # The version info for the project you're documenting, acts as replacement for @@ -300,6 +299,6 @@ intersphinx_mapping = {"https://docs.python.org/3/": None} -def setup(app): - here = os.path.dirname(os.path.abspath(__file__)) - shutil.copy(os.path.join(here, "..", "CHANGELOG.md"), "changelog.md") +def setup(_): + here = Path(__file__).parent.resolve() + shutil.copy(Path(here, "..", "CHANGELOG.md"), "changelog.md") diff --git a/jupyter.py b/jupyter.py index 6df921c1..852ee311 100644 --- a/jupyter.py +++ b/jupyter.py @@ -1,4 +1,6 @@ """Launch the root jupyter command""" +from __future__ import annotations + if __name__ == "__main__": from jupyter_core.command import main diff --git a/jupyter_core/__init__.py b/jupyter_core/__init__.py index d2cad7ae..e69de29b 100644 --- a/jupyter_core/__init__.py +++ b/jupyter_core/__init__.py @@ -1 +0,0 @@ -from .version import __version__, version_info # noqa diff --git a/jupyter_core/__main__.py b/jupyter_core/__main__.py index 3bebc9a3..1ebc6ea1 100644 --- a/jupyter_core/__main__.py +++ b/jupyter_core/__main__.py @@ -1,4 +1,6 @@ """Launch the root jupyter command""" +from __future__ import annotations + from .command import main main() diff --git a/jupyter_core/application.py b/jupyter_core/application.py index a3cd5de2..81d8efee 100644 --- a/jupyter_core/application.py +++ b/jupyter_core/application.py @@ -13,6 +13,7 @@ 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 @@ -62,7 +63,7 @@ base_flags.update(_jupyter_flags) -class NoStart(Exception): # noqa +class NoStart(Exception): """Exception to raise when an application shouldn't start""" @@ -135,9 +136,9 @@ def write_default_config(self) -> None: if self.config_file: config_file = self.config_file else: - config_file = os.path.join(self.config_dir, self.config_file_name + ".py") + config_file = str(Path(self.config_dir, self.config_file_name + ".py")) - if os.path.exists(config_file) and not self.answer_yes: + if Path(config_file).exists() and not self.answer_yes: answer = "" def ask() -> str: @@ -157,15 +158,15 @@ def ask() -> str: config_text = self.generate_config_file() print("Writing default config to: %s" % config_file) - ensure_dir_exists(os.path.abspath(os.path.dirname(config_file)), 0o700) - with open(config_file, mode="w", encoding="utf-8") as f: + 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 = open(os.path.join(self.config_dir, "migrated"), "r+") # noqa + f_marker = Path.open(Path(self.config_dir, "migrated"), "r+") except PermissionError: # not readable and/or writable return # so let's give up migration in such an environment except FileNotFoundError: # cannot find the marker file @@ -178,7 +179,7 @@ def migrate_config(self) -> None: from .migrate import get_ipython_dir, migrate # No IPython dir, nothing to migrate - if not os.path.exists(get_ipython_dir()): + if not Path(get_ipython_dir()).exists(): return migrate() @@ -262,7 +263,7 @@ def initialize(self, argv: t.Any = None) -> None: def start(self) -> None: """Start the whole thing""" if self.subcommand: - os.execv(self.subcommand, [self.subcommand] + self.argv[1:]) # noqa + os.execv(self.subcommand, [self.subcommand] + self.argv[1:]) # noqa: S606 raise NoStart() if self.subapp: diff --git a/jupyter_core/command.py b/jupyter_core/command.py index d65e3375..52e77422 100644 --- a/jupyter_core/command.py +++ b/jupyter_core/command.py @@ -15,6 +15,7 @@ import site import sys import sysconfig +from pathlib import Path from shutil import which from subprocess import Popen from typing import Any @@ -37,7 +38,6 @@ def epilog(self) -> str | None: @epilog.setter def epilog(self, x: Any) -> None: """Ignore epilog set in Parser.__init__""" - pass def argcomplete(self) -> None: """Trigger auto-completion, if enabled""" @@ -63,7 +63,7 @@ def jupyter_parser() -> JupyterParser: "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] + 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") @@ -98,7 +98,7 @@ def list_subcommands() -> list[str]: if name.startswith("jupyter-"): if sys.platform.startswith("win"): # remove file-extension on Windows - name = os.path.splitext(name)[0] # noqa + 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() @@ -120,7 +120,7 @@ def _execvp(cmd: str, argv: list[str]) -> None: cmd_path = which(cmd) if cmd_path is None: raise OSError("%r not found" % cmd, errno.ENOENT) - p = Popen([cmd_path] + argv[1:]) # noqa + 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 @@ -129,7 +129,7 @@ def _execvp(cmd: str, argv: list[str]) -> None: p.wait() sys.exit(p.returncode) else: - os.execvp(cmd, argv) # noqa + os.execvp(cmd, argv) # noqa: S606 def _jupyter_abspath(subcommand: str) -> str: @@ -177,13 +177,13 @@ def _path_with_self() -> list[str]: path_list.append(bindir) scripts = [sys.argv[0]] - if os.path.islink(scripts[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 = os.path.dirname(script) - if os.path.isdir(bindir) and os.access(script, os.X_OK): # only if it's a script + 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) @@ -211,9 +211,8 @@ def _evaluate_argcomplete(parser: JupyterParser) -> list[str]: # increment word from which to start handling arguments increment_argcomplete_index() return cwords - else: - # Otherwise no subcommand, directly autocomplete and exit - parser.argcomplete() + # 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 @@ -222,7 +221,7 @@ def _evaluate_argcomplete(parser: JupyterParser) -> list[str]: raise AssertionError(msg) -def main() -> None: # noqa +def main() -> None: """The command entry point.""" parser = jupyter_parser() argv = sys.argv diff --git a/jupyter_core/migrate.py b/jupyter_core/migrate.py index 1f9ea139..e70678a9 100644 --- a/jupyter_core/migrate.py +++ b/jupyter_core/migrate.py @@ -29,6 +29,7 @@ import re import shutil from datetime import datetime, timezone +from pathlib import Path from typing import Any from traitlets.config.loader import JSONFileConfigLoader, PyFileConfigLoader @@ -41,20 +42,18 @@ # mypy: disable-error-code="no-untyped-call" -pjoin = os.path.join - migrations = { - pjoin("{ipython_dir}", "nbextensions"): pjoin("{jupyter_data}", "nbextensions"), - pjoin("{ipython_dir}", "kernels"): pjoin("{jupyter_data}", "kernels"), - pjoin("{profile}", "nbconfig"): pjoin("{jupyter_config}", "nbconfig"), + 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 = pjoin("{profile}", "static", "custom") -custom_dst_t = pjoin("{jupyter_config}", "custom") +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 = pjoin("{profile}", "security", security_file) - dst = pjoin("{jupyter_data}", security_file) + src = str(Path("{profile}", "security", security_file)) + dst = str(Path("{jupyter_data}", security_file)) migrations[src] = dst config_migrations = ["notebook", "nbconvert", "qtconsole"] @@ -81,7 +80,7 @@ def get_ipython_dir() -> str: 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", os.path.expanduser("~/.ipython")) + return os.environ.get("IPYTHONDIR", str(Path("~/.ipython").expanduser())) def migrate_dir(src: str, dst: str) -> bool: @@ -90,38 +89,37 @@ def migrate_dir(src: str, dst: str) -> bool: if not os.listdir(src): log.debug("No files in %s", src) return False - if os.path.exists(dst): + if Path(dst).exists(): if os.listdir(dst): # already exists, non-empty log.debug("%s already exists", dst) return False - else: - os.rmdir(dst) + Path(dst).rmdir() log.info("Copying %s -> %s", src, dst) - ensure_dir_exists(os.path.dirname(dst)) + ensure_dir_exists(Path(dst).parent) shutil.copytree(src, dst, symlinks=True) return True -def migrate_file(src: str, dst: str, substitutions: Any = None) -> bool: +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 os.path.exists(dst): + if Path(dst).exists(): # already exists log.debug("%s already exists", dst) return False log.info("Copying %s -> %s", src, dst) - ensure_dir_exists(os.path.dirname(dst)) + ensure_dir_exists(Path(dst).parent) shutil.copy(src, dst) if substitutions: - with open(dst, encoding="utf-8") as f: + 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 open(dst, "w", encoding="utf-8") as f: + with Path.open(Path(dst), "w", encoding="utf-8") as f: f.write(text) return True @@ -132,13 +130,12 @@ def migrate_one(src: str, dst: str) -> bool: dispatches to migrate_dir/_file """ log = get_logger() - if os.path.isfile(src): + if Path(src).is_file(): return migrate_file(src, dst) - elif os.path.isdir(src): + if Path(src).is_dir(): return migrate_dir(src, dst) - else: - log.debug("Nothing to migrate for %s", src) - return False + log.debug("Nothing to migrate for %s", src) + return False def migrate_static_custom(src: str, dst: str) -> bool: @@ -149,12 +146,12 @@ def migrate_static_custom(src: str, dst: str) -> bool: log = get_logger() migrated = False - custom_js = pjoin(src, "custom.js") - custom_css = pjoin(src, "custom.css") + custom_js = Path(src, "custom.js") + custom_css = Path(src, "custom.css") # check if custom_js is empty: custom_js_empty = True - if os.path.isfile(custom_js): - with open(custom_js, encoding="utf-8") as f: + 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(("/*", "*", "//"))): @@ -163,8 +160,8 @@ def migrate_static_custom(src: str, dst: str) -> bool: # check if custom_css is empty: custom_css_empty = True - if os.path.isfile(custom_css): - with open(custom_css, encoding="utf-8") as f: + 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("*/") @@ -181,9 +178,9 @@ def migrate_static_custom(src: str, dst: str) -> bool: if not custom_js_empty or not custom_css_empty: ensure_dir_exists(dst) - if not custom_js_empty and migrate_file(custom_js, pjoin(dst, "custom.js")): + 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, pjoin(dst, "custom.css")): + if not custom_css_empty and migrate_file(custom_css, Path(dst, "custom.css")): migrated = True return migrated @@ -195,8 +192,8 @@ def migrate_config(name: str, env: Any) -> list[Any]: Includes substitutions for updated configurable names. """ log = get_logger() - src_base = pjoin("{profile}", "ipython_{name}_config").format(name=name, **env) - dst_base = pjoin("{jupyter_config}", "jupyter_{name}_config").format(name=name, **env) + 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, @@ -205,7 +202,7 @@ def migrate_config(name: str, env: Any) -> list[Any]: for ext in (".py", ".json"): src = src_base + ext dst = dst_base + ext - if os.path.exists(src): + if Path(src).exists(): cfg = loaders[ext](src).load_config() if cfg: if migrate_file(src, dst, substitutions=config_substitutions): @@ -222,13 +219,13 @@ def migrate() -> bool: "jupyter_data": jupyter_data_dir(), "jupyter_config": jupyter_config_dir(), "ipython_dir": get_ipython_dir(), - "profile": os.path.join(get_ipython_dir(), "profile_default"), + "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 os.path.exists(src) and migrate_one(src, dst): + if Path(src).exists() and migrate_one(src, dst): migrated = True for name in config_migrations: @@ -238,12 +235,12 @@ def migrate() -> bool: custom_src = custom_src_t.format(**env) custom_dst = custom_dst_t.format(**env) - if os.path.exists(custom_src) and migrate_static_custom(custom_src, custom_dst): + 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 open(os.path.join(env["jupyter_config"], "migrated"), "w", encoding="utf-8") as f: + with Path.open(Path(env["jupyter_config"], "migrated"), "w", encoding="utf-8") as f: f.write(datetime.now(tz=timezone.utc).isoformat()) return migrated diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index ebafd10c..d1f23210 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -6,7 +6,7 @@ # Derived from IPython.utils.path, which is # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. - +from __future__ import annotations import errno import os @@ -17,7 +17,7 @@ import warnings from contextlib import contextmanager from pathlib import Path -from typing import Any, Dict, Iterator, List, Optional +from typing import Any, Iterator, Optional import platformdirs @@ -63,14 +63,13 @@ def use_platform_dirs() -> bool: def get_home_dir() -> str: """Get the real path of the home directory""" - homedir = os.path.expanduser("~") + homedir = Path("~").expanduser() # Next line will make things work even when /home/ is a symlink to # /usr/home as it is on FreeBSD, for example - homedir = str(Path(homedir).resolve()) - return homedir + return str(Path(homedir).resolve()) -_dtemps: Dict[str, str] = {} +_dtemps: dict[str, str] = {} def _do_i_own(path: str) -> bool: @@ -85,7 +84,7 @@ def _do_i_own(path: str) -> bool: # not always implemented or available try: return p.owner() == os.getlogin() - except Exception: # noqa + except Exception: # noqa: S110 pass if hasattr(os, "geteuid"): @@ -174,19 +173,17 @@ def jupyter_data_dir() -> str: home = get_home_dir() if sys.platform == "darwin": - return os.path.join(home, "Library", "Jupyter") - elif os.name == "nt": + return str(Path(home, "Library", "Jupyter")) + if os.name == "nt": # type:ignore[unreachable] appdata = os.environ.get("APPDATA", None) if appdata: return str(Path(appdata, "jupyter").resolve()) - else: - return pjoin(jupyter_config_dir(), "data") - else: - # Linux, non-OS X Unix, AIX, etc. - xdg = env.get("XDG_DATA_HOME", None) - if not xdg: - xdg = pjoin(home, ".local", "share") - return pjoin(xdg, "jupyter") + return pjoin(jupyter_config_dir(), "data") + # Linux, non-OS X Unix, AIX, etc. + xdg = env.get("XDG_DATA_HOME", None) + if not xdg: + xdg = pjoin(home, ".local", "share") + return pjoin(xdg, "jupyter") def jupyter_runtime_dir() -> str: @@ -222,17 +219,17 @@ def jupyter_runtime_dir() -> str: if programdata: SYSTEM_JUPYTER_PATH = [pjoin(programdata, "jupyter")] else: # PROGRAMDATA is not defined by default on XP. - SYSTEM_JUPYTER_PATH = [os.path.join(sys.prefix, "share", "jupyter")] + SYSTEM_JUPYTER_PATH = [str(Path(sys.prefix, "share", "jupyter"))] else: SYSTEM_JUPYTER_PATH = [ "/usr/local/share/jupyter", "/usr/share/jupyter", ] -ENV_JUPYTER_PATH: List[str] = [os.path.join(sys.prefix, "share", "jupyter")] +ENV_JUPYTER_PATH: list[str] = [str(Path(sys.prefix, "share", "jupyter"))] -def jupyter_path(*subdirs: str) -> List[str]: +def jupyter_path(*subdirs: str) -> list[str]: """Return a list of directories to search for data files JUPYTER_PATH environment variable has highest priority. @@ -254,7 +251,7 @@ def jupyter_path(*subdirs: str) -> List[str]: ['~/.local/jupyter/kernels', '/usr/local/share/jupyter/kernels'] """ - paths: List[str] = [] + paths: list[str] = [] # highest priority is explicit environment variable if os.environ.get("JUPYTER_PATH"): @@ -269,7 +266,7 @@ def jupyter_path(*subdirs: str) -> List[str]: userbase = site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE if userbase: - userdir = os.path.join(userbase, "share", "jupyter") + userdir = str(Path(userbase, "share", "jupyter")) if userdir not in user: user.append(userdir) @@ -295,11 +292,11 @@ def jupyter_path(*subdirs: str) -> List[str]: SYSTEM_CONFIG_PATH = platformdirs.site_config_dir( APPNAME, appauthor=False, multipath=True ).split(os.pathsep) -else: # noqa: PLR5501 +else: if os.name == "nt": programdata = os.environ.get("PROGRAMDATA", None) - if programdata: # noqa - SYSTEM_CONFIG_PATH = [os.path.join(programdata, "jupyter")] + if programdata: # noqa: SIM108 + SYSTEM_CONFIG_PATH = [str(Path(programdata, "jupyter"))] else: # PROGRAMDATA is not defined by default on XP. SYSTEM_CONFIG_PATH = [] else: @@ -307,10 +304,10 @@ def jupyter_path(*subdirs: str) -> List[str]: "/usr/local/etc/jupyter", "/etc/jupyter", ] -ENV_CONFIG_PATH: List[str] = [os.path.join(sys.prefix, "etc", "jupyter")] +ENV_CONFIG_PATH: list[str] = [str(Path(sys.prefix, "etc", "jupyter"))] -def jupyter_config_path() -> List[str]: +def jupyter_config_path() -> list[str]: """Return the search path for Jupyter config files as a list. If the JUPYTER_PREFER_ENV_PATH environment variable is set, the @@ -324,7 +321,7 @@ def jupyter_config_path() -> List[str]: # jupyter_config_dir makes a blank config when JUPYTER_NO_CONFIG is set. return [jupyter_config_dir()] - paths: List[str] = [] + paths: list[str] = [] # highest priority is explicit environment variable if os.environ.get("JUPYTER_CONFIG_PATH"): @@ -339,7 +336,7 @@ def jupyter_config_path() -> List[str]: userbase = site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE if userbase: - userdir = os.path.join(userbase, "etc", "jupyter") + userdir = str(Path(userbase, "etc", "jupyter")) if userdir not in user: user.append(userdir) @@ -384,12 +381,12 @@ def is_file_hidden_win(abs_path: str, stat_res: Optional[Any] = None) -> bool: The result of calling stat() on abs_path. If not passed, this function will call stat() internally. """ - if os.path.basename(abs_path).startswith("."): + if Path(abs_path).name.startswith("."): return True if stat_res is None: try: - stat_res = os.stat(abs_path) + stat_res = Path(abs_path).stat() except OSError as e: if e.errno == errno.ENOENT: return False @@ -409,7 +406,6 @@ def is_file_hidden_win(abs_path: str, stat_res: Optional[Any] = None) -> bool: "hidden files are not detectable on this system, so no file will be marked as hidden.", stacklevel=2, ) - pass return False @@ -430,19 +426,19 @@ def is_file_hidden_posix(abs_path: str, stat_res: Optional[Any] = None) -> bool: The result of calling stat() on abs_path. If not passed, this function will call stat() internally. """ - if os.path.basename(abs_path).startswith("."): + if Path(abs_path).name.startswith("."): return True if stat_res is None or stat.S_ISLNK(stat_res.st_mode): try: - stat_res = os.stat(abs_path) + stat_res = Path(abs_path).stat() except OSError as e: if e.errno == errno.ENOENT: return False raise # check that dirs can be listed - if stat.S_ISDIR(stat_res.st_mode): # type:ignore[misc] # noqa + if stat.S_ISDIR(stat_res.st_mode): # noqa: SIM102 # use x-access, not actual listing, in case of slow/large listings if not os.access(abs_path, os.X_OK | os.R_OK): return True @@ -492,15 +488,15 @@ def is_hidden(abs_path: str, abs_root: str = "") -> bool: if not abs_root: abs_root = abs_path.split(os.sep, 1)[0] + os.sep inside_root = abs_path[len(abs_root) :] - if any(part.startswith(".") for part in inside_root.split(os.sep)): + if any(part.startswith(".") for part in Path(inside_root).parts): return True # check UF_HIDDEN on any location up to root. # is_file_hidden() already checked the file, so start from its parent dir - path = os.path.dirname(abs_path) + path = str(Path(abs_path).parent) while path and path.startswith(abs_root) and path != abs_root: - if not exists(path): - path = os.path.dirname(path) + if not Path(path).exists(): + path = str(Path(path).parent) continue try: # may fail on Windows junctions @@ -509,7 +505,7 @@ def is_hidden(abs_path: str, abs_root: str = "") -> bool: return True if getattr(st, "st_flags", 0) & UF_HIDDEN: return True - path = os.path.dirname(path) + path = str(Path(path).parent) return False @@ -555,9 +551,10 @@ def win32_restrict_file_to_user(fname: str) -> None: sd.SetSecurityDescriptorDacl(1, dacl, 0) win32security.SetFileSecurity(fname, win32security.DACL_SECURITY_INFORMATION, sd) + return None -def _win32_restrict_file_to_user_ctypes(fname: str) -> None: # noqa +def _win32_restrict_file_to_user_ctypes(fname: str) -> None: """Secure a windows file to read-only access for the user. Follows guidance from win32 library creator: @@ -611,7 +608,7 @@ def _win32_restrict_file_to_user_ctypes(fname: str) -> None: # noqa ) class ACL(ctypes.Structure): - _fields_ = [ # noqa + _fields_ = [ ("AclRevision", wintypes.BYTE), ("Sbz1", wintypes.BYTE), ("AclSize", wintypes.WORD), @@ -623,7 +620,7 @@ class ACL(ctypes.Structure): PACL = ctypes.POINTER(ACL) PSECURITY_DESCRIPTOR = ctypes.POINTER(wintypes.BYTE) - def _nonzero_success(result: int, func: Any, args: Any) -> Any: + def _nonzero_success(result: int, func: Any, args: Any) -> Any: # noqa: ARG001 if not result: raise ctypes.WinError(ctypes.get_last_error()) # type:ignore[attr-defined] return args @@ -950,7 +947,7 @@ def get_file_mode(fname: str) -> int: # the missing least significant bit on the third octal digit. In addition, we also tolerate # the sticky bit being set, so the lsb from the fourth octal digit is also removed. return ( - stat.S_IMODE(os.stat(fname).st_mode) & 0o6677 + stat.S_IMODE(Path(fname).stat().st_mode) & 0o6677 ) # Use 4 octal digits since S_IMODE does the same @@ -976,7 +973,7 @@ def secure_write(fname: str, binary: bool = False) -> Iterator[Any]: encoding = None if binary else "utf-8" open_flag = os.O_CREAT | os.O_WRONLY | os.O_TRUNC try: - os.remove(fname) + Path(fname).unlink() except OSError: # Skip any issues with the file not existing pass @@ -999,7 +996,7 @@ def secure_write(fname: str, binary: bool = False) -> Iterator[Any]: if os.name != "nt": # Enforce that the file got the requested permissions before writing file_mode = get_file_mode(fname) - if file_mode != 0o0600: # noqa + if file_mode != 0o0600: if allow_insecure_writes: issue_insecure_write_warning() else: @@ -1014,7 +1011,7 @@ def secure_write(fname: str, binary: bool = False) -> Iterator[Any]: def issue_insecure_write_warning() -> None: """Issue an insecure write warning.""" - def format_warning(msg: str, *args: Any, **kwargs: Any) -> str: + def format_warning(msg: str, *args: Any, **kwargs: Any) -> str: # noqa: ARG001 return str(msg) + "\n" warnings.formatwarning = format_warning # type:ignore[assignment] diff --git a/jupyter_core/troubleshoot.py b/jupyter_core/troubleshoot.py index a0a79112..5f35ff6d 100755 --- a/jupyter_core/troubleshoot.py +++ b/jupyter_core/troubleshoot.py @@ -3,30 +3,31 @@ 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, Dict, List, Optional, Union +from typing import Any, Optional, Union -def subs(cmd: Union[List[str], str]) -> Optional[str]: +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 + 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]: +def get_data() -> dict[str, Any]: """ returns a dict of various user environment data """ - env: Dict[str, Any] = {} + env: dict[str, Any] = {} env["path"] = os.environ.get("PATH") env["sys_path"] = sys.path env["sys_exe"] = sys.executable @@ -45,7 +46,7 @@ def get_data() -> Dict[str, Any]: return env -def main() -> None: # noqa +def main() -> None: """ print out useful info """ diff --git a/jupyter_core/utils/__init__.py b/jupyter_core/utils/__init__.py index 48cd9694..e8e11588 100644 --- a/jupyter_core/utils/__init__.py +++ b/jupyter_core/utils/__init__.py @@ -6,7 +6,6 @@ import atexit import errno import inspect -import os import sys import threading import warnings @@ -15,7 +14,7 @@ from typing import Any, Awaitable, Callable, TypeVar, cast -def ensure_dir_exists(path: str, mode: int = 0o777) -> None: +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 @@ -23,11 +22,11 @@ def ensure_dir_exists(path: str, mode: int = 0o777) -> None: The default permissions are determined by the current umask. """ try: - os.makedirs(path, mode=mode) + Path(path).mkdir(parents=True, mode=mode) except OSError as e: if e.errno != errno.EEXIST: raise - if not os.path.isdir(path): + if not Path(path).is_dir(): raise OSError("%r exists but is not a directory" % path) @@ -108,7 +107,7 @@ def _close(self) -> None: def _runner(self) -> None: loop = self.__io_loop - assert loop is not None # noqa + assert loop is not None try: loop.run_forever() finally: diff --git a/jupyter_core/version.py b/jupyter_core/version.py index 3198f05d..ababa9a2 100644 --- a/jupyter_core/version.py +++ b/jupyter_core/version.py @@ -1,8 +1,9 @@ """ store the current version info of the jupyter_core. """ +from __future__ import annotations + import re -from typing import List # Version string must appear intact for hatch versioning __version__ = "5.5.0" @@ -10,8 +11,8 @@ # Build up version_info tuple for backwards compatibility pattern = r"(?P\d+).(?P\d+).(?P\d+)(?P.*)" match = re.match(pattern, __version__) -assert match is not None # noqa -parts: List[object] = [int(match[part]) for part in ["major", "minor", "patch"]] +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) diff --git a/pyproject.toml b/pyproject.toml index 4a2e36c7..d7f61569 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -106,7 +106,6 @@ build = [ files = "jupyter_core" python_version = "3.8" strict = true -show_error_codes = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] warn_unreachable = true @@ -151,20 +150,39 @@ exclude_lines = [ line-length = 100 [tool.ruff.lint] -select = [ - "A", "B", "C", "DTZ", "E", "EM", "F", "FBT", "I", "ICN", "N", - "PLC", "PLE", "PLR", "PLW", "Q", "RUF", "S", "SIM", "T", "TID", "UP", - "W", "YTT", +extend-select = [ + "B", # flake8-bugbear + "I", # isort + "ARG", # flake8-unused-arguments + "C4", # flake8-comprehensions + "EM", # flake8-errmsg + "ICN", # flake8-import-conventions + "G", # flake8-logging-format + "PGH", # pygrep-hooks + "PIE", # flake8-pie + "PL", # pylint + "PT", # flake8-pytest-style + "PTH", # flake8-use-pathlib + "RET", # flake8-return + "RUF", # Ruff-specific + "SIM", # flake8-simplify + "T20", # flake8-print + "UP", # pyupgrade + "YTT", # flake8-2020 + "EXE", # flake8-executable + "NPY", # NumPy specific rules + "PD", # pandas-vet + "PYI", # flake8-pyi + "S", # flake8-bandit ] ignore = [ - # Q000 Single quotes found but double quotes preferred - "Q000", - # FBT001 Boolean positional arg in function definition - "FBT001", "FBT002", "FBT003", - # E501 Line too long (158 > 100 characters) - "E501", - # SIM105 Use `contextlib.suppress(...)` - "SIM105", + "PLR", # Design related pylint codes + "Q000", # Single quotes found but double quotes preferred + "E501", # Line too long (158 > 100 characters) + "UP007", # Use `X | Y` for type annotations" + "SIM105", # Use `contextlib.suppress(...)` + "S101", # Use of assert + "RUF012" # Mutable class attributes should be annotated ] unfixable = [ # Don't touch print statements @@ -172,6 +190,7 @@ unfixable = [ # Don't touch noqa lines "RUF100", ] +isort.required-imports = ["from __future__ import annotations"] [tool.ruff.lint.per-file-ignores] # B011 Do not call assert False since python -O removes these calls @@ -181,11 +200,9 @@ unfixable = [ # T201 `print` found # B007 Loop control variable `i` not used within the loop body. # N802 Function name `assertIn` should be lowercase -# S101 Use of `assert` detected -# S108 Probable insecure usage of temporary file or directory: "/tmp" # PLR2004 Magic value used in comparison, consider replacing b'WITNESS A' with a constant variable # S603 `subprocess` call: check for execution of untrusted input -"tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "S101", "S108", "PLR2004", "S603"] +"tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "S", "PTH", "ARG0"] # F821 Undefined name `get_config` "tests/**/profile_default/*_config.py" = ["F821"] # T201 `print` found @@ -210,6 +227,3 @@ exclude = ["docs", "tests"] [tool.check-wheel-contents] toplevel = ["jupyter_core/", "jupyter.py"] ignore = ["W002"] - -[tool.repo-review] -ignore = ["PY007", "PP308", "GH102"] diff --git a/scripts/jupyter b/scripts/jupyter index 543432b9..24168283 100755 --- a/scripts/jupyter +++ b/scripts/jupyter @@ -1,5 +1,6 @@ #!/usr/bin/env python """Launch the root jupyter command""" +from __future__ import annotations from jupyter_core.command import main diff --git a/scripts/jupyter-migrate b/scripts/jupyter-migrate index 090550db..98e65cb6 100755 --- a/scripts/jupyter-migrate +++ b/scripts/jupyter-migrate @@ -1,6 +1,7 @@ #!/usr/bin/env python # PYTHON_ARGCOMPLETE_OK """Migrate Jupyter config from IPython < 4.0""" +from __future__ import annotations from jupyter_core.migrate import main diff --git a/tests/dotipython/profile_default/ipython_config.py b/tests/dotipython/profile_default/ipython_config.py index 1c8ce115..f144b94a 100644 --- a/tests/dotipython/profile_default/ipython_config.py +++ b/tests/dotipython/profile_default/ipython_config.py @@ -1,4 +1,5 @@ # Configuration file for ipython. +from __future__ import annotations c = get_config() diff --git a/tests/dotipython/profile_default/ipython_console_config.py b/tests/dotipython/profile_default/ipython_console_config.py index f6478a04..7d0ad2f8 100644 --- a/tests/dotipython/profile_default/ipython_console_config.py +++ b/tests/dotipython/profile_default/ipython_console_config.py @@ -1,4 +1,5 @@ # Configuration file for ipython-console. +from __future__ import annotations c = get_config() diff --git a/tests/dotipython/profile_default/ipython_kernel_config.py b/tests/dotipython/profile_default/ipython_kernel_config.py index 998a5bf3..15ea12d9 100644 --- a/tests/dotipython/profile_default/ipython_kernel_config.py +++ b/tests/dotipython/profile_default/ipython_kernel_config.py @@ -1,4 +1,5 @@ # Configuration file for ipython-kernel. +from __future__ import annotations c = get_config() diff --git a/tests/dotipython/profile_default/ipython_nbconvert_config.py b/tests/dotipython/profile_default/ipython_nbconvert_config.py index edf17093..deb395da 100644 --- a/tests/dotipython/profile_default/ipython_nbconvert_config.py +++ b/tests/dotipython/profile_default/ipython_nbconvert_config.py @@ -1 +1,3 @@ +from __future__ import annotations + c.NbConvertApp.post_processors = [] diff --git a/tests/dotipython/profile_default/ipython_notebook_config.py b/tests/dotipython/profile_default/ipython_notebook_config.py index 344f638a..46fb9bf2 100644 --- a/tests/dotipython/profile_default/ipython_notebook_config.py +++ b/tests/dotipython/profile_default/ipython_notebook_config.py @@ -1 +1,3 @@ +from __future__ import annotations + c.NotebookApp.open_browser = False diff --git a/tests/dotipython_empty/profile_default/ipython_config.py b/tests/dotipython_empty/profile_default/ipython_config.py index 1c8ce115..f144b94a 100644 --- a/tests/dotipython_empty/profile_default/ipython_config.py +++ b/tests/dotipython_empty/profile_default/ipython_config.py @@ -1,4 +1,5 @@ # Configuration file for ipython. +from __future__ import annotations c = get_config() diff --git a/tests/dotipython_empty/profile_default/ipython_console_config.py b/tests/dotipython_empty/profile_default/ipython_console_config.py index f6478a04..7d0ad2f8 100644 --- a/tests/dotipython_empty/profile_default/ipython_console_config.py +++ b/tests/dotipython_empty/profile_default/ipython_console_config.py @@ -1,4 +1,5 @@ # Configuration file for ipython-console. +from __future__ import annotations c = get_config() diff --git a/tests/dotipython_empty/profile_default/ipython_kernel_config.py b/tests/dotipython_empty/profile_default/ipython_kernel_config.py index 998a5bf3..15ea12d9 100644 --- a/tests/dotipython_empty/profile_default/ipython_kernel_config.py +++ b/tests/dotipython_empty/profile_default/ipython_kernel_config.py @@ -1,4 +1,5 @@ # Configuration file for ipython-kernel. +from __future__ import annotations c = get_config() diff --git a/tests/dotipython_empty/profile_default/ipython_nbconvert_config.py b/tests/dotipython_empty/profile_default/ipython_nbconvert_config.py index d72411a8..dc22f365 100644 --- a/tests/dotipython_empty/profile_default/ipython_nbconvert_config.py +++ b/tests/dotipython_empty/profile_default/ipython_nbconvert_config.py @@ -1,4 +1,5 @@ # Configuration file for ipython-nbconvert. +from __future__ import annotations c = get_config() diff --git a/tests/dotipython_empty/profile_default/ipython_notebook_config.py b/tests/dotipython_empty/profile_default/ipython_notebook_config.py index 3aa0a304..5e432140 100644 --- a/tests/dotipython_empty/profile_default/ipython_notebook_config.py +++ b/tests/dotipython_empty/profile_default/ipython_notebook_config.py @@ -1,4 +1,5 @@ # Configuration file for ipython-notebook. +from __future__ import annotations c = get_config() diff --git a/tests/mocking.py b/tests/mocking.py index 4751886f..1becf2e9 100644 --- a/tests/mocking.py +++ b/tests/mocking.py @@ -2,6 +2,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations import os import sys diff --git a/tests/test_application.py b/tests/test_application.py index bdf8d0c8..6bc2d89c 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import os import shutil from tempfile import mkdtemp @@ -107,7 +109,7 @@ def test_load_bad_config(): with open(pjoin(config_dir, "dummy_app_config.py"), "w", encoding="utf-8") as f: f.write('c.DummyApp.m = "a\n') # Syntax error - with pytest.raises(SyntaxError): + with pytest.raises(SyntaxError): # noqa: PT012 app = DummyApp(config_dir=config_dir) app.raise_config_file_errors = True app.initialize([]) diff --git a/tests/test_command.py b/tests/test_command.py index a0833c13..ff8731ca 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -1,4 +1,5 @@ """Test the Jupyter command-line""" +from __future__ import annotations import json import os diff --git a/tests/test_migrate.py b/tests/test_migrate.py index 7d21ce9b..d13e8bed 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -1,6 +1,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. """Test config file migration""" +from __future__ import annotations import os import re @@ -28,15 +29,15 @@ dotipython_empty = pjoin(here, "dotipython_empty") -@pytest.fixture +@pytest.fixture() def td(request): """Fixture for a temporary directory""" td = mkdtemp("μnïcø∂e") - request.addfinalizer(lambda: shutil.rmtree(td)) - return td + yield td + shutil.rmtree(td) -@pytest.fixture +@pytest.fixture() def env(request): """Fixture for a full testing environment""" td = mkdtemp() @@ -51,14 +52,10 @@ def env(request): env_patch = patch.dict(os.environ, env) env_patch.start() - def fin(): - """Cleanup test env""" - env_patch.stop() - shutil.rmtree(td, ignore_errors=os.name == "nt") + yield env - request.addfinalizer(fin) - - return env + env_patch.stop() + shutil.rmtree(td, ignore_errors=os.name == "nt") def touch(path, content=""): diff --git a/tests/test_paths.py b/tests/test_paths.py index 6deca884..3d7d1a06 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -2,6 +2,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations import os import re @@ -468,13 +469,11 @@ def test_is_hidden(): reason="only run on windows/cpython or pypy >= 7.3.6: https://foss.heptapod.net/pypy/pypy/-/issues/3469", ) def test_is_hidden_win32_cpython(): - import ctypes # noqa - with tempfile.TemporaryDirectory() as root: subdir1 = os.path.join(root, "subdir") os.makedirs(subdir1) assert not is_hidden(subdir1, root) - subprocess.check_call(["attrib", "+h", subdir1]) # noqa + subprocess.check_call(["attrib", "+h", subdir1]) assert is_hidden(subdir1, root) assert is_file_hidden(subdir1) @@ -488,13 +487,11 @@ def test_is_hidden_win32_cpython(): reason="only run on windows/pypy < 7.3.6: https://foss.heptapod.net/pypy/pypy/-/issues/3469", ) def test_is_hidden_win32_pypy(): - import ctypes # noqa - with tempfile.TemporaryDirectory() as root: subdir1 = os.path.join(root, "subdir") os.makedirs(subdir1) assert not is_hidden(subdir1, root) - subprocess.check_call(["attrib", "+h", subdir1]) # noqa + subprocess.check_call(["attrib", "+h", subdir1]) with warnings.catch_warnings(record=True) as w: # Cause all warnings to always be triggered. @@ -527,12 +524,12 @@ def test_secure_write_win32(): def fetch_win32_permissions(filename): """Extracts file permissions on windows using icacls""" role_permissions = {} - proc = os.popen("icacls %s" % filename) # noqa + proc = os.popen("icacls %s" % filename) lines = proc.read().splitlines() proc.close() for index, line in enumerate(lines): if index == 0: - line = line.split(filename)[-1].strip().lower() # noqa + line = line.split(filename)[-1].strip().lower() # noqa: PLW2901 match = re.match(r"\s*([^:]+):\(([^\)]*)\)", line) if match: usergroup, permissions = match.groups() @@ -575,16 +572,16 @@ def test_secure_write_unix(): with secure_write(fname) as f: f.write("test 1") mode = os.stat(fname).st_mode - assert 0o0600 == (stat.S_IMODE(mode) & 0o7677) # noqa # tolerate owner-execute bit + assert (stat.S_IMODE(mode) & 0o7677) == 0o0600 # tolerate owner-execute bit with open(fname, encoding="utf-8") as f: assert f.read() == "test 1" # Try changing file permissions ahead of time - os.chmod(fname, 0o755) # noqa + os.chmod(fname, 0o755) with secure_write(fname) as f: f.write("test 2") mode = os.stat(fname).st_mode - assert 0o0600 == (stat.S_IMODE(mode) & 0o7677) # noqa # tolerate owner-execute bit + assert (stat.S_IMODE(mode) & 0o7677) == 0o0600 # tolerate owner-execute bit with open(fname, encoding="utf-8") as f: assert f.read() == "test 2" finally: diff --git a/tests/test_troubleshoot.py b/tests/test_troubleshoot.py index 8ad12fae..d921b495 100644 --- a/tests/test_troubleshoot.py +++ b/tests/test_troubleshoot.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from jupyter_core.troubleshoot import main diff --git a/tests/test_utils.py b/tests/test_utils.py index ad5cbb4f..f426faa8 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -2,6 +2,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations import asyncio import os