Skip to content

Commit b7e4937

Browse files
authored
Alias only_fields as fields and exclude_fields as exclude (#691)
* Create new fields and exclude options that are aliased to exclude_fields and only_fields * Update docs * Add some checking around fields and exclude definitions * Add all fields option * Update docs to include `__all__` option * Actual order of fields is not stable * Update docs/queries.rst Co-Authored-By: Semyon Pupkov <[email protected]> * Fix example code * Format code * Start raising PendingDeprecationWarnings for using only_fields and exclude_fields * Update tests
1 parent a2103c1 commit b7e4937

File tree

8 files changed

+182
-30
lines changed

8 files changed

+182
-30
lines changed

docs/queries.rst

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,18 @@ Full example
4141
return Question.objects.get(pk=question_id)
4242
4343
44-
Fields
45-
------
44+
Specifying which fields to include
45+
----------------------------------
4646

4747
By default, ``DjangoObjectType`` will present all fields on a Model through GraphQL.
48-
If you don't want to do this you can change this by setting either ``only_fields`` and ``exclude_fields``.
48+
If you only want a subset of fields to be present, you can do so using
49+
``fields`` or ``exclude``. It is strongly recommended that you explicitly set
50+
all fields that should be exposed using the fields attribute.
51+
This will make it less likely to result in unintentionally exposing data when
52+
your models change.
4953

50-
only_fields
51-
~~~~~~~~~~~
54+
``fields``
55+
~~~~~~~~~~
5256

5357
Show **only** these fields on the model:
5458

@@ -57,24 +61,35 @@ Show **only** these fields on the model:
5761
class QuestionType(DjangoObjectType):
5862
class Meta:
5963
model = Question
60-
only_fields = ('question_text')
64+
fields = ('id', 'question_text')
65+
66+
You can also set the ``fields`` attribute to the special value ``'__all__'`` to indicate that all fields in the model should be used.
67+
68+
For example:
69+
70+
.. code:: python
71+
72+
class QuestionType(DjangoObjectType):
73+
class Meta:
74+
model = Question
75+
fields = '__all__'
6176
6277
63-
exclude_fields
64-
~~~~~~~~~~~~~~
78+
``exclude``
79+
~~~~~~~~~~~
6580

66-
Show all fields **except** those in ``exclude_fields``:
81+
Show all fields **except** those in ``exclude``:
6782

6883
.. code:: python
6984
7085
class QuestionType(DjangoObjectType):
7186
class Meta:
7287
model = Question
73-
exclude_fields = ('question_text')
88+
exclude = ('question_text',)
7489
7590
76-
Customised fields
77-
~~~~~~~~~~~~~~~~~
91+
Customising fields
92+
------------------
7893

7994
You can completely overwrite a field, or add new fields, to a ``DjangoObjectType`` using a Resolver:
8095

@@ -84,7 +99,7 @@ You can completely overwrite a field, or add new fields, to a ``DjangoObjectType
8499
85100
class Meta:
86101
model = Question
87-
exclude_fields = ('question_text')
102+
fields = ('id', 'question_text')
88103
89104
extra_field = graphene.String()
90105
@@ -178,7 +193,7 @@ When ``Question`` is published as a ``DjangoObjectType`` and you want to add ``C
178193
class QuestionType(DjangoObjectType):
179194
class Meta:
180195
model = Question
181-
only_fields = ('category',)
196+
fields = ('category',)
182197
183198
Then all query-able related models must be defined as DjangoObjectType subclass,
184199
or they will fail to show if you are trying to query those relation fields. You only

graphene_django/filter/tests/test_fields.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -774,7 +774,7 @@ class Meta:
774774
model = Pet
775775
interfaces = (Node,)
776776
filter_fields = {"age": ["exact"]}
777-
only_fields = ["age"]
777+
fields = ("age",)
778778

779779
class Query(ObjectType):
780780
pets = DjangoFilterConnectionField(PetType)

graphene_django/rest_framework/serializer_converter.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ def convert_serializer_field(field, is_input=True):
5757

5858

5959
def convert_serializer_to_input_type(serializer_class):
60-
cached_type = convert_serializer_to_input_type.cache.get(serializer_class.__name__, None)
60+
cached_type = convert_serializer_to_input_type.cache.get(
61+
serializer_class.__name__, None
62+
)
6163
if cached_type:
6264
return cached_type
6365
serializer = serializer_class()

graphene_django/rest_framework/tests/test_multiple_model_serializers.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,12 @@ class MyFakeChildModel(models.Model):
1818
class MyFakeParentModel(models.Model):
1919
name = models.CharField(max_length=50)
2020
created = models.DateTimeField(auto_now_add=True)
21-
child1 = models.OneToOneField(MyFakeChildModel, related_name='parent1', on_delete=models.CASCADE)
22-
child2 = models.OneToOneField(MyFakeChildModel, related_name='parent2', on_delete=models.CASCADE)
21+
child1 = models.OneToOneField(
22+
MyFakeChildModel, related_name="parent1", on_delete=models.CASCADE
23+
)
24+
child2 = models.OneToOneField(
25+
MyFakeChildModel, related_name="parent2", on_delete=models.CASCADE
26+
)
2327

2428

2529
class ParentType(DjangoObjectType):

graphene_django/tests/test_query.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def test_should_query_only_fields():
2828
class ReporterType(DjangoObjectType):
2929
class Meta:
3030
model = Reporter
31-
only_fields = ("articles",)
31+
fields = ("articles",)
3232

3333
schema = graphene.Schema(query=ReporterType)
3434
query = """
@@ -44,7 +44,7 @@ def test_should_query_simplelazy_objects():
4444
class ReporterType(DjangoObjectType):
4545
class Meta:
4646
model = Reporter
47-
only_fields = ("id",)
47+
fields = ("id",)
4848

4949
class Query(graphene.ObjectType):
5050
reporter = graphene.Field(ReporterType)
@@ -289,7 +289,7 @@ class ReporterType(DjangoObjectType):
289289
class Meta:
290290
model = Reporter
291291
interfaces = (Node,)
292-
only_fields = ("articles",)
292+
fields = ("articles",)
293293

294294
class Query(graphene.ObjectType):
295295
all_reporters = DjangoConnectionField(ReporterType)
@@ -329,7 +329,7 @@ class ReporterType(DjangoObjectType):
329329
class Meta:
330330
model = Reporter
331331
interfaces = (Node,)
332-
only_fields = ("articles",)
332+
fields = ("articles",)
333333

334334
class ArticleType(DjangoObjectType):
335335
class Meta:

graphene_django/tests/test_schema.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,6 @@ def test_should_map_only_few_fields():
4848
class Reporter2(DjangoObjectType):
4949
class Meta:
5050
model = Reporter
51-
only_fields = ("id", "email")
51+
fields = ("id", "email")
5252

5353
assert list(Reporter2._meta.fields.keys()) == ["id", "email"]

graphene_django/tests/test_types.py

Lines changed: 89 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,26 +211,113 @@ def inner(*args, **kwargs):
211211

212212
@with_local_registry
213213
def test_django_objecttype_only_fields():
214+
with pytest.warns(PendingDeprecationWarning):
215+
216+
class Reporter(DjangoObjectType):
217+
class Meta:
218+
model = ReporterModel
219+
only_fields = ("id", "email", "films")
220+
221+
fields = list(Reporter._meta.fields.keys())
222+
assert fields == ["id", "email", "films"]
223+
224+
225+
@with_local_registry
226+
def test_django_objecttype_fields():
214227
class Reporter(DjangoObjectType):
215228
class Meta:
216229
model = ReporterModel
217-
only_fields = ("id", "email", "films")
230+
fields = ("id", "email", "films")
218231

219232
fields = list(Reporter._meta.fields.keys())
220233
assert fields == ["id", "email", "films"]
221234

222235

236+
@with_local_registry
237+
def test_django_objecttype_only_fields_and_fields():
238+
with pytest.raises(Exception):
239+
240+
class Reporter(DjangoObjectType):
241+
class Meta:
242+
model = ReporterModel
243+
only_fields = ("id", "email", "films")
244+
fields = ("id", "email", "films")
245+
246+
247+
@with_local_registry
248+
def test_django_objecttype_all_fields():
249+
class Reporter(DjangoObjectType):
250+
class Meta:
251+
model = ReporterModel
252+
fields = "__all__"
253+
254+
fields = list(Reporter._meta.fields.keys())
255+
assert len(fields) == len(ReporterModel._meta.get_fields())
256+
257+
223258
@with_local_registry
224259
def test_django_objecttype_exclude_fields():
260+
with pytest.warns(PendingDeprecationWarning):
261+
262+
class Reporter(DjangoObjectType):
263+
class Meta:
264+
model = ReporterModel
265+
exclude_fields = ["email"]
266+
267+
fields = list(Reporter._meta.fields.keys())
268+
assert "email" not in fields
269+
270+
271+
@with_local_registry
272+
def test_django_objecttype_exclude():
225273
class Reporter(DjangoObjectType):
226274
class Meta:
227275
model = ReporterModel
228-
exclude_fields = "email"
276+
exclude = ["email"]
229277

230278
fields = list(Reporter._meta.fields.keys())
231279
assert "email" not in fields
232280

233281

282+
@with_local_registry
283+
def test_django_objecttype_exclude_fields_and_exclude():
284+
with pytest.raises(Exception):
285+
286+
class Reporter(DjangoObjectType):
287+
class Meta:
288+
model = ReporterModel
289+
exclude = ["email"]
290+
exclude_fields = ["email"]
291+
292+
293+
@with_local_registry
294+
def test_django_objecttype_exclude_and_only():
295+
with pytest.raises(AssertionError):
296+
297+
class Reporter(DjangoObjectType):
298+
class Meta:
299+
model = ReporterModel
300+
exclude = ["email"]
301+
fields = ["id"]
302+
303+
304+
@with_local_registry
305+
def test_django_objecttype_fields_exclude_type_checking():
306+
with pytest.raises(TypeError):
307+
308+
class Reporter(DjangoObjectType):
309+
class Meta:
310+
model = ReporterModel
311+
fields = "foo"
312+
313+
with pytest.raises(TypeError):
314+
315+
class Reporter2(DjangoObjectType):
316+
class Meta:
317+
model = ReporterModel
318+
fields = "foo"
319+
320+
234321
class TestDjangoObjectType:
235322
@pytest.fixture
236323
def PetModel(self):

graphene_django/types.py

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import warnings
12
from collections import OrderedDict
23

34
import six
@@ -24,6 +25,9 @@
2425
from typing import Type
2526

2627

28+
ALL_FIELDS = "__all__"
29+
30+
2731
def construct_fields(
2832
model, registry, only_fields, exclude_fields, convert_choices_to_enum
2933
):
@@ -74,8 +78,10 @@ def __init_subclass_with_meta__(
7478
model=None,
7579
registry=None,
7680
skip_registry=False,
77-
only_fields=(),
78-
exclude_fields=(),
81+
only_fields=(), # deprecated in favour of `fields`
82+
fields=(),
83+
exclude_fields=(), # deprecated in favour of `exclude`
84+
exclude=(),
7985
filter_fields=None,
8086
filterset_class=None,
8187
connection=None,
@@ -109,10 +115,48 @@ def __init_subclass_with_meta__(
109115
)
110116
)
111117

118+
assert not (fields and exclude), (
119+
"Cannot set both 'fields' and 'exclude' options on "
120+
"DjangoObjectType {class_name}.".format(class_name=cls.__name__)
121+
)
122+
123+
# Alias only_fields -> fields
124+
if only_fields and fields:
125+
raise Exception("Can't set both only_fields and fields")
126+
if only_fields:
127+
warnings.warn(
128+
"Defining `only_fields` is deprecated in favour of `fields`.",
129+
PendingDeprecationWarning,
130+
stacklevel=2,
131+
)
132+
fields = only_fields
133+
if fields and fields != ALL_FIELDS and not isinstance(fields, (list, tuple)):
134+
raise TypeError(
135+
'The `fields` option must be a list or tuple or "__all__". '
136+
"Got %s." % type(fields).__name__
137+
)
138+
139+
if fields == ALL_FIELDS:
140+
fields = None
141+
142+
# Alias exclude_fields -> exclude
143+
if exclude_fields and exclude:
144+
raise Exception("Can't set both exclude_fields and exclude")
145+
if exclude_fields:
146+
warnings.warn(
147+
"Defining `exclude_fields` is deprecated in favour of `exclude`.",
148+
PendingDeprecationWarning,
149+
stacklevel=2,
150+
)
151+
exclude = exclude_fields
152+
if exclude and not isinstance(exclude, (list, tuple)):
153+
raise TypeError(
154+
"The `exclude` option must be a list or tuple. Got %s."
155+
% type(exclude).__name__
156+
)
157+
112158
django_fields = yank_fields_from_attrs(
113-
construct_fields(
114-
model, registry, only_fields, exclude_fields, convert_choices_to_enum
115-
),
159+
construct_fields(model, registry, fields, exclude, convert_choices_to_enum),
116160
_as=Field,
117161
)
118162

0 commit comments

Comments
 (0)