diff --git a/Makefile b/Makefile index 4933755f..b4309605 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/dynamic_rest/conf.py b/dynamic_rest/conf.py index 39cad400..12f49c67 100644 --- a/dynamic_rest/conf.py +++ b/dynamic_rest/conf.py @@ -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, {})) diff --git a/dynamic_rest/links.py b/dynamic_rest/links.py index c0743e8b..36b32884 100644 --- a/dynamic_rest/links.py +++ b/dynamic_rest/links.py @@ -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): @@ -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): diff --git a/dynamic_rest/prefetch.py b/dynamic_rest/prefetch.py index 460475ee..6a3b7d93 100644 --- a/dynamic_rest/prefetch.py +++ b/dynamic_rest/prefetch.py @@ -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) diff --git a/dynamic_rest/routers.py b/dynamic_rest/routers.py index f9992b15..f4f3f1f6 100644 --- a/dynamic_rest/routers.py +++ b/dynamic_rest/routers.py @@ -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): """ @@ -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: diff --git a/dynamic_rest/serializers.py b/dynamic_rest/serializers.py index e3e5dd04..75e91e57 100644 --- a/dynamic_rest/serializers.py +++ b/dynamic_rest/serializers.py @@ -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 @@ -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.""" @@ -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 { @@ -564,6 +580,58 @@ 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. @@ -571,6 +639,8 @@ def _faster_to_representation(self, instance): (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 @@ -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] @@ -619,6 +704,7 @@ def _faster_to_representation(self, instance): ) ) else: + # Non-optimized standard DRF approach... try: attribute = field.get_attribute(instance) except SkipField: @@ -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) diff --git a/install_requires.txt b/install_requires.txt index 3c1bf9f9..2bd72c4e 100644 --- a/install_requires.txt +++ b/install_requires.txt @@ -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 diff --git a/tests/test_serializers.py b/tests/test_serializers.py index 057d4d36..164d7fa2 100644 --- a/tests/test_serializers.py +++ b/tests/test_serializers.py @@ -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):