Skip to content
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

Import with DictImporter fails on custom classes when attributes dont match the constructors signature #86

Open
villmow opened this issue Apr 9, 2019 · 2 comments

Comments

@villmow
Copy link

villmow commented Apr 9, 2019

When using custom NodeMixin subclasses with attributes that don't match the constructors signature the import will fail. The DictImporter expects, that all attributes are present in the classes constructor.

See the following code, which reproduces the bug, I copied most of it from the documentation and annotated the changes.

from anytree import NodeMixin
from anytree.render import RenderTree
from anytree.exporter import DictExporter
from anytree.importer import DictImporter

class MyBaseClass(object):
     foo = 4

class MyClass(MyBaseClass, NodeMixin):
    def __init__(self, name, length, width, parent=None, children=None):
        super(MyClass, self).__init__()
        self.name = name
        self.length = length
        self._width = width  # private argument, will fail (does not match the signature)
        self.width2 = width  # name different, would also fail (does not match the signature)
        self.parent = parent
        if children:
            self.children = children

    @property
    def width(self):   #private argument may be a property, but this doesn't matter.
        return self._width


# taken from documentation, create a tree
my0 = MyClass('my0', 0, 0)
my1 = MyClass('my1', 1, 0, parent=my0)
my2 = MyClass('my2', 0, 2, parent=my0)
for pre, _, node in RenderTree(my0):
    treestr = u"%s%s" % (pre, node.name)
    print(treestr.ljust(8), node.length, node.width)

# export works
exporter = DictExporter()
my0_dict = exporter.export(my0)
print(my0_dict)  # dict shows attribute _width

importer = DictImporter(nodecls=MyClass)
my0_new = importer.import_(my0_dict)  # fails
@villmow
Copy link
Author

villmow commented Apr 10, 2019

This seems to be a pretty tricky problem and may not be solved.

There are some seperate cases:

  1. It is never possible to reimport the export of the following class. If not all constructor arguments are saved as instance attributes, it will always fail. See for example the following class. This applies also to renamed attributes.
class MyClass(MyBaseClass, NodeMixin):
    def __init__(self, name, length, parent=None, children=None):
        super(MyClass, self).__init__()
        self.name = name
        # self.length = length  # length is not saved but required during initialization

        self.parent = parent
        if children:
            self.children = children
  1. Not all attributes are in the construcor. See the following code, where the width attribute is computed from the length attribute.
class MyClass(MyBaseClass, NodeMixin):
    def __init__(self, name, length, parent=None, children=None):
        super(MyClass, self).__init__()
        self.name = name
        self.length = length
        self.width = length * 2  # some other argument, which is not in the constructor

        self.parent = parent
        if children:
            self.children = children

# taken from documentation, create a tree
my0 = MyClass('my0', 0)
my1 = MyClass('my1', 1, parent=my0)
my2 = MyClass('my2', 0, parent=my0)

However the call to the constructor of MyClass with the width attribute will produce the following error:

> python anytree_bug.py 
my0      0 0
├── my1  1 2
└── my2  0 0
{'name': 'my0', 'length': 0, 'width': 0, 'children': [{'name': 'my1', 'length': 1, 'width': 2}, {'name': 'my2', 'length': 0, 'width': 0}]}
Traceback (most recent call last):
  File "anytree_bug.py", line 43, in <module>
    my0_new = importer.import_(my0_dict)  # fails
  File "python3.7/site-packages/anytree/importer/dictimporter.py", line 38, in import_
    return self.__import(data)
  File "python3.7/site-packages/anytree/importer/dictimporter.py", line 45, in __import
    node = self.nodecls(parent=parent, **attrs)
TypeError: __init__() got an unexpected keyword argument 'width'

This case can be solved with a smarter attriter in DictExporter. I think this is the most common case and should be fixed in anytree. Here is an iterator that does so:

def matching_attriter(attrs, constructor):
    import inspect
    constructor_signature = inspect.signature(constructor)  # <Signature (name, length, parent=None, children=None)>

    # add all attributes that appear as arguments in the constructors signature
    matched_arguments = {k: v for k, v in attrs if k in constructor_signature.parameters}

    # check if all arguments are matched. Not necessary, but could prevent an error bacause of case 1 later on.
    try:
        constructor_signature.bind(**matched_arguments)
    except TypeError:
        pass

    yield from matched_arguments.items()

from functools import partial

# export works
exporter = DictExporter(attriter=partial(matching_attriter, constructor=my0.__init__))
my0_dict = exporter.export(my0)
print(my0_dict)
importer = DictImporter(nodecls=MyClass)
my0_new = importer.import_(my0_dict)  # works
  1. Arguments are saved as private arguments/properties. Here one could try to remove underscores and then use the matching_attriter. I'm not sure how feasible that is.

@vsuley
Copy link

vsuley commented Apr 4, 2020

Thanks for posting this! I just ran into the same issue. Just to get unblocked, I was wondering if I should just fork it and modify the base class for my own use.

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