|
import os |
|
import stat |
|
import shutil |
|
import warnings |
|
from pathlib import Path |
|
from unittest.mock import Mock |
|
|
|
import pytest |
|
import jaraco.path |
|
|
|
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): |
|
|
|
|
|
|
|
|
|
if os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib": |
|
|
|
|
|
warnings.filterwarnings( |
|
"ignore", |
|
"'encoding' argument not specified", |
|
module="distutils.text_file", |
|
|
|
) |
|
|
|
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() |
|
|
|
|
|
|
|
for f in [ |
|
"mypkg/tests/__init__.py", |
|
"mypkg/tests/test_mypkg.py", |
|
"mypkg/tests/test_file.txt", |
|
"mypkg/tests", |
|
]: |
|
with pytest.raises(AssertionError): |
|
|
|
|
|
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. |
|
""" |
|
|
|
|
|
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() |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
build_py.__dict__.pop('data_files', None) |
|
dist.reinitialize_command(egg_info) |
|
|
|
|
|
|
|
build_py.existing_egg_info_dir = None |
|
build_py.run() |
|
egg_info_run.assert_called() |
|
|
|
|
|
egg_info_run.reset_mock() |
|
build_py.__dict__.pop('data_files', None) |
|
dist.reinitialize_command(egg_info) |
|
|
|
|
|
|
|
build_py.existing_egg_info_dir = egg_info_dir |
|
build_py.run() |
|
egg_info_run.assert_not_called() |
|
assert build_py.data_files |
|
|
|
|
|
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() |
|
} |
|
|