1# coding: utf-8
2from __future__ import unicode_literals
3
4import sys
5import tempfile
6import os
7import zipfile
8import datetime
9import time
10import subprocess
11import stat
12import distutils.dist
13import distutils.command.install_egg_info
14
15from pkg_resources.extern.six.moves import map
16
17import pytest
18
19import pkg_resources
20
21try:
22    unicode
23except NameError:
24    unicode = str
25
26
27def timestamp(dt):
28    """
29    Return a timestamp for a local, naive datetime instance.
30    """
31    try:
32        return dt.timestamp()
33    except AttributeError:
34        # Python 3.2 and earlier
35        return time.mktime(dt.timetuple())
36
37
38class EggRemover(unicode):
39    def __call__(self):
40        if self in sys.path:
41            sys.path.remove(self)
42        if os.path.exists(self):
43            os.remove(self)
44
45
46class TestZipProvider(object):
47    finalizers = []
48
49    ref_time = datetime.datetime(2013, 5, 12, 13, 25, 0)
50    "A reference time for a file modification"
51
52    @classmethod
53    def setup_class(cls):
54        "create a zip egg and add it to sys.path"
55        egg = tempfile.NamedTemporaryFile(suffix='.egg', delete=False)
56        zip_egg = zipfile.ZipFile(egg, 'w')
57        zip_info = zipfile.ZipInfo()
58        zip_info.filename = 'mod.py'
59        zip_info.date_time = cls.ref_time.timetuple()
60        zip_egg.writestr(zip_info, 'x = 3\n')
61        zip_info = zipfile.ZipInfo()
62        zip_info.filename = 'data.dat'
63        zip_info.date_time = cls.ref_time.timetuple()
64        zip_egg.writestr(zip_info, 'hello, world!')
65        zip_info = zipfile.ZipInfo()
66        zip_info.filename = 'subdir/mod2.py'
67        zip_info.date_time = cls.ref_time.timetuple()
68        zip_egg.writestr(zip_info, 'x = 6\n')
69        zip_info = zipfile.ZipInfo()
70        zip_info.filename = 'subdir/data2.dat'
71        zip_info.date_time = cls.ref_time.timetuple()
72        zip_egg.writestr(zip_info, 'goodbye, world!')
73        zip_egg.close()
74        egg.close()
75
76        sys.path.append(egg.name)
77        subdir = os.path.join(egg.name, 'subdir')
78        sys.path.append(subdir)
79        cls.finalizers.append(EggRemover(subdir))
80        cls.finalizers.append(EggRemover(egg.name))
81
82    @classmethod
83    def teardown_class(cls):
84        for finalizer in cls.finalizers:
85            finalizer()
86
87    def test_resource_listdir(self):
88        import mod
89        zp = pkg_resources.ZipProvider(mod)
90
91        expected_root = ['data.dat', 'mod.py', 'subdir']
92        assert sorted(zp.resource_listdir('')) == expected_root
93        assert sorted(zp.resource_listdir('/')) == expected_root
94
95        expected_subdir = ['data2.dat', 'mod2.py']
96        assert sorted(zp.resource_listdir('subdir')) == expected_subdir
97        assert sorted(zp.resource_listdir('subdir/')) == expected_subdir
98
99        assert zp.resource_listdir('nonexistent') == []
100        assert zp.resource_listdir('nonexistent/') == []
101
102        import mod2
103        zp2 = pkg_resources.ZipProvider(mod2)
104
105        assert sorted(zp2.resource_listdir('')) == expected_subdir
106        assert sorted(zp2.resource_listdir('/')) == expected_subdir
107
108        assert zp2.resource_listdir('subdir') == []
109        assert zp2.resource_listdir('subdir/') == []
110
111    def test_resource_filename_rewrites_on_change(self):
112        """
113        If a previous call to get_resource_filename has saved the file, but
114        the file has been subsequently mutated with different file of the
115        same size and modification time, it should not be overwritten on a
116        subsequent call to get_resource_filename.
117        """
118        import mod
119        manager = pkg_resources.ResourceManager()
120        zp = pkg_resources.ZipProvider(mod)
121        filename = zp.get_resource_filename(manager, 'data.dat')
122        actual = datetime.datetime.fromtimestamp(os.stat(filename).st_mtime)
123        assert actual == self.ref_time
124        f = open(filename, 'w')
125        f.write('hello, world?')
126        f.close()
127        ts = timestamp(self.ref_time)
128        os.utime(filename, (ts, ts))
129        filename = zp.get_resource_filename(manager, 'data.dat')
130        with open(filename) as f:
131            assert f.read() == 'hello, world!'
132        manager.cleanup_resources()
133
134
135class TestResourceManager(object):
136    def test_get_cache_path(self):
137        mgr = pkg_resources.ResourceManager()
138        path = mgr.get_cache_path('foo')
139        type_ = str(type(path))
140        message = "Unexpected type from get_cache_path: " + type_
141        assert isinstance(path, (unicode, str)), message
142
143
144class TestIndependence:
145    """
146    Tests to ensure that pkg_resources runs independently from setuptools.
147    """
148
149    def test_setuptools_not_imported(self):
150        """
151        In a separate Python environment, import pkg_resources and assert
152        that action doesn't cause setuptools to be imported.
153        """
154        lines = (
155            'import pkg_resources',
156            'import sys',
157            (
158                'assert "setuptools" not in sys.modules, '
159                '"setuptools was imported"'
160            ),
161        )
162        cmd = [sys.executable, '-c', '; '.join(lines)]
163        subprocess.check_call(cmd)
164
165
166class TestDeepVersionLookupDistutils(object):
167    @pytest.fixture
168    def env(self, tmpdir):
169        """
170        Create a package environment, similar to a virtualenv,
171        in which packages are installed.
172        """
173
174        class Environment(str):
175            pass
176
177        env = Environment(tmpdir)
178        tmpdir.chmod(stat.S_IRWXU)
179        subs = 'home', 'lib', 'scripts', 'data', 'egg-base'
180        env.paths = dict(
181            (dirname, str(tmpdir / dirname))
182            for dirname in subs
183        )
184        list(map(os.mkdir, env.paths.values()))
185        return env
186
187    def create_foo_pkg(self, env, version):
188        """
189        Create a foo package installed (distutils-style) to env.paths['lib']
190        as version.
191        """
192        ld = "This package has unicode metadata! ❄"
193        attrs = dict(name='foo', version=version, long_description=ld)
194        dist = distutils.dist.Distribution(attrs)
195        iei_cmd = distutils.command.install_egg_info.install_egg_info(dist)
196        iei_cmd.initialize_options()
197        iei_cmd.install_dir = env.paths['lib']
198        iei_cmd.finalize_options()
199        iei_cmd.run()
200
201    def test_version_resolved_from_egg_info(self, env):
202        version = '1.11.0.dev0+2329eae'
203        self.create_foo_pkg(env, version)
204
205        # this requirement parsing will raise a VersionConflict unless the
206        # .egg-info file is parsed (see #419 on BitBucket)
207        req = pkg_resources.Requirement.parse('foo>=1.9')
208        dist = pkg_resources.WorkingSet([env.paths['lib']]).find(req)
209        assert dist.version == version
210