Skip to content

Commit 0067bdc

Browse files
committed
Find imported modules from sys.modules instead of tracking.
We can iterate over sys.modules on demand and extract the filesystem paths to each imported module. This feels less hacky than trying to use the import mechanisms to track imports as they occur.
1 parent 36f8404 commit 0067bdc

1 file changed

Lines changed: 24 additions & 47 deletions

File tree

pgfutils.py

Lines changed: 24 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,8 @@
1414

1515
import ast
1616
import builtins
17-
from collections.abc import Mapping, Sequence
17+
from collections.abc import Mapping
1818
import functools
19-
import importlib.abc
20-
from importlib.machinery import ModuleSpec
21-
import importlib.util
2219
import inspect
2320
import io
2421
import os
@@ -27,25 +24,38 @@
2724
import string
2825
import sys
2926
import tomllib
30-
import types
3127
from typing import Any, Callable, Literal, TypedDict, get_args, get_origin
3228
import warnings
3329

3430

35-
class Tracker(importlib.abc.MetaPathFinder):
31+
class Tracker:
3632
"""Track files that are read, written or imported.
3733
3834
This can then be used for generating dependency lists. Note that the tracker does
39-
not filter the files; that is up to the user.
40-
41-
Note that any imports that occur prior to the tracker being installed will not be
42-
tracked. Any code which loads a file opener prior to it being wrapped for tracking
43-
will also avoid tracking.
35+
not filter the files; that is up to the user. Functions must be wrapped before use.
4436
4537
"""
4638

47-
# Files that have imported.
48-
imported: set[Path]
39+
@property
40+
def imported(self) -> set[Path]:
41+
"""Absolute paths to modules that were imported.
42+
43+
This iterates over all modules cached in `sys.modules` and finds the paths to
44+
each module. Any modules which don't have a filesystem path specified will be
45+
ignored.
46+
47+
"""
48+
paths = set()
49+
50+
for mod in sys.modules.values():
51+
if mod.__spec__ is None or mod.__spec__.origin is None:
52+
continue
53+
54+
path = Path(mod.__spec__.origin)
55+
if path.exists() and path.is_file():
56+
paths.add(path.resolve())
57+
58+
return paths
4959

5060
# Files that were opened in a read mode.
5161
read: set[Path]
@@ -61,12 +71,9 @@ class Tracker(importlib.abc.MetaPathFinder):
6171

6272
def __init__(self):
6373
super().__init__()
64-
self._avoid_recursion = set()
65-
self.imported = set()
6674
self.read = set()
6775
self.explicit_dependencies = set()
6876
self.written = set()
69-
sys.meta_path.insert(0, self)
7077

7178
def add_dependencies(self, *args: os.PathLike[str] | str):
7279
"""Add a file dependency for the current figure.
@@ -87,36 +94,6 @@ def add_dependencies(self, *args: os.PathLike[str] | str):
8794
for fn in args:
8895
self.explicit_dependencies.add(Path(fn).resolve())
8996

90-
def find_spec(
91-
self,
92-
fullname: str,
93-
path: Sequence[str] | None,
94-
target: types.ModuleType | None = None,
95-
) -> ModuleSpec | None:
96-
# According to PEP451, this is mostly intended for a reload. I can't see a way
97-
# (without calling importlib._bootstrap._find_spec, which should not be imported
98-
# according to the note at the top of the module) to pass this information on.
99-
# Hence, lets skip tracking in this case.
100-
if target is not None: # pragma: no cover
101-
return None
102-
103-
# We use importlib to find the actual spec, so we need to avoid recursing when
104-
# this finder is called again.
105-
if fullname in self._avoid_recursion:
106-
return None
107-
108-
# Find the spec.
109-
self._avoid_recursion.add(fullname)
110-
spec = importlib.util._find_spec_from_path(fullname, path)
111-
self._avoid_recursion.remove(fullname)
112-
113-
# If it has an origin in one of our tracked dirs, log it.
114-
if spec is not None and spec.origin is not None and spec.origin != "built-in":
115-
self.imported.add(spec.origin)
116-
117-
# And return the result.
118-
return spec
119-
12097
def wrap_file_opener[**P, R](self, opener: Callable[P, R]) -> Callable[P, R]:
12198
"""Wrap a function which opens files for tracking.
12299
@@ -178,7 +155,7 @@ def wrapper(*args, **kwargs):
178155
io.open = tracker.wrap_file_opener(io.open)
179156

180157
# Now we can import Matplotlib. We couldn't do so earlier as it brings in NumPy, which
181-
# in turn caches references to io.open() and so we prevent us reliably tracking data
158+
# in turn caches references to io.open() and so would prevent us reliably tracking data
182159
# files it opens.
183160
import matplotlib # noqa: E402
184161

0 commit comments

Comments
 (0)