Skip to content

← Seams, Modules, Packaging and Tooling step 2 of 36

Easy Framework

Which copy of the package wins?

Implement a miniature version of the path-based finder, so that “why is it importing that one?” becomes a question you can answer from first principles.

def resolve_module(
    sys_path: Sequence[str],
    filesystem: Mapping[str, frozenset[str]],
    module: str,
) -> str | None:
  • sys_path is the search path, in order.
  • filesystem maps a directory path to the set of entry names directly inside it. A name is a directory exactly when f"{parent}/{name}" is itself a key of filesystem. Paths never carry a trailing slash.
  • Return the sys_path entry that wins for module, or None.

Reproduce the real rules, in this order, for each entry in turn:

  1. If the entry is not a key of filesystem (the directory does not exist), skip it. Nonexistent sys.path entries are legal and silently ignored.
  2. If module is a directory under the entry:
    • it contains __init__.py → a regular package. Return this entry immediately.
    • it does not → a namespace portion. Remember the first one you see and keep scanning.
  3. Otherwise, if module + ".py" exists under the entry, return this entry. A module file in the same directory beats a namespace portion there.
  4. If the whole scan finds nothing else, return the first namespace portion, or None.

Why the third rule is the interesting one. A regular package short-circuits the search; a namespace portion does not. That is why deleting an __init__.py can silently change which copy of a package your process runs — the directory stops winning, and a copy further down sys.path takes over without a single error message.

What this models in production. The first test is the flat-layout bug: "" (the working directory) comes first, so the working tree shadows the installed wheel and your suite never touches the artefact you ship. The second is the same project under src layout — the root holds no importable package, so the installed copy is the only candidate. Another is python -P: the leading entry simply is not there, and the answer changes.

Your submission must pass mypy --strict. filesystem.get(entry) returns frozenset[str] | None and you must narrow it before using it — that None is rule 1.