codekingpro/portable-devtools
114k
1import os2import shutil3import stat4import warnings5from pathlib import Path6from unittest.mock import Mock7 8import jaraco.path9import pytest10 11from setuptools import SetuptoolsDeprecationWarning12from setuptools.dist import Distribution13 14from .textwrap import DALS15 16 17def test_directories_in_package_data_glob(tmpdir_cwd):18 """19 Directories matching the glob in package_data should20 not be included in the package data.21 22 Regression test for #261.23 """24 dist = Distribution(25 dict(26 script_name='setup.py',27 script_args=['build_py'],28 packages=[''],29 package_data={'': ['path/*']},30 )31 )32 os.makedirs('path/subpath')33 dist.parse_command_line()34 dist.run_commands()35 36 37def test_recursive_in_package_data_glob(tmpdir_cwd):38 """39 Files matching recursive globs (**) in package_data should40 be included in the package data.41 42 #180643 """44 dist = Distribution(45 dict(46 script_name='setup.py',47 script_args=['build_py'],48 packages=[''],49 package_data={'': ['path/**/data']},50 )51 )52 os.makedirs('path/subpath/subsubpath')53 open('path/subpath/subsubpath/data', 'wb').close()54 55 dist.parse_command_line()56 dist.run_commands()57 58 assert stat.S_ISREG(os.stat('build/lib/path/subpath/subsubpath/data').st_mode), (59 "File is not included"60 )61 62 63def test_read_only(tmpdir_cwd):64 """65 Ensure read-only flag is not preserved in copy66 for package modules and package data, as that67 causes problems with deleting read-only files on68 Windows.69 70 #145171 """72 dist = Distribution(73 dict(74 script_name='setup.py',75 script_args=['build_py'],76 packages=['pkg'],77 package_data={'pkg': ['data.dat']},78 )79 )80 os.makedirs('pkg')81 open('pkg/__init__.py', 'wb').close()82 open('pkg/data.dat', 'wb').close()83 os.chmod('pkg/__init__.py', stat.S_IREAD)84 os.chmod('pkg/data.dat', stat.S_IREAD)85 dist.parse_command_line()86 dist.run_commands()87 shutil.rmtree('build')88 89 90@pytest.mark.xfail(91 'platform.system() == "Windows"',92 reason="On Windows, files do not have executable bits",93 raises=AssertionError,94 strict=True,95)96def test_executable_data(tmpdir_cwd):97 """98 Ensure executable bit is preserved in copy for99 package data, as users rely on it for scripts.100 101 #2041102 """103 dist = Distribution(104 dict(105 script_name='setup.py',106 script_args=['build_py'],107 packages=['pkg'],108 package_data={'pkg': ['run-me']},109 )110 )111 os.makedirs('pkg')112 open('pkg/__init__.py', 'wb').close()113 open('pkg/run-me', 'wb').close()114 os.chmod('pkg/run-me', 0o700)115 116 dist.parse_command_line()117 dist.run_commands()118 119 assert os.stat('build/lib/pkg/run-me').st_mode & stat.S_IEXEC, (120 "Script is not executable"121 )122 123 124EXAMPLE_WITH_MANIFEST = {125 "setup.cfg": DALS(126 """127 [metadata]128 name = mypkg129 version = 42130 131 [options]132 include_package_data = True133 packages = find:134 135 [options.packages.find]136 exclude = *.tests*137 """138 ),139 "mypkg": {140 "__init__.py": "",141 "resource_file.txt": "",142 "tests": {143 "__init__.py": "",144 "test_mypkg.py": "",145 "test_file.txt": "",146 },147 },148 "MANIFEST.in": DALS(149 """150 global-include *.py *.txt151 global-exclude *.py[cod]152 prune dist153 prune build154 prune *.egg-info155 """156 ),157}158 159 160def test_excluded_subpackages(tmpdir_cwd):161 jaraco.path.build(EXAMPLE_WITH_MANIFEST)162 dist = Distribution({"script_name": "%PEP 517%"})163 dist.parse_config_files()164 165 build_py = dist.get_command_obj("build_py")166 167 msg = r"Python recognizes 'mypkg\.tests' as an importable package"168 with pytest.warns(SetuptoolsDeprecationWarning, match=msg): # noqa: PT031169 # TODO: To fix #3260 we need some transition period to deprecate the170 # existing behavior of `include_package_data`. After the transition, we171 # should remove the warning and fix the behavior.172 173 if os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib":174 # pytest.warns reset the warning filter temporarily175 # https://github.com/pytest-dev/pytest/issues/4011#issuecomment-423494810176 warnings.filterwarnings(177 "ignore",178 "'encoding' argument not specified",179 module="distutils.text_file",180 # This warning is already fixed in pypa/distutils but not in stdlib181 )182 183 build_py.finalize_options()184 build_py.run()185 186 build_dir = Path(dist.get_command_obj("build_py").build_lib)187 assert (build_dir / "mypkg/__init__.py").exists()188 assert (build_dir / "mypkg/resource_file.txt").exists()189 190 # Setuptools is configured to ignore `mypkg.tests`, therefore the following191 # files/dirs should not be included in the distribution.192 for f in [193 "mypkg/tests/__init__.py",194 "mypkg/tests/test_mypkg.py",195 "mypkg/tests/test_file.txt",196 "mypkg/tests",197 ]:198 with pytest.raises(AssertionError):199 # TODO: Enforce the following assertion once #3260 is fixed200 # (remove context manager and the following xfail).201 assert not (build_dir / f).exists()202 203 pytest.xfail("#3260")204 205 206@pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning")207def test_existing_egg_info(tmpdir_cwd, monkeypatch):208 """When provided with the ``existing_egg_info_dir`` attribute, build_py should not209 attempt to run egg_info again.210 """211 # == Pre-condition ==212 # Generate an egg-info dir213 jaraco.path.build(EXAMPLE_WITH_MANIFEST)214 dist = Distribution({"script_name": "%PEP 517%"})215 dist.parse_config_files()216 assert dist.include_package_data217 218 egg_info = dist.get_command_obj("egg_info")219 dist.run_command("egg_info")220 egg_info_dir = next(Path(egg_info.egg_base).glob("*.egg-info"))221 assert egg_info_dir.is_dir()222 223 # == Setup ==224 build_py = dist.get_command_obj("build_py")225 build_py.finalize_options()226 egg_info = dist.get_command_obj("egg_info")227 egg_info_run = Mock(side_effect=egg_info.run)228 monkeypatch.setattr(egg_info, "run", egg_info_run)229 230 # == Remove caches ==231 # egg_info is called when build_py looks for data_files, which gets cached.232 # We need to ensure it is not cached yet, otherwise it may impact on the tests233 build_py.__dict__.pop('data_files', None)234 dist.reinitialize_command(egg_info)235 236 # == Sanity check ==237 # Ensure that if existing_egg_info is not given, build_py attempts to run egg_info238 build_py.existing_egg_info_dir = None239 build_py.run()240 egg_info_run.assert_called()241 242 # == Remove caches ==243 egg_info_run.reset_mock()244 build_py.__dict__.pop('data_files', None)245 dist.reinitialize_command(egg_info)246 247 # == Actual test ==248 # Ensure that if existing_egg_info_dir is given, egg_info doesn't run249 build_py.existing_egg_info_dir = egg_info_dir250 build_py.run()251 egg_info_run.assert_not_called()252 assert build_py.data_files253 254 # Make sure the list of outputs is actually OK255 outputs = map(lambda x: x.replace(os.sep, "/"), build_py.get_outputs())256 assert outputs257 example = str(Path(build_py.build_lib, "mypkg/__init__.py")).replace(os.sep, "/")258 assert example in outputs259 260 261EXAMPLE_ARBITRARY_MAPPING = {262 "pyproject.toml": DALS(263 """264 [project]265 name = "mypkg"266 version = "42"267 268 [tool.setuptools]269 packages = ["mypkg", "mypkg.sub1", "mypkg.sub2", "mypkg.sub2.nested"]270 271 [tool.setuptools.package-dir]272 "" = "src"273 "mypkg.sub2" = "src/mypkg/_sub2"274 "mypkg.sub2.nested" = "other"275 """276 ),277 "src": {278 "mypkg": {279 "__init__.py": "",280 "resource_file.txt": "",281 "sub1": {282 "__init__.py": "",283 "mod1.py": "",284 },285 "_sub2": {286 "mod2.py": "",287 },288 },289 },290 "other": {291 "__init__.py": "",292 "mod3.py": "",293 },294 "MANIFEST.in": DALS(295 """296 global-include *.py *.txt297 global-exclude *.py[cod]298 """299 ),300}301 302 303def test_get_outputs(tmpdir_cwd):304 jaraco.path.build(EXAMPLE_ARBITRARY_MAPPING)305 dist = Distribution({"script_name": "%test%"})306 dist.parse_config_files()307 308 build_py = dist.get_command_obj("build_py")309 build_py.editable_mode = True310 build_py.ensure_finalized()311 build_lib = build_py.build_lib.replace(os.sep, "/")312 outputs = {x.replace(os.sep, "/") for x in build_py.get_outputs()}313 assert outputs == {314 f"{build_lib}/mypkg/__init__.py",315 f"{build_lib}/mypkg/resource_file.txt",316 f"{build_lib}/mypkg/sub1/__init__.py",317 f"{build_lib}/mypkg/sub1/mod1.py",318 f"{build_lib}/mypkg/sub2/mod2.py",319 f"{build_lib}/mypkg/sub2/nested/__init__.py",320 f"{build_lib}/mypkg/sub2/nested/mod3.py",321 }322 mapping = {323 k.replace(os.sep, "/"): v.replace(os.sep, "/")324 for k, v in build_py.get_output_mapping().items()325 }326 assert mapping == {327 f"{build_lib}/mypkg/__init__.py": "src/mypkg/__init__.py",328 f"{build_lib}/mypkg/resource_file.txt": "src/mypkg/resource_file.txt",329 f"{build_lib}/mypkg/sub1/__init__.py": "src/mypkg/sub1/__init__.py",330 f"{build_lib}/mypkg/sub1/mod1.py": "src/mypkg/sub1/mod1.py",331 f"{build_lib}/mypkg/sub2/mod2.py": "src/mypkg/_sub2/mod2.py",332 f"{build_lib}/mypkg/sub2/nested/__init__.py": "other/__init__.py",333 f"{build_lib}/mypkg/sub2/nested/mod3.py": "other/mod3.py",334 }335 336 337class TestTypeInfoFiles:338 PYPROJECTS = {339 "default_pyproject": DALS(340 """341 [project]342 name = "foo"343 version = "1"344 """345 ),346 "dont_include_package_data": DALS(347 """348 [project]349 name = "foo"350 version = "1"351 352 [tool.setuptools]353 include-package-data = false354 """355 ),356 "exclude_type_info": DALS(357 """358 [project]359 name = "foo"360 version = "1"361 362 [tool.setuptools]363 include-package-data = false364 365 [tool.setuptools.exclude-package-data]366 "*" = ["py.typed", "*.pyi"]367 """368 ),369 }370 371 EXAMPLES = {372 "simple_namespace": {373 "directory_structure": {374 "foo": {375 "bar.pyi": "",376 "py.typed": "",377 "__init__.py": "",378 }379 },380 "expected_type_files": {"foo/bar.pyi", "foo/py.typed"},381 },382 "nested_inside_namespace": {383 "directory_structure": {384 "foo": {385 "bar": {386 "py.typed": "",387 "mod.pyi": "",388 }389 }390 },391 "expected_type_files": {"foo/bar/mod.pyi", "foo/bar/py.typed"},392 },393 "namespace_nested_inside_regular": {394 "directory_structure": {395 "foo": {396 "namespace": {397 "foo.pyi": "",398 },399 "__init__.pyi": "",400 "py.typed": "",401 }402 },403 "expected_type_files": {404 "foo/namespace/foo.pyi",405 "foo/__init__.pyi",406 "foo/py.typed",407 },408 },409 }410 411 @pytest.mark.parametrize(412 "pyproject",413 [414 "default_pyproject",415 pytest.param(416 "dont_include_package_data",417 marks=pytest.mark.xfail(reason="pypa/setuptools#4350"),418 ),419 ],420 )421 @pytest.mark.parametrize("example", EXAMPLES.keys())422 def test_type_files_included_by_default(self, tmpdir_cwd, pyproject, example):423 structure = {424 **self.EXAMPLES[example]["directory_structure"],425 "pyproject.toml": self.PYPROJECTS[pyproject],426 }427 expected_type_files = self.EXAMPLES[example]["expected_type_files"]428 jaraco.path.build(structure)429 430 build_py = get_finalized_build_py()431 outputs = get_outputs(build_py)432 assert expected_type_files <= outputs433 434 @pytest.mark.parametrize("pyproject", ["exclude_type_info"])435 @pytest.mark.parametrize("example", EXAMPLES.keys())436 def test_type_files_can_be_excluded(self, tmpdir_cwd, pyproject, example):437 structure = {438 **self.EXAMPLES[example]["directory_structure"],439 "pyproject.toml": self.PYPROJECTS[pyproject],440 }441 expected_type_files = self.EXAMPLES[example]["expected_type_files"]442 jaraco.path.build(structure)443 444 build_py = get_finalized_build_py()445 outputs = get_outputs(build_py)446 assert expected_type_files.isdisjoint(outputs)447 448 def test_stub_only_package(self, tmpdir_cwd):449 structure = {450 "pyproject.toml": DALS(451 """452 [project]453 name = "foo-stubs"454 version = "1"455 """456 ),457 "foo-stubs": {"__init__.pyi": "", "bar.pyi": ""},458 }459 expected_type_files = {"foo-stubs/__init__.pyi", "foo-stubs/bar.pyi"}460 jaraco.path.build(structure)461 462 build_py = get_finalized_build_py()463 outputs = get_outputs(build_py)464 assert expected_type_files <= outputs465 466 467def get_finalized_build_py(script_name="%build_py-test%"):468 dist = Distribution({"script_name": script_name})469 dist.parse_config_files()470 build_py = dist.get_command_obj("build_py")471 build_py.finalize_options()472 return build_py473 474 475def get_outputs(build_py):476 build_dir = Path(build_py.build_lib)477 return {478 os.path.relpath(x, build_dir).replace(os.sep, "/")479 for x in build_py.get_outputs()480 }481 