-
Notifications
You must be signed in to change notification settings - Fork 229
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
Improve auto creation of Graphene Enums. #98
Changes from all commits
67a621f
9dd34ca
2f9145c
3143397
8819829
c2c4a77
c278a6f
ec6d4a2
67d789d
082169e
361b44c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ __pycache__/ | |
|
||
# Distribution / packaging | ||
.Python | ||
.venv/ | ||
env/ | ||
build/ | ||
develop-eggs/ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,22 +1,83 @@ | ||
from collections import OrderedDict | ||
|
||
from sqlalchemy.types import Enum as SQLAlchemyEnumType | ||
|
||
from graphene import Enum | ||
|
||
from .utils import to_enum_value_name, to_type_name | ||
|
||
|
||
class Registry(object): | ||
def __init__(self): | ||
def __init__(self, check_duplicate_registration=False): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the legitimate cases where one would want to disable duplicate registration? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This case makes sense to me. I was asking about the legitimate cases where you one does not want this function to raise when there are duplicates. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, ok - good question. Not checking was the behavior until now, and |
||
self.check_duplicate_registration = check_duplicate_registration | ||
self._registry = {} | ||
self._registry_models = {} | ||
self._registry_composites = {} | ||
self._registry_enums = {} | ||
self._registry_sort_params = {} | ||
|
||
def register(self, cls): | ||
from .types import SQLAlchemyObjectType | ||
|
||
assert issubclass(cls, SQLAlchemyObjectType), ( | ||
"Only classes of type SQLAlchemyObjectType can be registered, " | ||
'received "{}"' | ||
).format(cls.__name__) | ||
assert cls._meta.registry == self, "Registry for a Model have to match." | ||
# assert self.get_type_for_model(cls._meta.model) in [None, cls], ( | ||
# 'SQLAlchemy model "{}" already associated with ' | ||
# 'another type "{}".' | ||
# ).format(cls._meta.model, self._registry[cls._meta.model]) | ||
self._registry[cls._meta.model] = cls | ||
if not issubclass(cls, SQLAlchemyObjectType): | ||
raise TypeError( | ||
"Only classes of type SQLAlchemyObjectType can be registered, " | ||
'received "{}"'.format(cls.__name__) | ||
) | ||
if cls._meta.registry != self: | ||
raise TypeError("Registry for a Model have to match.") | ||
|
||
registered_cls = ( | ||
self._registry.get(cls._meta.model) | ||
if self.check_duplicate_registration | ||
else None | ||
) | ||
if registered_cls: | ||
if cls != registered_cls: | ||
raise TypeError( | ||
"Different object types registered for the same model {}:" | ||
" tried to register {}, but {} existed already.".format( | ||
cls._meta.model, cls, registered_cls | ||
) | ||
) | ||
else: | ||
self._registry[cls._meta.model] = cls | ||
|
||
def register_enum(self, name, members): | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because not all enums are derived from SQLAlchemyEnumTypes. Some are created for sorting purposes, they don't exist as SQLAlchemyEnumTypes. All of the enums should use the same registry because Enum names in the schema must be unique - you want to get an error when a sort enum accidentally has the same name as an existing enum for a column, and you want to have a way for requesting registered enums by name. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. How about we instead have this function take an enum or a column that maps to SQLAlchemyEnumType to keep all that logic encapsulated here? In both cases, we would be able to extract a name. Regarding name collisions, the names need to be unique across all types (vs just within Enums). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right. I will think about the whole mechanism again and try to create a better-thought-out solution. But maybe I'll do this as a new PR. |
||
graphene_enum = self._registry_enums.get(name) | ||
if graphene_enum: | ||
registered_members = { | ||
key: value.value | ||
for key, value in graphene_enum._meta.enum.__members__.items() | ||
} | ||
if members != registered_members: | ||
raise TypeError( | ||
'Different enums with the same name "{}":' | ||
" tried to register {}, but {} existed already.".format( | ||
name, members, registered_members | ||
) | ||
) | ||
else: | ||
graphene_enum = Enum(name, members) | ||
self._registry_enums[name] = graphene_enum | ||
return graphene_enum | ||
|
||
def register_sort_params(self, cls, sort_params): | ||
registered_sort_params = ( | ||
self._registry_sort_params.get(cls) | ||
if self.check_duplicate_registration | ||
else None | ||
) | ||
if registered_sort_params: | ||
if registered_sort_params != sort_params: | ||
raise TypeError( | ||
"Different sort args for the same model {}:" | ||
" tried to register {}, but {} existed already.".format( | ||
cls, sort_params, registered_sort_params | ||
) | ||
) | ||
else: | ||
self._registry_sort_params[cls] = sort_params | ||
|
||
def get_type_for_model(self, model): | ||
return self._registry.get(model) | ||
|
@@ -27,6 +88,34 @@ def register_composite_converter(self, composite, converter): | |
def get_converter_for_composite(self, composite): | ||
return self._registry_composites.get(composite) | ||
|
||
def get_type_for_enum(self, sql_type): | ||
if not isinstance(sql_type, SQLAlchemyEnumType): | ||
raise TypeError( | ||
"Only sqlalchemy.Enum objects can be registered as enum, " | ||
'received "{}"'.format(sql_type) | ||
) | ||
enum_class = sql_type.enum_class | ||
if enum_class: | ||
name = enum_class.__name__ | ||
members = OrderedDict( | ||
(to_enum_value_name(key), value.value) | ||
for key, value in enum_class.__members__.items() | ||
) | ||
else: | ||
name = sql_type.name | ||
name = ( | ||
to_type_name(name) | ||
if name | ||
else "Enum{}".format(len(self._registry_enums) + 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm in theory all columns should have a name. I wonder if this can only happen in our tests because SQLAlchemy is not fully initialized. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the name of the SQLAlchemy Enum type, not the name of the column. That name may not exist. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see. What are the benefits of having this function take a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You would need to get the SQLAlchemy enum type anyway with |
||
) | ||
members = OrderedDict( | ||
(to_enum_value_name(key), key) for key in sql_type.enums | ||
) | ||
return self.register_enum(name, members) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why does There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The naming of the methods and division between registry and utils modules could certainly be improved. Need to sleep over it, or we refactor this in a new PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer to address this in this PR because this may lead to unexpected behaviors. For example, as is we are getting different enums for the same un-named sql enum:
Ideally a getter should not have any side effects. Why can't we just return the enum from the registry?
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See below, I'll try to come up with a better solution. |
||
|
||
def get_sort_params_for_model(self, model): | ||
return self._registry_sort_params.get(model) | ||
|
||
|
||
registry = None | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why this rename? You are still converting a column here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The functions must have specific names, because columns with ChoiceType and Enum both convert to Enum. The function name would then be
convert_column_to_enum
for both. Actually the other functions should also have more specific names, i.e. contain not only the target type, but also the source type.