python-attrs/cattrs

Python 3.13.0b2: 4 tests failuires

befeleme opened this issue · 9 comments

  • cattrs version: 23.2.3
  • Python version: 3.13.0b2
  • Operating System: Fedora Linux 41

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) =

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: #543

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 python/cpython#104904

The new behavior looks more correct to me.

Interestingly, on 3.12:

(nan,) == (nan,)

Very surprised by this.

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

Any progress on this?

Currently, with cattrs 24.4.1 and with Python 3.13.0rc2 on Fedora 41 (which is about to ship a beta release, and will be in Final Freeze not long after that!) I see the following failures.

=========================== short test summary info ============================
FAILED tests/test_generics.py::test_unstructure_deeply_nested_generics_list[True]
FAILED tests/test_generics.py::test_unstructure_deeply_nested_generics_list[False]
FAILED tests/test_converter.py::test_simple_roundtrip_defaults - AssertionErr...
FAILED tests/test_converter.py::test_simple_roundtrip - AssertionError: asser...
FAILED tests/test_baseconverter.py::test_simple_roundtrip - AssertionError: a...
FAILED tests/test_converter.py::test_simple_roundtrip_tuple - AssertionError:...
FAILED tests/test_converter.py::test_omit_default_roundtrip - AssertionError:...
FAILED tests/test_converter.py::test_simple_roundtrip_with_extra_keys_forbidden
FAILED tests/test_converter.py::test_union_field_roundtrip - AssertionError: ...
FAILED tests/test_gen_dict.py::test_unmodified_generated_structuring - Assert...
FAILED tests/test_structure_attrs.py::test_structure_simple_from_dict_default
FAILED tests/test_gen_dict.py::test_nodefs_generated_unstructuring_cl - Asser...
FAILED tests/test_converter.py::test_310_union_field_roundtrip - AssertionErr...
FAILED tests/test_converter.py::test_optional_field_roundtrip - AssertionErro...
FAILED tests/test_baseconverter.py::test_nested_roundtrip_tuple - AssertionEr...
FAILED tests/test_gen_dict.py::test_renaming - AssertionError: assert HypAttr...
FAILED tests/test_baseconverter.py::test_nested_roundtrip - AssertionError: a...
FAILED tests/test_baseconverter.py::test_union_field_roundtrip - AssertionErr...
FAILED tests/test_baseconverter.py::test_310_union_field_roundtrip - Assertio...
FAILED tests/test_baseconverter.py::test_310_optional_field_roundtrip - Asser...
FAILED tests/test_converter.py::test_nested_roundtrip - AssertionError: asser...
FAILED tests/test_converter.py::test_nested_roundtrip_tuple - AssertionError:...
= 22 failed, 706 passed, 1 skipped, 15 xfailed, 32 warnings in 446.96s (0:07:26) =

tests.txt

Note that this is without bson (the Fedora package is too old) or msgspec (just added to Fedora, not successfully built for F41 and later yet), so I could be missing some things. On the other hand, I did compare against a build for Fedora 40, and all of these are regressions. I assume they are mostly associated with Python 3.13, although there are some newer dependency versions that could also be involved. I’m just planning to skip these tests for now.

Additionally, the following test seems to be flaky:

FAILED tests/test_gen_dict.py::test_individual_overrides - AssertionError: as...

flaky-test.txt

With ae80674, I see almost all of these failures are fixed, but I am still seeing one:

=================================== FAILURES ===================================
________________________ test_simple_roundtrip_defaults ________________________
[gw1] linux -- Python 3.13.0 /usr/bin/python3

    @given(simple_typed_attrs(defaults=True, newtypes=False), unstructure_strats)
>   def test_simple_roundtrip_defaults(attr_and_strat, strat):

f          = <function given.<locals>.run_test_as_given.<locals>.wrapped_test at 0x7f1abe77e0c0>

tests/test_baseconverter.py:31:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

attr_and_strat = (_CountingAttr(counter=263, _default=nan, repr=True, eq=True, order=True, hash=None, init=True, on_setattr=None, alias=None, metadata={}), floats(allow_nan=True))
strat = <UnstructureStrategy.AS_DICT: 'asdict'>

    @given(simple_typed_attrs(defaults=True, newtypes=False), unstructure_strats)
    def test_simple_roundtrip_defaults(attr_and_strat, strat):
        """
        Simple classes with metadata can be unstructured and restructured.
        """
        a, _ = attr_and_strat
        assume(strat is UnstructureStrategy.AS_DICT or not a.kw_only)
        cl = make_class("HypClass", {"a": a})
        converter = BaseConverter(unstruct_strat=strat)
        inst = cl()
        assert converter.unstructure(converter.structure({}, cl)) == converter.unstructure(
            inst
        )   
>       assert inst == converter.structure(converter.unstructure(inst), cl)
E       AssertionError: assert HypClass(a=nan) == HypClass(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_defaults(
E           attr_and_strat=(_CountingAttr(counter=263, _default=nan, repr=True, eq=True, order=True, hash=None, init=True, on_setattr=None, alias=None, metadata={}),
E            floats(allow_nan=True)),
E           strat=UnstructureStrategy.AS_DICT,  # or any other generated value
E       )

_          = floats(allow_nan=True)
a          = _CountingAttr(counter=263, _default=nan, repr=True, eq=True, order=True, hash=None, init=True, on_setattr=None, alias=None, metadata={})
attr_and_strat = (_CountingAttr(counter=263, _default=nan, repr=True, eq=True, order=True, hash=None, init=True, on_setattr=None, alias=None, metadata={}), floats(allow_nan=True))
cl         = <class 'tests.test_baseconverter.HypClass'>
converter  = <cattrs.converters.BaseConverter object at 0x7f1ab00134c0>
inst       = HypClass(a=nan)
strat      = <UnstructureStrategy.AS_DICT: 'asdict'>

tests/test_baseconverter.py:43: AssertionError 
=============================== warnings summary ===============================
tests/typed.py:452: 16 warnings
  /builddir/build/BUILD/python-cattrs-24.1.2_20241004gitae80674-build/cattrs-ae806749f02502be1a8c073fd81050c04aa56c96/tests/typed.py:452: HypothesisWarning: Return-type annotation is `st.SearchStrategy[typing.Tuple[attr._make._CountingAttr, st.SearchStrategy]]`, but the decorated function should return a value (not a strategy)
    @composite

tests/typed.py:864: 16 warnings
  /builddir/build/BUILD/python-cattrs-24.1.2_20241004gitae80674-build/cattrs-ae806749f02502be1a8c073fd81050c04aa56c96/tests/typed.py:864: HypothesisWarning: Return-type annotation is `st.SearchStrategy[typing.Tuple[typing.Type, st.SearchStrategy[typing.Tuple[typing.Any]], st.SearchStrategy[typing.Dict[str, typing.Any]]]]`, but the decorated function should return a value (not a strategy)
    @composite

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=========================== short test summary info ============================
FAILED tests/test_baseconverter.py::test_simple_roundtrip_defaults - Assertio...
====== 1 failed, 729 passed, 1 skipped, 15 xfailed, 32 warnings in 31.82s ======

With ae80674, I see almost all of these failures are fixed, but I am still seeing one:

Upon further investigation, the AssertionError: assert HypClass(a=nan) == HypClass(a=nan) failure in test_simple_roundtrip_defaults described in #547 (comment) appears to be a regression from Python 3.13.0rc2 to 3.13.0rc3.