From f13358c60816420a69ee187e3211b7fe3ac046b2 Mon Sep 17 00:00:00 2001 From: Yeongseon Choe Date: Fri, 30 Oct 2026 19:34:47 +0900 Subject: [PATCH 1/2] fix(cli): name the --app target and exception type when the import fails Importing the --app module runs module-level user code, which can raise anything. Only ImportError, ValueError and AttributeError were reported with the target; any other exception fell through to a generic handler that printed just the exception text, for example a bare KeyError message for a missing environment variable. Report every exception raised by the import as: Error: Could not import module from --app : : . The exit code and the empty stdout are unchanged. Refs #860 Co-authored-by: sisyphus-dev-ai --- src/azure_functions_openapi/cli.py | 7 +++- tests/test_cli.py | 63 ++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/src/azure_functions_openapi/cli.py b/src/azure_functions_openapi/cli.py index 032e40b5..32c815a2 100644 --- a/src/azure_functions_openapi/cli.py +++ b/src/azure_functions_openapi/cli.py @@ -259,9 +259,12 @@ def handle_generate(args: argparse.Namespace) -> int: if getattr(args, "app", None): try: resolved_app, variable_given = _import_app_module(args.app) - except (ImportError, ValueError, AttributeError) as e: + except Exception as e: + # Module-level user code runs on import and can raise anything, so + # report the failure with the --app target and the exception type. print( - f"Error: Could not import module from --app {args.app!r}: {e}", + f"Error: Could not import module from --app {args.app!r}: " + f"{type(e).__name__}: {e}", file=sys.stderr, ) return 1 diff --git a/tests/test_cli.py b/tests/test_cli.py index 4a27380c..967515ac 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -726,6 +726,69 @@ def test_no_app_option_skips_import(self) -> None: mock_import.assert_not_called() +class TestAppImportFailureMessage: + """--app import failures name the target and the exception type (#860).""" + + @staticmethod + def _generate_with_app(monkeypatch: pytest.MonkeyPatch, project: Path, app: str) -> int: + monkeypatch.chdir(project) + monkeypatch.setattr(sys, "path", list(sys.path)) + monkeypatch.setattr(sys, "argv", ["azure-functions-openapi", "generate", "--app", app]) + return main() + + def test_exception_from_module_level_code_names_target_and_type( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + ) -> None: + # Given: an app module that reads a missing environment variable at import time. + (tmp_path / "import_time_failure_app.py").write_text( + "import os\nSECRET = os.environ['OPENAPI_TEST_MISSING_SECRET']\n", encoding="utf-8" + ) + monkeypatch.delenv("OPENAPI_TEST_MISSING_SECRET", raising=False) + + # When: the CLI imports that module through --app. + exit_code = self._generate_with_app(monkeypatch, tmp_path, "import_time_failure_app:app") + + # Then: it fails with exit code 1 and the message traces the failure to the target. + captured = capsys.readouterr() + assert exit_code == 1 + assert captured.out == "" + assert "--app 'import_time_failure_app:app'" in captured.err + assert "KeyError" in captured.err + assert "OPENAPI_TEST_MISSING_SECRET" in captured.err + + @pytest.mark.parametrize( + ("app", "exception_type", "detail"), + [ + ("nonexistent_module_xyz_12345", "ModuleNotFoundError", "nonexistent_module_xyz_12345"), + (":app", "ValueError", "Invalid --app value"), + ("json:no_such_variable", "AttributeError", "no attribute 'no_such_variable'"), + ], + ) + def test_designed_errors_keep_their_message_and_gain_the_type( + self, + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, + capsys: pytest.CaptureFixture[str], + app: str, + exception_type: str, + detail: str, + ) -> None: + # Given: an --app value that fails in one of the CLI's own designed ways. + # When: the CLI tries to import it. + exit_code = self._generate_with_app(monkeypatch, tmp_path, app) + + # Then: the original message is still reported, now with the exception type. + captured = capsys.readouterr() + assert exit_code == 1 + assert captured.out == "" + assert f"--app {app!r}" in captured.err + assert exception_type in captured.err + assert detail in captured.err + + class TestEmptyPathsWarning: """Tests for the empty-paths guard warning.""" From 9e5e3cd631f6345e33f1e0cba9f043fa55bfaaec Mon Sep 17 00:00:00 2001 From: Yeongseon Choe Date: Fri, 30 Oct 2026 19:34:47 +0900 Subject: [PATCH 2/2] docs(cli): show a CI-safe guard for import-time side effects The --app warning recommends guarding external initialization with an environment flag but showed no example. Add a minimal guard, the CI invocation, and the error message a still-failing import produces. Refs #860 Co-authored-by: sisyphus-dev-ai --- docs/cli.md | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/docs/cli.md b/docs/cli.md index 54b50134..b4d762f1 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -130,6 +130,30 @@ azure-functions-openapi generate --app function_app:app --output openapi.json build-time-safe defaults, for example by guarding external initialization with an environment flag when generating a spec in CI. +For example, guard the initialization that needs live credentials or network +access behind a flag that only the spec-generation job sets: + +```python +# function_app.py +import os + +import azure.functions as func +from azure.servicebus import ServiceBusClient + +app = func.FunctionApp() + +if not os.environ.get("SKIP_EXTERNAL_INIT"): + service_bus = ServiceBusClient.from_connection_string(os.environ["SERVICE_BUS_CONNECTION"]) +``` + +```bash +SKIP_EXTERNAL_INIT=1 azure-functions-openapi generate --app function_app:app --output openapi.json +``` + +If the module still raises while it is imported, the CLI exits with code `1` and +names the `--app` target and the exception type, for example +`Error: Could not import module from --app 'function_app:app': KeyError: 'SERVICE_BUS_CONNECTION'`. + Discovery semantics: - **`--app function_app`** (module only): decorators fire, but binding discovery