| import os |
| import sys |
| from configparser import ConfigParser |
| from itertools import product |
| from typing import cast |
|
|
| import jaraco.path |
| import pytest |
| from path import Path |
|
|
| import setuptools |
| from setuptools.command.sdist import sdist |
| from setuptools.discovery import find_package_path, find_parent_package |
| from setuptools.dist import Distribution |
| from setuptools.errors import PackageDiscoveryError |
|
|
| from .contexts import quiet |
| from .integration.helpers import get_sdist_members, get_wheel_members, run |
| from .textwrap import DALS |
|
|
| import distutils.core |
|
|
|
|
| class TestFindParentPackage: |
| def test_single_package(self, tmp_path): |
| |
| (tmp_path / "src/namespace/pkg/nested").mkdir(exist_ok=True, parents=True) |
| (tmp_path / "src/namespace/pkg/nested/__init__.py").touch() |
| (tmp_path / "src/namespace/pkg/__init__.py").touch() |
| packages = ["namespace", "namespace.pkg", "namespace.pkg.nested"] |
| assert find_parent_package(packages, {"": "src"}, tmp_path) == "namespace.pkg" |
|
|
| def test_multiple_toplevel(self, tmp_path): |
| |
| |
| multiple = ["pkg", "pkg1", "pkg2"] |
| for name in multiple: |
| (tmp_path / f"src/{name}").mkdir(exist_ok=True, parents=True) |
| (tmp_path / f"src/{name}/__init__.py").touch() |
| assert find_parent_package(multiple, {"": "src"}, tmp_path) is None |
|
|
|
|
| class TestDiscoverPackagesAndPyModules: |
| """Make sure discovered values for ``packages`` and ``py_modules`` work |
| similarly to explicit configuration for the simple scenarios. |
| """ |
|
|
| OPTIONS = { |
| |
| "explicit-src": {"package_dir": {"": "src"}, "packages": ["pkg"]}, |
| "variation-lib": { |
| "package_dir": {"": "lib"}, |
| }, |
| "explicit-flat": {"packages": ["pkg"]}, |
| "explicit-single_module": {"py_modules": ["pkg"]}, |
| "explicit-namespace": {"packages": ["ns", "ns.pkg"]}, |
| "automatic-src": {}, |
| "automatic-flat": {}, |
| "automatic-single_module": {}, |
| "automatic-namespace": {}, |
| } |
| FILES = { |
| "src": ["src/pkg/__init__.py", "src/pkg/main.py"], |
| "lib": ["lib/pkg/__init__.py", "lib/pkg/main.py"], |
| "flat": ["pkg/__init__.py", "pkg/main.py"], |
| "single_module": ["pkg.py"], |
| "namespace": ["ns/pkg/__init__.py"], |
| } |
|
|
| def _get_info(self, circumstance): |
| _, _, layout = circumstance.partition("-") |
| files = self.FILES[layout] |
| options = self.OPTIONS[circumstance] |
| return files, options |
|
|
| @pytest.mark.parametrize("circumstance", OPTIONS.keys()) |
| def test_sdist_filelist(self, tmp_path, circumstance): |
| files, options = self._get_info(circumstance) |
| _populate_project_dir(tmp_path, files, options) |
|
|
| _, cmd = _run_sdist_programatically(tmp_path, options) |
|
|
| manifest = [f.replace(os.sep, "/") for f in cmd.filelist.files] |
| for file in files: |
| assert any(f.endswith(file) for f in manifest) |
|
|
| @pytest.mark.parametrize("circumstance", OPTIONS.keys()) |
| def test_project(self, tmp_path, circumstance): |
| files, options = self._get_info(circumstance) |
| _populate_project_dir(tmp_path, files, options) |
|
|
| |
| (tmp_path / "build").mkdir() |
| (tmp_path / "build/lib").mkdir() |
| (tmp_path / "build/bdist.linux-x86_64").mkdir() |
| (tmp_path / "build/bdist.linux-x86_64/file.py").touch() |
| (tmp_path / "build/lib/__init__.py").touch() |
| (tmp_path / "build/lib/file.py").touch() |
| (tmp_path / "dist").mkdir() |
| (tmp_path / "dist/file.py").touch() |
|
|
| _run_build(tmp_path) |
|
|
| sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz"))) |
| print("~~~~~ sdist_members ~~~~~") |
| print('\n'.join(sdist_files)) |
| assert sdist_files >= set(files) |
|
|
| wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl"))) |
| print("~~~~~ wheel_members ~~~~~") |
| print('\n'.join(wheel_files)) |
| orig_files = {f.replace("src/", "").replace("lib/", "") for f in files} |
| assert wheel_files >= orig_files |
|
|
| |
| for file in wheel_files: |
| assert "build" not in files |
| assert "dist" not in files |
|
|
| PURPOSEFULLY_EMPY = { |
| "setup.cfg": DALS( |
| """ |
| [metadata] |
| name = myproj |
| version = 0.0.0 |
| |
| [options] |
| {param} = |
| """ |
| ), |
| "setup.py": DALS( |
| """ |
| __import__('setuptools').setup( |
| name="myproj", |
| version="0.0.0", |
| {param}=[] |
| ) |
| """ |
| ), |
| "pyproject.toml": DALS( |
| """ |
| [build-system] |
| requires = [] |
| build-backend = 'setuptools.build_meta' |
| |
| [project] |
| name = "myproj" |
| version = "0.0.0" |
| |
| [tool.setuptools] |
| {param} = [] |
| """ |
| ), |
| "template-pyproject.toml": DALS( |
| """ |
| [build-system] |
| requires = [] |
| build-backend = 'setuptools.build_meta' |
| """ |
| ), |
| } |
|
|
| @pytest.mark.parametrize( |
| ("config_file", "param", "circumstance"), |
| product( |
| ["setup.cfg", "setup.py", "pyproject.toml"], |
| ["packages", "py_modules"], |
| FILES.keys(), |
| ), |
| ) |
| def test_purposefully_empty(self, tmp_path, config_file, param, circumstance): |
| files = self.FILES[circumstance] + ["mod.py", "other.py", "src/pkg/__init__.py"] |
| _populate_project_dir(tmp_path, files, {}) |
|
|
| if config_file == "pyproject.toml": |
| template_param = param.replace("_", "-") |
| else: |
| |
| pyproject = self.PURPOSEFULLY_EMPY["template-pyproject.toml"] |
| (tmp_path / "pyproject.toml").write_text(pyproject, encoding="utf-8") |
| template_param = param |
|
|
| config = self.PURPOSEFULLY_EMPY[config_file].format(param=template_param) |
| (tmp_path / config_file).write_text(config, encoding="utf-8") |
|
|
| dist = _get_dist(tmp_path, {}) |
| |
| |
| assert getattr(dist, param) == [] |
| other = {"py_modules": "packages", "packages": "py_modules"}[param] |
| assert getattr(dist, other) is None |
|
|
| @pytest.mark.parametrize( |
| ("extra_files", "pkgs"), |
| [ |
| (["venv/bin/simulate_venv"], {"pkg"}), |
| (["pkg-stubs/__init__.pyi"], {"pkg", "pkg-stubs"}), |
| (["other-stubs/__init__.pyi"], {"pkg", "other-stubs"}), |
| ( |
| |
| ["namespace-stubs/pkg/__init__.pyi"], |
| {"pkg", "namespace-stubs", "namespace-stubs.pkg"}, |
| ), |
| ( |
| |
| ["namespace-stubs/pkg-stubs/__init__.pyi"], |
| {"pkg", "namespace-stubs"}, |
| ), |
| (["_hidden/file.py"], {"pkg"}), |
| (["news/finalize.py"], {"pkg"}), |
| ], |
| ) |
| def test_flat_layout_with_extra_files(self, tmp_path, extra_files, pkgs): |
| files = self.FILES["flat"] + extra_files |
| _populate_project_dir(tmp_path, files, {}) |
| dist = _get_dist(tmp_path, {}) |
| assert set(dist.packages) == pkgs |
|
|
| @pytest.mark.parametrize( |
| "extra_files", |
| [ |
| ["other/__init__.py"], |
| ["other/finalize.py"], |
| ], |
| ) |
| def test_flat_layout_with_dangerous_extra_files(self, tmp_path, extra_files): |
| files = self.FILES["flat"] + extra_files |
| _populate_project_dir(tmp_path, files, {}) |
| with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"): |
| _get_dist(tmp_path, {}) |
|
|
| def test_flat_layout_with_single_module(self, tmp_path): |
| files = self.FILES["single_module"] + ["invalid-module-name.py"] |
| _populate_project_dir(tmp_path, files, {}) |
| dist = _get_dist(tmp_path, {}) |
| assert set(dist.py_modules) == {"pkg"} |
|
|
| def test_flat_layout_with_multiple_modules(self, tmp_path): |
| files = self.FILES["single_module"] + ["valid_module_name.py"] |
| _populate_project_dir(tmp_path, files, {}) |
| with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"): |
| _get_dist(tmp_path, {}) |
|
|
| def test_py_modules_when_wheel_dir_is_cwd(self, tmp_path): |
| """Regression for issue 3692""" |
| from setuptools import build_meta |
|
|
| pyproject = '[project]\nname = "test"\nversion = "1"' |
| (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") |
| (tmp_path / "foo.py").touch() |
| with jaraco.path.DirectoryStack().context(tmp_path): |
| build_meta.build_wheel(".") |
| |
| wheel_files = get_wheel_members(next(tmp_path.glob("*.whl"))) |
| assert "foo.py" in wheel_files |
|
|
|
|
| class TestNoConfig: |
| DEFAULT_VERSION = "0.0.0" |
|
|
| EXAMPLES = { |
| "pkg1": ["src/pkg1.py"], |
| "pkg2": ["src/pkg2/__init__.py"], |
| "pkg3": ["src/pkg3/__init__.py", "src/pkg3-stubs/__init__.py"], |
| "pkg4": ["pkg4/__init__.py", "pkg4-stubs/__init__.py"], |
| "ns.nested.pkg1": ["src/ns/nested/pkg1/__init__.py"], |
| "ns.nested.pkg2": ["ns/nested/pkg2/__init__.py"], |
| } |
|
|
| @pytest.mark.parametrize("example", EXAMPLES.keys()) |
| def test_discover_name(self, tmp_path, example): |
| _populate_project_dir(tmp_path, self.EXAMPLES[example], {}) |
| dist = _get_dist(tmp_path, {}) |
| assert dist.get_name() == example |
|
|
| def test_build_with_discovered_name(self, tmp_path): |
| files = ["src/ns/nested/pkg/__init__.py"] |
| _populate_project_dir(tmp_path, files, {}) |
| _run_build(tmp_path, "--sdist") |
| |
| dist_file = tmp_path / f"dist/ns_nested_pkg-{self.DEFAULT_VERSION}.tar.gz" |
| assert dist_file.is_file() |
|
|
|
|
| class TestWithAttrDirective: |
| @pytest.mark.parametrize( |
| ("folder", "opts"), |
| [ |
| ("src", {}), |
| ("lib", {"packages": "find:", "packages.find": {"where": "lib"}}), |
| ], |
| ) |
| def test_setupcfg_metadata(self, tmp_path, folder, opts): |
| files = [f"{folder}/pkg/__init__.py", "setup.cfg"] |
| _populate_project_dir(tmp_path, files, opts) |
|
|
| config = (tmp_path / "setup.cfg").read_text(encoding="utf-8") |
| overwrite = { |
| folder: {"pkg": {"__init__.py": "version = 42"}}, |
| "setup.cfg": "[metadata]\nversion = attr: pkg.version\n" + config, |
| } |
| jaraco.path.build(overwrite, prefix=tmp_path) |
|
|
| dist = _get_dist(tmp_path, {}) |
| assert dist.get_name() == "pkg" |
| assert dist.get_version() == "42" |
| assert dist.package_dir |
| package_path = find_package_path("pkg", dist.package_dir, tmp_path) |
| assert os.path.exists(package_path) |
| assert folder in Path(package_path).parts() |
|
|
| _run_build(tmp_path, "--sdist") |
| dist_file = tmp_path / "dist/pkg-42.tar.gz" |
| assert dist_file.is_file() |
|
|
| def test_pyproject_metadata(self, tmp_path): |
| _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {}) |
|
|
| overwrite = { |
| "src": {"pkg": {"__init__.py": "version = 42"}}, |
| "pyproject.toml": ( |
| "[project]\nname = 'pkg'\ndynamic = ['version']\n" |
| "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n" |
| ), |
| } |
| jaraco.path.build(overwrite, prefix=tmp_path) |
|
|
| dist = _get_dist(tmp_path, {}) |
| assert dist.get_version() == "42" |
| assert dist.package_dir == {"": "src"} |
|
|
|
|
| class TestWithCExtension: |
| def _simulate_package_with_extension(self, tmp_path): |
| |
| files = [ |
| "benchmarks/file.py", |
| "docs/Makefile", |
| "docs/requirements.txt", |
| "docs/source/conf.py", |
| "proj/header.h", |
| "proj/file.py", |
| "py/proj.cpp", |
| "py/other.cpp", |
| "py/file.py", |
| "py/py.typed", |
| "py/tests/test_proj.py", |
| "README.rst", |
| ] |
| _populate_project_dir(tmp_path, files, {}) |
|
|
| setup_script = """ |
| from setuptools import Extension, setup |
| |
| ext_modules = [ |
| Extension( |
| "proj", |
| ["py/proj.cpp", "py/other.cpp"], |
| include_dirs=["."], |
| language="c++", |
| ), |
| ] |
| setup(ext_modules=ext_modules) |
| """ |
| (tmp_path / "setup.py").write_text(DALS(setup_script), encoding="utf-8") |
|
|
| def test_skip_discovery_with_setupcfg_metadata(self, tmp_path): |
| """Ensure that auto-discovery is not triggered when the project is based on |
| C-extensions only, for backward compatibility. |
| """ |
| self._simulate_package_with_extension(tmp_path) |
|
|
| pyproject = """ |
| [build-system] |
| requires = [] |
| build-backend = 'setuptools.build_meta' |
| """ |
| (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") |
|
|
| setupcfg = """ |
| [metadata] |
| name = proj |
| version = 42 |
| """ |
| (tmp_path / "setup.cfg").write_text(DALS(setupcfg), encoding="utf-8") |
|
|
| dist = _get_dist(tmp_path, {}) |
| assert dist.get_name() == "proj" |
| assert dist.get_version() == "42" |
| assert dist.py_modules is None |
| assert dist.packages is None |
| assert len(dist.ext_modules) == 1 |
| assert dist.ext_modules[0].name == "proj" |
|
|
| def test_dont_skip_discovery_with_pyproject_metadata(self, tmp_path): |
| """When opting-in to pyproject.toml metadata, auto-discovery will be active if |
| the package lists C-extensions, but does not configure py-modules or packages. |
| |
| This way we ensure users with complex package layouts that would lead to the |
| discovery of multiple top-level modules/packages see errors and are forced to |
| explicitly set ``packages`` or ``py-modules``. |
| """ |
| self._simulate_package_with_extension(tmp_path) |
|
|
| pyproject = """ |
| [project] |
| name = 'proj' |
| version = '42' |
| """ |
| (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8") |
| with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"): |
| _get_dist(tmp_path, {}) |
|
|
|
|
| class TestWithPackageData: |
| def _simulate_package_with_data_files(self, tmp_path, src_root): |
| files = [ |
| f"{src_root}/proj/__init__.py", |
| f"{src_root}/proj/file1.txt", |
| f"{src_root}/proj/nested/file2.txt", |
| ] |
| _populate_project_dir(tmp_path, files, {}) |
|
|
| manifest = """ |
| global-include *.py *.txt |
| """ |
| (tmp_path / "MANIFEST.in").write_text(DALS(manifest), encoding="utf-8") |
|
|
| EXAMPLE_SETUPCFG = """ |
| [metadata] |
| name = proj |
| version = 42 |
| |
| [options] |
| include_package_data = True |
| """ |
| EXAMPLE_PYPROJECT = """ |
| [project] |
| name = "proj" |
| version = "42" |
| """ |
|
|
| PYPROJECT_PACKAGE_DIR = """ |
| [tool.setuptools] |
| package-dir = {"" = "src"} |
| """ |
|
|
| @pytest.mark.parametrize( |
| ("src_root", "files"), |
| [ |
| (".", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}), |
| (".", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}), |
| ("src", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}), |
| ("src", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}), |
| ( |
| "src", |
| { |
| "setup.cfg": DALS(EXAMPLE_SETUPCFG) |
| + DALS( |
| """ |
| packages = find: |
| package_dir = |
| =src |
| |
| [options.packages.find] |
| where = src |
| """ |
| ) |
| }, |
| ), |
| ( |
| "src", |
| { |
| "pyproject.toml": DALS(EXAMPLE_PYPROJECT) |
| + DALS( |
| """ |
| [tool.setuptools] |
| package-dir = {"" = "src"} |
| """ |
| ) |
| }, |
| ), |
| ], |
| ) |
| def test_include_package_data(self, tmp_path, src_root, files): |
| """ |
| Make sure auto-discovery does not affect package include_package_data. |
| See issue #3196. |
| """ |
| jaraco.path.build(files, prefix=str(tmp_path)) |
| self._simulate_package_with_data_files(tmp_path, src_root) |
|
|
| expected = { |
| os.path.normpath(f"{src_root}/proj/file1.txt").replace(os.sep, "/"), |
| os.path.normpath(f"{src_root}/proj/nested/file2.txt").replace(os.sep, "/"), |
| } |
|
|
| _run_build(tmp_path) |
|
|
| sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz"))) |
| print("~~~~~ sdist_members ~~~~~") |
| print('\n'.join(sdist_files)) |
| assert sdist_files >= expected |
|
|
| wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl"))) |
| print("~~~~~ wheel_members ~~~~~") |
| print('\n'.join(wheel_files)) |
| orig_files = {f.replace("src/", "").replace("lib/", "") for f in expected} |
| assert wheel_files >= orig_files |
|
|
|
|
| def test_compatible_with_numpy_configuration(tmp_path): |
| files = [ |
| "dir1/__init__.py", |
| "dir2/__init__.py", |
| "file.py", |
| ] |
| _populate_project_dir(tmp_path, files, {}) |
| dist = Distribution({}) |
| dist.configuration = object() |
| dist.set_defaults() |
| assert dist.py_modules is None |
| assert dist.packages is None |
|
|
|
|
| def test_name_discovery_doesnt_break_cli(tmpdir_cwd): |
| jaraco.path.build({"pkg.py": ""}) |
| dist = Distribution({}) |
| dist.script_args = ["--name"] |
| dist.set_defaults() |
| dist.parse_command_line() |
| assert dist.get_name() == "pkg" |
|
|
|
|
| def test_preserve_explicit_name_with_dynamic_version(tmpdir_cwd, monkeypatch): |
| """According to #3545 it seems that ``name`` discovery is running, |
| even when the project already explicitly sets it. |
| This seems to be related to parsing of dynamic versions (via ``attr`` directive), |
| which requires the auto-discovery of ``package_dir``. |
| """ |
| files = { |
| "src": { |
| "pkg": {"__init__.py": "__version__ = 42\n"}, |
| }, |
| "pyproject.toml": DALS( |
| """ |
| [project] |
| name = "myproj" # purposefully different from package name |
| dynamic = ["version"] |
| [tool.setuptools.dynamic] |
| version = {"attr" = "pkg.__version__"} |
| """ |
| ), |
| } |
| jaraco.path.build(files) |
| dist = Distribution({}) |
| orig_analyse_name = dist.set_defaults.analyse_name |
|
|
| def spy_analyse_name(): |
| |
| |
| orig_analyse_name() |
| assert dist.get_name() == "myproj" |
|
|
| monkeypatch.setattr(dist.set_defaults, "analyse_name", spy_analyse_name) |
| dist.parse_config_files() |
| assert dist.get_version() == "42" |
| assert set(dist.packages) == {"pkg"} |
|
|
|
|
| def _populate_project_dir(root, files, options): |
| |
| |
| |
| basic = { |
| "setup.py": "import setuptools\nsetuptools.setup()", |
| "README.md": "# Example Package", |
| "LICENSE": "Copyright (c) 2018", |
| } |
| jaraco.path.build(basic, prefix=root) |
| _write_setupcfg(root, options) |
| paths = (root / f for f in files) |
| for path in paths: |
| path.parent.mkdir(exist_ok=True, parents=True) |
| path.touch() |
|
|
|
|
| def _write_setupcfg(root, options): |
| if not options: |
| print("~~~~~ **NO** setup.cfg ~~~~~") |
| return |
| setupcfg = ConfigParser() |
| setupcfg.add_section("options") |
| for key, value in options.items(): |
| if key == "packages.find": |
| setupcfg.add_section(f"options.{key}") |
| setupcfg[f"options.{key}"].update(value) |
| elif isinstance(value, list): |
| setupcfg["options"][key] = ", ".join(value) |
| elif isinstance(value, dict): |
| str_value = "\n".join(f"\t{k} = {v}" for k, v in value.items()) |
| setupcfg["options"][key] = "\n" + str_value |
| else: |
| setupcfg["options"][key] = str(value) |
| with open(root / "setup.cfg", "w", encoding="utf-8") as f: |
| setupcfg.write(f) |
| print("~~~~~ setup.cfg ~~~~~") |
| print((root / "setup.cfg").read_text(encoding="utf-8")) |
|
|
|
|
| def _run_build(path, *flags): |
| cmd = [sys.executable, "-m", "build", "--no-isolation", *flags, str(path)] |
| return run(cmd, env={'DISTUTILS_DEBUG': ''}) |
|
|
|
|
| def _get_dist(dist_path, attrs): |
| root = "/".join(os.path.split(dist_path)) |
|
|
| script = dist_path / 'setup.py' |
| if script.exists(): |
| with Path(dist_path): |
| dist = cast( |
| Distribution, |
| distutils.core.run_setup("setup.py", {}, stop_after="init"), |
| ) |
| else: |
| dist = Distribution(attrs) |
|
|
| dist.src_root = root |
| dist.script_name = "setup.py" |
| with Path(dist_path): |
| dist.parse_config_files() |
|
|
| dist.set_defaults() |
| return dist |
|
|
|
|
| def _run_sdist_programatically(dist_path, attrs): |
| dist = _get_dist(dist_path, attrs) |
| cmd = sdist(dist) |
| cmd.ensure_finalized() |
| assert cmd.distribution.packages or cmd.distribution.py_modules |
|
|
| with quiet(), Path(dist_path): |
| cmd.run() |
|
|
| return dist, cmd |
|
|