From d7266de1ae24f3c23de8e39527ace40b15e1e24b Mon Sep 17 00:00:00 2001 From: Manav Panchal Date: Fri, 18 Sep 2026 16:00:36 +0530 Subject: [PATCH] fix: pin opentelemetry-api to 1.29.0 and surface patchflow import errors pydantic-ai 0.0.37 imports opentelemetry._events, which was removed in opentelemetry-api>=1.30.0, breaking fresh installs. Pin opentelemetry-api to 1.29.0 and stop masking real module import failures as 'Patchflow X not found'. --- patchwork/app.py | 17 ++++++++++++++--- poetry.lock | 20 ++++++++++---------- pyproject.toml | 2 ++ tests/common/test_app.py | 36 ++++++++++++++++++++++++++++++++++++ 4 files changed, 62 insertions(+), 13 deletions(-) diff --git a/patchwork/app.py b/patchwork/app.py index 4149959a6..d06a5448a 100644 --- a/patchwork/app.py +++ b/patchwork/app.py @@ -68,6 +68,8 @@ def find_patchflow(possible_module_paths: Iterable[str], patchflow: str) -> Any return getattr(module, patchflow) except AttributeError: logger.debug(f"Patchflow {patchflow} not found in {module_path}") + except ImportError: + raise except Exception: logger.debug(f"Patchflow {patchflow} not found as a file/directory in {module_path}") @@ -75,8 +77,11 @@ def find_patchflow(possible_module_paths: Iterable[str], patchflow: str) -> Any module = importlib.import_module(module_path) logger.info(f"Patchflow {patchflow} loaded from {module_path}") return getattr(module, patchflow) - except ModuleNotFoundError: - logger.debug(f"Patchflow {patchflow} not found as a module in {module_path}") + except ModuleNotFoundError as e: + if e.name == module_path: + logger.debug(f"Patchflow {patchflow} not found as a module in {module_path}") + else: + raise except AttributeError: logger.debug(f"Patchflow {patchflow} not found in {module_path}") @@ -213,7 +218,13 @@ def cli( if debug: inputs["debug"] = True - patchflow_class = find_patchflow(possbile_module_paths, patchflow_name) + patchflow_class = None + try: + patchflow_class = find_patchflow(possbile_module_paths, patchflow_name) + except Exception as e: + logger.debug(traceback.format_exc()) + logger.error(f"Error loading patchflow {patchflow_name}: {e}") + exit(1) if patchflow_class is None: logger.error(f"Patchflow {patchflow_name} not found in {possbile_module_paths}") exit(1) diff --git a/poetry.lock b/poetry.lock index 981002280..5b051ae1c 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2591,14 +2591,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2 [[package]] name = "importlib-metadata" -version = "8.6.1" +version = "8.5.0" description = "Read metadata from Python packages" optional = false -python-versions = ">=3.9" +python-versions = ">=3.8" groups = ["main", "dev"] files = [ - {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, - {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, + {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, + {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, ] markers = {dev = "python_version <= \"3.11\""} @@ -2611,7 +2611,7 @@ cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] enabler = ["pytest-enabler (>=2.2)"] perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] +test = ["flufl.flake8", "importlib-resources (>=1.3) ; python_version < \"3.9\"", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] type = ["pytest-mypy"] [[package]] @@ -3852,19 +3852,19 @@ et-xmlfile = "*" [[package]] name = "opentelemetry-api" -version = "1.31.0" +version = "1.29.0" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.8" groups = ["main"] files = [ - {file = "opentelemetry_api-1.31.0-py3-none-any.whl", hash = "sha256:145b72c6c16977c005c568ec32f4946054ab793d8474a17fd884b0397582c5f2"}, - {file = "opentelemetry_api-1.31.0.tar.gz", hash = "sha256:d8da59e83e8e3993b4726e4c1023cd46f57c4d5a73142e239247e7d814309de1"}, + {file = "opentelemetry_api-1.29.0-py3-none-any.whl", hash = "sha256:5fcd94c4141cc49c736271f3e1efb777bebe9cc535759c54c936cca4f1b312b8"}, + {file = "opentelemetry_api-1.29.0.tar.gz", hash = "sha256:d04a6cf78aad09614f52964ecb38021e248f5714dc32c2e0d8fd99517b4d69cf"}, ] [package.dependencies] deprecated = ">=1.2.6" -importlib-metadata = ">=6.0,<8.7.0" +importlib-metadata = ">=6.0,<=8.5.0" [[package]] name = "oras" @@ -7548,4 +7548,4 @@ security = ["owasp-depscan", "semgrep"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "2d783f3340a7287ae75df712b3464e16fe83a0c42b7a929185340393398ffa3d" +content-hash = "f4a5728cf4360f3e6aa2d6921bb75ead52e90fc01c53af8b8813b7193cd3342d" diff --git a/pyproject.toml b/pyproject.toml index d136584c5..b4b4d8e26 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -61,6 +61,8 @@ numpy = "1.26.4" pandas = "2.2.3" scipy = "1.13.1" boto3 = "~1.37.11" +# pydantic-ai 0.0.37 imports opentelemetry._events, which was removed in opentelemetry-api>=1.30.0 +opentelemetry-api = "1.29.0" # extras # security diff --git a/tests/common/test_app.py b/tests/common/test_app.py index d8dffea50..b199a353f 100644 --- a/tests/common/test_app.py +++ b/tests/common/test_app.py @@ -112,6 +112,42 @@ def test_default_find_module(): assert patchflow.__name__ == "AutoFix" +def test_find_missing_patchflow_returns_none(): + patchflow = find_patchflow(["patchwork.patchflows"], "ThisPatchflowDoesNotExist") + assert patchflow is None + + +def test_find_fails_on_broken_patchflow_file(tmp_path): + flow_dir = tmp_path / "brokenflow" + flow_dir.mkdir(parents=True, exist_ok=True) + flow_file = flow_dir / "brokenflow.py" + flow_file.write_text("import missing_transitive_dependency_xyz\nclass BrokenFlow:\n pass\n") + + with pytest.raises(ModuleNotFoundError, match="missing_transitive_dependency_xyz"): + find_patchflow([str(flow_file.resolve())], "BrokenFlow") + + +def test_find_fails_on_broken_transitive_dependency(mocker): + def broken_import(name, *args, **kwargs): + raise ModuleNotFoundError("No module named 'opentelemetry._events'", name="opentelemetry._events") + + mocker.patch("patchwork.app.importlib.import_module", side_effect=broken_import) + + with pytest.raises(ModuleNotFoundError, match="opentelemetry._events"): + find_patchflow(["patchwork.patchflows"], "GenerateDocstring") + + +def test_cli_surfaces_broken_import(mocker, runner): + def broken_import(name, *args, **kwargs): + raise ModuleNotFoundError("No module named 'opentelemetry._events'", name="opentelemetry._events") + + mocker.patch("patchwork.app.importlib.import_module", side_effect=broken_import) + + result = runner.invoke(cli, ["GenerateDocstring"]) + assert result.exit_code == 1 + assert "opentelemetry._events" in result.output + + def test_config_find_module(patchflow_file): code = """\ class noop: