about summary refs log tree commit diff
path: root/.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py
diff options
context:
space:
mode:
authorS. Solomon Darnell2025-03-28 21:52:21 -0500
committerS. Solomon Darnell2025-03-28 21:52:21 -0500
commit4a52a71956a8d46fcb7294ac71734504bb09bcc2 (patch)
treeee3dc5af3b6313e921cd920906356f5d4febc4ed /.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are here HEAD master
Diffstat (limited to '.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py')
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py647
1 files changed, 647 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py
new file mode 100644
index 00000000..b5df8203
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py
@@ -0,0 +1,647 @@
+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
+
+    @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)
+
+        # 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'
+            """
+        ),
+    }
+
+    @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:
+            # 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
+
+    @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"}),
+            (
+                # Type stubs can also be namespaced
+                ["namespace-stubs/pkg/__init__.pyi"],
+                {"pkg", "namespace-stubs", "namespace-stubs.pkg"},
+            ),
+            (
+                # Just the top-level package can have `-stubs`, ignore nested ones
+                ["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(".")
+        # 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"],
+    }
+
+    @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")
+        # Expected distribution file
+        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):
+        # 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"}
+    """
+
+    @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()  # <-- 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