Skip to content

Commit ee359ec

Browse files
committed
Fix miscellaneous issues reported by static type checker
1 parent 83dda02 commit ee359ec

19 files changed

+235
-120
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"editor.defaultFormatter": "charliermarsh.ruff"
1616
},
1717
"cSpell.words": [
18+
"Levente",
1819
"pyopenapi",
1920
"pypi",
2021
"Sigstore",

MANIFEST.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
recursive-include tests *.py
2+
recursive-include tests *.md

check.bat

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
@echo off
2+
3+
rem Run static type checker and verify formatting guidelines
4+
ruff check
5+
if errorlevel 1 goto error
6+
ruff format --check
7+
if errorlevel 1 goto error
8+
python -m mypy pyopenapi
9+
if errorlevel 1 goto error
10+
python -m mypy tests
11+
if errorlevel 1 goto error
12+
13+
goto :EOF
14+
15+
:error
16+
exit /b 1

check.sh

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
set -e
2+
3+
PYTHON=python3
4+
5+
# Run static type checker and verify formatting guidelines
6+
ruff check
7+
ruff format --check
8+
$PYTHON -m mypy pyopenapi
9+
$PYTHON -m mypy tests

mypy.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,10 @@ disallow_untyped_decorators = True
55
disallow_untyped_defs = True
66
ignore_missing_imports = True
77
show_column_numbers = True
8+
strict = True
9+
strict_bytes = True
10+
strict_equality = True
811
warn_redundant_casts = True
12+
warn_return_any = True
13+
warn_unreachable = True
914
warn_unused_ignores = True

pyopenapi/__init__.py

Lines changed: 11 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,14 @@
1-
from typing import Any, Callable, Optional, TypeVar
1+
"""
2+
Generate an OpenAPI specification from a Python class definition
23
3-
from .metadata import WebMethod
4-
from .options import * # noqa: F403
5-
from .utility import Specification as Specification
4+
Copyright 2022-2025, Levente Hunyadi
65
7-
__version__ = "0.1.10"
8-
9-
T = TypeVar("T")
10-
11-
12-
def webmethod(
13-
route: Optional[str] = None,
14-
public: Optional[bool] = False,
15-
request_example: Optional[Any] = None,
16-
response_example: Optional[Any] = None,
17-
request_examples: Optional[list[Any]] = None,
18-
response_examples: Optional[list[Any]] = None,
19-
) -> Callable[[T], T]:
20-
"""
21-
Decorator that supplies additional metadata to an endpoint operation function.
22-
23-
:param route: The URL path pattern associated with this operation which path parameters are substituted into.
24-
:param public: True if the operation can be invoked without prior authentication.
25-
:param request_example: A sample request that the operation might take.
26-
:param response_example: A sample response that the operation might produce.
27-
:param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
28-
:param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
29-
"""
6+
:see: https://github.com/hunyadi/pyopenapi
7+
"""
308

31-
if request_example is not None and request_examples is not None:
32-
raise ValueError("arguments `request_example` and `request_examples` are exclusive")
33-
if response_example is not None and response_examples is not None:
34-
raise ValueError("arguments `response_example` and `response_examples` are exclusive")
35-
36-
if request_example:
37-
request_examples = [request_example]
38-
if response_example:
39-
response_examples = [response_example]
40-
41-
def wrap(cls: T) -> T:
42-
setattr(
43-
cls,
44-
"__webmethod__",
45-
WebMethod(
46-
route=route,
47-
public=public or False,
48-
request_examples=request_examples,
49-
response_examples=response_examples,
50-
),
51-
)
52-
return cls
53-
54-
return wrap
9+
__version__ = "0.1.10"
10+
__author__ = "Levente Hunyadi"
11+
__copyright__ = "Copyright 2022-2025, Levente Hunyadi"
12+
__license__ = "MIT"
13+
__maintainer__ = "Levente Hunyadi"
14+
__status__ = "Production"

pyopenapi/decorators.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
Generate an OpenAPI specification from a Python class definition
3+
4+
Copyright 2022-2025, Levente Hunyadi
5+
6+
:see: https://github.com/hunyadi/pyopenapi
7+
"""
8+
9+
from typing import Any, Callable, Optional, TypeVar
10+
11+
from .metadata import WebMethod
12+
from .options import * # noqa: F403
13+
from .utility import Specification as Specification
14+
15+
F = TypeVar("F", bound=Callable[..., Any])
16+
17+
18+
def webmethod(
19+
route: Optional[str] = None,
20+
public: Optional[bool] = False,
21+
request_example: Optional[Any] = None,
22+
response_example: Optional[Any] = None,
23+
request_examples: Optional[list[Any]] = None,
24+
response_examples: Optional[list[Any]] = None,
25+
) -> Callable[[F], F]:
26+
"""
27+
Decorator that supplies additional metadata to an endpoint operation function.
28+
29+
:param route: The URL path pattern associated with this operation which path parameters are substituted into.
30+
:param public: True if the operation can be invoked without prior authentication.
31+
:param request_example: A sample request that the operation might take.
32+
:param response_example: A sample response that the operation might produce.
33+
:param request_examples: Sample requests that the operation might take. Pass a list of objects, not JSON.
34+
:param response_examples: Sample responses that the operation might produce. Pass a list of objects, not JSON.
35+
"""
36+
37+
if request_example is not None and request_examples is not None:
38+
raise ValueError("arguments `request_example` and `request_examples` are exclusive")
39+
if response_example is not None and response_examples is not None:
40+
raise ValueError("arguments `response_example` and `response_examples` are exclusive")
41+
42+
if request_example:
43+
request_examples = [request_example]
44+
if response_example:
45+
response_examples = [response_example]
46+
47+
def wrap(cls: F) -> F:
48+
cls.__webmethod__ = WebMethod(route=route, public=public or False, request_examples=request_examples, response_examples=response_examples) # type: ignore[attr-defined]
49+
return cls
50+
51+
return wrap

pyopenapi/generator.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
"""
2+
Generate an OpenAPI specification from a Python class definition
3+
4+
Copyright 2022-2025, Levente Hunyadi
5+
6+
:see: https://github.com/hunyadi/pyopenapi
7+
"""
8+
19
import dataclasses
210
import hashlib
311
import ipaddress
@@ -6,11 +14,11 @@
614
from http import HTTPStatus
715
from typing import Any, Callable, Optional, Union
816

9-
from strong_typing.core import JsonType
17+
from strong_typing.core import JsonType, Schema
1018
from strong_typing.docstring import Docstring, parse_type
1119
from strong_typing.inspection import is_generic_list, is_type_optional, is_type_union, unwrap_generic_list, unwrap_optional_type, unwrap_union_types
1220
from strong_typing.name import python_type_to_name
13-
from strong_typing.schema import JsonSchemaGenerator, Schema, SchemaOptions, get_schema_identifier, register_schema
21+
from strong_typing.schema import JsonSchemaGenerator, SchemaOptions, get_schema_identifier, register_schema
1422
from strong_typing.serialization import json_dump_string, object_to_json
1523

1624
from .operations import EndpointOperation, HTTPMethod, get_endpoint_events, get_endpoint_operations
@@ -208,7 +216,7 @@ def __init__(
208216
if sample_transformer:
209217
self.sample_transformer = sample_transformer
210218
else:
211-
self.sample_transformer = lambda sample: sample # noqa: E731
219+
self.sample_transformer = lambda sample: sample
212220

213221
def _get_value(self, example: Any) -> JsonType:
214222
return self.sample_transformer(object_to_json(example))
@@ -221,7 +229,7 @@ def get_named(self, example: Any) -> tuple[str, JsonType]:
221229

222230
name: Optional[str] = None
223231

224-
if type(example).__str__ is not object.__str__:
232+
if type(example).__str__ is not object.__str__: # type: ignore[comparison-overlap]
225233
friendly_name = str(example)
226234
if friendly_name.isprintable():
227235
name = friendly_name
@@ -315,7 +323,7 @@ def build_response(self, options: ResponseOptions) -> dict[str, Union[Response,
315323

316324
def _build_response(
317325
self,
318-
response_type: type,
326+
response_type: Optional[type],
319327
description: str,
320328
examples: Optional[list[Any]] = None,
321329
) -> Response:

pyopenapi/metadata.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
"""
2+
Generate an OpenAPI specification from a Python class definition
3+
4+
Copyright 2022-2025, Levente Hunyadi
5+
6+
:see: https://github.com/hunyadi/pyopenapi
7+
"""
8+
19
from dataclasses import dataclass
210
from typing import Any, Optional
311

pyopenapi/operations.py

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
"""
2+
Generate an OpenAPI specification from a Python class definition
3+
4+
Copyright 2022-2025, Levente Hunyadi
5+
6+
:see: https://github.com/hunyadi/pyopenapi
7+
"""
8+
19
import collections.abc
210
import enum
311
import inspect
@@ -34,11 +42,11 @@ def split_prefix(s: str, sep: str, prefix: Union[str, Iterable[str]]) -> tuple[O
3442
return None, s
3543

3644

37-
def _get_annotation_type(annotation: Union[type, str], callable: Callable) -> type:
38-
"Maps a stringized reference to a type, as if using `from __future__ import annotations`."
45+
def _get_annotation_type(annotation: Union[type, str], callable: Callable[..., Any]) -> type:
46+
"Maps a string (forward) reference to a type, as if using `from __future__ import annotations`."
3947

4048
if isinstance(annotation, str):
41-
return eval(annotation, callable.__globals__)
49+
return typing.cast(type, eval(annotation, callable.__globals__))
4250
else:
4351
return annotation
4452

@@ -108,7 +116,7 @@ def get_route(self) -> str:
108116

109117

110118
class _FormatParameterExtractor:
111-
"A visitor to exract parameters in a format string."
119+
"A visitor to extract parameters in a format string."
112120

113121
keys: list[str]
114122

@@ -126,7 +134,7 @@ def _get_route_parameters(route: str) -> list[str]:
126134
return extractor.keys
127135

128136

129-
def _get_endpoint_functions(endpoint: type, prefixes: list[str]) -> Iterator[tuple[str, str, str, Callable]]:
137+
def _get_endpoint_functions(endpoint: type, prefixes: list[str]) -> Iterator[tuple[str, str, str, Callable[..., Any]]]:
130138
if not inspect.isclass(endpoint):
131139
raise ValidationError(f"object is not a class type: {endpoint}")
132140

@@ -247,7 +255,8 @@ async def get_object(self, uuid: str, version: int) -> Object:
247255
if request_param is not None:
248256
param = (param_name, param_type)
249257
raise ValidationError(
250-
f"only a single composite type is permitted in a signature but multiple composite types found in function '{func_name}': {request_param} and {param}"
258+
f"only a single composite type is permitted in a signature but multiple composite types found in function '{func_name}': "
259+
f"{request_param} and {param}"
251260
)
252261

253262
# composite types are read from body

0 commit comments

Comments
 (0)