Skip to content

Structure when input is already partially structured #78

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
nathanielford opened this issue Dec 3, 2019 · 3 comments
Open

Structure when input is already partially structured #78

nathanielford opened this issue Dec 3, 2019 · 3 comments

Comments

@nathanielford
Copy link

  • cattrs version: 0.9.0
  • Python version: 3.7.5
  • Operating System: Mac OSX

Description

I am attempting to structure a blob of data into a recursive structure, where part of the blob has already been structured. When it reaches a key that already has a structured value, it errors out. After reading through the documentation it's unclear how to get around this issue.

What I would expect to happen is if a given key already has (valid) structured data, it includes it as is, rather than attempting to (and failing to) structure it.

I'm not sure if I am missing an obvious workaround (converter or hook?), or if this would require a PR to run a check of some sort before attempting to structure a sub-element.

What I Did

This is a minimal example:

from attr import dataclass
from cattr import structure, unstructure

@dataclass(auto_attribs=True)
class B:
    name: str


@dataclass(auto_attribs=True)
class A:
    name: str
    subber: B


dx = {
    "name": "alphas",
    "subber": {
        "name": "subclass"
    }
}

a = structure(dx, A)
print(a)
b = structure(dx.get("subber"), B)
print(b)
dy = {
    "name": "tau",
    "subber": unstructure(b)
}
a2 = structure(dy, A)
print(a2)
dz = {
    "name": "omega",
    "subber": b
}
a3 = structure(dz, A)

The above produces the following output:

A(name='alphas', subber=B(name='subclass'))
B(name='subclass')
A(name='tau', subber=B(name='subclass'))
Traceback (most recent call last):
  File "/Users/nford/Library/Preferences/IntelliJIdea2019.2/scratches/dc.py", line 46, in <module>
    a3 = structure(dz, A)
  File "/Users/nford/echelon/rolevp/echelon/.venv/lib/python3.7/site-packages/cattr/converters.py", line 178, in structure
    return self._structure_func.dispatch(cl)(obj, cl)
  File "/Users/nford/echelon/rolevp/echelon/.venv/lib/python3.7/site-packages/cattr/converters.py", line 298, in structure_attrs_fromdict
    conv_obj[name] = dispatch(type_)(val, type_)
  File "/Users/nford/echelon/rolevp/echelon/.venv/lib/python3.7/site-packages/cattr/converters.py", line 285, in structure_attrs_fromdict
    conv_obj = obj.copy()  # Dict of converted parameters.
AttributeError: 'B' object has no attribute 'copy'

But what I am hoping to achieve is:

A(name='alpha', subber=B(name='subclass'))
B(name='subclass')
A(name='tau', subber=B(name='subclass'))
A(name='omega', subber=B(name='subclass'))
@kevin-d-omara
Copy link

Hi nathanielford, I faced this same issue and was able to solve it by doing a simple subclass of Converter and overriding the structure_attrs_fromdict method. You could override the structure_attrs_fromtuple method too, if you needed it:

from typing import Mapping, Type, Any
import cattr

class PartialConverter(cattr.Converter):
    """
    A subclass of Converter that lets you structure data that is already partially structured.
    
    Usage:
        PartialConverter().structure(partly_structured_data, FooClass)
    """

    def structure_attrs_fromdict(self, obj: Mapping, cl: Type) -> Any:
        if hasattr(obj, "__attrs_attrs__"):
            # Don't structure the object if it is already an attrs class instance.
            return obj
        else:
            return super().structure_attrs_fromdict(obj, cl)

It passes for your example above:

from attr import dataclass


# Re-direct the structure/unstructure methods to the PartialConverter:
partial_converter = PartialConverter()
structure = partial_converter.structure
unstructure = partial_converter.unstructure


@dataclass(auto_attribs=True)
class B:
    name: str


@dataclass(auto_attribs=True)
class A:
    name: str
    subber: B


dx = {
    "name": "alphas",
    "subber": {
        "name": "subclass"
    }
}

a = structure(dx, A)
print(a)
b = structure(dx.get("subber"), B)
print(b)
dy = {
    "name": "tau",
    "subber": unstructure(b)
}
a2 = structure(dy, A)
print(a2)
dz = {
    "name": "omega",
    "subber": b
}
a3 = structure(dz, A)

Which outputs:

A(name='alphas', subber=B(name='subclass'))
B(name='subclass')
A(name='tau', subber=B(name='subclass'))

I tend to write a lot of documentation, so here's a version of PartialConverter with more verbose documentation:

class PartialConverter(cattr.Converter):
    """
    A subclass of Converter that lets you structure data that is already partially structured.
    """

    def structure_attrs_fromdict(self, obj: Mapping, cl: Type) -> Any:
        """
        Instantiate an attrs class from a mapping (dict) of primitives and/or attrs classes.

        The parent version of this method can only structure primitive data (i.e. int, str, bool, list, etc.),
        whereas this version can also structure data that contains attrs classes. For example:
            partly_structured = {"leader": Person(name="foo")}
            cattr.structure_attrs_fromdict(partly_structured, Team)  # AttributeError:
                                                                     # 'Person' object has no attribute 'copy'
            PartialConverter().structure_attrs_fromdict(partly_structured, Team)  # Team(leader=Person("foo"))

        :param obj: A dictionary that maps strings to Python primitives (int, str, list, etc.) and/or attrs classes.
                    Note: the type hint is "Mapping" instead of "Dict" because this is what the parent method uses.
        :param cl: The attrs class type to structure the data into (i.e. a Python class that is decorated with @attrs).
        :return: An attrs class of the specified type, with the data contained in the provided object.
        """
        if hasattr(obj, "__attrs_attrs__"):
            # Don't structure the object if it is already an attrs class instance.
            return obj
        else:
            return super().structure_attrs_fromdict(obj, cl)

@nathanielford
Copy link
Author

This was super useful! I had started down a similar path, and this pushed me to a good solution. Thanks for posting it!

@kevin-d-omara
Copy link

You're very welcome! I'm so glad I could share.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants