Skip to content

Improve coverage for init false attributes #486

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ jobs:
echo "total=$TOTAL" >> $GITHUB_ENV

# Report again and fail if under the threshold.
python -Im coverage report --fail-under=97
python -Im coverage report --fail-under=98

- name: "Upload HTML report."
uses: "actions/upload-artifact@v3"
Expand Down
2 changes: 2 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Our backwards-compatibility policy can be found [here](https://github.com/python
Previously this behavior was underspecified and inconsistent, but followed this rule in the majority of cases.
Reverting old behavior is very dependent on the actual case; ask on the issue tracker if in doubt.
([#473](https://github.com/python-attrs/cattrs/pull/473))
- **Minor change**: Heterogeneous tuples are now unstructured into tuples instead of lists by default; this is significantly faster and widely supported by serialization libraries.
([#486](https://github.com/python-attrs/cattrs/pull/486))
- Introduce {meth}`BaseConverter.get_structure_hook` and {meth}`BaseConverter.get_unstructure_hook` methods.
([#432](https://github.com/python-attrs/cattrs/issues/432) [#472](https://github.com/python-attrs/cattrs/pull/472))
- Introduce the [_msgspec_](https://jcristharif.com/msgspec/) {mod}`preconf converter <cattrs.preconf.msgspec>`.
Expand Down
1 change: 1 addition & 0 deletions docs/defaulthooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ Any type parameters set to `typing.Any` will be passed through unconverted.
(1, '2', 3.0)
```

When unstructuring, heterogeneous tuples unstructure into tuples since it's faster and virtually all serialization libraries support tuples natively.

### Deques

Expand Down
210 changes: 187 additions & 23 deletions pdm.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1054,7 +1054,7 @@ def gen_unstructure_hetero_tuple(
self, cl: Any, unstructure_to: Any = None
) -> HeteroTupleUnstructureFn:
unstructure_to = self._unstruct_collection_overrides.get(
get_origin(cl) or cl, unstructure_to or list
get_origin(cl) or cl, unstructure_to or tuple
)
h = make_hetero_tuple_unstructure_fn(cl, self, unstructure_to=unstructure_to)
self._unstructure_func.register_cls_list([(cl, h)], direct=True)
Expand Down
9 changes: 5 additions & 4 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,7 +372,8 @@ def make_dict_structure_fn(
)

struct_handler_name = f"__c_structure_{an}"
internal_arg_parts[struct_handler_name] = handler
if handler is not None:
internal_arg_parts[struct_handler_name] = handler

ian = a.alias
if override.rename is None:
Expand All @@ -391,7 +392,7 @@ def make_dict_structure_fn(
i = f"{i} "
type_name = f"__c_type_{an}"
internal_arg_parts[type_name] = t
if handler:
if handler is not None:
if handler == converter._structure_call:
internal_arg_parts[struct_handler_name] = t
pi_lines.append(
Expand Down Expand Up @@ -511,7 +512,7 @@ def make_dict_structure_fn(
allowed_fields.add(kn)

if not a.init:
if handler:
if handler is not None:
struct_handler_name = f"__c_structure_{an}"
internal_arg_parts[struct_handler_name] = handler
if handler == converter._structure_call:
Expand Down Expand Up @@ -803,7 +804,7 @@ def make_mapping_structure_fn(
val_type=NOTHING,
detailed_validation: bool = True,
) -> MappingStructureFn[T]:
"""Generate a specialized unstructure function for a mapping."""
"""Generate a specialized structure function for a mapping."""
fn_name = "structure_mapping"

globs: dict[str, type] = {"__cattr_mapping_cl": structure_to}
Expand Down
5 changes: 3 additions & 2 deletions src/cattrs/gen/_shared.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
from __future__ import annotations

from typing import TYPE_CHECKING, Any, Callable
from typing import TYPE_CHECKING, Any

from attrs import NOTHING, Attribute, Factory

from .._compat import is_bare_final
from ..dispatch import StructureHook
from ..fns import raise_error

if TYPE_CHECKING: # pragma: no cover
Expand All @@ -13,7 +14,7 @@

def find_structure_handler(
a: Attribute, type: Any, c: BaseConverter, prefer_attrs_converters: bool = False
) -> Callable[[Any, Any], Any] | None:
) -> StructureHook | None:
"""Find the appropriate structure handler to use.

Return `None` if no handler should be used.
Expand Down
31 changes: 31 additions & 0 deletions tests/test_gen_collections.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
"""Tests for collections in `cattrs.gen`."""
from typing import Generic, Mapping, NewType, Tuple, TypeVar

from cattrs import Converter
from cattrs.gen import make_hetero_tuple_unstructure_fn, make_mapping_structure_fn


def test_structuring_mappings(genconverter: Converter):
"""The `key_type` parameter works for generics with 1 type variable."""
T = TypeVar("T")

class MyMapping(Generic[T], Mapping[str, T]):
pass

def key_hook(value, _):
return f"{value}1"

Key = NewType("Key", str)

genconverter.register_structure_hook(Key, key_hook)

fn = make_mapping_structure_fn(MyMapping[int], genconverter, key_type=Key)

assert fn({"a": 1}, MyMapping[int]) == {"a1": 1}


def test_unstructure_hetero_tuple_to_tuple(genconverter: Converter):
"""`make_hetero_tuple_unstructure_fn` works when unstructuring to tuple."""
fn = make_hetero_tuple_unstructure_fn(Tuple[int, str, int], genconverter, tuple)

assert fn((1, "1", 2)) == (1, "1", 2)
65 changes: 51 additions & 14 deletions tests/test_gen_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Dict, Type

import pytest
from attrs import NOTHING, Factory, define, field
from attrs import NOTHING, Factory, define, field, frozen
from hypothesis import assume, given
from hypothesis.strategies import data, just, one_of, sampled_from

Expand Down Expand Up @@ -450,12 +450,18 @@ class A:
def test_init_false_overridden(converter: BaseConverter) -> None:
"""init=False handling can be overriden."""

@frozen
class Inner:
a: int

@define
class A:
a: int
b: int = field(init=False)
_c: int = field(init=False)
d: int = field(init=False, default=4)
d: Inner = field(init=False)
e: int = field(init=False, default=4)
f: Inner = field(init=False, default=Inner(1))

converter.register_unstructure_hook(
A, make_dict_unstructure_fn(A, converter, _cattrs_include_init_false=True)
Expand All @@ -464,28 +470,36 @@ class A:
a = A(1)
a.b = 2
a._c = 3
a.d = Inner(4)

assert converter.unstructure(a) == {"a": 1, "b": 2, "_c": 3, "d": 4}
assert converter.unstructure(a) == {
"a": 1,
"b": 2,
"_c": 3,
"d": {"a": 4},
"e": 4,
"f": {"a": 1},
}

converter.register_structure_hook(
A,
make_dict_structure_fn(
A,
converter,
_cattrs_include_init_false=True,
_cattrs_detailed_validation=converter.detailed_validation,
),
A, make_dict_structure_fn(A, converter, _cattrs_include_init_false=True)
)

structured = converter.structure({"a": 1, "b": 2, "_c": 3}, A)
structured = converter.structure({"a": 1, "b": 2, "_c": 3, "d": {"a": 1}}, A)
assert structured.b == 2
assert structured._c == 3
assert structured.d == 4
assert structured.d == Inner(1)
assert structured.e == 4
assert structured.f == Inner(1)

structured = converter.structure({"a": 1, "b": 2, "_c": 3, "d": -4}, A)
structured = converter.structure(
{"a": 1, "b": 2, "_c": 3, "d": {"a": 5}, "e": -4, "f": {"a": 2}}, A
)
assert structured.b == 2
assert structured._c == 3
assert structured.d == -4
assert structured.d == Inner(5)
assert structured.e == -4
assert structured.f == Inner(2)


def test_init_false_field_override(converter: BaseConverter) -> None:
Expand Down Expand Up @@ -538,6 +552,29 @@ class A:
assert structured.d == -4


def test_init_false_no_structure_hook(converter: BaseConverter):
"""init=False attributes with converters and `prefer_attrs_converters` work."""

@define
class A:
a: int = field(converter=int, init=False)

converter.register_structure_hook(
A,
make_dict_structure_fn(
A,
converter,
_cattrs_prefer_attrib_converters=True,
_cattrs_include_init_false=True,
),
)

res = A()
res.a = 5

assert converter.structure({"a": "5"}, A) == res


@given(forbid_extra_keys=..., detailed_validation=...)
def test_forbid_extra_keys_from_converter(
forbid_extra_keys: bool, detailed_validation: bool
Expand Down