Skip to content
Merged
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
43 changes: 22 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,29 +89,30 @@ We encourage libraries to carefully document which interpretation they implement

### MinLen, MaxLen, Len

`Len()` implies that `min_inclusive <= len(value) < max_exclusive`.
`Len()` implies that `min_length <= len(value) <= max_length` - lower and upper bounds are inclusive.

As well as `Len()` which can optionally include upper and lower bounds, we also
provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_inclusive=x)`
and `Len(max_exclusive=y)` respectively.

We recommend that libraries interpret `slice` objects identically
to `Len()`, making all the following cases equivalent:

* `Annotated[list, :10]`
* `Annotated[list, 0:10]`
* `Annotated[list, None:10]`
* `Annotated[list, slice(0, 10)]`
* `Annotated[list, Len(0, 10)]`
* `Annotated[list, Len(max_exclusive=10)]`
* `Annotated[list, MaxLen(10)]`

And of course you can describe lists of three or more elements (`Len(min_inclusive=3)` or `MinLen(3)`),
four, five, or six elements (`Len(4, 7)` - note exclusive-maximum!) or *exactly*
eight elements (`Len(8, 9)`).

Implementors: note that `Len()` should always have an integer value for
`min_inclusive`, but `slice` objects can also have `start=None`.
provide `MinLen(x)` and `MaxLen(y)` which are equivalent to `Len(min_length=x)`
and `Len(max_length=y)` respectively.

`Len`, `MinLen`, and `MaxLen` may be used with any type which supports `len(value)`.

Examples of usage:

* `Annotated[list, MaxLen(10)]` (or `Annotated[list, Len(max_length=10))`) - list must have a length of 10 or less
* `Annotated[str, MaxLen(10)]` - string must have a length of 10 or less
* `Annotated[list, MinLen(3))` (or `Annotated[list, Len(min_length=3))`) - list must have a length of 3 or more
* `Annotated[list, Len(4, 6)]` - list must have a length of 4, 5, or 6
* `Annotated[list, Len(8, 8)]` - list must have a length of exactly 8

#### Changed in v0.4.0

* `min_inclusive` has been renamed to `min_length`, no change in meaning
* `max_exclusive` has been renamed to `max_length`, upper bound is now **inclusive** instead of **exclusive**
* The recommendation that slices are interpreted as `Len` has been removed due to ambiguity and different semantic
meaning of the upper bound in slices vs. `Len`

See [issue #23](https://github.com/annotated-types/annotated-types/issues/23) for discussion.

### Timezone

Expand Down
44 changes: 16 additions & 28 deletions annotated_types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
'__version__',
)

__version__ = '0.3.1'
__version__ = '0.4.0'


T = TypeVar('T')
Expand Down Expand Up @@ -223,52 +223,40 @@ class MultipleOf(BaseMetadata):
@dataclass(frozen=True, **SLOTS)
class MinLen(BaseMetadata):
"""
MinLen() implies minimum inclusive length.

For more details, see ``Len()`` below.
MinLen() implies minimum inclusive length,
e.g. ``len(value) >= min_length``.
"""

min_inclusive: Annotated[int, Ge(0)]
min_length: Annotated[int, Ge(0)]


@dataclass(frozen=True, **SLOTS)
class MaxLen(BaseMetadata):
"""
MaxLen() implies maximum exclusive length.

For more details, see ``Len()`` below.
MaxLen() implies maximum inclusive length,
e.g. ``len(value) <= max_length``.
"""

max_exclusive: Annotated[int, Ge(0)]
max_length: Annotated[int, Ge(0)]


@dataclass(frozen=True, **SLOTS)
class Len(GroupedMetadata):
"""Len() implies that ``min_inclusive <= len(value) < max_exclusive``.

We also recommend that libraries interpret ``slice`` objects identically
to Len(), meaning that the following cases are all equivalent:

- ``Annotated[list, :10]``
- ``Annotated[list, 0:10]``
- ``Annotated[list, None:10]``
- ``Annotated[list, slice(0, 10)]``
- ``Annotated[list, Len(0, 10)]``
- ``Annotated[list, Len(max_exclusive=10)]``
"""
Len() implies that ``min_length <= len(value) <= max_length``.

Implementors: note that Len() should always have an integer value for
``min_inclusive``, but ``slice`` objects can also have ``start=None``.
Upper bound may be omitted or ``None`` to indicate no upper length bound.
"""

min_inclusive: Annotated[int, Ge(0)] = 0
max_exclusive: Optional[Annotated[int, Ge(0)]] = None
min_length: Annotated[int, Ge(0)] = 0
max_length: Optional[Annotated[int, Ge(0)]] = None

def __iter__(self) -> Iterator[BaseMetadata]:
"""Unpack a Len into zone or more single-bounds."""
if self.min_inclusive > 0:
yield MinLen(self.min_inclusive)
if self.max_exclusive is not None:
yield MaxLen(self.max_exclusive)
if self.min_length > 0:
yield MinLen(self.min_length)
if self.max_length is not None:
yield MaxLen(self.max_length)


@dataclass(frozen=True, **SLOTS)
Expand Down
30 changes: 12 additions & 18 deletions annotated_types/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,20 @@ def cases() -> Iterable[Case]:

yield Case(Annotated[str, at.MinLen(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[str, at.Len(3)], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[str, 3:], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[str, 3:None], ('123', '1234', 'x' * 10), ('', '1', '12'))
yield Case(Annotated[List[int], at.MinLen(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[List[int], at.Len(3)], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[List[int], 3:], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))
yield Case(Annotated[List[int], 3:None], ([1, 2, 3], [1, 2, 3, 4], [1] * 10), ([], [1], [1, 2]))

yield Case(Annotated[str, at.MaxLen(4)], ('', '123'), ('1234', 'x' * 10))
yield Case(Annotated[str, at.Len(0, 4)], ('', '123'), ('1234', 'x' * 10))
yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 4, ['b'] * 5))
yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 4, ['b'] * 5))
yield Case(Annotated[str, 0:4], ('', '123'), ('1234', 'x' * 10))
yield Case(Annotated[str, :4], ('', '123'), ('1234', 'x' * 10))

yield Case(Annotated[str, at.Len(3, 5)], ('123', '1234'), ('', '1', '12', '12345', 'x' * 10))
yield Case(Annotated[str, 3:5], ('123', '1234'), ('', '1', '12', '12345', 'x' * 10))

yield Case(Annotated[Dict[int, int], at.Len(2, 4)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
yield Case(Annotated[Set[int], at.Len(2, 4)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
yield Case(Annotated[Tuple[int, ...], at.Len(2, 4)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))

yield Case(Annotated[str, at.MaxLen(4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[str, at.Len(0, 4)], ('', '1234'), ('12345', 'x' * 10))
yield Case(Annotated[List[str], at.MaxLen(4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))
yield Case(Annotated[List[str], at.Len(0, 4)], ([], ['a', 'bcdef'], ['a', 'b', 'c']), (['a'] * 5, ['b'] * 10))

yield Case(Annotated[str, at.Len(3, 5)], ('123', '12345'), ('', '1', '12', '123456', 'x' * 10))
yield Case(Annotated[str, at.Len(3, 3)], ('123',), ('12', '1234'))

yield Case(Annotated[Dict[int, int], at.Len(2, 3)], [{1: 1, 2: 2}], [{}, {1: 1}, {1: 1, 2: 2, 3: 3, 4: 4}])
yield Case(Annotated[Set[int], at.Len(2, 3)], ({1, 2}, {1, 2, 3}), (set(), {1}, {1, 2, 3, 4}))
yield Case(Annotated[Tuple[int, ...], at.Len(2, 3)], ((1, 2), (1, 2, 3)), ((), (1,), (1, 2, 3, 4)))

# Timezone

Expand Down
4 changes: 2 additions & 2 deletions tests/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,12 @@ def check_multiple_of(constraint: Constraint, val: Any) -> bool:

def check_min_len(constraint: Constraint, val: Any) -> bool:
assert isinstance(constraint, annotated_types.MinLen)
return len(val) >= constraint.min_inclusive
return len(val) >= constraint.min_length


def check_max_len(constraint: Constraint, val: Any) -> bool:
assert isinstance(constraint, annotated_types.MaxLen)
return len(val) < constraint.max_exclusive
return len(val) <= constraint.max_length


def check_predicate(constraint: Constraint, val: Any) -> bool:
Expand Down