From 88a679945ce405c5de8ed819aadad97e65abd2e0 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Tue, 29 Jul 2025 13:18:16 -0400 Subject: [PATCH] [ty] Add flow diagram for import resolution The diagram is written in the Dot language, which can be converted to SVG (or any other image) by GraphViz. I thought it was a good idea to write this down in preparation for adding routines that list modules. Code reuse is likely to be difficult and I wanted to be sure I understood how it worked. --- .../import-resolution-diagram.dot | 141 +++++++++ .../import-resolution-diagram.svg | 296 ++++++++++++++++++ .../src/module_resolver/resolver.rs | 10 + 3 files changed, 447 insertions(+) create mode 100644 crates/ty_python_semantic/src/module_resolver/import-resolution-diagram.dot create mode 100644 crates/ty_python_semantic/src/module_resolver/import-resolution-diagram.svg diff --git a/crates/ty_python_semantic/src/module_resolver/import-resolution-diagram.dot b/crates/ty_python_semantic/src/module_resolver/import-resolution-diagram.dot new file mode 100644 index 0000000000..04dd3d5332 --- /dev/null +++ b/crates/ty_python_semantic/src/module_resolver/import-resolution-diagram.dot @@ -0,0 +1,141 @@ +// 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=< + Python import resolution flow diagram for a single module name in a single "search path" +
(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=<START>]; + START -> non_shadowable; + + non_shadowable [label=< + Is the search path not the standard library and
+ 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`
+ correspond to a directory that contains an
+ `__init__.py` or an `__init__.pyi`? + >]; + determine_parent_kind -> maybe_package [label="Yes"]; + determine_parent_kind -> namespace_parent1 [label="No"]; + + namespace_parent1 [label=< + Is the direct parent package
+ a directory that contains
+ an `__init__.py` or `__init__.pyi`? + >]; + namespace_parent1 -> bail [label="Yes"]; + namespace_parent1 -> namespace_parent2 [label="No"]; + + namespace_parent2 [label=< + Does the direct parent package
+ have a sibling file with the same
+ basename and a `py` or `pyi` extension?
+ >]; + namespace_parent2 -> bail [label="Yes"]; + namespace_parent2 -> namespace_parent3 [label="No"]; + + namespace_parent3 [label=< + Is every parent above the direct
+ parent package a normal package or
+ otherwise satisfy the previous two
+ namespace package requirements? + >]; + namespace_parent3 -> bail [label="No"]; + namespace_parent3 -> maybe_package [label="Yes"]; + + maybe_package [label=< + After replacing `.` with `/` in module name,
+ does `{path}/__init__.py` or `{path}/__init__.pyi` exist? + >]; + maybe_package -> package [label="Yes"]; + maybe_package -> maybe_module [label="No"]; + + maybe_module [label=< + Does `{path}.py` or `{path}.pyi` exist? + >]; + maybe_module -> module [label="Yes"]; + maybe_module -> maybe_namespace [label="No"]; + + maybe_namespace [label=< + Is `{path}` a directory? + >]; + maybe_namespace -> namespace_package [label="Yes"]; + maybe_namespace -> 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; +} diff --git a/crates/ty_python_semantic/src/module_resolver/import-resolution-diagram.svg b/crates/ty_python_semantic/src/module_resolver/import-resolution-diagram.svg new file mode 100644 index 0000000000..7584e5134f --- /dev/null +++ b/crates/ty_python_semantic/src/module_resolver/import-resolution-diagram.svg @@ -0,0 +1,296 @@ + + + + + + +python_import_resolution + +         +Python import resolution flow diagram for a single module name in a single "search path" +         +(assumes that the module name is valid and that stubs are allowed)     + + +module + + +Single-file Module + + + +package + + +Package + + + + +namespace_package + + +Namespace Package + + + + +not_found + + +Module Not Found + + + + +START + +START + + + +non_shadowable + +        Is the search path not the standard library and +        the module name is `types` or some other built-in?     + + + +START->non_shadowable + + + + + +non_shadowable->not_found + + +Yes + + + +stub_package_check + +        Is the search path in the standard library?     + + + +non_shadowable->stub_package_check + + +No + + + +stub_package_set + +        Set `module_name` to `{top-package}-stubs.{rest}`     + + + +stub_package_check->stub_package_set + + +No + + + +determine_parent_kind + +        Does every parent package of `module_name` +        correspond to a directory that contains an +        `__init__.py` or an `__init__.pyi`?     + + + +stub_package_check->determine_parent_kind + + +Yes + + + +stub_package_set->determine_parent_kind + + + + + +maybe_package + +        After replacing `.` with `/` in module name, +        does `{path}/__init__.py` or `{path}/__init__.pyi` exist?     + + + +determine_parent_kind->maybe_package + + +Yes + + + +namespace_parent1 + +        Is the direct parent package +        a directory that contains +        an `__init__.py` or `__init__.pyi`?     + + + +determine_parent_kind->namespace_parent1 + + +No + + + +maybe_package->package + + +Yes + + + +maybe_module + +        Does `{path}.py` or `{path}.pyi` exist?     + + + +maybe_package->maybe_module + + +No + + + +bail + +        Is `module_name` set to a stub package candidate?     + + + +namespace_parent1->bail + + +Yes + + + +namespace_parent2 + +        Does the direct parent package +        have a sibling file with the same +        basename and a `py` or `pyi` extension? +     + + + +namespace_parent1->namespace_parent2 + + +No + + + +bail->not_found + + +No + + + +retry + +        Reset `module_name` to original     + + + +bail->retry + + +Yes + + + +namespace_parent2->bail + + +Yes + + + +namespace_parent3 + +        Is every parent above the direct +        parent package a normal package or +        otherwise satisfy the previous two +        namespace package requirements?     + + + +namespace_parent2->namespace_parent3 + + +No + + + +namespace_parent3->maybe_package + + +Yes + + + +namespace_parent3->bail + + +No + + + +maybe_module->module + + +Yes + + + +maybe_namespace + +        Is `{path}` a directory?     + + + +maybe_module->maybe_namespace + + +No + + + +maybe_namespace->namespace_package + + +Yes + + + +maybe_namespace->bail + + +No + + + +retry->determine_parent_kind + + + + + diff --git a/crates/ty_python_semantic/src/module_resolver/resolver.rs b/crates/ty_python_semantic/src/module_resolver/resolver.rs index a70f48ba23..a29af76c2c 100644 --- a/crates/ty_python_semantic/src/module_resolver/resolver.rs +++ b/crates/ty_python_semantic/src/module_resolver/resolver.rs @@ -1,3 +1,13 @@ +/*! +This module principally provides two routines for resolving a particular module +name to a `Module`: [`resolve_module`] and [`resolve_real_module`]. You'll +usually want the former, unless you're certain you want to forbid stubs, in +which case, use the latter. + +For implementors, see `import-resolution-diagram.svg` for a flow diagram that +specifies ty's implementation of Python's import resolution algorithm. +*/ + use std::borrow::Cow; use std::fmt; use std::iter::FusedIterator;