Skip to content

Commit

Permalink
alias work
Browse files Browse the repository at this point in the history
  • Loading branch information
Tinche committed Jun 14, 2023
1 parent 18dff36 commit 7b7d250
Show file tree
Hide file tree
Showing 6 changed files with 43 additions and 24 deletions.
1 change: 1 addition & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## 23.2.0 (UNRELEASED)

- [_attrs_ aliases](https://www.attrs.org/en/stable/init.html#private-attributes-and-aliases) are now supported, although aliased fields still map to their attribute name instead of their alias by default when un/structuring.
- 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.
Expand Down
3 changes: 2 additions & 1 deletion src/cattrs/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def fields(type):
raise Exception("Not an attrs or dataclass class.") from None


def _adapted_fields(cl) -> List[Attribute]:
def adapted_fields(cl) -> List[Attribute]:
"""Return the attrs format of `fields()` for attrs and dataclasses."""
if is_dataclass(cl):
attrs = dataclass_fields(cl)
Expand All @@ -95,6 +95,7 @@ def _adapted_fields(cl) -> List[Attribute]:
attr.init,
True,
type=type_hints.get(attr.name, attr.type),
alias=attr.name,
)
for attr in attrs
]
Expand Down
2 changes: 1 addition & 1 deletion src/cattrs/converters.py
Original file line number Diff line number Diff line change
Expand Up @@ -516,7 +516,7 @@ def structure_attrs_fromdict(self, obj: Mapping[str, Any], cl: Type[T]) -> T:
if name[0] == "_":
name = name[1:]

conv_obj[name] = self._structure_attribute(a, val)
conv_obj[getattr(a, "alias", a.name)] = self._structure_attribute(a, val)

return cl(**conv_obj)

Expand Down
29 changes: 16 additions & 13 deletions src/cattrs/gen/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@

import linecache
import re
from dataclasses import is_dataclass
from typing import TYPE_CHECKING, Any, Callable, Iterable, Mapping, Tuple, TypeVar

import attr
from attr import NOTHING, resolve_types

from .._compat import (
_adapted_fields,
adapted_fields,
get_args,
get_origin,
is_annotated,
Expand Down Expand Up @@ -53,14 +52,18 @@ def make_dict_unstructure_fn(
converter: BaseConverter,
_cattrs_omit_if_default: bool = False,
_cattrs_use_linecache: bool = True,
_cattrs_use_alias: bool = False,
**kwargs: AttributeOverride,
) -> Callable[[T], dict[str, Any]]:
"""
Generate a specialized dict unstructuring function for an attrs class or a
dataclass.
:param _cattrs_use_alias: If true, the attribute alias will be used as the
dictionary key by default.
"""
origin = get_origin(cl)
attrs = _adapted_fields(origin or cl) # type: ignore
attrs = adapted_fields(origin or cl) # type: ignore

if any(isinstance(a.type, str) for a in attrs):
# PEP 563 annotations - need to be resolved.
Expand Down Expand Up @@ -102,7 +105,10 @@ def make_dict_unstructure_fn(
override = kwargs.pop(attr_name, neutral)
if override.omit:
continue
kn = attr_name if override.rename is None else override.rename
if override.rename is None:
kn = attr_name if not _cattrs_use_alias else a.alias
else:
kn = override.rename
d = a.default

# For each attribute, we try resolving the type here and now.
Expand Down Expand Up @@ -263,8 +269,7 @@ def make_dict_structure_fn(
post_lines = []
invocation_lines = []

attrs = _adapted_fields(cl)
is_dc = is_dataclass(cl)
attrs = adapted_fields(cl)

if any(isinstance(a.type, str) for a in attrs):
# PEP 563 annotations - need to be resolved.
Expand Down Expand Up @@ -306,7 +311,7 @@ def make_dict_structure_fn(
struct_handler_name = f"__c_structure_{an}"
internal_arg_parts[struct_handler_name] = handler

ian = an if (is_dc or an[0] != "_") else an[1:]
ian = a.alias
kn = an if override.rename is None else override.rename
allowed_fields.add(kn)
i = " "
Expand Down Expand Up @@ -401,8 +406,7 @@ def make_dict_structure_fn(
invocation_line = f"o['{kn}'],"

if a.kw_only:
ian = an if (is_dc or an[0] != "_") else an[1:]
invocation_line = f"{ian}={invocation_line}"
invocation_line = f"{a.alias}={invocation_line}"
invocation_lines.append(invocation_line)

# The second loop is for optional args.
Expand Down Expand Up @@ -433,24 +437,23 @@ def make_dict_structure_fn(
struct_handler_name = f"__c_structure_{an}"
internal_arg_parts[struct_handler_name] = handler

ian = an if (is_dc or an[0] != "_") else an[1:]
kn = an if override.rename is None else override.rename
allowed_fields.add(kn)
post_lines.append(f" if '{kn}' in o:")
if handler:
if handler == converter._structure_call:
internal_arg_parts[struct_handler_name] = t
post_lines.append(
f" res['{ian}'] = {struct_handler_name}(o['{kn}'])"
f" res['{a.alias}'] = {struct_handler_name}(o['{kn}'])"
)
else:
tn = f"__c_type_{an}"
internal_arg_parts[tn] = t
post_lines.append(
f" res['{ian}'] = {struct_handler_name}(o['{kn}'], {tn})"
f" res['{a.alias}'] = {struct_handler_name}(o['{kn}'], {tn})"
)
else:
post_lines.append(f" res['{ian}'] = o['{kn}']")
post_lines.append(f" res['{a.alias}'] = o['{kn}']")
instantiation_lines = (
[" return __cl("] + [f" {line}" for line in invocation_lines] + [" )"]
)
Expand Down
6 changes: 3 additions & 3 deletions tests/test_gen_dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
from hypothesis.strategies import data, just, one_of, sampled_from

from cattrs import BaseConverter, Converter
from cattrs._compat import _adapted_fields, fields
from cattrs._compat import adapted_fields, fields
from cattrs.errors import ClassValidationError, ForbiddenExtraKeysError
from cattrs.gen import make_dict_structure_fn, make_dict_unstructure_fn, override

Expand Down Expand Up @@ -117,7 +117,7 @@ def test_individual_overrides(converter_cls, cl_and_vals):
converter = converter_cls()
cl, vals, kwargs = cl_and_vals

for attr in _adapted_fields(cl):
for attr in adapted_fields(cl):
if attr.default is not NOTHING:
break
else:
Expand All @@ -141,7 +141,7 @@ def test_individual_overrides(converter_cls, cl_and_vals):
assert "Hyp" not in repr(res)
assert "Factory" not in repr(res)

for attr, val in zip(_adapted_fields(cl), vals):
for attr, val in zip(adapted_fields(cl), vals):
if attr.name == chosen_name:
assert attr.name in res
elif attr.default is not NOTHING:
Expand Down
26 changes: 20 additions & 6 deletions tests/typed.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,13 +220,27 @@ def key(t):
a.counter = i
vals = tuple((a[1]) for a in attrs_and_strat if not a[0].kw_only)
note(f"Class fields: {attrs}")
attrs_dict = OrderedDict(zip(gen_attr_names(), attrs))
attrs_dict = {}

names = gen_attr_names()
kwarg_strats = {}
for attr_name, attr_and_strat in zip(gen_attr_names(), attrs_and_strat):
if attr_and_strat[0].kw_only:
if attr_name.startswith("_"):
attr_name = attr_name[1:]
kwarg_strats[attr_name] = attr_and_strat[1]

for ix, (attribute, strat) in enumerate(attrs_and_strat):
name = next(names)
attrs_dict[name] = attribute
if ix % 2 == 1:
# Every third attribute gets an alias, the next attribute name.
alias = next(names)
attribute.alias = alias
name = alias
else:
# No alias.
if name[0] == "_":
name = name[1:]

if attribute.kw_only:
kwarg_strats[name] = strat
note(f"Attributes: {attrs_dict}")

return tuples(
just(make_class("HypAttrsClass", attrs_dict, frozen=frozen)),
Expand Down

0 comments on commit 7b7d250

Please sign in to comment.