Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Add generic omit_if to support more than just omit_if_default #643

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

Open
wants to merge 6 commits into
base: main
Choose a base branch
Loading
from
Open
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
80 changes: 80 additions & 0 deletions 80 bench/test_attrs_omit_defaults.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from enum import IntEnum

import attr
import pytest

from cattr import Converter, UnstructureStrategy


class E(IntEnum):
ONE = 1
TWO = 2


@pytest.mark.parametrize(
"unstructure_strat", [UnstructureStrategy.AS_DICT, UnstructureStrategy.AS_TUPLE]
)
def test_unstructure_omit_primitive_defaults(benchmark, unstructure_strat):
"""Benchmark stripping default values with simple, non-factory primitives."""
c = Converter(unstruct_strat=unstructure_strat, omit_if_default=True)

@attr.define
class C:
a: int = 0
b: float = 0.0
c: str = "test"
d: bytes = b"test"
e: E = E.ONE
f: int = 0
g: float = 0.0
h: str = "test"
i: bytes = b"test"
j: E = E.ONE
k: int = 0
l: float = 0.0 # noqa: E741
m: str = "test"
n: bytes = b"test"
o: E = E.ONE
p: int = 0
q: float = 0.0
r: str = "test"
s: bytes = b"test"
t: E = E.ONE
u: int = 0
v: float = 0.0
w: str = "test"
x: bytes = b"test"
y: E = E.ONE
z: int = 0
aa: float = 0.0
ab: str = "test"
ac: bytes = b"test"
ad: E = E.ONE

c_instance = C()

benchmark(
c.unstructure,
c_instance,
)

@pytest.mark.parametrize(
"unstructure_strat", [UnstructureStrategy.AS_DICT, UnstructureStrategy.AS_TUPLE]
)
def test_unstructure_omit_factory_defaults(benchmark, unstructure_strat):
"""Benchmark stripping default values with factory-made primitives."""
c = Converter(unstruct_strat=unstructure_strat, omit_if_default=True)

@attr.define
class C:
a: dict = attr.field(factory=dict)
b: list = attr.field(factory=list)
c: tuple = attr.field(factory=tuple)
d: set = attr.field(factory=set)

c_instance = C()

benchmark(
c.unstructure,
c_instance,
)
1,587 changes: 0 additions & 1,587 deletions 1,587 pdm.lock

This file was deleted.

12 changes: 11 additions & 1 deletion 12 src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,7 @@ class Converter(BaseConverter):
__slots__ = (
"_unstruct_collection_overrides",
"forbid_extra_keys",
"omit_if",
"omit_if_default",
"type_overrides",
)
Expand All @@ -1030,6 +1031,7 @@ def __init__(
self,
dict_factory: Callable[[], Any] = dict,
unstruct_strat: UnstructureStrategy = UnstructureStrategy.AS_DICT,
omit_if: Callable[[Any, Any, Any], Any] | None = None,
omit_if_default: bool = False,
forbid_extra_keys: bool = False,
type_overrides: Mapping[type, AttributeOverride] = {},
Expand Down Expand Up @@ -1063,7 +1065,9 @@ def __init__(
unstructure_fallback_factory=unstructure_fallback_factory,
structure_fallback_factory=structure_fallback_factory,
)
# TODO: error if both `omit_if` and `omit_if_default` are specified
self.omit_if_default = omit_if_default
self.omit_if = omit_if
self.forbid_extra_keys = forbid_extra_keys
self.type_overrides = dict(type_overrides)

Expand Down Expand Up @@ -1244,7 +1248,11 @@ def gen_unstructure_attrs_fromdict(
}

return make_dict_unstructure_fn(
cl, self, _cattrs_omit_if_default=self.omit_if_default, **attrib_overrides
cl,
self,
_cattrs_omit_if_default=self.omit_if_default,
_cattrs_omit_if=self.omit_if,
**attrib_overrides,
)

def gen_unstructure_optional(self, cl: type[T]) -> Callable[[T], Any]:
Expand Down Expand Up @@ -1361,6 +1369,7 @@ def copy(
self,
dict_factory: Callable[[], Any] | None = None,
unstruct_strat: UnstructureStrategy | None = None,
omit_if: Callable[[Any, Any, Any], bool] | None = None,
omit_if_default: bool | None = None,
forbid_extra_keys: bool | None = None,
type_overrides: Mapping[type, AttributeOverride] | None = None,
Expand All @@ -1384,6 +1393,7 @@ def copy(
else UnstructureStrategy.AS_TUPLE
)
),
omit_if if omit_if is not None else self.omit_if,
omit_if_default if omit_if_default is not None else self.omit_if_default,
(
forbid_extra_keys
Expand Down
28 changes: 26 additions & 2 deletions 28 src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,15 +53,19 @@
def override(
omit_if_default: bool | None = None,
rename: str | None = None,
location: str | tuple[str] | None = None,
omit: bool | None = None,
omit_if: Callable[[Any, Any, Any], bool] | bool | None = None,
struct_hook: Callable[[Any, Any], Any] | None = None,
unstruct_hook: Callable[[Any], Any] | None = None,
) -> AttributeOverride:
"""Override how a particular field is handled.

:param omit: Whether to skip the field or not. `None` means apply default handling.
"""
return AttributeOverride(omit_if_default, rename, omit, struct_hook, unstruct_hook)
return AttributeOverride(
omit_if_default, rename, location, omit, omit_if, struct_hook, unstruct_hook
)


T = TypeVar("T")
Expand All @@ -73,6 +77,7 @@ def make_dict_unstructure_fn_from_attrs(
converter: BaseConverter,
typevar_map: dict[str, Any] = {},
_cattrs_omit_if_default: bool = False,
_cattrs_omit_if: Callable[[Any, Any, Any], bool] | None = None,
_cattrs_use_linecache: bool = True,
_cattrs_use_alias: bool = False,
_cattrs_include_init_false: bool = False,
Expand All @@ -88,8 +93,10 @@ def make_dict_unstructure_fn_from_attrs(

:param cl: The class for which the function is generated; used mostly for its name,
module name and qualname.
:param _cattrs_omit_if_default: if true, attributes equal to their default values
:param _cattrs_omit_if_default: If true, attributes equal to their default values
will be omitted in the result dictionary.
:param _cattrs_omit: Omits the attribute (at runtime) if the passed-in value
evaluates to true.
:param _cattrs_use_alias: If true, the attribute alias will be used as the
dictionary key by default.
:param _cattrs_include_init_false: If true, _attrs_ fields marked as `init=False`
Expand All @@ -116,6 +123,7 @@ def make_dict_unstructure_fn_from_attrs(
else:
kn = override.rename
d = a.default
omit_if = _cattrs_omit_if if override.omit_if is None else override.omit_if

# For each attribute, we try resolving the type here and now.
# If a type is manually overwritten, this function should be
Expand Down Expand Up @@ -181,6 +189,20 @@ def make_dict_unstructure_fn_from_attrs(
lines.append(f" if instance.{attr_name} != {def_name}:")
lines.append(f" res['{kn}'] = {invoke}")

elif omit_if: # callable
omit_callable = f"__c_omit_{attr_name}"
attr_attr = f"__c_attr_{attr_name}"

lines.append(
f" if not {omit_callable}(instance, {attr_attr}, instance.{attr_name}):"
)
lines.append(f" res['{kn}'] = {invoke}")

globs[omit_callable] = (
omit_if # _cattrs_omit_if if override.omit_if is None else override.omit_if
)
globs[attr_attr] = a

else:
# No default or no override.
invocation_lines.append(f"'{kn}': {invoke},")
Expand Down Expand Up @@ -216,6 +238,7 @@ def make_dict_unstructure_fn(
cl: type[T],
converter: BaseConverter,
_cattrs_omit_if_default: bool = False,
_cattrs_omit_if: Callable[[Any, Any, Any], bool] | bool | None = None,
_cattrs_use_linecache: bool = True,
_cattrs_use_alias: bool = False,
_cattrs_include_init_false: bool = False,
Expand Down Expand Up @@ -267,6 +290,7 @@ def make_dict_unstructure_fn(
converter,
mapping,
_cattrs_omit_if_default=_cattrs_omit_if_default,
_cattrs_omit_if=_cattrs_omit_if,
_cattrs_use_linecache=_cattrs_use_linecache,
_cattrs_use_alias=_cattrs_use_alias,
_cattrs_include_init_false=_cattrs_include_init_false,
Expand Down
4 changes: 4 additions & 0 deletions 4 src/cattrs/gen/_consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,11 @@
class AttributeOverride:
omit_if_default: bool | None = None
rename: str | None = None
location: str | tuple[str] | None = (None,)
omit: bool | None = None # Omit the field completely.
omit_if: Callable[[Any, Any, Any], bool] | bool | None = (
None # Omit if callable returns True.
)
struct_hook: Callable[[Any, Any], Any] | None = None # Structure hook to use.
unstruct_hook: Callable[[Any], Any] | None = None # Structure hook to use.

Expand Down
2 changes: 1 addition & 1 deletion 2 tests/test_copy.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ def test_copy_converter(
assert c.detailed_validation == copy.detailed_validation
assert c._prefer_attrib_converters == copy._prefer_attrib_converters
assert c._dict_factory == copy._dict_factory
assert c.omit_if_default == copy.omit_if_default
assert c.omit_if == copy.omit_if

another_copy = c.copy(omit_if_default=not omit_if_default)
assert c.omit_if_default != another_copy.omit_if_default
Expand Down
43 changes: 43 additions & 0 deletions 43 tests/test_gen_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -285,6 +285,49 @@ class A:
assert not hasattr(converter.structure({"a": 2}, A), "b")


def test_omit_callable_unstructure():
"""Test omitting via callable."""

@define
class Example:
a: int
b: int | None
c: int
d: int = 123
e: set = field(factory=set)
f: int = field()

@f.default
def f_default(self) -> int:
return self.d

overridden: None = None

converter = Converter()

def default_or_none(instance, attribute, value) -> bool:
if value is None:
return True
if isinstance(attribute.default, Factory):
if attribute.default.takes_self:
return value == attribute.default.factory(instance)
return value == attribute.default.factory()
return value == attribute.default

converter.register_unstructure_hook(
Example,
make_dict_unstructure_fn(
Example,
converter,
_cattrs_omit_if=default_or_none,
c=override(omit_if=lambda inst, attr, value: value < 0),
overridden=override(omit_if=False),
),
)

assert converter.unstructure(Example(100, None, c=-100, f=123)) == {"a": 100, "overridden": None}


@pytest.mark.parametrize("detailed_validation", [True, False])
def test_omitting_structure(detailed_validation: bool):
"""Omitting fields works with generated structuring functions."""
Expand Down
2 changes: 1 addition & 1 deletion 2 tests/test_generics.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ class TClass2(Generic[T]):


def test_raises_if_no_generic_params_supplied(
converter: Union[Converter, BaseConverter]
converter: Union[Converter, BaseConverter],
):
data = TClass(1, "a")

Expand Down
2 changes: 1 addition & 1 deletion 2 tests/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -275,7 +275,7 @@ def key(t):


def _create_hyp_class_and_strat(
attrs_and_strategy: list[tuple[_CountingAttr, SearchStrategy[PosArg]]]
attrs_and_strategy: list[tuple[_CountingAttr, SearchStrategy[PosArg]]],
) -> SearchStrategy[tuple[type, SearchStrategy[PosArgs], SearchStrategy[KwArgs]]]:
def key(t):
return (t[0].default is not NOTHING, t[0].kw_only)
Expand Down
Loading
Morty Proxy This is a proxified and sanitized view of the page, visit original site.