From 4dbf76fed2b49151990ae1d8fb02486e67308e09 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:58:06 +0000 Subject: [PATCH 1/5] Align ruff versions, add some ignores --- .pre-commit-config.yaml | 3 ++- pyproject.toml | 9 +++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0d72a2d239..153066f34b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,7 +61,8 @@ repos: ["traitlets>=5.13", "jupyter_core>=5.5", "jupyter_client>=8.5"] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.5.0 + # keep the revision in sync with the ruff version in pyproject.toml + rev: v0.14.6 hooks: - id: ruff types_or: [python, jupyter] diff --git a/pyproject.toml b/pyproject.toml index 81fbb5bb7f..f97deedf1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -130,6 +130,10 @@ skip-if-exists = ["jupyter_server/static/style/bootstrap.min.css"] install-pre-commit-hook = true optional-editable-build = true +[tool.hatch.envs.hatch-static-analysis] +# keep this version in sync with pre-commit version +dependencies = ["ruff==0.14.6"] + [tool.ruff] line-length = 100 @@ -137,7 +141,7 @@ line-length = 100 docstring-code-format = true [tool.ruff.lint] -ignore = ["ARG", "TRY", "RUF012", "TID252", "G", "INP001", "E402", "F401", "BLE001"] +ignore = ["ARG", "TRY", "RUF012", "TID252", "G", "INP001", "E402", "F401", "BLE001", "UP045"] extend-select = [ "B", # flake8-bugbear "I", # isort @@ -158,7 +162,8 @@ unfixable = [ "SIM105", "A001", "UP007", "PLR2004", "T201", "N818", "F403"] "jupyter_server/gateway/*" = ["TCH" ] "tests/*" = ["UP031", "PT", 'EM', "TRY", "RET", "SLF", "C408", "F841", "FBT", "A002", "FLY", "N", - "PERF", "ASYNC", "T201", "FA100", "E711", "INP", "TCH", "SIM105", "A001", "PLW0603"] + "PERF", "ASYNC", "T201", "FA100", "E711", "INP", "TCH", "SIM105", "A001", "PLW0603", + "PLC0415", "RUF059"] "examples/*_config.py" = ["F821"] "examples/*" = ["N815"] From c7ec378593e2bb91b63d2e7a8ce69140be02aedf Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:58:48 +0000 Subject: [PATCH 2/5] Apply safe ruff auto-fixes --- jupyter_server/__init__.py | 6 ++--- jupyter_server/auth/decorator.py | 8 +++--- jupyter_server/auth/identity.py | 6 ++--- jupyter_server/base/handlers.py | 26 +++++++++---------- jupyter_server/extension/handler.py | 20 +++++++------- jupyter_server/gateway/managers.py | 2 +- jupyter_server/prometheus/metrics.py | 2 +- jupyter_server/serverapp.py | 9 +++---- jupyter_server/services/api/handlers.py | 2 +- jupyter_server/services/events/handlers.py | 6 ++--- .../services/kernels/connection/channels.py | 2 +- .../services/kernels/kernelmanager.py | 4 +-- .../services/sessions/sessionmanager.py | 4 +-- jupyter_server/utils.py | 3 +-- tests/services/contents/test_manager.py | 2 +- .../test_serverapp_integration.py | 4 +-- 16 files changed, 52 insertions(+), 54 deletions(-) diff --git a/jupyter_server/__init__.py b/jupyter_server/__init__.py index 9b4cf72ea8..ca99f30fe0 100644 --- a/jupyter_server/__init__.py +++ b/jupyter_server/__init__.py @@ -17,12 +17,12 @@ from .base.call_context import CallContext __all__ = [ + "DEFAULT_EVENTS_SCHEMA_PATH", + "DEFAULT_JUPYTER_SERVER_PORT", "DEFAULT_STATIC_FILES_PATH", "DEFAULT_TEMPLATE_PATH_LIST", - "DEFAULT_JUPYTER_SERVER_PORT", "JUPYTER_SERVER_EVENTS_URI", - "DEFAULT_EVENTS_SCHEMA_PATH", + "CallContext", "__version__", "version_info", - "CallContext", ] diff --git a/jupyter_server/auth/decorator.py b/jupyter_server/auth/decorator.py index 4128c39086..daedb9061c 100644 --- a/jupyter_server/auth/decorator.py +++ b/jupyter_server/auth/decorator.py @@ -82,9 +82,9 @@ async def inner(self, *args, **kwargs): method = action action = None # no-arguments `@authorized` decorator called - return cast(FuncT, wrapper(method)) + return cast("FuncT", wrapper(method)) - return cast(FuncT, wrapper) + return cast("FuncT", wrapper) def allow_unauthenticated(method: FuncT) -> FuncT: @@ -111,7 +111,7 @@ def wrapper(self, *args, **kwargs): setattr(wrapper, "__allow_unauthenticated", True) - return cast(FuncT, wrapper) + return cast("FuncT", wrapper) def ws_authenticated(method: FuncT) -> FuncT: @@ -139,4 +139,4 @@ def wrapper(self, *args, **kwargs): setattr(wrapper, "__allow_unauthenticated", False) - return cast(FuncT, wrapper) + return cast("FuncT", wrapper) diff --git a/jupyter_server/auth/identity.py b/jupyter_server/auth/identity.py index fc4b029922..96b9e4cc31 100644 --- a/jupyter_server/auth/identity.py +++ b/jupyter_server/auth/identity.py @@ -252,7 +252,7 @@ async def _get_user(self, handler: web.RequestHandler) -> User | None: """Get the user.""" if getattr(handler, "_jupyter_current_user", None): # already authenticated - return t.cast(User, handler._jupyter_current_user) # type:ignore[attr-defined] + return t.cast("User", handler._jupyter_current_user) # type:ignore[attr-defined] _token_user: User | None | t.Awaitable[User | None] = self.get_user_token(handler) if isinstance(_token_user, t.Awaitable): _token_user = await _token_user @@ -298,7 +298,7 @@ def update_user( ) -> User: """Update user information and persist the user model.""" self.check_update(user_data) - current_user = t.cast(User, handler.current_user) + current_user = t.cast("User", handler.current_user) updated_user = self.update_user_model(current_user, user_data) self.persist_user_model(handler) return updated_user @@ -585,7 +585,7 @@ def process_login_form(self, handler: web.RequestHandler) -> User | None: return self.generate_anonymous_user(handler) if self.token and self.token == typed_password: - return t.cast(User, self.user_for_token(typed_password)) # type:ignore[attr-defined] + return t.cast("User", self.user_for_token(typed_password)) # type:ignore[attr-defined] return user diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 3909c70638..1ceafc14f9 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -75,7 +75,7 @@ def json_sys_info(): def log() -> Logger: """Get the application log.""" if Application.initialized(): - return cast(Logger, Application.instance().log) + return cast("Logger", Application.instance().log) else: return app_log @@ -85,7 +85,7 @@ class AuthenticatedHandler(web.RequestHandler): @property def base_url(self) -> str: - return cast(str, self.settings.get("base_url", "/")) + return cast("str", self.settings.get("base_url", "/")) @property def content_security_policy(self) -> str: @@ -95,7 +95,7 @@ def content_security_policy(self) -> str: """ if "Content-Security-Policy" in self.settings.get("headers", {}): # user-specified, don't override - return cast(str, self.settings["headers"]["Content-Security-Policy"]) + return cast("str", self.settings["headers"]["Content-Security-Policy"]) return "; ".join( [ @@ -173,7 +173,7 @@ def get_current_user(self) -> str: DeprecationWarning, stacklevel=2, ) - return cast(str, self._jupyter_current_user) + return cast("str", self._jupyter_current_user) # haven't called get_user in prepare, raise raise RuntimeError(msg) @@ -224,7 +224,7 @@ def login_available(self) -> bool: whether the user is already logged in or not. """ - return cast(bool, self.identity_provider.login_available) + return cast("bool", self.identity_provider.login_available) @property def authorizer(self) -> Authorizer: @@ -302,26 +302,26 @@ def serverapp(self) -> ServerApp | None: @property def version_hash(self) -> str: """The version hash to use for cache hints for static files""" - return cast(str, self.settings.get("version_hash", "")) + return cast("str", self.settings.get("version_hash", "")) @property def mathjax_url(self) -> str: - url = cast(str, self.settings.get("mathjax_url", "")) + url = cast("str", self.settings.get("mathjax_url", "")) if not url or url_is_absolute(url): return url return url_path_join(self.base_url, url) @property def mathjax_config(self) -> str: - return cast(str, self.settings.get("mathjax_config", "TeX-AMS-MML_HTMLorMML-full,Safe")) + return cast("str", self.settings.get("mathjax_config", "TeX-AMS-MML_HTMLorMML-full,Safe")) @property def default_url(self) -> str: - return cast(str, self.settings.get("default_url", "")) + return cast("str", self.settings.get("default_url", "")) @property def ws_url(self) -> str: - return cast(str, self.settings.get("websocket_url", "")) + return cast("str", self.settings.get("websocket_url", "")) @property def contents_js_source(self) -> str: @@ -329,7 +329,7 @@ def contents_js_source(self) -> str: "Using contents: %s", self.settings.get("contents_js_source", "services/contents"), ) - return cast(str, self.settings.get("contents_js_source", "services/contents")) + return cast("str", self.settings.get("contents_js_source", "services/contents")) # --------------------------------------------------------------- # Manager objects @@ -370,7 +370,7 @@ def event_logger(self) -> EventLogger: @property def allow_origin(self) -> str: """Normal Access-Control-Allow-Origin""" - return cast(str, self.settings.get("allow_origin", "")) + return cast("str", self.settings.get("allow_origin", "")) @property def allow_origin_pat(self) -> str | None: @@ -380,7 +380,7 @@ def allow_origin_pat(self) -> str | None: @property def allow_credentials(self) -> bool: """Whether to set Access-Control-Allow-Credentials""" - return cast(bool, self.settings.get("allow_credentials", False)) + return cast("bool", self.settings.get("allow_credentials", False)) def set_default_headers(self) -> None: """Add CORS headers, if defined""" diff --git a/jupyter_server/extension/handler.py b/jupyter_server/extension/handler.py index 4285c415b0..22ffc707c0 100644 --- a/jupyter_server/extension/handler.py +++ b/jupyter_server/extension/handler.py @@ -26,10 +26,10 @@ def get_template(self, name: str) -> Template: """Return the jinja template object for a given name""" try: env = f"{self.name}_jinja2_env" # type:ignore[attr-defined] - template = cast(Template, self.settings[env].get_template(name)) # type:ignore[attr-defined] + template = cast("Template", self.settings[env].get_template(name)) # type:ignore[attr-defined] return template except TemplateNotFound: - return cast(Template, super().get_template(name)) # type:ignore[misc] + return cast("Template", super().get_template(name)) # type:ignore[misc] class ExtensionHandlerMixin: @@ -64,12 +64,12 @@ def serverapp(self) -> ServerApp: @property def log(self) -> Logger: if not hasattr(self, "name"): - return cast(Logger, super().log) # type:ignore[misc] + return cast("Logger", super().log) # type:ignore[misc] # Attempt to pull the ExtensionApp's log, otherwise fall back to ServerApp. try: - return cast(Logger, self.extensionapp.log) + return cast("Logger", self.extensionapp.log) except AttributeError: - return cast(Logger, self.serverapp.log) + return cast("Logger", self.serverapp.log) @property def config(self) -> Config: @@ -81,7 +81,7 @@ def server_config(self) -> Config: @property def base_url(self) -> str: - return cast(str, self.settings.get("base_url", "/")) + return cast("str", self.settings.get("base_url", "/")) def render_template(self, name: str, **ns) -> str: """Override render template to handle static_paths @@ -90,12 +90,12 @@ def render_template(self, name: str, **ns) -> str: (e.g. default error pages) make sure our extension-specific static_url is _not_ used. """ - template = cast(Template, self.get_template(name)) # type:ignore[attr-defined] + template = cast("Template", self.get_template(name)) # type:ignore[attr-defined] ns.update(self.template_namespace) # type:ignore[attr-defined] if template.environment is self.settings["jinja2_env"]: # default template environment, use default static_url ns["static_url"] = super().static_url # type:ignore[misc] - return cast(str, template.render(**ns)) + return cast("str", template.render(**ns)) @property def static_url_prefix(self) -> str: @@ -103,7 +103,7 @@ def static_url_prefix(self) -> str: @property def static_path(self) -> str: - return cast(str, self.settings[f"{self.name}_static_paths"]) + return cast("str", self.settings[f"{self.name}_static_paths"]) def static_url(self, path: str, include_host: bool | None = None, **kwargs: Any) -> str: """Returns a static URL for the given relative static file path. @@ -151,4 +151,4 @@ def static_url(self, path: str, include_host: bool | None = None, **kwargs: Any) "static_url_prefix": self.static_url_prefix, } - return base + cast(str, get_url(settings, path, **kwargs)) + return base + cast("str", get_url(settings, path, **kwargs)) diff --git a/jupyter_server/gateway/managers.py b/jupyter_server/gateway/managers.py index daa6f99213..7845258de6 100644 --- a/jupyter_server/gateway/managers.py +++ b/jupyter_server/gateway/managers.py @@ -674,7 +674,7 @@ def stop(self) -> None: msgs.append(msg["msg_type"]) if self.channel_name == "iopub" and "shutdown_reply" in msgs: return - if len(msgs): + if msgs: self.log.warning( f"Stopping channel '{self.channel_name}' with {len(msgs)} unprocessed non-status messages: {msgs}." ) diff --git a/jupyter_server/prometheus/metrics.py b/jupyter_server/prometheus/metrics.py index 3340905375..50c7ecde60 100644 --- a/jupyter_server/prometheus/metrics.py +++ b/jupyter_server/prometheus/metrics.py @@ -72,7 +72,7 @@ __all__ = [ "HTTP_REQUEST_DURATION_SECONDS", - "TERMINAL_CURRENTLY_RUNNING_TOTAL", "KERNEL_CURRENTLY_RUNNING_TOTAL", "SERVER_INFO", + "TERMINAL_CURRENTLY_RUNNING_TOTAL", ] diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 1afbef4d0d..3aa2f8729e 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -1037,7 +1037,7 @@ def _default_ip(self) -> str: @validate("ip") def _validate_ip(self, proposal: t.Any) -> str: - value = t.cast(str, proposal["value"]) + value = t.cast("str", proposal["value"]) if value == "*": value = "" return value @@ -1539,7 +1539,7 @@ def _deprecated_cookie_config(self, change: t.Any) -> None: @validate("base_url") def _update_base_url(self, proposal: t.Any) -> str: - value = t.cast(str, proposal["value"]) + value = t.cast("str", proposal["value"]) if not value.startswith("/"): value = "/" + value if not value.endswith("/"): @@ -2335,8 +2335,7 @@ def init_resources(self) -> None: soft = self.min_open_files_limit hard = old_hard if soft is not None and old_soft < soft: - if hard < soft: - hard = soft + hard = max(hard, soft) self.log.debug( f"Raising open file limit: soft {old_soft}->{soft}; hard {old_hard}->{hard}" ) @@ -2870,7 +2869,7 @@ async def cleanup_extensions(self) -> None: def running_server_info(self, kernel_count: bool = True) -> str: """Return the current working directory and the server url information""" - info = t.cast(str, self.contents_manager.info_string()) + "\n" + info = t.cast("str", self.contents_manager.info_string()) + "\n" if kernel_count: n_kernels = len(self.kernel_manager.list_kernel_ids()) kernel_msg = trans.ngettext("%d active kernel", "%d active kernels", n_kernels) diff --git a/jupyter_server/services/api/handlers.py b/jupyter_server/services/api/handlers.py index 609d68601f..18293839f2 100644 --- a/jupyter_server/services/api/handlers.py +++ b/jupyter_server/services/api/handlers.py @@ -120,7 +120,7 @@ async def get(self): @web.authenticated async def patch(self): """Update user information.""" - user_data = cast(dict[UpdatableField, str], self.get_json_body()) + user_data = cast("dict[UpdatableField, str]", self.get_json_body()) if not user_data: raise web.HTTPError(400, "Invalid or missing JSON body") diff --git a/jupyter_server/services/events/handlers.py b/jupyter_server/services/events/handlers.py index fbc007341d..1f1aa1cfd5 100644 --- a/jupyter_server/services/events/handlers.py +++ b/jupyter_server/services/events/handlers.py @@ -81,12 +81,12 @@ def validate_model( if key not in data: message = f"Missing `{key}` in the JSON request body." raise Exception(message) - schema_id = cast(str, data.get("schema_id")) + schema_id = cast("str", data.get("schema_id")) # The case where a given schema_id isn't found, # jupyter_events raises a useful error, so there's no need to # handle that case here. schema = registry.get(schema_id) - version = str(cast(str, data.get("version"))) + version = str(cast("str", data.get("version"))) if schema.version != version: message = f"Unregistered version: {version!r}≠{schema.version!r} for `{schema_id}`" raise Exception(message) @@ -126,7 +126,7 @@ async def post(self): try: validate_model(payload, self.event_logger.schemas) self.event_logger.emit( - schema_id=cast(str, payload.get("schema_id")), + schema_id=cast("str", payload.get("schema_id")), data=cast("dict[str, Any]", payload.get("data")), timestamp_override=get_timestamp(payload), ) diff --git a/jupyter_server/services/kernels/connection/channels.py b/jupyter_server/services/kernels/connection/channels.py index 78f2dc126e..fc07ed3565 100644 --- a/jupyter_server/services/kernels/connection/channels.py +++ b/jupyter_server/services/kernels/connection/channels.py @@ -288,7 +288,7 @@ async def _register_session(self): self.kernel_id in self.multi_kernel_manager ): # only update open sessions if kernel is actively managed self._open_sessions[self.session_key] = t.cast( - KernelWebsocketHandler, self.websocket_handler + "KernelWebsocketHandler", self.websocket_handler ) async def prepare(self): diff --git a/jupyter_server/services/kernels/kernelmanager.py b/jupyter_server/services/kernels/kernelmanager.py index 8f4e8277f9..81b0e1c680 100644 --- a/jupyter_server/services/kernels/kernelmanager.py +++ b/jupyter_server/services/kernels/kernelmanager.py @@ -10,7 +10,7 @@ import asyncio import os -import pathlib # noqa: TCH003 +import pathlib # noqa: TC003 import sys import typing as t import warnings @@ -247,7 +247,7 @@ async def _async_start_kernel( # type:ignore[override] self.log.debug( "Kernel args (excluding env): %r", {k: v for k, v in kwargs.items() if k != "env"} ) - env = kwargs.get("env", None) + env = kwargs.get("env") if env and isinstance(env, dict): # type:ignore[unreachable] self.log.debug("Kernel argument 'env' passed with: %r", list(env.keys())) # type:ignore[unreachable] diff --git a/jupyter_server/services/sessions/sessionmanager.py b/jupyter_server/services/sessions/sessionmanager.py index 3aac78a0a9..a11a527645 100644 --- a/jupyter_server/services/sessions/sessionmanager.py +++ b/jupyter_server/services/sessions/sessionmanager.py @@ -291,7 +291,7 @@ async def create_session( session_id, path=path, name=name, type=type, kernel_id=kernel_id ) self._pending_sessions.remove(record) - return cast(dict[str, Any], result) + return cast("dict[str, Any]", result) def get_kernel_env( self, path: Optional[str], name: Optional[ModelName] = None @@ -345,7 +345,7 @@ async def start_kernel_for_session( kernel_name=kernel_name, env=kernel_env, ) - return cast(str, kernel_id) + return cast("str", kernel_id) async def save_session(self, session_id, path=None, name=None, type=None, kernel_id=None): """Saves the items for the session with the given session_id diff --git a/jupyter_server/utils.py b/jupyter_server/utils.py index d83e1be880..ea241e4f1f 100644 --- a/jupyter_server/utils.py +++ b/jupyter_server/utils.py @@ -146,8 +146,7 @@ def to_api_path(os_path: str, root: str = "") -> ApiPath: If given, root will be removed from the path. root must be a filesystem path already. """ - if os_path.startswith(root): - os_path = os_path[len(root) :] + os_path = os_path.removeprefix(root) parts = os_path.strip(os.path.sep).split(os.path.sep) parts = [p for p in parts if p != ""] # remove duplicate splits path = "/".join(parts) diff --git a/tests/services/contents/test_manager.py b/tests/services/contents/test_manager.py index ef8d40a28e..22374d2f1c 100644 --- a/tests/services/contents/test_manager.py +++ b/tests/services/contents/test_manager.py @@ -188,7 +188,7 @@ def test_invalid_root_dir(jp_file_contents_manager_class, tmp_path): def test_get_os_path(jp_file_contents_manager_class, tmp_path): fm = jp_file_contents_manager_class(root_dir=str(tmp_path)) path = fm._get_os_path("/path/to/notebook/test.ipynb") - rel_path_list = "/path/to/notebook/test.ipynb".split("/") + rel_path_list = ["", "path", "to", "notebook", "test.ipynb"] fs_path = os.path.join(fm.root_dir, *rel_path_list) assert path == fs_path diff --git a/tests/unix_sockets/test_serverapp_integration.py b/tests/unix_sockets/test_serverapp_integration.py index f60c99b1bc..107c1b5bd2 100644 --- a/tests/unix_sockets/test_serverapp_integration.py +++ b/tests/unix_sockets/test_serverapp_integration.py @@ -202,7 +202,7 @@ def test_shutdown_server(jp_environ): servers = [] while 1: servers = list(list_running_servers()) - if len(servers): + if servers: break time.sleep(0.1) while 1: @@ -227,7 +227,7 @@ def test_jupyter_server_apps(jp_environ): servers = [] while 1: servers = list(list_running_servers()) - if len(servers): + if servers: break time.sleep(0.1) From b8d55281b7c688cfb421f363c31f05513dade6b0 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:12:25 +0000 Subject: [PATCH 3/5] Apply more fixes/add noqa based on judgment --- .../authorization/jupyter_nbclassic_readonly_config.py | 4 +--- examples/authorization/jupyter_nbclassic_rw_config.py | 4 +--- examples/authorization/jupyter_temporary_config.py | 4 +--- jupyter_server/base/handlers.py | 9 +++++---- jupyter_server/extension/handler.py | 5 +++-- jupyter_server/gateway/gateway_client.py | 2 +- jupyter_server/kernelspecs/handlers.py | 2 +- jupyter_server/log.py | 2 +- jupyter_server/nbconvert/handlers.py | 2 +- jupyter_server/serverapp.py | 2 +- jupyter_server/services/contents/fileio.py | 4 ++-- jupyter_server/services/contents/filemanager.py | 4 ++-- jupyter_server/services/contents/largefilemanager.py | 2 +- jupyter_server/services/kernels/connection/channels.py | 2 +- jupyter_server/services/kernels/kernelmanager.py | 2 +- jupyter_server/services/sessions/sessionmanager.py | 2 +- jupyter_server/traittypes.py | 3 +-- pyproject.toml | 4 ++-- tests/extension/test_app.py | 6 +++--- tests/services/kernels/test_cull.py | 2 +- tests/test_gateway.py | 6 +++--- tests/utils.py | 4 +--- 22 files changed, 35 insertions(+), 42 deletions(-) diff --git a/examples/authorization/jupyter_nbclassic_readonly_config.py b/examples/authorization/jupyter_nbclassic_readonly_config.py index 95b095fd26..2dca22a159 100644 --- a/examples/authorization/jupyter_nbclassic_readonly_config.py +++ b/examples/authorization/jupyter_nbclassic_readonly_config.py @@ -8,9 +8,7 @@ class ReadOnly(Authorizer): def is_authorized(self, handler, user, action, resource): """Only allows `read` operations.""" - if action != "read": - return False - return True + return action == "read" c.ServerApp.authorizer_class = ReadOnly # type:ignore[name-defined] diff --git a/examples/authorization/jupyter_nbclassic_rw_config.py b/examples/authorization/jupyter_nbclassic_rw_config.py index 751cef64a8..65baa81466 100644 --- a/examples/authorization/jupyter_nbclassic_rw_config.py +++ b/examples/authorization/jupyter_nbclassic_rw_config.py @@ -8,9 +8,7 @@ class ReadWriteOnly(Authorizer): def is_authorized(self, handler, user, action, resource): """Only allows `read` and `write` operations.""" - if action not in {"read", "write"}: - return False - return True + return action in {"read", "write"} c.ServerApp.authorizer_class = ReadWriteOnly # type:ignore[name-defined] diff --git a/examples/authorization/jupyter_temporary_config.py b/examples/authorization/jupyter_temporary_config.py index 9756cafe7d..081816a401 100644 --- a/examples/authorization/jupyter_temporary_config.py +++ b/examples/authorization/jupyter_temporary_config.py @@ -8,9 +8,7 @@ class TemporaryServerPersonality(Authorizer): def is_authorized(self, handler, user, action, resource): """Allow everything but write on contents""" - if action == "write" and resource == "contents": - return False - return True + return not (action == "write" and resource == "contents") c.ServerApp.authorizer_class = TemporaryServerPersonality # type:ignore[name-defined] diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 1ceafc14f9..6550689eef 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -15,7 +15,6 @@ import warnings from collections.abc import Awaitable, Coroutine, Sequence from http.client import responses -from logging import Logger from typing import TYPE_CHECKING, Any, cast from urllib.parse import urlparse @@ -44,6 +43,8 @@ ) if TYPE_CHECKING: + from logging import Logger + from jupyter_client.kernelspec import KernelSpecManager from jupyter_events import EventLogger from jupyter_server_terminals.terminalmanager import TerminalManager @@ -774,7 +775,7 @@ def write_error(self, status_code: int, **kwargs: Any) -> None: # backward-compatibility: traceback field is present, # but always empty reply["traceback"] = "" - self.log.warning("wrote error: %r", reply["message"], exc_info=True) + self.log.warning("wrote error: %r", reply["message"]) self.finish(json.dumps(reply)) def get_login_url(self) -> str: @@ -1055,7 +1056,7 @@ def get_absolute_path(cls, roots: Sequence[str], path: str) -> str: log().debug(f"Path {path} served from {abspath}") return abspath - def validate_absolute_path(self, root: str, absolute_path: str) -> str | None: + def validate_absolute_path(self, _root: str, absolute_path: str) -> str | None: """check if the file should be served (raises 404, 403, etc.)""" if not absolute_path: raise web.HTTPError(404) @@ -1115,7 +1116,7 @@ class FilesRedirectHandler(JupyterHandler): """Handler for redirecting relative URLs to the /files/ handler""" @staticmethod - async def redirect_to_files(self: Any, path: str) -> None: + async def redirect_to_files(self: Any, path: str) -> None: # noqa: PLW0211 """make redirect logic a reusable static method so it can be called from other handlers. diff --git a/jupyter_server/extension/handler.py b/jupyter_server/extension/handler.py index 22ffc707c0..a0e6b850e7 100644 --- a/jupyter_server/extension/handler.py +++ b/jupyter_server/extension/handler.py @@ -2,15 +2,16 @@ from __future__ import annotations -from logging import Logger from typing import TYPE_CHECKING, Any, cast -from jinja2 import Template from jinja2.exceptions import TemplateNotFound from jupyter_server.base.handlers import FileFindHandler if TYPE_CHECKING: + from logging import Logger + + from jinja2 import Template from traitlets.config import Config from jupyter_server.extension.application import ExtensionApp diff --git a/jupyter_server/gateway/gateway_client.py b/jupyter_server/gateway/gateway_client.py index 533836730f..3c0bfb7975 100644 --- a/jupyter_server/gateway/gateway_client.py +++ b/jupyter_server/gateway/gateway_client.py @@ -534,7 +534,7 @@ def gateway_enabled(self): return bool(self.url is not None and len(self.url) > 0) # Ensure KERNEL_LAUNCH_TIMEOUT has a default value. - KERNEL_LAUNCH_TIMEOUT = int(os.environ.get("KERNEL_LAUNCH_TIMEOUT", 40)) + KERNEL_LAUNCH_TIMEOUT = int(os.environ.get("KERNEL_LAUNCH_TIMEOUT", "40")) _connection_args: dict[str, ty.Any] # initialized on first use diff --git a/jupyter_server/kernelspecs/handlers.py b/jupyter_server/kernelspecs/handlers.py index 650982c76d..f273da233a 100644 --- a/jupyter_server/kernelspecs/handlers.py +++ b/jupyter_server/kernelspecs/handlers.py @@ -29,7 +29,7 @@ async def get(self, kernel_name, path, include_body=True): """Get a kernelspec resource.""" ksm = self.kernel_spec_manager if path.lower().endswith(".png"): - self.set_header("Cache-Control", f"max-age={60*60*24*30}") + self.set_header("Cache-Control", f"max-age={60 * 60 * 24 * 30}") ksm = self.kernel_spec_manager if hasattr(ksm, "get_kernel_spec_resource"): # If the kernel spec manager defines a method to get kernelspec resources, diff --git a/jupyter_server/log.py b/jupyter_server/log.py index 14eef42aad..03c928ec17 100644 --- a/jupyter_server/log.py +++ b/jupyter_server/log.py @@ -33,7 +33,7 @@ def _scrub_uri(uri: str, extra_param_keys=None) -> str: parts = parsed.query.split("&") changed = False for i, s in enumerate(parts): - key, sep, value = s.partition("=") + key, sep, _value = s.partition("=") for substring in scrub_param_keys: if substring in key: parts[i] = f"{key}{sep}[secret]" diff --git a/jupyter_server/nbconvert/handlers.py b/jupyter_server/nbconvert/handlers.py index d0a17ba99b..e474ac2785 100644 --- a/jupyter_server/nbconvert/handlers.py +++ b/jupyter_server/nbconvert/handlers.py @@ -104,7 +104,7 @@ async def get(self, format, path): # give its path to nbconvert. if hasattr(self.contents_manager, "_get_os_path"): os_path = self.contents_manager._get_os_path(path) - ext_resources_dir, basename = os.path.split(os_path) + ext_resources_dir, _basename = os.path.split(os_path) else: ext_resources_dir = None diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index 3aa2f8729e..748c3e83c4 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -2472,7 +2472,7 @@ def _confirm_exit(self) -> None: no = _i18n("n") sys.stdout.write(_i18n("Shut down this Jupyter server (%s/[%s])? ") % (yes, no)) sys.stdout.flush() - r, w, x = select.select([sys.stdin], [], [], 5) + r, _w, _x = select.select([sys.stdin], [], [], 5) if r: line = sys.stdin.readline() if line.lower().startswith(yes) and no not in line.lower(): diff --git a/jupyter_server/services/contents/fileio.py b/jupyter_server/services/contents/fileio.py index 3b5e042812..d0833b7d35 100644 --- a/jupyter_server/services/contents/fileio.py +++ b/jupyter_server/services/contents/fileio.py @@ -42,7 +42,7 @@ def copy2_safe(src, dst, log=None): # if src file is not writable, avoid creating a back-up if not os.access(src, os.W_OK): if log: - log.debug("Source file, %s, is not writable", src, exc_info=True) + log.debug("Source file, %s, is not writable", src) raise PermissionError(errno.EACCES, f"File is not writable: {src}") shutil.copyfile(src, dst) @@ -60,7 +60,7 @@ async def async_copy2_safe(src, dst, log=None): """ if not os.access(src, os.W_OK): if log: - log.debug("Source file, %s, is not writable", src, exc_info=True) + log.debug("Source file, %s, is not writable", src) raise PermissionError(errno.EACCES, f"File is not writable: {src}") await run_sync(shutil.copyfile, src, dst) diff --git a/jupyter_server/services/contents/filemanager.py b/jupyter_server/services/contents/filemanager.py index 3b84446a17..a49b992342 100644 --- a/jupyter_server/services/contents/filemanager.py +++ b/jupyter_server/services/contents/filemanager.py @@ -669,7 +669,7 @@ def _copy_dir(self, from_path, to_path_original, to_name, to_path): """ try: os_from_path = self._get_os_path(from_path.strip("/")) - os_to_path = f'{self._get_os_path(to_path_original.strip("/"))}/{to_name}' + os_to_path = f"{self._get_os_path(to_path_original.strip('/'))}/{to_name}" shutil.copytree(os_from_path, os_to_path) model = self.get(to_path, content=False) except OSError as err: @@ -1154,7 +1154,7 @@ async def _copy_dir( """ try: os_from_path = self._get_os_path(from_path.strip("/")) - os_to_path = f'{self._get_os_path(to_path_original.strip("/"))}/{to_name}' + os_to_path = f"{self._get_os_path(to_path_original.strip('/'))}/{to_name}" shutil.copytree(os_from_path, os_to_path) model = await self.get(to_path, content=False) except OSError as err: diff --git a/jupyter_server/services/contents/largefilemanager.py b/jupyter_server/services/contents/largefilemanager.py index 78f0d55629..1515ef2943 100644 --- a/jupyter_server/services/contents/largefilemanager.py +++ b/jupyter_server/services/contents/largefilemanager.py @@ -151,5 +151,5 @@ async def _save_large_file(self, os_path, content, format): with self.perm_to_403(os_path): if os.path.islink(os_path): os_path = os.path.join(os.path.dirname(os_path), os.readlink(os_path)) - with open(os_path, "ab") as f: # noqa: ASYNC101 + with open(os_path, "ab") as f: # noqa: ASYNC230 await run_sync(f.write, bcontent) diff --git a/jupyter_server/services/kernels/connection/channels.py b/jupyter_server/services/kernels/connection/channels.py index fc07ed3565..bde7f2fc9f 100644 --- a/jupyter_server/services/kernels/connection/channels.py +++ b/jupyter_server/services/kernels/connection/channels.py @@ -599,7 +599,7 @@ def _handle_kernel_info_reply(self, msg): enabling msg spec adaptation, if necessary """ - idents, msg = self.session.feed_identities(msg) + _idents, msg = self.session.feed_identities(msg) try: msg = self.session.deserialize(msg) except BaseException: diff --git a/jupyter_server/services/kernels/kernelmanager.py b/jupyter_server/services/kernels/kernelmanager.py index 81b0e1c680..6df8ce22bb 100644 --- a/jupyter_server/services/kernels/kernelmanager.py +++ b/jupyter_server/services/kernels/kernelmanager.py @@ -598,7 +598,7 @@ def start_watching_activity(self, kernel_id): def record_activity(msg_list): """Record an IOPub message arriving from a kernel""" - idents, fed_msg_list = session.feed_identities(msg_list) + _idents, fed_msg_list = session.feed_identities(msg_list) msg = session.deserialize(fed_msg_list, content=False) msg_type = msg["header"]["msg_type"] diff --git a/jupyter_server/services/sessions/sessionmanager.py b/jupyter_server/services/sessions/sessionmanager.py index a11a527645..f02e04bc4b 100644 --- a/jupyter_server/services/sessions/sessionmanager.py +++ b/jupyter_server/services/sessions/sessionmanager.py @@ -33,7 +33,7 @@ class KernelSessionRecordConflict(Exception): @dataclass -class KernelSessionRecord: +class KernelSessionRecord: # noqa: PLW1641 - TODO: implement __hash__ """A record object for tracking a Jupyter Server Kernel Session. Two records that share a session_id must also share a kernel_id, while diff --git a/jupyter_server/traittypes.py b/jupyter_server/traittypes.py index c1537d353f..b2394490fa 100644 --- a/jupyter_server/traittypes.py +++ b/jupyter_server/traittypes.py @@ -164,8 +164,7 @@ class or its subclasses. Our implementation is quite different self.klasses = klasses else: raise TraitError( - "The klasses attribute must be a list of class names or classes" - " not: %r" % klasses + "The klasses attribute must be a list of class names or classes not: %r" % klasses ) if (kw is not None) and not isinstance(kw, dict): diff --git a/pyproject.toml b/pyproject.toml index f97deedf1c..0653f67ef3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -141,7 +141,7 @@ line-length = 100 docstring-code-format = true [tool.ruff.lint] -ignore = ["ARG", "TRY", "RUF012", "TID252", "G", "INP001", "E402", "F401", "BLE001", "UP045"] +ignore = ["ARG", "TRY", "RUF012", "TID252", "G", "INP001", "E402", "F401", "BLE001", "UP045", "PLC0415"] extend-select = [ "B", # flake8-bugbear "I", # isort @@ -163,7 +163,7 @@ unfixable = [ "jupyter_server/gateway/*" = ["TCH" ] "tests/*" = ["UP031", "PT", 'EM', "TRY", "RET", "SLF", "C408", "F841", "FBT", "A002", "FLY", "N", "PERF", "ASYNC", "T201", "FA100", "E711", "INP", "TCH", "SIM105", "A001", "PLW0603", - "PLC0415", "RUF059"] + "RUF059"] "examples/*_config.py" = ["F821"] "examples/*" = ["N815"] diff --git a/tests/extension/test_app.py b/tests/extension/test_app.py index 21275b6d8c..b7844545d6 100644 --- a/tests/extension/test_app.py +++ b/tests/extension/test_app.py @@ -143,9 +143,9 @@ async def test_load_parallel_extensions(monkeypatch, jp_environ): async def test_start_extension(jp_serverapp, mock_extension): await jp_serverapp._post_start() assert mock_extension.started - assert hasattr( - jp_serverapp, "mock1_started" - ), "Failed because the `_start_jupyter_server_extension` function in 'mock1.py' was never called" + assert hasattr(jp_serverapp, "mock1_started"), ( + "Failed because the `_start_jupyter_server_extension` function in 'mock1.py' was never called" + ) assert jp_serverapp.mock1_started diff --git a/tests/services/kernels/test_cull.py b/tests/services/kernels/test_cull.py index 5b0b8fd9a0..482eba0979 100644 --- a/tests/services/kernels/test_cull.py +++ b/tests/services/kernels/test_cull.py @@ -151,7 +151,7 @@ async def test_cull_connected(jp_fetch, jp_ws_fetch): "parent_header": {}, "metadata": {}, "content": { - "code": f"import time\ntime.sleep({CULL_TIMEOUT-1})", + "code": f"import time\ntime.sleep({CULL_TIMEOUT - 1})", "silent": False, "allow_stdin": False, "stop_on_error": True, diff --git a/tests/test_gateway.py b/tests/test_gateway.py index bf774bb431..6f4fcebc1a 100644 --- a/tests/test_gateway.py +++ b/tests/test_gateway.py @@ -766,9 +766,9 @@ async def test_websocket_connection_with_session_id(init_gateway, jp_serverapp, expected_ws_url = ( f"{mock_gateway_ws_url}/api/kernels/{kernel_id}/channels?session_id={conn.session_id}" ) - assert ( - expected_ws_url in caplog.text - ), "WebSocket URL does not contain the expected session_id." + assert expected_ws_url in caplog.text, ( + "WebSocket URL does not contain the expected session_id." + ) # Processing websocket messages happens in separate coroutines and any # errors in that process will show up in logs, but not bubble up to the diff --git a/tests/utils.py b/tests/utils.py index 0ca9c008f2..216af8e2de 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -27,9 +27,7 @@ def expected_http_error(error, expected_code, expected_message=None): if isinstance(e, HTTPError): if expected_code != e.status_code: return False - if expected_message is not None and expected_message != str(e): - return False - return True + return expected_message is not None and expected_message != str(e) elif any( [ isinstance(e, HTTPClientError), From faf929790f95024d46389ed60ba067a8ab8fcbb8 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:15:40 +0000 Subject: [PATCH 4/5] Use noqa on root being redefined to avoid changing the logic --- jupyter_server/base/handlers.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jupyter_server/base/handlers.py b/jupyter_server/base/handlers.py index 6550689eef..3e492a09be 100644 --- a/jupyter_server/base/handlers.py +++ b/jupyter_server/base/handlers.py @@ -1056,12 +1056,12 @@ def get_absolute_path(cls, roots: Sequence[str], path: str) -> str: log().debug(f"Path {path} served from {abspath}") return abspath - def validate_absolute_path(self, _root: str, absolute_path: str) -> str | None: + def validate_absolute_path(self, root: str, absolute_path: str) -> str | None: """check if the file should be served (raises 404, 403, etc.)""" if not absolute_path: raise web.HTTPError(404) - for root in self.root: + for root in self.root: # noqa: PLR1704 if (absolute_path + os.sep).startswith(root): break From 9c28df0c429e0a838229213d841384c8694eebf9 Mon Sep 17 00:00:00 2001 From: krassowski <5832902+krassowski@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:28:15 +0000 Subject: [PATCH 5/5] Fix `expected_http_error` --- tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/utils.py b/tests/utils.py index 216af8e2de..276cefc6d3 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -27,7 +27,7 @@ def expected_http_error(error, expected_code, expected_message=None): if isinstance(e, HTTPError): if expected_code != e.status_code: return False - return expected_message is not None and expected_message != str(e) + return not (expected_message is not None and expected_message != str(e)) elif any( [ isinstance(e, HTTPClientError),