Skip to content
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/moar optimisations #311

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ clean: clean_working_directory
@rm -rf $(INSTALL_DIR)

# Run tests
test: install lint
test: install
$(call header,"Running unit tests")
@$(INSTALL_DIR)/bin/py.test --cov=$(APP_NAME) tests/$(TEST)

Expand Down
1 change: 0 additions & 1 deletion dynamic_rest/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,6 @@ def __init__(self, name, defaults, settings, class_attrs=None):
self.defaults = defaults
self.keys = set(defaults.keys())
self.class_attrs = class_attrs

self._cache = {}
self._reload(getattr(settings, self.name, {}))

Expand Down
5 changes: 4 additions & 1 deletion dynamic_rest/links.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from django.utils import six

from dynamic_rest.conf import settings
from dynamic_rest.routers import DynamicRouter


def merge_link_object(serializer, data, instance):
Expand Down Expand Up @@ -32,10 +31,14 @@ def merge_link_object(serializer, data, instance):
if settings.ENABLE_HOST_RELATIVE_LINKS:
# if the resource isn't registered, this will default back to
# using resource-relative urls for links.
base_url = serializer.canonical_base_path or ''
base_url += "/%s/" % instance.pk
'''
base_url = DynamicRouter.get_canonical_path(
serializer.get_resource_key(),
instance.pk
) or ''
'''
link = '%s%s/' % (base_url, name)
# Default to DREST-generated relation endpoints.
elif callable(link):
Expand Down
2 changes: 2 additions & 0 deletions dynamic_rest/prefetch.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@

class FastObject(dict):

IS_FAST = True

def __init__(self, *args, **kwargs):
self.pk_field = kwargs.pop('pk_field', 'id')
return super(FastObject, self).__init__(*args)
Expand Down
14 changes: 9 additions & 5 deletions dynamic_rest/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,14 @@ def register_resource(self, viewset, namespace=None):
# map the resource name to the resource key for easier lookup
resource_name_map[resource_name] = resource_key

@staticmethod
def get_canonical_base_path(resource_key):
if resource_key not in resource_map:
# Note: Maybe raise?
return None

return get_script_prefix() + resource_map[resource_key]['path']

@staticmethod
def get_canonical_path(resource_key, pk=None):
"""
Expand All @@ -254,11 +262,7 @@ def get_canonical_path(resource_key, pk=None):
Returns: Absolute URL as string.
"""

if resource_key not in resource_map:
# Note: Maybe raise?
return None

base_path = get_script_prefix() + resource_map[resource_key]['path']
base_path = DynamicRouter.get_canonical_base_path(resource_key)
if pk:
return '%s/%s/' % (base_path, pk)
else:
Expand Down
122 changes: 105 additions & 17 deletions dynamic_rest/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,19 +10,29 @@
from rest_framework import __version__ as drf_version
from rest_framework import exceptions, fields, serializers
from rest_framework.relations import RelatedField
from rest_framework.fields import SkipField
from rest_framework.fields import (
BooleanField,
CharField,
DateField,
DateTimeField,
FloatField,
IntegerField,
SkipField,
UUIDField,
)
from rest_framework.utils.serializer_helpers import ReturnDict, ReturnList

from dynamic_rest import prefetch
from dynamic_rest.bases import (
CacheableFieldMixin,
DynamicSerializerBase,
resettable_cached_property
)
from dynamic_rest.conf import settings
from dynamic_rest.routers import DynamicRouter
from dynamic_rest.fields import (
DynamicRelationField,
DynamicGenericRelationField,
DynamicMethodField,
DynamicRelationField,
)
from dynamic_rest.links import merge_link_object
from dynamic_rest.meta import get_model_table
Expand Down Expand Up @@ -516,6 +526,12 @@ def is_field_sideloaded(self, field_name):
def get_link_fields(self):
return self._link_fields

@cached_property
def canonical_base_path(self):
return DynamicRouter.get_canonical_base_path(
self.get_resource_key()
)

@resettable_cached_property
def _link_fields(self):
"""Construct dict of name:field for linkable fields."""
Expand Down Expand Up @@ -548,7 +564,7 @@ def _readable_fields(self):
if not field.write_only
]

@cached_property
@resettable_cached_property
def _readable_id_fields(self):
fields = self._readable_fields
return {
Expand All @@ -564,13 +580,67 @@ def _readable_id_fields(self):
)
}

@resettable_cached_property
def _readable_static_fields(self):
return {
field for field in self._readable_fields
if not isinstance(field, (
DynamicGenericRelationField,
DynamicMethodField,
DynamicRelationField
))
} | self._readable_id_fields

@resettable_cached_property
def _simple_fields(self):
"""
Simple fields are fields that return serializable values straight
from the DB and therefore don't require logic on the model or Field
object to extract and serialize.
"""
if hasattr(self.Meta, 'simple_fields'):
return set(getattr(self.Meta, 'simple_fields', []))

if not hasattr(self, '_declared_fields'):
# The `_declared_fields` attr should be set by DRF, but since
# it's a private attribute, we'll be safe.
return []

# We assume inferred fields of these types to be "simple"
simple_field_classes = (
BooleanField, # also the base class for NullBooleanField
CharField, # also the base class for others, like SlugField
# DateField,
# DateTimeField,
FloatField,
IntegerField,
UUIDField,
)

# This meta attr can explicitly opt fields out, e.g. if it's a
# compatible field type but actually needs to go thru DRF Field
complex_fields = set(getattr(self.Meta, 'complex_fields', []))

simple_fields = set()
for name, field in six.iteritems(self.get_all_fields()):
if name in self._declared_fields:
continue
if name in complex_fields:
continue
if isinstance(field, simple_field_classes):
simple_fields.add(name)

return simple_fields

def _faster_to_representation(self, instance):
"""Modified to_representation with optimizations.

1) Returns a plain old dict as opposed to OrderedDict.
(Constructing ordered dict is ~100x slower than `{}`.)
2) Ensure we use a cached list of fields
(this optimization exists in DRF 3.2 but not 3.1)
3) Bypass DRF whenever possible, use simple dict lookup
if FastQuery is enabled (which returns dicts).

Arguments:
instance: a model instance or data object
Expand All @@ -581,26 +651,41 @@ def _faster_to_representation(self, instance):
ret = {}
fields = self._readable_fields

is_fast = isinstance(instance, prefetch.FastObject)
is_fast = getattr(instance, 'IS_FAST', False)
id_fields = self._readable_id_fields

# static fields are non-Dynamic fields
static_fields = self._readable_static_fields

# fields declared as being "simple" (i.e. doesn't require
# field.to_representation() to be serializable)
simple_field_names = self._simple_fields

for field in fields:
attribute = None

# we exclude dynamic fields here because the proper fastquery
# dereferencing happens in the `get_attribute` method now
if (
is_fast and
not isinstance(
field,
(DynamicGenericRelationField, DynamicRelationField)
)
):
if field in id_fields and field.source not in instance:
# TODO - make better.
attribute = instance.get(field.source + '_id')
ret[field.field_name] = attribute
continue
if (is_fast and field in static_fields):
if field in id_fields:
if field.source not in instance:
# TODO - make better.
attribute = instance.get(field.source + '_id')
ret[field.field_name] = attribute
continue
elif not instance[field.source]:
ret[field.field_name] = None
continue
elif 'id' in instance[field.source]:
# reverse of o2o field
print("Beep beep! s=%s f=%s" % (
self.__class__,
field.field_name
))
ret[field.field_name] = instance[field.source]['id']
continue
else:
attribute = field.get_attribute(instance)
else:
try:
attribute = instance[field.source]
Expand All @@ -619,6 +704,7 @@ def _faster_to_representation(self, instance):
)
)
else:
# Non-optimized standard DRF approach...
try:
attribute = field.get_attribute(instance)
except SkipField:
Expand All @@ -628,6 +714,8 @@ def _faster_to_representation(self, instance):
# We skip `to_representation` for `None` values so that
# fields do not have to explicitly deal with that case.
ret[field.field_name] = None
elif field.field_name in simple_field_names:
ret[field.field_name] = attribute
else:
ret[field.field_name] = field.to_representation(attribute)

Expand Down
2 changes: 1 addition & 1 deletion install_requires.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
Django>=1.11,<3.0.0
djangorestframework>=3.8.0,<3.12.0
inflection==0.4.0
inflection>=0.3.0,<0.5.0
requests
9 changes: 9 additions & 0 deletions tests/test_serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,15 @@ def test_serializer_propagation_consistency(self):
self.assertEqual(r1, r2)
self.assertEqual(r2, r3)

def test_simple_fields(self):
# Should return non-declared non-dynamic fields
# See WithDynamicSerializerMixin._simple_fields for more.
szr = LocationSerializer()
self.assertEqual(
set(['id', 'name']),
szr._simple_fields
)

@patch.dict('dynamic_rest.processors.POST_PROCESSORS', {})
def test_post_processors(self):

Expand Down