Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ jobs:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.8", "3.12", "3.13"]
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:
Expand Down
16 changes: 8 additions & 8 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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]"]
4 changes: 2 additions & 2 deletions jupyter_core/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 6 additions & 6 deletions jupyter_core/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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...")
Expand Down Expand Up @@ -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}")

Expand Down
27 changes: 15 additions & 12 deletions jupyter_core/paths.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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]
Expand Down
4 changes: 2 additions & 2 deletions jupyter_core/troubleshoot.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down
15 changes: 13 additions & 2 deletions jupyter_core/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -176,7 +177,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,
Copy link
Contributor

@Darshan808 Darshan808 Oct 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
DeprecationWarning,
category=DeprecationWarning,

This is causing TypeError in downstream package jupyterlab_server when testing on Python 3.14 on Windows.
CC: @Carreau

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opened #449

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)
Expand Down
11 changes: 8 additions & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -97,20 +97,25 @@ 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"
]

[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
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"
Expand Down
2 changes: 2 additions & 0 deletions tests/test_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import asyncio
import os
import shutil
import sys
from tempfile import mkdtemp
from unittest.mock import patch

Expand Down Expand Up @@ -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()
Expand Down
4 changes: 2 additions & 2 deletions tests/test_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"):
Expand Down
5 changes: 3 additions & 2 deletions tests/test_migrate.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading