481 lines
14 KiB
Python
481 lines
14 KiB
Python
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):
|
|
# 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 behaviour.
|
|
|
|
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()
|
|
}
|