Skip to content

Commit 37246a4

Browse files
authored
Merge pull request #158 from python-ellar/test_cov_100
100% test coverage
2 parents 4d89c90 + 18c045a commit 37246a4

12 files changed

+198
-61
lines changed

.coveragerc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ exclude_lines =
1616
omit =
1717
# omit this single file
1818
ellar/core/files/storages/aws_s3.py
19+
ellar/common/compatible/cache_properties.py

ellar/common/params/args/request_model.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .. import params
1515
from ..resolvers import (
16+
BodyParameterResolver,
1617
BulkFormParameterResolver,
1718
IRouteParameterResolver,
1819
RouteParameterModelField,
@@ -107,8 +108,10 @@ def build_body_field(self) -> None:
107108
if (
108109
body_resolvers
109110
and len(body_resolvers) == 1
110-
and not (
111-
body_resolvers[0].model_field.field_info.embed # type: ignore[attr-defined]
111+
and not isinstance(body_resolvers[0], BulkFormParameterResolver)
112+
and (
113+
isinstance(body_resolvers[0], BodyParameterResolver)
114+
and not body_resolvers[0].model_field.field_info.embed # type: ignore[attr-defined]
112115
)
113116
):
114117
check_file_field(body_resolvers[0].model_field)

ellar/common/params/resolvers/bulk_parameter.py

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ async def resolve_handle(
3535
errors: t.List[ErrorWrapper] = []
3636

3737
for parameter_resolver in self._resolvers:
38-
value_, errors_ = await parameter_resolver.resolve(ctx=ctx)
38+
value_, errors_ = await parameter_resolver.resolve(*args, ctx=ctx, **kwargs)
3939
if value_:
4040
values.update(
4141
{
@@ -46,15 +46,18 @@ async def resolve_handle(
4646
)
4747
if errors_:
4848
errors += self.validate_error_sequence(errors_)
49+
4950
if errors:
5051
return values, errors
5152

53+
# Combining resolved values into one pydantic model specified by the user in Route function parameter
5254
v_, errors_ = self.model_field.validate(
5355
values,
5456
{},
5557
loc=(self.model_field.field_info.in_.value, self.model_field.alias),
5658
)
57-
if errors_:
59+
if errors_: # pragma: no cover
60+
# Just in case error still happened after combining each field to one pydantic model.
5861
errors += self.validate_error_sequence(errors_)
5962
return values, errors
6063
return {self.model_field.name: v_}, []
@@ -82,13 +85,15 @@ async def resolve_grouped_fields(
8285
value, resolver_errors = await self._get_resolver_data(ctx, body, by_alias=True)
8386
if resolver_errors:
8487
return value, resolver_errors
85-
88+
# Combining resolved values into one pydantic model specified by the user in Route function parameter
8689
processed_value, processed_errors = self.model_field.validate(
8790
value,
8891
{},
8992
loc=(self.model_field.field_info.in_.value, self.model_field.alias),
9093
)
91-
if processed_errors:
94+
95+
if processed_errors: # pragma: no cover
96+
# Just in case error still happened after combining each field to one pydantic model.
9297
processed_errors = self.validate_error_sequence(processed_errors)
9398
return processed_value, processed_errors
9499
return {self.model_field.name: processed_value}, []
@@ -125,25 +130,22 @@ async def resolve_handle(
125130
) -> t.Tuple:
126131
request_logger.debug(f"Resolving Form Parameters - '{self.__class__.__name__}'")
127132
_body = await self.get_request_body(ctx)
128-
if self._resolvers:
129-
return await self._use_resolver(ctx, _body)
130-
return await super(BulkFormParameterResolver, self).resolve_handle(
131-
ctx, *args, **kwargs
132-
)
133+
return await self._use_resolver(ctx, _body)
133134

134135

135-
class BulkBodyParameterResolver(BodyParameterResolver, BulkParameterResolver):
136+
class BulkBodyParameterResolver(BulkParameterResolver, BodyParameterResolver):
136137
async def resolve_handle(
137138
self, ctx: IExecutionContext, *args: t.Any, **kwargs: t.Any
138139
) -> t.Tuple:
139140
request_logger.debug(
140141
f"Resolving Request Body Parameters - '{self.__class__.__name__}'"
141142
)
142143
_body = await self.get_request_body(ctx)
143-
values, errors = await super(BulkBodyParameterResolver, self).resolve_handle(
144-
ctx, *args, body=_body, **kwargs
145-
)
146-
if not errors:
147-
_, body_value = values.popitem()
148-
return body_value.dict(), []
149-
return values, self.validate_error_sequence(errors)
144+
145+
values, errors = await super().resolve_handle(ctx, *args, body=_body, **kwargs)
146+
147+
if errors:
148+
return values, self.validate_error_sequence(errors)
149+
150+
_, body_value = values.popitem()
151+
return body_value.dict(), []

ellar/common/params/resolvers/parameter.py

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55

66
import anyio
77
from ellar.common.constants import (
8-
# sequence_shape_to_type,
9-
# sequence_shapes,
108
sequence_types,
119
)
1210
from ellar.common.exceptions import RequestValidationError
@@ -130,6 +128,15 @@ async def resolve_handle(
130128
loc = ("body", self.model_field.alias) # type:ignore
131129
try:
132130
value = received_body.get(self.model_field.alias)
131+
132+
if value is None:
133+
if self.model_field.required:
134+
return None, [self.create_error(loc=loc)]
135+
else:
136+
return {
137+
self.model_field.name: copy.deepcopy(self.model_field.default)
138+
}, []
139+
133140
v_, errors_ = self.model_field.validate(value, {}, loc=loc)
134141
return {self.model_field.name: v_}, self.validate_error_sequence(errors_)
135142
except AttributeError:
@@ -227,7 +234,6 @@ async def resolve_handle(
227234
) -> t.Tuple:
228235
_body = body or await self.get_request_body(ctx)
229236
embed = getattr(self.model_field.field_info, "embed", False)
230-
values = {} # type: ignore
231237
received_body = {self.model_field.alias: _body}
232238
loc = ("body",)
233239

@@ -239,22 +245,28 @@ async def resolve_handle(
239245
loc = ("body", self.model_field.alias) # type: ignore
240246
value = _body.getlist(self.model_field.alias)
241247
else:
242-
try:
243-
value = received_body.get(self.model_field.alias) # type: ignore
244-
except AttributeError:
245-
errors = [self.create_error(loc=loc)]
246-
return values, errors
248+
value = received_body.get(self.model_field.alias) # type: ignore
247249

248-
if not value or is_sequence_field(self.model_field) and len(value) == 0:
250+
if (
251+
value is None
252+
or (
253+
isinstance(self.model_field.field_info.resolver, FormParameterResolver)
254+
and value == ""
255+
)
256+
or (
257+
isinstance(self.model_field.field_info.resolver, FormParameterResolver)
258+
and is_sequence_field(self.model_field)
259+
and len(value) == 0
260+
)
261+
):
249262
if self.model_field.required:
250-
return await self.process_and_validate(
251-
values=values, value=_body, loc=loc
252-
)
263+
return None, [self.create_error(loc=loc)]
253264
else:
254-
values[self.model_field.name] = copy.deepcopy(self.model_field.default)
255-
return values, []
265+
return {
266+
self.model_field.name: copy.deepcopy(self.model_field.default)
267+
}, []
256268

257-
return await self.process_and_validate(values=values, value=value, loc=loc)
269+
return await self.process_and_validate(values={}, value=value, loc=loc)
258270

259271

260272
class FileParameterResolver(FormParameterResolver):

ellar/openapi/decorators/extra_info.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from ellar.common.compatible import AttributeDict
44
from ellar.common.decorators import set_metadata as set_meta
5+
from ellar.common.exceptions import ImproperConfiguration
56
from ellar.openapi.constants import OPENAPI_OPERATION_KEY
67

78

@@ -23,6 +24,9 @@ def openapi_info(
2324
:param deprecated:
2425
:return:
2526
"""
27+
if tags and not isinstance(tags, list):
28+
raise ImproperConfiguration("tags must be a sequence of str eg, [tagA, tagB]")
29+
2630
return set_meta(
2731
OPENAPI_OPERATION_KEY,
2832
AttributeDict(

ellar/openapi/route_doc_models.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -150,9 +150,6 @@ def __init__(
150150
)
151151
self.guards = guards or []
152152

153-
if self.tags and not isinstance(self.tags, list):
154-
self.tags = [self.tags]
155-
156153
@cached_property
157154
def _openapi_models(self) -> t.List[t.Union[ModelField, RouteParameterModelField]]:
158155
_models: t.List[ModelField] = self.input_fields + self.output_fields
@@ -169,7 +166,8 @@ def input_fields(self) -> t.List[ModelField]:
169166
_models: t.List[ModelField] = self.global_route_parameters
170167

171168
for item in self.route.endpoint_parameter_model.get_all_models():
172-
if isinstance(item, BodyParameterResolver):
169+
if isinstance(item, BodyParameterResolver): # pragma: no cover
170+
# just incase we have a Body Field
173171
continue
174172

175173
if isinstance(item, BulkParameterResolver):
@@ -262,7 +260,7 @@ def get_openapi_operation_parameters(
262260
}
263261
if field_info.description:
264262
parameter["description"] = field_info.description
265-
if field_info.examples: # pragma:no cover
263+
if field_info.examples: # pragma: no cover
266264
parameter["examples"] = field_info.examples # type:ignore[assignment]
267265
if field_info.deprecated:
268266
parameter["deprecated"] = field_info.deprecated

tests/test_openapi/test_openapi.py renamed to tests/test_openapi/test_extra_info_decorator.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import pytest
12
from ellar.common.compatible import AttributeDict
3+
from ellar.common.exceptions import ImproperConfiguration
24
from ellar.core.connection import Request
35
from ellar.openapi import openapi_info
46
from ellar.openapi.constants import OPENAPI_OPERATION_KEY
@@ -24,3 +26,14 @@ def test_openapi_sets_endpoint_meta():
2426
assert open_api_data.deprecated is False
2527
assert open_api_data.operation_id == "4524d-z23zd-453ed-2342e"
2628
assert open_api_data.tags == ["endpoint", "endpoint-25"]
29+
30+
31+
def test_invalid_openapi_info_decorator_setup():
32+
with pytest.raises(ImproperConfiguration):
33+
34+
@openapi_info(
35+
operation_id="4524d-z23zd-453ed-2342e",
36+
tags="endpoint",
37+
)
38+
def endpoint(request: Request):
39+
pass # pragma: no cover

tests/test_openapi/test_open_api_route_documentation.py

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import typing as t
22

33
from ellar.auth.guards import GuardAPIKeyCookie
4-
from ellar.common import Body, ModuleRouter, Query
4+
from ellar.common import Body, GuardCanActivate, ModuleRouter, Query, UseGuards
55
from ellar.common.constants import CONTROLLER_OPERATION_HANDLER_KEY
66
from ellar.common.responses.models import ResponseModel, ResponseModelField
7+
from ellar.core import ExecutionContext
78
from ellar.core.connection import HTTPConnection
89
from ellar.core.routing import ModuleRouterFactory
910
from ellar.openapi import OpenAPIRouteDocumentation, openapi_info
@@ -14,6 +15,11 @@
1415
from ..schema import BlogObjectDTO, CreateCarSchema, Filter, NoteSchemaDC
1516

1617

18+
class JustAGuard(GuardCanActivate):
19+
async def can_activate(self, context: ExecutionContext) -> bool:
20+
return True
21+
22+
1723
class CustomCookieAPIKey(GuardAPIKeyCookie):
1824
parameter_name = "custom-key"
1925

@@ -41,16 +47,21 @@ class CustomResponseModel(ResponseModel):
4147
@openapi_info(
4248
summary="Endpoint Summary",
4349
description="Endpoint Description",
44-
deprecated=False,
50+
deprecated=True,
4551
tags=["endpoint", "endpoint-25"],
4652
)
47-
def get_car_by_id(car_id: int, filter: Filter = Query()):
53+
def get_car_by_id(
54+
car_id: int,
55+
filter: Filter = Query(),
56+
schema: int = Query(description="input field description", deprecated=True),
57+
):
4858
res = filter.dict()
49-
res.update(car_id=car_id)
59+
res.update(car_id=car_id, schema=schema)
5060
return res
5161

5262

5363
@router.get("/create", response={201: CreateCarSchema})
64+
@UseGuards(JustAGuard)
5465
def create_car(car: CreateCarSchema):
5566
return car
5667

@@ -68,7 +79,7 @@ def test_open_api_route_model_input_fields():
6879
CONTROLLER_OPERATION_HANDLER_KEY, get_car_by_id
6980
)
7081
openapi_route_doc = OpenAPIRouteDocumentation(route=route_operation)
71-
assert len(openapi_route_doc.input_fields) == 3
82+
assert len(openapi_route_doc.input_fields) == 4
7283

7384
for field in openapi_route_doc.input_fields:
7485
assert field.field_info.in_.value in ["query", "path"]
@@ -97,14 +108,16 @@ def test_open_api_route_model_get_openapi_operation_metadata():
97108
"summary": "Endpoint Summary",
98109
"description": "Endpoint Description",
99110
"operationId": "get_car_by_id_cars__car_id__post",
111+
"deprecated": True,
100112
}
101113

102114
result = openapi_route_doc.get_openapi_operation_metadata("some_http_method")
103115
assert result == {
104-
"tags": ["endpoint", "endpoint-25"],
105-
"summary": "Endpoint Summary",
116+
"deprecated": True,
106117
"description": "Endpoint Description",
107118
"operationId": "get_car_by_id_cars__car_id__some_http_method",
119+
"summary": "Endpoint Summary",
120+
"tags": ["endpoint", "endpoint-25"],
108121
}
109122

110123

@@ -160,6 +173,18 @@ def test_open_api_route_get_openapi_operation_parameters_works_for_empty_model_n
160173
"title": "From",
161174
},
162175
},
176+
{
177+
"name": "schema",
178+
"in": "query",
179+
"required": True,
180+
"schema": {
181+
"type": "integer",
182+
"description": "input field description",
183+
"title": "Schema",
184+
},
185+
"description": "input field description",
186+
"deprecated": True,
187+
},
163188
]
164189

165190

@@ -416,7 +441,7 @@ def test_open_api_route__get_openapi_path_object_works_for_routes_with_multiple_
416441
CONTROLLER_OPERATION_HANDLER_KEY, list_and_create_car
417442
)
418443
openapi_route_doc = OpenAPIRouteDocumentation(
419-
route=route_operation, guards=[CustomCookieAPIKey()]
444+
route=route_operation, guards=[CustomCookieAPIKey(), JustAGuard()]
420445
)
421446
field_mapping, _ = get_definitions(
422447
fields=openapi_route_doc.get_route_models(),

0 commit comments

Comments
 (0)