Skip to content

_discovery.py: 4 robustness issues with namespace pkgs, hidden dirs, broken submodules, false positive __init__ match #135

Description

@pramodavansaber

Hi maldoinc — found four edge-case crashes in wireup/_discovery.py while running adversarial test generation against the repo via tailtest. All four make _find_objects_in_module crash on legitimate Python project layouts; all four have small fixes.

1. module.__file__ AttributeError on PEP 420 namespace packages

Line 62: module.__file__ is accessed unconditionally. PEP 420 namespace packages don't have __file__ and crash with AttributeError.

Fix: getattr(module, '__file__', None)

2. endswith("__init__.py") false positives

Line 63: f.endswith("__init__.py") matches not__init__.py, treating it as a package and scanning its parent directory.

Fix: Path(f).name == "__init__.py"

3. Hidden directories traversed

Line 47: only __pycache__ is filtered. Dirs like .git, .mypy_cache, .pytest_cache, .hidden are traversed and importlib.import_module is called with invalid dot-prefixed names → ModuleNotFoundError.

Fix: also skip entry.name.startswith(".")

4. One broken submodule crashes entire discovery

Line 59: importlib.import_module is not wrapped in try/except. A single sub-module with an import error (e.g., missing optional dep) kills the whole discovery scan.

Fix: wrap in try/except (ImportError, Exception) and skip-with-warning instead of crashing.

Reproduction

Failing pytest cases for all four are below — drop into tests/test_discovery.py. Each test is ~10-15 LoC, fully isolated.

import os
import sys
import tempfile
import types
import pytest
from wireup._discovery import _find_objects_in_module


# Bug 1: namespace packages crash
def test_namespace_package_module_without_file_attr():
    """PEP 420 namespace packages have no __file__; should be handled."""
    mod = types.ModuleType("ns_pkg")
    if hasattr(mod, "__file__"):
        del mod.__file__
    # Should not raise AttributeError
    list(_find_objects_in_module(mod, predicate=lambda _: False))


# Bug 2: false-positive endswith("__init__.py")
def test_file_named_like_init_but_not_init(tmp_path):
    """File named not__init__.py should NOT be treated as a package."""
    pkg = tmp_path / "my_pkg"
    pkg.mkdir()
    (pkg / "not__init__.py").write_text("X = 1\n")
    sys.path.insert(0, str(tmp_path))
    try:
        import importlib
        mod = importlib.import_module("my_pkg.not__init__")
        list(_find_objects_in_module(mod, predicate=lambda _: False))
    finally:
        sys.path.remove(str(tmp_path))


# Bug 3: hidden dirs traversed
def test_dotdir_in_package_causes_import_attempt(tmp_path):
    """Dirs starting with '.' should be skipped during discovery."""
    pkg = tmp_path / "hidden_pkg"
    pkg.mkdir()
    (pkg / "__init__.py").write_text("")
    (pkg / ".hidden").mkdir()
    (pkg / ".hidden" / "__init__.py").write_text("")
    sys.path.insert(0, str(tmp_path))
    try:
        import importlib
        mod = importlib.import_module("hidden_pkg")
        # Should not raise ModuleNotFoundError on hidden_pkg..hidden
        list(_find_objects_in_module(mod, predicate=lambda _: False))
    finally:
        sys.path.remove(str(tmp_path))


# Bug 4: broken submodule crashes discovery
def test_broken_submodule_crashes_discovery(tmp_path):
    """One sub-module with an ImportError should not kill all discovery."""
    pkg = tmp_path / "brokenpkg"
    pkg.mkdir()
    (pkg / "__init__.py").write_text("")
    (pkg / "good.py").write_text("class A: ...")
    (pkg / "bad.py").write_text("import nonexistent_dep")
    sys.path.insert(0, str(tmp_path))
    try:
        import importlib
        mod = importlib.import_module("brokenpkg")
        # Should not raise; should return objects from good.py
        result = list(_find_objects_in_module(mod, predicate=lambda _: True))
    finally:
        sys.path.remove(str(tmp_path))

Happy to open a PR if useful — the changes are small enough to be a single commit. Found via tailtest's adversarial test generation.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions