From 22f9ee05f0c127fb7dd22bfdb1b034e2c1782050 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 28 Sep 2023 22:15:02 -0500 Subject: [PATCH 01/19] Typing cleanup --- pyproject.toml | 18 +++-- {traitlets/config/tests => tests}/__init__.py | 0 {traitlets/tests => tests}/_warnings.py | 0 {traitlets/tests => tests/config}/__init__.py | 0 .../config}/test_application.py | 7 +- .../config}/test_argcomplete.py | 0 .../config}/test_configurable.py | 3 +- .../tests => tests/config}/test_loader.py | 0 {traitlets/tests => tests}/test_traitlets.py | 79 +++++++++++++++++++ .../tests => tests}/test_traitlets_enum.py | 0 {traitlets/tests => tests}/test_typing.py | 0 .../tests/utils.py => tests/utilities.py | 0 .../utils/tests => tests/utils}/__init__.py | 0 .../utils/tests => tests/utils}/test_bunch.py | 2 +- .../tests => tests/utils}/test_decorators.py | 4 +- .../utils}/test_importstring.py | 2 +- traitlets/__init__.py | 4 +- traitlets/config/application.py | 4 +- traitlets/config/argcomplete_config.py | 6 +- traitlets/config/configurable.py | 2 +- traitlets/config/loader.py | 21 ++--- traitlets/config/manager.py | 15 ++-- traitlets/config/sphinxdoc.py | 1 + traitlets/traitlets.py | 9 ++- traitlets/utils/__init__.py | 9 ++- traitlets/utils/bunch.py | 9 ++- traitlets/utils/decorators.py | 4 +- traitlets/utils/descriptions.py | 19 +++-- traitlets/utils/getargspec.py | 4 +- traitlets/utils/nested_update.py | 3 +- traitlets/utils/text.py | 2 +- 31 files changed, 170 insertions(+), 57 deletions(-) rename {traitlets/config/tests => tests}/__init__.py (100%) rename {traitlets/tests => tests}/_warnings.py (100%) rename {traitlets/tests => tests/config}/__init__.py (100%) rename {traitlets/config/tests => tests/config}/test_application.py (99%) rename {traitlets/config/tests => tests/config}/test_argcomplete.py (100%) rename {traitlets/config/tests => tests/config}/test_configurable.py (99%) rename {traitlets/config/tests => tests/config}/test_loader.py (100%) rename {traitlets/tests => tests}/test_traitlets.py (97%) rename {traitlets/tests => tests}/test_traitlets_enum.py (100%) rename {traitlets/tests => tests}/test_typing.py (100%) rename traitlets/tests/utils.py => tests/utilities.py (100%) rename {traitlets/utils/tests => tests/utils}/__init__.py (100%) rename {traitlets/utils/tests => tests/utils}/test_bunch.py (86%) rename {traitlets/utils/tests => tests/utils}/test_decorators.py (97%) rename {traitlets/utils/tests => tests/utils}/test_importstring.py (93%) diff --git a/pyproject.toml b/pyproject.toml index c63a191f..e47675a4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -97,7 +97,7 @@ exclude = ["examples/docs/configs", "traitlets/tests/test_typing.py"] [tool.pytest.ini_options] addopts = "--durations=10 -ra --showlocals --doctest-modules --color yes --ignore examples/docs/configs" testpaths = [ - "traitlets", + "tests", "examples", ] filterwarnings = [ @@ -153,10 +153,16 @@ target-version = ["py37"] target-version = "py37" line-length = 100 select = [ - "A", "B", "C", "E", "F", "FBT", "I", "N", "Q", "RUF", "S", "T", + "A", "ANN", "B", "C", "E", "F", "FBT", "I", "N", "Q", "RUF", "S", "T", "UP", "W", "YTT", ] ignore = [ + # Dynamically typed expressions (typing.Any) are disallowed in `key` + "ANN401", + # Missing type annotation for `self` in method + "ANN101", + # ANN202 Missing return type annotation for private function + "ANN202", # Allow non-abstract empty methods in abstract base classes "B027", # Ignore McCabe complexity @@ -211,11 +217,13 @@ unfixable = [ # N802 Function name `assertIn` should be lowercase # F841 Local variable `t` is assigned to but never used # B018 Found useless expression -# S301 `pickle` and modules that wrap... -"traitlets/tests/*" = ["B011", "F841", "C408", "E402", "T201", "B007", "N802", "F841", +# S301 `pickle` and modules that wrap..." +"tests/*" = ["ANN", "B011", "F841", "C408", "E402", "T201", "B007", "N802", "F841", "B018", "S301"] # B003 Assigning to os.environ doesn't clear the environment -"traitlets/config/tests/*" = ["B003", "B018", "S301"] +"tests/config/*" = ["B003", "B018", "S301"] # F401 `_version.__version__` imported but unused # F403 `from .traitlets import *` used; unable to detect undefined names "traitlets/*__init__.py" = ["F401", "F403"] +"docs/*" = ["ANN"] +"examples/*" = ["ANN"] diff --git a/traitlets/config/tests/__init__.py b/tests/__init__.py similarity index 100% rename from traitlets/config/tests/__init__.py rename to tests/__init__.py diff --git a/traitlets/tests/_warnings.py b/tests/_warnings.py similarity index 100% rename from traitlets/tests/_warnings.py rename to tests/_warnings.py diff --git a/traitlets/tests/__init__.py b/tests/config/__init__.py similarity index 100% rename from traitlets/tests/__init__.py rename to tests/config/__init__.py diff --git a/traitlets/config/tests/test_application.py b/tests/config/test_application.py similarity index 99% rename from traitlets/config/tests/test_application.py rename to tests/config/test_application.py index 7c3644d5..811d7cbd 100644 --- a/traitlets/config/tests/test_application.py +++ b/tests/config/test_application.py @@ -23,7 +23,8 @@ from traitlets.config.application import Application from traitlets.config.configurable import Configurable from traitlets.config.loader import Config, KVArgParseConfigLoader -from traitlets.tests.utils import check_help_all_output, check_help_output, get_output_error_code + +from ..utilities import check_help_all_output, check_help_output, get_output_error_code try: from unittest import mock @@ -601,7 +602,7 @@ def test_raise_on_bad_config(self): with self.assertRaises(SyntaxError): app.load_config_file(name, path=[td]) - def test_subcommands_instanciation(self): + def test_subcommands_instantiation(self): """Try all ways to specify how to create sub-apps.""" app = Root.instance() app.parse_command_line(["sub1"]) @@ -694,7 +695,7 @@ class App(Application): class Root(Application): subcommands = { - "sub1": ("traitlets.config.tests.test_application.Sub1", "import string"), + "sub1": ("tests.config.test_application.Sub1", "import string"), } diff --git a/traitlets/config/tests/test_argcomplete.py b/tests/config/test_argcomplete.py similarity index 100% rename from traitlets/config/tests/test_argcomplete.py rename to tests/config/test_argcomplete.py diff --git a/traitlets/config/tests/test_configurable.py b/tests/config/test_configurable.py similarity index 99% rename from traitlets/config/tests/test_configurable.py rename to tests/config/test_configurable.py index 384d12f1..a699ff2b 100644 --- a/traitlets/config/tests/test_configurable.py +++ b/tests/config/test_configurable.py @@ -8,6 +8,7 @@ from pytest import mark +from tests._warnings import expected_warnings from traitlets.config.application import Application from traitlets.config.configurable import Configurable, LoggingConfigurable, SingletonConfigurable from traitlets.config.loader import Config @@ -26,8 +27,6 @@ ) from traitlets.utils.warnings import _deprecations_shown -from ...tests._warnings import expected_warnings - class MyConfigurable(Configurable): a = Integer(1, help="The integer a.").tag(config=True) diff --git a/traitlets/config/tests/test_loader.py b/tests/config/test_loader.py similarity index 100% rename from traitlets/config/tests/test_loader.py rename to tests/config/test_loader.py diff --git a/traitlets/tests/test_traitlets.py b/tests/test_traitlets.py similarity index 97% rename from traitlets/tests/test_traitlets.py rename to tests/test_traitlets.py index 62fa726f..673d20c4 100644 --- a/traitlets/tests/test_traitlets.py +++ b/tests/test_traitlets.py @@ -3139,3 +3139,82 @@ def test_all_attribute(): for name in traitlets.__all__: if name not in names: raise ValueError(f"{name} should be removed from __all__") + + +def test_handle_docstring(): + from traitlets.config import Configurable + + class SampleConfigurable(Configurable): + pass + + class TraitTypesSampleConfigurable(Configurable): + """TraitTypesSampleConfigurable docstring""" + + trait_integer = Integer( + help="""trait_integer help text""", + config=True, + ) + trait_integer_nohelp = Integer( + config=True, + ) + trait_integer_noconfig = Integer( + help="""trait_integer_noconfig help text""", + ) + + trait_unicode = Unicode( + help="""trait_unicode help text""", + config=True, + ) + trait_unicode_nohelp = Unicode( + config=True, + ) + trait_unicode_noconfig = Unicode( + help="""trait_unicode_noconfig help text""", + ) + + trait_dict = Dict( + help="""trait_dict help text""", + config=True, + ) + trait_dict_nohelp = Dict( + config=True, + ) + trait_dict_noconfig = Dict( + help="""trait_dict_noconfig help text""", + ) + + trait_instance = Instance( + klass=SampleConfigurable, + help="""trait_instance help text""", + config=True, + ) + trait_instance_nohelp = Instance( + klass=SampleConfigurable, + config=True, + ) + trait_instance_noconfig = Instance( + klass=SampleConfigurable, + help="""trait_instance_noconfig help text""", + ) + + trait_union = Union( + [Integer(), Unicode()], + help="""trait_union help text""", + config=True, + ) + trait_union_nohelp = Union( + [Integer(), Unicode()], + config=True, + ) + trait_union_noconfig = Union( + [Integer(), Unicode()], + help="""trait_union_noconfig help text""", + ) + + base_names = SampleConfigurable().trait_names() + for name in TraitTypesSampleConfigurable().trait_names(): + if name in base_names: + continue + doc = getattr(TraitTypesSampleConfigurable, name).__doc__ + if "nohelp" not in name: + assert doc == f"{name} help text" diff --git a/traitlets/tests/test_traitlets_enum.py b/tests/test_traitlets_enum.py similarity index 100% rename from traitlets/tests/test_traitlets_enum.py rename to tests/test_traitlets_enum.py diff --git a/traitlets/tests/test_typing.py b/tests/test_typing.py similarity index 100% rename from traitlets/tests/test_typing.py rename to tests/test_typing.py diff --git a/traitlets/tests/utils.py b/tests/utilities.py similarity index 100% rename from traitlets/tests/utils.py rename to tests/utilities.py diff --git a/traitlets/utils/tests/__init__.py b/tests/utils/__init__.py similarity index 100% rename from traitlets/utils/tests/__init__.py rename to tests/utils/__init__.py diff --git a/traitlets/utils/tests/test_bunch.py b/tests/utils/test_bunch.py similarity index 86% rename from traitlets/utils/tests/test_bunch.py rename to tests/utils/test_bunch.py index aa40f76a..223124d7 100644 --- a/traitlets/utils/tests/test_bunch.py +++ b/tests/utils/test_bunch.py @@ -1,4 +1,4 @@ -from ..bunch import Bunch +from traitlets.utils.bunch import Bunch def test_bunch(): diff --git a/traitlets/utils/tests/test_decorators.py b/tests/utils/test_decorators.py similarity index 97% rename from traitlets/utils/tests/test_decorators.py rename to tests/utils/test_decorators.py index b776b6bc..d6bf8414 100644 --- a/traitlets/utils/tests/test_decorators.py +++ b/tests/utils/test_decorators.py @@ -1,8 +1,8 @@ from inspect import Parameter, signature from unittest import TestCase -from ...traitlets import HasTraits, Int, Unicode -from ..decorators import signature_has_traits +from traitlets import HasTraits, Int, Unicode +from traitlets.utils.decorators import signature_has_traits class TestExpandSignature(TestCase): diff --git a/traitlets/utils/tests/test_importstring.py b/tests/utils/test_importstring.py similarity index 93% rename from traitlets/utils/tests/test_importstring.py rename to tests/utils/test_importstring.py index 1e5db490..8ce28add 100644 --- a/traitlets/utils/tests/test_importstring.py +++ b/tests/utils/test_importstring.py @@ -8,7 +8,7 @@ import os from unittest import TestCase -from ..importstring import import_item +from traitlets.utils.importstring import import_item class TestImportItem(TestCase): diff --git a/traitlets/__init__.py b/traitlets/__init__.py index 96ebe57f..ac6fb706 100644 --- a/traitlets/__init__.py +++ b/traitlets/__init__.py @@ -1,4 +1,6 @@ """Traitlets Python configuration system""" +from typing import Any + from . import traitlets from ._version import __version__, version_info from .traitlets import * @@ -19,7 +21,7 @@ class Sentinel(traitlets.Sentinel): # type:ignore[name-defined] - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) warn( """ diff --git a/traitlets/config/application.py b/traitlets/config/application.py index fb185f0a..60041778 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -3,7 +3,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. - +# ruff: noqa: ANN201, ANN001, ANN204, ANN102, ANN003, ANN206, ANN002 import functools import json import logging @@ -1032,7 +1032,7 @@ def exit(self, exit_status=0): self.close_handlers() sys.exit(exit_status) - def __del__(self): + def __del__(self) -> None: self.close_handlers() @classmethod diff --git a/traitlets/config/argcomplete_config.py b/traitlets/config/argcomplete_config.py index ee1e51b4..82112aaf 100644 --- a/traitlets/config/argcomplete_config.py +++ b/traitlets/config/argcomplete_config.py @@ -15,7 +15,7 @@ # This module and its utility methods are written to not crash even # if argcomplete is not installed. class StubModule: - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> t.Any: if not attr.startswith("__"): raise ModuleNotFoundError("No module named 'argcomplete'") raise AttributeError(f"argcomplete stub module has no attribute '{attr}'") @@ -63,7 +63,7 @@ def get_argcomplete_cwords() -> t.Optional[t.List[str]]: return comp_words -def increment_argcomplete_index(): +def increment_argcomplete_index() -> None: """Assumes ``$_ARGCOMPLETE`` is set and `argcomplete` is importable Increment the index pointed to by ``$_ARGCOMPLETE``, which is used to @@ -122,7 +122,7 @@ def match_class_completions(self, cword_prefix: str) -> t.List[t.Tuple[t.Any, st ] return matched_completions - def inject_class_to_parser(self, cls): + def inject_class_to_parser(self, cls: t.Any) -> None: """Add dummy arguments to our ArgumentParser for the traits of this class The argparse-based loader currently does not actually add any class traits to diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py index f448e696..620c26cb 100644 --- a/traitlets/config/configurable.py +++ b/traitlets/config/configurable.py @@ -2,7 +2,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. - +# ruff: noqa: ANN201, ANN001, ANN204, ANN102, ANN003, ANN206, ANN002 import logging import typing as t diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py index 34d62e5a..e2ab5c84 100644 --- a/traitlets/config/loader.py +++ b/traitlets/config/loader.py @@ -2,6 +2,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +# ruff: noqa: ANN201, ANN001, ANN204, ANN102, ANN003, ANN206, ANN002 from __future__ import annotations import argparse @@ -50,10 +51,10 @@ class ArgumentError(ConfigLoaderError): class _Sentinel: - def __repr__(self): + def __repr__(self) -> str: return "" - def __str__(self): + def __str__(self) -> str: return "" @@ -208,7 +209,7 @@ def to_dict(self): d["inserts"] = self._inserts return d - def __repr__(self): + def __repr__(self) -> str: if self._value is not None: return f"<{self.__class__.__name__} value={self._value!r}>" else: @@ -294,7 +295,7 @@ def collisions(self, other: Config) -> dict[str, t.Any]: collisions[section][key] = f"{mine[key]!r} ignored, using {theirs[key]!r}" return collisions - def __contains__(self, key): + def __contains__(self, key) -> bool: # allow nested contains of the form `"Section.key" in config` if "." in key: first, remainder = key.split(".", 1) @@ -344,7 +345,7 @@ def __getitem__(self, key): else: raise - def __setitem__(self, key, value): + def __setitem__(self, key, value) -> None: if _is_section_key(key): if not isinstance(value, Config): raise ValueError( @@ -361,7 +362,7 @@ def __getattr__(self, key): except KeyError as e: raise AttributeError(e) from e - def __setattr__(self, key, value): + def __setattr__(self, key, value) -> None: if key.startswith("__"): return dict.__setattr__(self, key, value) try: @@ -369,7 +370,7 @@ def __setattr__(self, key, value): except KeyError as e: raise AttributeError(e) from e - def __delattr__(self, key): + def __delattr__(self, key) -> None: if key.startswith("__"): return dict.__delattr__(self, key) try: @@ -420,7 +421,7 @@ def get_value(self, trait): # this will raise a more informative error when config is loaded. return s - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({self._super_repr()})" @@ -462,7 +463,7 @@ def get_value(self, trait): # this will raise a more informative error when config is loaded. return src - def __repr__(self): + def __repr__(self) -> str: return f"{self.__class__.__name__}({self._super_repr()})" @@ -749,7 +750,7 @@ def _add_kv_action(self, key): metavar=key.lstrip("-"), ) - def __contains__(self, key): + def __contains__(self, key) -> bool: if "=" in key: return False if super().__contains__(key): diff --git a/traitlets/config/manager.py b/traitlets/config/manager.py index 728cd2f2..9102544e 100644 --- a/traitlets/config/manager.py +++ b/traitlets/config/manager.py @@ -2,15 +2,18 @@ """ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations + import errno import json import os +from typing import Any from traitlets.config import LoggingConfigurable from traitlets.traitlets import Unicode -def recursive_update(target, new): +def recursive_update(target: dict[Any, Any], new: dict[Any, Any]) -> None: """Recursively update one dictionary using another. None values will delete their keys. @@ -39,17 +42,17 @@ class BaseJSONConfigManager(LoggingConfigurable): config_dir = Unicode(".") - def ensure_config_dir_exists(self): + def ensure_config_dir_exists(self) -> None: try: os.makedirs(self.config_dir, 0o755) except OSError as e: if e.errno != errno.EEXIST: raise - def file_name(self, section_name): + def file_name(self, section_name: str) -> str: return os.path.join(self.config_dir, section_name + ".json") - def get(self, section_name): + def get(self, section_name: str) -> Any: """Retrieve the config data for the specified section. Returns the data as a dictionary, or an empty dictionary if the file @@ -62,7 +65,7 @@ def get(self, section_name): else: return {} - def set(self, section_name, data): + def set(self, section_name: str, data: Any) -> None: """Store the given config data.""" filename = self.file_name(section_name) self.ensure_config_dir_exists() @@ -71,7 +74,7 @@ def set(self, section_name, data): with f: json.dump(data, f, indent=2) - def update(self, section_name, new_data): + def update(self, section_name: str, new_data: Any) -> Any: """Modify the config section by recursively updating it with new_data. Returns the modified config data as a dictionary. diff --git a/traitlets/config/sphinxdoc.py b/traitlets/config/sphinxdoc.py index a69d89f9..300c0a0b 100644 --- a/traitlets/config/sphinxdoc.py +++ b/traitlets/config/sphinxdoc.py @@ -32,6 +32,7 @@ Cross reference like this: :configtrait:`Application.log_datefmt`. """ +# ruff: noqa: ANN201, ANN001, ANN204, ANN102, ANN003, ANN206, ANN002 from collections import defaultdict from textwrap import dedent diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index 036f51aa..2e7436cb 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -38,6 +38,9 @@ # # Adapted from enthought.traits, Copyright (c) Enthought, Inc., # also under the terms of the Modified BSD License. + +# ruff: noqa: ANN201, ANN001, ANN204, ANN102, ANN003, ANN206, ANN002 + from __future__ import annotations import contextlib @@ -213,16 +216,16 @@ def parse_notifier_name(names: Sentinel | str | t.Iterable[Sentinel | str]) -> t class _SimpleTest: - def __init__(self, value): + def __init__(self, value) -> None: self.value = value def __call__(self, test): return test == self.value - def __repr__(self): + def __repr__(self) -> str: return " str: return self.__repr__() diff --git a/traitlets/utils/__init__.py b/traitlets/utils/__init__.py index dfec4ee3..e8ee7f98 100644 --- a/traitlets/utils/__init__.py +++ b/traitlets/utils/__init__.py @@ -1,15 +1,18 @@ +from __future__ import annotations + import os import pathlib +from typing import Sequence # vestigal things from IPython_genutils. -def cast_unicode(s, encoding="utf-8"): +def cast_unicode(s: str | bytes, encoding: str = "utf-8") -> str: if isinstance(s, bytes): return s.decode(encoding, "replace") return s -def filefind(filename, path_dirs=None): +def filefind(filename: str, path_dirs: Sequence[str] | None = None) -> str: """Find a file by looking through a sequence of paths. This iterates through a sequence of paths looking for a file and returns @@ -65,7 +68,7 @@ def filefind(filename, path_dirs=None): raise OSError(f"File {filename!r} does not exist in any of the search paths: {path_dirs!r}") -def expand_path(s): +def expand_path(s: str) -> str: """Expand $VARS and ~names in a string, like a shell :Examples: diff --git a/traitlets/utils/bunch.py b/traitlets/utils/bunch.py index 6b3fffeb..498563e0 100644 --- a/traitlets/utils/bunch.py +++ b/traitlets/utils/bunch.py @@ -5,21 +5,24 @@ # Copyright (c) Jupyter Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations + +from typing import Any class Bunch(dict): # type:ignore[type-arg] """A dict with attribute-access""" - def __getattr__(self, key): + def __getattr__(self, key: str) -> Any: try: return self.__getitem__(key) except KeyError as e: raise AttributeError(key) from e - def __setattr__(self, key, value): + def __setattr__(self, key: str, value: Any) -> None: self.__setitem__(key, value) - def __dir__(self): + def __dir__(self) -> list[str]: # py2-compat: can't use super because dict doesn't have __dir__ names = dir({}) names.extend(self.keys()) diff --git a/traitlets/utils/decorators.py b/traitlets/utils/decorators.py index a59e8167..aa992503 100644 --- a/traitlets/utils/decorators.py +++ b/traitlets/utils/decorators.py @@ -2,12 +2,12 @@ import copy from inspect import Parameter, Signature, signature -from typing import Type, TypeVar +from typing import Any, Type, TypeVar from ..traitlets import HasTraits, Undefined -def _get_default(value): +def _get_default(value: Any): """Get default argument value, given the trait default value.""" return Parameter.empty if value == Undefined else value diff --git a/traitlets/utils/descriptions.py b/traitlets/utils/descriptions.py index 232eb0e7..b795f230 100644 --- a/traitlets/utils/descriptions.py +++ b/traitlets/utils/descriptions.py @@ -1,9 +1,18 @@ +from __future__ import annotations + import inspect import re import types +from typing import Any -def describe(article, value, name=None, verbose=False, capital=False): +def describe( + article: str | None, + value: Any, + name: str | None = None, + verbose: bool = False, + capital: bool = False, +) -> str: """Return string that describes a value Parameters @@ -110,7 +119,7 @@ class name where an object was defined. ) -def _prefix(value): +def _prefix(value: Any): if isinstance(value, types.MethodType): name = describe(None, value.__self__, verbose=True) + "." else: @@ -122,7 +131,7 @@ def _prefix(value): return name -def class_of(value): +def class_of(value: Any) -> Any: """Returns a string of the value's type with an indefinite article. For example 'an Image' or 'a PlotValue'. @@ -133,7 +142,7 @@ def class_of(value): return class_of(type(value)) -def add_article(name, definite=False, capital=False): +def add_article(name: str, definite: bool = False, capital: bool = False) -> str: """Returns the string with a prepended article. The input does not need to begin with a charater. @@ -164,7 +173,7 @@ def add_article(name, definite=False, capital=False): return result -def repr_type(obj): +def repr_type(obj: Any) -> str: """Return a string representation of a value and its type for readable error messages. diff --git a/traitlets/utils/getargspec.py b/traitlets/utils/getargspec.py index e2b1f235..7cbc8265 100644 --- a/traitlets/utils/getargspec.py +++ b/traitlets/utils/getargspec.py @@ -7,14 +7,14 @@ :copyright: Copyright 2007-2015 by the Sphinx team, see AUTHORS. :license: BSD, see LICENSE for details. """ - import inspect from functools import partial +from typing import Any # Unmodified from sphinx below this line -def getargspec(func): +def getargspec(func: Any) -> inspect.FullArgSpec: """Like inspect.getargspec but supports functools.partial as well.""" if inspect.ismethod(func): func = func.__func__ diff --git a/traitlets/utils/nested_update.py b/traitlets/utils/nested_update.py index 7f09e171..37e2d27c 100644 --- a/traitlets/utils/nested_update.py +++ b/traitlets/utils/nested_update.py @@ -1,8 +1,9 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +from typing import Any, Dict -def nested_update(this, that): +def nested_update(this: Dict[Any, Any], that: Dict[Any, Any]) -> Dict[Any, Any]: """Merge two nested dictionaries. Effectively a recursive ``dict.update``. diff --git a/traitlets/utils/text.py b/traitlets/utils/text.py index c7d49ede..72ad98fc 100644 --- a/traitlets/utils/text.py +++ b/traitlets/utils/text.py @@ -9,7 +9,7 @@ from typing import List -def indent(val): +def indent(val: str) -> str: res = _indent(val, " ") return res From e78b65d5a476ad077899cc157bba3d92d1f78b1e Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Thu, 28 Sep 2023 22:22:52 -0500 Subject: [PATCH 02/19] fixups --- tests/test_typing.py | 16 ++++++---------- traitlets/__init__.py | 4 ++-- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 92e5bd24..2d145424 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -354,18 +354,14 @@ class T(HasTraits): oinst_string = Instance("Foo", allow_none=True) t = T() - reveal_type(t.inst) # R: traitlets.tests.test_typing.Foo - reveal_type(T.inst) # R: traitlets.traitlets.Instance[traitlets.tests.test_typing.Foo] - reveal_type( - T.inst.tag(sync=True) # R: traitlets.traitlets.Instance[traitlets.tests.test_typing.Foo] - ) - reveal_type(t.oinst) # R: Union[traitlets.tests.test_typing.Foo, None] + reveal_type(t.inst) # R: tests.test_typing.Foo + reveal_type(T.inst) # R: traitlets.traitlets.Instance[tests.test_typing.Foo] + reveal_type(T.inst.tag(sync=True)) # R: traitlets.traitlets.Instance[tests.test_typing.Foo] + reveal_type(t.oinst) # R: Union[tests.test_typing.Foo, None] reveal_type(t.oinst_string) # R: Union[Any, None] + reveal_type(T.oinst) # R: traitlets.traitlets.Instance[Union[tests.test_typing.Foo, None]] reveal_type( - T.oinst # R: traitlets.traitlets.Instance[Union[traitlets.tests.test_typing.Foo, None]] - ) - reveal_type( - T.oinst.tag( # R: traitlets.traitlets.Instance[Union[traitlets.tests.test_typing.Foo, None]] + T.oinst.tag( # R: traitlets.traitlets.Instance[Union[tests.test_typing.Foo, None]] sync=True ) ) diff --git a/traitlets/__init__.py b/traitlets/__init__.py index ac6fb706..2641c443 100644 --- a/traitlets/__init__.py +++ b/traitlets/__init__.py @@ -1,5 +1,5 @@ """Traitlets Python configuration system""" -from typing import Any +import typing as _t from . import traitlets from ._version import __version__, version_info @@ -21,7 +21,7 @@ class Sentinel(traitlets.Sentinel): # type:ignore[name-defined] - def __init__(self, *args: Any, **kwargs: Any) -> None: + def __init__(self, *args: _t.Any, **kwargs: _t.Any) -> None: super().__init__(*args, **kwargs) warn( """ From 46209db1a362628b0aec86daa6369af2f1edac33 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 29 Sep 2023 04:31:52 -0500 Subject: [PATCH 03/19] Restore tests api --- tests/config/test_application.py | 3 +- tests/test_traitlets.py | 54 +---------------- traitlets/tests/__init__.py | 0 traitlets/tests/test_traitlets.py | 59 +++++++++++++++++++ .../utilities.py => traitlets/tests/utils.py | 8 ++- 5 files changed, 66 insertions(+), 58 deletions(-) create mode 100644 traitlets/tests/__init__.py create mode 100644 traitlets/tests/test_traitlets.py rename tests/utilities.py => traitlets/tests/utils.py (79%) diff --git a/tests/config/test_application.py b/tests/config/test_application.py index 811d7cbd..3830e818 100644 --- a/tests/config/test_application.py +++ b/tests/config/test_application.py @@ -23,8 +23,7 @@ from traitlets.config.application import Application from traitlets.config.configurable import Configurable from traitlets.config.loader import Config, KVArgParseConfigLoader - -from ..utilities import check_help_all_output, check_help_output, get_output_error_code +from traitlets.tests.utils import check_help_all_output, check_help_output, get_output_error_code try: from unittest import mock diff --git a/tests/test_traitlets.py b/tests/test_traitlets.py index 673d20c4..19813137 100644 --- a/tests/test_traitlets.py +++ b/tests/test_traitlets.py @@ -60,6 +60,7 @@ traitlets, validate, ) +from traitlets.tests.test_traitlets import TraitTestBase from traitlets.utils import cast_unicode from ._warnings import expected_warnings @@ -1233,59 +1234,6 @@ class Tree(HasTraits): tree.leaves = [1, 2] -class TraitTestBase(TestCase): - """A best testing class for basic trait types.""" - - def assign(self, value): - self.obj.value = value # type:ignore - - def coerce(self, value): - return value - - def test_good_values(self): - if hasattr(self, "_good_values"): - for value in self._good_values: - self.assign(value) - self.assertEqual(self.obj.value, self.coerce(value)) # type:ignore - - def test_bad_values(self): - if hasattr(self, "_bad_values"): - for value in self._bad_values: - try: - self.assertRaises(TraitError, self.assign, value) - except AssertionError: - assert False, value - - def test_default_value(self): - if hasattr(self, "_default_value"): - self.assertEqual(self._default_value, self.obj.value) # type:ignore - - def test_allow_none(self): - if ( - hasattr(self, "_bad_values") - and hasattr(self, "_good_values") - and None in self._bad_values - ): - trait = self.obj.traits()["value"] # type:ignore - try: - trait.allow_none = True - self._bad_values.remove(None) - # skip coerce. Allow None casts None to None. - self.assign(None) - self.assertEqual(self.obj.value, None) # type:ignore - self.test_good_values() - self.test_bad_values() - finally: - # tear down - trait.allow_none = False - self._bad_values.append(None) - - def tearDown(self): - # restore default value after tests, if set - if hasattr(self, "_default_value"): - self.obj.value = self._default_value # type:ignore - - class AnyTrait(HasTraits): value = Any() diff --git a/traitlets/tests/__init__.py b/traitlets/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/traitlets/tests/test_traitlets.py b/traitlets/tests/test_traitlets.py new file mode 100644 index 00000000..10243b9e --- /dev/null +++ b/traitlets/tests/test_traitlets.py @@ -0,0 +1,59 @@ +from __future__ import annotations + +from typing import Any +from unittest import TestCase + +from traitlets import TraitError + + +class TraitTestBase(TestCase): + """A best testing class for basic trait types.""" + + def assign(self, value: Any) -> None: + self.obj.value = value # type:ignore + + def coerce(self, value: Any) -> Any: + return value + + def test_good_values(self) -> None: + if hasattr(self, "_good_values"): + for value in self._good_values: + self.assign(value) + self.assertEqual(self.obj.value, self.coerce(value)) # type:ignore + + def test_bad_values(self) -> None: + if hasattr(self, "_bad_values"): + for value in self._bad_values: + try: + self.assertRaises(TraitError, self.assign, value) + except AssertionError: + raise AssertionError(value) from None + + def test_default_value(self) -> None: + if hasattr(self, "_default_value"): + self.assertEqual(self._default_value, self.obj.value) # type:ignore + + def test_allow_none(self) -> None: + if ( + hasattr(self, "_bad_values") + and hasattr(self, "_good_values") + and None in self._bad_values + ): + trait = self.obj.traits()["value"] # type:ignore + try: + trait.allow_none = True + self._bad_values.remove(None) + # skip coerce. Allow None casts None to None. + self.assign(None) + self.assertEqual(self.obj.value, None) # type:ignore + self.test_good_values() + self.test_bad_values() + finally: + # tear down + trait.allow_none = False + self._bad_values.append(None) + + def tearDown(self) -> None: + # restore default value after tests, if set + if hasattr(self, "_default_value"): + self.obj.value = self._default_value # type:ignore diff --git a/tests/utilities.py b/traitlets/tests/utils.py similarity index 79% rename from tests/utilities.py rename to traitlets/tests/utils.py index 636effad..aca6516b 100644 --- a/tests/utilities.py +++ b/traitlets/tests/utils.py @@ -1,8 +1,10 @@ +from __future__ import annotations + import sys from subprocess import PIPE, Popen -def get_output_error_code(cmd): +def get_output_error_code(cmd: str) -> tuple[str, str, int]: """Get stdout, stderr, and exit code from running a command""" p = Popen(cmd, stdout=PIPE, stderr=PIPE) # noqa out, err = p.communicate() @@ -11,7 +13,7 @@ def get_output_error_code(cmd): return out, err, p.returncode -def check_help_output(pkg, subcommand=None): +def check_help_output(pkg: str, subcommand: str | None = None) -> tuple[str, str]: """test that `python -m PKG [subcommand] -h` works""" cmd = [sys.executable, "-m", pkg] if subcommand: @@ -25,7 +27,7 @@ def check_help_output(pkg, subcommand=None): return out, err -def check_help_all_output(pkg, subcommand=None): +def check_help_all_output(pkg: str, subcommand: str | None = None) -> tuple[str, str]: """test that `python -m PKG --help-all` works""" cmd = [sys.executable, "-m", pkg] if subcommand: From ba1a71c62e38fb572c5fa9074cc1e13a91a4785a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Fri, 29 Sep 2023 04:37:50 -0500 Subject: [PATCH 04/19] cleanup --- pyproject.toml | 3 ++- traitlets/config/loader.py | 10 +++++----- traitlets/tests/utils.py | 9 +++++---- traitlets/traitlets.py | 6 +++--- traitlets/utils/decorators.py | 2 +- traitlets/utils/descriptions.py | 2 +- 6 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e47675a4..e4913467 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,7 +56,7 @@ nowarn = "test -W default {args}" [tool.hatch.envs.typing] features = ["test"] [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"] @@ -74,6 +74,7 @@ fmt = [ ] [tool.mypy] +files = "traitlets" python_version = "3.8" check_untyped_defs = true disallow_any_generics = true diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py index e2ab5c84..716c2fd3 100644 --- a/traitlets/config/loader.py +++ b/traitlets/config/loader.py @@ -295,7 +295,7 @@ def collisions(self, other: Config) -> dict[str, t.Any]: collisions[section][key] = f"{mine[key]!r} ignored, using {theirs[key]!r}" return collisions - def __contains__(self, key) -> bool: + def __contains__(self, key: t.Any) -> bool: # allow nested contains of the form `"Section.key" in config` if "." in key: first, remainder = key.split(".", 1) @@ -345,7 +345,7 @@ def __getitem__(self, key): else: raise - def __setitem__(self, key, value) -> None: + def __setitem__(self, key: str, value: t.Any) -> None: if _is_section_key(key): if not isinstance(value, Config): raise ValueError( @@ -362,7 +362,7 @@ def __getattr__(self, key): except KeyError as e: raise AttributeError(e) from e - def __setattr__(self, key, value) -> None: + def __setattr__(self, key: str, value: t.Any) -> None: if key.startswith("__"): return dict.__setattr__(self, key, value) try: @@ -370,7 +370,7 @@ def __setattr__(self, key, value) -> None: except KeyError as e: raise AttributeError(e) from e - def __delattr__(self, key) -> None: + def __delattr__(self, key: str) -> None: if key.startswith("__"): return dict.__delattr__(self, key) try: @@ -750,7 +750,7 @@ def _add_kv_action(self, key): metavar=key.lstrip("-"), ) - def __contains__(self, key) -> bool: + def __contains__(self, key: t.Any) -> bool: if "=" in key: return False if super().__contains__(key): diff --git a/traitlets/tests/utils.py b/traitlets/tests/utils.py index aca6516b..7e10b5b8 100644 --- a/traitlets/tests/utils.py +++ b/traitlets/tests/utils.py @@ -2,15 +2,16 @@ import sys from subprocess import PIPE, Popen +from typing import Any -def get_output_error_code(cmd: str) -> tuple[str, str, int]: +def get_output_error_code(cmd: str | list[str]) -> tuple[str, str, Any]: """Get stdout, stderr, and exit code from running a command""" p = Popen(cmd, stdout=PIPE, stderr=PIPE) # noqa out, err = p.communicate() - out = out.decode("utf8", "replace") # type:ignore - err = err.decode("utf8", "replace") # type:ignore - return out, err, p.returncode + out_str = out.decode("utf8", "replace") + err_str = err.decode("utf8", "replace") + return out_str, err_str, p.returncode def check_help_output(pkg: str, subcommand: str | None = None) -> tuple[str, str]: diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index 2e7436cb..072ff440 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -216,11 +216,11 @@ def parse_notifier_name(names: Sentinel | str | t.Iterable[Sentinel | str]) -> t class _SimpleTest: - def __init__(self, value) -> None: + def __init__(self, value: t.Any) -> None: self.value = value - def __call__(self, test): - return test == self.value + def __call__(self, test: t.Any) -> bool: + return bool(test == self.value) def __repr__(self) -> str: return " Any: """Get default argument value, given the trait default value.""" return Parameter.empty if value == Undefined else value diff --git a/traitlets/utils/descriptions.py b/traitlets/utils/descriptions.py index b795f230..c068ecdb 100644 --- a/traitlets/utils/descriptions.py +++ b/traitlets/utils/descriptions.py @@ -119,7 +119,7 @@ class name where an object was defined. ) -def _prefix(value: Any): +def _prefix(value: Any) -> str: if isinstance(value, types.MethodType): name = describe(None, value.__self__, verbose=True) + "." else: From c9fdf6c50c6004b1af3bd24edb4e8cf4729fb2d8 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 10:19:21 -0500 Subject: [PATCH 05/19] fix handling of Type --- tests/test_typing.py | 25 +++++++++++++++++++++++++ traitlets/traitlets.py | 10 +++++----- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 2d145424..9abb9167 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -119,6 +119,31 @@ class T(HasTraits): reveal_type(t.foo) # R: builtins.dict[Any, Any] +@pytest.mark.mypy_testing +def mypy_type_typing(): + class KernelSpec: + item = Unicode("foo") + + class KernelSpecManager(HasTraits): + """A manager for kernel specs.""" + + kernel_spec_class = Type( + KernelSpec, + config=True, + help="""The kernel spec class. This is configurable to allow + subclassing of the KernelSpecManager for customized behavior. + """, + ) + + t = KernelSpecManager() + reveal_type( + Type(KernelSpec) + ) # R: traitlets.traitlets.Type[def () -> tests.test_typing.KernelSpec@125, def () -> tests.test_typing.KernelSpec@125] + reveal_type(t.kernel_spec_class) # R: def () -> tests.test_typing.KernelSpec@125 + reveal_type(t.kernel_spec_class()) # R: tests.test_typing.KernelSpec@125 + reveal_type(t.kernel_spec_class().item) # R: builtins.str + + @pytest.mark.mypy_testing def mypy_unicode_typing(): class T(HasTraits): diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index 072ff440..bfe325a1 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -2030,7 +2030,7 @@ class Type(ClassBasedTraitType[G, S]): @t.overload def __init__( - self: Type[object, object], + self: Type[type, type], default_value: Sentinel | None | str = ..., klass: None | str = ..., allow_none: Literal[False] = ..., @@ -2043,8 +2043,8 @@ def __init__( @t.overload def __init__( - self: Type[object | None, object | None], - default_value: S | Sentinel | None | str = ..., + self: Type[type | None, type | None], + default_value: Sentinel | None | str = ..., klass: None | str = ..., allow_none: Literal[True] = ..., read_only: bool | None = ..., @@ -2057,7 +2057,7 @@ def __init__( @t.overload def __init__( self: Type[S, S], - default_value: S | Sentinel | str = ..., + default_value: S = ..., klass: type[S] = ..., allow_none: Literal[False] = ..., read_only: bool | None = ..., @@ -2070,7 +2070,7 @@ def __init__( @t.overload def __init__( self: Type[S | None, S | None], - default_value: S | Sentinel | None | str = ..., + default_value: S | None = ..., klass: type[S] = ..., allow_none: Literal[True] = ..., read_only: bool | None = ..., From 2d2231d287158b72d01119a7ad21c02815533635 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 10:26:45 -0500 Subject: [PATCH 06/19] update tests --- tests/test_typing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 9abb9167..3a3f014a 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -138,9 +138,9 @@ class KernelSpecManager(HasTraits): t = KernelSpecManager() reveal_type( Type(KernelSpec) - ) # R: traitlets.traitlets.Type[def () -> tests.test_typing.KernelSpec@125, def () -> tests.test_typing.KernelSpec@125] - reveal_type(t.kernel_spec_class) # R: def () -> tests.test_typing.KernelSpec@125 - reveal_type(t.kernel_spec_class()) # R: tests.test_typing.KernelSpec@125 + ) # R: traitlets.traitlets.Type[def () -> tests.test_typing.KernelSpec@124, def () -> tests.test_typing.KernelSpec@124] + reveal_type(t.kernel_spec_class) # R: def () -> tests.test_typing.KernelSpec@124 + reveal_type(t.kernel_spec_class()) # R: tests.test_typing.KernelSpec@124 reveal_type(t.kernel_spec_class().item) # R: builtins.str From 4811b59ff40ff40a1499def3d44b2d1b9c3307aa Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 10:55:04 -0500 Subject: [PATCH 07/19] fix test --- tests/test_typing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 3a3f014a..fd534b98 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -137,8 +137,10 @@ class KernelSpecManager(HasTraits): t = KernelSpecManager() reveal_type( - Type(KernelSpec) - ) # R: traitlets.traitlets.Type[def () -> tests.test_typing.KernelSpec@124, def () -> tests.test_typing.KernelSpec@124] + Type( + KernelSpec + ) # R: traitlets.traitlets.Type[def () -> tests.test_typing.KernelSpec@124, def () -> tests.test_typing.KernelSpec@124] + ) reveal_type(t.kernel_spec_class) # R: def () -> tests.test_typing.KernelSpec@124 reveal_type(t.kernel_spec_class()) # R: tests.test_typing.KernelSpec@124 reveal_type(t.kernel_spec_class().item) # R: builtins.str From a204cfcbb516d6046e34743cd613c41920a209c0 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 10:58:36 -0500 Subject: [PATCH 08/19] make a separate file --- tests/test_traitlets.py | 79 ----------------------------- tests/test_traitlets_docstring.py | 84 +++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 79 deletions(-) create mode 100644 tests/test_traitlets_docstring.py diff --git a/tests/test_traitlets.py b/tests/test_traitlets.py index 19813137..ac4cb50a 100644 --- a/tests/test_traitlets.py +++ b/tests/test_traitlets.py @@ -3087,82 +3087,3 @@ def test_all_attribute(): for name in traitlets.__all__: if name not in names: raise ValueError(f"{name} should be removed from __all__") - - -def test_handle_docstring(): - from traitlets.config import Configurable - - class SampleConfigurable(Configurable): - pass - - class TraitTypesSampleConfigurable(Configurable): - """TraitTypesSampleConfigurable docstring""" - - trait_integer = Integer( - help="""trait_integer help text""", - config=True, - ) - trait_integer_nohelp = Integer( - config=True, - ) - trait_integer_noconfig = Integer( - help="""trait_integer_noconfig help text""", - ) - - trait_unicode = Unicode( - help="""trait_unicode help text""", - config=True, - ) - trait_unicode_nohelp = Unicode( - config=True, - ) - trait_unicode_noconfig = Unicode( - help="""trait_unicode_noconfig help text""", - ) - - trait_dict = Dict( - help="""trait_dict help text""", - config=True, - ) - trait_dict_nohelp = Dict( - config=True, - ) - trait_dict_noconfig = Dict( - help="""trait_dict_noconfig help text""", - ) - - trait_instance = Instance( - klass=SampleConfigurable, - help="""trait_instance help text""", - config=True, - ) - trait_instance_nohelp = Instance( - klass=SampleConfigurable, - config=True, - ) - trait_instance_noconfig = Instance( - klass=SampleConfigurable, - help="""trait_instance_noconfig help text""", - ) - - trait_union = Union( - [Integer(), Unicode()], - help="""trait_union help text""", - config=True, - ) - trait_union_nohelp = Union( - [Integer(), Unicode()], - config=True, - ) - trait_union_noconfig = Union( - [Integer(), Unicode()], - help="""trait_union_noconfig help text""", - ) - - base_names = SampleConfigurable().trait_names() - for name in TraitTypesSampleConfigurable().trait_names(): - if name in base_names: - continue - doc = getattr(TraitTypesSampleConfigurable, name).__doc__ - if "nohelp" not in name: - assert doc == f"{name} help text" diff --git a/tests/test_traitlets_docstring.py b/tests/test_traitlets_docstring.py new file mode 100644 index 00000000..70019910 --- /dev/null +++ b/tests/test_traitlets_docstring.py @@ -0,0 +1,84 @@ +"""Tests for traitlets.traitlets.""" + +# Copyright (c) IPython Development Team. +# Distributed under the terms of the Modified BSD License. +# +from traitlets import Dict, Instance, Integer, Unicode, Union +from traitlets.config import Configurable + + +def test_handle_docstring(): + class SampleConfigurable(Configurable): + pass + + class TraitTypesSampleConfigurable(Configurable): + """TraitTypesSampleConfigurable docstring""" + + trait_integer = Integer( + help="""trait_integer help text""", + config=True, + ) + trait_integer_nohelp = Integer( + config=True, + ) + trait_integer_noconfig = Integer( + help="""trait_integer_noconfig help text""", + ) + + trait_unicode = Unicode( + help="""trait_unicode help text""", + config=True, + ) + trait_unicode_nohelp = Unicode( + config=True, + ) + trait_unicode_noconfig = Unicode( + help="""trait_unicode_noconfig help text""", + ) + + trait_dict = Dict( + help="""trait_dict help text""", + config=True, + ) + trait_dict_nohelp = Dict( + config=True, + ) + trait_dict_noconfig = Dict( + help="""trait_dict_noconfig help text""", + ) + + trait_instance = Instance( + klass=SampleConfigurable, + help="""trait_instance help text""", + config=True, + ) + trait_instance_nohelp = Instance( + klass=SampleConfigurable, + config=True, + ) + trait_instance_noconfig = Instance( + klass=SampleConfigurable, + help="""trait_instance_noconfig help text""", + ) + + trait_union = Union( + [Integer(), Unicode()], + help="""trait_union help text""", + config=True, + ) + trait_union_nohelp = Union( + [Integer(), Unicode()], + config=True, + ) + trait_union_noconfig = Union( + [Integer(), Unicode()], + help="""trait_union_noconfig help text""", + ) + + base_names = SampleConfigurable().trait_names() + for name in TraitTypesSampleConfigurable().trait_names(): + if name in base_names: + continue + doc = getattr(TraitTypesSampleConfigurable, name).__doc__ + if "nohelp" not in name: + assert doc == f"{name} help text" From 98bbd43a443a4eaad63777b991641ace5e5f6130 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 10:59:37 -0500 Subject: [PATCH 09/19] undo changes to test file --- tests/test_traitlets.py | 54 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/test_traitlets.py b/tests/test_traitlets.py index ac4cb50a..62fa726f 100644 --- a/tests/test_traitlets.py +++ b/tests/test_traitlets.py @@ -60,7 +60,6 @@ traitlets, validate, ) -from traitlets.tests.test_traitlets import TraitTestBase from traitlets.utils import cast_unicode from ._warnings import expected_warnings @@ -1234,6 +1233,59 @@ class Tree(HasTraits): tree.leaves = [1, 2] +class TraitTestBase(TestCase): + """A best testing class for basic trait types.""" + + def assign(self, value): + self.obj.value = value # type:ignore + + def coerce(self, value): + return value + + def test_good_values(self): + if hasattr(self, "_good_values"): + for value in self._good_values: + self.assign(value) + self.assertEqual(self.obj.value, self.coerce(value)) # type:ignore + + def test_bad_values(self): + if hasattr(self, "_bad_values"): + for value in self._bad_values: + try: + self.assertRaises(TraitError, self.assign, value) + except AssertionError: + assert False, value + + def test_default_value(self): + if hasattr(self, "_default_value"): + self.assertEqual(self._default_value, self.obj.value) # type:ignore + + def test_allow_none(self): + if ( + hasattr(self, "_bad_values") + and hasattr(self, "_good_values") + and None in self._bad_values + ): + trait = self.obj.traits()["value"] # type:ignore + try: + trait.allow_none = True + self._bad_values.remove(None) + # skip coerce. Allow None casts None to None. + self.assign(None) + self.assertEqual(self.obj.value, None) # type:ignore + self.test_good_values() + self.test_bad_values() + finally: + # tear down + trait.allow_none = False + self._bad_values.append(None) + + def tearDown(self): + # restore default value after tests, if set + if hasattr(self, "_default_value"): + self.obj.value = self._default_value # type:ignore + + class AnyTrait(HasTraits): value = Any() From c1b5d0e4a82aa64497bd7909ea73203cf917cbd7 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 11:02:51 -0500 Subject: [PATCH 10/19] simplify test --- tests/test_typing.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index fd534b98..509af244 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -136,11 +136,6 @@ class KernelSpecManager(HasTraits): ) t = KernelSpecManager() - reveal_type( - Type( - KernelSpec - ) # R: traitlets.traitlets.Type[def () -> tests.test_typing.KernelSpec@124, def () -> tests.test_typing.KernelSpec@124] - ) reveal_type(t.kernel_spec_class) # R: def () -> tests.test_typing.KernelSpec@124 reveal_type(t.kernel_spec_class()) # R: tests.test_typing.KernelSpec@124 reveal_type(t.kernel_spec_class().item) # R: builtins.str From 86af599289a54f7c3c5c3f41c38914a2cf3e3d1a Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 11:04:29 -0500 Subject: [PATCH 11/19] add test for importstring --- tests/test_typing.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/test_typing.py b/tests/test_typing.py index 509af244..3d9734c0 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -134,11 +134,14 @@ class KernelSpecManager(HasTraits): subclassing of the KernelSpecManager for customized behavior. """, ) + other_class = Type("foo.bar.baz") t = KernelSpecManager() reveal_type(t.kernel_spec_class) # R: def () -> tests.test_typing.KernelSpec@124 reveal_type(t.kernel_spec_class()) # R: tests.test_typing.KernelSpec@124 reveal_type(t.kernel_spec_class().item) # R: builtins.str + reveal_type(t.other_class) # R: Any + reveal_type(t.other_class()) # R: Any @pytest.mark.mypy_testing From 8d39beccee6927cf8d3f7e24eaa01c20bbfdd30b Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 12:14:43 -0500 Subject: [PATCH 12/19] Fix test --- tests/test_typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_typing.py b/tests/test_typing.py index 3d9734c0..2b4073ec 100644 --- a/tests/test_typing.py +++ b/tests/test_typing.py @@ -140,7 +140,7 @@ class KernelSpecManager(HasTraits): reveal_type(t.kernel_spec_class) # R: def () -> tests.test_typing.KernelSpec@124 reveal_type(t.kernel_spec_class()) # R: tests.test_typing.KernelSpec@124 reveal_type(t.kernel_spec_class().item) # R: builtins.str - reveal_type(t.other_class) # R: Any + reveal_type(t.other_class) # R: builtins.type reveal_type(t.other_class()) # R: Any From 6ba6537c38d9f4dd9e6c1946138956e87e4be6a5 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 20:11:07 -0500 Subject: [PATCH 13/19] wip --- pyproject.toml | 2 + traitlets/config/application.py | 189 ++++++++++++++++--------------- traitlets/config/configurable.py | 59 ++++++---- 3 files changed, 134 insertions(+), 116 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e4913467..81861e5a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -162,6 +162,8 @@ ignore = [ "ANN401", # Missing type annotation for `self` in method "ANN101", + # Missing type annotation for `cls` in classmethod + "ANN102", # ANN202 Missing return type annotation for private function "ANN202", # Allow non-abstract empty methods in abstract base classes diff --git a/traitlets/config/application.py b/traitlets/config/application.py index 60041778..6c3dfede 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -2,8 +2,8 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. +from __future__ import annotations -# ruff: noqa: ANN201, ANN001, ANN204, ANN102, ANN003, ANN206, ANN002 import functools import json import logging @@ -19,7 +19,7 @@ from textwrap import dedent from typing import Any, Callable, TypeVar, cast -from traitlets.config.configurable import Configurable, SingletonConfigurable +from traitlets.config.configurable import Bunch, Configurable, SingletonConfigurable from traitlets.config.loader import ( ArgumentError, Config, @@ -96,6 +96,9 @@ IS_PYTHONW = sys.executable and sys.executable.endswith("pythonw.exe") T = TypeVar("T", bound=Callable[..., Any]) +AnyLogger = logging.Logger | logging.LoggerAdapter +StrDict = dict[str, t.Any] +ArgvType = list[str] | None def catch_config_error(method: T) -> T: @@ -108,7 +111,7 @@ def catch_config_error(method: T) -> T: """ @functools.wraps(method) - def inner(app, *args, **kwargs): + def inner(app: Application, *args: t.Any, **kwargs: t.Any) -> t.Any: try: return method(app, *args, **kwargs) except (TraitError, ArgumentError) as e: @@ -136,7 +139,7 @@ class LevelFormatter(logging.Formatter): highlevel_limit = logging.WARN highlevel_format = " %(levelname)s |" - def format(self, record): + def format(self, record: logging.LogRecord) -> str: if record.levelno >= self.highlevel_limit: record.highlevel = self.highlevel_format % record.__dict__ else: @@ -149,35 +152,27 @@ class Application(SingletonConfigurable): # The name of the application, will usually match the name of the command # line application - name: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode("application") + name = Unicode("application") # The description of the application that is printed at the beginning # of the help. - description: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode( - "This is an application." - ) + description = Unicode("This is an application.") # default section descriptions - option_description: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode( - option_description - ) - keyvalue_description: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode( - keyvalue_description - ) - subcommand_description: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode( - subcommand_description - ) + option_description = Unicode(option_description) + keyvalue_description = Unicode(keyvalue_description) + subcommand_description = Unicode(subcommand_description) python_config_loader_class = PyFileConfigLoader json_config_loader_class = JSONFileConfigLoader # The usage and example string that goes at the end of the help string. - examples: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode() + example = Unicode() # A sequence of Configurable subclasses whose config=True attributes will # be exposed at the command line. - classes: t.List[t.Type[t.Any]] = [] + classes: list[type[t.Any]] = [] - def _classes_inc_parents(self, classes=None): + def _classes_inc_parents(self, classes: list[type[Any]] | None = None): """Iterate through configurable classes, including configurable parents :param classes: @@ -198,18 +193,16 @@ def _classes_inc_parents(self, classes=None): yield parent # The version string of this application. - version: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode("0.0") + version = Unicode("0.0") # the argv used to initialize the application - argv: t.Union[t.List[str], List] = List() + argv: list[str] = List() # Whether failing to load config files should prevent startup - raise_config_file_errors: t.Union[bool, Bool[bool, t.Union[bool, int]]] = Bool( - TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR - ) + raise_config_file_errors = Bool(TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR) # The log level for the application - log_level: t.Union[str, int, Enum[t.Any, t.Any]] = Enum( + log_level: str | int = Enum( (0, 10, 20, 30, 40, 50, "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"), default_value=logging.WARN, help="Set the log level by value or name.", @@ -217,16 +210,16 @@ def _classes_inc_parents(self, classes=None): _log_formatter_cls = LevelFormatter - log_datefmt: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode( + log_datefmt = Unicode( "%Y-%m-%d %H:%M:%S", help="The date format used by logging formatters for %(asctime)s" ).tag(config=True) - log_format: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode( + log_format = Unicode( "[%(name)s]%(highlevel)s %(message)s", help="The Logging format template", ).tag(config=True) - def get_default_logging_config(self): + def get_default_logging_config(self) -> StrDict: """Return the base logging configuration. The default is to log to stderr using a StreamHandler, if no default @@ -239,7 +232,7 @@ def get_default_logging_config(self): control of logging. """ - config: t.Dict[str, t.Any] = { + config: StrDict = { "version": 1, "handlers": { "console": { @@ -278,7 +271,7 @@ def get_default_logging_config(self): return config @observe("log_datefmt", "log_format", "log_level", "logging_config") - def _observe_logging_change(self, change): + def _observe_logging_change(self, change: Bunch) -> None: # convert log level strings to ints log_level = self.log_level if isinstance(log_level, str): @@ -286,10 +279,10 @@ def _observe_logging_change(self, change): self._configure_logging() @observe("log", type="default") - def _observe_logging_default(self, change): + def _observe_logging_default(self, change: Bunch) -> None: self._configure_logging() - def _configure_logging(self): + def _configure_logging(self) -> None: config = self.get_default_logging_config() nested_update(config, self.logging_config or {}) dictConfig(config) @@ -297,7 +290,7 @@ def _configure_logging(self): self._logging_configured = True @default("log") - def _log_default(self): + def _log_default(self) -> AnyLogger: """Start logging for this application.""" log = logging.getLogger(self.__class__.__name__) log.propagate = False @@ -366,17 +359,13 @@ def _log_default(self): #: Values might be like "Class.trait" strings of two-tuples: (Class.trait, help-text), # or just the "Class.trait" string, in which case the help text is inferred from the # corresponding trait - aliases: t.Dict[t.Union[str, t.Tuple[str, ...]], t.Union[str, t.Tuple[str, str]]] = { - "log-level": "Application.log_level" - } + aliases: StrDict = {"log-level": "Application.log_level"} # flags for loading Configurables or store_const style flags # flags are loaded from this dict by '--key' flags # this must be a dict of two-tuples, the first element being the Config/dict # and the second being the help string for the flag - flags: t.Dict[ - t.Union[str, t.Tuple[str, ...]], t.Tuple[t.Union[t.Dict[str, t.Any], Config], str] - ] = { + flags: StrDict = { "debug": ( { "Application": { @@ -408,12 +397,14 @@ def _log_default(self): # this must be a dict of two-tuples, # the first element being the application class/import string # and the second being the help string for the subcommand - subcommands: t.Union[t.Dict[str, t.Tuple[t.Any, str]], Dict] = Dict() + subcommands: dict[str, Any] = Dict() # parse_command_line will initialize a subapp, if requested - subapp = Instance("traitlets.config.application.Application", allow_none=True) + subapp: Application | None = Instance( + "traitlets.config.application.Application", allow_none=True + ) # extra command-line arguments that don't set config values - extra_args: t.Union[t.List[str], List] = List(Unicode()) + extra_args: list[str] = List(Unicode()) cli_config = Instance( Config, @@ -426,22 +417,22 @@ def _log_default(self): """, ) - _loaded_config_files = List() + _loaded_config_files: list[str] = List() - show_config: t.Union[bool, Bool[bool, t.Union[bool, int]]] = Bool( + show_config = Bool( help="Instead of starting the Application, dump configuration to stdout" ).tag(config=True) - show_config_json: t.Union[bool, Bool[bool, t.Union[bool, int]]] = Bool( + show_config_json = Bool( help="Instead of starting the Application, dump configuration to stdout (as JSON)" ).tag(config=True) @observe("show_config_json") - def _show_config_json_changed(self, change): + def _show_config_json_changed(self, change: Bunch) -> None: self.show_config = change.new @observe("show_config") - def _show_config_changed(self, change): + def _show_config_changed(self, change: Bunch) -> None: if change.new: self._save_start = self.start self.start = self.start_show_config # type:ignore[method-assign] @@ -460,19 +451,19 @@ def __init__(self, **kwargs: t.Any) -> None: @observe("config") @observe_compat - def _config_changed(self, change): + def _config_changed(self, change: Bunch) -> None: super()._config_changed(change) self.log.debug("Config changed: %r", change.new) @catch_config_error - def initialize(self, argv=None): + def initialize(self, argv: ArgvType = None) -> None: """Do the basic steps to configure me. Override in subclasses. """ self.parse_command_line(argv) - def start(self): + def start(self) -> None: """Start the app mainloop. Override in subclasses. @@ -480,7 +471,7 @@ def start(self): if self.subapp is not None: return self.subapp.start() - def start_show_config(self): + def start_show_config(self) -> None: """start function used when show_config is True""" config = self.config.copy() # exclude show_config flags from displayed config @@ -507,17 +498,17 @@ def start_show_config(self): if not class_config: continue print(classname) - pformat_kwargs: t.Dict[str, t.Any] = dict(indent=4, compact=True) + pformat_kwargs: StrDict = dict(indent=4, compact=True) for traitname in sorted(class_config): value = class_config[traitname] print(f" .{traitname} = {pprint.pformat(value, **pformat_kwargs)}") - def print_alias_help(self): + def print_alias_help(self) -> None: """Print the alias parts of the help.""" print("\n".join(self.emit_alias_help())) - def emit_alias_help(self): + def emit_alias_help(self) -> None: """Yield the lines for alias part of the help.""" if not self.aliases: return @@ -528,7 +519,7 @@ def emit_alias_help(self): for c in cls.mro()[:-3]: classdict[c.__name__] = c - fhelp: t.Optional[str] + fhelp: str | None for alias, longname in self.aliases.items(): try: if isinstance(longname, tuple): @@ -556,11 +547,11 @@ def emit_alias_help(self): self.log.error("Failed collecting help-message for alias %r, due to: %s", alias, ex) raise - def print_flag_help(self): + def print_flag_help(self) -> None: """Print the flag part of the help.""" print("\n".join(self.emit_flag_help())) - def emit_flag_help(self): + def emit_flag_help(self) -> None: """Yield the lines for the flag part of the help.""" if not self.flags: return @@ -584,11 +575,11 @@ def emit_flag_help(self): self.log.error("Failed collecting help-message for flag %r, due to: %s", flags, ex) raise - def print_options(self): + def print_options(self) -> None: """Print the options part of the help.""" print("\n".join(self.emit_options_help())) - def emit_options_help(self): + def emit_options_help(self) -> None: """Yield the lines for the options part of the help.""" if not self.flags and not self.aliases: return @@ -603,11 +594,11 @@ def emit_options_help(self): yield from self.emit_alias_help() yield "" - def print_subcommands(self): + def print_subcommands(self) -> None: """Print the subcommand part of the help.""" print("\n".join(self.emit_subcommands_help())) - def emit_subcommands_help(self): + def emit_subcommands_help(self) -> None: """Yield the lines for the subcommand part of the help.""" if not self.subcommands: return @@ -624,7 +615,7 @@ def emit_subcommands_help(self): yield indent(dedent(help.strip())) yield "" - def emit_help_epilogue(self, classes): + def emit_help_epilogue(self, classes: bool) -> t.Generator[str, None, None]: """Yield the very bottom lines of the help message. If classes=False (the default), print `--help-all` msg. @@ -633,14 +624,14 @@ def emit_help_epilogue(self, classes): yield "To see all available configurables, use `--help-all`." yield "" - def print_help(self, classes=False): + def print_help(self, classes: bool = False) -> None: """Print the help for each Configurable class in self.classes. If classes=False (the default), only flags and aliases are printed. """ print("\n".join(self.emit_help(classes=classes))) - def emit_help(self, classes=False): + def emit_help(self, classes: bool = False) -> None: """Yield the help-lines for each Configurable class in self.classes. If classes=False (the default), only flags and aliases are printed. @@ -665,28 +656,28 @@ def emit_help(self, classes=False): yield from self.emit_help_epilogue(classes) - def document_config_options(self): + def document_config_options(self) -> str: """Generate rST format documentation for the config options this application Returns a multiline string. """ return "\n".join(c.class_config_rst_doc() for c in self._classes_inc_parents()) - def print_description(self): + def print_description(self) -> None: """Print the application description.""" print("\n".join(self.emit_description())) - def emit_description(self): + def emit_description(self) -> t.Generator[str, None, None]: """Yield lines with the application description.""" for p in wrap_paragraphs(self.description or self.__doc__ or ""): yield p yield "" - def print_examples(self): + def print_examples(self) -> None: """Print usage and examples (see `emit_examples()`).""" print("\n".join(self.emit_examples())) - def emit_examples(self): + def emit_examples(self) -> None: """Yield lines with the usage and examples. This usage string goes at the end of the command line help string @@ -699,12 +690,12 @@ def emit_examples(self): yield indent(dedent(self.examples.strip())) yield "" - def print_version(self): + def print_version(self) -> None: """Print the version string.""" print(self.version) @catch_config_error - def initialize_subcommand(self, subc, argv=None): + def initialize_subcommand(self, subc: str, argv: ArgvType = None) -> None: """Initialize a subcommand with argv.""" val = self.subcommands.get(subc) assert val is not None @@ -728,7 +719,7 @@ def initialize_subcommand(self, subc, argv=None): # ... and finally initialize subapp. self.subapp.initialize(argv) - def flatten_flags(self): + def flatten_flags(self) -> tuple[dict[str, Any], dict[str, Any]]: """Flatten flags and aliases for loaders, so cl-args override as expected. This prevents issues such as an alias pointing to InteractiveShell, @@ -751,7 +742,7 @@ def flatten_flags(self): mro_tree[parent.__name__].append(clsname) # flatten aliases, which have the form: # { 'alias' : 'Class.trait' } - aliases: t.Dict[str, str] = {} + aliases: dict[str, str] = {} for alias, longname in self.aliases.items(): if isinstance(longname, tuple): longname, _ = longname @@ -769,7 +760,7 @@ def flatten_flags(self): # { 'key' : ({'Cls' : {'trait' : value}}, 'help')} flags = {} for key, (flagdict, help) in self.flags.items(): - newflag: t.Dict[t.Any, t.Any] = {} + newflag: dict[t.Any, t.Any] = {} for cls, subdict in flagdict.items(): # type:ignore children = mro_tree[cls] # type:ignore[index] # exactly one descendent, promote flag section @@ -787,13 +778,19 @@ def flatten_flags(self): flags[k] = (newflag, help) return flags, aliases - def _create_loader(self, argv, aliases, flags, classes): + def _create_loader( + self, + argv: list[str] | None, + aliases: StrDict, + flags: StrDict, + classes: list[type[t.Any]] | None, + ): return KVArgParseConfigLoader( argv, aliases, flags, classes=classes, log=self.log, subcommands=self.subcommands ) @classmethod - def _get_sys_argv(cls, check_argcomplete: bool = False) -> t.List[str]: + def _get_sys_argv(cls, check_argcomplete: bool = False) -> list[str]: """Get `sys.argv` or equivalent from `argcomplete` `argcomplete`'s strategy is to call the python script with no arguments, @@ -819,7 +816,7 @@ def _get_sys_argv(cls, check_argcomplete: bool = False) -> t.List[str]: return sys.argv @classmethod - def _handle_argcomplete_for_subcommand(cls): + def _handle_argcomplete_for_subcommand(cls) -> None: """Helper for `argcomplete` to recognize `traitlets` subcommands `argcomplete` does not know that `traitlets` has already consumed subcommands, @@ -839,7 +836,7 @@ def _handle_argcomplete_for_subcommand(cls): pass @catch_config_error - def parse_command_line(self, argv=None): + def parse_command_line(self, argv: ArgvType = None) -> None: """Parse the command line arguments.""" assert not isinstance(argv, str) if argv is None: @@ -890,7 +887,13 @@ def parse_command_line(self, argv=None): self.extra_args = loader.extra_args @classmethod - def _load_config_files(cls, basefilename, path=None, log=None, raise_config_file_errors=False): + def _load_config_files( + cls, + basefilename: str, + path: str | None = None, + log: AnyLogger | None = None, + raise_config_file_errors: bool = False, + ) -> t.Generator[t.Any, None, None]: """Load config files (py,json) by filename and path. yield each config object in turn. @@ -904,8 +907,8 @@ def _load_config_files(cls, basefilename, path=None, log=None, raise_config_file if log: log.debug("Looking for %s in %s", basefilename, current or os.getcwd()) jsonloader = cls.json_config_loader_class(basefilename + ".json", path=current, log=log) - loaded: t.List[t.Any] = [] - filenames: t.List[str] = [] + loaded: list[t.Any] = [] + filenames: list[str] = [] for loader in [pyloader, jsonloader]: config = None try: @@ -941,12 +944,12 @@ def _load_config_files(cls, basefilename, path=None, log=None, raise_config_file filenames.append(loader.full_filename) @property - def loaded_config_files(self): + def loaded_config_files(self) -> list[str]: """Currently loaded configuration files""" return self._loaded_config_files[:] @catch_config_error - def load_config_file(self, filename, path=None): + def load_config_file(self, filename: str, path: str | None = None) -> None: """Load config files by filename and path.""" filename, ext = os.path.splitext(filename) new_config = Config() @@ -965,7 +968,9 @@ def load_config_file(self, filename, path=None): new_config.merge(self.cli_config) self.update_config(new_config) - def _classes_with_config_traits(self, classes=None): + def _classes_with_config_traits( + self, classes: list[type[t.Any]] | None = None + ) -> t.Generator[t.Any, None, None]: """ Yields only classes with configurable traits, and their subclasses. @@ -987,7 +992,7 @@ def _classes_with_config_traits(self, classes=None): for cls in self._classes_inc_parents(classes) ) - def is_any_parent_included(cls): + def is_any_parent_included(cls: t.Any): return any(b in cls_to_config and cls_to_config[b] for b in cls.__bases__) # Mark "empty" classes for inclusion if their parents own-traits, @@ -1005,7 +1010,7 @@ def is_any_parent_included(cls): if inc_yes: yield cl - def generate_config_file(self, classes=None): + def generate_config_file(self, classes: list[type[t.Any]] | None = None) -> str: """generate default config file from Configurables""" lines = ["# Configuration file for %s." % self.name] lines.append("") @@ -1017,7 +1022,7 @@ def generate_config_file(self, classes=None): lines.append(cls.class_config_section(config_classes)) return "\n".join(lines) - def close_handlers(self): + def close_handlers(self) -> None: if getattr(self, "_logging_configured", False): # don't attempt to close handlers unless they have been opened # (note accessing self.log.handlers will create handlers if they @@ -1027,7 +1032,7 @@ def close_handlers(self): handler.close() self._logging_configured = False - def exit(self, exit_status=0): + def exit(self, exit_status: int = 0) -> None: self.log.debug("Exiting application: %s" % self.name) self.close_handlers() sys.exit(exit_status) @@ -1036,7 +1041,7 @@ def __del__(self) -> None: self.close_handlers() @classmethod - def launch_instance(cls, argv=None, **kwargs): + def launch_instance(cls, argv: ArgvType = None, **kwargs: Any) -> None: """Launch a global instance of this Application If a global instance already exists, this reinitializes and starts it @@ -1054,7 +1059,7 @@ def launch_instance(cls, argv=None, **kwargs): default_flags = Application.flags -def boolean_flag(name, configurable, set_help="", unset_help=""): +def boolean_flag(name: str, configurable: str, set_help: str = "", unset_help: str = "") -> StrDict: """Helper for building basic --trait, --no-trait flags. Parameters @@ -1085,7 +1090,7 @@ def boolean_flag(name, configurable, set_help="", unset_help=""): return {name: (setter, set_help), "no-" + name: (unsetter, unset_help)} -def get_config(): +def get_config() -> Config: """Get the config object for the global Application instance, if there is one otherwise return an empty config object diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py index 620c26cb..80d10ed3 100644 --- a/traitlets/config/configurable.py +++ b/traitlets/config/configurable.py @@ -2,7 +2,7 @@ # Copyright (c) IPython Development Team. # Distributed under the terms of the Modified BSD License. -# ruff: noqa: ANN201, ANN001, ANN204, ANN102, ANN003, ANN206, ANN002 +from __future__ import annotations import logging import typing as t @@ -11,10 +11,12 @@ from traitlets.traitlets import ( Any, + Bunch, Container, Dict, HasTraits, Instance, + TraitType, default, observe, observe_compat, @@ -45,7 +47,9 @@ class MultipleInstanceError(ConfigurableError): class Configurable(HasTraits): config = Instance(Config, (), {}) - parent = Instance("traitlets.config.configurable.Configurable", allow_none=True) + parent: Configurable | None = Instance( + "traitlets.config.configurable.Configurable", allow_none=True + ) def __init__(self, **kwargs: t.Any) -> None: """Create a configurable given a config config. @@ -87,7 +91,7 @@ def __init__(self, config=None): # record traits set by config config_override_names = set() - def notice_config_override(change): + def notice_config_override(change: Bunch) -> None: """Record traits set by both config and kwargs. They will need to be overridden again after loading config. @@ -120,7 +124,7 @@ def notice_config_override(change): # ------------------------------------------------------------------------- @classmethod - def section_names(cls): + def section_names(cls) -> list[str]: """return section names as a list""" return [ c.__name__ @@ -128,7 +132,7 @@ def section_names(cls): if issubclass(c, Configurable) and issubclass(cls, c) ] - def _find_my_config(self, cfg): + def _find_my_config(self, cfg: Config) -> t.Any: """extract my config from a global Config object will construct a Config object of only the config values that apply to me @@ -153,7 +157,9 @@ def _find_my_config(self, cfg): my_config.merge(c[sname]) return my_config - def _load_config(self, cfg, section_names=None, traits=None): + def _load_config( + self, cfg: Config, section_names: list[str] | None = None, traits: list[str] | None = None + ) -> None: """load traits from a Config object""" if traits is None: @@ -187,7 +193,7 @@ def _load_config(self, cfg, section_names=None, traits=None): warn = self.log.warning else: - def warn(msg): + def warn(msg: str) -> None: return warnings.warn(msg, UserWarning, stacklevel=9) matches = get_close_matches(name, traits) @@ -203,7 +209,7 @@ def warn(msg): @observe("config") @observe_compat - def _config_changed(self, change): + def _config_changed(self, change: Bunch) -> None: """Update all the class traits having ``config=True`` in metadata. For any class trait with a ``config`` metadata attribute that is @@ -219,7 +225,7 @@ def _config_changed(self, change): section_names = self.section_names() self._load_config(change.new, traits=traits, section_names=section_names) - def update_config(self, config): + def update_config(self, config: Config) -> None: """Update config and load the new values""" # traitlets prior to 4.2 created a copy of self.config in order to trigger change events. # Some projects (IPython < 5) relied upon one side effect of this, @@ -236,7 +242,7 @@ def update_config(self, config): # DO NOT trigger full trait-change @classmethod - def class_get_help(cls, inst=None): + def class_get_help(cls, inst: HasTraits | None = None) -> str: """Get the help string for this class in ReST format. If `inst` is given, its current trait values will be used in place of @@ -253,7 +259,9 @@ class defaults. return "\n".join(final_help) @classmethod - def class_get_trait_help(cls, trait, inst=None, helptext=None): + def class_get_trait_help( + cls, trait: TraitType, inst: HasTraits | None = None, helptext: str | None = None + ) -> str: """Get the helptext string for a single trait. :param inst: @@ -305,12 +313,12 @@ def class_get_trait_help(cls, trait, inst=None, helptext=None): return "\n".join(lines) @classmethod - def class_print_help(cls, inst=None): + def class_print_help(cls, inst: HasTraits | None = None) -> None: """Get the help string for a single trait and print it.""" print(cls.class_get_help(inst)) @classmethod - def _defining_class(cls, trait, classes): + def _defining_class(cls, trait: TraitType, classes: list[HasTraits]) -> Configurable: """Get the class that defines a trait For reducing redundant help output in config files. @@ -338,7 +346,7 @@ def _defining_class(cls, trait, classes): return defining_cls @classmethod - def class_config_section(cls, classes=None): + def class_config_section(cls, classes: list[HasTraits] | None = None) -> str: """Get the config section for this class. Parameters @@ -348,7 +356,7 @@ def class_config_section(cls, classes=None): Used to reduce redundant information. """ - def c(s): + def c(s: str) -> str: """return a commented, wrapped block.""" s = "\n\n".join(wrap_paragraphs(s, 78)) @@ -398,7 +406,7 @@ def c(s): return "\n".join(lines) @classmethod - def class_config_rst_doc(cls): + def class_config_rst_doc(cls) -> str: """Generate rST documentation for this class' config options. Excludes traits defined on parent classes. @@ -447,10 +455,10 @@ class LoggingConfigurable(Configurable): is to get the logger from the currently running Application. """ - log = Any(help="Logger or LoggerAdapter instance") + log: logging.Logger | logging.LoggerAdapter = Any(help="Logger or LoggerAdapter instance") @validate("log") - def _validate_log(self, proposal): + def _validate_log(self, proposal: Bunch) -> logging.Logger | logging.LoggerAdapter: if not isinstance(proposal.value, (logging.Logger, logging.LoggerAdapter)): # warn about unsupported type, but be lenient to allow for duck typing warnings.warn( @@ -462,7 +470,7 @@ def _validate_log(self, proposal): return proposal.value @default("log") - def _log_default(self): + def _log_default(self) -> logging.Logger | logging.LoggerAdapter: if isinstance(self.parent, LoggingConfigurable): assert self.parent is not None return self.parent.log @@ -470,7 +478,7 @@ def _log_default(self): return log.get_logger() - def _get_log_handler(self): + def _get_log_handler(self) -> logging.Handler: """Return the default Handler Returns None if none can be found @@ -487,6 +495,9 @@ def _get_log_handler(self): return logger.handlers[0] +T = t.TypeVar('T', bound='SingletonConfigurable') + + class SingletonConfigurable(LoggingConfigurable): """A configurable that only allows one instance. @@ -498,7 +509,7 @@ class SingletonConfigurable(LoggingConfigurable): _instance = None @classmethod - def _walk_mro(cls): + def _walk_mro(cls) -> t.Generator[HasTraits, None, None]: """Walk the cls.mro() for parent classes that are also singletons For use in instance() @@ -513,7 +524,7 @@ def _walk_mro(cls): yield subclass @classmethod - def clear_instance(cls): + def clear_instance(cls) -> None: """unset _instance for this class and singleton parents.""" if not cls.initialized(): return @@ -524,7 +535,7 @@ def clear_instance(cls): subclass._instance = None @classmethod - def instance(cls, *args, **kwargs): + def instance(cls: type[T], *args: t.Any, **kwargs: t.Any) -> T: """Returns a global instance of this class. This method create a new instance if none have previously been created @@ -568,6 +579,6 @@ def instance(cls, *args, **kwargs): ) @classmethod - def initialized(cls): + def initialized(cls) -> bool: """Has an instance been created?""" return hasattr(cls, "_instance") and cls._instance is not None From 1357c3e2aab89a006c2340a622b102ee65ab9493 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 20:14:47 -0500 Subject: [PATCH 14/19] wip --- traitlets/config/application.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/traitlets/config/application.py b/traitlets/config/application.py index 6c3dfede..45c93f90 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -99,6 +99,7 @@ AnyLogger = logging.Logger | logging.LoggerAdapter StrDict = dict[str, t.Any] ArgvType = list[str] | None +ClassesType = list[type[Configurable]] def catch_config_error(method: T) -> T: @@ -166,13 +167,13 @@ class Application(SingletonConfigurable): json_config_loader_class = JSONFileConfigLoader # The usage and example string that goes at the end of the help string. - example = Unicode() + examples = Unicode() # A sequence of Configurable subclasses whose config=True attributes will # be exposed at the command line. - classes: list[type[t.Any]] = [] + classes: ClassesType = [] - def _classes_inc_parents(self, classes: list[type[Any]] | None = None): + def _classes_inc_parents(self, classes: ClassesType | None = None): """Iterate through configurable classes, including configurable parents :param classes: @@ -783,7 +784,7 @@ def _create_loader( argv: list[str] | None, aliases: StrDict, flags: StrDict, - classes: list[type[t.Any]] | None, + classes: ClassesType | None, ): return KVArgParseConfigLoader( argv, aliases, flags, classes=classes, log=self.log, subcommands=self.subcommands @@ -969,7 +970,7 @@ def load_config_file(self, filename: str, path: str | None = None) -> None: self.update_config(new_config) def _classes_with_config_traits( - self, classes: list[type[t.Any]] | None = None + self, classes: ClassesType | None = None ) -> t.Generator[t.Any, None, None]: """ Yields only classes with configurable traits, and their subclasses. @@ -1010,7 +1011,7 @@ def is_any_parent_included(cls: t.Any): if inc_yes: yield cl - def generate_config_file(self, classes: list[type[t.Any]] | None = None) -> str: + def generate_config_file(self, classes: ClassesType | None = None) -> str: """generate default config file from Configurables""" lines = ["# Configuration file for %s." % self.name] lines.append("") From 5893fd0f0c2121b230287836cbd2db61a4fc6e94 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 20:20:25 -0500 Subject: [PATCH 15/19] defer typing declarations --- traitlets/config/application.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/traitlets/config/application.py b/traitlets/config/application.py index 45c93f90..840b620a 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -95,11 +95,12 @@ IS_PYTHONW = sys.executable and sys.executable.endswith("pythonw.exe") -T = TypeVar("T", bound=Callable[..., Any]) -AnyLogger = logging.Logger | logging.LoggerAdapter -StrDict = dict[str, t.Any] -ArgvType = list[str] | None -ClassesType = list[type[Configurable]] +if t.TYPE_CHECKING: + T = TypeVar("T", bound=Callable[..., Any]) + AnyLogger = logging.Logger | logging.LoggerAdapter + StrDict = dict[str, t.Any] + ArgvType = list[str] | None + ClassesType = list[type[Configurable]] def catch_config_error(method: T) -> T: From 8a977ba93ef79f3a1072dfae2e66611475321625 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 20:22:29 -0500 Subject: [PATCH 16/19] fix usage of T --- traitlets/config/application.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/traitlets/config/application.py b/traitlets/config/application.py index 840b620a..afb1e102 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -95,8 +95,9 @@ IS_PYTHONW = sys.executable and sys.executable.endswith("pythonw.exe") +T = TypeVar("T", bound=Callable[..., Any]) + if t.TYPE_CHECKING: - T = TypeVar("T", bound=Callable[..., Any]) AnyLogger = logging.Logger | logging.LoggerAdapter StrDict = dict[str, t.Any] ArgvType = list[str] | None From 2618a0586e2d6a2d441d95c5aea555945095f0c4 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 21:45:07 -0500 Subject: [PATCH 17/19] type cleanup --- traitlets/config/application.py | 89 ++++++++++++++++---------------- traitlets/config/configurable.py | 49 ++++++++++-------- traitlets/config/loader.py | 7 ++- traitlets/log.py | 5 +- traitlets/traitlets.py | 10 ++-- 5 files changed, 83 insertions(+), 77 deletions(-) diff --git a/traitlets/config/application.py b/traitlets/config/application.py index afb1e102..316c2a4e 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -17,9 +17,8 @@ from copy import deepcopy from logging.config import dictConfig from textwrap import dedent -from typing import Any, Callable, TypeVar, cast -from traitlets.config.configurable import Bunch, Configurable, SingletonConfigurable +from traitlets.config.configurable import Configurable, SingletonConfigurable from traitlets.config.loader import ( ArgumentError, Config, @@ -40,6 +39,7 @@ observe, observe_compat, ) +from traitlets.utils.bunch import Bunch from traitlets.utils.nested_update import nested_update from traitlets.utils.text import indent, wrap_paragraphs @@ -95,13 +95,11 @@ IS_PYTHONW = sys.executable and sys.executable.endswith("pythonw.exe") -T = TypeVar("T", bound=Callable[..., Any]) - -if t.TYPE_CHECKING: - AnyLogger = logging.Logger | logging.LoggerAdapter - StrDict = dict[str, t.Any] - ArgvType = list[str] | None - ClassesType = list[type[Configurable]] +T = t.TypeVar("T", bound=t.Callable[..., t.Any]) +AnyLogger = t.Union[logging.Logger, logging.LoggerAdapter] +StrDict = t.Dict[str, t.Any] +ArgvType = t.Optional[t.List[str]] +ClassesType = t.List[t.Type[Configurable]] def catch_config_error(method: T) -> T: @@ -122,7 +120,7 @@ def inner(app: Application, *args: t.Any, **kwargs: t.Any) -> t.Any: app.log.debug("Config at the time: %s", app.config) app.exit(1) - return cast(T, inner) + return t.cast(T, inner) class ApplicationError(Exception): @@ -175,7 +173,9 @@ class Application(SingletonConfigurable): # be exposed at the command line. classes: ClassesType = [] - def _classes_inc_parents(self, classes: ClassesType | None = None): + def _classes_inc_parents( + self, classes: ClassesType | None = None + ) -> t.Generator[type[Configurable], None, None]: """Iterate through configurable classes, including configurable parents :param classes: @@ -199,13 +199,13 @@ def _classes_inc_parents(self, classes: ClassesType | None = None): version = Unicode("0.0") # the argv used to initialize the application - argv: list[str] = List() + argv = List() # Whether failing to load config files should prevent startup raise_config_file_errors = Bool(TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR) # The log level for the application - log_level: str | int = Enum( + log_level = Enum( (0, 10, 20, 30, 40, 50, "DEBUG", "INFO", "WARN", "ERROR", "CRITICAL"), default_value=logging.WARN, help="Set the log level by value or name.", @@ -400,14 +400,12 @@ def _log_default(self) -> AnyLogger: # this must be a dict of two-tuples, # the first element being the application class/import string # and the second being the help string for the subcommand - subcommands: dict[str, Any] = Dict() + subcommands = Dict() # parse_command_line will initialize a subapp, if requested - subapp: Application | None = Instance( - "traitlets.config.application.Application", allow_none=True - ) + subapp = Instance("traitlets.config.application.Application", allow_none=True) # extra command-line arguments that don't set config values - extra_args: list[str] = List(Unicode()) + extra_args = List(Unicode()) cli_config = Instance( Config, @@ -420,7 +418,7 @@ def _log_default(self) -> AnyLogger: """, ) - _loaded_config_files: list[str] = List() + _loaded_config_files = List() show_config = Bool( help="Instead of starting the Application, dump configuration to stdout" @@ -472,6 +470,7 @@ def start(self) -> None: Override in subclasses. """ if self.subapp is not None: + assert isinstance(self.subapp, Application) return self.subapp.start() def start_show_config(self) -> None: @@ -511,16 +510,16 @@ def print_alias_help(self) -> None: """Print the alias parts of the help.""" print("\n".join(self.emit_alias_help())) - def emit_alias_help(self) -> None: + def emit_alias_help(self) -> t.Generator[str, None, None]: """Yield the lines for alias part of the help.""" if not self.aliases: return - classdict = {} + classdict: dict[str, type[Configurable]] = {} for cls in self.classes: # include all parents (up to, but excluding Configurable) in available names for c in cls.mro()[:-3]: - classdict[c.__name__] = c + classdict[c.__name__] = t.cast(t.Type[Configurable], c) fhelp: str | None for alias, longname in self.aliases.items(): @@ -534,17 +533,16 @@ def emit_alias_help(self) -> None: cls = classdict[classname] trait = cls.class_traits(config=True)[traitname] - fhelp = cls.class_get_trait_help(trait, helptext=fhelp).splitlines() + fhelp_lines = cls.class_get_trait_help(trait, helptext=fhelp).splitlines() if not isinstance(alias, tuple): - alias = (alias,) + alias = (alias,) # type:ignore[assignment] alias = sorted(alias, key=len) # type:ignore[assignment] alias = ", ".join(("--%s" if len(m) > 1 else "-%s") % m for m in alias) # reformat first line - assert fhelp is not None - fhelp[0] = fhelp[0].replace("--" + longname, alias) # type:ignore - yield from fhelp + fhelp_lines[0] = fhelp_lines[0].replace("--" + longname, alias) + yield from fhelp_lines yield indent("Equivalent to: [--%s]" % longname) except Exception as ex: self.log.error("Failed collecting help-message for alias %r, due to: %s", alias, ex) @@ -554,7 +552,7 @@ def print_flag_help(self) -> None: """Print the flag part of the help.""" print("\n".join(self.emit_flag_help())) - def emit_flag_help(self) -> None: + def emit_flag_help(self) -> t.Generator[str, None, None]: """Yield the lines for the flag part of the help.""" if not self.flags: return @@ -562,7 +560,7 @@ def emit_flag_help(self) -> None: for flags, (cfg, fhelp) in self.flags.items(): try: if not isinstance(flags, tuple): - flags = (flags,) + flags = (flags,) # type:ignore[assignment] flags = sorted(flags, key=len) # type:ignore[assignment] flags = ", ".join(("--%s" if len(m) > 1 else "-%s") % m for m in flags) yield flags @@ -582,7 +580,7 @@ def print_options(self) -> None: """Print the options part of the help.""" print("\n".join(self.emit_options_help())) - def emit_options_help(self) -> None: + def emit_options_help(self) -> t.Generator[str, None, None]: """Yield the lines for the options part of the help.""" if not self.flags and not self.aliases: return @@ -601,7 +599,7 @@ def print_subcommands(self) -> None: """Print the subcommand part of the help.""" print("\n".join(self.emit_subcommands_help())) - def emit_subcommands_help(self) -> None: + def emit_subcommands_help(self) -> t.Generator[str, None, None]: """Yield the lines for the subcommand part of the help.""" if not self.subcommands: return @@ -634,7 +632,7 @@ def print_help(self, classes: bool = False) -> None: """ print("\n".join(self.emit_help(classes=classes))) - def emit_help(self, classes: bool = False) -> None: + def emit_help(self, classes: bool = False) -> t.Generator[str, None, None]: """Yield the help-lines for each Configurable class in self.classes. If classes=False (the default), only flags and aliases are printed. @@ -680,7 +678,7 @@ def print_examples(self) -> None: """Print usage and examples (see `emit_examples()`).""" print("\n".join(self.emit_examples())) - def emit_examples(self) -> None: + def emit_examples(self) -> t.Generator[str, None, None]: """Yield lines with the usage and examples. This usage string goes at the end of the command line help string @@ -720,9 +718,9 @@ def initialize_subcommand(self, subc: str, argv: ArgvType = None) -> None: raise AssertionError("Invalid mappings for subcommand '%s'!" % subc) # ... and finally initialize subapp. - self.subapp.initialize(argv) + self.subapp.initialize(argv) # type:ignore[union-attr] - def flatten_flags(self) -> tuple[dict[str, Any], dict[str, Any]]: + def flatten_flags(self) -> tuple[dict[str, t.Any], dict[str, t.Any]]: """Flatten flags and aliases for loaders, so cl-args override as expected. This prevents issues such as an alias pointing to InteractiveShell, @@ -749,7 +747,7 @@ def flatten_flags(self) -> tuple[dict[str, Any], dict[str, Any]]: for alias, longname in self.aliases.items(): if isinstance(longname, tuple): longname, _ = longname - cls, trait = longname.split(".", 1) # type:ignore + cls, trait = longname.split(".", 1) children = mro_tree[cls] # type:ignore[index] if len(children) == 1: # exactly one descendent, promote alias @@ -764,7 +762,7 @@ def flatten_flags(self) -> tuple[dict[str, Any], dict[str, Any]]: flags = {} for key, (flagdict, help) in self.flags.items(): newflag: dict[t.Any, t.Any] = {} - for cls, subdict in flagdict.items(): # type:ignore + for cls, subdict in flagdict.items(): children = mro_tree[cls] # type:ignore[index] # exactly one descendent, promote flag section if len(children) == 1: @@ -776,7 +774,7 @@ def flatten_flags(self) -> tuple[dict[str, Any], dict[str, Any]]: newflag[cls] = subdict if not isinstance(key, tuple): - key = (key,) + key = (key,) # type:ignore[assignment] for k in key: flags[k] = (newflag, help) return flags, aliases @@ -787,7 +785,7 @@ def _create_loader( aliases: StrDict, flags: StrDict, classes: ClassesType | None, - ): + ) -> KVArgParseConfigLoader: return KVArgParseConfigLoader( argv, aliases, flags, classes=classes, log=self.log, subcommands=self.subcommands ) @@ -877,7 +875,7 @@ def parse_command_line(self, argv: ArgvType = None) -> None: # flatten flags&aliases, so cl-args get appropriate priority: flags, aliases = self.flatten_flags() - classes = tuple(self._classes_with_config_traits()) + classes = list(self._classes_with_config_traits()) loader = self._create_loader(argv, aliases, flags, classes=classes) try: self.cli_config = deepcopy(loader.load_config()) @@ -893,7 +891,7 @@ def parse_command_line(self, argv: ArgvType = None) -> None: def _load_config_files( cls, basefilename: str, - path: str | None = None, + path: list[str] | str | None = None, log: AnyLogger | None = None, raise_config_file_errors: bool = False, ) -> t.Generator[t.Any, None, None]: @@ -901,7 +899,8 @@ def _load_config_files( yield each config object in turn. """ - + if not path: + return if not isinstance(path, list): path = [path] for current in reversed(path): @@ -973,7 +972,7 @@ def load_config_file(self, filename: str, path: str | None = None) -> None: def _classes_with_config_traits( self, classes: ClassesType | None = None - ) -> t.Generator[t.Any, None, None]: + ) -> t.Generator[type[Configurable], None, None]: """ Yields only classes with configurable traits, and their subclasses. @@ -995,7 +994,7 @@ def _classes_with_config_traits( for cls in self._classes_inc_parents(classes) ) - def is_any_parent_included(cls: t.Any): + def is_any_parent_included(cls: t.Any) -> bool: return any(b in cls_to_config and cls_to_config[b] for b in cls.__bases__) # Mark "empty" classes for inclusion if their parents own-traits, @@ -1044,7 +1043,7 @@ def __del__(self) -> None: self.close_handlers() @classmethod - def launch_instance(cls, argv: ArgvType = None, **kwargs: Any) -> None: + def launch_instance(cls, argv: ArgvType = None, **kwargs: t.Any) -> None: """Launch a global instance of this Application If a global instance already exists, this reinitializes and starts it diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py index 80d10ed3..fa041a75 100644 --- a/traitlets/config/configurable.py +++ b/traitlets/config/configurable.py @@ -11,7 +11,6 @@ from traitlets.traitlets import ( Any, - Bunch, Container, Dict, HasTraits, @@ -23,6 +22,7 @@ validate, ) from traitlets.utils import warnings +from traitlets.utils.bunch import Bunch from traitlets.utils.text import indent, wrap_paragraphs from .loader import Config, DeferredConfig, LazyConfigValue, _is_section_key @@ -31,6 +31,8 @@ # Helper classes for Configurables # ----------------------------------------------------------------------------- +LoggerType = t.Union[logging.Logger, logging.LoggerAdapter[t.Any]] + class ConfigurableError(Exception): pass @@ -47,9 +49,7 @@ class MultipleInstanceError(ConfigurableError): class Configurable(HasTraits): config = Instance(Config, (), {}) - parent: Configurable | None = Instance( - "traitlets.config.configurable.Configurable", allow_none=True - ) + parent = Instance("traitlets.config.configurable.Configurable", allow_none=True) def __init__(self, **kwargs: t.Any) -> None: """Create a configurable given a config config. @@ -193,7 +193,7 @@ def _load_config( warn = self.log.warning else: - def warn(msg: str) -> None: + def warn(msg: t.Any) -> None: return warnings.warn(msg, UserWarning, stacklevel=9) matches = get_close_matches(name, traits) @@ -260,7 +260,10 @@ class defaults. @classmethod def class_get_trait_help( - cls, trait: TraitType, inst: HasTraits | None = None, helptext: str | None = None + cls, + trait: TraitType[t.Any, t.Any], + inst: HasTraits | None = None, + helptext: str | None = None, ) -> str: """Get the helptext string for a single trait. @@ -299,7 +302,7 @@ def class_get_trait_help( lines.append(indent("Choices: %s" % trait.info())) if inst is not None: - lines.append(indent(f"Current: {getattr(inst, trait.name)!r}")) + lines.append(indent(f"Current: {getattr(inst, trait.name or '')!r}")) else: try: dvr = trait.default_value_repr() @@ -318,7 +321,9 @@ def class_print_help(cls, inst: HasTraits | None = None) -> None: print(cls.class_get_help(inst)) @classmethod - def _defining_class(cls, trait: TraitType, classes: list[HasTraits]) -> Configurable: + def _defining_class( + cls, trait: TraitType[t.Any, t.Any], classes: t.Sequence[type[HasTraits]] + ) -> type[Configurable]: """Get the class that defines a trait For reducing redundant help output in config files. @@ -346,7 +351,7 @@ def _defining_class(cls, trait: TraitType, classes: list[HasTraits]) -> Configur return defining_cls @classmethod - def class_config_section(cls, classes: list[HasTraits] | None = None) -> str: + def class_config_section(cls, classes: t.Sequence[type[HasTraits]] | None = None) -> str: """Get the config section for this class. Parameters @@ -455,10 +460,10 @@ class LoggingConfigurable(Configurable): is to get the logger from the currently running Application. """ - log: logging.Logger | logging.LoggerAdapter = Any(help="Logger or LoggerAdapter instance") + log = Any(help="Logger or LoggerAdapter instance", allow_none=False) @validate("log") - def _validate_log(self, proposal: Bunch) -> logging.Logger | logging.LoggerAdapter: + def _validate_log(self, proposal: Bunch) -> LoggerType: if not isinstance(proposal.value, (logging.Logger, logging.LoggerAdapter)): # warn about unsupported type, but be lenient to allow for duck typing warnings.warn( @@ -467,18 +472,18 @@ def _validate_log(self, proposal: Bunch) -> logging.Logger | logging.LoggerAdapt UserWarning, stacklevel=2, ) - return proposal.value + return proposal.value # type:ignore[no-any-return] @default("log") - def _log_default(self) -> logging.Logger | logging.LoggerAdapter: + def _log_default(self) -> LoggerType: if isinstance(self.parent, LoggingConfigurable): assert self.parent is not None - return self.parent.log + return t.cast(logging.Logger, self.parent.log) from traitlets import log return log.get_logger() - def _get_log_handler(self) -> logging.Handler: + def _get_log_handler(self) -> logging.Handler | None: """Return the default Handler Returns None if none can be found @@ -486,16 +491,16 @@ def _get_log_handler(self) -> logging.Handler: Deprecated, this now returns the first log handler which may or may not be the default one. """ - logger = self.log - if isinstance(logger, logging.LoggerAdapter): - logger = logger.logger + if not self.log: + return None + logger = self.log if isinstance(self.log, logging.Logger) else self.log.logger if not getattr(logger, "handlers", None): # no handlers attribute or empty handlers list return None - return logger.handlers[0] + return logger.handlers[0] # type:ignore[no-any-return] -T = t.TypeVar('T', bound='SingletonConfigurable') +CT = t.TypeVar('CT', bound='SingletonConfigurable') class SingletonConfigurable(LoggingConfigurable): @@ -509,7 +514,7 @@ class SingletonConfigurable(LoggingConfigurable): _instance = None @classmethod - def _walk_mro(cls) -> t.Generator[HasTraits, None, None]: + def _walk_mro(cls) -> t.Generator[type[SingletonConfigurable], None, None]: """Walk the cls.mro() for parent classes that are also singletons For use in instance() @@ -535,7 +540,7 @@ def clear_instance(cls) -> None: subclass._instance = None @classmethod - def instance(cls: type[T], *args: t.Any, **kwargs: t.Any) -> T: + def instance(cls: type[CT], *args: t.Any, **kwargs: t.Any) -> CT: """Returns a global instance of this class. This method create a new instance if none have previously been created diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py index 716c2fd3..437c8c17 100644 --- a/traitlets/config/loader.py +++ b/traitlets/config/loader.py @@ -786,7 +786,6 @@ def parse_known_args(self, args=None, namespace=None): # type aliases -Flags = t.Union[str, t.Tuple[str, ...]] SubcommandsDict = t.Dict[str, t.Any] @@ -798,8 +797,8 @@ class ArgParseConfigLoader(CommandLineConfigLoader): def __init__( self, argv: list[str] | None = None, - aliases: dict[Flags, str] | None = None, - flags: dict[Flags, str] | None = None, + aliases: dict[str, str] | None = None, + flags: dict[str, str] | None = None, log: t.Any = None, classes: list[type[t.Any]] | None = None, subcommands: SubcommandsDict | None = None, @@ -916,7 +915,7 @@ def _parse_args(self, args): if alias in self.flags: continue if not isinstance(alias, tuple): - alias = (alias,) + alias = (alias,) # type:ignore[assignment] for al in alias: if len(al) == 1: unpacked_aliases["-" + al] = "--" + alias_target diff --git a/traitlets/log.py b/traitlets/log.py index 468c7c3c..d90a9c52 100644 --- a/traitlets/log.py +++ b/traitlets/log.py @@ -5,11 +5,12 @@ from __future__ import annotations import logging +from typing import Any -_logger: logging.Logger | None = None +_logger: logging.Logger | logging.LoggerAdapter[Any] | None = None -def get_logger() -> logging.Logger: +def get_logger() -> logging.Logger | logging.LoggerAdapter[Any]: """Grab the global logger instance. If a global Application is instantiated, grab its logger. diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index bfe325a1..33a9129b 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -1126,7 +1126,7 @@ def observe(*names: Sentinel | str, type: str = "change") -> ObserveHandler: return ObserveHandler(names, type=type) -def observe_compat(func): +def observe_compat(func: FuncT) -> FuncT: """Backward-compatibility shim decorator for observers Use with: @@ -1140,9 +1140,11 @@ def _foo_changed(self, change): Allows adoption of new observer API without breaking subclasses that override and super. """ - def compatible_observer(self, change_or_name, old=Undefined, new=Undefined): + def compatible_observer( + self: t.Any, change_or_name: str, old: t.Any = Undefined, new: t.Any = Undefined + ) -> t.Any: if isinstance(change_or_name, dict): - change = change_or_name + change = Bunch(change_or_name) else: clsname = self.__class__.__name__ warn( @@ -1159,7 +1161,7 @@ def compatible_observer(self, change_or_name, old=Undefined, new=Undefined): ) return func(self, change) - return compatible_observer + return t.cast(FuncT, compatible_observer) def validate(*names: Sentinel | str) -> ValidateHandler: From cf400728b37caf7608b2bc791a3f8998e26fcc68 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 21:51:31 -0500 Subject: [PATCH 18/19] cleanup --- traitlets/config/configurable.py | 5 ++++- traitlets/traitlets.py | 10 +++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py index fa041a75..77b4214e 100644 --- a/traitlets/config/configurable.py +++ b/traitlets/config/configurable.py @@ -31,7 +31,10 @@ # Helper classes for Configurables # ----------------------------------------------------------------------------- -LoggerType = t.Union[logging.Logger, logging.LoggerAdapter[t.Any]] +if t.TYPE_CHECKING: + LoggerType = t.Union[logging.Logger, logging.LoggerAdapter[t.Any]] +else: + LoggerType = t.Any class ConfigurableError(Exception): diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index 33a9129b..50d6face 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -39,7 +39,7 @@ # Adapted from enthought.traits, Copyright (c) Enthought, Inc., # also under the terms of the Modified BSD License. -# ruff: noqa: ANN201, ANN001, ANN204, ANN102, ANN003, ANN206, ANN002 +# ruff: noqa: ANN001, ANN204, ANN201, ANN003, ANN206, ANN002 from __future__ import annotations @@ -297,7 +297,7 @@ def __init__(self, source: t.Any, target: t.Any, transform: t.Any = None) -> Non self.link() - def link(self): + def link(self) -> None: try: setattr( self.target[0], @@ -337,7 +337,7 @@ def _update_source(self, change): f"Broken link {self}: the target value changed while updating " "the source." ) - def unlink(self): + def unlink(self) -> None: self.source[0].unobserve(self._update_target, names=self.source[1]) self.target[0].unobserve(self._update_source, names=self.target[1]) @@ -381,7 +381,7 @@ def __init__(self, source: t.Any, target: t.Any, transform: t.Any = None) -> Non self.source, self.target = source, target self.link() - def link(self): + def link(self) -> None: try: setattr( self.target[0], @@ -405,7 +405,7 @@ def _update(self, change): with self._busy_updating(): setattr(self.target[0], self.target[1], self._transform(change.new)) - def unlink(self): + def unlink(self) -> None: self.source[0].unobserve(self._update, names=self.source[1]) From f24b7a2f8c7ecbecbe2818e05eec887215b37d41 Mon Sep 17 00:00:00 2001 From: Steven Silvester Date: Sat, 30 Sep 2023 22:26:32 -0500 Subject: [PATCH 19/19] fix regression --- traitlets/config/application.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/traitlets/config/application.py b/traitlets/config/application.py index 316c2a4e..8c15abd8 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -891,7 +891,7 @@ def parse_command_line(self, argv: ArgvType = None) -> None: def _load_config_files( cls, basefilename: str, - path: list[str] | str | None = None, + path: list[str | None] | str | None = None, log: AnyLogger | None = None, raise_config_file_errors: bool = False, ) -> t.Generator[t.Any, None, None]: @@ -899,8 +899,6 @@ def _load_config_files( yield each config object in turn. """ - if not path: - return if not isinstance(path, list): path = [path] for current in reversed(path):