Dataclass default_factory field treated as required in union disambiguation
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 72/100
Research direction
Start by running the reproduction script in the issue to confirm the dataclass union returns D2 for a payload missing tags. The default check is in create_default_dis_func in src/cattrs/disambiguators.py around lines 154-157, and fields_dict and adapted_fields in src/cattrs/_compat.py show why default_factory is invisible there. Done when the dataclass case matches the attrs behaviour, with a default_factory test added to tests/test_disambiguators.py.
Written by the indexing model from the issue text.
Description
A dataclass field with default_factory is treated as a required field when cattrs builds the disambiguator for a union of dataclasses, so a payload that omits it is silently structured as the other class.
Minimal example
from dataclasses import dataclass, field
from typing import Union
from attrs import Factory, define
from cattrs import Converter
@dataclass
class D1:
x: int
tags: list = field(default_factory=list)
@dataclass
class D2:
x: int
c = Converter(omit_if_default=True)
payload = c.unstructure(D1(1))
print("payload:", payload)
for union in (Union[D1, D2], Union[D2, D1]):
print(f"structure(payload, {union}) observed:", repr(c.structure(payload, union)))
print("expected: D1(x=1, tags=[]) or a 'Could not disambiguate' TypeError, as for attrs below")
@define
class A1:
x: int
tags: list = Factory(list)
@define
class A2:
x: int
c2 = Converter(omit_if_default=True)
try:
print("attrs twin:", repr(c2.structure(c2.unstructure(A1(1)), Union[A1, A2])))
except TypeError as e:
print("attrs twin raises:", type(e).__name__, e)
Observed output
payload: {'x': 1}
structure(payload, typing.Union[__main__.D1, __main__.D2]) observed: D2(x=1)
structure(payload, typing.Union[__main__.D2, __main__.D1]) observed: D2(x=1)
expected: D1(x=1, tags=[]) or a 'Could not disambiguate' TypeError, as for attrs below
attrs twin raises: TypeError Could not disambiguate between <class '__main__.A1'>, <class '__main__.A2'>: no unique non-default attributes
D1(1) round-trips to D2(x=1) in both union orders, with no error. The same classes written with attrs and Factory(list) raise when the union hook is built.
Expected behaviour
A field with default_factory should count as having a default, the same as an attrs Factory and a dataclass plain default. The dataclass union should then raise the same "Could not disambiguate" TypeError as the attrs version, instead of picking tags as the key and falling back to D2.
Versions
- Python 3.12.3
- cattrs 26.2.1 (26.1.0 gives the same output)
- attrs 26.1.0
- OS: Linux
Cause
In create_default_dis_func (src/cattrs/disambiguators.py, main at 73fa273), line 154 takes cl_fields = fields_dict(get_origin(cl) or cl) and line 157 treats a field as required when cl_fields[orig_name].default in (NOTHING, MISSING). For a dataclass, fields_dict (src/cattrs/_compat.py lines 131-135) returns the raw dataclasses.Field, whose .default is MISSING when only default_factory is set. adapted_fields (_compat.py lines 155-159) already turns default_factory into Factory, but the default check doesn't use it, so tags becomes the unique required field of D1, and a payload without it goes to D2.
Proposed fix
Check defaults on the adapted_fields view, which already maps default_factory to Factory, or also treat default_factory is not MISSING as a default at line 157. A dataclass default_factory case in tests/test_disambiguators.py would cover it; the existing dataclass tests there only use required fields.
I'm happy to open a PR with this fix and a test.
- Dominant language
- Python
- Stars
- 1.1k
- Forks
- 171
- Avg merge
- 14h 46m
- Merged PRs (30d)
- 5
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from python-attrs/cattrs
-
decimal.Decimal not handled in preconf converters (json, pyyaml)Possibly taken @Binilkks claimed this 112 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
python-attrs/cattrs#761 · 2 comments ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
python-attrs/cattrs#513 · 4 comments ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 1-2 days Newbie friendliness 62/100
python-attrs/cattrs#797 ·
Maintainers usually reply within 1 day
-
Difficulty 3/5 Half a day Newbie friendliness 64/100
python-attrs/cattrs#795 ·
Maintainers usually reply within 1 day
-
Difficulty 4/5 3-5 days Newbie friendliness 25/100
python-attrs/cattrs#794 ·
Maintainers usually reply within 1 day
All issues in python-attrs/cattrs
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
noontide-co/mainbranch#1211 · 1 comment ·
Maintainers usually reply within 1 day
-
[1.x Bug] run-operation --sql reports success but rolls back writes on transaction-capable adaptersOpentriage
Difficulty 2/5 1-3 hours Newbie friendliness 64/100
dbt-labs/dbt#16784 · 1 comment ·
Maintainers usually reply within 1 day
-
bug needs-triage
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
Maintainers usually reply within 1 day
-
bug settings ui
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
DewaldOosthuizen/py-tray-command-launcher#162 ·
Maintainers usually reply within 1 day
-
run_all sleeps after the final job despite documenting delays between jobsPossibly taken @Gambit-Checkmate claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 74/100