From cde7c08aba4d2020dfadd8cc362e1c47caf00e0e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Wed, 13 Sep 2023 20:54:03 -0500 Subject: [PATCH] Adopt sp-repo-review (#360) * adopt sp-repo-review * fix typing * fix typing * fix typing * fix typing * fix typing * fix typing * test troubleshoot * restore additional mdformat deps --- .flake8 | 17 ---- .github/workflows/test.yml | 4 +- .pre-commit-config.yaml | 39 ++++++++- .readthedocs.yaml | 2 +- CHANGELOG.md | 2 +- jupyter_core/application.py | 79 +++++++++---------- jupyter_core/command.py | 21 ++--- jupyter_core/migrate.py | 21 +++-- jupyter_core/paths.py | 33 +++++--- jupyter_core/utils/__init__.py | 35 ++++---- pyproject.toml | 45 ++++++----- {jupyter_core/tests => tests}/__init__.py | 0 .../dotipython/nbextensions/myext.js | 0 .../profile_default/ipython_config.py | 2 +- .../profile_default/ipython_console_config.py | 2 +- .../profile_default/ipython_kernel_config.py | 2 +- .../ipython_nbconvert_config.py | 0 .../ipython_notebook_config.py | 0 .../profile_default/static/custom/custom.css | 0 .../profile_default/static/custom/custom.js | 0 .../profile_default/ipython_config.py | 2 +- .../profile_default/ipython_console_config.py | 2 +- .../profile_default/ipython_kernel_config.py | 2 +- .../ipython_nbconvert_config.py | 2 +- .../ipython_notebook_config.py | 0 .../profile_default/static/custom/custom.css | 0 .../profile_default/static/custom/custom.js | 0 {jupyter_core/tests => tests}/mocking.py | 0 .../tests => tests}/test_application.py | 0 {jupyter_core/tests => tests}/test_async.py | 0 {jupyter_core/tests => tests}/test_command.py | 0 {jupyter_core/tests => tests}/test_migrate.py | 2 +- {jupyter_core/tests => tests}/test_paths.py | 0 tests/test_troubleshoot.py | 8 ++ 34 files changed, 179 insertions(+), 143 deletions(-) delete mode 100644 .flake8 rename {jupyter_core/tests => tests}/__init__.py (100%) rename {jupyter_core/tests => tests}/dotipython/nbextensions/myext.js (100%) rename {jupyter_core/tests/dotipython_empty => tests/dotipython}/profile_default/ipython_config.py (99%) rename {jupyter_core/tests/dotipython_empty => tests/dotipython}/profile_default/ipython_console_config.py (99%) rename {jupyter_core/tests/dotipython_empty => tests/dotipython}/profile_default/ipython_kernel_config.py (99%) rename {jupyter_core/tests => tests}/dotipython/profile_default/ipython_nbconvert_config.py (100%) rename {jupyter_core/tests => tests}/dotipython/profile_default/ipython_notebook_config.py (100%) rename {jupyter_core/tests => tests}/dotipython/profile_default/static/custom/custom.css (100%) rename {jupyter_core/tests => tests}/dotipython/profile_default/static/custom/custom.js (100%) rename {jupyter_core/tests/dotipython => tests/dotipython_empty}/profile_default/ipython_config.py (99%) rename {jupyter_core/tests/dotipython => tests/dotipython_empty}/profile_default/ipython_console_config.py (99%) rename {jupyter_core/tests/dotipython => tests/dotipython_empty}/profile_default/ipython_kernel_config.py (99%) rename {jupyter_core/tests => tests}/dotipython_empty/profile_default/ipython_nbconvert_config.py (99%) rename {jupyter_core/tests => tests}/dotipython_empty/profile_default/ipython_notebook_config.py (100%) rename {jupyter_core/tests => tests}/dotipython_empty/profile_default/static/custom/custom.css (100%) rename {jupyter_core/tests => tests}/dotipython_empty/profile_default/static/custom/custom.js (100%) rename {jupyter_core/tests => tests}/mocking.py (100%) rename {jupyter_core/tests => tests}/test_application.py (100%) rename {jupyter_core/tests => tests}/test_async.py (100%) rename {jupyter_core/tests => tests}/test_command.py (100%) rename {jupyter_core/tests => tests}/test_migrate.py (99%) rename {jupyter_core/tests => tests}/test_paths.py (100%) create mode 100644 tests/test_troubleshoot.py diff --git a/.flake8 b/.flake8 deleted file mode 100644 index b571016f..00000000 --- a/.flake8 +++ /dev/null @@ -1,17 +0,0 @@ -[flake8] -ignore = E501, W503, E402 -builtins = c, get_config -exclude = - .cache, - .github, - docs, - setup.py -enable-extensions = G -extend-ignore = - G001, G002, G004, G200, G201, G202, - # black adds spaces around ':' - E203, -per-file-ignores = - # B011: Do not call assert False since python -O removes these calls - # F841 local variable 'foo' is assigned to but never used - jupyter_core/tests/*: B011, F841 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6bcf4e2c..f19360dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -107,8 +107,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: jupyterlab/maintainer-tools/.github/actions/base-setup@v1 - - name: Run Linters - run: | + - name: Run Linters + run: | hatch run typing:test hatch run lint:style pipx run interrogate -v . diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 891a9770..fa47c7e2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,6 @@ ci: autoupdate_schedule: monthly + autoupdate_commit_msg: "chore: update pre-commit hooks" repos: - repo: https://github.com/pre-commit/pre-commit-hooks @@ -28,15 +29,47 @@ repos: rev: 0.7.17 hooks: - id: mdformat + additional_dependencies: + [mdformat-gfm, mdformat-frontmatter, mdformat-footnote] - - repo: https://github.com/psf/black + - repo: https://github.com/pre-commit/mirrors-prettier + rev: "v3.0.2" + hooks: + - id: prettier + types_or: [yaml, html, json] + + - repo: https://github.com/adamchainz/blacken-docs + rev: "1.16.0" + hooks: + - id: blacken-docs + additional_dependencies: [black==23.7.0] + + - repo: https://github.com/psf/black-pre-commit-mirror rev: 23.7.0 hooks: - id: black + - repo: https://github.com/codespell-project/codespell + rev: "v2.2.5" + hooks: + - id: codespell + args: ["-L", "sur,nd"] + + - repo: https://github.com/pre-commit/pygrep-hooks + rev: "v1.10.0" + hooks: + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal + - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.0.287 hooks: - id: ruff - args: ["--fix"] - exclude: script + args: ["--fix", "--show-fixes"] + + - repo: https://github.com/scientific-python/cookie + rev: "2023.08.23" + hooks: + - id: sp-repo-review + additional_dependencies: ["repo-review[cli]"] diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 07bd852a..7c163a22 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -9,4 +9,4 @@ python: build: os: ubuntu-22.04 tools: - python: "3.11" + python: "3.11" diff --git a/CHANGELOG.md b/CHANGELOG.md index ab25d14f..e75da5e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -391,7 +391,7 @@ closed. [on GitHub](https://github.com/jupyter/jupyter_core/releases/tag/4.6.2) - Add ability to allow insecure writes with - JUPYTER_ALLOW_INSECURE_WRITES environement variable + JUPYTER_ALLOW_INSECURE_WRITES environment variable ([#182](https://github.com/jupyter/jupyter_core/pull/182)). - Docs typo and build fixes - Added python 3.7 and 3.8 builds to testing diff --git a/jupyter_core/application.py b/jupyter_core/application.py index 8b55a8a5..7b65f49b 100644 --- a/jupyter_core/application.py +++ b/jupyter_core/application.py @@ -6,6 +6,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations import logging import os @@ -29,23 +30,25 @@ ) from .utils import ensure_dir_exists +# mypy: disable-error-code="no-untyped-call" + # aliases and flags -base_aliases: dict = {} +base_aliases: dict[str, t.Any] = {} if isinstance(Application.aliases, dict): # traitlets 5 - base_aliases.update(Application.aliases) + base_aliases.update(Application.aliases) # type:ignore[arg-type] _jupyter_aliases = { "log-level": "Application.log_level", "config": "JupyterApp.config_file", } base_aliases.update(_jupyter_aliases) -base_flags: dict = {} +base_flags: dict[str, t.Any] = {} if isinstance(Application.flags, dict): # traitlets 5 - base_flags.update(Application.flags) -_jupyter_flags: dict = { + base_flags.update(Application.flags) # type:ignore[arg-type] +_jupyter_flags: dict[str, t.Any] = { "debug": ( {"Application": {"log_level": logging.DEBUG}}, "set log level to logging.DEBUG (maximize logging output)", @@ -69,71 +72,67 @@ class JupyterApp(Application): name = "jupyter" # override in subclasses description = "A Jupyter Application" - aliases = base_aliases - flags = base_flags + aliases = base_aliases # type:ignore[assignment] + flags = base_flags # type:ignore[assignment] - def _log_level_default(self): + def _log_level_default(self) -> int: return logging.INFO - jupyter_path: t.Union[t.List[str], List] = List(Unicode()) + jupyter_path: list[str] | List = List(Unicode()) - def _jupyter_path_default(self): + def _jupyter_path_default(self) -> list[str]: return jupyter_path() - config_dir: t.Union[str, Unicode] = Unicode() + config_dir: str | Unicode = Unicode() - def _config_dir_default(self): + def _config_dir_default(self) -> str: return jupyter_config_dir() @property - def config_file_paths(self): + 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: t.Union[str, Unicode] = Unicode() + data_dir: str | Unicode = Unicode() - def _data_dir_default(self): + def _data_dir_default(self) -> str: d = jupyter_data_dir() ensure_dir_exists(d, mode=0o700) return d - runtime_dir: t.Union[str, Unicode] = Unicode() + runtime_dir: str | Unicode = Unicode() - def _runtime_dir_default(self): + 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): + @observe("runtime_dir") # type:ignore[misc] + def _runtime_dir_changed(self, change: t.Any) -> None: ensure_dir_exists(change["new"], mode=0o700) - generate_config: t.Union[bool, Bool] = Bool( + generate_config: bool | Bool = Bool( False, config=True, help="""Generate default config file.""" ) - config_file_name: t.Union[str, Unicode] = Unicode( - config=True, help="Specify a config file to load." - ) + config_file_name: str | Unicode = Unicode(config=True, help="Specify a config file to load.") - def _config_file_name_default(self): + def _config_file_name_default(self) -> str: if not self.name: return "" return self.name.replace("-", "_") + "_config" - config_file: t.Union[str, Unicode] = Unicode( + config_file: str | Unicode = Unicode( config=True, help="""Full path of a config file.""", ) - answer_yes: t.Union[bool, Bool] = Bool( - False, config=True, help="""Answer yes to any prompts.""" - ) + answer_yes: bool | Bool = Bool(False, config=True, help="""Answer yes to any prompts.""") - def write_default_config(self): + def write_default_config(self) -> None: """Write our default config to a .py config file""" if self.config_file: config_file = self.config_file @@ -143,7 +142,7 @@ def write_default_config(self): if os.path.exists(config_file) and not self.answer_yes: answer = "" - def ask(): + def ask() -> str: prompt = "Overwrite %s with default config? [y/N]" % config_file try: return input(prompt).lower() or "n" @@ -166,7 +165,7 @@ def ask(): with open(config_file, mode="w", encoding="utf-8") as f: f.write(config_text) - def migrate_config(self): + 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) @@ -188,7 +187,7 @@ def migrate_config(self): migrate() - def load_config_file(self, suppress_errors=True): + 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 @@ -209,7 +208,7 @@ def load_config_file(self, suppress_errors=True): if self.config_file: path, config_file_name = os.path.split(self.config_file) else: - path = self.config_file_paths + 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): @@ -227,12 +226,12 @@ def load_config_file(self, suppress_errors=True): self.log.warning("Error loading config file: %s", config_file_name, exc_info=True) # subcommand-related - def _find_subcommand(self, name): + def _find_subcommand(self, name: str) -> str: name = f"{self.name}-{name}" - return which(name) + return which(name) or "" @property - def _dispatching(self): + def _dispatching(self) -> bool: """Return whether we are dispatching to another command or running ourselves. @@ -242,7 +241,7 @@ def _dispatching(self): subcommand = Unicode() @catch_config_error - def initialize(self, argv=None): + 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: @@ -264,7 +263,7 @@ def initialize(self, argv=None): if allow_insecure_writes: issue_insecure_write_warning() - def start(self): + def start(self) -> None: """Start the whole thing""" if self.subcommand: os.execv(self.subcommand, [self.subcommand] + self.argv[1:]) # noqa @@ -279,10 +278,10 @@ def start(self): raise NoStart() @classmethod - def launch_instance(cls, argv=None, **kwargs): + def launch_instance(cls, argv: t.Any = None, **kwargs: t.Any) -> None: """Launch an instance of a Jupyter Application""" try: - return super().launch_instance(argv=argv, **kwargs) + super().launch_instance(argv=argv, **kwargs) except NoStart: return diff --git a/jupyter_core/command.py b/jupyter_core/command.py index b3fef8c2..81549fe2 100644 --- a/jupyter_core/command.py +++ b/jupyter_core/command.py @@ -6,6 +6,7 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations import argparse import errno @@ -16,7 +17,7 @@ import sysconfig from shutil import which from subprocess import Popen -from typing import List +from typing import Any from . import paths from .version import __version__ @@ -26,7 +27,7 @@ class JupyterParser(argparse.ArgumentParser): """A Jupyter argument parser.""" @property - def epilog(self): + def epilog(self) -> str | None: """Add subcommands to epilog on request Avoids searching PATH for subcommands unless help output is requested. @@ -34,11 +35,11 @@ def epilog(self): return "Available subcommands: %s" % " ".join(list_subcommands()) @epilog.setter - def epilog(self, x): + def epilog(self, x: Any) -> None: """Ignore epilog set in Parser.__init__""" pass - def argcomplete(self): + def argcomplete(self) -> None: """Trigger auto-completion, if enabled""" try: import argcomplete # type: ignore[import] @@ -78,7 +79,7 @@ def jupyter_parser() -> JupyterParser: return parser -def list_subcommands() -> List[str]: +def list_subcommands() -> list[str]: """List all jupyter subcommands searches PATH for `jupyter-name` @@ -108,7 +109,7 @@ def list_subcommands() -> List[str]: return sorted(subcommands) -def _execvp(cmd, argv): +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). @@ -131,7 +132,7 @@ def _execvp(cmd, argv): os.execvp(cmd, argv) # noqa -def _jupyter_abspath(subcommand): +def _jupyter_abspath(subcommand: str) -> str: """This method get the abspath of a specified jupyter-subcommand with no changes on ENV. """ @@ -151,7 +152,7 @@ def _jupyter_abspath(subcommand): return abs_path -def _path_with_self(): +def _path_with_self() -> list[str]: """Put `jupyter`'s dir at the front of PATH Ensures that /path/to/jupyter subcommand @@ -189,7 +190,7 @@ def _path_with_self(): return path_list -def _evaluate_argcomplete(parser: JupyterParser) -> List[str]: +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 @@ -208,7 +209,7 @@ def _evaluate_argcomplete(parser: JupyterParser) -> List[str]: 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() + increment_argcomplete_index() # type:ignore[no-untyped-call] return cwords else: # Otherwise no subcommand, directly autocomplete and exit diff --git a/jupyter_core/migrate.py b/jupyter_core/migrate.py index b550ea66..1f9ea139 100644 --- a/jupyter_core/migrate.py +++ b/jupyter_core/migrate.py @@ -23,11 +23,13 @@ # 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 typing import Any from traitlets.config.loader import JSONFileConfigLoader, PyFileConfigLoader from traitlets.log import get_logger @@ -36,6 +38,9 @@ from .paths import jupyter_config_dir, jupyter_data_dir from .utils import ensure_dir_exists +# mypy: disable-error-code="no-untyped-call" + + pjoin = os.path.join migrations = { @@ -65,7 +70,7 @@ } -def get_ipython_dir(): +def get_ipython_dir() -> str: """Return the IPython directory location. Not imported from IPython because the IPython implementation @@ -79,7 +84,7 @@ def get_ipython_dir(): return os.environ.get("IPYTHONDIR", os.path.expanduser("~/.ipython")) -def migrate_dir(src, dst): +def migrate_dir(src: str, dst: str) -> bool: """Migrate a directory from src to dst""" log = get_logger() if not os.listdir(src): @@ -98,7 +103,7 @@ def migrate_dir(src, dst): return True -def migrate_file(src, dst, substitutions=None): +def migrate_file(src: str, dst: str, 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. @@ -121,7 +126,7 @@ def migrate_file(src, dst, substitutions=None): return True -def migrate_one(src, dst): +def migrate_one(src: str, dst: str) -> bool: """Migrate one item dispatches to migrate_dir/_file @@ -136,7 +141,7 @@ def migrate_one(src, dst): return False -def migrate_static_custom(src, dst): +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} @@ -184,7 +189,7 @@ def migrate_static_custom(src, dst): return migrated -def migrate_config(name, env): +def migrate_config(name: str, env: Any) -> list[Any]: """Migrate a config file. Includes substitutions for updated configurable names. @@ -211,7 +216,7 @@ def migrate_config(name, env): return migrated -def migrate(): +def migrate() -> bool: """Migrate IPython configuration to Jupyter""" env = { "jupyter_data": jupyter_data_dir(), @@ -264,7 +269,7 @@ class JupyterMigrate(JupyterApp): If the destinations already exist, nothing will be done. """ - def start(self): + def start(self) -> None: """Start the application.""" if not migrate(): self.log.info("Found nothing to migrate.") diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index 79369f29..74c21dce 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -391,7 +391,10 @@ def is_file_hidden_win(abs_path: str, stat_res: Optional[Any] = None) -> bool: raise try: - if stat_res.st_file_attributes & stat.FILE_ATTRIBUTE_HIDDEN: # type:ignore + if ( + stat_res.st_file_attributes # type:ignore[union-attr] + & stat.FILE_ATTRIBUTE_HIDDEN # type:ignore[attr-defined] + ): return True except AttributeError: # allow AttributeError on PyPy for Windows @@ -615,7 +618,7 @@ class ACL(ctypes.Structure): PACL = ctypes.POINTER(ACL) PSECURITY_DESCRIPTOR = ctypes.POINTER(wintypes.BYTE) - def _nonzero_success(result, func, args): + def _nonzero_success(result: int, func: Any, args: Any) -> Any: if not result: raise ctypes.WinError(ctypes.get_last_error()) # type:ignore[attr-defined] return args @@ -717,7 +720,7 @@ def _nonzero_success(result, func, args): wintypes.DWORD, # DWORD dwAclRevision ) - def CreateWellKnownSid(WellKnownSidType): + def CreateWellKnownSid(WellKnownSidType: Any) -> Any: # return a SID for predefined aliases pSid = (ctypes.c_char * 1)() cbSid = wintypes.DWORD() @@ -730,7 +733,7 @@ def CreateWellKnownSid(WellKnownSidType): advapi32.CreateWellKnownSid(WellKnownSidType, None, pSid, ctypes.byref(cbSid)) return pSid[:] - def GetUserNameEx(NameFormat): + def GetUserNameEx(NameFormat: Any) -> Any: # return the user or other security principal associated with # the calling thread nSize = ctypes.pointer(ctypes.c_ulong(0)) @@ -745,7 +748,7 @@ def GetUserNameEx(NameFormat): secur32.GetUserNameExW(NameFormat, lpNameBuffer, nSize) return lpNameBuffer.value - def LookupAccountName(lpSystemName, lpAccountName): + def LookupAccountName(lpSystemName: Any, lpAccountName: Any) -> Any: # return a security identifier (SID) for an account on a system # and the name of the domain on which the account was found cbSid = wintypes.DWORD(0) @@ -780,12 +783,12 @@ def LookupAccountName(lpSystemName, lpAccountName): raise ctypes.WinError() # type:ignore[attr-defined] return pSid, lpReferencedDomainName.value, peUse.value - def AddAccessAllowedAce(pAcl, dwAceRevision, AccessMask, pSid): + def AddAccessAllowedAce(pAcl: Any, dwAceRevision: Any, AccessMask: Any, pSid: Any) -> Any: # add an access-allowed access control entry (ACE) # to an access control list (ACL) advapi32.AddAccessAllowedAce(pAcl, dwAceRevision, AccessMask, pSid) - def GetFileSecurity(lpFileName, RequestedInformation): + def GetFileSecurity(lpFileName: Any, RequestedInformation: Any) -> Any: # return information about the security of a file or directory nLength = wintypes.DWORD(0) try: @@ -811,15 +814,19 @@ def GetFileSecurity(lpFileName, RequestedInformation): ) return pSecurityDescriptor - def SetFileSecurity(lpFileName, RequestedInformation, pSecurityDescriptor): + def SetFileSecurity( + lpFileName: Any, RequestedInformation: Any, pSecurityDescriptor: Any + ) -> Any: # set the security of a file or directory object advapi32.SetFileSecurityW(lpFileName, RequestedInformation, pSecurityDescriptor) - def SetSecurityDescriptorDacl(pSecurityDescriptor, bDaclPresent, pDacl, bDaclDefaulted): + def SetSecurityDescriptorDacl( + pSecurityDescriptor: Any, bDaclPresent: Any, pDacl: Any, bDaclDefaulted: Any + ) -> Any: # set information in a discretionary access control list (DACL) advapi32.SetSecurityDescriptorDacl(pSecurityDescriptor, bDaclPresent, pDacl, bDaclDefaulted) - def MakeAbsoluteSD(pSelfRelativeSecurityDescriptor): + def MakeAbsoluteSD(pSelfRelativeSecurityDescriptor: Any) -> Any: # return a security descriptor in absolute format # by using a security descriptor in self-relative format as a template pAbsoluteSecurityDescriptor = None @@ -873,7 +880,7 @@ def MakeAbsoluteSD(pSelfRelativeSecurityDescriptor): ) return pAbsoluteSecurityDescriptor - def MakeSelfRelativeSD(pAbsoluteSecurityDescriptor): + def MakeSelfRelativeSD(pAbsoluteSecurityDescriptor: Any) -> Any: # return a security descriptor in self-relative format # by using a security descriptor in absolute format as a template pSelfRelativeSecurityDescriptor = None @@ -895,7 +902,7 @@ def MakeSelfRelativeSD(pAbsoluteSecurityDescriptor): ) return pSelfRelativeSecurityDescriptor - def NewAcl(): + def NewAcl() -> Any: # return a new, initialized ACL (access control list) structure nAclLength = 32767 # TODO: calculate this: ctypes.sizeof(ACL) + ? acl_data = ctypes.create_string_buffer(nAclLength) @@ -1002,7 +1009,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, *args, **kwargs): + def format_warning(msg: str, *args: Any, **kwargs: Any) -> str: return str(msg) + "\n" warnings.formatwarning = format_warning # type:ignore[assignment] diff --git a/jupyter_core/utils/__init__.py b/jupyter_core/utils/__init__.py index 68973e6c..6e26c17c 100644 --- a/jupyter_core/utils/__init__.py +++ b/jupyter_core/utils/__init__.py @@ -1,5 +1,6 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations import asyncio import atexit @@ -11,10 +12,10 @@ import warnings from pathlib import Path from types import FrameType -from typing import Awaitable, Callable, List, Optional, TypeVar, Union, cast +from typing import Any, Awaitable, Callable, TypeVar, cast -def ensure_dir_exists(path, mode=0o777): +def ensure_dir_exists(path: str, mode: int = 0o777) -> None: """Ensure that a directory exists If it doesn't exist, try to create it, protecting against a race condition @@ -30,7 +31,7 @@ def ensure_dir_exists(path, mode=0o777): raise OSError("%r exists but is not a directory" % path) -def _get_frame(level: int) -> Optional[FrameType]: +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() @@ -50,7 +51,7 @@ def _get_frame(level: int) -> Optional[FrameType]: # 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: +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 @@ -72,14 +73,14 @@ def _external_stacklevel(internal: List[str]) -> int: return level - 1 -def deprecation(message: str, internal: Union[str, List[str]] = "jupyter_core/") -> None: +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 examnple, + 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: list[str] _internal = [internal] if isinstance(internal, str) else internal # stack level of the first external frame from here @@ -95,17 +96,17 @@ def deprecation(message: str, internal: Union[str, List[str]] = "jupyter_core/") class _TaskRunner: """A task runner that runs an asyncio event loop on a background thread.""" - def __init__(self): - self.__io_loop: Optional[asyncio.AbstractEventLoop] = None - self.__runner_thread: Optional[threading.Thread] = None + 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): + def _close(self) -> None: if self.__io_loop: self.__io_loop.stop() - def _runner(self): + def _runner(self) -> None: loop = self.__io_loop assert loop is not None # noqa try: @@ -113,7 +114,7 @@ def _runner(self): finally: loop.close() - def run(self, coro): + def run(self, coro: Any) -> Any: """Synchronously run a coroutine on a background thread.""" with self.__lock: name = f"{threading.current_thread().name} - runner" @@ -125,8 +126,8 @@ def run(self, coro): return fut.result(None) -_runner_map = {} -_loop_map = {} +_runner_map: dict[str, _TaskRunner] = {} +_loop_map: dict[str, asyncio.AbstractEventLoop] = {} def run_sync(coro: Callable[..., Awaitable[T]]) -> Callable[..., T]: @@ -146,7 +147,7 @@ def run_sync(coro: Callable[..., Awaitable[T]]) -> Callable[..., T]: if not inspect.iscoroutinefunction(coro): raise AssertionError - def wrapped(*args, **kwargs): + def wrapped(*args: Any, **kwargs: Any) -> Any: name = threading.current_thread().name inner = coro(*args, **kwargs) try: @@ -169,7 +170,7 @@ def wrapped(*args, **kwargs): return wrapped -async def ensure_async(obj: Union[Awaitable[T], T]) -> T: +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. diff --git a/pyproject.toml b/pyproject.toml index 1f9801b2..6deba32a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -88,9 +88,9 @@ nowarn = "test -W default {args}" [tool.hatch.envs.typing] features = ["test"] -dependencies = ["mypy>=0.990"] +dependencies = ["mypy>=1.5.1"] [tool.hatch.envs.typing.scripts] -test = "mypy --install-types --non-interactive {args:.}" +test = "mypy --install-types --non-interactive {args}" [tool.hatch.envs.lint] dependencies = ["black==23.3.0", "mdformat>0.7", "ruff==0.0.281"] @@ -108,28 +108,24 @@ fmt = [ ] [tool.mypy] -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_untyped_decorators = true -no_implicit_optional = true -no_implicit_reexport = true -pretty = true -show_error_context = true +files = "jupyter_core" +python_version = "3.8" +strict = true show_error_codes = true -strict_equality = true -strict_optional = true -warn_unused_configs = true -warn_redundant_casts = true -warn_return_any = true -warn_unused_ignores = true -exclude = [ - "jupyter_core/tests/.*/profile_default/.*_config.py" -] +enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] +warn_unreachable = true [tool.pytest.ini_options] -addopts = "-raXs --durations 10 --color=yes --doctest-modules --ignore-glob=jupyter_core/tests/dotipython*" +minversion = "6.0" +xfail_strict = true +log_cli_level = "info" +addopts = [ + "-raXs", "--durations=10", "--color=yes", "--doctest-modules", + "--showlocals", "--strict-markers", "--strict-config", + "--ignore-glob=tests/dotipython*" +] testpaths = [ - "jupyter_core/tests/" + "tests/" ] filterwarnings= [ # Fail on warnings @@ -198,9 +194,9 @@ unfixable = [ # 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 -"jupyter_core/tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "S101", "S108", "PLR2004", "S603"] +"tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "S101", "S108", "PLR2004", "S603"] # F821 Undefined name `get_config` -"jupyter_core/tests/**/profile_default/*_config.py" = ["F821"] +"tests/**/profile_default/*_config.py" = ["F821"] # T201 `print` found "jupyter_core/application.py" = ["T201"] "jupyter_core/command.py" = ["T201"] @@ -218,8 +214,11 @@ ignore-property-decorators=true ignore-nested-functions=true ignore-nested-classes=true fail-under=100 -exclude = ["docs", "*/tests"] +exclude = ["docs", "tests"] [tool.check-wheel-contents] toplevel = ["jupyter_core/", "jupyter.py"] ignore = ["W002"] + +[tool.repo-review] +ignore = ["PY007", "PP308", "GH102", "PC140"] diff --git a/jupyter_core/tests/__init__.py b/tests/__init__.py similarity index 100% rename from jupyter_core/tests/__init__.py rename to tests/__init__.py diff --git a/jupyter_core/tests/dotipython/nbextensions/myext.js b/tests/dotipython/nbextensions/myext.js similarity index 100% rename from jupyter_core/tests/dotipython/nbextensions/myext.js rename to tests/dotipython/nbextensions/myext.js diff --git a/jupyter_core/tests/dotipython_empty/profile_default/ipython_config.py b/tests/dotipython/profile_default/ipython_config.py similarity index 99% rename from jupyter_core/tests/dotipython_empty/profile_default/ipython_config.py rename to tests/dotipython/profile_default/ipython_config.py index 86777f6e..1c8ce115 100644 --- a/jupyter_core/tests/dotipython_empty/profile_default/ipython_config.py +++ b/tests/dotipython/profile_default/ipython_config.py @@ -369,7 +369,7 @@ # Options for configuring the SQLite connection # # These options are passed as keyword args to sqlite3.connect when establishing -# database conenctions. +# database connections. # c.HistoryManager.connection_options = {} # Should the history database include output? (default: no) diff --git a/jupyter_core/tests/dotipython_empty/profile_default/ipython_console_config.py b/tests/dotipython/profile_default/ipython_console_config.py similarity index 99% rename from jupyter_core/tests/dotipython_empty/profile_default/ipython_console_config.py rename to tests/dotipython/profile_default/ipython_console_config.py index f078b181..f6478a04 100644 --- a/jupyter_core/tests/dotipython_empty/profile_default/ipython_console_config.py +++ b/tests/dotipython/profile_default/ipython_console_config.py @@ -222,7 +222,7 @@ # Callable object called via 'callable' image handler with one argument, `data`, # which is `msg["content"]["data"]` where `msg` is the message from iopub -# channel. For exmaple, you can find base64 encoded PNG data as +# channel. For example, you can find base64 encoded PNG data as # `data['image/png']`. # c.ZMQTerminalInteractiveShell.callable_image_handler = None diff --git a/jupyter_core/tests/dotipython_empty/profile_default/ipython_kernel_config.py b/tests/dotipython/profile_default/ipython_kernel_config.py similarity index 99% rename from jupyter_core/tests/dotipython_empty/profile_default/ipython_kernel_config.py rename to tests/dotipython/profile_default/ipython_kernel_config.py index 3f7fb7c8..998a5bf3 100644 --- a/jupyter_core/tests/dotipython_empty/profile_default/ipython_kernel_config.py +++ b/tests/dotipython/profile_default/ipython_kernel_config.py @@ -163,7 +163,7 @@ # # c.IPythonKernel._execute_sleep = 0.0005 -# Whether to use appnope for compatiblity with OS X App Nap. +# Whether to use appnope for compatibility with OS X App Nap. # # Only affects OS X >= 10.9. # c.IPythonKernel._darwin_app_nap = True diff --git a/jupyter_core/tests/dotipython/profile_default/ipython_nbconvert_config.py b/tests/dotipython/profile_default/ipython_nbconvert_config.py similarity index 100% rename from jupyter_core/tests/dotipython/profile_default/ipython_nbconvert_config.py rename to tests/dotipython/profile_default/ipython_nbconvert_config.py diff --git a/jupyter_core/tests/dotipython/profile_default/ipython_notebook_config.py b/tests/dotipython/profile_default/ipython_notebook_config.py similarity index 100% rename from jupyter_core/tests/dotipython/profile_default/ipython_notebook_config.py rename to tests/dotipython/profile_default/ipython_notebook_config.py diff --git a/jupyter_core/tests/dotipython/profile_default/static/custom/custom.css b/tests/dotipython/profile_default/static/custom/custom.css similarity index 100% rename from jupyter_core/tests/dotipython/profile_default/static/custom/custom.css rename to tests/dotipython/profile_default/static/custom/custom.css diff --git a/jupyter_core/tests/dotipython/profile_default/static/custom/custom.js b/tests/dotipython/profile_default/static/custom/custom.js similarity index 100% rename from jupyter_core/tests/dotipython/profile_default/static/custom/custom.js rename to tests/dotipython/profile_default/static/custom/custom.js diff --git a/jupyter_core/tests/dotipython/profile_default/ipython_config.py b/tests/dotipython_empty/profile_default/ipython_config.py similarity index 99% rename from jupyter_core/tests/dotipython/profile_default/ipython_config.py rename to tests/dotipython_empty/profile_default/ipython_config.py index 86777f6e..1c8ce115 100644 --- a/jupyter_core/tests/dotipython/profile_default/ipython_config.py +++ b/tests/dotipython_empty/profile_default/ipython_config.py @@ -369,7 +369,7 @@ # Options for configuring the SQLite connection # # These options are passed as keyword args to sqlite3.connect when establishing -# database conenctions. +# database connections. # c.HistoryManager.connection_options = {} # Should the history database include output? (default: no) diff --git a/jupyter_core/tests/dotipython/profile_default/ipython_console_config.py b/tests/dotipython_empty/profile_default/ipython_console_config.py similarity index 99% rename from jupyter_core/tests/dotipython/profile_default/ipython_console_config.py rename to tests/dotipython_empty/profile_default/ipython_console_config.py index f078b181..f6478a04 100644 --- a/jupyter_core/tests/dotipython/profile_default/ipython_console_config.py +++ b/tests/dotipython_empty/profile_default/ipython_console_config.py @@ -222,7 +222,7 @@ # Callable object called via 'callable' image handler with one argument, `data`, # which is `msg["content"]["data"]` where `msg` is the message from iopub -# channel. For exmaple, you can find base64 encoded PNG data as +# channel. For example, you can find base64 encoded PNG data as # `data['image/png']`. # c.ZMQTerminalInteractiveShell.callable_image_handler = None diff --git a/jupyter_core/tests/dotipython/profile_default/ipython_kernel_config.py b/tests/dotipython_empty/profile_default/ipython_kernel_config.py similarity index 99% rename from jupyter_core/tests/dotipython/profile_default/ipython_kernel_config.py rename to tests/dotipython_empty/profile_default/ipython_kernel_config.py index 3f7fb7c8..998a5bf3 100644 --- a/jupyter_core/tests/dotipython/profile_default/ipython_kernel_config.py +++ b/tests/dotipython_empty/profile_default/ipython_kernel_config.py @@ -163,7 +163,7 @@ # # c.IPythonKernel._execute_sleep = 0.0005 -# Whether to use appnope for compatiblity with OS X App Nap. +# Whether to use appnope for compatibility with OS X App Nap. # # Only affects OS X >= 10.9. # c.IPythonKernel._darwin_app_nap = True diff --git a/jupyter_core/tests/dotipython_empty/profile_default/ipython_nbconvert_config.py b/tests/dotipython_empty/profile_default/ipython_nbconvert_config.py similarity index 99% rename from jupyter_core/tests/dotipython_empty/profile_default/ipython_nbconvert_config.py rename to tests/dotipython_empty/profile_default/ipython_nbconvert_config.py index ae8f9313..ad3c8e37 100644 --- a/jupyter_core/tests/dotipython_empty/profile_default/ipython_nbconvert_config.py +++ b/tests/dotipython_empty/profile_default/ipython_nbconvert_config.py @@ -772,7 +772,7 @@ # # you can overwrite :meth:`preprocess_cell` to apply a transformation # independently on each cell or :meth:`preprocess` if you prefer your own logic. -# See corresponding docstring for informations. +# See corresponding docstring for information. # # Disabled by default and can be enabled via the config by # 'c.YourPreprocessorName.enabled = True' diff --git a/jupyter_core/tests/dotipython_empty/profile_default/ipython_notebook_config.py b/tests/dotipython_empty/profile_default/ipython_notebook_config.py similarity index 100% rename from jupyter_core/tests/dotipython_empty/profile_default/ipython_notebook_config.py rename to tests/dotipython_empty/profile_default/ipython_notebook_config.py diff --git a/jupyter_core/tests/dotipython_empty/profile_default/static/custom/custom.css b/tests/dotipython_empty/profile_default/static/custom/custom.css similarity index 100% rename from jupyter_core/tests/dotipython_empty/profile_default/static/custom/custom.css rename to tests/dotipython_empty/profile_default/static/custom/custom.css diff --git a/jupyter_core/tests/dotipython_empty/profile_default/static/custom/custom.js b/tests/dotipython_empty/profile_default/static/custom/custom.js similarity index 100% rename from jupyter_core/tests/dotipython_empty/profile_default/static/custom/custom.js rename to tests/dotipython_empty/profile_default/static/custom/custom.js diff --git a/jupyter_core/tests/mocking.py b/tests/mocking.py similarity index 100% rename from jupyter_core/tests/mocking.py rename to tests/mocking.py diff --git a/jupyter_core/tests/test_application.py b/tests/test_application.py similarity index 100% rename from jupyter_core/tests/test_application.py rename to tests/test_application.py diff --git a/jupyter_core/tests/test_async.py b/tests/test_async.py similarity index 100% rename from jupyter_core/tests/test_async.py rename to tests/test_async.py diff --git a/jupyter_core/tests/test_command.py b/tests/test_command.py similarity index 100% rename from jupyter_core/tests/test_command.py rename to tests/test_command.py diff --git a/jupyter_core/tests/test_migrate.py b/tests/test_migrate.py similarity index 99% rename from jupyter_core/tests/test_migrate.py rename to tests/test_migrate.py index b86887ee..8d645275 100644 --- a/jupyter_core/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -143,7 +143,7 @@ def notice_m_dir(src, dst): assert migrate_one(srcdir, dstdir) assert called == {"migrate_dir": True} called.clear() - assert not migrate_one(pjoin(td, "dne"), dst) + assert not migrate_one(pjoin(td, "does_not_exist"), dst) assert called == {} diff --git a/jupyter_core/tests/test_paths.py b/tests/test_paths.py similarity index 100% rename from jupyter_core/tests/test_paths.py rename to tests/test_paths.py diff --git a/tests/test_troubleshoot.py b/tests/test_troubleshoot.py new file mode 100644 index 00000000..8ad12fae --- /dev/null +++ b/tests/test_troubleshoot.py @@ -0,0 +1,8 @@ +from jupyter_core.troubleshoot import main + + +def test_troubleshoot(capsys): + """Smoke test the troubleshoot function""" + main() + out = capsys.readouterr().out + assert "pip list" in out