Coverage for tests/test_generation/test_partial_types.py: 100%
109 statements
« prev ^ index » next coverage.py v7.2.7, created at 2024-08-27 18:25 +0000
« prev ^ index » next coverage.py v7.2.7, created at 2024-08-27 18:25 +0000
1# pyright: reportUnusedFunction=false
3import subprocess
4from typing_extensions import Literal
6import pytest
8from prisma._compat import PYDANTIC_V2
10from ..utils import Testdir
12SCHEMA = """
13datasource db {{
14 provider = "postgres"
15 url = env("DB_URL")
16}}
18generator db {{
19 provider = "coverage run -m prisma"
20 output = "{output}"
21 {options}
22}}
24model Post {{
25 id String @id @default(cuid())
26 created_at DateTime @default(now())
27 updated_at DateTime @updatedAt
28 title String
29 published Boolean
30 desc String?
31 meta Json?
32 comments Comment[]
33 author_id String
34 author User @relation(fields: [author_id], references: [id])
35 thumbnail Bytes?
36}}
38model Comment {{
39 id String @id @default(cuid())
40 created_at DateTime @default(now())
41 updated_at DateTime @updatedAt
42 content String
43 post Post? @relation(fields: [post_id], references: [id])
44 post_id String?
45}}
47model User {{
48 id String @id @default(cuid())
49 created_at DateTime @default(now())
50 updated_at DateTime @updatedAt
51 name String
52 bytes Bytes
53 bytes_list Bytes[]
54 posts Post[]
55}}
57model Foo {{
58 id String @id @default(cuid())
59 text String
60}}
61"""
64@pytest.mark.parametrize(
65 'location,options',
66 [
67 ('prisma/partial_types.py', ''),
68 (
69 'prisma/partial_types.py',
70 'partial_type_generator = "prisma/partial_types.py"',
71 ),
72 (
73 'scripts/partials_generator.py',
74 'partial_type_generator = "scripts.partials_generator"',
75 ),
76 ],
77)
78def test_partial_types(testdir: Testdir, location: str, options: str) -> None:
79 """Grouped tests for partial types to improve test speed"""
81 def tests() -> None: # mark: filedef
82 import sys
83 import datetime
84 from typing import Set, Dict, Type, Tuple, TypeVar, Iterator, Optional
86 from pydantic import BaseModel
88 from prisma import Base64
89 from prisma._compat import (
90 PYDANTIC_V2,
91 get_args,
92 is_union,
93 get_origin,
94 model_parse,
95 model_fields,
96 model_field_type,
97 is_field_required,
98 )
99 from prisma.partials import ( # type: ignore[attr-defined]
100 PostOnlyId, # pyright: ignore
101 UserBytesList, # pyright: ignore
102 PostNoRelations, # pyright: ignore
103 PostWithoutDesc, # pyright: ignore
104 PostRequiredDesc, # pyright: ignore
105 UserModifiedPosts, # pyright: ignore
106 PostModifiedAuthor, # pyright: ignore
107 PostRequiredAuthor, # pyright: ignore
108 PostOptionalInclude, # pyright: ignore
109 PostOptionalPublished, # pyright: ignore
110 PostNoRelationsAndExclude, # pyright: ignore
111 )
113 base_fields = {
114 'id': True,
115 'created_at': True,
116 'updated_at': True,
117 'title': True,
118 'published': True,
119 'desc': False,
120 'meta': False,
121 'comments': False,
122 'author': False,
123 'author_id': True,
124 'thumbnail': False,
125 }
127 _T = TypeVar('_T')
128 _T2 = TypeVar('_T2')
130 def common_entries(dct: Dict[str, _T], other: Dict[str, _T2]) -> Iterator[Tuple[str, _T, _T2]]:
131 for key in dct.keys() & other.keys():
132 yield key, dct[key], other[key]
134 def assert_expected(
135 model: Type[BaseModel],
136 fields: Dict[str, bool],
137 removed: Optional[Set[str]],
138 ) -> None:
139 for _, required, info in common_entries(fields, model_fields(model)):
140 assert is_field_required(info) == required
142 if removed is None:
143 removed = set()
145 assert fields.keys() - model_fields(model).keys() == removed
147 def test_without_desc() -> None:
148 """Removing one field"""
149 assert_expected(PostWithoutDesc, base_fields, {'desc'})
151 def test_optional_published() -> None:
152 """Making one field optional"""
153 assert_expected(
154 PostOptionalPublished,
155 fields={**base_fields, 'published': False},
156 removed=None,
157 )
159 def test_required_desc() -> None:
160 """Making one field required"""
161 assert_expected(
162 PostRequiredDesc,
163 fields={**base_fields, 'desc': True},
164 removed=None,
165 )
167 def test_required_relational_author() -> None:
168 """Making relational field required"""
169 assert_expected(
170 PostRequiredAuthor,
171 fields={**base_fields, 'author': True},
172 removed=None,
173 )
175 def test_only_id() -> None:
176 """Removing all fields except from one"""
177 assert_expected(
178 PostOnlyId,
179 fields=base_fields,
180 removed={
181 'created_at',
182 'updated_at',
183 'title',
184 'published',
185 'desc',
186 'meta',
187 'comments',
188 'author',
189 'author_id',
190 'thumbnail',
191 },
192 )
194 def test_optional_include() -> None:
195 """Both including and making optional on the same field"""
196 assert_expected(
197 PostOptionalInclude,
198 fields={**base_fields, 'title': False},
199 removed={
200 'id',
201 'created_at',
202 'updated_at',
203 'published',
204 'desc',
205 'meta',
206 'comments',
207 'author',
208 'author_id',
209 'thumbnail',
210 },
211 )
213 def test_modified_relational_type() -> None:
214 """Changing one-to-one relational field type"""
215 assert_expected(PostModifiedAuthor, base_fields, None)
217 field = model_fields(PostModifiedAuthor)['author']
218 type_ = model_field_type(field)
219 assert type_ is not None
221 if PYDANTIC_V2:
222 assert is_union(get_origin(type_) or type_)
224 inner_type, _ = get_args(type_)
225 assert inner_type.__name__ == 'UserOnlyName'
226 assert inner_type.__module__ == 'prisma.partials'
227 else:
228 assert type_.__name__ == 'UserOnlyName'
229 assert type_.__module__ == 'prisma.partials'
231 def test_exclude_relations() -> None:
232 """Removing all relational fields using `exclude_relations`"""
233 assert_expected(
234 PostNoRelations,
235 base_fields,
236 removed={
237 'author',
238 'comments',
239 },
240 )
242 def test_exclude_relations_and_others() -> None:
243 """Removing all relational fields using `exclude_relations` in combination with `exclude`"""
244 assert_expected(
245 PostNoRelationsAndExclude,
246 base_fields,
247 removed={
248 'title',
249 'author',
250 'comments',
251 },
252 )
254 def test_modified_relational_list_type() -> None:
255 """Changing one-to-many relation field type"""
256 UserModifiedPosts(
257 id='1',
258 name='Robert',
259 created_at=datetime.datetime.now(datetime.timezone.utc),
260 updated_at=datetime.datetime.now(datetime.timezone.utc),
261 posts=[PostOnlyId(id='2')],
262 )
263 field = model_fields(UserModifiedPosts)['posts']
264 type_ = model_field_type(field)
265 assert type_ is not None
267 if PYDANTIC_V2:
268 assert is_union(get_origin(type_) or type_)
270 posts_type, none_type = get_args(type_)
272 assert none_type.__name__ == 'NoneType'
274 assert posts_type.__module__ == 'typing'
275 if sys.version_info >= (3, 7):
276 assert posts_type._name == 'List'
277 else:
278 assert posts_type.__name__ == 'List'
280 items_type = get_args(posts_type)[0]
281 assert items_type.__name__ == 'PostOnlyId'
282 assert items_type.__module__ == 'prisma.partials'
283 else:
284 assert type_.__name__ == 'PostOnlyId'
285 assert type_.__module__ == 'prisma.partials'
287 if sys.version_info >= (3, 7):
288 assert field.outer_type_._name == 'List' # type: ignore
289 else:
290 assert field.outer_type_.__name__ == 'List'
292 assert field.outer_type_.__module__ == 'typing' # type: ignore
294 def test_bytes() -> None:
295 """Ensure Base64 fields can be used"""
296 # mock prisma behaviour
297 model = model_parse(
298 UserBytesList,
299 {
300 'bytes': str(Base64.encode(b'bar')),
301 'bytes_list': [
302 str(Base64.encode(b'foo')),
303 str(Base64.encode(b'baz')),
304 ],
305 },
306 )
307 assert model.bytes == Base64.encode(b'bar')
308 assert model.bytes_list == [
309 Base64.encode(b'foo'),
310 Base64.encode(b'baz'),
311 ]
313 def generator() -> None: # mark: filedef
314 from prisma.models import Post, User
316 User.create_partial('UserOnlyName', include={'name'})
318 Post.create_partial(
319 'PostWithoutDesc',
320 exclude=['desc'], # pyright: ignore
321 ) # pyright: ignore
322 Post.create_partial('PostOptionalPublished', optional=['published'])
323 Post.create_partial(
324 'PostRequiredDesc',
325 required=['desc'], # pyright: ignore
326 ) # pyright: ignore
327 Post.create_partial('PostOnlyId', include={'id'})
328 Post.create_partial('PostOptionalInclude', include={'title'}, optional={'title'})
329 Post.create_partial('PostRequiredAuthor', required=['author'])
330 Post.create_partial('PostModifiedAuthor', relations={'author': 'UserOnlyName'})
331 Post.create_partial('PostNoRelations', exclude_relational_fields=True)
332 Post.create_partial(
333 'PostNoRelationsAndExclude',
334 exclude={'title'},
335 exclude_relational_fields=True,
336 )
338 User.create_partial(
339 'UserModifiedPosts',
340 exclude={'bytes', 'bytes_list'}, # type: ignore
341 relations={'posts': 'PostOnlyId'},
342 )
343 User.create_partial(
344 'UserBytesList',
345 include={'bytes', 'bytes_list'}, # type: ignore
346 ) # pyright: ignore
348 testdir.make_from_function(generator, name=location)
349 testdir.generate(SCHEMA, options)
350 testdir.make_from_function(tests)
351 testdir.runpytest().assert_outcomes(passed=11)
354@pytest.mark.parametrize('argument', ['exclude', 'include', 'required', 'optional'])
355def test_partial_types_incorrect_key(
356 testdir: Testdir,
357 argument: Literal['exclude', 'include', 'required', 'optional'],
358) -> None:
359 """Invalid field name raises error"""
361 def generator() -> None: # mark: filedef
362 from prisma.models import Post
364 Post.create_partial('PostWithoutFoo', **{argument: ['foo']}) # type: ignore
366 testdir.make_from_function(generator, name='prisma/partial_types.py', argument=argument)
368 with pytest.raises(subprocess.CalledProcessError) as exc:
369 testdir.generate(SCHEMA)
371 assert 'foo is not a valid Post / PostWithoutFoo field' in str(exc.value.output)
374def test_partial_types_same_required_and_optional(testdir: Testdir) -> None:
375 """Making the same field required and optional raises an error"""
377 def generator() -> None: # mark: filedef
378 from prisma.models import Post
380 Post.create_partial(
381 'PostPartial',
382 required={'desc', 'published', 'title'}, # pyright: ignore
383 optional={
384 'desc', # pyright: ignore
385 'published',
386 },
387 )
389 testdir.make_from_function(generator, name='prisma/partial_types.py')
391 with pytest.raises(subprocess.CalledProcessError) as exc:
392 testdir.generate(SCHEMA)
394 assert 'Cannot make the same field(s) required and optional' in str(exc.value.output)
397def test_partial_types_excluded_required(testdir: Testdir) -> None:
398 """Excluding and requiring the same field raises an error"""
400 def generator() -> None: # mark: filedef
401 from prisma.models import Post
403 Post.create_partial(
404 'PostPartial',
405 exclude={'desc'}, # pyright: ignore
406 required={
407 'desc', # pyright: ignore
408 },
409 )
411 testdir.make_from_function(generator, name='prisma/partial_types.py')
413 with pytest.raises(subprocess.CalledProcessError) as exc:
414 testdir.generate(SCHEMA)
416 assert 'desc is not a valid Post / PostPartial field' in str(exc.value.output)
419def test_partial_type_generator_not_found(testdir: Testdir) -> None:
420 """Unknown partial type generator option value raises an error"""
421 with pytest.raises(subprocess.CalledProcessError) as exc:
422 testdir.generate(SCHEMA, 'partial_type_generator = "foo.bar.baz"')
424 output = exc.value.output.decode('utf8')
425 assert 'ValidationError' in output
427 if PYDANTIC_V2:
428 line = output.splitlines()[-4]
429 assert (
430 line
431 == " Value error, Could not find a python file or module at foo.bar.baz [type=value_error, input_value='foo.bar.baz', input_type=str]"
432 )
433 else:
434 assert 'generator -> config -> partial_type_generator' in output
435 assert 'Could not find a python file or module at foo.bar.baz' in output
438def test_partial_type_generator_error_while_running(testdir: Testdir) -> None:
439 """Exception ocurring while running partial type generator logs exception"""
441 def generator() -> None: # mark: filedef
442 import foo as foo # type: ignore
444 testdir.make_from_function(generator, name='prisma/partial_types.py')
446 with pytest.raises(subprocess.CalledProcessError) as exc:
447 testdir.generate(SCHEMA)
449 output = str(exc.value.output)
450 assert 'prisma/partial_types.py' in output
451 assert "No module named \\'foo\\'" in output
452 assert 'An exception ocurred while running the partial type generator' in output
455def test_partial_type_already_created(testdir: Testdir) -> None:
456 """Creating same partial type twice raises error"""
458 def generator() -> None: # mark: filedef
459 from prisma.models import Post
461 for _ in range(2):
462 Post.create_partial(
463 'PostPartial',
464 exclude={'desc'}, # pyright: ignore
465 )
467 testdir.make_from_function(generator, name='prisma/partial_types.py')
469 with pytest.raises(subprocess.CalledProcessError) as exc:
470 testdir.generate(SCHEMA)
472 output = str(exc.value.output)
473 assert 'prisma/partial_types.py' in output
474 assert 'Partial type "PostPartial" has already been created.' in output
475 assert 'An exception ocurred while running the partial type generator' in output
478def test_unknown_partial_type(testdir: Testdir) -> None:
479 """Modifying relational field type to unknown partial type raises error"""
481 def generator() -> None: # mark: filedef
482 from prisma.models import Post
484 Post.create_partial('PostPartial', relations={'author': 'UnknownUser'})
486 testdir.make_from_function(generator, name='prisma/partial_types.py')
488 with pytest.raises(subprocess.CalledProcessError) as exc:
489 testdir.generate(SCHEMA)
491 output = str(exc.value.output)
492 assert 'ValueError' in output
493 assert 'prisma/partial_types.py' in output
494 assert 'Unknown partial type: "UnknownUser"' in output
495 assert 'An exception ocurred while running the partial type generator' in output
496 assert 'Did you remember to generate the UnknownUser type before this one?' in output
499def test_passing_type_for_excluded_field(testdir: Testdir) -> None:
500 """Passing relational type for an excluded field raises error"""
502 def generator() -> None: # mark: filedef
503 from prisma.models import Post, User
505 User.create_partial('CustomUser')
506 Post.create_partial(
507 'PostPartial',
508 exclude={'author'},
509 relations={'author': 'CustomUser'},
510 )
512 testdir.make_from_function(generator, name='prisma/partial_types.py')
514 with pytest.raises(subprocess.CalledProcessError) as exc:
515 testdir.generate(SCHEMA)
517 output = str(exc.value.output)
518 assert 'ValueError' in output
519 assert 'prisma/partial_types.py' in output
520 assert 'author is not a valid Post / PostPartial field' in output
521 assert 'An exception ocurred while running the partial type generator' in output
524def test_partial_type_types_non_relational(testdir: Testdir) -> None:
525 """Passing non-relational field to relations raises an error"""
527 def generator() -> None: # mark: filedef
528 from prisma.models import Post
530 Post.create_partial('Placeholder')
531 Post.create_partial(
532 'PostPartial',
533 relations={'published': 'Placeholder'}, # type: ignore[dict-item]
534 )
536 testdir.make_from_function(generator, name='prisma/partial_types.py')
538 with pytest.raises(subprocess.CalledProcessError) as exc:
539 testdir.generate(SCHEMA)
541 output = str(exc.value.output)
542 assert 'prisma/partial_types.py' in output
543 assert 'prisma.errors.UnknownRelationalFieldError' in output
544 assert 'An exception ocurred while running the partial type generator' in output
545 assert 'Field: "published" either does not exist or is not a relational field on the Post model' in output
548def test_partial_type_relations_no_relational_fields(testdir: Testdir) -> None:
549 """Passing relations option to model with no relational fields raises an error"""
551 def generator() -> None: # mark: filedef
552 from prisma.models import Foo # type: ignore[attr-defined]
554 Foo.create_partial('Placeholder', relations={'wow': 'Placeholder'})
556 testdir.make_from_function(generator, name='prisma/partial_types.py')
558 with pytest.raises(subprocess.CalledProcessError) as exc:
559 testdir.generate(SCHEMA)
561 output = exc.value.output.decode('utf-8')
562 assert 'prisma/partial_types.py' in output
563 assert 'ValueError' in output
564 assert 'An exception ocurred while running the partial type generator' in output
565 assert 'Model: "Foo" has no relational fields.' in output
568def test_exclude_relational_fields_and_relations_exclusive(
569 testdir: Testdir,
570) -> None:
571 """exclude_relational_fields and relations cannot be passed at the same time"""
573 def generator() -> None: # mark: filedef
574 from prisma.models import Post
576 Post.create_partial(
577 'Placeholder',
578 relations={'author': 'Placeholder'},
579 exclude_relational_fields=True,
580 )
582 testdir.make_from_function(generator, name='prisma/partial_types.py')
584 with pytest.raises(subprocess.CalledProcessError) as exc:
585 testdir.generate(SCHEMA)
587 output = exc.value.output.decode('utf-8')
588 assert 'prisma/partial_types.py' in output
589 assert 'ValueError' in output
590 assert 'An exception ocurred while running the partial type generator' in output
591 assert 'exclude_relational_fields and relations are mutually exclusive' in output
594def test_exclude_relational_fields_and_include_exclusive(
595 testdir: Testdir,
596) -> None:
597 """exclude_relational_fields and include cannot be passed at the same time"""
599 def generator() -> None: # mark: filedef
600 from prisma.models import Post
602 Post.create_partial(
603 'Placeholder',
604 include={'author'},
605 exclude_relational_fields=True,
606 )
608 testdir.make_from_function(generator, name='prisma/partial_types.py')
610 with pytest.raises(subprocess.CalledProcessError) as exc:
611 testdir.generate(SCHEMA)
613 output = exc.value.output.decode('utf-8')
614 assert 'prisma/partial_types.py' in output
615 assert 'TypeError' in output
616 assert 'An exception ocurred while running the partial type generator' in output
617 assert 'Include and exclude_relational_fields=True are mutually exclusive.' in output