-
-
Notifications
You must be signed in to change notification settings - Fork 31.9k
With #116680, test_importlib
started leaking on refleaks
#116731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
Example of leak:
Originally posted by @vstinner in #116680 (comment) |
That's interesting - the ref leak failure is in a importlib.metadata test, whereas the PR only made changes to the importlib.resources tests. My guess right now is that the importlib.resources module leak was masking the importlib.metadata ref leaks. If that's the case, it should be possible to replicate the failure prior to the PR merge. |
Indeed, the ref leak existed prior to the PR:
|
Disabling the logic under test, the leaks go away, so it appears the leak is in the call to
|
The leaks don't emerge when calling cpython main @ git diff
diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py
index 33c6e85ee9..643f17726e 100644
--- a/Lib/test/test_importlib/test_metadata_api.py
+++ b/Lib/test/test_importlib/test_metadata_api.py
@@ -219,8 +219,8 @@ def test_requires_egg_info_empty(self):
},
self.site_dir.joinpath('egginfo_pkg.egg-info'),
)
- deps = requires('egginfo-pkg')
- assert deps == []
+ Distribution.discover(name='egginfo-pkg')
+ dist, = Distribution.discover(name='egginfo-pkg')
def test_requires_dist_info(self):
deps = requires('distinfo-pkg') So it's the evaluating of the generator returned by |
Digging deeper, I can trigger the leak with:
|
Just to confirm, this bug exists at least as far back as Python 3.11, and likely has been around for even longer.
|
The issue goes away when removing the lru_cache on FastPath: diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py
index c612fbefee..7d3b8a7733 100644
--- a/Lib/importlib/metadata/__init__.py
+++ b/Lib/importlib/metadata/__init__.py
@@ -665,7 +665,6 @@ class FastPath:
['...']
"""
- @functools.lru_cache() # type: ignore
def __new__(cls, root):
return super().__new__(cls) |
It seems that because each iteration of the test creates a new temporary site dir and because those are cached in
|
This patch also works around the ref leak, at least for tests in APITests: cpython main @ git diff
diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py
index 33c6e85ee9..b30240e291 100644
--- a/Lib/test/test_importlib/test_metadata_api.py
+++ b/Lib/test/test_importlib/test_metadata_api.py
@@ -8,6 +8,7 @@
from . import fixtures
from importlib.metadata import (
Distribution,
+ FastPath,
PackageNotFoundError,
distribution,
entry_points,
@@ -37,6 +38,9 @@ class APITests(
):
version_pattern = r'\d+\.\d+(\.\d)?'
+ def tearDown(self):
+ FastPath.__new__.cache_clear()
+
def test_retrieves_version_of_self(self):
pkg_version = version('egginfo-pkg')
assert isinstance(pkg_version, str) |
These ref leaks are by design - this cache is there to improve performance. I'm not quite sure how to handle this situation robustly. I know there's the It does look like the following patch also addresses the issue: cpython main @ git diff
diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py
index 46ddceed07..b26be8583d 100644
--- a/Lib/importlib/_bootstrap_external.py
+++ b/Lib/importlib/_bootstrap_external.py
@@ -1470,6 +1470,9 @@ def invalidate_caches():
# https://bugs.python.org/issue45703
_NamespacePath._epoch += 1
+ from importlib.metadata import MetadataPathFinder
+ MetadataPathFinder.invalidate_caches()
+
@staticmethod
def _path_hooks(path):
"""Search sys.path_hooks for a finder for 'path'."""
diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py
index c612fbefee..41c2a4a608 100644
--- a/Lib/importlib/metadata/__init__.py
+++ b/Lib/importlib/metadata/__init__.py
@@ -797,6 +797,7 @@ def _search_paths(cls, name, paths):
path.search(prepared) for path in map(FastPath, paths)
)
+ @classmethod
def invalidate_caches(cls) -> None:
FastPath.__new__.cache_clear()
diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py
index 33c6e85ee9..e2936303ac 100644
--- a/Lib/test/test_importlib/test_metadata_api.py
+++ b/Lib/test/test_importlib/test_metadata_api.py
@@ -37,6 +37,9 @@ class APITests(
):
version_pattern = r'\d+\.\d+(\.\d)?'
+ def tearDown(self):
+ importlib.invalidate_caches()
+
def test_retrieves_version_of_self(self):
pkg_version = version('egginfo-pkg')
assert isinstance(pkg_version, str) That patch feels like it's closer to the right thing to do. It first exposes the cache invalidation in the PathFinder, so the caches will be invalidated as a matter of course. That seems worthwhile no matter what. I'm less confident about using the |
There's another ref leak in diff --git a/Lib/test/test_importlib/resources/test_files.py b/Lib/test/test_importlib/resources/test_files.py
index 26c8b04e44..90834fcab2 100644
--- a/Lib/test/test_importlib/resources/test_files.py
+++ b/Lib/test/test_importlib/resources/test_files.py
@@ -99,7 +99,7 @@ def test_implicit_files(self):
'__init__.py': textwrap.dedent(
"""
import importlib.resources as res
- val = res.files().joinpath('res.txt').read_text(encoding='utf-8')
+ val = 'resources are the best'
"""
),
'res.txt': 'resources are the best', |
Distilling that issue, it looks like it boils down to calling diff --git a/Lib/test/test_importlib/resources/test_files.py b/Lib/test/test_importlib/resources/test_files.py
index 26c8b04e44..335f0b6718 100644
--- a/Lib/test/test_importlib/resources/test_files.py
+++ b/Lib/test/test_importlib/resources/test_files.py
@@ -95,18 +95,15 @@ def test_implicit_files(self):
Without any parameter, files() will infer the location as the caller.
"""
spec = {
- 'somepkg': {
- '__init__.py': textwrap.dedent(
- """
- import importlib.resources as res
- val = res.files().joinpath('res.txt').read_text(encoding='utf-8')
- """
- ),
- 'res.txt': 'resources are the best',
- },
+ 'somepkg.py': textwrap.dedent(
+ """
+ import inspect
+ inspect.stack()
+ """
+ ),
}
_path.build(spec, self.site_dir)
- assert importlib.import_module('somepkg').val == 'resources are the best'
+ importlib.import_module('somepkg')
if __name__ == '__main__': |
In c6d794f, I devised a workaround, clearing some global state after running the test. This change and the other allows the refleaks test to pass for |
Thank you for hunting this down! There's a place in regrtest for clearing caches. I'd add it there, and be very narrow -- when calling all of |
…clear_caches (GH-116805) Co-authored-by: Jason R. Coombs <[email protected]>
…es in clear_caches (pythonGH-116805) (cherry picked from commit bae6579) Co-authored-by: Petr Viktorin <[email protected]> Co-authored-by: Jason R. Coombs <[email protected]>
…hes in clear_caches (GH-116805) (GH-116820) gh-116731: libregrtest: Clear inspect & importlib.metadata caches in clear_caches (GH-116805) (cherry picked from commit bae6579) Co-authored-by: Petr Viktorin <[email protected]> Co-authored-by: Jason R. Coombs <[email protected]>
…es in clear_caches (pythonGH-116805) Co-authored-by: Jason R. Coombs <[email protected]>
…es in clear_caches (pythonGH-116805) Co-authored-by: Jason R. Coombs <[email protected]>
…es in clear_caches (pythonGH-116805) Co-authored-by: Jason R. Coombs <[email protected]>
Ironically, since this was merged,
test_importlib
started leaking on refleaks buildbots.Originally posted by @encukou in #116680 (comment)
Linked PRs
The text was updated successfully, but these errors were encountered: