• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1import sys
2import ast
3import os
4import glob
5import re
6import stat
7import time
8from typing import List, Tuple
9
10import pytest
11from jaraco import path
12
13from setuptools.command.egg_info import (
14    egg_info, manifest_maker, EggInfoDeprecationWarning, get_pkg_info_revision,
15)
16from setuptools.dist import Distribution
17
18from . import environment
19from .textwrap import DALS
20from . import contexts
21
22
23class Environment(str):
24    pass
25
26
27class TestEggInfo:
28
29    setup_script = DALS("""
30        from setuptools import setup
31
32        setup(
33            name='foo',
34            py_modules=['hello'],
35            entry_points={'console_scripts': ['hi = hello.run']},
36            zip_safe=False,
37        )
38        """)
39
40    def _create_project(self):
41        path.build({
42            'setup.py': self.setup_script,
43            'hello.py': DALS("""
44                def run():
45                    print('hello')
46                """)
47        })
48
49    @staticmethod
50    def _extract_mv_version(pkg_info_lines: List[str]) -> Tuple[int, int]:
51        version_str = pkg_info_lines[0].split(' ')[1]
52        return tuple(map(int, version_str.split('.')[:2]))
53
54    @pytest.fixture
55    def env(self):
56        with contexts.tempdir(prefix='setuptools-test.') as env_dir:
57            env = Environment(env_dir)
58            os.chmod(env_dir, stat.S_IRWXU)
59            subs = 'home', 'lib', 'scripts', 'data', 'egg-base'
60            env.paths = dict(
61                (dirname, os.path.join(env_dir, dirname))
62                for dirname in subs
63            )
64            list(map(os.mkdir, env.paths.values()))
65            path.build({
66                env.paths['home']: {
67                    '.pydistutils.cfg': DALS("""
68                    [egg_info]
69                    egg-base = %(egg-base)s
70                    """ % env.paths)
71                }
72            })
73            yield env
74
75    def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env):
76        """
77        When the egg_info section is empty or not present, running
78        save_version_info should add the settings to the setup.cfg
79        in a deterministic order.
80        """
81        setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')
82        dist = Distribution()
83        ei = egg_info(dist)
84        ei.initialize_options()
85        ei.save_version_info(setup_cfg)
86
87        with open(setup_cfg, 'r') as f:
88            content = f.read()
89
90        assert '[egg_info]' in content
91        assert 'tag_build =' in content
92        assert 'tag_date = 0' in content
93
94        expected_order = 'tag_build', 'tag_date',
95
96        self._validate_content_order(content, expected_order)
97
98    @staticmethod
99    def _validate_content_order(content, expected):
100        """
101        Assert that the strings in expected appear in content
102        in order.
103        """
104        pattern = '.*'.join(expected)
105        flags = re.MULTILINE | re.DOTALL
106        assert re.search(pattern, content, flags)
107
108    def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env):
109        """
110        When running save_version_info on an existing setup.cfg
111        with the 'default' values present from a previous run,
112        the file should remain unchanged.
113        """
114        setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')
115        path.build({
116            setup_cfg: DALS("""
117            [egg_info]
118            tag_build =
119            tag_date = 0
120            """),
121        })
122        dist = Distribution()
123        ei = egg_info(dist)
124        ei.initialize_options()
125        ei.save_version_info(setup_cfg)
126
127        with open(setup_cfg, 'r') as f:
128            content = f.read()
129
130        assert '[egg_info]' in content
131        assert 'tag_build =' in content
132        assert 'tag_date = 0' in content
133
134        expected_order = 'tag_build', 'tag_date',
135
136        self._validate_content_order(content, expected_order)
137
138    def test_expected_files_produced(self, tmpdir_cwd, env):
139        self._create_project()
140
141        self._run_egg_info_command(tmpdir_cwd, env)
142        actual = os.listdir('foo.egg-info')
143
144        expected = [
145            'PKG-INFO',
146            'SOURCES.txt',
147            'dependency_links.txt',
148            'entry_points.txt',
149            'not-zip-safe',
150            'top_level.txt',
151        ]
152        assert sorted(actual) == expected
153
154    def test_license_is_a_string(self, tmpdir_cwd, env):
155        setup_config = DALS("""
156            [metadata]
157            name=foo
158            version=0.0.1
159            license=file:MIT
160            """)
161
162        setup_script = DALS("""
163            from setuptools import setup
164
165            setup()
166            """)
167
168        path.build({
169            'setup.py': setup_script,
170            'setup.cfg': setup_config,
171        })
172
173        # This command should fail with a ValueError, but because it's
174        # currently configured to use a subprocess, the actual traceback
175        # object is lost and we need to parse it from stderr
176        with pytest.raises(AssertionError) as exc:
177            self._run_egg_info_command(tmpdir_cwd, env)
178
179        # Hopefully this is not too fragile: the only argument to the
180        # assertion error should be a traceback, ending with:
181        #     ValueError: ....
182        #
183        #     assert not 1
184        tb = exc.value.args[0].split('\n')
185        assert tb[-3].lstrip().startswith('ValueError')
186
187    def test_rebuilt(self, tmpdir_cwd, env):
188        """Ensure timestamps are updated when the command is re-run."""
189        self._create_project()
190
191        self._run_egg_info_command(tmpdir_cwd, env)
192        timestamp_a = os.path.getmtime('foo.egg-info')
193
194        # arbitrary sleep just to handle *really* fast systems
195        time.sleep(.001)
196
197        self._run_egg_info_command(tmpdir_cwd, env)
198        timestamp_b = os.path.getmtime('foo.egg-info')
199
200        assert timestamp_a != timestamp_b
201
202    def test_manifest_template_is_read(self, tmpdir_cwd, env):
203        self._create_project()
204        path.build({
205            'MANIFEST.in': DALS("""
206                recursive-include docs *.rst
207            """),
208            'docs': {
209                'usage.rst': "Run 'hi'",
210            }
211        })
212        self._run_egg_info_command(tmpdir_cwd, env)
213        egg_info_dir = os.path.join('.', 'foo.egg-info')
214        sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt')
215        with open(sources_txt) as f:
216            assert 'docs/usage.rst' in f.read().split('\n')
217
218    def _setup_script_with_requires(self, requires, use_setup_cfg=False):
219        setup_script = DALS(
220            '''
221            from setuptools import setup
222
223            setup(name='foo', zip_safe=False, %s)
224            '''
225        ) % ('' if use_setup_cfg else requires)
226        setup_config = requires if use_setup_cfg else ''
227        path.build({
228            'setup.py': setup_script,
229            'setup.cfg': setup_config,
230        })
231
232    mismatch_marker = "python_version<'{this_ver}'".format(
233        this_ver=sys.version_info[0],
234    )
235    # Alternate equivalent syntax.
236    mismatch_marker_alternate = 'python_version < "{this_ver}"'.format(
237        this_ver=sys.version_info[0],
238    )
239    invalid_marker = "<=>++"
240
241    class RequiresTestHelper:
242
243        @staticmethod
244        def parametrize(*test_list, **format_dict):
245            idlist = []
246            argvalues = []
247            for test in test_list:
248                test_params = test.lstrip().split('\n\n', 3)
249                name_kwargs = test_params.pop(0).split('\n')
250                if len(name_kwargs) > 1:
251                    val = name_kwargs[1].strip()
252                    install_cmd_kwargs = ast.literal_eval(val)
253                else:
254                    install_cmd_kwargs = {}
255                name = name_kwargs[0].strip()
256                setup_py_requires, setup_cfg_requires, expected_requires = (
257                    DALS(a).format(**format_dict) for a in test_params
258                )
259                for id_, requires, use_cfg in (
260                    (name, setup_py_requires, False),
261                    (name + '_in_setup_cfg', setup_cfg_requires, True),
262                ):
263                    idlist.append(id_)
264                    marks = ()
265                    if requires.startswith('@xfail\n'):
266                        requires = requires[7:]
267                        marks = pytest.mark.xfail
268                    argvalues.append(pytest.param(requires, use_cfg,
269                                                  expected_requires,
270                                                  install_cmd_kwargs,
271                                                  marks=marks))
272            return pytest.mark.parametrize(
273                'requires,use_setup_cfg,'
274                'expected_requires,install_cmd_kwargs',
275                argvalues, ids=idlist,
276            )
277
278    @RequiresTestHelper.parametrize(
279        # Format of a test:
280        #
281        # id
282        # install_cmd_kwargs [optional]
283        #
284        # requires block (when used in setup.py)
285        #
286        # requires block (when used in setup.cfg)
287        #
288        # expected contents of requires.txt
289
290        '''
291        install_requires_deterministic
292
293        install_requires=["wheel>=0.5", "pytest"]
294
295        [options]
296        install_requires =
297            wheel>=0.5
298            pytest
299
300        wheel>=0.5
301        pytest
302        ''',
303
304        '''
305        install_requires_ordered
306
307        install_requires=["pytest>=3.0.2,!=10.9999"]
308
309        [options]
310        install_requires =
311            pytest>=3.0.2,!=10.9999
312
313        pytest!=10.9999,>=3.0.2
314        ''',
315
316        '''
317        install_requires_with_marker
318
319        install_requires=["barbazquux;{mismatch_marker}"],
320
321        [options]
322        install_requires =
323            barbazquux; {mismatch_marker}
324
325        [:{mismatch_marker_alternate}]
326        barbazquux
327        ''',
328
329        '''
330        install_requires_with_extra
331        {'cmd': ['egg_info']}
332
333        install_requires=["barbazquux [test]"],
334
335        [options]
336        install_requires =
337            barbazquux [test]
338
339        barbazquux[test]
340        ''',
341
342        '''
343        install_requires_with_extra_and_marker
344
345        install_requires=["barbazquux [test]; {mismatch_marker}"],
346
347        [options]
348        install_requires =
349            barbazquux [test]; {mismatch_marker}
350
351        [:{mismatch_marker_alternate}]
352        barbazquux[test]
353        ''',
354
355        '''
356        setup_requires_with_markers
357
358        setup_requires=["barbazquux;{mismatch_marker}"],
359
360        [options]
361        setup_requires =
362            barbazquux; {mismatch_marker}
363
364        ''',
365
366        '''
367        tests_require_with_markers
368        {'cmd': ['test'], 'output': "Ran 0 tests in"}
369
370        tests_require=["barbazquux;{mismatch_marker}"],
371
372        [options]
373        tests_require =
374            barbazquux; {mismatch_marker}
375
376        ''',
377
378        '''
379        extras_require_with_extra
380        {'cmd': ['egg_info']}
381
382        extras_require={{"extra": ["barbazquux [test]"]}},
383
384        [options.extras_require]
385        extra = barbazquux [test]
386
387        [extra]
388        barbazquux[test]
389        ''',
390
391        '''
392        extras_require_with_extra_and_marker_in_req
393
394        extras_require={{"extra": ["barbazquux [test]; {mismatch_marker}"]}},
395
396        [options.extras_require]
397        extra =
398            barbazquux [test]; {mismatch_marker}
399
400        [extra]
401
402        [extra:{mismatch_marker_alternate}]
403        barbazquux[test]
404        ''',
405
406        # FIXME: ConfigParser does not allow : in key names!
407        '''
408        extras_require_with_marker
409
410        extras_require={{":{mismatch_marker}": ["barbazquux"]}},
411
412        @xfail
413        [options.extras_require]
414        :{mismatch_marker} = barbazquux
415
416        [:{mismatch_marker}]
417        barbazquux
418        ''',
419
420        '''
421        extras_require_with_marker_in_req
422
423        extras_require={{"extra": ["barbazquux; {mismatch_marker}"]}},
424
425        [options.extras_require]
426        extra =
427            barbazquux; {mismatch_marker}
428
429        [extra]
430
431        [extra:{mismatch_marker_alternate}]
432        barbazquux
433        ''',
434
435        '''
436        extras_require_with_empty_section
437
438        extras_require={{"empty": []}},
439
440        [options.extras_require]
441        empty =
442
443        [empty]
444        ''',
445        # Format arguments.
446        invalid_marker=invalid_marker,
447        mismatch_marker=mismatch_marker,
448        mismatch_marker_alternate=mismatch_marker_alternate,
449    )
450    def test_requires(
451            self, tmpdir_cwd, env, requires, use_setup_cfg,
452            expected_requires, install_cmd_kwargs):
453        self._setup_script_with_requires(requires, use_setup_cfg)
454        self._run_egg_info_command(tmpdir_cwd, env, **install_cmd_kwargs)
455        egg_info_dir = os.path.join('.', 'foo.egg-info')
456        requires_txt = os.path.join(egg_info_dir, 'requires.txt')
457        if os.path.exists(requires_txt):
458            with open(requires_txt) as fp:
459                install_requires = fp.read()
460        else:
461            install_requires = ''
462        assert install_requires.lstrip() == expected_requires
463        assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
464
465    def test_install_requires_unordered_disallowed(self, tmpdir_cwd, env):
466        """
467        Packages that pass unordered install_requires sequences
468        should be rejected as they produce non-deterministic
469        builds. See #458.
470        """
471        req = 'install_requires={"fake-factory==0.5.2", "pytz"}'
472        self._setup_script_with_requires(req)
473        with pytest.raises(AssertionError):
474            self._run_egg_info_command(tmpdir_cwd, env)
475
476    def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env):
477        tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},'
478        req = tmpl.format(marker=self.invalid_marker)
479        self._setup_script_with_requires(req)
480        with pytest.raises(AssertionError):
481            self._run_egg_info_command(tmpdir_cwd, env)
482        assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
483
484    def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env):
485        tmpl = 'extras_require={{"extra": ["barbazquux; {marker}"]}},'
486        req = tmpl.format(marker=self.invalid_marker)
487        self._setup_script_with_requires(req)
488        with pytest.raises(AssertionError):
489            self._run_egg_info_command(tmpdir_cwd, env)
490        assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
491
492    def test_provides_extra(self, tmpdir_cwd, env):
493        self._setup_script_with_requires(
494            'extras_require={"foobar": ["barbazquux"]},')
495        environ = os.environ.copy().update(
496            HOME=env.paths['home'],
497        )
498        code, data = environment.run_setup_py(
499            cmd=['egg_info'],
500            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
501            data_stream=1,
502            env=environ,
503        )
504        egg_info_dir = os.path.join('.', 'foo.egg-info')
505        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
506            pkg_info_lines = pkginfo_file.read().split('\n')
507        assert 'Provides-Extra: foobar' in pkg_info_lines
508        assert 'Metadata-Version: 2.1' in pkg_info_lines
509
510    def test_doesnt_provides_extra(self, tmpdir_cwd, env):
511        self._setup_script_with_requires(
512            '''install_requires=["spam ; python_version<'3.6'"]''')
513        environ = os.environ.copy().update(
514            HOME=env.paths['home'],
515        )
516        environment.run_setup_py(
517            cmd=['egg_info'],
518            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
519            data_stream=1,
520            env=environ,
521        )
522        egg_info_dir = os.path.join('.', 'foo.egg-info')
523        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
524            pkg_info_text = pkginfo_file.read()
525        assert 'Provides-Extra:' not in pkg_info_text
526
527    @pytest.mark.parametrize("files, license_in_sources", [
528        ({
529            'setup.cfg': DALS("""
530                              [metadata]
531                              license_file = LICENSE
532                              """),
533            'LICENSE': "Test license"
534        }, True),  # with license
535        ({
536            'setup.cfg': DALS("""
537                              [metadata]
538                              license_file = INVALID_LICENSE
539                              """),
540            'LICENSE': "Test license"
541        }, False),  # with an invalid license
542        ({
543            'setup.cfg': DALS("""
544                              """),
545            'LICENSE': "Test license"
546        }, True),  # no license_file attribute, LICENSE auto-included
547        ({
548            'setup.cfg': DALS("""
549                              [metadata]
550                              license_file = LICENSE
551                              """),
552            'MANIFEST.in': "exclude LICENSE",
553            'LICENSE': "Test license"
554        }, True),  # manifest is overwritten by license_file
555        pytest.param({
556            'setup.cfg': DALS("""
557                              [metadata]
558                              license_file = LICEN[CS]E*
559                              """),
560            'LICENSE': "Test license",
561            }, True,
562            id="glob_pattern"),
563    ])
564    def test_setup_cfg_license_file(
565            self, tmpdir_cwd, env, files, license_in_sources):
566        self._create_project()
567        path.build(files)
568
569        environment.run_setup_py(
570            cmd=['egg_info'],
571            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)])
572        )
573        egg_info_dir = os.path.join('.', 'foo.egg-info')
574
575        with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file:
576            sources_text = sources_file.read()
577
578        if license_in_sources:
579            assert 'LICENSE' in sources_text
580        else:
581            assert 'LICENSE' not in sources_text
582            # for invalid license test
583            assert 'INVALID_LICENSE' not in sources_text
584
585    @pytest.mark.parametrize("files, incl_licenses, excl_licenses", [
586        ({
587            'setup.cfg': DALS("""
588                              [metadata]
589                              license_files =
590                                  LICENSE-ABC
591                                  LICENSE-XYZ
592                              """),
593            'LICENSE-ABC': "ABC license",
594            'LICENSE-XYZ': "XYZ license"
595        }, ['LICENSE-ABC', 'LICENSE-XYZ'], []),  # with licenses
596        ({
597            'setup.cfg': DALS("""
598                              [metadata]
599                              license_files = LICENSE-ABC, LICENSE-XYZ
600                              """),
601            'LICENSE-ABC': "ABC license",
602            'LICENSE-XYZ': "XYZ license"
603        }, ['LICENSE-ABC', 'LICENSE-XYZ'], []),  # with commas
604        ({
605            'setup.cfg': DALS("""
606                              [metadata]
607                              license_files =
608                                  LICENSE-ABC
609                              """),
610            'LICENSE-ABC': "ABC license",
611            'LICENSE-XYZ': "XYZ license"
612        }, ['LICENSE-ABC'], ['LICENSE-XYZ']),  # with one license
613        ({
614            'setup.cfg': DALS("""
615                              [metadata]
616                              license_files =
617                              """),
618            'LICENSE-ABC': "ABC license",
619            'LICENSE-XYZ': "XYZ license"
620        }, [], ['LICENSE-ABC', 'LICENSE-XYZ']),  # empty
621        ({
622            'setup.cfg': DALS("""
623                              [metadata]
624                              license_files = LICENSE-XYZ
625                              """),
626            'LICENSE-ABC': "ABC license",
627            'LICENSE-XYZ': "XYZ license"
628        }, ['LICENSE-XYZ'], ['LICENSE-ABC']),  # on same line
629        ({
630            'setup.cfg': DALS("""
631                              [metadata]
632                              license_files =
633                                  LICENSE-ABC
634                                  INVALID_LICENSE
635                              """),
636            'LICENSE-ABC': "Test license"
637        }, ['LICENSE-ABC'], ['INVALID_LICENSE']),  # with an invalid license
638        ({
639            'setup.cfg': DALS("""
640                              """),
641            'LICENSE': "Test license"
642        }, ['LICENSE'], []),  # no license_files attribute, LICENSE auto-included
643        ({
644            'setup.cfg': DALS("""
645                              [metadata]
646                              license_files = LICENSE
647                              """),
648            'MANIFEST.in': "exclude LICENSE",
649            'LICENSE': "Test license"
650        }, ['LICENSE'], []),  # manifest is overwritten by license_files
651        ({
652            'setup.cfg': DALS("""
653                              [metadata]
654                              license_files =
655                                  LICENSE-ABC
656                                  LICENSE-XYZ
657                              """),
658            'MANIFEST.in': "exclude LICENSE-XYZ",
659            'LICENSE-ABC': "ABC license",
660            'LICENSE-XYZ': "XYZ license"
661            # manifest is overwritten by license_files
662        }, ['LICENSE-ABC', 'LICENSE-XYZ'], []),
663        pytest.param({
664            'setup.cfg': "",
665            'LICENSE-ABC': "ABC license",
666            'COPYING-ABC': "ABC copying",
667            'NOTICE-ABC': "ABC notice",
668            'AUTHORS-ABC': "ABC authors",
669            'LICENCE-XYZ': "XYZ license",
670            'LICENSE': "License",
671            'INVALID-LICENSE': "Invalid license",
672            }, [
673            'LICENSE-ABC',
674            'COPYING-ABC',
675            'NOTICE-ABC',
676            'AUTHORS-ABC',
677            'LICENCE-XYZ',
678            'LICENSE',
679            ], ['INVALID-LICENSE'],
680            # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
681            id="default_glob_patterns"),
682        pytest.param({
683            'setup.cfg': DALS("""
684                              [metadata]
685                              license_files =
686                                  LICENSE*
687                              """),
688            'LICENSE-ABC': "ABC license",
689            'NOTICE-XYZ': "XYZ notice",
690            }, ['LICENSE-ABC'], ['NOTICE-XYZ'],
691            id="no_default_glob_patterns"),
692        pytest.param({
693            'setup.cfg': DALS("""
694                              [metadata]
695                              license_files =
696                                  LICENSE-ABC
697                                  LICENSE*
698                              """),
699            'LICENSE-ABC': "ABC license",
700            }, ['LICENSE-ABC'], [],
701            id="files_only_added_once",
702        ),
703    ])
704    def test_setup_cfg_license_files(
705            self, tmpdir_cwd, env, files, incl_licenses, excl_licenses):
706        self._create_project()
707        path.build(files)
708
709        environment.run_setup_py(
710            cmd=['egg_info'],
711            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)])
712        )
713        egg_info_dir = os.path.join('.', 'foo.egg-info')
714
715        with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file:
716            sources_lines = list(line.strip() for line in sources_file)
717
718        for lf in incl_licenses:
719            assert sources_lines.count(lf) == 1
720
721        for lf in excl_licenses:
722            assert sources_lines.count(lf) == 0
723
724    @pytest.mark.parametrize("files, incl_licenses, excl_licenses", [
725        ({
726            'setup.cfg': DALS("""
727                              [metadata]
728                              license_file =
729                              license_files =
730                              """),
731            'LICENSE-ABC': "ABC license",
732            'LICENSE-XYZ': "XYZ license"
733        }, [], ['LICENSE-ABC', 'LICENSE-XYZ']),  # both empty
734        ({
735            'setup.cfg': DALS("""
736                              [metadata]
737                              license_file =
738                                  LICENSE-ABC
739                                  LICENSE-XYZ
740                              """),
741            'LICENSE-ABC': "ABC license",
742            'LICENSE-XYZ': "XYZ license"
743            # license_file is still singular
744        }, [], ['LICENSE-ABC', 'LICENSE-XYZ']),
745        ({
746            'setup.cfg': DALS("""
747                              [metadata]
748                              license_file = LICENSE-ABC
749                              license_files =
750                                  LICENSE-XYZ
751                                  LICENSE-PQR
752                              """),
753            'LICENSE-ABC': "ABC license",
754            'LICENSE-PQR': "PQR license",
755            'LICENSE-XYZ': "XYZ license"
756        }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []),  # combined
757        ({
758            'setup.cfg': DALS("""
759                              [metadata]
760                              license_file = LICENSE-ABC
761                              license_files =
762                                  LICENSE-ABC
763                                  LICENSE-XYZ
764                                  LICENSE-PQR
765                              """),
766            'LICENSE-ABC': "ABC license",
767            'LICENSE-PQR': "PQR license",
768            'LICENSE-XYZ': "XYZ license"
769            # duplicate license
770        }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []),
771        ({
772            'setup.cfg': DALS("""
773                              [metadata]
774                              license_file = LICENSE-ABC
775                              license_files =
776                                  LICENSE-XYZ
777                              """),
778            'LICENSE-ABC': "ABC license",
779            'LICENSE-PQR': "PQR license",
780            'LICENSE-XYZ': "XYZ license"
781            # combined subset
782        }, ['LICENSE-ABC', 'LICENSE-XYZ'], ['LICENSE-PQR']),
783        ({
784            'setup.cfg': DALS("""
785                              [metadata]
786                              license_file = LICENSE-ABC
787                              license_files =
788                                  LICENSE-XYZ
789                                  LICENSE-PQR
790                              """),
791            'LICENSE-PQR': "Test license"
792            # with invalid licenses
793        }, ['LICENSE-PQR'], ['LICENSE-ABC', 'LICENSE-XYZ']),
794        ({
795            'setup.cfg': DALS("""
796                              [metadata]
797                              license_file = LICENSE-ABC
798                              license_files =
799                                LICENSE-PQR
800                                LICENSE-XYZ
801                              """),
802            'MANIFEST.in': "exclude LICENSE-ABC\nexclude LICENSE-PQR",
803            'LICENSE-ABC': "ABC license",
804            'LICENSE-PQR': "PQR license",
805            'LICENSE-XYZ': "XYZ license"
806            # manifest is overwritten
807        }, ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'], []),
808        pytest.param({
809            'setup.cfg': DALS("""
810                              [metadata]
811                              license_file = LICENSE*
812                              """),
813            'LICENSE-ABC': "ABC license",
814            'NOTICE-XYZ': "XYZ notice",
815            }, ['LICENSE-ABC'], ['NOTICE-XYZ'],
816            id="no_default_glob_patterns"),
817        pytest.param({
818            'setup.cfg': DALS("""
819                              [metadata]
820                              license_file = LICENSE*
821                              license_files =
822                                NOTICE*
823                              """),
824            'LICENSE-ABC': "ABC license",
825            'NOTICE-ABC': "ABC notice",
826            'AUTHORS-ABC': "ABC authors",
827            }, ['LICENSE-ABC', 'NOTICE-ABC'], ['AUTHORS-ABC'],
828            id="combined_glob_patterrns"),
829    ])
830    def test_setup_cfg_license_file_license_files(
831            self, tmpdir_cwd, env, files, incl_licenses, excl_licenses):
832        self._create_project()
833        path.build(files)
834
835        environment.run_setup_py(
836            cmd=['egg_info'],
837            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)])
838        )
839        egg_info_dir = os.path.join('.', 'foo.egg-info')
840
841        with open(os.path.join(egg_info_dir, 'SOURCES.txt')) as sources_file:
842            sources_lines = list(line.strip() for line in sources_file)
843
844        for lf in incl_licenses:
845            assert sources_lines.count(lf) == 1
846
847        for lf in excl_licenses:
848            assert sources_lines.count(lf) == 0
849
850    def test_license_file_attr_pkg_info(self, tmpdir_cwd, env):
851        """All matched license files should have a corresponding License-File."""
852        self._create_project()
853        path.build({
854            "setup.cfg": DALS("""
855                              [metadata]
856                              license_files =
857                                  NOTICE*
858                                  LICENSE*
859                              """),
860            "LICENSE-ABC": "ABC license",
861            "LICENSE-XYZ": "XYZ license",
862            "NOTICE": "included",
863            "IGNORE": "not include",
864        })
865
866        environment.run_setup_py(
867            cmd=['egg_info'],
868            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)])
869        )
870        egg_info_dir = os.path.join('.', 'foo.egg-info')
871        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
872            pkg_info_lines = pkginfo_file.read().split('\n')
873        license_file_lines = [
874            line for line in pkg_info_lines if line.startswith('License-File:')]
875
876        # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched
877        # Also assert that order from license_files is keeped
878        assert "License-File: NOTICE" == license_file_lines[0]
879        assert "License-File: LICENSE-ABC" in license_file_lines[1:]
880        assert "License-File: LICENSE-XYZ" in license_file_lines[1:]
881
882    def test_metadata_version(self, tmpdir_cwd, env):
883        """Make sure latest metadata version is used by default."""
884        self._setup_script_with_requires("")
885        code, data = environment.run_setup_py(
886            cmd=['egg_info'],
887            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
888            data_stream=1,
889        )
890        egg_info_dir = os.path.join('.', 'foo.egg-info')
891        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
892            pkg_info_lines = pkginfo_file.read().split('\n')
893        # Update metadata version if changed
894        assert self._extract_mv_version(pkg_info_lines) == (2, 1)
895
896    def test_long_description_content_type(self, tmpdir_cwd, env):
897        # Test that specifying a `long_description_content_type` keyword arg to
898        # the `setup` function results in writing a `Description-Content-Type`
899        # line to the `PKG-INFO` file in the `<distribution>.egg-info`
900        # directory.
901        # `Description-Content-Type` is described at
902        # https://github.com/pypa/python-packaging-user-guide/pull/258
903
904        self._setup_script_with_requires(
905            """long_description_content_type='text/markdown',""")
906        environ = os.environ.copy().update(
907            HOME=env.paths['home'],
908        )
909        code, data = environment.run_setup_py(
910            cmd=['egg_info'],
911            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
912            data_stream=1,
913            env=environ,
914        )
915        egg_info_dir = os.path.join('.', 'foo.egg-info')
916        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
917            pkg_info_lines = pkginfo_file.read().split('\n')
918        expected_line = 'Description-Content-Type: text/markdown'
919        assert expected_line in pkg_info_lines
920        assert 'Metadata-Version: 2.1' in pkg_info_lines
921
922    def test_long_description(self, tmpdir_cwd, env):
923        # Test that specifying `long_description` and `long_description_content_type`
924        # keyword args to the `setup` function results in writing
925        # the description in the message payload of the `PKG-INFO` file
926        # in the `<distribution>.egg-info` directory.
927        self._setup_script_with_requires(
928            "long_description='This is a long description\\nover multiple lines',"
929            "long_description_content_type='text/markdown',"
930        )
931        code, data = environment.run_setup_py(
932            cmd=['egg_info'],
933            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
934            data_stream=1,
935        )
936        egg_info_dir = os.path.join('.', 'foo.egg-info')
937        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
938            pkg_info_lines = pkginfo_file.read().split('\n')
939        assert 'Metadata-Version: 2.1' in pkg_info_lines
940        assert '' == pkg_info_lines[-1]  # last line should be empty
941        long_desc_lines = pkg_info_lines[pkg_info_lines.index(''):]
942        assert 'This is a long description' in long_desc_lines
943        assert 'over multiple lines' in long_desc_lines
944
945    def test_project_urls(self, tmpdir_cwd, env):
946        # Test that specifying a `project_urls` dict to the `setup`
947        # function results in writing multiple `Project-URL` lines to
948        # the `PKG-INFO` file in the `<distribution>.egg-info`
949        # directory.
950        # `Project-URL` is described at https://packaging.python.org
951        #     /specifications/core-metadata/#project-url-multiple-use
952
953        self._setup_script_with_requires(
954            """project_urls={
955                'Link One': 'https://example.com/one/',
956                'Link Two': 'https://example.com/two/',
957                },""")
958        environ = os.environ.copy().update(
959            HOME=env.paths['home'],
960        )
961        code, data = environment.run_setup_py(
962            cmd=['egg_info'],
963            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
964            data_stream=1,
965            env=environ,
966        )
967        egg_info_dir = os.path.join('.', 'foo.egg-info')
968        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
969            pkg_info_lines = pkginfo_file.read().split('\n')
970        expected_line = 'Project-URL: Link One, https://example.com/one/'
971        assert expected_line in pkg_info_lines
972        expected_line = 'Project-URL: Link Two, https://example.com/two/'
973        assert expected_line in pkg_info_lines
974        assert self._extract_mv_version(pkg_info_lines) >= (1, 2)
975
976    def test_license(self, tmpdir_cwd, env):
977        """Test single line license."""
978        self._setup_script_with_requires(
979            "license='MIT',"
980        )
981        code, data = environment.run_setup_py(
982            cmd=['egg_info'],
983            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
984            data_stream=1,
985        )
986        egg_info_dir = os.path.join('.', 'foo.egg-info')
987        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
988            pkg_info_lines = pkginfo_file.read().split('\n')
989        assert 'License: MIT' in pkg_info_lines
990
991    def test_license_escape(self, tmpdir_cwd, env):
992        """Test license is escaped correctly if longer than one line."""
993        self._setup_script_with_requires(
994            "license='This is a long license text \\nover multiple lines',"
995        )
996        code, data = environment.run_setup_py(
997            cmd=['egg_info'],
998            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
999            data_stream=1,
1000        )
1001        egg_info_dir = os.path.join('.', 'foo.egg-info')
1002        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
1003            pkg_info_lines = pkginfo_file.read().split('\n')
1004
1005        assert 'License: This is a long license text ' in pkg_info_lines
1006        assert '        over multiple lines' in pkg_info_lines
1007        assert 'text \n        over multiple' in '\n'.join(pkg_info_lines)
1008
1009    def test_python_requires_egg_info(self, tmpdir_cwd, env):
1010        self._setup_script_with_requires(
1011            """python_requires='>=2.7.12',""")
1012        environ = os.environ.copy().update(
1013            HOME=env.paths['home'],
1014        )
1015        code, data = environment.run_setup_py(
1016            cmd=['egg_info'],
1017            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
1018            data_stream=1,
1019            env=environ,
1020        )
1021        egg_info_dir = os.path.join('.', 'foo.egg-info')
1022        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
1023            pkg_info_lines = pkginfo_file.read().split('\n')
1024        assert 'Requires-Python: >=2.7.12' in pkg_info_lines
1025        assert self._extract_mv_version(pkg_info_lines) >= (1, 2)
1026
1027    def test_manifest_maker_warning_suppression(self):
1028        fixtures = [
1029            "standard file not found: should have one of foo.py, bar.py",
1030            "standard file 'setup.py' not found"
1031        ]
1032
1033        for msg in fixtures:
1034            assert manifest_maker._should_suppress_warning(msg)
1035
1036    def test_egg_info_includes_setup_py(self, tmpdir_cwd):
1037        self._create_project()
1038        dist = Distribution({"name": "foo", "version": "0.0.1"})
1039        dist.script_name = "non_setup.py"
1040        egg_info_instance = egg_info(dist)
1041        egg_info_instance.finalize_options()
1042        egg_info_instance.run()
1043
1044        assert 'setup.py' in egg_info_instance.filelist.files
1045
1046        with open(egg_info_instance.egg_info + "/SOURCES.txt") as f:
1047            sources = f.read().split('\n')
1048            assert 'setup.py' in sources
1049
1050    def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None):
1051        environ = os.environ.copy().update(
1052            HOME=env.paths['home'],
1053        )
1054        if cmd is None:
1055            cmd = [
1056                'egg_info',
1057            ]
1058        code, data = environment.run_setup_py(
1059            cmd=cmd,
1060            pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
1061            data_stream=1,
1062            env=environ,
1063        )
1064        assert not code, data
1065
1066        if output:
1067            assert output in data
1068
1069    def test_egg_info_tag_only_once(self, tmpdir_cwd, env):
1070        self._create_project()
1071        path.build({
1072            'setup.cfg': DALS("""
1073                              [egg_info]
1074                              tag_build = dev
1075                              tag_date = 0
1076                              tag_svn_revision = 0
1077                              """),
1078        })
1079        self._run_egg_info_command(tmpdir_cwd, env)
1080        egg_info_dir = os.path.join('.', 'foo.egg-info')
1081        with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
1082            pkg_info_lines = pkginfo_file.read().split('\n')
1083        assert 'Version: 0.0.0.dev0' in pkg_info_lines
1084
1085    def test_get_pkg_info_revision_deprecated(self):
1086        pytest.warns(EggInfoDeprecationWarning, get_pkg_info_revision)
1087