Files
jcloud-docsgen/src/jcloud_docsgen/core/python/arguments.py
T
jakob.scheid cc786a6c69 fix(python-generator): make signatures compliant with PEP 8
The '=' character at a parameter in the signature was surrounded with
spaces previously. That is no longer the case, unless there is a type
hint.
2026-09-22 15:55:57 +02:00

296 lines
9.2 KiB
Python

# Copyright 2026 jCloud
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import ast
import keyword
import types
from enum import Enum
from ...exceptions import (
InvalidPythonAnnotationError,
InvalidPythonIdentifierError,
PythonArgumentStructureError,
)
from ...utils import assert_that_is_instance
__all__ = [
'PythonArgumentKind',
'PythonFunctionArgument',
'PythonASTArgumentsListParser',
]
class PythonArgumentKind(Enum):
NORMAL = 0
POSITIONAL_ONLY = 1
KEYWORD_ONLY = 2
VARARG = 3
KWARGS = 4
class PythonFunctionArgument:
"""
Represents an argument of a Python function.
:param name: The name of the argument.
:type name: str
:param kind: The kind of the argument.
:type kind: ArgumentKind
:param default: The default value of the argument.
:type default: str | None
:param annotation: The type annotation of the argument.
:type annotation: str | None
"""
def __init__(
self,
name: str,
kind: PythonArgumentKind,
default: str | None,
annotation: str | None,
) -> None:
assert_that_is_instance(name, str)
assert_that_is_instance(kind, PythonArgumentKind)
assert_that_is_instance(default, (str, types.NoneType))
assert_that_is_instance(annotation, (str, types.NoneType))
# check whether name is a valid identifier
if not name or not name.isidentifier() or keyword.iskeyword(name):
raise InvalidPythonIdentifierError(
'invalid identifier',
identifier=name,
)
# check whether the annotation is a valid annotation
# Currently, it is only checked whether the parameter is not
# empty.
if annotation is not None and not annotation:
raise InvalidPythonAnnotationError(
'invalid annotation',
annotation=annotation,
)
self.name = name
self.kind = kind
self.default = default
self.annotation = annotation
if (
self.kind
in (
PythonArgumentKind.VARARG,
PythonArgumentKind.KWARGS,
)
and self.default is not None
):
raise PythonArgumentStructureError(
'* or ** arguments cannot have default values',
definition=self.signature_repr(),
)
def __eq__(self, value: PythonFunctionArgument) -> bool:
if not isinstance(value, PythonFunctionArgument):
return False
return (
self.name == value.name
and self.kind == value.kind
and self.default == value.default
and self.annotation == value.annotation
)
def __repr__(self) -> str:
return type(self).__name__ + repr(
(
self.name,
self.kind,
self.default,
self.annotation,
)
)
def signature_repr(self) -> str:
"""
Returns the function signature representation of the argument.
:return: The string representation of the argument.
:rtype: str
"""
if self.kind == PythonArgumentKind.KWARGS:
str_repr = '**'
elif self.kind == PythonArgumentKind.VARARG:
str_repr = '*'
else:
str_repr = ''
str_repr += self.name
if self.annotation is not None:
str_repr += ': ' + self.annotation
if self.default is not None:
if self.annotation is None:
str_repr += '=' + self.default
else:
str_repr += ' = ' + self.default
return str_repr
class PythonASTArgumentsListParser:
"""
A parser for making ``PythonFunctionArgument`` objects from an
``ast.arguments`` arguments list.
:param ast_arguments_list: The ``ast.arguments`` arguments list.
:type ast_arguments_list: ast.arguments
"""
def __init__(self, ast_arguments_list: ast.arguments) -> None:
self.ast_arguments_list = ast_arguments_list
def to_argument_list(self) -> list[PythonFunctionArgument]:
"""
Converts the AST arguments list to a list of Python function
argument objects.
:return: The list of Python function argument objects.
:rtype: list[PythonFunctionArgument]
"""
defaults = (
[
None
for _ in range(
len(
self.ast_arguments_list.posonlyargs
+ self.ast_arguments_list.args
+ self.ast_arguments_list.kwonlyargs
)
- len(self.ast_arguments_list.defaults)
- len(self.ast_arguments_list.kw_defaults)
)
]
+ [ast.unparse(d) for d in self.ast_arguments_list.defaults]
+ [
ast.unparse(d) if d is not None else None
for d in self.ast_arguments_list.kw_defaults
]
)
i = 0
arguments = []
for arg in self.ast_arguments_list.posonlyargs:
if arg.annotation is None:
arguments.append(
PythonFunctionArgument(
arg.arg,
PythonArgumentKind.POSITIONAL_ONLY,
defaults[i],
None,
),
)
else:
arguments.append(
PythonFunctionArgument(
arg.arg,
PythonArgumentKind.POSITIONAL_ONLY,
defaults[i],
ast.unparse(arg.annotation),
),
)
i += 1
for arg in self.ast_arguments_list.args:
if arg.annotation is None:
arguments.append(
PythonFunctionArgument(
arg.arg,
PythonArgumentKind.NORMAL,
defaults[i],
None,
),
)
else:
arguments.append(
PythonFunctionArgument(
arg.arg,
PythonArgumentKind.NORMAL,
defaults[i],
ast.unparse(arg.annotation),
),
)
i += 1
if self.ast_arguments_list.vararg is not None:
if self.ast_arguments_list.vararg.annotation is None:
arguments.append(
PythonFunctionArgument(
self.ast_arguments_list.vararg.arg,
PythonArgumentKind.VARARG,
None,
None,
),
)
else:
arguments.append(
PythonFunctionArgument(
self.ast_arguments_list.vararg.arg,
PythonArgumentKind.VARARG,
None,
ast.unparse(self.ast_arguments_list.vararg.annotation),
),
)
for arg in self.ast_arguments_list.kwonlyargs:
if arg.annotation is None:
arguments.append(
PythonFunctionArgument(
arg.arg,
PythonArgumentKind.KEYWORD_ONLY,
defaults[i],
None,
),
)
else:
arguments.append(
PythonFunctionArgument(
arg.arg,
PythonArgumentKind.KEYWORD_ONLY,
defaults[i],
ast.unparse(arg.annotation),
),
)
i += 1
if self.ast_arguments_list.kwarg is not None:
if self.ast_arguments_list.kwarg.annotation is None:
arguments.append(
PythonFunctionArgument(
self.ast_arguments_list.kwarg.arg,
PythonArgumentKind.KWARGS,
None,
None,
),
)
else:
arguments.append(
PythonFunctionArgument(
self.ast_arguments_list.kwarg.arg,
PythonArgumentKind.KWARGS,
None,
ast.unparse(self.ast_arguments_list.kwarg.annotation),
),
)
return arguments