diff --git a/examples/simple/simple_ext1/application.py b/examples/simple/simple_ext1/application.py index d420279a3c..b28d8f8781 100644 --- a/examples/simple/simple_ext1/application.py +++ b/examples/simple/simple_ext1/application.py @@ -31,10 +31,10 @@ class SimpleApp1(ExtensionAppJinjaMixin, ExtensionApp): load_other_extensions = True # Local path to static files directory. - static_paths = [DEFAULT_STATIC_FILES_PATH] + static_paths = [DEFAULT_STATIC_FILES_PATH] # type:ignore[assignment] # Local path to templates directory. - template_paths = [DEFAULT_TEMPLATE_FILES_PATH] + template_paths = [DEFAULT_TEMPLATE_FILES_PATH] # type:ignore[assignment] configA = Unicode("", config=True, help="Config A example.") # noqa diff --git a/examples/simple/simple_ext2/application.py b/examples/simple/simple_ext2/application.py index b4fbbadb97..6f8498407d 100644 --- a/examples/simple/simple_ext2/application.py +++ b/examples/simple/simple_ext2/application.py @@ -24,10 +24,10 @@ class SimpleApp2(ExtensionAppJinjaMixin, ExtensionApp): load_other_extensions = True # Local path to static files directory. - static_paths = [DEFAULT_STATIC_FILES_PATH] + static_paths = [DEFAULT_STATIC_FILES_PATH] # type:ignore[assignment] # Local path to templates directory. - template_paths = [DEFAULT_TEMPLATE_FILES_PATH] + template_paths = [DEFAULT_TEMPLATE_FILES_PATH] # type:ignore[assignment] configD = Unicode("", config=True, help="Config D example.") # noqa diff --git a/jupyter_server/auth/identity.py b/jupyter_server/auth/identity.py index 42c4ce839a..72f4b469e0 100644 --- a/jupyter_server/auth/identity.py +++ b/jupyter_server/auth/identity.py @@ -702,22 +702,24 @@ def auth_enabled(self): def get_user(self, handler: JupyterHandler) -> User | None: """Get the user.""" - user = self.login_handler_class.get_user(handler) + user = self.login_handler_class.get_user(handler) # type:ignore[attr-defined] if user is None: return None return _backward_compat_user(user) @property def login_available(self): - return self.login_handler_class.get_login_available(self.settings) + return self.login_handler_class.get_login_available( # type:ignore[attr-defined] + self.settings + ) def should_check_origin(self, handler: JupyterHandler) -> bool: """Whether we should check origin.""" - return self.login_handler_class.should_check_origin(handler) + return self.login_handler_class.should_check_origin(handler) # type:ignore[attr-defined] def is_token_authenticated(self, handler: JupyterHandler) -> bool: """Whether we are token authenticated.""" - return self.login_handler_class.is_token_authenticated(handler) + return self.login_handler_class.is_token_authenticated(handler) # type:ignore[attr-defined] def validate_security( self, @@ -732,4 +734,6 @@ def validate_security( self.log.critical(_i18n("Hint: run the following command to set a password")) self.log.critical(_i18n("\t$ python -m jupyter_server.auth password")) sys.exit(1) - return self.login_handler_class.validate_security(app, ssl_options) + return self.login_handler_class.validate_security( # type:ignore[attr-defined] + app, ssl_options + ) diff --git a/jupyter_server/extension/manager.py b/jupyter_server/extension/manager.py index 8b2f156d52..02aad0f262 100644 --- a/jupyter_server/extension/manager.py +++ b/jupyter_server/extension/manager.py @@ -351,7 +351,7 @@ def load_extension(self, name): """Load an extension by name.""" extension = self.extensions.get(name) - if extension.enabled: + if extension and extension.enabled: try: extension.load_all_points(self.serverapp) except Exception as e: diff --git a/jupyter_server/gateway/connections.py b/jupyter_server/gateway/connections.py index 24a3375b9a..0fe973eef6 100644 --- a/jupyter_server/gateway/connections.py +++ b/jupyter_server/gateway/connections.py @@ -73,7 +73,7 @@ def disconnect(self): if self.ws is not None: # Close connection self.ws.close() - elif not self.ws_future.done(): + elif self.ws_future and not self.ws_future.done(): # Cancel pending connection. Since future.cancel() is a noop on tornado, we'll track cancellation locally self.ws_future.cancel() self.log.debug(f"_disconnect: future cancelled, disconnected: {self.disconnected}") @@ -93,6 +93,8 @@ async def _read_messages(self): if not self.disconnected: self.log.warning(f"Lost connection to Gateway: {self.kernel_id}") break + if isinstance(message, bytes): + message = message.decode("utf8") self.handle_outgoing_message( message ) # pass back to notebook client (see self.on_open and WebSocketChannelsHandler.open) @@ -136,7 +138,7 @@ def handle_outgoing_message(self, incoming_msg: str, *args: Any) -> None: def handle_incoming_message(self, message: str) -> None: """Send message to gateway server.""" - if self.ws is None: + if self.ws is None and self.ws_future is not None: loop = IOLoop.current() loop.add_future(self.ws_future, lambda future: self.handle_incoming_message(message)) else: diff --git a/jupyter_server/gateway/gateway_client.py b/jupyter_server/gateway/gateway_client.py index d73c0245aa..487e318923 100644 --- a/jupyter_server/gateway/gateway_client.py +++ b/jupyter_server/gateway/gateway_client.py @@ -156,7 +156,7 @@ def _url_validate(self, proposal): @default("ws_url") def _ws_url_default(self): default_value = os.environ.get(self.ws_url_env) - if default_value is None and self.gateway_enabled: + if self.url is not None and default_value is None and self.gateway_enabled: default_value = self.url.lower().replace("http", "ws") return default_value @@ -462,7 +462,7 @@ def _gateway_retry_max_default(self): ) gateway_token_renewer_class_env = "JUPYTER_GATEWAY_TOKEN_RENEWER_CLASS" gateway_token_renewer_class = Type( - klass=GatewayTokenRenewerBase, + klass=GatewayTokenRenewerBase, # type:ignore[type-abstract] config=True, help="""The class to use for Gateway token renewal. (JUPYTER_GATEWAY_TOKEN_RENEWER_CLASS env var)""", ) @@ -546,7 +546,9 @@ def __init__(self, **kwargs): """Initialize a gateway client.""" super().__init__(**kwargs) self._connection_args = {} # initialized on first use - self.gateway_token_renewer = self.gateway_token_renewer_class(parent=self, log=self.log) + self.gateway_token_renewer = self.gateway_token_renewer_class( + parent=self, log=self.log + ) # type:ignore[operator] # store of cookies with store time self._cookies: ty.Dict[str, ty.Tuple[Morsel, datetime]] = {} @@ -570,11 +572,12 @@ def init_connection_args(self): # Ensure any adjustments are reflected in env. os.environ["KERNEL_LAUNCH_TIMEOUT"] = str(GatewayClient.KERNEL_LAUNCH_TIMEOUT) - self._connection_args["headers"] = json.loads(self.headers) - if self.auth_header_key not in self._connection_args["headers"]: - self._connection_args["headers"].update( - {f"{self.auth_header_key}": f"{self.auth_scheme} {self.auth_token}"} - ) + if self.headers: + self._connection_args["headers"] = json.loads(self.headers) + if self.auth_header_key not in self._connection_args["headers"]: + self._connection_args["headers"].update( + {f"{self.auth_header_key}": f"{self.auth_scheme} {self.auth_token}"} + ) self._connection_args["connect_timeout"] = self.connect_timeout self._connection_args["request_timeout"] = self.request_timeout self._connection_args["validate_cert"] = self.validate_cert @@ -598,18 +601,19 @@ def load_connection_args(self, **kwargs): # Give token renewal a shot at renewing the token prev_auth_token = self.auth_token - try: - self.auth_token = self.gateway_token_renewer.get_token( - self.auth_header_key, self.auth_scheme, self.auth_token - ) - except Exception as ex: - self.log.error( - f"An exception occurred attempting to renew the " - f"Gateway authorization token using an instance of class " - f"'{self.gateway_token_renewer_class}'. The request will " - f"proceed using the current token value. Exception was: {ex}" - ) - self.auth_token = prev_auth_token + if self.auth_token: + try: + self.auth_token = self.gateway_token_renewer.get_token( + self.auth_header_key, self.auth_scheme, self.auth_token + ) + except Exception as ex: + self.log.error( + f"An exception occurred attempting to renew the " + f"Gateway authorization token using an instance of class " + f"'{self.gateway_token_renewer_class}'. The request will " + f"proceed using the current token value. Exception was: {ex}" + ) + self.auth_token = prev_auth_token for arg, value in self._connection_args.items(): if arg == "headers": diff --git a/jupyter_server/gateway/managers.py b/jupyter_server/gateway/managers.py index 919d28d854..5a09ede926 100644 --- a/jupyter_server/gateway/managers.py +++ b/jupyter_server/gateway/managers.py @@ -236,6 +236,8 @@ def _replace_path_kernelspec_resources(self, kernel_specs): This enables clients to properly route through jupyter_server to a gateway for kernel resources such as logo files """ + if not self.parent: + return {} kernelspecs = kernel_specs["kernelspecs"] for kernel_name in kernelspecs: resources = kernelspecs[kernel_name]["resources"] @@ -273,6 +275,8 @@ async def get_all_specs(self): # If different log a warning and reset the default. However, the # caller of this method will still return this server's value until # the next fetch of kernelspecs - at which time they'll match. + if not self.parent: + return {} km = self.parent.kernel_manager remote_default_kernel_name = fetched_kspecs.get("default") if remote_default_kernel_name != km.default_kernel_name: @@ -416,7 +420,7 @@ def client(self, **kwargs): # add kwargs last, for manual overrides kw.update(kwargs) - return self.client_factory(**kw) + return self.client_factory(**kw) # type:ignore[operator] async def refresh_model(self, model=None): """Refresh the kernel model. diff --git a/jupyter_server/serverapp.py b/jupyter_server/serverapp.py index f9b9917772..09c2078871 100644 --- a/jupyter_server/serverapp.py +++ b/jupyter_server/serverapp.py @@ -1879,21 +1879,25 @@ def init_configurables(self): # this determination, instantiate the GatewayClient config singleton. self.gateway_config = GatewayClient.instance(parent=self) - if not issubclass(self.kernel_manager_class, AsyncMappingKernelManager): + if not issubclass( + self.kernel_manager_class, AsyncMappingKernelManager # type:ignore[arg-type] + ): warnings.warn( "The synchronous MappingKernelManager class is deprecated and will not be supported in Jupyter Server 3.0", DeprecationWarning, stacklevel=2, ) - if not issubclass(self.contents_manager_class, AsyncContentsManager): + if not issubclass( + self.contents_manager_class, AsyncContentsManager # type:ignore[arg-type] + ): warnings.warn( "The synchronous ContentsManager classes are deprecated and will not be supported in Jupyter Server 3.0", DeprecationWarning, stacklevel=2, ) - self.kernel_spec_manager = self.kernel_spec_manager_class( + self.kernel_spec_manager = self.kernel_spec_manager_class( # type:ignore[operator] parent=self, ) @@ -1915,21 +1919,21 @@ def init_configurables(self): "because jupyter-client's version does not allow them (should be >8.3.0)." ) - self.kernel_manager = self.kernel_manager_class(**kwargs) - self.contents_manager = self.contents_manager_class( + self.kernel_manager = self.kernel_manager_class(**kwargs) # type:ignore[operator] + self.contents_manager = self.contents_manager_class( # type:ignore[operator] parent=self, log=self.log, ) # Trigger a default/validation here explicitly while we still support the # deprecated trait on ServerApp (FIXME remove when deprecation finalized) self.contents_manager.preferred_dir # noqa - self.session_manager = self.session_manager_class( + self.session_manager = self.session_manager_class( # type:ignore[operator] parent=self, log=self.log, kernel_manager=self.kernel_manager, contents_manager=self.contents_manager, ) - self.config_manager = self.config_manager_class( + self.config_manager = self.config_manager_class( # type:ignore[operator] parent=self, log=self.log, ) @@ -1958,7 +1962,9 @@ def init_configurables(self): f"Ignoring deprecated config ServerApp.login_handler_class={self.login_handler_class}." " Superseded by ServerApp.identity_provider_class={self.identity_provider_class}." ) - self.identity_provider = self.identity_provider_class(**identity_provider_kwargs) + self.identity_provider = self.identity_provider_class( + **identity_provider_kwargs + ) # type:ignore[operator] if self.identity_provider_class is LegacyIdentityProvider: # legacy config stored the password in tornado_settings @@ -1979,7 +1985,7 @@ def init_configurables(self): # that means it has some config that should take higher priority than deprecated ServerApp.token self.log.warning("Ignoring deprecated ServerApp.token config") - self.authorizer = self.authorizer_class( + self.authorizer = self.authorizer_class( # type:ignore[operator] parent=self, log=self.log, identity_provider=self.identity_provider ) @@ -2100,7 +2106,7 @@ def init_webapp(self): if not self.ssl_options: # could be an empty dict or None # None indicates no SSL config - self.ssl_options = None + self.ssl_options = None # type:ignore[assignment] else: # SSL may be missing, so only import it if it's to be used import ssl @@ -2130,7 +2136,7 @@ def init_resources(self): old_soft, old_hard = resource.getrlimit(resource.RLIMIT_NOFILE) soft = self.min_open_files_limit hard = old_hard - if old_soft < soft: + if soft is not None and old_soft < soft: if hard < soft: hard = soft self.log.debug( @@ -2911,7 +2917,7 @@ async def _cleanup(self): await self.cleanup_extensions() await self.cleanup_kernels() try: - await self.kernel_websocket_connection_class.close_all() + await self.kernel_websocket_connection_class.close_all() # type:ignore[attr-defined] except AttributeError: # This can happen in two different scenarios: # diff --git a/jupyter_server/services/contents/filecheckpoints.py b/jupyter_server/services/contents/filecheckpoints.py index 00f36f61be..f6d1ef44e7 100644 --- a/jupyter_server/services/contents/filecheckpoints.py +++ b/jupyter_server/services/contents/filecheckpoints.py @@ -43,10 +43,9 @@ class FileCheckpoints(FileManagerMixin, Checkpoints): root_dir = Unicode(config=True) def _root_dir_default(self): - try: - return self.parent.root_dir - except AttributeError: + if not self.parent: return os.getcwd() + return self.parent.root_dir # ContentsManager-dependent checkpoint API def create_checkpoint(self, contents_mgr, path): diff --git a/jupyter_server/services/contents/filemanager.py b/jupyter_server/services/contents/filemanager.py index 4ecfa6e00f..4ddf9cd721 100644 --- a/jupyter_server/services/contents/filemanager.py +++ b/jupyter_server/services/contents/filemanager.py @@ -48,10 +48,9 @@ class FileContentsManager(FileManagerMixin, ContentsManager): @default("root_dir") def _default_root_dir(self): - try: - return self.parent.root_dir - except AttributeError: + if not self.parent: return os.getcwd() + return self.parent.root_dir @validate("root_dir") def _validate_root_dir(self, proposal): @@ -65,6 +64,8 @@ def _validate_root_dir(self, proposal): @default("preferred_dir") def _default_preferred_dir(self): + if not self.parent: + return "" try: value = self.parent.preferred_dir if value == self.parent.root_dir: diff --git a/jupyter_server/services/contents/manager.py b/jupyter_server/services/contents/manager.py index 9482d42884..dc9c754f84 100644 --- a/jupyter_server/services/contents/manager.py +++ b/jupyter_server/services/contents/manager.py @@ -100,11 +100,12 @@ def _validate_preferred_dir(self, proposal): raise TraitError(e.log_message) from e if not dir_exists: raise TraitError(_i18n("Preferred directory not found: %r") % value) - try: - if value != self.parent.preferred_dir: - self.parent.preferred_dir = os.path.join(self.root_dir, *value.split("/")) - except (AttributeError, TraitError): - pass + if self.parent: + try: + if value != self.parent.preferred_dir: + self.parent.preferred_dir = os.path.join(self.root_dir, *value.split("/")) + except TraitError: + pass return value allow_hidden = Bool(False, config=True, help="Allow access to hidden files") @@ -324,7 +325,7 @@ def run_post_save_hooks(self, model, os_path): @default("checkpoints") def _default_checkpoints(self): - return self.checkpoints_class(**self.checkpoints_kwargs) + return self.checkpoints_class(**self.checkpoints_kwargs) # type:ignore[operator] @default("checkpoints_kwargs") def _default_checkpoints_kwargs(self): @@ -760,7 +761,7 @@ class AsyncContentsManager(ContentsManager): @default("checkpoints") def _default_checkpoints(self): - return self.checkpoints_class(**self.checkpoints_kwargs) + return self.checkpoints_class(**self.checkpoints_kwargs) # type:ignore[operator] @default("checkpoints_kwargs") def _default_checkpoints_kwargs(self): diff --git a/jupyter_server/services/events/handlers.py b/jupyter_server/services/events/handlers.py index 3f3a5c64c2..9f58692ea3 100644 --- a/jupyter_server/services/events/handlers.py +++ b/jupyter_server/services/events/handlers.py @@ -44,7 +44,8 @@ async def get(self, *args, **kwargs): """Get an event socket.""" self.pre_get() res = super().get(*args, **kwargs) - await res + if res is not None: + await res async def event_listener(self, logger: EventLogger, schema_id: str, data: dict) -> None: """Write an event message.""" diff --git a/jupyter_server/services/kernels/connection/channels.py b/jupyter_server/services/kernels/connection/channels.py index c90964ed82..fca4bbd31c 100644 --- a/jupyter_server/services/kernels/connection/channels.py +++ b/jupyter_server/services/kernels/connection/channels.py @@ -6,7 +6,7 @@ from concurrent.futures import Future from textwrap import dedent from typing import Dict as Dict_t -from typing import MutableSet +from typing import MutableSet, cast from jupyter_client import protocol_version as client_protocol_version from tornado import gen, web @@ -85,6 +85,8 @@ class ZMQChannelsWebsocketConnection(BaseKernelWebsocketConnection): ), ) + websocket_handler = Instance(KernelWebsocketHandler) + @property def write_message(self): """Alias to the websocket handler's write_message method.""" @@ -283,7 +285,9 @@ async def _register_session(self): if ( self.kernel_id in self.multi_kernel_manager ): # only update open sessions if kernel is actively managed - self._open_sessions[self.session_key] = self.websocket_handler + self._open_sessions[self.session_key] = cast( + KernelWebsocketHandler, self.websocket_handler + ) async def prepare(self): """Prepare a kernel connection.""" diff --git a/jupyter_server/services/kernels/kernelmanager.py b/jupyter_server/services/kernels/kernelmanager.py index ca57228753..312abeb73a 100644 --- a/jupyter_server/services/kernels/kernelmanager.py +++ b/jupyter_server/services/kernels/kernelmanager.py @@ -72,10 +72,9 @@ def _default_kernel_manager_class(self): @default("root_dir") def _default_root_dir(self): - try: - return self.parent.root_dir - except AttributeError: + if not self.parent: return os.getcwd() + return self.parent.root_dir @validate("root_dir") def _update_root_dir(self, proposal): diff --git a/jupyter_server/traittypes.py b/jupyter_server/traittypes.py index 536f2b24ab..bd6f28a36b 100644 --- a/jupyter_server/traittypes.py +++ b/jupyter_server/traittypes.py @@ -114,7 +114,7 @@ def _resolve_classes(self): self.importable_klasses.append(klass) if isinstance(self.default_value, str): - self.default_value = self._resolve_string(self.default_value) + self.default_value = self._resolve_string(self.default_value) # type:ignore[arg-type] def default_value_repr(self): """The default value repr.""" diff --git a/pyproject.toml b/pyproject.toml index 5ba07d5fb4..47695dc9b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -104,7 +104,7 @@ nowarn = "test -W default {args}" [tool.hatch.envs.typing] features = ["test"] -dependencies = [ "mypy>=1.5.1" ] +dependencies = [ "mypy>=1.5.1", "traitlets>=5.10.1" ] [tool.hatch.envs.typing.scripts] test = "mypy --install-types --non-interactive {args:.}" diff --git a/tests/extension/mockextensions/app.py b/tests/extension/mockextensions/app.py index 77010c1a56..51757ae9f2 100644 --- a/tests/extension/mockextensions/app.py +++ b/tests/extension/mockextensions/app.py @@ -45,7 +45,7 @@ def get(self): class MockExtensionApp(ExtensionAppJinjaMixin, ExtensionApp): name = "mockextension" template_paths = List().tag(config=True) - static_paths = [STATIC_PATH] + static_paths = [STATIC_PATH] # type:ignore[assignment] mock_trait = Unicode("mock trait", config=True) loaded = False