diff options
Diffstat (limited to 'pkg_resources/tests/test_pkg_resources.py')
-rw-r--r-- | pkg_resources/tests/test_pkg_resources.py | 240 |
1 files changed, 223 insertions, 17 deletions
diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index 7442b79..6518820 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -1,6 +1,3 @@ -# coding: utf-8 -from __future__ import unicode_literals - import sys import tempfile import os @@ -12,17 +9,19 @@ import stat import distutils.dist import distutils.command.install_egg_info -from pkg_resources.extern.six.moves import map +try: + from unittest import mock +except ImportError: + import mock + +from pkg_resources import ( + DistInfoDistribution, Distribution, EggInfoDistribution, +) import pytest import pkg_resources -try: - unicode -except NameError: - unicode = str - def timestamp(dt): """ @@ -35,7 +34,7 @@ def timestamp(dt): return time.mktime(dt.timetuple()) -class EggRemover(unicode): +class EggRemover(str): def __call__(self): if self in sys.path: sys.path.remove(self) @@ -43,7 +42,7 @@ class EggRemover(unicode): os.remove(self) -class TestZipProvider(object): +class TestZipProvider: finalizers = [] ref_time = datetime.datetime(2013, 5, 12, 13, 25, 0) @@ -90,7 +89,6 @@ class TestZipProvider(object): expected_root = ['data.dat', 'mod.py', 'subdir'] assert sorted(zp.resource_listdir('')) == expected_root - assert sorted(zp.resource_listdir('/')) == expected_root expected_subdir = ['data2.dat', 'mod2.py'] assert sorted(zp.resource_listdir('subdir')) == expected_subdir @@ -103,7 +101,6 @@ class TestZipProvider(object): zp2 = pkg_resources.ZipProvider(mod2) assert sorted(zp2.resource_listdir('')) == expected_subdir - assert sorted(zp2.resource_listdir('/')) == expected_subdir assert zp2.resource_listdir('subdir') == [] assert zp2.resource_listdir('subdir/') == [] @@ -132,16 +129,42 @@ class TestZipProvider(object): manager.cleanup_resources() -class TestResourceManager(object): +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, (unicode, str)), message + 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'] -class TestIndependence: """ Tests to ensure that pkg_resources runs independently from setuptools. """ @@ -163,7 +186,137 @@ class TestIndependence: subprocess.check_call(cmd) -class TestDeepVersionLookupDistutils(object): +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() + + if sys.version_info < (3, 6): + dist_dir = str(dist_dir) + + 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), + ], +) +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) == expected_dist_type + + +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) + + +class TestDeepVersionLookupDistutils: @pytest.fixture def env(self, tmpdir): """ @@ -207,3 +360,56 @@ class TestDeepVersionLookupDistutils(object): 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) |