Skip to content

Commit 88a6799

Browse files
committed
[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.
1 parent 941be52 commit 88a6799

File tree

3 files changed

+447
-0
lines changed

3 files changed

+447
-0
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// This is a Dot representation of a flow diagram meant to describe Python's
2+
// import resolution rules. This particular diagram starts with one particular
3+
// search path and one particular module name. (Typical import resolution
4+
// implementation will try multiple search paths.)
5+
//
6+
// This diagram also assumes that stubs are allowed. The ty implementation
7+
// of import resolution makes this a configurable parameter, but it should
8+
// be straight-forward to adapt this flow diagram to one where no stubs
9+
// are allowed. (i.e., Remove `.pyi` checks and remove the `package-stubs`
10+
// handling.)
11+
//
12+
// This flow diagram exists to act as a sort of specification. At the time
13+
// of writing (2025-07-29), it was written to capture the implementation of
14+
// resolving a *particular* module name. We wanted to add another code path for
15+
// *listing* available module names. Since code reuse is somewhat difficult
16+
// between these two access patterns, I wrote this flow diagram as a way of 1)
17+
// learning how module resolution works and 2) to provide a "source of truth"
18+
// that we can compare implementations to.
19+
//
20+
// To convert this file into an actual image, you'll need the `dot` program
21+
// (which is typically part of a `graphviz` package in a Linux distro):
22+
//
23+
// dot -Tsvg import-resolution-diagram.dot > import-resolution-diagram.svg
24+
//
25+
// And then view it in a web browser (or some other svg viewer):
26+
//
27+
// firefox ./import-resolution-diagram.svg
28+
//
29+
// [Dot]: https://graphviz.org/doc/info/lang.html
30+
31+
digraph python_import_resolution {
32+
labelloc="t";
33+
label=<
34+
<b>Python import resolution flow diagram for a single module name in a single "search path"</b>
35+
<br/>(assumes that the module name is valid and that stubs are allowed)
36+
>;
37+
38+
// These are the final affirmative states we can end up in. A
39+
// module is a regular `foo.py` file module. A package is a
40+
// directory containing an `__init__.py`. A namespace package is a
41+
// directory that does *not* contain an `__init__.py`.
42+
module [label="Single-file Module",peripheries=2];
43+
package [label="Package",peripheries=2];
44+
namespace_package [label="Namespace Package",peripheries=2];
45+
not_found [label="Module Not Found",peripheries=2];
46+
47+
// The final states are wrapped in a subgraph with invisible edges
48+
// to convince GraphViz to give a more human digestible rendering.
49+
// Without this, the nodes are scattered every which way and the
50+
// flow diagram is pretty hard to follow. This encourages (but does
51+
// not guarantee) GraphViz to put these nodes "close" together, and
52+
// this generally gets us something grokable.
53+
subgraph final {
54+
rank = same;
55+
module -> package -> namespace_package -> not_found [style=invis];
56+
}
57+
58+
START [label=<<b>START</b>>];
59+
START -> non_shadowable;
60+
61+
non_shadowable [label=<
62+
Is the search path not the standard library and<br/>
63+
the module name is `types` or some other built-in?
64+
>];
65+
non_shadowable -> not_found [label="Yes"];
66+
non_shadowable -> stub_package_check [label="No"];
67+
68+
stub_package_check [label=<
69+
Is the search path in the standard library?
70+
>];
71+
stub_package_check -> stub_package_set [label="No"];
72+
stub_package_check -> determine_parent_kind [label="Yes"];
73+
74+
stub_package_set [label=<
75+
Set `module_name` to `{top-package}-stubs.{rest}`
76+
>];
77+
stub_package_set -> determine_parent_kind;
78+
79+
determine_parent_kind [label=<
80+
Does every parent package of `module_name`<br/>
81+
correspond to a directory that contains an<br/>
82+
`__init__.py` or an `__init__.pyi`?
83+
>];
84+
determine_parent_kind -> maybe_package [label="Yes"];
85+
determine_parent_kind -> namespace_parent1 [label="No"];
86+
87+
namespace_parent1 [label=<
88+
Is the direct parent package<br/>
89+
a directory that contains<br/>
90+
an `__init__.py` or `__init__.pyi`?
91+
>];
92+
namespace_parent1 -> bail [label="Yes"];
93+
namespace_parent1 -> namespace_parent2 [label="No"];
94+
95+
namespace_parent2 [label=<
96+
Does the direct parent package<br/>
97+
have a sibling file with the same<br/>
98+
basename and a `py` or `pyi` extension?<br/>
99+
>];
100+
namespace_parent2 -> bail [label="Yes"];
101+
namespace_parent2 -> namespace_parent3 [label="No"];
102+
103+
namespace_parent3 [label=<
104+
Is every parent above the direct<br/>
105+
parent package a normal package or<br/>
106+
otherwise satisfy the previous two<br/>
107+
namespace package requirements?
108+
>];
109+
namespace_parent3 -> bail [label="No"];
110+
namespace_parent3 -> maybe_package [label="Yes"];
111+
112+
maybe_package [label=<
113+
After replacing `.` with `/` in module name,<br/>
114+
does `{path}/__init__.py` or `{path}/__init__.pyi` exist?
115+
>];
116+
maybe_package -> package [label="Yes"];
117+
maybe_package -> maybe_module [label="No"];
118+
119+
maybe_module [label=<
120+
Does `{path}.py` or `{path}.pyi` exist?
121+
>];
122+
maybe_module -> module [label="Yes"];
123+
maybe_module -> maybe_namespace [label="No"];
124+
125+
maybe_namespace [label=<
126+
Is `{path}` a directory?
127+
>];
128+
maybe_namespace -> namespace_package [label="Yes"];
129+
maybe_namespace -> bail [label="No"];
130+
131+
bail [label=<
132+
Is `module_name` set to a stub package candidate?
133+
>];
134+
bail -> not_found [label="No"];
135+
bail -> retry [label="Yes"];
136+
137+
retry [label=<
138+
Reset `module_name` to original
139+
>];
140+
retry -> determine_parent_kind;
141+
}

0 commit comments

Comments
 (0)