Skip to content

python_reader: correctly handle decorators and nested functions #2277

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 26 additions & 28 deletions strictdoc/backend/sdoc_source_code/reader_python.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ def read(
functions_stack.append(function)
map_function_to_node[function] = node_
if len(node_.children) > 0:
# look for the docstring within the first 30 children (arbitrary chosen limit)
# so that we dont miss it if the file starts with comments (#!, encoding marker, etc...)
# Look for the docstring within the first 30 children (arbitrary chosen limit)
# so that we dont miss it if the file starts with comments (#!, encoding marker, etc...).
first_match = next(
(
child
Expand All @@ -92,7 +92,7 @@ def read(
if first_match is not None:
block_comment = first_match.children[0]

# string contains of three parts:
# String contains of three parts:
# string_start string_content string_end
string_content = block_comment.children[1]
assert string_content.text is not None
Expand Down Expand Up @@ -152,7 +152,7 @@ def read(
block_comment = function_block.children[0].children[
0
]
# string contains of three parts:
# String contains of three parts:
# string_start string_content string_end
string_content = block_comment.children[1]
assert string_content.text is not None
Expand Down Expand Up @@ -277,29 +277,27 @@ def read_from_file(self, file_path: str) -> SourceFileTraceabilityInfo:
@staticmethod
def get_node_ns(node: Node) -> Sequence[str]:
"""
Walk up the tree and find parent classes.
Walk up the tree to collect enclosing function and class names (identifier) for full qualification.
Handles nested functions, methods, and classes.
"""
parent_scopes = []
parent_scopes: List[str] = []
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

while cursor is not None:
if cursor.type in ("class_definition", "function_definition"):
# Look for the identifier child (i.e., the name).
name_node = next(
(
child
for child in cursor.children
if child.type == "identifier"
),
None,
)
if name_node and name_node.text:
parent_scopes.insert(0, name_node.text.decode("utf-8"))
cursor = cursor.parent

# The array now contains the "fully qualified" node name,
# we want to return the namespace, so don't return the last part.
return parent_scopes[:-1]
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#
# Some No-Op Decorators.
#
def decorator_1(func):
return func

def decorator_2(func):
return func

def decorator_3(func):
return func

def decorator_4(func):
return func

class Foo:

def hello_world(self):
"""
@relation(REQ-1, scope=function)
"""
print("hello world") # noqa: T201
print("hello world") # noqa: T201
print("hello world") # noqa: T201

@decorator_1
def hello_world_decorated_once(self):
"""
@relation(REQ-2, scope=function)
"""
print("hello world") # noqa: T201
print("hello world") # noqa: T201
print("hello world") # noqa: T201

@decorator_1
@decorator_2
def hello_world_decorated_twice(self):
"""
@relation(REQ-3, scope=function)
"""
print("hello world") # noqa: T201
print("hello world") # noqa: T201
print("hello world") # noqa: T201

@decorator_1
@decorator_2
@decorator_3
def hello_world_decorated_three_times(self):
"""
@relation(REQ-4, scope=function)
"""
print("hello world") # noqa: T201
print("hello world") # noqa: T201
print("hello world") # noqa: T201

@decorator_1
@decorator_2
@decorator_3
@decorator_4
def hello_world_decorated_four_times(self):
"""
@relation(REQ-5, scope=function)
"""
print("hello world") # noqa: T201
print("hello world") # noqa: T201
print("hello world") # noqa: T201

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[DOCUMENT]
TITLE: Hello world doc

[REQUIREMENT]
UID: REQ-1
TITLE: Requirement Title
STATEMENT: Requirement Statement

[REQUIREMENT]
UID: REQ-2
TITLE: Requirement Title
STATEMENT: Requirement Statement

[REQUIREMENT]
UID: REQ-3
TITLE: Requirement Title
STATEMENT: Requirement Statement

[REQUIREMENT]
UID: REQ-4
TITLE: Requirement Title
STATEMENT: Requirement Statement

[REQUIREMENT]
UID: REQ-5
TITLE: Requirement Title
STATEMENT: Requirement Statement
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]

features = [
"REQUIREMENT_TO_SOURCE_TRACEABILITY",
"SOURCE_FILE_LANGUAGE_PARSERS",
"PROJECT_STATISTICS_SCREEN"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
REQUIRES: PYTHON_39_OR_HIGHER

RUN: %strictdoc export %S --output-dir Output | filecheck %s --dump-input=fail
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 --check-prefix CHECK-HTML
CHECK-HTML: <a{{.*}}href="../_source_files/file.py.html#REQ-1#18#24">

RUN: %cat %S/Output/html/_source_files/file.py.html | filecheck %s --check-prefix CHECK-SOURCE-FILE
CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-1#18#24"
CHECK-SOURCE-FILE: <b>[ 18-24 ]</b>
CHECK-SOURCE-FILE: file.py, function Foo.hello_world

CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-2#27#33"
CHECK-SOURCE-FILE: <b>[ 27-33 ]</b>
CHECK-SOURCE-FILE: file.py, function Foo.hello_world_decorated_once

CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-3#37#43"
CHECK-SOURCE-FILE: <b>[ 37-43 ]</b>
CHECK-SOURCE-FILE: file.py, function Foo.hello_world_decorated_twice

CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-4#48#54"
CHECK-SOURCE-FILE: <b>[ 48-54 ]</b>
CHECK-SOURCE-FILE: file.py, function Foo.hello_world_decorated_three_times

CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-5#60#66"
CHECK-SOURCE-FILE: <b>[ 60-66 ]</b>
CHECK-SOURCE-FILE: file.py, function Foo.hello_world_decorated_four_times

RUN: %cat %S/Output/html/source_coverage.html | filecheck %s --check-prefix CHECK-SOURCE-COVERAGE
CHECK-SOURCE-COVERAGE: 50.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

class Foo:

def hello_world(self):
"""
@relation(REQ-1, scope=function)
"""
print("hello world") # noqa: T201
print("hello world") # noqa: T201
print("hello world") # noqa: T201

def nested_function():
"""
@relation(REQ-2, scope=function)
"""
print("hello world") # noqa: T201

nested_function()

def hello_world_2():
"""
@relation(REQ-3, scope=function)
"""
print("hello world") # noqa: T201
print("hello world") # noqa: T201
print("hello world") # noqa: T201

def nested_function():
"""
@relation(REQ-4, scope=function)
"""
print("hello world") # noqa: T201

nested_function()

class Outer:
class Inner:
def hello_world(self):
"""
@relation(REQ-5, scope=function)
"""
print("hello world") # noqa: T201
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[DOCUMENT]
TITLE: Hello world doc

[REQUIREMENT]
UID: REQ-1
TITLE: Requirement Title
STATEMENT: Requirement Statement

[REQUIREMENT]
UID: REQ-2
TITLE: Requirement Title
STATEMENT: Requirement Statement

[REQUIREMENT]
UID: REQ-3
TITLE: Requirement Title
STATEMENT: Requirement Statement

[REQUIREMENT]
UID: REQ-4
TITLE: Requirement Title
STATEMENT: Requirement Statement

[REQUIREMENT]
UID: REQ-5
TITLE: Requirement Title
STATEMENT: Requirement Statement
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]

features = [
"REQUIREMENT_TO_SOURCE_TRACEABILITY",
"SOURCE_FILE_LANGUAGE_PARSERS",
"PROJECT_STATISTICS_SCREEN"
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
REQUIRES: PYTHON_39_OR_HIGHER

RUN: %strictdoc export %S --output-dir Output | filecheck %s --dump-input=fail
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 --check-prefix CHECK-HTML
CHECK-HTML: <a{{.*}}href="../_source_files/file.py.html#REQ-1#4#18">

RUN: %cat %S/Output/html/_source_files/file.py.html | filecheck %s --check-prefix CHECK-SOURCE-FILE
CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-1#4#18"
CHECK-SOURCE-FILE: <b>[ 4-18 ]</b>
CHECK-SOURCE-FILE: file.py, function Foo.hello_world

CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-2#12#16"
CHECK-SOURCE-FILE: <b>[ 12-16 ]</b>
CHECK-SOURCE-FILE: file.py, function Foo.hello_world.nested_function

CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-3#20#34"
CHECK-SOURCE-FILE: <b>[ 20-34 ]</b>
CHECK-SOURCE-FILE: file.py, function hello_world_2

CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-4#28#32"
CHECK-SOURCE-FILE: <b>[ 28-32 ]</b>
CHECK-SOURCE-FILE: file.py, function hello_world_2.nested_function

CHECK-SOURCE-FILE: href="../_source_files/file.py.html#REQ-5#38#42"
CHECK-SOURCE-FILE: <b>[ 38-42 ]</b>
CHECK-SOURCE-FILE: file.py, function Outer.Inner.hello_world

RUN: %cat %S/Output/html/source_coverage.html | filecheck %s --check-prefix CHECK-SOURCE-COVERAGE
CHECK-SOURCE-COVERAGE: 62.5
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,12 @@ def hello_3_1_1():

function_1_1 = function_1.child_functions[0]
assert isinstance(function_1_1, Function)
assert function_1_1.name == "hello_1_1"
assert function_1_1.name == "hello_1.hello_1_1"
assert len(function_1_1.child_functions) == 1

function_1_1_1 = function_1_1.child_functions[0]
assert isinstance(function_1_1_1, Function)
assert function_1_1_1.name == "hello_1_1_1"
assert function_1_1_1.name == "hello_1.hello_1_1.hello_1_1_1"
assert len(function_1_1_1.child_functions) == 0

function_2 = info.functions[3]
Expand All @@ -105,12 +105,12 @@ def hello_3_1_1():

function_2_1 = function_2.child_functions[0]
assert isinstance(function_2_1, Function)
assert function_2_1.name == "hello_2_1"
assert function_2_1.name == "hello_2.hello_2_1"
assert len(function_2_1.child_functions) == 1

function_2_1_1 = function_2_1.child_functions[0]
assert isinstance(function_2_1_1, Function)
assert function_2_1_1.name == "hello_2_1_1"
assert function_2_1_1.name == "hello_2.hello_2_1.hello_2_1_1"
assert len(function_2_1_1.child_functions) == 0

function_3 = info.functions[6]
Expand All @@ -120,12 +120,12 @@ def hello_3_1_1():

function_3_1 = function_3.child_functions[0]
assert isinstance(function_3_1, Function)
assert function_3_1.name == "hello_3_1"
assert function_3_1.name == "hello_3.hello_3_1"
assert len(function_3_1.child_functions) == 1

function_3_1_1 = function_3_1.child_functions[0]
assert isinstance(function_3_1_1, Function)
assert function_3_1_1.name == "hello_3_1_1"
assert function_3_1_1.name == "hello_3.hello_3_1.hello_3_1_1"
assert len(function_3_1_1.child_functions) == 0


Expand Down
Loading