|
| 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