1# -*- coding: utf-8 -*-
2"""sdist tests"""
3
4from __future__ import print_function, unicode_literals
5
6import os
7import sys
8import tempfile
9import unicodedata
10import contextlib
11import io
12
13from setuptools.extern import six
14from setuptools.extern.six.moves import map
15
16import pytest
17
18import pkg_resources
19from setuptools.command.sdist import sdist
20from setuptools.command.egg_info import manifest_maker
21from setuptools.dist import Distribution
22from setuptools.tests import fail_on_ascii
23from .text import Filenames
24from . import py3_only
25
26
27SETUP_ATTRS = {
28    'name': 'sdist_test',
29    'version': '0.0',
30    'packages': ['sdist_test'],
31    'package_data': {'sdist_test': ['*.txt']},
32    'data_files': [("data", [os.path.join("d", "e.dat")])],
33}
34
35SETUP_PY = """\
36from setuptools import setup
37
38setup(**%r)
39""" % SETUP_ATTRS
40
41
42@contextlib.contextmanager
43def quiet():
44    old_stdout, old_stderr = sys.stdout, sys.stderr
45    sys.stdout, sys.stderr = six.StringIO(), six.StringIO()
46    try:
47        yield
48    finally:
49        sys.stdout, sys.stderr = old_stdout, old_stderr
50
51
52# Convert to POSIX path
53def posix(path):
54    if not six.PY2 and not isinstance(path, str):
55        return path.replace(os.sep.encode('ascii'), b'/')
56    else:
57        return path.replace(os.sep, '/')
58
59
60# HFS Plus uses decomposed UTF-8
61def decompose(path):
62    if isinstance(path, six.text_type):
63        return unicodedata.normalize('NFD', path)
64    try:
65        path = path.decode('utf-8')
66        path = unicodedata.normalize('NFD', path)
67        path = path.encode('utf-8')
68    except UnicodeError:
69        pass  # Not UTF-8
70    return path
71
72
73def read_all_bytes(filename):
74    with io.open(filename, 'rb') as fp:
75        return fp.read()
76
77
78def latin1_fail():
79    try:
80        desc, filename = tempfile.mkstemp(suffix=Filenames.latin_1)
81        os.close(desc)
82        os.remove(filename)
83    except Exception:
84        return True
85
86
87fail_on_latin1_encoded_filenames = pytest.mark.xfail(
88    latin1_fail(),
89    reason="System does not support latin-1 filenames",
90)
91
92
93def touch(path):
94    path.write_text('', encoding='utf-8')
95
96
97class TestSdistTest:
98    @pytest.fixture(autouse=True)
99    def source_dir(self, tmpdir):
100        (tmpdir / 'setup.py').write_text(SETUP_PY, encoding='utf-8')
101
102        # Set up the rest of the test package
103        test_pkg = tmpdir / 'sdist_test'
104        test_pkg.mkdir()
105        data_folder = tmpdir / 'd'
106        data_folder.mkdir()
107        # *.rst was not included in package_data, so c.rst should not be
108        # automatically added to the manifest when not under version control
109        for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']:
110            touch(test_pkg / fname)
111        touch(data_folder / 'e.dat')
112
113        with tmpdir.as_cwd():
114            yield
115
116    def test_package_data_in_sdist(self):
117        """Regression test for pull request #4: ensures that files listed in
118        package_data are included in the manifest even if they're not added to
119        version control.
120        """
121
122        dist = Distribution(SETUP_ATTRS)
123        dist.script_name = 'setup.py'
124        cmd = sdist(dist)
125        cmd.ensure_finalized()
126
127        with quiet():
128            cmd.run()
129
130        manifest = cmd.filelist.files
131        assert os.path.join('sdist_test', 'a.txt') in manifest
132        assert os.path.join('sdist_test', 'b.txt') in manifest
133        assert os.path.join('sdist_test', 'c.rst') not in manifest
134        assert os.path.join('d', 'e.dat') in manifest
135
136    def test_setup_py_exists(self):
137        dist = Distribution(SETUP_ATTRS)
138        dist.script_name = 'foo.py'
139        cmd = sdist(dist)
140        cmd.ensure_finalized()
141
142        with quiet():
143            cmd.run()
144
145        manifest = cmd.filelist.files
146        assert 'setup.py' in manifest
147
148    def test_setup_py_missing(self):
149        dist = Distribution(SETUP_ATTRS)
150        dist.script_name = 'foo.py'
151        cmd = sdist(dist)
152        cmd.ensure_finalized()
153
154        if os.path.exists("setup.py"):
155            os.remove("setup.py")
156        with quiet():
157            cmd.run()
158
159        manifest = cmd.filelist.files
160        assert 'setup.py' not in manifest
161
162    def test_setup_py_excluded(self):
163        with open("MANIFEST.in", "w") as manifest_file:
164            manifest_file.write("exclude setup.py")
165
166        dist = Distribution(SETUP_ATTRS)
167        dist.script_name = 'foo.py'
168        cmd = sdist(dist)
169        cmd.ensure_finalized()
170
171        with quiet():
172            cmd.run()
173
174        manifest = cmd.filelist.files
175        assert 'setup.py' not in manifest
176
177    def test_defaults_case_sensitivity(self, tmpdir):
178        """
179        Make sure default files (README.*, etc.) are added in a case-sensitive
180        way to avoid problems with packages built on Windows.
181        """
182
183        touch(tmpdir / 'readme.rst')
184        touch(tmpdir / 'SETUP.cfg')
185
186        dist = Distribution(SETUP_ATTRS)
187        # the extension deliberately capitalized for this test
188        # to make sure the actual filename (not capitalized) gets added
189        # to the manifest
190        dist.script_name = 'setup.PY'
191        cmd = sdist(dist)
192        cmd.ensure_finalized()
193
194        with quiet():
195            cmd.run()
196
197        # lowercase all names so we can test in a
198        # case-insensitive way to make sure the files
199        # are not included.
200        manifest = map(lambda x: x.lower(), cmd.filelist.files)
201        assert 'readme.rst' not in manifest, manifest
202        assert 'setup.py' not in manifest, manifest
203        assert 'setup.cfg' not in manifest, manifest
204
205    @fail_on_ascii
206    def test_manifest_is_written_with_utf8_encoding(self):
207        # Test for #303.
208        dist = Distribution(SETUP_ATTRS)
209        dist.script_name = 'setup.py'
210        mm = manifest_maker(dist)
211        mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
212        os.mkdir('sdist_test.egg-info')
213
214        # UTF-8 filename
215        filename = os.path.join('sdist_test', 'smörbröd.py')
216
217        # Must create the file or it will get stripped.
218        open(filename, 'w').close()
219
220        # Add UTF-8 filename and write manifest
221        with quiet():
222            mm.run()
223            mm.filelist.append(filename)
224            mm.write_manifest()
225
226        contents = read_all_bytes(mm.manifest)
227
228        # The manifest should be UTF-8 encoded
229        u_contents = contents.decode('UTF-8')
230
231        # The manifest should contain the UTF-8 filename
232        assert posix(filename) in u_contents
233
234    @py3_only
235    @fail_on_ascii
236    def test_write_manifest_allows_utf8_filenames(self):
237        # Test for #303.
238        dist = Distribution(SETUP_ATTRS)
239        dist.script_name = 'setup.py'
240        mm = manifest_maker(dist)
241        mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
242        os.mkdir('sdist_test.egg-info')
243
244        filename = os.path.join(b'sdist_test', Filenames.utf_8)
245
246        # Must touch the file or risk removal
247        open(filename, "w").close()
248
249        # Add filename and write manifest
250        with quiet():
251            mm.run()
252            u_filename = filename.decode('utf-8')
253            mm.filelist.files.append(u_filename)
254            # Re-write manifest
255            mm.write_manifest()
256
257        contents = read_all_bytes(mm.manifest)
258
259        # The manifest should be UTF-8 encoded
260        contents.decode('UTF-8')
261
262        # The manifest should contain the UTF-8 filename
263        assert posix(filename) in contents
264
265        # The filelist should have been updated as well
266        assert u_filename in mm.filelist.files
267
268    @py3_only
269    def test_write_manifest_skips_non_utf8_filenames(self):
270        """
271        Files that cannot be encoded to UTF-8 (specifically, those that
272        weren't originally successfully decoded and have surrogate
273        escapes) should be omitted from the manifest.
274        See https://bitbucket.org/tarek/distribute/issue/303 for history.
275        """
276        dist = Distribution(SETUP_ATTRS)
277        dist.script_name = 'setup.py'
278        mm = manifest_maker(dist)
279        mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
280        os.mkdir('sdist_test.egg-info')
281
282        # Latin-1 filename
283        filename = os.path.join(b'sdist_test', Filenames.latin_1)
284
285        # Add filename with surrogates and write manifest
286        with quiet():
287            mm.run()
288            u_filename = filename.decode('utf-8', 'surrogateescape')
289            mm.filelist.append(u_filename)
290            # Re-write manifest
291            mm.write_manifest()
292
293        contents = read_all_bytes(mm.manifest)
294
295        # The manifest should be UTF-8 encoded
296        contents.decode('UTF-8')
297
298        # The Latin-1 filename should have been skipped
299        assert posix(filename) not in contents
300
301        # The filelist should have been updated as well
302        assert u_filename not in mm.filelist.files
303
304    @fail_on_ascii
305    def test_manifest_is_read_with_utf8_encoding(self):
306        # Test for #303.
307        dist = Distribution(SETUP_ATTRS)
308        dist.script_name = 'setup.py'
309        cmd = sdist(dist)
310        cmd.ensure_finalized()
311
312        # Create manifest
313        with quiet():
314            cmd.run()
315
316        # Add UTF-8 filename to manifest
317        filename = os.path.join(b'sdist_test', Filenames.utf_8)
318        cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
319        manifest = open(cmd.manifest, 'ab')
320        manifest.write(b'\n' + filename)
321        manifest.close()
322
323        # The file must exist to be included in the filelist
324        open(filename, 'w').close()
325
326        # Re-read manifest
327        cmd.filelist.files = []
328        with quiet():
329            cmd.read_manifest()
330
331        # The filelist should contain the UTF-8 filename
332        if not six.PY2:
333            filename = filename.decode('utf-8')
334        assert filename in cmd.filelist.files
335
336    @py3_only
337    @fail_on_latin1_encoded_filenames
338    def test_read_manifest_skips_non_utf8_filenames(self):
339        # Test for #303.
340        dist = Distribution(SETUP_ATTRS)
341        dist.script_name = 'setup.py'
342        cmd = sdist(dist)
343        cmd.ensure_finalized()
344
345        # Create manifest
346        with quiet():
347            cmd.run()
348
349        # Add Latin-1 filename to manifest
350        filename = os.path.join(b'sdist_test', Filenames.latin_1)
351        cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
352        manifest = open(cmd.manifest, 'ab')
353        manifest.write(b'\n' + filename)
354        manifest.close()
355
356        # The file must exist to be included in the filelist
357        open(filename, 'w').close()
358
359        # Re-read manifest
360        cmd.filelist.files = []
361        with quiet():
362            cmd.read_manifest()
363
364        # The Latin-1 filename should have been skipped
365        filename = filename.decode('latin-1')
366        assert filename not in cmd.filelist.files
367
368    @fail_on_ascii
369    @fail_on_latin1_encoded_filenames
370    def test_sdist_with_utf8_encoded_filename(self):
371        # Test for #303.
372        dist = Distribution(self.make_strings(SETUP_ATTRS))
373        dist.script_name = 'setup.py'
374        cmd = sdist(dist)
375        cmd.ensure_finalized()
376
377        filename = os.path.join(b'sdist_test', Filenames.utf_8)
378        open(filename, 'w').close()
379
380        with quiet():
381            cmd.run()
382
383        if sys.platform == 'darwin':
384            filename = decompose(filename)
385
386        if not six.PY2:
387            fs_enc = sys.getfilesystemencoding()
388
389            if sys.platform == 'win32':
390                if fs_enc == 'cp1252':
391                    # Python 3 mangles the UTF-8 filename
392                    filename = filename.decode('cp1252')
393                    assert filename in cmd.filelist.files
394                else:
395                    filename = filename.decode('mbcs')
396                    assert filename in cmd.filelist.files
397            else:
398                filename = filename.decode('utf-8')
399                assert filename in cmd.filelist.files
400        else:
401            assert filename in cmd.filelist.files
402
403    @classmethod
404    def make_strings(cls, item):
405        if isinstance(item, dict):
406            return {
407                key: cls.make_strings(value) for key, value in item.items()}
408        if isinstance(item, list):
409            return list(map(cls.make_strings, item))
410        return str(item)
411
412    @fail_on_latin1_encoded_filenames
413    def test_sdist_with_latin1_encoded_filename(self):
414        # Test for #303.
415        dist = Distribution(self.make_strings(SETUP_ATTRS))
416        dist.script_name = 'setup.py'
417        cmd = sdist(dist)
418        cmd.ensure_finalized()
419
420        # Latin-1 filename
421        filename = os.path.join(b'sdist_test', Filenames.latin_1)
422        open(filename, 'w').close()
423        assert os.path.isfile(filename)
424
425        with quiet():
426            cmd.run()
427
428        if six.PY2:
429            # Under Python 2 there seems to be no decoded string in the
430            # filelist.  However, due to decode and encoding of the
431            # file name to get utf-8 Manifest the latin1 maybe excluded
432            try:
433                # fs_enc should match how one is expect the decoding to
434                # be proformed for the manifest output.
435                fs_enc = sys.getfilesystemencoding()
436                filename.decode(fs_enc)
437                assert filename in cmd.filelist.files
438            except UnicodeDecodeError:
439                filename not in cmd.filelist.files
440        else:
441            # not all windows systems have a default FS encoding of cp1252
442            if sys.platform == 'win32':
443                # Latin-1 is similar to Windows-1252 however
444                # on mbcs filesys it is not in latin-1 encoding
445                fs_enc = sys.getfilesystemencoding()
446                if fs_enc != 'mbcs':
447                    fs_enc = 'latin-1'
448                filename = filename.decode(fs_enc)
449
450                assert filename in cmd.filelist.files
451            else:
452                # The Latin-1 filename should have been skipped
453                filename = filename.decode('latin-1')
454                filename not in cmd.filelist.files
455
456    def test_pyproject_toml_in_sdist(self, tmpdir):
457        """
458        Check if pyproject.toml is included in source distribution if present
459        """
460        touch(tmpdir / 'pyproject.toml')
461        dist = Distribution(SETUP_ATTRS)
462        dist.script_name = 'setup.py'
463        cmd = sdist(dist)
464        cmd.ensure_finalized()
465        with quiet():
466            cmd.run()
467        manifest = cmd.filelist.files
468        assert 'pyproject.toml' in manifest
469
470    def test_pyproject_toml_excluded(self, tmpdir):
471        """
472        Check that pyproject.toml can excluded even if present
473        """
474        touch(tmpdir / 'pyproject.toml')
475        with open('MANIFEST.in', 'w') as mts:
476            print('exclude pyproject.toml', file=mts)
477        dist = Distribution(SETUP_ATTRS)
478        dist.script_name = 'setup.py'
479        cmd = sdist(dist)
480        cmd.ensure_finalized()
481        with quiet():
482            cmd.run()
483        manifest = cmd.filelist.files
484        assert 'pyproject.toml' not in manifest
485
486
487def test_default_revctrl():
488    """
489    When _default_revctrl was removed from the `setuptools.command.sdist`
490    module in 10.0, it broke some systems which keep an old install of
491    setuptools (Distribute) around. Those old versions require that the
492    setuptools package continue to implement that interface, so this
493    function provides that interface, stubbed. See #320 for details.
494
495    This interface must be maintained until Ubuntu 12.04 is no longer
496    supported (by Setuptools).
497    """
498    ep_def = 'svn_cvs = setuptools.command.sdist:_default_revctrl'
499    ep = pkg_resources.EntryPoint.parse(ep_def)
500    res = ep.resolve()
501    assert hasattr(res, '__iter__')
502