diff --git a/docs/source/conf.py b/docs/source/conf.py index 5adeaf82..528130cb 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -22,22 +22,13 @@ HERE = osp.abspath(osp.dirname(__file__)) ROOT = osp.dirname(osp.dirname(HERE)) +from traitlets import version_info + # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) -# We load the ipython release info into a dict by explicit execution -_release = {} # type:ignore -exec( # noqa - compile( - open(osp.join(ROOT, "traitlets/_version.py")).read(), - "../../traitlets/_version.py", - "exec", - ), - _release, -) - # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. @@ -64,7 +55,7 @@ source_suffix = ".rst" # Add dev disclaimer. -if _release["version_info"][-1] == "dev": +if version_info[-1] == "dev": rst_prolog = """ .. note:: @@ -89,9 +80,9 @@ # built documents. # # The short X.Y version. -version = ".".join(map(str, _release["version_info"][:2])) +version = ".".join(map(str, version_info[:2])) # The full version, including alpha/beta/rc tags. -release = _release["__version__"] +release = "__version__" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. diff --git a/examples/docs/from_string.py b/examples/docs/from_string.py index eb38cb78..a8b7304b 100755 --- a/examples/docs/from_string.py +++ b/examples/docs/from_string.py @@ -25,7 +25,7 @@ class App(Application): ) def start(self): - print(f"key={self.key}") + print(f"key={self.key.decode('utf8')}") if __name__ == "__main__": diff --git a/examples/myapp.py b/examples/myapp.py index bd04be7d..009f9209 100755 --- a/examples/myapp.py +++ b/examples/myapp.py @@ -94,6 +94,7 @@ def start(self): print("app.config:") print(self.config) print("try running with --help-all to see all available flags") + assert self.log is not None self.log.debug("Debug Message") self.log.info("Info Message") self.log.warning("Warning Message") diff --git a/pyproject.toml b/pyproject.toml index 13c0b7db..8c2df586 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,9 +20,10 @@ classifiers = [ urls = {Homepage = "https://github.com/ipython/traitlets"} requires-python = ">=3.7" dynamic = ["version"] +dependencies = ["typing_extensions>=4.0.1"] [project.optional-dependencies] -test = ["pytest", "pytest-mock", "pre-commit", "argcomplete>=2.0"] +test = ["pytest>=7.0,<7.2", "pytest-mock", "pre-commit", "argcomplete>=2.0", "pytest-mypy-testing", "mypy @ git+https://github.com/python/mypy.git@cb1d1a0baba37f35268cb605b7345726f257f960#egg=mypy"] docs = [ "myst-parser", "pydata-sphinx-theme", @@ -32,6 +33,9 @@ docs = [ [tool.hatch.version] path = "traitlets/_version.py" +[tool.hatch.metadata] +allow-direct-references = true + [tool.hatch.envs.docs] features = ["docs"] [tool.hatch.envs.docs.scripts] @@ -52,7 +56,7 @@ nowarn = "test -W default {args}" [tool.hatch.envs.typing] features = ["test"] -dependencies = ["mypy>=0.990"] +dependencies = ["mypy @ git+https://github.com/python/mypy.git@cb1d1a0baba37f35268cb605b7345726f257f960#egg=mypy"] [tool.hatch.envs.typing.scripts] test = "mypy --install-types --non-interactive {args:.}" @@ -89,7 +93,7 @@ warn_unused_configs = true warn_redundant_casts = true warn_return_any = true warn_unused_ignores = true -exclude = ["examples/docs/configs"] +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" diff --git a/traitlets/config/application.py b/traitlets/config/application.py index af8fe536..6da29feb 100644 --- a/traitlets/config/application.py +++ b/traitlets/config/application.py @@ -149,21 +149,29 @@ class Application(SingletonConfigurable): # The name of the application, will usually match the name of the command # line application - name: t.Union[str, Unicode] = Unicode("application") + name: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode("application") # The description of the application that is printed at the beginning # of the help. - description: t.Union[str, Unicode] = Unicode("This is an application.") + description: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode( + "This is an application." + ) # default section descriptions - option_description: t.Union[str, Unicode] = Unicode(option_description) - keyvalue_description: t.Union[str, Unicode] = Unicode(keyvalue_description) - subcommand_description: t.Union[str, Unicode] = Unicode(subcommand_description) + 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 + ) 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] = Unicode() + examples: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode() # A sequence of Configurable subclasses whose config=True attributes will # be exposed at the command line. @@ -190,18 +198,18 @@ def _classes_inc_parents(self, classes=None): yield parent # The version string of this application. - version: t.Union[str, Unicode] = Unicode("0.0") + version: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode("0.0") # the argv used to initialize the application argv: t.Union[t.List[str], List] = List() # Whether failing to load config files should prevent startup - raise_config_file_errors: t.Union[bool, Bool] = Bool( + raise_config_file_errors: t.Union[bool, Bool[bool, t.Union[bool, int]]] = Bool( TRAITLETS_APPLICATION_RAISE_CONFIG_FILE_ERROR ) # The log level for the application - log_level: t.Union[str, int, Enum] = Enum( + log_level: t.Union[str, int, Enum[t.Any, t.Any]] = 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.", @@ -209,11 +217,11 @@ def _classes_inc_parents(self, classes=None): _log_formatter_cls = LevelFormatter - log_datefmt: t.Union[str, Unicode] = Unicode( + log_datefmt: t.Union[str, Unicode[str, t.Union[str, bytes]]] = 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] = Unicode( + log_format: t.Union[str, Unicode[str, t.Union[str, bytes]]] = Unicode( "[%(name)s]%(highlevel)s %(message)s", help="The Logging format template", ).tag(config=True) @@ -420,11 +428,11 @@ def _log_default(self): _loaded_config_files = List() - show_config: t.Union[bool, Bool] = Bool( + show_config: t.Union[bool, Bool[bool, t.Union[bool, int]]] = Bool( help="Instead of starting the Application, dump configuration to stdout" ).tag(config=True) - show_config_json: t.Union[bool, Bool] = Bool( + show_config_json: t.Union[bool, Bool[bool, t.Union[bool, int]]] = Bool( help="Instead of starting the Application, dump configuration to stdout (as JSON)" ).tag(config=True) @@ -436,7 +444,7 @@ def _show_config_json_changed(self, change): def _show_config_changed(self, change): if change.new: self._save_start = self.start - self.start = self.start_show_config # type:ignore[method-assign] + self.start = self.start_show_config # type:ignore[assignment] def __init__(self, **kwargs): SingletonConfigurable.__init__(self, **kwargs) diff --git a/traitlets/config/configurable.py b/traitlets/config/configurable.py index c3b2ed16..1bfa0457 100644 --- a/traitlets/config/configurable.py +++ b/traitlets/config/configurable.py @@ -182,6 +182,7 @@ def _load_config(self, cfg, section_names=None, traits=None): from difflib import get_close_matches if isinstance(self, LoggingConfigurable): + assert self.log is not None warn = self.log.warning else: @@ -462,6 +463,7 @@ def _validate_log(self, proposal): @default("log") def _log_default(self): if isinstance(self.parent, LoggingConfigurable): + assert self.parent is not None return self.parent.log from traitlets import log diff --git a/traitlets/config/loader.py b/traitlets/config/loader.py index a9136dc3..1c6b8e8c 100644 --- a/traitlets/config/loader.py +++ b/traitlets/config/loader.py @@ -96,9 +96,9 @@ class LazyConfigValue(HasTraits): _value = None # list methods - _extend = List() - _prepend = List() - _inserts = List() + _extend: List = List() + _prepend: List = List() + _inserts: List = List() def append(self, obj): """Append an item to a List""" diff --git a/traitlets/tests/test_traitlets.py b/traitlets/tests/test_traitlets.py index 3b41fe4b..62fa726f 100644 --- a/traitlets/tests/test_traitlets.py +++ b/traitlets/tests/test_traitlets.py @@ -112,7 +112,7 @@ class A(HasTraitsStub): self.assertEqual(a._notify_new, 10) def test_validate(self): - class MyTT(TraitType): + class MyTT(TraitType[int, int]): def validate(self, inst, value): return -1 @@ -127,7 +127,7 @@ class A(HasTraitsStub): self.assertEqual(a.tt, -1) def test_default_validate(self): - class MyIntTT(TraitType): + class MyIntTT(TraitType[int, int]): def validate(self, obj, value): if isinstance(value, int): return value @@ -154,7 +154,7 @@ class A(HasTraits): def test_error(self): class A(HasTraits): - tt = TraitType() + tt = TraitType[int, int]() a = A() self.assertRaises(TraitError, A.tt.error, a, 10) @@ -270,14 +270,14 @@ def _default_x(self): self.assertEqual(a._trait_values, {"x": 11}) def test_tag_metadata(self): - class MyIntTT(TraitType): + class MyIntTT(TraitType[int, int]): metadata = {"a": 1, "b": 2} a = MyIntTT(10).tag(b=3, c=4) self.assertEqual(a.metadata, {"a": 1, "b": 3, "c": 4}) def test_metadata_localized_instance(self): - class MyIntTT(TraitType): + class MyIntTT(TraitType[int, int]): metadata = {"a": 1, "b": 2} a = MyIntTT(10) @@ -325,7 +325,7 @@ class Foo(HasTraits): self.assertEqual(Foo().bar, {}) def test_deprecated_metadata_access(self): - class MyIntTT(TraitType): + class MyIntTT(TraitType[int, int]): metadata = {"a": 1, "b": 2} a = MyIntTT(10) @@ -394,12 +394,12 @@ class C(HasTraits): def test_this_class(self): class A(HasTraits): - t = This() - tt = This() + t = This["A"]() + tt = This["A"]() class B(A): - tt = This() - ttt = This() + tt = This["A"]() + ttt = This["A"]() self.assertEqual(A.t.this_class, A) self.assertEqual(B.t.this_class, A) @@ -1094,7 +1094,7 @@ class Bar(Foo): class Bah: pass - class FooInstance(Instance): + class FooInstance(Instance[Foo]): klass = Foo class A(HasTraits): @@ -1170,7 +1170,7 @@ class Foo: def inner(): class A(HasTraits): - inst = Instance(Foo()) + inst = Instance(Foo()) # type:ignore self.assertRaises(TraitError, inner) @@ -1178,7 +1178,7 @@ class A(HasTraits): class TestThis(TestCase): def test_this_class(self): class Foo(HasTraits): - this = This() + this = This["Foo"]() f = Foo() self.assertEqual(f.this, None) @@ -1189,7 +1189,7 @@ class Foo(HasTraits): def test_this_inst(self): class Foo(HasTraits): - this = This() + this = This["Foo"]() f = Foo() f.this = Foo() @@ -1197,7 +1197,7 @@ class Foo(HasTraits): def test_subclass(self): class Foo(HasTraits): - t = This() + t = This["Foo"]() class Bar(Foo): pass @@ -1211,7 +1211,7 @@ class Bar(Foo): def test_subclass_override(self): class Foo(HasTraits): - t = This() + t = This["Foo"]() class Bar(Foo): t = This() @@ -2423,11 +2423,11 @@ def test_notification_order(): # Traits for Forward Declaration Tests ### class ForwardDeclaredInstanceTrait(HasTraits): - value = ForwardDeclaredInstance("ForwardDeclaredBar", allow_none=True) + value = ForwardDeclaredInstance["ForwardDeclaredBar"]("ForwardDeclaredBar", allow_none=True) class ForwardDeclaredTypeTrait(HasTraits): - value = ForwardDeclaredType("ForwardDeclaredBar", allow_none=True) + value = ForwardDeclaredType[t.Any, t.Any]("ForwardDeclaredBar", allow_none=True) class ForwardDeclaredInstanceListTrait(HasTraits): diff --git a/traitlets/tests/test_typing.py b/traitlets/tests/test_typing.py new file mode 100644 index 00000000..adce8820 --- /dev/null +++ b/traitlets/tests/test_typing.py @@ -0,0 +1,172 @@ +import typing +from typing import Optional + +import pytest + +from traitlets import Bool, CInt, HasTraits, Instance, Int, TCPAddress + +if not typing.TYPE_CHECKING: + + def reveal_type(*args, **kwargs): + pass + + +class Foo: + def __init__(self, c): + self.c = c + + +@pytest.mark.mypy_testing +def mypy_bool_typing(): + class T(HasTraits): + b = Bool(True).tag(sync=True) + ob = Bool(None, allow_none=True).tag(sync=True) + + t = T() + reveal_type( + Bool(True) # R: traitlets.traitlets.Bool[builtins.bool, Union[builtins.bool, builtins.int]] + ) + reveal_type( + Bool( # R: traitlets.traitlets.Bool[builtins.bool, Union[builtins.bool, builtins.int]] + True + ).tag(sync=True) + ) + reveal_type( + Bool( # R: traitlets.traitlets.Bool[Union[builtins.bool, None], Union[builtins.bool, builtins.int, None]] + None, allow_none=True + ) + ) + reveal_type( + Bool( # R: traitlets.traitlets.Bool[Union[builtins.bool, None], Union[builtins.bool, builtins.int, None]] + None, allow_none=True + ).tag( + sync=True + ) + ) + reveal_type( + T.b # R: traitlets.traitlets.Bool[builtins.bool, Union[builtins.bool, builtins.int]] + ) + reveal_type(t.b) # R: builtins.bool + reveal_type(t.ob) # R: Union[builtins.bool, None] + reveal_type( + T.b # R: traitlets.traitlets.Bool[builtins.bool, Union[builtins.bool, builtins.int]] + ) + reveal_type( + T.ob # R: traitlets.traitlets.Bool[Union[builtins.bool, None], Union[builtins.bool, builtins.int, None]] + ) + # we would expect this to be Optional[Union[bool, int]], but... + t.b = "foo" # E: Incompatible types in assignment (expression has type "str", variable has type "Union[bool, int]") [assignment] + t.b = None # E: Incompatible types in assignment (expression has type "None", variable has type "Union[bool, int]") [assignment] + + +@pytest.mark.mypy_testing +def mypy_int_typing(): + class T(HasTraits): + i: Int[int, int] = Int(42).tag(sync=True) + oi: Int[Optional[int], Optional[int]] = Int(42, allow_none=True).tag(sync=True) + + t = T() + reveal_type(Int(True)) # R: traitlets.traitlets.Int[builtins.int, builtins.int] + reveal_type(Int(True).tag(sync=True)) # R: traitlets.traitlets.Int[builtins.int, builtins.int] + reveal_type( + Int( # R: traitlets.traitlets.Int[Union[builtins.int, None], Union[builtins.int, None]] + None, allow_none=True + ) + ) + reveal_type( + Int( # R: traitlets.traitlets.Int[Union[builtins.int, None], Union[builtins.int, None]] + None, allow_none=True + ).tag(sync=True) + ) + reveal_type(T.i) # R: traitlets.traitlets.Int[builtins.int, builtins.int] + reveal_type(t.i) # R: builtins.int + reveal_type(t.oi) # R: Union[builtins.int, None] + reveal_type(T.i) # R: traitlets.traitlets.Int[builtins.int, builtins.int] + reveal_type( + T.oi # R: traitlets.traitlets.Int[Union[builtins.int, None], Union[builtins.int, None]] + ) + t.i = "foo" # E: Incompatible types in assignment (expression has type "str", variable has type "int") [assignment] + t.i = None # E: Incompatible types in assignment (expression has type "None", variable has type "int") [assignment] + t.i = 1.2 # E: Incompatible types in assignment (expression has type "float", variable has type "int") [assignment] + + +@pytest.mark.mypy_testing +def mypy_cint_typing(): + class T(HasTraits): + i = CInt(42).tag(sync=True) + oi = CInt(42, allow_none=True).tag(sync=True) + + t = T() + reveal_type(CInt(42)) # R: traitlets.traitlets.CInt[builtins.int, Any] + reveal_type(CInt(42).tag(sync=True)) # R: traitlets.traitlets.CInt[builtins.int, Any] + reveal_type( + CInt(None, allow_none=True) # R: traitlets.traitlets.CInt[Union[builtins.int, None], Any] + ) + reveal_type( + CInt( # R: traitlets.traitlets.CInt[Union[builtins.int, None], Any] + None, allow_none=True + ).tag(sync=True) + ) + reveal_type(T.i) # R: traitlets.traitlets.CInt[builtins.int, Any] + reveal_type(t.i) # R: builtins.int + reveal_type(t.oi) # R: Union[builtins.int, None] + reveal_type(T.i) # R: traitlets.traitlets.CInt[builtins.int, Any] + reveal_type(T.oi) # R: traitlets.traitlets.CInt[Union[builtins.int, None], Any] + + +@pytest.mark.mypy_testing +def mypy_tcp_typing(): + class T(HasTraits): + tcp = TCPAddress() + otcp = TCPAddress(None, allow_none=True) + + t = T() + reveal_type(t.tcp) # R: Tuple[builtins.str, builtins.int] + reveal_type( + T.tcp # R: traitlets.traitlets.TCPAddress[Tuple[builtins.str, builtins.int], Tuple[builtins.str, builtins.int]] + ) + reveal_type( + T.tcp.tag( # R:traitlets.traitlets.TCPAddress[Tuple[builtins.str, builtins.int], Tuple[builtins.str, builtins.int]] + sync=True + ) + ) + reveal_type(t.otcp) # R: Union[Tuple[builtins.str, builtins.int], None] + reveal_type( + T.otcp # R: traitlets.traitlets.TCPAddress[Union[Tuple[builtins.str, builtins.int], None], Union[Tuple[builtins.str, builtins.int], None]] + ) + reveal_type( + T.otcp.tag( # R: traitlets.traitlets.TCPAddress[Union[Tuple[builtins.str, builtins.int], None], Union[Tuple[builtins.str, builtins.int], None]] + sync=True + ) + ) + t.tcp = "foo" # E: Incompatible types in assignment (expression has type "str", variable has type "Tuple[str, int]") [assignment] + t.otcp = "foo" # E: Incompatible types in assignment (expression has type "str", variable has type "Optional[Tuple[str, int]]") [assignment] + t.tcp = None # E: Incompatible types in assignment (expression has type "None", variable has type "Tuple[str, int]") [assignment] + + +@pytest.mark.mypy_testing +def mypy_instance_typing(): + class T(HasTraits): + inst = Instance(Foo) + oinst = Instance(Foo, allow_none=True) + 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.oinst_string) # R: Union[Any, 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]] + sync=True + ) + ) + t.inst = "foo" # E: Incompatible types in assignment (expression has type "str", variable has type "Foo") [assignment] + t.oinst = "foo" # E: Incompatible types in assignment (expression has type "str", variable has type "Optional[Foo]") [assignment] + t.inst = None # E: Incompatible types in assignment (expression has type "None", variable has type "Foo") [assignment] diff --git a/traitlets/traitlets.py b/traitlets/traitlets.py index e5376738..22354f03 100644 --- a/traitlets/traitlets.py +++ b/traitlets/traitlets.py @@ -474,23 +474,36 @@ def instance_init(self, obj): pass -class TraitType(BaseDescriptor): +G = t.TypeVar("G") +S = t.TypeVar("S") +T = t.TypeVar("T") + + +# Self from typing extension doesn't work well with mypy https://github.com/python/mypy/pull/14041 +# see https://peps.python.org/pep-0673/#use-in-generic-classes +# Self = t.TypeVar("Self", bound="TraitType[Any, Any]") +from typing_extensions import Literal, Self + + +# We use a type for the getter (G) and setter (G) because we allow +# for traits to cast (for instance CInt will use G=int, S=t.Any) +class TraitType(BaseDescriptor, t.Generic[G, S]): """A base class for all trait types.""" metadata: t.Dict[str, t.Any] = {} - allow_none = False - read_only = False - info_text = "any value" + allow_none: bool = False + read_only: bool = False + info_text: str = "any value" default_value: t.Optional[t.Any] = Undefined def __init__( - self, - default_value=Undefined, - allow_none=False, - read_only=None, - help=None, - config=None, - **kwargs, + self: "TraitType[G, S]", + default_value: t.Any = Undefined, + allow_none: bool = False, + read_only: t.Optional[bool] = None, + help: t.Optional[str] = None, + config: t.Any = None, + **kwargs: t.Any, ): """Declare a traitlet. @@ -605,9 +618,9 @@ def init_default_value(self, obj): obj._trait_values[self.name] = value return value - def get(self, obj, cls=None): + def get(self, obj: "HasTraits", cls: t.Any = None) -> t.Optional[G]: try: - value = obj._trait_values[self.name] + value = obj._trait_values[self.name] # type: ignore except KeyError: # Check for a dynamic initializer. default = obj.trait_defaults(self.name) @@ -627,7 +640,7 @@ def get(self, obj, cls=None): value = self._validate(obj, default) finally: obj._cross_validation_lock = _cross_validation_lock - obj._trait_values[self.name] = value + obj._trait_values[self.name] = value # type: ignore obj._notify_observers( Bunch( name=self.name, @@ -636,14 +649,60 @@ def get(self, obj, cls=None): type="default", ) ) - return value + return value # type: ignore except Exception as e: # This should never be reached. raise TraitError("Unexpected error in TraitType: default value not set properly") from e else: - return value + return value # type: ignore + + if t.TYPE_CHECKING: + # This gives ok type information, but not specific enough (e.g. it will) + # always be a TraitType, not a subclass, like Bool. + @t.overload + def __new__( # type: ignore[misc] + cls, + default_value: t.Union[S, Sentinel] = Undefined, + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = None, + help: t.Optional[str] = None, + config: t.Any = None, + **kwargs: t.Any, + ) -> "TraitType[G, S]": + ... + + @t.overload + def __new__( + cls, + default_value: t.Union[S, None, Sentinel] = Undefined, + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = None, + help: t.Optional[str] = None, + config: t.Any = None, + **kwargs: t.Any, + ) -> "TraitType[t.Optional[G], S]": + ... + + def __new__( # type: ignore[no-untyped-def, misc] + cls, + default_value: t.Union[S, None, Sentinel] = Undefined, + allow_none: Literal[True, False] = False, + read_only=None, + help=None, + config=None, + **kwargs, + ) -> t.Union["TraitType[t.Optional[G], S]", "TraitType[G, S]"]: + ... + + @t.overload + def __get__(self, obj: None, cls: t.Type[t.Any]) -> Self: + ... + + @t.overload + def __get__(self, obj: t.Any, cls: t.Type[t.Any]) -> G: + ... - def __get__(self, obj, cls=None): + def __get__(self, obj: t.Union["HasTraits", None], cls: t.Type[t.Any]) -> t.Union[Self, G]: """Get the value of the trait by self.name for the instance. Default values are instantiated when :meth:`HasTraits.__new__` @@ -654,7 +713,7 @@ def __get__(self, obj, cls=None): if obj is None: return self else: - return self.get(obj, cls) + return t.cast(G, self.get(obj, cls)) # the G should encode the Optional def set(self, obj, value): new_value = self._validate(obj, value) @@ -674,7 +733,7 @@ def set(self, obj, value): # comparison above returns something other than True/False obj._notify_trait(self.name, old_value, new_value) - def __set__(self, obj, value): + def __set__(self, obj: "HasTraits", value: S) -> None: """Set the value of the trait by self.name for the instance. Values pass through a validation stage where errors are raised when @@ -819,7 +878,7 @@ def set_metadata(self, key, value): warn("Deprecated in traitlets 4.1, " + msg, DeprecationWarning, stacklevel=2) self.metadata[key] = value - def tag(self, **metadata): + def tag(self, **metadata: t.Any) -> "Self": """Sets metadata and returns self. This allows convenient metadata tagging when initializing the trait, such as: @@ -1291,7 +1350,7 @@ def __init__(self, *args, **kwargs): def ignore(*_ignore_args): pass - self.notify_change = ignore # type:ignore[method-assign] + self.notify_change = ignore # type:ignore[assignment] self._cross_validation_lock = True changes = {} for key, value in kwargs.items(): @@ -1422,7 +1481,7 @@ def hold(change): try: # Replace notify_change with `hold`, caching and compressing # notifications, disable cross validation and yield. - self.notify_change = hold # type:ignore[method-assign] + self.notify_change = hold # type:ignore[assignment] self._cross_validation_lock = True yield # Cross validate final values when context is released. @@ -1432,7 +1491,7 @@ def hold(change): self.set_trait(name, value) except TraitError as e: # Roll back in case of TraitError during final cross validation. - self.notify_change = lambda x: None # type:ignore[method-assign] + self.notify_change = lambda x: None # type:ignore[assignment] for name, changes in cache.items(): for change in changes[::-1]: # TODO: Separate in a rollback function per notification type. @@ -1937,7 +1996,7 @@ def trait_events(cls, name=None): # ----------------------------------------------------------------------------- -class ClassBasedTraitType(TraitType): +class ClassBasedTraitType(TraitType[G, S]): """ A trait with error reporting and string -> type resolution for Type, Instance and This. @@ -1950,10 +2009,64 @@ def _resolve_string(self, string): return import_item(string) -class Type(ClassBasedTraitType): +class Type(ClassBasedTraitType[G, S]): """A trait whose value must be a subclass of a specified class.""" - def __init__(self, default_value=Undefined, klass=None, **kwargs): + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "Type[object, object]", + default_value: t.Union[Sentinel, None, str] = ..., + klass: t.Union[None, str] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Type[t.Optional[object], t.Optional[object]]", + default_value: t.Union[S, Sentinel, None, str] = ..., + klass: t.Union[None, str] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Type[S, S]", + default_value: t.Union[S, Sentinel, str] = ..., + klass: t.Type[S] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Type[t.Optional[S], t.Optional[S]]", + default_value: t.Union[S, Sentinel, None, str] = ..., + klass: t.Type[S] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + def __init__(self, default_value=Undefined, klass=None, allow_none=False, **kwargs): """Construct a Type trait A Type trait specifies that its values must be subclasses of @@ -1995,7 +2108,7 @@ def __init__(self, default_value=Undefined, klass=None, **kwargs): self.klass = klass - super().__init__(new_default_value, **kwargs) + super().__init__(new_default_value, allow_none=allow_none, **kwargs) def validate(self, obj, value): """Validates that the value is a valid object instance.""" @@ -2046,7 +2159,7 @@ def default_value_repr(self): return repr(f"{value.__module__}.{value.__name__}") -class Instance(ClassBasedTraitType): +class Instance(ClassBasedTraitType[T, T]): """A trait whose value must be an instance of a specified class. The value can also be an instance of a subclass of the specified class. @@ -2054,9 +2167,72 @@ class Instance(ClassBasedTraitType): Subclasses can declare default classes by overriding the klass attribute """ - klass = None + klass: t.Union[str, t.Type[T], None] = None + + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "Instance[T]", + klass: t.Type[T] = ..., + args: t.Optional[t.Tuple[t.Any, ...]] = ..., + kw: t.Optional[t.Dict[str, t.Any]] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + **kwargs: t.Any, + ) -> None: + ... + + @t.overload + def __init__( + self: "Instance[t.Optional[T]]", + klass: t.Type[T] = ..., + args: t.Optional[t.Tuple[t.Any, ...]] = ..., + kw: t.Optional[t.Dict[str, t.Any]] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + **kwargs: t.Any, + ) -> None: + ... + + @t.overload + def __init__( + self: "Instance[t.Any]", + klass: t.Union[str, None] = ..., + args: t.Optional[t.Tuple[t.Any, ...]] = ..., + kw: t.Optional[t.Dict[str, t.Any]] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + **kwargs: t.Any, + ) -> None: + ... + + @t.overload + def __init__( + self: "Instance[t.Optional[t.Any]]", + klass: t.Union[str, None] = ..., + args: t.Optional[t.Tuple[t.Any, ...]] = ..., + kw: t.Optional[t.Dict[str, t.Any]] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + **kwargs: t.Any, + ) -> None: + ... - def __init__(self, klass=None, args=None, kw=None, **kwargs): + def __init__( + self, + klass: t.Union[str, t.Type[T], None] = None, + args: t.Optional[t.Tuple[t.Any, ...]] = None, + kw: t.Optional[t.Dict[str, t.Any]] = None, + allow_none: bool = False, + read_only: t.Optional[bool] = None, + help: t.Optional[str] = None, + **kwargs: t.Any, + ) -> None: """Construct an Instance trait. This trait allows values that are instances of a particular @@ -2101,7 +2277,7 @@ class or its subclasses. Our implementation is quite different self.default_args = args self.default_kwargs = kw - super().__init__(**kwargs) + super().__init__(allow_none=allow_none, **kwargs) def validate(self, obj, value): assert self.klass is not None @@ -2157,7 +2333,7 @@ def _resolve_string(self, string): return import_item(".".join([modname, string])) -class ForwardDeclaredType(ForwardDeclaredMixin, Type): +class ForwardDeclaredType(ForwardDeclaredMixin, Type[G, S]): """ Forward-declared version of Type. """ @@ -2165,7 +2341,7 @@ class ForwardDeclaredType(ForwardDeclaredMixin, Type): pass -class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance): +class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance[T]): """ Forward-declared version of Instance. """ @@ -2173,7 +2349,7 @@ class ForwardDeclaredInstance(ForwardDeclaredMixin, Instance): pass -class This(ClassBasedTraitType): +class This(ClassBasedTraitType[t.Optional[T], t.Optional[T]]): """A trait for instances of the class containing this trait. Because how how and when class bodies are executed, the ``This`` @@ -2197,7 +2373,7 @@ def validate(self, obj, value): self.error(obj, value) -class Union(TraitType): +class Union(TraitType[t.Any, t.Any]): """A trait type representing a Union type.""" def __init__(self, trait_types, **kwargs): @@ -2286,9 +2462,73 @@ def from_string(self, s): # ----------------------------------------------------------------------------- -class Any(TraitType): +class Any(TraitType[t.Optional[t.Any], t.Optional[t.Any]]): """A trait which allows any value.""" + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "Any", + default_value: str = ..., + *, + allow_none: Literal[False], + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Any", + default_value: str = ..., + *, + allow_none: Literal[True], + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Any", + default_value: str = ..., + *, + allow_none: Literal[True, False] = ..., + help: t.Optional[str] = ..., + read_only: t.Optional[bool] = False, + config: t.Any = None, + **kwargs: t.Any, + ): + ... + + def __init__( + self: "Any", + default_value: str = ..., + *, + allow_none: t.Optional[bool] = False, + help: t.Optional[str] = "", + read_only: t.Optional[bool] = False, + config: t.Any = None, + **kwargs: t.Any, + ): + ... + + @t.overload + def __get__(self, obj: None, cls: t.Type[t.Any]) -> "Any": + ... + + @t.overload + def __get__(self, obj: t.Any, cls: t.Type[t.Any]) -> t.Any: + ... + + def __get__(self, obj: t.Union[t.Any, None], cls: t.Type[t.Any]) -> t.Union[t.Any, "Any"]: + ... + default_value: t.Optional[t.Any] = None allow_none = True info_text = "any value" @@ -2319,12 +2559,39 @@ def _validate_bounds(trait, obj, value): return value -class Int(TraitType): +# I = t.TypeVar('I', t.Optional[int], int) + + +class Int(TraitType[G, S]): """An int trait.""" default_value = 0 info_text = "an int" + @t.overload + def __init__( + self: "Int[int, int]", + default_value: t.Union[int, Sentinel] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Int[t.Optional[int], t.Optional[int]]", + default_value: t.Union[int, Sentinel, None] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + def __init__(self, default_value=Undefined, allow_none=False, **kwargs): self.min = kwargs.pop("min", None) self.max = kwargs.pop("max", None) @@ -2344,9 +2611,38 @@ def subclass_init(self, cls): pass # fully opt out of instance_init -class CInt(Int): +class CInt(Int[G, S]): """A casting version of the int trait.""" + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "CInt[int, t.Any]", + default_value: t.Union[t.Any, Sentinel] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "CInt[t.Optional[int], t.Any]", + default_value: t.Union[t.Any, Sentinel, None] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + def __init__(self, default_value=Undefined, allow_none=False, **kwargs): + ... + def validate(self, obj, value): try: value = int(value) @@ -2359,12 +2655,36 @@ def validate(self, obj, value): Integer = Int -class Float(TraitType): +class Float(TraitType[G, S]): """A float trait.""" default_value = 0.0 info_text = "a float" + @t.overload + def __init__( + self: "Float[float, t.Union[int, float]]", + default_value: t.Union[float, Sentinel] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Float[t.Optional[int], t.Union[int, float, None]]", + default_value: t.Union[float, Sentinel, None] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + def __init__(self, default_value=Undefined, allow_none=False, **kwargs): self.min = kwargs.pop("min", -float("inf")) self.max = kwargs.pop("max", float("inf")) @@ -2386,9 +2706,38 @@ def subclass_init(self, cls): pass # fully opt out of instance_init -class CFloat(Float): +class CFloat(Float[G, S]): """A casting version of the float trait.""" + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "CFloat[float, t.Any]", + default_value: t.Any = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "CFloat[t.Optional[float], t.Any]", + default_value: t.Any = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + def __init__(self, default_value=Undefined, allow_none=False, **kwargs): + ... + def validate(self, obj, value): try: value = float(value) @@ -2397,7 +2746,7 @@ def validate(self, obj, value): return _validate_bounds(self, obj, value) -class Complex(TraitType): +class Complex(TraitType[complex, t.Union[complex, float, int]]): """A trait for complex numbers.""" default_value = 0.0 + 0.0j @@ -2419,7 +2768,7 @@ def subclass_init(self, cls): pass # fully opt out of instance_init -class CComplex(Complex): +class CComplex(Complex, TraitType[complex, t.Any]): """A casting version of the complex number trait.""" def validate(self, obj, value): @@ -2432,7 +2781,7 @@ def validate(self, obj, value): # We should always be explicit about whether we're using bytes or unicode, both # for Python 3 conversion and for reliable unicode behaviour on Python 2. So # we don't have a Str type. -class Bytes(TraitType): +class Bytes(TraitType[bytes, bytes]): """A trait for byte strings.""" default_value = b"" @@ -2465,7 +2814,7 @@ def subclass_init(self, cls): pass # fully opt out of instance_init -class CBytes(Bytes): +class CBytes(Bytes, TraitType[bytes, t.Any]): """A casting version of the byte string trait.""" def validate(self, obj, value): @@ -2475,12 +2824,41 @@ def validate(self, obj, value): self.error(obj, value) -class Unicode(TraitType): +class Unicode(TraitType[G, S]): """A trait for unicode strings.""" default_value = "" info_text = "a unicode string" + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "Unicode[str, t.Union[str, bytes]]", + default_value: t.Union[str, Sentinel] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Unicode[t.Optional[str], t.Union[str, bytes, None]]", + default_value: t.Union[str, Sentinel, None] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + def __init__(self, **kwargs): + ... + def validate(self, obj, value): if isinstance(value, str): return value @@ -2514,9 +2892,38 @@ def subclass_init(self, cls): pass # fully opt out of instance_init -class CUnicode(Unicode): +class CUnicode(Unicode[G, S], TraitType[str, t.Any]): """A casting version of the unicode trait.""" + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "CUnicode[str, t.Any]", + default_value: t.Union[str, Sentinel] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "CUnicode[t.Optional[str], t.Any]", + default_value: t.Union[str, Sentinel, None] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + def __init__(self, **kwargs): + ... + def validate(self, obj, value): try: return str(value) @@ -2524,14 +2931,14 @@ def validate(self, obj, value): self.error(obj, value) -class ObjectName(TraitType): +class ObjectName(TraitType[str, str]): """A string holding a valid object name in this version of Python. This does not check that the name exists in any scope.""" info_text = "a valid object identifier in Python" - coerce_str = staticmethod(lambda _, s: s) + coerce_str = staticmethod(lambda _, s: s) # type:ignore[no-any-return] def validate(self, obj, value): value = self.coerce_str(obj, value) @@ -2557,12 +2964,41 @@ def validate(self, obj, value): self.error(obj, value) -class Bool(TraitType): +class Bool(TraitType[G, S]): """A boolean (True, False) trait.""" default_value = False info_text = "a boolean" + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "Bool[bool, t.Union[bool, int]]", + default_value: t.Union[bool, Sentinel] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Bool[t.Optional[bool], t.Union[bool, int, None]]", + default_value: t.Union[bool, Sentinel, None] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + def __init__(self, **kwargs): + ... + def validate(self, obj, value): if isinstance(value, bool): return value @@ -2595,9 +3031,38 @@ def argcompleter(self, **kwargs): return completions -class CBool(Bool): +class CBool(Bool[G, S]): """A casting version of the boolean trait.""" + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "CBool[bool, t.Any]", + default_value: t.Union[bool, Sentinel] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "CBool[t.Optional[bool], t.Any]", + default_value: t.Union[bool, Sentinel, None] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + def __init__(self, **kwargs): + ... + def validate(self, obj, value): try: return bool(value) @@ -2605,10 +3070,12 @@ def validate(self, obj, value): self.error(obj, value) -class Enum(TraitType): +class Enum(TraitType[G, S]): """An enum whose value must be in a given sequence.""" - def __init__(self, values, default_value=Undefined, **kwargs): + def __init__( + self: "Enum[t.Any, t.Any]", values: t.Any, default_value: t.Any = Undefined, **kwargs: t.Any + ): self.values = values if kwargs.get("allow_none", False) and default_value is Undefined: default_value = None @@ -2653,10 +3120,15 @@ def argcompleter(self, **kwargs): return [str(v) for v in self.values] -class CaselessStrEnum(Enum): +class CaselessStrEnum(Enum[G, S]): """An enum of strings where the case should be ignored.""" - def __init__(self, values, default_value=Undefined, **kwargs): + def __init__( + self: "CaselessStrEnum[t.Any, t.Any]", + values: t.Any, + default_value: t.Any = Undefined, + **kwargs: t.Any, + ): super().__init__(values, default_value=default_value, **kwargs) def validate(self, obj, value): @@ -2680,7 +3152,7 @@ def info_rst(self): return self._info(as_rst=True) -class FuzzyEnum(Enum): +class FuzzyEnum(Enum[G, S]): """An case-ignoring enum matching choices by unique prefixes/substrings.""" case_sensitive = False @@ -2688,12 +3160,12 @@ class FuzzyEnum(Enum): substring_matching = False def __init__( - self, - values, - default_value=Undefined, - case_sensitive=False, - substring_matching=False, - **kwargs, + self: "FuzzyEnum[t.Any, t.Any]", + values: t.Any, + default_value: t.Any = Undefined, + case_sensitive: bool = False, + substring_matching: bool = False, + **kwargs: t.Any, ): self.case_sensitive = case_sensitive self.substring_matching = substring_matching @@ -2705,7 +3177,11 @@ def validate(self, obj, value): conv_func = (lambda c: c) if self.case_sensitive else lambda c: c.lower() substring_matching = self.substring_matching - match_func = (lambda v, c: v in c) if substring_matching else (lambda v, c: c.startswith(v)) + match_func = ( + (lambda v, c: v in c) + if substring_matching + else (lambda v, c: c.startswith(v)) # type:ignore[no-any-return] + ) value = conv_func(value) choices = self.values matches = [match_func(value, conv_func(c)) for c in choices] @@ -2730,18 +3206,58 @@ def info_rst(self): return self._info(as_rst=True) -class Container(Instance): +class Container(Instance[T]): """An instance of a container (list, set, etc.) To be subclassed by overriding klass. """ - klass: t.Optional[t.Union[str, t.Type[t.Any]]] = None + klass: t.Optional[t.Type[T]] = None _cast_types: t.Any = () _valid_defaults = SequenceTypes _trait = None _literal_from_string_pairs: t.Any = ("[]", "()") + @t.overload + def __init__( + self: "Container[T]", + kind: t.Type[T], + *, + allow_none: Literal[False], + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Container[T | None]", + kind: t.Type[T], + *, + allow_none: Literal[True], + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Optional[t.Any] = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "Container[T]", + kind: t.Type[T], + *, + help: str = ..., + read_only: bool = ..., + config: t.Any = ..., + trait: t.Any = ..., + default_value: t.Any = ..., + **kwargs: t.Any, + ): + ... + def __init__(self, trait=None, default_value=Undefined, **kwargs): """Create a container trait type from a list, set, or tuple. @@ -2817,7 +3333,7 @@ def __init__(self, trait=None, default_value=Undefined, **kwargs): def validate(self, obj, value): if isinstance(value, self._cast_types): assert self.klass is not None - value = self.klass(value) # type:ignore[operator] + value = self.klass(value) # type:ignore[call-arg] value = super().validate(obj, value) if value is None: return value @@ -2838,7 +3354,7 @@ def validate_elements(self, obj, value): else: validated.append(v) assert self.klass is not None - return self.klass(validated) # type:ignore[operator] + return self.klass(validated) # type:ignore[call-arg] def class_init(self, cls, name): if isinstance(self._trait, TraitType): @@ -2889,18 +3405,18 @@ def from_string_list(self, s_list): DeprecationWarning, stacklevel=2, ) - return self.klass(literal_eval(r)) # type:ignore[operator] + return self.klass(literal_eval(r)) # type:ignore[call-arg] sig = inspect.signature(self.item_from_string) if "index" in sig.parameters: item_from_string = self.item_from_string else: # backward-compat: allow item_from_string to ignore index arg def item_from_string(s, index=None): - return self.item_from_string(s) # noqa[E371] + return self.item_from_string(s) - return self.klass( + return self.klass( # type:ignore[call-arg] [item_from_string(s, index=idx) for idx, s in enumerate(s_list)] - ) # type:ignore[operator] + ) def item_from_string(self, s, index=None): """Cast a single item from a string @@ -2913,7 +3429,7 @@ def item_from_string(self, s, index=None): return s -class List(Container): +class List(Container[t.List[t.Any]]): """An instance of a Python list.""" klass = list @@ -3031,7 +3547,7 @@ def default_value_repr(self): return "{" + list_repr[1:-1] + "}" -class Tuple(Container): +class Tuple(Container[t.Tuple[t.Any, ...]]): """An instance of a Python tuple.""" klass = tuple @@ -3160,7 +3676,7 @@ def subclass_init(self, cls): # to opt out of instance_init -class Dict(Instance): +class Dict(Instance[t.Dict[t.Any, t.Any]]): """An instance of a Python dict. One or more traits can be passed to the constructor @@ -3431,7 +3947,7 @@ def item_from_string(self, s): return {key: value} -class TCPAddress(TraitType): +class TCPAddress(TraitType[G, S]): """A trait for an (ip, port) tuple. This allows for both IPv4 IP addresses as well as hostnames. @@ -3440,6 +3956,46 @@ class TCPAddress(TraitType): default_value = ("127.0.0.1", 0) info_text = "an (ip, port) tuple" + if t.TYPE_CHECKING: + + @t.overload + def __init__( + self: "TCPAddress[t.Tuple[str, int], t.Tuple[str, int]]", + default_value: t.Union[bool, Sentinel] = ..., + allow_none: Literal[False] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + @t.overload + def __init__( + self: "TCPAddress[t.Optional[t.Tuple[str, int]], t.Optional[t.Tuple[str, int]]]", + default_value: t.Union[bool, None, Sentinel] = ..., + allow_none: Literal[True] = ..., + read_only: t.Optional[bool] = ..., + help: t.Optional[str] = ..., + config: t.Any = ..., + **kwargs: t.Any, + ): + ... + + def __init__( + self: t.Union[ + "TCPAddress[t.Optional[t.Tuple[str, int]], t.Optional[t.Tuple[str, int]]]", + "TCPAddress[t.Tuple[str, int], t.Tuple[str, int]]", + ], + default_value: t.Union[bool, None, Sentinel] = Undefined, + allow_none: Literal[True, False] = False, + read_only: t.Optional[bool] = None, + help: t.Optional[str] = None, + config: t.Any = None, + **kwargs: t.Any, + ): + ... + def validate(self, obj, value): if isinstance(value, tuple): if len(value) == 2: @@ -3459,7 +4015,7 @@ def from_string(self, s): return (ip, port) -class CRegExp(TraitType): +class CRegExp(TraitType["re.Pattern[t.Any]", t.Union["re.Pattern[t.Any]", str]]): """A casting compiled regular expression trait. Accepts both strings and compiled regular expressions. The resulting @@ -3474,7 +4030,7 @@ def validate(self, obj, value): self.error(obj, value) -class UseEnum(TraitType): +class UseEnum(TraitType[t.Any, t.Any]): """Use a Enum class as model for the data type description. Note that if no default-value is provided, the first enum-value is used as default-value. @@ -3571,7 +4127,7 @@ def info_rst(self): return self._info(as_rst=True) -class Callable(TraitType): +class Callable(TraitType[t.Callable[..., t.Any], t.Callable[..., t.Any]]): """A trait which is callable. Notes