Add class for Python docstrings

This commit is contained in:
2026-04-13 14:15:49 +02:00
parent 7f02b5b7ec
commit 16852f3d68
7 changed files with 86 additions and 31 deletions
+1
View File
@@ -20,3 +20,4 @@
- Add classes for Python definitions - Add classes for Python definitions
- Add class for Python modules - Add class for Python modules
- Add feature to get the signature representation of a Python function argument - Add feature to get the signature representation of a Python function argument
- Add class for Python docstrings
+29 -10
View File
@@ -16,8 +16,11 @@ from __future__ import annotations
from typing import Union from typing import Union
import ast import ast
from .arguments import PythonFunctionArgument, PythonASTArgumentsListParser from .arguments import PythonFunctionArgument, PythonASTArgumentsListParser
from .docstrings import PythonDocstring
import keyword import keyword
from ...exceptions import InvalidPythonIdentifierError from ...exceptions import InvalidPythonIdentifierError
from ...utils import assert_that_is_instance
import types
__all__ = [ __all__ = [
'PythonDefinition', 'PythonDefinition',
@@ -26,6 +29,20 @@ __all__ = [
'PythonClassDefinition' 'PythonClassDefinition'
] ]
def _python_docstring(doc: Union[str, None]) -> Union[PythonDocstring, None]:
'''
Returns the PythonDocstring object if the docstring is a string and
not ``None``.
:param doc: The docstring.
:type doc: Union[str, None]
:return: The PythonDocstring object or ``None``.
:rtype: Union[PythonDocstring, None]
'''
return PythonDocstring(doc) if doc is not None else None
class PythonDefinition: class PythonDefinition:
''' '''
A base class for Python definition (a class, function or asynchronous A base class for Python definition (a class, function or asynchronous
@@ -34,7 +51,7 @@ class PythonDefinition:
:param name: The name of the definition. :param name: The name of the definition.
:type name: str :type name: str
:param doc: The docstring. :param doc: The docstring.
:type doc: Union[str, None] :type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators. :param decorators: The definition decorators.
:type decorators: list[str] :type decorators: list[str]
@@ -42,10 +59,12 @@ class PythonDefinition:
Python identifier. Python identifier.
''' '''
def __init__(self, name: str, doc: Union[str, None], decorators: list[str]) -> None: def __init__(self, name: str, doc: Union[PythonDocstring, None], decorators: list[str]) -> None:
if keyword.iskeyword(name) or not name.isidentifier() or not name: if keyword.iskeyword(name) or not name.isidentifier() or not name:
raise InvalidPythonIdentifierError('invalid identifier', identifier = name) raise InvalidPythonIdentifierError('invalid identifier', identifier = name)
assert_that_is_instance(doc, (PythonDocstring, types.NoneType))
self.name = name self.name = name
self.doc = doc self.doc = doc
self.decorators = decorators self.decorators = decorators
@@ -71,7 +90,7 @@ class PythonDefinition:
return cls( return cls(
node.name, node.name,
ast.get_docstring(node), _python_docstring(ast.get_docstring(node)),
[ast.unparse(d) for d in node.decorator_list] [ast.unparse(d) for d in node.decorator_list]
) )
@@ -92,7 +111,7 @@ class PythonFunctionDefinition(PythonDefinition):
:param returns: The return type of the function or class. :param returns: The return type of the function or class.
:type returns: Union[str, None] :type returns: Union[str, None]
:param doc: The docstring. :param doc: The docstring.
:type doc: Union[str, None] :type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators. :param decorators: The definition decorators.
:type decorators: list[str] :type decorators: list[str]
@@ -100,7 +119,7 @@ class PythonFunctionDefinition(PythonDefinition):
Python identifier. Python identifier.
''' '''
def __init__(self, name: str, args: list[PythonFunctionArgument], returns: Union[str, None], doc: Union[str, None], decorators: list[str]) -> None: def __init__(self, name: str, args: list[PythonFunctionArgument], returns: Union[str, None], doc: Union[PythonDocstring, None], decorators: list[str]) -> None:
super().__init__(name, doc, decorators) super().__init__(name, doc, decorators)
self.args = args self.args = args
self.returns = returns self.returns = returns
@@ -127,7 +146,7 @@ class PythonFunctionDefinition(PythonDefinition):
node.name, node.name,
PythonASTArgumentsListParser(node.args).to_argument_list(), PythonASTArgumentsListParser(node.args).to_argument_list(),
ast.unparse(node.returns) if node.returns is not None else None, ast.unparse(node.returns) if node.returns is not None else None,
ast.get_docstring(node), _python_docstring(ast.get_docstring(node)),
[ast.unparse(d) for d in node.decorator_list] [ast.unparse(d) for d in node.decorator_list]
) )
@@ -143,7 +162,7 @@ class PythonAsyncFunctionDefinition(PythonFunctionDefinition):
:param returns: The return type of the function or class. :param returns: The return type of the function or class.
:type returns: Union[str, None] :type returns: Union[str, None]
:param doc: The docstring. :param doc: The docstring.
:type doc: Union[str, None] :type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators. :param decorators: The definition decorators.
:type decorators: list[str] :type decorators: list[str]
@@ -160,7 +179,7 @@ class PythonClassDefinition(PythonDefinition):
:param bases: The base classes. :param bases: The base classes.
:type bases: list[str] :type bases: list[str]
:param doc: The docstring. :param doc: The docstring.
:type doc: Union[str, None] :type doc: Union[PythonDocstring, None]
:param decorators: The definition decorators. :param decorators: The definition decorators.
:type decorators: list[str] :type decorators: list[str]
:param body: :param body:
@@ -170,7 +189,7 @@ class PythonClassDefinition(PythonDefinition):
Python identifier. Python identifier.
''' '''
def __init__(self, name: str, bases: list[str], doc: Union[str, None], decorators: list[str], body: list[PythonDefinition]) -> None: def __init__(self, name: str, bases: list[str], doc: Union[PythonDocstring, None], decorators: list[str], body: list[PythonDefinition]) -> None:
super().__init__(name, doc, decorators) super().__init__(name, doc, decorators)
self.bases = bases self.bases = bases
self.body = body self.body = body
@@ -196,7 +215,7 @@ class PythonClassDefinition(PythonDefinition):
return cls( return cls(
node.name, node.name,
[ast.unparse(b) for b in node.bases], [ast.unparse(b) for b in node.bases],
ast.get_docstring(node), _python_docstring(ast.get_docstring(node)),
[ast.unparse(d) for d in node.decorator_list], [ast.unparse(d) for d in node.decorator_list],
[ [
PythonFunctionDefinition.from_node(expr) PythonFunctionDefinition.from_node(expr)
@@ -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.
__all__ = [
'PythonDocstring'
]
class PythonDocstring:
'''
Represents a Python docstring.
:param docstring: The docstring as a string.
:type docstring: str
'''
def __init__(self, docstring: str) -> None:
self.docstring = docstring
def __repr__(self) -> str:
return type(self).__name__ + '(' + repr(self.docstring) + ')'
def __eq__(self, value) -> bool:
if not isinstance(value, type(self)):
return False
return self.docstring == value.docstring
@@ -15,6 +15,7 @@
from src.jcloud_docsgen.core.python._core import _collect_definitions from src.jcloud_docsgen.core.python._core import _collect_definitions
from src.jcloud_docsgen.core.python.definitions import PythonClassDefinition, PythonFunctionDefinition from src.jcloud_docsgen.core.python.definitions import PythonClassDefinition, PythonFunctionDefinition
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
import pytest import pytest
import ast import ast
@@ -65,18 +66,18 @@ class Something:
PythonFunctionDefinition('add', [ PythonFunctionDefinition('add', [
PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int | float'), PythonFunctionArgument('a', PythonArgumentKind.NORMAL, None, 'int | float'),
PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int | float'), PythonFunctionArgument('b', PythonArgumentKind.NORMAL, None, 'int | float'),
], 'int | float', 'Adds two numbers.', []), ], 'int | float', PythonDocstring('Adds two numbers.'), []),
PythonClassDefinition('Something', [], 'A class.', [], [ PythonClassDefinition('Something', [], PythonDocstring('A class.'), [], [
PythonFunctionDefinition('__init__', [ PythonFunctionDefinition('__init__', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None), PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
PythonFunctionArgument('value', PythonArgumentKind.NORMAL, '42', 'typing.Any'), PythonFunctionArgument('value', PythonArgumentKind.NORMAL, '42', 'typing.Any'),
], 'None', None, []), ], 'None', None, []),
PythonFunctionDefinition('value', [ PythonFunctionDefinition('value', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None), PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
], 'typing.Any', 'The value.', ['property']), ], 'typing.Any', PythonDocstring('The value.'), ['property']),
PythonFunctionDefinition('get_value', [ PythonFunctionDefinition('get_value', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None), PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None),
], 'typing.Any', 'Returns the value.', []), ], 'typing.Any', PythonDocstring('Returns the value.'), []),
]) ])
]), ]),
(module('''from src.jcloud_docsgen.core.python import PythonDocumentationGenerator (module('''from src.jcloud_docsgen.core.python import PythonDocumentationGenerator
@@ -14,13 +14,14 @@
from src.jcloud_docsgen.core.python.definitions import PythonFunctionDefinition, PythonClassDefinition from src.jcloud_docsgen.core.python.definitions import PythonFunctionDefinition, PythonClassDefinition
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
from tests.utils.ast_node import ast_node from tests.utils.ast_node import ast_node
import pytest import pytest
@pytest.mark.parametrize('definition,expected', [ @pytest.mark.parametrize('definition,expected', [
(PythonClassDefinition('A', [], None, None, []), 'PythonClassDefinition(\'A\', [], None, None, [])'), (PythonClassDefinition('A', [], None, None, []), 'PythonClassDefinition(\'A\', [], None, None, [])'),
(PythonClassDefinition('A', [], None, 'Hello, World!', []), 'PythonClassDefinition(\'A\', [], None, \'Hello, World!\', [])'), (PythonClassDefinition('A', [], None, PythonDocstring('Hello, World!'), []), 'PythonClassDefinition(\'A\', [], None, PythonDocstring(\'Hello, World!\'), [])'),
]) ])
def test_PythonDefinition_string_representation(definition, expected): def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected assert repr(definition) == expected
@@ -80,13 +81,13 @@ def test_PythonFunctionDefinition_exceptions(name, args, returns, doc, decorator
], 'object', None, ['property']) ], 'object', None, ['property'])
])), ])),
(ast_node('class Class: ...'), PythonClassDefinition('Class', [], None, [], [])), (ast_node('class Class: ...'), PythonClassDefinition('Class', [], None, [], [])),
(ast_node('class Class: \'\'\'docstring\'\'\''), PythonClassDefinition('Class', [], 'docstring', [], [])), (ast_node('class Class: \'\'\'docstring\'\'\''), PythonClassDefinition('Class', [], PythonDocstring('docstring'), [], [])),
(ast_node('class Class:\n\tdef __init__(self): ...'), PythonClassDefinition('Class', [], None, [], [ (ast_node('class Class:\n\tdef __init__(self): ...'), PythonClassDefinition('Class', [], None, [], [
PythonFunctionDefinition('__init__', [ PythonFunctionDefinition('__init__', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None) PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None)
], None, None, []) ], None, None, [])
])), ])),
(ast_node('class Class:\n\t\'\'\'doc\'\'\'\n\tdef __init__(self): ...'), PythonClassDefinition('Class', [], 'doc', [], [ (ast_node('class Class:\n\t\'\'\'doc\'\'\'\n\tdef __init__(self): ...'), PythonClassDefinition('Class', [], PythonDocstring('doc'), [], [
PythonFunctionDefinition('__init__', [ PythonFunctionDefinition('__init__', [
PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None) PythonFunctionArgument('self', PythonArgumentKind.NORMAL, None, None)
], None, None, []) ], None, None, [])
@@ -13,13 +13,13 @@
# limitations under the License. # limitations under the License.
from src.jcloud_docsgen.core.python.definitions import PythonDefinition from src.jcloud_docsgen.core.python.definitions import PythonDefinition
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
from tests.utils.ast_node import ast_node
import pytest import pytest
@pytest.mark.parametrize('definition,expected', [ @pytest.mark.parametrize('definition,expected', [
(PythonDefinition('a', None, []), 'PythonDefinition(\'a\', None, [])'), (PythonDefinition('a', None, []), 'PythonDefinition(\'a\', None, [])'),
(PythonDefinition('a', 'Hello, World!', []), 'PythonDefinition(\'a\', \'Hello, World!\', [])'), (PythonDefinition('a', PythonDocstring('Hello, World!'), []), 'PythonDefinition(\'a\', PythonDocstring(\'Hello, World!\'), [])'),
]) ])
def test_PythonDefinition_string_representation(definition, expected): def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected assert repr(definition) == expected
@@ -67,17 +67,17 @@ def test_PythonDefinition_exceptions(name, doc, decorators, expected_exception,
@pytest.mark.parametrize('definition1,definition2,expected', [ @pytest.mark.parametrize('definition1,definition2,expected', [
(PythonDefinition('a', None, []), PythonDefinition('a', None, []), True), (PythonDefinition('a', None, []), PythonDefinition('a', None, []), True),
(PythonDefinition('a', 'doc', []), PythonDefinition('a', 'doc', []), True), (PythonDefinition('a', PythonDocstring('doc'), []), PythonDefinition('a', PythonDocstring('doc'), []), True),
(PythonDefinition('a', 'doc', ['a']), PythonDefinition('a', 'doc', ['a']), True), (PythonDefinition('a', PythonDocstring('doc'), ['a']), PythonDefinition('a', PythonDocstring('doc'), ['a']), True),
(PythonDefinition('a', 'doc', ['a', 'b']), PythonDefinition('a', 'doc', ['a', 'b']), True), (PythonDefinition('a', PythonDocstring('doc'), ['a', 'b']), PythonDefinition('a', PythonDocstring('doc'), ['a', 'b']), True),
(PythonDefinition('a', None, []), PythonDefinition('b', None, []), False), (PythonDefinition('a', None, []), PythonDefinition('b', None, []), False),
(PythonDefinition('a', 'doc', []), PythonDefinition('a', None, []), False), (PythonDefinition('a', PythonDocstring('doc'), []), PythonDefinition('a', None, []), False),
(PythonDefinition('a', 'doc', []), PythonDefinition('a', 'docs', []), False), (PythonDefinition('a', PythonDocstring('doc'), []), PythonDefinition('a', PythonDocstring('docs'), []), False),
(PythonDefinition('a', None, ['a']), PythonDefinition('a', None, ['b']), 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, []), False),
(PythonDefinition('a', None, ['a']), PythonDefinition('a', None, ['a', 'b']), False), (PythonDefinition('a', None, ['a']), PythonDefinition('a', None, ['a', 'b']), False),
(PythonDefinition('a', None, ['b', '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), (PythonDefinition('a', PythonDocstring('doc'), []), PythonDefinition('b', None, ['a', 'b']), False),
]) ])
def test_PythonDefinition___eq__(definition1, definition2, expected): def test_PythonDefinition___eq__(definition1, definition2, expected):
assert (definition1 == definition2) == expected assert (definition1 == definition2) == expected
@@ -14,13 +14,14 @@
from src.jcloud_docsgen.core.python.definitions import PythonFunctionDefinition from src.jcloud_docsgen.core.python.definitions import PythonFunctionDefinition
from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind from src.jcloud_docsgen.core.python.arguments import PythonFunctionArgument, PythonArgumentKind
from src.jcloud_docsgen.core.python.docstrings import PythonDocstring
from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError from src.jcloud_docsgen.exceptions import InvalidPythonIdentifierError
from tests.utils.ast_node import ast_node from tests.utils.ast_node import ast_node
import pytest import pytest
@pytest.mark.parametrize('definition,expected', [ @pytest.mark.parametrize('definition,expected', [
(PythonFunctionDefinition('a', [], None, None, []), 'PythonFunctionDefinition(\'a\', [], None, None, [])'), (PythonFunctionDefinition('a', [], None, None, []), 'PythonFunctionDefinition(\'a\', [], None, None, [])'),
(PythonFunctionDefinition('a', [], None, 'Hello, World!', []), 'PythonFunctionDefinition(\'a\', [], None, \'Hello, World!\', [])'), (PythonFunctionDefinition('a', [], None, PythonDocstring('Hello, World!'), []), 'PythonFunctionDefinition(\'a\', [], None, PythonDocstring(\'Hello, World!\'), [])'),
]) ])
def test_PythonDefinition_string_representation(definition, expected): def test_PythonDefinition_string_representation(definition, expected):
assert repr(definition) == expected assert repr(definition) == expected
@@ -81,10 +82,6 @@ def test_PythonFunctionDefinition_exceptions(name, args, returns, doc, decorator
], 'int', None, ['decorator1', 'decorator2'])), ], 'int', None, ['decorator1', 'decorator2'])),
(ast_node('def func(): ...'), PythonFunctionDefinition('func', [], None, None, [])), (ast_node('def func(): ...'), PythonFunctionDefinition('func', [], None, None, [])),
(ast_node('@decorator\ndef func(): ...'), PythonFunctionDefinition('func', [], None, None, ['decorator'])), (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): def test_PythonDefinition_from_node(node, expected):
assert PythonFunctionDefinition.from_node(node) == expected assert PythonFunctionDefinition.from_node(node) == expected