66from itertools import chain , zip_longest
77from json import dumps , loads
88from typing import Dict , List , Literal , Set , Tuple
9+ from unittest .mock import call , patch
910from uuid import UUID , uuid4
1011
1112import pytest
2021from starlette .datastructures import QueryParams
2122
2223from fastapi_jsonapi .api import RoutersJSONAPI
24+ from fastapi_jsonapi .schema_builder import SchemaBuilder
2325from fastapi_jsonapi .views .view_base import ViewBase
2426from tests .common import is_postgres_tests
2527from tests .fixtures .app import build_alphabet_app , build_app_custom
5254 CustomUUIDItemAttributesSchema ,
5355 PostAttributesBaseSchema ,
5456 PostCommentAttributesBaseSchema ,
57+ PostCommentSchema ,
58+ PostSchema ,
5559 SelfRelationshipAttributesSchema ,
5660 SelfRelationshipSchema ,
5761 UserAttributesBaseSchema ,
@@ -360,6 +364,215 @@ async def test_select_custom_fields_for_includes_without_requesting_includes(
360364 "meta" : {"count" : 1 , "totalPages" : 1 },
361365 }
362366
367+ def _get_clear_mock_calls (self , mock_obj ) -> list [call ]:
368+ mock_calls = mock_obj .mock_calls
369+ return [call_ for call_ in mock_calls if call_ not in [call .__len__ (), call .__str__ ()]]
370+
371+ def _prepare_info_schema_calls_to_assert (self , mock_calls ) -> list [call ]:
372+ calls_to_check = []
373+ for wrapper_call in mock_calls :
374+ kwargs = wrapper_call .kwargs
375+ kwargs ["includes" ] = sorted (kwargs ["includes" ], key = lambda x : x )
376+
377+ calls_to_check .append (
378+ call (
379+ * wrapper_call .args ,
380+ ** kwargs ,
381+ ),
382+ )
383+
384+ return sorted (
385+ calls_to_check ,
386+ key = lambda x : (x .kwargs ["base_name" ], x .kwargs ["includes" ]),
387+ )
388+
389+ async def test_check_get_info_schema_cache (
390+ self ,
391+ user_1 : User ,
392+ ):
393+ resource_type = "user_with_cache"
394+ with suppress (KeyError ):
395+ RoutersJSONAPI .all_jsonapi_routers .pop (resource_type )
396+
397+ app_with_cache = build_app_custom (
398+ model = User ,
399+ schema = UserSchema ,
400+ schema_in_post = UserInSchemaAllowIdOnPost ,
401+ schema_in_patch = UserPatchSchema ,
402+ resource_type = resource_type ,
403+ # set cache size to enable caching
404+ max_cache_size = 128 ,
405+ )
406+
407+ target_func_name = "_get_info_from_schema_for_building"
408+ url = app_with_cache .url_path_for (f"get_{ resource_type } _list" )
409+ params = {
410+ "include" : "posts,posts.comments" ,
411+ }
412+
413+ expected_len_with_cache = 6
414+ expected_len_without_cache = 10
415+
416+ with patch .object (
417+ SchemaBuilder ,
418+ target_func_name ,
419+ wraps = app_with_cache .jsonapi_routers .schema_builder ._get_info_from_schema_for_building ,
420+ ) as wrapped_func :
421+ async with AsyncClient (app = app_with_cache , base_url = "http://test" ) as client :
422+ response = await client .get (url , params = params )
423+ assert response .status_code == status .HTTP_200_OK , response .text
424+
425+ calls_to_check = self ._prepare_info_schema_calls_to_assert (self ._get_clear_mock_calls (wrapped_func ))
426+
427+ # there are no duplicated calls
428+ assert calls_to_check == sorted (
429+ [
430+ call (
431+ base_name = "UserSchema" ,
432+ schema = UserSchema ,
433+ includes = ["posts" ],
434+ non_optional_relationships = False ,
435+ ),
436+ call (
437+ base_name = "UserSchema" ,
438+ schema = UserSchema ,
439+ includes = ["posts" , "posts.comments" ],
440+ non_optional_relationships = False ,
441+ ),
442+ call (
443+ base_name = "PostSchema" ,
444+ schema = PostSchema ,
445+ includes = [],
446+ non_optional_relationships = False ,
447+ ),
448+ call (
449+ base_name = "PostSchema" ,
450+ schema = PostSchema ,
451+ includes = ["comments" ],
452+ non_optional_relationships = False ,
453+ ),
454+ call (
455+ base_name = "PostCommentSchema" ,
456+ schema = PostCommentSchema ,
457+ includes = [],
458+ non_optional_relationships = False ,
459+ ),
460+ call (
461+ base_name = "PostCommentSchema" ,
462+ schema = PostCommentSchema ,
463+ includes = ["posts" ],
464+ non_optional_relationships = False ,
465+ ),
466+ ],
467+ key = lambda x : (x .kwargs ["base_name" ], x .kwargs ["includes" ]),
468+ )
469+ assert wrapped_func .call_count == expected_len_with_cache
470+
471+ response = await client .get (url , params = params )
472+ assert response .status_code == status .HTTP_200_OK , response .text
473+
474+ # there are no new calls
475+ assert wrapped_func .call_count == expected_len_with_cache
476+
477+ resource_type = "user_without_cache"
478+ with suppress (KeyError ):
479+ RoutersJSONAPI .all_jsonapi_routers .pop (resource_type )
480+
481+ app_without_cache = build_app_custom (
482+ model = User ,
483+ schema = UserSchema ,
484+ schema_in_post = UserInSchemaAllowIdOnPost ,
485+ schema_in_patch = UserPatchSchema ,
486+ resource_type = resource_type ,
487+ max_cache_size = 0 ,
488+ )
489+
490+ with patch .object (
491+ SchemaBuilder ,
492+ target_func_name ,
493+ wraps = app_without_cache .jsonapi_routers .schema_builder ._get_info_from_schema_for_building ,
494+ ) as wrapped_func :
495+ async with AsyncClient (app = app_without_cache , base_url = "http://test" ) as client :
496+ response = await client .get (url , params = params )
497+ assert response .status_code == status .HTTP_200_OK , response .text
498+
499+ calls_to_check = self ._prepare_info_schema_calls_to_assert (self ._get_clear_mock_calls (wrapped_func ))
500+
501+ # there are duplicated calls
502+ assert calls_to_check == sorted (
503+ [
504+ call (
505+ base_name = "UserSchema" ,
506+ schema = UserSchema ,
507+ includes = ["posts" ],
508+ non_optional_relationships = False ,
509+ ),
510+ call (
511+ base_name = "UserSchema" ,
512+ schema = UserSchema ,
513+ includes = ["posts" ],
514+ non_optional_relationships = False ,
515+ ), # duplicate
516+ call (
517+ base_name = "UserSchema" ,
518+ schema = UserSchema ,
519+ includes = ["posts" , "posts.comments" ],
520+ non_optional_relationships = False ,
521+ ),
522+ call (
523+ base_name = "PostSchema" ,
524+ schema = PostSchema ,
525+ includes = [],
526+ non_optional_relationships = False ,
527+ ),
528+ call (
529+ base_name = "PostSchema" ,
530+ schema = PostSchema ,
531+ includes = [],
532+ non_optional_relationships = False ,
533+ ), # duplicate
534+ call (
535+ base_name = "PostSchema" ,
536+ schema = PostSchema ,
537+ includes = [],
538+ non_optional_relationships = False ,
539+ ), # duplicate
540+ call (
541+ base_name = "PostSchema" ,
542+ schema = PostSchema ,
543+ includes = ["comments" ],
544+ non_optional_relationships = False ,
545+ ),
546+ call (
547+ base_name = "PostSchema" ,
548+ schema = PostSchema ,
549+ includes = ["comments" ],
550+ non_optional_relationships = False ,
551+ ), # duplicate
552+ call (
553+ base_name = "PostCommentSchema" ,
554+ schema = PostCommentSchema ,
555+ includes = [],
556+ non_optional_relationships = False ,
557+ ),
558+ call (
559+ base_name = "PostCommentSchema" ,
560+ schema = PostCommentSchema ,
561+ includes = ["posts" ],
562+ non_optional_relationships = False ,
563+ ), # duplicate
564+ ],
565+ key = lambda x : (x .kwargs ["base_name" ], x .kwargs ["includes" ]),
566+ )
567+
568+ assert wrapped_func .call_count == expected_len_without_cache
569+
570+ response = await client .get (url , params = params )
571+ assert response .status_code == status .HTTP_200_OK , response .text
572+
573+ # there are new calls
574+ assert wrapped_func .call_count == expected_len_without_cache * 2
575+
363576
364577class TestCreatePostAndComments :
365578 async def test_get_posts_with_users (
@@ -371,6 +584,13 @@ async def test_get_posts_with_users(
371584 user_1_posts : List [Post ],
372585 user_2_posts : List [Post ],
373586 ):
587+ call (
588+ base_name = "UserSchema" ,
589+ schema = UserSchema ,
590+ includes = ["posts" ],
591+ non_optional_relationships = False ,
592+ on_optional_relationships = False ,
593+ )
374594 url = app .url_path_for ("get_post_list" )
375595 url = f"{ url } ?include=user"
376596 response = await client .get (url )
0 commit comments