Skip to content

Commit 94957a3

Browse files
authored
Added documentation for multi-line preconditions (#1305)
1 parent b63df6c commit 94957a3

4 files changed

Lines changed: 64 additions & 5 deletions

File tree

packages/python-ta/docs/checkers/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2697,7 +2697,7 @@ def f(x: int) -> int:
26972697

26982698
This error occurs when a function precondition contains invalid Python expression syntax. Valid Python statements
26992699
(e.g. assignment statements like x = 5) are also flagged, as they aren't valid Python expressions.
2700-
Does not currently support multi-line preconditions.
2700+
Multi-line preconditions are supported using a backslash ('\') following a space at the end of each line except the last.
27012701

27022702
Example:
27032703

packages/python-ta/docs/contracts/index.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,13 +124,16 @@ PythonTA uses the [typeguard] library to check types.
124124

125125
### Functions: custom preconditions
126126

127-
You can write arbitrary preconditions as Python expressions in the function docstring, using the following syntax:
127+
You can write arbitrary preconditions as Python expressions in the function docstring, using the following syntax.
128+
Multi-line preconditions can be created by using a backslash ('\') following a space at the end of each line except the last:
128129

129130
```
130131
Preconditions:
131132
- <expr>
132133
- <expr>
133-
- <expr>
134+
- <expr> \
135+
<expr> \
136+
<expr>
134137
```
135138

136139
Each expression is evaluated in the same scope as the function body, in top-down order.

packages/python-ta/src/python_ta/checkers/contract_checker.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"""
44

55
import ast
6+
import re
67

78
from astroid import nodes
89
from pylint.checkers import BaseChecker
@@ -33,10 +34,13 @@ def visit_functiondef(self, node: nodes.FunctionDef) -> None:
3334

3435
preconditions = parse_assertions(node, parse_token="Precondition")
3536
for condition in preconditions:
37+
cleaned_condition = re.sub(r"\s+", " ", condition)
3638
try:
37-
ast.parse(condition, mode="eval")
39+
ast.parse(cleaned_condition, mode="eval")
3840
except SyntaxError:
39-
self.add_message("invalid-precondition-syntax", node=node, args=(condition,))
41+
self.add_message(
42+
"invalid-precondition-syntax", node=node, args=(cleaned_condition,)
43+
)
4044

4145

4246
def register(linter: PyLinter) -> None:

packages/python-ta/tests/test_custom_checkers/test_contract_checker.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,3 +127,55 @@ def f(x: int) -> int:
127127
func_node, *_ = mod.nodes_of_class(nodes.FunctionDef)
128128
with self.assertNoMessages():
129129
self.checker.visit_functiondef(func_node)
130+
131+
def test_valid_multi_line_preconditions(self) -> None:
132+
src = """
133+
def f(data: list, threshold: int) -> int:
134+
'''Return list length of a positive integer array
135+
136+
Preconditions:
137+
- len(data) > 0 and \
138+
all(isinstance(x, int) for x in data) and \
139+
threshold >= 0
140+
- data != []
141+
'''
142+
return len(data)
143+
"""
144+
145+
mod = parse(src)
146+
func_node, *_ = mod.nodes_of_class(nodes.FunctionDef)
147+
with self.assertNoMessages():
148+
self.checker.visit_functiondef(func_node)
149+
150+
def test_invalid_multi_line_preconditions(self) -> None:
151+
src = """
152+
def f(data: list, threshold: int) -> int:
153+
'''Return list length of a positive integer array
154+
155+
Preconditions:
156+
- len(data) > 0 and \
157+
all(isinstance(x, int) for x in data) and \
158+
threshold > = 0
159+
- data !== []
160+
'''
161+
return len(data)
162+
"""
163+
164+
mod = parse(src)
165+
func_node, *_ = mod.nodes_of_class(nodes.FunctionDef)
166+
with self.assertAddsMessages(
167+
pylint.testutils.MessageTest(
168+
msg_id="invalid-precondition-syntax",
169+
node=func_node,
170+
args=(
171+
"len(data) > 0 and all(isinstance(x, int) for x in data) and threshold > = 0",
172+
),
173+
),
174+
pylint.testutils.MessageTest(
175+
msg_id="invalid-precondition-syntax",
176+
node=func_node,
177+
args=("data !== []",),
178+
),
179+
ignore_position=True,
180+
):
181+
self.checker.visit_functiondef(func_node)

0 commit comments

Comments
 (0)