Skip to content

Commit

Permalink
feat: add ParameterView and tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
peterschutt committed Sep 26, 2023
1 parent fe26db6 commit 0d3cf21
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 4 deletions.
36 changes: 36 additions & 0 deletions tests/test_parameter_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
from __future__ import annotations

from inspect import Parameter

import pytest

from type_lens.exc import ParameterViewError
from type_lens.parameter_view import ParameterView
from type_lens.types import Empty


def test_param_view() -> None:
"""Test ParameterView."""
param = Parameter("foo", Parameter.POSITIONAL_OR_KEYWORD, annotation=int)
param_view = ParameterView.from_parameter(param, {"foo": int})
assert param_view.name == "foo"
assert param_view.default is Empty
assert param_view.type_view.annotation is int


def test_param_view_raises_improperly_configured_if_no_annotation() -> None:
"""Test ParameterView raises ImproperlyConfigured if no annotation."""
param = Parameter("foo", Parameter.POSITIONAL_OR_KEYWORD)
with pytest.raises(ParameterViewError):
ParameterView.from_parameter(param, {})


def test_param_view_has_default_predicate() -> None:
"""Test ParameterView.has_default."""
param = Parameter("foo", Parameter.POSITIONAL_OR_KEYWORD, annotation=int)
param_view = ParameterView.from_parameter(param, {"foo": int})
assert param_view.has_default is False

param = Parameter("foo", Parameter.POSITIONAL_OR_KEYWORD, annotation=int, default=42)
param_view = ParameterView.from_parameter(param, {"foo": int})
assert param_view.has_default is True
4 changes: 2 additions & 2 deletions type_lens/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
from __future__ import annotations

from .type_lens import TypeLens
from .type_view import TypeView

__all__ = ("TypeLens",)
__all__ = ("TypeView",)
17 changes: 17 additions & 0 deletions type_lens/exc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
__all__ = (
"TypeLensError",
"TypeViewError",
"ParameterViewError",
)


class TypeLensError(Exception):
"""Base class for library exceptions."""


class TypeViewError(TypeLensError):
"""Base class for TypeView exceptions."""


class ParameterViewError(TypeLensError):
"""Base class for ParameterView exceptions."""
63 changes: 63 additions & 0 deletions type_lens/parameter_view.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
from __future__ import annotations

from dataclasses import dataclass
from inspect import Signature
from typing import TYPE_CHECKING

from type_lens.exc import ParameterViewError
from type_lens.type_view import TypeView
from type_lens.types import Empty

__all__ = ("ParameterView",)


if TYPE_CHECKING:
from inspect import Parameter
from typing import Any, Self


@dataclass(frozen=True)
class ParameterView:
"""Represents the parameters of a callable."""

__slots__ = (
"name",
"default",
"type_view",
)

name: str
"""The name of the parameter."""
default: Any | Empty
"""The default value of the parameter."""
type_view: TypeView
"""View of the parameter's annotation type."""

@property
def has_default(self) -> bool:
"""Whether the parameter has a default value or not."""
return self.default is not Empty

@classmethod
def from_parameter(cls, parameter: Parameter, fn_type_hints: dict[str, Any]) -> Self:
"""Initialize ParsedSignatureParameter.
Args:
parameter: inspect.Parameter
fn_type_hints: mapping of names to types. Should be result of ``get_type_hints()``, preferably via the
:attr:``get_fn_type_hints() <.utils.signature_parsing.get_fn_type_hints>` helper.
Returns:
ParsedSignatureParameter.
"""
try:
annotation = fn_type_hints[parameter.name]
except KeyError as err:
msg = f"No annotation found for '{parameter.name}'"
raise ParameterViewError(msg) from err

return cls(
name=parameter.name,
default=Empty if parameter.default is Signature.empty else parameter.default,
type_view=TypeView(annotation),
)
6 changes: 5 additions & 1 deletion type_lens/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import sys
from typing import Union

__all__ = ["UNION_TYPES", "NoneType"]
__all__ = ["UNION_TYPES", "Empty", "NoneType"]

if sys.version_info >= (3, 10):
from types import UnionType
Expand All @@ -13,3 +13,7 @@
UNION_TYPES = {Union}

NoneType: type[None] = type(None)


class Empty:
"""A sentinel class used as placeholder."""
2 changes: 1 addition & 1 deletion type_lens/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
_WRAPPER_TYPES: te.Final = {te.Annotated, te.Required, te.NotRequired}
"""Types that always contain a wrapped type annotation as their first arg."""

_GENERIC_ORIGIN_MAP: te.Final = {
_GENERIC_ORIGIN_MAP: te.Final[dict[t.Any, t.Any]] = {
set: t.AbstractSet,
defaultdict: t.DefaultDict,
deque: t.Deque,
Expand Down

0 comments on commit 0d3cf21

Please sign in to comment.