Compare commits

..

5 Commits

Author SHA1 Message Date
Raymond Hettinger c8a7b8fa1b
bpo-42781: Document the mechanics of cached_property from a user viewpoint (GH-24031) 2020-12-31 17:05:58 -08:00
Jason R. Coombs b5711c940f
bpo-37193: Remove thread objects which finished process its request (GH-23127)
This reverts commit aca67da4fe.
2020-12-31 20:19:30 +00:00
Tao He 3631d6deab
Fixes a typo in importlib.metadata. (#23921)
Signed-off-by: Tao He <sighingnow@gmail.com>
2020-12-31 11:37:53 -08:00
Jason R. Coombs a6fd0f414c
bpo-42163, bpo-42189, bpo-42659: Support uname_tuple._replace (for all but processor) (#23010)
* Add test capturing missed expectation with uname_result._replace.

* bpo-42163: Override uname_result._make to allow uname_result._replace to work (for everything but 'processor'.

* Replace hard-coded length with one derived from the definition.

* Add test capturing missed expectation with copy/deepcopy on namedtuple (bpo-42189).

* bpo-42189: Exclude processor parameter when constructing uname_result.

* In _make, rely on __new__ to strip processor.

* Add blurb.

* iter is not necessary here.

* Rely on num_fields in __new__

* Add test for slices on uname

* Add test for copy and pickle.

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>

* import pickle

* Fix equality test after pickling.

* Simply rely on __reduce__ for pickling.

Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
2020-12-31 14:08:03 -05:00
Jason R. Coombs dfdca85dfa
bpo-42382: In importlib.metadata, `EntryPoint` objects now expose `dist` (#23758)
* bpo-42382: In importlib.metadata, `EntryPoint` objects now expose a `.dist` object referencing the `Distribution` when constructed from a `Distribution`.

Also, sync importlib_metadata 3.3:

- Add support for package discovery under package normalization rules.
- The object returned by `metadata()` now has a formally-defined protocol called `PackageMetadata` with declared support for the `.get_all()` method.

* Add blurb

* Remove latent footnote.
2020-12-31 12:56:43 -05:00
14 changed files with 416 additions and 174 deletions

View File

@ -62,16 +62,26 @@ The :mod:`functools` module defines the following functions:
Example::
class DataSet:
def __init__(self, sequence_of_numbers):
self._data = sequence_of_numbers
self._data = tuple(sequence_of_numbers)
@cached_property
def stdev(self):
return statistics.stdev(self._data)
@cached_property
def variance(self):
return statistics.variance(self._data)
The mechanics of :func:`cached_property` are somewhat different from
:func:`property`. A regular property blocks attribute writes unless a
setter is defined. In contrast, a *cached_property* allows writes.
The *cached_property* decorator only runs on lookups and only when an
attribute of the same name doesn't exist. When it does run, the
*cached_property* writes to the attribute with the same name. Subsequent
attribute reads and writes take precedence over the *cached_property*
method and it works like a normal attribute.
The cached value can be cleared by deleting the attribute. This
allows the *cached_property* method to run again.
Note, this decorator interferes with the operation of :pep:`412`
key-sharing dictionaries. This means that instance dictionaries

View File

@ -115,8 +115,9 @@ Every distribution includes some metadata, which you can extract using the
>>> wheel_metadata = metadata('wheel') # doctest: +SKIP
The keys of the returned data structure [#f1]_ name the metadata keywords, and
their values are returned unparsed from the distribution metadata::
The keys of the returned data structure, a ``PackageMetadata``,
name the metadata keywords, and
the values are returned unparsed from the distribution metadata::
>>> wheel_metadata['Requires-Python'] # doctest: +SKIP
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
@ -206,9 +207,9 @@ Thus, an alternative way to get the version number is through the
There are all kinds of additional metadata available on the ``Distribution``
instance::
>>> d.metadata['Requires-Python'] # doctest: +SKIP
>>> dist.metadata['Requires-Python'] # doctest: +SKIP
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'
>>> d.metadata['License'] # doctest: +SKIP
>>> dist.metadata['License'] # doctest: +SKIP
'MIT'
The full set of available metadata is not described here. See :pep:`566`
@ -259,9 +260,3 @@ a custom finder, return instances of this derived ``Distribution`` in the
.. rubric:: Footnotes
.. [#f1] Technically, the returned distribution metadata object is an
:class:`email.message.EmailMessage`
instance, but this is an implementation detail, and not part of the
stable API. You should only use dictionary-like methods and syntax
to access the metadata contents.

View File

@ -1,4 +1,3 @@
import io
import os
import re
import abc
@ -18,6 +17,7 @@ from contextlib import suppress
from importlib import import_module
from importlib.abc import MetaPathFinder
from itertools import starmap
from typing import Any, List, Optional, Protocol, TypeVar, Union
__all__ = [
@ -31,7 +31,7 @@ __all__ = [
'metadata',
'requires',
'version',
]
]
class PackageNotFoundError(ModuleNotFoundError):
@ -43,7 +43,7 @@ class PackageNotFoundError(ModuleNotFoundError):
@property
def name(self):
name, = self.args
(name,) = self.args
return name
@ -77,6 +77,8 @@ class EntryPoint(
following the attr, and following any extras.
"""
dist: Optional['Distribution'] = None
def load(self):
"""Load the entry point from its definition. If only a module
is indicated by the value, return that module. Otherwise,
@ -104,23 +106,27 @@ class EntryPoint(
@classmethod
def _from_config(cls, config):
return [
return (
cls(name, value, group)
for group in config.sections()
for name, value in config.items(group)
]
)
@classmethod
def _from_text(cls, text):
config = ConfigParser(delimiters='=')
# case sensitive: https://stackoverflow.com/q/1611799/812183
config.optionxform = str
try:
config.read_string(text)
except AttributeError: # pragma: nocover
# Python 2 has no read_string
config.readfp(io.StringIO(text))
return EntryPoint._from_config(config)
return cls._from_config(config)
@classmethod
def _from_text_for(cls, text, dist):
return (ep._for(dist) for ep in cls._from_text(text))
def _for(self, dist):
self.dist = dist
return self
def __iter__(self):
"""
@ -159,6 +165,25 @@ class FileHash:
return '<FileHash mode: {} value: {}>'.format(self.mode, self.value)
_T = TypeVar("_T")
class PackageMetadata(Protocol):
def __len__(self) -> int:
... # pragma: no cover
def __contains__(self, item: str) -> bool:
... # pragma: no cover
def __getitem__(self, key: str) -> str:
... # pragma: no cover
def get_all(self, name: str, failobj: _T = ...) -> Union[List[Any], _T]:
"""
Return all values associated with a possibly multi-valued key.
"""
class Distribution:
"""A Python distribution package."""
@ -210,8 +235,7 @@ class Distribution:
raise ValueError("cannot accept context and kwargs")
context = context or DistributionFinder.Context(**kwargs)
return itertools.chain.from_iterable(
resolver(context)
for resolver in cls._discover_resolvers()
resolver(context) for resolver in cls._discover_resolvers()
)
@staticmethod
@ -227,14 +251,14 @@ class Distribution:
def _discover_resolvers():
"""Search the meta_path for resolvers."""
declared = (
getattr(finder, 'find_distributions', None)
for finder in sys.meta_path
getattr(finder, 'find_distributions', None) for finder in sys.meta_path
)
return filter(None, declared)
@classmethod
def _local(cls, root='.'):
from pep517 import build, meta
system = build.compat_system(root)
builder = functools.partial(
meta.build,
@ -244,7 +268,7 @@ class Distribution:
return PathDistribution(zipfile.Path(meta.build_as_zip(builder)))
@property
def metadata(self):
def metadata(self) -> PackageMetadata:
"""Return the parsed metadata for this Distribution.
The returned object will have keys that name the various bits of
@ -260,6 +284,11 @@ class Distribution:
)
return email.message_from_string(text)
@property
def name(self):
"""Return the 'Name' metadata for the distribution package."""
return self.metadata['Name']
@property
def version(self):
"""Return the 'Version' metadata for the distribution package."""
@ -267,7 +296,7 @@ class Distribution:
@property
def entry_points(self):
return EntryPoint._from_text(self.read_text('entry_points.txt'))
return list(EntryPoint._from_text_for(self.read_text('entry_points.txt'), self))
@property
def files(self):
@ -324,8 +353,9 @@ class Distribution:
section_pairs = cls._read_sections(source.splitlines())
sections = {
section: list(map(operator.itemgetter('line'), results))
for section, results in
itertools.groupby(section_pairs, operator.itemgetter('section'))
for section, results in itertools.groupby(
section_pairs, operator.itemgetter('section')
)
}
return cls._convert_egg_info_reqs_to_simple_reqs(sections)
@ -350,6 +380,7 @@ class Distribution:
requirement. This method converts the former to the
latter. See _test_deps_from_requires_text for an example.
"""
def make_condition(name):
return name and 'extra == "{name}"'.format(name=name)
@ -438,48 +469,69 @@ class FastPath:
names = zip_path.root.namelist()
self.joinpath = zip_path.joinpath
return dict.fromkeys(
child.split(posixpath.sep, 1)[0]
for child in names
)
def is_egg(self, search):
base = self.base
return (
base == search.versionless_egg_name
or base.startswith(search.prefix)
and base.endswith('.egg'))
return dict.fromkeys(child.split(posixpath.sep, 1)[0] for child in names)
def search(self, name):
for child in self.children():
n_low = child.lower()
if (n_low in name.exact_matches
or n_low.startswith(name.prefix)
and n_low.endswith(name.suffixes)
# legacy case:
or self.is_egg(name) and n_low == 'egg-info'):
yield self.joinpath(child)
return (
self.joinpath(child)
for child in self.children()
if name.matches(child, self.base)
)
class Prepared:
"""
A prepared search for metadata on a possibly-named package.
"""
normalized = ''
prefix = ''
normalized = None
suffixes = '.dist-info', '.egg-info'
exact_matches = [''][:0]
versionless_egg_name = ''
def __init__(self, name):
self.name = name
if name is None:
return
self.normalized = name.lower().replace('-', '_')
self.prefix = self.normalized + '-'
self.exact_matches = [
self.normalized + suffix for suffix in self.suffixes]
self.versionless_egg_name = self.normalized + '.egg'
self.normalized = self.normalize(name)
self.exact_matches = [self.normalized + suffix for suffix in self.suffixes]
@staticmethod
def normalize(name):
"""
PEP 503 normalization plus dashes as underscores.
"""
return re.sub(r"[-_.]+", "-", name).lower().replace('-', '_')
@staticmethod
def legacy_normalize(name):
"""
Normalize the package name as found in the convention in
older packaging tools versions and specs.
"""
return name.lower().replace('-', '_')
def matches(self, cand, base):
low = cand.lower()
pre, ext = os.path.splitext(low)
name, sep, rest = pre.partition('-')
return (
low in self.exact_matches
or ext in self.suffixes
and (not self.normalized or name.replace('.', '_') == self.normalized)
# legacy case:
or self.is_egg(base)
and low == 'egg-info'
)
def is_egg(self, base):
normalized = self.legacy_normalize(self.name or '')
prefix = normalized + '-' if normalized else ''
versionless_egg_name = normalized + '.egg' if self.name else ''
return (
base == versionless_egg_name
or base.startswith(prefix)
and base.endswith('.egg')
)
class MetadataPathFinder(DistributionFinder):
@ -500,8 +552,7 @@ class MetadataPathFinder(DistributionFinder):
def _search_paths(cls, name, paths):
"""Find metadata directories in paths heuristically."""
return itertools.chain.from_iterable(
path.search(Prepared(name))
for path in map(FastPath, paths)
path.search(Prepared(name)) for path in map(FastPath, paths)
)
@ -515,9 +566,15 @@ class PathDistribution(Distribution):
self._path = path
def read_text(self, filename):
with suppress(FileNotFoundError, IsADirectoryError, KeyError,
NotADirectoryError, PermissionError):
with suppress(
FileNotFoundError,
IsADirectoryError,
KeyError,
NotADirectoryError,
PermissionError,
):
return self._path.joinpath(filename).read_text(encoding='utf-8')
read_text.__doc__ = Distribution.read_text.__doc__
def locate_file(self, path):
@ -541,11 +598,11 @@ def distributions(**kwargs):
return Distribution.discover(**kwargs)
def metadata(distribution_name):
def metadata(distribution_name) -> PackageMetadata:
"""Get the metadata for the named package.
:param distribution_name: The name of the distribution package to query.
:return: An email.Message containing the parsed metadata.
:return: A PackageMetadata containing the parsed metadata.
"""
return Distribution.from_name(distribution_name).metadata
@ -565,15 +622,11 @@ def entry_points():
:return: EntryPoint objects for all installed packages.
"""
eps = itertools.chain.from_iterable(
dist.entry_points for dist in distributions())
eps = itertools.chain.from_iterable(dist.entry_points for dist in distributions())
by_group = operator.attrgetter('group')
ordered = sorted(eps, key=by_group)
grouped = itertools.groupby(ordered, by_group)
return {
group: tuple(eps)
for group, eps in grouped
}
return {group: tuple(eps) for group, eps in grouped}
def files(distribution_name):

View File

@ -769,7 +769,7 @@ class uname_result(
):
"""
A uname_result that's largely compatible with a
simple namedtuple except that 'platform' is
simple namedtuple except that 'processor' is
resolved late and cached to avoid calling "uname"
except when needed.
"""
@ -784,12 +784,25 @@ class uname_result(
(self.processor,)
)
@classmethod
def _make(cls, iterable):
# override factory to affect length check
num_fields = len(cls._fields)
result = cls.__new__(cls, *iterable)
if len(result) != num_fields + 1:
msg = f'Expected {num_fields} arguments, got {len(result)}'
raise TypeError(msg)
return result
def __getitem__(self, key):
return tuple(iter(self))[key]
return tuple(self)[key]
def __len__(self):
return len(tuple(iter(self)))
def __reduce__(self):
return uname_result, tuple(self)[:len(self._fields)]
_uname_cache = None

View File

@ -628,6 +628,39 @@ if hasattr(os, "fork"):
self.collect_children(blocking=self.block_on_close)
class _Threads(list):
"""
Joinable list of all non-daemon threads.
"""
def append(self, thread):
self.reap()
if thread.daemon:
return
super().append(thread)
def pop_all(self):
self[:], result = [], self[:]
return result
def join(self):
for thread in self.pop_all():
thread.join()
def reap(self):
self[:] = (thread for thread in self if thread.is_alive())
class _NoThreads:
"""
Degenerate version of _Threads.
"""
def append(self, thread):
pass
def join(self):
pass
class ThreadingMixIn:
"""Mix-in class to handle each request in a new thread."""
@ -636,9 +669,9 @@ class ThreadingMixIn:
daemon_threads = False
# If true, server_close() waits until all non-daemonic threads terminate.
block_on_close = True
# For non-daemonic threads, list of threading.Threading objects
# Threads object
# used by server_close() to wait for all threads completion.
_threads = None
_threads = _NoThreads()
def process_request_thread(self, request, client_address):
"""Same as in BaseServer but as a thread.
@ -655,23 +688,17 @@ class ThreadingMixIn:
def process_request(self, request, client_address):
"""Start a new thread to process the request."""
if self.block_on_close:
vars(self).setdefault('_threads', _Threads())
t = threading.Thread(target = self.process_request_thread,
args = (request, client_address))
t.daemon = self.daemon_threads
if not t.daemon and self.block_on_close:
if self._threads is None:
self._threads = []
self._threads.append(t)
t.start()
def server_close(self):
super().server_close()
if self.block_on_close:
threads = self._threads
self._threads = None
if threads:
for thread in threads:
thread.join()
self._threads.join()
if hasattr(os, "fork"):

View File

@ -7,6 +7,7 @@ import textwrap
import contextlib
from test.support.os_helper import FS_NONASCII
from typing import Dict, Union
@contextlib.contextmanager
@ -71,8 +72,13 @@ class OnSysPath(Fixtures):
self.fixtures.enter_context(self.add_sys_path(self.site_dir))
# Except for python/mypy#731, prefer to define
# FilesDef = Dict[str, Union['FilesDef', str]]
FilesDef = Dict[str, Union[Dict[str, Union[Dict[str, str], str]], str]]
class DistInfoPkg(OnSysPath, SiteDir):
files = {
files: FilesDef = {
"distinfo_pkg-1.0.0.dist-info": {
"METADATA": """
Name: distinfo-pkg
@ -86,7 +92,7 @@ class DistInfoPkg(OnSysPath, SiteDir):
[entries]
main = mod:main
ns:sub = mod:main
"""
""",
},
"mod.py": """
def main():
@ -99,6 +105,42 @@ class DistInfoPkg(OnSysPath, SiteDir):
build_files(DistInfoPkg.files, self.site_dir)
class DistInfoPkgWithDot(OnSysPath, SiteDir):
files: FilesDef = {
"pkg_dot-1.0.0.dist-info": {
"METADATA": """
Name: pkg.dot
Version: 1.0.0
""",
},
}
def setUp(self):
super(DistInfoPkgWithDot, self).setUp()
build_files(DistInfoPkgWithDot.files, self.site_dir)
class DistInfoPkgWithDotLegacy(OnSysPath, SiteDir):
files: FilesDef = {
"pkg.dot-1.0.0.dist-info": {
"METADATA": """
Name: pkg.dot
Version: 1.0.0
""",
},
"pkg.lot.egg-info": {
"METADATA": """
Name: pkg.lot
Version: 1.0.0
""",
},
}
def setUp(self):
super(DistInfoPkgWithDotLegacy, self).setUp()
build_files(DistInfoPkgWithDotLegacy.files, self.site_dir)
class DistInfoPkgOffPath(SiteDir):
def setUp(self):
super(DistInfoPkgOffPath, self).setUp()
@ -106,7 +148,7 @@ class DistInfoPkgOffPath(SiteDir):
class EggInfoPkg(OnSysPath, SiteDir):
files = {
files: FilesDef = {
"egginfo_pkg.egg-info": {
"PKG-INFO": """
Name: egginfo-pkg
@ -129,7 +171,7 @@ class EggInfoPkg(OnSysPath, SiteDir):
[test]
pytest
""",
"top_level.txt": "mod\n"
"top_level.txt": "mod\n",
},
"mod.py": """
def main():
@ -143,7 +185,7 @@ class EggInfoPkg(OnSysPath, SiteDir):
class EggInfoFile(OnSysPath, SiteDir):
files = {
files: FilesDef = {
"egginfo_file.egg-info": """
Metadata-Version: 1.0
Name: egginfo_file
@ -164,7 +206,7 @@ class EggInfoFile(OnSysPath, SiteDir):
class LocalPackage:
files = {
files: FilesDef = {
"setup.py": """
import setuptools
setuptools.setup(name="local-pkg", version="2.0.1")
@ -214,8 +256,7 @@ def build_files(file_defs, prefix=pathlib.Path()):
class FileBuilder:
def unicode_filename(self):
return FS_NONASCII or \
self.skip("File system does not support non-ascii.")
return FS_NONASCII or self.skip("File system does not support non-ascii.")
def DALS(str):

View File

@ -1,5 +1,3 @@
# coding: utf-8
import re
import json
import pickle
@ -14,10 +12,14 @@ except ImportError:
from . import fixtures
from importlib.metadata import (
Distribution, EntryPoint,
PackageNotFoundError, distributions,
entry_points, metadata, version,
)
Distribution,
EntryPoint,
PackageNotFoundError,
distributions,
entry_points,
metadata,
version,
)
class BasicTests(fixtures.DistInfoPkg, unittest.TestCase):
@ -74,8 +76,7 @@ class ImportTests(fixtures.DistInfoPkg, unittest.TestCase):
assert ep.load() is importlib.metadata
class NameNormalizationTests(
fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
class NameNormalizationTests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
@staticmethod
def pkg_with_dashes(site_dir):
"""
@ -144,11 +145,15 @@ class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
metadata_dir.mkdir()
metadata = metadata_dir / 'METADATA'
with metadata.open('w', encoding='utf-8') as fp:
fp.write(textwrap.dedent("""
fp.write(
textwrap.dedent(
"""
Name: portend
pôrˈtend
""").lstrip())
"""
).lstrip()
)
return 'portend'
def test_metadata_loads(self):
@ -162,24 +167,12 @@ class NonASCIITests(fixtures.OnSysPath, fixtures.SiteDir, unittest.TestCase):
assert meta.get_payload() == 'pôrˈtend\n'
class DiscoveryTests(fixtures.EggInfoPkg,
fixtures.DistInfoPkg,
unittest.TestCase):
class DiscoveryTests(fixtures.EggInfoPkg, fixtures.DistInfoPkg, unittest.TestCase):
def test_package_discovery(self):
dists = list(distributions())
assert all(
isinstance(dist, Distribution)
for dist in dists
)
assert any(
dist.metadata['Name'] == 'egginfo-pkg'
for dist in dists
)
assert any(
dist.metadata['Name'] == 'distinfo-pkg'
for dist in dists
)
assert all(isinstance(dist, Distribution) for dist in dists)
assert any(dist.metadata['Name'] == 'egginfo-pkg' for dist in dists)
assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
def test_invalid_usage(self):
with self.assertRaises(ValueError):
@ -265,10 +258,21 @@ class TestEntryPoints(unittest.TestCase):
def test_attr(self):
assert self.ep.attr is None
def test_sortable(self):
"""
EntryPoint objects are sortable, but result is undefined.
"""
sorted(
[
EntryPoint('b', 'val', 'group'),
EntryPoint('a', 'val', 'group'),
]
)
class FileSystem(
fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder,
unittest.TestCase):
fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder, unittest.TestCase
):
def test_unicode_dir_on_sys_path(self):
"""
Ensure a Unicode subdirectory of a directory on sys.path

View File

@ -2,20 +2,26 @@ import re
import textwrap
import unittest
from collections.abc import Iterator
from . import fixtures
from importlib.metadata import (
Distribution, PackageNotFoundError, distribution,
entry_points, files, metadata, requires, version,
)
Distribution,
PackageNotFoundError,
distribution,
entry_points,
files,
metadata,
requires,
version,
)
class APITests(
fixtures.EggInfoPkg,
fixtures.DistInfoPkg,
fixtures.DistInfoPkgWithDot,
fixtures.EggInfoFile,
unittest.TestCase):
unittest.TestCase,
):
version_pattern = r'\d+\.\d+(\.\d)?'
@ -33,15 +39,27 @@ class APITests(
with self.assertRaises(PackageNotFoundError):
distribution('does-not-exist')
def test_name_normalization(self):
names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
for name in names:
with self.subTest(name):
assert distribution(name).metadata['Name'] == 'pkg.dot'
def test_prefix_not_matched(self):
prefixes = 'p', 'pkg', 'pkg.'
for prefix in prefixes:
with self.subTest(prefix):
with self.assertRaises(PackageNotFoundError):
distribution(prefix)
def test_for_top_level(self):
self.assertEqual(
distribution('egginfo-pkg').read_text('top_level.txt').strip(),
'mod')
distribution('egginfo-pkg').read_text('top_level.txt').strip(), 'mod'
)
def test_read_text(self):
top_level = [
path for path in files('egginfo-pkg')
if path.name == 'top_level.txt'
path for path in files('egginfo-pkg') if path.name == 'top_level.txt'
][0]
self.assertEqual(top_level.read_text(), 'mod\n')
@ -51,6 +69,13 @@ class APITests(
self.assertEqual(ep.value, 'mod:main')
self.assertEqual(ep.extras, [])
def test_entry_points_distribution(self):
entries = dict(entry_points()['entries'])
for entry in ("main", "ns:sub"):
ep = entries[entry]
self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg'))
self.assertEqual(ep.dist.version, "1.0.0")
def test_metadata_for_this_package(self):
md = metadata('egginfo-pkg')
assert md['author'] == 'Steven Ma'
@ -75,13 +100,8 @@ class APITests(
def test_file_hash_repr(self):
assertRegex = self.assertRegex
util = [
p for p in files('distinfo-pkg')
if p.name == 'mod.py'
][0]
assertRegex(
repr(util.hash),
'<FileHash mode: sha256 value: .*>')
util = [p for p in files('distinfo-pkg') if p.name == 'mod.py'][0]
assertRegex(repr(util.hash), '<FileHash mode: sha256 value: .*>')
def test_files_dist_info(self):
self._test_files(files('distinfo-pkg'))
@ -99,10 +119,7 @@ class APITests(
def test_requires_egg_info(self):
deps = requires('egginfo-pkg')
assert len(deps) == 2
assert any(
dep == 'wheel >= 1.0; python_version >= "2.7"'
for dep in deps
)
assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps)
def test_requires_dist_info(self):
deps = requires('distinfo-pkg')
@ -112,7 +129,8 @@ class APITests(
assert "pytest; extra == 'test'" in deps
def test_more_complex_deps_requires_text(self):
requires = textwrap.dedent("""
requires = textwrap.dedent(
"""
dep1
dep2
@ -124,7 +142,8 @@ class APITests(
[extra2:python_version < "3"]
dep5
""")
"""
)
deps = sorted(Distribution._deps_from_requires_text(requires))
expected = [
'dep1',
@ -140,17 +159,27 @@ class APITests(
assert deps == expected
class LegacyDots(fixtures.DistInfoPkgWithDotLegacy, unittest.TestCase):
def test_name_normalization(self):
names = 'pkg.dot', 'pkg_dot', 'pkg-dot', 'pkg..dot', 'Pkg.Dot'
for name in names:
with self.subTest(name):
assert distribution(name).metadata['Name'] == 'pkg.dot'
def test_name_normalization_versionless_egg_info(self):
names = 'pkg.lot', 'pkg_lot', 'pkg-lot', 'pkg..lot', 'Pkg.Lot'
for name in names:
with self.subTest(name):
assert distribution(name).metadata['Name'] == 'pkg.lot'
class OffSysPathTests(fixtures.DistInfoPkgOffPath, unittest.TestCase):
def test_find_distributions_specified_path(self):
dists = Distribution.discover(path=[str(self.site_dir)])
assert any(
dist.metadata['Name'] == 'distinfo-pkg'
for dist in dists
)
assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists)
def test_distribution_at_pathlib(self):
"""Demonstrate how to load metadata direct from a directory.
"""
"""Demonstrate how to load metadata direct from a directory."""
dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info'
dist = Distribution.at(dist_info_path)
assert dist.version == '1.0.0'

View File

@ -3,8 +3,12 @@ import unittest
from contextlib import ExitStack
from importlib.metadata import (
distribution, entry_points, files, PackageNotFoundError,
version, distributions,
PackageNotFoundError,
distribution,
distributions,
entry_points,
files,
version,
)
from importlib import resources

View File

@ -1,4 +1,6 @@
import os
import copy
import pickle
import platform
import subprocess
import sys
@ -234,6 +236,38 @@ class PlatformTest(unittest.TestCase):
)
self.assertEqual(tuple(res), expected)
def test_uname_replace(self):
res = platform.uname()
new = res._replace(
system='system', node='node', release='release',
version='version', machine='machine')
self.assertEqual(new.system, 'system')
self.assertEqual(new.node, 'node')
self.assertEqual(new.release, 'release')
self.assertEqual(new.version, 'version')
self.assertEqual(new.machine, 'machine')
# processor cannot be replaced
self.assertEqual(new.processor, res.processor)
def test_uname_copy(self):
uname = platform.uname()
self.assertEqual(copy.copy(uname), uname)
self.assertEqual(copy.deepcopy(uname), uname)
def test_uname_pickle(self):
orig = platform.uname()
for proto in range(pickle.HIGHEST_PROTOCOL + 1):
with self.subTest(protocol=proto):
pickled = pickle.dumps(orig, proto)
restored = pickle.loads(pickled)
self.assertEqual(restored, orig)
def test_uname_slices(self):
res = platform.uname()
expected = tuple(res)
self.assertEqual(res[:], expected)
self.assertEqual(res[:5], expected[:5])
@unittest.skipIf(sys.platform in ['win32', 'OpenVMS'], "uname -p not used")
def test_uname_processor(self):
"""

View File

@ -277,6 +277,13 @@ class SocketServerTest(unittest.TestCase):
t.join()
s.server_close()
def test_close_immediately(self):
class MyServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
server = MyServer((HOST, 0), lambda: None)
server.server_close()
def test_tcpserver_bind_leak(self):
# Issue #22435: the server socket wouldn't be closed if bind()/listen()
# failed.
@ -491,6 +498,22 @@ class MiscTestCase(unittest.TestCase):
self.assertEqual(server.shutdown_called, 1)
server.server_close()
def test_threads_reaped(self):
"""
In #37193, users reported a memory leak
due to the saving of every request thread. Ensure that
not all threads are kept forever.
"""
class MyServer(socketserver.ThreadingMixIn, socketserver.TCPServer):
pass
server = MyServer((HOST, 0), socketserver.StreamRequestHandler)
for n in range(10):
with socket.create_connection(server.server_address):
server.handle_request()
self.assertLess(len(server._threads), 10)
server.server_close()
if __name__ == "__main__":
unittest.main()

View File

@ -0,0 +1,2 @@
Fixed memory leak in ``socketserver.ThreadingMixIn`` introduced in Python
3.7.

View File

@ -0,0 +1 @@
Restore compatibility for ``uname_result`` around deepcopy and _replace.

View File

@ -0,0 +1,6 @@
In ``importlib.metadata``: - ``EntryPoint`` objects now expose a ``.dist``
object referencing the ``Distribution`` when constructed from a
``Distribution``. - Add support for package discovery under package
normalization rules. - The object returned by ``metadata()`` now has a
formally-defined protocol called ``PackageMetadata`` with declared support
for the ``.get_all()`` method. - Synced with importlib_metadata 3.3.