From 8d9753d733e43cb5d65adf65bdd5e71b1c78309c Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 8 Oct 2025 14:37:44 +0200 Subject: [PATCH 1/4] Start to test on 3.14 and 3.14-free-threaded This starts testing on 3.14 and and 3.14t as 3.14 introduce some eventloop policy deprecation we are not ready to deal with, we ignore warnings only on Python 3.14 Some tornado tests are failing on all windows platform, we skip those. --- .github/workflows/test.yml | 2 +- jupyter_core/utils/__init__.py | 12 +++++++++++- tests/test_application.py | 2 ++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7cb17b..78e2713 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,7 +28,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.8", "3.12", "3.13"] + python-version: ["3.8", "3.12", "3.13", "3.14", "3.14t"] include: - os: windows-latest python-version: "3.9" diff --git a/jupyter_core/utils/__init__.py b/jupyter_core/utils/__init__.py index 665eac2..5ffa5b9 100644 --- a/jupyter_core/utils/__init__.py +++ b/jupyter_core/utils/__init__.py @@ -176,7 +176,17 @@ def ensure_event_loop(prefer_selector_loop: bool = False) -> asyncio.AbstractEve loop = asyncio.get_running_loop() except RuntimeError: if sys.platform == "win32" and prefer_selector_loop: - loop = asyncio.WindowsSelectorEventLoopPolicy().new_event_loop() + if (3, 14) <= sys.version_info < (3, 15): + # ignore deprecation only for 3.14 and revisit later. + with warnings.catch_warnings(): + warnings.filterwarnings( + "ignore", + DeprecationWarning, + message=".*WindowsSelectorEventLoopPolicy.*", + ) + loop = asyncio.WindowsSelectorEventLoopPolicy().new_event_loop() + else: + loop = asyncio.WindowsSelectorEventLoopPolicy().new_event_loop() else: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) diff --git a/tests/test_application.py b/tests/test_application.py index 5c6e2ba..2d074e9 100644 --- a/tests/test_application.py +++ b/tests/test_application.py @@ -3,6 +3,7 @@ import asyncio import os import shutil +import sys from tempfile import mkdtemp from unittest.mock import patch @@ -179,6 +180,7 @@ class AsyncTornadoApp(AsyncApp): _prefer_selector_loop = True +@pytest.mark.skipif(sys.platform == "win32", reason="does not run on windows") def test_async_tornado_app(): AsyncTornadoApp.launch_instance([]) app = AsyncApp.instance() From baae8251466f2939d0318f63dd53c71097371b50 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 8 Oct 2025 17:04:45 +0200 Subject: [PATCH 2/4] bump min to 3.10 --- .github/workflows/test.yml | 6 +++--- pyproject.toml | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 78e2713..cb58c32 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -28,14 +28,14 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest, macos-latest] - python-version: ["3.8", "3.12", "3.13", "3.14", "3.14t"] + python-version: ["3.12", "3.13", "3.14", "3.14t"] include: - os: windows-latest - python-version: "3.9" + python-version: "3.10" - os: ubuntu-latest python-version: "3.11" - os: ubuntu-latest - python-version: "pypy-3.9" + python-version: "pypy-3.10" - os: macos-latest python-version: "3.10" steps: diff --git a/pyproject.toml b/pyproject.toml index 2ba6fc0..b8eaba7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = [ "Programming Language :: Python", "Programming Language :: Python :: 3" ] -requires-python = ">=3.8" +requires-python = ">=3.10" dependencies = [ "platformdirs>=2.5", "traitlets>=5.3", @@ -103,7 +103,7 @@ build = [ [tool.mypy] files = "jupyter_core" -python_version = "3.8" +python_version = "3.14" strict = true enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"] warn_unreachable = true From d51b9c06d788943a8b10ca1e58a59e392c825b84 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Wed, 8 Oct 2025 18:11:13 +0200 Subject: [PATCH 3/4] Run ruff check --fix --- jupyter_core/utils/__init__.py | 3 ++- tests/test_migrate.py | 5 +++-- tests/test_paths.py | 6 +++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/jupyter_core/utils/__init__.py b/jupyter_core/utils/__init__.py index 5ffa5b9..8dbe5f0 100644 --- a/jupyter_core/utils/__init__.py +++ b/jupyter_core/utils/__init__.py @@ -9,10 +9,11 @@ import sys import threading import warnings +from collections.abc import Awaitable, Callable from contextvars import ContextVar from pathlib import Path from types import FrameType -from typing import Any, Awaitable, Callable, TypeVar, cast +from typing import Any, TypeVar, cast def ensure_dir_exists(path: str | Path, mode: int = 0o777) -> None: diff --git a/tests/test_migrate.py b/tests/test_migrate.py index 2beb2c8..0fe4d75 100644 --- a/tests/test_migrate.py +++ b/tests/test_migrate.py @@ -132,8 +132,9 @@ def notice_m_dir(src, dst): called["migrate_dir"] = True return migrate_dir(src, dst) - with patch.object(migrate_mod, "migrate_file", notice_m_file), patch.object( - migrate_mod, "migrate_dir", notice_m_dir + with ( + patch.object(migrate_mod, "migrate_file", notice_m_file), + patch.object(migrate_mod, "migrate_dir", notice_m_dir), ): assert migrate_one(src, dst) assert called == {"migrate_file": True} diff --git a/tests/test_paths.py b/tests/test_paths.py index 9984467..d626b7a 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -301,7 +301,7 @@ def test_jupyter_path_user_site(): ] ) ) - for p, v in zip(path, values): + for p, v in zip(path, values, strict=False): assert p == v @@ -358,7 +358,7 @@ def test_jupyter_config_path(): ] ) ) - for p, v in zip(path, values): + for p, v in zip(path, values, strict=False): assert p == v @@ -383,7 +383,7 @@ def test_jupyter_config_path_prefer_env(): ] ) ) - for p, v in zip(path, values): + for p, v in zip(path, values, strict=False): assert p == v From 5d8b4868310126dae9c032daf8e9eca20a87b107 Mon Sep 17 00:00:00 2001 From: M Bussonnier Date: Sat, 11 Oct 2025 09:11:18 +0200 Subject: [PATCH 4/4] Update precommit hooks to latest version This include updating some repo, renaming some hooks, and run them to fix the corresponding files. --- .pre-commit-config.yaml | 16 ++++++++-------- jupyter_core/application.py | 4 ++-- jupyter_core/command.py | 12 ++++++------ jupyter_core/paths.py | 27 +++++++++++++++------------ jupyter_core/troubleshoot.py | 4 ++-- pyproject.toml | 7 ++++++- tests/test_command.py | 4 ++-- tests/test_paths.py | 2 +- 8 files changed, 42 insertions(+), 34 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b22a1a..c6aefa8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-case-conflict - id: check-ast @@ -21,11 +21,11 @@ repos: - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.0 + rev: 0.34.0 hooks: - id: check-github-workflows - - repo: https://github.com/executablebooks/mdformat + - repo: https://github.com/hukkin/mdformat rev: 0.7.22 hooks: - id: mdformat @@ -39,13 +39,13 @@ repos: types_or: [yaml, html, json] - repo: https://github.com/adamchainz/blacken-docs - rev: "1.19.1" + rev: "1.20.0" hooks: - id: blacken-docs additional_dependencies: [black==23.7.0] - repo: https://github.com/pre-commit/mirrors-mypy - rev: "v1.15.0" + rev: "v1.18.2" hooks: - id: mypy files: jupyter_core @@ -67,16 +67,16 @@ repos: - id: rst-inline-touching-normal - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.11 + rev: v0.14.0 hooks: - - id: ruff + - id: ruff-check types_or: [python, jupyter] args: ["--fix", "--show-fixes"] - id: ruff-format types_or: [python, jupyter] - repo: https://github.com/scientific-python/cookie - rev: "2025.05.02" + rev: "2025.10.01" hooks: - id: sp-repo-review additional_dependencies: ["repo-review[cli]"] diff --git a/jupyter_core/application.py b/jupyter_core/application.py index b9c0166..99c77f2 100644 --- a/jupyter_core/application.py +++ b/jupyter_core/application.py @@ -177,7 +177,7 @@ def migrate_config(self) -> None: f_marker.close() return # so we must have already migrated -> bail out - from .migrate import get_ipython_dir, migrate + from .migrate import get_ipython_dir, migrate # noqa: PLC0415 # No IPython dir, nothing to migrate if not Path(get_ipython_dir()).exists(): @@ -264,7 +264,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: S606 + 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 9c9317d..b6a3876 100644 --- a/jupyter_core/command.py +++ b/jupyter_core/command.py @@ -43,7 +43,7 @@ def epilog(self, x: Any) -> None: def argcomplete(self) -> None: """Trigger auto-completion, if enabled""" try: - import argcomplete + import argcomplete # noqa: PLC0415 argcomplete.autocomplete(self) except ImportError: @@ -123,10 +123,10 @@ def _execvp(cmd: str, argv: list[str]) -> None: if cmd_path is None: msg = f"{cmd!r} not found" raise OSError(msg, errno.ENOENT) - p = Popen([cmd_path] + argv[1:]) # noqa: S603 + 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 + import signal # noqa: PLC0415 signal.signal(signal.SIGINT, signal.SIG_IGN) p.wait() @@ -203,7 +203,7 @@ def _evaluate_argcomplete(parser: JupyterParser) -> list[str]: try: # traitlets >= 5.8 provides some argcomplete support, # use helper methods to jump to argcomplete - from traitlets.config.argcomplete_config import ( + from traitlets.config.argcomplete_config import ( # noqa: PLC0415 get_argcomplete_cwords, increment_argcomplete_index, ) @@ -237,7 +237,7 @@ def main() -> None: # Avoids argparse gobbling up args passed to subcommand, such as `-h`. subcommand = argv[1] else: - args, opts = parser.parse_known_args() + args, _opts = parser.parse_known_args() subcommand = args.subcommand if args.version: print("Selected Jupyter core packages...") @@ -399,7 +399,7 @@ def main() -> None: sys.exit(str(e)) try: - _execvp(command, [command] + argv[2:]) + _execvp(command, [command, *argv[2:]]) except OSError as e: sys.exit(f"Error executing Jupyter command {subcommand!r}: {e}") diff --git a/jupyter_core/paths.py b/jupyter_core/paths.py index 34829f1..ba94a1e 100644 --- a/jupyter_core/paths.py +++ b/jupyter_core/paths.py @@ -15,9 +15,10 @@ import sys import tempfile import warnings +from collections.abc import Iterator from contextlib import contextmanager from pathlib import Path -from typing import Any, Iterator, Literal, Optional, overload +from typing import Any, overload import platformdirs @@ -43,10 +44,10 @@ def envset(name: str, default: bool = False) -> bool: ... @overload -def envset(name: str, default: Literal[None]) -> Optional[bool]: ... +def envset(name: str, default: None) -> bool | None: ... -def envset(name: str, default: Optional[bool] = False) -> Optional[bool]: +def envset(name: str, default: bool | None = False) -> bool | None: """Return the boolean value of a given environment variable. An environment variable is considered set if it is assigned to a value @@ -182,12 +183,14 @@ def jupyter_data_dir() -> str: if sys.platform == "darwin": return str(Path(home, "Library", "Jupyter")) + # Bug in mypy which thinks it's unreachable: https://github.com/python/mypy/issues/10773 if sys.platform == "win32": appdata = os.environ.get("APPDATA", None) if appdata: return str(Path(appdata, "jupyter").resolve()) return pjoin(jupyter_config_dir(), "data") # Linux, non-OS X Unix, AIX, etc. + # Bug in mypy which thinks it's unreachable: https://github.com/python/mypy/issues/10773 xdg = env.get("XDG_DATA_HOME", None) if not xdg: xdg = pjoin(home, ".local", "share") @@ -303,7 +306,7 @@ def jupyter_path(*subdirs: str) -> list[str]: if site.ENABLE_USER_SITE: # Check if site.getuserbase() exists to be compatible with virtualenv, # which often does not have this method. - userbase: Optional[str] + userbase: str | None userbase = site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE if userbase: @@ -400,7 +403,7 @@ def jupyter_config_path() -> list[str]: # Next is environment or user, depending on the JUPYTER_PREFER_ENV_PATH flag user = [jupyter_config_dir()] if site.ENABLE_USER_SITE: - userbase: Optional[str] + userbase: str | None # Check if site.getuserbase() exists to be compatible with virtualenv, # which often does not have this method. userbase = site.getuserbase() if hasattr(site, "getuserbase") else site.USER_BASE @@ -442,7 +445,7 @@ def exists(path: str) -> bool: return True -def is_file_hidden_win(abs_path: str, stat_res: Optional[Any] = None) -> bool: +def is_file_hidden_win(abs_path: str, stat_res: Any | None = None) -> bool: """Is a file hidden? This only checks the file itself; it should be called in combination with @@ -487,7 +490,7 @@ def is_file_hidden_win(abs_path: str, stat_res: Optional[Any] = None) -> bool: return False -def is_file_hidden_posix(abs_path: str, stat_res: Optional[Any] = None) -> bool: +def is_file_hidden_posix(abs_path: str, stat_res: Any | None = None) -> bool: """Is a file hidden? This only checks the file itself; it should be called in combination with @@ -602,12 +605,12 @@ def win32_restrict_file_to_user(fname: str) -> None: The path to the file to secure """ try: - import win32api + import win32api # noqa: PLC0415 except ImportError: return _win32_restrict_file_to_user_ctypes(fname) - import ntsecuritycon as con - import win32security + import ntsecuritycon as con # noqa: PLC0415 + import win32security # noqa: PLC0415 # everyone, _domain, _type = win32security.LookupAccountName("", "Everyone") admins = win32security.CreateWellKnownSid(win32security.WinBuiltinAdministratorsSid) @@ -646,8 +649,8 @@ def _win32_restrict_file_to_user_ctypes(fname: str) -> None: fname : unicode The path to the file to secure """ - import ctypes - from ctypes import wintypes + import ctypes # noqa: PLC0415 + from ctypes import wintypes # noqa: PLC0415 advapi32 = ctypes.WinDLL("advapi32", use_last_error=True) # type:ignore[attr-defined] secur32 = ctypes.WinDLL("secur32", use_last_error=True) # type:ignore[attr-defined] diff --git a/jupyter_core/troubleshoot.py b/jupyter_core/troubleshoot.py index cd64c3d..db376da 100755 --- a/jupyter_core/troubleshoot.py +++ b/jupyter_core/troubleshoot.py @@ -10,10 +10,10 @@ import platform import subprocess import sys -from typing import Any, Optional, Union +from typing import Any, Union -def subs(cmd: Union[list[str], str]) -> Optional[str]: +def subs(cmd: Union[list[str], str]) -> str | None: """ get data from commands that we need to run outside of python """ diff --git a/pyproject.toml b/pyproject.toml index b8eaba7..4224847 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,7 @@ dependencies = ["pre-commit"] detached = true [tool.hatch.envs.lint.scripts] build = [ - "pre-commit run --all-files ruff", + "pre-commit run --all-files ruff-check", "pre-commit run --all-files ruff-format" ] @@ -111,6 +111,11 @@ disallow_incomplete_defs = true disallow_untyped_defs = true warn_redundant_casts = true disallow_untyped_calls = true +# This is a workaround for a mypy bug which is incapable of treating sys.platform +# correctly https://github.com/python/mypy/issues/10773 +# With pre-commit running on user platform and linux on CI you can't get the +# types right, so we pin platform to linux +platform = "linux" [tool.pytest.ini_options] minversion = "7.0" diff --git a/tests/test_command.py b/tests/test_command.py index 30066ed..87945fb 100644 --- a/tests/test_command.py +++ b/tests/test_command.py @@ -65,7 +65,7 @@ def write_executable(path, source): if sys.platform == "win32": try: - import importlib.resources + import importlib.resources # noqa: PLC0415 if not hasattr(importlib.resources, "files"): raise ImportError @@ -152,7 +152,7 @@ def test_subcommand_not_found(): assert "Jupyter command `jupyter-nonexistant-subcommand` not found." in stderr -@patch.object(sys, "argv", [__file__] + sys.argv[1:]) +@patch.object(sys, "argv", [__file__, *sys.argv[1:]]) def test_subcommand_list(tmpdir): a = tmpdir.mkdir("a") for cmd in ("jupyter-foo-bar", "jupyter-xyz", "jupyter-babel-fish"): diff --git a/tests/test_paths.py b/tests/test_paths.py index d626b7a..b83b477 100644 --- a/tests/test_paths.py +++ b/tests/test_paths.py @@ -499,7 +499,7 @@ 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: F401 + import ctypes # noqa: F401, PLC0415 with tempfile.TemporaryDirectory() as root: subdir1 = os.path.join(root, "subdir")