diff --git a/strictdoc/backend/sdoc_source_code/reader_python.py b/strictdoc/backend/sdoc_source_code/reader_python.py index 975eae99a..213caa795 100644 --- a/strictdoc/backend/sdoc_source_code/reader_python.py +++ b/strictdoc/backend/sdoc_source_code/reader_python.py @@ -1,7 +1,7 @@ # mypy: disable-error-code="no-redef,no-untyped-call,no-untyped-def,type-arg,var-annotated" import sys import traceback -from typing import List, Optional, Union +from typing import List, Optional, Sequence, Union import tree_sitter_python from tree_sitter import Language, Node, Parser @@ -119,25 +119,9 @@ def read(self, input_buffer: bytes, file_path=None): assert function_name is not None, "Function name" - parent_class_name: Optional[str] = None - if (function_parent_node := node_.parent) is not None and ( - class_node_or_node := function_parent_node.parent - ) is not None: - if ( - class_node_or_node.type == "class_definition" - and len(class_node_or_node.children) > 1 - ): - second_node_or_none = class_node_or_node.children[1] - if ( - second_node_or_none.type == "identifier" - and second_node_or_none.text is not None - ): - parent_class_name = second_node_or_none.text.decode( - "utf8" - ) - - if parent_class_name is not None: - function_name = parent_class_name + "." + function_name + parent_names = self.get_node_ns(node_) + if parent_names: + function_name = f"{'.'.join(parent_names)}.{function_name}" block_comment = None if ( @@ -271,3 +255,31 @@ def read_from_file(self, file_path): # TODO: when --debug is provided # traceback.print_exc() # noqa: ERA001 sys.exit(1) + + @staticmethod + def get_node_ns(node: Node) -> Sequence[str]: + """Walk up the tree and find parent classes""" + parent_scopes = [] + cursor: Optional[Node] = node + while cursor: + if (block_node := cursor.parent) is not None and ( + class_node_or_node := block_node.parent + ) is not None: + cursor = class_node_or_node + if ( + class_node_or_node.type == "class_definition" + and len(class_node_or_node.children) > 1 + ): + second_node_or_none = class_node_or_node.children[1] + if ( + second_node_or_none.type == "identifier" + and second_node_or_none.text is not None + ): + parent_class_name = second_node_or_none.text.decode( + "utf8" + ) + parent_scopes.append(parent_class_name) + else: + cursor = None + parent_scopes.reverse() + return parent_scopes diff --git a/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/file.py b/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/file.py index c986414ee..919caf789 100644 --- a/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/file.py +++ b/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/file.py @@ -1,4 +1,9 @@ class Foo: + class Bar: + class Baz: + def hello_world(self): + print("hello world") # noqa: T201 + def hello_world(self): """ @relation(REQ-1, scope=function) diff --git a/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/input.sdoc b/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/input.sdoc index 020643899..b40d3c665 100644 --- a/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/input.sdoc +++ b/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/input.sdoc @@ -17,4 +17,4 @@ STATEMENT: Requirement Statement RELATIONS: - TYPE: File VALUE: file.py - FUNCTION: Foo.hello_world + FUNCTION: Foo.Bar.Baz.hello_world diff --git a/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/test.itest b/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/test.itest index 2094a5259..31ebba440 100644 --- a/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/test.itest +++ b/tests/integration/features/file_traceability/_language_parsers/python/04_python_forward_function_in_class/test.itest @@ -6,17 +6,19 @@ CHECK: Published: Hello world doc RUN: %check_exists --file "%S/Output/html/_source_files/file.py.html" RUN: %cat %S/Output/html/%THIS_TEST_FOLDER/input.html | filecheck %s --dump-input=fail --check-prefix CHECK-HTML -CHECK-HTML: -CHECK-HTML: -CHECK-HTML: -CHECK-HTML: +CHECK-HTML: +CHECK-HTML: +CHECK-HTML: +CHECK-HTML: +CHECK-HTML: RUN: %cat %S/Output/html/_source_files/file.py.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-FILE -CHECK-SOURCE-FILE: def hello_world -CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-1#2#11" -CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-2#2#11" CHECK-SOURCE-FILE:
-CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-1#2#11" +CHECK-SOURCE-FILE: def hello_world +CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-2#4#5" +CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-1#7#16" +CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-1#11#16" +CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-1#13#13" RUN: %cat %S/Output/html/source_coverage.html | filecheck %s --dump-input=fail --check-prefix CHECK-SOURCE-COVERAGE -CHECK-SOURCE-COVERAGE: 71.4% +CHECK-SOURCE-COVERAGE: 63.2%