# 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