python-attrs / cattrs

Composable custom class converters for attrs, dataclasses and friends.
https://catt.rs
MIT License
779 stars 108 forks source link

Python 3.13.0b2: 4 tests failuires #547

Open befeleme opened 3 weeks ago

befeleme commented 3 weeks ago

Description

I'm trying to build cattrs as RPM for Fedora Linux 41 with Python 3.13.0b2. 4 tests fail with two types of failures. See traceback below.

What I Did

_________________ test_collection_unstructure_override_mapping _________________
[gw9] linux -- Python 3.13.0 /usr/bin/python3
    @pytest.mark.skipif(not is_py39_plus, reason="Requires Python 3.9+")
    def test_collection_unstructure_override_mapping():
        """Test overriding unstructuring mappings."""

        # Using Counter
        c = Converter(unstruct_collection_overrides={Counter: Map})
        assert c.unstructure(Counter({1: 2})) == Map({1: 2})
>       assert c.unstructure(Counter({1: 2}), unstructure_as=Counter[int]) == Map({1: 2})
E       assert Counter({1: 2}) == immutables.Map({1: 2})
E         Use -v to get more diff
c          = <cattrs.converters.Converter object at 0xffff8413d620>
tests/test_unstructure_collections.py:168: AssertionError
________________________________ test_renaming _________________________________
[gw5] linux -- Python 3.13.0 /usr/bin/python3
    @given(
>       simple_typed_classes(min_attrs=1) | simple_typed_dataclasses(min_attrs=1), data()
    )
f          = <function given.<locals>.run_test_as_given.<locals>.wrapped_test at 0xffff7c5dbba0>
tests/test_gen_dict.py:190: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cl_and_vals = (<class 'tests.typed.HypDataclass'>, (nan,), {}), data = data(...)
    @given(
        simple_typed_classes(min_attrs=1) | simple_typed_dataclasses(min_attrs=1), data()
    )
    def test_renaming(cl_and_vals, data):
        converter = Converter()
        cl, vals, kwargs = cl_and_vals
        attrs = fields(cl)

        to_replace = data.draw(sampled_from(attrs))

        u_fn = make_dict_unstructure_fn(
            cl, converter, **{to_replace.name: override(rename="class")}
        )
        s_fn = make_dict_structure_fn(
            cl, converter, **{to_replace.name: override(rename="class")}
        )

        converter.register_structure_hook(cl, s_fn)
        converter.register_unstructure_hook(cl, u_fn)

        inst = cl(*vals, **kwargs)

        raw = converter.unstructure(inst)

        assert "class" in raw

        new_inst = converter.structure(raw, cl)

>       assert inst == new_inst
E       AssertionError: assert HypDataclass(a=nan) == HypDataclass(a=nan)
E         
E         Differing attributes:
E         ['a']
E         
E         Drill down into differing attribute a:
E           a: nan != nan
E       Falsifying example: test_renaming(
E           cl_and_vals=(tests.typed.HypDataclass, (nan,), {}),  # Saw 1 signaling NaN
E           data=data(...),
E       )
E       Draw 1: Field(name='a',type=<class 'float'>,default=<dataclasses._MISSING_TYPE object at 0xffff7f1c2660>,default_factory=<dataclasses._MISSING_TYPE object at 0xffff7f1c2660>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)
E       Explanation:
E           These lines were always and only run by failing examples:
E               /usr/lib/python3.13/site-packages/_pytest/assertion/util.py:456
attrs      = (Field(name='a',type=<class 'float'>,default=<dataclasses._MISSING_TYPE object at 0xffff7f1c2660>,default_factory=<dat...xffff7f1c2660>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD),)
cl         = <class 'tests.typed.HypDataclass'>
cl_and_vals = (<class 'tests.typed.HypDataclass'>, (nan,), {})
converter  = <cattrs.converters.Converter object at 0xffff72bc2de0>
data       = data(...)
inst       = HypDataclass(a=nan)
kwargs     = {}
new_inst   = HypDataclass(a=nan)
raw        = {'class': nan}
s_fn       = <function structure_HypDataclass at 0xffff72bd7b00>
to_replace = Field(name='a',type=<class 'float'>,default=<dataclasses._MISSING_TYPE object at 0xffff7f1c2660>,default_factory=<data... 0xffff7f1c2660>,init=True,repr=True,hash=None,compare=True,metadata=mappingproxy({}),kw_only=False,_field_type=_FIELD)
u_fn       = <function unstructure_HypDataclass at 0xffff77496f20>
vals       = (nan,)
tests/test_gen_dict.py:217: AssertionError
_________________________ test_simple_roundtrip_tuple __________________________
[gw6] linux -- Python 3.13.0 /usr/bin/python3
    @given(
>       simple_typed_classes(kw_only=False, newtypes=False)
        | simple_typed_dataclasses(newtypes=False),
        booleans(),
    )
f          = <function given.<locals>.run_test_as_given.<locals>.wrapped_test at 0xffff7c35a980>
tests/test_converter.py:54: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cls_and_vals = (<class 'tests.typed.HypDataclass'>, (nan,), {}), dv = False
    @given(
        simple_typed_classes(kw_only=False, newtypes=False)
        | simple_typed_dataclasses(newtypes=False),
        booleans(),
    )
    def test_simple_roundtrip_tuple(cls_and_vals, dv: bool):
        """
        Simple classes with metadata can be unstructured and restructured.
        """
        converter = Converter(
            unstruct_strat=UnstructureStrategy.AS_TUPLE, detailed_validation=dv
        )
        cl, vals, _ = cls_and_vals
        inst = cl(*vals)
        unstructured = converter.unstructure(inst)
        assert "Hyp" not in repr(unstructured)
>       assert inst == converter.structure(unstructured, cl)
E       AssertionError: assert HypDataclass(a=nan) == HypDataclass(a=nan)
E         
E         Differing attributes:
E         ['a']
E         
E         Drill down into differing attribute a:
E           a: nan != nan
E       Falsifying example: test_simple_roundtrip_tuple(
E           cls_and_vals=(tests.typed.HypDataclass, (nan,), {}),  # Saw 1 signaling NaN
E           dv=False,  # or any other generated value
E       )
E       Explanation:
E           These lines were always and only run by failing examples:
E               /usr/lib/python3.13/site-packages/_pytest/assertion/util.py:456
_          = {}
cl         = <class 'tests.typed.HypDataclass'>
cls_and_vals = (<class 'tests.typed.HypDataclass'>, (nan,), {})
converter  = <cattrs.converters.Converter object at 0xffff725bc4a0>
dv         = False
inst       = HypDataclass(a=nan)
unstructured = (nan,)
vals       = (nan,)
tests/test_converter.py:69: AssertionError
_______________ test_simple_roundtrip_with_extra_keys_forbidden ________________
[gw6] linux -- Python 3.13.0 /usr/bin/python3
    @given(
>       simple_typed_classes(newtypes=False) | simple_typed_dataclasses(newtypes=False),
        unstructure_strats,
    )
f          = <function given.<locals>.run_test_as_given.<locals>.wrapped_test at 0xffff7c35b920>
tests/test_converter.py:103: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
cls_and_vals = (<class 'tests.typed.HypDataclass'>, (nan,), {})
strat = <UnstructureStrategy.AS_DICT: 'asdict'>
    @given(
        simple_typed_classes(newtypes=False) | simple_typed_dataclasses(newtypes=False),
        unstructure_strats,
    )
    def test_simple_roundtrip_with_extra_keys_forbidden(cls_and_vals, strat):
        """
        Simple classes can be unstructured and restructured with forbid_extra_keys=True.
        """
        converter = Converter(unstruct_strat=strat, forbid_extra_keys=True)
        cl, vals, kwargs = cls_and_vals
        assume(strat is UnstructureStrategy.AS_DICT or not kwargs)
        inst = cl(*vals, **kwargs)
        unstructured = converter.unstructure(inst)
        assert "Hyp" not in repr(unstructured)
>       assert inst == converter.structure(unstructured, cl)
E       AssertionError: assert HypDataclass(a=nan) == HypDataclass(a=nan)
E         
E         Differing attributes:
E         ['a']
E         
E         Drill down into differing attribute a:
E           a: nan != nan
E       Falsifying example: test_simple_roundtrip_with_extra_keys_forbidden(
E           cls_and_vals=(tests.typed.HypDataclass, (nan,), {}),  # Saw 1 signaling NaN
E           strat=UnstructureStrategy.AS_DICT,  # or any other generated value
E       )
E       Explanation:
E           These lines were always and only run by failing examples:
E               /usr/lib/python3.13/site-packages/_pytest/assertion/util.py:456
E               /usr/lib/python3.13/site-packages/_pytest/assertion/util.py:468
E               /usr/lib64/python3.13/reprlib.py:23
cl         = <class 'tests.typed.HypDataclass'>
cls_and_vals = (<class 'tests.typed.HypDataclass'>, (nan,), {})
converter  = <cattrs.converters.Converter object at 0xffff63042ca0>
inst       = HypDataclass(a=nan)
kwargs     = {}
strat      = <UnstructureStrategy.AS_DICT: 'asdict'>
unstructured = {'a': nan}
vals       = (nan,)
tests/test_converter.py:116: AssertionError
=========================== short test summary info ============================
FAILED tests/test_unstructure_collections.py::test_collection_unstructure_override_mapping
FAILED tests/test_gen_dict.py::test_renaming - AssertionError: assert HypData...
FAILED tests/test_converter.py::test_simple_roundtrip_tuple - AssertionError:...
FAILED tests/test_converter.py::test_simple_roundtrip_with_extra_keys_forbidden
= 4 failed, 576 passed, 1 skipped, 15 xfailed, 24 warnings in 215.73s (0:03:35) =
Tinche commented 3 weeks ago

Unfortunately 3.13 testing for the main branch is currently blocked by some of our dependencies, so I'm waiting on that to proceed.

I don't plan to test 23.2.3 on 3.12, it'll be 24.1.0.

Here's the PR: https://github.com/python-attrs/cattrs/pull/543

hroncok commented 2 weeks ago

3/4 of the failures are likely caused by this difference between Python 3.12 and 3.13:

>>> from dataclasses import dataclass
>>> @dataclass
... class a:
...     a: float
...     
>>> nan = float('nan')
>>> nan == nan  # on both
False
>>> a(nan) == a(nan)  # on 3.12
True
>>> a(nan) == a(nan)  # on 3.13
False

I believe this might be related to https://github.com/python/cpython/pull/104904

hroncok commented 2 weeks ago

I've reported https://github.com/python/cpython/issues/120645

Tinche commented 2 weeks ago

The new behavior looks more correct to me.

Interestingly, on 3.12:

(nan,) == (nan,)

Very surprised by this.

Tinche commented 2 weeks ago

I think I fixed the nan issues on the tin/3.13 branch.