slough 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- slough-0.1.0/PKG-INFO +31 -0
- slough-0.1.0/README.md +21 -0
- slough-0.1.0/pyproject.toml +31 -0
- slough-0.1.0/src/slough/__init__.py +2 -0
- slough-0.1.0/src/slough/__main__.py +4 -0
- slough-0.1.0/src/slough/cli.py +112 -0
- slough-0.1.0/src/slough/formatter.py +64 -0
- slough-0.1.0/src/slough/models.py +24 -0
- slough-0.1.0/src/slough/parser.py +119 -0
- slough-0.1.0/src/slough/runner.py +93 -0
- slough-0.1.0/src/slough/tracer.py +41 -0
slough-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: slough
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Experimental toy library to dry-run Python code with line-by-line variable tracing
|
|
5
|
+
Author: prakash S
|
|
6
|
+
Author-email: prakash S <prakashsellathurai@gmail.com>
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Project-URL: Homepage, https://github.com/prakashsellathurai/slough
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
# slough
|
|
12
|
+
|
|
13
|
+
Experimental toy library to dry-run Python code traces every variable value line-by-line through execution.
|
|
14
|
+
|
|
15
|
+
## Usage
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
slough path/to/solution.py -t test_cases.json
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## How it works
|
|
22
|
+
|
|
23
|
+
Hooks `sys.settrace` before calling your solution function, recording locals at each line. Outputs source code with live variable annotations and pass/fail for each test case.
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
uv tool install slough
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Status:** Experimental. Not intended for production use.
|
slough-0.1.0/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# slough
|
|
2
|
+
|
|
3
|
+
Experimental toy library to dry-run Python code traces every variable value line-by-line through execution.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
slough path/to/solution.py -t test_cases.json
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## How it works
|
|
12
|
+
|
|
13
|
+
Hooks `sys.settrace` before calling your solution function, recording locals at each line. Outputs source code with live variable annotations and pass/fail for each test case.
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
uv tool install slough
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**Status:** Experimental. Not intended for production use.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "slough"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Experimental toy library to dry-run Python code with line-by-line variable tracing"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [
|
|
7
|
+
{ name = "prakash S", email = "prakashsellathurai@gmail.com" }
|
|
8
|
+
]
|
|
9
|
+
requires-python = ">=3.12"
|
|
10
|
+
dependencies = []
|
|
11
|
+
|
|
12
|
+
[project.scripts]
|
|
13
|
+
slough = "slough.cli:main"
|
|
14
|
+
|
|
15
|
+
[project.urls]
|
|
16
|
+
Homepage = "https://github.com/prakashsellathurai/slough"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.9.26,<0.10.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[tool.pytest.ini_options]
|
|
23
|
+
testpaths = ["tests"]
|
|
24
|
+
filterwarnings = [
|
|
25
|
+
"ignore:cannot collect test class 'TestCase'",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"pytest>=9.0.3",
|
|
31
|
+
]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from slough.formatter import format_results
|
|
7
|
+
from slough.models import TestCase
|
|
8
|
+
from slough.parser import parse_md_examples
|
|
9
|
+
from slough.runner import run_test_cases
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description="Trace Python variable values through LeetCode-style solution execution.",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("solution", help="Path to solution.py or directory with solution + README.md")
|
|
17
|
+
parser.add_argument(
|
|
18
|
+
"-t", "--test-cases",
|
|
19
|
+
default=None,
|
|
20
|
+
help="Path to JSON test cases file (optional if README.md exists)",
|
|
21
|
+
)
|
|
22
|
+
parser.add_argument(
|
|
23
|
+
"-o", "--output",
|
|
24
|
+
default=None,
|
|
25
|
+
help="Write output to file instead of stdout",
|
|
26
|
+
)
|
|
27
|
+
return parser.parse_args(argv)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _load_test_cases(path: str) -> list[TestCase]:
|
|
31
|
+
with open(path) as f:
|
|
32
|
+
raw = json.load(f)
|
|
33
|
+
test_cases = []
|
|
34
|
+
for item in raw:
|
|
35
|
+
inputs = tuple(item["inputs"])
|
|
36
|
+
expected = item.get("expected")
|
|
37
|
+
test_cases.append(TestCase(inputs=inputs, expected=expected))
|
|
38
|
+
return test_cases
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _resolve_solution_path(solution: str) -> str | None:
|
|
42
|
+
if os.path.isdir(solution):
|
|
43
|
+
py_files = [f for f in os.listdir(solution) if f.endswith(".py")]
|
|
44
|
+
if not py_files:
|
|
45
|
+
return None
|
|
46
|
+
return os.path.join(solution, py_files[0])
|
|
47
|
+
return solution
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _find_readme(solution_path: str) -> str | None:
|
|
51
|
+
solution_dir = solution_path if os.path.isdir(solution_path) else os.path.dirname(solution_path)
|
|
52
|
+
for name in ("README.md", "readme.md", "README", "readme"):
|
|
53
|
+
candidate = os.path.join(solution_dir, name)
|
|
54
|
+
if os.path.isfile(candidate):
|
|
55
|
+
return candidate
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def main(argv: list[str] | None = None) -> int:
|
|
60
|
+
args = parse_args(argv)
|
|
61
|
+
solution_path = args.solution
|
|
62
|
+
|
|
63
|
+
is_dir = os.path.isdir(solution_path)
|
|
64
|
+
|
|
65
|
+
if is_dir:
|
|
66
|
+
solution_path = _resolve_solution_path(solution_path)
|
|
67
|
+
if solution_path is None:
|
|
68
|
+
print(f"Error: No Python solution file found in directory '{args.solution}'")
|
|
69
|
+
return 1
|
|
70
|
+
|
|
71
|
+
try:
|
|
72
|
+
with open(solution_path) as f:
|
|
73
|
+
source_lines = f.readlines()
|
|
74
|
+
except FileNotFoundError as e:
|
|
75
|
+
print(f"Error: Solution file not found: {e}")
|
|
76
|
+
return 1
|
|
77
|
+
|
|
78
|
+
if args.test_cases:
|
|
79
|
+
try:
|
|
80
|
+
test_cases = _load_test_cases(args.test_cases)
|
|
81
|
+
except FileNotFoundError as e:
|
|
82
|
+
print(f"Error: Test cases file not found: {e}")
|
|
83
|
+
return 1
|
|
84
|
+
except json.JSONDecodeError as e:
|
|
85
|
+
print(f"Error: Invalid JSON in test cases file: {e}")
|
|
86
|
+
return 1
|
|
87
|
+
else:
|
|
88
|
+
readme_path = _find_readme(args.solution)
|
|
89
|
+
if readme_path:
|
|
90
|
+
with open(readme_path) as f:
|
|
91
|
+
test_cases = parse_md_examples(f.read())
|
|
92
|
+
if not test_cases:
|
|
93
|
+
print(f"Error: No test cases found in '{readme_path}'")
|
|
94
|
+
return 1
|
|
95
|
+
else:
|
|
96
|
+
print("Error: No test cases provided (use --test-cases or place README.md alongside the solution)")
|
|
97
|
+
return 1
|
|
98
|
+
|
|
99
|
+
results = run_test_cases(solution_path, test_cases)
|
|
100
|
+
output = format_results(results, source_lines)
|
|
101
|
+
|
|
102
|
+
if args.output:
|
|
103
|
+
with open(args.output, "w") as f:
|
|
104
|
+
f.write(output + "\n")
|
|
105
|
+
else:
|
|
106
|
+
print(output)
|
|
107
|
+
|
|
108
|
+
return 0
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
sys.exit(main())
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from slough.models import TraceResult, TraceStep
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def _format_vars(vars_dict: dict) -> str:
|
|
5
|
+
items = []
|
|
6
|
+
for k, v in vars_dict.items():
|
|
7
|
+
if k == "self":
|
|
8
|
+
continue
|
|
9
|
+
items.append(f"{k}={repr(v)}")
|
|
10
|
+
return ", ".join(items)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _format_return_comparison(result: TraceResult) -> str:
|
|
14
|
+
line = f" Returned: {repr(result.return_value)}"
|
|
15
|
+
if result.test_case.expected is not None:
|
|
16
|
+
if result.return_value == result.test_case.expected:
|
|
17
|
+
line += f" (expected: {repr(result.test_case.expected)})"
|
|
18
|
+
else:
|
|
19
|
+
line += f" (expected: {repr(result.test_case.expected)}) ✗ MISMATCH"
|
|
20
|
+
return line
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def format_results(results: list[TraceResult], source_lines: list[str]) -> str:
|
|
24
|
+
output_parts: list[str] = []
|
|
25
|
+
|
|
26
|
+
for idx, result in enumerate(results):
|
|
27
|
+
idx_line = f"\n{'=' * 60}"
|
|
28
|
+
output_parts.append(idx_line)
|
|
29
|
+
output_parts.append(f" Test Case {idx + 1}")
|
|
30
|
+
|
|
31
|
+
steps = result.steps
|
|
32
|
+
|
|
33
|
+
# Build step index: for each step lineno, collect var changes
|
|
34
|
+
line_vars: dict[int, list[str]] = {}
|
|
35
|
+
for step in steps:
|
|
36
|
+
if step.event in ("line", "return") and step.vars:
|
|
37
|
+
var_str = _format_vars(step.vars)
|
|
38
|
+
if var_str:
|
|
39
|
+
line_vars.setdefault(step.lineno, []).append(var_str)
|
|
40
|
+
if step.event == "call" and step.vars:
|
|
41
|
+
var_str = _format_vars(step.vars)
|
|
42
|
+
if var_str:
|
|
43
|
+
output_parts.append(f"\n Call: {step.func_name}({var_str})")
|
|
44
|
+
|
|
45
|
+
# Print source with variable annotations
|
|
46
|
+
for line_no, line in enumerate(source_lines, start=1):
|
|
47
|
+
code = line.rstrip("\n")
|
|
48
|
+
output_parts.append(f"\n {line_no:>3} │ {code}")
|
|
49
|
+
if line_no in line_vars:
|
|
50
|
+
for var_str in line_vars[line_no]:
|
|
51
|
+
output_parts.append(f" → {var_str}")
|
|
52
|
+
|
|
53
|
+
# Show return events
|
|
54
|
+
for step in steps:
|
|
55
|
+
if step.event == "return" and step.return_value is not None:
|
|
56
|
+
output_parts.append(f"\n Return: {step.func_name} → {repr(step.return_value)}")
|
|
57
|
+
elif step.event == "exception":
|
|
58
|
+
output_parts.append(f"\n Exception: {repr(step.return_value)}")
|
|
59
|
+
|
|
60
|
+
output_parts.append("")
|
|
61
|
+
output_parts.append(f" {'─' * 58}")
|
|
62
|
+
output_parts.append(f" {_format_return_comparison(result)}")
|
|
63
|
+
|
|
64
|
+
return "\n".join(output_parts)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@dataclass
|
|
6
|
+
class TraceStep:
|
|
7
|
+
lineno: int
|
|
8
|
+
event: str
|
|
9
|
+
func_name: str
|
|
10
|
+
vars: dict[str, Any] = field(default_factory=dict)
|
|
11
|
+
return_value: Any = None
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class TestCase:
|
|
16
|
+
inputs: tuple[Any, ...] = ()
|
|
17
|
+
expected: Any = None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class TraceResult:
|
|
22
|
+
test_case: TestCase
|
|
23
|
+
steps: list[TraceStep]
|
|
24
|
+
return_value: Any = None
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import ast
|
|
2
|
+
import html as html_module
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from slough.models import TestCase
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _normalize_content(content: str) -> str:
|
|
10
|
+
"""Convert HTML content to plain text with preserved line structure."""
|
|
11
|
+
if "<" not in content:
|
|
12
|
+
return content
|
|
13
|
+
|
|
14
|
+
content = re.sub(r"<br\s*/?>", "\n", content, flags=re.IGNORECASE)
|
|
15
|
+
content = re.sub(r"</pre\s*>", "\n", content, flags=re.IGNORECASE)
|
|
16
|
+
content = re.sub(r"<pre[^>]*>", "\n", content, flags=re.IGNORECASE)
|
|
17
|
+
content = re.sub(r"<[^>]+>", "", content)
|
|
18
|
+
content = html_module.unescape(content)
|
|
19
|
+
content = re.sub(r"\n{3,}", "\n\n", content)
|
|
20
|
+
return content.strip()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _parse_value(raw: str) -> Any:
|
|
24
|
+
raw = raw.strip()
|
|
25
|
+
if raw in ("true", "True"):
|
|
26
|
+
return True
|
|
27
|
+
if raw in ("false", "False"):
|
|
28
|
+
return False
|
|
29
|
+
if raw in ("null", "None"):
|
|
30
|
+
return None
|
|
31
|
+
try:
|
|
32
|
+
return ast.literal_eval(raw)
|
|
33
|
+
except (ValueError, SyntaxError):
|
|
34
|
+
return raw
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _split_input_pairs(line: str) -> dict[str, Any]:
|
|
38
|
+
pairs: dict[str, Any] = {}
|
|
39
|
+
key_pattern = re.compile(r"(\w+)\s*=")
|
|
40
|
+
|
|
41
|
+
# Find positions of all '=' signs with preceding key names
|
|
42
|
+
parts: list[tuple[str, str]] = []
|
|
43
|
+
pos = 0
|
|
44
|
+
while pos < len(line):
|
|
45
|
+
m = key_pattern.search(line, pos)
|
|
46
|
+
if m is None:
|
|
47
|
+
break
|
|
48
|
+
key = m.group(1)
|
|
49
|
+
val_start = m.end()
|
|
50
|
+
# Find the value: everything until the next ',' at depth 0
|
|
51
|
+
depth = 0
|
|
52
|
+
in_quote: str | None = None
|
|
53
|
+
val_end = val_start
|
|
54
|
+
while val_end < len(line):
|
|
55
|
+
ch = line[val_end]
|
|
56
|
+
if in_quote:
|
|
57
|
+
if ch == in_quote and (val_end == 0 or line[val_end - 1] != "\\"):
|
|
58
|
+
in_quote = None
|
|
59
|
+
elif ch in ("\"", "'"):
|
|
60
|
+
in_quote = ch
|
|
61
|
+
elif ch in ("[", "("):
|
|
62
|
+
depth += 1
|
|
63
|
+
elif ch in ("]", ")"):
|
|
64
|
+
depth -= 1
|
|
65
|
+
elif ch == "," and depth == 0:
|
|
66
|
+
break
|
|
67
|
+
val_end += 1
|
|
68
|
+
|
|
69
|
+
value_raw = line[val_start:val_end].strip().rstrip(",")
|
|
70
|
+
pairs[key] = _parse_value(value_raw)
|
|
71
|
+
pos = val_end + 1
|
|
72
|
+
|
|
73
|
+
return pairs
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_example_lines(input_line: str, output_line: str | None) -> TestCase:
|
|
77
|
+
input_line = re.sub(r"^Input:\s*", "", input_line, flags=re.IGNORECASE).strip()
|
|
78
|
+
pairs = _split_input_pairs(input_line)
|
|
79
|
+
inputs = tuple(pairs.values())
|
|
80
|
+
|
|
81
|
+
expected = None
|
|
82
|
+
if output_line:
|
|
83
|
+
output_line = re.sub(r"^Output:\s*", "", output_line, flags=re.IGNORECASE).strip()
|
|
84
|
+
expected = _parse_value(output_line)
|
|
85
|
+
|
|
86
|
+
return TestCase(inputs=inputs, expected=expected)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def parse_md_examples(md_content: str) -> list[TestCase]:
|
|
90
|
+
content = _normalize_content(md_content)
|
|
91
|
+
cases: list[TestCase] = []
|
|
92
|
+
lines = content.split("\n")
|
|
93
|
+
|
|
94
|
+
example_header = re.compile(
|
|
95
|
+
r"(?:#+\s*)?Example\s*\d*\s*:?\s*$", re.IGNORECASE,
|
|
96
|
+
)
|
|
97
|
+
example_indices = [i for i, l in enumerate(lines) if example_header.match(l.strip())]
|
|
98
|
+
|
|
99
|
+
for idx, start in enumerate(example_indices):
|
|
100
|
+
end = example_indices[idx + 1] if idx + 1 < len(example_indices) else len(lines)
|
|
101
|
+
block = lines[start:end]
|
|
102
|
+
|
|
103
|
+
input_line = None
|
|
104
|
+
output_line = None
|
|
105
|
+
for l in block:
|
|
106
|
+
stripped = l.strip()
|
|
107
|
+
if not stripped:
|
|
108
|
+
continue
|
|
109
|
+
if re.match(r"Input:", stripped, re.IGNORECASE):
|
|
110
|
+
if input_line is None:
|
|
111
|
+
input_line = stripped
|
|
112
|
+
elif re.match(r"Output:", stripped, re.IGNORECASE):
|
|
113
|
+
if output_line is None:
|
|
114
|
+
output_line = stripped
|
|
115
|
+
|
|
116
|
+
if input_line:
|
|
117
|
+
cases.append(parse_example_lines(input_line, output_line))
|
|
118
|
+
|
|
119
|
+
return cases
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from slough.models import TestCase, TraceResult
|
|
5
|
+
from slough.tracer import trace_function_call
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
_COMMON_TYPING_IMPORTS = {
|
|
9
|
+
"List": __import__("typing").List,
|
|
10
|
+
"Dict": __import__("typing").Dict,
|
|
11
|
+
"Optional": __import__("typing").Optional,
|
|
12
|
+
"Tuple": __import__("typing").Tuple,
|
|
13
|
+
"Set": __import__("typing").Set,
|
|
14
|
+
"Deque": __import__("collections").deque,
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _load_solution_class(filepath: str) -> type:
|
|
19
|
+
with open(filepath) as f:
|
|
20
|
+
source = f.read()
|
|
21
|
+
|
|
22
|
+
ns: dict[str, Any] = dict(_COMMON_TYPING_IMPORTS)
|
|
23
|
+
compiled = compile(source, filepath, "exec")
|
|
24
|
+
exec(compiled, ns)
|
|
25
|
+
|
|
26
|
+
for name, obj in ns.items():
|
|
27
|
+
if inspect.isclass(obj):
|
|
28
|
+
methods = [
|
|
29
|
+
m for m in obj.__dict__.values()
|
|
30
|
+
if inspect.isfunction(m) and not m.__name__.startswith("_")
|
|
31
|
+
]
|
|
32
|
+
if methods:
|
|
33
|
+
return obj
|
|
34
|
+
|
|
35
|
+
raise ValueError("No class with methods found in the solution file")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _find_method(solution_class: type, test_case: TestCase) -> str:
|
|
39
|
+
for name, method in solution_class.__dict__.items():
|
|
40
|
+
if inspect.isfunction(method) and not name.startswith("_"):
|
|
41
|
+
try:
|
|
42
|
+
sig = inspect.signature(method)
|
|
43
|
+
params = list(sig.parameters.keys())
|
|
44
|
+
if len(params) - 1 == len(test_case.inputs):
|
|
45
|
+
return name
|
|
46
|
+
except (ValueError, NameError):
|
|
47
|
+
continue
|
|
48
|
+
for name in solution_class.__dict__:
|
|
49
|
+
if not name.startswith("_"):
|
|
50
|
+
return name
|
|
51
|
+
raise ValueError("No public method found on Solution class")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _is_inplace_method(solution_class: type, method_name: str) -> bool:
|
|
55
|
+
method = solution_class.__dict__.get(method_name)
|
|
56
|
+
if not method:
|
|
57
|
+
return False
|
|
58
|
+
try:
|
|
59
|
+
sig = inspect.signature(method)
|
|
60
|
+
return sig.return_annotation is None or sig.return_annotation is inspect.Parameter.empty
|
|
61
|
+
except (ValueError, NameError):
|
|
62
|
+
return False
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def run_test_cases(
|
|
66
|
+
filepath: str,
|
|
67
|
+
test_cases: list[TestCase],
|
|
68
|
+
) -> list[TraceResult]:
|
|
69
|
+
results: list[TraceResult] = []
|
|
70
|
+
|
|
71
|
+
solution_class = _load_solution_class(filepath)
|
|
72
|
+
|
|
73
|
+
for tc in test_cases:
|
|
74
|
+
method_name = _find_method(solution_class, tc)
|
|
75
|
+
instance = solution_class()
|
|
76
|
+
method = getattr(instance, method_name)
|
|
77
|
+
|
|
78
|
+
# Make a mutable copy for in-place methods
|
|
79
|
+
inputs = list(tc.inputs)
|
|
80
|
+
steps, return_value = trace_function_call(
|
|
81
|
+
method, tuple(inputs), filepath
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if return_value is None and _is_inplace_method(solution_class, method_name):
|
|
85
|
+
return_value = inputs[0]
|
|
86
|
+
|
|
87
|
+
results.append(TraceResult(
|
|
88
|
+
test_case=tc,
|
|
89
|
+
steps=steps,
|
|
90
|
+
return_value=return_value,
|
|
91
|
+
))
|
|
92
|
+
|
|
93
|
+
return results
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import Any, Callable
|
|
3
|
+
|
|
4
|
+
from slough.models import TraceStep
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _make_trace_callback(target_filename: str, steps: list[TraceStep]):
|
|
8
|
+
def trace_cb(frame, event, arg):
|
|
9
|
+
if frame.f_code.co_filename != target_filename:
|
|
10
|
+
return trace_cb
|
|
11
|
+
|
|
12
|
+
if event in ("line", "call", "return", "exception"):
|
|
13
|
+
steps.append(TraceStep(
|
|
14
|
+
lineno=frame.f_lineno,
|
|
15
|
+
event=event,
|
|
16
|
+
func_name=frame.f_code.co_name,
|
|
17
|
+
vars=dict(frame.f_locals),
|
|
18
|
+
return_value=arg if event in ("return", "exception") else None,
|
|
19
|
+
))
|
|
20
|
+
|
|
21
|
+
return trace_cb
|
|
22
|
+
|
|
23
|
+
return trace_cb
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def trace_function_call(
|
|
27
|
+
fn: Callable,
|
|
28
|
+
args: tuple,
|
|
29
|
+
target_filename: str,
|
|
30
|
+
) -> tuple[list[TraceStep], Any]:
|
|
31
|
+
steps: list[TraceStep] = []
|
|
32
|
+
trace_cb = _make_trace_callback(target_filename, steps)
|
|
33
|
+
|
|
34
|
+
old_trace = sys.gettrace()
|
|
35
|
+
sys.settrace(trace_cb)
|
|
36
|
+
try:
|
|
37
|
+
result = fn(*args)
|
|
38
|
+
finally:
|
|
39
|
+
sys.settrace(old_trace)
|
|
40
|
+
|
|
41
|
+
return steps, result
|