Open AadamZ5 opened 3 years ago
This is an equivalent (taken from one of the example classes) that I imagine what the use of some sort of field decorator may look like:
Original example
from graphene import ObjectType, String
class Person(ObjectType):
first_name = String()
last_name = String()
full_name = String()
def resolve_full_name(parent, info):
return f"{parent.first_name} {parent.last_name}"
Decorated example
from graphene import ObjectType, String, field # <- `field` is a new class for decorating.
class Person(ObjectType):
first_name = String()
last_name = String()
@field(type=String)
def full_name(self, info):
"""
The docstring here can be used to implicitly gather the description
"""
return f"{self.first_name} {self.last_name}"
Perhaps the decorator requires you use the native @property
decorator above it first.
...
@property
@field(type=String)
def full_name(self, info):
"""
The docstring here can be used to implicitly gather the description
"""
return f"{self.first_name} {self.last_name}"
It may be worth investigating if the decorator should be compatible with (possibly subclassed from) Python's @property
decorator. Regardless, the implementation would might be mostly similar to this property remake example but as I mentioned in the original example, inherits from Field
found in graphene.types.field
. By inheriting from field, you can treat the field the same as one defined as normal, with just the __get__
method overloaded. However, the complexity arises by adding the decorator parameters as shown in my example. The implementation now differs, and will be detailed in a separate comment drafting an implementation.
My first-draft implementation
class FieldDecorator(Field):
"""
FieldDecorator is a class used to make a property-style attribute a Graphene field.
Warning! This class shouldn't be used as a decorator itself!
"""
def __init__(self, fget: Callable, fset: Callable, fdel: Callable, type_:UnmountedType, args=None, name:str=None, description:str=None, required:bool=None, default_value=None, deprecation_reason:str=None, **extra_args):
self.fget = fget
self.fset = fset
self.fdel = fdel
if description is None and self.fget is not None:
description = getdoc(self.fget)
#We should assume that `fget` is our property getter, and thus is our resolver method.
super(FieldDecorator, self).__init__(type_, args=args, name=name, resolver=self.fget, deprecation_reason=deprecation_reason, description=description, required=required, default_value=default_value, **extra_args)
#The get descriptor will call the function passed to us initially.
def __get__(self, obj, objType=None):
if obj is None:
return self
if self.fget is None:
return AttributeError("Can't get attribute!")
return self.fget(obj)
#The set descriptor will call the function passed to us by using @<prop>.setter
def __set__(self, obj, value):
if self.fset is None:
raise AttributeError("Can't set attribute!")
self.fset(obj, value)
#The delete descriptor will call the function passed to us by using @<prop>.deleter
def __delete__(self, obj):
if self.fdel is None:
raise AttributeError("Can't delete attribute!")
self.fdel(obj)
def getter(self, fget):
required = isinstance(self.type, NonNull)
return type(self)(fget, self.fset, self.fdel, self._type, self.args, self.name, self.description, required, self.default_value, self.deprecation_reason)
def setter(self, fset):
required = isinstance(self.type, NonNull)
return type(self)(self.fget, fset, self.fdel, self._type, self.args, self.name, self.description, required, self.default_value, self.deprecation_reason)
def deleter(self, fdel):
required = isinstance(self.type, NonNull)
return type(self)(self.fget, self.fset, fdel, self._type, self.args, self.name, self.description, required, self.default_value, self.deprecation_reason)
def field_property(func_=None, *a, type_=None, name=None, description=None, required=None, args=None, default_value=None, deprication_reason=None, **extra_args):
"""
Denotes a property-style attribute as a Graphene field of type `type_`
"""
#Check if type_ is supplied and valid
if type_ is None or not isinstance(type_, UnmountedType):
raise TypeError("type_ Must be of type UnmountedType!")
# If `field` is called without any arguments whatsoever, then `func` is implicitly supplied in the call.
# If it is called with keyword arguments, then `func` is no longer implicitly supplied.
if func_ != None:
return FieldDecorator(func_, None, None, type_, args=args, name=name, description=description, required=required, default_value=default_value, deprecation_reason=deprication_reason, **extra_args)
else:
def wrapper(func):
return FieldDecorator(func, None, None, type_, args=args, name=name, description=description, required=required, default_value=default_value, deprecation_reason=deprication_reason, **extra_args)
return wrapper
As I'm trying to make a first-draft implementation - just to try - I find myself stuck with questions of how the Schema builder works.
Field
type used for, and where are it's properties like name
and description
used?As long as the Schema builder uses raw types to build the schema, and not instances, the FieldDecorator will show itself with this logic.
...
def __get__(self, obj, objType=None):
if obj is None:
return self
...
When inspecting a type (not an instance), there is no obj
instance passed in as the implicit first argument, so the class returns itself for inspection.
I have all of these changes in a forked repo branch, and am ready to open a pull request with some basic tests implemented. I'd still like to discuss if this is a feasible and useful feature before moving ahead with a PR.
Thanks!
Is your feature request related to a problem? Please describe. I am trying to learn how to use Graphene with my existing DataModels. The problem I've come across is my use of python's
@property
decorator. My DataModel uses these to ensure proper logic when new attributes are set, and only allowing certain ones to be used. With Graphene's implementation, I am expected to define my properties as attributes instead, which eliminates my ability to use logic when a property is set, or decide which properties can be set at all.The documentation does explain that Graphene objects can be used as real data-retaining objects, but I am not willing to sacrifice my current inheritance and implementation for a simple
ObjectType
class implementation.Additionally, the documentation also explains that any dictionary-like object can be used to lookup attributes with the same name, but now I am tasked with maintaining an additional class just to build the schema. I would prefer to keep my definition as tightly coupled to my actual implementation as I can, meaning I want to define it directly in my data-model.
Describe the solution you'd like I want a way to integrate Graphene's schema building with my model's properties. I would prefer not to write custom resolvers and specially name all of the attributes I want to expose. Perhaps a decorator approach could be used
I imagine this decorator can be a subclass of
Field
fromgraphene.types.field
. Maybe a name like@property_field
better suits this. I am definitely willing to try and open a PR for this, but I wanted to have some initial discussion first. I want to explore this later using a descriptor.Describe alternatives you've considered I am currently not using Django, but I've considered investing more into the Django stack only to be able to use the additional
Meta
class option ofmodel
. I have not done enough research, and adopting the entire Django stack into my program is not what I'd like to do.Additional context I am currently not using Django. I would not like to implement it to fix this problem.
If you can think of a way that works around this issue without implementing a new feature, I'd be happy to discuss. I don't know the ins and outs of Graphene, and I want to express this all very humbly. Please let me know if I'm horribly overlooking something, or you believe my implementation is bonkers. Thanks for your time!