1"""Tests for the 'setuptools' package"""
2
3import sys
4import os
5import distutils.core
6import distutils.cmd
7from distutils.errors import DistutilsOptionError, DistutilsPlatformError
8from distutils.errors import DistutilsSetupError
9from distutils.core import Extension
10from distutils.version import LooseVersion
11
12import pytest
13
14import setuptools
15import setuptools.dist
16import setuptools.depends as dep
17from setuptools import Feature
18from setuptools.depends import Require
19from setuptools.extern import six
20
21
22def makeSetup(**args):
23    """Return distribution from 'setup(**args)', without executing commands"""
24
25    distutils.core._setup_stop_after = "commandline"
26
27    # Don't let system command line leak into tests!
28    args.setdefault('script_args', ['install'])
29
30    try:
31        return setuptools.setup(**args)
32    finally:
33        distutils.core._setup_stop_after = None
34
35
36needs_bytecode = pytest.mark.skipif(
37    not hasattr(dep, 'get_module_constant'),
38    reason="bytecode support not available",
39)
40
41
42class TestDepends:
43    def testExtractConst(self):
44        if not hasattr(dep, 'extract_constant'):
45            # skip on non-bytecode platforms
46            return
47
48        def f1():
49            global x, y, z
50            x = "test"
51            y = z
52
53        fc = six.get_function_code(f1)
54
55        # unrecognized name
56        assert dep.extract_constant(fc, 'q', -1) is None
57
58        # constant assigned
59        dep.extract_constant(fc, 'x', -1) == "test"
60
61        # expression assigned
62        dep.extract_constant(fc, 'y', -1) == -1
63
64        # recognized name, not assigned
65        dep.extract_constant(fc, 'z', -1) is None
66
67    def testFindModule(self):
68        with pytest.raises(ImportError):
69            dep.find_module('no-such.-thing')
70        with pytest.raises(ImportError):
71            dep.find_module('setuptools.non-existent')
72        f, p, i = dep.find_module('setuptools.tests')
73        f.close()
74
75    @needs_bytecode
76    def testModuleExtract(self):
77        from json import __version__
78        assert dep.get_module_constant('json', '__version__') == __version__
79        assert dep.get_module_constant('sys', 'version') == sys.version
80        assert dep.get_module_constant('setuptools.tests.test_setuptools', '__doc__') == __doc__
81
82    @needs_bytecode
83    def testRequire(self):
84        req = Require('Json', '1.0.3', 'json')
85
86        assert req.name == 'Json'
87        assert req.module == 'json'
88        assert req.requested_version == '1.0.3'
89        assert req.attribute == '__version__'
90        assert req.full_name() == 'Json-1.0.3'
91
92        from json import __version__
93        assert req.get_version() == __version__
94        assert req.version_ok('1.0.9')
95        assert not req.version_ok('0.9.1')
96        assert not req.version_ok('unknown')
97
98        assert req.is_present()
99        assert req.is_current()
100
101        req = Require('Json 3000', '03000', 'json', format=LooseVersion)
102        assert req.is_present()
103        assert not req.is_current()
104        assert not req.version_ok('unknown')
105
106        req = Require('Do-what-I-mean', '1.0', 'd-w-i-m')
107        assert not req.is_present()
108        assert not req.is_current()
109
110        req = Require('Tests', None, 'tests', homepage="http://example.com")
111        assert req.format is None
112        assert req.attribute is None
113        assert req.requested_version is None
114        assert req.full_name() == 'Tests'
115        assert req.homepage == 'http://example.com'
116
117        from setuptools.tests import __path__
118        paths = [os.path.dirname(p) for p in __path__]
119        assert req.is_present(paths)
120        assert req.is_current(paths)
121
122
123class TestDistro:
124    def setup_method(self, method):
125        self.e1 = Extension('bar.ext', ['bar.c'])
126        self.e2 = Extension('c.y', ['y.c'])
127
128        self.dist = makeSetup(
129            packages=['a', 'a.b', 'a.b.c', 'b', 'c'],
130            py_modules=['b.d', 'x'],
131            ext_modules=(self.e1, self.e2),
132            package_dir={},
133        )
134
135    def testDistroType(self):
136        assert isinstance(self.dist, setuptools.dist.Distribution)
137
138    def testExcludePackage(self):
139        self.dist.exclude_package('a')
140        assert self.dist.packages == ['b', 'c']
141
142        self.dist.exclude_package('b')
143        assert self.dist.packages == ['c']
144        assert self.dist.py_modules == ['x']
145        assert self.dist.ext_modules == [self.e1, self.e2]
146
147        self.dist.exclude_package('c')
148        assert self.dist.packages == []
149        assert self.dist.py_modules == ['x']
150        assert self.dist.ext_modules == [self.e1]
151
152        # test removals from unspecified options
153        makeSetup().exclude_package('x')
154
155    def testIncludeExclude(self):
156        # remove an extension
157        self.dist.exclude(ext_modules=[self.e1])
158        assert self.dist.ext_modules == [self.e2]
159
160        # add it back in
161        self.dist.include(ext_modules=[self.e1])
162        assert self.dist.ext_modules == [self.e2, self.e1]
163
164        # should not add duplicate
165        self.dist.include(ext_modules=[self.e1])
166        assert self.dist.ext_modules == [self.e2, self.e1]
167
168    def testExcludePackages(self):
169        self.dist.exclude(packages=['c', 'b', 'a'])
170        assert self.dist.packages == []
171        assert self.dist.py_modules == ['x']
172        assert self.dist.ext_modules == [self.e1]
173
174    def testEmpty(self):
175        dist = makeSetup()
176        dist.include(packages=['a'], py_modules=['b'], ext_modules=[self.e2])
177        dist = makeSetup()
178        dist.exclude(packages=['a'], py_modules=['b'], ext_modules=[self.e2])
179
180    def testContents(self):
181        assert self.dist.has_contents_for('a')
182        self.dist.exclude_package('a')
183        assert not self.dist.has_contents_for('a')
184
185        assert self.dist.has_contents_for('b')
186        self.dist.exclude_package('b')
187        assert not self.dist.has_contents_for('b')
188
189        assert self.dist.has_contents_for('c')
190        self.dist.exclude_package('c')
191        assert not self.dist.has_contents_for('c')
192
193    def testInvalidIncludeExclude(self):
194        with pytest.raises(DistutilsSetupError):
195            self.dist.include(nonexistent_option='x')
196        with pytest.raises(DistutilsSetupError):
197            self.dist.exclude(nonexistent_option='x')
198        with pytest.raises(DistutilsSetupError):
199            self.dist.include(packages={'x': 'y'})
200        with pytest.raises(DistutilsSetupError):
201            self.dist.exclude(packages={'x': 'y'})
202        with pytest.raises(DistutilsSetupError):
203            self.dist.include(ext_modules={'x': 'y'})
204        with pytest.raises(DistutilsSetupError):
205            self.dist.exclude(ext_modules={'x': 'y'})
206
207        with pytest.raises(DistutilsSetupError):
208            self.dist.include(package_dir=['q'])
209        with pytest.raises(DistutilsSetupError):
210            self.dist.exclude(package_dir=['q'])
211
212
213class TestFeatures:
214    def setup_method(self, method):
215        self.req = Require('Distutils', '1.0.3', 'distutils')
216        self.dist = makeSetup(
217            features={
218                'foo': Feature("foo", standard=True, require_features=['baz', self.req]),
219                'bar': Feature("bar", standard=True, packages=['pkg.bar'],
220                               py_modules=['bar_et'], remove=['bar.ext'],
221                               ),
222                'baz': Feature(
223                        "baz", optional=False, packages=['pkg.baz'],
224                        scripts=['scripts/baz_it'],
225                        libraries=[('libfoo', 'foo/foofoo.c')]
226                       ),
227                'dwim': Feature("DWIM", available=False, remove='bazish'),
228            },
229            script_args=['--without-bar', 'install'],
230            packages=['pkg.bar', 'pkg.foo'],
231            py_modules=['bar_et', 'bazish'],
232            ext_modules=[Extension('bar.ext', ['bar.c'])]
233        )
234
235    def testDefaults(self):
236        assert not Feature(
237            "test", standard=True, remove='x', available=False
238        ).include_by_default()
239        assert Feature("test", standard=True, remove='x').include_by_default()
240        # Feature must have either kwargs, removes, or require_features
241        with pytest.raises(DistutilsSetupError):
242            Feature("test")
243
244    def testAvailability(self):
245        with pytest.raises(DistutilsPlatformError):
246            self.dist.features['dwim'].include_in(self.dist)
247
248    def testFeatureOptions(self):
249        dist = self.dist
250        assert (
251            ('with-dwim', None, 'include DWIM') in dist.feature_options
252        )
253        assert (
254            ('without-dwim', None, 'exclude DWIM (default)') in dist.feature_options
255        )
256        assert (
257            ('with-bar', None, 'include bar (default)') in dist.feature_options
258        )
259        assert (
260            ('without-bar', None, 'exclude bar') in dist.feature_options
261        )
262        assert dist.feature_negopt['without-foo'] == 'with-foo'
263        assert dist.feature_negopt['without-bar'] == 'with-bar'
264        assert dist.feature_negopt['without-dwim'] == 'with-dwim'
265        assert ('without-baz' not in dist.feature_negopt)
266
267    def testUseFeatures(self):
268        dist = self.dist
269        assert dist.with_foo == 1
270        assert dist.with_bar == 0
271        assert dist.with_baz == 1
272        assert ('bar_et' not in dist.py_modules)
273        assert ('pkg.bar' not in dist.packages)
274        assert ('pkg.baz' in dist.packages)
275        assert ('scripts/baz_it' in dist.scripts)
276        assert (('libfoo', 'foo/foofoo.c') in dist.libraries)
277        assert dist.ext_modules == []
278        assert dist.require_features == [self.req]
279
280        # If we ask for bar, it should fail because we explicitly disabled
281        # it on the command line
282        with pytest.raises(DistutilsOptionError):
283            dist.include_feature('bar')
284
285    def testFeatureWithInvalidRemove(self):
286        with pytest.raises(SystemExit):
287            makeSetup(features={'x': Feature('x', remove='y')})
288
289
290class TestCommandTests:
291    def testTestIsCommand(self):
292        test_cmd = makeSetup().get_command_obj('test')
293        assert (isinstance(test_cmd, distutils.cmd.Command))
294
295    def testLongOptSuiteWNoDefault(self):
296        ts1 = makeSetup(script_args=['test', '--test-suite=foo.tests.suite'])
297        ts1 = ts1.get_command_obj('test')
298        ts1.ensure_finalized()
299        assert ts1.test_suite == 'foo.tests.suite'
300
301    def testDefaultSuite(self):
302        ts2 = makeSetup(test_suite='bar.tests.suite').get_command_obj('test')
303        ts2.ensure_finalized()
304        assert ts2.test_suite == 'bar.tests.suite'
305
306    def testDefaultWModuleOnCmdLine(self):
307        ts3 = makeSetup(
308            test_suite='bar.tests',
309            script_args=['test', '-m', 'foo.tests']
310        ).get_command_obj('test')
311        ts3.ensure_finalized()
312        assert ts3.test_module == 'foo.tests'
313        assert ts3.test_suite == 'foo.tests.test_suite'
314
315    def testConflictingOptions(self):
316        ts4 = makeSetup(
317            script_args=['test', '-m', 'bar.tests', '-s', 'foo.tests.suite']
318        ).get_command_obj('test')
319        with pytest.raises(DistutilsOptionError):
320            ts4.ensure_finalized()
321
322    def testNoSuite(self):
323        ts5 = makeSetup().get_command_obj('test')
324        ts5.ensure_finalized()
325        assert ts5.test_suite is None
326
327
328@pytest.fixture
329def example_source(tmpdir):
330    tmpdir.mkdir('foo')
331    (tmpdir / 'foo/bar.py').write('')
332    (tmpdir / 'readme.txt').write('')
333    return tmpdir
334
335
336def test_findall(example_source):
337    found = list(setuptools.findall(str(example_source)))
338    expected = ['readme.txt', 'foo/bar.py']
339    expected = [example_source.join(fn) for fn in expected]
340    assert found == expected
341
342
343def test_findall_curdir(example_source):
344    with example_source.as_cwd():
345        found = list(setuptools.findall())
346    expected = ['readme.txt', os.path.join('foo', 'bar.py')]
347    assert found == expected
348
349
350@pytest.fixture
351def can_symlink(tmpdir):
352    """
353    Skip if cannot create a symbolic link
354    """
355    link_fn = 'link'
356    target_fn = 'target'
357    try:
358        os.symlink(target_fn, link_fn)
359    except (OSError, NotImplementedError, AttributeError):
360        pytest.skip("Cannot create symbolic links")
361    os.remove(link_fn)
362
363
364def test_findall_missing_symlink(tmpdir, can_symlink):
365    with tmpdir.as_cwd():
366        os.symlink('foo', 'bar')
367        found = list(setuptools.findall())
368        assert found == []
369