Skip to content

fix(suod): defer optional suod import + actionable error (closes #640)#676

Merged
yzhao062 merged 1 commit into
yzhao062:developmentfrom
jbbqqf:feat/640-suod-import-error
May 12, 2026
Merged

fix(suod): defer optional suod import + actionable error (closes #640)#676
yzhao062 merged 1 commit into
yzhao062:developmentfrom
jbbqqf:feat/640-suod-import-error

Conversation

@jbbqqf
Copy link
Copy Markdown
Contributor

@jbbqqf jbbqqf commented May 9, 2026

Summary

pyod/models/suod.py had a try/except ImportError that printed a
friendly hint when the optional suod package was missing — and then,
on the very next line, unconditionally ran
from suod.models.base import SUOD as SUOD_model, which raised a bare
ImportError. Users importing pyod.models.suod without suod
installed therefore got a crash, not the friendly hint. This PR makes
the module load side-effect-free and raises an actionable error
(pip install suod ...) only when the user constructs SUOD().

Fixes #640Possible import error in suod.py

Context

The reporter installed pyod via pip, tried from pyod.models.suod import SUOD, and got an ImportError. The bug is in lines 12-16 of master:

try:
    import suod
except ImportError:
    print('please install suod first for SUOD by `pip install suod`')
from suod.models.base import SUOD as SUOD_model   # <- this still runs

The print is harmless but the next line is not guarded — so the
"helpful" branch of the try/except never gets a chance to take effect.

Changes

  • pyod/models/suod.py
    • Move the real from suod.models.base import SUOD as SUOD_model
      inside the try/except.
    • Capture the original ImportError into _SUOD_IMPORT_ERROR so the
      constructor can include it in its error message (debuggability).
    • In SUOD.__init__, if SUOD_model is None, raise a clear
      ImportError naming the install command and including the original
      cause.
  • pyod/test/test_suod.py
    • Add TestSUODOptionalImport::test_constructor_raises_actionable_error_when_suod_missing
      which monkey-patches the module-level sentinels and confirms the
      new error message format. (Doesn't actually uninstall suod — that
      would break the rest of the suite.)

Reproduce BEFORE/AFTER yourself (copy-paste)

# --- one-time setup ---
git clone https://github.com/yzhao062/pyod.git /tmp/repro && cd /tmp/repro
python -m venv .venv && . .venv/bin/activate
pip install -e .       # NOTE: do NOT install suod -> reproduce missing-dep state

# --- BEFORE (origin/master) ---
git checkout origin/master
python -c "from pyod.models.suod import SUOD; SUOD()" 2>&1 | tail -5
# Expected: prints the friendly hint AND then raises a bare ImportError
#           ('No module named suod' or similar) without telling the user
#           what to do — issue #640.

# --- AFTER (this PR) ---
git fetch https://github.com/jbbqqf/pyod.git feat/640-suod-import-error
git checkout FETCH_HEAD
python -c "from pyod.models.suod import SUOD; SUOD()" 2>&1 | tail -5
# Expected: importing the module is silent; constructing SUOD() raises
#           ImportError("pyod.models.suod requires the optional `suod`
#           package. Install it with `pip install suod` and retry. ...")

What I ran locally

  • pytest pyod/test/test_suod.py -q -> 17 passed (16 prior + 1 new
    regression test) using the project's test environment with suod
    installed.
  • The new regression test fails on master (verified via git stash):
    AttributeError: module 'pyod.models.suod' has no attribute '_SUOD_IMPORT_ERROR' — i.e. the sentinel didn't exist because the
    module crashed during the unconditional import on master without
    suod.

Edge cases tested

# Scenario Input Expected Verified by
1 suod installed SUOD() construct succeeds (existing behaviour) existing TestSUOD setUp
2 suod missing import pyod.models.suod succeeds silently (no print, no crash) new TestSUODOptionalImport (monkey-patch path)
3 suod missing SUOD() raises ImportError containing pip install suod new TestSUODOptionalImport

Risk / blast radius

  • Public API behaviour with suod installed is unchanged.
  • pyod.models.suod now imports cleanly even when suod is absent;
    any code that did import pyod.models.suod to introspect (without
    intending to fit a model) will no longer be broken by an absent
    optional dep.
  • Users who were relying on the bare ImportError to gate their own
    optional code paths now see a more verbose message; behaviour-wise,
    it is still an ImportError so try/except ImportError continues to
    work.

Release note

fix(suod): importing `pyod.models.suod` no longer requires the optional `suod` package. The constructor now raises an actionable ImportError pointing at `pip install suod` if the package is missing.

PR template checklist (per PULL_REQUEST_TEMPLATE.md)

  • Followed CONTRIBUTING guidance (small, focused, with regression test).
  • No other open PRs for this change (searched repo).
  • Linked to a specific issue: Possible import error in suod.py #640.
  • Explanation included above.
  • New regression test added (TestSUODOptionalImport).
  • Tests run locally: pytest pyod/test/test_suod.py -q -> 17 passed.
  • CI (CircleCI / Travis / AppVeyor): will be exercised by repo workflows once the PR runs.
  • Code coverage: net-positive (new test method + import guard branch).
  • Lint: no new warnings introduced.

PR drafted with assistance from Claude Code (Anthropic). The change was
reviewed manually against pyod's source. The reproducer block above is
the one I used during development; reviewers can paste it verbatim.

…hao062#640)

`pyod/models/suod.py` had a `try/except ImportError` that printed a
friendly hint when the optional `suod` package was missing — and then,
on the very next line, unconditionally executed `from suod.models.base
import SUOD as SUOD_model`, which raised a bare ImportError. Net result:
users who imported `pyod.models.suod` without `suod` installed got a
crash, not the helpful message.

This change moves the real import inside the try/except, captures the
original ImportError, and raises an actionable message
("pip install suod ...") only when the user constructs `SUOD()`. The
module-load path is now side-effect-free (no print, no crash), so
`pyod.models.suod` can be safely introspected without the optional
dep.

Regression test
(`TestSUODOptionalImport::test_constructor_raises_actionable_error_when_suod_missing`)
monkey-patches the module-level sentinel and confirms the new error
message is raised at constructor time.

AI disclosure: drafted with assistance from Claude (Anthropic).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@yzhao062 yzhao062 changed the base branch from master to development May 12, 2026 06:05
@yzhao062 yzhao062 merged commit bde67e1 into yzhao062:development May 12, 2026
yzhao062 added a commit that referenced this pull request May 12, 2026
Cleanup follow-up to #676. The new test block landed with CRLF line
endings (likely a contributor-side editor / git config artifact) and
was the only section flagged by `git diff --cached --check` as
trailing whitespace. Normalizes only the new block to LF; the rest of
the file keeps its existing CRLF until a broader normalization pass.
No behavior change.
yzhao062 added a commit that referenced this pull request May 13, 2026
Bump version to 3.5.1 and add CHANGES.txt entry summarizing the
patch release: the jbbqqf bundle (#673/#674/#675/#676/#677), the
tuanaiseo GAAL torch-optional fix (#660) with our follow-up across
mo_gaal/so_gaal/so_gaal_new, and the NSF POSE Phase II funding
acknowledgment. Issues closed: #502 #546 #635 #638 #640. No new
public API and no breaking changes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Possible import error in suod.py

2 participants