modeling_simple
/
.venv
/lib
/python3.14
/site-packages
/setuptools
/tests
/test_config_discovery.py
| 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 # noqa: F401 # force distutils.core to be patched | |
| 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): | |
| # find_parent_package should find a non-namespace parent package | |
| (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): | |
| # find_parent_package should return null if the given list of packages does not | |
| # have a single parent package | |
| 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 = { | |
| # Different options according to the circumstance being tested | |
| "explicit-src": {"package_dir": {"": "src"}, "packages": ["pkg"]}, | |
| "variation-lib": { | |
| "package_dir": {"": "lib"}, # variation of the source-layout | |
| }, | |
| "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 | |
| 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) | |
| def test_project(self, tmp_path, circumstance): | |
| files, options = self._get_info(circumstance) | |
| _populate_project_dir(tmp_path, files, options) | |
| # Simulate a pre-existing `build` directory | |
| (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 | |
| # Make sure build files are not included by mistake | |
| 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' | |
| """ | |
| ), | |
| } | |
| 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: | |
| # Make sure build works with or without setup.cfg | |
| 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, {}) | |
| # When either parameter package or py_modules is an empty list, | |
| # then there should be no discovery | |
| assert getattr(dist, param) == [] | |
| other = {"py_modules": "packages", "packages": "py_modules"}[param] | |
| assert getattr(dist, other) is None | |
| 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 | |
| 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(".") | |
| # Ensure py_modules are found | |
| wheel_files = get_wheel_members(next(tmp_path.glob("*.whl"))) | |
| assert "foo.py" in wheel_files | |
| class TestNoConfig: | |
| DEFAULT_VERSION = "0.0.0" # Default version given by setuptools | |
| 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"], | |
| } | |
| 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") | |
| # Expected distribution file | |
| dist_file = tmp_path / f"dist/ns_nested_pkg-{self.DEFAULT_VERSION}.tar.gz" | |
| assert dist_file.is_file() | |
| class TestWithAttrDirective: | |
| 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): | |
| # This example is based on: https://github.com/nucleic/kiwi/tree/1.4.0 | |
| 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"} | |
| """ | |
| 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() # <-- no exception should be raised here. | |
| 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(): | |
| # We can check if name discovery was triggered by ensuring the original | |
| # name remains instead of the package 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): | |
| # NOTE: Currently pypa/build will refuse to build the project if no | |
| # `pyproject.toml` or `setup.py` is found. So it is impossible to do | |
| # completely "config-less" projects. | |
| 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)) # POSIX-style | |
| 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 | |