django-fastdrf 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. django_fastdrf-0.1.0.dist-info/METADATA +214 -0
  2. django_fastdrf-0.1.0.dist-info/RECORD +44 -0
  3. django_fastdrf-0.1.0.dist-info/WHEEL +4 -0
  4. django_fastdrf-0.1.0.dist-info/licenses/LICENSE +28 -0
  5. django_fastdrf-0.1.0.dist-info/licenses/NOTICE +8 -0
  6. fastdrf/__init__.py +3 -0
  7. fastdrf/_classify.py +210 -0
  8. fastdrf/_compiled.py +131 -0
  9. fastdrf/_field_cache.py +158 -0
  10. fastdrf/_field_copy.py +244 -0
  11. fastdrf/_field_options.py +68 -0
  12. fastdrf/_inspection.py +25 -0
  13. fastdrf/_relations.py +206 -0
  14. fastdrf/apps.py +22 -0
  15. fastdrf/checks.py +108 -0
  16. fastdrf/codecs.py +97 -0
  17. fastdrf/compat.py +7 -0
  18. fastdrf/compiler.py +1681 -0
  19. fastdrf/convert.py +1327 -0
  20. fastdrf/inputs.py +891 -0
  21. fastdrf/list_serializers.py +112 -0
  22. fastdrf/management/__init__.py +0 -0
  23. fastdrf/management/commands/__init__.py +0 -0
  24. fastdrf/management/commands/fastdrf_convert.py +105 -0
  25. fastdrf/management/commands/fastdrf_inspect_serializers.py +257 -0
  26. fastdrf/mixins.py +124 -0
  27. fastdrf/msgspec/__init__.py +36 -0
  28. fastdrf/msgspec/compiler.py +220 -0
  29. fastdrf/msgspec/parsers.py +29 -0
  30. fastdrf/msgspec/renderers.py +87 -0
  31. fastdrf/msgspec/serializers.py +364 -0
  32. fastdrf/output.py +189 -0
  33. fastdrf/prefetch.py +315 -0
  34. fastdrf/py.typed +0 -0
  35. fastdrf/pydantic/__init__.py +21 -0
  36. fastdrf/pydantic/compiler.py +80 -0
  37. fastdrf/pydantic/serializers.py +341 -0
  38. fastdrf/renderers.py +84 -0
  39. fastdrf/response.py +259 -0
  40. fastdrf/serializers.py +228 -0
  41. fastdrf/settings.py +135 -0
  42. fastdrf/typed.py +705 -0
  43. fastdrf/utils.py +193 -0
  44. fastdrf/views.py +445 -0
@@ -0,0 +1,214 @@
1
+ Metadata-Version: 2.5
2
+ Name: django-fastdrf
3
+ Version: 0.1.0
4
+ Summary: Opt-in serializer and query optimizations for synchronous Django REST framework.
5
+ Project-URL: Homepage, https://github.com/ctolon/django-fastdrf
6
+ Project-URL: Documentation, https://github.com/ctolon/django-fastdrf/tree/main/docs
7
+ Project-URL: Changelog, https://github.com/ctolon/django-fastdrf/blob/main/CHANGELOG.md
8
+ Project-URL: Issues, https://github.com/ctolon/django-fastdrf/issues
9
+ Project-URL: Source, https://github.com/ctolon/django-fastdrf
10
+ Author: Cevat Batuhan Tolon
11
+ License-Expression: BSD-3-Clause
12
+ License-File: LICENSE
13
+ License-File: NOTICE
14
+ Keywords: django,django-rest-framework,msgspec,performance,serializers
15
+ Classifier: Development Status :: 3 - Alpha
16
+ Classifier: Environment :: Web Environment
17
+ Classifier: Framework :: Django
18
+ Classifier: Framework :: Django :: 5.2
19
+ Classifier: Framework :: Django :: 6.0
20
+ Classifier: Framework :: Django :: 6.1
21
+ Classifier: Intended Audience :: Developers
22
+ Classifier: Operating System :: OS Independent
23
+ Classifier: Programming Language :: Python :: 3
24
+ Classifier: Programming Language :: Python :: 3 :: Only
25
+ Classifier: Programming Language :: Python :: 3.12
26
+ Classifier: Programming Language :: Python :: 3.13
27
+ Classifier: Programming Language :: Python :: 3.14
28
+ Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
29
+ Classifier: Topic :: Internet :: WWW/HTTP
30
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
31
+ Classifier: Typing :: Typed
32
+ Requires-Python: >=3.12
33
+ Requires-Dist: django>=5.2
34
+ Requires-Dist: djangorestframework>=3.16
35
+ Provides-Extra: msgspec
36
+ Requires-Dist: msgspec>=0.19; extra == 'msgspec'
37
+ Provides-Extra: pydantic
38
+ Requires-Dist: pydantic>=2.9; extra == 'pydantic'
39
+ Description-Content-Type: text/markdown
40
+
41
+ # django-fastdrf
42
+
43
+ Opt-in serializer, query and response optimizations for synchronous
44
+ [Django REST framework](https://www.django-rest-framework.org/) projects.
45
+
46
+ django-fastdrf is for DRF projects that stay synchronous (WSGI, or ASGI with
47
+ synchronous views) and want less time per request without leaving DRF. It
48
+ adds subclasses, mixins and settings next to DRF's own classes. Each
49
+ optimization is enabled explicitly, per project, per view or per serializer,
50
+ and falls back to DRF's code wherever it cannot produce DRF's result.
51
+
52
+ It does not replace or patch any Django or DRF class, add async support, or
53
+ change DRF's validation errors.
54
+
55
+ ## Installation
56
+
57
+ ```console
58
+ pip install django-fastdrf
59
+ ```
60
+
61
+ The distribution is `django-fastdrf`; the import name is `fastdrf`. Two
62
+ extras install the optional backends:
63
+
64
+ ```console
65
+ pip install "django-fastdrf[msgspec]" # msgspec backend, renderer, parser, codec
66
+ pip install "django-fastdrf[pydantic]" # pydantic backend, schema serializers, codec
67
+ ```
68
+
69
+ Adding `"fastdrf"` to `INSTALLED_APPS` is optional. It registers system checks
70
+ and two management commands; everything else works without it.
71
+
72
+ ## Quick start
73
+
74
+ Use fastdrf's serializer bases and choose a backend for their output:
75
+
76
+ ```python
77
+ # settings.py
78
+ FASTDRF = {
79
+ "SERIALIZER_BACKEND": "msgspec", # or "pydantic", or "python" (no dependency)
80
+ "CACHE_SERIALIZER_FIELDS": True,
81
+ "FIELD_COPY_MODE": "compiled",
82
+ }
83
+ ```
84
+
85
+ ```python
86
+ # serializers.py
87
+ from fastdrf import serializers
88
+
89
+
90
+ class ArticleSerializer(serializers.ModelSerializer):
91
+ class Meta:
92
+ model = Article
93
+ fields = ["id", "title", "author", "published_at"]
94
+ auto_prefetch = True
95
+ ```
96
+
97
+ ```python
98
+ # views.py
99
+ from rest_framework import viewsets
100
+
101
+ from fastdrf.views import DispatchOptimizationMixin, QueryOptimizationMixin
102
+
103
+
104
+ class ArticleViewSet(
105
+ DispatchOptimizationMixin, QueryOptimizationMixin, viewsets.ModelViewSet
106
+ ):
107
+ queryset = Article.objects.all()
108
+ serializer_class = ArticleSerializer
109
+ ```
110
+
111
+ `.data`, `.is_valid()`, `.save()`, `many=True`, routers, pagination,
112
+ permissions and error responses are DRF's. With `"fastdrf"` in
113
+ `INSTALLED_APPS`, this command lists which serializers the backend compiles,
114
+ and why the others stay on DRF:
115
+
116
+ ```console
117
+ python manage.py fastdrf_inspect_serializers
118
+ ```
119
+
120
+ ## Features
121
+
122
+ Serialization:
123
+
124
+ - Compiled serializer output with the msgspec, pydantic or dependency-free
125
+ `python` backend, equal to DRF's `.data` in the default strict parity mode
126
+ ([serializers](https://github.com/ctolon/django-fastdrf/blob/main/docs/serializers.md#output-backends)).
127
+ - Input recognition: JSON input that DRF would accept unchanged is validated
128
+ by a compiled class; everything else, including every error, is DRF's
129
+ ([input recognition](https://github.com/ctolon/django-fastdrf/blob/main/docs/serializers.md#input-recognition)).
130
+ - Cached field templates, copied per request with `deepcopy`, a scalar clone
131
+ or a compiled copy plan
132
+ ([field caching](https://github.com/ctolon/django-fastdrf/blob/main/docs/serializers.md#field-caching-and-copying)).
133
+ - List serializers whose child refers to the list weakly
134
+ ([list serializers](https://github.com/ctolon/django-fastdrf/blob/main/docs/serializers.md#list-serializers)).
135
+ - Schema serializers: a msgspec `Struct` or a pydantic model as a DRF
136
+ serializer, and `SchemaViewMixin` for views
137
+ ([schema serializers](https://github.com/ctolon/django-fastdrf/blob/main/docs/schema-serializers.md)).
138
+
139
+ Queries:
140
+
141
+ - `select_related` and `prefetch_related` derived from the serializer by
142
+ `QueryOptimizationMixin`, explicit `Meta.prefetch` hints, Django 6.1's
143
+ `FETCH_MODE`, one query for many-valued primary-key input, and
144
+ `PrefetchListSerializer` for per-list enrichment
145
+ ([queries](https://github.com/ctolon/django-fastdrf/blob/main/docs/queries.md)).
146
+
147
+ Views and responses:
148
+
149
+ - Dispatch mixins that keep content negotiation and request construction
150
+ between requests, compiled create and update responses, a `Response` that
151
+ releases its request objects when closed, and `DataResponse`, rendered
152
+ without DRF's template response
153
+ ([views and responses](https://github.com/ctolon/django-fastdrf/blob/main/docs/views.md)).
154
+ - A `JSONRenderer` that keeps its encoder, msgspec's JSON renderer and
155
+ parser, and msgspec and pydantic codecs for Django's Redis cache
156
+ ([rendering and codecs](https://github.com/ctolon/django-fastdrf/blob/main/docs/rendering.md)).
157
+
158
+ Tooling:
159
+
160
+ - System checks for the `FASTDRF` setting and two management commands:
161
+ `fastdrf_inspect_serializers` and `fastdrf_convert`, which writes a schema
162
+ for a serializer or a serializer for a schema
163
+ ([commands](https://github.com/ctolon/django-fastdrf/blob/main/docs/commands.md)).
164
+
165
+ ## Guarantees and limits
166
+
167
+ - No Django or DRF class is replaced, patched or monkeypatched. fastdrf's
168
+ classes are subclasses and mixins that a project selects.
169
+ - Everything is opt-in. With the default settings, fastdrf's serializer bases
170
+ behave as DRF's.
171
+ - In strict parity, compiled output equals DRF's output; a field, value or
172
+ hook the compiler cannot prove equal keeps the serializer, or that one
173
+ source, on DRF. Input recognition never produces an error of its own.
174
+ - Synchronous only: no ORM call becomes asynchronous, and transactions,
175
+ authentication, permissions, throttling and pagination are DRF's and
176
+ Django's.
177
+ - Schema serializers, `fast` parity and the msgspec renderer have their own
178
+ documented output and validation rules; they are not DRF-identical by
179
+ design.
180
+ - A serializer instance belongs to one request; do not share it between
181
+ threads.
182
+
183
+ See [architecture](https://github.com/ctolon/django-fastdrf/blob/main/docs/architecture.md)
184
+ for how the caches are bounded and invalidated, and for the deliberate
185
+ differences.
186
+
187
+ ## Compatibility
188
+
189
+ | Django | DRF | Python |
190
+ | --- | --- | --- |
191
+ | 5.2 | 3.16, 3.17, 3.18 | 3.12, 3.13, 3.14 |
192
+ | 6.0 | 3.17, 3.18 | 3.12, 3.13, 3.14 |
193
+ | 6.1 | 3.18 | 3.12, 3.13, 3.14 |
194
+
195
+ The test suite also runs on free-threaded Python 3.14t (Django 6.1,
196
+ DRF 3.18) and at the declared minimum versions (Django 5.2, DRF 3.16,
197
+ msgspec 0.19, pydantic 2.9). `FETCH_MODE` needs Django 6.1.
198
+
199
+ ## Documentation
200
+
201
+ - [Documentation index](https://github.com/ctolon/django-fastdrf/blob/main/docs/README.md)
202
+ - [Configuration reference](https://github.com/ctolon/django-fastdrf/blob/main/docs/configuration.md)
203
+ - [Example project](https://github.com/ctolon/django-fastdrf/blob/main/examples/blog/README.md):
204
+ the same API with plain DRF and with fastdrf, with parity tests and a
205
+ measurement script
206
+ - [Changelog](https://github.com/ctolon/django-fastdrf/blob/main/CHANGELOG.md)
207
+ - [Contributing](https://github.com/ctolon/django-fastdrf/blob/main/CONTRIBUTING.md)
208
+ - [Security policy](https://github.com/ctolon/django-fastdrf/blob/main/SECURITY.md)
209
+
210
+ ## License
211
+
212
+ BSD 3-Clause. See
213
+ [LICENSE](https://github.com/ctolon/django-fastdrf/blob/main/LICENSE) and
214
+ [NOTICE](https://github.com/ctolon/django-fastdrf/blob/main/NOTICE): the optimizations derive from the aiodrf project, and the list-construction protocol from Django REST framework.
@@ -0,0 +1,44 @@
1
+ fastdrf/__init__.py,sha256=5I3edBDLCmaTShByretrBMm6FtyNFqPlAywvfmXj3fU,99
2
+ fastdrf/_classify.py,sha256=K4mvUSJ8RlwvAwZpNxCdaJcxwchYI-k2YlmMY9cYWcQ,8067
3
+ fastdrf/_compiled.py,sha256=IGePq5r-TXOWacY90MN_KYWH84s2KSONDNRd6gOs0gU,4631
4
+ fastdrf/_field_cache.py,sha256=zOWIef98mMlrcGT_4i47K5abpcBCjj-nfcan8X9Moik,5525
5
+ fastdrf/_field_copy.py,sha256=HTUiH2maQMae_sOexSsMnxCEt6zJAWHnvB63byBLEY4,8938
6
+ fastdrf/_field_options.py,sha256=zBg0PXBlY5SILY4sL8DxK8IiFOXRimsfE-uxAdxzqeY,2772
7
+ fastdrf/_inspection.py,sha256=ll0m5IJUl0aE18TfoCi9RS0rShGG1erAzPvyJ3WvqVA,659
8
+ fastdrf/_relations.py,sha256=qMkgKQVOKl3bFgLLB6NAPZsfAFs2l0x81NZEFEvvZOM,7851
9
+ fastdrf/apps.py,sha256=MrWUij4rMWQeRi7HcqV9x_3TDvUP9mq3C0nk7Z0mWlg,718
10
+ fastdrf/checks.py,sha256=P311eRZecAP0AP8m70GxwTKohWvkzHHB0A6ueXNkkQ8,4184
11
+ fastdrf/codecs.py,sha256=6cprANo93rmc7vx3uZcqeNTEIDrCZtbwWmMlC19oBmg,3613
12
+ fastdrf/compat.py,sha256=WjVmr7qawlkIDYHqAU1rTIJRmFvdrUuYRXWzYIvFdFk,309
13
+ fastdrf/compiler.py,sha256=CDnljlEY3ylKO7tRZgbPaRwUXurAnjW2tf0XVuq1Ch4,66465
14
+ fastdrf/convert.py,sha256=3Fny7qZu8rVX1NDxfDm26BfyuOh0ZaySEe8f9-mk25g,48396
15
+ fastdrf/inputs.py,sha256=NPA3CinEez8n53GXJIAudcNeytrJrnFUl9G7OKcIASI,32656
16
+ fastdrf/list_serializers.py,sha256=Xx8lXG4xaBUaM7iboE4yx6nwEpp60NadRAdK_t8eX6Q,4315
17
+ fastdrf/mixins.py,sha256=zMxSm1HgkMmVWo4gNlvvO-KJwJ7KszR4dfNdewHDsPI,4477
18
+ fastdrf/output.py,sha256=182k315wkYn-of8T8QEvGnVV6ZTz97stBNAw2WS2lXw,5700
19
+ fastdrf/prefetch.py,sha256=FQwnjdDISsfKNcaMqO6M4H4ggsezz--x-P6rv236HZM,11645
20
+ fastdrf/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
+ fastdrf/renderers.py,sha256=SF_PwbZ25Ppp9-DfnxqoWI0_ffMuYQBGCqQXADxUI3M,3348
22
+ fastdrf/response.py,sha256=rdQxxT_5H8ctbDatZCzSF3-7tPITB7ZaGnVOFW3zrpk,9584
23
+ fastdrf/serializers.py,sha256=uQhtnYGRNndgjPkHY8nNGja6cjbWP7Rf3JkevlUiTQ8,6842
24
+ fastdrf/settings.py,sha256=S8jfn5dx8y5U4aCvXnFadhW3vD1olDhr4wx2T3TdOuk,4375
25
+ fastdrf/typed.py,sha256=LLljN-olgYWPeYW5IfueKbnKMZGjt6TE2JBBflLKXgA,28120
26
+ fastdrf/utils.py,sha256=yJ0p93NzyFj5KuW8GUTeKdoODBiWwDYEkTMYK_54gco,6200
27
+ fastdrf/views.py,sha256=e3RxOSler5VRxsA6eL3MAg-iHeJ-J3cc50Wencyw6Zk,16156
28
+ fastdrf/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
29
+ fastdrf/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
30
+ fastdrf/management/commands/fastdrf_convert.py,sha256=gF8YJ4NPL_DX3ToUte27ggNU5oh6yvaLD31ffHXdchs,3967
31
+ fastdrf/management/commands/fastdrf_inspect_serializers.py,sha256=SuGsOMxRUoBQh00fB_wChJzim9ccE2i2ETXQCj-KSEc,11069
32
+ fastdrf/msgspec/__init__.py,sha256=P1_WRWIhV3RQBbtzdMeWK2W05qjcfdibFeduxKygX4w,985
33
+ fastdrf/msgspec/compiler.py,sha256=9YbUvhy3ZOjDAcc1-2jduJTu6NdfDr586BgSv7A0vEg,8225
34
+ fastdrf/msgspec/parsers.py,sha256=Bw29XHdkv1onYuMTehfY6GajjiYdWgnbCoB_0g5wsxM,1067
35
+ fastdrf/msgspec/renderers.py,sha256=euFS4ybf4NMlAxkurvL9pxeeYlMDe29TIbtriED27gc,3795
36
+ fastdrf/msgspec/serializers.py,sha256=DocG9PCtpR49Nlos2VGVk9PZbvRh5p9_csV1hTS3HWQ,12897
37
+ fastdrf/pydantic/__init__.py,sha256=Wkl4T1sUef_tvTEetoTkqV1dDd1iKwm7shlJwtRCqe4,639
38
+ fastdrf/pydantic/compiler.py,sha256=3r64qZHcE6HBshX1bRAZWwYn-V7GyGAwhkTaH5IopCc,2836
39
+ fastdrf/pydantic/serializers.py,sha256=jwy2On3dFYEgVyUzQDQqX3NTMkJ1obKU0sJEmASkAko,12268
40
+ django_fastdrf-0.1.0.dist-info/METADATA,sha256=yF-gNJ7S-NesjRLtd7adUDTwaVliDqy53MBX_1a843I,8860
41
+ django_fastdrf-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
42
+ django_fastdrf-0.1.0.dist-info/licenses/LICENSE,sha256=rLcfHn1I1iqXETdM-Wi-o3r5l3WbYvqSE_bX-QV74IY,1493
43
+ django_fastdrf-0.1.0.dist-info/licenses/NOTICE,sha256=bpZ9XXWwEQOxItXGwBQXzW_OEPzav_1yHKZSHMc8oWY,494
44
+ django_fastdrf-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,28 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, ctolon
4
+
5
+ Redistribution and use in source and binary forms, with or without
6
+ modification, are permitted provided that the following conditions are met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright notice, this
9
+ list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ 3. Neither the name of the copyright holder nor the names of its
16
+ contributors may be used to endorse or promote products derived from
17
+ this software without specific prior written permission.
18
+
19
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
22
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
23
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
25
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
26
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
27
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,8 @@
1
+ django-fastdrf includes code derived from aiodrf, Copyright (c) ctolon,
2
+ distributed under the BSD 3-Clause license included in LICENSE.
3
+
4
+ The many_init list-construction protocol derives from Django REST framework:
5
+ Copyright (c) 2011-present, Encode OSS Ltd. All rights reserved.
6
+ Redistribution and use in source and binary forms, with or without modification,
7
+ are permitted under the BSD 3-Clause license in LICENSE. The original DRF
8
+ project is https://github.com/encode/django-rest-framework.
fastdrf/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Synchronous, explicitly selected extensions to Django REST framework."""
2
+
3
+ __version__ = "0.1.0"
fastdrf/_classify.py ADDED
@@ -0,0 +1,210 @@
1
+ """Which serializers are a function of their class; coroutine hooks refused."""
2
+
3
+ import inspect
4
+ import weakref
5
+ from inspect import iscoroutinefunction
6
+
7
+ from django.db import models
8
+ from django.utils.choices import CallableChoiceIterator
9
+ from rest_framework import fields, relations, serializers
10
+
11
+ from fastdrf._inspection import _FIELD_HOOKS, _PLAIN_KWARGS
12
+ from fastdrf.utils import (
13
+ class_cache,
14
+ depends_on_classification,
15
+ is_framework_class,
16
+ user_defines,
17
+ )
18
+
19
+ # Exact DRF field classes, defined in DRF's modules: a class of the
20
+ # project's, or one a module only imported, may bind or represent otherwise.
21
+ _BUILTIN_FIELDS = frozenset(
22
+ value
23
+ for module in (fields, relations)
24
+ for value in vars(module).values()
25
+ if inspect.isclass(value) and value.__module__ == module.__name__
26
+ )
27
+
28
+
29
+ @depends_on_classification
30
+ @class_cache
31
+ def _model_fields_call_code(model):
32
+ """
33
+ Return True if building a serializer field from a field of ``model`` may
34
+ call the project's code: callable ``choices`` or ``limit_choices_to``
35
+ (commonly a query), a ``limit_choices_to`` applied through the related
36
+ model's default manager of the project's (its ``get_queryset()``), a
37
+ model field class of the project's, whose attributes DRF reads, or a
38
+ FilePathField, which lists its directory.
39
+ """
40
+ for field in model._meta.get_fields():
41
+ if isinstance(field, models.FilePathField) or not is_framework_class(
42
+ type(field)
43
+ ):
44
+ return True
45
+ if isinstance(getattr(field, "choices", None), CallableChoiceIterator):
46
+ return True
47
+ limit = getattr(getattr(field, "remote_field", None), "limit_choices_to", None)
48
+ if callable(limit) or (
49
+ limit and user_defines(field.related_model._default_manager, "get_queryset")
50
+ ):
51
+ return True
52
+ return False
53
+
54
+
55
+ # What DRF sets on a serializer instance over a class attribute of the same
56
+ # name. Any other instance attribute that shadows one of the class may change
57
+ # what DRF does: a method assigned to the instance, fields materialized (and
58
+ # perhaps edited), a ``Meta`` or ``url_field_name`` of its own.
59
+ _DRF_SHADOWS = frozenset({"_creation_counter", "initial", "default_empty_html"})
60
+
61
+ # Declared serializers are deep-copied into every instance, which builds them
62
+ # again from their arguments; these hooks could make the copy differ.
63
+ _BUILD_HOOKS = (*_FIELD_HOOKS, "bind", "__deepcopy__", "__new__")
64
+
65
+ # class -> the names an instance must not shadow, or None when it is not static
66
+ _static_classes: weakref.WeakKeyDictionary[type, frozenset[str] | None] = (
67
+ weakref.WeakKeyDictionary()
68
+ )
69
+ depends_on_classification(_static_classes)
70
+
71
+
72
+ def is_static(serializer):
73
+ """
74
+ Return True if the fields of ``serializer`` (the child of a list
75
+ serializer), and those of the serializers nested in it, are a function
76
+ of its class.
77
+
78
+ That holds for an instance built with the usual arguments only that
79
+ shadows nothing of its class (no fields materialized, no method assigned),
80
+ of a class that builds its fields with DRF's code alone from declarations
81
+ and a model, whose declared fields are DRF's, children included, and
82
+ whose declared serializers are static too. The compiler, the input
83
+ recognizer and the classification below keep per-class answers for these
84
+ alone.
85
+ """
86
+ if isinstance(serializer, serializers.ListSerializer):
87
+ serializer = serializer.child
88
+ names = _instance_shadow_names(type(serializer))
89
+ return (
90
+ names is not None
91
+ and _PLAIN_KWARGS.issuperset(serializer._kwargs)
92
+ and names.isdisjoint(vars(serializer))
93
+ )
94
+
95
+
96
+ def _instance_shadow_names(cls):
97
+ """
98
+ The names an instance of ``cls`` must not set for its fields to be those
99
+ of its class (:func:`is_static`), or None when they never are.
100
+ """
101
+ try:
102
+ return _static_classes[cls]
103
+ except KeyError:
104
+ pass
105
+ meta = getattr(cls, "Meta", None)
106
+ model = getattr(meta, "model", None)
107
+ static = (
108
+ issubclass(cls, serializers.Serializer)
109
+ # A child may build fields from its parent's context, or change them
110
+ # when bound.
111
+ and not getattr(meta, "depth", 0)
112
+ and not user_defines(cls, *_BUILD_HOOKS)
113
+ # The project's code, run whenever a ModelSerializer builds its fields.
114
+ and not (
115
+ issubclass(cls, serializers.ModelSerializer)
116
+ and model is not None
117
+ and _model_fields_call_code(model)
118
+ )
119
+ and all(_static_declaration(field) for field in cls._declared_fields.values())
120
+ )
121
+ names = frozenset(dir(cls)) - _DRF_SHADOWS if static else None
122
+ # Racing threads store the same value.
123
+ return _static_classes.setdefault(cls, names)
124
+
125
+
126
+ def _static_declaration(field):
127
+ if isinstance(field, serializers.ListSerializer):
128
+ return not user_defines(field, *_BUILD_HOOKS) and _static_declaration(
129
+ field.child
130
+ )
131
+ if isinstance(field, serializers.BaseSerializer):
132
+ return _instance_shadow_names(type(field)) is not None
133
+ # DRF's own fields; a custom one may bind differently per parent. DRF's
134
+ # collections and to-many relations bind the child they were declared with.
135
+ return type(field) in _BUILTIN_FIELDS and all(
136
+ _static_declaration(child)
137
+ for child in (
138
+ getattr(field, "child", None),
139
+ getattr(field, "child_relation", None),
140
+ )
141
+ if child is not None
142
+ )
143
+
144
+
145
+ # What DRF materializes on an instance the first time it is read. Once any of
146
+ # these exists, the instance's fields may have been changed; the class says
147
+ # nothing about such an instance.
148
+ _MATERIALIZED = frozenset(
149
+ {"fields", "_validators", "_readable_fields", "_writable_fields"}
150
+ )
151
+
152
+ # Static serializer class -> whether its representation has a coroutine hook.
153
+ _class_representation: weakref.WeakKeyDictionary[type, bool] = (
154
+ weakref.WeakKeyDictionary()
155
+ )
156
+ depends_on_classification(_class_representation)
157
+
158
+
159
+ def _classified_by_class(serializer):
160
+ """
161
+ Whether :func:`has_async_representation` of ``serializer`` is a property
162
+ of its class: its fields are (:func:`is_static`), none exists yet, and
163
+ nothing callable was set on the instance.
164
+ """
165
+ state = vars(serializer)
166
+ return (
167
+ isinstance(serializer, serializers.Serializer)
168
+ and is_static(serializer)
169
+ and _MATERIALIZED.isdisjoint(state)
170
+ # ``default`` is ``fields.empty``, a class, on every instance.
171
+ and not any(
172
+ callable(value) and not isinstance(value, type) for value in state.values()
173
+ )
174
+ )
175
+
176
+
177
+ def has_async_representation(serializer):
178
+ """
179
+ Whether representing ``serializer`` reaches a coroutine function: an
180
+ ``async def to_representation`` of a serializer or field, or an
181
+ ``ato_representation`` / ``adata`` of the project's. Such a serializer is
182
+ not represented by a synchronous plan. A static serializer is answered
183
+ once per class, without building its fields again.
184
+ """
185
+ if not _classified_by_class(serializer):
186
+ return _async_representation(serializer)
187
+ cls = type(serializer)
188
+ try:
189
+ return _class_representation[cls]
190
+ except KeyError:
191
+ pass
192
+ # Racing threads store the same value.
193
+ return _class_representation.setdefault(cls, _async_representation(serializer))
194
+
195
+
196
+ def _async_representation(serializer):
197
+ if isinstance(serializer, serializers.ListSerializer):
198
+ return has_async_representation(serializer.child)
199
+ if not isinstance(serializer, serializers.Serializer):
200
+ return iscoroutinefunction(type(serializer).to_representation)
201
+ if iscoroutinefunction(
202
+ getattr(type(serializer), "to_representation", None)
203
+ ) or user_defines(serializer, "ato_representation", "adata"):
204
+ return True
205
+ return any(
206
+ has_async_representation(field)
207
+ if isinstance(field, serializers.BaseSerializer)
208
+ else iscoroutinefunction(type(field).to_representation)
209
+ for field in serializer.fields.values()
210
+ )
fastdrf/_compiled.py ADDED
@@ -0,0 +1,131 @@
1
+ """``serializer.data`` from a compiled class, for fastdrf's serializer bases."""
2
+
3
+ from django.db import models
4
+ from rest_framework import serializers
5
+
6
+ from fastdrf._classify import (
7
+ _instance_shadow_names,
8
+ has_async_representation,
9
+ is_static,
10
+ )
11
+ from fastdrf._inspection import _PLAIN_KWARGS
12
+ from fastdrf.settings import fastdrf_settings
13
+ from fastdrf.utils import user_defines
14
+
15
+ #: Set by :mod:`fastdrf.mixins` on a serializer that a generic view built,
16
+ #: validated and saved with framework code alone (:func:`fields_from_class`).
17
+ FIELDS_FROM_CLASS = "_fastdrf_fields_from_class"
18
+
19
+
20
+ def fields_from_class(serializer):
21
+ """
22
+ :func:`~fastdrf._classify.is_static` for a serializer whose fields exist
23
+ on the instance because framework code built and used them: a generic
24
+ view's serializer after validation and the save, when no
25
+ ``get_serializer*`` or ``perform_*`` of the project's and no method of
26
+ the serializer's class could have edited them. Only the write mixins of
27
+ :mod:`fastdrf.mixins` mark such an instance (:data:`FIELDS_FROM_CLASS`).
28
+ """
29
+ if not vars(serializer).get(FIELDS_FROM_CLASS, False):
30
+ return False
31
+ if isinstance(serializer, serializers.ListSerializer):
32
+ serializer = serializer.child
33
+ return _instance_shadow_names(type(serializer)) is not None and (
34
+ _PLAIN_KWARGS.issuperset(serializer._kwargs)
35
+ )
36
+
37
+
38
+ def compiled_data(serializer):
39
+ """
40
+ ``serializer.data`` produced by a compiled class, when
41
+ ``SERIALIZER_BACKEND`` (or ``Meta.serializer_backend``) asks for one and
42
+ the serializer compiles (see :mod:`fastdrf.compiler`), None for
43
+ DRF's code.
44
+ """
45
+ source = serializer.instance
46
+ if (
47
+ (
48
+ fastdrf_settings.SERIALIZER_BACKEND == "drf"
49
+ and not _declares_backend(serializer)
50
+ )
51
+ or user_defines(serializer, "data")
52
+ or source is None
53
+ or getattr(serializer, "_errors", None)
54
+ or hasattr(serializer, "_data")
55
+ ):
56
+ return None
57
+ from fastdrf.compiler import compiled_for, declines_source
58
+
59
+ # A static serializer is looked up by its class before it is classified:
60
+ # classifying one with nested serializers builds all their fields, which
61
+ # is what the compiled class saves.
62
+ if not is_static(serializer) and has_async_representation(serializer):
63
+ return None
64
+ encoder = compiled_for(serializer)
65
+ if encoder is None:
66
+ return None
67
+ if not isinstance(serializer, serializers.ListSerializer) and declines_source(
68
+ serializer, source
69
+ ):
70
+ return None
71
+ return _producer(serializer, encoder, source)
72
+
73
+
74
+ def _producer(serializer, encoder, source):
75
+ if isinstance(serializer, serializers.ListSerializer):
76
+
77
+ def produce(serializer):
78
+ from fastdrf.compiler import declines_source
79
+
80
+ items = (
81
+ source.all()
82
+ if isinstance(source, models.manager.BaseManager)
83
+ else source
84
+ )
85
+ items = list(items)
86
+ if declines_source(serializer, items):
87
+ # DRF's ListSerializer.to_representation, on the items read.
88
+ serializer._data = serializer.to_representation(items)
89
+ return serializer.data
90
+ try:
91
+ serializer._data = encoder.dump_many(items, serializer.context)
92
+ except encoder.error as exc:
93
+ serializer._data = _unreadable(serializer, exc, items)
94
+ return serializer.data
95
+
96
+ else:
97
+
98
+ def produce(serializer):
99
+ try:
100
+ serializer._data = encoder.dump(source, serializer.context)
101
+ except encoder.error as exc:
102
+ serializer._data = _unreadable(serializer, exc, source)
103
+ return serializer.data
104
+
105
+ return produce
106
+
107
+
108
+ def _unreadable(serializer, error, source):
109
+ """
110
+ DRF's representation of a source the compiled class failed to read, or
111
+ the backend's ``error`` when that is the result
112
+ (:func:`fastdrf.compiler.unreadable_source`). The backends take any
113
+ exception raised while reading an attribute for a missing attribute;
114
+ DRF's own read raises it as itself.
115
+ """
116
+ from fastdrf.compiler import unreadable_source
117
+
118
+ if not unreadable_source(serializer, error):
119
+ raise error
120
+ return serializer.to_representation(source)
121
+
122
+
123
+ def _declares_backend(serializer):
124
+ target = (
125
+ serializer.child
126
+ if isinstance(serializer, serializers.ListSerializer)
127
+ else serializer
128
+ )
129
+ return (
130
+ getattr(getattr(target, "Meta", None), "serializer_backend", None) is not None
131
+ )