feat: reascript-first — built-in REAPER plugin insertion via ReaScript API
Hybrid pipeline: RPPBuilder writes VST3/MIDI/audio skeleton, ReaScript handles built-in plugins (ReaEQ, ReaComp) via TrackFX_AddByName + TrackFX_SetParam with multi-action dispatch, adaptive API check, and builtin plugin auto-detection from PLUGIN_REGISTRY. 326 tests (298 existing + 28 new), 12/12 spec scenarios compliant.
This commit is contained in:
@@ -20,6 +20,17 @@ from src.reaper_scripting.commands import (
|
||||
write_command,
|
||||
)
|
||||
from src.reaper_scripting import ReaScriptGenerator
|
||||
from src.core.schema import (
|
||||
SongDefinition,
|
||||
SongMeta,
|
||||
TrackDef,
|
||||
PluginDef,
|
||||
)
|
||||
from src.reaper_builder import (
|
||||
RPPBuilder,
|
||||
REAPER_BUILTINS,
|
||||
get_builtin_plugins,
|
||||
)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -302,3 +313,515 @@ class TestReaScriptGeneratorOutput:
|
||||
source = path.read_text(encoding="utf-8")
|
||||
assert "def main():" in source
|
||||
assert "main()" in source
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 4: Multi-Action Dispatch, Adaptive API, JSON Round-Trip
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class TestMultiActionDispatch:
|
||||
"""4.1 Multi-action dispatch generates correct per-action functions."""
|
||||
|
||||
@pytest.mark.parametrize("action_list,expected_funcs,unexpected_funcs", [
|
||||
# calibrate only → calibrate function present, render/add_plugins NOT emitted
|
||||
(["calibrate"], ["def calibrate("], ["def add_plugins(", "def configure_fx_params(", "def do_render("]),
|
||||
# add_plugins only → add_plugins and find_track present, calibrate/render NOT
|
||||
(["add_plugins"], ["def add_plugins(", "def find_track("], ["def calibrate(", "def configure_fx_params(", "def do_render("]),
|
||||
# add_plugins + verify_fx → both present, calibrate/render NOT
|
||||
(["add_plugins", "verify_fx"], ["def add_plugins(", "def verify_fx(", "def find_track("], ["def calibrate(", "def do_render("]),
|
||||
# full pipeline → all 4 action functions present
|
||||
(["add_plugins", "configure_fx_params", "calibrate", "render"],
|
||||
["def add_plugins(", "def configure_fx_params(", "def calibrate(", "def do_render("], []),
|
||||
])
|
||||
def test_per_action_functions_conditional(self, tmp_path, action_list, expected_funcs, unexpected_funcs):
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=action_list,
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
)
|
||||
path = tmp_path / "phase2.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
|
||||
for func in expected_funcs:
|
||||
assert func in source, f"Expected '{func}' in output for actions={action_list}"
|
||||
|
||||
for func in unexpected_funcs:
|
||||
assert func not in source, f"Unexpected '{func}' in output for actions={action_list}"
|
||||
|
||||
def test_string_action_backward_compat(self, tmp_path):
|
||||
"""String action 'calibrate' produces same output as list ['calibrate']."""
|
||||
# Generate with string
|
||||
cmd_str = ReaScriptCommand(
|
||||
version=1,
|
||||
action="calibrate",
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
)
|
||||
path_str = tmp_path / "str_action.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path_str, cmd_str)
|
||||
source_str = path_str.read_text(encoding="utf-8")
|
||||
|
||||
# Generate with list
|
||||
cmd_list = ReaScriptCommand(
|
||||
version=1,
|
||||
action=["calibrate"],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
)
|
||||
path_list = tmp_path / "list_action.py"
|
||||
gen.generate(path_list, cmd_list)
|
||||
source_list = path_list.read_text(encoding="utf-8")
|
||||
|
||||
# Both should be identical
|
||||
assert source_str == source_list, (
|
||||
"String action 'calibrate' should produce same output as list ['calibrate']"
|
||||
)
|
||||
|
||||
def test_empty_action_defaults_to_calibrate(self, tmp_path):
|
||||
"""Empty action list defaults to ['calibrate']."""
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=[],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
)
|
||||
path = tmp_path / "phase2.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
assert "def calibrate(" in source
|
||||
# Render functions are NOT emitted since action defaults to ["calibrate"] only
|
||||
assert "def do_render(" not in source
|
||||
|
||||
def test_dispatch_loop_present_with_multiple_actions(self, tmp_path):
|
||||
"""When multiple actions present, dispatch loop iterates them."""
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=["add_plugins", "verify_fx", "calibrate"],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
)
|
||||
path = tmp_path / "phase2.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
|
||||
# Each action should appear in the dispatch if/elif chain
|
||||
assert '"add_plugins"' in source
|
||||
assert '"verify_fx"' in source
|
||||
assert '"calibrate"' in source
|
||||
# action_list variable exists
|
||||
assert "action_list" in source
|
||||
|
||||
|
||||
class TestAdaptiveApiCheck:
|
||||
"""4.2 Adaptive check_api requires FX APIs only with add_plugins."""
|
||||
|
||||
def test_check_api_requires_fx_apis_with_add_plugins(self, tmp_path):
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=["add_plugins"],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
)
|
||||
path = tmp_path / "phase2.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
assert "TrackFX_AddByName" in source
|
||||
assert "TrackFX_SetParam" in source
|
||||
assert "check_api" in source
|
||||
|
||||
def test_check_api_requires_fx_apis_with_configure_fx_params(self, tmp_path):
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=["add_plugins", "configure_fx_params"],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
)
|
||||
path = tmp_path / "phase2.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
assert "TrackFX_AddByName" in source
|
||||
assert "TrackFX_SetParam" in source
|
||||
|
||||
def test_check_api_omits_fx_apis_for_calibrate_only(self, tmp_path):
|
||||
"""String 'calibrate' (backward compat) should NOT include FX APIs."""
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action="calibrate",
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
)
|
||||
path = tmp_path / "phase2.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
|
||||
# Verify check_api exists but does NOT include FX APIs
|
||||
assert "check_api" in source
|
||||
# The check_api function defines required list; TrackFX_AddByName should NOT be in that list
|
||||
# Find the check_api function body
|
||||
check_api_start = source.find("def check_api():")
|
||||
check_api_end = source.find("def ", check_api_start + 1)
|
||||
if check_api_end == -1:
|
||||
check_api_section = source[check_api_start:]
|
||||
else:
|
||||
check_api_section = source[check_api_start:check_api_end]
|
||||
|
||||
assert "TrackFX_AddByName" not in check_api_section
|
||||
assert "TrackFX_SetParam" not in check_api_section
|
||||
|
||||
def test_check_api_includes_fx_apis_with_add_plugins_in_list(self, tmp_path):
|
||||
"""When add_plugins is in the action list, FX APIs are required."""
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=["add_plugins", "calibrate"],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
)
|
||||
path = tmp_path / "phase2.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
|
||||
check_api_start = source.find("def check_api():")
|
||||
check_api_end = source.find("def ", check_api_start + 1)
|
||||
if check_api_end == -1:
|
||||
check_api_section = source[check_api_start:]
|
||||
else:
|
||||
check_api_section = source[check_api_start:check_api_end]
|
||||
|
||||
assert "TrackFX_AddByName" in check_api_section
|
||||
assert "TrackFX_SetParam" in check_api_section
|
||||
|
||||
|
||||
class TestPluginsToAddRoundTrip:
|
||||
"""4.3 plugins_to_add/added_plugins JSON round-trip serialization."""
|
||||
|
||||
def test_write_read_roundtrip_plugins_to_add(self, tmp_path):
|
||||
"""Write command with plugins_to_add, read back, verify fields survive."""
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=["add_plugins", "configure_fx_params", "calibrate"],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
plugins_to_add=[
|
||||
{"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0, "5": 3.0}},
|
||||
{"track_name": "Lead", "fx_name": "ReaComp", "params": {"0": -18.0}},
|
||||
],
|
||||
)
|
||||
cmd_path = tmp_path / "cmd.json"
|
||||
write_command(cmd_path, cmd)
|
||||
|
||||
# Read back raw JSON
|
||||
raw = json.loads(cmd_path.read_text(encoding="utf-8"))
|
||||
assert "plugins_to_add" in raw
|
||||
assert len(raw["plugins_to_add"]) == 2
|
||||
assert raw["plugins_to_add"][0]["track_name"] == "Bass"
|
||||
assert raw["plugins_to_add"][0]["fx_name"] == "ReaEQ"
|
||||
assert raw["plugins_to_add"][0]["params"]["2"] == 200.0
|
||||
|
||||
def test_read_result_includes_added_plugins(self, tmp_path):
|
||||
"""Read a result JSON with added_plugins field."""
|
||||
data = {
|
||||
"version": 1,
|
||||
"status": "ok",
|
||||
"message": "",
|
||||
"fx_errors": [],
|
||||
"tracks_verified": 2,
|
||||
"added_plugins": [
|
||||
{"fx_name": "ReaEQ", "instance_id": 0, "track_name": "Bass", "status": "ok"},
|
||||
{"fx_name": "ReaComp", "instance_id": 0, "track_name": "Lead", "status": "ok"},
|
||||
],
|
||||
}
|
||||
res_path = tmp_path / "result.json"
|
||||
res_path.write_text(json.dumps(data), encoding="utf-8")
|
||||
result = read_result(res_path)
|
||||
assert len(result.added_plugins) == 2
|
||||
assert result.added_plugins[0]["fx_name"] == "ReaEQ"
|
||||
assert result.added_plugins[0]["instance_id"] == 0
|
||||
assert result.added_plugins[1]["status"] == "ok"
|
||||
|
||||
def test_write_command_omits_empty_plugins_to_add(self, tmp_path):
|
||||
"""When plugins_to_add is empty, it should NOT appear in JSON."""
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action="calibrate",
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
plugins_to_add=[],
|
||||
)
|
||||
cmd_path = tmp_path / "cmd.json"
|
||||
write_command(cmd_path, cmd)
|
||||
raw = json.loads(cmd_path.read_text(encoding="utf-8"))
|
||||
assert "plugins_to_add" not in raw
|
||||
|
||||
def test_read_result_handles_missing_added_plugins(self, tmp_path):
|
||||
"""Old result JSON without added_plugins should default to empty list."""
|
||||
data = {
|
||||
"version": 1,
|
||||
"status": "ok",
|
||||
"message": "",
|
||||
"fx_errors": [],
|
||||
"tracks_verified": 0,
|
||||
}
|
||||
res_path = tmp_path / "result.json"
|
||||
res_path.write_text(json.dumps(data), encoding="utf-8")
|
||||
result = read_result(res_path)
|
||||
assert result.added_plugins == []
|
||||
|
||||
|
||||
class TestBuiltinPluginSkipping:
|
||||
"""4.4 builtin=True PluginDef skipped in RPP .rpp output, listed in command."""
|
||||
|
||||
def test_builtin_plugin_not_in_rpp_output(self, tmp_path):
|
||||
"""A PluginDef with builtin=True is excluded from .rpp VST elements."""
|
||||
song = SongDefinition(
|
||||
meta=SongMeta(bpm=95, key="Am", title="Test Builtin"),
|
||||
tracks=[
|
||||
TrackDef(
|
||||
name="Bass",
|
||||
volume=0.85,
|
||||
plugins=[
|
||||
PluginDef(
|
||||
name="ReaEQ",
|
||||
path="VST: ReaEQ (Cockos)",
|
||||
index=0,
|
||||
params={2: 200.0, 5: 3.0},
|
||||
builtin=True,
|
||||
),
|
||||
PluginDef(
|
||||
name="Serum_2",
|
||||
path="VST3i: Serum 2 (Xfer Records)",
|
||||
index=1,
|
||||
builtin=False,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
builder = RPPBuilder(song)
|
||||
output_path = tmp_path / "test_builtin.rpp"
|
||||
builder.write(output_path)
|
||||
rpp_content = output_path.read_text(encoding="utf-8")
|
||||
|
||||
# ReaEQ (builtin=True) should NOT appear in the VST chain
|
||||
assert "ReaEQ" not in rpp_content, (
|
||||
"Builtin plugin ReaEQ should NOT be written to .rpp VST section"
|
||||
)
|
||||
# Serum_2 (builtin=False) SHOULD appear
|
||||
assert "Serum 2" in rpp_content, (
|
||||
"Non-builtin plugin Serum 2 SHOULD be written to .rpp"
|
||||
)
|
||||
|
||||
def test_builtin_plugin_in_get_builtin_plugins(self):
|
||||
"""get_builtin_plugins() returns builtin=True plugins for the command."""
|
||||
song = SongDefinition(
|
||||
meta=SongMeta(bpm=95, key="Am", title="Test Builtin"),
|
||||
tracks=[
|
||||
TrackDef(
|
||||
name="Bass",
|
||||
plugins=[
|
||||
PluginDef(
|
||||
name="ReaEQ",
|
||||
path="VST: ReaEQ (Cockos)",
|
||||
index=0,
|
||||
params={2: 200.0, 5: 3.0},
|
||||
builtin=True,
|
||||
),
|
||||
PluginDef(
|
||||
name="ReaComp",
|
||||
path="VST: ReaComp (Cockos)",
|
||||
index=1,
|
||||
params={0: -18.0},
|
||||
builtin=False,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
builtins = get_builtin_plugins(song)
|
||||
# ReaEQ has builtin=True → should appear
|
||||
assert len(builtins) >= 1
|
||||
reaeq_entries = [b for b in builtins if b["fx_name"] == "ReaEQ"]
|
||||
assert len(reaeq_entries) == 1
|
||||
assert reaeq_entries[0]["track_name"] == "Bass"
|
||||
# ReaComp with builtin=False BUT name in REAPER_BUILTINS → should also appear
|
||||
assert "ReaComp" in REAPER_BUILTINS
|
||||
reacomp_entries = [b for b in builtins if b["fx_name"] == "ReaComp"]
|
||||
assert len(reacomp_entries) == 1
|
||||
|
||||
def test_non_builtin_not_in_reaper_builtins(self):
|
||||
"""A non-builtin, non-Cockos plugin name is NOT in REAPER_BUILTINS."""
|
||||
assert "Serum_2" not in REAPER_BUILTINS
|
||||
assert "Omnisphere" not in REAPER_BUILTINS
|
||||
# But Cockos plugins ARE
|
||||
assert "ReaEQ" in REAPER_BUILTINS
|
||||
assert "ReaComp" in REAPER_BUILTINS
|
||||
|
||||
def test_params_survive_in_get_builtin_plugins(self):
|
||||
"""Plugin params are correctly converted to string-keyed dict."""
|
||||
song = SongDefinition(
|
||||
meta=SongMeta(bpm=95, key="Am", title="Test Params"),
|
||||
tracks=[
|
||||
TrackDef(
|
||||
name="Bass",
|
||||
plugins=[
|
||||
PluginDef(
|
||||
name="ReaEQ",
|
||||
path="VST: ReaEQ (Cockos)",
|
||||
index=0,
|
||||
params={2: 200.0, 5: 3.0},
|
||||
builtin=True,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
builtins = get_builtin_plugins(song)
|
||||
reaeq = builtins[0]
|
||||
assert reaeq["params"] == {"2": 200.0, "5": 3.0}
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Phase 5: Generated Code Content Assertions
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
class TestAddPluginsSourceContent:
|
||||
"""Verify add_plugins generated source contains correct ReaScript API calls."""
|
||||
|
||||
@pytest.fixture
|
||||
def source_with_add_plugins(self, tmp_path: Path) -> str:
|
||||
"""Generate script with add_plugins action and return source."""
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=["add_plugins"],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
plugins_to_add=[
|
||||
{"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0}},
|
||||
],
|
||||
)
|
||||
path = tmp_path / "content_test.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
def test_add_plugins_calls_trackfx_addbyname(self, source_with_add_plugins: str):
|
||||
"""Insert ReaEQ on target track — TrackFX_AddByName is called."""
|
||||
assert "RPR_TrackFX_AddByName" in source_with_add_plugins
|
||||
|
||||
def test_add_plugins_handles_track_not_found(self, source_with_add_plugins: str):
|
||||
"""Track not found — error written, script continues."""
|
||||
assert "track not found" in source_with_add_plugins.lower()
|
||||
assert "continue" in source_with_add_plugins
|
||||
|
||||
def test_add_plugins_verifies_with_getfxname(self, source_with_add_plugins: str):
|
||||
"""Plugin loaded successfully — GetFXName case-insensitive match."""
|
||||
assert "RPR_TrackFX_GetFXName" in source_with_add_plugins
|
||||
assert ".lower()" in source_with_add_plugins # case-insensitive comparison
|
||||
|
||||
def test_add_plugins_records_ok_status(self, source_with_add_plugins: str):
|
||||
"""Plugin loaded — 'ok' status recorded in results."""
|
||||
assert '"status": "ok"' in source_with_add_plugins
|
||||
|
||||
def test_add_plugins_records_failed_status(self, source_with_add_plugins: str):
|
||||
"""Plugin failed to load — 'failed to load' status recorded."""
|
||||
assert "failed to load" in source_with_add_plugins
|
||||
|
||||
|
||||
class TestConfigureFxParamsSourceContent:
|
||||
"""Verify configure_fx_params generated source contains correct parameter setting."""
|
||||
|
||||
@pytest.fixture
|
||||
def source_with_configure(self, tmp_path: Path) -> str:
|
||||
"""Generate script with configure_fx_params action and return source."""
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=["add_plugins", "configure_fx_params"],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
plugins_to_add=[
|
||||
{"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0, "5": 3.0}},
|
||||
],
|
||||
)
|
||||
path = tmp_path / "content_test.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
return path.read_text(encoding="utf-8")
|
||||
|
||||
def test_configure_calls_setparam(self, source_with_configure: str):
|
||||
"""Set ReaEQ frequency and gain — TrackFX_SetParam called."""
|
||||
assert "RPR_TrackFX_SetParam" in source_with_configure
|
||||
|
||||
def test_configure_iterates_params_dict(self, source_with_configure: str):
|
||||
"""Iterates params dict, converts string keys to int indices."""
|
||||
assert "int(param_idx_str)" in source_with_configure
|
||||
|
||||
def test_configure_finds_fx_by_name(self, source_with_configure: str):
|
||||
"""Looks up FX by name to get target index before setting params."""
|
||||
assert "RPR_TrackFX_GetCount" in source_with_configure
|
||||
assert "RPR_TrackFX_GetFXName" in source_with_configure
|
||||
|
||||
|
||||
class TestNoBareExcept:
|
||||
"""Verify generated script template has no bare except clauses."""
|
||||
|
||||
def test_no_bare_except_in_generated_source(self, tmp_path: Path):
|
||||
"""Generated ReaScript must not contain bare 'except:' (only 'except ...')."""
|
||||
cmd = ReaScriptCommand(
|
||||
version=1,
|
||||
action=["add_plugins", "configure_fx_params", "calibrate", "render"],
|
||||
rpp_path="C:/song.rpp",
|
||||
render_path="C:/song.wav",
|
||||
timeout=120,
|
||||
track_calibration=[],
|
||||
plugins_to_add=[
|
||||
{"track_name": "Bass", "fx_name": "ReaEQ", "params": {"2": 200.0}},
|
||||
],
|
||||
)
|
||||
path = tmp_path / "no_bare_except.py"
|
||||
gen = ReaScriptGenerator()
|
||||
gen.generate(path, cmd)
|
||||
source = path.read_text(encoding="utf-8")
|
||||
|
||||
# Check no bare "except:" (without exception type)
|
||||
lines = source.split("\n")
|
||||
for i, line in enumerate(lines):
|
||||
stripped = line.strip()
|
||||
if stripped.startswith("except:") and len(stripped) == len("except:"):
|
||||
pytest.fail(
|
||||
f"Bare 'except:' found at line {i + 1}: {line}"
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user