From 91cecae2306b6f3700b73d7df2e589cb39b877ba Mon Sep 17 00:00:00 2001 From: Jakob Scheid Date: Sat, 11 Apr 2026 13:33:42 +0200 Subject: [PATCH] Add classes for Python function arguments --- docs/CHANGELOG.md | 3 +- src/jcloud_docsgen/core/python/arguments.py | 152 ++++++++++++++++ src/jcloud_docsgen/exceptions.py | 34 +++- .../test_PythonASTArgumentsListParser.py | 163 ++++++++++++++++++ .../arguments/test_PythonFunctionArgument.py | 103 +++++++++++ .../test_PythonFunctionArgumentDefault.py | 55 ++++++ 6 files changed, 508 insertions(+), 2 deletions(-) create mode 100644 src/jcloud_docsgen/core/python/arguments.py create mode 100644 tests/unit/core/python/arguments/test_PythonASTArgumentsListParser.py create mode 100644 tests/unit/core/python/arguments/test_PythonFunctionArgument.py create mode 100644 tests/unit/core/python/arguments/test_PythonFunctionArgumentDefault.py diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 3056253..6bb5a63 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,4 +15,5 @@ - Add feature to compare `core.python.namespaces.PythonPackageNamespace` instances or `core.python.namespaces.PythonModuleNamespace` instances - Add string representation for python namespace objects - Add `PythonDocumentationGenerator` method to collect all namespaces -- Add class for existing files \ No newline at end of file +- Add class for existing files +- Add classes for Python function arguments \ No newline at end of file diff --git a/src/jcloud_docsgen/core/python/arguments.py b/src/jcloud_docsgen/core/python/arguments.py new file mode 100644 index 0000000..ada2e5f --- /dev/null +++ b/src/jcloud_docsgen/core/python/arguments.py @@ -0,0 +1,152 @@ +# Copyright 2026 jCloud Services GbR + +# 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 +from enum import Enum +from typing import Union +import ast +from ...utils import assert_that_is_instance +from ...exceptions import InvalidPythonIdentifierError, InvalidPythonAnnotationError +import keyword +import types + +__all__ = [ + 'PythonArgumentKind', + 'PythonFunctionArgumentDefault', + 'PythonFunctionArgument', + 'PythonASTArgumentsListParser' +] + +class PythonArgumentKind(Enum): + NORMAL = 0 + POSITIONAL_ONLY = 1 + KEYWORD_ONLY = 2 + VARARG = 3 + KWARGS = 4 + +class PythonFunctionArgumentDefault: + ''' + Represents the default value of a Python function argument. + + :param value: The value + :type value: ast.expr + ''' + + def __init__(self, value: ast.expr) -> None: + assert_that_is_instance(value, ast.expr) + + self.value = value + + def __eq__(self, value: PythonFunctionArgumentDefault) -> bool: + if not isinstance(value, PythonFunctionArgumentDefault): + return False + return ast.dump(value.value, include_attributes = False) == ast.dump(self.value, include_attributes = False) + +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: Union[PythonFunctionArgumentDefault, None] + :param annotation: The type annotation of the argument. + :type annotation: Union[str, None] + ''' + + def __init__(self, name: str, kind: PythonArgumentKind, default: Union[PythonFunctionArgumentDefault, None], annotation: Union[str, None]) -> None: + assert_that_is_instance(name, str) + assert_that_is_instance(kind, PythonArgumentKind) + assert_that_is_instance(default, (PythonFunctionArgumentDefault, 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 + + 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 + )) + +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] + ''' + + arguments = [] + for arg in self.ast_arguments_list.posonlyargs: + if arg.annotation is None: + arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.POSITIONAL_ONLY, None, None)) + else: + arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.POSITIONAL_ONLY, None, arg.annotation.id)) + for arg in self.ast_arguments_list.args: + if arg.annotation is None: + arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.NORMAL, None, None)) + else: + arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.NORMAL, None, arg.annotation.id)) + 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, self.ast_arguments_list.vararg.annotation.id)) + for arg in self.ast_arguments_list.kwonlyargs: + if arg.annotation is None: + arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.KEYWORD_ONLY, None, None)) + else: + arguments.append(PythonFunctionArgument(arg.arg, PythonArgumentKind.KEYWORD_ONLY, None, arg.annotation.id)) + 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, self.ast_arguments_list.kwarg.annotation.id)) + + return arguments \ No newline at end of file diff --git a/src/jcloud_docsgen/exceptions.py b/src/jcloud_docsgen/exceptions.py index 3cbc18f..26bbb87 100644 --- a/src/jcloud_docsgen/exceptions.py +++ b/src/jcloud_docsgen/exceptions.py @@ -28,4 +28,36 @@ class NamespaceError(ValueError): class InvalidNamespaceError(NamespaceError): ... class NamespaceNotFoundError(NamespaceError): ... -class NamespaceExistsError(NamespaceError): ... \ No newline at end of file +class NamespaceExistsError(NamespaceError): ... + +class PythonIdentifierError(ValueError): + ''' + Base class for Python identifier errors. + ''' + def __init__(self, *args: object, identifier: str = '') -> None: + super().__init__(*args) + self.identifier = identifier + + def __str__(self): + if not self.args: + return '' + else: + return f'{self.args[0]}{": " if self.identifier and self.args[0] else ""}{self.identifier if self.args[0] else ""}' + +class InvalidPythonIdentifierError(PythonIdentifierError): ... + +class PythonAnnotationError(ValueError): + ''' + Base class for Python annotation errors. + ''' + def __init__(self, *args: object, annotation: str = '') -> None: + super().__init__(*args) + self.annotation = annotation + + def __str__(self): + if not self.args: + return '' + else: + return f'{self.args[0]}{": " if self.annotation and self.args[0] else ""}{self.annotation if self.args[0] else ""}' + +class InvalidPythonAnnotationError(PythonAnnotationError): ... \ No newline at end of file diff --git a/tests/unit/core/python/arguments/test_PythonASTArgumentsListParser.py b/tests/unit/core/python/arguments/test_PythonASTArgumentsListParser.py new file mode 100644 index 0000000..ecaf5c3 --- /dev/null +++ b/tests/unit/core/python/arguments/test_PythonASTArgumentsListParser.py @@ -0,0 +1,163 @@ +# Copyright 2026 jCloud Services GbR + +# 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 src.jcloud_docsgen.core.python.arguments import PythonASTArgumentsListParser, PythonFunctionArgument, PythonArgumentKind +import pytest +import ast + +def argument_list(argument_list: str) -> ast.arguments: + ''' + Returns an AST argument list from the argument list source code + + :param argument_list: The argument list. + :type argument_list: str + + :return: The AST argument list. + :rtype: ast.arguments + ''' + + return ast.parse('def func(' + argument_list + '): ...').body[0].args + +@pytest.mark.parametrize('ast_arguments_list,expected', [ + (argument_list('a'), [PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None)]), + (argument_list('a: str'), [PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'str')]), + (argument_list('a: str, b'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'str'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None) + ]), + (argument_list('a: str, b: int'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'str'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int') + ]), + (argument_list('a, b: int'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int') + ]), + (argument_list('a, b, *args'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None) + ]), + (argument_list('a: int, b, *args'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None) + ]), + (argument_list('a, b: str, *args'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'str'), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None) + ]), + (argument_list('a: int, b: str, *args'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'str'), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None) + ]), + (argument_list('a: int, b: str, *args: float'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'str'), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'float') + ]), + (argument_list('a: int, b, *args: float'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'float') + ]), + (argument_list('a: int, *args: float'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'float') + ]), + (argument_list('a: int, /, **kwargs'), [ + PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'), + PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None) + ]), + (argument_list('a: int, **kwargs: str'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, 'str') + ]), + (argument_list('a: int, /, b, **kwargs'), [ + PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None) + ]), + (argument_list('a: int, /, b'), [ + PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None), + ]), + (argument_list('a: int, /, b, *, c'), [ + PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None), + ]), + (argument_list('a, b, *, c'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None), + ]), + (argument_list('a, b, *args, c'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None), + PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None), + ]), + (argument_list('a, b, *args, c, **kwargs'), [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, None), + PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None), + PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None), + ]), + (argument_list('a, b, /, *, c, **kwargs'), [ + PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None), + PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None), + PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, None), + PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None), + ]), + (argument_list('a, b, /, c, *, d, **kwargs'), [ + PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, None), + PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, None), + PythonFunctionArgument('c', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, None, None), + PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, None), + ]), + (argument_list('a: str, b: int, /, c: float, d: list, *, e: tuple, f: dict, **kwargs: set'), [ + PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'str'), + PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'), + PythonFunctionArgument('c', PythonArgumentKind.NORMAL, None, 'float'), + PythonFunctionArgument('d', PythonArgumentKind.NORMAL, None, 'list'), + PythonFunctionArgument('e', PythonArgumentKind.KEYWORD_ONLY, None, 'tuple'), + PythonFunctionArgument('f', PythonArgumentKind.KEYWORD_ONLY, None, 'dict'), + PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, 'set'), + ]), + (argument_list('a: str, b: int, /, c: float, d: list, *args: bytes, e: tuple, f: dict, **kwargs: set'), [ + PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'str'), + PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'), + PythonFunctionArgument('c', PythonArgumentKind.NORMAL, None, 'float'), + PythonFunctionArgument('d', PythonArgumentKind.NORMAL, None, 'list'), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'bytes'), + PythonFunctionArgument('e', PythonArgumentKind.KEYWORD_ONLY, None, 'tuple'), + PythonFunctionArgument('f', PythonArgumentKind.KEYWORD_ONLY, None, 'dict'), + PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, 'set'), + ]), + (argument_list('a: str, b: int, /, *args: bytes, c: tuple, d: dict, **kwargs: set'), [ + PythonFunctionArgument('a', PythonArgumentKind.POSITIONAL_ONLY, None, 'str'), + PythonFunctionArgument('b', PythonArgumentKind.POSITIONAL_ONLY, None, 'int'), + PythonFunctionArgument('args', PythonArgumentKind.VARARG, None, 'bytes'), + PythonFunctionArgument('c', PythonArgumentKind.KEYWORD_ONLY, None, 'tuple'), + PythonFunctionArgument('d', PythonArgumentKind.KEYWORD_ONLY, None, 'dict'), + PythonFunctionArgument('kwargs', PythonArgumentKind.KWARGS, None, 'set'), + ]), +]) +def test_PythonASTArgumentsListParser_to_argument_list(ast_arguments_list, expected): + assert PythonASTArgumentsListParser(ast_arguments_list).to_argument_list() == expected \ No newline at end of file diff --git a/tests/unit/core/python/arguments/test_PythonFunctionArgument.py b/tests/unit/core/python/arguments/test_PythonFunctionArgument.py new file mode 100644 index 0000000..4b17f2d --- /dev/null +++ b/tests/unit/core/python/arguments/test_PythonFunctionArgument.py @@ -0,0 +1,103 @@ +# Copyright 2026 jCloud Services GbR + +# 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 src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind, PythonFunctionArgumentDefault +from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError, InvalidPythonAnnotationError +import ast +import pytest + +@pytest.mark.parametrize('name,kind,default,annotation,expected_exception,expected_exception_msg', [ + (None, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'NoneType\''), + (1, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'int\''), + (42, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'int\''), + (4.2, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'float\''), + (4.2, PythonArgumentKind.NORMAL, None, None, TypeError, 'expected \'str\', got \'float\''), + ('a', PythonArgumentKind.NORMAL, 1, None, TypeError, 'expected either \'PythonFunctionArgumentDefault\' or \'NoneType\', got \'int\''), + ('a', PythonArgumentKind.NORMAL, None, 1, TypeError, 'expected either \'str\' or \'NoneType\', got \'int\''), + ('a', 0, None, None, TypeError, 'expected \'PythonArgumentKind\', got \'int\''), + ('a', PythonArgumentKind.NORMAL, None, '', InvalidPythonAnnotationError, 'invalid annotation'), + ('', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier'), + ('.', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: .'), + ('#', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: #'), + ('1a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: 1a'), + ('1_', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: 1_'), + ('1,', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: 1,'), + (',', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: ,'), + (':', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: :'), + ('-', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: -'), + (';', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: ;'), + ('|', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: |'), + ('<', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: <'), + ('>', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: >'), + ('a,', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a,'), + (',a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: ,a'), + ('a:', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a:'), + (':a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: :a'), + ('a-', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a-'), + ('-a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: -a'), + ('a;', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a;'), + (';a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: ;a'), + ('a|', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a|'), + ('|a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: |a'), + ('a<', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a<'), + ('', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: a>'), + ('>a', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: >a'), + ('class', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: class'), + ('def', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: def'), + ('lambda', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: lambda'), + ('None', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: None'), + ('True', PythonArgumentKind.NORMAL, None, None, InvalidPythonIdentifierError, 'invalid identifier: True'), +]) +def test_PythonFunctionArgument_exceptions(name, kind, default, annotation, expected_exception, expected_exception_msg): + with pytest.raises(expected_exception) as exc_info: + PythonFunctionArgument(name, kind, default, annotation) + assert str(exc_info.value) == expected_exception_msg + +@pytest.mark.parametrize('name', [ + 'a', + '_a', + 'a1', + 'a0', + 'a_', + '_', + '_abc', + 'abc', + 'abc_', + 'match', + 'case', + 'type', + 'รค', +]) +@pytest.mark.parametrize('kind', [ + PythonArgumentKind.NORMAL, + PythonArgumentKind.POSITIONAL_ONLY, + PythonArgumentKind.KEYWORD_ONLY, + PythonArgumentKind.VARARG, + PythonArgumentKind.KWARGS, +]) +@pytest.mark.parametrize('default', [ + None, + PythonFunctionArgumentDefault(ast.Constant(value = 'x')), + PythonFunctionArgumentDefault(ast.Constant(value = 1)), + PythonFunctionArgumentDefault(ast.Constant(value = None)), +]) +@pytest.mark.parametrize('annotation', [ + None, + 'a', + 'list[int]', + '_', +]) +def test_PythonFunctionArgument(name, kind, default, annotation): + PythonFunctionArgument(name, kind, default, annotation) \ No newline at end of file diff --git a/tests/unit/core/python/arguments/test_PythonFunctionArgumentDefault.py b/tests/unit/core/python/arguments/test_PythonFunctionArgumentDefault.py new file mode 100644 index 0000000..eccd860 --- /dev/null +++ b/tests/unit/core/python/arguments/test_PythonFunctionArgumentDefault.py @@ -0,0 +1,55 @@ +# Copyright 2026 jCloud Services GbR + +# 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 src.jcloud_docsgen.core.python.arguments import PythonFunctionArgumentDefault +import pytest +import ast + +@pytest.mark.parametrize('default,expected', [ + (PythonFunctionArgumentDefault(ast.Constant(value = '')), ast.Constant), + (PythonFunctionArgumentDefault(ast.Constant(value = 'x')), ast.Constant), + (PythonFunctionArgumentDefault(ast.Constant(value = None)), ast.Constant), + (PythonFunctionArgumentDefault(ast.Constant(value = 1)), ast.Constant), +]) +def test_PythonFunctionArgumentDefault_value_attribute_type(default, expected): + assert isinstance(default.value, expected) + +@pytest.mark.parametrize('value,expected_exception_msg', [ + (None, 'expected \'expr\', got \'NoneType\''), + (1, 'expected \'expr\', got \'int\''), + ('x', 'expected \'expr\', got \'str\''), + (42, 'expected \'expr\', got \'int\''), + (4.2, 'expected \'expr\', got \'float\''), +]) +def test_PythonFunctionArgumentDefault_raises_TypeError(value, expected_exception_msg): + with pytest.raises(TypeError) as exc_info: + PythonFunctionArgumentDefault(value) + assert str(exc_info.value) == expected_exception_msg + +@pytest.mark.parametrize('default1,default2,expected', [ + (PythonFunctionArgumentDefault(ast.Constant(value = 1)), PythonFunctionArgumentDefault(ast.Constant(value = 1)), True), + (PythonFunctionArgumentDefault(ast.Constant(value = 1)), PythonFunctionArgumentDefault(ast.Constant(value = 2)), False), + (PythonFunctionArgumentDefault(ast.Constant(value = 'a')), PythonFunctionArgumentDefault(ast.Constant(value = 2)), False), + (PythonFunctionArgumentDefault(ast.Constant(value = 'a')), PythonFunctionArgumentDefault(ast.Constant(value = 'a')), True), + (PythonFunctionArgumentDefault(ast.Constant(value = 'a')), PythonFunctionArgumentDefault(ast.Constant(value = 'b')), False), + (PythonFunctionArgumentDefault(ast.List(elts = [])), PythonFunctionArgumentDefault(ast.List(elts = [])), True), + (PythonFunctionArgumentDefault(ast.List(elts = [1])), PythonFunctionArgumentDefault(ast.List(elts = [])), False), + (PythonFunctionArgumentDefault(ast.List(elts = [1])), PythonFunctionArgumentDefault(ast.List(elts = [1])), True), + (PythonFunctionArgumentDefault(ast.List(elts = [1])), PythonFunctionArgumentDefault(ast.List(elts = [2])), False), + (PythonFunctionArgumentDefault(ast.List(elts = [1])), PythonFunctionArgumentDefault(ast.List(elts = [1, 2])), False), + (PythonFunctionArgumentDefault(ast.List(elts = [1, 2])), PythonFunctionArgumentDefault(ast.List(elts = [1, 2])), True), + (PythonFunctionArgumentDefault(ast.List(elts = [2, 1])), PythonFunctionArgumentDefault(ast.List(elts = [1, 2])), False), +]) +def test_PythonFunctionArgumentDefault___eq__(default1, default2, expected): + assert (default1 == default2) == expected \ No newline at end of file