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