From 06a3a5858b2fa05102bae1874bcef10f9957f350 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Tr=C4=99bski?= Date: Sun, 1 Mar 2020 23:36:55 +0100 Subject: [PATCH 1/3] [TYPING] test variations on mapping structures --- typesafety/test_dicts.yml | 747 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 747 insertions(+) create mode 100644 typesafety/test_dicts.yml diff --git a/typesafety/test_dicts.yml b/typesafety/test_dicts.yml new file mode 100644 index 00000000..027d094d --- /dev/null +++ b/typesafety/test_dicts.yml @@ -0,0 +1,747 @@ +--- +- case: alias + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.alias + parameters: + - in: query + name: filters + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + FilterQuery = t.Dict[str, str] + + @oas_endpoint + async def alias( + filters: t.Optional[FilterQuery]=None, + ) -> response.Response: + return {} +- case: alias_bad_type + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.alias_bad_type + parameters: + - in: query + name: filters + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + FilterQuery = t.List[str] + + @oas_endpoint + async def alias_bad_type( + filters: t.Optional[FilterQuery]=None, + ) -> response.Response: + return {} + out: | + main:11: error: [alias_bad_type(filters -> filters)] expected "Union[Mapping[str, str], Dict[str, str], None]", but got "Optional[List[str]]" [axion-arg-type] +- case: new_type + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.new_type + parameters: + - in: query + name: filters + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + - in: query + name: extras + content: + application/json: + schema: + type: object + properties: + lan: + type: number + lon: + type: number + required: + - lan + - lon + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + FilterQuery = t.NewType('FilterQuery', t.Dict[str, str]) + ExtrasQuery = t.NewType('ExtrasQuery', t.Mapping[str, float]) + + @oas_endpoint + async def new_type( + filters: t.Optional[FilterQuery]=None, + extras: t.Optional[ExtrasQuery]=None, + ) -> response.Response: + return {} +- case: dict_different_prop_types_ok + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.dict_different_prop_types_ok + parameters: + - in: query + name: filters + required: True + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: number + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + @oas_endpoint + async def dict_different_prop_types_ok( + filters: t.Dict[str, t.Any], # must be VT=t.Any + ) -> response.Response: + return {} +- case: dict_different_prop_types_fail + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.dict_all_required + parameters: + - in: query + name: filters + required: True + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: number + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + @oas_endpoint + async def dict_all_required( + filters: t.Mapping[str, str], # E: str is not only possible type inside parameter definition + ) -> response.Response: + return {} +- case: dict_all_required + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.dict_all_required + parameters: + - in: query + name: filters + required: True + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + @oas_endpoint + async def dict_all_required( + filters: t.Dict[str, str], + ) -> response.Response: + return {} +- case: dict_properties_required + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.dict_properties_required + parameters: + - in: query + name: filters + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + @oas_endpoint + async def dict_properties_required( + filters: t.Optional[t.Dict[str, str]]=None, + ) -> response.Response: + return {} +- case: typed_dict_all_required + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.typed_dict_all_required + parameters: + - in: query + name: filters + required: true + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + import typing as t + import typing_extensions as te + + from axion import oas_endpoint + from axion import response + + class FilterQuery(te.TypedDict): + lang: str + country: str + + @oas_endpoint + async def typed_dict_all_required( + filters: FilterQuery, + ) -> response.Response: + return {} +- case: typed_dict_missing_property + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.typed_dict_all_required + parameters: + - in: query + name: filters + required: true + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + import typing as t + import typing_extensions as te + + from axion import oas_endpoint + from axion import response + + class FilterQuery(te.TypedDict): + country: str + + @oas_endpoint + async def typed_dict_all_required( + filters: FilterQuery, # E: "lang: str" property is missing [axion-arg-type] + ) -> response.Response: + return {} +- case: typed_dict_properties_required + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.typed_dict_properties_required + parameters: + - in: query + name: filters + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + import typing as t + import typing_extensions as te + + from axion import oas_endpoint + from axion import response + + class FilterQuery(te.TypedDict): + page: int + limit: int + + @oas_endpoint + async def typed_dict_properties_required( + filters: t.Optional[FilterQuery]=None, + ) -> response.Response: + return {} +- case: typed_dict_all_properties_optional + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.typed_dict_all_properties_optional + parameters: + - in: query + name: filters + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + responses: + default: + description: unexpected error + main: | + import typing as t + import typing_extensions as te + + from axion import oas_endpoint + from axion import response + + class FilterQuery(te.TypedDict): + page: t.Optional[int] + limit: t.Optional[int] + + @oas_endpoint + async def typed_dict_all_properties_optional( + filters: t.Optional[FilterQuery], + ) -> response.Response: + return {} +- case: dataclass_not_frozen + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.dataclass_not_frozen + parameters: + - in: query + name: filters + required: true + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + @dataclass(frozen=False) # E: Using not frozen dataclasses stands against immutability of HTTP request + class FilterQuery: + page: int + limit: int + + @dataclass(frozen=False) # no error here because NotQuery is not used inside function definition + class NotQuery: + test: str + foo: str + bar: str + car: int + + @oas_endpoint + async def dataclass_frozen_properties_required( + filters: FilterQuery, + ) -> response.Response: + nq = NotQuery(test='', foo='foo', bar='bar', car=666) + return {} +- case: dataclass_frozen_properties_required + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.dataclass_frozen_properties_required + parameters: + - in: query + name: filters + required: true + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + @dataclass(frozen=True) + class FilterQuery: + page: int + limit: int = 1000 + + @oas_endpoint + async def dataclass_frozen_properties_required( + filters: FilterQuery, + ) -> response.Response: + return {} +- case: namedtuple_properties_required + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.namedtuple_properties_required + parameters: + - in: query + name: filters + required: true + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + import typing as t + + from axion import oas_endpoint + from axion import response + + class FilterQuery(t.NamedTuple): + page: int + limit: int + + @oas_endpoint + async def namedtuple_properties_required( + filters: FilterQuery, + ) -> response.Response: + return {} +- case: namedtuple_properties_nullable + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.namedtuple_properties_nullable + parameters: + - in: query + name: filters + required: true + content: + application/json: + schema: + type: object + properties: + lang: + type: string + nullable: true + country: + type: string + nullable: true + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + import typing as t + + from axion import oas_endpoint + from axion import response + + class FilterQuery(t.NamedTuple): + page: t.Optional[int] = None + limit: t.Optional[int] = None + + @oas_endpoint + async def namedtuple_properties_nullable( + filters: FilterQuery, + ) -> response.Response: + return {} + +- case: namedtuple_missing_property + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.namedtuple_properties_nullable + parameters: + - in: query + name: filters + required: true + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: string + required: + - lang + responses: + default: + description: unexpected error + main: | + import typing as t + + from axion import oas_endpoint + from axion import response + + class FilterQuery(t.NamedTuple): + lang: int + + @oas_endpoint + async def namedtuple_properties_nullable( + filters: FilterQuery, # E: "country: str" property is missing + ) -> response.Response: + return {} From 1bb3b98922afd3c379be28b381ec01e123d75045 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Tr=C4=99bski?= Date: Mon, 16 Mar 2020 21:01:52 +0100 Subject: [PATCH 2/3] some basic cleanup --- axion/oas_mypy/__init__.py | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/axion/oas_mypy/__init__.py b/axion/oas_mypy/__init__.py index 46b8967b..345bcbfe 100644 --- a/axion/oas_mypy/__init__.py +++ b/axion/oas_mypy/__init__.py @@ -20,7 +20,7 @@ from mypy.nodes import FloatExpr, FuncDef, IntExpr, NameExpr from mypy.options import Options from mypy.plugin import (FunctionContext, MethodContext, Plugin) -from mypy.sametypes import is_same_type, simplify_union +from mypy.sametypes import is_same_type from mypy.subtypes import is_equivalent as is_equivalent_type from mypy.typeops import try_getting_instance_fallback from mypy.types import ( @@ -284,12 +284,10 @@ def transform_parameter_to_type( if needs_optional: items.append(NoneType()) - return simplify_union( - UnionType.make_union( - items=items, - line=(handler_arg_type.line or handler_arg_type.end_line) or -1, - column=handler_arg_type.column, - ), + return UnionType.make_union( + items=items, + line=(handler_arg_type.line or handler_arg_type.end_line) or -1, + column=handler_arg_type.column, ) @@ -336,11 +334,10 @@ def transform_oas_type( if m_type is not None: m_type.set_line(handler_type) union_members.append(m_type) - if oas_type.nullable: union_members.append(NoneType()) - ut = simplify_union(UnionType.make_union(items=union_members)) + ut = UnionType.make_union(items=union_members) ut.set_line(handler_type) return ut @@ -387,25 +384,27 @@ def transform_oas_object_type( # - Dict assert isinstance(ctx.api, TypeChecker) + assert isinstance(oas_type, oas_model.OASObjectType) vt = get_generic_type_vt(ctx, handler_arg_type, oas_type) td_type = get_typed_dict_type(ctx, handler_arg_type, oas_type) members: List[Type] = [ + td_type, ctx.api.named_generic_type( - name='collections.abc.Mapping', + name='dict', args=[ctx.api.str_type(), vt], ), ctx.api.named_generic_type( - name='dict', + name='collections.abc.Mapping', args=[ctx.api.str_type(), vt], ), - td_type, ] - if oas_type.nullable: - members.append(NoneType()) - return simplify_union(UnionType.make_union(items=members)) + ot = UnionType.make_union(items=members) + ot.set_line(handler_arg_type) + + return ot def get_typed_dict_type( From bd448bac372bb59c857cb018323210c4c42cdaf1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tomasz=20Tr=C4=99bski?= Date: Wed, 3 Jun 2020 00:03:05 +0200 Subject: [PATCH 3/3] add some more test cases --- typesafety/test_dicts.yml | 45 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/typesafety/test_dicts.yml b/typesafety/test_dicts.yml index 027d094d..4b4cb33f 100644 --- a/typesafety/test_dicts.yml +++ b/typesafety/test_dicts.yml @@ -149,7 +149,7 @@ extras: t.Optional[ExtrasQuery]=None, ) -> response.Response: return {} -- case: dict_different_prop_types_ok +- case: dict_different_prop_types_ok_any oas_spec: | openapi: 3.0.1 info: @@ -192,6 +192,49 @@ filters: t.Dict[str, t.Any], # must be VT=t.Any ) -> response.Response: return {} +- case: dict_different_prop_types_ok_exact + oas_spec: | + openapi: 3.0.1 + info: + title: title + version: 1.0.0 + servers: + - url: / + paths: + /: + get: + operationId: main.dict_different_prop_types_ok + parameters: + - in: query + name: filters + required: True + content: + application/json: + schema: + type: object + properties: + lang: + type: string + country: + type: number + required: + - lang + - country + responses: + default: + description: unexpected error + main: | + from dataclasses import dataclass + import typing as t + + from axion import oas_endpoint + from axion import response + + @oas_endpoint + async def dict_different_prop_types_ok( + filters: t.Dict[str, t.Union[str, float]], + ) -> response.Response: + return {} - case: dict_different_prop_types_fail oas_spec: | openapi: 3.0.1