Coverage for tests/test_generation/test_partial_types.py: 100%

109 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2024-04-28 15:17 +0000

1# pyright: reportUnusedFunction=false 

2 

3import subprocess 

4from typing_extensions import Literal 

5 

6import pytest 

7 

8from prisma._compat import PYDANTIC_V2 

9 

10from ..utils import Testdir 

11 

12SCHEMA = """ 

13datasource db {{ 

14 provider = "postgres" 

15 url = env("DB_URL") 

16}} 

17 

18generator db {{ 

19 provider = "coverage run -m prisma" 

20 output = "{output}" 

21 {options} 

22}} 

23 

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}} 

37 

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}} 

46 

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}} 

56 

57model Foo {{ 

58 id String @id @default(cuid()) 

59 text String 

60}} 

61""" 

62 

63 

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""" 

80 

81 def tests() -> None: # mark: filedef 

82 import sys 

83 import datetime 

84 from typing import Set, Dict, Type, Tuple, TypeVar, Iterator, Optional 

85 

86 from pydantic import BaseModel 

87 

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 ) 

112 

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 } 

126 

127 _T = TypeVar('_T') 

128 _T2 = TypeVar('_T2') 

129 

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] 

133 

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 

141 

142 if removed is None: 

143 removed = set() 

144 

145 assert fields.keys() - model_fields(model).keys() == removed 

146 

147 def test_without_desc() -> None: 

148 """Removing one field""" 

149 assert_expected(PostWithoutDesc, base_fields, {'desc'}) 

150 

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 ) 

158 

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 ) 

166 

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 ) 

174 

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 ) 

193 

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 ) 

212 

213 def test_modified_relational_type() -> None: 

214 """Changing one-to-one relational field type""" 

215 assert_expected(PostModifiedAuthor, base_fields, None) 

216 

217 field = model_fields(PostModifiedAuthor)['author'] 

218 type_ = model_field_type(field) 

219 assert type_ is not None 

220 

221 if PYDANTIC_V2: 

222 assert is_union(get_origin(type_) or type_) 

223 

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' 

230 

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 ) 

241 

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 ) 

253 

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 

266 

267 if PYDANTIC_V2: 

268 assert is_union(get_origin(type_) or type_) 

269 

270 posts_type, none_type = get_args(type_) 

271 

272 assert none_type.__name__ == 'NoneType' 

273 

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' 

279 

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' 

286 

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' 

291 

292 assert field.outer_type_.__module__ == 'typing' # type: ignore 

293 

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 ] 

312 

313 def generator() -> None: # mark: filedef 

314 from prisma.models import Post, User 

315 

316 User.create_partial('UserOnlyName', include={'name'}) 

317 

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 ) 

337 

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 

347 

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) 

352 

353 

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""" 

360 

361 def generator() -> None: # mark: filedef 

362 from prisma.models import Post 

363 

364 Post.create_partial('PostWithoutFoo', **{argument: ['foo']}) # type: ignore 

365 

366 testdir.make_from_function(generator, name='prisma/partial_types.py', argument=argument) 

367 

368 with pytest.raises(subprocess.CalledProcessError) as exc: 

369 testdir.generate(SCHEMA) 

370 

371 assert 'foo is not a valid Post / PostWithoutFoo field' in str(exc.value.output) 

372 

373 

374def test_partial_types_same_required_and_optional(testdir: Testdir) -> None: 

375 """Making the same field required and optional raises an error""" 

376 

377 def generator() -> None: # mark: filedef 

378 from prisma.models import Post 

379 

380 Post.create_partial( 

381 'PostPartial', 

382 required={'desc', 'published', 'title'}, # pyright: ignore 

383 optional={ 

384 'desc', # pyright: ignore 

385 'published', 

386 }, 

387 ) 

388 

389 testdir.make_from_function(generator, name='prisma/partial_types.py') 

390 

391 with pytest.raises(subprocess.CalledProcessError) as exc: 

392 testdir.generate(SCHEMA) 

393 

394 assert 'Cannot make the same field(s) required and optional' in str(exc.value.output) 

395 

396 

397def test_partial_types_excluded_required(testdir: Testdir) -> None: 

398 """Excluding and requiring the same field raises an error""" 

399 

400 def generator() -> None: # mark: filedef 

401 from prisma.models import Post 

402 

403 Post.create_partial( 

404 'PostPartial', 

405 exclude={'desc'}, # pyright: ignore 

406 required={ 

407 'desc', # pyright: ignore 

408 }, 

409 ) 

410 

411 testdir.make_from_function(generator, name='prisma/partial_types.py') 

412 

413 with pytest.raises(subprocess.CalledProcessError) as exc: 

414 testdir.generate(SCHEMA) 

415 

416 assert 'desc is not a valid Post / PostPartial field' in str(exc.value.output) 

417 

418 

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"') 

423 

424 output = exc.value.output.decode('utf8') 

425 assert 'ValidationError' in output 

426 

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 

436 

437 

438def test_partial_type_generator_error_while_running(testdir: Testdir) -> None: 

439 """Exception ocurring while running partial type generator logs exception""" 

440 

441 def generator() -> None: # mark: filedef 

442 import foo as foo # type: ignore 

443 

444 testdir.make_from_function(generator, name='prisma/partial_types.py') 

445 

446 with pytest.raises(subprocess.CalledProcessError) as exc: 

447 testdir.generate(SCHEMA) 

448 

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 

453 

454 

455def test_partial_type_already_created(testdir: Testdir) -> None: 

456 """Creating same partial type twice raises error""" 

457 

458 def generator() -> None: # mark: filedef 

459 from prisma.models import Post 

460 

461 for _ in range(2): 

462 Post.create_partial( 

463 'PostPartial', 

464 exclude={'desc'}, # pyright: ignore 

465 ) 

466 

467 testdir.make_from_function(generator, name='prisma/partial_types.py') 

468 

469 with pytest.raises(subprocess.CalledProcessError) as exc: 

470 testdir.generate(SCHEMA) 

471 

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 

476 

477 

478def test_unknown_partial_type(testdir: Testdir) -> None: 

479 """Modifying relational field type to unknown partial type raises error""" 

480 

481 def generator() -> None: # mark: filedef 

482 from prisma.models import Post 

483 

484 Post.create_partial('PostPartial', relations={'author': 'UnknownUser'}) 

485 

486 testdir.make_from_function(generator, name='prisma/partial_types.py') 

487 

488 with pytest.raises(subprocess.CalledProcessError) as exc: 

489 testdir.generate(SCHEMA) 

490 

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 

497 

498 

499def test_passing_type_for_excluded_field(testdir: Testdir) -> None: 

500 """Passing relational type for an excluded field raises error""" 

501 

502 def generator() -> None: # mark: filedef 

503 from prisma.models import Post, User 

504 

505 User.create_partial('CustomUser') 

506 Post.create_partial( 

507 'PostPartial', 

508 exclude={'author'}, 

509 relations={'author': 'CustomUser'}, 

510 ) 

511 

512 testdir.make_from_function(generator, name='prisma/partial_types.py') 

513 

514 with pytest.raises(subprocess.CalledProcessError) as exc: 

515 testdir.generate(SCHEMA) 

516 

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 

522 

523 

524def test_partial_type_types_non_relational(testdir: Testdir) -> None: 

525 """Passing non-relational field to relations raises an error""" 

526 

527 def generator() -> None: # mark: filedef 

528 from prisma.models import Post 

529 

530 Post.create_partial('Placeholder') 

531 Post.create_partial( 

532 'PostPartial', 

533 relations={'published': 'Placeholder'}, # type: ignore[dict-item] 

534 ) 

535 

536 testdir.make_from_function(generator, name='prisma/partial_types.py') 

537 

538 with pytest.raises(subprocess.CalledProcessError) as exc: 

539 testdir.generate(SCHEMA) 

540 

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 

546 

547 

548def test_partial_type_relations_no_relational_fields(testdir: Testdir) -> None: 

549 """Passing relations option to model with no relational fields raises an error""" 

550 

551 def generator() -> None: # mark: filedef 

552 from prisma.models import Foo # type: ignore[attr-defined] 

553 

554 Foo.create_partial('Placeholder', relations={'wow': 'Placeholder'}) 

555 

556 testdir.make_from_function(generator, name='prisma/partial_types.py') 

557 

558 with pytest.raises(subprocess.CalledProcessError) as exc: 

559 testdir.generate(SCHEMA) 

560 

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 

566 

567 

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""" 

572 

573 def generator() -> None: # mark: filedef 

574 from prisma.models import Post 

575 

576 Post.create_partial( 

577 'Placeholder', 

578 relations={'author': 'Placeholder'}, 

579 exclude_relational_fields=True, 

580 ) 

581 

582 testdir.make_from_function(generator, name='prisma/partial_types.py') 

583 

584 with pytest.raises(subprocess.CalledProcessError) as exc: 

585 testdir.generate(SCHEMA) 

586 

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 

592 

593 

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""" 

598 

599 def generator() -> None: # mark: filedef 

600 from prisma.models import Post 

601 

602 Post.create_partial( 

603 'Placeholder', 

604 include={'author'}, 

605 exclude_relational_fields=True, 

606 ) 

607 

608 testdir.make_from_function(generator, name='prisma/partial_types.py') 

609 

610 with pytest.raises(subprocess.CalledProcessError) as exc: 

611 testdir.generate(SCHEMA) 

612 

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