Skip to content

Commit

Permalink
Typing upgrades (#868)
Browse files Browse the repository at this point in the history
  • Loading branch information
blink1073 authored Sep 26, 2023
1 parent 2ce600f commit c0a3afd
Show file tree
Hide file tree
Showing 10 changed files with 474 additions and 131 deletions.
2 changes: 1 addition & 1 deletion traitlets/config/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -446,7 +446,7 @@ def _show_config_changed(self, change):
self._save_start = self.start
self.start = self.start_show_config # type:ignore[method-assign]

def __init__(self, **kwargs):
def __init__(self, **kwargs: t.Any) -> None:
SingletonConfigurable.__init__(self, **kwargs)
# Ensure my class is in self.classes, so my attributes appear in command line
# options and config files.
Expand Down
3 changes: 2 additions & 1 deletion traitlets/config/configurable.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@


import logging
import typing as t
from copy import deepcopy
from textwrap import dedent

Expand Down Expand Up @@ -46,7 +47,7 @@ class Configurable(HasTraits):
config = Instance(Config, (), {})
parent = Instance("traitlets.config.configurable.Configurable", allow_none=True)

def __init__(self, **kwargs):
def __init__(self, **kwargs: t.Any) -> None:
"""Create a configurable given a config config.
Parameters
Expand Down
39 changes: 18 additions & 21 deletions traitlets/config/loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations

import argparse
import copy
Expand Down Expand Up @@ -236,7 +237,7 @@ class Config(dict): # type:ignore[type-arg]
"""

def __init__(self, *args, **kwds):
def __init__(self, *args: t.Any, **kwds: t.Any) -> None:
dict.__init__(self, *args, **kwds)
self._ensure_subconfig()

Expand Down Expand Up @@ -273,15 +274,15 @@ def merge(self, other):

self.update(to_update)

def collisions(self, other: "Config") -> t.Dict[str, t.Any]:
def collisions(self, other: Config) -> dict[str, t.Any]:
"""Check for collisions between two config objects.
Returns a dict of the form {"Class": {"trait": "collision message"}}`,
indicating which values have been ignored.
An empty dict indicates no collisions.
"""
collisions: t.Dict[str, t.Any] = {}
collisions: dict[str, t.Any] = {}
for section in self:
if section not in other:
continue
Expand Down Expand Up @@ -490,7 +491,7 @@ def _log_default(self):

return get_logger()

def __init__(self, log=None):
def __init__(self, log: t.Any = None) -> None:
"""A base class for config loaders.
log : instance of :class:`logging.Logger` to use.
Expand Down Expand Up @@ -532,7 +533,7 @@ class FileConfigLoader(ConfigLoader):
here.
"""

def __init__(self, filename, path=None, **kw):
def __init__(self, filename: str, path: str | None = None, **kw: t.Any) -> None:
"""Build a config loader for a filename and path.
Parameters
Expand Down Expand Up @@ -795,12 +796,12 @@ class ArgParseConfigLoader(CommandLineConfigLoader):

def __init__(
self,
argv: t.Optional[t.List[str]] = None,
aliases: t.Optional[t.Dict[Flags, str]] = None,
flags: t.Optional[t.Dict[Flags, str]] = None,
argv: list[str] | None = None,
aliases: dict[Flags, str] | None = None,
flags: dict[Flags, str] | None = None,
log: t.Any = None,
classes: t.Optional[t.List[t.Type[t.Any]]] = None,
subcommands: t.Optional[SubcommandsDict] = None,
classes: list[type[t.Any]] | None = None,
subcommands: SubcommandsDict | None = None,
*parser_args: t.Any,
**parser_kw: t.Any,
) -> None:
Expand Down Expand Up @@ -899,17 +900,15 @@ def _create_parser(self):
def _add_arguments(self, aliases, flags, classes):
raise NotImplementedError("subclasses must implement _add_arguments")

def _argcomplete(
self, classes: t.List[t.Any], subcommands: t.Optional[SubcommandsDict]
) -> None:
def _argcomplete(self, classes: list[t.Any], subcommands: SubcommandsDict | None) -> None:
"""If argcomplete is enabled, allow triggering command-line autocompletion"""
pass

def _parse_args(self, args):
"""self.parser->self.parsed_data"""
uargs = [cast_unicode(a) for a in args]

unpacked_aliases: t.Dict[str, str] = {}
unpacked_aliases: dict[str, str] = {}
if self.aliases:
unpacked_aliases = {}
for alias, alias_target in self.aliases.items():
Expand Down Expand Up @@ -957,7 +956,7 @@ def _convert_to_config(self):
class _FlagAction(argparse.Action):
"""ArgParse action to handle a flag"""

def __init__(self, *args, **kwargs):
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
self.flag = kwargs.pop("flag")
self.alias = kwargs.pop("alias", None)
kwargs["const"] = Undefined
Expand All @@ -983,8 +982,8 @@ class KVArgParseConfigLoader(ArgParseConfigLoader):
parser_class = _KVArgParser # type:ignore[assignment]

def _add_arguments(self, aliases, flags, classes):
alias_flags: t.Dict[str, t.Any] = {}
argparse_kwds: t.Dict[str, t.Any]
alias_flags: dict[str, t.Any] = {}
argparse_kwds: dict[str, t.Any]
paa = self.parser.add_argument
self.parser.set_defaults(_flags=[])
paa("extra_args", nargs="*")
Expand Down Expand Up @@ -1108,9 +1107,7 @@ def _handle_unrecognized_alias(self, arg: str) -> None:
"""
self.log.warning("Unrecognized alias: '%s', it will have no effect.", arg)

def _argcomplete(
self, classes: t.List[t.Any], subcommands: t.Optional[SubcommandsDict]
) -> None:
def _argcomplete(self, classes: list[t.Any], subcommands: SubcommandsDict | None) -> None:
"""If argcomplete is enabled, allow triggering command-line autocompletion"""
try:
import argcomplete # noqa
Expand All @@ -1132,7 +1129,7 @@ class KeyValueConfigLoader(KVArgParseConfigLoader):
Use KVArgParseConfigLoader
"""

def __init__(self, *args, **kwargs):
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
warnings.warn(
"KeyValueConfigLoader is deprecated since Traitlets 5.0."
" Use KVArgParseConfigLoader instead.",
Expand Down
5 changes: 3 additions & 2 deletions traitlets/log.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

# Copyright (c) IPython Development Team.
# Distributed under the terms of the Modified BSD License.
from __future__ import annotations

import logging

_logger = None
_logger: logging.Logger | None = None


def get_logger():
def get_logger() -> logging.Logger:
"""Grab the global logger instance.
If a global Application is instantiated, grab its logger.
Expand Down
203 changes: 202 additions & 1 deletion traitlets/tests/test_typing.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,220 @@

import pytest

from traitlets import Bool, CInt, HasTraits, Instance, Int, TCPAddress
from traitlets import (
Any,
Bool,
CInt,
Dict,
HasTraits,
Instance,
Int,
List,
Set,
TCPAddress,
Type,
Unicode,
Union,
default,
observe,
validate,
)
from traitlets.config import Config

if not typing.TYPE_CHECKING:

def reveal_type(*args, **kwargs):
pass


# mypy: disallow-untyped-calls


class Foo:
def __init__(self, c):
self.c = c


@pytest.mark.mypy_testing
def mypy_decorator_typing():
class T(HasTraits):
foo = Unicode("").tag(config=True)

@default("foo")
def _default_foo(self) -> str:
return "hi"

@observe("foo")
def _foo_observer(self, change: typing.Any) -> bool:
return True

@validate("foo")
def _foo_validate(self, commit: typing.Any) -> bool:
return True

t = T()
reveal_type(t.foo) # R: builtins.str
reveal_type(t._foo_observer) # R: Any
reveal_type(t._foo_validate) # R: Any


@pytest.mark.mypy_testing
def mypy_config_typing():
c = Config(
{
"ExtractOutputPreprocessor": {"enabled": True},
}
)
reveal_type(c) # R: traitlets.config.loader.Config


@pytest.mark.mypy_testing
def mypy_union_typing():
class T(HasTraits):
style = Union(
[Unicode("default"), Type(klass=object)],
help="Name of the pygments style to use",
default_value="hi",
).tag(config=True)

t = T()
reveal_type(Union("foo")) # R: traitlets.traitlets.Union
reveal_type(Union("").tag(sync=True)) # R: traitlets.traitlets.Union
reveal_type(Union(None, allow_none=True)) # R: traitlets.traitlets.Union
reveal_type(Union(None, allow_none=True).tag(sync=True)) # R: traitlets.traitlets.Union
reveal_type(T.style) # R: traitlets.traitlets.Union
reveal_type(t.style) # R: Any


@pytest.mark.mypy_testing
def mypy_list_typing():
class T(HasTraits):
latex_command = List(
["xelatex", "{filename}", "-quiet"], help="Shell command used to compile latex."
).tag(config=True)

t = T()
reveal_type(List("foo")) # R: traitlets.traitlets.List
reveal_type(List("").tag(sync=True)) # R: traitlets.traitlets.List
reveal_type(List(None, allow_none=True)) # R: traitlets.traitlets.List
reveal_type(List(None, allow_none=True).tag(sync=True)) # R: traitlets.traitlets.List
reveal_type(T.latex_command) # R: traitlets.traitlets.List
reveal_type(t.latex_command) # R: builtins.list[Any]


@pytest.mark.mypy_testing
def mypy_dict_typing():
class T(HasTraits):
foo = Dict({}, help="Shell command used to compile latex.").tag(config=True)

t = T()
reveal_type(Dict("foo")) # R: traitlets.traitlets.Dict
reveal_type(Dict("").tag(sync=True)) # R: traitlets.traitlets.Dict
reveal_type(Dict(None, allow_none=True)) # R: traitlets.traitlets.Dict
reveal_type(Dict(None, allow_none=True).tag(sync=True)) # R: traitlets.traitlets.Dict
reveal_type(T.foo) # R: traitlets.traitlets.Dict
reveal_type(t.foo) # R: builtins.dict[Any, Any]


@pytest.mark.mypy_testing
def mypy_unicode_typing():
class T(HasTraits):
export_format = Unicode(
allow_none=False,
help="""The export format to be used, either one of the built-in formats
or a dotted object name that represents the import path for an
``Exporter`` class""",
).tag(config=True)

t = T()
reveal_type(
Unicode( # R: traitlets.traitlets.Unicode[builtins.str, Union[builtins.str, builtins.bytes]]
"foo"
)
)
reveal_type(
Unicode( # R: traitlets.traitlets.Unicode[builtins.str, Union[builtins.str, builtins.bytes]]
""
).tag(
sync=True
)
)
reveal_type(
Unicode( # R: traitlets.traitlets.Unicode[Union[builtins.str, None], Union[builtins.str, builtins.bytes, None]]
None, allow_none=True
)
)
reveal_type(
Unicode( # R: traitlets.traitlets.Unicode[Union[builtins.str, None], Union[builtins.str, builtins.bytes, None]]
None, allow_none=True
).tag(
sync=True
)
)
reveal_type(
T.export_format # R: traitlets.traitlets.Unicode[builtins.str, Union[builtins.str, builtins.bytes]]
)
reveal_type(t.export_format) # R: builtins.str


@pytest.mark.mypy_testing
def mypy_set_typing():
class T(HasTraits):
remove_cell_tags = Set(
Unicode(),
default_value=[],
help=(
"Tags indicating which cells are to be removed,"
"matches tags in ``cell.metadata.tags``."
),
).tag(config=True)

safe_output_keys = Set(
config=True,
default_value={
"metadata", # Not a mimetype per-se, but expected and safe.
"text/plain",
"text/latex",
"application/json",
"image/png",
"image/jpeg",
},
help="Cell output mimetypes to render without modification",
)

t = T()
reveal_type(Set("foo")) # R: traitlets.traitlets.Set
reveal_type(Set("").tag(sync=True)) # R: traitlets.traitlets.Set
reveal_type(Set(None, allow_none=True)) # R: traitlets.traitlets.Set
reveal_type(Set(None, allow_none=True).tag(sync=True)) # R: traitlets.traitlets.Set
reveal_type(T.remove_cell_tags) # R: traitlets.traitlets.Set
reveal_type(t.remove_cell_tags) # R: builtins.set[Any]
reveal_type(T.safe_output_keys) # R: traitlets.traitlets.Set
reveal_type(t.safe_output_keys) # R: builtins.set[Any]


@pytest.mark.mypy_testing
def mypy_any_typing():
class T(HasTraits):
attributes = Any(
config=True,
default_value={
"a": ["href", "title"],
"abbr": ["title"],
"acronym": ["title"],
},
help="Allowed HTML tag attributes",
)

t = T()
reveal_type(Any("foo")) # R: traitlets.traitlets.Any
reveal_type(Any("").tag(sync=True)) # R: traitlets.traitlets.Any
reveal_type(Any(None, allow_none=True)) # R: traitlets.traitlets.Any
reveal_type(Any(None, allow_none=True).tag(sync=True)) # R: traitlets.traitlets.Any
reveal_type(T.attributes) # R: traitlets.traitlets.Any
reveal_type(t.attributes) # R: Any


@pytest.mark.mypy_testing
def mypy_bool_typing():
class T(HasTraits):
Expand Down
Loading

0 comments on commit c0a3afd

Please sign in to comment.