Provide a canonical, declarative mechanism for import redirects (and their reverse resolution)
还没有人认领这个 Issue。
- 主要语言
- Python
- 星标
- 77.2k
- 派生
- 36k
- PR 合并指标
- PR 指标待抓取
描述
Feature or enhancement
Proposal
Python has no canonical way to declare that an importable name should resolve to a location other than where the standard path finders would look for it, nor any way for a tool to resolve such a redirection backwards (given a file, what name would it be imported under?).
Every tool that needs this capability — most visibly build backends implementing editable installs (PEP 660) — invents its own mechanism, and every one of those mechanisms is opaque to anything other than the interpreter's own import machinery at run time. The result is a long tail of broken behavior in type checkers, linters, test runners, and IDEs, and a growing reliance on reverse-engineered implementation details of individual build backends.
This issue distills the problem from astral-sh/ty#475 (see also python/mypy#13392, DetachHead/basedpyright#731), where the conclusion was that the ecosystem needs a standard for this and that no static analyzer can solve it on its own. I'd like to consider it more broadly than static analysis: the same mechanism, if supported by the import system itself, would remove the need for proxy modules and .pth tricks in the first place.
The need
Two capabilities, which should be two views of a single declaration:
- Forward: a name (module or package) is importable, but its content lives somewhere other than a path entry on
sys.path. - Reverse: given a file path, determine the fully-qualified name under which it is (or would be) imported.
The reverse direction is what is missing everywhere today, and it is what type checkers, linters (e.g. Ruff's N999, which flags a package under development as not matching its module name), test collectors (pytest deriving a module name from a collected file), coverage tools, and IDEs all need.
Beyond "look over there too"
The basic need is for a name to be discoverable in another location. But a mechanism limited to "add this directory to the search path" is insufficient for real layouts; redirection must also be able to point a name at a location that does not itself contain that name:
-
A single name exposed from a directory of many. A project may want
mypkgto resolve to/src/mypkgwithout exposing every other sibling in/srcas importable. Adding/srctosys.path(what a plain.pthline does, and what several backends do for editable installs) leaks siblings, shadows unrelated distributions, and makes the resulting import namespace depend on unrelated directory contents. -
A name whose target directory has a different — possibly non-importable — name. In the Coherent "essential" layout, the repo is the package: the checkout directory for
coherent.buildis namedcoherent.buildand contains__init__.pydirectly. The import name iscoherent.build(a namespace packagecoherentcontainingbuild), but there is nocoherent/directory anywhere, andcoherent.buildis not a legal identifier for a directory on the path. A checkout ofzope.interfacelaid out this way has the same shape. In other systems the directory simply doesn't match the name at all (my-project/providingmy_project, or a build directory providing a differently-named extension package). No path-entry-based mechanism can express this; only a name→location mapping can.
How this is solved today, and why each approach falls short
1. Path entries in .pth files
hatchling and others write a .pth file containing the project's source directory. Static tools can read these, so this is the case that mostly works today.
Limitations:
- Exposes every sibling of the target directory, not the declared name (case 1 above).
- Cannot express a name that differs from the directory name (case 2 above).
- The reverse mapping is only an inference: a tool must guess that a file under that directory corresponds to a dotted name derived from its relative path, which is exactly the assumption that breaks for namespace packages and non-matching layouts.
2. import lines in .pth files installing a meta path finder
setuptools' default editable mode writes __editable__.<dist>.pth containing an import line that installs __editable___<dist>_finder.py, whose MAPPING and NAMESPACES dicts hold the real name→path mapping.
Limitations:
- Requires executing Python to learn the mapping, so every static analyzer is blind to it — the direct cause of astral-sh/ty#475 and its siblings.
- The mapping exists in a precise, machine-readable form, but in a private module whose shape is an implementation detail. Tools are now weighing whether to parse it anyway (astral-sh/ty#475 (comment)), which is bad for them and for setuptools, which loses the ability to change it.
- With PEP 829 deprecating
importlines in.pthfiles, this technique is on its way out regardless, and backends relying on it will need somewhere to go.
3. Proxy modules
coherent.build installs a real module into site-packages at the imported name whose body sets __path__ to the checkout and execs the target's __init__.py (backend.py).
Limitations:
- The redirection is only discoverable by executing the proxy.
- The reverse mapping is unavailable: nothing in the checkout says what name it is exposed under. This breaks
ty, Ruff'sN999, and pytest's module-name derivation for Coherent projects. - Tracebacks,
__file__, and tooling that round-trips a file back to a module all see the seams.
4. Ad-hoc redirection outside packaging
The same need appears well outside editable installs: vendored/bundled dependencies aliased under another name, compatibility shims that map an old name to a new location, test layouts that want one directory importable under a specific name, and in-tree builds that expose generated artifacts under the source name. Each of these is solved today with a sys.path hack, a meta path finder, or a stub module — none of them declarative, none of them reversible.
The common thread
Every one of these is a one-way, tool-specific, execution-dependent encoding of what is fundamentally a small, static, declarative fact: the name X lives at path P. Each build backend rolls its own because there is nothing to adopt; each consuming tool rolls its own detection because there is nothing to read.
What a solution should provide
- R1. A declarative, static, machine-readable statement that name
Nresolves to pathP, discoverable without executing arbitrary code. - R2.
Pneed not be (and often is not) a directory whose basename equals the last component ofN; and declaringNmust not implicitly expose siblings ofP. - R3. Package redirections are inherited: declaring
mypkg -> /src/mypkgcoversmypkg.sub.modwithout enumerating every module. A file-by-file mapping is not acceptable ergonomically or for a working tree that changes. - R4. Resolvable in both directions: name → path for the import system, and path → name for static tooling.
- R5. Honored by CPython's own import system, so a redirect is the mechanism rather than metadata describing a separately-implemented hack. That eliminates proxy modules and finder-installing
.pthlines entirely, and means the runtime and the static view cannot disagree. - R6. Support for namespace packages, including a redirect that contributes a portion of a namespace (
zope.interface -> /repos/zope.interface) without materializingzope/. - R7. Introspectable at run time (something like
importlibexposing the active redirections), so that debuggers, test runners, and error messages can explain what happened.
Prototype: # import redirect comments in .pth files
As an early experiment, coherent.build 0.41 emits alongside the proxy a <name>-redirects.pth containing only comments:
# import redirect coherent.build -> /home/jaraco/code/coherent/coherent.build
# import redirect editable_1 -> /users/other/py/editable_1
# import redirect editable_mod -> /users/other/py/editable_mod.py
Comments are ignored by site, so the file is inert today, and a static tool that understands the convention gets exactly the name→path mapping it needs, from a location it already scans.
I offer this only as an existence proof that the data is small and that emitting it is easy — not as a proposed format. Smuggling a second syntax inside the comments of a file format that already has two meanings is dirty, and a .pth file is a poor host for structured data (as noted in the ty thread, .pth syntax is essentially "a path, or an import line," and the latter is being deprecated by PEP 829).
A dedicated, structured file seems clearly preferable — for example <name>.redirect in a site directory (parallel to how PEP 829 introduced .start alongside .pth), or an entry in the distribution's .dist-info, in JSON or TOML:
# mydist.redirect
[redirects]
"coherent.build" = "/home/jaraco/code/coherent/coherent.build"
"editable_1" = "/users/other/py/editable_1"
"editable_mod" = "/users/other/py/editable_mod.py"
Open questions on placement and format:
- Site directory file vs.
.dist-infometadata. A site-directory file is visible to a bare interpreter with no packaging metadata reader;.dist-infoties the redirect to the distribution that owns it and to uninstallation. Possibly both, with one canonical form. - Relative vs. absolute paths, and what relative is relative to.
- Precedence: against regular
sys.pathentries, against other redirects, and within a namespace portion. - Whether a redirect target may itself be redirected, and whether cycles need detection.
- Whether redirects may point at zip/other importer-backed locations or only the filesystem.
- Where the spec lives: the file format and packaging integration are a packaging standard (PEP), while honoring it in the import system is CPython's part. I expect this issue to feed a discussion on discuss.python.org rather than to be resolved here alone, but the import-system half is what makes the rest coherent, so I'd like to start the conversation here.
Value
- Editable installs become statically analyzable for every tool, without anyone parsing anyone else's private finder module.
- Build backends stop inventing (and maintaining) bespoke import machinery, and setuptools gets a path off of
.pthimportlines ahead of PEP 829's removal. - Layouts where the directory name doesn't match the import name become first-class instead of requiring a proxy module.
- Tools that must map a file back to a module name (type checkers, Ruff
N999, pytest collection, coverage, tracebacks, IDE "go to definition") get an answer instead of a heuristic.
Has this already been discussed elsewhere?
This is a minor feature, which does not need previous discussion elsewhere
Links to previous discussion of this feature:
- https://github.com/astral-sh/ty/issues/475
- https://github.com/python/mypy/issues/13392
- https://github.com/python/cpython/issues/92054 (implementing PEP 420 namespace packages via import hooks for the PEP 660 use case)
- https://peps.python.org/pep-0829/
贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
调研方向
首先阅读 issue 中描述的导入系统和 site 行为,以及 PEP 829 和链接的讨论。未解决的问题涉及文件放置、优先级、路径、命名空间、循环和运行时内省,因此这项工作属于设计,而不是局部实现。要完成这项工作,需要一个达成共识的声明式机制,以及明确定义的 CPython 集成。
由索引模型根据 Issue 内容生成。
评估
- 技术栈
- python
- 领域
- backend-api-design, devtools
- Issue 类型
- 功能
- 难度
- 5/5
- 预计耗时
- 一周以上
- 活跃度
- 活跃
- 描述清晰度
- 需要澄清
- 新手友好度
- 25/100