From 1ebeea0b69d28daf06877df55b1092e6f5b5b581 Mon Sep 17 00:00:00 2001 From: Matt Bogosian Date: Sun, 14 Nov 2021 13:16:40 -0600 Subject: [PATCH] Split ``SupportsRealImag`` into ``SupportsRealImagProperties`` and ``SupportsRealImagAsMethod`` Now ``sympy.core.numbers`` primitives are adequately supported (excepting general false positives; see #5). Fixes #4. --- README.md | 20 ++- docs/notes.md | 2 + docs/numerary.types.md | 10 +- docs/perf_rational_big_protocol.ipy | 4 +- docs/perf_rational_big_protocol.out | 6 +- numerary/types.py | 228 +++++++++++++++++++----- tests/test_real_like.py | 4 +- tests/test_supports_complex_ops_pow.py | 4 +- tests/test_supports_conjugate.py | 28 +-- tests/test_supports_divmod.py | 50 +++--- tests/test_supports_floor_ceil.py | 4 +- tests/test_supports_integral_ops_pow.py | 4 +- tests/test_supports_real_imag.py | 201 +++++++++++++-------- tests/test_supports_real_ops.py | 4 +- tests/test_supports_trunc.py | 4 +- 15 files changed, 401 insertions(+), 172 deletions(-) diff --git a/README.md b/README.md index f00b6c7..a735120 100644 --- a/README.md +++ b/README.md @@ -368,8 +368,24 @@ True True >>> hasattr(pants_on_fire, "real") or hasattr(pants_on_fire, "imag") # somebody's tellin' stories False ->>> from numerary.types import SupportsRealImag ->>> real_imag: SupportsRealImag = pants_on_fire # fails to detect the lie +>>> from numerary.types import SupportsRealImagProperties +>>> real_imag: SupportsRealImagProperties = pants_on_fire # fails to detect the lie +>>> real_imag.real +Traceback (most recent call last): + ... +AttributeError: 'One' object has no attribute 'real' + +``` + +In this particular case, ``numerary`` provides us with a defensive mechanism. + +``` python +>>> from numerary.types import SupportsRealImagMixedU, real, imag +>>> real_imag_defense: SupportsRealImagMixedU = pants_on_fire +>>> real(real_imag_defense) +1 +>>> imag(real_imag) +0 ``` diff --git a/docs/notes.md b/docs/notes.md index c17d1cc..4fafa9c 100644 --- a/docs/notes.md +++ b/docs/notes.md @@ -17,6 +17,8 @@ ## [0.1.2](https://github.com/posita/numerary/releases/tag/v0.1.2) +* Splits ``SupportsRealImag`` into [``SupportsRealImagProperties``][numerary.types.SupportsRealImagProperties] and [``SupportsRealImagAsMethod``][numerary.types.SupportsRealImagAsMethod] and provides the [``real``][numerary.types.real] and [``imag``][numerary.types.imag] helper functions for better support of ``sympy.core.numbers`` primitives. + ## [0.1.1](https://github.com/posita/numerary/releases/tag/v0.1.1) * Removes obsoleted ``…SCT`` aliases. diff --git a/docs/numerary.types.md b/docs/numerary.types.md index 3310069..df1c2ae 100644 --- a/docs/numerary.types.md +++ b/docs/numerary.types.md @@ -66,7 +66,8 @@ from numerary.bt import beartype # will resolve to the identity decorator if be - "SupportsIndex" - "SupportsRound" - "SupportsConjugate" - - "SupportsRealImag" + - "SupportsRealImagProperties" + - "SupportsRealImagAsMethod" - "SupportsTrunc" - "SupportsFloorCeil" - "SupportsDivmod" @@ -77,6 +78,8 @@ from numerary.bt import beartype # will resolve to the identity decorator if be - "SupportsRealOps" - "SupportsIntegralOps" - "SupportsIntegralPow" + - "real" + - "imag" - "trunc" - "floor" - "ceil" @@ -102,7 +105,10 @@ from numerary.bt import beartype # will resolve to the identity decorator if be - "_SupportsIndex" - "_SupportsRound" - "_SupportsConjugate" - - "_SupportsRealImag" + - "_SupportsRealImagProperties" + - "_SupportsRealImagAsMethod" + - "SupportsRealImagMixedT" + - "SupportsRealImagMixedU" - "_SupportsTrunc" - "_SupportsFloorCeil" - "_SupportsDivmod" diff --git a/docs/perf_rational_big_protocol.ipy b/docs/perf_rational_big_protocol.ipy index 85b7059..bd1b1d0 100644 --- a/docs/perf_rational_big_protocol.ipy +++ b/docs/perf_rational_big_protocol.ipy @@ -5,7 +5,7 @@ from numerary.types import ( # "raw" (non-caching) versions _SupportsConjugate, _SupportsFloorCeil, _SupportsDivmod, - _SupportsRealImag, + _SupportsRealImagProperties, _SupportsRealOps, _SupportsTrunc, ) @@ -33,7 +33,7 @@ class SupportsLotsOfNumberStuff( _SupportsTrunc, _SupportsFloorCeil, _SupportsConjugate, - _SupportsRealImag, + _SupportsRealImagProperties, SupportsAbs, SupportsFloat, SupportsComplex, diff --git a/docs/perf_rational_big_protocol.out b/docs/perf_rational_big_protocol.out index 0c5c77e..c6fc668 100644 --- a/docs/perf_rational_big_protocol.out +++ b/docs/perf_rational_big_protocol.out @@ -1,6 +1,6 @@ %timeit isinstance(builtins.int(1), SupportsLotsOfNumberStuff) -132 µs ± 1.15 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) +135 µs ± 1.02 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) %timeit isinstance(fractions.Fraction(2), SupportsLotsOfNumberStuff) -139 µs ± 2.01 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) +146 µs ± 14.4 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) %timeit isinstance(builtins.float(3.0), SupportsLotsOfNumberStuff) -131 µs ± 1.13 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) +134 µs ± 1.84 µs per loop (mean ± std. dev. of 7 runs, 10000 loops each) diff --git a/numerary/types.py b/numerary/types.py index 2a94dc7..301b48e 100644 --- a/numerary/types.py +++ b/numerary/types.py @@ -520,10 +520,10 @@ class SupportsConjugate( @runtime_checkable -class _SupportsRealImag(Protocol): +class _SupportsRealImagProperties(Protocol): r""" The raw, non-caching version of - [``SupportsRealImag``][numerary.types.SupportsRealImag]. + [``SupportsRealImagProperties``][numerary.types.SupportsRealImagProperties]. """ __slots__: Union[str, Iterable[str]] = () @@ -537,8 +537,8 @@ def imag(self) -> Any: @runtime_checkable -class SupportsRealImag( - _SupportsRealImag, +class SupportsRealImagProperties( + _SupportsRealImagProperties, Protocol, metaclass=CachingProtocolMeta, ): @@ -548,17 +548,17 @@ class SupportsRealImag( [``imag``](https://docs.python.org/3/library/numbers.html#numbers.Complex.imag) properties. - ([``_SupportsRealImag``][numerary.types._SupportsRealImag] is the raw, non-caching - version that defines the actual methods.) + ([``_SupportsRealImagProperties``][numerary.types._SupportsRealImagProperties] is + the raw, non-caching version that defines the actual methods.) ``` python >>> from typing import Any, Tuple, TypeVar - >>> from numerary.types import SupportsRealImag - >>> MyRealImagT = TypeVar("MyRealImagT", bound=SupportsRealImag) + >>> from numerary.types import SupportsRealImagProperties, real, imag + >>> MyRealImagPropertiesT = TypeVar("MyRealImagPropertiesT", bound=SupportsRealImagProperties) - >>> def real_imag_my_thing(arg: MyRealImagT) -> Tuple[Any, Any]: - ... assert isinstance(arg, SupportsRealImag) - ... return (arg.real, arg.imag) + >>> def real_imag_my_thing(arg: MyRealImagPropertiesT) -> Tuple[Any, Any]: + ... assert isinstance(arg, SupportsRealImagProperties) + ... return (real(arg), imag(arg)) >>> real_imag_my_thing(3) (3, 0) @@ -567,7 +567,7 @@ class SupportsRealImag( >>> real_imag_my_thing(Decimal(2.5)) (Decimal('2.5'), Decimal('0')) - >>> # error: Value of type variable "MyRealImagT" of "real_imag_my_thing" cannot be "str" + >>> # error: Value of type variable "MyRealImagPropertiesT" of "real_imag_my_thing" cannot be "str" >>> real_imag_my_thing("not-a-number") # type: ignore [type-var] Traceback (most recent call last): ... @@ -578,8 +578,88 @@ class SupportsRealImag( __slots__: Union[str, Iterable[str]] = () -_assert_isinstance(int, float, bool, Decimal, Fraction, target_t=SupportsRealImag) -SupportsRealImagSCU = Union[int, float, bool, Complex, SupportsRealImag] +_assert_isinstance( + int, float, bool, Decimal, Fraction, target_t=SupportsRealImagProperties +) +SupportsRealImagPropertiesSCU = Union[ + int, float, bool, Complex, SupportsRealImagProperties +] + + +@runtime_checkable +class _SupportsRealImagAsMethod(Protocol): + r""" + The raw, non-caching version of + [``SupportsRealImagAsMethod``][numerary.types.SupportsRealImagAsMethod]. + """ + __slots__: Union[str, Iterable[str]] = () + + @abstractmethod + def as_real_imag(self) -> Tuple[Any, Any]: + pass + + +@runtime_checkable +class SupportsRealImagAsMethod( + _SupportsRealImagAsMethod, + Protocol, + metaclass=CachingProtocolMeta, +): + r""" + A caching ABC defining the ``#!python as_real_imag`` method that returns a 2-tuple. + + ([``_SupportsRealImagAsMethod``][numerary.types._SupportsRealImagAsMethod] + is the raw, non-caching version that defines the actual methods.) + + See also the [``real``][numerary.types.real] and [``imag``][numerary.types.imag] + helper functions. + + ``` python + >>> from typing import Any, Tuple, TypeVar + >>> from numerary.types import SupportsRealImagAsMethod, real, imag + >>> MyRealImagAsMethodT = TypeVar("MyRealImagAsMethodT", bound=SupportsRealImagAsMethod) + + >>> def as_real_imag_my_thing(arg: MyRealImagAsMethodT) -> Tuple[Any, Any]: + ... assert isinstance(arg, SupportsRealImagAsMethod) + ... return (real(arg), imag(arg)) + + >>> as_real_imag_my_thing(sympy.core.numbers.Float(3.5)) + (3.5, 0) + >>> tuple(type(i) for i in _) + (, ) + + >>> # error: Value of type variable "MyRealImagAsMethodT" of "as_real_imag_my_thing" cannot be "str" + >>> as_real_imag_my_thing("not-a-number") # type: ignore [type-var] + Traceback (most recent call last): + ... + AssertionError + + ``` + """ + __slots__: Union[str, Iterable[str]] = () + + +# See +SupportsRealImagMixedU = Union[ + SupportsRealImagProperties, + SupportsRealImagAsMethod, +] +fr""" +{SupportsRealImagMixedU!r} +""" +SupportsRealImagMixedT = ( + SupportsRealImagProperties, + SupportsRealImagAsMethod, +) +fr""" +{SupportsRealImagMixedT!r} +""" +assert SupportsRealImagMixedU.__args__ == SupportsRealImagMixedT # type: ignore [attr-defined] + +SupportsRealImagMixedSCU = Union[ + SupportsRealImagPropertiesSCU, + SupportsRealImagAsMethod, +] @runtime_checkable @@ -907,6 +987,9 @@ class SupportsNumeratorDenominatorMethods( ([``_SupportsNumeratorDenominatorMethods``][numerary.types._SupportsNumeratorDenominatorMethods] is the raw, non-caching version that defines the actual methods.) + + See also the [``numerator``][numerary.types.numerator] and + [``denominator``][numerary.types.denominator] helper functions. """ __slots__: Union[str, Iterable[str]] = () @@ -1406,7 +1489,7 @@ class RealLike( * [``SupportsComplex``][numerary.types.SupportsComplex] * [``SupportsConjugate``][numerary.types.SupportsConjugate] - * [``SupportsRealImag``][numerary.types.SupportsRealImag] + * [``SupportsRealImagProperties``][numerary.types.SupportsRealImagProperties] * [``SupportsRound``][numerary.types.SupportsRound] * [``SupportsTrunc``][numerary.types.SupportsTrunc] * [``SupportsFloorCeil``][numerary.types.SupportsFloorCeil] @@ -1475,7 +1558,7 @@ class RationalLikeProperties( * [``SupportsComplex``][numerary.types.SupportsComplex] * [``SupportsConjugate``][numerary.types.SupportsConjugate] - * [``SupportsRealImag``][numerary.types.SupportsRealImag] + * [``SupportsRealImagProperties``][numerary.types.SupportsRealImagProperties] * [``SupportsRound``][numerary.types.SupportsRound] * [``SupportsTrunc``][numerary.types.SupportsTrunc] * [``SupportsFloorCeil``][numerary.types.SupportsFloorCeil] @@ -1545,6 +1628,9 @@ class RationalLikeMethods( This is probably not very useful on its own, but is important to the construction of [``RationalLikeMixedU``][numerary.types.RationalLikeMixedU] and [``RationalLikeMixedT``][numerary.types.RationalLikeMixedT]. + + See also the [``numerator``][numerary.types.numerator] and + [``denominator``][numerary.types.denominator] helper functions. """ __slots__: Union[str, Iterable[str]] = () @@ -1636,7 +1722,7 @@ class IntegralLike( * [``SupportsComplex``][numerary.types.SupportsComplex] * [``SupportsConjugate``][numerary.types.SupportsConjugate] - * [``SupportsRealImag``][numerary.types.SupportsRealImag] + * [``SupportsRealImagProperties``][numerary.types.SupportsRealImagProperties] * [``SupportsRound``][numerary.types.SupportsRound] * [``SupportsTrunc``][numerary.types.SupportsTrunc] * [``SupportsFloorCeil``][numerary.types.SupportsFloorCeil] @@ -1663,27 +1749,87 @@ def __hash__(self) -> int: @beartype -def ceil(operand: Union[SupportsFloat, SupportsFloorCeil]): +def real(operand: SupportsRealImagMixedU): r""" - Helper function that wraps ``math.ceil``. + Helper function that extracts the real part from *operand* including resolving + non-compliant implementations that implement such extraction via a ``as_real_imag`` + method rather than as properties. ``` python - >>> from numerary.types import SupportsFloat, SupportsFloorCeil, ceil - >>> my_ceil: SupportsFloorCeil - >>> my_ceil = 1 - >>> ceil(my_ceil) + >>> import sympy + >>> from numerary.types import real + >>> real(sympy.core.numbers.Float(3.5)) + 3.5 + + ``` + + See + [SupportsRealImagProperties][numerary.types.SupportsRealImagProperties] + and + [SupportsRealImagAsMethod][numerary.types.SupportsRealImagAsMethod]. + """ + if callable(getattr(operand, "as_real_imag", None)): + real_part, _ = operand.as_real_imag() # type: ignore [union-attr] + + return real_part + elif hasattr(operand, "real"): + return operand.real # type: ignore [union-attr] + else: + raise TypeError(f"{operand!r} has no real or as_real_imag") + + +@beartype +def imag(operand: SupportsRealImagMixedU): + r""" + Helper function that extracts the imaginary part from *operand* including resolving + non-compliant implementations that implement such extraction via a ``as_real_imag`` + method rather than as properties. + + ``` python + >>> import sympy + >>> from numerary.types import real + >>> imag(sympy.core.numbers.Float(3.5)) + 0 + + ``` + + See + [SupportsRealImagProperties][numerary.types.SupportsRealImagProperties] + and + [SupportsRealImagAsMethod][numerary.types.SupportsRealImagAsMethod]. + """ + if callable(getattr(operand, "as_real_imag", None)): + _, imag_part = operand.as_real_imag() # type: ignore [union-attr] + + return imag_part + elif hasattr(operand, "imag"): + return operand.imag # type: ignore [union-attr] + else: + raise TypeError(f"{operand!r} has no real or as_real_imag") + + +@beartype +def trunc(operand: Union[SupportsFloat, SupportsTrunc]): + r""" + Helper function that wraps ``math.trunc``. + + ``` python + >>> from numerary.types import SupportsFloat, SupportsTrunc, trunc + >>> my_trunc: SupportsTrunc + >>> my_trunc = 1 + >>> trunc(my_trunc) 1 >>> from fractions import Fraction - >>> my_ceil = Fraction(1, 2) - >>> ceil(my_ceil) + >>> my_trunc = Fraction(1, 2) + >>> trunc(my_trunc) + 0 + >>> my_trunc_float: SupportsFloat = 1.2 + >>> trunc(my_trunc_float) 1 - >>> my_ceil_float: SupportsFloat = 1.2 - >>> ceil(my_ceil_float) - 2 ``` """ - return math.ceil(operand) # type: ignore [arg-type] + return math.trunc(operand) # type: ignore [arg-type] @beartype @@ -1711,27 +1857,27 @@ def floor(operand: Union[SupportsFloat, SupportsFloorCeil]): @beartype -def trunc(operand: Union[SupportsFloat, SupportsTrunc]): +def ceil(operand: Union[SupportsFloat, SupportsFloorCeil]): r""" - Helper function that wraps ``math.trunc``. + Helper function that wraps ``math.ceil``. ``` python - >>> from numerary.types import SupportsFloat, SupportsTrunc, trunc - >>> my_trunc: SupportsTrunc - >>> my_trunc = 1 - >>> trunc(my_trunc) + >>> from numerary.types import SupportsFloat, SupportsFloorCeil, ceil + >>> my_ceil: SupportsFloorCeil + >>> my_ceil = 1 + >>> ceil(my_ceil) 1 >>> from fractions import Fraction - >>> my_trunc = Fraction(1, 2) - >>> trunc(my_trunc) - 0 - >>> my_trunc_float: SupportsFloat = 1.2 - >>> trunc(my_trunc_float) + >>> my_ceil = Fraction(1, 2) + >>> ceil(my_ceil) 1 + >>> my_ceil_float: SupportsFloat = 1.2 + >>> ceil(my_ceil_float) + 2 ``` """ - return math.trunc(operand) # type: ignore [arg-type] + return math.ceil(operand) # type: ignore [arg-type] @beartype diff --git a/tests/test_real_like.py b/tests/test_real_like.py index b0d59a2..d9bd7f5 100644 --- a/tests/test_real_like.py +++ b/tests/test_real_like.py @@ -233,7 +233,7 @@ def test_real_like_numpy_beartype() -> None: def test_real_like_sympy() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") integer_val: RealLike = sympy.Integer(-273) rational_val: RealLike = sympy.Rational(-27315, 100) float_val: RealLike = sympy.Float(-273.15) @@ -270,7 +270,7 @@ def test_real_like_sympy() -> None: def test_real_like_sympy_beartype() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") pytest.importorskip("beartype.roar", reason="requires beartype") for good_val in ( diff --git a/tests/test_supports_complex_ops_pow.py b/tests/test_supports_complex_ops_pow.py index 7602c8a..a8f10e8 100644 --- a/tests/test_supports_complex_ops_pow.py +++ b/tests/test_supports_complex_ops_pow.py @@ -266,7 +266,7 @@ def test_supports_complex_ops_pow_numpy_beartype() -> None: def test_supports_complex_ops_pow_sympy() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") integer_val: SupportsComplexOps = sympy.Integer(-273) rational_val: SupportsComplexOps = sympy.Rational(-27315, 100) float_val: SupportsComplexOps = sympy.Float(-273.15) @@ -295,7 +295,7 @@ def test_supports_complex_ops_pow_sympy() -> None: def test_supports_complex_ops_pow_sympy_beartype() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") pytest.importorskip("beartype.roar", reason="requires beartype") for good_val in ( diff --git a/tests/test_supports_conjugate.py b/tests/test_supports_conjugate.py index e3fdcb3..642ada3 100644 --- a/tests/test_supports_conjugate.py +++ b/tests/test_supports_conjugate.py @@ -112,18 +112,6 @@ def test_supports_conjugate_beartype() -> None: supports_conjugate_func(cast(SupportsConjugate, good_val)) supports_conjugate_func_t(cast(SupportsConjugateSCU, good_val)) - for lying_val in ( - # These have lied about supporting this interface when they registered - # themselves in the number tower - NumberwangRegistered(-273), - WangernumbRegistered(-273.15), - ): - with pytest.raises(roar.BeartypeException): - supports_conjugate_func(cast(SupportsConjugate, lying_val)) - - with pytest.raises(AssertionError): # gets past beartype - supports_conjugate_func_t(cast(SupportsConjugateSCU, lying_val)) - for bad_val in ( TestFlag.B, Numberwang(-273), @@ -136,6 +124,18 @@ def test_supports_conjugate_beartype() -> None: with pytest.raises(roar.BeartypeException): supports_conjugate_func_t(cast(SupportsConjugateSCU, bad_val)) + for lying_val in ( + # These have lied about supporting this interface when they registered + # themselves in the number tower + NumberwangRegistered(-273), + WangernumbRegistered(-273.15), + ): + with pytest.raises(roar.BeartypeException): + supports_conjugate_func(cast(SupportsConjugate, lying_val)) + + with pytest.raises(AssertionError): # gets past beartype + supports_conjugate_func_t(cast(SupportsConjugateSCU, lying_val)) + def test_supports_conjugate_numpy() -> None: numpy = pytest.importorskip("numpy", reason="requires numpy") @@ -202,7 +202,7 @@ def test_supports_conjugate_numpy_beartype() -> None: def test_supports_conjugate_sympy() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") integer_val: SupportsConjugate = sympy.Integer(-273) rational_val: SupportsConjugate = sympy.Rational(-27315, 100) float_val: SupportsConjugate = sympy.Float(-273.15) @@ -219,7 +219,7 @@ def test_supports_conjugate_sympy() -> None: def test_supports_conjugate_sympy_beartype() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") pytest.importorskip("beartype.roar", reason="requires beartype") for good_val in ( diff --git a/tests/test_supports_divmod.py b/tests/test_supports_divmod.py index d57feca..f11fb84 100644 --- a/tests/test_supports_divmod.py +++ b/tests/test_supports_divmod.py @@ -74,17 +74,6 @@ def test_supports_divmod() -> None: assert isinstance(good_val, SupportsDivmod), f"{good_val!r}" assert divmod(good_val, good_val), f"{good_val!r}" - nwr_bad_val: SupportsDivmod = NumberwangRegistered(-273) # type: ignore [assignment] - wnr_bad_val: SupportsDivmod = WangernumbRegistered(-273.15) # type: ignore [assignment] - - for lying_val in ( - # These have lied about supporting this interface when they registered - # themselves in the number tower - nwr_bad_val, - wnr_bad_val, - ): - assert not isinstance(lying_val, SupportsDivmod), f"{lying_val!r}" - complex_bad_val: SupportsDivmod = complex(-273.15) # type: ignore [assignment] test_flag_bad_val: SupportsDivmod = TestFlag.B # type: ignore [assignment] nw_bad_val: SupportsDivmod = Numberwang(-273) # type: ignore [assignment] @@ -99,6 +88,17 @@ def test_supports_divmod() -> None: ): assert not isinstance(bad_val, SupportsDivmod), f"{bad_val!r}" + nwr_bad_val: SupportsDivmod = NumberwangRegistered(-273) # type: ignore [assignment] + wnr_bad_val: SupportsDivmod = WangernumbRegistered(-273.15) # type: ignore [assignment] + + for lying_val in ( + # These have lied about supporting this interface when they registered + # themselves in the number tower + nwr_bad_val, + wnr_bad_val, + ): + assert not isinstance(lying_val, SupportsDivmod), f"{lying_val!r}" + def test_supports_divmod_beartype() -> None: roar = pytest.importorskip("beartype.roar", reason="requires beartype") @@ -118,18 +118,6 @@ def test_supports_divmod_beartype() -> None: supports_divmod_func(cast(SupportsDivmod, good_val)) supports_divmod_func_t(cast(SupportsDivmodSCU, good_val)) - for lying_val in ( - # These have lied about supporting this interface when they registered - # themselves in the number tower - NumberwangRegistered(-273), - WangernumbRegistered(-273.15), - ): - with pytest.raises(roar.BeartypeException): - supports_divmod_func(cast(SupportsDivmod, lying_val)) - - with pytest.raises(AssertionError): # gets past beartype - supports_divmod_func_t(cast(SupportsDivmodSCU, lying_val)) - for bad_val in ( complex(-273.15), TestFlag.B, @@ -143,6 +131,18 @@ def test_supports_divmod_beartype() -> None: with pytest.raises(roar.BeartypeException): supports_divmod_func_t(cast(SupportsDivmodSCU, bad_val)) + for lying_val in ( + # These have lied about supporting this interface when they registered + # themselves in the number tower + NumberwangRegistered(-273), + WangernumbRegistered(-273.15), + ): + with pytest.raises(roar.BeartypeException): + supports_divmod_func(cast(SupportsDivmod, lying_val)) + + with pytest.raises(AssertionError): # gets past beartype + supports_divmod_func_t(cast(SupportsDivmodSCU, lying_val)) + def test_supports_divmod_numpy() -> None: numpy = pytest.importorskip("numpy", reason="requires numpy") @@ -223,7 +223,7 @@ def test_supports_divmod_numpy_beartype() -> None: def test_supports_divmod_sympy() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") integer_val: SupportsDivmod = sympy.Integer(-273) rational_val: SupportsDivmod = sympy.Rational(-27315, 100) float_val: SupportsDivmod = sympy.Float(-273.15) @@ -240,7 +240,7 @@ def test_supports_divmod_sympy() -> None: def test_supports_divmod_sympy_beartype() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") pytest.importorskip("beartype.roar", reason="requires beartype") for good_val in ( diff --git a/tests/test_supports_floor_ceil.py b/tests/test_supports_floor_ceil.py index 21f861e..3861773 100644 --- a/tests/test_supports_floor_ceil.py +++ b/tests/test_supports_floor_ceil.py @@ -215,7 +215,7 @@ def test_floor_ceil_numpy_beartype() -> None: def test_floor_ceil_sympy() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") integer_val: SupportsFloorCeil = sympy.Integer(-273) rational_val: SupportsFloorCeil = sympy.Rational(-27315, 100) float_val: SupportsFloorCeil = sympy.Float(-273.15) @@ -237,7 +237,7 @@ def test_floor_ceil_sympy() -> None: def test_floor_ceil_sympy_beartype() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") roar = pytest.importorskip("beartype.roar", reason="requires beartype") for good_val in ( diff --git a/tests/test_supports_integral_ops_pow.py b/tests/test_supports_integral_ops_pow.py index 9a229ab..41220dd 100644 --- a/tests/test_supports_integral_ops_pow.py +++ b/tests/test_supports_integral_ops_pow.py @@ -290,7 +290,7 @@ def test_supports_integral_ops_pow_numpy_beartype() -> None: def test_supports_integral_ops_pow_sympy() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") integral_val: SupportsIntegralOps = sympy.Integer(-273) _: SupportsIntegralPow _ = sympy.Integer(-273) @@ -327,7 +327,7 @@ def test_supports_integral_ops_pow_sympy() -> None: def test_supports_integral_ops_pow_sympy_beartype() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") roar = pytest.importorskip("beartype.roar", reason="requires beartype") for good_val in (sympy.Integer(-273),): diff --git a/tests/test_supports_real_imag.py b/tests/test_supports_real_imag.py index bf2ab9a..7dd1e2c 100644 --- a/tests/test_supports_real_imag.py +++ b/tests/test_supports_real_imag.py @@ -15,7 +15,16 @@ import pytest from numerary.bt import beartype -from numerary.types import SupportsRealImag, SupportsRealImagSCU +from numerary.types import ( + SupportsRealImagAsMethod, + SupportsRealImagMixedSCU, + SupportsRealImagMixedT, + SupportsRealImagMixedU, + SupportsRealImagProperties, + SupportsRealImagPropertiesSCU, + imag, + real, +) from .numberwang import ( Numberwang, @@ -36,29 +45,39 @@ @beartype -def supports_real_imag_func(arg: SupportsRealImag): - assert isinstance(arg, SupportsRealImag), f"{arg!r}" +def supports_real_imag_func(arg: SupportsRealImagMixedU): + assert isinstance(arg, SupportsRealImagMixedT), f"{arg!r}" + + +@beartype +def supports_real_imag_func_t(arg: SupportsRealImagMixedSCU): + assert isinstance(arg, SupportsRealImagMixedT), f"{arg!r}" + + +@beartype +def supports_real_imag_properties_func(arg: SupportsRealImagProperties): + assert isinstance(arg, SupportsRealImagProperties), f"{arg!r}" @beartype -def supports_real_imag_func_t(arg: SupportsRealImagSCU): - assert isinstance(arg, SupportsRealImag), f"{arg!r}" +def supports_real_imag_properties_func_t(arg: SupportsRealImagPropertiesSCU): + assert isinstance(arg, SupportsRealImagProperties), f"{arg!r}" # ---- Tests --------------------------------------------------------------------------- def test_supports_real_imag() -> None: - bool_val: SupportsRealImag = True - int_val: SupportsRealImag = -273 - float_val: SupportsRealImag = -273.15 - frac_val: SupportsRealImag = Fraction(-27315, 100) - dec_val: SupportsRealImag = Decimal("-273.15") - test_int_enum: SupportsRealImag = TestIntEnum.ZERO - test_int_flag: SupportsRealImag = TestIntFlag.B + bool_val: SupportsRealImagProperties = True + int_val: SupportsRealImagProperties = -273 + float_val: SupportsRealImagProperties = -273.15 + frac_val: SupportsRealImagProperties = Fraction(-27315, 100) + dec_val: SupportsRealImagProperties = Decimal("-273.15") + test_int_enum: SupportsRealImagProperties = TestIntEnum.ZERO + test_int_flag: SupportsRealImagProperties = TestIntFlag.B # These have inherited this interface by deriving from number tower ABCs - nwd_val: SupportsRealImag = NumberwangDerived(-273) - wnd_val: SupportsRealImag = WangernumbDerived(-273.15) + nwd_val: SupportsRealImagProperties = NumberwangDerived(-273) + wnd_val: SupportsRealImagProperties = WangernumbDerived(-273.15) for good_val in ( bool_val, @@ -71,15 +90,15 @@ def test_supports_real_imag() -> None: nwd_val, wnd_val, ): - assert isinstance(good_val, SupportsRealImag), f"{good_val!r}" - assert hasattr(good_val, "real"), f"{good_val!r}" - assert hasattr(good_val, "imag"), f"{good_val!r}" + assert isinstance(good_val, SupportsRealImagMixedT), f"{good_val!r}" + assert real(good_val) is not None, f"{good_val!r}" + assert imag(good_val) is not None, f"{good_val!r}" - test_flag_bad_val: SupportsRealImag = TestFlag.B # type: ignore [assignment] - nw_bad_val: SupportsRealImag = Numberwang(-273) # type: ignore [assignment] - nwr_bad_val: SupportsRealImag = NumberwangRegistered(-273) # type: ignore [assignment] - wn_bad_val: SupportsRealImag = Wangernumb(-273.15) # type: ignore [assignment] - wnr_bad_val: SupportsRealImag = WangernumbRegistered(-273.15) # type: ignore [assignment] + test_flag_bad_val: SupportsRealImagMixedU = TestFlag.B # type: ignore [assignment] + nw_bad_val: SupportsRealImagMixedU = Numberwang(-273) # type: ignore [assignment] + nwr_bad_val: SupportsRealImagMixedU = NumberwangRegistered(-273) # type: ignore [assignment] + wn_bad_val: SupportsRealImagMixedU = Wangernumb(-273.15) # type: ignore [assignment] + wnr_bad_val: SupportsRealImagMixedU = WangernumbRegistered(-273.15) # type: ignore [assignment] for bad_val in ( test_flag_bad_val, @@ -89,7 +108,7 @@ def test_supports_real_imag() -> None: wnr_bad_val, "-273.15", ): - assert not isinstance(bad_val, SupportsRealImag), f"{bad_val!r}" + assert not isinstance(bad_val, SupportsRealImagMixedT), f"{bad_val!r}" def test_supports_real_imag_beartype() -> None: @@ -108,20 +127,8 @@ def test_supports_real_imag_beartype() -> None: NumberwangDerived(-273), WangernumbDerived(-273.15), ): - supports_real_imag_func(cast(SupportsRealImag, good_val)) - supports_real_imag_func_t(cast(SupportsRealImagSCU, good_val)) - - for lying_val in ( - # These have lied about supporting this interface when they registered - # themselves in the number tower - NumberwangRegistered(-273), - WangernumbRegistered(-273.15), - ): - with pytest.raises(roar.BeartypeException): - supports_real_imag_func(cast(SupportsRealImag, lying_val)) - - with pytest.raises(AssertionError): # gets past beartype - supports_real_imag_func_t(cast(SupportsRealImagSCU, lying_val)) + supports_real_imag_func(cast(SupportsRealImagMixedU, good_val)) + supports_real_imag_func_t(cast(SupportsRealImagMixedSCU, good_val)) for bad_val in ( TestFlag.B, @@ -130,29 +137,41 @@ def test_supports_real_imag_beartype() -> None: "-273.15", ): with pytest.raises(roar.BeartypeException): - supports_real_imag_func(cast(SupportsRealImag, bad_val)) + supports_real_imag_func(cast(SupportsRealImagMixedU, bad_val)) with pytest.raises(roar.BeartypeException): - supports_real_imag_func_t(cast(SupportsRealImagSCU, bad_val)) + supports_real_imag_func_t(cast(SupportsRealImagMixedSCU, bad_val)) + + for lying_val in ( + # These have lied about supporting this interface when they registered + # themselves in the number tower + NumberwangRegistered(-273), + WangernumbRegistered(-273.15), + ): + with pytest.raises(roar.BeartypeException): + supports_real_imag_func(cast(SupportsRealImagMixedU, lying_val)) + + with pytest.raises(AssertionError): # gets past beartype + supports_real_imag_func_t(cast(SupportsRealImagMixedSCU, lying_val)) def test_supports_real_imag_numpy() -> None: numpy = pytest.importorskip("numpy", reason="requires numpy") - uint8_val: SupportsRealImag = numpy.uint8(2) - uint16_val: SupportsRealImag = numpy.uint16(273) - uint32_val: SupportsRealImag = numpy.uint32(273) - uint64_val: SupportsRealImag = numpy.uint64(273) - int8_val: SupportsRealImag = numpy.int8(-2) - int16_val: SupportsRealImag = numpy.int16(-273) - int32_val: SupportsRealImag = numpy.int32(-273) - int64_val: SupportsRealImag = numpy.int64(-273) - float16_val: SupportsRealImag = numpy.float16(-1.8) - float32_val: SupportsRealImag = numpy.float32(-273.15) - float64_val: SupportsRealImag = numpy.float64(-273.15) - float128_val: SupportsRealImag = numpy.float128(-273.15) - csingle_val: SupportsRealImag = numpy.float32(-273.15) - cdouble_val: SupportsRealImag = numpy.float64(-273.15) - clongdouble_val: SupportsRealImag = numpy.float128(-273.15) + uint8_val: SupportsRealImagProperties = numpy.uint8(2) + uint16_val: SupportsRealImagProperties = numpy.uint16(273) + uint32_val: SupportsRealImagProperties = numpy.uint32(273) + uint64_val: SupportsRealImagProperties = numpy.uint64(273) + int8_val: SupportsRealImagProperties = numpy.int8(-2) + int16_val: SupportsRealImagProperties = numpy.int16(-273) + int32_val: SupportsRealImagProperties = numpy.int32(-273) + int64_val: SupportsRealImagProperties = numpy.int64(-273) + float16_val: SupportsRealImagProperties = numpy.float16(-1.8) + float32_val: SupportsRealImagProperties = numpy.float32(-273.15) + float64_val: SupportsRealImagProperties = numpy.float64(-273.15) + float128_val: SupportsRealImagProperties = numpy.float128(-273.15) + csingle_val: SupportsRealImagProperties = numpy.float32(-273.15) + cdouble_val: SupportsRealImagProperties = numpy.float64(-273.15) + clongdouble_val: SupportsRealImagProperties = numpy.float128(-273.15) for good_val in ( uint8_val, @@ -171,9 +190,9 @@ def test_supports_real_imag_numpy() -> None: cdouble_val, clongdouble_val, ): - assert isinstance(good_val, SupportsRealImag), f"{good_val!r}" - assert hasattr(good_val, "real"), f"{good_val!r}" - assert hasattr(good_val, "imag"), f"{good_val!r}" + assert isinstance(good_val, SupportsRealImagMixedT), f"{good_val!r}" + assert real(good_val) is not None, f"{good_val!r}" + assert imag(good_val) is not None, f"{good_val!r}" def test_supports_real_imag_numpy_beartype() -> None: @@ -197,17 +216,49 @@ def test_supports_real_imag_numpy_beartype() -> None: numpy.cdouble(-273.15), numpy.clongdouble(-273.15), ): - supports_real_imag_func(cast(SupportsRealImag, good_val)) - supports_real_imag_func_t(cast(SupportsRealImagSCU, good_val)) + supports_real_imag_func(cast(SupportsRealImagMixedU, good_val)) + supports_real_imag_func_t(cast(SupportsRealImagMixedSCU, good_val)) def test_supports_real_imag_sympy() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") + integer_val: SupportsRealImagAsMethod = sympy.Integer(-273) + rational_val: SupportsRealImagAsMethod = sympy.Rational(-27315, 100) + float_val: SupportsRealImagAsMethod = sympy.Float(-273.15) + sym_val: SupportsRealImagAsMethod = sympy.symbols("x") + + for good_val in ( + integer_val, + rational_val, + float_val, + sym_val, + ): + assert isinstance(good_val, SupportsRealImagMixedT), f"{good_val!r}" + assert real(good_val) is not None, f"{good_val!r}" + assert imag(good_val) is not None, f"{good_val!r}" + + +def test_supports_real_imag_sympy_beartype() -> None: + sympy = pytest.importorskip("sympy", reason="requires sympy") + pytest.importorskip("beartype.roar", reason="requires beartype") + + for good_val in ( + sympy.Integer(-273), + sympy.Rational(-27315, 100), + sympy.Float(-273.15), + sympy.symbols("x"), + ): + supports_real_imag_func(cast(SupportsRealImagMixedU, good_val)) + supports_real_imag_func_t(cast(SupportsRealImagMixedSCU, good_val)) + + +def test_supports_real_imag_sympy_false_positives() -> None: + sympy = pytest.importorskip("sympy", reason="requires sympy") # TODO(posita): These should not validate - integer_val: SupportsRealImag = sympy.Integer(-273) - rational_val: SupportsRealImag = sympy.Rational(-27315, 100) - float_val: SupportsRealImag = sympy.Float(-273.15) - sym_val: SupportsRealImag = sympy.symbols("x") + integer_val: SupportsRealImagProperties = sympy.Integer(-273) + rational_val: SupportsRealImagProperties = sympy.Rational(-27315, 100) + float_val: SupportsRealImagProperties = sympy.Float(-273.15) + sym_val: SupportsRealImagProperties = sympy.symbols("x") for bad_val in ( integer_val, @@ -215,11 +266,11 @@ def test_supports_real_imag_sympy() -> None: float_val, sym_val, ): - assert not isinstance(bad_val, SupportsRealImag), f"{bad_val!r}" + assert not isinstance(bad_val, SupportsRealImagProperties), f"{bad_val!r}" -def test_supports_real_imag_sympy_beartype() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") +def test_supports_real_imag_sympy_beartype_false_positives() -> None: + sympy = pytest.importorskip("sympy", reason="requires sympy") roar = pytest.importorskip("beartype.roar", reason="requires beartype") for lying_val in ( @@ -230,14 +281,22 @@ def test_supports_real_imag_sympy_beartype() -> None: sympy.Float(-273.15), ): with pytest.raises(roar.BeartypeException): - supports_real_imag_func(cast(SupportsRealImag, lying_val)) + supports_real_imag_properties_func( + cast(SupportsRealImagProperties, lying_val) + ) with pytest.raises(AssertionError): # gets past beartype - supports_real_imag_func_t(cast(SupportsRealImagSCU, lying_val)) + supports_real_imag_properties_func_t( + cast(SupportsRealImagPropertiesSCU, lying_val) + ) for bad_val in (sympy.symbols("x"),): with pytest.raises(roar.BeartypeException): - supports_real_imag_func(cast(SupportsRealImag, bad_val)) + supports_real_imag_properties_func( + cast(SupportsRealImagProperties, bad_val) + ) with pytest.raises(roar.BeartypeException): - supports_real_imag_func_t(cast(SupportsRealImagSCU, bad_val)) + supports_real_imag_properties_func_t( + cast(SupportsRealImagPropertiesSCU, bad_val) + ) diff --git a/tests/test_supports_real_ops.py b/tests/test_supports_real_ops.py index 6a8cf57..6719f72 100644 --- a/tests/test_supports_real_ops.py +++ b/tests/test_supports_real_ops.py @@ -206,7 +206,7 @@ def test_supports_real_ops_numpy_beartype() -> None: def test_supports_real_ops_sympy() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") integer_val: SupportsRealOps = sympy.Integer(-273) rational_val: SupportsRealOps = sympy.Rational(-27315, 100) float_val: SupportsRealOps = sympy.Float(-273.15) @@ -230,7 +230,7 @@ def test_supports_real_ops_sympy() -> None: def test_supports_real_ops_sympy_beartype() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") pytest.importorskip("beartype.roar", reason="requires beartype") for good_val in ( diff --git a/tests/test_supports_trunc.py b/tests/test_supports_trunc.py index ecee698..2dcd7b5 100644 --- a/tests/test_supports_trunc.py +++ b/tests/test_supports_trunc.py @@ -212,7 +212,7 @@ def test_trunc_numpy_beartype() -> None: def test_trunc_sympy() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") integer_val: SupportsTrunc = sympy.Integer(-273) rational_val: SupportsTrunc = sympy.Rational(-27315, 100) float_val: SupportsTrunc = sympy.Float(-273.15) @@ -237,7 +237,7 @@ def test_trunc_sympy() -> None: def test_trunc_sympy_beartype() -> None: - sympy = pytest.importorskip("sympy", reason="requires numpy") + sympy = pytest.importorskip("sympy", reason="requires sympy") pytest.importorskip("beartype.roar", reason="requires beartype") for good_val in (