From 439a946af0b3f4fa202104546483c6c33a3a0aed Mon Sep 17 00:00:00 2001 From: burtenshaw Date: Fri, 19 Jun 2026 13:52:52 +0200 Subject: [PATCH] refactor validation main guard check --- src/openenv/cli/_validation.py | 47 +++++++++++++++++++-------------- tests/test_cli/test_validate.py | 18 +++++++++++++ 2 files changed, 45 insertions(+), 20 deletions(-) diff --git a/src/openenv/cli/_validation.py b/src/openenv/cli/_validation.py index 45b7be2181..42a685fca2 100644 --- a/src/openenv/cli/_validation.py +++ b/src/openenv/cli/_validation.py @@ -440,32 +440,39 @@ def _has_main_guard_call(app_content: str) -> bool: ) for node in ast.iter_child_nodes(tree): - if not isinstance(node, ast.If): + if not isinstance(node, ast.If) or not _is_main_guard(node.test): continue - test = node.test - if not ( - isinstance(test, ast.Compare) - and isinstance(test.left, ast.Name) - and test.left.id == "__name__" - and len(test.ops) == 1 - and isinstance(test.ops[0], ast.Eq) - and len(test.comparators) == 1 - and isinstance(test.comparators[0], ast.Constant) - and test.comparators[0].value == "__main__" - ): - continue - - for guarded_node in node.body: - for candidate in ast.walk(guarded_node): - if isinstance(candidate, ast.Call): - func = candidate.func - if isinstance(func, ast.Name) and func.id == "main": - return True + if any(_contains_main_call(guarded_node) for guarded_node in node.body): + return True return False +def _is_main_guard(test: ast.expr) -> bool: + """Return True for `if __name__ == "__main__"` tests.""" + return ( + isinstance(test, ast.Compare) + and isinstance(test.left, ast.Name) + and test.left.id == "__name__" + and len(test.ops) == 1 + and isinstance(test.ops[0], ast.Eq) + and len(test.comparators) == 1 + and isinstance(test.comparators[0], ast.Constant) + and test.comparators[0].value == "__main__" + ) + + +def _contains_main_call(node: ast.AST) -> bool: + """Return True when an AST node contains a direct `main(...)` call.""" + return any( + isinstance(candidate, ast.Call) + and isinstance(candidate.func, ast.Name) + and candidate.func.id == "main" + for candidate in ast.walk(node) + ) + + _OPENENV_RUNTIME_DEP_RE = re.compile(r"^openenv(?:\s*(?:$|[<>=!~@;])|\[)") _LEGACY_OPENENV_CORE_DEP_RE = re.compile(r"^openenv-core(?:\s*(?:$|[<>=!~@;])|\[)") _OPENENV_DOCKER_INSTALL_RE = re.compile( diff --git a/tests/test_cli/test_validate.py b/tests/test_cli/test_validate.py index 4ea45c3017..07e0d0bcdc 100644 --- a/tests/test_cli/test_validate.py +++ b/tests/test_cli/test_validate.py @@ -297,6 +297,24 @@ def test_validate_command_rejects_nested_main_guard(tmp_path: Path) -> None: assert "main() function not callable" in result.output +def test_validate_command_accepts_later_top_level_main_guard(tmp_path: Path) -> None: + """Local validation scans each top-level __main__ guard for a main() call.""" + env_dir = tmp_path / "test_env" + _write_minimal_valid_env(env_dir) + (env_dir / "server" / "app.py").write_text( + "def main():\n return None\n\n" + "if __name__ == '__main__':\n" + " print('starting')\n\n" + "if __name__ == '__main__':\n" + " main()\n" + ) + + result = runner.invoke(app, ["validate", str(env_dir)]) + + assert result.exit_code == 0 + assert "[OK]" in result.output + + def test_validate_command_syntax_error_fallback_requires_dunder_main( tmp_path: Path, ) -> None: