genomoncology / related

Nested Object Models in Python with dictionary, YAML, and JSON transformation support
MIT License
198 stars 15 forks source link

Nested/recursive models with self-reference #18

Closed mmmeri closed 5 years ago

mmmeri commented 5 years ago

I am trying to parse a nested JSON structure, where each mapping can hold a sequence of children of the same model/type. Since it is not possible to reference the class itself inside the class variables, I was hoping, that I can work around this by adding the class variable outside of the class definition. But somehow, this does not appear to work: See minimal "working" example at the bottom.

Is there a (preferable) way to do this?

Thanks a lot for your work on this package - I really dig it :)

Example

import related

@related.mutable
class Node(object):
    name = related.StringField()

Node.nodes = related.SequenceField(Node)

original_json = '''
{
    "name": "root",
    "nodes": [{
            "name": "childA"
        }, {
            "name": "childB"
        }
    ]
}'''

json_dict = related.from_json(original_json)
node_data = related.to_model(Node, json_dict)
print(node_data.nodes)

with the following output

_CountingAttr(counter=11, _default=NOTHING, repr=False, cmp=True, hash=None, init=True, metadata={'key': None})
mmmeri commented 5 years ago

Ok, I fiddled around a bit and got to a solution on this, but with some caveats (see comments in example below). The solution is based on a class decorator which will dynamically add the desired field to the class, before constructing the class itself. Metaclasses won't work because attrs does not support them. I could not find a better name for the decorator than selfnested.

Also, I do not know if this has any implications... :see_no_evil: Do you have any opinion on this?

Thanks!

import related

def selfnested(field_name, field_type):
    def decorator(cls):
        setattr(cls, field_name, field_type(
            cls,
            required=False                   # Needs to be false
        ))
        return cls
    return decorator

@related.mutable                             # Needs to be mutable
@selfnested('nodes', related.SequenceField)
class Node(object):
    name = related.StringField()

original_json = '''
{
    "name": "root",
    "nodes": [{
            "name": "childA",
            "nodes": [{
               "name": "childC"
            }]
        }, {
            "name": "childB",
            "nodes": [{
               "name": "childD"
            }]
        }
    ]
}'''

json_dict = related.from_json(original_json)
node_data = related.to_model(Node, json_dict)
assert(node_data.nodes[0].name == 'childA')
assert(node_data.nodes[1].name == 'childB')
assert(node_data.nodes[0].nodes[0].name == 'childC')
assert(node_data.nodes[1].nodes[0].name == 'childD')
imaurer commented 5 years ago

Thanks @lmNt for bringing this up. Always wanted to add self-referencing and out-of-order referencing, but wanted to handle it like the Django ORM does it, with strings instead of other tricks.

I implemented a flavor of this in a "self-reference" branch. Here is the unit test example:

https://github.com/genomoncology/related/tree/5b2fae06cf291c15834b5bc26200e66c93cf9af5/tests/ex08_self_reference

Take a look and let me know if it meets your needs.

Note you need to provide a full reference to the model for it to work because I use importlib to dereference the model class from it's name.

mmmeri commented 5 years ago

Sorry for my late reply, I was on holiday and did not had a chance to check this out until today.

This looks great to me and works as expected :) Thank you very much for the super quick implementation of this feature.