ruff/crates/ty_python_semantic/src/module_resolver/import-resolution-diagram.dot

210 lines
8.3 KiB
Plaintext

// This is a Dot representation of a flow diagram meant to describe Python's
// import resolution rules. This particular diagram starts with one particular
// search path and one particular module name. (Typical import resolution
// implementation will try multiple search paths.)
//
// This diagram also assumes that stubs are allowed. The ty implementation
// of import resolution makes this a configurable parameter, but it should
// be straight-forward to adapt this flow diagram to one where no stubs
// are allowed. (i.e., Remove `.pyi` checks and remove the `package-stubs`
// handling.)
//
// This flow diagram exists to act as a sort of specification. At the time
// of writing (2025-07-29), it was written to capture the implementation of
// resolving a *particular* module name. We wanted to add another code path for
// *listing* available module names. Since code reuse is somewhat difficult
// between these two access patterns, I wrote this flow diagram as a way of 1)
// learning how module resolution works and 2) to provide a "source of truth"
// that we can compare implementations to.
//
// To convert this file into an actual image, you'll need the `dot` program
// (which is typically part of a `graphviz` package in a Linux distro):
//
// dot -Tsvg import-resolution-diagram.dot > import-resolution-diagram.svg
//
// And then view it in a web browser (or some other svg viewer):
//
// firefox ./import-resolution-diagram.svg
//
// [Dot]: https://graphviz.org/doc/info/lang.html
digraph python_import_resolution {
labelloc="t";
label=<
<b>Python import resolution flow diagram for a single module name in a single "search path"</b>
<br/>(assumes that the module name is valid and that stubs are allowed)
>;
// These are the final affirmative states we can end up in. A
// module is a regular `foo.py` file module. A package is a
// directory containing an `__init__.py`. A namespace package is a
// directory that does *not* contain an `__init__.py`.
module [label="Single-file Module",peripheries=2];
package [label="Package",peripheries=2];
namespace_package [label="Namespace Package",peripheries=2];
not_found [label="Module Not Found",peripheries=2];
// The final states are wrapped in a subgraph with invisible edges
// to convince GraphViz to give a more human digestible rendering.
// Without this, the nodes are scattered every which way and the
// flow diagram is pretty hard to follow. This encourages (but does
// not guarantee) GraphViz to put these nodes "close" together, and
// this generally gets us something grokable.
subgraph final {
rank = same;
module -> package -> namespace_package -> not_found [style=invis];
}
START [label=<<b>START</b>>];
START -> non_shadowable;
non_shadowable [label=<
Is the search path not the standard library and<br/>
the module name is `types` or some other built-in?
>];
non_shadowable -> not_found [label="Yes"];
non_shadowable -> stub_package_check [label="No"];
stub_package_check [label=<
Is the search path in the standard library?
>];
stub_package_check -> stub_package_set [label="No"];
stub_package_check -> determine_parent_kind [label="Yes"];
stub_package_set [label=<
Set `module_name` to `{top-package}-stubs.{rest}`
>];
stub_package_set -> determine_parent_kind;
determine_parent_kind [label=<
Does every parent package of `module_name`<br/>
correspond to a directory that contains an<br/>
`__init__.py` or an `__init__.pyi`?
>];
determine_parent_kind -> regular_parent_std [label="Yes"];
determine_parent_kind -> namespace_parent_regular_check [label="No"];
regular_parent_std [label=<
Does the search path correspond to the standard library?
>];
regular_parent_std -> resolved_parent_package [label="No"];
regular_parent_std -> regular_parent_typeshed_check [label="Yes"];
regular_parent_typeshed_check [label=<
Does every parent package of<br/>
`module_name` exist on the configured<br/>
Python version according to <br/>
typeshed's VERSIONS file?
>];
regular_parent_typeshed_check -> resolved_parent_package [label="Yes"];
regular_parent_typeshed_check -> bail [label="No"];
namespace_parent_regular_check [label=<
Is the direct parent package<br/>
a directory that contains<br/>
an `__init__.py` or `__init__.pyi`?
>];
namespace_parent_regular_check -> bail [label="Yes"];
namespace_parent_regular_check -> namespace_parent_std [label="No"];
namespace_parent_std [label=<
Does the search path correspond to the standard library?
>];
namespace_parent_std -> namespace_parent_module_check [label="No"];
namespace_parent_std -> namespace_parent_typeshed_check [label="Yes"];
namespace_parent_typeshed_check [label=<
Does the direct parent package of<br/>
`module_name` exist on the configured<br/>
Python version according to <br/>
typeshed's VERSIONS file?
>];
namespace_parent_typeshed_check -> namespace_parent_module_check [label="Yes"];
namespace_parent_typeshed_check -> bail [label="No"];
namespace_parent_module_check [label=<
Does the direct parent package<br/>
have a sibling file with the same<br/>
basename and a `py` or `pyi` extension?<br/>
>];
namespace_parent_module_check -> bail [label="Yes"];
namespace_parent_module_check -> namespace_parent_above [label="No"];
namespace_parent_above [label=<
Is every parent above the direct<br/>
parent package a normal package or<br/>
otherwise satisfy the previous two<br/>
namespace package requirements?
>];
namespace_parent_above -> bail [label="No"];
namespace_parent_above -> resolved_parent_package [label="Yes"];
resolved_parent_package [label=<
After replacing `.` with `/` in module name,<br/>
does `{path}/__init__.py` or `{path}/__init__.pyi` exist?
>];
resolved_parent_package -> package [label="Yes"];
resolved_parent_package -> maybe_module [label="No"];
maybe_module [label=<
Does `{path}.py` or `{path}.pyi` exist?
>];
maybe_module -> maybe_module_std [label="Yes"];
maybe_module -> maybe_namespace [label="No"];
maybe_module_std [label=<
Does the search path correspond to the standard library?
>];
maybe_module_std -> module [label="No"];
maybe_module_std -> maybe_module_typeshed_check [label="Yes"];
maybe_module_typeshed_check [label=<
Does the module corresponding to `{path}`<br/>
exist on the configured<br/>
Python version according to <br/>
typeshed's VERSIONS file?
>];
maybe_module_typeshed_check -> module [label="Yes"];
maybe_module_typeshed_check -> maybe_namespace [label="No"];
// N.B. In the actual implementation, this check is
// only done when the search path *isn't* the standard
// library. That's because typeshed doesn't use namespace
// packages, so this (and the typeshed VERSIONS check)
// can all be skipped as an optimization. But the flow
// diagram still represents this because this could in
// theory change and optimizations really should be the
// domain of the implementation, not the spec.
maybe_namespace [label=<
Is `{path}` a directory?
>];
maybe_namespace -> maybe_namespace_std [label="Yes"];
maybe_namespace -> bail [label="No"];
maybe_namespace_std [label=<
Does the search path correspond to the standard library?
>];
maybe_namespace_std -> namespace_package [label="No"];
maybe_namespace_std -> maybe_namespace_typeshed_check [label="Yes"];
maybe_namespace_typeshed_check [label=<
Does the module corresponding to `{path}`<br/>
exist on the configured<br/>
Python version according to <br/>
typeshed's VERSIONS file?
>];
maybe_namespace_typeshed_check -> namespace_package [label="Yes"];
maybe_namespace_typeshed_check -> bail [label="No"];
bail [label=<
Is `module_name` set to a stub package candidate?
>];
bail -> not_found [label="No"];
bail -> retry [label="Yes"];
retry [label=<
Reset `module_name` to original
>];
retry -> determine_parent_kind;
}