marshmallow icon indicating copy to clipboard operation
marshmallow copied to clipboard

RFC: Use a context variable to pass Schema context

Open lafrech opened this issue 3 years ago • 1 comments

Python 3.7 brings context variables. I've been using this along with a context manager to pass contextual values in a thread-safe way.

https://github.com/Scille/umongo/blob/master/umongo/expose_missing.py https://stackoverflow.com/a/67807219/4653485 https://stackoverflow.com/a/59911117/4653485

This could be suited to pass Schema context and it could provide a better API, allowing a Schema instance to have different contexts in different threads.

This could be done in different ways:

Global context

Context unique to all schema classes.

# Provided by marshmallow in schema.py
CONTEXT = ContextVar("session", default=None)

class Context(AbstractContextManager):
    def __init__(self, context):
        self.context = context

    def __enter__(self):
        self.token = CONTEXT.set(self.context)

    def __exit__(self, *args, **kwargs):
        CONTEXT.reset(self.token)

# User code
class Schema_1(Schema):
    """Schema 1"""

class Schema_2(Schema):
    """Schema 2"""


with Context({... insert context here ...}):
    Schema_1().dump(...)
    Schema_2().dump(...)

The good thing with this is that we don't even have to propagate the context through nested schemas.

The limitation is that the same context is used for both schemas. Is this an issue?

We could provide the context manager from the Schema class (as a static) but it would be ambiguous as it could let the user think the context is specific to that class while it is not the case.

Schema-wise context

One context per schema class.

# Provided by marshmallow in schema.py
class Schema():
    ctx = ContextVar("session", default=None)

    # TODO: Add method here to provide context manager
    # ...

# User code
class Schema_1:
    """Schema 1"""

class Schema_2:
    """Schema 2"""

schema_1 = Schema_1()
schema_2 = Schema_2()

with schema_1.context({... insert context here ...}):
    schema_1.dump(...)   # Uses context
    schema_2.dump(...)  # Does not use context

In this example, there is no reason to put schema_2.dump in the with block but maybe the with is outside the function. The point is it is not affected. Or there may be multiple contexts.

with schema_1.context({... insert context here ...}), schema_2.context({... insert context here ...}):
    schema_1.dump(...)
    schema_2.dump(...)

This requires to propagate the context through nested schemas. Not that nice.

Or we could pass the context on call to load/dump, without a context manager:

# Provided by marshmallow in schema.py
CONTEXT = ContextVar("session", default=None)

class Schema():
    def dump(..., context=None):
        if context is not None:
            # wrap dump work in a context manager to set/unset context

    # TODO: same for load

# User code
class Schema_1:
    """Schema 1"""

schema_1 = Schema_1()

schema_1.dump(data,... context = {... insert context here ...})

This way, the scope is limited to the load/dump operation and there is no possibility to leak from one schema to the other, so there is no need to propagate through nested schemas.


This may be a bit draftish. My gut feeling is that context variables provide an opportunity to rework passing context around in a nicer way.

lafrech avatar Jun 11 '21 21:06 lafrech

This would avoid issues with users sharing schema instances between requests (#1825) and it would solve nested schema issues (#1617).

I like the idea of a global context variable common to all schemas that the user would use by passing context to dump/load methods, ensuring the context is only used by that schema (and only in current thread because context variable).

We could also expose the context manager for the explicit form

with Context({... insert context here ...}):
    Schema_1().dump(...)
    Schema_2().dump(...)

with the limitation that the context is the same for both schemas, but this is not a blocker because 1/ it's obvious 2/ in most cases it is acceptable 3/ if really needed the user doesn't use this form and passes the context in load/dump.

I hope I'm making sense. I don't know when we'll start working on marshmallow 4 and I wanted to write this down quickly before I forget.

lafrech avatar Jun 14 '21 08:06 lafrech