Skip to content

Commit

Permalink
add GroupedMetadata as a base class for Interval (#12)
Browse files Browse the repository at this point in the history
Co-authored-by: Zac Hatfield-Dodds <zac.hatfield.dodds@gmail.com>
  • Loading branch information
adriangb and Zac-HD committed Jul 25, 2022
1 parent 12855f7 commit f59cf6d
Show file tree
Hide file tree
Showing 2 changed files with 54 additions and 10 deletions.
51 changes: 48 additions & 3 deletions annotated_types/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import sys
from abc import ABC, abstractmethod
from dataclasses import dataclass
from datetime import timezone
from typing import Any, Callable, Iterator, Optional, TypeVar, Union
Expand Down Expand Up @@ -82,7 +83,13 @@ def __div__(self: T, __other: T) -> T:


class BaseMetadata:
pass
"""Base class for all metadata.
This exists mainly so that implementers
can do `isinstance(..., BaseMetadata)` while traversing field annotations.
"""

__slots__ = ()


@dataclass(frozen=True, **SLOTS)
Expand Down Expand Up @@ -129,8 +136,46 @@ class Le(BaseMetadata):
le: SupportsLe


class GroupedMetadata(ABC):
"""A grouping of multiple BaseMetadata objects.
`GroupedMetadata` on its own is not metadata and has no meaning.
All it the the constraint and metadata should be fully expressable
in terms of the `BaseMetadata`'s returned by `GroupedMetadata.__iter__()`.
Concrete implementations should override `GroupedMetadata.__iter__()`
to add their own metadata.
For example:
>>> @dataclass
>>> class Field(GroupedMetadata):
>>> gt: float | None = None
>>> description: str | None = None
...
>>> def __iter__(self) -> Iterable[BaseMetadata]:
>>> if self.gt is not None:
>>> yield Gt(self.gt)
>>> if self.description is not None:
>>> yield Description(self.gt)
Also see the implementation of `Interval` below for an example.
Parsers should recognize this and unpack it so that it can be used
both with and without unpacking:
- `Annotated[int, Field(...)]` (parser must unpack Field)
- `Annotated[int, *Field(...)]` (PEP-646)
""" # noqa: trailing-whitespace

__slots__ = ()

@abstractmethod
def __iter__(self) -> Iterator[BaseMetadata]: # pragma: no cover
pass


@dataclass(frozen=True, **KW_ONLY, **SLOTS)
class Interval(BaseMetadata):
class Interval(GroupedMetadata):
"""Interval can express inclusive or exclusive bounds with a single object.
It accepts keyword arguments ``gt``, ``ge``, ``lt``, and/or ``le``, which
Expand All @@ -143,7 +188,7 @@ class Interval(BaseMetadata):
le: Union[SupportsLe, None] = None

def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack an Interval into zero or more single-bounds, as per PEP-646."""
"""Unpack an Interval into zero or more single-bounds."""
if self.gt is not None:
yield Gt(self.gt)
if self.ge is not None:
Expand Down
13 changes: 6 additions & 7 deletions tests/test_main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import re
import sys
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Any, Callable, Dict, Iterable, Iterator, Type, Union
Expand All @@ -15,7 +16,7 @@
import annotated_types
from annotated_types.test_cases import Case, cases

Constraint = Union[annotated_types.BaseMetadata, slice]
Constraint = Union[annotated_types.BaseMetadata, slice, "re.Pattern[bytes]", "re.Pattern[str]"]


def check_gt(constraint: Constraint, val: Any) -> bool:
Expand Down Expand Up @@ -96,12 +97,10 @@ def get_constraints(tp: type) -> Iterator[Constraint]:
args = iter(get_args(tp))
next(args)
for arg in args:
if isinstance(arg, (annotated_types.BaseMetadata, slice)):
if isinstance(arg, annotated_types.Interval):
for case in arg:
yield case
else:
yield arg
if isinstance(arg, (annotated_types.BaseMetadata, re.Pattern, slice)):
yield arg
elif isinstance(arg, annotated_types.GroupedMetadata):
yield from arg


def is_valid(tp: type, value: Any) -> bool:
Expand Down

0 comments on commit f59cf6d

Please sign in to comment.