-
-
Notifications
You must be signed in to change notification settings - Fork 450
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
Fix bugs when using resolvers with dictionaries #872
Fix bugs when using resolvers with dictionaries #872
Conversation
Hi @OtherBarry I've been checking this for a while, and I think there more issues than wins:
but as for making easier backwards compatibility - maybe context should notify the stage at which resolver is working class Data(Schema):
@staticmethod
def resolve_name(obj, context: Optional[dict]):
return ... currently
maybe the way it should be:
that way with is_response flag you can achive previous behaviour Let me know what you think |
@vitalik thanks for taking a look at this.
I'm not sure this is really the case. In a standard flow, there's only one extra if statement (a simple isinstance check) and one extra dictionary get. I would be surprised if this made a noticeable impact on performance, but very happy to run some benchmarks or something to compare if you can point me to them.
The primary motivation for this was input vs output. The resolvers are intended for converting more complex server side data structures into simpler json for the client. It doesn't make sense to me for a client to send json that doesn't match the OpenAPI schema, and have that automatically resolved into something else without them having any knowledge of it, so having some way to skip that is nice. Agreed that having it be decided by dicts / not-dicts isn't the best way to do it, but the logic was that inputs will always be dicts, and outputs (in my experience at least) are almost always not dicts. I think the context flags you mention are a better way to do it. I'd also note that most users not having a clue about something would preclude half the features of most libraries/programming languages.
100% agree, that's why
Big fan of this. Ideally there'd still be a way to skip resolving at a Schema level, but to be fair I've never had a schema with more than a couple of resolvers, so not really a problem. To be honest, the biggest part of this for me now is the dict to object wrapper. It doesn't make sense to me to have resolvers that are going to be run on input (dicts) and output (likely not dicts), if there's no unified way to access the data. Without this for every value you want to get in the resolver, you'd have to check the input type: e.g. @classmethod
def resolve_value(cls, obj):
if isinstance(obj, dict):
foo = obj["foo"]
bar = obj["bar"]
else:
foo = obj.foo
bar = obj.bar
return f"{foo} {bar}" Compared to this with the dict wrapper: @classmethod
def resolve_value(cls, obj):
return f"{obj.foo} {obj.bar}" It also works nicer with type hinting, as you can type hint it as the expected object, and it'll work whether it's a dict or the expected object. If you're happy with the above, I can replace the |
well the thing is that when handle response - it's not always that you can get an object - you are pretty free to return custom list of dicts : @api.get('/test', response=list[Some])
def test(request):
qs = SomeModel.objects.all()
return [
{
"id": item.id,
"some-special": item.some_function(), # <-- here the key has a "-"
} for item in qs
] so guaranteeing an object(non-dict) in On the other hand you can use your _DictToObjectWrapper object inside resolver @classmethod
def resolve_value(cls, obj):
obj = _DictToObjectWrapper(obj) # if isinstance dict ?
return f"{obj.foo} {obj.bar}" or create decorator that will enforce this def force_obj_decorator(func):
def wrapper(*args, **kwargs):
obj = _DictWrapper(args[0])
return func(obj, **kwargs)
return wrapper
class Data(Schema):
x: int
@staticmethod
@force_obj_decorator # <<<-------------
def resolve_x(obj, context):
return obj.x+ 1 and even make some metaclass that will apply this decorator for all resolvers: from ninja.schema import ResolverMetaclass
class DictToObjMeta(ResolverMetaclass):
def __new__(cls, name, bases, namespace, **kwargs):
result_cls = super().__new__(cls, name, bases, namespace, **kwargs)
for attr, resolver in result_cls._ninja_resolvers.items():
resolver._func = force_obj_decorator(resolver._func) # <---- !!!!!!!!!!!
return result_cls
class Data(Schema, metaclass=DictToObjMeta) # <---- !!!!!!!!!!!
x: int
@staticmethod
def resolve_x(obj, context):
print("resolve_x", obj, context)
return obj.x + 1 |
The non attribute accessible parameters are the primary issue I guess, which there's no perfect solution to. I'm fine to leave that part out as well then, as long as there's a good explanation in the docs. Given how much it stumped me, I can see this being confusing behaviour for new users (or maybe I'm just a bit slow 😅). |
Allows skipping resolvers by adding
always_resolve = False
to a schema config, which will not run any resolvers if theGetterDict
object is a dictionary. This is still a breaking change, asalways_resolve
defaults toTrue
, but this was kept to ensure that new features with context are easy to use.Additionally, this now wraps dicts passed to resolvers in a
_DictToObjectWrapper
instance, allowing resolvers to always use attributes. As having dicts or objects passed to resolvers meant resolvers would only work for input or output, otherwise they would have convolutedly handle accessing data from a parameter that could be an object or a dict.Closes #871