marshmallow icon indicating copy to clipboard operation
marshmallow copied to clipboard

Meta Decorator

Open deckar01 opened this issue 2 years ago • 7 comments

Motivation

I often have lots of small schemas containing unique Meta classes with only a few attributes. The Meta class sometimes requires more lines than the schema definition and makes a file containing multiple schemas harder to read. This is especially true when using marshmallow-jsonapi due to type_.

Proposal

Provide a meta decorator that will inject its kwargs into a Meta class for the class it is wrapping.

Example

Before:

class TestSchema(Schema):
    foo = fields.String()
    bar = fields.String()

    class Meta:
        type_ = 'tests'
        ordered = True

After:

@meta(type_='tests', ordered=True)
class TestSchema(Schema):
    foo = fields.String()
    bar = fields.String()

deckar01 avatar Sep 15 '21 13:09 deckar01

Would it replace or update an existing (inherited) Meta class?

Updating would allow stacking, although I don't really see the use case for stacking.

lafrech avatar Sep 15 '21 13:09 lafrech

Inheritance is another good use case where Meta is likely to only need a few attributes. I was initially imagining spreading the parent meta manually like @meta(..., **vars(ParentSchema.Meta)), but that doesn't actually work. Implicitly inheriting into a new class would be convenient.

A use case for stacking might be if you have enough attributes to justify a meta class, but still preferred the decorator syntax. It would avoid opening and indenting the meta arguments into a block (which a linter might do automatically).

@meta(
    type_='tests',
    include=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'I', 'j', 'k', ...],
)
class TestSchema(Schema):
    foo = fields.String()
    bar = fields.String()
@meta(type_='tests')
@meta(include=['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'I', 'j', 'k', ...])
class TestSchema(Schema):
    foo = fields.String()
    bar = fields.String()

deckar01 avatar Sep 15 '21 14:09 deckar01

I ended up having to subclass the schema to get SchemaMeta.__new__ to rebuild opts from Meta. I am going to test drive this utility for a little while.

def meta(**kwargs):
    def wrapper(schema):
        class _Meta(schema.Meta):
            pass

        for key in kwargs:
            setattr(_Meta, key, kwargs[key])

        class _Schema(schema):
            Meta = _Meta

        _Meta.__name__ = schema.Meta.__name__
        _Schema.__name__ = schema.__name__
        return _Schema

    return wrapper

deckar01 avatar Sep 15 '21 20:09 deckar01

I quite like the ergonomics of this proposal. I've no strong objections to adding this

sloria avatar Oct 17 '21 18:10 sloria

How will this interact with Meta.ordered = True?

When the specific class has set ordered=True, the _declared_fields is set to an OrderedDict() instance with inherited fields listed first, followed by the fields declared on the class directly, in declaration order, followed by any Meta.include pairs.

The issue I can see is that if the current class is inheriting from another Schema with Meta.ordered=False, then by the time the class decorator comes to the scene the _declared_fields dictionary will be a regular dictionary and the directly-declared fields are no longer easily distinguished from the inherited fields or Meta.include fields, especially if the latter overrides some of the class or inherited fields.

mjpieters avatar Oct 21 '21 16:10 mjpieters

I ended up having to subclass the schema to get SchemaMeta.__new__ to rebuild opts from Meta.

This also rebuilds _declared_fields with the new meta options.

deckar01 avatar Oct 21 '21 17:10 deckar01

I published a module for this functionality. I think this should just be a community library for now. Once I cover it with tests I will add it to the wiki and close this issue.

https://github.com/deckar01/marshmallow-meta

deckar01 avatar May 24 '22 20:05 deckar01