aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/setuptools/tests/config
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/config
parentcc961e04ba734dd72309fb548a2f97d67d578813 (diff)
downloadgn-ai-master.tar.gz
two version of R2R are hereHEADmaster
Diffstat (limited to '.venv/lib/python3.12/site-packages/setuptools/tests/config')
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/downloads/__init__.py59
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/downloads/preload.py18
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/setupcfg_examples.txt22
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py772
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/test_expand.py247
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/test_pyprojecttoml.py396
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py109
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/config/test_setupcfg.py969
9 files changed, 2592 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/tests/config/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/downloads/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/tests/config/downloads/__init__.py
new file mode 100644
index 00000000..00a16423
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/downloads/__init__.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+import re
+import time
+from pathlib import Path
+from urllib.error import HTTPError
+from urllib.request import urlopen
+
+__all__ = ["DOWNLOAD_DIR", "retrieve_file", "output_file", "urls_from_file"]
+
+
+NAME_REMOVE = ("http://", "https://", "github.com/", "/raw/")
+DOWNLOAD_DIR = Path(__file__).parent
+
+
+# ----------------------------------------------------------------------
+# Please update ./preload.py accordingly when modifying this file
+# ----------------------------------------------------------------------
+
+
+def output_file(url: str, download_dir: Path = DOWNLOAD_DIR) -> Path:
+ file_name = url.strip()
+ for part in NAME_REMOVE:
+ file_name = file_name.replace(part, '').strip().strip('/:').strip()
+ return Path(download_dir, re.sub(r"[^\-_\.\w\d]+", "_", file_name))
+
+
+def retrieve_file(url: str, download_dir: Path = DOWNLOAD_DIR, wait: float = 5) -> Path:
+ path = output_file(url, download_dir)
+ if path.exists():
+ print(f"Skipping {url} (already exists: {path})")
+ else:
+ download_dir.mkdir(exist_ok=True, parents=True)
+ print(f"Downloading {url} to {path}")
+ try:
+ download(url, path)
+ except HTTPError:
+ time.sleep(wait) # wait a few seconds and try again.
+ download(url, path)
+ return path
+
+
+def urls_from_file(list_file: Path) -> list[str]:
+ """``list_file`` should be a text file where each line corresponds to a URL to
+ download.
+ """
+ print(f"file: {list_file}")
+ content = list_file.read_text(encoding="utf-8")
+ return [url for url in content.splitlines() if not url.startswith("#")]
+
+
+def download(url: str, dest: Path):
+ with urlopen(url) as f:
+ data = f.read()
+
+ with open(dest, "wb") as f:
+ f.write(data)
+
+ assert Path(dest).exists()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/downloads/preload.py b/.venv/lib/python3.12/site-packages/setuptools/tests/config/downloads/preload.py
new file mode 100644
index 00000000..8eeb5dd7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/downloads/preload.py
@@ -0,0 +1,18 @@
+"""This file can be used to preload files needed for testing.
+
+For example you can use::
+
+ cd setuptools/tests/config
+ python -m downloads.preload setupcfg_examples.txt
+
+to make sure the `setup.cfg` examples are downloaded before starting the tests.
+"""
+
+import sys
+from pathlib import Path
+
+from . import retrieve_file, urls_from_file
+
+if __name__ == "__main__":
+ urls = urls_from_file(Path(sys.argv[1]))
+ list(map(retrieve_file, urls))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/setupcfg_examples.txt b/.venv/lib/python3.12/site-packages/setuptools/tests/config/setupcfg_examples.txt
new file mode 100644
index 00000000..6aab887f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/setupcfg_examples.txt
@@ -0,0 +1,22 @@
+# ====================================================================
+# Some popular packages that use setup.cfg (and others not so popular)
+# Reference: https://hugovk.github.io/top-pypi-packages/
+# ====================================================================
+https://github.com/pypa/setuptools/raw/52c990172fec37766b3566679724aa8bf70ae06d/setup.cfg
+https://github.com/pypa/wheel/raw/0acd203cd896afec7f715aa2ff5980a403459a3b/setup.cfg
+https://github.com/python/importlib_metadata/raw/2f05392ca980952a6960d82b2f2d2ea10aa53239/setup.cfg
+https://github.com/jaraco/skeleton/raw/d9008b5c510cd6969127a6a2ab6f832edddef296/setup.cfg
+https://github.com/jaraco/zipp/raw/700d3a96390e970b6b962823bfea78b4f7e1c537/setup.cfg
+https://github.com/pallets/jinja/raw/7d72eb7fefb7dce065193967f31f805180508448/setup.cfg
+https://github.com/tkem/cachetools/raw/2fd87a94b8d3861d80e9e4236cd480bfdd21c90d/setup.cfg
+https://github.com/aio-libs/aiohttp/raw/5e0e6b7080f2408d5f1dd544c0e1cf88378b7b10/setup.cfg
+https://github.com/pallets/flask/raw/9486b6cf57bd6a8a261f67091aca8ca78eeec1e3/setup.cfg
+https://github.com/pallets/click/raw/6411f425fae545f42795665af4162006b36c5e4a/setup.cfg
+https://github.com/sqlalchemy/sqlalchemy/raw/533f5718904b620be8d63f2474229945d6f8ba5d/setup.cfg
+https://github.com/pytest-dev/pluggy/raw/461ef63291d13589c4e21aa182cd1529257e9a0a/setup.cfg
+https://github.com/pytest-dev/pytest/raw/c7be96dae487edbd2f55b561b31b68afac1dabe6/setup.cfg
+https://github.com/platformdirs/platformdirs/raw/7b7852128dd6f07511b618d6edea35046bd0c6ff/setup.cfg
+https://github.com/pandas-dev/pandas/raw/bc17343f934a33dc231c8c74be95d8365537c376/setup.cfg
+https://github.com/django/django/raw/4e249d11a6e56ca8feb4b055b681cec457ef3a3d/setup.cfg
+https://github.com/pyscaffold/pyscaffold/raw/de7aa5dc059fbd04307419c667cc4961bc9df4b8/setup.cfg
+https://github.com/pypa/virtualenv/raw/f92eda6e3da26a4d28c2663ffb85c4960bdb990c/setup.cfg
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py
new file mode 100644
index 00000000..489fd98e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_apply_pyprojecttoml.py
@@ -0,0 +1,772 @@
+"""Make sure that applying the configuration from pyproject.toml is equivalent to
+applying a similar configuration from setup.cfg
+
+To run these tests offline, please have a look on ``./downloads/preload.py``
+"""
+
+from __future__ import annotations
+
+import io
+import re
+import tarfile
+from inspect import cleandoc
+from pathlib import Path
+from unittest.mock import Mock
+
+import pytest
+from ini2toml.api import LiteTranslator
+from packaging.metadata import Metadata
+
+import setuptools # noqa: F401 # ensure monkey patch to metadata
+from setuptools._static import is_static
+from setuptools.command.egg_info import write_requirements
+from setuptools.config import expand, pyprojecttoml, setupcfg
+from setuptools.config._apply_pyprojecttoml import _MissingDynamic, _some_attrgetter
+from setuptools.dist import Distribution
+from setuptools.errors import InvalidConfigError, RemovedConfigError
+from setuptools.warnings import InformationOnly, SetuptoolsDeprecationWarning
+
+from .downloads import retrieve_file, urls_from_file
+
+HERE = Path(__file__).parent
+EXAMPLES_FILE = "setupcfg_examples.txt"
+
+
+def makedist(path, **attrs):
+ return Distribution({"src_root": path, **attrs})
+
+
+def _mock_expand_patterns(patterns, *_, **__):
+ """
+ Allow comparing the given patterns for 2 dist objects.
+ We need to strip special chars to avoid errors when validating.
+ """
+ return [re.sub("[^a-z0-9]+", "", p, flags=re.I) or "empty" for p in patterns]
+
+
+@pytest.mark.parametrize("url", urls_from_file(HERE / EXAMPLES_FILE))
+@pytest.mark.filterwarnings("ignore")
+@pytest.mark.uses_network
+def test_apply_pyproject_equivalent_to_setupcfg(url, monkeypatch, tmp_path):
+ monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.0.1"))
+ monkeypatch.setattr(
+ Distribution, "_expand_patterns", Mock(side_effect=_mock_expand_patterns)
+ )
+ setupcfg_example = retrieve_file(url)
+ pyproject_example = Path(tmp_path, "pyproject.toml")
+ setupcfg_text = setupcfg_example.read_text(encoding="utf-8")
+ toml_config = LiteTranslator().translate(setupcfg_text, "setup.cfg")
+ pyproject_example.write_text(toml_config, encoding="utf-8")
+
+ dist_toml = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject_example)
+ dist_cfg = setupcfg.apply_configuration(makedist(tmp_path), setupcfg_example)
+
+ pkg_info_toml = core_metadata(dist_toml)
+ pkg_info_cfg = core_metadata(dist_cfg)
+ assert pkg_info_toml == pkg_info_cfg
+
+ if any(getattr(d, "license_files", None) for d in (dist_toml, dist_cfg)):
+ assert set(dist_toml.license_files) == set(dist_cfg.license_files)
+
+ if any(getattr(d, "entry_points", None) for d in (dist_toml, dist_cfg)):
+ print(dist_cfg.entry_points)
+ ep_toml = {
+ (k, *sorted(i.replace(" ", "") for i in v))
+ for k, v in dist_toml.entry_points.items()
+ }
+ ep_cfg = {
+ (k, *sorted(i.replace(" ", "") for i in v))
+ for k, v in dist_cfg.entry_points.items()
+ }
+ assert ep_toml == ep_cfg
+
+ if any(getattr(d, "package_data", None) for d in (dist_toml, dist_cfg)):
+ pkg_data_toml = {(k, *sorted(v)) for k, v in dist_toml.package_data.items()}
+ pkg_data_cfg = {(k, *sorted(v)) for k, v in dist_cfg.package_data.items()}
+ assert pkg_data_toml == pkg_data_cfg
+
+ if any(getattr(d, "data_files", None) for d in (dist_toml, dist_cfg)):
+ data_files_toml = {(k, *sorted(v)) for k, v in dist_toml.data_files}
+ data_files_cfg = {(k, *sorted(v)) for k, v in dist_cfg.data_files}
+ assert data_files_toml == data_files_cfg
+
+ assert set(dist_toml.install_requires) == set(dist_cfg.install_requires)
+ if any(getattr(d, "extras_require", None) for d in (dist_toml, dist_cfg)):
+ extra_req_toml = {(k, *sorted(v)) for k, v in dist_toml.extras_require.items()}
+ extra_req_cfg = {(k, *sorted(v)) for k, v in dist_cfg.extras_require.items()}
+ assert extra_req_toml == extra_req_cfg
+
+
+PEP621_EXAMPLE = """\
+[project]
+name = "spam"
+version = "2020.0.0"
+description = "Lovely Spam! Wonderful Spam!"
+readme = "README.rst"
+requires-python = ">=3.8"
+license-files = ["LICENSE.txt"] # Updated to be PEP 639 compliant
+keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
+authors = [
+ {email = "hi@pradyunsg.me"},
+ {name = "Tzu-Ping Chung"}
+]
+maintainers = [
+ {name = "Brett Cannon", email = "brett@python.org"},
+ {name = "John X. Ãørçeč", email = "john@utf8.org"},
+ {name = "Γαμα קּ 東", email = "gama@utf8.org"},
+]
+classifiers = [
+ "Development Status :: 4 - Beta",
+ "Programming Language :: Python"
+]
+
+dependencies = [
+ "httpx",
+ "gidgethub[httpx]>4.0.0",
+ "django>2.1; os_name != 'nt'",
+ "django>2.0; os_name == 'nt'"
+]
+
+[project.optional-dependencies]
+test = [
+ "pytest < 5.0.0",
+ "pytest-cov[all]"
+]
+
+[project.urls]
+homepage = "http://example.com"
+documentation = "http://readthedocs.org"
+repository = "http://github.com"
+changelog = "http://github.com/me/spam/blob/master/CHANGELOG.md"
+
+[project.scripts]
+spam-cli = "spam:main_cli"
+
+[project.gui-scripts]
+spam-gui = "spam:main_gui"
+
+[project.entry-points."spam.magical"]
+tomatoes = "spam:main_tomatoes"
+"""
+
+PEP621_INTERNATIONAL_EMAIL_EXAMPLE = """\
+[project]
+name = "spam"
+version = "2020.0.0"
+authors = [
+ {email = "hi@pradyunsg.me"},
+ {name = "Tzu-Ping Chung"}
+]
+maintainers = [
+ {name = "Степан Бандера", email = "криївка@оун-упа.укр"},
+]
+"""
+
+PEP621_EXAMPLE_SCRIPT = """
+def main_cli(): pass
+def main_gui(): pass
+def main_tomatoes(): pass
+"""
+
+PEP639_LICENSE_TEXT = """\
+[project]
+name = "spam"
+version = "2020.0.0"
+authors = [
+ {email = "hi@pradyunsg.me"},
+ {name = "Tzu-Ping Chung"}
+]
+license = {text = "MIT"}
+"""
+
+PEP639_LICENSE_EXPRESSION = """\
+[project]
+name = "spam"
+version = "2020.0.0"
+authors = [
+ {email = "hi@pradyunsg.me"},
+ {name = "Tzu-Ping Chung"}
+]
+license = "mit or apache-2.0" # should be normalized in metadata
+classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Programming Language :: Python",
+]
+"""
+
+
+def _pep621_example_project(
+ tmp_path,
+ readme="README.rst",
+ pyproject_text=PEP621_EXAMPLE,
+):
+ pyproject = tmp_path / "pyproject.toml"
+ text = pyproject_text
+ replacements = {'readme = "README.rst"': f'readme = "{readme}"'}
+ for orig, subst in replacements.items():
+ text = text.replace(orig, subst)
+ pyproject.write_text(text, encoding="utf-8")
+
+ (tmp_path / readme).write_text("hello world", encoding="utf-8")
+ (tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---", encoding="utf-8")
+ (tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT, encoding="utf-8")
+ return pyproject
+
+
+def test_pep621_example(tmp_path):
+ """Make sure the example in PEP 621 works"""
+ pyproject = _pep621_example_project(tmp_path)
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+ assert set(dist.metadata.license_files) == {"LICENSE.txt"}
+
+
+@pytest.mark.parametrize(
+ ("readme", "ctype"),
+ [
+ ("Readme.txt", "text/plain"),
+ ("readme.md", "text/markdown"),
+ ("text.rst", "text/x-rst"),
+ ],
+)
+def test_readme_content_type(tmp_path, readme, ctype):
+ pyproject = _pep621_example_project(tmp_path, readme)
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+ assert dist.metadata.long_description_content_type == ctype
+
+
+def test_undefined_content_type(tmp_path):
+ pyproject = _pep621_example_project(tmp_path, "README.tex")
+ with pytest.raises(ValueError, match="Undefined content type for README.tex"):
+ pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+
+def test_no_explicit_content_type_for_missing_extension(tmp_path):
+ pyproject = _pep621_example_project(tmp_path, "README")
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+ assert dist.metadata.long_description_content_type is None
+
+
+@pytest.mark.parametrize(
+ ("pyproject_text", "expected_maintainers_meta_value"),
+ (
+ pytest.param(
+ PEP621_EXAMPLE,
+ (
+ 'Brett Cannon <brett@python.org>, "John X. Ãørçeč" <john@utf8.org>, '
+ 'Γαμα קּ 東 <gama@utf8.org>'
+ ),
+ id='non-international-emails',
+ ),
+ pytest.param(
+ PEP621_INTERNATIONAL_EMAIL_EXAMPLE,
+ 'Степан Бандера <криївка@оун-упа.укр>',
+ marks=pytest.mark.xfail(
+ reason="CPython's `email.headerregistry.Address` only supports "
+ 'RFC 5322, as of Nov 10, 2022 and latest Python 3.11.0',
+ strict=True,
+ ),
+ id='international-email',
+ ),
+ ),
+)
+def test_utf8_maintainer_in_metadata( # issue-3663
+ expected_maintainers_meta_value,
+ pyproject_text,
+ tmp_path,
+):
+ pyproject = _pep621_example_project(
+ tmp_path,
+ "README",
+ pyproject_text=pyproject_text,
+ )
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+ assert dist.metadata.maintainer_email == expected_maintainers_meta_value
+ pkg_file = tmp_path / "PKG-FILE"
+ with open(pkg_file, "w", encoding="utf-8") as fh:
+ dist.metadata.write_pkg_file(fh)
+ content = pkg_file.read_text(encoding="utf-8")
+ assert f"Maintainer-email: {expected_maintainers_meta_value}" in content
+
+
+@pytest.mark.parametrize(
+ (
+ 'pyproject_text',
+ 'license',
+ 'license_expression',
+ 'content_str',
+ 'not_content_str',
+ ),
+ (
+ pytest.param(
+ PEP639_LICENSE_TEXT,
+ 'MIT',
+ None,
+ 'License: MIT',
+ 'License-Expression: ',
+ id='license-text',
+ marks=[
+ pytest.mark.filterwarnings(
+ "ignore:.project.license. as a TOML table is deprecated",
+ )
+ ],
+ ),
+ pytest.param(
+ PEP639_LICENSE_EXPRESSION,
+ None,
+ 'MIT OR Apache-2.0',
+ 'License-Expression: MIT OR Apache-2.0',
+ 'License: ',
+ id='license-expression',
+ ),
+ ),
+)
+def test_license_in_metadata(
+ license,
+ license_expression,
+ content_str,
+ not_content_str,
+ pyproject_text,
+ tmp_path,
+):
+ pyproject = _pep621_example_project(
+ tmp_path,
+ "README",
+ pyproject_text=pyproject_text,
+ )
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+ assert dist.metadata.license == license
+ assert dist.metadata.license_expression == license_expression
+ pkg_file = tmp_path / "PKG-FILE"
+ with open(pkg_file, "w", encoding="utf-8") as fh:
+ dist.metadata.write_pkg_file(fh)
+ content = pkg_file.read_text(encoding="utf-8")
+ assert "Metadata-Version: 2.4" in content
+ assert content_str in content
+ assert not_content_str not in content
+
+
+def test_license_classifier_with_license_expression(tmp_path):
+ text = PEP639_LICENSE_EXPRESSION.rsplit("\n", 2)[0]
+ pyproject = _pep621_example_project(
+ tmp_path,
+ "README",
+ f"{text}\n \"License :: OSI Approved :: MIT License\"\n]",
+ )
+ msg = "License classifiers have been superseded by license expressions"
+ with pytest.raises(InvalidConfigError, match=msg) as exc:
+ pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+ assert "License :: OSI Approved :: MIT License" in str(exc.value)
+
+
+def test_license_classifier_without_license_expression(tmp_path):
+ text = """\
+ [project]
+ name = "spam"
+ version = "2020.0.0"
+ license = {text = "mit or apache-2.0"}
+ classifiers = ["License :: OSI Approved :: MIT License"]
+ """
+ pyproject = _pep621_example_project(tmp_path, "README", text)
+
+ msg1 = "License classifiers are deprecated(?:.|\n)*MIT License"
+ msg2 = ".project.license. as a TOML table is deprecated"
+ with (
+ pytest.warns(SetuptoolsDeprecationWarning, match=msg1),
+ pytest.warns(SetuptoolsDeprecationWarning, match=msg2),
+ ):
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+ # Check license classifier is still included
+ assert dist.metadata.get_classifiers() == ["License :: OSI Approved :: MIT License"]
+
+
+class TestLicenseFiles:
+ def base_pyproject(
+ self,
+ tmp_path,
+ additional_text="",
+ license_toml='license = {file = "LICENSE.txt"}\n',
+ ):
+ text = PEP639_LICENSE_EXPRESSION
+
+ # Sanity-check
+ assert 'license = "mit or apache-2.0"' in text
+ assert 'license-files' not in text
+ assert "[tool.setuptools]" not in text
+
+ text = re.sub(
+ r"(license = .*)\n",
+ license_toml,
+ text,
+ count=1,
+ )
+ assert license_toml in text # sanity check
+ text = f"{text}\n{additional_text}\n"
+ pyproject = _pep621_example_project(tmp_path, "README", pyproject_text=text)
+ return pyproject
+
+ def base_pyproject_license_pep639(self, tmp_path, additional_text=""):
+ return self.base_pyproject(
+ tmp_path,
+ additional_text=additional_text,
+ license_toml='license = "licenseref-Proprietary"'
+ '\nlicense-files = ["_FILE*"]\n',
+ )
+
+ def test_both_license_and_license_files_defined(self, tmp_path):
+ setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]'
+ pyproject = self.base_pyproject(tmp_path, setuptools_config)
+
+ (tmp_path / "_FILE.txt").touch()
+ (tmp_path / "_FILE.rst").touch()
+
+ # Would normally match the `license_files` patterns, but we want to exclude it
+ # by being explicit. On the other hand, contents should be added to `license`
+ license = tmp_path / "LICENSE.txt"
+ license.write_text("LicenseRef-Proprietary\n", encoding="utf-8")
+
+ msg1 = "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'"
+ msg2 = ".project.license. as a TOML table is deprecated"
+ with (
+ pytest.warns(SetuptoolsDeprecationWarning, match=msg1),
+ pytest.warns(SetuptoolsDeprecationWarning, match=msg2),
+ ):
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+ assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
+ assert dist.metadata.license == "LicenseRef-Proprietary\n"
+
+ def test_both_license_and_license_files_defined_pep639(self, tmp_path):
+ # Set license and license-files
+ pyproject = self.base_pyproject_license_pep639(tmp_path)
+
+ (tmp_path / "_FILE.txt").touch()
+ (tmp_path / "_FILE.rst").touch()
+
+ msg = "Normalizing.*LicenseRef"
+ with pytest.warns(InformationOnly, match=msg):
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+ assert set(dist.metadata.license_files) == {"_FILE.rst", "_FILE.txt"}
+ assert dist.metadata.license is None
+ assert dist.metadata.license_expression == "LicenseRef-Proprietary"
+
+ def test_license_files_defined_twice(self, tmp_path):
+ # Set project.license-files and tools.setuptools.license-files
+ setuptools_config = '[tool.setuptools]\nlicense-files = ["_FILE*"]'
+ pyproject = self.base_pyproject_license_pep639(tmp_path, setuptools_config)
+
+ msg = "'project.license-files' is defined already. Remove 'tool.setuptools.license-files'"
+ with pytest.raises(InvalidConfigError, match=msg):
+ pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+ def test_default_patterns(self, tmp_path):
+ setuptools_config = '[tool.setuptools]\nzip-safe = false'
+ # ^ used just to trigger section validation
+ pyproject = self.base_pyproject(tmp_path, setuptools_config, license_toml="")
+
+ license_files = "LICENCE-a.html COPYING-abc.txt AUTHORS-xyz NOTICE,def".split()
+
+ for fname in license_files:
+ (tmp_path / fname).write_text(f"{fname}\n", encoding="utf-8")
+
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+ assert (tmp_path / "LICENSE.txt").exists() # from base example
+ assert set(dist.metadata.license_files) == {*license_files, "LICENSE.txt"}
+
+ def test_missing_patterns(self, tmp_path):
+ pyproject = self.base_pyproject_license_pep639(tmp_path)
+ assert list(tmp_path.glob("_FILE*")) == [] # sanity check
+
+ msg1 = "Cannot find any files for the given pattern.*"
+ msg2 = "Normalizing 'licenseref-Proprietary' to 'LicenseRef-Proprietary'"
+ with (
+ pytest.warns(SetuptoolsDeprecationWarning, match=msg1),
+ pytest.warns(InformationOnly, match=msg2),
+ ):
+ pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+ def test_deprecated_file_expands_to_text(self, tmp_path):
+ """Make sure the old example with ``license = {text = ...}`` works"""
+
+ assert 'license-files = ["LICENSE.txt"]' in PEP621_EXAMPLE # sanity check
+ text = PEP621_EXAMPLE.replace(
+ 'license-files = ["LICENSE.txt"]',
+ 'license = {file = "LICENSE.txt"}',
+ )
+ pyproject = _pep621_example_project(tmp_path, pyproject_text=text)
+
+ msg = ".project.license. as a TOML table is deprecated"
+ with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
+ dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+ assert dist.metadata.license == "--- LICENSE stub ---"
+ assert set(dist.metadata.license_files) == {"LICENSE.txt"} # auto-filled
+
+
+class TestPyModules:
+ # https://github.com/pypa/setuptools/issues/4316
+
+ def dist(self, name):
+ toml_config = f"""
+ [project]
+ name = "test"
+ version = "42.0"
+ [tool.setuptools]
+ py-modules = [{name!r}]
+ """
+ pyproject = Path("pyproject.toml")
+ pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
+ return pyprojecttoml.apply_configuration(Distribution({}), pyproject)
+
+ @pytest.mark.parametrize("module", ["pip-run", "abc-d.λ-xyz-e"])
+ def test_valid_module_name(self, tmp_path, monkeypatch, module):
+ monkeypatch.chdir(tmp_path)
+ assert module in self.dist(module).py_modules
+
+ @pytest.mark.parametrize("module", ["pip run", "-pip-run", "pip-run-stubs"])
+ def test_invalid_module_name(self, tmp_path, monkeypatch, module):
+ monkeypatch.chdir(tmp_path)
+ with pytest.raises(ValueError, match="py-modules"):
+ self.dist(module).py_modules
+
+
+class TestExtModules:
+ def test_pyproject_sets_attribute(self, tmp_path, monkeypatch):
+ monkeypatch.chdir(tmp_path)
+ pyproject = Path("pyproject.toml")
+ toml_config = """
+ [project]
+ name = "test"
+ version = "42.0"
+ [tool.setuptools]
+ ext-modules = [
+ {name = "my.ext", sources = ["hello.c", "world.c"]}
+ ]
+ """
+ pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
+ with pytest.warns(pyprojecttoml._ExperimentalConfiguration):
+ dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject)
+ assert len(dist.ext_modules) == 1
+ assert dist.ext_modules[0].name == "my.ext"
+ assert set(dist.ext_modules[0].sources) == {"hello.c", "world.c"}
+
+
+class TestDeprecatedFields:
+ def test_namespace_packages(self, tmp_path):
+ pyproject = tmp_path / "pyproject.toml"
+ config = """
+ [project]
+ name = "myproj"
+ version = "42"
+ [tool.setuptools]
+ namespace-packages = ["myproj.pkg"]
+ """
+ pyproject.write_text(cleandoc(config), encoding="utf-8")
+ with pytest.raises(RemovedConfigError, match="namespace-packages"):
+ pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
+
+
+class TestPresetField:
+ def pyproject(self, tmp_path, dynamic, extra_content=""):
+ content = f"[project]\nname = 'proj'\ndynamic = {dynamic!r}\n"
+ if "version" not in dynamic:
+ content += "version = '42'\n"
+ file = tmp_path / "pyproject.toml"
+ file.write_text(content + extra_content, encoding="utf-8")
+ return file
+
+ @pytest.mark.parametrize(
+ ("attr", "field", "value"),
+ [
+ ("license_expression", "license", "MIT"),
+ pytest.param(
+ *("license", "license", "Not SPDX"),
+ marks=[pytest.mark.filterwarnings("ignore:.*license. overwritten")],
+ ),
+ ("classifiers", "classifiers", ["Private :: Classifier"]),
+ ("entry_points", "scripts", {"console_scripts": ["foobar=foobar:main"]}),
+ ("entry_points", "gui-scripts", {"gui_scripts": ["bazquux=bazquux:main"]}),
+ pytest.param(
+ *("install_requires", "dependencies", ["six"]),
+ marks=[
+ pytest.mark.filterwarnings("ignore:.*install_requires. overwritten")
+ ],
+ ),
+ ],
+ )
+ def test_not_listed_in_dynamic(self, tmp_path, attr, field, value):
+ """Setuptools cannot set a field if not listed in ``dynamic``"""
+ pyproject = self.pyproject(tmp_path, [])
+ dist = makedist(tmp_path, **{attr: value})
+ msg = re.compile(f"defined outside of `pyproject.toml`:.*{field}", re.S)
+ with pytest.warns(_MissingDynamic, match=msg):
+ dist = pyprojecttoml.apply_configuration(dist, pyproject)
+
+ dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist)
+ assert not dist_value
+
+ @pytest.mark.parametrize(
+ ("attr", "field", "value"),
+ [
+ ("license_expression", "license", "MIT"),
+ ("install_requires", "dependencies", []),
+ ("extras_require", "optional-dependencies", {}),
+ ("install_requires", "dependencies", ["six"]),
+ ("classifiers", "classifiers", ["Private :: Classifier"]),
+ ],
+ )
+ def test_listed_in_dynamic(self, tmp_path, attr, field, value):
+ pyproject = self.pyproject(tmp_path, [field])
+ dist = makedist(tmp_path, **{attr: value})
+ dist = pyprojecttoml.apply_configuration(dist, pyproject)
+ dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist)
+ assert dist_value == value
+
+ def test_license_files_exempt_from_dynamic(self, monkeypatch, tmp_path):
+ """
+ license-file is currently not considered in the context of dynamic.
+ As per 2025-02-19, https://packaging.python.org/en/latest/specifications/pyproject-toml/#license-files
+ allows setuptools to fill-in `license-files` the way it sees fit:
+
+ > If the license-files key is not defined, tools can decide how to handle license files.
+ > For example they can choose not to include any files or use their own
+ > logic to discover the appropriate files in the distribution.
+
+ Using license_files from setup.py to fill-in the value is in accordance
+ with this rule.
+ """
+ monkeypatch.chdir(tmp_path)
+ pyproject = self.pyproject(tmp_path, [])
+ dist = makedist(tmp_path, license_files=["LIC*"])
+ (tmp_path / "LIC1").write_text("42", encoding="utf-8")
+ dist = pyprojecttoml.apply_configuration(dist, pyproject)
+ assert dist.metadata.license_files == ["LIC1"]
+
+ def test_warning_overwritten_dependencies(self, tmp_path):
+ src = "[project]\nname='pkg'\nversion='0.1'\ndependencies=['click']\n"
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(src, encoding="utf-8")
+ dist = makedist(tmp_path, install_requires=["wheel"])
+ with pytest.warns(match="`install_requires` overwritten"):
+ dist = pyprojecttoml.apply_configuration(dist, pyproject)
+ assert "wheel" not in dist.install_requires
+
+ def test_optional_dependencies_dont_remove_env_markers(self, tmp_path):
+ """
+ Internally setuptools converts dependencies with markers to "extras".
+ If ``install_requires`` is given by ``setup.py``, we have to ensure that
+ applying ``optional-dependencies`` does not overwrite the mandatory
+ dependencies with markers (see #3204).
+ """
+ # If setuptools replace its internal mechanism that uses `requires.txt`
+ # this test has to be rewritten to adapt accordingly
+ extra = "\n[project.optional-dependencies]\nfoo = ['bar>1']\n"
+ pyproject = self.pyproject(tmp_path, ["dependencies"], extra)
+ install_req = ['importlib-resources (>=3.0.0) ; python_version < "3.7"']
+ dist = makedist(tmp_path, install_requires=install_req)
+ dist = pyprojecttoml.apply_configuration(dist, pyproject)
+ assert "foo" in dist.extras_require
+ egg_info = dist.get_command_obj("egg_info")
+ write_requirements(egg_info, tmp_path, tmp_path / "requires.txt")
+ reqs = (tmp_path / "requires.txt").read_text(encoding="utf-8")
+ assert "importlib-resources" in reqs
+ assert "bar" in reqs
+ assert ':python_version < "3.7"' in reqs
+
+ @pytest.mark.parametrize(
+ ("field", "group"),
+ [("scripts", "console_scripts"), ("gui-scripts", "gui_scripts")],
+ )
+ @pytest.mark.filterwarnings("error")
+ def test_scripts_dont_require_dynamic_entry_points(self, tmp_path, field, group):
+ # Issue 3862
+ pyproject = self.pyproject(tmp_path, [field])
+ dist = makedist(tmp_path, entry_points={group: ["foobar=foobar:main"]})
+ dist = pyprojecttoml.apply_configuration(dist, pyproject)
+ assert group in dist.entry_points
+
+
+class TestMeta:
+ def test_example_file_in_sdist(self, setuptools_sdist):
+ """Meta test to ensure tests can run from sdist"""
+ with tarfile.open(setuptools_sdist) as tar:
+ assert any(name.endswith(EXAMPLES_FILE) for name in tar.getnames())
+
+
+class TestInteropCommandLineParsing:
+ def test_version(self, tmp_path, monkeypatch, capsys):
+ # See pypa/setuptools#4047
+ # This test can be removed once the CLI interface of setup.py is removed
+ monkeypatch.chdir(tmp_path)
+ toml_config = """
+ [project]
+ name = "test"
+ version = "42.0"
+ """
+ pyproject = Path(tmp_path, "pyproject.toml")
+ pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
+ opts = {"script_args": ["--version"]}
+ dist = pyprojecttoml.apply_configuration(Distribution(opts), pyproject)
+ dist.parse_command_line() # <-- there should be no exception here.
+ captured = capsys.readouterr()
+ assert "42.0" in captured.out
+
+
+class TestStaticConfig:
+ def test_mark_static_fields(self, tmp_path, monkeypatch):
+ monkeypatch.chdir(tmp_path)
+ toml_config = """
+ [project]
+ name = "test"
+ version = "42.0"
+ dependencies = ["hello"]
+ keywords = ["world"]
+ classifiers = ["private :: hello world"]
+ [tool.setuptools]
+ obsoletes = ["abcd"]
+ provides = ["abcd"]
+ platforms = ["abcd"]
+ """
+ pyproject = Path(tmp_path, "pyproject.toml")
+ pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
+ dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject)
+ assert is_static(dist.install_requires)
+ assert is_static(dist.metadata.keywords)
+ assert is_static(dist.metadata.classifiers)
+ assert is_static(dist.metadata.obsoletes)
+ assert is_static(dist.metadata.provides)
+ assert is_static(dist.metadata.platforms)
+
+
+# --- Auxiliary Functions ---
+
+
+def core_metadata(dist) -> str:
+ with io.StringIO() as buffer:
+ dist.metadata.write_pkg_file(buffer)
+ pkg_file_txt = buffer.getvalue()
+
+ # Make sure core metadata is valid
+ Metadata.from_email(pkg_file_txt, validate=True) # can raise exceptions
+
+ skip_prefixes: tuple[str, ...] = ()
+ skip_lines = set()
+ # ---- DIFF NORMALISATION ----
+ # PEP 621 is very particular about author/maintainer metadata conversion, so skip
+ skip_prefixes += ("Author:", "Author-email:", "Maintainer:", "Maintainer-email:")
+ # May be redundant with Home-page
+ skip_prefixes += ("Project-URL: Homepage,", "Home-page:")
+ # May be missing in original (relying on default) but backfilled in the TOML
+ skip_prefixes += ("Description-Content-Type:",)
+ # Remove empty lines
+ skip_lines.add("")
+
+ result = []
+ for line in pkg_file_txt.splitlines():
+ if line.startswith(skip_prefixes) or line in skip_lines:
+ continue
+ result.append(line + "\n")
+
+ return "".join(result)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_expand.py b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_expand.py
new file mode 100644
index 00000000..c5710ec6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_expand.py
@@ -0,0 +1,247 @@
+import os
+import sys
+from pathlib import Path
+
+import pytest
+
+from setuptools._static import is_static
+from setuptools.config import expand
+from setuptools.discovery import find_package_path
+
+from distutils.errors import DistutilsOptionError
+
+
+def write_files(files, root_dir):
+ for file, content in files.items():
+ path = root_dir / file
+ path.parent.mkdir(exist_ok=True, parents=True)
+ path.write_text(content, encoding="utf-8")
+
+
+def test_glob_relative(tmp_path, monkeypatch):
+ files = {
+ "dir1/dir2/dir3/file1.txt",
+ "dir1/dir2/file2.txt",
+ "dir1/file3.txt",
+ "a.ini",
+ "b.ini",
+ "dir1/c.ini",
+ "dir1/dir2/a.ini",
+ }
+
+ write_files({k: "" for k in files}, tmp_path)
+ patterns = ["**/*.txt", "[ab].*", "**/[ac].ini"]
+ monkeypatch.chdir(tmp_path)
+ assert set(expand.glob_relative(patterns)) == files
+ # Make sure the same APIs work outside cwd
+ assert set(expand.glob_relative(patterns, tmp_path)) == files
+
+
+def test_read_files(tmp_path, monkeypatch):
+ dir_ = tmp_path / "dir_"
+ (tmp_path / "_dir").mkdir(exist_ok=True)
+ (tmp_path / "a.txt").touch()
+ files = {"a.txt": "a", "dir1/b.txt": "b", "dir1/dir2/c.txt": "c"}
+ write_files(files, dir_)
+
+ secrets = Path(str(dir_) + "secrets")
+ secrets.mkdir(exist_ok=True)
+ write_files({"secrets.txt": "secret keys"}, secrets)
+
+ with monkeypatch.context() as m:
+ m.chdir(dir_)
+ assert expand.read_files(list(files)) == "a\nb\nc"
+
+ cannot_access_msg = r"Cannot access '.*\.\..a\.txt'"
+ with pytest.raises(DistutilsOptionError, match=cannot_access_msg):
+ expand.read_files(["../a.txt"])
+
+ cannot_access_secrets_msg = r"Cannot access '.*secrets\.txt'"
+ with pytest.raises(DistutilsOptionError, match=cannot_access_secrets_msg):
+ expand.read_files(["../dir_secrets/secrets.txt"])
+
+ # Make sure the same APIs work outside cwd
+ assert expand.read_files(list(files), dir_) == "a\nb\nc"
+ with pytest.raises(DistutilsOptionError, match=cannot_access_msg):
+ expand.read_files(["../a.txt"], dir_)
+
+
+class TestReadAttr:
+ @pytest.mark.parametrize(
+ "example",
+ [
+ # No cookie means UTF-8:
+ b"__version__ = '\xc3\xa9'\nraise SystemExit(1)\n",
+ # If a cookie is present, honor it:
+ b"# -*- coding: utf-8 -*-\n__version__ = '\xc3\xa9'\nraise SystemExit(1)\n",
+ b"# -*- coding: latin1 -*-\n__version__ = '\xe9'\nraise SystemExit(1)\n",
+ ],
+ )
+ def test_read_attr_encoding_cookie(self, example, tmp_path):
+ (tmp_path / "mod.py").write_bytes(example)
+ assert expand.read_attr('mod.__version__', root_dir=tmp_path) == 'é'
+
+ def test_read_attr(self, tmp_path, monkeypatch):
+ files = {
+ "pkg/__init__.py": "",
+ "pkg/sub/__init__.py": "VERSION = '0.1.1'",
+ "pkg/sub/mod.py": (
+ "VALUES = {'a': 0, 'b': {42}, 'c': (0, 1, 1)}\nraise SystemExit(1)"
+ ),
+ }
+ write_files(files, tmp_path)
+
+ with monkeypatch.context() as m:
+ m.chdir(tmp_path)
+ # Make sure it can read the attr statically without evaluating the module
+ version = expand.read_attr('pkg.sub.VERSION')
+ values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'})
+
+ assert version == '0.1.1'
+ assert is_static(values)
+
+ assert values['a'] == 0
+ assert values['b'] == {42}
+ assert is_static(values)
+
+ # Make sure the same APIs work outside cwd
+ assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1'
+ values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'}, tmp_path)
+ assert values['c'] == (0, 1, 1)
+
+ @pytest.mark.parametrize(
+ "example",
+ [
+ "VERSION: str\nVERSION = '0.1.1'\nraise SystemExit(1)\n",
+ "VERSION: str = '0.1.1'\nraise SystemExit(1)\n",
+ ],
+ )
+ def test_read_annotated_attr(self, tmp_path, example):
+ files = {
+ "pkg/__init__.py": "",
+ "pkg/sub/__init__.py": example,
+ }
+ write_files(files, tmp_path)
+ # Make sure this attribute can be read statically
+ version = expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path)
+ assert version == '0.1.1'
+ assert is_static(version)
+
+ @pytest.mark.parametrize(
+ "example",
+ [
+ "VERSION = (lambda: '0.1.1')()\n",
+ "def fn(): return '0.1.1'\nVERSION = fn()\n",
+ "VERSION: str = (lambda: '0.1.1')()\n",
+ ],
+ )
+ def test_read_dynamic_attr(self, tmp_path, monkeypatch, example):
+ files = {
+ "pkg/__init__.py": "",
+ "pkg/sub/__init__.py": example,
+ }
+ write_files(files, tmp_path)
+ monkeypatch.chdir(tmp_path)
+ version = expand.read_attr('pkg.sub.VERSION')
+ assert version == '0.1.1'
+ assert not is_static(version)
+
+ def test_import_order(self, tmp_path):
+ """
+ Sometimes the import machinery will import the parent package of a nested
+ module, which triggers side-effects and might create problems (see issue #3176)
+
+ ``read_attr`` should bypass these limitations by resolving modules statically
+ (via ast.literal_eval).
+ """
+ files = {
+ "src/pkg/__init__.py": "from .main import func\nfrom .about import version",
+ "src/pkg/main.py": "import super_complicated_dep\ndef func(): return 42",
+ "src/pkg/about.py": "version = '42'",
+ }
+ write_files(files, tmp_path)
+ attr_desc = "pkg.about.version"
+ package_dir = {"": "src"}
+ # `import super_complicated_dep` should not run, otherwise the build fails
+ assert expand.read_attr(attr_desc, package_dir, tmp_path) == "42"
+
+
+@pytest.mark.parametrize(
+ ("package_dir", "file", "module", "return_value"),
+ [
+ ({"": "src"}, "src/pkg/main.py", "pkg.main", 42),
+ ({"pkg": "lib"}, "lib/main.py", "pkg.main", 13),
+ ({}, "single_module.py", "single_module", 70),
+ ({}, "flat_layout/pkg.py", "flat_layout.pkg", 836),
+ ],
+)
+def test_resolve_class(monkeypatch, tmp_path, package_dir, file, module, return_value):
+ monkeypatch.setattr(sys, "modules", {}) # reproducibility
+ files = {file: f"class Custom:\n def testing(self): return {return_value}"}
+ write_files(files, tmp_path)
+ cls = expand.resolve_class(f"{module}.Custom", package_dir, tmp_path)
+ assert cls().testing() == return_value
+
+
+@pytest.mark.parametrize(
+ ("args", "pkgs"),
+ [
+ ({"where": ["."], "namespaces": False}, {"pkg", "other"}),
+ ({"where": [".", "dir1"], "namespaces": False}, {"pkg", "other", "dir2"}),
+ ({"namespaces": True}, {"pkg", "other", "dir1", "dir1.dir2"}),
+ ({}, {"pkg", "other", "dir1", "dir1.dir2"}), # default value for `namespaces`
+ ],
+)
+def test_find_packages(tmp_path, args, pkgs):
+ files = {
+ "pkg/__init__.py",
+ "other/__init__.py",
+ "dir1/dir2/__init__.py",
+ }
+ write_files({k: "" for k in files}, tmp_path)
+
+ package_dir = {}
+ kwargs = {"root_dir": tmp_path, "fill_package_dir": package_dir, **args}
+ where = kwargs.get("where", ["."])
+ assert set(expand.find_packages(**kwargs)) == pkgs
+ for pkg in pkgs:
+ pkg_path = find_package_path(pkg, package_dir, tmp_path)
+ assert os.path.exists(pkg_path)
+
+ # Make sure the same APIs work outside cwd
+ where = [
+ str((tmp_path / p).resolve()).replace(os.sep, "/") # ensure posix-style paths
+ for p in args.pop("where", ["."])
+ ]
+
+ assert set(expand.find_packages(where=where, **args)) == pkgs
+
+
+@pytest.mark.parametrize(
+ ("files", "where", "expected_package_dir"),
+ [
+ (["pkg1/__init__.py", "pkg1/other.py"], ["."], {}),
+ (["pkg1/__init__.py", "pkg2/__init__.py"], ["."], {}),
+ (["src/pkg1/__init__.py", "src/pkg1/other.py"], ["src"], {"": "src"}),
+ (["src/pkg1/__init__.py", "src/pkg2/__init__.py"], ["src"], {"": "src"}),
+ (
+ ["src1/pkg1/__init__.py", "src2/pkg2/__init__.py"],
+ ["src1", "src2"],
+ {"pkg1": "src1/pkg1", "pkg2": "src2/pkg2"},
+ ),
+ (
+ ["src/pkg1/__init__.py", "pkg2/__init__.py"],
+ ["src", "."],
+ {"pkg1": "src/pkg1"},
+ ),
+ ],
+)
+def test_fill_package_dir(tmp_path, files, where, expected_package_dir):
+ write_files({k: "" for k in files}, tmp_path)
+ pkg_dir = {}
+ kwargs = {"root_dir": tmp_path, "fill_package_dir": pkg_dir, "namespaces": False}
+ pkgs = expand.find_packages(where=where, **kwargs)
+ assert set(pkg_dir.items()) == set(expected_package_dir.items())
+ for pkg in pkgs:
+ pkg_path = find_package_path(pkg, pkg_dir, tmp_path)
+ assert os.path.exists(pkg_path)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_pyprojecttoml.py b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_pyprojecttoml.py
new file mode 100644
index 00000000..db40fcd2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_pyprojecttoml.py
@@ -0,0 +1,396 @@
+import re
+from configparser import ConfigParser
+from inspect import cleandoc
+
+import jaraco.path
+import pytest
+import tomli_w
+from path import Path
+
+import setuptools # noqa: F401 # force distutils.core to be patched
+from setuptools.config.pyprojecttoml import (
+ _ToolsTypoInMetadata,
+ apply_configuration,
+ expand_configuration,
+ read_configuration,
+ validate,
+)
+from setuptools.dist import Distribution
+from setuptools.errors import OptionError
+
+import distutils.core
+
+EXAMPLE = """
+[project]
+name = "myproj"
+keywords = ["some", "key", "words"]
+dynamic = ["version", "readme"]
+requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+dependencies = [
+ 'importlib-metadata>=0.12;python_version<"3.8"',
+ 'importlib-resources>=1.0;python_version<"3.7"',
+ 'pathlib2>=2.3.3,<3;python_version < "3.4" and sys.platform != "win32"',
+]
+
+[project.optional-dependencies]
+docs = [
+ "sphinx>=3",
+ "sphinx-argparse>=0.2.5",
+ "sphinx-rtd-theme>=0.4.3",
+]
+testing = [
+ "pytest>=1",
+ "coverage>=3,<5",
+]
+
+[project.scripts]
+exec = "pkg.__main__:exec"
+
+[build-system]
+requires = ["setuptools", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[tool.setuptools]
+package-dir = {"" = "src"}
+zip-safe = true
+platforms = ["any"]
+
+[tool.setuptools.packages.find]
+where = ["src"]
+
+[tool.setuptools.cmdclass]
+sdist = "pkg.mod.CustomSdist"
+
+[tool.setuptools.dynamic.version]
+attr = "pkg.__version__.VERSION"
+
+[tool.setuptools.dynamic.readme]
+file = ["README.md"]
+content-type = "text/markdown"
+
+[tool.setuptools.package-data]
+"*" = ["*.txt"]
+
+[tool.setuptools.data-files]
+"data" = ["_files/*.txt"]
+
+[tool.distutils.sdist]
+formats = "gztar"
+
+[tool.distutils.bdist_wheel]
+universal = true
+"""
+
+
+def create_example(path, pkg_root):
+ files = {
+ "pyproject.toml": EXAMPLE,
+ "README.md": "hello world",
+ "_files": {
+ "file.txt": "",
+ },
+ }
+ packages = {
+ "pkg": {
+ "__init__.py": "",
+ "mod.py": "class CustomSdist: pass",
+ "__version__.py": "VERSION = (3, 10)",
+ "__main__.py": "def exec(): print('hello')",
+ },
+ }
+
+ assert pkg_root # Meta-test: cannot be empty string.
+
+ if pkg_root == ".":
+ files = {**files, **packages}
+ # skip other files: flat-layout will raise error for multi-package dist
+ else:
+ # Use this opportunity to ensure namespaces are discovered
+ files[pkg_root] = {**packages, "other": {"nested": {"__init__.py": ""}}}
+
+ jaraco.path.build(files, prefix=path)
+
+
+def verify_example(config, path, pkg_root):
+ pyproject = path / "pyproject.toml"
+ pyproject.write_text(tomli_w.dumps(config), encoding="utf-8")
+ expanded = expand_configuration(config, path)
+ expanded_project = expanded["project"]
+ assert read_configuration(pyproject, expand=True) == expanded
+ assert expanded_project["version"] == "3.10"
+ assert expanded_project["readme"]["text"] == "hello world"
+ assert "packages" in expanded["tool"]["setuptools"]
+ if pkg_root == ".":
+ # Auto-discovery will raise error for multi-package dist
+ assert set(expanded["tool"]["setuptools"]["packages"]) == {"pkg"}
+ else:
+ assert set(expanded["tool"]["setuptools"]["packages"]) == {
+ "pkg",
+ "other",
+ "other.nested",
+ }
+ assert expanded["tool"]["setuptools"]["include-package-data"] is True
+ assert "" in expanded["tool"]["setuptools"]["package-data"]
+ assert "*" not in expanded["tool"]["setuptools"]["package-data"]
+ assert expanded["tool"]["setuptools"]["data-files"] == [
+ ("data", ["_files/file.txt"])
+ ]
+
+
+def test_read_configuration(tmp_path):
+ create_example(tmp_path, "src")
+ pyproject = tmp_path / "pyproject.toml"
+
+ config = read_configuration(pyproject, expand=False)
+ assert config["project"].get("version") is None
+ assert config["project"].get("readme") is None
+
+ verify_example(config, tmp_path, "src")
+
+
+@pytest.mark.parametrize(
+ ("pkg_root", "opts"),
+ [
+ (".", {}),
+ ("src", {}),
+ ("lib", {"packages": {"find": {"where": ["lib"]}}}),
+ ],
+)
+def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, pkg_root, opts):
+ create_example(tmp_path, pkg_root)
+
+ pyproject = tmp_path / "pyproject.toml"
+
+ config = read_configuration(pyproject, expand=False)
+ assert config["project"].get("version") is None
+ assert config["project"].get("readme") is None
+ config["tool"]["setuptools"].pop("packages", None)
+ config["tool"]["setuptools"].pop("package-dir", None)
+
+ config["tool"]["setuptools"].update(opts)
+ verify_example(config, tmp_path, pkg_root)
+
+
+ENTRY_POINTS = {
+ "console_scripts": {"a": "mod.a:func"},
+ "gui_scripts": {"b": "mod.b:func"},
+ "other": {"c": "mod.c:func [extra]"},
+}
+
+
+class TestEntryPoints:
+ def write_entry_points(self, tmp_path):
+ entry_points = ConfigParser()
+ entry_points.read_dict(ENTRY_POINTS)
+ with open(tmp_path / "entry-points.txt", "w", encoding="utf-8") as f:
+ entry_points.write(f)
+
+ def pyproject(self, dynamic=None):
+ project = {"dynamic": dynamic or ["scripts", "gui-scripts", "entry-points"]}
+ tool = {"dynamic": {"entry-points": {"file": "entry-points.txt"}}}
+ return {"project": project, "tool": {"setuptools": tool}}
+
+ def test_all_listed_in_dynamic(self, tmp_path):
+ self.write_entry_points(tmp_path)
+ expanded = expand_configuration(self.pyproject(), tmp_path)
+ expanded_project = expanded["project"]
+ assert len(expanded_project["scripts"]) == 1
+ assert expanded_project["scripts"]["a"] == "mod.a:func"
+ assert len(expanded_project["gui-scripts"]) == 1
+ assert expanded_project["gui-scripts"]["b"] == "mod.b:func"
+ assert len(expanded_project["entry-points"]) == 1
+ assert expanded_project["entry-points"]["other"]["c"] == "mod.c:func [extra]"
+
+ @pytest.mark.parametrize("missing_dynamic", ("scripts", "gui-scripts"))
+ def test_scripts_not_listed_in_dynamic(self, tmp_path, missing_dynamic):
+ self.write_entry_points(tmp_path)
+ dynamic = {"scripts", "gui-scripts", "entry-points"} - {missing_dynamic}
+
+ msg = f"defined outside of `pyproject.toml`:.*{missing_dynamic}"
+ with pytest.raises(OptionError, match=re.compile(msg, re.S)):
+ expand_configuration(self.pyproject(dynamic), tmp_path)
+
+
+class TestClassifiers:
+ def test_dynamic(self, tmp_path):
+ # Let's create a project example that has dynamic classifiers
+ # coming from a txt file.
+ create_example(tmp_path, "src")
+ classifiers = cleandoc(
+ """
+ Framework :: Flask
+ Programming Language :: Haskell
+ """
+ )
+ (tmp_path / "classifiers.txt").write_text(classifiers, encoding="utf-8")
+
+ pyproject = tmp_path / "pyproject.toml"
+ config = read_configuration(pyproject, expand=False)
+ dynamic = config["project"]["dynamic"]
+ config["project"]["dynamic"] = list({*dynamic, "classifiers"})
+ dynamic_config = config["tool"]["setuptools"]["dynamic"]
+ dynamic_config["classifiers"] = {"file": "classifiers.txt"}
+
+ # When the configuration is expanded,
+ # each line of the file should be an different classifier.
+ validate(config, pyproject)
+ expanded = expand_configuration(config, tmp_path)
+
+ assert set(expanded["project"]["classifiers"]) == {
+ "Framework :: Flask",
+ "Programming Language :: Haskell",
+ }
+
+ def test_dynamic_without_config(self, tmp_path):
+ config = """
+ [project]
+ name = "myproj"
+ version = '42'
+ dynamic = ["classifiers"]
+ """
+
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(cleandoc(config), encoding="utf-8")
+ with pytest.raises(OptionError, match="No configuration .* .classifiers."):
+ read_configuration(pyproject)
+
+ def test_dynamic_readme_from_setup_script_args(self, tmp_path):
+ config = """
+ [project]
+ name = "myproj"
+ version = '42'
+ dynamic = ["readme"]
+ """
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(cleandoc(config), encoding="utf-8")
+ dist = Distribution(attrs={"long_description": "42"})
+ # No error should occur because of missing `readme`
+ dist = apply_configuration(dist, pyproject)
+ assert dist.metadata.long_description == "42"
+
+ def test_dynamic_without_file(self, tmp_path):
+ config = """
+ [project]
+ name = "myproj"
+ version = '42'
+ dynamic = ["classifiers"]
+
+ [tool.setuptools.dynamic]
+ classifiers = {file = ["classifiers.txt"]}
+ """
+
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(cleandoc(config), encoding="utf-8")
+ with pytest.warns(UserWarning, match="File .*classifiers.txt. cannot be found"):
+ expanded = read_configuration(pyproject)
+ assert "classifiers" not in expanded["project"]
+
+
+@pytest.mark.parametrize(
+ "example",
+ (
+ """
+ [project]
+ name = "myproj"
+ version = "1.2"
+
+ [my-tool.that-disrespect.pep518]
+ value = 42
+ """,
+ ),
+)
+def test_ignore_unrelated_config(tmp_path, example):
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(cleandoc(example), encoding="utf-8")
+
+ # Make sure no error is raised due to 3rd party configs in pyproject.toml
+ assert read_configuration(pyproject) is not None
+
+
+@pytest.mark.parametrize(
+ ("example", "error_msg"),
+ [
+ (
+ """
+ [project]
+ name = "myproj"
+ version = "1.2"
+ requires = ['pywin32; platform_system=="Windows"' ]
+ """,
+ "configuration error: .project. must not contain ..requires.. properties",
+ ),
+ ],
+)
+def test_invalid_example(tmp_path, example, error_msg):
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(cleandoc(example), encoding="utf-8")
+
+ pattern = re.compile(f"invalid pyproject.toml.*{error_msg}.*", re.M | re.S)
+ with pytest.raises(ValueError, match=pattern):
+ read_configuration(pyproject)
+
+
+@pytest.mark.parametrize("config", ("", "[tool.something]\nvalue = 42"))
+def test_empty(tmp_path, config):
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(config, encoding="utf-8")
+
+ # Make sure no error is raised
+ assert read_configuration(pyproject) == {}
+
+
+@pytest.mark.parametrize("config", ("[project]\nname = 'myproj'\nversion='42'\n",))
+def test_include_package_data_by_default(tmp_path, config):
+ """Builds with ``pyproject.toml`` should consider ``include-package-data=True`` as
+ default.
+ """
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(config, encoding="utf-8")
+
+ config = read_configuration(pyproject)
+ assert config["tool"]["setuptools"]["include-package-data"] is True
+
+
+def test_include_package_data_in_setuppy(tmp_path):
+ """Builds with ``pyproject.toml`` should consider ``include_package_data`` set in
+ ``setup.py``.
+
+ See https://github.com/pypa/setuptools/issues/3197#issuecomment-1079023889
+ """
+ files = {
+ "pyproject.toml": "[project]\nname = 'myproj'\nversion='42'\n",
+ "setup.py": "__import__('setuptools').setup(include_package_data=False)",
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+
+ with Path(tmp_path):
+ dist = distutils.core.run_setup("setup.py", {}, stop_after="config")
+
+ assert dist.get_name() == "myproj"
+ assert dist.get_version() == "42"
+ assert dist.include_package_data is False
+
+
+def test_warn_tools_typo(tmp_path):
+ """Test that the common ``tools.setuptools`` typo in ``pyproject.toml`` issues a warning
+
+ See https://github.com/pypa/setuptools/issues/4150
+ """
+ config = """
+ [build-system]
+ requires = ["setuptools"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "myproj"
+ version = '42'
+
+ [tools.setuptools]
+ packages = ["package"]
+ """
+
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(cleandoc(config), encoding="utf-8")
+
+ with pytest.warns(_ToolsTypoInMetadata):
+ read_configuration(pyproject)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py
new file mode 100644
index 00000000..e42f28ff
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_pyprojecttoml_dynamic_deps.py
@@ -0,0 +1,109 @@
+from inspect import cleandoc
+
+import pytest
+from jaraco import path
+
+from setuptools.config.pyprojecttoml import apply_configuration
+from setuptools.dist import Distribution
+from setuptools.warnings import SetuptoolsWarning
+
+
+def test_dynamic_dependencies(tmp_path):
+ files = {
+ "requirements.txt": "six\n # comment\n",
+ "pyproject.toml": cleandoc(
+ """
+ [project]
+ name = "myproj"
+ version = "1.0"
+ dynamic = ["dependencies"]
+
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+
+ [tool.setuptools.dynamic.dependencies]
+ file = ["requirements.txt"]
+ """
+ ),
+ }
+ path.build(files, prefix=tmp_path)
+ dist = Distribution()
+ dist = apply_configuration(dist, tmp_path / "pyproject.toml")
+ assert dist.install_requires == ["six"]
+
+
+def test_dynamic_optional_dependencies(tmp_path):
+ files = {
+ "requirements-docs.txt": "sphinx\n # comment\n",
+ "pyproject.toml": cleandoc(
+ """
+ [project]
+ name = "myproj"
+ version = "1.0"
+ dynamic = ["optional-dependencies"]
+
+ [tool.setuptools.dynamic.optional-dependencies.docs]
+ file = ["requirements-docs.txt"]
+
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+ """
+ ),
+ }
+ path.build(files, prefix=tmp_path)
+ dist = Distribution()
+ dist = apply_configuration(dist, tmp_path / "pyproject.toml")
+ assert dist.extras_require == {"docs": ["sphinx"]}
+
+
+def test_mixed_dynamic_optional_dependencies(tmp_path):
+ """
+ Test that if PEP 621 was loosened to allow mixing of dynamic and static
+ configurations in the case of fields containing sub-fields (groups),
+ things would work out.
+ """
+ files = {
+ "requirements-images.txt": "pillow~=42.0\n # comment\n",
+ "pyproject.toml": cleandoc(
+ """
+ [project]
+ name = "myproj"
+ version = "1.0"
+ dynamic = ["optional-dependencies"]
+
+ [project.optional-dependencies]
+ docs = ["sphinx"]
+
+ [tool.setuptools.dynamic.optional-dependencies.images]
+ file = ["requirements-images.txt"]
+ """
+ ),
+ }
+
+ path.build(files, prefix=tmp_path)
+ pyproject = tmp_path / "pyproject.toml"
+ with pytest.raises(ValueError, match="project.optional-dependencies"):
+ apply_configuration(Distribution(), pyproject)
+
+
+def test_mixed_extras_require_optional_dependencies(tmp_path):
+ files = {
+ "pyproject.toml": cleandoc(
+ """
+ [project]
+ name = "myproj"
+ version = "1.0"
+ optional-dependencies.docs = ["sphinx"]
+ """
+ ),
+ }
+
+ path.build(files, prefix=tmp_path)
+ pyproject = tmp_path / "pyproject.toml"
+
+ with pytest.warns(SetuptoolsWarning, match=".extras_require. overwritten"):
+ dist = Distribution({"extras_require": {"hello": ["world"]}})
+ dist = apply_configuration(dist, pyproject)
+ assert dist.extras_require == {"docs": ["sphinx"]}
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_setupcfg.py b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_setupcfg.py
new file mode 100644
index 00000000..a199871f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/config/test_setupcfg.py
@@ -0,0 +1,969 @@
+import configparser
+import contextlib
+import inspect
+import re
+from pathlib import Path
+from unittest.mock import Mock, patch
+
+import pytest
+from packaging.requirements import InvalidRequirement
+
+from setuptools.config.setupcfg import ConfigHandler, Target, read_configuration
+from setuptools.dist import Distribution, _Distribution
+from setuptools.errors import InvalidConfigError
+from setuptools.warnings import SetuptoolsDeprecationWarning
+
+from ..textwrap import DALS
+
+from distutils.errors import DistutilsFileError, DistutilsOptionError
+
+
+class ErrConfigHandler(ConfigHandler[Target]):
+ """Erroneous handler. Fails to implement required methods."""
+
+ section_prefix = "**err**"
+
+
+def make_package_dir(name, base_dir, ns=False):
+ dir_package = base_dir
+ for dir_name in name.split('/'):
+ dir_package = dir_package.mkdir(dir_name)
+ init_file = None
+ if not ns:
+ init_file = dir_package.join('__init__.py')
+ init_file.write('')
+ return dir_package, init_file
+
+
+def fake_env(
+ tmpdir, setup_cfg, setup_py=None, encoding='ascii', package_path='fake_package'
+):
+ if setup_py is None:
+ setup_py = 'from setuptools import setup\nsetup()\n'
+
+ tmpdir.join('setup.py').write(setup_py)
+ config = tmpdir.join('setup.cfg')
+ config.write(setup_cfg.encode(encoding), mode='wb')
+
+ package_dir, init_file = make_package_dir(package_path, tmpdir)
+
+ init_file.write(
+ 'VERSION = (1, 2, 3)\n'
+ '\n'
+ 'VERSION_MAJOR = 1'
+ '\n'
+ 'def get_version():\n'
+ ' return [3, 4, 5, "dev"]\n'
+ '\n'
+ )
+
+ return package_dir, config
+
+
+@contextlib.contextmanager
+def get_dist(tmpdir, kwargs_initial=None, parse=True):
+ kwargs_initial = kwargs_initial or {}
+
+ with tmpdir.as_cwd():
+ dist = Distribution(kwargs_initial)
+ dist.script_name = 'setup.py'
+ parse and dist.parse_config_files()
+
+ yield dist
+
+
+def test_parsers_implemented():
+ with pytest.raises(NotImplementedError):
+ handler = ErrConfigHandler(None, {}, False, Mock())
+ handler.parsers
+
+
+class TestConfigurationReader:
+ def test_basic(self, tmpdir):
+ _, config = fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'version = 10.1.1\n'
+ 'keywords = one, two\n'
+ '\n'
+ '[options]\n'
+ 'scripts = bin/a.py, bin/b.py\n',
+ )
+ config_dict = read_configuration(str(config))
+ assert config_dict['metadata']['version'] == '10.1.1'
+ assert config_dict['metadata']['keywords'] == ['one', 'two']
+ assert config_dict['options']['scripts'] == ['bin/a.py', 'bin/b.py']
+
+ def test_no_config(self, tmpdir):
+ with pytest.raises(DistutilsFileError):
+ read_configuration(str(tmpdir.join('setup.cfg')))
+
+ def test_ignore_errors(self, tmpdir):
+ _, config = fake_env(
+ tmpdir,
+ '[metadata]\nversion = attr: none.VERSION\nkeywords = one, two\n',
+ )
+ with pytest.raises(ImportError):
+ read_configuration(str(config))
+
+ config_dict = read_configuration(str(config), ignore_option_errors=True)
+
+ assert config_dict['metadata']['keywords'] == ['one', 'two']
+ assert 'version' not in config_dict['metadata']
+
+ config.remove()
+
+
+class TestMetadata:
+ def test_basic(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'version = 10.1.1\n'
+ 'description = Some description\n'
+ 'long_description_content_type = text/something\n'
+ 'long_description = file: README\n'
+ 'name = fake_name\n'
+ 'keywords = one, two\n'
+ 'provides = package, package.sub\n'
+ 'license = otherlic\n'
+ 'download_url = http://test.test.com/test/\n'
+ 'maintainer_email = test@test.com\n',
+ )
+
+ tmpdir.join('README').write('readme contents\nline2')
+
+ meta_initial = {
+ # This will be used so `otherlic` won't replace it.
+ 'license': 'BSD 3-Clause License',
+ }
+
+ with get_dist(tmpdir, meta_initial) as dist:
+ metadata = dist.metadata
+
+ assert metadata.version == '10.1.1'
+ assert metadata.description == 'Some description'
+ assert metadata.long_description_content_type == 'text/something'
+ assert metadata.long_description == 'readme contents\nline2'
+ assert metadata.provides == ['package', 'package.sub']
+ assert metadata.license == 'BSD 3-Clause License'
+ assert metadata.name == 'fake_name'
+ assert metadata.keywords == ['one', 'two']
+ assert metadata.download_url == 'http://test.test.com/test/'
+ assert metadata.maintainer_email == 'test@test.com'
+
+ def test_license_cfg(self, tmpdir):
+ fake_env(
+ tmpdir,
+ DALS(
+ """
+ [metadata]
+ name=foo
+ version=0.0.1
+ license=Apache 2.0
+ """
+ ),
+ )
+
+ with get_dist(tmpdir) as dist:
+ metadata = dist.metadata
+
+ assert metadata.name == "foo"
+ assert metadata.version == "0.0.1"
+ assert metadata.license == "Apache 2.0"
+
+ def test_file_mixed(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[metadata]\nlong_description = file: README.rst, CHANGES.rst\n\n',
+ )
+
+ tmpdir.join('README.rst').write('readme contents\nline2')
+ tmpdir.join('CHANGES.rst').write('changelog contents\nand stuff')
+
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.long_description == (
+ 'readme contents\nline2\nchangelog contents\nand stuff'
+ )
+
+ def test_file_sandboxed(self, tmpdir):
+ tmpdir.ensure("README")
+ project = tmpdir.join('depth1', 'depth2')
+ project.ensure(dir=True)
+ fake_env(project, '[metadata]\nlong_description = file: ../../README\n')
+
+ with get_dist(project, parse=False) as dist:
+ with pytest.raises(DistutilsOptionError):
+ dist.parse_config_files() # file: out of sandbox
+
+ def test_aliases(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'author_email = test@test.com\n'
+ 'home_page = http://test.test.com/test/\n'
+ 'summary = Short summary\n'
+ 'platform = a, b\n'
+ 'classifier =\n'
+ ' Framework :: Django\n'
+ ' Programming Language :: Python :: 3.5\n',
+ )
+
+ with get_dist(tmpdir) as dist:
+ metadata = dist.metadata
+ assert metadata.author_email == 'test@test.com'
+ assert metadata.url == 'http://test.test.com/test/'
+ assert metadata.description == 'Short summary'
+ assert metadata.platforms == ['a', 'b']
+ assert metadata.classifiers == [
+ 'Framework :: Django',
+ 'Programming Language :: Python :: 3.5',
+ ]
+
+ def test_multiline(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'name = fake_name\n'
+ 'keywords =\n'
+ ' one\n'
+ ' two\n'
+ 'classifiers =\n'
+ ' Framework :: Django\n'
+ ' Programming Language :: Python :: 3.5\n',
+ )
+ with get_dist(tmpdir) as dist:
+ metadata = dist.metadata
+ assert metadata.keywords == ['one', 'two']
+ assert metadata.classifiers == [
+ 'Framework :: Django',
+ 'Programming Language :: Python :: 3.5',
+ ]
+
+ def test_dict(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'project_urls =\n'
+ ' Link One = https://example.com/one/\n'
+ ' Link Two = https://example.com/two/\n',
+ )
+ with get_dist(tmpdir) as dist:
+ metadata = dist.metadata
+ assert metadata.project_urls == {
+ 'Link One': 'https://example.com/one/',
+ 'Link Two': 'https://example.com/two/',
+ }
+
+ def test_version(self, tmpdir):
+ package_dir, config = fake_env(
+ tmpdir, '[metadata]\nversion = attr: fake_package.VERSION\n'
+ )
+
+ sub_a = package_dir.mkdir('subpkg_a')
+ sub_a.join('__init__.py').write('')
+ sub_a.join('mod.py').write('VERSION = (2016, 11, 26)')
+
+ sub_b = package_dir.mkdir('subpkg_b')
+ sub_b.join('__init__.py').write('')
+ sub_b.join('mod.py').write(
+ 'import third_party_module\nVERSION = (2016, 11, 26)'
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.version == '1.2.3'
+
+ config.write('[metadata]\nversion = attr: fake_package.get_version\n')
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.version == '3.4.5.dev'
+
+ config.write('[metadata]\nversion = attr: fake_package.VERSION_MAJOR\n')
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.version == '1'
+
+ config.write('[metadata]\nversion = attr: fake_package.subpkg_a.mod.VERSION\n')
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.version == '2016.11.26'
+
+ config.write('[metadata]\nversion = attr: fake_package.subpkg_b.mod.VERSION\n')
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.version == '2016.11.26'
+
+ def test_version_file(self, tmpdir):
+ fake_env(tmpdir, '[metadata]\nversion = file: fake_package/version.txt\n')
+ tmpdir.join('fake_package', 'version.txt').write('1.2.3\n')
+
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.version == '1.2.3'
+
+ tmpdir.join('fake_package', 'version.txt').write('1.2.3\n4.5.6\n')
+ with pytest.raises(DistutilsOptionError):
+ with get_dist(tmpdir) as dist:
+ dist.metadata.version
+
+ def test_version_with_package_dir_simple(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'version = attr: fake_package_simple.VERSION\n'
+ '[options]\n'
+ 'package_dir =\n'
+ ' = src\n',
+ package_path='src/fake_package_simple',
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.version == '1.2.3'
+
+ def test_version_with_package_dir_rename(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'version = attr: fake_package_rename.VERSION\n'
+ '[options]\n'
+ 'package_dir =\n'
+ ' fake_package_rename = fake_dir\n',
+ package_path='fake_dir',
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.version == '1.2.3'
+
+ def test_version_with_package_dir_complex(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[metadata]\n'
+ 'version = attr: fake_package_complex.VERSION\n'
+ '[options]\n'
+ 'package_dir =\n'
+ ' fake_package_complex = src/fake_dir\n',
+ package_path='src/fake_dir',
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.version == '1.2.3'
+
+ def test_unknown_meta_item(self, tmpdir):
+ fake_env(tmpdir, '[metadata]\nname = fake_name\nunknown = some\n')
+ with get_dist(tmpdir, parse=False) as dist:
+ dist.parse_config_files() # Skip unknown.
+
+ def test_usupported_section(self, tmpdir):
+ fake_env(tmpdir, '[metadata.some]\nkey = val\n')
+ with get_dist(tmpdir, parse=False) as dist:
+ with pytest.raises(DistutilsOptionError):
+ dist.parse_config_files()
+
+ def test_classifiers(self, tmpdir):
+ expected = set([
+ 'Framework :: Django',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.5',
+ ])
+
+ # From file.
+ _, config = fake_env(tmpdir, '[metadata]\nclassifiers = file: classifiers\n')
+
+ tmpdir.join('classifiers').write(
+ 'Framework :: Django\n'
+ 'Programming Language :: Python :: 3\n'
+ 'Programming Language :: Python :: 3.5\n'
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert set(dist.metadata.classifiers) == expected
+
+ # From list notation
+ config.write(
+ '[metadata]\n'
+ 'classifiers =\n'
+ ' Framework :: Django\n'
+ ' Programming Language :: Python :: 3\n'
+ ' Programming Language :: Python :: 3.5\n'
+ )
+ with get_dist(tmpdir) as dist:
+ assert set(dist.metadata.classifiers) == expected
+
+ def test_interpolation(self, tmpdir):
+ fake_env(tmpdir, '[metadata]\ndescription = %(message)s\n')
+ with pytest.raises(configparser.InterpolationMissingOptionError):
+ with get_dist(tmpdir):
+ pass
+
+ def test_non_ascii_1(self, tmpdir):
+ fake_env(tmpdir, '[metadata]\ndescription = éàïôñ\n', encoding='utf-8')
+ with get_dist(tmpdir):
+ pass
+
+ def test_non_ascii_3(self, tmpdir):
+ fake_env(tmpdir, '\n# -*- coding: invalid\n')
+ with get_dist(tmpdir):
+ pass
+
+ def test_non_ascii_4(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '# -*- coding: utf-8\n[metadata]\ndescription = éàïôñ\n',
+ encoding='utf-8',
+ )
+ with get_dist(tmpdir) as dist:
+ assert dist.metadata.description == 'éàïôñ'
+
+ def test_not_utf8(self, tmpdir):
+ """
+ Config files encoded not in UTF-8 will fail
+ """
+ fake_env(
+ tmpdir,
+ '# vim: set fileencoding=iso-8859-15 :\n[metadata]\ndescription = éàïôñ\n',
+ encoding='iso-8859-15',
+ )
+ with pytest.raises(UnicodeDecodeError):
+ with get_dist(tmpdir):
+ pass
+
+ @pytest.mark.parametrize(
+ ("error_msg", "config"),
+ [
+ (
+ "Invalid dash-separated key 'author-email' in 'metadata' (setup.cfg)",
+ DALS(
+ """
+ [metadata]
+ author-email = test@test.com
+ maintainer_email = foo@foo.com
+ """
+ ),
+ ),
+ (
+ "Invalid uppercase key 'Name' in 'metadata' (setup.cfg)",
+ DALS(
+ """
+ [metadata]
+ Name = foo
+ description = Some description
+ """
+ ),
+ ),
+ ],
+ )
+ def test_invalid_options_previously_deprecated(self, tmpdir, error_msg, config):
+ # this test and related methods can be removed when no longer needed
+ fake_env(tmpdir, config)
+ with pytest.raises(InvalidConfigError, match=re.escape(error_msg)):
+ get_dist(tmpdir).__enter__()
+
+
+class TestOptions:
+ def test_basic(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[options]\n'
+ 'zip_safe = True\n'
+ 'include_package_data = yes\n'
+ 'package_dir = b=c, =src\n'
+ 'packages = pack_a, pack_b.subpack\n'
+ 'namespace_packages = pack1, pack2\n'
+ 'scripts = bin/one.py, bin/two.py\n'
+ 'eager_resources = bin/one.py, bin/two.py\n'
+ 'install_requires = docutils>=0.3; pack ==1.1, ==1.3; hey\n'
+ 'setup_requires = docutils>=0.3; spack ==1.1, ==1.3; there\n'
+ 'dependency_links = http://some.com/here/1, '
+ 'http://some.com/there/2\n'
+ 'python_requires = >=1.0, !=2.8\n'
+ 'py_modules = module1, module2\n',
+ )
+ deprec = pytest.warns(SetuptoolsDeprecationWarning, match="namespace_packages")
+ with deprec, get_dist(tmpdir) as dist:
+ assert dist.zip_safe
+ assert dist.include_package_data
+ assert dist.package_dir == {'': 'src', 'b': 'c'}
+ assert dist.packages == ['pack_a', 'pack_b.subpack']
+ assert dist.namespace_packages == ['pack1', 'pack2']
+ assert dist.scripts == ['bin/one.py', 'bin/two.py']
+ assert dist.dependency_links == ([
+ 'http://some.com/here/1',
+ 'http://some.com/there/2',
+ ])
+ assert dist.install_requires == ([
+ 'docutils>=0.3',
+ 'pack==1.1,==1.3',
+ 'hey',
+ ])
+ assert dist.setup_requires == ([
+ 'docutils>=0.3',
+ 'spack ==1.1, ==1.3',
+ 'there',
+ ])
+ assert dist.python_requires == '>=1.0, !=2.8'
+ assert dist.py_modules == ['module1', 'module2']
+
+ def test_multiline(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[options]\n'
+ 'package_dir = \n'
+ ' b=c\n'
+ ' =src\n'
+ 'packages = \n'
+ ' pack_a\n'
+ ' pack_b.subpack\n'
+ 'namespace_packages = \n'
+ ' pack1\n'
+ ' pack2\n'
+ 'scripts = \n'
+ ' bin/one.py\n'
+ ' bin/two.py\n'
+ 'eager_resources = \n'
+ ' bin/one.py\n'
+ ' bin/two.py\n'
+ 'install_requires = \n'
+ ' docutils>=0.3\n'
+ ' pack ==1.1, ==1.3\n'
+ ' hey\n'
+ 'setup_requires = \n'
+ ' docutils>=0.3\n'
+ ' spack ==1.1, ==1.3\n'
+ ' there\n'
+ 'dependency_links = \n'
+ ' http://some.com/here/1\n'
+ ' http://some.com/there/2\n',
+ )
+ deprec = pytest.warns(SetuptoolsDeprecationWarning, match="namespace_packages")
+ with deprec, get_dist(tmpdir) as dist:
+ assert dist.package_dir == {'': 'src', 'b': 'c'}
+ assert dist.packages == ['pack_a', 'pack_b.subpack']
+ assert dist.namespace_packages == ['pack1', 'pack2']
+ assert dist.scripts == ['bin/one.py', 'bin/two.py']
+ assert dist.dependency_links == ([
+ 'http://some.com/here/1',
+ 'http://some.com/there/2',
+ ])
+ assert dist.install_requires == ([
+ 'docutils>=0.3',
+ 'pack==1.1,==1.3',
+ 'hey',
+ ])
+ assert dist.setup_requires == ([
+ 'docutils>=0.3',
+ 'spack ==1.1, ==1.3',
+ 'there',
+ ])
+
+ def test_package_dir_fail(self, tmpdir):
+ fake_env(tmpdir, '[options]\npackage_dir = a b\n')
+ with get_dist(tmpdir, parse=False) as dist:
+ with pytest.raises(DistutilsOptionError):
+ dist.parse_config_files()
+
+ def test_package_data(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[options.package_data]\n'
+ '* = *.txt, *.rst\n'
+ 'hello = *.msg\n'
+ '\n'
+ '[options.exclude_package_data]\n'
+ '* = fake1.txt, fake2.txt\n'
+ 'hello = *.dat\n',
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert dist.package_data == {
+ '': ['*.txt', '*.rst'],
+ 'hello': ['*.msg'],
+ }
+ assert dist.exclude_package_data == {
+ '': ['fake1.txt', 'fake2.txt'],
+ 'hello': ['*.dat'],
+ }
+
+ def test_packages(self, tmpdir):
+ fake_env(tmpdir, '[options]\npackages = find:\n')
+
+ with get_dist(tmpdir) as dist:
+ assert dist.packages == ['fake_package']
+
+ def test_find_directive(self, tmpdir):
+ dir_package, config = fake_env(tmpdir, '[options]\npackages = find:\n')
+
+ make_package_dir('sub_one', dir_package)
+ make_package_dir('sub_two', dir_package)
+
+ with get_dist(tmpdir) as dist:
+ assert set(dist.packages) == set([
+ 'fake_package',
+ 'fake_package.sub_two',
+ 'fake_package.sub_one',
+ ])
+
+ config.write(
+ '[options]\n'
+ 'packages = find:\n'
+ '\n'
+ '[options.packages.find]\n'
+ 'where = .\n'
+ 'include =\n'
+ ' fake_package.sub_one\n'
+ ' two\n'
+ )
+ with get_dist(tmpdir) as dist:
+ assert dist.packages == ['fake_package.sub_one']
+
+ config.write(
+ '[options]\n'
+ 'packages = find:\n'
+ '\n'
+ '[options.packages.find]\n'
+ 'exclude =\n'
+ ' fake_package.sub_one\n'
+ )
+ with get_dist(tmpdir) as dist:
+ assert set(dist.packages) == set(['fake_package', 'fake_package.sub_two'])
+
+ def test_find_namespace_directive(self, tmpdir):
+ dir_package, config = fake_env(
+ tmpdir, '[options]\npackages = find_namespace:\n'
+ )
+
+ make_package_dir('sub_one', dir_package)
+ make_package_dir('sub_two', dir_package, ns=True)
+
+ with get_dist(tmpdir) as dist:
+ assert set(dist.packages) == {
+ 'fake_package',
+ 'fake_package.sub_two',
+ 'fake_package.sub_one',
+ }
+
+ config.write(
+ '[options]\n'
+ 'packages = find_namespace:\n'
+ '\n'
+ '[options.packages.find]\n'
+ 'where = .\n'
+ 'include =\n'
+ ' fake_package.sub_one\n'
+ ' two\n'
+ )
+ with get_dist(tmpdir) as dist:
+ assert dist.packages == ['fake_package.sub_one']
+
+ config.write(
+ '[options]\n'
+ 'packages = find_namespace:\n'
+ '\n'
+ '[options.packages.find]\n'
+ 'exclude =\n'
+ ' fake_package.sub_one\n'
+ )
+ with get_dist(tmpdir) as dist:
+ assert set(dist.packages) == {'fake_package', 'fake_package.sub_two'}
+
+ def test_extras_require(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[options.extras_require]\n'
+ 'pdf = ReportLab>=1.2; RXP\n'
+ 'rest = \n'
+ ' docutils>=0.3\n'
+ ' pack ==1.1, ==1.3\n',
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert dist.extras_require == {
+ 'pdf': ['ReportLab>=1.2', 'RXP'],
+ 'rest': ['docutils>=0.3', 'pack==1.1,==1.3'],
+ }
+ assert set(dist.metadata.provides_extras) == {'pdf', 'rest'}
+
+ @pytest.mark.parametrize(
+ "config",
+ [
+ "[options.extras_require]\nfoo = bar;python_version<'3'",
+ "[options.extras_require]\nfoo = bar;os_name=='linux'",
+ "[options.extras_require]\nfoo = bar;python_version<'3'\n",
+ "[options.extras_require]\nfoo = bar;os_name=='linux'\n",
+ "[options]\ninstall_requires = bar;python_version<'3'",
+ "[options]\ninstall_requires = bar;os_name=='linux'",
+ "[options]\ninstall_requires = bar;python_version<'3'\n",
+ "[options]\ninstall_requires = bar;os_name=='linux'\n",
+ ],
+ )
+ def test_raises_accidental_env_marker_misconfig(self, config, tmpdir):
+ fake_env(tmpdir, config)
+ match = (
+ r"One of the parsed requirements in `(install_requires|extras_require.+)` "
+ "looks like a valid environment marker.*"
+ )
+ with pytest.raises(InvalidRequirement, match=match):
+ with get_dist(tmpdir) as _:
+ pass
+
+ @pytest.mark.parametrize(
+ "config",
+ [
+ "[options.extras_require]\nfoo = bar;python_version<3",
+ "[options.extras_require]\nfoo = bar;python_version<3\n",
+ "[options]\ninstall_requires = bar;python_version<3",
+ "[options]\ninstall_requires = bar;python_version<3\n",
+ ],
+ )
+ def test_warn_accidental_env_marker_misconfig(self, config, tmpdir):
+ fake_env(tmpdir, config)
+ match = (
+ r"One of the parsed requirements in `(install_requires|extras_require.+)` "
+ "looks like a valid environment marker.*"
+ )
+ with pytest.warns(SetuptoolsDeprecationWarning, match=match):
+ with get_dist(tmpdir) as _:
+ pass
+
+ @pytest.mark.parametrize(
+ "config",
+ [
+ "[options.extras_require]\nfoo =\n bar;python_version<'3'",
+ "[options.extras_require]\nfoo = bar;baz\nboo = xxx;yyy",
+ "[options.extras_require]\nfoo =\n bar;python_version<'3'\n",
+ "[options.extras_require]\nfoo = bar;baz\nboo = xxx;yyy\n",
+ "[options.extras_require]\nfoo =\n bar\n python_version<3\n",
+ "[options]\ninstall_requires =\n bar;python_version<'3'",
+ "[options]\ninstall_requires = bar;baz\nboo = xxx;yyy",
+ "[options]\ninstall_requires =\n bar;python_version<'3'\n",
+ "[options]\ninstall_requires = bar;baz\nboo = xxx;yyy\n",
+ "[options]\ninstall_requires =\n bar\n python_version<3\n",
+ ],
+ )
+ @pytest.mark.filterwarnings("error::setuptools.SetuptoolsDeprecationWarning")
+ def test_nowarn_accidental_env_marker_misconfig(self, config, tmpdir, recwarn):
+ fake_env(tmpdir, config)
+ num_warnings = len(recwarn)
+ with get_dist(tmpdir) as _:
+ pass
+ # The examples are valid, no warnings shown
+ assert len(recwarn) == num_warnings
+
+ def test_dash_preserved_extras_require(self, tmpdir):
+ fake_env(tmpdir, '[options.extras_require]\nfoo-a = foo\nfoo_b = test\n')
+
+ with get_dist(tmpdir) as dist:
+ assert dist.extras_require == {'foo-a': ['foo'], 'foo_b': ['test']}
+
+ def test_entry_points(self, tmpdir):
+ _, config = fake_env(
+ tmpdir,
+ '[options.entry_points]\n'
+ 'group1 = point1 = pack.module:func, '
+ '.point2 = pack.module2:func_rest [rest]\n'
+ 'group2 = point3 = pack.module:func2\n',
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert dist.entry_points == {
+ 'group1': [
+ 'point1 = pack.module:func',
+ '.point2 = pack.module2:func_rest [rest]',
+ ],
+ 'group2': ['point3 = pack.module:func2'],
+ }
+
+ expected = (
+ '[blogtool.parsers]\n'
+ '.rst = some.nested.module:SomeClass.some_classmethod[reST]\n'
+ )
+
+ tmpdir.join('entry_points').write(expected)
+
+ # From file.
+ config.write('[options]\nentry_points = file: entry_points\n')
+
+ with get_dist(tmpdir) as dist:
+ assert dist.entry_points == expected
+
+ def test_case_sensitive_entry_points(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[options.entry_points]\n'
+ 'GROUP1 = point1 = pack.module:func, '
+ '.point2 = pack.module2:func_rest [rest]\n'
+ 'group2 = point3 = pack.module:func2\n',
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert dist.entry_points == {
+ 'GROUP1': [
+ 'point1 = pack.module:func',
+ '.point2 = pack.module2:func_rest [rest]',
+ ],
+ 'group2': ['point3 = pack.module:func2'],
+ }
+
+ def test_data_files(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[options.data_files]\n'
+ 'cfg =\n'
+ ' a/b.conf\n'
+ ' c/d.conf\n'
+ 'data = e/f.dat, g/h.dat\n',
+ )
+
+ with get_dist(tmpdir) as dist:
+ expected = [
+ ('cfg', ['a/b.conf', 'c/d.conf']),
+ ('data', ['e/f.dat', 'g/h.dat']),
+ ]
+ assert sorted(dist.data_files) == sorted(expected)
+
+ def test_data_files_globby(self, tmpdir):
+ fake_env(
+ tmpdir,
+ '[options.data_files]\n'
+ 'cfg =\n'
+ ' a/b.conf\n'
+ ' c/d.conf\n'
+ 'data = *.dat\n'
+ 'icons = \n'
+ ' *.ico\n'
+ 'audio = \n'
+ ' *.wav\n'
+ ' sounds.db\n',
+ )
+
+ # Create dummy files for glob()'s sake:
+ tmpdir.join('a.dat').write('')
+ tmpdir.join('b.dat').write('')
+ tmpdir.join('c.dat').write('')
+ tmpdir.join('a.ico').write('')
+ tmpdir.join('b.ico').write('')
+ tmpdir.join('c.ico').write('')
+ tmpdir.join('beep.wav').write('')
+ tmpdir.join('boop.wav').write('')
+ tmpdir.join('sounds.db').write('')
+
+ with get_dist(tmpdir) as dist:
+ expected = [
+ ('cfg', ['a/b.conf', 'c/d.conf']),
+ ('data', ['a.dat', 'b.dat', 'c.dat']),
+ ('icons', ['a.ico', 'b.ico', 'c.ico']),
+ ('audio', ['beep.wav', 'boop.wav', 'sounds.db']),
+ ]
+ assert sorted(dist.data_files) == sorted(expected)
+
+ def test_python_requires_simple(self, tmpdir):
+ fake_env(
+ tmpdir,
+ DALS(
+ """
+ [options]
+ python_requires=>=2.7
+ """
+ ),
+ )
+ with get_dist(tmpdir) as dist:
+ dist.parse_config_files()
+
+ def test_python_requires_compound(self, tmpdir):
+ fake_env(
+ tmpdir,
+ DALS(
+ """
+ [options]
+ python_requires=>=2.7,!=3.0.*
+ """
+ ),
+ )
+ with get_dist(tmpdir) as dist:
+ dist.parse_config_files()
+
+ def test_python_requires_invalid(self, tmpdir):
+ fake_env(
+ tmpdir,
+ DALS(
+ """
+ [options]
+ python_requires=invalid
+ """
+ ),
+ )
+ with pytest.raises(Exception):
+ with get_dist(tmpdir) as dist:
+ dist.parse_config_files()
+
+ def test_cmdclass(self, tmpdir):
+ module_path = Path(tmpdir, "src/custom_build.py") # auto discovery for src
+ module_path.parent.mkdir(parents=True, exist_ok=True)
+ module_path.write_text(
+ "from distutils.core import Command\nclass CustomCmd(Command): pass\n",
+ encoding="utf-8",
+ )
+
+ setup_cfg = """
+ [options]
+ cmdclass =
+ customcmd = custom_build.CustomCmd
+ """
+ fake_env(tmpdir, inspect.cleandoc(setup_cfg))
+
+ with get_dist(tmpdir) as dist:
+ cmdclass = dist.cmdclass['customcmd']
+ assert cmdclass.__name__ == "CustomCmd"
+ assert cmdclass.__module__ == "custom_build"
+ assert module_path.samefile(inspect.getfile(cmdclass))
+
+ def test_requirements_file(self, tmpdir):
+ fake_env(
+ tmpdir,
+ DALS(
+ """
+ [options]
+ install_requires = file:requirements.txt
+ [options.extras_require]
+ colors = file:requirements-extra.txt
+ """
+ ),
+ )
+
+ tmpdir.join('requirements.txt').write('\ndocutils>=0.3\n\n')
+ tmpdir.join('requirements-extra.txt').write('colorama')
+
+ with get_dist(tmpdir) as dist:
+ assert dist.install_requires == ['docutils>=0.3']
+ assert dist.extras_require == {'colors': ['colorama']}
+
+
+saved_dist_init = _Distribution.__init__
+
+
+class TestExternalSetters:
+ # During creation of the setuptools Distribution() object, we call
+ # the init of the parent distutils Distribution object via
+ # _Distribution.__init__ ().
+ #
+ # It's possible distutils calls out to various keyword
+ # implementations (i.e. distutils.setup_keywords entry points)
+ # that may set a range of variables.
+ #
+ # This wraps distutil's Distribution.__init__ and simulates
+ # pbr or something else setting these values.
+ def _fake_distribution_init(self, dist, attrs):
+ saved_dist_init(dist, attrs)
+ # see self._DISTUTILS_UNSUPPORTED_METADATA
+ dist.metadata.long_description_content_type = 'text/something'
+ # Test overwrite setup() args
+ dist.metadata.project_urls = {
+ 'Link One': 'https://example.com/one/',
+ 'Link Two': 'https://example.com/two/',
+ }
+
+ @patch.object(_Distribution, '__init__', autospec=True)
+ def test_external_setters(self, mock_parent_init, tmpdir):
+ mock_parent_init.side_effect = self._fake_distribution_init
+
+ dist = Distribution(attrs={'project_urls': {'will_be': 'ignored'}})
+
+ assert dist.metadata.long_description_content_type == 'text/something'
+ assert dist.metadata.project_urls == {
+ 'Link One': 'https://example.com/one/',
+ 'Link Two': 'https://example.com/two/',
+ }