From dceb9f3a5b50c1c9c058590f4f3efdb572715b21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Thu, 8 Jun 2023 00:16:36 +0200 Subject: [PATCH 1/2] Fix typeddicts with period fields --- HISTORY.md | 4 ++++ src/cattrs/gen/typeddicts.py | 34 ++++++++++++++++++---------------- tests/typeddicts.py | 14 +++++++++++++- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index cb353ee2..4837f24e 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,13 +1,17 @@ # History ## 23.2.0 (UNRELEASED) + - Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry. - _cattrs_ is now linted with [Ruff](https://beta.ruff.rs/docs/). +- Fix TypedDicts with periods in their field names. ([#376](https://github.com/python-attrs/cattrs/issues/376)) ## 23.1.2 (2023-06-02) + - Improve `typing_extensions` version bound. ([#372](https://github.com/python-attrs/cattrs/issues/372)) ## 23.1.1 (2023-05-30) + - Add `typing_extensions` as a direct dependency on 3.10. ([#369](https://github.com/python-attrs/cattrs/issues/369) [#370](https://github.com/python-attrs/cattrs/pull/370)) ## 23.1.0 (2023-05-30) diff --git a/src/cattrs/gen/typeddicts.py b/src/cattrs/gen/typeddicts.py index c80325ef..4b36b7f3 100644 --- a/src/cattrs/gen/typeddicts.py +++ b/src/cattrs/gen/typeddicts.py @@ -147,7 +147,7 @@ def make_dict_unstructure_fn( # We've not broken the loop. return converter._unstructure_identity - for a in attrs: + for ix, a in enumerate(attrs): attr_name = a.name override = kwargs.get(attr_name, neutral) if override.omit: @@ -190,7 +190,7 @@ def make_dict_unstructure_fn( is_identity = handler == converter._unstructure_identity if not is_identity: - unstruct_handler_name = f"__c_unstr_{attr_name}" + unstruct_handler_name = f"__c_unstr_{ix}" globs[unstruct_handler_name] = handler internal_arg_parts[unstruct_handler_name] = handler invoke = f"{unstruct_handler_name}(instance['{attr_name}'])" @@ -315,7 +315,7 @@ def make_dict_structure_fn( lines.append(" errors = []") internal_arg_parts["__c_cve"] = ClassValidationError internal_arg_parts["__c_avn"] = AttributeValidationNote - for a in attrs: + for ix, a in enumerate(attrs): an = a.name attr_required = an in req_keys override = kwargs.get(an, neutral) @@ -340,7 +340,7 @@ def make_dict_structure_fn( else: handler = find_structure_handler(a, t, converter) - struct_handler_name = f"__c_structure_{an}" + struct_handler_name = f"__c_structure_{ix}" internal_arg_parts[struct_handler_name] = handler kn = an if override.rename is None else override.rename @@ -351,15 +351,17 @@ def make_dict_structure_fn( i = f"{i} " lines.append(f"{i}try:") i = f"{i} " - type_name = f"__c_type_{an}" - internal_arg_parts[type_name] = t + + tn = f"__c_type_{ix}" + internal_arg_parts[tn] = t + if handler: if handler == converter._structure_call: internal_arg_parts[struct_handler_name] = t lines.append(f"{i}res['{an}'] = {struct_handler_name}(o['{kn}'])") else: lines.append( - f"{i}res['{an}'] = {struct_handler_name}(o['{kn}'], {type_name})" + f"{i}res['{an}'] = {struct_handler_name}(o['{kn}'], {tn})" ) else: lines.append(f"{i}res['{an}'] = o['{kn}']") @@ -369,7 +371,7 @@ def make_dict_structure_fn( lines.append(f"{i}except Exception as e:") i = f"{i} " lines.append( - f'{i}e.__notes__ = getattr(e, \'__notes__\', []) + [__c_avn("Structuring typeddict {cl.__qualname__} @ attribute {an}", "{an}", __c_type_{an})]' + f'{i}e.__notes__ = [*getattr(e, \'__notes__\', []), __c_avn("Structuring typeddict {cl.__qualname__} @ attribute {an}", "{an}", {tn})]' ) lines.append(f"{i}errors.append(e)") @@ -387,7 +389,7 @@ def make_dict_structure_fn( non_required = [] # The first loop deals with required args. - for a in attrs: + for ix, a in enumerate(attrs): an = a.name attr_required = an in req_keys override = kwargs.get(an, neutral) @@ -419,7 +421,7 @@ def make_dict_structure_fn( allowed_fields.add(kn) if handler: - struct_handler_name = f"__c_structure_{an}" + struct_handler_name = f"__c_structure_{ix}" internal_arg_parts[struct_handler_name] = handler if handler == converter._structure_call: internal_arg_parts[struct_handler_name] = t @@ -427,10 +429,10 @@ def make_dict_structure_fn( f" res['{an}'] = {struct_handler_name}(o['{kn}'])" ) else: - type_name = f"__c_type_{an}" - internal_arg_parts[type_name] = t + tn = f"__c_type_{ix}" + internal_arg_parts[tn] = t invocation_line = ( - f" res['{an}'] = {struct_handler_name}(o['{kn}'], {type_name})" + f" res['{an}'] = {struct_handler_name}(o['{kn}'], {tn})" ) else: invocation_line = f" res['{an}'] = o['{kn}']" @@ -441,7 +443,7 @@ def make_dict_structure_fn( # The second loop is for optional args. if non_required: - for a in non_required: + for ix, a in enumerate(non_required, start=ix + 1): an = a.name override = kwargs.get(an, neutral) t = a.type @@ -463,7 +465,7 @@ def make_dict_structure_fn( else: handler = converter.structure - struct_handler_name = f"__c_structure_{an}" + struct_handler_name = f"__c_structure_{ix}" internal_arg_parts[struct_handler_name] = handler ian = an @@ -477,7 +479,7 @@ def make_dict_structure_fn( f" res['{ian}'] = {struct_handler_name}(o['{kn}'])" ) else: - tn = f"__c_type_{an}" + tn = f"__c_type_{ix}" internal_arg_parts[tn] = t post_lines.append( f" res['{ian}'] = {struct_handler_name}(o['{kn}'], {tn})" diff --git a/tests/typeddicts.py b/tests/typeddicts.py index 27d76948..b6d4cc70 100644 --- a/tests/typeddicts.py +++ b/tests/typeddicts.py @@ -28,6 +28,18 @@ T3 = TypeVar("T3") +def gen_typeddict_attr_names(): + """Typed dicts can have periods in their field names.""" + counter = 0 + for n in gen_attr_names(): + counter += 1 + + if counter % 2 == 0: + n = f"{n}.suffix" + + yield n + + @composite def int_attributes( draw: DrawFn, total: bool = True, not_required: bool = False @@ -112,7 +124,7 @@ def simple_typeddicts( ) ) - attrs_dict = {n: attr[0] for n, attr in zip(gen_attr_names(), attrs)} + attrs_dict = {n: attr[0] for n, attr in zip(gen_typeddict_attr_names(), attrs)} success_payload = {} for n, a in zip(attrs_dict, attrs): v = draw(a[1]) From 1987594933e00fade62c104e50f4f8b244f603e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tin=20Tvrtkovi=C4=87?= Date: Thu, 8 Jun 2023 00:20:08 +0200 Subject: [PATCH 2/2] Tweaks --- HISTORY.md | 3 ++- src/cattrs/gen/typeddicts.py | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/HISTORY.md b/HISTORY.md index 4837f24e..9e1152e8 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -4,7 +4,8 @@ - Use [PDM](https://pdm.fming.dev/latest/) instead of Poetry. - _cattrs_ is now linted with [Ruff](https://beta.ruff.rs/docs/). -- Fix TypedDicts with periods in their field names. ([#376](https://github.com/python-attrs/cattrs/issues/376)) +- Fix TypedDicts with periods in their field names. + ([#376](https://github.com/python-attrs/cattrs/issues/376) [#377](https://github.com/python-attrs/cattrs/pull/377)) ## 23.1.2 (2023-06-02) diff --git a/src/cattrs/gen/typeddicts.py b/src/cattrs/gen/typeddicts.py index 4b36b7f3..445763a6 100644 --- a/src/cattrs/gen/typeddicts.py +++ b/src/cattrs/gen/typeddicts.py @@ -396,7 +396,7 @@ def make_dict_structure_fn( if override.omit: continue if not attr_required: - non_required.append(a) + non_required.append((ix, a)) continue t = a.type @@ -443,7 +443,7 @@ def make_dict_structure_fn( # The second loop is for optional args. if non_required: - for ix, a in enumerate(non_required, start=ix + 1): + for ix, a in non_required: an = a.name override = kwargs.get(an, neutral) t = a.type