pFad - Phone/Frame/Anonymizer/Declutterfier! Saves Data!


--- a PPN by Garber Painting Akron. With Image Size Reduction included!

URL: http://github.com/python/cpython/commit/3e0fec7e07a71bdeeab7554e980110fbc47763b9

8faa60c69660fa.css" /> Sync with importlib_metadata 6.5 (GH-103584) · python/cpython@3e0fec7 · GitHub
Skip to content

Commit 3e0fec7

Browse files
authored
Sync with importlib_metadata 6.5 (GH-103584)
1 parent 5c00a62 commit 3e0fec7

10 files changed

Lines changed: 531 additions & 72 deletions

File tree

Doc/library/importlib.metadata.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,10 @@ Python module or `Import Package <https://packaging.python.org/en/latest/glossar
308308
>>> packages_distributions()
309309
{'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...}
310310

311+
Some editable installs, `do not supply top-level names
312+
<https://github.com/pypa/packaging-problems/issues/609>`_, and thus this
313+
function is not reliable with such installs.
314+
311315
.. versionadded:: 3.10
312316

313317
.. _distributions:

Lib/importlib/metadata/__init__.py

Lines changed: 84 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@
1212
import functools
1313
import itertools
1414
import posixpath
15+
import contextlib
1516
import collections
17+
import inspect
1618

1719
from . import _adapters, _meta
1820
from ._collections import FreezableDefaultDict, Pair
@@ -24,7 +26,7 @@
2426
from importlib import import_module
2527
from importlib.abc import MetaPathFinder
2628
from itertools import starmap
27-
from typing import List, Mapping, Optional
29+
from typing import List, Mapping, Optional, cast
2830

2931

3032
__all__ = [
@@ -341,11 +343,30 @@ def __repr__(self):
341343
return f'<FileHash mode: {self.mode} value: {self.value}>'
342344

343345

344-
class Distribution:
346+
class DeprecatedNonAbstract:
347+
def __new__(cls, *args, **kwargs):
348+
all_names = {
349+
name for subclass in inspect.getmro(cls) for name in vars(subclass)
350+
}
351+
abstract = {
352+
name
353+
for name in all_names
354+
if getattr(getattr(cls, name), '__isabstractmethod__', False)
355+
}
356+
if abstract:
357+
warnings.warn(
358+
f"Unimplemented abstract methods {abstract}",
359+
DeprecationWarning,
360+
stacklevel=2,
361+
)
362+
return super().__new__(cls)
363+
364+
365+
class Distribution(DeprecatedNonAbstract):
345366
"""A Python distribution package."""
346367

347368
@abc.abstractmethod
348-
def read_text(self, filename):
369+
def read_text(self, filename) -> Optional[str]:
349370
"""Attempt to load metadata file given by the name.
350371
351372
:param filename: The name of the file in the distribution info.
@@ -419,14 +440,15 @@ def metadata(self) -> _meta.PackageMetadata:
419440
The returned object will have keys that name the various bits of
420441
metadata. See PEP 566 for details.
421442
"""
422-
text = (
443+
opt_text = (
423444
self.read_text('METADATA')
424445
or self.read_text('PKG-INFO')
425446
# This last clause is here to support old egg-info files. Its
426447
# effect is to just end up using the PathDistribution's self._path
427448
# (which points to the egg-info file) attribute unchanged.
428449
or self.read_text('')
429450
)
451+
text = cast(str, opt_text)
430452
return _adapters.Message(email.message_from_string(text))
431453

432454
@property
@@ -455,8 +477,8 @@ def files(self):
455477
:return: List of PackagePath for this distribution or None
456478
457479
Result is `None` if the metadata file that enumerates files
458-
(i.e. RECORD for dist-info or SOURCES.txt for egg-info) is
459-
missing.
480+
(i.e. RECORD for dist-info, or installed-files.txt or
481+
SOURCES.txt for egg-info) is missing.
460482
Result may be empty if the metadata exists but is empty.
461483
"""
462484

@@ -469,9 +491,19 @@ def make_file(name, hash=None, size_str=None):
469491

470492
@pass_none
471493
def make_files(lines):
472-
return list(starmap(make_file, csv.reader(lines)))
494+
return starmap(make_file, csv.reader(lines))
473495

474-
return make_files(self._read_files_distinfo() or self._read_files_egginfo())
496+
@pass_none
497+
def skip_missing_files(package_paths):
498+
return list(filter(lambda path: path.locate().exists(), package_paths))
499+
500+
return skip_missing_files(
501+
make_files(
502+
self._read_files_distinfo()
503+
or self._read_files_egginfo_installed()
504+
or self._read_files_egginfo_sources()
505+
)
506+
)
475507

476508
def _read_files_distinfo(self):
477509
"""
@@ -480,10 +512,43 @@ def _read_files_distinfo(self):
480512
text = self.read_text('RECORD')
481513
return text and text.splitlines()
482514

483-
def _read_files_egginfo(self):
515+
def _read_files_egginfo_installed(self):
516+
"""
517+
Read installed-files.txt and return lines in a similar
518+
CSV-parsable format as RECORD: each file must be placed
519+
relative to the site-packages directory, and must also be
520+
quoted (since file names can contain literal commas).
521+
522+
This file is written when the package is installed by pip,
523+
but it might not be written for other installation methods.
524+
Hence, even if we can assume that this file is accurate
525+
when it exists, we cannot assume that it always exists.
484526
"""
485-
SOURCES.txt might contain literal commas, so wrap each line
486-
in quotes.
527+
text = self.read_text('installed-files.txt')
528+
# We need to prepend the .egg-info/ subdir to the lines in this file.
529+
# But this subdir is only available in the PathDistribution's self._path
530+
# which is not easily accessible from this base class...
531+
subdir = getattr(self, '_path', None)
532+
if not text or not subdir:
533+
return
534+
with contextlib.suppress(Exception):
535+
ret = [
536+
str((subdir / line).resolve().relative_to(self.locate_file('')))
537+
for line in text.splitlines()
538+
]
539+
return map('"{}"'.format, ret)
540+
541+
def _read_files_egginfo_sources(self):
542+
"""
543+
Read SOURCES.txt and return lines in a similar CSV-parsable
544+
format as RECORD: each file name must be quoted (since it
545+
might contain literal commas).
546+
547+
Note that SOURCES.txt is not a reliable source for what
548+
files are installed by a package. This file is generated
549+
for a source archive, and the files that are present
550+
there (e.g. setup.py) may not correctly reflect the files
551+
that are present after the package has been installed.
487552
"""
488553
text = self.read_text('SOURCES.txt')
489554
return text and map('"{}"'.format, text.splitlines())
@@ -886,8 +951,13 @@ def _top_level_declared(dist):
886951

887952

888953
def _top_level_inferred(dist):
889-
return {
890-
f.parts[0] if len(f.parts) > 1 else f.with_suffix('').name
954+
opt_names = {
955+
f.parts[0] if len(f.parts) > 1 else inspect.getmodulename(f)
891956
for f in always_iterable(dist.files)
892-
if f.suffix == ".py"
893957
}
958+
959+
@pass_none
960+
def importable_name(name):
961+
return '.' not in name
962+
963+
return filter(importable_name, opt_names)

Lib/importlib/metadata/_adapters.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,21 @@
1+
import functools
2+
import warnings
13
import re
24
import textwrap
35
import email.message
46

57
from ._text import FoldedCase
68

79

10+
# Do not remove prior to 2024-01-01 or Python 3.14
11+
_warn = functools.partial(
12+
warnings.warn,
13+
"Implicit None on return values is deprecated and will raise KeyErrors.",
14+
DeprecationWarning,
15+
stacklevel=2,
16+
)
17+
18+
819
class Message(email.message.Message):
920
multiple_use_keys = set(
1021
map(
@@ -39,6 +50,16 @@ def __init__(self, *args, **kwargs):
3950
def __iter__(self):
4051
return super().__iter__()
4152

53+
def __getitem__(self, item):
54+
"""
55+
Warn users that a ``KeyError`` can be expected when a
56+
mising key is supplied. Ref python/importlib_metadata#371.
57+
"""
58+
res = super().__getitem__(item)
59+
if res is None:
60+
_warn()
61+
return res
62+
4263
def _repair_headers(self):
4364
def redent(value):
4465
"Correct for RFC822 indentation"

Lib/importlib/metadata/_meta.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
from typing import Any, Dict, Iterator, List, Protocol, TypeVar, Union
1+
from typing import Protocol
2+
from typing import Any, Dict, Iterator, List, Optional, TypeVar, Union, overload
23

34

45
_T = TypeVar("_T")
@@ -17,7 +18,21 @@ def __getitem__(self, key: str) -> str:
1718
def __iter__(self) -> Iterator[str]:
1819
... # pragma: no cover
1920

20-
def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
21+
@overload
22+
def get(self, name: str, failobj: None = None) -> Optional[str]:
23+
... # pragma: no cover
24+
25+
@overload
26+
def get(self, name: str, failobj: _T) -> Union[str, _T]:
27+
... # pragma: no cover
28+
29+
# overload per python/importlib_metadata#435
30+
@overload
31+
def get_all(self, name: str, failobj: None = None) -> Optional[List[Any]]:
32+
... # pragma: no cover
33+
34+
@overload
35+
def get_all(self, name: str, failobj: _T) -> Union[List[Any], _T]:
2136
"""
2237
Return all values associated with a possibly multi-valued key.
2338
"""
@@ -29,18 +44,19 @@ def json(self) -> Dict[str, Union[str, List[str]]]:
2944
"""
3045

3146

32-
class SimplePath(Protocol):
47+
class SimplePath(Protocol[_T]):
3348
"""
3449
A minimal subset of pathlib.Path required by PathDistribution.
3550
"""
3651

37-
def joinpath(self) -> 'SimplePath':
52+
def joinpath(self) -> _T:
3853
... # pragma: no cover
3954

40-
def __truediv__(self) -> 'SimplePath':
55+
def __truediv__(self, other: Union[str, _T]) -> _T:
4156
... # pragma: no cover
4257

43-
def parent(self) -> 'SimplePath':
58+
@property
59+
def parent(self) -> _T:
4460
... # pragma: no cover
4561

4662
def read_text(self) -> str:
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import contextlib
2+
3+
4+
# from jaraco.context 4.3
5+
class suppress(contextlib.suppress, contextlib.ContextDecorator):
6+
"""
7+
A version of contextlib.suppress with decorator support.
8+
9+
>>> @suppress(KeyError)
10+
... def key_error():
11+
... {}['']
12+
>>> key_error()
13+
"""

Lib/test/test_importlib/_path.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
# from jaraco.path 3.5
2+
3+
import functools
4+
import pathlib
5+
from typing import Dict, Union
6+
7+
try:
8+
from typing import Protocol, runtime_checkable
9+
except ImportError: # pragma: no cover
10+
# Python 3.7
11+
from typing_extensions import Protocol, runtime_checkable # type: ignore
12+
13+
14+
FilesSpec = Dict[str, Union[str, bytes, 'FilesSpec']] # type: ignore
15+
16+
17+
@runtime_checkable
18+
class TreeMaker(Protocol):
19+
def __truediv__(self, *args, **kwargs):
20+
... # pragma: no cover
21+
22+
def mkdir(self, **kwargs):
23+
... # pragma: no cover
24+
25+
def write_text(self, content, **kwargs):
26+
... # pragma: no cover
27+
28+
def write_bytes(self, content):
29+
... # pragma: no cover
30+
31+
32+
def _ensure_tree_maker(obj: Union[str, TreeMaker]) -> TreeMaker:
33+
return obj if isinstance(obj, TreeMaker) else pathlib.Path(obj) # type: ignore
34+
35+
36+
def build(
37+
spec: FilesSpec,
38+
prefix: Union[str, TreeMaker] = pathlib.Path(), # type: ignore
39+
):
40+
"""
41+
Build a set of files/directories, as described by the spec.
42+
43+
Each key represents a pathname, and the value represents
44+
the content. Content may be a nested directory.
45+
46+
>>> spec = {
47+
... 'README.txt': "A README file",
48+
... "foo": {
49+
... "__init__.py": "",
50+
... "bar": {
51+
... "__init__.py": "",
52+
... },
53+
... "baz.py": "# Some code",
54+
... }
55+
... }
56+
>>> target = getfixture('tmp_path')
57+
>>> build(spec, target)
58+
>>> target.joinpath('foo/baz.py').read_text(encoding='utf-8')
59+
'# Some code'
60+
"""
61+
for name, contents in spec.items():
62+
create(contents, _ensure_tree_maker(prefix) / name)
63+
64+
65+
@functools.singledispatch
66+
def create(content: Union[str, bytes, FilesSpec], path):
67+
path.mkdir(exist_ok=True)
68+
build(content, prefix=path) # type: ignore
69+
70+
71+
@create.register
72+
def _(content: bytes, path):
73+
path.write_bytes(content)
74+
75+
76+
@create.register
77+
def _(content: str, path):
78+
path.write_text(content, encoding='utf-8')
79+
80+
81+
@create.register
82+
def _(content: str, path):
83+
path.write_text(content, encoding='utf-8')
84+
85+
86+
class Recording:
87+
"""
88+
A TreeMaker object that records everything that would be written.
89+
90+
>>> r = Recording()
91+
>>> build({'foo': {'foo1.txt': 'yes'}, 'bar.txt': 'abc'}, r)
92+
>>> r.record
93+
['foo/foo1.txt', 'bar.txt']
94+
"""
95+
96+
def __init__(self, loc=pathlib.PurePosixPath(), record=None):
97+
self.loc = loc
98+
self.record = record if record is not None else []
99+
100+
def __truediv__(self, other):
101+
return Recording(self.loc / other, self.record)
102+
103+
def write_text(self, content, **kwargs):
104+
self.record.append(str(self.loc))
105+
106+
write_bytes = write_text
107+
108+
def mkdir(self, **kwargs):
109+
return

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad © 2024 Your Company Name. All rights reserved.





Check this box to remove all script contents from the fetched content.



Check this box to remove all images from the fetched content.


Check this box to remove all CSS styles from the fetched content.


Check this box to keep images inefficiently compressed and original size.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy