Skip to content

Commit fc39b40

Browse files
authored
ci: Add Pylint (#472)
* docs: Update Pydantic schema docstrings to use links instead of copied strings. * refactor: Cleanup for pylint * fix: Stop assuming OpenAPI is semver since it soon won't be * ci: Clean up all remaining pylint issues and add to `task check` and CI jobs. * ci: Fix pylint in CI * ci: Fix typing Literal in Python < 3.8 * ci: Disable pylint no-name-in-module * ci: Disable pylint import-error
1 parent 4072c47 commit fc39b40

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+645
-1433
lines changed

.github/workflows/checks.yml

+3
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ jobs:
5050
- name: Run mypy
5151
run: poetry run mypy --show-error-codes openapi_python_client
5252

53+
- name: Run pylint
54+
run: poetry run pylint openapi_python_client
55+
5356
- name: Run pytest
5457
run: poetry run pytest --cov=openapi_python_client --cov-report=term-missing tests end_to_end_tests/test_end_to_end.py
5558

openapi_python_client/__init__.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828

2929

3030
class MetaType(str, Enum):
31+
"""The types of metadata supported for project generation."""
32+
3133
NONE = "none"
3234
POETRY = "poetry"
3335
SETUP = "setup"
@@ -41,7 +43,9 @@ class MetaType(str, Enum):
4143
}
4244

4345

44-
class Project:
46+
class Project: # pylint: disable=too-many-instance-attributes
47+
"""Represents a Python project (the top level file-tree) to generate"""
48+
4549
def __init__(
4650
self,
4751
*,
@@ -129,15 +133,19 @@ def _reformat(self) -> None:
129133
shell=True,
130134
stdout=subprocess.PIPE,
131135
stderr=subprocess.PIPE,
136+
check=True,
132137
)
133138
subprocess.run(
134139
"isort .",
135140
cwd=self.project_dir,
136141
shell=True,
137142
stdout=subprocess.PIPE,
138143
stderr=subprocess.PIPE,
144+
check=True,
145+
)
146+
subprocess.run(
147+
"black .", cwd=self.project_dir, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True
139148
)
140-
subprocess.run("black .", cwd=self.project_dir, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
141149

142150
def _get_errors(self) -> Sequence[GeneratorError]:
143151
errors = []
@@ -263,7 +271,7 @@ def _build_api(self) -> None:
263271
module_path.write_text(endpoint_template.render(endpoint=endpoint), encoding=self.file_encoding)
264272

265273

266-
def _get_project_for_url_or_path(
274+
def _get_project_for_url_or_path( # pylint: disable=too-many-arguments
267275
url: Optional[str],
268276
path: Optional[Path],
269277
meta: MetaType,

openapi_python_client/cli.py

+15-13
Original file line numberDiff line numberDiff line change
@@ -27,28 +27,28 @@ def _process_config(path: Optional[pathlib.Path]) -> Config:
2727

2828
try:
2929
return Config.load_from_path(path=path)
30-
except: # noqa
31-
raise typer.BadParameter("Unable to parse config")
30+
except Exception as err:
31+
raise typer.BadParameter("Unable to parse config") from err
3232

3333

3434
# noinspection PyUnusedLocal
35+
# pylint: disable=unused-argument
3536
@app.callback(name="openapi-python-client")
3637
def cli(
3738
version: bool = typer.Option(False, "--version", callback=_version_callback, help="Print the version and exit"),
3839
) -> None:
3940
"""Generate a Python client from an OpenAPI JSON document"""
40-
pass
4141

4242

43-
def _print_parser_error(e: GeneratorError, color: str) -> None:
44-
typer.secho(e.header, bold=True, fg=color, err=True)
43+
def _print_parser_error(err: GeneratorError, color: str) -> None:
44+
typer.secho(err.header, bold=True, fg=color, err=True)
4545
typer.echo()
46-
if e.detail:
47-
typer.secho(e.detail, fg=color, err=True)
46+
if err.detail:
47+
typer.secho(err.detail, fg=color, err=True)
4848
typer.echo()
4949

50-
if isinstance(e, ParseError) and e.data is not None:
51-
formatted_data = pformat(e.data)
50+
if isinstance(err, ParseError) and err.data is not None:
51+
formatted_data = pformat(err.data)
5252
typer.secho(formatted_data, fg=color, err=True)
5353

5454
typer.echo()
@@ -111,6 +111,7 @@ def handle_errors(errors: Sequence[GeneratorError], fail_on_warning: bool = Fals
111111
CONFIG_OPTION = typer.Option(None, "--config", help="Path to the config file to use")
112112

113113

114+
# pylint: disable=too-many-arguments
114115
@app.command()
115116
def generate(
116117
url: Optional[str] = typer.Option(None, help="A URL to read the JSON from"),
@@ -133,9 +134,9 @@ def generate(
133134

134135
try:
135136
codecs.getencoder(file_encoding)
136-
except LookupError:
137+
except LookupError as err:
137138
typer.secho("Unknown encoding : {}".format(file_encoding), fg=typer.colors.RED)
138-
raise typer.Exit(code=1)
139+
raise typer.Exit(code=1) from err
139140

140141
config = _process_config(config_path)
141142
errors = create_new_client(
@@ -149,6 +150,7 @@ def generate(
149150
handle_errors(errors, fail_on_warning)
150151

151152

153+
# pylint: disable=too-many-arguments
152154
@app.command()
153155
def update(
154156
url: Optional[str] = typer.Option(None, help="A URL to read the JSON from"),
@@ -171,9 +173,9 @@ def update(
171173

172174
try:
173175
codecs.getencoder(file_encoding)
174-
except LookupError:
176+
except LookupError as err:
175177
typer.secho("Unknown encoding : {}".format(file_encoding), fg=typer.colors.RED)
176-
raise typer.Exit(code=1)
178+
raise typer.Exit(code=1) from err
177179

178180
config = _process_config(config_path)
179181
errors = update_existing_client(

openapi_python_client/config.py

+10
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,21 @@
66

77

88
class ClassOverride(BaseModel):
9+
"""An override of a single generated class.
10+
11+
See https://github.com/openapi-generators/openapi-python-client#class_overrides
12+
"""
13+
914
class_name: Optional[str] = None
1015
module_name: Optional[str] = None
1116

1217

1318
class Config(BaseModel):
19+
"""Contains any configurable values passed by the user.
20+
21+
See https://github.com/openapi-generators/openapi-python-client#configuration
22+
"""
23+
1424
class_overrides: Dict[str, ClassOverride] = {}
1525
project_name_override: Optional[str]
1626
package_name_override: Optional[str]

openapi_python_client/parser/errors.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,4 @@ class PropertyError(ParseError):
4040

4141

4242
class ValidationError(Exception):
43-
pass
43+
"""Used internally to exit quickly from property parsing due to some internal exception."""

openapi_python_client/parser/openapi.py

+40-12
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ def generate_operation_id(*, path: str, method: str) -> str:
8181
return f"{method}_{clean_path}"
8282

8383

84+
# pylint: disable=too-many-instance-attributes
8485
@dataclass
8586
class Endpoint:
8687
"""
@@ -244,10 +245,32 @@ def _add_responses(
244245
endpoint.responses.append(response)
245246
return endpoint, schemas
246247

248+
# pylint: disable=too-many-return-statements
247249
@staticmethod
248250
def add_parameters(
249251
*, endpoint: "Endpoint", data: Union[oai.Operation, oai.PathItem], schemas: Schemas, config: Config
250252
) -> Tuple[Union["Endpoint", ParseError], Schemas]:
253+
"""Process the defined `parameters` for an Endpoint.
254+
255+
Any existing parameters will be ignored, so earlier instances of a parameter take precedence. PathItem
256+
parameters should therefore be added __after__ operation parameters.
257+
258+
Args:
259+
endpoint: The endpoint to add parameters to.
260+
data: The Operation or PathItem to add parameters from.
261+
schemas: The cumulative Schemas of processing so far which should contain details for any references.
262+
config: User-provided config for overrides within parameters.
263+
264+
Returns:
265+
`(result, schemas)` where `result` is either an updated Endpoint containing the parameters or a ParseError
266+
describing what went wrong. `schemas` is an updated version of the `schemas` input, adding any new enums
267+
or classes.
268+
269+
See Also:
270+
- https://swagger.io/docs/specification/describing-parameters/
271+
- https://swagger.io/docs/specification/paths-and-operations/
272+
"""
273+
251274
endpoint = deepcopy(endpoint)
252275
if data.parameters is None:
253276
return endpoint, schemas
@@ -329,6 +352,16 @@ def add_parameters(
329352

330353
@staticmethod
331354
def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]:
355+
"""
356+
Sorts the path parameters of an `endpoint` so that they match the order declared in `endpoint.path`.
357+
358+
Args:
359+
endpoint: The endpoint to sort the parameters of.
360+
361+
Returns:
362+
Either an updated `endpoint` with sorted path parameters or a `ParseError` if something was wrong with
363+
the path parameters and they could not be sorted.
364+
"""
332365
endpoint = deepcopy(endpoint)
333366
parameters_from_path = re.findall(_PATH_PARAM_REGEX, endpoint.path)
334367
try:
@@ -338,8 +371,8 @@ def sort_parameters(*, endpoint: "Endpoint") -> Union["Endpoint", ParseError]:
338371
endpoint.path_parameters = OrderedDict((param.name, param) for param in sorted_params)
339372
except ValueError:
340373
pass # We're going to catch the difference down below
341-
path_parameter_names = [name for name in endpoint.path_parameters]
342-
if parameters_from_path != path_parameter_names:
374+
375+
if parameters_from_path != list(endpoint.path_parameters):
343376
return ParseError(
344377
detail=f"Incorrect path templating for {endpoint.path} (Path parameters do not match with path)",
345378
)
@@ -397,22 +430,17 @@ class GeneratorData:
397430
enums: Iterator[EnumProperty]
398431

399432
@staticmethod
400-
def from_dict(d: Dict[str, Any], *, config: Config) -> Union["GeneratorData", GeneratorError]:
433+
def from_dict(data: Dict[str, Any], *, config: Config) -> Union["GeneratorData", GeneratorError]:
401434
"""Create an OpenAPI from dict"""
402435
try:
403-
openapi = oai.OpenAPI.parse_obj(d)
404-
except ValidationError as e:
405-
detail = str(e)
406-
if "swagger" in d:
436+
openapi = oai.OpenAPI.parse_obj(data)
437+
except ValidationError as err:
438+
detail = str(err)
439+
if "swagger" in data:
407440
detail = (
408441
"You may be trying to use a Swagger document; this is not supported by this project.\n\n" + detail
409442
)
410443
return GeneratorError(header="Failed to parse OpenAPI document", detail=detail)
411-
if openapi.openapi.major != 3:
412-
return GeneratorError(
413-
header="openapi-python-client only supports OpenAPI 3.x",
414-
detail=f"The version of the provided document was {openapi.openapi}",
415-
)
416444
schemas = Schemas()
417445
if openapi.components and openapi.components.schemas:
418446
schemas = build_schemas(components=openapi.components.schemas, schemas=schemas, config=config)

0 commit comments

Comments
 (0)