diff --git a/tartiflette/__init__.py b/tartiflette/__init__.py index e6c1119d..c13cc018 100644 --- a/tartiflette/__init__.py +++ b/tartiflette/__init__.py @@ -33,6 +33,7 @@ async def create_engine( query_cache_decorator: Optional[Callable] = UNDEFINED_VALUE, json_loader: Optional[Callable[[str], Dict[str, Any]]] = None, custom_default_arguments_coercer: Optional[Callable] = None, + coerce_list_concurrently: Optional[bool] = None, ) -> "Engine": """ Create an engine by analyzing the SDL and connecting it with the imported @@ -56,6 +57,8 @@ async def create_engine( :param json_loader: A callable that will replace default python json module.loads for ast_json loading :param custom_default_arguments_coercer: callable that will replace the + :param coerce_list_concurrently: whether or not list will be coerced + concurrently tartiflette `default_arguments_coercer :type sdl: Union[str, List[str]] :type schema_name: str @@ -66,6 +69,7 @@ async def create_engine( :type query_cache_decorator: Optional[Callable] :type json_loader: Optional[Callable[[str], Dict[str, Any]]] :type custom_default_arguments_coercer: Optional[Callable] + :type coerce_list_concurrently: Optional[bool] :return: a Cooked Engine instance :rtype: Engine @@ -90,6 +94,7 @@ async def create_engine( query_cache_decorator=query_cache_decorator, json_loader=json_loader, custom_default_arguments_coercer=custom_default_arguments_coercer, + coerce_list_concurrently=coerce_list_concurrently, ) return e diff --git a/tartiflette/engine.py b/tartiflette/engine.py index 1a7b9454..34f423a2 100644 --- a/tartiflette/engine.py +++ b/tartiflette/engine.py @@ -139,6 +139,8 @@ class Engine: Tartiflette GraphQL engine. """ + # pylint: disable=too-many-instance-attributes + def __init__( self, sdl=None, @@ -150,6 +152,7 @@ def __init__( query_cache_decorator=UNDEFINED_VALUE, json_loader=None, custom_default_arguments_coercer=None, + coerce_list_concurrently=None, ) -> None: """ Creates an uncooked Engine instance. @@ -163,6 +166,7 @@ def __init__( self._custom_default_arguments_coercer = ( custom_default_arguments_coercer ) + self._coerce_list_concurrently = coerce_list_concurrently self._modules = modules self._query_cache_decorator = ( query_cache_decorator @@ -189,6 +193,7 @@ async def cook( query_cache_decorator: Optional[Callable] = UNDEFINED_VALUE, json_loader: Optional[Callable[[str], Dict[str, Any]]] = None, custom_default_arguments_coercer: Optional[Callable] = None, + coerce_list_concurrently: Optional[bool] = None, schema_name: Optional[str] = None, ) -> None: """ @@ -213,6 +218,8 @@ async def cook( json module.loads for ast_json loading :param custom_default_arguments_coercer: callable that will replace the tartiflette `default_arguments_coercer` + :param coerce_list_concurrently: whether or not list will be coerced + concurrently :param schema_name: name of the SDL :type sdl: Union[str, List[str]] :type error_coercer: Callable[[Exception, Dict[str, Any]], Dict[str, Any]] @@ -222,6 +229,7 @@ async def cook( :type query_cache_decorator: Optional[Callable] :type json_loader: Optional[Callable[[str], Dict[str, Any]]] :type custom_default_arguments_coercer: Optional[Callable] + :type coerce_list_concurrently: Optional[bool] :type schema_name: Optional[str] """ # pylint: disable=too-many-arguments,too-many-locals @@ -294,6 +302,11 @@ async def cook( custom_default_resolver, custom_default_type_resolver, custom_default_arguments_coercer, + ( + coerce_list_concurrently + if coerce_list_concurrently is not None + else self._coerce_list_concurrently + ), ) self._build_response = partial( build_response, error_coercer=self._error_coercer diff --git a/tartiflette/schema/bakery.py b/tartiflette/schema/bakery.py index dd983975..5be3357b 100644 --- a/tartiflette/schema/bakery.py +++ b/tartiflette/schema/bakery.py @@ -33,6 +33,7 @@ async def bake( custom_default_resolver: Optional[Callable] = None, custom_default_type_resolver: Optional[Callable] = None, custom_default_arguments_coercer: Optional[Callable] = None, + coerce_list_concurrently: Optional[bool] = None, ) -> "GraphQLSchema": """ Bakes and returns a GraphQLSchema instance. @@ -44,10 +45,13 @@ async def bake( to deduct the type of a result) :param custom_default_arguments_coercer: callable that will replace the tartiflette `default_arguments_coercer` + :param coerce_list_concurrently: whether or not list will be coerced + concurrently :type schema_name: str :type custom_default_resolver: Optional[Callable] :type custom_default_type_resolver: Optional[Callable] :type custom_default_arguments_coercer: Optional[Callable] + :type coerce_list_concurrently: Optional[bool] :return: a baked GraphQLSchema instance :rtype: GraphQLSchema """ @@ -56,5 +60,6 @@ async def bake( custom_default_resolver, custom_default_type_resolver, custom_default_arguments_coercer, + coerce_list_concurrently, ) return schema diff --git a/tartiflette/schema/schema.py b/tartiflette/schema/schema.py index 73e821cd..9db74bbf 100644 --- a/tartiflette/schema/schema.py +++ b/tartiflette/schema/schema.py @@ -179,6 +179,7 @@ def __init__(self, name: str = "default") -> None: self.name = name self.default_type_resolver: Optional[Callable] = None self.default_arguments_coercer: Optional[Callable] = None + self.coerce_list_concurrently: Optional[bool] = None # Operation type names self.query_operation_name: str = _DEFAULT_QUERY_OPERATION_NAME @@ -1115,6 +1116,7 @@ async def bake( custom_default_resolver: Optional[Callable] = None, custom_default_type_resolver: Optional[Callable] = None, custom_default_arguments_coercer: Optional[Callable] = None, + coerce_list_concurrently: Optional[bool] = None, ) -> None: """ Bake the final schema (it should not change after this) used for @@ -1126,9 +1128,12 @@ async def bake( to deduct the type of a result) :param custom_default_arguments_coercer: callable that will replace the tartiflette `default_arguments_coercer` + :param coerce_list_concurrently: whether or not list will be coerced + concurrently :type custom_default_resolver: Optional[Callable] :type custom_default_type_resolver: Optional[Callable] :type custom_default_arguments_coercer: Optional[Callable] + :type coerce_list_concurrently: Optional[bool] """ self.default_type_resolver = ( custom_default_type_resolver or default_type_resolver @@ -1136,6 +1141,11 @@ async def bake( self.default_arguments_coercer = ( custom_default_arguments_coercer or gather_arguments_coercer ) + self.coerce_list_concurrently = ( + coerce_list_concurrently + if coerce_list_concurrently is not None + else True + ) self._inject_introspection_fields() self._validate_extensions() # Validate this before bake diff --git a/tartiflette/types/field.py b/tartiflette/types/field.py index 5bcc7a12..22c528a6 100644 --- a/tartiflette/types/field.py +++ b/tartiflette/types/field.py @@ -169,8 +169,8 @@ def bake( self.concurrently = self.subscription_concurrently elif self.query_concurrently is not None: self.concurrently = self.query_concurrently - else: # TODO: handle a default value at schema level - self.concurrently = True + else: + self.concurrently = schema.coerce_list_concurrently # Directives directives_definition = compute_directive_nodes( diff --git a/tests/functional/regressions/issue278/test_issue457.py b/tests/functional/regressions/issue278/test_issue457.py index 6c5497aa..a0033892 100644 --- a/tests/functional/regressions/issue278/test_issue457.py +++ b/tests/functional/regressions/issue278/test_issue457.py @@ -5,16 +5,7 @@ from tartiflette import Resolver, create_engine -_BOOKS = [ - {"id": 1, "title": "Book #1"}, - {"id": 2, "title": "Book #2"}, - {"id": 3, "title": "Book #3"}, - {"id": 4, "title": "Book #4"}, - {"id": 5, "title": "Book #5"}, - {"id": 6, "title": "Book #6"}, - {"id": 7, "title": "Book #7"}, - {"id": 8, "title": "Book #8"}, -] +_BOOKS = [{"id": i, "title": f"Book #{i}"} for i in range(25)] _SDL = """ type Book { @@ -40,7 +31,7 @@ async def test_query_books(parent, args, ctx, info): @Resolver("Book.id", schema_name=random_schema_name) async def test_book_id(parent, args, ctx, info): - await asyncio.sleep(random.randint(1, 10) / 10) + await asyncio.sleep(random.randint(0, 10) / 100) books_parsing_order.append(parent["id"]) return parent["id"] @@ -61,7 +52,7 @@ async def test_query_books(parent, args, ctx, info): @Resolver("Book.id", schema_name=random_schema_name) async def test_book_id(parent, args, ctx, info): - await asyncio.sleep(random.randint(1, 10) / 10) + await asyncio.sleep(random.randint(0, 10) / 100) books_parsing_order.append(parent["id"]) return parent["id"] @@ -70,3 +61,41 @@ async def test_book_id(parent, args, ctx, info): "data": {"books": _BOOKS} } assert books_parsing_order != [book["id"] for book in _BOOKS] + + +@pytest.mark.asyncio +async def test_issue_457_sequentially_schema_level(random_schema_name): + books_parsing_order = [] + + @Resolver("Book.id", schema_name=random_schema_name) + async def test_book_id(parent, args, ctx, info): + await asyncio.sleep(random.randint(0, 10) / 100) + books_parsing_order.append(parent["id"]) + return parent["id"] + + engine = await create_engine( + _SDL, coerce_list_concurrently=False, schema_name=random_schema_name + ) + assert await engine.execute( + "{ books { id title } }", initial_value={"books": _BOOKS} + ) == {"data": {"books": _BOOKS}} + assert books_parsing_order == [book["id"] for book in _BOOKS] + + +@pytest.mark.asyncio +async def test_issue_457_concurrently_schema_level(random_schema_name): + books_parsing_order = [] + + @Resolver("Book.id", schema_name=random_schema_name) + async def test_book_id(parent, args, ctx, info): + await asyncio.sleep(random.randint(0, 10) / 100) + books_parsing_order.append(parent["id"]) + return parent["id"] + + engine = await create_engine( + _SDL, coerce_list_concurrently=True, schema_name=random_schema_name + ) + assert await engine.execute( + "{ books { id title } }", initial_value={"books": _BOOKS} + ) == {"data": {"books": _BOOKS}} + assert books_parsing_order != [book["id"] for book in _BOOKS]