Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

gh-146228: Better fork support in cached FastPath#146231

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
jaraco merged 4 commits intopython:mainfromjaraco:gh-146228/forking-fastpath
Mar 20, 2026
Merged
Show file tree
Hide file tree
Changes fromall commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 21 additions & 4 deletionsLib/importlib/metadata/__init__.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -31,7 +31,7 @@

from .import_meta
from ._collectionsimportFreezableDefaultDict,Pair
from ._functoolsimportmethod_cache,pass_none
from ._functoolsimportmethod_cache,noop,pass_none,passthrough
from ._itertoolsimportalways_iterable,bucket,unique_everseen
from ._metaimportPackageMetadata,SimplePath
from ._typingimportmd_none
Expand DownExpand Up@@ -783,6 +783,20 @@ def find_distributions(self, context=Context()) -> Iterable[Distribution]:
"""


@passthrough
def_clear_after_fork(cached):
"""Ensure ``func`` clears cached state after ``fork`` when supported.
``FastPath`` caches zip-backed ``pathlib.Path`` objects that retain a
reference to the parent's open ``ZipFile`` handle. Re-using a cached
instance in a forked child can therefore resurrect invalid file pointers
and trigger ``BadZipFile``/``OSError`` failures (python/importlib_metadata#520).
Registering ``cache_clear`` with ``os.register_at_fork`` keeps each process
on its own cache.
"""
getattr(os,'register_at_fork',noop)(after_in_child=cached.cache_clear)


classFastPath:
"""
Micro-optimized class for searching a root for children.
Expand All@@ -799,7 +813,8 @@ class FastPath:
True
"""

@functools.lru_cache()# type: ignore[misc]
@_clear_after_fork# type: ignore[misc]
@functools.lru_cache()
def__new__(cls,root):
returnsuper().__new__(cls)

Expand DownExpand Up@@ -925,10 +940,12 @@ def __init__(self, name: str | None):
defnormalize(name):
"""
PEP 503 normalization plus dashes as underscores.
Specifically avoids ``re.sub`` as prescribed for performance
benefits (see python/cpython#143658).
"""
# Much faster than re.sub, and even faster than str.translate
value=name.lower().replace("-","_").replace(".","_")
# Condense repeats (faster than regex)
# Condense repeats
while"__"invalue:
value=value.replace("__","_")
returnvalue
Expand Down
3 changes: 2 additions & 1 deletionLib/importlib/metadata/_adapters.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -9,7 +9,8 @@
classRawPolicy(email.policy.EmailPolicy):
deffold(self,name,value):
folded=self.linesep.join(
textwrap.indent(value,prefix=' '*8,predicate=lambdaline:True)
textwrap
.indent(value,prefix=' '*8,predicate=lambdaline:True)
.lstrip()
.splitlines()
)
Expand Down
32 changes: 32 additions & 0 deletionsLib/importlib/metadata/_functools.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
import functools
import types
from collections.abc import Callable
from typing import TypeVar


# from jaraco.functools 3.3
Expand DownExpand Up@@ -102,3 +104,33 @@ def wrapper(param, *args, **kwargs):
return func(param, *args, **kwargs)

return wrapper


# From jaraco.functools 4.4
def noop(*args, **kwargs):
"""
A no-operation function that does nothing.

>>> noop(1, 2, three=3)
"""


_T = TypeVar('_T')


# From jaraco.functools 4.4
def passthrough(func: Callable[..., object]) -> Callable[[_T], _T]:
"""
Wrap the function to always return the first parameter.

>>> passthrough(print)('3')
3
'3'
"""

@functools.wraps(func)
def wrapper(first: _T, *args, **kwargs) -> _T:
func(first, *args, **kwargs)
return first

return wrapper # type: ignore[return-value]
8 changes: 1 addition & 7 deletionsLib/test/test_importlib/metadata/fixtures.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,7 @@
importshutil
importsys
importtextwrap
fromimportlibimportresources

fromtest.supportimportimport_helper
fromtest.supportimportos_helper
Expand All@@ -14,11 +15,6 @@
from .import_path
from ._pathimportFilesSpec

ifsys.version_info>= (3,9):
fromimportlibimportresources
else:
importimportlib_resourcesasresources


@contextlib.contextmanager
deftmp_path():
Expand DownExpand Up@@ -374,8 +370,6 @@ def setUp(self):
# Add self.zip_name to the front of sys.path.
self.resources=contextlib.ExitStack()
self.addCleanup(self.resources.close)
# workaround for #138313
self.addCleanup(lambda:None)


defparameterize(*args_set):
Expand Down
58 changes: 28 additions & 30 deletionsLib/test/test_importlib/metadata/test_api.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -317,33 +317,31 @@ def test_invalidate_cache(self):


classPreparedTests(unittest.TestCase):
deftest_normalize(self):
tests= [
# Simple
("sample","sample"),
# Mixed case
("Sample","sample"),
("SAMPLE","sample"),
("SaMpLe","sample"),
# Separator conversions
("sample-pkg","sample_pkg"),
("sample.pkg","sample_pkg"),
("sample_pkg","sample_pkg"),
# Multiple separators
("sample---pkg","sample_pkg"),
("sample___pkg","sample_pkg"),
("sample...pkg","sample_pkg"),
# Mixed separators
("sample-._pkg","sample_pkg"),
("sample_.-pkg","sample_pkg"),
# Complex
("Sample__Pkg-name.foo","sample_pkg_name_foo"),
("Sample__Pkg.name__foo","sample_pkg_name_foo"),
# Uppercase with separators
("SAMPLE-PKG","sample_pkg"),
("Sample.Pkg","sample_pkg"),
("SAMPLE_PKG","sample_pkg"),
]
forname,expectedintests:
withself.subTest(name=name):
self.assertEqual(Prepared.normalize(name),expected)
@fixtures.parameterize(
# Simple
dict(input='sample',expected='sample'),
# Mixed case
dict(input='Sample',expected='sample'),
dict(input='SAMPLE',expected='sample'),
dict(input='SaMpLe',expected='sample'),
# Separator conversions
dict(input='sample-pkg',expected='sample_pkg'),
dict(input='sample.pkg',expected='sample_pkg'),
dict(input='sample_pkg',expected='sample_pkg'),
# Multiple separators
dict(input='sample---pkg',expected='sample_pkg'),
dict(input='sample___pkg',expected='sample_pkg'),
dict(input='sample...pkg',expected='sample_pkg'),
# Mixed separators
dict(input='sample-._pkg',expected='sample_pkg'),
dict(input='sample_.-pkg',expected='sample_pkg'),
# Complex
dict(input='Sample__Pkg-name.foo',expected='sample_pkg_name_foo'),
dict(input='Sample__Pkg.name__foo',expected='sample_pkg_name_foo'),
# Uppercase with separators
dict(input='SAMPLE-PKG',expected='sample_pkg'),
dict(input='Sample.Pkg',expected='sample_pkg'),
dict(input='SAMPLE_PKG',expected='sample_pkg'),
)
deftest_normalize(self,input,expected):
self.assertEqual(Prepared.normalize(input),expected)
2 changes: 1 addition & 1 deletionLib/test/test_importlib/metadata/test_main.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -2,12 +2,12 @@
importpickle
importre
importunittest
fromtest.supportimportos_helper

try:
importpyfakefs.fake_filesystem_unittestasffs
exceptImportError:
from .stubsimportfake_filesystem_unittestasffs
fromtest.supportimportos_helper

fromimportlib.metadataimport (
Distribution,
Expand Down
37 changes: 37 additions & 0 deletionsLib/test/test_importlib/metadata/test_zip.py
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
importmultiprocessing
importos
importsys
importunittest

fromtest.supportimportwarnings_helper

fromimportlib.metadataimport (
FastPath,
PackageNotFoundError,
distribution,
distributions,
Expand DownExpand Up@@ -47,6 +52,38 @@ def test_one_distribution(self):
dists=list(distributions(path=sys.path[:1]))
assertlen(dists)==1

@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
@unittest.skipUnless(
hasattr(os,'register_at_fork')
and'fork'inmultiprocessing.get_all_start_methods(),
'requires fork-based multiprocessing support',
)
deftest_fastpath_cache_cleared_in_forked_child(self):
zip_path=sys.path[0]

FastPath(zip_path)
assertFastPath.__new__.cache_info().currsize>=1

ctx=multiprocessing.get_context('fork')
parent_conn,child_conn=ctx.Pipe()

defchild(conn,root):
try:
before=FastPath.__new__.cache_info().currsize
FastPath(root)
after=FastPath.__new__.cache_info().currsize
conn.send((before,after))
finally:
conn.close()

proc=ctx.Process(target=child,args=(child_conn,zip_path))
proc.start()
child_conn.close()
cache_sizes=parent_conn.recv()
proc.join()

self.assertEqual(cache_sizes, (0,1))


classTestEgg(TestZip):
defsetUp(self):
Expand Down
View file
Open in desktop
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
Cached FastPath objects in importlib.metadata are now cleared on fork,
avoiding broken references to zip files during fork.
Loading

[8]ページ先頭

©2009-2026 Movatter.jp