Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions fgmetric/_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,27 @@ def is_collection(annotation: TypeAnnotation | None) -> bool:
return any(has_origin(annotation, origin) for origin in (list, set, frozenset, tuple))


def is_mapping(annotation: TypeAnnotation | None) -> bool:
"""
Check if a type annotation is a `dict` type.

Matches parameterized `dict[K, V]`, including its optional form. `Counter` is a `dict`
subclass but has a distinct origin, so it is not a mapping for this purpose (it is handled
separately, see `is_counter`).

Examples:
>>> is_mapping(dict[str, int])
True
>>> is_mapping(dict[str, int] | None)
True
>>> is_mapping(Counter[str])
False
>>> is_mapping(dict) # bare dict, no type parameters
False
"""
return has_origin(annotation, dict)


def is_counter(annotation: TypeAnnotation | None) -> bool:
"""
True if the type annotation is a Counter.
Expand Down
30 changes: 30 additions & 0 deletions tests/test_typing_extensions.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from fgmetric._typing_extensions import has_origin
from fgmetric._typing_extensions import is_collection
from fgmetric._typing_extensions import is_list
from fgmetric._typing_extensions import is_mapping
from fgmetric._typing_extensions import is_optional
from fgmetric._typing_extensions import unpack_optional

Expand Down Expand Up @@ -111,6 +112,35 @@ def test_is_not_collection(annotation: TypeAnnotation) -> None:
assert not is_collection(annotation)


@pytest.mark.parametrize(
"annotation",
[
dict[str, int],
dict[int, float],
Optional[dict[str, int]],
dict[str, int] | None,
],
)
def test_is_mapping(annotation: TypeAnnotation) -> None:
"""Should identify dict types, even within an Optional."""
assert is_mapping(annotation)


@pytest.mark.parametrize(
"annotation",
[
str,
list[int],
set[str],
Counter[str],
dict,
],
)
def test_is_not_mapping(annotation: TypeAnnotation) -> None:
"""Should reject Counters, bare dict, and non-dict types."""
assert not is_mapping(annotation)


@pytest.mark.parametrize(
"annotation,collection_type",
[
Expand Down