diff --git a/src/jcloud_docsgen/core/python/definitions.py b/src/jcloud_docsgen/core/python/definitions.py new file mode 100644 index 0000000..76cf28d --- /dev/null +++ b/src/jcloud_docsgen/core/python/definitions.py @@ -0,0 +1,213 @@ +# 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 typing import Union +import ast +from .arguments import PythonFunctionArgument, PythonASTArgumentsListParser +import keyword +from ...exceptions import InvalidPythonIdentifierError + +__all__ = [ + 'PythonDefinition', + 'PythonFunctionDefinition', + 'PythonAsyncFunctionDefinition', + 'PythonClassDefinition' +] + +class PythonDefinition: + ''' + A base class for Python definition (a class, function or asynchronous + function definition) classes. + + :param name: The name of the definition. + :type name: str + :param doc: The docstring. + :type doc: Union[str, None] + :param decorators: The definition decorators. + :type decorators: list[str] + + :raises InvalidPythonIdentifierError: If the name is not a valid + Python identifier. + ''' + + def __init__(self, name: str, doc: Union[str, None], decorators: list[str]) -> None: + if keyword.iskeyword(name) or not name.isidentifier() or not name: + raise InvalidPythonIdentifierError('invalid identifier', identifier = name) + + self.name = name + self.doc = doc + self.decorators = decorators + + def _arg_list(self) -> tuple: + return ( + self.name, + self.doc, + self.decorators + ) + + def __repr__(self) -> str: + return type(self).__name__ + repr(self._arg_list()) + + @classmethod + def from_node(cls, node: ast.stmt) -> PythonDefinition: + ''' + From an AST node. + + :param node: The AST node. + :type node: ast.stmt + ''' + + return cls( + node.name, + ast.get_docstring(node), + [ast.unparse(d) for d in node.decorator_list] + ) + + def __eq__(self, value: object) -> bool: + if not isinstance(value, PythonDefinition): + return False + return self._arg_list() == value._arg_list() + +class PythonFunctionDefinition(PythonDefinition): + ''' + A PythonDefinition subclass representing a Python function + definition. + + :param name: The name of the definition. + :type name: str + :param args: The arguments. + :type args: list[PythonFunctionArgument] + :param returns: The return type of the function or class. + :type returns: Union[str, None] + :param doc: The docstring. + :type doc: Union[str, None] + :param decorators: The definition decorators. + :type decorators: list[str] + + :raises InvalidPythonIdentifierError: If the name is not a valid + Python identifier. + ''' + + def __init__(self, name: str, args: list[PythonFunctionArgument], returns: Union[str, None], doc: Union[str, None], decorators: list[str]) -> None: + super().__init__(name, doc, decorators) + self.args = args + self.returns = returns + + def _arg_list(self) -> tuple: + return ( + self.name, + self.args, + self.returns, + self.doc, + self.decorators + ) + + @classmethod + def from_node(cls, node: ast.stmt) -> PythonFunctionDefinition: + ''' + From an AST node. + + :param node: The AST node. + :type node: ast.stmt + ''' + + return cls( + node.name, + PythonASTArgumentsListParser(node.args).to_argument_list(), + ast.unparse(node.returns) if node.returns is not None else None, + ast.get_docstring(node), + [ast.unparse(d) for d in node.decorator_list] + ) + +class PythonAsyncFunctionDefinition(PythonFunctionDefinition): + ''' + A PythonDefinition subclass representing an asynchronous Python function + definition. + + :param name: The name of the definition. + :type name: str + :param args: The arguments. + :type args: list[PythonFunctionArgument] + :param returns: The return type of the function or class. + :type returns: Union[str, None] + :param doc: The docstring. + :type doc: Union[str, None] + :param decorators: The definition decorators. + :type decorators: list[str] + + :raises InvalidPythonIdentifierError: If the name is not a valid + Python identifier. + ''' + +class PythonClassDefinition(PythonDefinition): + ''' + A PythonDefinition subclass representing a Python class definition. + + :param name: The name of the definition. + :type name: str + :param bases: The base classes. + :type bases: list[str] + :param doc: The docstring. + :type doc: Union[str, None] + :param decorators: The definition decorators. + :type decorators: list[str] + :param body: + :type body: list[PythonDefinition] + + :raises InvalidPythonIdentifierError: If the name is not a valid + Python identifier. + ''' + + def __init__(self, name: str, bases: list[str], doc: Union[str, None], decorators: list[str], body: list[PythonDefinition]) -> None: + super().__init__(name, doc, decorators) + self.bases = bases + self.body = body + + def _arg_list(self) -> tuple: + return ( + self.name, + self.bases, + self.doc, + self.decorators, + self.body + ) + + @classmethod + def from_node(cls, node: ast.stmt) -> PythonClassDefinition: + ''' + From an AST node. + + :param node: The AST node. + :type node: ast.stmt + ''' + + return cls( + node.name, + [ast.unparse(b) for b in node.bases], + ast.get_docstring(node), + [ast.unparse(d) for d in node.decorator_list], + [ + PythonFunctionDefinition.from_node(expr) + if isinstance(expr, ast.FunctionDef) + + else PythonAsyncFunctionDefinition.from_node(expr) + if isinstance(expr, ast.AsyncFunctionDef) + + else PythonClassDefinition.from_node(expr) + + for expr in node.body + if isinstance(expr, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) + ] + ) \ No newline at end of file diff --git a/tests/unit/core/python/definitions/test_PythonClassDefinition.py b/tests/unit/core/python/definitions/test_PythonClassDefinition.py new file mode 100644 index 0000000..3980214 --- /dev/null +++ b/tests/unit/core/python/definitions/test_PythonClassDefinition.py @@ -0,0 +1,96 @@ +# 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.definitions import PythonFunctionDefinition, PythonClassDefinition +from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind +from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError +from tests.utils.ast_node import ast_node +import pytest + +@pytest.mark.parametrize('definition,expected', [ + (PythonClassDefinition('A', [], None, None, []), 'PythonClassDefinition(\'A\', [], None, None, [])'), + (PythonClassDefinition('A', [], None, 'Hello, World!', []), 'PythonClassDefinition(\'A\', [], None, \'Hello, World!\', [])'), +]) +def test_PythonDefinition_string_representation(definition, expected): + assert repr(definition) == expected + + +@pytest.mark.parametrize('name,args,returns,doc,decorators,expected_exception,expected_exception_message', [ + ('', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier'), + ('.', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: .'), + ('#', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: #'), + ('1a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1a'), + ('1_', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1_'), + ('1,', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1,'), + (',', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ,'), + (':', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: :'), + ('-', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: -'), + (';', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ;'), + ('|', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: |'), + ('<', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: <'), + ('>', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: >'), + ('a,', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a,'), + (',a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ,a'), + ('a:', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a:'), + (':a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: :a'), + ('a-', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a-'), + ('-a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: -a'), + ('a;', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a;'), + (';a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ;a'), + ('a|', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a|'), + ('|a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: |a'), + ('a<', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a<'), + ('', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a>'), + ('>a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: >a'), + ('class', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: class'), + ('def', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: def'), + ('lambda', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: lambda'), + ('None', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: None'), + ('True', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: True'), +]) +def test_PythonFunctionDefinition_exceptions(name, args, returns, doc, decorators, expected_exception, expected_exception_message): + with pytest.raises(expected_exception) as exc_info: + PythonClassDefinition(name, args, returns, doc, decorators) + assert str(exc_info.value) == expected_exception_message + + +@pytest.mark.parametrize('node,expected', [ + (ast_node('@decorator\nclass Class(BaseClass1, BaseClass2):\n\tdef __init__(self, value: object) -> None:\n\t\tself._value = value\n\n\t@property\n\tdef value(self) -> object:\n\t\treturn self._value'), PythonClassDefinition('Class', [ + 'BaseClass1', + 'BaseClass2' + ], None, ['decorator'], [ + PythonFunctionDefinition('__init__', [ + PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None), + PythonFunctionArgument('value', PythonArgumentKind.NORMAL, None, 'object') + ], 'None', None, []), + PythonFunctionDefinition('value', [ + PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None) + ], 'object', None, ['property']) + ])), + (ast_node('class Class: ...'), PythonClassDefinition('Class', [], None, [], [])), + (ast_node('class Class: \'\'\'docstring\'\'\''), PythonClassDefinition('Class', [], 'docstring', [], [])), + (ast_node('class Class:\n\tdef __init__(self): ...'), PythonClassDefinition('Class', [], None, [], [ + PythonFunctionDefinition('__init__', [ + PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None) + ], None, None, []) + ])), + (ast_node('class Class:\n\t\'\'\'doc\'\'\'\n\tdef __init__(self): ...'), PythonClassDefinition('Class', [], 'doc', [], [ + PythonFunctionDefinition('__init__', [ + PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None) + ], None, None, []) + ])), +]) +def test_PythonDefinition_from_node(node, expected): + assert PythonClassDefinition.from_node(node) == expected \ No newline at end of file diff --git a/tests/unit/core/python/definitions/test_PythonDefinition.py b/tests/unit/core/python/definitions/test_PythonDefinition.py new file mode 100644 index 0000000..d675ca0 --- /dev/null +++ b/tests/unit/core/python/definitions/test_PythonDefinition.py @@ -0,0 +1,84 @@ +# 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.definitions import PythonDefinition +from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError +from tests.utils.ast_node import ast_node +import pytest + +@pytest.mark.parametrize('definition,expected', [ + (PythonDefinition('a', None, []), 'PythonDefinition(\'a\', None, [])'), + (PythonDefinition('a', 'Hello, World!', []), 'PythonDefinition(\'a\', \'Hello, World!\', [])'), +]) +def test_PythonDefinition_string_representation(definition, expected): + assert repr(definition) == expected + + +@pytest.mark.parametrize('name,doc,decorators,expected_exception,expected_exception_message', [ + ('', None, [], InvalidPythonIdentifierError, 'invalid identifier'), + ('.', None, [], InvalidPythonIdentifierError, 'invalid identifier: .'), + ('#', None, [], InvalidPythonIdentifierError, 'invalid identifier: #'), + ('1a', None, [], InvalidPythonIdentifierError, 'invalid identifier: 1a'), + ('1_', None, [], InvalidPythonIdentifierError, 'invalid identifier: 1_'), + ('1,', None, [], InvalidPythonIdentifierError, 'invalid identifier: 1,'), + (',', None, [], InvalidPythonIdentifierError, 'invalid identifier: ,'), + (':', None, [], InvalidPythonIdentifierError, 'invalid identifier: :'), + ('-', None, [], InvalidPythonIdentifierError, 'invalid identifier: -'), + (';', None, [], InvalidPythonIdentifierError, 'invalid identifier: ;'), + ('|', None, [], InvalidPythonIdentifierError, 'invalid identifier: |'), + ('<', None, [], InvalidPythonIdentifierError, 'invalid identifier: <'), + ('>', None, [], InvalidPythonIdentifierError, 'invalid identifier: >'), + ('a,', None, [], InvalidPythonIdentifierError, 'invalid identifier: a,'), + (',a', None, [], InvalidPythonIdentifierError, 'invalid identifier: ,a'), + ('a:', None, [], InvalidPythonIdentifierError, 'invalid identifier: a:'), + (':a', None, [], InvalidPythonIdentifierError, 'invalid identifier: :a'), + ('a-', None, [], InvalidPythonIdentifierError, 'invalid identifier: a-'), + ('-a', None, [], InvalidPythonIdentifierError, 'invalid identifier: -a'), + ('a;', None, [], InvalidPythonIdentifierError, 'invalid identifier: a;'), + (';a', None, [], InvalidPythonIdentifierError, 'invalid identifier: ;a'), + ('a|', None, [], InvalidPythonIdentifierError, 'invalid identifier: a|'), + ('|a', None, [], InvalidPythonIdentifierError, 'invalid identifier: |a'), + ('a<', None, [], InvalidPythonIdentifierError, 'invalid identifier: a<'), + ('', None, [], InvalidPythonIdentifierError, 'invalid identifier: a>'), + ('>a', None, [], InvalidPythonIdentifierError, 'invalid identifier: >a'), + ('class', None, [], InvalidPythonIdentifierError, 'invalid identifier: class'), + ('def', None, [], InvalidPythonIdentifierError, 'invalid identifier: def'), + ('lambda', None, [], InvalidPythonIdentifierError, 'invalid identifier: lambda'), + ('None', None, [], InvalidPythonIdentifierError, 'invalid identifier: None'), + ('True', None, [], InvalidPythonIdentifierError, 'invalid identifier: True'), +]) +def test_PythonDefinition_exceptions(name, doc, decorators, expected_exception, expected_exception_message): + with pytest.raises(expected_exception) as exc_info: + PythonDefinition(name, doc, decorators) + assert str(exc_info.value) == expected_exception_message + + +@pytest.mark.parametrize('definition1,definition2,expected', [ + (PythonDefinition('a', None, []), PythonDefinition('a', None, []), True), + (PythonDefinition('a', 'doc', []), PythonDefinition('a', 'doc', []), True), + (PythonDefinition('a', 'doc', ['a']), PythonDefinition('a', 'doc', ['a']), True), + (PythonDefinition('a', 'doc', ['a', 'b']), PythonDefinition('a', 'doc', ['a', 'b']), True), + (PythonDefinition('a', None, []), PythonDefinition('b', None, []), False), + (PythonDefinition('a', 'doc', []), PythonDefinition('a', None, []), False), + (PythonDefinition('a', 'doc', []), PythonDefinition('a', 'docs', []), False), + (PythonDefinition('a', None, ['a']), PythonDefinition('a', None, ['b']), False), + (PythonDefinition('a', None, ['a']), PythonDefinition('a', None, []), False), + (PythonDefinition('a', None, ['a']), PythonDefinition('a', None, ['a', 'b']), False), + (PythonDefinition('a', None, ['b', 'a']), PythonDefinition('a', None, ['a', 'b']), False), + (PythonDefinition('a', 'doc', []), PythonDefinition('b', None, ['a', 'b']), False), +]) +def test_PythonDefinition___eq__(definition1, definition2, expected): + assert (definition1 == definition2) == expected + assert (definition2 == definition1) == expected \ No newline at end of file diff --git a/tests/unit/core/python/definitions/test_PythonFunctionDefinition.py b/tests/unit/core/python/definitions/test_PythonFunctionDefinition.py new file mode 100644 index 0000000..0fe61c0 --- /dev/null +++ b/tests/unit/core/python/definitions/test_PythonFunctionDefinition.py @@ -0,0 +1,90 @@ +# 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.definitions import PythonFunctionDefinition +from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind +from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError +from tests.utils.ast_node import ast_node +import pytest + +@pytest.mark.parametrize('definition,expected', [ + (PythonFunctionDefinition('a', [], None, None, []), 'PythonFunctionDefinition(\'a\', [], None, None, [])'), + (PythonFunctionDefinition('a', [], None, 'Hello, World!', []), 'PythonFunctionDefinition(\'a\', [], None, \'Hello, World!\', [])'), +]) +def test_PythonDefinition_string_representation(definition, expected): + assert repr(definition) == expected + + +@pytest.mark.parametrize('name,args,returns,doc,decorators,expected_exception,expected_exception_message', [ + ('', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier'), + ('.', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: .'), + ('#', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: #'), + ('1a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1a'), + ('1_', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1_'), + ('1,', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: 1,'), + (',', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ,'), + (':', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: :'), + ('-', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: -'), + (';', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ;'), + ('|', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: |'), + ('<', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: <'), + ('>', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: >'), + ('a,', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a,'), + (',a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ,a'), + ('a:', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a:'), + (':a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: :a'), + ('a-', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a-'), + ('-a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: -a'), + ('a;', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a;'), + (';a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: ;a'), + ('a|', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a|'), + ('|a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: |a'), + ('a<', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a<'), + ('', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: a>'), + ('>a', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: >a'), + ('class', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: class'), + ('def', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: def'), + ('lambda', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: lambda'), + ('None', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: None'), + ('True', [], None, None, [], InvalidPythonIdentifierError, 'invalid identifier: True'), +]) +def test_PythonFunctionDefinition_exceptions(name, args, returns, doc, decorators, expected_exception, expected_exception_message): + with pytest.raises(expected_exception) as exc_info: + PythonFunctionDefinition(name, args, returns, doc, decorators) + assert str(exc_info.value) == expected_exception_message + + +@pytest.mark.parametrize('node,expected', [ + (ast_node('def add(a: int, b: int) -> int: return a + b'), PythonFunctionDefinition('add', [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int'), + ], 'int', None, [])), + (ast_node('@decorator\ndef add(a: int, b: int) -> int: return a + b'), PythonFunctionDefinition('add', [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int'), + ], 'int', None, ['decorator'])), + (ast_node('@decorator1\n@decorator2\ndef add(a: int, b: int) -> int: return a + b'), PythonFunctionDefinition('add', [ + PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int'), + ], 'int', None, ['decorator1', 'decorator2'])), + (ast_node('def func(): ...'), PythonFunctionDefinition('func', [], None, None, [])), + (ast_node('@decorator\ndef func(): ...'), PythonFunctionDefinition('func', [], None, None, ['decorator'])), + # (ast_node('class Class(BaseClass1, BaseClass2):\n\tdef __init__(self, value: object) -> None:\n\t\tself._value = value\n\n\t@property\n\tdef value(self) -> object:\n\t\treturn self._value'), PythonFunctionDefinition('add', [ + # PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int'), + # PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int'), + # ], 'int', None, [])), +]) +def test_PythonDefinition_from_node(node, expected): + assert PythonFunctionDefinition.from_node(node) == expected \ No newline at end of file diff --git a/tests/utils/ast_node.py b/tests/utils/ast_node.py new file mode 100644 index 0000000..0e9bc7b --- /dev/null +++ b/tests/utils/ast_node.py @@ -0,0 +1,36 @@ +# 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 typing import Union +import ast + +__all__ = [ + 'ast_node' +] + +def ast_node(statement: str) -> Union[ast.stmt, None]: + ''' + Returns an AST node from the statement. + + :param statement: The statement. + :type statement: str + + :return: The node. + :rtype: Union[ast.stmt, None] + ''' + + body = ast.parse(statement).body + if not body: + return None + return body[0] \ No newline at end of file