Skip to content

fix: classify user-package import failures in load_python_modules - #1473

Open
EngHabu wants to merge 2 commits into
mainfrom
fix/sentry-module-pkg-import
Open

fix: classify user-package import failures in load_python_modules#1473
EngHabu wants to merge 2 commits into
mainfrom
fix/sentry-module-pkg-import

Conversation

@EngHabu

@EngHabu EngHabu commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

What

load_python_modules has three importlib.import_module(<user-module>) call sites. #1168 gave two of them the broad "anything raised inside a user module is a user error" treatment. The third — the branch that imports a directory as a package when it holds no top-level .py files — kept its original narrow handler:

if not python_files:
    # If no .py files found, try importing as a module
    try:
        rel_path = path.resolve().relative_to(root_dir)
        mod = ".".join(rel_path.parts)
        imported_module = importlib.import_module(mod)
        loaded_modules.append(imported_module)
    except (ValueError, ModuleNotFoundError):
        pass

That leaks in both directions.

1. Loud half — raw exception to Sentry. Anything else raised by the user's __init__.py (TypeError, OSError, SyntaxError, PydanticUserError, yaml ScannerError, …) escapes load_python_modules unclassified and crash-reports, even though it is by definition a bug in the user's code or one of its third-party imports. This is the same class the Sentry corpus shows for the other two sites (FLYTE-SDK-39 and friends) and that #1168 already closed there.

2. Silent half — swallowed dependency error. A ModuleNotFoundError raised from inside the package (a dependency the user has not installed) is eaten by the bare pass. flyte deploy then proceeds with zero environments and no explanation. Arguably worse than the crash, since the user gets no signal at all.

Verified on clean main:

raises          -> RAW TypeError: user init blew up   <-- LEAKS TO SENTRY
missing_dep     -> mods=0 failed=0  (silently skipped)
not_importable  -> mods=1 failed=0
healthy         -> mods=1 failed=0

and on this branch:

raises          -> failed_paths=['TypeError: user init blew up']  (clean ClickException, no Sentry)
missing_dep     -> failed_paths=["ModuleNotFoundError: No module named 'totally_missing_dep_xyz'"]
not_importable  -> mods=1 failed=0     # unchanged
healthy         -> mods=1 failed=0     # unchanged

How

The narrow catch was not wrong when it was written — it is control flow for "this directory is not an importable package, skip it". This PR keeps that, and only that:

  • ValueError is scoped to the relative_to call that actually raises it, so a ValueError from user code at import time is no longer mistaken for a path-outside-the-root.
  • ModuleNotFoundError is skipped only when it names the target module (or a parent package of it) — i.e. the directory genuinely is not importable here. ModuleNotFoundError.name distinguishes that from a missing inner import, which is now surfaced.
  • Everything else goes to failed_paths, consistent with the sibling directory branch, so deploy raises a clean click.ClickException (already on the _sentry.py user-error allow-list) and still honours --ignore-load-errors.

Behaviour change worth a reviewer's eye

A package that fails to import now records a failed_path, so flyte deploy aborts with a clean error instead of silently deploying nothing. --ignore-load-errors restores the old permissive behaviour. A package that is simply not importable here is still skipped silently, exactly as before.

Sentry status

No fixes FLYTE-SDK-XX claimed. No issue in the current corpus lands on this branch — the module-loader issues in Sentry (66, 6D, 56, 5Q, 4X, …) map to the other two call sites on releases predating #1168. I checked v2.5.1:module_loader.py:117 directly to confirm 66 is the dir-of-files loop, not this one. This is the completeness fix for the site #1168 missed, plus the silent-skip bug sitting next to it.

Tests

Six new tests. The two that pin the leak and the silent skip fail on main:

FAILED test_load_python_modules_package_records_user_init_errors
FAILED test_load_python_modules_package_records_missing_dependency

The four that pin the preserved skip-and-continue behaviour (target missing, parent package missing, path outside root, healthy package) pass on both sides by design — they are regression guards for the behaviour this PR must not change, not repros.

Each test uses a distinct package name so a cached sys.modules entry from one cannot change what another imports.

Local run: tests/flyte + tests/cli = 4057 passed / 7 failed, byte-identical failure set to clean main (4051 passed / 7 failed — same 7, all pre-existing in this sandbox). ruff format, ruff check, check_docstring_style.py (492 files clean) and mypy all clean.

`load_python_modules` has three `importlib.import_module(<user-module>)` call
sites. #1168 gave two of them the broad "anything raised inside a user module is
a user error" treatment, but the third — the branch that imports a *directory*
as a package when it contains no top-level `.py` files — kept its original
narrow handler:

    except (ValueError, ModuleNotFoundError):
        pass

That leaks in both directions:

1. Any other exception from the user's `__init__.py` (`TypeError`, `OSError`,
   `SyntaxError`, `PydanticUserError`, yaml `ScannerError`, ...) escapes
   `load_python_modules` raw and crash-reports to Sentry, even though it is by
   definition a bug in the user's code or one of its third-party imports. This
   is the same class the Sentry corpus shows for the other two sites
   (FLYTE-SDK-39 and friends), which #1168 already closed.

2. A `ModuleNotFoundError` raised from *inside* the package — a dependency the
   user has not installed — is swallowed by the bare `pass`. Deploy then
   proceeds with zero environments and no explanation. The silent half is
   arguably worse than the crash.

The narrow catch was not wrong when it was written: it is control flow for
"this directory is not an importable package, skip it". This keeps that, and
only that:

- `ValueError` is now scoped to the `relative_to` call that actually raises it,
  so a `ValueError` from user code at import time is no longer mistaken for a
  path-outside-the-root.
- `ModuleNotFoundError` is skipped only when it names the target module (or a
  parent package of it) — i.e. the directory genuinely is not importable here.
  `ModuleNotFoundError.name` distinguishes that from a missing inner import.
- Everything else is recorded in `failed_paths`, consistent with the sibling
  directory branch, so `flyte deploy` surfaces a clean `ClickException` and
  honours `--ignore-load-errors` instead of crash-reporting.

No Sentry issue is currently firing on this branch, so nothing is claimed as
fixed — this closes the site #1168 missed, plus the silent-skip bug next to it.

Six tests; the two that pin the leak and the silent skip fail on main, and the
four that pin the preserved skip-and-continue behaviour pass on both sides by
design.

Signed-off-by: Haytham Abuelfutuh <haytham@afutuh.com>
@EngHabu EngHabu added the sentry-fix Fix for an issue surfaced by Sentry label Aug 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

sentry-fix Fix for an issue surfaced by Sentry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants