python-harness 0.0.3__tar.gz → 0.0.6__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.
Files changed (22) hide show
  1. {python_harness-0.0.3/python_harness.egg-info → python_harness-0.0.6}/PKG-INFO +1 -1
  2. {python_harness-0.0.3 → python_harness-0.0.6}/pyproject.toml +1 -1
  3. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness/cli.py +14 -1
  4. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness/hard_evaluator.py +19 -3
  5. {python_harness-0.0.3 → python_harness-0.0.6/python_harness.egg-info}/PKG-INFO +1 -1
  6. python_harness-0.0.6/tests/test_hard_evaluator.py +95 -0
  7. python_harness-0.0.3/tests/test_hard_evaluator.py +0 -29
  8. {python_harness-0.0.3 → python_harness-0.0.6}/LICENSE +0 -0
  9. {python_harness-0.0.3 → python_harness-0.0.6}/README.md +0 -0
  10. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness/__init__.py +0 -0
  11. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness/evaluator.py +0 -0
  12. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness/qc_evaluator.py +0 -0
  13. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness/soft_evaluator.py +0 -0
  14. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness.egg-info/SOURCES.txt +0 -0
  15. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness.egg-info/dependency_links.txt +0 -0
  16. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness.egg-info/entry_points.txt +0 -0
  17. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness.egg-info/requires.txt +0 -0
  18. {python_harness-0.0.3 → python_harness-0.0.6}/python_harness.egg-info/top_level.txt +0 -0
  19. {python_harness-0.0.3 → python_harness-0.0.6}/setup.cfg +0 -0
  20. {python_harness-0.0.3 → python_harness-0.0.6}/tests/test_cli.py +0 -0
  21. {python_harness-0.0.3 → python_harness-0.0.6}/tests/test_evaluator.py +0 -0
  22. {python_harness-0.0.3 → python_harness-0.0.6}/tests/test_soft_evaluator.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-harness
3
- Version: 0.0.3
3
+ Version: 0.0.6
4
4
  Summary: An agentic codebase evaluation and evolution tool for Python projects.
5
5
  Author-email: Mingli Yuan <mingli.yuan@gmail.com>
6
6
  License: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "python-harness"
3
- version = "0.0.3"
3
+ version = "0.0.6"
4
4
  description = "An agentic codebase evaluation and evolution tool for Python projects."
5
5
  requires-python = ">=3.10"
6
6
  readme = "README.md"
@@ -104,7 +104,7 @@ def measure(path: str = typer.Argument(".", help="The path to evaluate")) -> Non
104
104
  if hard_results["mypy"]["status"] != "success":
105
105
  output = hard_results["mypy"].get("output", "")
106
106
  console.print(f"[red]Mypy issues found:[/red]\n{output}")
107
- if hard_results["ty"]["status"] != "success":
107
+ if hard_results["ty"]["status"] not in ("success", "warning"):
108
108
  output = hard_results["ty"].get("output", "")
109
109
  # ty might print to stderr instead of stdout, or it might be missing
110
110
  error_msg = hard_results["ty"].get("error_message", "")
@@ -116,6 +116,9 @@ def measure(path: str = typer.Argument(".", help="The path to evaluate")) -> Non
116
116
  console.print(
117
117
  "[red]Ty failed, but no standard output was captured.[/red]"
118
118
  )
119
+ elif hard_results["ty"]["status"] == "warning":
120
+ msg = hard_results["ty"].get("error_message", "ty not found")
121
+ console.print(f"[yellow]Ty warning:[/yellow] {msg}")
119
122
  if hard_results["radon_cc"]["status"] != "success":
120
123
  issues = hard_results["radon_cc"].get("issues", [])
121
124
  console.print(
@@ -127,6 +130,16 @@ def measure(path: str = typer.Argument(".", help="The path to evaluate")) -> Non
127
130
  f" - {issue['file']}: {issue['type']} '{issue['name']}' "
128
131
  f"has CC {issue['complexity']}"
129
132
  )
133
+
134
+ # If radon failed for another reason
135
+ # (e.g. radon not installed or syntax error)
136
+ if not issues and hard_results["radon_cc"].get("error_message"):
137
+ err_msg = hard_results['radon_cc'].get('error_message')
138
+ console.print(f"[red]Radon CC Error:[/red] {err_msg}")
139
+ elif not issues:
140
+ console.print(
141
+ "[red]Radon CC failed but no specific issues were parsed.[/red]"
142
+ )
130
143
  sys.exit(1)
131
144
 
132
145
  console.print("[bold green]Hard Evaluation Passed![/bold green]")
@@ -63,6 +63,7 @@ class HardEvaluator:
63
63
  def run_ty(self) -> dict[str, Any]:
64
64
  """
65
65
  Run ty language server checks.
66
+ If ty is not installed, fail gracefully rather than crashing.
66
67
  """
67
68
  try:
68
69
  result = subprocess.run(
@@ -79,7 +80,18 @@ class HardEvaluator:
79
80
  "output": output,
80
81
  "return_code": result.returncode,
81
82
  }
83
+ except FileNotFoundError:
84
+ return {
85
+ "status": "warning",
86
+ "error_message": "ty executable not found. Skipping ty checks."
87
+ }
82
88
  except Exception as e:
89
+ # Handle cases where ty is found but fails to run or throws other OS errors
90
+ if "No such file or directory: 'ty'" in str(e):
91
+ return {
92
+ "status": "warning",
93
+ "error_message": "ty executable not found. Skipping ty checks."
94
+ }
83
95
  return {"status": "error", "error_message": str(e)}
84
96
 
85
97
  def run_radon_cc(self) -> dict[str, Any]:
@@ -111,14 +123,18 @@ class HardEvaluator:
111
123
  "complexity": block.get('complexity')
112
124
  })
113
125
 
114
- if issues:
126
+ if result.returncode != 0:
127
+ # E.g. syntax error in target code preventing radon from parsing
128
+ status = "failed"
129
+ elif issues:
115
130
  status = "failed"
116
131
 
117
132
  return {
118
133
  "status": status,
119
134
  "issues": issues,
120
135
  "return_code": result.returncode,
121
- "output": result.stdout
136
+ "output": result.stdout,
137
+ "error_message": result.stderr if result.returncode != 0 else ""
122
138
  }
123
139
  except Exception as e:
124
140
  return {"status": "error", "error_message": str(e)}
@@ -188,7 +204,7 @@ class HardEvaluator:
188
204
  all_passed = (
189
205
  ruff_res.get("status") == "success" and
190
206
  mypy_res.get("status") == "success" and
191
- ty_res.get("status") == "success" and
207
+ ty_res.get("status") in ("success", "warning") and
192
208
  radon_cc_res.get("status") == "success"
193
209
  )
194
210
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-harness
3
- Version: 0.0.3
3
+ Version: 0.0.6
4
4
  Summary: An agentic codebase evaluation and evolution tool for Python projects.
5
5
  Author-email: Mingli Yuan <mingli.yuan@gmail.com>
6
6
  License: MIT
@@ -0,0 +1,95 @@
1
+ """
2
+ Tests for hard evaluation logic.
3
+ """
4
+
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from python_harness.hard_evaluator import HardEvaluator
9
+
10
+
11
+ def test_hard_evaluator_methods() -> None:
12
+ """
13
+ Test methods of HardEvaluator.
14
+ """
15
+ evaluator = HardEvaluator(".")
16
+
17
+ ruff_result = evaluator.run_ruff()
18
+ assert "status" in ruff_result
19
+
20
+ mypy_result = evaluator.run_mypy()
21
+ assert "status" in mypy_result
22
+
23
+ ty_result = evaluator.run_ty()
24
+ assert "status" in ty_result
25
+
26
+ # pytest_result = evaluator.run_pytest() # Causes infinite loop when run in test
27
+ # assert "status" in pytest_result
28
+
29
+ eval_result = evaluator.evaluate()
30
+ assert "ruff" in eval_result
31
+ assert "mypy" in eval_result
32
+ assert "ty" in eval_result
33
+
34
+ def test_ty_fallback_behavior(monkeypatch: Any) -> None:
35
+ """
36
+ Test that run_ty gracefully falls back to a warning when 'ty' is not installed.
37
+ """
38
+ # Create a mock subprocess.run that always raises FileNotFoundError
39
+ # to simulate 'ty' not being on the system PATH
40
+ def mock_run(*args: Any, **kwargs: Any) -> Any:
41
+ raise FileNotFoundError("[Errno 2] No such file or directory: 'ty'")
42
+
43
+ monkeypatch.setattr("subprocess.run", mock_run)
44
+
45
+ evaluator = HardEvaluator(".")
46
+ result = evaluator.run_ty()
47
+
48
+ assert result["status"] == "warning"
49
+ assert "ty executable not found" in result["error_message"]
50
+
51
+ def test_ty_fallback_behavior_oserror(monkeypatch: Any) -> None:
52
+ """
53
+ Test that run_ty gracefully falls back to a warning when a generic Exception
54
+ containing the Errno 2 string is thrown.
55
+ """
56
+ def mock_run(*args: Any, **kwargs: Any) -> Any:
57
+ raise Exception("[Errno 2] No such file or directory: 'ty'")
58
+
59
+ monkeypatch.setattr("subprocess.run", mock_run)
60
+
61
+ evaluator = HardEvaluator(".")
62
+ result = evaluator.run_ty()
63
+
64
+ assert result["status"] == "warning"
65
+ assert "ty executable not found" in result["error_message"]
66
+
67
+ def test_radon_cc_syntax_error(monkeypatch: Any, tmp_path: Path) -> None:
68
+ """
69
+ Test that run_radon_cc correctly captures and reports stderr when radon
70
+ fails (e.g. due to syntax errors in the target codebase).
71
+ """
72
+ # Create a mock subprocess.run that simulates radon exiting with code 1
73
+ # and writing an error to stderr (which happens when there are syntax errors)
74
+ import subprocess
75
+ original_run = subprocess.run
76
+
77
+ def mock_run(args: Any, **kwargs: Any) -> Any:
78
+ if args and args[0] == "radon" and args[1] == "cc":
79
+ # Simulate radon failing on syntax error
80
+ class MockResult:
81
+ returncode = 1
82
+ stdout = ""
83
+ stderr = "ERROR: SyntaxError in bad.py"
84
+ return MockResult()
85
+ return original_run(args, **kwargs)
86
+
87
+ monkeypatch.setattr("subprocess.run", mock_run)
88
+
89
+ evaluator = HardEvaluator(str(tmp_path))
90
+ result = evaluator.run_radon_cc()
91
+
92
+ assert result["status"] == "failed"
93
+ assert len(result["issues"]) == 0
94
+ # Radon should output to stderr because of the syntax error
95
+ assert "SyntaxError" in result["error_message"] or result["return_code"] != 0
@@ -1,29 +0,0 @@
1
- """
2
- Tests for hard evaluation logic.
3
- """
4
-
5
- from python_harness.hard_evaluator import HardEvaluator
6
-
7
-
8
- def test_hard_evaluator_methods() -> None:
9
- """
10
- Test methods of HardEvaluator.
11
- """
12
- evaluator = HardEvaluator(".")
13
-
14
- ruff_result = evaluator.run_ruff()
15
- assert "status" in ruff_result
16
-
17
- mypy_result = evaluator.run_mypy()
18
- assert "status" in mypy_result
19
-
20
- ty_result = evaluator.run_ty()
21
- assert "status" in ty_result
22
-
23
- # pytest_result = evaluator.run_pytest() # Causes infinite loop when run in test
24
- # assert "status" in pytest_result
25
-
26
- eval_result = evaluator.evaluate()
27
- assert "ruff" in eval_result
28
- assert "mypy" in eval_result
29
- assert "ty" in eval_result
File without changes
File without changes
File without changes