| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480 |
- import os
- import shutil
- import stat
- import warnings
- from pathlib import Path
- from unittest.mock import Mock
- import jaraco.path
- import pytest
- from setuptools import SetuptoolsDeprecationWarning
- from setuptools.dist import Distribution
- from .textwrap import DALS
- def test_directories_in_package_data_glob(tmpdir_cwd):
- """
- Directories matching the glob in package_data should
- not be included in the package data.
- Regression test for #261.
- """
- dist = Distribution(
- dict(
- script_name='setup.py',
- script_args=['build_py'],
- packages=[''],
- package_data={'': ['path/*']},
- )
- )
- os.makedirs('path/subpath')
- dist.parse_command_line()
- dist.run_commands()
- def test_recursive_in_package_data_glob(tmpdir_cwd):
- """
- Files matching recursive globs (**) in package_data should
- be included in the package data.
- #1806
- """
- dist = Distribution(
- dict(
- script_name='setup.py',
- script_args=['build_py'],
- packages=[''],
- package_data={'': ['path/**/data']},
- )
- )
- os.makedirs('path/subpath/subsubpath')
- open('path/subpath/subsubpath/data', 'wb').close()
- dist.parse_command_line()
- dist.run_commands()
- assert stat.S_ISREG(os.stat('build/lib/path/subpath/subsubpath/data').st_mode), (
- "File is not included"
- )
- def test_read_only(tmpdir_cwd):
- """
- Ensure read-only flag is not preserved in copy
- for package modules and package data, as that
- causes problems with deleting read-only files on
- Windows.
- #1451
- """
- dist = Distribution(
- dict(
- script_name='setup.py',
- script_args=['build_py'],
- packages=['pkg'],
- package_data={'pkg': ['data.dat']},
- )
- )
- os.makedirs('pkg')
- open('pkg/__init__.py', 'wb').close()
- open('pkg/data.dat', 'wb').close()
- os.chmod('pkg/__init__.py', stat.S_IREAD)
- os.chmod('pkg/data.dat', stat.S_IREAD)
- dist.parse_command_line()
- dist.run_commands()
- shutil.rmtree('build')
- @pytest.mark.xfail(
- 'platform.system() == "Windows"',
- reason="On Windows, files do not have executable bits",
- raises=AssertionError,
- strict=True,
- )
- def test_executable_data(tmpdir_cwd):
- """
- Ensure executable bit is preserved in copy for
- package data, as users rely on it for scripts.
- #2041
- """
- dist = Distribution(
- dict(
- script_name='setup.py',
- script_args=['build_py'],
- packages=['pkg'],
- package_data={'pkg': ['run-me']},
- )
- )
- os.makedirs('pkg')
- open('pkg/__init__.py', 'wb').close()
- open('pkg/run-me', 'wb').close()
- os.chmod('pkg/run-me', 0o700)
- dist.parse_command_line()
- dist.run_commands()
- assert os.stat('build/lib/pkg/run-me').st_mode & stat.S_IEXEC, (
- "Script is not executable"
- )
- EXAMPLE_WITH_MANIFEST = {
- "setup.cfg": DALS(
- """
- [metadata]
- name = mypkg
- version = 42
- [options]
- include_package_data = True
- packages = find:
- [options.packages.find]
- exclude = *.tests*
- """
- ),
- "mypkg": {
- "__init__.py": "",
- "resource_file.txt": "",
- "tests": {
- "__init__.py": "",
- "test_mypkg.py": "",
- "test_file.txt": "",
- },
- },
- "MANIFEST.in": DALS(
- """
- global-include *.py *.txt
- global-exclude *.py[cod]
- prune dist
- prune build
- prune *.egg-info
- """
- ),
- }
- def test_excluded_subpackages(tmpdir_cwd):
- jaraco.path.build(EXAMPLE_WITH_MANIFEST)
- dist = Distribution({"script_name": "%PEP 517%"})
- dist.parse_config_files()
- build_py = dist.get_command_obj("build_py")
- msg = r"Python recognizes 'mypkg\.tests' as an importable package"
- with pytest.warns(SetuptoolsDeprecationWarning, match=msg): # noqa: PT031
- # TODO: To fix #3260 we need some transition period to deprecate the
- # existing behavior of `include_package_data`. After the transition, we
- # should remove the warning and fix the behavior.
- if os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib":
- # pytest.warns reset the warning filter temporarily
- # https://github.com/pytest-dev/pytest/issues/4011#issuecomment-423494810
- warnings.filterwarnings(
- "ignore",
- "'encoding' argument not specified",
- module="distutils.text_file",
- # This warning is already fixed in pypa/distutils but not in stdlib
- )
- build_py.finalize_options()
- build_py.run()
- build_dir = Path(dist.get_command_obj("build_py").build_lib)
- assert (build_dir / "mypkg/__init__.py").exists()
- assert (build_dir / "mypkg/resource_file.txt").exists()
- # Setuptools is configured to ignore `mypkg.tests`, therefore the following
- # files/dirs should not be included in the distribution.
- for f in [
- "mypkg/tests/__init__.py",
- "mypkg/tests/test_mypkg.py",
- "mypkg/tests/test_file.txt",
- "mypkg/tests",
- ]:
- with pytest.raises(AssertionError):
- # TODO: Enforce the following assertion once #3260 is fixed
- # (remove context manager and the following xfail).
- assert not (build_dir / f).exists()
- pytest.xfail("#3260")
- @pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning")
- def test_existing_egg_info(tmpdir_cwd, monkeypatch):
- """When provided with the ``existing_egg_info_dir`` attribute, build_py should not
- attempt to run egg_info again.
- """
- # == Pre-condition ==
- # Generate an egg-info dir
- jaraco.path.build(EXAMPLE_WITH_MANIFEST)
- dist = Distribution({"script_name": "%PEP 517%"})
- dist.parse_config_files()
- assert dist.include_package_data
- egg_info = dist.get_command_obj("egg_info")
- dist.run_command("egg_info")
- egg_info_dir = next(Path(egg_info.egg_base).glob("*.egg-info"))
- assert egg_info_dir.is_dir()
- # == Setup ==
- build_py = dist.get_command_obj("build_py")
- build_py.finalize_options()
- egg_info = dist.get_command_obj("egg_info")
- egg_info_run = Mock(side_effect=egg_info.run)
- monkeypatch.setattr(egg_info, "run", egg_info_run)
- # == Remove caches ==
- # egg_info is called when build_py looks for data_files, which gets cached.
- # We need to ensure it is not cached yet, otherwise it may impact on the tests
- build_py.__dict__.pop('data_files', None)
- dist.reinitialize_command(egg_info)
- # == Sanity check ==
- # Ensure that if existing_egg_info is not given, build_py attempts to run egg_info
- build_py.existing_egg_info_dir = None
- build_py.run()
- egg_info_run.assert_called()
- # == Remove caches ==
- egg_info_run.reset_mock()
- build_py.__dict__.pop('data_files', None)
- dist.reinitialize_command(egg_info)
- # == Actual test ==
- # Ensure that if existing_egg_info_dir is given, egg_info doesn't run
- build_py.existing_egg_info_dir = egg_info_dir
- build_py.run()
- egg_info_run.assert_not_called()
- assert build_py.data_files
- # Make sure the list of outputs is actually OK
- outputs = map(lambda x: x.replace(os.sep, "/"), build_py.get_outputs())
- assert outputs
- example = str(Path(build_py.build_lib, "mypkg/__init__.py")).replace(os.sep, "/")
- assert example in outputs
- EXAMPLE_ARBITRARY_MAPPING = {
- "pyproject.toml": DALS(
- """
- [project]
- name = "mypkg"
- version = "42"
- [tool.setuptools]
- packages = ["mypkg", "mypkg.sub1", "mypkg.sub2", "mypkg.sub2.nested"]
- [tool.setuptools.package-dir]
- "" = "src"
- "mypkg.sub2" = "src/mypkg/_sub2"
- "mypkg.sub2.nested" = "other"
- """
- ),
- "src": {
- "mypkg": {
- "__init__.py": "",
- "resource_file.txt": "",
- "sub1": {
- "__init__.py": "",
- "mod1.py": "",
- },
- "_sub2": {
- "mod2.py": "",
- },
- },
- },
- "other": {
- "__init__.py": "",
- "mod3.py": "",
- },
- "MANIFEST.in": DALS(
- """
- global-include *.py *.txt
- global-exclude *.py[cod]
- """
- ),
- }
- def test_get_outputs(tmpdir_cwd):
- jaraco.path.build(EXAMPLE_ARBITRARY_MAPPING)
- dist = Distribution({"script_name": "%test%"})
- dist.parse_config_files()
- build_py = dist.get_command_obj("build_py")
- build_py.editable_mode = True
- build_py.ensure_finalized()
- build_lib = build_py.build_lib.replace(os.sep, "/")
- outputs = {x.replace(os.sep, "/") for x in build_py.get_outputs()}
- assert outputs == {
- f"{build_lib}/mypkg/__init__.py",
- f"{build_lib}/mypkg/resource_file.txt",
- f"{build_lib}/mypkg/sub1/__init__.py",
- f"{build_lib}/mypkg/sub1/mod1.py",
- f"{build_lib}/mypkg/sub2/mod2.py",
- f"{build_lib}/mypkg/sub2/nested/__init__.py",
- f"{build_lib}/mypkg/sub2/nested/mod3.py",
- }
- mapping = {
- k.replace(os.sep, "/"): v.replace(os.sep, "/")
- for k, v in build_py.get_output_mapping().items()
- }
- assert mapping == {
- f"{build_lib}/mypkg/__init__.py": "src/mypkg/__init__.py",
- f"{build_lib}/mypkg/resource_file.txt": "src/mypkg/resource_file.txt",
- f"{build_lib}/mypkg/sub1/__init__.py": "src/mypkg/sub1/__init__.py",
- f"{build_lib}/mypkg/sub1/mod1.py": "src/mypkg/sub1/mod1.py",
- f"{build_lib}/mypkg/sub2/mod2.py": "src/mypkg/_sub2/mod2.py",
- f"{build_lib}/mypkg/sub2/nested/__init__.py": "other/__init__.py",
- f"{build_lib}/mypkg/sub2/nested/mod3.py": "other/mod3.py",
- }
- class TestTypeInfoFiles:
- PYPROJECTS = {
- "default_pyproject": DALS(
- """
- [project]
- name = "foo"
- version = "1"
- """
- ),
- "dont_include_package_data": DALS(
- """
- [project]
- name = "foo"
- version = "1"
- [tool.setuptools]
- include-package-data = false
- """
- ),
- "exclude_type_info": DALS(
- """
- [project]
- name = "foo"
- version = "1"
- [tool.setuptools]
- include-package-data = false
- [tool.setuptools.exclude-package-data]
- "*" = ["py.typed", "*.pyi"]
- """
- ),
- }
- EXAMPLES = {
- "simple_namespace": {
- "directory_structure": {
- "foo": {
- "bar.pyi": "",
- "py.typed": "",
- "__init__.py": "",
- }
- },
- "expected_type_files": {"foo/bar.pyi", "foo/py.typed"},
- },
- "nested_inside_namespace": {
- "directory_structure": {
- "foo": {
- "bar": {
- "py.typed": "",
- "mod.pyi": "",
- }
- }
- },
- "expected_type_files": {"foo/bar/mod.pyi", "foo/bar/py.typed"},
- },
- "namespace_nested_inside_regular": {
- "directory_structure": {
- "foo": {
- "namespace": {
- "foo.pyi": "",
- },
- "__init__.pyi": "",
- "py.typed": "",
- }
- },
- "expected_type_files": {
- "foo/namespace/foo.pyi",
- "foo/__init__.pyi",
- "foo/py.typed",
- },
- },
- }
- @pytest.mark.parametrize(
- "pyproject",
- [
- "default_pyproject",
- pytest.param(
- "dont_include_package_data",
- marks=pytest.mark.xfail(reason="pypa/setuptools#4350"),
- ),
- ],
- )
- @pytest.mark.parametrize("example", EXAMPLES.keys())
- def test_type_files_included_by_default(self, tmpdir_cwd, pyproject, example):
- structure = {
- **self.EXAMPLES[example]["directory_structure"],
- "pyproject.toml": self.PYPROJECTS[pyproject],
- }
- expected_type_files = self.EXAMPLES[example]["expected_type_files"]
- jaraco.path.build(structure)
- build_py = get_finalized_build_py()
- outputs = get_outputs(build_py)
- assert expected_type_files <= outputs
- @pytest.mark.parametrize("pyproject", ["exclude_type_info"])
- @pytest.mark.parametrize("example", EXAMPLES.keys())
- def test_type_files_can_be_excluded(self, tmpdir_cwd, pyproject, example):
- structure = {
- **self.EXAMPLES[example]["directory_structure"],
- "pyproject.toml": self.PYPROJECTS[pyproject],
- }
- expected_type_files = self.EXAMPLES[example]["expected_type_files"]
- jaraco.path.build(structure)
- build_py = get_finalized_build_py()
- outputs = get_outputs(build_py)
- assert expected_type_files.isdisjoint(outputs)
- def test_stub_only_package(self, tmpdir_cwd):
- structure = {
- "pyproject.toml": DALS(
- """
- [project]
- name = "foo-stubs"
- version = "1"
- """
- ),
- "foo-stubs": {"__init__.pyi": "", "bar.pyi": ""},
- }
- expected_type_files = {"foo-stubs/__init__.pyi", "foo-stubs/bar.pyi"}
- jaraco.path.build(structure)
- build_py = get_finalized_build_py()
- outputs = get_outputs(build_py)
- assert expected_type_files <= outputs
- def get_finalized_build_py(script_name="%build_py-test%"):
- dist = Distribution({"script_name": script_name})
- dist.parse_config_files()
- build_py = dist.get_command_obj("build_py")
- build_py.finalize_options()
- return build_py
- def get_outputs(build_py):
- build_dir = Path(build_py.build_lib)
- return {
- os.path.relpath(x, build_dir).replace(os.sep, "/")
- for x in build_py.get_outputs()
- }
|