I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,424 @@
Pluggable Distributions of Python Software
==========================================
Distributions
-------------
A "Distribution" is a collection of files that represent a "Release" of a
"Project" as of a particular point in time, denoted by a
"Version"::
>>> import sys, pkg_resources
>>> from pkg_resources import Distribution
>>> Distribution(project_name="Foo", version="1.2")
Foo 1.2
Distributions have a location, which can be a filename, URL, or really anything
else you care to use::
>>> dist = Distribution(
... location="http://example.com/something",
... project_name="Bar", version="0.9"
... )
>>> dist
Bar 0.9 (http://example.com/something)
Distributions have various introspectable attributes::
>>> dist.location
'http://example.com/something'
>>> dist.project_name
'Bar'
>>> dist.version
'0.9'
>>> dist.py_version == '{}.{}'.format(*sys.version_info)
True
>>> print(dist.platform)
None
Including various computed attributes::
>>> from pkg_resources import parse_version
>>> dist.parsed_version == parse_version(dist.version)
True
>>> dist.key # case-insensitive form of the project name
'bar'
Distributions are compared (and hashed) by version first::
>>> Distribution(version='1.0') == Distribution(version='1.0')
True
>>> Distribution(version='1.0') == Distribution(version='1.1')
False
>>> Distribution(version='1.0') < Distribution(version='1.1')
True
but also by project name (case-insensitive), platform, Python version,
location, etc.::
>>> Distribution(project_name="Foo",version="1.0") == \
... Distribution(project_name="Foo",version="1.0")
True
>>> Distribution(project_name="Foo",version="1.0") == \
... Distribution(project_name="foo",version="1.0")
True
>>> Distribution(project_name="Foo",version="1.0") == \
... Distribution(project_name="Foo",version="1.1")
False
>>> Distribution(project_name="Foo",py_version="2.3",version="1.0") == \
... Distribution(project_name="Foo",py_version="2.4",version="1.0")
False
>>> Distribution(location="spam",version="1.0") == \
... Distribution(location="spam",version="1.0")
True
>>> Distribution(location="spam",version="1.0") == \
... Distribution(location="baz",version="1.0")
False
Hash and compare distribution by prio/plat
Get version from metadata
provider capabilities
egg_name()
as_requirement()
from_location, from_filename (w/path normalization)
Releases may have zero or more "Requirements", which indicate
what releases of another project the release requires in order to
function. A Requirement names the other project, expresses some criteria
as to what releases of that project are acceptable, and lists any "Extras"
that the requiring release may need from that project. (An Extra is an
optional feature of a Release, that can only be used if its additional
Requirements are satisfied.)
The Working Set
---------------
A collection of active distributions is called a Working Set. Note that a
Working Set can contain any importable distribution, not just pluggable ones.
For example, the Python standard library is an importable distribution that
will usually be part of the Working Set, even though it is not pluggable.
Similarly, when you are doing development work on a project, the files you are
editing are also a Distribution. (And, with a little attention to the
directory names used, and including some additional metadata, such a
"development distribution" can be made pluggable as well.)
>>> from pkg_resources import WorkingSet
A working set's entries are the sys.path entries that correspond to the active
distributions. By default, the working set's entries are the items on
``sys.path``::
>>> ws = WorkingSet()
>>> ws.entries == sys.path
True
But you can also create an empty working set explicitly, and add distributions
to it::
>>> ws = WorkingSet([])
>>> ws.add(dist)
>>> ws.entries
['http://example.com/something']
>>> dist in ws
True
>>> Distribution('foo',version="") in ws
False
And you can iterate over its distributions::
>>> list(ws)
[Bar 0.9 (http://example.com/something)]
Adding the same distribution more than once is a no-op::
>>> ws.add(dist)
>>> list(ws)
[Bar 0.9 (http://example.com/something)]
For that matter, adding multiple distributions for the same project also does
nothing, because a working set can only hold one active distribution per
project -- the first one added to it::
>>> ws.add(
... Distribution(
... 'http://example.com/something', project_name="Bar",
... version="7.2"
... )
... )
>>> list(ws)
[Bar 0.9 (http://example.com/something)]
You can append a path entry to a working set using ``add_entry()``::
>>> ws.entries
['http://example.com/something']
>>> ws.add_entry(pkg_resources.__file__)
>>> ws.entries
['http://example.com/something', '...pkg_resources...']
Multiple additions result in multiple entries, even if the entry is already in
the working set (because ``sys.path`` can contain the same entry more than
once)::
>>> ws.add_entry(pkg_resources.__file__)
>>> ws.entries
['...example.com...', '...pkg_resources...', '...pkg_resources...']
And you can specify the path entry a distribution was found under, using the
optional second parameter to ``add()``::
>>> ws = WorkingSet([])
>>> ws.add(dist,"foo")
>>> ws.entries
['foo']
But even if a distribution is found under multiple path entries, it still only
shows up once when iterating the working set:
>>> ws.add_entry(ws.entries[0])
>>> list(ws)
[Bar 0.9 (http://example.com/something)]
You can ask a WorkingSet to ``find()`` a distribution matching a requirement::
>>> from pkg_resources import Requirement
>>> print(ws.find(Requirement.parse("Foo==1.0"))) # no match, return None
None
>>> ws.find(Requirement.parse("Bar==0.9")) # match, return distribution
Bar 0.9 (http://example.com/something)
Note that asking for a conflicting version of a distribution already in a
working set triggers a ``pkg_resources.VersionConflict`` error:
>>> try:
... ws.find(Requirement.parse("Bar==1.0"))
... except pkg_resources.VersionConflict as exc:
... print(str(exc))
... else:
... raise AssertionError("VersionConflict was not raised")
(Bar 0.9 (http://example.com/something), Requirement.parse('Bar==1.0'))
You can subscribe a callback function to receive notifications whenever a new
distribution is added to a working set. The callback is immediately invoked
once for each existing distribution in the working set, and then is called
again for new distributions added thereafter::
>>> def added(dist): print("Added %s" % dist)
>>> ws.subscribe(added)
Added Bar 0.9
>>> foo12 = Distribution(project_name="Foo", version="1.2", location="f12")
>>> ws.add(foo12)
Added Foo 1.2
Note, however, that only the first distribution added for a given project name
will trigger a callback, even during the initial ``subscribe()`` callback::
>>> foo14 = Distribution(project_name="Foo", version="1.4", location="f14")
>>> ws.add(foo14) # no callback, because Foo 1.2 is already active
>>> ws = WorkingSet([])
>>> ws.add(foo12)
>>> ws.add(foo14)
>>> ws.subscribe(added)
Added Foo 1.2
And adding a callback more than once has no effect, either::
>>> ws.subscribe(added) # no callbacks
# and no double-callbacks on subsequent additions, either
>>> just_a_test = Distribution(project_name="JustATest", version="0.99")
>>> ws.add(just_a_test)
Added JustATest 0.99
Finding Plugins
---------------
``WorkingSet`` objects can be used to figure out what plugins in an
``Environment`` can be loaded without any resolution errors::
>>> from pkg_resources import Environment
>>> plugins = Environment([]) # normally, a list of plugin directories
>>> plugins.add(foo12)
>>> plugins.add(foo14)
>>> plugins.add(just_a_test)
In the simplest case, we just get the newest version of each distribution in
the plugin environment::
>>> ws = WorkingSet([])
>>> ws.find_plugins(plugins)
([JustATest 0.99, Foo 1.4 (f14)], {})
But if there's a problem with a version conflict or missing requirements, the
method falls back to older versions, and the error info dict will contain an
exception instance for each unloadable plugin::
>>> ws.add(foo12) # this will conflict with Foo 1.4
>>> ws.find_plugins(plugins)
([JustATest 0.99, Foo 1.2 (f12)], {Foo 1.4 (f14): VersionConflict(...)})
But if you disallow fallbacks, the failed plugin will be skipped instead of
trying older versions::
>>> ws.find_plugins(plugins, fallback=False)
([JustATest 0.99], {Foo 1.4 (f14): VersionConflict(...)})
Platform Compatibility Rules
----------------------------
On the Mac, there are potential compatibility issues for modules compiled
on newer versions of macOS than what the user is running. Additionally,
macOS will soon have two platforms to contend with: Intel and PowerPC.
Basic equality works as on other platforms::
>>> from pkg_resources import compatible_platforms as cp
>>> reqd = 'macosx-10.4-ppc'
>>> cp(reqd, reqd)
True
>>> cp("win32", reqd)
False
Distributions made on other machine types are not compatible::
>>> cp("macosx-10.4-i386", reqd)
False
Distributions made on earlier versions of the OS are compatible, as
long as they are from the same top-level version. The patchlevel version
number does not matter::
>>> cp("macosx-10.4-ppc", reqd)
True
>>> cp("macosx-10.3-ppc", reqd)
True
>>> cp("macosx-10.5-ppc", reqd)
False
>>> cp("macosx-9.5-ppc", reqd)
False
Backwards compatibility for packages made via earlier versions of
setuptools is provided as well::
>>> cp("darwin-8.2.0-Power_Macintosh", reqd)
True
>>> cp("darwin-7.2.0-Power_Macintosh", reqd)
True
>>> cp("darwin-8.2.0-Power_Macintosh", "macosx-10.3-ppc")
False
Environment Markers
-------------------
>>> from pkg_resources import invalid_marker as im, evaluate_marker as em
>>> import os
>>> print(im("sys_platform"))
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
sys_platform
^
>>> print(im("sys_platform=="))
Expected a marker variable or quoted string
sys_platform==
^
>>> print(im("sys_platform=='win32'"))
False
>>> print(im("sys=='x'"))
Expected a marker variable or quoted string
sys=='x'
^
>>> print(im("(extra)"))
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
(extra)
^
>>> print(im("(extra"))
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
(extra
^
>>> print(im("os.open('foo')=='y'"))
Expected a marker variable or quoted string
os.open('foo')=='y'
^
>>> print(im("'x'=='y' and os.open('foo')=='y'")) # no short-circuit!
Expected a marker variable or quoted string
'x'=='y' and os.open('foo')=='y'
^
>>> print(im("'x'=='x' or os.open('foo')=='y'")) # no short-circuit!
Expected a marker variable or quoted string
'x'=='x' or os.open('foo')=='y'
^
>>> print(im("r'x'=='x'"))
Expected a marker variable or quoted string
r'x'=='x'
^
>>> print(im("'''x'''=='x'"))
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
'''x'''=='x'
^
>>> print(im('"""x"""=="x"'))
Expected marker operator, one of <=, <, !=, ==, >=, >, ~=, ===, in, not in
"""x"""=="x"
^
>>> print(im(r"x\n=='x'"))
Expected a marker variable or quoted string
x\n=='x'
^
>>> print(im("os.open=='y'"))
Expected a marker variable or quoted string
os.open=='y'
^
>>> em("sys_platform=='win32'") == (sys.platform=='win32')
True
>>> em("python_version >= '2.7'")
True
>>> em("python_version > '2.6'")
True
>>> im("implementation_name=='cpython'")
False
>>> im("platform_python_implementation=='CPython'")
False
>>> im("implementation_version=='3.5.1'")
False

View File

@ -0,0 +1,7 @@
import setuptools
setuptools.setup(
name="my-test-package",
version="1.0",
zip_safe=True,
)

View File

@ -0,0 +1,10 @@
Metadata-Version: 1.0
Name: my-test-package
Version: 1.0
Summary: UNKNOWN
Home-page: UNKNOWN
Author: UNKNOWN
Author-email: UNKNOWN
License: UNKNOWN
Description: UNKNOWN
Platform: UNKNOWN

View File

@ -0,0 +1,7 @@
setup.cfg
setup.py
my_test_package.egg-info/PKG-INFO
my_test_package.egg-info/SOURCES.txt
my_test_package.egg-info/dependency_links.txt
my_test_package.egg-info/top_level.txt
my_test_package.egg-info/zip-safe

View File

@ -0,0 +1,56 @@
import shutil
from pathlib import Path
import pytest
import pkg_resources
TESTS_DATA_DIR = Path(__file__).parent / 'data'
class TestFindDistributions:
@pytest.fixture
def target_dir(self, tmpdir):
target_dir = tmpdir.mkdir('target')
# place a .egg named directory in the target that is not an egg:
target_dir.mkdir('not.an.egg')
return target_dir
def test_non_egg_dir_named_egg(self, target_dir):
dists = pkg_resources.find_distributions(str(target_dir))
assert not list(dists)
def test_standalone_egg_directory(self, target_dir):
shutil.copytree(
TESTS_DATA_DIR / 'my-test-package_unpacked-egg',
target_dir,
dirs_exist_ok=True,
)
dists = pkg_resources.find_distributions(str(target_dir))
assert [dist.project_name for dist in dists] == ['my-test-package']
dists = pkg_resources.find_distributions(str(target_dir), only=True)
assert not list(dists)
def test_zipped_egg(self, target_dir):
shutil.copytree(
TESTS_DATA_DIR / 'my-test-package_zipped-egg',
target_dir,
dirs_exist_ok=True,
)
dists = pkg_resources.find_distributions(str(target_dir))
assert [dist.project_name for dist in dists] == ['my-test-package']
dists = pkg_resources.find_distributions(str(target_dir), only=True)
assert not list(dists)
def test_zipped_sdist_one_level_removed(self, target_dir):
shutil.copytree(
TESTS_DATA_DIR / 'my-test-package-zip', target_dir, dirs_exist_ok=True
)
dists = pkg_resources.find_distributions(
str(target_dir / "my-test-package.zip")
)
assert [dist.project_name for dist in dists] == ['my-test-package']
dists = pkg_resources.find_distributions(
str(target_dir / "my-test-package.zip"), only=True
)
assert not list(dists)

View File

@ -0,0 +1,54 @@
import platform
from inspect import cleandoc
import jaraco.path
import pytest
pytestmark = pytest.mark.integration
# For the sake of simplicity this test uses fixtures defined in
# `setuptools.test.fixtures`,
# and it also exercise conditions considered deprecated...
# So if needed this test can be deleted.
@pytest.mark.skipif(
platform.system() != "Linux",
reason="only demonstrated to fail on Linux in #4399",
)
def test_interop_pkg_resources_iter_entry_points(tmp_path, venv):
"""
Importing pkg_resources.iter_entry_points on console_scripts
seems to cause trouble with zope-interface, when deprecates installation method
is used. See #4399.
"""
project = {
"pkg": {
"foo.py": cleandoc(
"""
from pkg_resources import iter_entry_points
def bar():
print("Print me if you can")
"""
),
"setup.py": cleandoc(
"""
from setuptools import setup, find_packages
setup(
install_requires=["zope-interface==6.4.post2"],
entry_points={
"console_scripts": [
"foo=foo:bar",
],
},
)
"""
),
}
}
jaraco.path.build(project, prefix=tmp_path)
cmd = ["pip", "install", "-e", ".", "--no-use-pep517"]
venv.run(cmd, cwd=tmp_path / "pkg") # Needs this version of pkg_resources installed
out = venv.run(["foo"])
assert "Print me if you can" in out

View File

@ -0,0 +1,8 @@
from unittest import mock
from pkg_resources import evaluate_marker
@mock.patch('platform.python_version', return_value='2.7.10')
def test_ordering(python_version_mock):
assert evaluate_marker("python_full_version > '2.7.3'") is True

View File

@ -0,0 +1,427 @@
from __future__ import annotations
import builtins
import datetime
import os
import plistlib
import stat
import subprocess
import sys
import tempfile
import zipfile
from unittest import mock
import pytest
import pkg_resources
from pkg_resources import DistInfoDistribution, Distribution, EggInfoDistribution
import distutils.command.install_egg_info
import distutils.dist
class EggRemover(str):
def __call__(self):
if self in sys.path:
sys.path.remove(self)
if os.path.exists(self):
os.remove(self)
class TestZipProvider:
finalizers: list[EggRemover] = []
ref_time = datetime.datetime(2013, 5, 12, 13, 25, 0)
"A reference time for a file modification"
@classmethod
def setup_class(cls):
"create a zip egg and add it to sys.path"
egg = tempfile.NamedTemporaryFile(suffix='.egg', delete=False)
zip_egg = zipfile.ZipFile(egg, 'w')
zip_info = zipfile.ZipInfo()
zip_info.filename = 'mod.py'
zip_info.date_time = cls.ref_time.timetuple()
zip_egg.writestr(zip_info, 'x = 3\n')
zip_info = zipfile.ZipInfo()
zip_info.filename = 'data.dat'
zip_info.date_time = cls.ref_time.timetuple()
zip_egg.writestr(zip_info, 'hello, world!')
zip_info = zipfile.ZipInfo()
zip_info.filename = 'subdir/mod2.py'
zip_info.date_time = cls.ref_time.timetuple()
zip_egg.writestr(zip_info, 'x = 6\n')
zip_info = zipfile.ZipInfo()
zip_info.filename = 'subdir/data2.dat'
zip_info.date_time = cls.ref_time.timetuple()
zip_egg.writestr(zip_info, 'goodbye, world!')
zip_egg.close()
egg.close()
sys.path.append(egg.name)
subdir = os.path.join(egg.name, 'subdir')
sys.path.append(subdir)
cls.finalizers.append(EggRemover(subdir))
cls.finalizers.append(EggRemover(egg.name))
@classmethod
def teardown_class(cls):
for finalizer in cls.finalizers:
finalizer()
def test_resource_listdir(self):
import mod # pyright: ignore[reportMissingImports] # Temporary package for test
zp = pkg_resources.ZipProvider(mod)
expected_root = ['data.dat', 'mod.py', 'subdir']
assert sorted(zp.resource_listdir('')) == expected_root
expected_subdir = ['data2.dat', 'mod2.py']
assert sorted(zp.resource_listdir('subdir')) == expected_subdir
assert sorted(zp.resource_listdir('subdir/')) == expected_subdir
assert zp.resource_listdir('nonexistent') == []
assert zp.resource_listdir('nonexistent/') == []
import mod2 # pyright: ignore[reportMissingImports] # Temporary package for test
zp2 = pkg_resources.ZipProvider(mod2)
assert sorted(zp2.resource_listdir('')) == expected_subdir
assert zp2.resource_listdir('subdir') == []
assert zp2.resource_listdir('subdir/') == []
def test_resource_filename_rewrites_on_change(self):
"""
If a previous call to get_resource_filename has saved the file, but
the file has been subsequently mutated with different file of the
same size and modification time, it should not be overwritten on a
subsequent call to get_resource_filename.
"""
import mod # pyright: ignore[reportMissingImports] # Temporary package for test
manager = pkg_resources.ResourceManager()
zp = pkg_resources.ZipProvider(mod)
filename = zp.get_resource_filename(manager, 'data.dat')
actual = datetime.datetime.fromtimestamp(os.stat(filename).st_mtime)
assert actual == self.ref_time
f = open(filename, 'w', encoding="utf-8")
f.write('hello, world?')
f.close()
ts = self.ref_time.timestamp()
os.utime(filename, (ts, ts))
filename = zp.get_resource_filename(manager, 'data.dat')
with open(filename, encoding="utf-8") as f:
assert f.read() == 'hello, world!'
manager.cleanup_resources()
class TestResourceManager:
def test_get_cache_path(self):
mgr = pkg_resources.ResourceManager()
path = mgr.get_cache_path('foo')
type_ = str(type(path))
message = "Unexpected type from get_cache_path: " + type_
assert isinstance(path, str), message
def test_get_cache_path_race(self, tmpdir):
# Patch to os.path.isdir to create a race condition
def patched_isdir(dirname, unpatched_isdir=pkg_resources.isdir):
patched_isdir.dirnames.append(dirname)
was_dir = unpatched_isdir(dirname)
if not was_dir:
os.makedirs(dirname)
return was_dir
patched_isdir.dirnames = []
# Get a cache path with a "race condition"
mgr = pkg_resources.ResourceManager()
mgr.set_extraction_path(str(tmpdir))
archive_name = os.sep.join(('foo', 'bar', 'baz'))
with mock.patch.object(pkg_resources, 'isdir', new=patched_isdir):
mgr.get_cache_path(archive_name)
# Because this test relies on the implementation details of this
# function, these assertions are a sentinel to ensure that the
# test suite will not fail silently if the implementation changes.
called_dirnames = patched_isdir.dirnames
assert len(called_dirnames) == 2
assert called_dirnames[0].split(os.sep)[-2:] == ['foo', 'bar']
assert called_dirnames[1].split(os.sep)[-1:] == ['foo']
"""
Tests to ensure that pkg_resources runs independently from setuptools.
"""
def test_setuptools_not_imported(self):
"""
In a separate Python environment, import pkg_resources and assert
that action doesn't cause setuptools to be imported.
"""
lines = (
'import pkg_resources',
'import sys',
('assert "setuptools" not in sys.modules, "setuptools was imported"'),
)
cmd = [sys.executable, '-c', '; '.join(lines)]
subprocess.check_call(cmd)
def make_test_distribution(metadata_path, metadata):
"""
Make a test Distribution object, and return it.
:param metadata_path: the path to the metadata file that should be
created. This should be inside a distribution directory that should
also be created. For example, an argument value might end with
"<project>.dist-info/METADATA".
:param metadata: the desired contents of the metadata file, as bytes.
"""
dist_dir = os.path.dirname(metadata_path)
os.mkdir(dist_dir)
with open(metadata_path, 'wb') as f:
f.write(metadata)
dists = list(pkg_resources.distributions_from_metadata(dist_dir))
(dist,) = dists
return dist
def test_get_metadata__bad_utf8(tmpdir):
"""
Test a metadata file with bytes that can't be decoded as utf-8.
"""
filename = 'METADATA'
# Convert the tmpdir LocalPath object to a string before joining.
metadata_path = os.path.join(str(tmpdir), 'foo.dist-info', filename)
# Encode a non-ascii string with the wrong encoding (not utf-8).
metadata = 'née'.encode('iso-8859-1')
dist = make_test_distribution(metadata_path, metadata=metadata)
with pytest.raises(UnicodeDecodeError) as excinfo:
dist.get_metadata(filename)
exc = excinfo.value
actual = str(exc)
expected = (
# The error message starts with "'utf-8' codec ..." However, the
# spelling of "utf-8" can vary (e.g. "utf8") so we don't include it
"codec can't decode byte 0xe9 in position 1: "
'invalid continuation byte in METADATA file at path: '
)
assert expected in actual, 'actual: {}'.format(actual)
assert actual.endswith(metadata_path), 'actual: {}'.format(actual)
def make_distribution_no_version(tmpdir, basename):
"""
Create a distribution directory with no file containing the version.
"""
dist_dir = tmpdir / basename
dist_dir.ensure_dir()
# Make the directory non-empty so distributions_from_metadata()
# will detect it and yield it.
dist_dir.join('temp.txt').ensure()
dists = list(pkg_resources.distributions_from_metadata(dist_dir))
assert len(dists) == 1
(dist,) = dists
return dist, dist_dir
@pytest.mark.parametrize(
'suffix, expected_filename, expected_dist_type',
[
('egg-info', 'PKG-INFO', EggInfoDistribution),
('dist-info', 'METADATA', DistInfoDistribution),
],
)
@pytest.mark.xfail(
sys.version_info[:2] == (3, 12) and sys.version_info.releaselevel != 'final',
reason="https://github.com/python/cpython/issues/103632",
)
def test_distribution_version_missing(
tmpdir, suffix, expected_filename, expected_dist_type
):
"""
Test Distribution.version when the "Version" header is missing.
"""
basename = 'foo.{}'.format(suffix)
dist, dist_dir = make_distribution_no_version(tmpdir, basename)
expected_text = ("Missing 'Version:' header and/or {} file at path: ").format(
expected_filename
)
metadata_path = os.path.join(dist_dir, expected_filename)
# Now check the exception raised when the "version" attribute is accessed.
with pytest.raises(ValueError) as excinfo:
dist.version
err = str(excinfo.value)
# Include a string expression after the assert so the full strings
# will be visible for inspection on failure.
assert expected_text in err, str((expected_text, err))
# Also check the args passed to the ValueError.
msg, dist = excinfo.value.args
assert expected_text in msg
# Check that the message portion contains the path.
assert metadata_path in msg, str((metadata_path, msg))
assert type(dist) is expected_dist_type
@pytest.mark.xfail(
sys.version_info[:2] == (3, 12) and sys.version_info.releaselevel != 'final',
reason="https://github.com/python/cpython/issues/103632",
)
def test_distribution_version_missing_undetected_path():
"""
Test Distribution.version when the "Version" header is missing and
the path can't be detected.
"""
# Create a Distribution object with no metadata argument, which results
# in an empty metadata provider.
dist = Distribution('/foo')
with pytest.raises(ValueError) as excinfo:
dist.version
msg, dist = excinfo.value.args
expected = (
"Missing 'Version:' header and/or PKG-INFO file at path: [could not detect]"
)
assert msg == expected
@pytest.mark.parametrize('only', [False, True])
def test_dist_info_is_not_dir(tmp_path, only):
"""Test path containing a file with dist-info extension."""
dist_info = tmp_path / 'foobar.dist-info'
dist_info.touch()
assert not pkg_resources.dist_factory(str(tmp_path), str(dist_info), only)
def test_macos_vers_fallback(monkeypatch, tmp_path):
"""Regression test for pkg_resources._macos_vers"""
orig_open = builtins.open
# Pretend we need to use the plist file
monkeypatch.setattr('platform.mac_ver', mock.Mock(return_value=('', (), '')))
# Create fake content for the fake plist file
with open(tmp_path / 'fake.plist', 'wb') as fake_file:
plistlib.dump({"ProductVersion": "11.4"}, fake_file)
# Pretend the fake file exists
monkeypatch.setattr('os.path.exists', mock.Mock(return_value=True))
def fake_open(file, *args, **kwargs):
return orig_open(tmp_path / 'fake.plist', *args, **kwargs)
# Ensure that the _macos_vers works correctly
with mock.patch('builtins.open', mock.Mock(side_effect=fake_open)) as m:
pkg_resources._macos_vers.cache_clear()
assert pkg_resources._macos_vers() == ["11", "4"]
pkg_resources._macos_vers.cache_clear()
m.assert_called()
class TestDeepVersionLookupDistutils:
@pytest.fixture
def env(self, tmpdir):
"""
Create a package environment, similar to a virtualenv,
in which packages are installed.
"""
class Environment(str):
pass
env = Environment(tmpdir)
tmpdir.chmod(stat.S_IRWXU)
subs = 'home', 'lib', 'scripts', 'data', 'egg-base'
env.paths = dict((dirname, str(tmpdir / dirname)) for dirname in subs)
list(map(os.mkdir, env.paths.values()))
return env
def create_foo_pkg(self, env, version):
"""
Create a foo package installed (distutils-style) to env.paths['lib']
as version.
"""
ld = "This package has unicode metadata! ❄"
attrs = dict(name='foo', version=version, long_description=ld)
dist = distutils.dist.Distribution(attrs)
iei_cmd = distutils.command.install_egg_info.install_egg_info(dist)
iei_cmd.initialize_options()
iei_cmd.install_dir = env.paths['lib']
iei_cmd.finalize_options()
iei_cmd.run()
def test_version_resolved_from_egg_info(self, env):
version = '1.11.0.dev0+2329eae'
self.create_foo_pkg(env, version)
# this requirement parsing will raise a VersionConflict unless the
# .egg-info file is parsed (see #419 on BitBucket)
req = pkg_resources.Requirement.parse('foo>=1.9')
dist = pkg_resources.WorkingSet([env.paths['lib']]).find(req)
assert dist.version == version
@pytest.mark.parametrize(
'unnormalized, normalized',
[
('foo', 'foo'),
('foo/', 'foo'),
('foo/bar', 'foo/bar'),
('foo/bar/', 'foo/bar'),
],
)
def test_normalize_path_trailing_sep(self, unnormalized, normalized):
"""Ensure the trailing slash is cleaned for path comparison.
See pypa/setuptools#1519.
"""
result_from_unnormalized = pkg_resources.normalize_path(unnormalized)
result_from_normalized = pkg_resources.normalize_path(normalized)
assert result_from_unnormalized == result_from_normalized
@pytest.mark.skipif(
os.path.normcase('A') != os.path.normcase('a'),
reason='Testing case-insensitive filesystems.',
)
@pytest.mark.parametrize(
'unnormalized, normalized',
[
('MiXeD/CasE', 'mixed/case'),
],
)
def test_normalize_path_normcase(self, unnormalized, normalized):
"""Ensure mixed case is normalized on case-insensitive filesystems."""
result_from_unnormalized = pkg_resources.normalize_path(unnormalized)
result_from_normalized = pkg_resources.normalize_path(normalized)
assert result_from_unnormalized == result_from_normalized
@pytest.mark.skipif(
os.path.sep != '\\',
reason='Testing systems using backslashes as path separators.',
)
@pytest.mark.parametrize(
'unnormalized, expected',
[
('forward/slash', 'forward\\slash'),
('forward/slash/', 'forward\\slash'),
('backward\\slash\\', 'backward\\slash'),
],
)
def test_normalize_path_backslash_sep(self, unnormalized, expected):
"""Ensure path seps are cleaned on backslash path sep systems."""
result = pkg_resources.normalize_path(unnormalized)
assert result.endswith(expected)

View File

@ -0,0 +1,869 @@
import itertools
import os
import platform
import string
import sys
import pytest
from packaging.specifiers import SpecifierSet
import pkg_resources
from pkg_resources import (
Distribution,
EntryPoint,
Requirement,
VersionConflict,
WorkingSet,
parse_requirements,
parse_version,
safe_name,
safe_version,
)
# from Python 3.6 docs. Available from itertools on Python 3.10
def pairwise(iterable):
"s -> (s0,s1), (s1,s2), (s2, s3), ..."
a, b = itertools.tee(iterable)
next(b, None)
return zip(a, b)
class Metadata(pkg_resources.EmptyProvider):
"""Mock object to return metadata as if from an on-disk distribution"""
def __init__(self, *pairs):
self.metadata = dict(pairs)
def has_metadata(self, name) -> bool:
return name in self.metadata
def get_metadata(self, name):
return self.metadata[name]
def get_metadata_lines(self, name):
return pkg_resources.yield_lines(self.get_metadata(name))
dist_from_fn = pkg_resources.Distribution.from_filename
class TestDistro:
def testCollection(self):
# empty path should produce no distributions
ad = pkg_resources.Environment([], platform=None, python=None)
assert list(ad) == []
assert ad['FooPkg'] == []
ad.add(dist_from_fn("FooPkg-1.3_1.egg"))
ad.add(dist_from_fn("FooPkg-1.4-py2.4-win32.egg"))
ad.add(dist_from_fn("FooPkg-1.2-py2.4.egg"))
# Name is in there now
assert ad['FooPkg']
# But only 1 package
assert list(ad) == ['foopkg']
# Distributions sort by version
expected = ['1.4', '1.3-1', '1.2']
assert [dist.version for dist in ad['FooPkg']] == expected
# Removing a distribution leaves sequence alone
ad.remove(ad['FooPkg'][1])
assert [dist.version for dist in ad['FooPkg']] == ['1.4', '1.2']
# And inserting adds them in order
ad.add(dist_from_fn("FooPkg-1.9.egg"))
assert [dist.version for dist in ad['FooPkg']] == ['1.9', '1.4', '1.2']
ws = WorkingSet([])
foo12 = dist_from_fn("FooPkg-1.2-py2.4.egg")
foo14 = dist_from_fn("FooPkg-1.4-py2.4-win32.egg")
(req,) = parse_requirements("FooPkg>=1.3")
# Nominal case: no distros on path, should yield all applicable
assert ad.best_match(req, ws).version == '1.9'
# If a matching distro is already installed, should return only that
ws.add(foo14)
assert ad.best_match(req, ws).version == '1.4'
# If the first matching distro is unsuitable, it's a version conflict
ws = WorkingSet([])
ws.add(foo12)
ws.add(foo14)
with pytest.raises(VersionConflict):
ad.best_match(req, ws)
# If more than one match on the path, the first one takes precedence
ws = WorkingSet([])
ws.add(foo14)
ws.add(foo12)
ws.add(foo14)
assert ad.best_match(req, ws).version == '1.4'
def checkFooPkg(self, d):
assert d.project_name == "FooPkg"
assert d.key == "foopkg"
assert d.version == "1.3.post1"
assert d.py_version == "2.4"
assert d.platform == "win32"
assert d.parsed_version == parse_version("1.3-1")
def testDistroBasics(self):
d = Distribution(
"/some/path",
project_name="FooPkg",
version="1.3-1",
py_version="2.4",
platform="win32",
)
self.checkFooPkg(d)
d = Distribution("/some/path")
assert d.py_version == '{}.{}'.format(*sys.version_info)
assert d.platform is None
def testDistroParse(self):
d = dist_from_fn("FooPkg-1.3.post1-py2.4-win32.egg")
self.checkFooPkg(d)
d = dist_from_fn("FooPkg-1.3.post1-py2.4-win32.egg-info")
self.checkFooPkg(d)
def testDistroMetadata(self):
d = Distribution(
"/some/path",
project_name="FooPkg",
py_version="2.4",
platform="win32",
metadata=Metadata(('PKG-INFO', "Metadata-Version: 1.0\nVersion: 1.3-1\n")),
)
self.checkFooPkg(d)
def distRequires(self, txt):
return Distribution("/foo", metadata=Metadata(('depends.txt', txt)))
def checkRequires(self, dist, txt, extras=()):
assert list(dist.requires(extras)) == list(parse_requirements(txt))
def testDistroDependsSimple(self):
for v in "Twisted>=1.5", "Twisted>=1.5\nZConfig>=2.0":
self.checkRequires(self.distRequires(v), v)
needs_object_dir = pytest.mark.skipif(
not hasattr(object, '__dir__'),
reason='object.__dir__ necessary for self.__dir__ implementation',
)
def test_distribution_dir(self):
d = pkg_resources.Distribution()
dir(d)
@needs_object_dir
def test_distribution_dir_includes_provider_dir(self):
d = pkg_resources.Distribution()
before = d.__dir__()
assert 'test_attr' not in before
d._provider.test_attr = None
after = d.__dir__()
assert len(after) == len(before) + 1
assert 'test_attr' in after
@needs_object_dir
def test_distribution_dir_ignores_provider_dir_leading_underscore(self):
d = pkg_resources.Distribution()
before = d.__dir__()
assert '_test_attr' not in before
d._provider._test_attr = None
after = d.__dir__()
assert len(after) == len(before)
assert '_test_attr' not in after
def testResolve(self):
ad = pkg_resources.Environment([])
ws = WorkingSet([])
# Resolving no requirements -> nothing to install
assert list(ws.resolve([], ad)) == []
# Request something not in the collection -> DistributionNotFound
with pytest.raises(pkg_resources.DistributionNotFound):
ws.resolve(parse_requirements("Foo"), ad)
Foo = Distribution.from_filename(
"/foo_dir/Foo-1.2.egg",
metadata=Metadata(('depends.txt', "[bar]\nBaz>=2.0")),
)
ad.add(Foo)
ad.add(Distribution.from_filename("Foo-0.9.egg"))
# Request thing(s) that are available -> list to activate
for i in range(3):
targets = list(ws.resolve(parse_requirements("Foo"), ad))
assert targets == [Foo]
list(map(ws.add, targets))
with pytest.raises(VersionConflict):
ws.resolve(parse_requirements("Foo==0.9"), ad)
ws = WorkingSet([]) # reset
# Request an extra that causes an unresolved dependency for "Baz"
with pytest.raises(pkg_resources.DistributionNotFound):
ws.resolve(parse_requirements("Foo[bar]"), ad)
Baz = Distribution.from_filename(
"/foo_dir/Baz-2.1.egg", metadata=Metadata(('depends.txt', "Foo"))
)
ad.add(Baz)
# Activation list now includes resolved dependency
assert list(ws.resolve(parse_requirements("Foo[bar]"), ad)) == [Foo, Baz]
# Requests for conflicting versions produce VersionConflict
with pytest.raises(VersionConflict) as vc:
ws.resolve(parse_requirements("Foo==1.2\nFoo!=1.2"), ad)
msg = 'Foo 0.9 is installed but Foo==1.2 is required'
assert vc.value.report() == msg
def test_environment_marker_evaluation_negative(self):
"""Environment markers are evaluated at resolution time."""
ad = pkg_resources.Environment([])
ws = WorkingSet([])
res = ws.resolve(parse_requirements("Foo;python_version<'2'"), ad)
assert list(res) == []
def test_environment_marker_evaluation_positive(self):
ad = pkg_resources.Environment([])
ws = WorkingSet([])
Foo = Distribution.from_filename("/foo_dir/Foo-1.2.dist-info")
ad.add(Foo)
res = ws.resolve(parse_requirements("Foo;python_version>='2'"), ad)
assert list(res) == [Foo]
def test_environment_marker_evaluation_called(self):
"""
If one package foo requires bar without any extras,
markers should pass for bar without extras.
"""
(parent_req,) = parse_requirements("foo")
(req,) = parse_requirements("bar;python_version>='2'")
req_extras = pkg_resources._ReqExtras({req: parent_req.extras})
assert req_extras.markers_pass(req)
(parent_req,) = parse_requirements("foo[]")
(req,) = parse_requirements("bar;python_version>='2'")
req_extras = pkg_resources._ReqExtras({req: parent_req.extras})
assert req_extras.markers_pass(req)
def test_marker_evaluation_with_extras(self):
"""Extras are also evaluated as markers at resolution time."""
ad = pkg_resources.Environment([])
ws = WorkingSet([])
Foo = Distribution.from_filename(
"/foo_dir/Foo-1.2.dist-info",
metadata=Metadata((
"METADATA",
"Provides-Extra: baz\nRequires-Dist: quux; extra=='baz'",
)),
)
ad.add(Foo)
assert list(ws.resolve(parse_requirements("Foo"), ad)) == [Foo]
quux = Distribution.from_filename("/foo_dir/quux-1.0.dist-info")
ad.add(quux)
res = list(ws.resolve(parse_requirements("Foo[baz]"), ad))
assert res == [Foo, quux]
def test_marker_evaluation_with_extras_normlized(self):
"""Extras are also evaluated as markers at resolution time."""
ad = pkg_resources.Environment([])
ws = WorkingSet([])
Foo = Distribution.from_filename(
"/foo_dir/Foo-1.2.dist-info",
metadata=Metadata((
"METADATA",
"Provides-Extra: baz-lightyear\n"
"Requires-Dist: quux; extra=='baz-lightyear'",
)),
)
ad.add(Foo)
assert list(ws.resolve(parse_requirements("Foo"), ad)) == [Foo]
quux = Distribution.from_filename("/foo_dir/quux-1.0.dist-info")
ad.add(quux)
res = list(ws.resolve(parse_requirements("Foo[baz-lightyear]"), ad))
assert res == [Foo, quux]
def test_marker_evaluation_with_multiple_extras(self):
ad = pkg_resources.Environment([])
ws = WorkingSet([])
Foo = Distribution.from_filename(
"/foo_dir/Foo-1.2.dist-info",
metadata=Metadata((
"METADATA",
"Provides-Extra: baz\n"
"Requires-Dist: quux; extra=='baz'\n"
"Provides-Extra: bar\n"
"Requires-Dist: fred; extra=='bar'\n",
)),
)
ad.add(Foo)
quux = Distribution.from_filename("/foo_dir/quux-1.0.dist-info")
ad.add(quux)
fred = Distribution.from_filename("/foo_dir/fred-0.1.dist-info")
ad.add(fred)
res = list(ws.resolve(parse_requirements("Foo[baz,bar]"), ad))
assert sorted(res) == [fred, quux, Foo]
def test_marker_evaluation_with_extras_loop(self):
ad = pkg_resources.Environment([])
ws = WorkingSet([])
a = Distribution.from_filename(
"/foo_dir/a-0.2.dist-info",
metadata=Metadata(("METADATA", "Requires-Dist: c[a]")),
)
b = Distribution.from_filename(
"/foo_dir/b-0.3.dist-info",
metadata=Metadata(("METADATA", "Requires-Dist: c[b]")),
)
c = Distribution.from_filename(
"/foo_dir/c-1.0.dist-info",
metadata=Metadata((
"METADATA",
"Provides-Extra: a\n"
"Requires-Dist: b;extra=='a'\n"
"Provides-Extra: b\n"
"Requires-Dist: foo;extra=='b'",
)),
)
foo = Distribution.from_filename("/foo_dir/foo-0.1.dist-info")
for dist in (a, b, c, foo):
ad.add(dist)
res = list(ws.resolve(parse_requirements("a"), ad))
assert res == [a, c, b, foo]
@pytest.mark.xfail(
sys.version_info[:2] == (3, 12) and sys.version_info.releaselevel != 'final',
reason="https://github.com/python/cpython/issues/103632",
)
def testDistroDependsOptions(self):
d = self.distRequires(
"""
Twisted>=1.5
[docgen]
ZConfig>=2.0
docutils>=0.3
[fastcgi]
fcgiapp>=0.1"""
)
self.checkRequires(d, "Twisted>=1.5")
self.checkRequires(
d, "Twisted>=1.5 ZConfig>=2.0 docutils>=0.3".split(), ["docgen"]
)
self.checkRequires(d, "Twisted>=1.5 fcgiapp>=0.1".split(), ["fastcgi"])
self.checkRequires(
d,
"Twisted>=1.5 ZConfig>=2.0 docutils>=0.3 fcgiapp>=0.1".split(),
["docgen", "fastcgi"],
)
self.checkRequires(
d,
"Twisted>=1.5 fcgiapp>=0.1 ZConfig>=2.0 docutils>=0.3".split(),
["fastcgi", "docgen"],
)
with pytest.raises(pkg_resources.UnknownExtra):
d.requires(["foo"])
class TestWorkingSet:
def test_find_conflicting(self):
ws = WorkingSet([])
Foo = Distribution.from_filename("/foo_dir/Foo-1.2.egg")
ws.add(Foo)
# create a requirement that conflicts with Foo 1.2
req = next(parse_requirements("Foo<1.2"))
with pytest.raises(VersionConflict) as vc:
ws.find(req)
msg = 'Foo 1.2 is installed but Foo<1.2 is required'
assert vc.value.report() == msg
def test_resolve_conflicts_with_prior(self):
"""
A ContextualVersionConflict should be raised when a requirement
conflicts with a prior requirement for a different package.
"""
# Create installation where Foo depends on Baz 1.0 and Bar depends on
# Baz 2.0.
ws = WorkingSet([])
md = Metadata(('depends.txt', "Baz==1.0"))
Foo = Distribution.from_filename("/foo_dir/Foo-1.0.egg", metadata=md)
ws.add(Foo)
md = Metadata(('depends.txt', "Baz==2.0"))
Bar = Distribution.from_filename("/foo_dir/Bar-1.0.egg", metadata=md)
ws.add(Bar)
Baz = Distribution.from_filename("/foo_dir/Baz-1.0.egg")
ws.add(Baz)
Baz = Distribution.from_filename("/foo_dir/Baz-2.0.egg")
ws.add(Baz)
with pytest.raises(VersionConflict) as vc:
ws.resolve(parse_requirements("Foo\nBar\n"))
msg = "Baz 1.0 is installed but Baz==2.0 is required by "
msg += repr(set(['Bar']))
assert vc.value.report() == msg
class TestEntryPoints:
def assertfields(self, ep):
assert ep.name == "foo"
assert ep.module_name == "pkg_resources.tests.test_resources"
assert ep.attrs == ("TestEntryPoints",)
assert ep.extras == ("x",)
assert ep.load() is TestEntryPoints
expect = "foo = pkg_resources.tests.test_resources:TestEntryPoints [x]"
assert str(ep) == expect
def setup_method(self, method):
self.dist = Distribution.from_filename(
"FooPkg-1.2-py2.4.egg", metadata=Metadata(('requires.txt', '[x]'))
)
def testBasics(self):
ep = EntryPoint(
"foo",
"pkg_resources.tests.test_resources",
["TestEntryPoints"],
["x"],
self.dist,
)
self.assertfields(ep)
def testParse(self):
s = "foo = pkg_resources.tests.test_resources:TestEntryPoints [x]"
ep = EntryPoint.parse(s, self.dist)
self.assertfields(ep)
ep = EntryPoint.parse("bar baz= spammity[PING]")
assert ep.name == "bar baz"
assert ep.module_name == "spammity"
assert ep.attrs == ()
assert ep.extras == ("ping",)
ep = EntryPoint.parse(" fizzly = wocka:foo")
assert ep.name == "fizzly"
assert ep.module_name == "wocka"
assert ep.attrs == ("foo",)
assert ep.extras == ()
# plus in the name
spec = "html+mako = mako.ext.pygmentplugin:MakoHtmlLexer"
ep = EntryPoint.parse(spec)
assert ep.name == 'html+mako'
reject_specs = "foo", "x=a:b:c", "q=x/na", "fez=pish:tush-z", "x=f[a]>2"
@pytest.mark.parametrize("reject_spec", reject_specs)
def test_reject_spec(self, reject_spec):
with pytest.raises(ValueError):
EntryPoint.parse(reject_spec)
def test_printable_name(self):
"""
Allow any printable character in the name.
"""
# Create a name with all printable characters; strip the whitespace.
name = string.printable.strip()
spec = "{name} = module:attr".format(**locals())
ep = EntryPoint.parse(spec)
assert ep.name == name
def checkSubMap(self, m):
assert len(m) == len(self.submap_expect)
for key, ep in self.submap_expect.items():
assert m.get(key).name == ep.name
assert m.get(key).module_name == ep.module_name
assert sorted(m.get(key).attrs) == sorted(ep.attrs)
assert sorted(m.get(key).extras) == sorted(ep.extras)
submap_expect = dict(
feature1=EntryPoint('feature1', 'somemodule', ['somefunction']),
feature2=EntryPoint(
'feature2', 'another.module', ['SomeClass'], ['extra1', 'extra2']
),
feature3=EntryPoint('feature3', 'this.module', extras=['something']),
)
submap_str = """
# define features for blah blah
feature1 = somemodule:somefunction
feature2 = another.module:SomeClass [extra1,extra2]
feature3 = this.module [something]
"""
def testParseList(self):
self.checkSubMap(EntryPoint.parse_group("xyz", self.submap_str))
with pytest.raises(ValueError):
EntryPoint.parse_group("x a", "foo=bar")
with pytest.raises(ValueError):
EntryPoint.parse_group("x", ["foo=baz", "foo=bar"])
def testParseMap(self):
m = EntryPoint.parse_map({'xyz': self.submap_str})
self.checkSubMap(m['xyz'])
assert list(m.keys()) == ['xyz']
m = EntryPoint.parse_map("[xyz]\n" + self.submap_str)
self.checkSubMap(m['xyz'])
assert list(m.keys()) == ['xyz']
with pytest.raises(ValueError):
EntryPoint.parse_map(["[xyz]", "[xyz]"])
with pytest.raises(ValueError):
EntryPoint.parse_map(self.submap_str)
def testDeprecationWarnings(self):
ep = EntryPoint(
"foo", "pkg_resources.tests.test_resources", ["TestEntryPoints"], ["x"]
)
with pytest.warns(pkg_resources.PkgResourcesDeprecationWarning):
ep.load(require=False)
class TestRequirements:
def testBasics(self):
r = Requirement.parse("Twisted>=1.2")
assert str(r) == "Twisted>=1.2"
assert repr(r) == "Requirement.parse('Twisted>=1.2')"
assert r == Requirement("Twisted>=1.2")
assert r == Requirement("twisTed>=1.2")
assert r != Requirement("Twisted>=2.0")
assert r != Requirement("Zope>=1.2")
assert r != Requirement("Zope>=3.0")
assert r != Requirement("Twisted[extras]>=1.2")
def testOrdering(self):
r1 = Requirement("Twisted==1.2c1,>=1.2")
r2 = Requirement("Twisted>=1.2,==1.2c1")
assert r1 == r2
assert str(r1) == str(r2)
assert str(r2) == "Twisted==1.2c1,>=1.2"
assert Requirement("Twisted") != Requirement(
"Twisted @ https://localhost/twisted.zip"
)
def testBasicContains(self):
r = Requirement("Twisted>=1.2")
foo_dist = Distribution.from_filename("FooPkg-1.3_1.egg")
twist11 = Distribution.from_filename("Twisted-1.1.egg")
twist12 = Distribution.from_filename("Twisted-1.2.egg")
assert parse_version('1.2') in r
assert parse_version('1.1') not in r
assert '1.2' in r
assert '1.1' not in r
assert foo_dist not in r
assert twist11 not in r
assert twist12 in r
def testOptionsAndHashing(self):
r1 = Requirement.parse("Twisted[foo,bar]>=1.2")
r2 = Requirement.parse("Twisted[bar,FOO]>=1.2")
assert r1 == r2
assert set(r1.extras) == set(("foo", "bar"))
assert set(r2.extras) == set(("foo", "bar"))
assert hash(r1) == hash(r2)
assert hash(r1) == hash((
"twisted",
None,
SpecifierSet(">=1.2"),
frozenset(["foo", "bar"]),
None,
))
assert hash(
Requirement.parse("Twisted @ https://localhost/twisted.zip")
) == hash((
"twisted",
"https://localhost/twisted.zip",
SpecifierSet(),
frozenset(),
None,
))
def testVersionEquality(self):
r1 = Requirement.parse("foo==0.3a2")
r2 = Requirement.parse("foo!=0.3a4")
d = Distribution.from_filename
assert d("foo-0.3a4.egg") not in r1
assert d("foo-0.3a1.egg") not in r1
assert d("foo-0.3a4.egg") not in r2
assert d("foo-0.3a2.egg") in r1
assert d("foo-0.3a2.egg") in r2
assert d("foo-0.3a3.egg") in r2
assert d("foo-0.3a5.egg") in r2
def testSetuptoolsProjectName(self):
"""
The setuptools project should implement the setuptools package.
"""
assert Requirement.parse('setuptools').project_name == 'setuptools'
# setuptools 0.7 and higher means setuptools.
assert Requirement.parse('setuptools == 0.7').project_name == 'setuptools'
assert Requirement.parse('setuptools == 0.7a1').project_name == 'setuptools'
assert Requirement.parse('setuptools >= 0.7').project_name == 'setuptools'
class TestParsing:
def testEmptyParse(self):
assert list(parse_requirements('')) == []
def testYielding(self):
for inp, out in [
([], []),
('x', ['x']),
([[]], []),
(' x\n y', ['x', 'y']),
(['x\n\n', 'y'], ['x', 'y']),
]:
assert list(pkg_resources.yield_lines(inp)) == out
def testSplitting(self):
sample = """
x
[Y]
z
a
[b ]
# foo
c
[ d]
[q]
v
"""
assert list(pkg_resources.split_sections(sample)) == [
(None, ["x"]),
("Y", ["z", "a"]),
("b", ["c"]),
("d", []),
("q", ["v"]),
]
with pytest.raises(ValueError):
list(pkg_resources.split_sections("[foo"))
def testSafeName(self):
assert safe_name("adns-python") == "adns-python"
assert safe_name("WSGI Utils") == "WSGI-Utils"
assert safe_name("WSGI Utils") == "WSGI-Utils"
assert safe_name("Money$$$Maker") == "Money-Maker"
assert safe_name("peak.web") != "peak-web"
def testSafeVersion(self):
assert safe_version("1.2-1") == "1.2.post1"
assert safe_version("1.2 alpha") == "1.2.alpha"
assert safe_version("2.3.4 20050521") == "2.3.4.20050521"
assert safe_version("Money$$$Maker") == "Money-Maker"
assert safe_version("peak.web") == "peak.web"
def testSimpleRequirements(self):
assert list(parse_requirements('Twis-Ted>=1.2-1')) == [
Requirement('Twis-Ted>=1.2-1')
]
assert list(parse_requirements('Twisted >=1.2, \\ # more\n<2.0')) == [
Requirement('Twisted>=1.2,<2.0')
]
assert Requirement.parse("FooBar==1.99a3") == Requirement("FooBar==1.99a3")
with pytest.raises(ValueError):
Requirement.parse(">=2.3")
with pytest.raises(ValueError):
Requirement.parse("x\\")
with pytest.raises(ValueError):
Requirement.parse("x==2 q")
with pytest.raises(ValueError):
Requirement.parse("X==1\nY==2")
with pytest.raises(ValueError):
Requirement.parse("#")
def test_requirements_with_markers(self):
assert Requirement.parse("foobar;os_name=='a'") == Requirement.parse(
"foobar;os_name=='a'"
)
assert Requirement.parse(
"name==1.1;python_version=='2.7'"
) != Requirement.parse("name==1.1;python_version=='3.6'")
assert Requirement.parse(
"name==1.0;python_version=='2.7'"
) != Requirement.parse("name==1.2;python_version=='2.7'")
assert Requirement.parse(
"name[foo]==1.0;python_version=='3.6'"
) != Requirement.parse("name[foo,bar]==1.0;python_version=='3.6'")
def test_local_version(self):
(req,) = parse_requirements('foo==1.0+org1')
def test_spaces_between_multiple_versions(self):
(req,) = parse_requirements('foo>=1.0, <3')
(req,) = parse_requirements('foo >= 1.0, < 3')
@pytest.mark.parametrize(
['lower', 'upper'],
[
('1.2-rc1', '1.2rc1'),
('0.4', '0.4.0'),
('0.4.0.0', '0.4.0'),
('0.4.0-0', '0.4-0'),
('0post1', '0.0post1'),
('0pre1', '0.0c1'),
('0.0.0preview1', '0c1'),
('0.0c1', '0-rc1'),
('1.2a1', '1.2.a.1'),
('1.2.a', '1.2a'),
],
)
def testVersionEquality(self, lower, upper):
assert parse_version(lower) == parse_version(upper)
torture = """
0.80.1-3 0.80.1-2 0.80.1-1 0.79.9999+0.80.0pre4-1
0.79.9999+0.80.0pre2-3 0.79.9999+0.80.0pre2-2
0.77.2-1 0.77.1-1 0.77.0-1
"""
@pytest.mark.parametrize(
['lower', 'upper'],
[
('2.1', '2.1.1'),
('2a1', '2b0'),
('2a1', '2.1'),
('2.3a1', '2.3'),
('2.1-1', '2.1-2'),
('2.1-1', '2.1.1'),
('2.1', '2.1post4'),
('2.1a0-20040501', '2.1'),
('1.1', '02.1'),
('3.2', '3.2.post0'),
('3.2post1', '3.2post2'),
('0.4', '4.0'),
('0.0.4', '0.4.0'),
('0post1', '0.4post1'),
('2.1.0-rc1', '2.1.0'),
('2.1dev', '2.1a0'),
]
+ list(pairwise(reversed(torture.split()))),
)
def testVersionOrdering(self, lower, upper):
assert parse_version(lower) < parse_version(upper)
def testVersionHashable(self):
"""
Ensure that our versions stay hashable even though we've subclassed
them and added some shim code to them.
"""
assert hash(parse_version("1.0")) == hash(parse_version("1.0"))
class TestNamespaces:
ns_str = "__import__('pkg_resources').declare_namespace(__name__)\n"
@pytest.fixture
def symlinked_tmpdir(self, tmpdir):
"""
Where available, return the tempdir as a symlink,
which as revealed in #231 is more fragile than
a natural tempdir.
"""
if not hasattr(os, 'symlink'):
yield str(tmpdir)
return
link_name = str(tmpdir) + '-linked'
os.symlink(str(tmpdir), link_name)
try:
yield type(tmpdir)(link_name)
finally:
os.unlink(link_name)
@pytest.fixture(autouse=True)
def patched_path(self, tmpdir):
"""
Patch sys.path to include the 'site-pkgs' dir. Also
restore pkg_resources._namespace_packages to its
former state.
"""
saved_ns_pkgs = pkg_resources._namespace_packages.copy()
saved_sys_path = sys.path[:]
site_pkgs = tmpdir.mkdir('site-pkgs')
sys.path.append(str(site_pkgs))
try:
yield
finally:
pkg_resources._namespace_packages = saved_ns_pkgs
sys.path = saved_sys_path
issue591 = pytest.mark.xfail(platform.system() == 'Windows', reason="#591")
@issue591
def test_two_levels_deep(self, symlinked_tmpdir):
"""
Test nested namespace packages
Create namespace packages in the following tree :
site-packages-1/pkg1/pkg2
site-packages-2/pkg1/pkg2
Check both are in the _namespace_packages dict and that their __path__
is correct
"""
real_tmpdir = symlinked_tmpdir.realpath()
tmpdir = symlinked_tmpdir
sys.path.append(str(tmpdir / 'site-pkgs2'))
site_dirs = tmpdir / 'site-pkgs', tmpdir / 'site-pkgs2'
for site in site_dirs:
pkg1 = site / 'pkg1'
pkg2 = pkg1 / 'pkg2'
pkg2.ensure_dir()
(pkg1 / '__init__.py').write_text(self.ns_str, encoding='utf-8')
(pkg2 / '__init__.py').write_text(self.ns_str, encoding='utf-8')
with pytest.warns(DeprecationWarning, match="pkg_resources.declare_namespace"):
import pkg1 # pyright: ignore[reportMissingImports] # Temporary package for test
assert "pkg1" in pkg_resources._namespace_packages
# attempt to import pkg2 from site-pkgs2
with pytest.warns(DeprecationWarning, match="pkg_resources.declare_namespace"):
import pkg1.pkg2 # pyright: ignore[reportMissingImports] # Temporary package for test
# check the _namespace_packages dict
assert "pkg1.pkg2" in pkg_resources._namespace_packages
assert pkg_resources._namespace_packages["pkg1"] == ["pkg1.pkg2"]
# check the __path__ attribute contains both paths
expected = [
str(real_tmpdir / "site-pkgs" / "pkg1" / "pkg2"),
str(real_tmpdir / "site-pkgs2" / "pkg1" / "pkg2"),
]
assert pkg1.pkg2.__path__ == expected
@issue591
def test_path_order(self, symlinked_tmpdir):
"""
Test that if multiple versions of the same namespace package subpackage
are on different sys.path entries, that only the one earliest on
sys.path is imported, and that the namespace package's __path__ is in
the correct order.
Regression test for https://github.com/pypa/setuptools/issues/207
"""
tmpdir = symlinked_tmpdir
site_dirs = (
tmpdir / "site-pkgs",
tmpdir / "site-pkgs2",
tmpdir / "site-pkgs3",
)
vers_str = "__version__ = %r"
for number, site in enumerate(site_dirs, 1):
if number > 1:
sys.path.append(str(site))
nspkg = site / 'nspkg'
subpkg = nspkg / 'subpkg'
subpkg.ensure_dir()
(nspkg / '__init__.py').write_text(self.ns_str, encoding='utf-8')
(subpkg / '__init__.py').write_text(vers_str % number, encoding='utf-8')
with pytest.warns(DeprecationWarning, match="pkg_resources.declare_namespace"):
import nspkg # pyright: ignore[reportMissingImports] # Temporary package for test
import nspkg.subpkg # pyright: ignore[reportMissingImports] # Temporary package for test
expected = [str(site.realpath() / 'nspkg') for site in site_dirs]
assert nspkg.__path__ == expected
assert nspkg.subpkg.__version__ == 1

View File

@ -0,0 +1,501 @@
import functools
import inspect
import re
import textwrap
import pytest
import pkg_resources
from .test_resources import Metadata
def strip_comments(s):
return '\n'.join(
line
for line in s.split('\n')
if line.strip() and not line.strip().startswith('#')
)
def parse_distributions(s):
"""
Parse a series of distribution specs of the form:
{project_name}-{version}
[optional, indented requirements specification]
Example:
foo-0.2
bar-1.0
foo>=3.0
[feature]
baz
yield 2 distributions:
- project_name=foo, version=0.2
- project_name=bar, version=1.0,
requires=['foo>=3.0', 'baz; extra=="feature"']
"""
s = s.strip()
for spec in re.split(r'\n(?=[^\s])', s):
if not spec:
continue
fields = spec.split('\n', 1)
assert 1 <= len(fields) <= 2
name, version = fields.pop(0).rsplit('-', 1)
if fields:
requires = textwrap.dedent(fields.pop(0))
metadata = Metadata(('requires.txt', requires))
else:
metadata = None
dist = pkg_resources.Distribution(
project_name=name, version=version, metadata=metadata
)
yield dist
class FakeInstaller:
def __init__(self, installable_dists):
self._installable_dists = installable_dists
def __call__(self, req):
return next(
iter(filter(lambda dist: dist in req, self._installable_dists)), None
)
def parametrize_test_working_set_resolve(*test_list):
idlist = []
argvalues = []
for test in test_list:
(
name,
installed_dists,
installable_dists,
requirements,
expected1,
expected2,
) = (
strip_comments(s.lstrip())
for s in textwrap.dedent(test).lstrip().split('\n\n', 5)
)
installed_dists = list(parse_distributions(installed_dists))
installable_dists = list(parse_distributions(installable_dists))
requirements = list(pkg_resources.parse_requirements(requirements))
for id_, replace_conflicting, expected in (
(name, False, expected1),
(name + '_replace_conflicting', True, expected2),
):
idlist.append(id_)
expected = strip_comments(expected.strip())
if re.match(r'\w+$', expected):
expected = getattr(pkg_resources, expected)
assert issubclass(expected, Exception)
else:
expected = list(parse_distributions(expected))
argvalues.append(
pytest.param(
installed_dists,
installable_dists,
requirements,
replace_conflicting,
expected,
)
)
return pytest.mark.parametrize(
'installed_dists,installable_dists,'
'requirements,replace_conflicting,'
'resolved_dists_or_exception',
argvalues,
ids=idlist,
)
@parametrize_test_working_set_resolve(
"""
# id
noop
# installed
# installable
# wanted
# resolved
# resolved [replace conflicting]
""",
"""
# id
already_installed
# installed
foo-3.0
# installable
# wanted
foo>=2.1,!=3.1,<4
# resolved
foo-3.0
# resolved [replace conflicting]
foo-3.0
""",
"""
# id
installable_not_installed
# installed
# installable
foo-3.0
foo-4.0
# wanted
foo>=2.1,!=3.1,<4
# resolved
foo-3.0
# resolved [replace conflicting]
foo-3.0
""",
"""
# id
not_installable
# installed
# installable
# wanted
foo>=2.1,!=3.1,<4
# resolved
DistributionNotFound
# resolved [replace conflicting]
DistributionNotFound
""",
"""
# id
no_matching_version
# installed
# installable
foo-3.1
# wanted
foo>=2.1,!=3.1,<4
# resolved
DistributionNotFound
# resolved [replace conflicting]
DistributionNotFound
""",
"""
# id
installable_with_installed_conflict
# installed
foo-3.1
# installable
foo-3.5
# wanted
foo>=2.1,!=3.1,<4
# resolved
VersionConflict
# resolved [replace conflicting]
foo-3.5
""",
"""
# id
not_installable_with_installed_conflict
# installed
foo-3.1
# installable
# wanted
foo>=2.1,!=3.1,<4
# resolved
VersionConflict
# resolved [replace conflicting]
DistributionNotFound
""",
"""
# id
installed_with_installed_require
# installed
foo-3.9
baz-0.1
foo>=2.1,!=3.1,<4
# installable
# wanted
baz
# resolved
foo-3.9
baz-0.1
# resolved [replace conflicting]
foo-3.9
baz-0.1
""",
"""
# id
installed_with_conflicting_installed_require
# installed
foo-5
baz-0.1
foo>=2.1,!=3.1,<4
# installable
# wanted
baz
# resolved
VersionConflict
# resolved [replace conflicting]
DistributionNotFound
""",
"""
# id
installed_with_installable_conflicting_require
# installed
foo-5
baz-0.1
foo>=2.1,!=3.1,<4
# installable
foo-2.9
# wanted
baz
# resolved
VersionConflict
# resolved [replace conflicting]
baz-0.1
foo-2.9
""",
"""
# id
installed_with_installable_require
# installed
baz-0.1
foo>=2.1,!=3.1,<4
# installable
foo-3.9
# wanted
baz
# resolved
foo-3.9
baz-0.1
# resolved [replace conflicting]
foo-3.9
baz-0.1
""",
"""
# id
installable_with_installed_require
# installed
foo-3.9
# installable
baz-0.1
foo>=2.1,!=3.1,<4
# wanted
baz
# resolved
foo-3.9
baz-0.1
# resolved [replace conflicting]
foo-3.9
baz-0.1
""",
"""
# id
installable_with_installable_require
# installed
# installable
foo-3.9
baz-0.1
foo>=2.1,!=3.1,<4
# wanted
baz
# resolved
foo-3.9
baz-0.1
# resolved [replace conflicting]
foo-3.9
baz-0.1
""",
"""
# id
installable_with_conflicting_installable_require
# installed
foo-5
# installable
foo-2.9
baz-0.1
foo>=2.1,!=3.1,<4
# wanted
baz
# resolved
VersionConflict
# resolved [replace conflicting]
baz-0.1
foo-2.9
""",
"""
# id
conflicting_installables
# installed
# installable
foo-2.9
foo-5.0
# wanted
foo>=2.1,!=3.1,<4
foo>=4
# resolved
VersionConflict
# resolved [replace conflicting]
VersionConflict
""",
"""
# id
installables_with_conflicting_requires
# installed
# installable
foo-2.9
dep==1.0
baz-5.0
dep==2.0
dep-1.0
dep-2.0
# wanted
foo
baz
# resolved
VersionConflict
# resolved [replace conflicting]
VersionConflict
""",
"""
# id
installables_with_conflicting_nested_requires
# installed
# installable
foo-2.9
dep1
dep1-1.0
subdep<1.0
baz-5.0
dep2
dep2-1.0
subdep>1.0
subdep-0.9
subdep-1.1
# wanted
foo
baz
# resolved
VersionConflict
# resolved [replace conflicting]
VersionConflict
""",
"""
# id
wanted_normalized_name_installed_canonical
# installed
foo.bar-3.6
# installable
# wanted
foo-bar==3.6
# resolved
foo.bar-3.6
# resolved [replace conflicting]
foo.bar-3.6
""",
)
def test_working_set_resolve(
installed_dists,
installable_dists,
requirements,
replace_conflicting,
resolved_dists_or_exception,
):
ws = pkg_resources.WorkingSet([])
list(map(ws.add, installed_dists))
resolve_call = functools.partial(
ws.resolve,
requirements,
installer=FakeInstaller(installable_dists),
replace_conflicting=replace_conflicting,
)
if inspect.isclass(resolved_dists_or_exception):
with pytest.raises(resolved_dists_or_exception):
resolve_call()
else:
assert sorted(resolve_call()) == sorted(resolved_dists_or_exception)