diff --git a/dirty_equals/__init__.py b/dirty_equals/__init__.py index ac4b1c6..23e9e4e 100644 --- a/dirty_equals/__init__.py +++ b/dirty_equals/__init__.py @@ -1,7 +1,6 @@ -from ._base import AnyThing, IsInstance +from ._base import AnyThing, DirtyEquals, IsInstance, IsOneOf from ._datetime import IsDatetime, IsNow from ._dict import IsDict, IsIgnoreDict, IsPartialDict, IsStrictDict -from ._list_tuple import Contains, HasLen, IsList, IsListOrTuple, IsTuple from ._numeric import ( IsApprox, IsFloat, @@ -18,12 +17,15 @@ IsPositiveInt, ) from ._other import FunctionCheck, IsJson, IsUUID +from ._sequence import Contains, HasLen, IsList, IsListOrTuple, IsTuple from ._strings import IsAnyStr, IsBytes, IsStr __all__ = ( # base + 'DirtyEquals', 'AnyThing', 'IsInstance', + 'IsOneOf', # datetime 'IsDatetime', 'IsNow', @@ -32,7 +34,7 @@ 'IsPartialDict', 'IsIgnoreDict', 'IsStrictDict', - # list or tuple + # sequence 'Contains', 'HasLen', 'IsList', diff --git a/dirty_equals/_base.py b/dirty_equals/_base.py index c4b906e..9481a8f 100644 --- a/dirty_equals/_base.py +++ b/dirty_equals/_base.py @@ -12,7 +12,7 @@ if TYPE_CHECKING: from typing import TypeAlias -__all__ = 'DirtyEqualsMeta', 'DirtyEquals', 'IsInstance', 'AnyThing' +__all__ = 'DirtyEqualsMeta', 'DirtyEquals', 'IsInstance', 'AnyThing', 'IsOneOf' class DirtyEqualsMeta(ABCMeta): @@ -257,3 +257,31 @@ class AnyThing(DirtyEquals[Any]): def equals(self, other: Any) -> bool: return True + + +class IsOneOf(DirtyEquals[Any]): + """ + A type which checks that the value is equal to one of the given values. + + Can be useful with boolean operators. + """ + + def __init__(self, expected_value: Any, *more_expected_values: Any): + """ + Args: + expected_value: Expected value for equals to return true. + *more_expected_values: More expected values for equals to return true. + + ```py title="IsOneOf" + from dirty_equals import IsOneOf, Contains + + assert 1 == IsOneOf(1, 2, 3) + assert 4 != IsOneOf(1, 2, 3) + assert [1, 2, 3] == Contains(1) | IsOneOf([]) + assert [] == Contains(1) | IsOneOf([]) + """ + self.expected_values: Tuple[Any, ...] = (expected_value,) + more_expected_values + super().__init__(*self.expected_values) + + def equals(self, other: Any) -> bool: + return any(other == e for e in self.expected_values) diff --git a/dirty_equals/_list_tuple.py b/dirty_equals/_sequence.py similarity index 100% rename from dirty_equals/_list_tuple.py rename to dirty_equals/_sequence.py diff --git a/docs/types/custom.md b/docs/types/custom.md new file mode 100644 index 0000000..98ada47 --- /dev/null +++ b/docs/types/custom.md @@ -0,0 +1,38 @@ +# Custom Types + +::: dirty_equals._base.DirtyEquals + rendering: + merge_init_into_class: false + +## Custom Type Example + +To demonstrate the use of custom types, we'll create a custom type that matches any even number. + +We won't inherit from [`IsNumeric`][dirty_equals.IsNumeric] in this case to keep the example simple. + +```py +title="IsEven" +from decimal import Decimal +from typing import Any, Union +from dirty_equals import IsOneOf +from dirty_equals import DirtyEquals + +class IsEven(DirtyEquals[Union[int, float, Decimal]]): + def equals(self, other: Any) -> bool: + return other % 2 == 0 + +assert 2 == IsEven +assert 3 != IsEven +assert 'foobar' != IsEven +assert 3 == IsEven | IsOneOf(3) +``` + +There are a few advantages of inheriting from [`DirtyEquals`][dirty_equals.DirtyEquals] compared to just +implementing your own class with an `__eq__` method: + +1. `TypeError` and `ValueError` in `equals` are caught and result in a not-equals result. +2. A useful `__repr__` is generated, and modified if the `==` operation returns `True`, + see [pytest compatibility](../usage.md#__repr__-and-pytest-compatibility) +3. [boolean logic](../usage.md#boolean-logic) works out of the box +4. [Uninitialised usage](../usage.md#initialised-vs-class-comparison) + (`IsEven` rather than `IsEven()`) works out of the box diff --git a/docs/types.md b/docs/types/datetime.md similarity index 51% rename from docs/types.md rename to docs/types/datetime.md index e001458..1ea3af0 100644 --- a/docs/types.md +++ b/docs/types/datetime.md @@ -1,60 +1,8 @@ -## Numeric Types - -::: dirty_equals.IsInt - rendering: - merge_init_into_class: false - separate_signature: false - -::: dirty_equals.IsFloat - rendering: - merge_init_into_class: false - separate_signature: false - -::: dirty_equals.IsPositive - rendering: - merge_init_into_class: false - -::: dirty_equals.IsNegative - rendering: - merge_init_into_class: false - -::: dirty_equals.IsNonNegative - rendering: - merge_init_into_class: false - -::: dirty_equals.IsNonPositive - rendering: - merge_init_into_class: false - -::: dirty_equals.IsPositiveInt - rendering: - merge_init_into_class: false - -::: dirty_equals.IsNegativeInt - rendering: - merge_init_into_class: false - -::: dirty_equals.IsPositiveFloat - rendering: - merge_init_into_class: false - -::: dirty_equals.IsNegativeFloat - rendering: - merge_init_into_class: false - -::: dirty_equals.IsApprox - -::: dirty_equals.IsNumber - rendering: - merge_init_into_class: false - -::: dirty_equals.IsNumeric - -## Date and Time Types +# Date and Time Types ::: dirty_equals.IsDatetime -#### Timezones +### Timezones Timezones are hard, anyone who claims otherwise is either a genius, a liar, or an idiot. @@ -98,49 +46,3 @@ assert new_year_eve_nyc != IsDatetime(approx=new_year_naive, enforce_tz=False) ``` ::: dirty_equals.IsNow - -## Dictionary Types - -::: dirty_equals.IsDict - -::: dirty_equals.IsPartialDict - -::: dirty_equals.IsIgnoreDict - -::: dirty_equals.IsStrictDict - -## List and Tuples Types - -::: dirty_equals.IsListOrTuple - -::: dirty_equals.IsList - -::: dirty_equals.IsTuple - -::: dirty_equals.HasLen - -::: dirty_equals.Contains - -## String Types - -::: dirty_equals.IsAnyStr - -::: dirty_equals.IsStr - -::: dirty_equals.IsBytes - -## Other Types - -::: dirty_equals.FunctionCheck - -::: dirty_equals.IsInstance - -::: dirty_equals.IsJson - -::: dirty_equals.IsUUID - -::: dirty_equals.AnyThing - -::: dirty_equals._base.DirtyEquals - rendering: - merge_init_into_class: false diff --git a/docs/types/dict.md b/docs/types/dict.md new file mode 100644 index 0000000..ec30ed7 --- /dev/null +++ b/docs/types/dict.md @@ -0,0 +1,9 @@ +# Dictionary Types + +::: dirty_equals.IsDict + +::: dirty_equals.IsPartialDict + +::: dirty_equals.IsIgnoreDict + +::: dirty_equals.IsStrictDict diff --git a/docs/types/numeric.md b/docs/types/numeric.md new file mode 100644 index 0000000..df71921 --- /dev/null +++ b/docs/types/numeric.md @@ -0,0 +1,51 @@ +# Numeric Types + +::: dirty_equals.IsInt + rendering: + merge_init_into_class: false + separate_signature: false + +::: dirty_equals.IsFloat + rendering: + merge_init_into_class: false + separate_signature: false + +::: dirty_equals.IsPositive + rendering: + merge_init_into_class: false + +::: dirty_equals.IsNegative + rendering: + merge_init_into_class: false + +::: dirty_equals.IsNonNegative + rendering: + merge_init_into_class: false + +::: dirty_equals.IsNonPositive + rendering: + merge_init_into_class: false + +::: dirty_equals.IsPositiveInt + rendering: + merge_init_into_class: false + +::: dirty_equals.IsNegativeInt + rendering: + merge_init_into_class: false + +::: dirty_equals.IsPositiveFloat + rendering: + merge_init_into_class: false + +::: dirty_equals.IsNegativeFloat + rendering: + merge_init_into_class: false + +::: dirty_equals.IsApprox + +::: dirty_equals.IsNumber + rendering: + merge_init_into_class: false + +::: dirty_equals.IsNumeric diff --git a/docs/types/other.md b/docs/types/other.md new file mode 100644 index 0000000..0a783c8 --- /dev/null +++ b/docs/types/other.md @@ -0,0 +1,13 @@ +# Other Types + +::: dirty_equals.FunctionCheck + +::: dirty_equals.IsInstance + +::: dirty_equals.IsJson + +::: dirty_equals.IsUUID + +::: dirty_equals.AnyThing + +::: dirty_equals.IsOneOf diff --git a/docs/types/sequence.md b/docs/types/sequence.md new file mode 100644 index 0000000..895c08e --- /dev/null +++ b/docs/types/sequence.md @@ -0,0 +1,11 @@ +# Sequence Types + +::: dirty_equals.IsListOrTuple + +::: dirty_equals.IsList + +::: dirty_equals.IsTuple + +::: dirty_equals.HasLen + +::: dirty_equals.Contains diff --git a/docs/types/string.md b/docs/types/string.md new file mode 100644 index 0000000..80ba27e --- /dev/null +++ b/docs/types/string.md @@ -0,0 +1,7 @@ +# String Types + +::: dirty_equals.IsAnyStr + +::: dirty_equals.IsStr + +::: dirty_equals.IsBytes diff --git a/docs/usage.md b/docs/usage.md index 3e8c025..4506d63 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -29,7 +29,8 @@ This saves users adding `()` in lots of places. Example: -```py title="Initialised vs. Uninitialised" +```py +title="Initialised vs. Uninitialised" from dirty_equals import IsInt # these two cases are the same @@ -37,6 +38,10 @@ assert 1 == IsInt assert 1 == IsInt() ``` +!!! Note + Types that require at least on argument when being initialised (like [`IsApprox`][dirty_equals.IsApprox]) + cannot be used like this, comparisons will just return `False`. + ## `__repr__` and pytest compatibility dirty-equals types have reasonable `__repr__` methods, which describe types and generally are a close match diff --git a/mkdocs.yml b/mkdocs.yml index 8290a3b..0769342 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -34,12 +34,18 @@ edit_uri: '' nav: - Introduction: index.md - Usage: usage.md - - Types: types.md + - Types: + - types/numeric.md + - types/datetime.md + - types/dict.md + - types/sequence.md + - types/string.md + - types/other.md + - types/custom.md markdown_extensions: - toc: permalink: true - toc_depth: 3 - admonition - pymdownx.details - pymdownx.superfences @@ -72,7 +78,7 @@ plugins: show_root_heading: true show_root_full_path: false show_source: false - heading_level: 3 + heading_level: 2 merge_init_into_class: true show_signature_annotations: true separate_signature: true diff --git a/tests/test_base.py b/tests/test_base.py index 588e525..29bcf93 100644 --- a/tests/test_base.py +++ b/tests/test_base.py @@ -1,6 +1,6 @@ import pytest -from dirty_equals import IsApprox, IsInstance, IsInt, IsNegative, IsPositive, IsStr +from dirty_equals import Contains, IsApprox, IsInstance, IsInt, IsNegative, IsOneOf, IsPositive, IsStr def test_or(): @@ -120,6 +120,7 @@ def test_repr(): (IsInt() & IsPositive, 'IsInt() & IsPositive'), (IsInt() | IsPositive, 'IsInt() | IsPositive'), (IsPositive & IsInt(lt=5), 'IsPositive & IsInt(lt=5)'), + (IsOneOf(1, 2, 3), 'IsOneOf(1, 2, 3)'), ], ) def test_repr_class(v, v_repr): @@ -137,3 +138,17 @@ def test_ne_repr(): assert 'x' != v assert repr(v) == 'IsInt' + + +@pytest.mark.parametrize( + 'value,dirty', + [ + (1, IsOneOf(1, 2, 3)), + (4, ~IsOneOf(1, 2, 3)), + ([1, 2, 3], Contains(1) | IsOneOf([])), + ([], Contains(1) | IsOneOf([])), + ([2], ~(Contains(1) | IsOneOf([]))), + ], +) +def test_is_one_of(value, dirty): + assert value == dirty