match_face/.venv/Lib/site-packages/setuptools/tests/test_config_discovery.py

644 lines
22 KiB
Python
Raw Normal View History

import os
import sys
from configparser import ConfigParser
from itertools import product
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 = 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