diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index e149bb3..d0691bd 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -18,4 +18,5 @@ - Add class for existing files - Add classes for Python function arguments - Add classes for Python definitions -- Add class for Python modules \ No newline at end of file +- Add class for Python modules +- Add feature to get the signature representation of a Python function argument \ No newline at end of file diff --git a/src/jcloud_docsgen/core/python/arguments.py b/src/jcloud_docsgen/core/python/arguments.py index b67491d..8710c67 100644 --- a/src/jcloud_docsgen/core/python/arguments.py +++ b/src/jcloud_docsgen/core/python/arguments.py @@ -17,7 +17,7 @@ from enum import Enum from typing import Union import ast from ...utils import assert_that_is_instance -from ...exceptions import InvalidPythonIdentifierError, InvalidPythonAnnotationError +from ...exceptions import InvalidPythonIdentifierError, InvalidPythonAnnotationError, PythonArgumentStructureError import keyword import types @@ -68,6 +68,9 @@ class PythonFunctionArgument: 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): @@ -82,6 +85,31 @@ class PythonFunctionArgument: 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: + str_repr += ' = ' + self.default + + return str_repr + class PythonASTArgumentsListParser: ''' A parser for making ``PythonFunctionArgument`` objects from an diff --git a/src/jcloud_docsgen/exceptions.py b/src/jcloud_docsgen/exceptions.py index 26bbb87..15aa04c 100644 --- a/src/jcloud_docsgen/exceptions.py +++ b/src/jcloud_docsgen/exceptions.py @@ -60,4 +60,20 @@ class PythonAnnotationError(ValueError): 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 +class InvalidPythonAnnotationError(PythonAnnotationError): ... + +class PythonDefinitionError(ValueError): + ''' + Base class for Python definition errors. + ''' + def __init__(self, *args: object, definition: str = '') -> None: + super().__init__(*args) + self.definition = definition + + def __str__(self): + if not self.args: + return '' + else: + return f'{self.args[0]}{": " if self.definition and self.args[0] else ""}{self.definition if self.args[0] else ""}' + +class PythonArgumentStructureError(PythonDefinitionError): ... \ 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 index f128d40..b242fd3 100644 --- a/tests/unit/core/python/arguments/test_PythonFunctionArgument.py +++ b/tests/unit/core/python/arguments/test_PythonFunctionArgument.py @@ -13,8 +13,7 @@ # limitations under the License. from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind -from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError, InvalidPythonAnnotationError -import ast +from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError, InvalidPythonAnnotationError, PythonArgumentStructureError import pytest @pytest.mark.parametrize('name,kind,default,annotation,expected_exception,expected_exception_msg', [ @@ -59,6 +58,8 @@ import pytest ('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'), + ('a', PythonArgumentKind.VARARG, '1', None, PythonArgumentStructureError, '* or ** arguments cannot have default values: *a = 1'), + ('a', PythonArgumentKind.KWARGS, '1', None, PythonArgumentStructureError, '* or ** arguments cannot have default values: **a = 1'), ]) def test_PythonFunctionArgument_exceptions(name, kind, default, annotation, expected_exception, expected_exception_msg): with pytest.raises(expected_exception) as exc_info: @@ -79,13 +80,14 @@ def test_PythonFunctionArgument_exceptions(name, kind, default, annotation, expe 'case', 'type', 'ä', + 'none', + 'true', + 'false', ]) @pytest.mark.parametrize('kind', [ PythonArgumentKind.NORMAL, PythonArgumentKind.POSITIONAL_ONLY, PythonArgumentKind.KEYWORD_ONLY, - PythonArgumentKind.VARARG, - PythonArgumentKind.KWARGS, ]) @pytest.mark.parametrize('default', [ None, @@ -100,4 +102,35 @@ def test_PythonFunctionArgument_exceptions(name, kind, default, annotation, expe '_', ]) def test_PythonFunctionArgument(name, kind, default, annotation): - PythonFunctionArgument(name, kind, default, annotation) \ No newline at end of file + PythonFunctionArgument(name, kind, default, annotation) + +@pytest.mark.parametrize('name', [ + 'a', + '_a', + 'a1', + 'a0', + 'a_', + '_', + '_abc', + 'abc', + 'abc_', + 'match', + 'case', + 'type', + 'ä', + 'none', + 'true', + 'false', +]) +@pytest.mark.parametrize('kind', [ + PythonArgumentKind.VARARG, + PythonArgumentKind.KWARGS, +]) +@pytest.mark.parametrize('annotation', [ + None, + 'a', + 'list[int]', + '_', +]) +def test_PythonFunctionArgument_asterik(name, kind, annotation): + PythonFunctionArgument(name, kind, None, annotation) \ No newline at end of file