aboutsummaryrefslogtreecommitdiff
path: root/.venv/lib/python3.12/site-packages/setuptools/tests
diff options
context:
space:
mode:
Diffstat (limited to '.venv/lib/python3.12/site-packages/setuptools/tests')
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/__init__.py13
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/compat/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/compat/py39.py3
-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
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/contexts.py145
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/environment.py95
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/fixtures.py157
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/indexes/test_links_priority/external.html3
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/indexes/test_links_priority/simple/foobar/index.html4
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/integration/__init__.py0
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/integration/helpers.py77
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/integration/test_pip_install_sdist.py223
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/mod_with_constant.py1
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/namespaces.py90
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/script-with-bom.py1
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/server.py86
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_archive_util.py36
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_deprecations.py28
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_egg.py73
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_wheel.py708
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_build.py33
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_build_clib.py84
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_build_ext.py293
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_build_meta.py983
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_build_py.py480
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py647
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_core_metadata.py622
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_depends.py15
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_develop.py175
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_dist.py278
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_dist_info.py210
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_distutils_adoption.py198
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_easy_install.py1476
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_editable_install.py1289
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py1305
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_extern.py15
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_find_packages.py218
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_find_py_modules.py73
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_glob.py45
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_install_scripts.py89
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_logging.py76
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_manifest.py622
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_namespaces.py138
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_packageindex.py267
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_sandbox.py134
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_sdist.py984
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_setopt.py40
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_setuptools.py290
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_shutil_wrapper.py23
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_unicode_utils.py10
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_virtualenv.py113
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_warnings.py106
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_wheel.py714
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/test_windows_wrappers.py259
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/text.py4
-rw-r--r--.venv/lib/python3.12/site-packages/setuptools/tests/textwrap.py6
64 files changed, 16649 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/tests/__init__.py
new file mode 100644
index 00000000..eb70bfb7
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/__init__.py
@@ -0,0 +1,13 @@
+import locale
+import sys
+
+import pytest
+
+__all__ = ['fail_on_ascii']
+
+if sys.version_info >= (3, 11):
+ locale_encoding = locale.getencoding()
+else:
+ locale_encoding = locale.getpreferredencoding(False)
+is_ascii = locale_encoding == 'ANSI_X3.4-1968'
+fail_on_ascii = pytest.mark.xfail(is_ascii, reason="Test fails in this locale")
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/compat/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/tests/compat/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/compat/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/compat/py39.py b/.venv/lib/python3.12/site-packages/setuptools/tests/compat/py39.py
new file mode 100644
index 00000000..1fdb9dac
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/compat/py39.py
@@ -0,0 +1,3 @@
+from jaraco.test.cpython import from_test_support, try_import
+
+os_helper = try_import('os_helper') or from_test_support('can_symlink')
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/',
+ }
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/contexts.py b/.venv/lib/python3.12/site-packages/setuptools/tests/contexts.py
new file mode 100644
index 00000000..97cceea0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/contexts.py
@@ -0,0 +1,145 @@
+import contextlib
+import io
+import os
+import shutil
+import site
+import sys
+import tempfile
+
+from filelock import FileLock
+
+
+@contextlib.contextmanager
+def tempdir(cd=lambda dir: None, **kwargs):
+ temp_dir = tempfile.mkdtemp(**kwargs)
+ orig_dir = os.getcwd()
+ try:
+ cd(temp_dir)
+ yield temp_dir
+ finally:
+ cd(orig_dir)
+ shutil.rmtree(temp_dir)
+
+
+@contextlib.contextmanager
+def environment(**replacements):
+ """
+ In a context, patch the environment with replacements. Pass None values
+ to clear the values.
+ """
+ saved = dict((key, os.environ[key]) for key in replacements if key in os.environ)
+
+ # remove values that are null
+ remove = (key for (key, value) in replacements.items() if value is None)
+ for key in list(remove):
+ os.environ.pop(key, None)
+ replacements.pop(key)
+
+ os.environ.update(replacements)
+
+ try:
+ yield saved
+ finally:
+ for key in replacements:
+ os.environ.pop(key, None)
+ os.environ.update(saved)
+
+
+@contextlib.contextmanager
+def quiet():
+ """
+ Redirect stdout/stderr to StringIO objects to prevent console output from
+ distutils commands.
+ """
+
+ old_stdout = sys.stdout
+ old_stderr = sys.stderr
+ new_stdout = sys.stdout = io.StringIO()
+ new_stderr = sys.stderr = io.StringIO()
+ try:
+ yield new_stdout, new_stderr
+ finally:
+ new_stdout.seek(0)
+ new_stderr.seek(0)
+ sys.stdout = old_stdout
+ sys.stderr = old_stderr
+
+
+@contextlib.contextmanager
+def save_user_site_setting():
+ saved = site.ENABLE_USER_SITE
+ try:
+ yield saved
+ finally:
+ site.ENABLE_USER_SITE = saved
+
+
+@contextlib.contextmanager
+def save_pkg_resources_state():
+ import pkg_resources
+
+ pr_state = pkg_resources.__getstate__()
+ # also save sys.path
+ sys_path = sys.path[:]
+ try:
+ yield pr_state, sys_path
+ finally:
+ sys.path[:] = sys_path
+ pkg_resources.__setstate__(pr_state)
+
+
+@contextlib.contextmanager
+def suppress_exceptions(*excs):
+ try:
+ yield
+ except excs:
+ pass
+
+
+def multiproc(request):
+ """
+ Return True if running under xdist and multiple
+ workers are used.
+ """
+ try:
+ worker_id = request.getfixturevalue('worker_id')
+ except Exception:
+ return False
+ return worker_id != 'master'
+
+
+@contextlib.contextmanager
+def session_locked_tmp_dir(request, tmp_path_factory, name):
+ """Uses a file lock to guarantee only one worker can access a temp dir"""
+ # get the temp directory shared by all workers
+ base = tmp_path_factory.getbasetemp()
+ shared_dir = base.parent if multiproc(request) else base
+
+ locked_dir = shared_dir / name
+ with FileLock(locked_dir.with_suffix(".lock")):
+ # ^-- prevent multiple workers to access the directory at once
+ locked_dir.mkdir(exist_ok=True, parents=True)
+ yield locked_dir
+
+
+@contextlib.contextmanager
+def save_paths():
+ """Make sure ``sys.path``, ``sys.meta_path`` and ``sys.path_hooks`` are preserved"""
+ prev = sys.path[:], sys.meta_path[:], sys.path_hooks[:]
+
+ try:
+ yield
+ finally:
+ sys.path, sys.meta_path, sys.path_hooks = prev
+
+
+@contextlib.contextmanager
+def save_sys_modules():
+ """Make sure initial ``sys.modules`` is preserved"""
+ prev_modules = sys.modules
+
+ try:
+ sys.modules = sys.modules.copy()
+ yield
+ finally:
+ sys.modules = prev_modules
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/environment.py b/.venv/lib/python3.12/site-packages/setuptools/tests/environment.py
new file mode 100644
index 00000000..ed5499ef
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/environment.py
@@ -0,0 +1,95 @@
+import os
+import subprocess
+import sys
+import unicodedata
+from subprocess import PIPE as _PIPE, Popen as _Popen
+
+import jaraco.envs
+
+
+class VirtualEnv(jaraco.envs.VirtualEnv):
+ name = '.env'
+ # Some version of PyPy will import distutils on startup, implicitly
+ # importing setuptools, and thus leading to BackendInvalid errors
+ # when upgrading Setuptools. Bypass this behavior by avoiding the
+ # early availability and need to upgrade.
+ create_opts = ['--no-setuptools']
+
+ def run(self, cmd, *args, **kwargs):
+ cmd = [self.exe(cmd[0])] + cmd[1:]
+ kwargs = {"cwd": self.root, "encoding": "utf-8", **kwargs} # Allow overriding
+ # In some environments (eg. downstream distro packaging), where:
+ # - tox isn't used to run tests and
+ # - PYTHONPATH is set to point to a specific setuptools codebase and
+ # - no custom env is explicitly set by a test
+ # PYTHONPATH will leak into the spawned processes.
+ # In that case tests look for module in the wrong place (on PYTHONPATH).
+ # Unless the test sets its own special env, pass a copy of the existing
+ # environment with removed PYTHONPATH to the subprocesses.
+ if "env" not in kwargs:
+ env = dict(os.environ)
+ if "PYTHONPATH" in env:
+ del env["PYTHONPATH"]
+ kwargs["env"] = env
+ return subprocess.check_output(cmd, *args, **kwargs)
+
+
+def _which_dirs(cmd):
+ result = set()
+ for path in os.environ.get('PATH', '').split(os.pathsep):
+ filename = os.path.join(path, cmd)
+ if os.access(filename, os.X_OK):
+ result.add(path)
+ return result
+
+
+def run_setup_py(cmd, pypath=None, path=None, data_stream=0, env=None):
+ """
+ Execution command for tests, separate from those used by the
+ code directly to prevent accidental behavior issues
+ """
+ if env is None:
+ env = dict()
+ for envname in os.environ:
+ env[envname] = os.environ[envname]
+
+ # override the python path if needed
+ if pypath is not None:
+ env["PYTHONPATH"] = pypath
+
+ # override the execution path if needed
+ if path is not None:
+ env["PATH"] = path
+ if not env.get("PATH", ""):
+ env["PATH"] = _which_dirs("tar").union(_which_dirs("gzip"))
+ env["PATH"] = os.pathsep.join(env["PATH"])
+
+ cmd = [sys.executable, "setup.py"] + list(cmd)
+
+ # https://bugs.python.org/issue8557
+ shell = sys.platform == 'win32'
+
+ try:
+ proc = _Popen(
+ cmd,
+ stdout=_PIPE,
+ stderr=_PIPE,
+ shell=shell,
+ env=env,
+ encoding="utf-8",
+ )
+
+ if isinstance(data_stream, tuple):
+ data_stream = slice(*data_stream)
+ data = proc.communicate()[data_stream]
+ except OSError:
+ return 1, ''
+
+ # decode the console string if needed
+ if hasattr(data, "decode"):
+ # use the default encoding
+ data = data.decode()
+ data = unicodedata.normalize('NFC', data)
+
+ # communicate calls wait()
+ return proc.returncode, data
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/fixtures.py b/.venv/lib/python3.12/site-packages/setuptools/tests/fixtures.py
new file mode 100644
index 00000000..a5472984
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/fixtures.py
@@ -0,0 +1,157 @@
+import contextlib
+import os
+import subprocess
+import sys
+from pathlib import Path
+
+import path
+import pytest
+
+from . import contexts, environment
+
+
+@pytest.fixture
+def user_override(monkeypatch):
+ """
+ Override site.USER_BASE and site.USER_SITE with temporary directories in
+ a context.
+ """
+ with contexts.tempdir() as user_base:
+ monkeypatch.setattr('site.USER_BASE', user_base)
+ with contexts.tempdir() as user_site:
+ monkeypatch.setattr('site.USER_SITE', user_site)
+ with contexts.save_user_site_setting():
+ yield
+
+
+@pytest.fixture
+def tmpdir_cwd(tmpdir):
+ with tmpdir.as_cwd() as orig:
+ yield orig
+
+
+@pytest.fixture(autouse=True, scope="session")
+def workaround_xdist_376(request):
+ """
+ Workaround pytest-dev/pytest-xdist#376
+
+ ``pytest-xdist`` tends to inject '' into ``sys.path``,
+ which may break certain isolation expectations.
+ Remove the entry so the import
+ machinery behaves the same irrespective of xdist.
+ """
+ if not request.config.pluginmanager.has_plugin('xdist'):
+ return
+
+ with contextlib.suppress(ValueError):
+ sys.path.remove('')
+
+
+@pytest.fixture
+def sample_project(tmp_path):
+ """
+ Clone the 'sampleproject' and return a path to it.
+ """
+ cmd = ['git', 'clone', 'https://github.com/pypa/sampleproject']
+ try:
+ subprocess.check_call(cmd, cwd=str(tmp_path))
+ except Exception:
+ pytest.skip("Unable to clone sampleproject")
+ return tmp_path / 'sampleproject'
+
+
+# sdist and wheel artifacts should be stable across a round of tests
+# so we can build them once per session and use the files as "readonly"
+
+# In the case of setuptools, building the wheel without sdist may cause
+# it to contain the `build` directory, and therefore create situations with
+# `setuptools/build/lib/build/lib/...`. To avoid that, build both artifacts at once.
+
+
+def _build_distributions(tmp_path_factory, request):
+ with contexts.session_locked_tmp_dir(
+ request, tmp_path_factory, "dist_build"
+ ) as tmp: # pragma: no cover
+ sdist = next(tmp.glob("*.tar.gz"), None)
+ wheel = next(tmp.glob("*.whl"), None)
+ if sdist and wheel:
+ return (sdist, wheel)
+
+ # Sanity check: should not create recursive setuptools/build/lib/build/lib/...
+ assert not Path(request.config.rootdir, "build/lib/build").exists()
+
+ subprocess.check_output([
+ sys.executable,
+ "-m",
+ "build",
+ "--outdir",
+ str(tmp),
+ str(request.config.rootdir),
+ ])
+
+ # Sanity check: should not create recursive setuptools/build/lib/build/lib/...
+ assert not Path(request.config.rootdir, "build/lib/build").exists()
+
+ return next(tmp.glob("*.tar.gz")), next(tmp.glob("*.whl"))
+
+
+@pytest.fixture(scope="session")
+def setuptools_sdist(tmp_path_factory, request):
+ prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_SDIST")
+ if prebuilt and os.path.exists(prebuilt): # pragma: no cover
+ return Path(prebuilt).resolve()
+
+ sdist, _ = _build_distributions(tmp_path_factory, request)
+ return sdist
+
+
+@pytest.fixture(scope="session")
+def setuptools_wheel(tmp_path_factory, request):
+ prebuilt = os.getenv("PRE_BUILT_SETUPTOOLS_WHEEL")
+ if prebuilt and os.path.exists(prebuilt): # pragma: no cover
+ return Path(prebuilt).resolve()
+
+ _, wheel = _build_distributions(tmp_path_factory, request)
+ return wheel
+
+
+@pytest.fixture
+def venv(tmp_path, setuptools_wheel):
+ """Virtual env with the version of setuptools under test installed"""
+ env = environment.VirtualEnv()
+ env.root = path.Path(tmp_path / 'venv')
+ env.create_opts = ['--no-setuptools', '--wheel=bundle']
+ # TODO: Use `--no-wheel` when setuptools implements its own bdist_wheel
+ env.req = str(setuptools_wheel)
+ # In some environments (eg. downstream distro packaging),
+ # where tox isn't used to run tests and PYTHONPATH is set to point to
+ # a specific setuptools codebase, PYTHONPATH will leak into the spawned
+ # processes.
+ # env.create() should install the just created setuptools
+ # wheel, but it doesn't if it finds another existing matching setuptools
+ # installation present on PYTHONPATH:
+ # `setuptools is already installed with the same version as the provided
+ # wheel. Use --force-reinstall to force an installation of the wheel.`
+ # This prevents leaking PYTHONPATH to the created environment.
+ with contexts.environment(PYTHONPATH=None):
+ return env.create()
+
+
+@pytest.fixture
+def venv_without_setuptools(tmp_path):
+ """Virtual env without any version of setuptools installed"""
+ env = environment.VirtualEnv()
+ env.root = path.Path(tmp_path / 'venv_without_setuptools')
+ env.create_opts = ['--no-setuptools', '--no-wheel']
+ env.ensure_env()
+ return env
+
+
+@pytest.fixture
+def bare_venv(tmp_path):
+ """Virtual env without any common packages installed"""
+ env = environment.VirtualEnv()
+ env.root = path.Path(tmp_path / 'bare_venv')
+ env.create_opts = ['--no-setuptools', '--no-pip', '--no-wheel', '--no-seed']
+ env.ensure_env()
+ return env
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/indexes/test_links_priority/external.html b/.venv/lib/python3.12/site-packages/setuptools/tests/indexes/test_links_priority/external.html
new file mode 100644
index 00000000..92e4702f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/indexes/test_links_priority/external.html
@@ -0,0 +1,3 @@
+<html><body>
+<a href="/foobar-0.1.tar.gz#md5=1__bad_md5___">bad old link</a>
+</body></html>
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/indexes/test_links_priority/simple/foobar/index.html b/.venv/lib/python3.12/site-packages/setuptools/tests/indexes/test_links_priority/simple/foobar/index.html
new file mode 100644
index 00000000..fefb028b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/indexes/test_links_priority/simple/foobar/index.html
@@ -0,0 +1,4 @@
+<html><body>
+<a href="/foobar-0.1.tar.gz#md5=0_correct_md5">foobar-0.1.tar.gz</a><br/>
+<a href="../../external.html" rel="homepage">external homepage</a><br/>
+</body></html>
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/integration/__init__.py b/.venv/lib/python3.12/site-packages/setuptools/tests/integration/__init__.py
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/integration/__init__.py
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/integration/helpers.py b/.venv/lib/python3.12/site-packages/setuptools/tests/integration/helpers.py
new file mode 100644
index 00000000..77b196e0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/integration/helpers.py
@@ -0,0 +1,77 @@
+"""Reusable functions and classes for different types of integration tests.
+
+For example ``Archive`` can be used to check the contents of distribution built
+with setuptools, and ``run`` will always try to be as verbose as possible to
+facilitate debugging.
+"""
+
+import os
+import subprocess
+import tarfile
+from pathlib import Path
+from zipfile import ZipFile
+
+
+def run(cmd, env=None):
+ r = subprocess.run(
+ cmd,
+ capture_output=True,
+ text=True,
+ encoding="utf-8",
+ env={**os.environ, **(env or {})},
+ # ^-- allow overwriting instead of discarding the current env
+ )
+
+ out = r.stdout + "\n" + r.stderr
+ # pytest omits stdout/err by default, if the test fails they help debugging
+ print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
+ print(f"Command: {cmd}\nreturn code: {r.returncode}\n\n{out}")
+
+ if r.returncode == 0:
+ return out
+ raise subprocess.CalledProcessError(r.returncode, cmd, r.stdout, r.stderr)
+
+
+class Archive:
+ """Compatibility layer for ZipFile/Info and TarFile/Info"""
+
+ def __init__(self, filename):
+ self._filename = filename
+ if filename.endswith("tar.gz"):
+ self._obj = tarfile.open(filename, "r:gz")
+ elif filename.endswith("zip"):
+ self._obj = ZipFile(filename)
+ else:
+ raise ValueError(f"{filename} doesn't seem to be a zip or tar.gz")
+
+ def __iter__(self):
+ if hasattr(self._obj, "infolist"):
+ return iter(self._obj.infolist())
+ return iter(self._obj)
+
+ def get_name(self, zip_or_tar_info):
+ if hasattr(zip_or_tar_info, "filename"):
+ return zip_or_tar_info.filename
+ return zip_or_tar_info.name
+
+ def get_content(self, zip_or_tar_info):
+ if hasattr(self._obj, "extractfile"):
+ content = self._obj.extractfile(zip_or_tar_info)
+ if content is None:
+ msg = f"Invalid {zip_or_tar_info.name} in {self._filename}"
+ raise ValueError(msg)
+ return str(content.read(), "utf-8")
+ return str(self._obj.read(zip_or_tar_info), "utf-8")
+
+
+def get_sdist_members(sdist_path):
+ with tarfile.open(sdist_path, "r:gz") as tar:
+ files = [Path(f) for f in tar.getnames()]
+ # remove root folder
+ relative_files = ("/".join(f.parts[1:]) for f in files)
+ return {f for f in relative_files if f}
+
+
+def get_wheel_members(wheel_path):
+ with ZipFile(wheel_path) as zipfile:
+ return set(zipfile.namelist())
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/integration/test_pip_install_sdist.py b/.venv/lib/python3.12/site-packages/setuptools/tests/integration/test_pip_install_sdist.py
new file mode 100644
index 00000000..4e84f218
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/integration/test_pip_install_sdist.py
@@ -0,0 +1,223 @@
+# https://github.com/python/mypy/issues/16936
+# mypy: disable-error-code="has-type"
+"""Integration tests for setuptools that focus on building packages via pip.
+
+The idea behind these tests is not to exhaustively check all the possible
+combinations of packages, operating systems, supporting libraries, etc, but
+rather check a limited number of popular packages and how they interact with
+the exposed public API. This way if any change in API is introduced, we hope to
+identify backward compatibility problems before publishing a release.
+
+The number of tested packages is purposefully kept small, to minimise duration
+and the associated maintenance cost (changes in the way these packages define
+their build process may require changes in the tests).
+"""
+
+import json
+import os
+import shutil
+import sys
+from enum import Enum
+from glob import glob
+from hashlib import md5
+from urllib.request import urlopen
+
+import pytest
+from packaging.requirements import Requirement
+
+from .helpers import Archive, run
+
+pytestmark = pytest.mark.integration
+
+
+(LATEST,) = Enum("v", "LATEST") # type: ignore[misc] # https://github.com/python/mypy/issues/16936
+"""Default version to be checked"""
+# There are positive and negative aspects of checking the latest version of the
+# packages.
+# The main positive aspect is that the latest version might have already
+# removed the use of APIs deprecated in previous releases of setuptools.
+
+
+# Packages to be tested:
+# (Please notice the test environment cannot support EVERY library required for
+# compiling binary extensions. In Ubuntu/Debian nomenclature, we only assume
+# that `build-essential`, `gfortran` and `libopenblas-dev` are installed,
+# due to their relevance to the numerical/scientific programming ecosystem)
+EXAMPLES = [
+ ("pip", LATEST), # just in case...
+ ("pytest", LATEST), # uses setuptools_scm
+ ("mypy", LATEST), # custom build_py + ext_modules
+ # --- Popular packages: https://hugovk.github.io/top-pypi-packages/ ---
+ ("botocore", LATEST),
+ ("kiwisolver", LATEST), # build_ext
+ ("brotli", LATEST), # not in the list but used by urllib3
+ ("pyyaml", LATEST), # cython + custom build_ext + custom distclass
+ ("charset-normalizer", LATEST), # uses mypyc, used by aiohttp
+ ("protobuf", LATEST),
+ # ("requests", LATEST), # XXX: https://github.com/psf/requests/pull/6920
+ ("celery", LATEST),
+ # When adding packages to this list, make sure they expose a `__version__`
+ # attribute, or modify the tests below
+]
+
+
+# Some packages have "optional" dependencies that modify their build behaviour
+# and are not listed in pyproject.toml, others still use `setup_requires`
+EXTRA_BUILD_DEPS = {
+ "pyyaml": ("Cython<3.0",), # constraint to avoid errors
+ "charset-normalizer": ("mypy>=1.4.1",), # no pyproject.toml available
+}
+
+EXTRA_ENV_VARS = {
+ "pyyaml": {"PYYAML_FORCE_CYTHON": "1"},
+ "charset-normalizer": {"CHARSET_NORMALIZER_USE_MYPYC": "1"},
+}
+
+IMPORT_NAME = {
+ "pyyaml": "yaml",
+ "protobuf": "google.protobuf",
+}
+
+
+VIRTUALENV = (sys.executable, "-m", "virtualenv")
+
+
+# By default, pip will try to build packages in isolation (PEP 517), which
+# means it will download the previous stable version of setuptools.
+# `pip` flags can avoid that (the version of setuptools under test
+# should be the one to be used)
+INSTALL_OPTIONS = (
+ "--ignore-installed",
+ "--no-build-isolation",
+ # Omit "--no-binary :all:" the sdist is supplied directly.
+ # Allows dependencies as wheels.
+)
+# The downside of `--no-build-isolation` is that pip will not download build
+# dependencies. The test script will have to also handle that.
+
+
+@pytest.fixture
+def venv_python(tmp_path):
+ run([*VIRTUALENV, str(tmp_path / ".venv")])
+ possible_path = (str(p.parent) for p in tmp_path.glob(".venv/*/python*"))
+ return shutil.which("python", path=os.pathsep.join(possible_path))
+
+
+@pytest.fixture(autouse=True)
+def _prepare(tmp_path, venv_python, monkeypatch):
+ download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path))
+ os.makedirs(download_path, exist_ok=True)
+
+ # Environment vars used for building some of the packages
+ monkeypatch.setenv("USE_MYPYC", "1")
+
+ yield
+
+ # Let's provide the maximum amount of information possible in the case
+ # it is necessary to debug the tests directly from the CI logs.
+ print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
+ print("Temporary directory:")
+ map(print, tmp_path.glob("*"))
+ print("Virtual environment:")
+ run([venv_python, "-m", "pip", "freeze"])
+
+
+@pytest.mark.parametrize(("package", "version"), EXAMPLES)
+@pytest.mark.uses_network
+def test_install_sdist(package, version, tmp_path, venv_python, setuptools_wheel):
+ venv_pip = (venv_python, "-m", "pip")
+ sdist = retrieve_sdist(package, version, tmp_path)
+ deps = build_deps(package, sdist)
+ if deps:
+ print("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~")
+ print("Dependencies:", deps)
+ run([*venv_pip, "install", *deps])
+
+ # Use a virtualenv to simulate PEP 517 isolation
+ # but install fresh setuptools wheel to ensure the version under development
+ env = EXTRA_ENV_VARS.get(package, {})
+ run([*venv_pip, "install", "--force-reinstall", setuptools_wheel])
+ run([*venv_pip, "install", *INSTALL_OPTIONS, sdist], env)
+
+ # Execute a simple script to make sure the package was installed correctly
+ pkg = IMPORT_NAME.get(package, package).replace("-", "_")
+ script = f"import {pkg}; print(getattr({pkg}, '__version__', 0))"
+ run([venv_python, "-c", script])
+
+
+# ---- Helper Functions ----
+
+
+def retrieve_sdist(package, version, tmp_path):
+ """Either use cached sdist file or download it from PyPI"""
+ # `pip download` cannot be used due to
+ # https://github.com/pypa/pip/issues/1884
+ # https://discuss.python.org/t/pep-625-file-name-of-a-source-distribution/4686
+ # We have to find the correct distribution file and download it
+ download_path = os.getenv("DOWNLOAD_PATH", str(tmp_path))
+ dist = retrieve_pypi_sdist_metadata(package, version)
+
+ # Remove old files to prevent cache to grow indefinitely
+ for file in glob(os.path.join(download_path, f"{package}*")):
+ if dist["filename"] != file:
+ os.unlink(file)
+
+ dist_file = os.path.join(download_path, dist["filename"])
+ if not os.path.exists(dist_file):
+ download(dist["url"], dist_file, dist["md5_digest"])
+ return dist_file
+
+
+def retrieve_pypi_sdist_metadata(package, version):
+ # https://warehouse.pypa.io/api-reference/json.html
+ id_ = package if version is LATEST else f"{package}/{version}"
+ with urlopen(f"https://pypi.org/pypi/{id_}/json") as f:
+ metadata = json.load(f)
+
+ if metadata["info"]["yanked"]:
+ raise ValueError(f"Release for {package} {version} was yanked")
+
+ version = metadata["info"]["version"]
+ release = metadata["releases"][version] if version is LATEST else metadata["urls"]
+ (sdist,) = filter(lambda d: d["packagetype"] == "sdist", release)
+ return sdist
+
+
+def download(url, dest, md5_digest):
+ with urlopen(url) as f:
+ data = f.read()
+
+ assert md5(data).hexdigest() == md5_digest
+
+ with open(dest, "wb") as f:
+ f.write(data)
+
+ assert os.path.exists(dest)
+
+
+def build_deps(package, sdist_file):
+ """Find out what are the build dependencies for a package.
+
+ "Manually" install them, since pip will not install build
+ deps with `--no-build-isolation`.
+ """
+ # delay importing, since pytest discovery phase may hit this file from a
+ # testenv without tomli
+ from setuptools.compat.py310 import tomllib
+
+ archive = Archive(sdist_file)
+ info = tomllib.loads(_read_pyproject(archive))
+ deps = info.get("build-system", {}).get("requires", [])
+ deps += EXTRA_BUILD_DEPS.get(package, [])
+ # Remove setuptools from requirements (and deduplicate)
+ requirements = {Requirement(d).name: d for d in deps}
+ return [v for k, v in requirements.items() if k != "setuptools"]
+
+
+def _read_pyproject(archive):
+ contents = (
+ archive.get_content(member)
+ for member in archive
+ if os.path.basename(archive.get_name(member)) == "pyproject.toml"
+ )
+ return next(contents, "")
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/mod_with_constant.py b/.venv/lib/python3.12/site-packages/setuptools/tests/mod_with_constant.py
new file mode 100644
index 00000000..ef755dd1
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/mod_with_constant.py
@@ -0,0 +1 @@
+value = 'three, sir!'
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/namespaces.py b/.venv/lib/python3.12/site-packages/setuptools/tests/namespaces.py
new file mode 100644
index 00000000..248db98f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/namespaces.py
@@ -0,0 +1,90 @@
+import ast
+import json
+import textwrap
+from pathlib import Path
+
+
+def iter_namespace_pkgs(namespace):
+ parts = namespace.split(".")
+ for i in range(len(parts)):
+ yield ".".join(parts[: i + 1])
+
+
+def build_namespace_package(tmpdir, name, version="1.0", impl="pkg_resources"):
+ src_dir = tmpdir / name
+ src_dir.mkdir()
+ setup_py = src_dir / 'setup.py'
+ namespace, _, rest = name.rpartition('.')
+ namespaces = list(iter_namespace_pkgs(namespace))
+ setup_args = {
+ "name": name,
+ "version": version,
+ "packages": namespaces,
+ }
+
+ if impl == "pkg_resources":
+ tmpl = '__import__("pkg_resources").declare_namespace(__name__)'
+ setup_args["namespace_packages"] = namespaces
+ elif impl == "pkgutil":
+ tmpl = '__path__ = __import__("pkgutil").extend_path(__path__, __name__)'
+ else:
+ raise ValueError(f"Cannot recognise {impl=} when creating namespaces")
+
+ args = json.dumps(setup_args, indent=4)
+ assert ast.literal_eval(args) # ensure it is valid Python
+
+ script = textwrap.dedent(
+ """\
+ import setuptools
+ args = {args}
+ setuptools.setup(**args)
+ """
+ ).format(args=args)
+ setup_py.write_text(script, encoding='utf-8')
+
+ ns_pkg_dir = Path(src_dir, namespace.replace(".", "/"))
+ ns_pkg_dir.mkdir(parents=True)
+
+ for ns in namespaces:
+ pkg_init = src_dir / ns.replace(".", "/") / '__init__.py'
+ pkg_init.write_text(tmpl, encoding='utf-8')
+
+ pkg_mod = ns_pkg_dir / (rest + '.py')
+ some_functionality = 'name = {rest!r}'.format(**locals())
+ pkg_mod.write_text(some_functionality, encoding='utf-8')
+ return src_dir
+
+
+def build_pep420_namespace_package(tmpdir, name):
+ src_dir = tmpdir / name
+ src_dir.mkdir()
+ pyproject = src_dir / "pyproject.toml"
+ namespace, _, rest = name.rpartition(".")
+ script = f"""\
+ [build-system]
+ requires = ["setuptools"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "{name}"
+ version = "3.14159"
+ """
+ pyproject.write_text(textwrap.dedent(script), encoding='utf-8')
+ ns_pkg_dir = Path(src_dir, namespace.replace(".", "/"))
+ ns_pkg_dir.mkdir(parents=True)
+ pkg_mod = ns_pkg_dir / (rest + ".py")
+ some_functionality = f"name = {rest!r}"
+ pkg_mod.write_text(some_functionality, encoding='utf-8')
+ return src_dir
+
+
+def make_site_dir(target):
+ """
+ Add a sitecustomize.py module in target to cause
+ target to be added to site dirs such that .pth files
+ are processed there.
+ """
+ sc = target / 'sitecustomize.py'
+ target_str = str(target)
+ tmpl = '__import__("site").addsitedir({target_str!r})'
+ sc.write_text(tmpl.format(**locals()), encoding='utf-8')
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/script-with-bom.py b/.venv/lib/python3.12/site-packages/setuptools/tests/script-with-bom.py
new file mode 100644
index 00000000..c074d263
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/script-with-bom.py
@@ -0,0 +1 @@
+result = 'passed'
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/server.py b/.venv/lib/python3.12/site-packages/setuptools/tests/server.py
new file mode 100644
index 00000000..623a49a5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/server.py
@@ -0,0 +1,86 @@
+"""Basic http server for tests to simulate PyPI or custom indexes"""
+
+import http.server
+import os
+import threading
+import time
+import urllib.parse
+import urllib.request
+
+
+class IndexServer(http.server.HTTPServer):
+ """Basic single-threaded http server simulating a package index
+
+ You can use this server in unittest like this::
+ s = IndexServer()
+ s.start()
+ index_url = s.base_url() + 'mytestindex'
+ # do some test requests to the index
+ # The index files should be located in setuptools/tests/indexes
+ s.stop()
+ """
+
+ def __init__(
+ self,
+ server_address=('', 0),
+ RequestHandlerClass=http.server.SimpleHTTPRequestHandler,
+ ):
+ http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
+ self._run = True
+
+ def start(self):
+ self.thread = threading.Thread(target=self.serve_forever)
+ self.thread.start()
+
+ def stop(self):
+ "Stop the server"
+
+ # Let the server finish the last request and wait for a new one.
+ time.sleep(0.1)
+
+ self.shutdown()
+ self.thread.join()
+ self.socket.close()
+
+ def base_url(self):
+ port = self.server_port
+ return f'http://127.0.0.1:{port}/setuptools/tests/indexes/'
+
+
+class RequestRecorder(http.server.BaseHTTPRequestHandler):
+ def do_GET(self):
+ requests = vars(self.server).setdefault('requests', [])
+ requests.append(self)
+ self.send_response(200, 'OK')
+
+
+class MockServer(http.server.HTTPServer, threading.Thread):
+ """
+ A simple HTTP Server that records the requests made to it.
+ """
+
+ def __init__(self, server_address=('', 0), RequestHandlerClass=RequestRecorder):
+ http.server.HTTPServer.__init__(self, server_address, RequestHandlerClass)
+ threading.Thread.__init__(self)
+ self.daemon = True
+ self.requests = []
+
+ def run(self):
+ self.serve_forever()
+
+ @property
+ def netloc(self):
+ return f'localhost:{self.server_port}'
+
+ @property
+ def url(self):
+ return f'http://{self.netloc}/'
+
+
+def path_to_url(path, authority=None):
+ """Convert a path to a file: URL."""
+ path = os.path.normpath(os.path.abspath(path))
+ base = 'file:'
+ if authority is not None:
+ base += '//' + authority
+ return urllib.parse.urljoin(base, urllib.request.pathname2url(path))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_archive_util.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_archive_util.py
new file mode 100644
index 00000000..e3efc628
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_archive_util.py
@@ -0,0 +1,36 @@
+import io
+import tarfile
+
+import pytest
+
+from setuptools import archive_util
+
+
+@pytest.fixture
+def tarfile_with_unicode(tmpdir):
+ """
+ Create a tarfile containing only a file whose name is
+ a zero byte file called testimäge.png.
+ """
+ tarobj = io.BytesIO()
+
+ with tarfile.open(fileobj=tarobj, mode="w:gz") as tgz:
+ data = b""
+
+ filename = "testimäge.png"
+
+ t = tarfile.TarInfo(filename)
+ t.size = len(data)
+
+ tgz.addfile(t, io.BytesIO(data))
+
+ target = tmpdir / 'unicode-pkg-1.0.tar.gz'
+ with open(str(target), mode='wb') as tf:
+ tf.write(tarobj.getvalue())
+ return str(target)
+
+
+@pytest.mark.xfail(reason="#710 and #712")
+def test_unicode_files(tarfile_with_unicode, tmpdir):
+ target = tmpdir / 'out'
+ archive_util.unpack_archive(tarfile_with_unicode, str(target))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_deprecations.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_deprecations.py
new file mode 100644
index 00000000..d9d67b06
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_deprecations.py
@@ -0,0 +1,28 @@
+"""develop tests"""
+
+import sys
+from unittest import mock
+
+import pytest
+
+from setuptools import SetuptoolsDeprecationWarning
+from setuptools.dist import Distribution
+
+
+@pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only')
+@pytest.mark.xfail(reason="bdist_rpm is long deprecated, should we remove it? #1988")
+@mock.patch('distutils.command.bdist_rpm.bdist_rpm')
+def test_bdist_rpm_warning(distutils_cmd, tmpdir_cwd):
+ dist = Distribution(
+ dict(
+ script_name='setup.py',
+ script_args=['bdist_rpm'],
+ name='foo',
+ py_modules=['hi'],
+ )
+ )
+ dist.parse_command_line()
+ with pytest.warns(SetuptoolsDeprecationWarning):
+ dist.run_commands()
+
+ distutils_cmd.run.assert_called_once()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_egg.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_egg.py
new file mode 100644
index 00000000..036167dd
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_egg.py
@@ -0,0 +1,73 @@
+"""develop tests"""
+
+import os
+import re
+import zipfile
+
+import pytest
+
+from setuptools.dist import Distribution
+
+from . import contexts
+
+SETUP_PY = """\
+from setuptools import setup
+
+setup(py_modules=['hi'])
+"""
+
+
+@pytest.fixture
+def setup_context(tmpdir):
+ with (tmpdir / 'setup.py').open('w') as f:
+ f.write(SETUP_PY)
+ with (tmpdir / 'hi.py').open('w') as f:
+ f.write('1\n')
+ with tmpdir.as_cwd():
+ yield tmpdir
+
+
+class Test:
+ @pytest.mark.usefixtures("user_override")
+ @pytest.mark.usefixtures("setup_context")
+ def test_bdist_egg(self):
+ dist = Distribution(
+ dict(
+ script_name='setup.py',
+ script_args=['bdist_egg'],
+ name='foo',
+ py_modules=['hi'],
+ )
+ )
+ os.makedirs(os.path.join('build', 'src'))
+ with contexts.quiet():
+ dist.parse_command_line()
+ dist.run_commands()
+
+ # let's see if we got our egg link at the right place
+ [content] = os.listdir('dist')
+ assert re.match(r'foo-0.0.0-py[23].\d+.egg$', content)
+
+ @pytest.mark.xfail(
+ os.environ.get('PYTHONDONTWRITEBYTECODE', False),
+ reason="Byte code disabled",
+ )
+ @pytest.mark.usefixtures("user_override")
+ @pytest.mark.usefixtures("setup_context")
+ def test_exclude_source_files(self):
+ dist = Distribution(
+ dict(
+ script_name='setup.py',
+ script_args=['bdist_egg', '--exclude-source-files'],
+ py_modules=['hi'],
+ )
+ )
+ with contexts.quiet():
+ dist.parse_command_line()
+ dist.run_commands()
+ [dist_name] = os.listdir('dist')
+ dist_filename = os.path.join('dist', dist_name)
+ zip = zipfile.ZipFile(dist_filename)
+ names = list(zi.filename for zi in zip.filelist)
+ assert 'hi.pyc' in names
+ assert 'hi.py' not in names
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_wheel.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_wheel.py
new file mode 100644
index 00000000..2ab4e9cf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_bdist_wheel.py
@@ -0,0 +1,708 @@
+from __future__ import annotations
+
+import builtins
+import importlib
+import os.path
+import platform
+import shutil
+import stat
+import struct
+import sys
+import sysconfig
+from contextlib import suppress
+from inspect import cleandoc
+from zipfile import ZipFile
+
+import jaraco.path
+import pytest
+from packaging import tags
+
+import setuptools
+from setuptools.command.bdist_wheel import bdist_wheel, get_abi_tag
+from setuptools.dist import Distribution
+from setuptools.warnings import SetuptoolsDeprecationWarning
+
+from distutils.core import run_setup
+
+DEFAULT_FILES = {
+ "dummy_dist-1.0.dist-info/top_level.txt",
+ "dummy_dist-1.0.dist-info/METADATA",
+ "dummy_dist-1.0.dist-info/WHEEL",
+ "dummy_dist-1.0.dist-info/RECORD",
+}
+DEFAULT_LICENSE_FILES = {
+ "LICENSE",
+ "LICENSE.txt",
+ "LICENCE",
+ "LICENCE.txt",
+ "COPYING",
+ "COPYING.md",
+ "NOTICE",
+ "NOTICE.rst",
+ "AUTHORS",
+ "AUTHORS.txt",
+}
+OTHER_IGNORED_FILES = {
+ "LICENSE~",
+ "AUTHORS~",
+}
+SETUPPY_EXAMPLE = """\
+from setuptools import setup
+
+setup(
+ name='dummy_dist',
+ version='1.0',
+)
+"""
+
+
+EXAMPLES = {
+ "dummy-dist": {
+ "setup.py": SETUPPY_EXAMPLE,
+ "licenses_dir": {"DUMMYFILE": ""},
+ **dict.fromkeys(DEFAULT_LICENSE_FILES | OTHER_IGNORED_FILES, ""),
+ },
+ "simple-dist": {
+ "setup.py": cleandoc(
+ """
+ from setuptools import setup
+
+ setup(
+ name="simple.dist",
+ version="0.1",
+ description="A testing distribution \N{SNOWMAN}",
+ extras_require={"voting": ["beaglevote"]},
+ )
+ """
+ ),
+ "simpledist": "",
+ },
+ "complex-dist": {
+ "setup.py": cleandoc(
+ """
+ from setuptools import setup
+
+ setup(
+ name="complex-dist",
+ version="0.1",
+ description="Another testing distribution \N{SNOWMAN}",
+ long_description="Another testing distribution \N{SNOWMAN}",
+ author="Illustrious Author",
+ author_email="illustrious@example.org",
+ url="http://example.org/exemplary",
+ packages=["complexdist"],
+ setup_requires=["setuptools"],
+ install_requires=["quux", "splort"],
+ extras_require={"simple": ["simple.dist"]},
+ entry_points={
+ "console_scripts": [
+ "complex-dist=complexdist:main",
+ "complex-dist2=complexdist:main",
+ ],
+ },
+ )
+ """
+ ),
+ "complexdist": {"__init__.py": "def main(): return"},
+ },
+ "headers-dist": {
+ "setup.py": cleandoc(
+ """
+ from setuptools import setup
+
+ setup(
+ name="headers.dist",
+ version="0.1",
+ description="A distribution with headers",
+ headers=["header.h"],
+ )
+ """
+ ),
+ "headersdist.py": "",
+ "header.h": "",
+ },
+ "commasinfilenames-dist": {
+ "setup.py": cleandoc(
+ """
+ from setuptools import setup
+
+ setup(
+ name="testrepo",
+ version="0.1",
+ packages=["mypackage"],
+ description="A test package with commas in file names",
+ include_package_data=True,
+ package_data={"mypackage.data": ["*"]},
+ )
+ """
+ ),
+ "mypackage": {
+ "__init__.py": "",
+ "data": {"__init__.py": "", "1,2,3.txt": ""},
+ },
+ "testrepo-0.1.0": {
+ "mypackage": {"__init__.py": ""},
+ },
+ },
+ "unicode-dist": {
+ "setup.py": cleandoc(
+ """
+ from setuptools import setup
+
+ setup(
+ name="unicode.dist",
+ version="0.1",
+ description="A testing distribution \N{SNOWMAN}",
+ packages=["unicodedist"],
+ zip_safe=True,
+ )
+ """
+ ),
+ "unicodedist": {"__init__.py": "", "åäö_日本語.py": ""},
+ },
+ "utf8-metadata-dist": {
+ "setup.cfg": cleandoc(
+ """
+ [metadata]
+ name = utf8-metadata-dist
+ version = 42
+ author_email = "John X. Ãørçeč" <john@utf8.org>, Γαμα קּ 東 <gama@utf8.org>
+ long_description = file: README.rst
+ """
+ ),
+ "README.rst": "UTF-8 描述 説明",
+ },
+ "licenses-dist": {
+ "setup.cfg": cleandoc(
+ """
+ [metadata]
+ name = licenses-dist
+ version = 1.0
+ license_files = **/LICENSE
+ """
+ ),
+ "LICENSE": "",
+ "src": {
+ "vendor": {"LICENSE": ""},
+ },
+ },
+}
+
+
+if sys.platform != "win32":
+ # ABI3 extensions don't really work on Windows
+ EXAMPLES["abi3extension-dist"] = {
+ "setup.py": cleandoc(
+ """
+ from setuptools import Extension, setup
+
+ setup(
+ name="extension.dist",
+ version="0.1",
+ description="A testing distribution \N{SNOWMAN}",
+ ext_modules=[
+ Extension(
+ name="extension", sources=["extension.c"], py_limited_api=True
+ )
+ ],
+ )
+ """
+ ),
+ "setup.cfg": "[bdist_wheel]\npy_limited_api=cp32",
+ "extension.c": "#define Py_LIMITED_API 0x03020000\n#include <Python.h>",
+ }
+
+
+def bdist_wheel_cmd(**kwargs):
+ """Run command in the same process so that it is easier to collect coverage"""
+ dist_obj = (
+ run_setup("setup.py", stop_after="init")
+ if os.path.exists("setup.py")
+ else Distribution({"script_name": "%%build_meta%%"})
+ )
+ dist_obj.parse_config_files()
+ cmd = bdist_wheel(dist_obj)
+ for attr, value in kwargs.items():
+ setattr(cmd, attr, value)
+ cmd.finalize_options()
+ return cmd
+
+
+def mkexample(tmp_path_factory, name):
+ basedir = tmp_path_factory.mktemp(name)
+ jaraco.path.build(EXAMPLES[name], prefix=str(basedir))
+ return basedir
+
+
+@pytest.fixture(scope="session")
+def wheel_paths(tmp_path_factory):
+ build_base = tmp_path_factory.mktemp("build")
+ dist_dir = tmp_path_factory.mktemp("dist")
+ for name in EXAMPLES:
+ example_dir = mkexample(tmp_path_factory, name)
+ build_dir = build_base / name
+ with jaraco.path.DirectoryStack().context(example_dir):
+ bdist_wheel_cmd(bdist_dir=str(build_dir), dist_dir=str(dist_dir)).run()
+
+ return sorted(str(fname) for fname in dist_dir.glob("*.whl"))
+
+
+@pytest.fixture
+def dummy_dist(tmp_path_factory):
+ return mkexample(tmp_path_factory, "dummy-dist")
+
+
+@pytest.fixture
+def licenses_dist(tmp_path_factory):
+ return mkexample(tmp_path_factory, "licenses-dist")
+
+
+def test_no_scripts(wheel_paths):
+ """Make sure entry point scripts are not generated."""
+ path = next(path for path in wheel_paths if "complex_dist" in path)
+ for entry in ZipFile(path).infolist():
+ assert ".data/scripts/" not in entry.filename
+
+
+def test_unicode_record(wheel_paths):
+ path = next(path for path in wheel_paths if "unicode_dist" in path)
+ with ZipFile(path) as zf:
+ record = zf.read("unicode_dist-0.1.dist-info/RECORD")
+
+ assert "åäö_日本語.py".encode() in record
+
+
+UTF8_PKG_INFO = """\
+Metadata-Version: 2.1
+Name: helloworld
+Version: 42
+Author-email: "John X. Ãørçeč" <john@utf8.org>, Γαμα קּ 東 <gama@utf8.org>
+
+
+UTF-8 描述 説明
+"""
+
+
+def test_preserve_unicode_metadata(monkeypatch, tmp_path):
+ monkeypatch.chdir(tmp_path)
+ egginfo = tmp_path / "dummy_dist.egg-info"
+ distinfo = tmp_path / "dummy_dist.dist-info"
+
+ egginfo.mkdir()
+ (egginfo / "PKG-INFO").write_text(UTF8_PKG_INFO, encoding="utf-8")
+ (egginfo / "dependency_links.txt").touch()
+
+ class simpler_bdist_wheel(bdist_wheel):
+ """Avoid messing with setuptools/distutils internals"""
+
+ def __init__(self):
+ pass
+
+ @property
+ def license_paths(self):
+ return []
+
+ cmd_obj = simpler_bdist_wheel()
+ cmd_obj.egg2dist(egginfo, distinfo)
+
+ metadata = (distinfo / "METADATA").read_text(encoding="utf-8")
+ assert 'Author-email: "John X. Ãørçeč"' in metadata
+ assert "Γαμα קּ 東 " in metadata
+ assert "UTF-8 描述 説明" in metadata
+
+
+def test_licenses_default(dummy_dist, monkeypatch, tmp_path):
+ monkeypatch.chdir(dummy_dist)
+ bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()
+ with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:
+ license_files = {
+ "dummy_dist-1.0.dist-info/licenses/" + fname
+ for fname in DEFAULT_LICENSE_FILES
+ }
+ assert set(wf.namelist()) == DEFAULT_FILES | license_files
+
+
+def test_licenses_deprecated(dummy_dist, monkeypatch, tmp_path):
+ dummy_dist.joinpath("setup.cfg").write_text(
+ "[metadata]\nlicense_file=licenses_dir/DUMMYFILE", encoding="utf-8"
+ )
+ monkeypatch.chdir(dummy_dist)
+
+ bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()
+
+ with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:
+ license_files = {"dummy_dist-1.0.dist-info/licenses/licenses_dir/DUMMYFILE"}
+ assert set(wf.namelist()) == DEFAULT_FILES | license_files
+
+
+@pytest.mark.parametrize(
+ ("config_file", "config"),
+ [
+ ("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*\n LICENSE"),
+ ("setup.cfg", "[metadata]\nlicense_files=licenses_dir/*, LICENSE"),
+ (
+ "setup.py",
+ SETUPPY_EXAMPLE.replace(
+ ")", " license_files=['licenses_dir/DUMMYFILE', 'LICENSE'])"
+ ),
+ ),
+ ],
+)
+def test_licenses_override(dummy_dist, monkeypatch, tmp_path, config_file, config):
+ dummy_dist.joinpath(config_file).write_text(config, encoding="utf-8")
+ monkeypatch.chdir(dummy_dist)
+ bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()
+ with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:
+ license_files = {
+ "dummy_dist-1.0.dist-info/licenses/" + fname
+ for fname in {"licenses_dir/DUMMYFILE", "LICENSE"}
+ }
+ assert set(wf.namelist()) == DEFAULT_FILES | license_files
+ metadata = wf.read("dummy_dist-1.0.dist-info/METADATA").decode("utf8")
+ assert "License-File: licenses_dir/DUMMYFILE" in metadata
+ assert "License-File: LICENSE" in metadata
+
+
+def test_licenses_preserve_folder_structure(licenses_dist, monkeypatch, tmp_path):
+ monkeypatch.chdir(licenses_dist)
+ bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()
+ print(os.listdir("dist"))
+ with ZipFile("dist/licenses_dist-1.0-py3-none-any.whl") as wf:
+ default_files = {name.replace("dummy_", "licenses_") for name in DEFAULT_FILES}
+ license_files = {
+ "licenses_dist-1.0.dist-info/licenses/LICENSE",
+ "licenses_dist-1.0.dist-info/licenses/src/vendor/LICENSE",
+ }
+ assert set(wf.namelist()) == default_files | license_files
+ metadata = wf.read("licenses_dist-1.0.dist-info/METADATA").decode("utf8")
+ assert "License-File: src/vendor/LICENSE" in metadata
+ assert "License-File: LICENSE" in metadata
+
+
+def test_licenses_disabled(dummy_dist, monkeypatch, tmp_path):
+ dummy_dist.joinpath("setup.cfg").write_text(
+ "[metadata]\nlicense_files=\n", encoding="utf-8"
+ )
+ monkeypatch.chdir(dummy_dist)
+ bdist_wheel_cmd(bdist_dir=str(tmp_path)).run()
+ with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:
+ assert set(wf.namelist()) == DEFAULT_FILES
+
+
+def test_build_number(dummy_dist, monkeypatch, tmp_path):
+ monkeypatch.chdir(dummy_dist)
+ bdist_wheel_cmd(bdist_dir=str(tmp_path), build_number="2").run()
+ with ZipFile("dist/dummy_dist-1.0-2-py3-none-any.whl") as wf:
+ filenames = set(wf.namelist())
+ assert "dummy_dist-1.0.dist-info/RECORD" in filenames
+ assert "dummy_dist-1.0.dist-info/METADATA" in filenames
+
+
+def test_universal_deprecated(dummy_dist, monkeypatch, tmp_path):
+ monkeypatch.chdir(dummy_dist)
+ with pytest.warns(SetuptoolsDeprecationWarning, match=".*universal is deprecated"):
+ bdist_wheel_cmd(bdist_dir=str(tmp_path), universal=True).run()
+
+ # For now we still respect the option
+ assert os.path.exists("dist/dummy_dist-1.0-py2.py3-none-any.whl")
+
+
+EXTENSION_EXAMPLE = """\
+#include <Python.h>
+
+static PyMethodDef methods[] = {
+ { NULL, NULL, 0, NULL }
+};
+
+static struct PyModuleDef module_def = {
+ PyModuleDef_HEAD_INIT,
+ "extension",
+ "Dummy extension module",
+ -1,
+ methods
+};
+
+PyMODINIT_FUNC PyInit_extension(void) {
+ return PyModule_Create(&module_def);
+}
+"""
+EXTENSION_SETUPPY = """\
+from __future__ import annotations
+
+from setuptools import Extension, setup
+
+setup(
+ name="extension.dist",
+ version="0.1",
+ description="A testing distribution \N{SNOWMAN}",
+ ext_modules=[Extension(name="extension", sources=["extension.c"])],
+)
+"""
+
+
+@pytest.mark.filterwarnings(
+ "once:Config variable '.*' is unset.*, Python ABI tag may be incorrect"
+)
+def test_limited_abi(monkeypatch, tmp_path, tmp_path_factory):
+ """Test that building a binary wheel with the limited ABI works."""
+ source_dir = tmp_path_factory.mktemp("extension_dist")
+ (source_dir / "setup.py").write_text(EXTENSION_SETUPPY, encoding="utf-8")
+ (source_dir / "extension.c").write_text(EXTENSION_EXAMPLE, encoding="utf-8")
+ build_dir = tmp_path.joinpath("build")
+ dist_dir = tmp_path.joinpath("dist")
+ monkeypatch.chdir(source_dir)
+ bdist_wheel_cmd(bdist_dir=str(build_dir), dist_dir=str(dist_dir)).run()
+
+
+def test_build_from_readonly_tree(dummy_dist, monkeypatch, tmp_path):
+ basedir = str(tmp_path.joinpath("dummy"))
+ shutil.copytree(str(dummy_dist), basedir)
+ monkeypatch.chdir(basedir)
+
+ # Make the tree read-only
+ for root, _dirs, files in os.walk(basedir):
+ for fname in files:
+ os.chmod(os.path.join(root, fname), stat.S_IREAD)
+
+ bdist_wheel_cmd().run()
+
+
+@pytest.mark.parametrize(
+ ("option", "compress_type"),
+ list(bdist_wheel.supported_compressions.items()),
+ ids=list(bdist_wheel.supported_compressions),
+)
+def test_compression(dummy_dist, monkeypatch, tmp_path, option, compress_type):
+ monkeypatch.chdir(dummy_dist)
+ bdist_wheel_cmd(bdist_dir=str(tmp_path), compression=option).run()
+ with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:
+ filenames = set(wf.namelist())
+ assert "dummy_dist-1.0.dist-info/RECORD" in filenames
+ assert "dummy_dist-1.0.dist-info/METADATA" in filenames
+ for zinfo in wf.filelist:
+ assert zinfo.compress_type == compress_type
+
+
+def test_wheelfile_line_endings(wheel_paths):
+ for path in wheel_paths:
+ with ZipFile(path) as wf:
+ wheelfile = next(fn for fn in wf.filelist if fn.filename.endswith("WHEEL"))
+ wheelfile_contents = wf.read(wheelfile)
+ assert b"\r" not in wheelfile_contents
+
+
+def test_unix_epoch_timestamps(dummy_dist, monkeypatch, tmp_path):
+ monkeypatch.setenv("SOURCE_DATE_EPOCH", "0")
+ monkeypatch.chdir(dummy_dist)
+ bdist_wheel_cmd(bdist_dir=str(tmp_path), build_number="2a").run()
+ with ZipFile("dist/dummy_dist-1.0-2a-py3-none-any.whl") as wf:
+ for zinfo in wf.filelist:
+ assert zinfo.date_time >= (1980, 1, 1, 0, 0, 0) # min epoch is used
+
+
+def test_get_abi_tag_windows(monkeypatch):
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "cp")
+ monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "cp313-win_amd64")
+ assert get_abi_tag() == "cp313"
+ monkeypatch.setattr(sys, "gettotalrefcount", lambda: 1, False)
+ assert get_abi_tag() == "cp313d"
+ monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "cp313t-win_amd64")
+ assert get_abi_tag() == "cp313td"
+ monkeypatch.delattr(sys, "gettotalrefcount")
+ assert get_abi_tag() == "cp313t"
+
+
+def test_get_abi_tag_pypy_old(monkeypatch):
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "pp")
+ monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy36-pp73")
+ assert get_abi_tag() == "pypy36_pp73"
+
+
+def test_get_abi_tag_pypy_new(monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "pypy37-pp73-darwin")
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "pp")
+ assert get_abi_tag() == "pypy37_pp73"
+
+
+def test_get_abi_tag_graalpy(monkeypatch):
+ monkeypatch.setattr(
+ sysconfig, "get_config_var", lambda x: "graalpy231-310-native-x86_64-linux"
+ )
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "graalpy")
+ assert get_abi_tag() == "graalpy231_310_native"
+
+
+def test_get_abi_tag_fallback(monkeypatch):
+ monkeypatch.setattr(sysconfig, "get_config_var", lambda x: "unknown-python-310")
+ monkeypatch.setattr(tags, "interpreter_name", lambda: "unknown-python")
+ assert get_abi_tag() == "unknown_python_310"
+
+
+def test_platform_with_space(dummy_dist, monkeypatch):
+ """Ensure building on platforms with a space in the name succeed."""
+ monkeypatch.chdir(dummy_dist)
+ bdist_wheel_cmd(plat_name="isilon onefs").run()
+
+
+def test_data_dir_with_tag_build(monkeypatch, tmp_path):
+ """
+ Setuptools allow authors to set PEP 440's local version segments
+ using ``egg_info.tag_build``. This should be reflected not only in the
+ ``.whl`` file name, but also in the ``.dist-info`` and ``.data`` dirs.
+ See pypa/setuptools#3997.
+ """
+ monkeypatch.chdir(tmp_path)
+ files = {
+ "setup.py": """
+ from setuptools import setup
+ setup(headers=["hello.h"])
+ """,
+ "setup.cfg": """
+ [metadata]
+ name = test
+ version = 1.0
+
+ [options.data_files]
+ hello/world = file.txt
+
+ [egg_info]
+ tag_build = +what
+ tag_date = 0
+ """,
+ "file.txt": "",
+ "hello.h": "",
+ }
+ for file, content in files.items():
+ with open(file, "w", encoding="utf-8") as fh:
+ fh.write(cleandoc(content))
+
+ bdist_wheel_cmd().run()
+
+ # Ensure .whl, .dist-info and .data contain the local segment
+ wheel_path = "dist/test-1.0+what-py3-none-any.whl"
+ assert os.path.exists(wheel_path)
+ entries = set(ZipFile(wheel_path).namelist())
+ for expected in (
+ "test-1.0+what.data/headers/hello.h",
+ "test-1.0+what.data/data/hello/world/file.txt",
+ "test-1.0+what.dist-info/METADATA",
+ "test-1.0+what.dist-info/WHEEL",
+ ):
+ assert expected in entries
+
+ for not_expected in (
+ "test.data/headers/hello.h",
+ "test-1.0.data/data/hello/world/file.txt",
+ "test.dist-info/METADATA",
+ "test-1.0.dist-info/WHEEL",
+ ):
+ assert not_expected not in entries
+
+
+@pytest.mark.parametrize(
+ ("reported", "expected"),
+ [("linux-x86_64", "linux_i686"), ("linux-aarch64", "linux_armv7l")],
+)
+@pytest.mark.skipif(
+ platform.system() != "Linux", reason="Only makes sense to test on Linux"
+)
+def test_platform_linux32(reported, expected, monkeypatch):
+ monkeypatch.setattr(struct, "calcsize", lambda x: 4)
+ dist = setuptools.Distribution()
+ cmd = bdist_wheel(dist)
+ cmd.plat_name = reported
+ cmd.root_is_pure = False
+ _, _, actual = cmd.get_tag()
+ assert actual == expected
+
+
+def test_no_ctypes(monkeypatch) -> None:
+ def _fake_import(name: str, *args, **kwargs):
+ if name == "ctypes":
+ raise ModuleNotFoundError(f"No module named {name}")
+
+ return importlib.__import__(name, *args, **kwargs)
+
+ with suppress(KeyError):
+ monkeypatch.delitem(sys.modules, "wheel.macosx_libfile")
+
+ # Install an importer shim that refuses to load ctypes
+ monkeypatch.setattr(builtins, "__import__", _fake_import)
+ with pytest.raises(ModuleNotFoundError, match="No module named ctypes"):
+ import wheel.macosx_libfile # noqa: F401
+
+ # Unload and reimport the bdist_wheel command module to make sure it won't try to
+ # import ctypes
+ monkeypatch.delitem(sys.modules, "setuptools.command.bdist_wheel")
+
+ import setuptools.command.bdist_wheel # noqa: F401
+
+
+def test_dist_info_provided(dummy_dist, monkeypatch, tmp_path):
+ monkeypatch.chdir(dummy_dist)
+ distinfo = tmp_path / "dummy_dist.dist-info"
+
+ distinfo.mkdir()
+ (distinfo / "METADATA").write_text("name: helloworld", encoding="utf-8")
+
+ # We don't control the metadata. According to PEP-517, "The hook MAY also
+ # create other files inside this directory, and a build frontend MUST
+ # preserve".
+ (distinfo / "FOO").write_text("bar", encoding="utf-8")
+
+ bdist_wheel_cmd(bdist_dir=str(tmp_path), dist_info_dir=str(distinfo)).run()
+ expected = {
+ "dummy_dist-1.0.dist-info/FOO",
+ "dummy_dist-1.0.dist-info/RECORD",
+ }
+ with ZipFile("dist/dummy_dist-1.0-py3-none-any.whl") as wf:
+ files_found = set(wf.namelist())
+ # Check that all expected files are there.
+ assert expected - files_found == set()
+ # Make sure there is no accidental egg-info bleeding into the wheel.
+ assert not [path for path in files_found if 'egg-info' in str(path)]
+
+
+def test_allow_grace_period_parent_directory_license(monkeypatch, tmp_path):
+ # Motivation: https://github.com/pypa/setuptools/issues/4892
+ # TODO: Remove this test after deprecation period is over
+ files = {
+ "LICENSE.txt": "parent license", # <---- the license files are outside
+ "NOTICE.txt": "parent notice",
+ "python": {
+ "pyproject.toml": cleandoc(
+ """
+ [project]
+ name = "test-proj"
+ dynamic = ["version"] # <---- testing dynamic will not break
+ [tool.setuptools.dynamic]
+ version.file = "VERSION"
+ """
+ ),
+ "setup.cfg": cleandoc(
+ """
+ [metadata]
+ license_files =
+ ../LICENSE.txt
+ ../NOTICE.txt
+ """
+ ),
+ "VERSION": "42",
+ },
+ }
+ jaraco.path.build(files, prefix=str(tmp_path))
+ monkeypatch.chdir(tmp_path / "python")
+ msg = "Pattern '../.*.txt' cannot contain '..'"
+ with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
+ bdist_wheel_cmd().run()
+ with ZipFile("dist/test_proj-42-py3-none-any.whl") as wf:
+ files_found = set(wf.namelist())
+ expected_files = {
+ "test_proj-42.dist-info/licenses/LICENSE.txt",
+ "test_proj-42.dist-info/licenses/NOTICE.txt",
+ }
+ assert expected_files <= files_found
+
+ metadata = wf.read("test_proj-42.dist-info/METADATA").decode("utf8")
+ assert "License-File: LICENSE.txt" in metadata
+ assert "License-File: NOTICE.txt" in metadata
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_build.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build.py
new file mode 100644
index 00000000..f0f1d9dc
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build.py
@@ -0,0 +1,33 @@
+from setuptools import Command
+from setuptools.command.build import build
+from setuptools.dist import Distribution
+
+
+def test_distribution_gives_setuptools_build_obj(tmpdir_cwd):
+ """
+ Check that the setuptools Distribution uses the
+ setuptools specific build object.
+ """
+
+ dist = Distribution(
+ dict(
+ script_name='setup.py',
+ script_args=['build'],
+ packages=[],
+ package_data={'': ['path/*']},
+ )
+ )
+ assert isinstance(dist.get_command_obj("build"), build)
+
+
+class Subcommand(Command):
+ """Dummy command to be used in tests"""
+
+ def initialize_options(self):
+ pass
+
+ def finalize_options(self):
+ pass
+
+ def run(self):
+ raise NotImplementedError("just to check if the command runs")
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_clib.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_clib.py
new file mode 100644
index 00000000..b5315df4
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_clib.py
@@ -0,0 +1,84 @@
+import random
+from unittest import mock
+
+import pytest
+
+from setuptools.command.build_clib import build_clib
+from setuptools.dist import Distribution
+
+from distutils.errors import DistutilsSetupError
+
+
+class TestBuildCLib:
+ @mock.patch('setuptools.command.build_clib.newer_pairwise_group')
+ def test_build_libraries(self, mock_newer):
+ dist = Distribution()
+ cmd = build_clib(dist)
+
+ # this will be a long section, just making sure all
+ # exceptions are properly raised
+ libs = [('example', {'sources': 'broken.c'})]
+ with pytest.raises(DistutilsSetupError):
+ cmd.build_libraries(libs)
+
+ obj_deps = 'some_string'
+ libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})]
+ with pytest.raises(DistutilsSetupError):
+ cmd.build_libraries(libs)
+
+ obj_deps = {'': ''}
+ libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})]
+ with pytest.raises(DistutilsSetupError):
+ cmd.build_libraries(libs)
+
+ obj_deps = {'source.c': ''}
+ libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})]
+ with pytest.raises(DistutilsSetupError):
+ cmd.build_libraries(libs)
+
+ # with that out of the way, let's see if the crude dependency
+ # system works
+ cmd.compiler = mock.MagicMock(spec=cmd.compiler)
+ mock_newer.return_value = ([], [])
+
+ obj_deps = {'': ('global.h',), 'example.c': ('example.h',)}
+ libs = [('example', {'sources': ['example.c'], 'obj_deps': obj_deps})]
+
+ cmd.build_libraries(libs)
+ assert [['example.c', 'global.h', 'example.h']] in mock_newer.call_args[0]
+ assert not cmd.compiler.compile.called
+ assert cmd.compiler.create_static_lib.call_count == 1
+
+ # reset the call numbers so we can test again
+ cmd.compiler.reset_mock()
+
+ mock_newer.return_value = '' # anything as long as it's not ([],[])
+ cmd.build_libraries(libs)
+ assert cmd.compiler.compile.call_count == 1
+ assert cmd.compiler.create_static_lib.call_count == 1
+
+ @mock.patch('setuptools.command.build_clib.newer_pairwise_group')
+ def test_build_libraries_reproducible(self, mock_newer):
+ dist = Distribution()
+ cmd = build_clib(dist)
+
+ # with that out of the way, let's see if the crude dependency
+ # system works
+ cmd.compiler = mock.MagicMock(spec=cmd.compiler)
+ mock_newer.return_value = ([], [])
+
+ original_sources = ['a-example.c', 'example.c']
+ sources = original_sources
+
+ obj_deps = {'': ('global.h',), 'example.c': ('example.h',)}
+ libs = [('example', {'sources': sources, 'obj_deps': obj_deps})]
+
+ cmd.build_libraries(libs)
+ computed_call_args = mock_newer.call_args[0]
+
+ while sources == original_sources:
+ sources = random.sample(original_sources, len(original_sources))
+ libs = [('example', {'sources': sources, 'obj_deps': obj_deps})]
+
+ cmd.build_libraries(libs)
+ assert computed_call_args == mock_newer.call_args[0]
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_ext.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_ext.py
new file mode 100644
index 00000000..c7b60ac3
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_ext.py
@@ -0,0 +1,293 @@
+from __future__ import annotations
+
+import os
+import sys
+from importlib.util import cache_from_source as _compiled_file_name
+
+import pytest
+from jaraco import path
+
+from setuptools.command.build_ext import build_ext, get_abi3_suffix
+from setuptools.dist import Distribution
+from setuptools.errors import CompileError
+from setuptools.extension import Extension
+
+from . import environment
+from .textwrap import DALS
+
+import distutils.command.build_ext as orig
+from distutils.sysconfig import get_config_var
+
+IS_PYPY = '__pypy__' in sys.builtin_module_names
+
+
+class TestBuildExt:
+ def test_get_ext_filename(self):
+ """
+ Setuptools needs to give back the same
+ result as distutils, even if the fullname
+ is not in ext_map.
+ """
+ dist = Distribution()
+ cmd = build_ext(dist)
+ cmd.ext_map['foo/bar'] = ''
+ res = cmd.get_ext_filename('foo')
+ wanted = orig.build_ext.get_ext_filename(cmd, 'foo')
+ assert res == wanted
+
+ def test_abi3_filename(self):
+ """
+ Filename needs to be loadable by several versions
+ of Python 3 if 'is_abi3' is truthy on Extension()
+ """
+ print(get_abi3_suffix())
+
+ extension = Extension('spam.eggs', ['eggs.c'], py_limited_api=True)
+ dist = Distribution(dict(ext_modules=[extension]))
+ cmd = build_ext(dist)
+ cmd.finalize_options()
+ assert 'spam.eggs' in cmd.ext_map
+ res = cmd.get_ext_filename('spam.eggs')
+
+ if not get_abi3_suffix():
+ assert res.endswith(get_config_var('EXT_SUFFIX'))
+ elif sys.platform == 'win32':
+ assert res.endswith('eggs.pyd')
+ else:
+ assert 'abi3' in res
+
+ def test_ext_suffix_override(self):
+ """
+ SETUPTOOLS_EXT_SUFFIX variable always overrides
+ default extension options.
+ """
+ dist = Distribution()
+ cmd = build_ext(dist)
+ cmd.ext_map['for_abi3'] = ext = Extension(
+ 'for_abi3',
+ ['s.c'],
+ # Override shouldn't affect abi3 modules
+ py_limited_api=True,
+ )
+ # Mock value needed to pass tests
+ ext._links_to_dynamic = False
+
+ if not IS_PYPY:
+ expect = cmd.get_ext_filename('for_abi3')
+ else:
+ # PyPy builds do not use ABI3 tag, so they will
+ # also get the overridden suffix.
+ expect = 'for_abi3.test-suffix'
+
+ try:
+ os.environ['SETUPTOOLS_EXT_SUFFIX'] = '.test-suffix'
+ res = cmd.get_ext_filename('normal')
+ assert 'normal.test-suffix' == res
+ res = cmd.get_ext_filename('for_abi3')
+ assert expect == res
+ finally:
+ del os.environ['SETUPTOOLS_EXT_SUFFIX']
+
+ def dist_with_example(self):
+ files = {
+ "src": {"mypkg": {"subpkg": {"ext2.c": ""}}},
+ "c-extensions": {"ext1": {"main.c": ""}},
+ }
+
+ ext1 = Extension("mypkg.ext1", ["c-extensions/ext1/main.c"])
+ ext2 = Extension("mypkg.subpkg.ext2", ["src/mypkg/subpkg/ext2.c"])
+ ext3 = Extension("ext3", ["c-extension/ext3.c"])
+
+ path.build(files)
+ return Distribution({
+ "script_name": "%test%",
+ "ext_modules": [ext1, ext2, ext3],
+ "package_dir": {"": "src"},
+ })
+
+ def test_get_outputs(self, tmpdir_cwd, monkeypatch):
+ monkeypatch.setenv('SETUPTOOLS_EXT_SUFFIX', '.mp3') # make test OS-independent
+ monkeypatch.setattr('setuptools.command.build_ext.use_stubs', False)
+ dist = self.dist_with_example()
+
+ # Regular build: get_outputs not empty, but get_output_mappings is empty
+ build_ext = dist.get_command_obj("build_ext")
+ build_ext.editable_mode = False
+ build_ext.ensure_finalized()
+ build_lib = build_ext.build_lib.replace(os.sep, "/")
+ outputs = [x.replace(os.sep, "/") for x in build_ext.get_outputs()]
+ assert outputs == [
+ f"{build_lib}/ext3.mp3",
+ f"{build_lib}/mypkg/ext1.mp3",
+ f"{build_lib}/mypkg/subpkg/ext2.mp3",
+ ]
+ assert build_ext.get_output_mapping() == {}
+
+ # Editable build: get_output_mappings should contain everything in get_outputs
+ dist.reinitialize_command("build_ext")
+ build_ext.editable_mode = True
+ build_ext.ensure_finalized()
+ mapping = {
+ k.replace(os.sep, "/"): v.replace(os.sep, "/")
+ for k, v in build_ext.get_output_mapping().items()
+ }
+ assert mapping == {
+ f"{build_lib}/ext3.mp3": "src/ext3.mp3",
+ f"{build_lib}/mypkg/ext1.mp3": "src/mypkg/ext1.mp3",
+ f"{build_lib}/mypkg/subpkg/ext2.mp3": "src/mypkg/subpkg/ext2.mp3",
+ }
+
+ def test_get_output_mapping_with_stub(self, tmpdir_cwd, monkeypatch):
+ monkeypatch.setenv('SETUPTOOLS_EXT_SUFFIX', '.mp3') # make test OS-independent
+ monkeypatch.setattr('setuptools.command.build_ext.use_stubs', True)
+ dist = self.dist_with_example()
+
+ # Editable build should create compiled stubs (.pyc files only, no .py)
+ build_ext = dist.get_command_obj("build_ext")
+ build_ext.editable_mode = True
+ build_ext.ensure_finalized()
+ for ext in build_ext.extensions:
+ monkeypatch.setattr(ext, "_needs_stub", True)
+
+ build_lib = build_ext.build_lib.replace(os.sep, "/")
+ mapping = {
+ k.replace(os.sep, "/"): v.replace(os.sep, "/")
+ for k, v in build_ext.get_output_mapping().items()
+ }
+
+ def C(file):
+ """Make it possible to do comparisons and tests in a OS-independent way"""
+ return _compiled_file_name(file).replace(os.sep, "/")
+
+ assert mapping == {
+ C(f"{build_lib}/ext3.py"): C("src/ext3.py"),
+ f"{build_lib}/ext3.mp3": "src/ext3.mp3",
+ C(f"{build_lib}/mypkg/ext1.py"): C("src/mypkg/ext1.py"),
+ f"{build_lib}/mypkg/ext1.mp3": "src/mypkg/ext1.mp3",
+ C(f"{build_lib}/mypkg/subpkg/ext2.py"): C("src/mypkg/subpkg/ext2.py"),
+ f"{build_lib}/mypkg/subpkg/ext2.mp3": "src/mypkg/subpkg/ext2.mp3",
+ }
+
+ # Ensure only the compiled stubs are present not the raw .py stub
+ assert f"{build_lib}/mypkg/ext1.py" not in mapping
+ assert f"{build_lib}/mypkg/subpkg/ext2.py" not in mapping
+
+ # Visualize what the cached stub files look like
+ example_stub = C(f"{build_lib}/mypkg/ext1.py")
+ assert example_stub in mapping
+ assert example_stub.startswith(f"{build_lib}/mypkg/__pycache__/ext1")
+ assert example_stub.endswith(".pyc")
+
+
+class TestBuildExtInplace:
+ def get_build_ext_cmd(self, optional: bool, **opts) -> build_ext:
+ files: dict[str, str | dict[str, dict[str, str]]] = {
+ "eggs.c": "#include missingheader.h\n",
+ ".build": {"lib": {}, "tmp": {}},
+ }
+ path.build(files)
+ extension = Extension('spam.eggs', ['eggs.c'], optional=optional)
+ dist = Distribution(dict(ext_modules=[extension]))
+ dist.script_name = 'setup.py'
+ cmd = build_ext(dist)
+ vars(cmd).update(build_lib=".build/lib", build_temp=".build/tmp", **opts)
+ cmd.ensure_finalized()
+ return cmd
+
+ def get_log_messages(self, caplog, capsys):
+ """
+ Historically, distutils "logged" by printing to sys.std*.
+ Later versions adopted the logging framework. Grab
+ messages regardless of how they were captured.
+ """
+ std = capsys.readouterr()
+ return std.out.splitlines() + std.err.splitlines() + caplog.messages
+
+ def test_optional(self, tmpdir_cwd, caplog, capsys):
+ """
+ If optional extensions fail to build, setuptools should show the error
+ in the logs but not fail to build
+ """
+ cmd = self.get_build_ext_cmd(optional=True, inplace=True)
+ cmd.run()
+ assert any(
+ 'build_ext: building extension "spam.eggs" failed'
+ for msg in self.get_log_messages(caplog, capsys)
+ )
+ # No compile error exception should be raised
+
+ def test_non_optional(self, tmpdir_cwd):
+ # Non-optional extensions should raise an exception
+ cmd = self.get_build_ext_cmd(optional=False, inplace=True)
+ with pytest.raises(CompileError):
+ cmd.run()
+
+
+def test_build_ext_config_handling(tmpdir_cwd):
+ files = {
+ 'setup.py': DALS(
+ """
+ from setuptools import Extension, setup
+ setup(
+ name='foo',
+ version='0.0.0',
+ ext_modules=[Extension('foo', ['foo.c'])],
+ )
+ """
+ ),
+ 'foo.c': DALS(
+ """
+ #include "Python.h"
+
+ #if PY_MAJOR_VERSION >= 3
+
+ static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "foo",
+ NULL,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL
+ };
+
+ #define INITERROR return NULL
+
+ PyMODINIT_FUNC PyInit_foo(void)
+
+ #else
+
+ #define INITERROR return
+
+ void initfoo(void)
+
+ #endif
+ {
+ #if PY_MAJOR_VERSION >= 3
+ PyObject *module = PyModule_Create(&moduledef);
+ #else
+ PyObject *module = Py_InitModule("extension", NULL);
+ #endif
+ if (module == NULL)
+ INITERROR;
+ #if PY_MAJOR_VERSION >= 3
+ return module;
+ #endif
+ }
+ """
+ ),
+ 'setup.cfg': DALS(
+ """
+ [build]
+ build_base = foo_build
+ """
+ ),
+ }
+ path.build(files)
+ code, (stdout, stderr) = environment.run_setup_py(
+ cmd=['build'],
+ data_stream=(0, 2),
+ )
+ assert code == 0, f'\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}'
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_meta.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_meta.py
new file mode 100644
index 00000000..624bba86
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_meta.py
@@ -0,0 +1,983 @@
+import contextlib
+import importlib
+import os
+import re
+import shutil
+import signal
+import sys
+import tarfile
+import warnings
+from concurrent import futures
+from pathlib import Path
+from typing import Any, Callable
+from zipfile import ZipFile
+
+import pytest
+from jaraco import path
+from packaging.requirements import Requirement
+
+from setuptools.warnings import SetuptoolsDeprecationWarning
+
+from .textwrap import DALS
+
+SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
+
+
+TIMEOUT = int(os.getenv("TIMEOUT_BACKEND_TEST", "180")) # in seconds
+IS_PYPY = '__pypy__' in sys.builtin_module_names
+
+
+pytestmark = pytest.mark.skipif(
+ sys.platform == "win32" and IS_PYPY,
+ reason="The combination of PyPy + Windows + pytest-xdist + ProcessPoolExecutor "
+ "is flaky and problematic",
+)
+
+
+class BuildBackendBase:
+ def __init__(self, cwd='.', env=None, backend_name='setuptools.build_meta'):
+ self.cwd = cwd
+ self.env = env or {}
+ self.backend_name = backend_name
+
+
+class BuildBackend(BuildBackendBase):
+ """PEP 517 Build Backend"""
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.pool = futures.ProcessPoolExecutor(max_workers=1)
+
+ def __getattr__(self, name: str) -> Callable[..., Any]:
+ """Handles arbitrary function invocations on the build backend."""
+
+ def method(*args, **kw):
+ root = os.path.abspath(self.cwd)
+ caller = BuildBackendCaller(root, self.env, self.backend_name)
+ pid = None
+ try:
+ pid = self.pool.submit(os.getpid).result(TIMEOUT)
+ return self.pool.submit(caller, name, *args, **kw).result(TIMEOUT)
+ except futures.TimeoutError:
+ self.pool.shutdown(wait=False) # doesn't stop already running processes
+ self._kill(pid)
+ pytest.xfail(f"Backend did not respond before timeout ({TIMEOUT} s)")
+ except (futures.process.BrokenProcessPool, MemoryError, OSError):
+ if IS_PYPY:
+ pytest.xfail("PyPy frequently fails tests with ProcessPoolExector")
+ raise
+
+ return method
+
+ def _kill(self, pid):
+ if pid is None:
+ return
+ with contextlib.suppress(ProcessLookupError, OSError):
+ os.kill(pid, signal.SIGTERM if os.name == "nt" else signal.SIGKILL)
+
+
+class BuildBackendCaller(BuildBackendBase):
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+
+ (self.backend_name, _, self.backend_obj) = self.backend_name.partition(':')
+
+ def __call__(self, name, *args, **kw):
+ """Handles arbitrary function invocations on the build backend."""
+ os.chdir(self.cwd)
+ os.environ.update(self.env)
+ mod = importlib.import_module(self.backend_name)
+
+ if self.backend_obj:
+ backend = getattr(mod, self.backend_obj)
+ else:
+ backend = mod
+
+ return getattr(backend, name)(*args, **kw)
+
+
+defns = [
+ { # simple setup.py script
+ 'setup.py': DALS(
+ """
+ __import__('setuptools').setup(
+ name='foo',
+ version='0.0.0',
+ py_modules=['hello'],
+ setup_requires=['six'],
+ )
+ """
+ ),
+ 'hello.py': DALS(
+ """
+ def run():
+ print('hello')
+ """
+ ),
+ },
+ { # setup.py that relies on __name__
+ 'setup.py': DALS(
+ """
+ assert __name__ == '__main__'
+ __import__('setuptools').setup(
+ name='foo',
+ version='0.0.0',
+ py_modules=['hello'],
+ setup_requires=['six'],
+ )
+ """
+ ),
+ 'hello.py': DALS(
+ """
+ def run():
+ print('hello')
+ """
+ ),
+ },
+ { # setup.py script that runs arbitrary code
+ 'setup.py': DALS(
+ """
+ variable = True
+ def function():
+ return variable
+ assert variable
+ __import__('setuptools').setup(
+ name='foo',
+ version='0.0.0',
+ py_modules=['hello'],
+ setup_requires=['six'],
+ )
+ """
+ ),
+ 'hello.py': DALS(
+ """
+ def run():
+ print('hello')
+ """
+ ),
+ },
+ { # setup.py script that constructs temp files to be included in the distribution
+ 'setup.py': DALS(
+ """
+ # Some packages construct files on the fly, include them in the package,
+ # and immediately remove them after `setup()` (e.g. pybind11==2.9.1).
+ # Therefore, we cannot use `distutils.core.run_setup(..., stop_after=...)`
+ # to obtain a distribution object first, and then run the distutils
+ # commands later, because these files will be removed in the meantime.
+
+ with open('world.py', 'w', encoding="utf-8") as f:
+ f.write('x = 42')
+
+ try:
+ __import__('setuptools').setup(
+ name='foo',
+ version='0.0.0',
+ py_modules=['world'],
+ setup_requires=['six'],
+ )
+ finally:
+ # Some packages will clean temporary files
+ __import__('os').unlink('world.py')
+ """
+ ),
+ },
+ { # setup.cfg only
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ name = foo
+ version = 0.0.0
+
+ [options]
+ py_modules=hello
+ setup_requires=six
+ """
+ ),
+ 'hello.py': DALS(
+ """
+ def run():
+ print('hello')
+ """
+ ),
+ },
+ { # setup.cfg and setup.py
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ name = foo
+ version = 0.0.0
+
+ [options]
+ py_modules=hello
+ setup_requires=six
+ """
+ ),
+ 'setup.py': "__import__('setuptools').setup()",
+ 'hello.py': DALS(
+ """
+ def run():
+ print('hello')
+ """
+ ),
+ },
+]
+
+
+class TestBuildMetaBackend:
+ backend_name = 'setuptools.build_meta'
+
+ def get_build_backend(self):
+ return BuildBackend(backend_name=self.backend_name)
+
+ @pytest.fixture(params=defns)
+ def build_backend(self, tmpdir, request):
+ path.build(request.param, prefix=str(tmpdir))
+ with tmpdir.as_cwd():
+ yield self.get_build_backend()
+
+ def test_get_requires_for_build_wheel(self, build_backend):
+ actual = build_backend.get_requires_for_build_wheel()
+ expected = ['six']
+ assert sorted(actual) == sorted(expected)
+
+ def test_get_requires_for_build_sdist(self, build_backend):
+ actual = build_backend.get_requires_for_build_sdist()
+ expected = ['six']
+ assert sorted(actual) == sorted(expected)
+
+ def test_build_wheel(self, build_backend):
+ dist_dir = os.path.abspath('pip-wheel')
+ os.makedirs(dist_dir)
+ wheel_name = build_backend.build_wheel(dist_dir)
+
+ wheel_file = os.path.join(dist_dir, wheel_name)
+ assert os.path.isfile(wheel_file)
+
+ # Temporary files should be removed
+ assert not os.path.isfile('world.py')
+
+ with ZipFile(wheel_file) as zipfile:
+ wheel_contents = set(zipfile.namelist())
+
+ # Each one of the examples have a single module
+ # that should be included in the distribution
+ python_scripts = (f for f in wheel_contents if f.endswith('.py'))
+ modules = [f for f in python_scripts if not f.endswith('setup.py')]
+ assert len(modules) == 1
+
+ @pytest.mark.parametrize('build_type', ('wheel', 'sdist'))
+ def test_build_with_existing_file_present(self, build_type, tmpdir_cwd):
+ # Building a sdist/wheel should still succeed if there's
+ # already a sdist/wheel in the destination directory.
+ files = {
+ 'setup.py': "from setuptools import setup\nsetup()",
+ 'VERSION': "0.0.1",
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ name = foo
+ version = file: VERSION
+ """
+ ),
+ 'pyproject.toml': DALS(
+ """
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+ """
+ ),
+ }
+
+ path.build(files)
+
+ dist_dir = os.path.abspath('preexisting-' + build_type)
+
+ build_backend = self.get_build_backend()
+ build_method = getattr(build_backend, 'build_' + build_type)
+
+ # Build a first sdist/wheel.
+ # Note: this also check the destination directory is
+ # successfully created if it does not exist already.
+ first_result = build_method(dist_dir)
+
+ # Change version.
+ with open("VERSION", "wt", encoding="utf-8") as version_file:
+ version_file.write("0.0.2")
+
+ # Build a *second* sdist/wheel.
+ second_result = build_method(dist_dir)
+
+ assert os.path.isfile(os.path.join(dist_dir, first_result))
+ assert first_result != second_result
+
+ # And if rebuilding the exact same sdist/wheel?
+ open(os.path.join(dist_dir, second_result), 'wb').close()
+ third_result = build_method(dist_dir)
+ assert third_result == second_result
+ assert os.path.getsize(os.path.join(dist_dir, third_result)) > 0
+
+ @pytest.mark.parametrize("setup_script", [None, SETUP_SCRIPT_STUB])
+ def test_build_with_pyproject_config(self, tmpdir, setup_script):
+ files = {
+ 'pyproject.toml': DALS(
+ """
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "foo"
+ license = {text = "MIT"}
+ description = "This is a Python package"
+ dynamic = ["version", "readme"]
+ classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers"
+ ]
+ urls = {Homepage = "http://github.com"}
+ dependencies = [
+ "appdirs",
+ ]
+
+ [project.optional-dependencies]
+ all = [
+ "tomli>=1",
+ "pyscaffold>=4,<5",
+ 'importlib; python_version == "2.6"',
+ ]
+
+ [project.scripts]
+ foo = "foo.cli:main"
+
+ [tool.setuptools]
+ zip-safe = false
+ package-dir = {"" = "src"}
+ packages = {find = {where = ["src"]}}
+ license-files = ["LICENSE*"]
+
+ [tool.setuptools.dynamic]
+ version = {attr = "foo.__version__"}
+ readme = {file = "README.rst"}
+
+ [tool.distutils.sdist]
+ formats = "gztar"
+ """
+ ),
+ "MANIFEST.in": DALS(
+ """
+ global-include *.py *.txt
+ global-exclude *.py[cod]
+ """
+ ),
+ "README.rst": "This is a ``README``",
+ "LICENSE.txt": "---- placeholder MIT license ----",
+ "src": {
+ "foo": {
+ "__init__.py": "__version__ = '0.1'",
+ "__init__.pyi": "__version__: str",
+ "cli.py": "def main(): print('hello world')",
+ "data.txt": "def main(): print('hello world')",
+ "py.typed": "",
+ }
+ },
+ }
+ if setup_script:
+ files["setup.py"] = setup_script
+
+ build_backend = self.get_build_backend()
+ with tmpdir.as_cwd():
+ path.build(files)
+ msgs = [
+ "'tool.setuptools.license-files' is deprecated in favor of 'project.license-files'",
+ "`project.license` as a TOML table is deprecated",
+ ]
+ with warnings.catch_warnings():
+ for msg in msgs:
+ warnings.filterwarnings("ignore", msg, SetuptoolsDeprecationWarning)
+ sdist_path = build_backend.build_sdist("temp")
+ wheel_file = build_backend.build_wheel("temp")
+
+ with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:
+ sdist_contents = set(tar.getnames())
+
+ with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile:
+ wheel_contents = set(zipfile.namelist())
+ metadata = str(zipfile.read("foo-0.1.dist-info/METADATA"), "utf-8")
+ license = str(
+ zipfile.read("foo-0.1.dist-info/licenses/LICENSE.txt"), "utf-8"
+ )
+ epoints = str(zipfile.read("foo-0.1.dist-info/entry_points.txt"), "utf-8")
+
+ assert sdist_contents - {"foo-0.1/setup.py"} == {
+ 'foo-0.1',
+ 'foo-0.1/LICENSE.txt',
+ 'foo-0.1/MANIFEST.in',
+ 'foo-0.1/PKG-INFO',
+ 'foo-0.1/README.rst',
+ 'foo-0.1/pyproject.toml',
+ 'foo-0.1/setup.cfg',
+ 'foo-0.1/src',
+ 'foo-0.1/src/foo',
+ 'foo-0.1/src/foo/__init__.py',
+ 'foo-0.1/src/foo/__init__.pyi',
+ 'foo-0.1/src/foo/cli.py',
+ 'foo-0.1/src/foo/data.txt',
+ 'foo-0.1/src/foo/py.typed',
+ 'foo-0.1/src/foo.egg-info',
+ 'foo-0.1/src/foo.egg-info/PKG-INFO',
+ 'foo-0.1/src/foo.egg-info/SOURCES.txt',
+ 'foo-0.1/src/foo.egg-info/dependency_links.txt',
+ 'foo-0.1/src/foo.egg-info/entry_points.txt',
+ 'foo-0.1/src/foo.egg-info/requires.txt',
+ 'foo-0.1/src/foo.egg-info/top_level.txt',
+ 'foo-0.1/src/foo.egg-info/not-zip-safe',
+ }
+ assert wheel_contents == {
+ "foo/__init__.py",
+ "foo/__init__.pyi", # include type information by default
+ "foo/cli.py",
+ "foo/data.txt", # include_package_data defaults to True
+ "foo/py.typed", # include type information by default
+ "foo-0.1.dist-info/licenses/LICENSE.txt",
+ "foo-0.1.dist-info/METADATA",
+ "foo-0.1.dist-info/WHEEL",
+ "foo-0.1.dist-info/entry_points.txt",
+ "foo-0.1.dist-info/top_level.txt",
+ "foo-0.1.dist-info/RECORD",
+ }
+ assert license == "---- placeholder MIT license ----"
+
+ for line in (
+ "Summary: This is a Python package",
+ "License: MIT",
+ "License-File: LICENSE.txt",
+ "Classifier: Intended Audience :: Developers",
+ "Requires-Dist: appdirs",
+ "Requires-Dist: " + str(Requirement('tomli>=1 ; extra == "all"')),
+ "Requires-Dist: "
+ + str(Requirement('importlib; python_version=="2.6" and extra =="all"')),
+ ):
+ assert line in metadata, (line, metadata)
+
+ assert metadata.strip().endswith("This is a ``README``")
+ assert epoints.strip() == "[console_scripts]\nfoo = foo.cli:main"
+
+ def test_static_metadata_in_pyproject_config(self, tmpdir):
+ # Make sure static metadata in pyproject.toml is not overwritten by setup.py
+ # as required by PEP 621
+ files = {
+ 'pyproject.toml': DALS(
+ """
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "foo"
+ description = "This is a Python package"
+ version = "42"
+ dependencies = ["six"]
+ """
+ ),
+ 'hello.py': DALS(
+ """
+ def run():
+ print('hello')
+ """
+ ),
+ 'setup.py': DALS(
+ """
+ __import__('setuptools').setup(
+ name='bar',
+ version='13',
+ )
+ """
+ ),
+ }
+ build_backend = self.get_build_backend()
+ with tmpdir.as_cwd():
+ path.build(files)
+ sdist_path = build_backend.build_sdist("temp")
+ wheel_file = build_backend.build_wheel("temp")
+
+ assert (tmpdir / "temp/foo-42.tar.gz").exists()
+ assert (tmpdir / "temp/foo-42-py3-none-any.whl").exists()
+ assert not (tmpdir / "temp/bar-13.tar.gz").exists()
+ assert not (tmpdir / "temp/bar-42.tar.gz").exists()
+ assert not (tmpdir / "temp/foo-13.tar.gz").exists()
+ assert not (tmpdir / "temp/bar-13-py3-none-any.whl").exists()
+ assert not (tmpdir / "temp/bar-42-py3-none-any.whl").exists()
+ assert not (tmpdir / "temp/foo-13-py3-none-any.whl").exists()
+
+ with tarfile.open(os.path.join(tmpdir, "temp", sdist_path)) as tar:
+ pkg_info = str(tar.extractfile('foo-42/PKG-INFO').read(), "utf-8")
+ members = tar.getnames()
+ assert "bar-13/PKG-INFO" not in members
+
+ with ZipFile(os.path.join(tmpdir, "temp", wheel_file)) as zipfile:
+ metadata = str(zipfile.read("foo-42.dist-info/METADATA"), "utf-8")
+ members = zipfile.namelist()
+ assert "bar-13.dist-info/METADATA" not in members
+
+ for file in pkg_info, metadata:
+ for line in ("Name: foo", "Version: 42"):
+ assert line in file
+ for line in ("Name: bar", "Version: 13"):
+ assert line not in file
+
+ def test_build_sdist(self, build_backend):
+ dist_dir = os.path.abspath('pip-sdist')
+ os.makedirs(dist_dir)
+ sdist_name = build_backend.build_sdist(dist_dir)
+
+ assert os.path.isfile(os.path.join(dist_dir, sdist_name))
+
+ def test_prepare_metadata_for_build_wheel(self, build_backend):
+ dist_dir = os.path.abspath('pip-dist-info')
+ os.makedirs(dist_dir)
+
+ dist_info = build_backend.prepare_metadata_for_build_wheel(dist_dir)
+
+ assert os.path.isfile(os.path.join(dist_dir, dist_info, 'METADATA'))
+
+ def test_prepare_metadata_inplace(self, build_backend):
+ """
+ Some users might pass metadata_directory pre-populated with `.tox` or `.venv`.
+ See issue #3523.
+ """
+ for pre_existing in [
+ ".tox/python/lib/python3.10/site-packages/attrs-22.1.0.dist-info",
+ ".tox/python/lib/python3.10/site-packages/autocommand-2.2.1.dist-info",
+ ".nox/python/lib/python3.10/site-packages/build-0.8.0.dist-info",
+ ".venv/python3.10/site-packages/click-8.1.3.dist-info",
+ "venv/python3.10/site-packages/distlib-0.3.5.dist-info",
+ "env/python3.10/site-packages/docutils-0.19.dist-info",
+ ]:
+ os.makedirs(pre_existing, exist_ok=True)
+ dist_info = build_backend.prepare_metadata_for_build_wheel(".")
+ assert os.path.isfile(os.path.join(dist_info, 'METADATA'))
+
+ def test_build_sdist_explicit_dist(self, build_backend):
+ # explicitly specifying the dist folder should work
+ # the folder sdist_directory and the ``--dist-dir`` can be the same
+ dist_dir = os.path.abspath('dist')
+ sdist_name = build_backend.build_sdist(dist_dir)
+ assert os.path.isfile(os.path.join(dist_dir, sdist_name))
+
+ def test_build_sdist_version_change(self, build_backend):
+ sdist_into_directory = os.path.abspath("out_sdist")
+ os.makedirs(sdist_into_directory)
+
+ sdist_name = build_backend.build_sdist(sdist_into_directory)
+ assert os.path.isfile(os.path.join(sdist_into_directory, sdist_name))
+
+ # if the setup.py changes subsequent call of the build meta
+ # should still succeed, given the
+ # sdist_directory the frontend specifies is empty
+ setup_loc = os.path.abspath("setup.py")
+ if not os.path.exists(setup_loc):
+ setup_loc = os.path.abspath("setup.cfg")
+
+ with open(setup_loc, 'rt', encoding="utf-8") as file_handler:
+ content = file_handler.read()
+ with open(setup_loc, 'wt', encoding="utf-8") as file_handler:
+ file_handler.write(content.replace("version='0.0.0'", "version='0.0.1'"))
+
+ shutil.rmtree(sdist_into_directory)
+ os.makedirs(sdist_into_directory)
+
+ sdist_name = build_backend.build_sdist("out_sdist")
+ assert os.path.isfile(os.path.join(os.path.abspath("out_sdist"), sdist_name))
+
+ def test_build_sdist_pyproject_toml_exists(self, tmpdir_cwd):
+ files = {
+ 'setup.py': DALS(
+ """
+ __import__('setuptools').setup(
+ name='foo',
+ version='0.0.0',
+ py_modules=['hello']
+ )"""
+ ),
+ 'hello.py': '',
+ 'pyproject.toml': DALS(
+ """
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+ """
+ ),
+ }
+ path.build(files)
+ build_backend = self.get_build_backend()
+ targz_path = build_backend.build_sdist("temp")
+ with tarfile.open(os.path.join("temp", targz_path)) as tar:
+ assert any('pyproject.toml' in name for name in tar.getnames())
+
+ def test_build_sdist_setup_py_exists(self, tmpdir_cwd):
+ # If build_sdist is called from a script other than setup.py,
+ # ensure setup.py is included
+ path.build(defns[0])
+
+ build_backend = self.get_build_backend()
+ targz_path = build_backend.build_sdist("temp")
+ with tarfile.open(os.path.join("temp", targz_path)) as tar:
+ assert any('setup.py' in name for name in tar.getnames())
+
+ def test_build_sdist_setup_py_manifest_excluded(self, tmpdir_cwd):
+ # Ensure that MANIFEST.in can exclude setup.py
+ files = {
+ 'setup.py': DALS(
+ """
+ __import__('setuptools').setup(
+ name='foo',
+ version='0.0.0',
+ py_modules=['hello']
+ )"""
+ ),
+ 'hello.py': '',
+ 'MANIFEST.in': DALS(
+ """
+ exclude setup.py
+ """
+ ),
+ }
+
+ path.build(files)
+
+ build_backend = self.get_build_backend()
+ targz_path = build_backend.build_sdist("temp")
+ with tarfile.open(os.path.join("temp", targz_path)) as tar:
+ assert not any('setup.py' in name for name in tar.getnames())
+
+ def test_build_sdist_builds_targz_even_if_zip_indicated(self, tmpdir_cwd):
+ files = {
+ 'setup.py': DALS(
+ """
+ __import__('setuptools').setup(
+ name='foo',
+ version='0.0.0',
+ py_modules=['hello']
+ )"""
+ ),
+ 'hello.py': '',
+ 'setup.cfg': DALS(
+ """
+ [sdist]
+ formats=zip
+ """
+ ),
+ }
+
+ path.build(files)
+
+ build_backend = self.get_build_backend()
+ build_backend.build_sdist("temp")
+
+ _relative_path_import_files = {
+ 'setup.py': DALS(
+ """
+ __import__('setuptools').setup(
+ name='foo',
+ version=__import__('hello').__version__,
+ py_modules=['hello']
+ )"""
+ ),
+ 'hello.py': '__version__ = "0.0.0"',
+ 'setup.cfg': DALS(
+ """
+ [sdist]
+ formats=zip
+ """
+ ),
+ }
+
+ def test_build_sdist_relative_path_import(self, tmpdir_cwd):
+ path.build(self._relative_path_import_files)
+ build_backend = self.get_build_backend()
+ with pytest.raises(ImportError, match="^No module named 'hello'$"):
+ build_backend.build_sdist("temp")
+
+ _simple_pyproject_example = {
+ "pyproject.toml": DALS(
+ """
+ [project]
+ name = "proj"
+ version = "42"
+ """
+ ),
+ "src": {"proj": {"__init__.py": ""}},
+ }
+
+ def _assert_link_tree(self, parent_dir):
+ """All files in the directory should be either links or hard links"""
+ files = list(Path(parent_dir).glob("**/*"))
+ assert files # Should not be empty
+ for file in files:
+ assert file.is_symlink() or os.stat(file).st_nlink > 0
+
+ def test_editable_without_config_settings(self, tmpdir_cwd):
+ """
+ Sanity check to ensure tests with --mode=strict are different from the ones
+ without --mode.
+
+ --mode=strict should create a local directory with a package tree.
+ The directory should not get created otherwise.
+ """
+ path.build(self._simple_pyproject_example)
+ build_backend = self.get_build_backend()
+ assert not Path("build").exists()
+ build_backend.build_editable("temp")
+ assert not Path("build").exists()
+
+ def test_build_wheel_inplace(self, tmpdir_cwd):
+ config_settings = {"--build-option": ["build_ext", "--inplace"]}
+ path.build(self._simple_pyproject_example)
+ build_backend = self.get_build_backend()
+ assert not Path("build").exists()
+ Path("build").mkdir()
+ build_backend.prepare_metadata_for_build_wheel("build", config_settings)
+ build_backend.build_wheel("build", config_settings)
+ assert Path("build/proj-42-py3-none-any.whl").exists()
+
+ @pytest.mark.parametrize("config_settings", [{"editable-mode": "strict"}])
+ def test_editable_with_config_settings(self, tmpdir_cwd, config_settings):
+ path.build({**self._simple_pyproject_example, '_meta': {}})
+ assert not Path("build").exists()
+ build_backend = self.get_build_backend()
+ build_backend.prepare_metadata_for_build_editable("_meta", config_settings)
+ build_backend.build_editable("temp", config_settings, "_meta")
+ self._assert_link_tree(next(Path("build").glob("__editable__.*")))
+
+ @pytest.mark.parametrize(
+ ("setup_literal", "requirements"),
+ [
+ ("'foo'", ['foo']),
+ ("['foo']", ['foo']),
+ (r"'foo\n'", ['foo']),
+ (r"'foo\n\n'", ['foo']),
+ ("['foo', 'bar']", ['foo', 'bar']),
+ (r"'# Has a comment line\nfoo'", ['foo']),
+ (r"'foo # Has an inline comment'", ['foo']),
+ (r"'foo \\\n >=3.0'", ['foo>=3.0']),
+ (r"'foo\nbar'", ['foo', 'bar']),
+ (r"'foo\nbar\n'", ['foo', 'bar']),
+ (r"['foo\n', 'bar\n']", ['foo', 'bar']),
+ ],
+ )
+ @pytest.mark.parametrize('use_wheel', [True, False])
+ def test_setup_requires(self, setup_literal, requirements, use_wheel, tmpdir_cwd):
+ files = {
+ 'setup.py': DALS(
+ """
+ from setuptools import setup
+
+ setup(
+ name="qux",
+ version="0.0.0",
+ py_modules=["hello"],
+ setup_requires={setup_literal},
+ )
+ """
+ ).format(setup_literal=setup_literal),
+ 'hello.py': DALS(
+ """
+ def run():
+ print('hello')
+ """
+ ),
+ }
+
+ path.build(files)
+
+ build_backend = self.get_build_backend()
+
+ if use_wheel:
+ get_requires = build_backend.get_requires_for_build_wheel
+ else:
+ get_requires = build_backend.get_requires_for_build_sdist
+
+ # Ensure that the build requirements are properly parsed
+ expected = sorted(requirements)
+ actual = get_requires()
+
+ assert expected == sorted(actual)
+
+ def test_setup_requires_with_auto_discovery(self, tmpdir_cwd):
+ # Make sure patches introduced to retrieve setup_requires don't accidentally
+ # activate auto-discovery and cause problems due to the incomplete set of
+ # attributes passed to MinimalDistribution
+ files = {
+ 'pyproject.toml': DALS(
+ """
+ [project]
+ name = "proj"
+ version = "42"
+ """
+ ),
+ "setup.py": DALS(
+ """
+ __import__('setuptools').setup(
+ setup_requires=["foo"],
+ py_modules = ["hello", "world"]
+ )
+ """
+ ),
+ 'hello.py': "'hello'",
+ 'world.py': "'world'",
+ }
+ path.build(files)
+ build_backend = self.get_build_backend()
+ setup_requires = build_backend.get_requires_for_build_wheel()
+ assert setup_requires == ["foo"]
+
+ def test_dont_install_setup_requires(self, tmpdir_cwd):
+ files = {
+ 'setup.py': DALS(
+ """
+ from setuptools import setup
+
+ setup(
+ name="qux",
+ version="0.0.0",
+ py_modules=["hello"],
+ setup_requires=["does-not-exist >99"],
+ )
+ """
+ ),
+ 'hello.py': DALS(
+ """
+ def run():
+ print('hello')
+ """
+ ),
+ }
+
+ path.build(files)
+
+ build_backend = self.get_build_backend()
+
+ dist_dir = os.path.abspath('pip-dist-info')
+ os.makedirs(dist_dir)
+
+ # does-not-exist can't be satisfied, so if it attempts to install
+ # setup_requires, it will fail.
+ build_backend.prepare_metadata_for_build_wheel(dist_dir)
+
+ _sys_argv_0_passthrough = {
+ 'setup.py': DALS(
+ """
+ import os
+ import sys
+
+ __import__('setuptools').setup(
+ name='foo',
+ version='0.0.0',
+ )
+
+ sys_argv = os.path.abspath(sys.argv[0])
+ file_path = os.path.abspath('setup.py')
+ assert sys_argv == file_path
+ """
+ )
+ }
+
+ def test_sys_argv_passthrough(self, tmpdir_cwd):
+ path.build(self._sys_argv_0_passthrough)
+ build_backend = self.get_build_backend()
+ with pytest.raises(AssertionError):
+ build_backend.build_sdist("temp")
+
+ _setup_py_file_abspath = {
+ 'setup.py': DALS(
+ """
+ import os
+ assert os.path.isabs(__file__)
+ __import__('setuptools').setup(
+ name='foo',
+ version='0.0.0',
+ py_modules=['hello'],
+ setup_requires=['six'],
+ )
+ """
+ )
+ }
+
+ def test_setup_py_file_abspath(self, tmpdir_cwd):
+ path.build(self._setup_py_file_abspath)
+ build_backend = self.get_build_backend()
+ build_backend.build_sdist("temp")
+
+ @pytest.mark.parametrize('build_hook', ('build_sdist', 'build_wheel'))
+ def test_build_with_empty_setuppy(self, build_backend, build_hook):
+ files = {'setup.py': ''}
+ path.build(files)
+
+ msg = re.escape('No distribution was found.')
+ with pytest.raises(ValueError, match=msg):
+ getattr(build_backend, build_hook)("temp")
+
+
+class TestBuildMetaLegacyBackend(TestBuildMetaBackend):
+ backend_name = 'setuptools.build_meta:__legacy__'
+
+ # build_meta_legacy-specific tests
+ def test_build_sdist_relative_path_import(self, tmpdir_cwd):
+ # This must fail in build_meta, but must pass in build_meta_legacy
+ path.build(self._relative_path_import_files)
+
+ build_backend = self.get_build_backend()
+ build_backend.build_sdist("temp")
+
+ def test_sys_argv_passthrough(self, tmpdir_cwd):
+ path.build(self._sys_argv_0_passthrough)
+
+ build_backend = self.get_build_backend()
+ build_backend.build_sdist("temp")
+
+
+def test_legacy_editable_install(venv, tmpdir, tmpdir_cwd):
+ pyproject = """
+ [build-system]
+ requires = ["setuptools"]
+ build-backend = "setuptools.build_meta"
+ [project]
+ name = "myproj"
+ version = "42"
+ """
+ path.build({"pyproject.toml": DALS(pyproject), "mymod.py": ""})
+
+ # First: sanity check
+ cmd = ["pip", "install", "--no-build-isolation", "-e", "."]
+ output = venv.run(cmd, cwd=tmpdir).lower()
+ assert "running setup.py develop for myproj" not in output
+ assert "created wheel for myproj" in output
+
+ # Then: real test
+ env = {**os.environ, "SETUPTOOLS_ENABLE_FEATURES": "legacy-editable"}
+ cmd = ["pip", "install", "--no-build-isolation", "-e", "."]
+ output = venv.run(cmd, cwd=tmpdir, env=env).lower()
+ assert "running setup.py develop for myproj" in output
+
+
+@pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning")
+def test_sys_exit_0_in_setuppy(monkeypatch, tmp_path):
+ """Setuptools should be resilient to setup.py with ``sys.exit(0)`` (#3973)."""
+ monkeypatch.chdir(tmp_path)
+ setuppy = """
+ import sys, setuptools
+ setuptools.setup(name='foo', version='0.0.0')
+ sys.exit(0)
+ """
+ (tmp_path / "setup.py").write_text(DALS(setuppy), encoding="utf-8")
+ backend = BuildBackend(backend_name="setuptools.build_meta")
+ assert backend.get_requires_for_build_wheel() == []
+
+
+def test_system_exit_in_setuppy(monkeypatch, tmp_path):
+ monkeypatch.chdir(tmp_path)
+ setuppy = "import sys; sys.exit('some error')"
+ (tmp_path / "setup.py").write_text(setuppy, encoding="utf-8")
+ with pytest.raises(SystemExit, match="some error"):
+ backend = BuildBackend(backend_name="setuptools.build_meta")
+ backend.get_requires_for_build_wheel()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_py.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_py.py
new file mode 100644
index 00000000..1e3a6608
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_build_py.py
@@ -0,0 +1,480 @@
+import os
+import shutil
+import stat
+import warnings
+from pathlib import Path
+from unittest.mock import Mock
+
+import jaraco.path
+import pytest
+
+from setuptools import SetuptoolsDeprecationWarning
+from setuptools.dist import Distribution
+
+from .textwrap import DALS
+
+
+def test_directories_in_package_data_glob(tmpdir_cwd):
+ """
+ Directories matching the glob in package_data should
+ not be included in the package data.
+
+ Regression test for #261.
+ """
+ dist = Distribution(
+ dict(
+ script_name='setup.py',
+ script_args=['build_py'],
+ packages=[''],
+ package_data={'': ['path/*']},
+ )
+ )
+ os.makedirs('path/subpath')
+ dist.parse_command_line()
+ dist.run_commands()
+
+
+def test_recursive_in_package_data_glob(tmpdir_cwd):
+ """
+ Files matching recursive globs (**) in package_data should
+ be included in the package data.
+
+ #1806
+ """
+ dist = Distribution(
+ dict(
+ script_name='setup.py',
+ script_args=['build_py'],
+ packages=[''],
+ package_data={'': ['path/**/data']},
+ )
+ )
+ os.makedirs('path/subpath/subsubpath')
+ open('path/subpath/subsubpath/data', 'wb').close()
+
+ dist.parse_command_line()
+ dist.run_commands()
+
+ assert stat.S_ISREG(os.stat('build/lib/path/subpath/subsubpath/data').st_mode), (
+ "File is not included"
+ )
+
+
+def test_read_only(tmpdir_cwd):
+ """
+ Ensure read-only flag is not preserved in copy
+ for package modules and package data, as that
+ causes problems with deleting read-only files on
+ Windows.
+
+ #1451
+ """
+ dist = Distribution(
+ dict(
+ script_name='setup.py',
+ script_args=['build_py'],
+ packages=['pkg'],
+ package_data={'pkg': ['data.dat']},
+ )
+ )
+ os.makedirs('pkg')
+ open('pkg/__init__.py', 'wb').close()
+ open('pkg/data.dat', 'wb').close()
+ os.chmod('pkg/__init__.py', stat.S_IREAD)
+ os.chmod('pkg/data.dat', stat.S_IREAD)
+ dist.parse_command_line()
+ dist.run_commands()
+ shutil.rmtree('build')
+
+
+@pytest.mark.xfail(
+ 'platform.system() == "Windows"',
+ reason="On Windows, files do not have executable bits",
+ raises=AssertionError,
+ strict=True,
+)
+def test_executable_data(tmpdir_cwd):
+ """
+ Ensure executable bit is preserved in copy for
+ package data, as users rely on it for scripts.
+
+ #2041
+ """
+ dist = Distribution(
+ dict(
+ script_name='setup.py',
+ script_args=['build_py'],
+ packages=['pkg'],
+ package_data={'pkg': ['run-me']},
+ )
+ )
+ os.makedirs('pkg')
+ open('pkg/__init__.py', 'wb').close()
+ open('pkg/run-me', 'wb').close()
+ os.chmod('pkg/run-me', 0o700)
+
+ dist.parse_command_line()
+ dist.run_commands()
+
+ assert os.stat('build/lib/pkg/run-me').st_mode & stat.S_IEXEC, (
+ "Script is not executable"
+ )
+
+
+EXAMPLE_WITH_MANIFEST = {
+ "setup.cfg": DALS(
+ """
+ [metadata]
+ name = mypkg
+ version = 42
+
+ [options]
+ include_package_data = True
+ packages = find:
+
+ [options.packages.find]
+ exclude = *.tests*
+ """
+ ),
+ "mypkg": {
+ "__init__.py": "",
+ "resource_file.txt": "",
+ "tests": {
+ "__init__.py": "",
+ "test_mypkg.py": "",
+ "test_file.txt": "",
+ },
+ },
+ "MANIFEST.in": DALS(
+ """
+ global-include *.py *.txt
+ global-exclude *.py[cod]
+ prune dist
+ prune build
+ prune *.egg-info
+ """
+ ),
+}
+
+
+def test_excluded_subpackages(tmpdir_cwd):
+ jaraco.path.build(EXAMPLE_WITH_MANIFEST)
+ dist = Distribution({"script_name": "%PEP 517%"})
+ dist.parse_config_files()
+
+ build_py = dist.get_command_obj("build_py")
+
+ msg = r"Python recognizes 'mypkg\.tests' as an importable package"
+ with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
+ # TODO: To fix #3260 we need some transition period to deprecate the
+ # existing behavior of `include_package_data`. After the transition, we
+ # should remove the warning and fix the behavior.
+
+ if os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib":
+ # pytest.warns reset the warning filter temporarily
+ # https://github.com/pytest-dev/pytest/issues/4011#issuecomment-423494810
+ warnings.filterwarnings(
+ "ignore",
+ "'encoding' argument not specified",
+ module="distutils.text_file",
+ # This warning is already fixed in pypa/distutils but not in stdlib
+ )
+
+ build_py.finalize_options()
+ build_py.run()
+
+ build_dir = Path(dist.get_command_obj("build_py").build_lib)
+ assert (build_dir / "mypkg/__init__.py").exists()
+ assert (build_dir / "mypkg/resource_file.txt").exists()
+
+ # Setuptools is configured to ignore `mypkg.tests`, therefore the following
+ # files/dirs should not be included in the distribution.
+ for f in [
+ "mypkg/tests/__init__.py",
+ "mypkg/tests/test_mypkg.py",
+ "mypkg/tests/test_file.txt",
+ "mypkg/tests",
+ ]:
+ with pytest.raises(AssertionError):
+ # TODO: Enforce the following assertion once #3260 is fixed
+ # (remove context manager and the following xfail).
+ assert not (build_dir / f).exists()
+
+ pytest.xfail("#3260")
+
+
+@pytest.mark.filterwarnings("ignore::setuptools.SetuptoolsDeprecationWarning")
+def test_existing_egg_info(tmpdir_cwd, monkeypatch):
+ """When provided with the ``existing_egg_info_dir`` attribute, build_py should not
+ attempt to run egg_info again.
+ """
+ # == Pre-condition ==
+ # Generate an egg-info dir
+ jaraco.path.build(EXAMPLE_WITH_MANIFEST)
+ dist = Distribution({"script_name": "%PEP 517%"})
+ dist.parse_config_files()
+ assert dist.include_package_data
+
+ egg_info = dist.get_command_obj("egg_info")
+ dist.run_command("egg_info")
+ egg_info_dir = next(Path(egg_info.egg_base).glob("*.egg-info"))
+ assert egg_info_dir.is_dir()
+
+ # == Setup ==
+ build_py = dist.get_command_obj("build_py")
+ build_py.finalize_options()
+ egg_info = dist.get_command_obj("egg_info")
+ egg_info_run = Mock(side_effect=egg_info.run)
+ monkeypatch.setattr(egg_info, "run", egg_info_run)
+
+ # == Remove caches ==
+ # egg_info is called when build_py looks for data_files, which gets cached.
+ # We need to ensure it is not cached yet, otherwise it may impact on the tests
+ build_py.__dict__.pop('data_files', None)
+ dist.reinitialize_command(egg_info)
+
+ # == Sanity check ==
+ # Ensure that if existing_egg_info is not given, build_py attempts to run egg_info
+ build_py.existing_egg_info_dir = None
+ build_py.run()
+ egg_info_run.assert_called()
+
+ # == Remove caches ==
+ egg_info_run.reset_mock()
+ build_py.__dict__.pop('data_files', None)
+ dist.reinitialize_command(egg_info)
+
+ # == Actual test ==
+ # Ensure that if existing_egg_info_dir is given, egg_info doesn't run
+ build_py.existing_egg_info_dir = egg_info_dir
+ build_py.run()
+ egg_info_run.assert_not_called()
+ assert build_py.data_files
+
+ # Make sure the list of outputs is actually OK
+ outputs = map(lambda x: x.replace(os.sep, "/"), build_py.get_outputs())
+ assert outputs
+ example = str(Path(build_py.build_lib, "mypkg/__init__.py")).replace(os.sep, "/")
+ assert example in outputs
+
+
+EXAMPLE_ARBITRARY_MAPPING = {
+ "pyproject.toml": DALS(
+ """
+ [project]
+ name = "mypkg"
+ version = "42"
+
+ [tool.setuptools]
+ packages = ["mypkg", "mypkg.sub1", "mypkg.sub2", "mypkg.sub2.nested"]
+
+ [tool.setuptools.package-dir]
+ "" = "src"
+ "mypkg.sub2" = "src/mypkg/_sub2"
+ "mypkg.sub2.nested" = "other"
+ """
+ ),
+ "src": {
+ "mypkg": {
+ "__init__.py": "",
+ "resource_file.txt": "",
+ "sub1": {
+ "__init__.py": "",
+ "mod1.py": "",
+ },
+ "_sub2": {
+ "mod2.py": "",
+ },
+ },
+ },
+ "other": {
+ "__init__.py": "",
+ "mod3.py": "",
+ },
+ "MANIFEST.in": DALS(
+ """
+ global-include *.py *.txt
+ global-exclude *.py[cod]
+ """
+ ),
+}
+
+
+def test_get_outputs(tmpdir_cwd):
+ jaraco.path.build(EXAMPLE_ARBITRARY_MAPPING)
+ dist = Distribution({"script_name": "%test%"})
+ dist.parse_config_files()
+
+ build_py = dist.get_command_obj("build_py")
+ build_py.editable_mode = True
+ build_py.ensure_finalized()
+ build_lib = build_py.build_lib.replace(os.sep, "/")
+ outputs = {x.replace(os.sep, "/") for x in build_py.get_outputs()}
+ assert outputs == {
+ f"{build_lib}/mypkg/__init__.py",
+ f"{build_lib}/mypkg/resource_file.txt",
+ f"{build_lib}/mypkg/sub1/__init__.py",
+ f"{build_lib}/mypkg/sub1/mod1.py",
+ f"{build_lib}/mypkg/sub2/mod2.py",
+ f"{build_lib}/mypkg/sub2/nested/__init__.py",
+ f"{build_lib}/mypkg/sub2/nested/mod3.py",
+ }
+ mapping = {
+ k.replace(os.sep, "/"): v.replace(os.sep, "/")
+ for k, v in build_py.get_output_mapping().items()
+ }
+ assert mapping == {
+ f"{build_lib}/mypkg/__init__.py": "src/mypkg/__init__.py",
+ f"{build_lib}/mypkg/resource_file.txt": "src/mypkg/resource_file.txt",
+ f"{build_lib}/mypkg/sub1/__init__.py": "src/mypkg/sub1/__init__.py",
+ f"{build_lib}/mypkg/sub1/mod1.py": "src/mypkg/sub1/mod1.py",
+ f"{build_lib}/mypkg/sub2/mod2.py": "src/mypkg/_sub2/mod2.py",
+ f"{build_lib}/mypkg/sub2/nested/__init__.py": "other/__init__.py",
+ f"{build_lib}/mypkg/sub2/nested/mod3.py": "other/mod3.py",
+ }
+
+
+class TestTypeInfoFiles:
+ PYPROJECTS = {
+ "default_pyproject": DALS(
+ """
+ [project]
+ name = "foo"
+ version = "1"
+ """
+ ),
+ "dont_include_package_data": DALS(
+ """
+ [project]
+ name = "foo"
+ version = "1"
+
+ [tool.setuptools]
+ include-package-data = false
+ """
+ ),
+ "exclude_type_info": DALS(
+ """
+ [project]
+ name = "foo"
+ version = "1"
+
+ [tool.setuptools]
+ include-package-data = false
+
+ [tool.setuptools.exclude-package-data]
+ "*" = ["py.typed", "*.pyi"]
+ """
+ ),
+ }
+
+ EXAMPLES = {
+ "simple_namespace": {
+ "directory_structure": {
+ "foo": {
+ "bar.pyi": "",
+ "py.typed": "",
+ "__init__.py": "",
+ }
+ },
+ "expected_type_files": {"foo/bar.pyi", "foo/py.typed"},
+ },
+ "nested_inside_namespace": {
+ "directory_structure": {
+ "foo": {
+ "bar": {
+ "py.typed": "",
+ "mod.pyi": "",
+ }
+ }
+ },
+ "expected_type_files": {"foo/bar/mod.pyi", "foo/bar/py.typed"},
+ },
+ "namespace_nested_inside_regular": {
+ "directory_structure": {
+ "foo": {
+ "namespace": {
+ "foo.pyi": "",
+ },
+ "__init__.pyi": "",
+ "py.typed": "",
+ }
+ },
+ "expected_type_files": {
+ "foo/namespace/foo.pyi",
+ "foo/__init__.pyi",
+ "foo/py.typed",
+ },
+ },
+ }
+
+ @pytest.mark.parametrize(
+ "pyproject",
+ [
+ "default_pyproject",
+ pytest.param(
+ "dont_include_package_data",
+ marks=pytest.mark.xfail(reason="pypa/setuptools#4350"),
+ ),
+ ],
+ )
+ @pytest.mark.parametrize("example", EXAMPLES.keys())
+ def test_type_files_included_by_default(self, tmpdir_cwd, pyproject, example):
+ structure = {
+ **self.EXAMPLES[example]["directory_structure"],
+ "pyproject.toml": self.PYPROJECTS[pyproject],
+ }
+ expected_type_files = self.EXAMPLES[example]["expected_type_files"]
+ jaraco.path.build(structure)
+
+ build_py = get_finalized_build_py()
+ outputs = get_outputs(build_py)
+ assert expected_type_files <= outputs
+
+ @pytest.mark.parametrize("pyproject", ["exclude_type_info"])
+ @pytest.mark.parametrize("example", EXAMPLES.keys())
+ def test_type_files_can_be_excluded(self, tmpdir_cwd, pyproject, example):
+ structure = {
+ **self.EXAMPLES[example]["directory_structure"],
+ "pyproject.toml": self.PYPROJECTS[pyproject],
+ }
+ expected_type_files = self.EXAMPLES[example]["expected_type_files"]
+ jaraco.path.build(structure)
+
+ build_py = get_finalized_build_py()
+ outputs = get_outputs(build_py)
+ assert expected_type_files.isdisjoint(outputs)
+
+ def test_stub_only_package(self, tmpdir_cwd):
+ structure = {
+ "pyproject.toml": DALS(
+ """
+ [project]
+ name = "foo-stubs"
+ version = "1"
+ """
+ ),
+ "foo-stubs": {"__init__.pyi": "", "bar.pyi": ""},
+ }
+ expected_type_files = {"foo-stubs/__init__.pyi", "foo-stubs/bar.pyi"}
+ jaraco.path.build(structure)
+
+ build_py = get_finalized_build_py()
+ outputs = get_outputs(build_py)
+ assert expected_type_files <= outputs
+
+
+def get_finalized_build_py(script_name="%build_py-test%"):
+ dist = Distribution({"script_name": script_name})
+ dist.parse_config_files()
+ build_py = dist.get_command_obj("build_py")
+ build_py.finalize_options()
+ return build_py
+
+
+def get_outputs(build_py):
+ build_dir = Path(build_py.build_lib)
+ return {
+ os.path.relpath(x, build_dir).replace(os.sep, "/")
+ for x in build_py.get_outputs()
+ }
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py
new file mode 100644
index 00000000..b5df8203
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_config_discovery.py
@@ -0,0 +1,647 @@
+import os
+import sys
+from configparser import ConfigParser
+from itertools import product
+from typing import cast
+
+import jaraco.path
+import pytest
+from path import Path
+
+import setuptools # noqa: F401 # force distutils.core to be patched
+from setuptools.command.sdist import sdist
+from setuptools.discovery import find_package_path, find_parent_package
+from setuptools.dist import Distribution
+from setuptools.errors import PackageDiscoveryError
+
+from .contexts import quiet
+from .integration.helpers import get_sdist_members, get_wheel_members, run
+from .textwrap import DALS
+
+import distutils.core
+
+
+class TestFindParentPackage:
+ def test_single_package(self, tmp_path):
+ # find_parent_package should find a non-namespace parent package
+ (tmp_path / "src/namespace/pkg/nested").mkdir(exist_ok=True, parents=True)
+ (tmp_path / "src/namespace/pkg/nested/__init__.py").touch()
+ (tmp_path / "src/namespace/pkg/__init__.py").touch()
+ packages = ["namespace", "namespace.pkg", "namespace.pkg.nested"]
+ assert find_parent_package(packages, {"": "src"}, tmp_path) == "namespace.pkg"
+
+ def test_multiple_toplevel(self, tmp_path):
+ # find_parent_package should return null if the given list of packages does not
+ # have a single parent package
+ multiple = ["pkg", "pkg1", "pkg2"]
+ for name in multiple:
+ (tmp_path / f"src/{name}").mkdir(exist_ok=True, parents=True)
+ (tmp_path / f"src/{name}/__init__.py").touch()
+ assert find_parent_package(multiple, {"": "src"}, tmp_path) is None
+
+
+class TestDiscoverPackagesAndPyModules:
+ """Make sure discovered values for ``packages`` and ``py_modules`` work
+ similarly to explicit configuration for the simple scenarios.
+ """
+
+ OPTIONS = {
+ # Different options according to the circumstance being tested
+ "explicit-src": {"package_dir": {"": "src"}, "packages": ["pkg"]},
+ "variation-lib": {
+ "package_dir": {"": "lib"}, # variation of the source-layout
+ },
+ "explicit-flat": {"packages": ["pkg"]},
+ "explicit-single_module": {"py_modules": ["pkg"]},
+ "explicit-namespace": {"packages": ["ns", "ns.pkg"]},
+ "automatic-src": {},
+ "automatic-flat": {},
+ "automatic-single_module": {},
+ "automatic-namespace": {},
+ }
+ FILES = {
+ "src": ["src/pkg/__init__.py", "src/pkg/main.py"],
+ "lib": ["lib/pkg/__init__.py", "lib/pkg/main.py"],
+ "flat": ["pkg/__init__.py", "pkg/main.py"],
+ "single_module": ["pkg.py"],
+ "namespace": ["ns/pkg/__init__.py"],
+ }
+
+ def _get_info(self, circumstance):
+ _, _, layout = circumstance.partition("-")
+ files = self.FILES[layout]
+ options = self.OPTIONS[circumstance]
+ return files, options
+
+ @pytest.mark.parametrize("circumstance", OPTIONS.keys())
+ def test_sdist_filelist(self, tmp_path, circumstance):
+ files, options = self._get_info(circumstance)
+ _populate_project_dir(tmp_path, files, options)
+
+ _, cmd = _run_sdist_programatically(tmp_path, options)
+
+ manifest = [f.replace(os.sep, "/") for f in cmd.filelist.files]
+ for file in files:
+ assert any(f.endswith(file) for f in manifest)
+
+ @pytest.mark.parametrize("circumstance", OPTIONS.keys())
+ def test_project(self, tmp_path, circumstance):
+ files, options = self._get_info(circumstance)
+ _populate_project_dir(tmp_path, files, options)
+
+ # Simulate a pre-existing `build` directory
+ (tmp_path / "build").mkdir()
+ (tmp_path / "build/lib").mkdir()
+ (tmp_path / "build/bdist.linux-x86_64").mkdir()
+ (tmp_path / "build/bdist.linux-x86_64/file.py").touch()
+ (tmp_path / "build/lib/__init__.py").touch()
+ (tmp_path / "build/lib/file.py").touch()
+ (tmp_path / "dist").mkdir()
+ (tmp_path / "dist/file.py").touch()
+
+ _run_build(tmp_path)
+
+ sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz")))
+ print("~~~~~ sdist_members ~~~~~")
+ print('\n'.join(sdist_files))
+ assert sdist_files >= set(files)
+
+ wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl")))
+ print("~~~~~ wheel_members ~~~~~")
+ print('\n'.join(wheel_files))
+ orig_files = {f.replace("src/", "").replace("lib/", "") for f in files}
+ assert wheel_files >= orig_files
+
+ # Make sure build files are not included by mistake
+ for file in wheel_files:
+ assert "build" not in files
+ assert "dist" not in files
+
+ PURPOSEFULLY_EMPY = {
+ "setup.cfg": DALS(
+ """
+ [metadata]
+ name = myproj
+ version = 0.0.0
+
+ [options]
+ {param} =
+ """
+ ),
+ "setup.py": DALS(
+ """
+ __import__('setuptools').setup(
+ name="myproj",
+ version="0.0.0",
+ {param}=[]
+ )
+ """
+ ),
+ "pyproject.toml": DALS(
+ """
+ [build-system]
+ requires = []
+ build-backend = 'setuptools.build_meta'
+
+ [project]
+ name = "myproj"
+ version = "0.0.0"
+
+ [tool.setuptools]
+ {param} = []
+ """
+ ),
+ "template-pyproject.toml": DALS(
+ """
+ [build-system]
+ requires = []
+ build-backend = 'setuptools.build_meta'
+ """
+ ),
+ }
+
+ @pytest.mark.parametrize(
+ ("config_file", "param", "circumstance"),
+ product(
+ ["setup.cfg", "setup.py", "pyproject.toml"],
+ ["packages", "py_modules"],
+ FILES.keys(),
+ ),
+ )
+ def test_purposefully_empty(self, tmp_path, config_file, param, circumstance):
+ files = self.FILES[circumstance] + ["mod.py", "other.py", "src/pkg/__init__.py"]
+ _populate_project_dir(tmp_path, files, {})
+
+ if config_file == "pyproject.toml":
+ template_param = param.replace("_", "-")
+ else:
+ # Make sure build works with or without setup.cfg
+ pyproject = self.PURPOSEFULLY_EMPY["template-pyproject.toml"]
+ (tmp_path / "pyproject.toml").write_text(pyproject, encoding="utf-8")
+ template_param = param
+
+ config = self.PURPOSEFULLY_EMPY[config_file].format(param=template_param)
+ (tmp_path / config_file).write_text(config, encoding="utf-8")
+
+ dist = _get_dist(tmp_path, {})
+ # When either parameter package or py_modules is an empty list,
+ # then there should be no discovery
+ assert getattr(dist, param) == []
+ other = {"py_modules": "packages", "packages": "py_modules"}[param]
+ assert getattr(dist, other) is None
+
+ @pytest.mark.parametrize(
+ ("extra_files", "pkgs"),
+ [
+ (["venv/bin/simulate_venv"], {"pkg"}),
+ (["pkg-stubs/__init__.pyi"], {"pkg", "pkg-stubs"}),
+ (["other-stubs/__init__.pyi"], {"pkg", "other-stubs"}),
+ (
+ # Type stubs can also be namespaced
+ ["namespace-stubs/pkg/__init__.pyi"],
+ {"pkg", "namespace-stubs", "namespace-stubs.pkg"},
+ ),
+ (
+ # Just the top-level package can have `-stubs`, ignore nested ones
+ ["namespace-stubs/pkg-stubs/__init__.pyi"],
+ {"pkg", "namespace-stubs"},
+ ),
+ (["_hidden/file.py"], {"pkg"}),
+ (["news/finalize.py"], {"pkg"}),
+ ],
+ )
+ def test_flat_layout_with_extra_files(self, tmp_path, extra_files, pkgs):
+ files = self.FILES["flat"] + extra_files
+ _populate_project_dir(tmp_path, files, {})
+ dist = _get_dist(tmp_path, {})
+ assert set(dist.packages) == pkgs
+
+ @pytest.mark.parametrize(
+ "extra_files",
+ [
+ ["other/__init__.py"],
+ ["other/finalize.py"],
+ ],
+ )
+ def test_flat_layout_with_dangerous_extra_files(self, tmp_path, extra_files):
+ files = self.FILES["flat"] + extra_files
+ _populate_project_dir(tmp_path, files, {})
+ with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
+ _get_dist(tmp_path, {})
+
+ def test_flat_layout_with_single_module(self, tmp_path):
+ files = self.FILES["single_module"] + ["invalid-module-name.py"]
+ _populate_project_dir(tmp_path, files, {})
+ dist = _get_dist(tmp_path, {})
+ assert set(dist.py_modules) == {"pkg"}
+
+ def test_flat_layout_with_multiple_modules(self, tmp_path):
+ files = self.FILES["single_module"] + ["valid_module_name.py"]
+ _populate_project_dir(tmp_path, files, {})
+ with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
+ _get_dist(tmp_path, {})
+
+ def test_py_modules_when_wheel_dir_is_cwd(self, tmp_path):
+ """Regression for issue 3692"""
+ from setuptools import build_meta
+
+ pyproject = '[project]\nname = "test"\nversion = "1"'
+ (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8")
+ (tmp_path / "foo.py").touch()
+ with jaraco.path.DirectoryStack().context(tmp_path):
+ build_meta.build_wheel(".")
+ # Ensure py_modules are found
+ wheel_files = get_wheel_members(next(tmp_path.glob("*.whl")))
+ assert "foo.py" in wheel_files
+
+
+class TestNoConfig:
+ DEFAULT_VERSION = "0.0.0" # Default version given by setuptools
+
+ EXAMPLES = {
+ "pkg1": ["src/pkg1.py"],
+ "pkg2": ["src/pkg2/__init__.py"],
+ "pkg3": ["src/pkg3/__init__.py", "src/pkg3-stubs/__init__.py"],
+ "pkg4": ["pkg4/__init__.py", "pkg4-stubs/__init__.py"],
+ "ns.nested.pkg1": ["src/ns/nested/pkg1/__init__.py"],
+ "ns.nested.pkg2": ["ns/nested/pkg2/__init__.py"],
+ }
+
+ @pytest.mark.parametrize("example", EXAMPLES.keys())
+ def test_discover_name(self, tmp_path, example):
+ _populate_project_dir(tmp_path, self.EXAMPLES[example], {})
+ dist = _get_dist(tmp_path, {})
+ assert dist.get_name() == example
+
+ def test_build_with_discovered_name(self, tmp_path):
+ files = ["src/ns/nested/pkg/__init__.py"]
+ _populate_project_dir(tmp_path, files, {})
+ _run_build(tmp_path, "--sdist")
+ # Expected distribution file
+ dist_file = tmp_path / f"dist/ns_nested_pkg-{self.DEFAULT_VERSION}.tar.gz"
+ assert dist_file.is_file()
+
+
+class TestWithAttrDirective:
+ @pytest.mark.parametrize(
+ ("folder", "opts"),
+ [
+ ("src", {}),
+ ("lib", {"packages": "find:", "packages.find": {"where": "lib"}}),
+ ],
+ )
+ def test_setupcfg_metadata(self, tmp_path, folder, opts):
+ files = [f"{folder}/pkg/__init__.py", "setup.cfg"]
+ _populate_project_dir(tmp_path, files, opts)
+
+ config = (tmp_path / "setup.cfg").read_text(encoding="utf-8")
+ overwrite = {
+ folder: {"pkg": {"__init__.py": "version = 42"}},
+ "setup.cfg": "[metadata]\nversion = attr: pkg.version\n" + config,
+ }
+ jaraco.path.build(overwrite, prefix=tmp_path)
+
+ dist = _get_dist(tmp_path, {})
+ assert dist.get_name() == "pkg"
+ assert dist.get_version() == "42"
+ assert dist.package_dir
+ package_path = find_package_path("pkg", dist.package_dir, tmp_path)
+ assert os.path.exists(package_path)
+ assert folder in Path(package_path).parts()
+
+ _run_build(tmp_path, "--sdist")
+ dist_file = tmp_path / "dist/pkg-42.tar.gz"
+ assert dist_file.is_file()
+
+ def test_pyproject_metadata(self, tmp_path):
+ _populate_project_dir(tmp_path, ["src/pkg/__init__.py"], {})
+
+ overwrite = {
+ "src": {"pkg": {"__init__.py": "version = 42"}},
+ "pyproject.toml": (
+ "[project]\nname = 'pkg'\ndynamic = ['version']\n"
+ "[tool.setuptools.dynamic]\nversion = {attr = 'pkg.version'}\n"
+ ),
+ }
+ jaraco.path.build(overwrite, prefix=tmp_path)
+
+ dist = _get_dist(tmp_path, {})
+ assert dist.get_version() == "42"
+ assert dist.package_dir == {"": "src"}
+
+
+class TestWithCExtension:
+ def _simulate_package_with_extension(self, tmp_path):
+ # This example is based on: https://github.com/nucleic/kiwi/tree/1.4.0
+ files = [
+ "benchmarks/file.py",
+ "docs/Makefile",
+ "docs/requirements.txt",
+ "docs/source/conf.py",
+ "proj/header.h",
+ "proj/file.py",
+ "py/proj.cpp",
+ "py/other.cpp",
+ "py/file.py",
+ "py/py.typed",
+ "py/tests/test_proj.py",
+ "README.rst",
+ ]
+ _populate_project_dir(tmp_path, files, {})
+
+ setup_script = """
+ from setuptools import Extension, setup
+
+ ext_modules = [
+ Extension(
+ "proj",
+ ["py/proj.cpp", "py/other.cpp"],
+ include_dirs=["."],
+ language="c++",
+ ),
+ ]
+ setup(ext_modules=ext_modules)
+ """
+ (tmp_path / "setup.py").write_text(DALS(setup_script), encoding="utf-8")
+
+ def test_skip_discovery_with_setupcfg_metadata(self, tmp_path):
+ """Ensure that auto-discovery is not triggered when the project is based on
+ C-extensions only, for backward compatibility.
+ """
+ self._simulate_package_with_extension(tmp_path)
+
+ pyproject = """
+ [build-system]
+ requires = []
+ build-backend = 'setuptools.build_meta'
+ """
+ (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8")
+
+ setupcfg = """
+ [metadata]
+ name = proj
+ version = 42
+ """
+ (tmp_path / "setup.cfg").write_text(DALS(setupcfg), encoding="utf-8")
+
+ dist = _get_dist(tmp_path, {})
+ assert dist.get_name() == "proj"
+ assert dist.get_version() == "42"
+ assert dist.py_modules is None
+ assert dist.packages is None
+ assert len(dist.ext_modules) == 1
+ assert dist.ext_modules[0].name == "proj"
+
+ def test_dont_skip_discovery_with_pyproject_metadata(self, tmp_path):
+ """When opting-in to pyproject.toml metadata, auto-discovery will be active if
+ the package lists C-extensions, but does not configure py-modules or packages.
+
+ This way we ensure users with complex package layouts that would lead to the
+ discovery of multiple top-level modules/packages see errors and are forced to
+ explicitly set ``packages`` or ``py-modules``.
+ """
+ self._simulate_package_with_extension(tmp_path)
+
+ pyproject = """
+ [project]
+ name = 'proj'
+ version = '42'
+ """
+ (tmp_path / "pyproject.toml").write_text(DALS(pyproject), encoding="utf-8")
+ with pytest.raises(PackageDiscoveryError, match="multiple (packages|modules)"):
+ _get_dist(tmp_path, {})
+
+
+class TestWithPackageData:
+ def _simulate_package_with_data_files(self, tmp_path, src_root):
+ files = [
+ f"{src_root}/proj/__init__.py",
+ f"{src_root}/proj/file1.txt",
+ f"{src_root}/proj/nested/file2.txt",
+ ]
+ _populate_project_dir(tmp_path, files, {})
+
+ manifest = """
+ global-include *.py *.txt
+ """
+ (tmp_path / "MANIFEST.in").write_text(DALS(manifest), encoding="utf-8")
+
+ EXAMPLE_SETUPCFG = """
+ [metadata]
+ name = proj
+ version = 42
+
+ [options]
+ include_package_data = True
+ """
+ EXAMPLE_PYPROJECT = """
+ [project]
+ name = "proj"
+ version = "42"
+ """
+
+ PYPROJECT_PACKAGE_DIR = """
+ [tool.setuptools]
+ package-dir = {"" = "src"}
+ """
+
+ @pytest.mark.parametrize(
+ ("src_root", "files"),
+ [
+ (".", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}),
+ (".", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}),
+ ("src", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}),
+ ("src", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}),
+ (
+ "src",
+ {
+ "setup.cfg": DALS(EXAMPLE_SETUPCFG)
+ + DALS(
+ """
+ packages = find:
+ package_dir =
+ =src
+
+ [options.packages.find]
+ where = src
+ """
+ )
+ },
+ ),
+ (
+ "src",
+ {
+ "pyproject.toml": DALS(EXAMPLE_PYPROJECT)
+ + DALS(
+ """
+ [tool.setuptools]
+ package-dir = {"" = "src"}
+ """
+ )
+ },
+ ),
+ ],
+ )
+ def test_include_package_data(self, tmp_path, src_root, files):
+ """
+ Make sure auto-discovery does not affect package include_package_data.
+ See issue #3196.
+ """
+ jaraco.path.build(files, prefix=str(tmp_path))
+ self._simulate_package_with_data_files(tmp_path, src_root)
+
+ expected = {
+ os.path.normpath(f"{src_root}/proj/file1.txt").replace(os.sep, "/"),
+ os.path.normpath(f"{src_root}/proj/nested/file2.txt").replace(os.sep, "/"),
+ }
+
+ _run_build(tmp_path)
+
+ sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz")))
+ print("~~~~~ sdist_members ~~~~~")
+ print('\n'.join(sdist_files))
+ assert sdist_files >= expected
+
+ wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl")))
+ print("~~~~~ wheel_members ~~~~~")
+ print('\n'.join(wheel_files))
+ orig_files = {f.replace("src/", "").replace("lib/", "") for f in expected}
+ assert wheel_files >= orig_files
+
+
+def test_compatible_with_numpy_configuration(tmp_path):
+ files = [
+ "dir1/__init__.py",
+ "dir2/__init__.py",
+ "file.py",
+ ]
+ _populate_project_dir(tmp_path, files, {})
+ dist = Distribution({})
+ dist.configuration = object()
+ dist.set_defaults()
+ assert dist.py_modules is None
+ assert dist.packages is None
+
+
+def test_name_discovery_doesnt_break_cli(tmpdir_cwd):
+ jaraco.path.build({"pkg.py": ""})
+ dist = Distribution({})
+ dist.script_args = ["--name"]
+ dist.set_defaults()
+ dist.parse_command_line() # <-- no exception should be raised here.
+ assert dist.get_name() == "pkg"
+
+
+def test_preserve_explicit_name_with_dynamic_version(tmpdir_cwd, monkeypatch):
+ """According to #3545 it seems that ``name`` discovery is running,
+ even when the project already explicitly sets it.
+ This seems to be related to parsing of dynamic versions (via ``attr`` directive),
+ which requires the auto-discovery of ``package_dir``.
+ """
+ files = {
+ "src": {
+ "pkg": {"__init__.py": "__version__ = 42\n"},
+ },
+ "pyproject.toml": DALS(
+ """
+ [project]
+ name = "myproj" # purposefully different from package name
+ dynamic = ["version"]
+ [tool.setuptools.dynamic]
+ version = {"attr" = "pkg.__version__"}
+ """
+ ),
+ }
+ jaraco.path.build(files)
+ dist = Distribution({})
+ orig_analyse_name = dist.set_defaults.analyse_name
+
+ def spy_analyse_name():
+ # We can check if name discovery was triggered by ensuring the original
+ # name remains instead of the package name.
+ orig_analyse_name()
+ assert dist.get_name() == "myproj"
+
+ monkeypatch.setattr(dist.set_defaults, "analyse_name", spy_analyse_name)
+ dist.parse_config_files()
+ assert dist.get_version() == "42"
+ assert set(dist.packages) == {"pkg"}
+
+
+def _populate_project_dir(root, files, options):
+ # NOTE: Currently pypa/build will refuse to build the project if no
+ # `pyproject.toml` or `setup.py` is found. So it is impossible to do
+ # completely "config-less" projects.
+ basic = {
+ "setup.py": "import setuptools\nsetuptools.setup()",
+ "README.md": "# Example Package",
+ "LICENSE": "Copyright (c) 2018",
+ }
+ jaraco.path.build(basic, prefix=root)
+ _write_setupcfg(root, options)
+ paths = (root / f for f in files)
+ for path in paths:
+ path.parent.mkdir(exist_ok=True, parents=True)
+ path.touch()
+
+
+def _write_setupcfg(root, options):
+ if not options:
+ print("~~~~~ **NO** setup.cfg ~~~~~")
+ return
+ setupcfg = ConfigParser()
+ setupcfg.add_section("options")
+ for key, value in options.items():
+ if key == "packages.find":
+ setupcfg.add_section(f"options.{key}")
+ setupcfg[f"options.{key}"].update(value)
+ elif isinstance(value, list):
+ setupcfg["options"][key] = ", ".join(value)
+ elif isinstance(value, dict):
+ str_value = "\n".join(f"\t{k} = {v}" for k, v in value.items())
+ setupcfg["options"][key] = "\n" + str_value
+ else:
+ setupcfg["options"][key] = str(value)
+ with open(root / "setup.cfg", "w", encoding="utf-8") as f:
+ setupcfg.write(f)
+ print("~~~~~ setup.cfg ~~~~~")
+ print((root / "setup.cfg").read_text(encoding="utf-8"))
+
+
+def _run_build(path, *flags):
+ cmd = [sys.executable, "-m", "build", "--no-isolation", *flags, str(path)]
+ return run(cmd, env={'DISTUTILS_DEBUG': ''})
+
+
+def _get_dist(dist_path, attrs):
+ root = "/".join(os.path.split(dist_path)) # POSIX-style
+
+ script = dist_path / 'setup.py'
+ if script.exists():
+ with Path(dist_path):
+ dist = cast(
+ Distribution,
+ distutils.core.run_setup("setup.py", {}, stop_after="init"),
+ )
+ else:
+ dist = Distribution(attrs)
+
+ dist.src_root = root
+ dist.script_name = "setup.py"
+ with Path(dist_path):
+ dist.parse_config_files()
+
+ dist.set_defaults()
+ return dist
+
+
+def _run_sdist_programatically(dist_path, attrs):
+ dist = _get_dist(dist_path, attrs)
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+ assert cmd.distribution.packages or cmd.distribution.py_modules
+
+ with quiet(), Path(dist_path):
+ cmd.run()
+
+ return dist, cmd
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_core_metadata.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_core_metadata.py
new file mode 100644
index 00000000..0d925111
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_core_metadata.py
@@ -0,0 +1,622 @@
+from __future__ import annotations
+
+import functools
+import importlib
+import io
+from email import message_from_string
+from email.generator import Generator
+from email.message import EmailMessage, Message
+from email.parser import Parser
+from email.policy import EmailPolicy
+from inspect import cleandoc
+from pathlib import Path
+from unittest.mock import Mock
+
+import jaraco.path
+import pytest
+from packaging.metadata import Metadata
+from packaging.requirements import Requirement
+
+from setuptools import _reqs, sic
+from setuptools._core_metadata import rfc822_escape, rfc822_unescape
+from setuptools.command.egg_info import egg_info, write_requirements
+from setuptools.config import expand, setupcfg
+from setuptools.dist import Distribution
+
+from .config.downloads import retrieve_file, urls_from_file
+
+EXAMPLE_BASE_INFO = dict(
+ name="package",
+ version="0.0.1",
+ author="Foo Bar",
+ author_email="foo@bar.net",
+ long_description="Long\ndescription",
+ description="Short description",
+ keywords=["one", "two"],
+)
+
+
+@pytest.mark.parametrize(
+ ("content", "result"),
+ (
+ pytest.param(
+ "Just a single line",
+ None,
+ id="single_line",
+ ),
+ pytest.param(
+ "Multiline\nText\nwithout\nextra indents\n",
+ None,
+ id="multiline",
+ ),
+ pytest.param(
+ "Multiline\n With\n\nadditional\n indentation",
+ None,
+ id="multiline_with_indentation",
+ ),
+ pytest.param(
+ " Leading whitespace",
+ "Leading whitespace",
+ id="remove_leading_whitespace",
+ ),
+ pytest.param(
+ " Leading whitespace\nIn\n Multiline comment",
+ "Leading whitespace\nIn\n Multiline comment",
+ id="remove_leading_whitespace_multiline",
+ ),
+ ),
+)
+def test_rfc822_unescape(content, result):
+ assert (result or content) == rfc822_unescape(rfc822_escape(content))
+
+
+def __read_test_cases():
+ base = EXAMPLE_BASE_INFO
+
+ params = functools.partial(dict, base)
+
+ return [
+ ('Metadata version 1.0', params()),
+ (
+ 'Metadata Version 1.0: Short long description',
+ params(
+ long_description='Short long description',
+ ),
+ ),
+ (
+ 'Metadata version 1.1: Classifiers',
+ params(
+ classifiers=[
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.7',
+ 'License :: OSI Approved :: MIT License',
+ ],
+ ),
+ ),
+ (
+ 'Metadata version 1.1: Download URL',
+ params(
+ download_url='https://example.com',
+ ),
+ ),
+ (
+ 'Metadata Version 1.2: Requires-Python',
+ params(
+ python_requires='>=3.7',
+ ),
+ ),
+ pytest.param(
+ 'Metadata Version 1.2: Project-Url',
+ params(project_urls=dict(Foo='https://example.bar')),
+ marks=pytest.mark.xfail(
+ reason="Issue #1578: project_urls not read",
+ ),
+ ),
+ (
+ 'Metadata Version 2.1: Long Description Content Type',
+ params(
+ long_description_content_type='text/x-rst; charset=UTF-8',
+ ),
+ ),
+ (
+ 'License',
+ params(
+ license='MIT',
+ ),
+ ),
+ (
+ 'License multiline',
+ params(
+ license='This is a long license \nover multiple lines',
+ ),
+ ),
+ pytest.param(
+ 'Metadata Version 2.1: Provides Extra',
+ params(provides_extras=['foo', 'bar']),
+ marks=pytest.mark.xfail(reason="provides_extras not read"),
+ ),
+ (
+ 'Missing author',
+ dict(
+ name='foo',
+ version='1.0.0',
+ author_email='snorri@sturluson.name',
+ ),
+ ),
+ (
+ 'Missing author e-mail',
+ dict(
+ name='foo',
+ version='1.0.0',
+ author='Snorri Sturluson',
+ ),
+ ),
+ (
+ 'Missing author and e-mail',
+ dict(
+ name='foo',
+ version='1.0.0',
+ ),
+ ),
+ (
+ 'Bypass normalized version',
+ dict(
+ name='foo',
+ version=sic('1.0.0a'),
+ ),
+ ),
+ ]
+
+
+@pytest.mark.parametrize(("name", "attrs"), __read_test_cases())
+def test_read_metadata(name, attrs):
+ dist = Distribution(attrs)
+ metadata_out = dist.metadata
+ dist_class = metadata_out.__class__
+
+ # Write to PKG_INFO and then load into a new metadata object
+ PKG_INFO = io.StringIO()
+
+ metadata_out.write_pkg_file(PKG_INFO)
+ PKG_INFO.seek(0)
+ pkg_info = PKG_INFO.read()
+ assert _valid_metadata(pkg_info)
+
+ PKG_INFO.seek(0)
+ metadata_in = dist_class()
+ metadata_in.read_pkg_file(PKG_INFO)
+
+ tested_attrs = [
+ ('name', dist_class.get_name),
+ ('version', dist_class.get_version),
+ ('author', dist_class.get_contact),
+ ('author_email', dist_class.get_contact_email),
+ ('metadata_version', dist_class.get_metadata_version),
+ ('provides', dist_class.get_provides),
+ ('description', dist_class.get_description),
+ ('long_description', dist_class.get_long_description),
+ ('download_url', dist_class.get_download_url),
+ ('keywords', dist_class.get_keywords),
+ ('platforms', dist_class.get_platforms),
+ ('obsoletes', dist_class.get_obsoletes),
+ ('requires', dist_class.get_requires),
+ ('classifiers', dist_class.get_classifiers),
+ ('project_urls', lambda s: getattr(s, 'project_urls', {})),
+ ('provides_extras', lambda s: getattr(s, 'provides_extras', {})),
+ ]
+
+ for attr, getter in tested_attrs:
+ assert getter(metadata_in) == getter(metadata_out)
+
+
+def __maintainer_test_cases():
+ attrs = {"name": "package", "version": "1.0", "description": "xxx"}
+
+ def merge_dicts(d1, d2):
+ d1 = d1.copy()
+ d1.update(d2)
+
+ return d1
+
+ return [
+ ('No author, no maintainer', attrs.copy()),
+ (
+ 'Author (no e-mail), no maintainer',
+ merge_dicts(attrs, {'author': 'Author Name'}),
+ ),
+ (
+ 'Author (e-mail), no maintainer',
+ merge_dicts(
+ attrs, {'author': 'Author Name', 'author_email': 'author@name.com'}
+ ),
+ ),
+ (
+ 'No author, maintainer (no e-mail)',
+ merge_dicts(attrs, {'maintainer': 'Maintainer Name'}),
+ ),
+ (
+ 'No author, maintainer (e-mail)',
+ merge_dicts(
+ attrs,
+ {
+ 'maintainer': 'Maintainer Name',
+ 'maintainer_email': 'maintainer@name.com',
+ },
+ ),
+ ),
+ (
+ 'Author (no e-mail), Maintainer (no-email)',
+ merge_dicts(
+ attrs, {'author': 'Author Name', 'maintainer': 'Maintainer Name'}
+ ),
+ ),
+ (
+ 'Author (e-mail), Maintainer (e-mail)',
+ merge_dicts(
+ attrs,
+ {
+ 'author': 'Author Name',
+ 'author_email': 'author@name.com',
+ 'maintainer': 'Maintainer Name',
+ 'maintainer_email': 'maintainer@name.com',
+ },
+ ),
+ ),
+ (
+ 'No author (e-mail), no maintainer (e-mail)',
+ merge_dicts(
+ attrs,
+ {
+ 'author_email': 'author@name.com',
+ 'maintainer_email': 'maintainer@name.com',
+ },
+ ),
+ ),
+ ('Author unicode', merge_dicts(attrs, {'author': '鉄沢寛'})),
+ ('Maintainer unicode', merge_dicts(attrs, {'maintainer': 'Jan Łukasiewicz'})),
+ ]
+
+
+@pytest.mark.parametrize(("name", "attrs"), __maintainer_test_cases())
+def test_maintainer_author(name, attrs, tmpdir):
+ tested_keys = {
+ 'author': 'Author',
+ 'author_email': 'Author-email',
+ 'maintainer': 'Maintainer',
+ 'maintainer_email': 'Maintainer-email',
+ }
+
+ # Generate a PKG-INFO file
+ dist = Distribution(attrs)
+ fn = tmpdir.mkdir('pkg_info')
+ fn_s = str(fn)
+
+ dist.metadata.write_pkg_info(fn_s)
+
+ with open(str(fn.join('PKG-INFO')), 'r', encoding='utf-8') as f:
+ pkg_info = f.read()
+
+ assert _valid_metadata(pkg_info)
+
+ # Drop blank lines and strip lines from default description
+ raw_pkg_lines = pkg_info.splitlines()
+ pkg_lines = list(filter(None, raw_pkg_lines[:-2]))
+
+ pkg_lines_set = set(pkg_lines)
+
+ # Duplicate lines should not be generated
+ assert len(pkg_lines) == len(pkg_lines_set)
+
+ for fkey, dkey in tested_keys.items():
+ val = attrs.get(dkey, None)
+ if val is None:
+ for line in pkg_lines:
+ assert not line.startswith(fkey + ':')
+ else:
+ line = f'{fkey}: {val}'
+ assert line in pkg_lines_set
+
+
+class TestParityWithMetadataFromPyPaWheel:
+ def base_example(self):
+ attrs = dict(
+ **EXAMPLE_BASE_INFO,
+ # Example with complex requirement definition
+ python_requires=">=3.8",
+ install_requires="""
+ packaging==23.2
+ more-itertools==8.8.0; extra == "other"
+ jaraco.text==3.7.0
+ importlib-resources==5.10.2; python_version<"3.8"
+ importlib-metadata==6.0.0 ; python_version<"3.8"
+ colorama>=0.4.4; sys_platform == "win32"
+ """,
+ extras_require={
+ "testing": """
+ pytest >= 6
+ pytest-checkdocs >= 2.4
+ tomli ; \\
+ # Using stdlib when possible
+ python_version < "3.11"
+ ini2toml[lite]>=0.9
+ """,
+ "other": [],
+ },
+ )
+ # Generate a PKG-INFO file using setuptools
+ return Distribution(attrs)
+
+ def test_requires_dist(self, tmp_path):
+ dist = self.base_example()
+ pkg_info = _get_pkginfo(dist)
+ assert _valid_metadata(pkg_info)
+
+ # Ensure Requires-Dist is present
+ expected = [
+ 'Metadata-Version:',
+ 'Requires-Python: >=3.8',
+ 'Provides-Extra: other',
+ 'Provides-Extra: testing',
+ 'Requires-Dist: tomli; python_version < "3.11" and extra == "testing"',
+ 'Requires-Dist: more-itertools==8.8.0; extra == "other"',
+ 'Requires-Dist: ini2toml[lite]>=0.9; extra == "testing"',
+ ]
+ for line in expected:
+ assert line in pkg_info
+
+ HERE = Path(__file__).parent
+ EXAMPLES_FILE = HERE / "config/setupcfg_examples.txt"
+
+ @pytest.fixture(params=[None, *urls_from_file(EXAMPLES_FILE)])
+ def dist(self, request, monkeypatch, tmp_path):
+ """Example of distribution with arbitrary configuration"""
+ monkeypatch.chdir(tmp_path)
+ monkeypatch.setattr(expand, "read_attr", Mock(return_value="0.42"))
+ monkeypatch.setattr(expand, "read_files", Mock(return_value="hello world"))
+ monkeypatch.setattr(
+ Distribution, "_finalize_license_files", Mock(return_value=None)
+ )
+ if request.param is None:
+ yield self.base_example()
+ else:
+ # Real-world usage
+ config = retrieve_file(request.param)
+ yield setupcfg.apply_configuration(Distribution({}), config)
+
+ @pytest.mark.uses_network
+ def test_equivalent_output(self, tmp_path, dist):
+ """Ensure output from setuptools is equivalent to the one from `pypa/wheel`"""
+ # Generate a METADATA file using pypa/wheel for comparison
+ wheel_metadata = importlib.import_module("wheel.metadata")
+ pkginfo_to_metadata = getattr(wheel_metadata, "pkginfo_to_metadata", None)
+
+ if pkginfo_to_metadata is None: # pragma: nocover
+ pytest.xfail(
+ "wheel.metadata.pkginfo_to_metadata is undefined, "
+ "(this is likely to be caused by API changes in pypa/wheel"
+ )
+
+ # Generate an simplified "egg-info" dir for pypa/wheel to convert
+ pkg_info = _get_pkginfo(dist)
+ egg_info_dir = tmp_path / "pkg.egg-info"
+ egg_info_dir.mkdir(parents=True)
+ (egg_info_dir / "PKG-INFO").write_text(pkg_info, encoding="utf-8")
+ write_requirements(egg_info(dist), egg_info_dir, egg_info_dir / "requires.txt")
+
+ # Get pypa/wheel generated METADATA but normalize requirements formatting
+ metadata_msg = pkginfo_to_metadata(egg_info_dir, egg_info_dir / "PKG-INFO")
+ metadata_str = _normalize_metadata(metadata_msg)
+ pkg_info_msg = message_from_string(pkg_info)
+ pkg_info_str = _normalize_metadata(pkg_info_msg)
+
+ # Compare setuptools PKG-INFO x pypa/wheel METADATA
+ assert metadata_str == pkg_info_str
+
+ # Make sure it parses/serializes well in pypa/wheel
+ _assert_roundtrip_message(pkg_info)
+
+
+class TestPEP643:
+ STATIC_CONFIG = {
+ "setup.cfg": cleandoc(
+ """
+ [metadata]
+ name = package
+ version = 0.0.1
+ author = Foo Bar
+ author_email = foo@bar.net
+ long_description = Long
+ description
+ description = Short description
+ keywords = one, two
+ platforms = abcd
+ [options]
+ install_requires = requests
+ """
+ ),
+ "pyproject.toml": cleandoc(
+ """
+ [project]
+ name = "package"
+ version = "0.0.1"
+ authors = [
+ {name = "Foo Bar", email = "foo@bar.net"}
+ ]
+ description = "Short description"
+ readme = {text = "Long\\ndescription", content-type = "text/plain"}
+ keywords = ["one", "two"]
+ dependencies = ["requests"]
+ license = "AGPL-3.0-or-later"
+ [tool.setuptools]
+ provides = ["abcd"]
+ obsoletes = ["abcd"]
+ """
+ ),
+ }
+
+ @pytest.mark.parametrize("file", STATIC_CONFIG.keys())
+ def test_static_config_has_no_dynamic(self, file, tmpdir_cwd):
+ Path(file).write_text(self.STATIC_CONFIG[file], encoding="utf-8")
+ metadata = _get_metadata()
+ assert metadata.get_all("Dynamic") is None
+ assert metadata.get_all("dynamic") is None
+
+ @pytest.mark.parametrize("file", STATIC_CONFIG.keys())
+ @pytest.mark.parametrize(
+ "fields",
+ [
+ # Single dynamic field
+ {"requires-python": ("python_requires", ">=3.12")},
+ {"author-email": ("author_email", "snoopy@peanuts.com")},
+ {"keywords": ("keywords", ["hello", "world"])},
+ {"platform": ("platforms", ["abcd"])},
+ # Multiple dynamic fields
+ {
+ "summary": ("description", "hello world"),
+ "description": ("long_description", "bla bla bla bla"),
+ "requires-dist": ("install_requires", ["hello-world"]),
+ },
+ ],
+ )
+ def test_modified_fields_marked_as_dynamic(self, file, fields, tmpdir_cwd):
+ # We start with a static config
+ Path(file).write_text(self.STATIC_CONFIG[file], encoding="utf-8")
+ dist = _makedist()
+
+ # ... but then we simulate the effects of a plugin modifying the distribution
+ for attr, value in fields.values():
+ # `dist` and `dist.metadata` are complicated...
+ # Some attributes work when set on `dist`, others on `dist.metadata`...
+ # Here we set in both just in case (this also avoids calling `_finalize_*`)
+ setattr(dist, attr, value)
+ setattr(dist.metadata, attr, value)
+
+ # Then we should be able to list the modified fields as Dynamic
+ metadata = _get_metadata(dist)
+ assert set(metadata.get_all("Dynamic")) == set(fields)
+
+ @pytest.mark.parametrize(
+ "extra_toml",
+ [
+ "# Let setuptools autofill license-files",
+ "license-files = ['LICENSE*', 'AUTHORS*', 'NOTICE']",
+ ],
+ )
+ def test_license_files_dynamic(self, extra_toml, tmpdir_cwd):
+ # For simplicity (and for the time being) setuptools is not making
+ # any special handling to guarantee `License-File` is considered static.
+ # Instead we rely in the fact that, although suboptimal, it is OK to have
+ # it as dynamics, as per:
+ # https://github.com/pypa/setuptools/issues/4629#issuecomment-2331233677
+ files = {
+ "pyproject.toml": self.STATIC_CONFIG["pyproject.toml"].replace(
+ 'license = "AGPL-3.0-or-later"',
+ f"dynamic = ['license']\n{extra_toml}",
+ ),
+ "LICENSE.md": "--- mock license ---",
+ "NOTICE": "--- mock notice ---",
+ "AUTHORS.txt": "--- me ---",
+ }
+ # Sanity checks:
+ assert extra_toml in files["pyproject.toml"]
+ assert 'license = "AGPL-3.0-or-later"' not in extra_toml
+
+ jaraco.path.build(files)
+ dist = _makedist(license_expression="AGPL-3.0-or-later")
+ metadata = _get_metadata(dist)
+ assert set(metadata.get_all("Dynamic")) == {
+ 'license-file',
+ 'license-expression',
+ }
+ assert metadata.get("License-Expression") == "AGPL-3.0-or-later"
+ assert set(metadata.get_all("License-File")) == {
+ "NOTICE",
+ "AUTHORS.txt",
+ "LICENSE.md",
+ }
+
+
+def _makedist(**attrs):
+ dist = Distribution(attrs)
+ dist.parse_config_files()
+ return dist
+
+
+def _assert_roundtrip_message(metadata: str) -> None:
+ """Emulate the way wheel.bdist_wheel parses and regenerates the message,
+ then ensures the metadata generated by setuptools is compatible.
+ """
+ with io.StringIO(metadata) as buffer:
+ msg = Parser(EmailMessage).parse(buffer)
+
+ serialization_policy = EmailPolicy(
+ utf8=True,
+ mangle_from_=False,
+ max_line_length=0,
+ )
+ with io.BytesIO() as buffer:
+ out = io.TextIOWrapper(buffer, encoding="utf-8")
+ Generator(out, policy=serialization_policy).flatten(msg)
+ out.flush()
+ regenerated = buffer.getvalue()
+
+ raw_metadata = bytes(metadata, "utf-8")
+ # Normalise newlines to avoid test errors on Windows:
+ raw_metadata = b"\n".join(raw_metadata.splitlines())
+ regenerated = b"\n".join(regenerated.splitlines())
+ assert regenerated == raw_metadata
+
+
+def _normalize_metadata(msg: Message) -> str:
+ """Allow equivalent metadata to be compared directly"""
+ # The main challenge regards the requirements and extras.
+ # Both setuptools and wheel already apply some level of normalization
+ # but they differ regarding which character is chosen, according to the
+ # following spec it should be "-":
+ # https://packaging.python.org/en/latest/specifications/name-normalization/
+
+ # Related issues:
+ # https://github.com/pypa/packaging/issues/845
+ # https://github.com/pypa/packaging/issues/644#issuecomment-2429813968
+
+ extras = {x.replace("_", "-"): x for x in msg.get_all("Provides-Extra", [])}
+ reqs = [
+ _normalize_req(req, extras)
+ for req in _reqs.parse(msg.get_all("Requires-Dist", []))
+ ]
+ del msg["Requires-Dist"]
+ del msg["Provides-Extra"]
+
+ # Ensure consistent ord
+ for req in sorted(reqs):
+ msg["Requires-Dist"] = req
+ for extra in sorted(extras):
+ msg["Provides-Extra"] = extra
+
+ # TODO: Handle lack of PEP 643 implementation in pypa/wheel?
+ del msg["Metadata-Version"]
+
+ return msg.as_string()
+
+
+def _normalize_req(req: Requirement, extras: dict[str, str]) -> str:
+ """Allow equivalent requirement objects to be compared directly"""
+ as_str = str(req).replace(req.name, req.name.replace("_", "-"))
+ for norm, orig in extras.items():
+ as_str = as_str.replace(orig, norm)
+ return as_str
+
+
+def _get_pkginfo(dist: Distribution):
+ with io.StringIO() as fp:
+ dist.metadata.write_pkg_file(fp)
+ return fp.getvalue()
+
+
+def _get_metadata(dist: Distribution | None = None):
+ return message_from_string(_get_pkginfo(dist or _makedist()))
+
+
+def _valid_metadata(text: str) -> bool:
+ metadata = Metadata.from_email(text, validate=True) # can raise exceptions
+ return metadata is not None
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_depends.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_depends.py
new file mode 100644
index 00000000..1714c041
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_depends.py
@@ -0,0 +1,15 @@
+import sys
+
+from setuptools import depends
+
+
+class TestGetModuleConstant:
+ def test_basic(self):
+ """
+ Invoke get_module_constant on a module in
+ the test package.
+ """
+ mod_name = 'setuptools.tests.mod_with_constant'
+ val = depends.get_module_constant(mod_name, 'value')
+ assert val == 'three, sir!'
+ assert 'setuptools.tests.mod_with_constant' not in sys.modules
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_develop.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_develop.py
new file mode 100644
index 00000000..929fa9c2
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_develop.py
@@ -0,0 +1,175 @@
+"""develop tests"""
+
+import os
+import pathlib
+import platform
+import subprocess
+import sys
+
+import pytest
+
+from setuptools._path import paths_on_pythonpath
+from setuptools.command.develop import develop
+from setuptools.dist import Distribution
+
+from . import contexts, namespaces
+
+SETUP_PY = """\
+from setuptools import setup
+
+setup(name='foo',
+ packages=['foo'],
+)
+"""
+
+INIT_PY = """print "foo"
+"""
+
+
+@pytest.fixture
+def temp_user(monkeypatch):
+ with contexts.tempdir() as user_base:
+ with contexts.tempdir() as user_site:
+ monkeypatch.setattr('site.USER_BASE', user_base)
+ monkeypatch.setattr('site.USER_SITE', user_site)
+ yield
+
+
+@pytest.fixture
+def test_env(tmpdir, temp_user):
+ target = tmpdir
+ foo = target.mkdir('foo')
+ setup = target / 'setup.py'
+ if setup.isfile():
+ raise ValueError(dir(target))
+ with setup.open('w') as f:
+ f.write(SETUP_PY)
+ init = foo / '__init__.py'
+ with init.open('w') as f:
+ f.write(INIT_PY)
+ with target.as_cwd():
+ yield target
+
+
+class TestDevelop:
+ in_virtualenv = hasattr(sys, 'real_prefix')
+ in_venv = hasattr(sys, 'base_prefix') and sys.base_prefix != sys.prefix
+
+ def test_console_scripts(self, tmpdir):
+ """
+ Test that console scripts are installed and that they reference
+ only the project by name and not the current version.
+ """
+ pytest.skip(
+ "TODO: needs a fixture to cause 'develop' "
+ "to be invoked without mutating environment."
+ )
+ settings = dict(
+ name='foo',
+ packages=['foo'],
+ version='0.0',
+ entry_points={
+ 'console_scripts': [
+ 'foocmd = foo:foo',
+ ],
+ },
+ )
+ dist = Distribution(settings)
+ dist.script_name = 'setup.py'
+ cmd = develop(dist)
+ cmd.ensure_finalized()
+ cmd.install_dir = tmpdir
+ cmd.run()
+ # assert '0.0' not in foocmd_text
+
+ @pytest.mark.xfail(reason="legacy behavior retained for compatibility #4167")
+ def test_egg_link_filename(self):
+ settings = dict(
+ name='Foo $$$ Bar_baz-bing',
+ )
+ dist = Distribution(settings)
+ cmd = develop(dist)
+ cmd.ensure_finalized()
+ link = pathlib.Path(cmd.egg_link)
+ assert link.suffix == '.egg-link'
+ assert link.stem == 'Foo_Bar_baz_bing'
+
+
+class TestResolver:
+ """
+ TODO: These tests were written with a minimal understanding
+ of what _resolve_setup_path is intending to do. Come up with
+ more meaningful cases that look like real-world scenarios.
+ """
+
+ def test_resolve_setup_path_cwd(self):
+ assert develop._resolve_setup_path('.', '.', '.') == '.'
+
+ def test_resolve_setup_path_one_dir(self):
+ assert develop._resolve_setup_path('pkgs', '.', 'pkgs') == '../'
+
+ def test_resolve_setup_path_one_dir_trailing_slash(self):
+ assert develop._resolve_setup_path('pkgs/', '.', 'pkgs') == '../'
+
+
+class TestNamespaces:
+ @staticmethod
+ def install_develop(src_dir, target):
+ develop_cmd = [
+ sys.executable,
+ 'setup.py',
+ 'develop',
+ '--install-dir',
+ str(target),
+ ]
+ with src_dir.as_cwd():
+ with paths_on_pythonpath([str(target)]):
+ subprocess.check_call(develop_cmd)
+
+ @pytest.mark.skipif(
+ bool(os.environ.get("APPVEYOR")),
+ reason="https://github.com/pypa/setuptools/issues/851",
+ )
+ @pytest.mark.skipif(
+ platform.python_implementation() == 'PyPy',
+ reason="https://github.com/pypa/setuptools/issues/1202",
+ )
+ def test_namespace_package_importable(self, tmpdir):
+ """
+ Installing two packages sharing the same namespace, one installed
+ naturally using pip or `--single-version-externally-managed`
+ and the other installed using `develop` should leave the namespace
+ in tact and both packages reachable by import.
+ """
+ pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA')
+ pkg_B = namespaces.build_namespace_package(tmpdir, 'myns.pkgB')
+ target = tmpdir / 'packages'
+ # use pip to install to the target directory
+ install_cmd = [
+ sys.executable,
+ '-m',
+ 'pip',
+ 'install',
+ str(pkg_A),
+ '-t',
+ str(target),
+ ]
+ subprocess.check_call(install_cmd)
+ self.install_develop(pkg_B, target)
+ namespaces.make_site_dir(target)
+ try_import = [
+ sys.executable,
+ '-c',
+ 'import myns.pkgA; import myns.pkgB',
+ ]
+ with paths_on_pythonpath([str(target)]):
+ subprocess.check_call(try_import)
+
+ # additionally ensure that pkg_resources import works
+ pkg_resources_imp = [
+ sys.executable,
+ '-c',
+ 'import pkg_resources',
+ ]
+ with paths_on_pythonpath([str(target)]):
+ subprocess.check_call(pkg_resources_imp)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_dist.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_dist.py
new file mode 100644
index 00000000..e65ab310
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_dist.py
@@ -0,0 +1,278 @@
+import os
+import re
+import urllib.parse
+import urllib.request
+
+import pytest
+
+from setuptools import Distribution
+from setuptools.dist import check_package_data, check_specifier
+
+from .test_easy_install import make_trivial_sdist
+from .test_find_packages import ensure_files
+from .textwrap import DALS
+
+from distutils.errors import DistutilsSetupError
+
+
+def test_dist_fetch_build_egg(tmpdir):
+ """
+ Check multiple calls to `Distribution.fetch_build_egg` work as expected.
+ """
+ index = tmpdir.mkdir('index')
+ index_url = urllib.parse.urljoin('file://', urllib.request.pathname2url(str(index)))
+
+ def sdist_with_index(distname, version):
+ dist_dir = index.mkdir(distname)
+ dist_sdist = f'{distname}-{version}.tar.gz'
+ make_trivial_sdist(str(dist_dir.join(dist_sdist)), distname, version)
+ with dist_dir.join('index.html').open('w') as fp:
+ fp.write(
+ DALS(
+ """
+ <!DOCTYPE html><html><body>
+ <a href="{dist_sdist}" rel="internal">{dist_sdist}</a><br/>
+ </body></html>
+ """
+ ).format(dist_sdist=dist_sdist)
+ )
+
+ sdist_with_index('barbazquux', '3.2.0')
+ sdist_with_index('barbazquux-runner', '2.11.1')
+ with tmpdir.join('setup.cfg').open('w') as fp:
+ fp.write(
+ DALS(
+ """
+ [easy_install]
+ index_url = {index_url}
+ """
+ ).format(index_url=index_url)
+ )
+ reqs = """
+ barbazquux-runner
+ barbazquux
+ """.split()
+ with tmpdir.as_cwd():
+ dist = Distribution()
+ dist.parse_config_files()
+ resolved_dists = [dist.fetch_build_egg(r) for r in reqs]
+ assert [dist.key for dist in resolved_dists if dist] == reqs
+
+
+EXAMPLE_BASE_INFO = dict(
+ name="package",
+ version="0.0.1",
+ author="Foo Bar",
+ author_email="foo@bar.net",
+ long_description="Long\ndescription",
+ description="Short description",
+ keywords=["one", "two"],
+)
+
+
+def test_provides_extras_deterministic_order():
+ attrs = dict(extras_require=dict(a=['foo'], b=['bar']))
+ dist = Distribution(attrs)
+ assert list(dist.metadata.provides_extras) == ['a', 'b']
+ attrs['extras_require'] = dict(reversed(attrs['extras_require'].items()))
+ dist = Distribution(attrs)
+ assert list(dist.metadata.provides_extras) == ['b', 'a']
+
+
+CHECK_PACKAGE_DATA_TESTS = (
+ # Valid.
+ (
+ {
+ '': ['*.txt', '*.rst'],
+ 'hello': ['*.msg'],
+ },
+ None,
+ ),
+ # Not a dictionary.
+ (
+ (
+ ('', ['*.txt', '*.rst']),
+ ('hello', ['*.msg']),
+ ),
+ (
+ "'package_data' must be a dictionary mapping package"
+ " names to lists of string wildcard patterns"
+ ),
+ ),
+ # Invalid key type.
+ (
+ {
+ 400: ['*.txt', '*.rst'],
+ },
+ ("keys of 'package_data' dict must be strings (got 400)"),
+ ),
+ # Invalid value type.
+ (
+ {
+ 'hello': '*.msg',
+ },
+ (
+ "\"values of 'package_data' dict\" must be of type <tuple[str, ...] | list[str]>"
+ " (got '*.msg')"
+ ),
+ ),
+ # Invalid value type (generators are single use)
+ (
+ {
+ 'hello': (x for x in "generator"),
+ },
+ (
+ "\"values of 'package_data' dict\" must be of type <tuple[str, ...] | list[str]>"
+ " (got <generator object"
+ ),
+ ),
+)
+
+
+@pytest.mark.parametrize(('package_data', 'expected_message'), CHECK_PACKAGE_DATA_TESTS)
+def test_check_package_data(package_data, expected_message):
+ if expected_message is None:
+ assert check_package_data(None, 'package_data', package_data) is None
+ else:
+ with pytest.raises(DistutilsSetupError, match=re.escape(expected_message)):
+ check_package_data(None, 'package_data', package_data)
+
+
+def test_check_specifier():
+ # valid specifier value
+ attrs = {'name': 'foo', 'python_requires': '>=3.0, !=3.1'}
+ dist = Distribution(attrs)
+ check_specifier(dist, attrs, attrs['python_requires'])
+
+ attrs = {'name': 'foo', 'python_requires': ['>=3.0', '!=3.1']}
+ dist = Distribution(attrs)
+ check_specifier(dist, attrs, attrs['python_requires'])
+
+ # invalid specifier value
+ attrs = {'name': 'foo', 'python_requires': '>=invalid-version'}
+ with pytest.raises(DistutilsSetupError):
+ dist = Distribution(attrs)
+
+
+def test_metadata_name():
+ with pytest.raises(DistutilsSetupError, match='missing.*name'):
+ Distribution()._validate_metadata()
+
+
+@pytest.mark.parametrize(
+ ('dist_name', 'py_module'),
+ [
+ ("my.pkg", "my_pkg"),
+ ("my-pkg", "my_pkg"),
+ ("my_pkg", "my_pkg"),
+ ("pkg", "pkg"),
+ ],
+)
+def test_dist_default_py_modules(tmp_path, dist_name, py_module):
+ (tmp_path / f"{py_module}.py").touch()
+
+ (tmp_path / "setup.py").touch()
+ (tmp_path / "noxfile.py").touch()
+ # ^-- make sure common tool files are ignored
+
+ attrs = {**EXAMPLE_BASE_INFO, "name": dist_name, "src_root": str(tmp_path)}
+ # Find `py_modules` corresponding to dist_name if not given
+ dist = Distribution(attrs)
+ dist.set_defaults()
+ assert dist.py_modules == [py_module]
+ # When `py_modules` is given, don't do anything
+ dist = Distribution({**attrs, "py_modules": ["explicity_py_module"]})
+ dist.set_defaults()
+ assert dist.py_modules == ["explicity_py_module"]
+ # When `packages` is given, don't do anything
+ dist = Distribution({**attrs, "packages": ["explicity_package"]})
+ dist.set_defaults()
+ assert not dist.py_modules
+
+
+@pytest.mark.parametrize(
+ ('dist_name', 'package_dir', 'package_files', 'packages'),
+ [
+ ("my.pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]),
+ ("my-pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]),
+ ("my_pkg", None, ["my_pkg/__init__.py", "my_pkg/mod.py"], ["my_pkg"]),
+ ("my.pkg", None, ["my/pkg/__init__.py"], ["my", "my.pkg"]),
+ (
+ "my_pkg",
+ None,
+ ["src/my_pkg/__init__.py", "src/my_pkg2/__init__.py"],
+ ["my_pkg", "my_pkg2"],
+ ),
+ (
+ "my_pkg",
+ {"pkg": "lib", "pkg2": "lib2"},
+ ["lib/__init__.py", "lib/nested/__init__.pyt", "lib2/__init__.py"],
+ ["pkg", "pkg.nested", "pkg2"],
+ ),
+ ],
+)
+def test_dist_default_packages(
+ tmp_path, dist_name, package_dir, package_files, packages
+):
+ ensure_files(tmp_path, package_files)
+
+ (tmp_path / "setup.py").touch()
+ (tmp_path / "noxfile.py").touch()
+ # ^-- should not be included by default
+
+ attrs = {
+ **EXAMPLE_BASE_INFO,
+ "name": dist_name,
+ "src_root": str(tmp_path),
+ "package_dir": package_dir,
+ }
+ # Find `packages` either corresponding to dist_name or inside src
+ dist = Distribution(attrs)
+ dist.set_defaults()
+ assert not dist.py_modules
+ assert not dist.py_modules
+ assert set(dist.packages) == set(packages)
+ # When `py_modules` is given, don't do anything
+ dist = Distribution({**attrs, "py_modules": ["explicit_py_module"]})
+ dist.set_defaults()
+ assert not dist.packages
+ assert set(dist.py_modules) == {"explicit_py_module"}
+ # When `packages` is given, don't do anything
+ dist = Distribution({**attrs, "packages": ["explicit_package"]})
+ dist.set_defaults()
+ assert not dist.py_modules
+ assert set(dist.packages) == {"explicit_package"}
+
+
+@pytest.mark.parametrize(
+ ('dist_name', 'package_dir', 'package_files'),
+ [
+ ("my.pkg.nested", None, ["my/pkg/nested/__init__.py"]),
+ ("my.pkg", None, ["my/pkg/__init__.py", "my/pkg/file.py"]),
+ ("my_pkg", None, ["my_pkg.py"]),
+ ("my_pkg", None, ["my_pkg/__init__.py", "my_pkg/nested/__init__.py"]),
+ ("my_pkg", None, ["src/my_pkg/__init__.py", "src/my_pkg/nested/__init__.py"]),
+ (
+ "my_pkg",
+ {"my_pkg": "lib", "my_pkg.lib2": "lib2"},
+ ["lib/__init__.py", "lib/nested/__init__.pyt", "lib2/__init__.py"],
+ ),
+ # Should not try to guess a name from multiple py_modules/packages
+ ("UNKNOWN", None, ["src/mod1.py", "src/mod2.py"]),
+ ("UNKNOWN", None, ["src/pkg1/__ini__.py", "src/pkg2/__init__.py"]),
+ ],
+)
+def test_dist_default_name(tmp_path, dist_name, package_dir, package_files):
+ """Make sure dist.name is discovered from packages/py_modules"""
+ ensure_files(tmp_path, package_files)
+ attrs = {
+ **EXAMPLE_BASE_INFO,
+ "src_root": "/".join(os.path.split(tmp_path)), # POSIX-style
+ "package_dir": package_dir,
+ }
+ del attrs["name"]
+
+ dist = Distribution(attrs)
+ dist.set_defaults()
+ assert dist.py_modules or dist.packages
+ assert dist.get_name() == dist_name
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_dist_info.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_dist_info.py
new file mode 100644
index 00000000..426694e0
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_dist_info.py
@@ -0,0 +1,210 @@
+"""Test .dist-info style distributions."""
+
+import pathlib
+import re
+import shutil
+import subprocess
+import sys
+from functools import partial
+
+import pytest
+
+import pkg_resources
+from setuptools.archive_util import unpack_archive
+
+from .textwrap import DALS
+
+read = partial(pathlib.Path.read_text, encoding="utf-8")
+
+
+class TestDistInfo:
+ metadata_base = DALS(
+ """
+ Metadata-Version: 1.2
+ Requires-Dist: splort (==4)
+ Provides-Extra: baz
+ Requires-Dist: quux (>=1.1); extra == 'baz'
+ """
+ )
+
+ @classmethod
+ def build_metadata(cls, **kwargs):
+ lines = ('{key}: {value}\n'.format(**locals()) for key, value in kwargs.items())
+ return cls.metadata_base + ''.join(lines)
+
+ @pytest.fixture
+ def metadata(self, tmpdir):
+ dist_info_name = 'VersionedDistribution-2.718.dist-info'
+ versioned = tmpdir / dist_info_name
+ versioned.mkdir()
+ filename = versioned / 'METADATA'
+ content = self.build_metadata(
+ Name='VersionedDistribution',
+ )
+ filename.write_text(content, encoding='utf-8')
+
+ dist_info_name = 'UnversionedDistribution.dist-info'
+ unversioned = tmpdir / dist_info_name
+ unversioned.mkdir()
+ filename = unversioned / 'METADATA'
+ content = self.build_metadata(
+ Name='UnversionedDistribution',
+ Version='0.3',
+ )
+ filename.write_text(content, encoding='utf-8')
+
+ return str(tmpdir)
+
+ def test_distinfo(self, metadata):
+ dists = dict(
+ (d.project_name, d) for d in pkg_resources.find_distributions(metadata)
+ )
+
+ assert len(dists) == 2, dists
+
+ unversioned = dists['UnversionedDistribution']
+ versioned = dists['VersionedDistribution']
+
+ assert versioned.version == '2.718' # from filename
+ assert unversioned.version == '0.3' # from METADATA
+
+ def test_conditional_dependencies(self, metadata):
+ specs = 'splort==4', 'quux>=1.1'
+ requires = list(map(pkg_resources.Requirement.parse, specs))
+
+ for d in pkg_resources.find_distributions(metadata):
+ assert d.requires() == requires[:1]
+ assert d.requires(extras=('baz',)) == [
+ requires[0],
+ pkg_resources.Requirement.parse('quux>=1.1;extra=="baz"'),
+ ]
+ assert d.extras == ['baz']
+
+ def test_invalid_version(self, tmp_path):
+ """
+ Supplying an invalid version crashes dist_info.
+ """
+ config = "[metadata]\nname=proj\nversion=42\n[egg_info]\ntag_build=invalid!!!\n"
+ (tmp_path / "setup.cfg").write_text(config, encoding="utf-8")
+ msg = re.compile("invalid version", re.M | re.I)
+ proc = run_command_inner("dist_info", cwd=tmp_path, check=False)
+ assert proc.returncode
+ assert msg.search(proc.stdout)
+ assert not list(tmp_path.glob("*.dist-info"))
+
+ def test_tag_arguments(self, tmp_path):
+ config = """
+ [metadata]
+ name=proj
+ version=42
+ [egg_info]
+ tag_date=1
+ tag_build=.post
+ """
+ (tmp_path / "setup.cfg").write_text(config, encoding="utf-8")
+
+ print(run_command("dist_info", "--no-date", cwd=tmp_path))
+ dist_info = next(tmp_path.glob("*.dist-info"))
+ assert dist_info.name.startswith("proj-42")
+ shutil.rmtree(dist_info)
+
+ print(run_command("dist_info", "--tag-build", ".a", cwd=tmp_path))
+ dist_info = next(tmp_path.glob("*.dist-info"))
+ assert dist_info.name.startswith("proj-42a")
+
+ @pytest.mark.parametrize("keep_egg_info", (False, True))
+ def test_output_dir(self, tmp_path, keep_egg_info):
+ config = "[metadata]\nname=proj\nversion=42\n"
+ (tmp_path / "setup.cfg").write_text(config, encoding="utf-8")
+ out = tmp_path / "__out"
+ out.mkdir()
+ opts = ["--keep-egg-info"] if keep_egg_info else []
+ run_command("dist_info", "--output-dir", out, *opts, cwd=tmp_path)
+ assert len(list(out.glob("*.dist-info"))) == 1
+ assert len(list(tmp_path.glob("*.dist-info"))) == 0
+ expected_egg_info = int(keep_egg_info)
+ assert len(list(out.glob("*.egg-info"))) == expected_egg_info
+ assert len(list(tmp_path.glob("*.egg-info"))) == 0
+ assert len(list(out.glob("*.__bkp__"))) == 0
+ assert len(list(tmp_path.glob("*.__bkp__"))) == 0
+
+
+class TestWheelCompatibility:
+ """Make sure the .dist-info directory produced with the ``dist_info`` command
+ is the same as the one produced by ``bdist_wheel``.
+ """
+
+ SETUPCFG = DALS(
+ """
+ [metadata]
+ name = {name}
+ version = {version}
+
+ [options]
+ install_requires =
+ foo>=12; sys_platform != "linux"
+
+ [options.extras_require]
+ test = pytest
+
+ [options.entry_points]
+ console_scripts =
+ executable-name = my_package.module:function
+ discover =
+ myproj = my_package.other_module:function
+ """
+ )
+
+ EGG_INFO_OPTS = [
+ # Related: #3088 #2872
+ ("", ""),
+ (".post", "[egg_info]\ntag_build = post\n"),
+ (".post", "[egg_info]\ntag_build = .post\n"),
+ (".post", "[egg_info]\ntag_build = post\ntag_date = 1\n"),
+ (".dev", "[egg_info]\ntag_build = .dev\n"),
+ (".dev", "[egg_info]\ntag_build = .dev\ntag_date = 1\n"),
+ ("a1", "[egg_info]\ntag_build = .a1\n"),
+ ("+local", "[egg_info]\ntag_build = +local\n"),
+ ]
+
+ @pytest.mark.parametrize("name", "my-proj my_proj my.proj My.Proj".split())
+ @pytest.mark.parametrize("version", ["0.42.13"])
+ @pytest.mark.parametrize(("suffix", "cfg"), EGG_INFO_OPTS)
+ def test_dist_info_is_the_same_as_in_wheel(
+ self, name, version, tmp_path, suffix, cfg
+ ):
+ config = self.SETUPCFG.format(name=name, version=version) + cfg
+
+ for i in "dir_wheel", "dir_dist":
+ (tmp_path / i).mkdir()
+ (tmp_path / i / "setup.cfg").write_text(config, encoding="utf-8")
+
+ run_command("bdist_wheel", cwd=tmp_path / "dir_wheel")
+ wheel = next(tmp_path.glob("dir_wheel/dist/*.whl"))
+ unpack_archive(wheel, tmp_path / "unpack")
+ wheel_dist_info = next(tmp_path.glob("unpack/*.dist-info"))
+
+ run_command("dist_info", cwd=tmp_path / "dir_dist")
+ dist_info = next(tmp_path.glob("dir_dist/*.dist-info"))
+
+ assert dist_info.name == wheel_dist_info.name
+ assert dist_info.name.startswith(f"my_proj-{version}{suffix}")
+ for file in "METADATA", "entry_points.txt":
+ assert read(dist_info / file) == read(wheel_dist_info / file)
+
+
+def run_command_inner(*cmd, **kwargs):
+ opts = {
+ "stderr": subprocess.STDOUT,
+ "stdout": subprocess.PIPE,
+ "text": True,
+ "encoding": "utf-8",
+ "check": True,
+ **kwargs,
+ }
+ cmd = [sys.executable, "-c", "__import__('setuptools').setup()", *map(str, cmd)]
+ return subprocess.run(cmd, **opts)
+
+
+def run_command(*args, **kwargs):
+ return run_command_inner(*args, **kwargs).stdout
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_distutils_adoption.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_distutils_adoption.py
new file mode 100644
index 00000000..f99a5884
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_distutils_adoption.py
@@ -0,0 +1,198 @@
+import os
+import platform
+import sys
+import textwrap
+
+import pytest
+
+IS_PYPY = '__pypy__' in sys.builtin_module_names
+
+_TEXT_KWARGS = {"text": True, "encoding": "utf-8"} # For subprocess.run
+
+
+def win_sr(env):
+ """
+ On Windows, SYSTEMROOT must be present to avoid
+
+ > Fatal Python error: _Py_HashRandomization_Init: failed to
+ > get random numbers to initialize Python
+ """
+ if env and platform.system() == 'Windows':
+ env['SYSTEMROOT'] = os.environ['SYSTEMROOT']
+ return env
+
+
+def find_distutils(venv, imports='distutils', env=None, **kwargs):
+ py_cmd = 'import {imports}; print(distutils.__file__)'.format(**locals())
+ cmd = ['python', '-c', py_cmd]
+ return venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS, **kwargs)
+
+
+def count_meta_path(venv, env=None):
+ py_cmd = textwrap.dedent(
+ """
+ import sys
+ is_distutils = lambda finder: finder.__class__.__name__ == "DistutilsMetaFinder"
+ print(len(list(filter(is_distutils, sys.meta_path))))
+ """
+ )
+ cmd = ['python', '-c', py_cmd]
+ return int(venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS))
+
+
+skip_without_stdlib_distutils = pytest.mark.skipif(
+ sys.version_info >= (3, 12),
+ reason='stdlib distutils is removed from Python 3.12+',
+)
+
+
+@skip_without_stdlib_distutils
+def test_distutils_stdlib(venv):
+ """
+ Ensure stdlib distutils is used when appropriate.
+ """
+ env = dict(SETUPTOOLS_USE_DISTUTILS='stdlib')
+ assert venv.name not in find_distutils(venv, env=env).split(os.sep)
+ assert count_meta_path(venv, env=env) == 0
+
+
+def test_distutils_local_with_setuptools(venv):
+ """
+ Ensure local distutils is used when appropriate.
+ """
+ env = dict(SETUPTOOLS_USE_DISTUTILS='local')
+ loc = find_distutils(venv, imports='setuptools, distutils', env=env)
+ assert venv.name in loc.split(os.sep)
+ assert count_meta_path(venv, env=env) <= 1
+
+
+@pytest.mark.xfail('IS_PYPY', reason='pypy imports distutils on startup')
+def test_distutils_local(venv):
+ """
+ Even without importing, the setuptools-local copy of distutils is
+ preferred.
+ """
+ env = dict(SETUPTOOLS_USE_DISTUTILS='local')
+ assert venv.name in find_distutils(venv, env=env).split(os.sep)
+ assert count_meta_path(venv, env=env) <= 1
+
+
+def test_pip_import(venv):
+ """
+ Ensure pip can be imported.
+ Regression test for #3002.
+ """
+ cmd = ['python', '-c', 'import pip']
+ venv.run(cmd, **_TEXT_KWARGS)
+
+
+def test_distutils_has_origin():
+ """
+ Distutils module spec should have an origin. #2990.
+ """
+ assert __import__('distutils').__spec__.origin
+
+
+ENSURE_IMPORTS_ARE_NOT_DUPLICATED = r"""
+# Depending on the importlib machinery and _distutils_hack, some imports are
+# duplicated resulting in different module objects being loaded, which prevents
+# patches as shown in #3042.
+# This script provides a way of verifying if this duplication is happening.
+
+from distutils import cmd
+import distutils.command.sdist as sdist
+
+# import last to prevent caching
+from distutils import {imported_module}
+
+for mod in (cmd, sdist):
+ assert mod.{imported_module} == {imported_module}, (
+ f"\n{{mod.dir_util}}\n!=\n{{{imported_module}}}"
+ )
+
+print("success")
+"""
+
+
+@pytest.mark.usefixtures("tmpdir_cwd")
+@pytest.mark.parametrize(
+ ('distutils_version', 'imported_module'),
+ [
+ pytest.param("stdlib", "dir_util", marks=skip_without_stdlib_distutils),
+ pytest.param("stdlib", "file_util", marks=skip_without_stdlib_distutils),
+ pytest.param("stdlib", "archive_util", marks=skip_without_stdlib_distutils),
+ ("local", "dir_util"),
+ ("local", "file_util"),
+ ("local", "archive_util"),
+ ],
+)
+def test_modules_are_not_duplicated_on_import(distutils_version, imported_module, venv):
+ env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version)
+ script = ENSURE_IMPORTS_ARE_NOT_DUPLICATED.format(imported_module=imported_module)
+ cmd = ['python', '-c', script]
+ output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip()
+ assert output == "success"
+
+
+ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED = r"""
+import types
+import distutils.dist as dist
+from distutils import log
+if isinstance(dist.log, types.ModuleType):
+ assert dist.log == log, f"\n{dist.log}\n!=\n{log}"
+print("success")
+"""
+
+
+@pytest.mark.usefixtures("tmpdir_cwd")
+@pytest.mark.parametrize(
+ "distutils_version",
+ [
+ "local",
+ pytest.param("stdlib", marks=skip_without_stdlib_distutils),
+ ],
+)
+def test_log_module_is_not_duplicated_on_import(distutils_version, venv):
+ env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version)
+ cmd = ['python', '-c', ENSURE_LOG_IMPORT_IS_NOT_DUPLICATED]
+ output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip()
+ assert output == "success"
+
+
+ENSURE_CONSISTENT_ERROR_FROM_MODIFIED_PY = r"""
+from setuptools.modified import newer
+from {imported_module}.errors import DistutilsError
+
+# Can't use pytest.raises in this context
+try:
+ newer("", "")
+except DistutilsError:
+ print("success")
+else:
+ raise AssertionError("Expected to raise")
+"""
+
+
+@pytest.mark.usefixtures("tmpdir_cwd")
+@pytest.mark.parametrize(
+ ('distutils_version', 'imported_module'),
+ [
+ ("local", "distutils"),
+ # Unfortunately we still get ._distutils.errors.DistutilsError with SETUPTOOLS_USE_DISTUTILS=stdlib
+ # But that's a deprecated use-case we don't mind not fully supporting in newer code
+ pytest.param(
+ "stdlib", "setuptools._distutils", marks=skip_without_stdlib_distutils
+ ),
+ ],
+)
+def test_consistent_error_from_modified_py(distutils_version, imported_module, venv):
+ env = dict(SETUPTOOLS_USE_DISTUTILS=distutils_version)
+ cmd = [
+ 'python',
+ '-c',
+ ENSURE_CONSISTENT_ERROR_FROM_MODIFIED_PY.format(
+ imported_module=imported_module
+ ),
+ ]
+ output = venv.run(cmd, env=win_sr(env), **_TEXT_KWARGS).strip()
+ assert output == "success"
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_easy_install.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_easy_install.py
new file mode 100644
index 00000000..b58b0b66
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_easy_install.py
@@ -0,0 +1,1476 @@
+"""Easy install Tests"""
+
+import contextlib
+import io
+import itertools
+import logging
+import os
+import pathlib
+import re
+import site
+import subprocess
+import sys
+import tarfile
+import tempfile
+import time
+import warnings
+import zipfile
+from pathlib import Path
+from typing import NamedTuple
+from unittest import mock
+
+import pytest
+from jaraco import path
+
+import pkg_resources
+import setuptools.command.easy_install as ei
+from pkg_resources import Distribution as PRDistribution, normalize_path, working_set
+from setuptools import sandbox
+from setuptools._normalization import safer_name
+from setuptools.command.easy_install import PthDistributions
+from setuptools.dist import Distribution
+from setuptools.sandbox import run_setup
+from setuptools.tests import fail_on_ascii
+from setuptools.tests.server import MockServer, path_to_url
+
+from . import contexts
+from .textwrap import DALS
+
+import distutils.errors
+
+
+@pytest.fixture(autouse=True)
+def pip_disable_index(monkeypatch):
+ """
+ Important: Disable the default index for pip to avoid
+ querying packages in the index and potentially resolving
+ and installing packages there.
+ """
+ monkeypatch.setenv('PIP_NO_INDEX', 'true')
+
+
+class FakeDist:
+ def get_entry_map(self, group):
+ if group != 'console_scripts':
+ return {}
+ return {'name': 'ep'}
+
+ def as_requirement(self):
+ return 'spec'
+
+
+SETUP_PY = DALS(
+ """
+ from setuptools import setup
+
+ setup()
+ """
+)
+
+
+class TestEasyInstallTest:
+ def test_get_script_args(self):
+ header = ei.CommandSpec.best().from_environment().as_header()
+ dist = FakeDist()
+ args = next(ei.ScriptWriter.get_args(dist))
+ _name, script = itertools.islice(args, 2)
+ assert script.startswith(header)
+ assert "'spec'" in script
+ assert "'console_scripts'" in script
+ assert "'name'" in script
+ assert re.search('^# EASY-INSTALL-ENTRY-SCRIPT', script, flags=re.MULTILINE)
+
+ def test_no_find_links(self):
+ # new option '--no-find-links', that blocks find-links added at
+ # the project level
+ dist = Distribution()
+ cmd = ei.easy_install(dist)
+ cmd.check_pth_processing = lambda: True
+ cmd.no_find_links = True
+ cmd.find_links = ['link1', 'link2']
+ cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
+ cmd.args = ['ok']
+ cmd.ensure_finalized()
+ assert cmd.package_index.scanned_urls == {}
+
+ # let's try without it (default behavior)
+ cmd = ei.easy_install(dist)
+ cmd.check_pth_processing = lambda: True
+ cmd.find_links = ['link1', 'link2']
+ cmd.install_dir = os.path.join(tempfile.mkdtemp(), 'ok')
+ cmd.args = ['ok']
+ cmd.ensure_finalized()
+ keys = sorted(cmd.package_index.scanned_urls.keys())
+ assert keys == ['link1', 'link2']
+
+ def test_write_exception(self):
+ """
+ Test that `cant_write_to_target` is rendered as a DistutilsError.
+ """
+ dist = Distribution()
+ cmd = ei.easy_install(dist)
+ cmd.install_dir = os.getcwd()
+ with pytest.raises(distutils.errors.DistutilsError):
+ cmd.cant_write_to_target()
+
+ def test_all_site_dirs(self, monkeypatch):
+ """
+ get_site_dirs should always return site dirs reported by
+ site.getsitepackages.
+ """
+ path = normalize_path('/setuptools/test/site-packages')
+
+ def mock_gsp():
+ return [path]
+
+ monkeypatch.setattr(site, 'getsitepackages', mock_gsp, raising=False)
+ assert path in ei.get_site_dirs()
+
+ def test_all_site_dirs_works_without_getsitepackages(self, monkeypatch):
+ monkeypatch.delattr(site, 'getsitepackages', raising=False)
+ assert ei.get_site_dirs()
+
+ @pytest.fixture
+ def sdist_unicode(self, tmpdir):
+ files = [
+ (
+ 'setup.py',
+ DALS(
+ """
+ import setuptools
+ setuptools.setup(
+ name="setuptools-test-unicode",
+ version="1.0",
+ packages=["mypkg"],
+ include_package_data=True,
+ )
+ """
+ ),
+ ),
+ (
+ 'mypkg/__init__.py',
+ "",
+ ),
+ (
+ 'mypkg/☃.txt',
+ "",
+ ),
+ ]
+ sdist_name = 'setuptools-test-unicode-1.0.zip'
+ sdist = tmpdir / sdist_name
+ # can't use make_sdist, because the issue only occurs
+ # with zip sdists.
+ sdist_zip = zipfile.ZipFile(str(sdist), 'w')
+ for filename, content in files:
+ sdist_zip.writestr(filename, content)
+ sdist_zip.close()
+ return str(sdist)
+
+ @fail_on_ascii
+ def test_unicode_filename_in_sdist(self, sdist_unicode, tmpdir, monkeypatch):
+ """
+ The install command should execute correctly even if
+ the package has unicode filenames.
+ """
+ dist = Distribution({'script_args': ['easy_install']})
+ target = (tmpdir / 'target').ensure_dir()
+ cmd = ei.easy_install(
+ dist,
+ install_dir=str(target),
+ args=['x'],
+ )
+ monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
+ cmd.ensure_finalized()
+ cmd.easy_install(sdist_unicode)
+
+ @pytest.fixture
+ def sdist_unicode_in_script(self, tmpdir):
+ files = [
+ (
+ "setup.py",
+ DALS(
+ """
+ import setuptools
+ setuptools.setup(
+ name="setuptools-test-unicode",
+ version="1.0",
+ packages=["mypkg"],
+ include_package_data=True,
+ scripts=['mypkg/unicode_in_script'],
+ )
+ """
+ ),
+ ),
+ ("mypkg/__init__.py", ""),
+ (
+ "mypkg/unicode_in_script",
+ DALS(
+ """
+ #!/bin/sh
+ # á
+
+ non_python_fn() {
+ }
+ """
+ ),
+ ),
+ ]
+ sdist_name = "setuptools-test-unicode-script-1.0.zip"
+ sdist = tmpdir / sdist_name
+ # can't use make_sdist, because the issue only occurs
+ # with zip sdists.
+ sdist_zip = zipfile.ZipFile(str(sdist), "w")
+ for filename, content in files:
+ sdist_zip.writestr(filename, content.encode('utf-8'))
+ sdist_zip.close()
+ return str(sdist)
+
+ @fail_on_ascii
+ def test_unicode_content_in_sdist(
+ self, sdist_unicode_in_script, tmpdir, monkeypatch
+ ):
+ """
+ The install command should execute correctly even if
+ the package has unicode in scripts.
+ """
+ dist = Distribution({"script_args": ["easy_install"]})
+ target = (tmpdir / "target").ensure_dir()
+ cmd = ei.easy_install(dist, install_dir=str(target), args=["x"])
+ monkeypatch.setitem(os.environ, "PYTHONPATH", str(target))
+ cmd.ensure_finalized()
+ cmd.easy_install(sdist_unicode_in_script)
+
+ @pytest.fixture
+ def sdist_script(self, tmpdir):
+ files = [
+ (
+ 'setup.py',
+ DALS(
+ """
+ import setuptools
+ setuptools.setup(
+ name="setuptools-test-script",
+ version="1.0",
+ scripts=["mypkg_script"],
+ )
+ """
+ ),
+ ),
+ (
+ 'mypkg_script',
+ DALS(
+ """
+ #/usr/bin/python
+ print('mypkg_script')
+ """
+ ),
+ ),
+ ]
+ sdist_name = 'setuptools-test-script-1.0.zip'
+ sdist = str(tmpdir / sdist_name)
+ make_sdist(sdist, files)
+ return sdist
+
+ @pytest.mark.skipif(
+ not sys.platform.startswith('linux'), reason="Test can only be run on Linux"
+ )
+ def test_script_install(self, sdist_script, tmpdir, monkeypatch):
+ """
+ Check scripts are installed.
+ """
+ dist = Distribution({'script_args': ['easy_install']})
+ target = (tmpdir / 'target').ensure_dir()
+ cmd = ei.easy_install(
+ dist,
+ install_dir=str(target),
+ args=['x'],
+ )
+ monkeypatch.setitem(os.environ, 'PYTHONPATH', str(target))
+ cmd.ensure_finalized()
+ cmd.easy_install(sdist_script)
+ assert (target / 'mypkg_script').exists()
+
+
+@pytest.mark.filterwarnings('ignore:Unbuilt egg')
+class TestPTHFileWriter:
+ def test_add_from_cwd_site_sets_dirty(self):
+ """a pth file manager should set dirty
+ if a distribution is in site but also the cwd
+ """
+ pth = PthDistributions('does-not_exist', [os.getcwd()])
+ assert not pth.dirty
+ pth.add(PRDistribution(os.getcwd()))
+ assert pth.dirty
+
+ def test_add_from_site_is_ignored(self):
+ location = '/test/location/does-not-have-to-exist'
+ # PthDistributions expects all locations to be normalized
+ location = pkg_resources.normalize_path(location)
+ pth = PthDistributions(
+ 'does-not_exist',
+ [
+ location,
+ ],
+ )
+ assert not pth.dirty
+ pth.add(PRDistribution(location))
+ assert not pth.dirty
+
+ def test_many_pth_distributions_merge_together(self, tmpdir):
+ """
+ If the pth file is modified under the hood, then PthDistribution
+ will refresh its content before saving, merging contents when
+ necessary.
+ """
+ # putting the pth file in a dedicated sub-folder,
+ pth_subdir = tmpdir.join("pth_subdir")
+ pth_subdir.mkdir()
+ pth_path = str(pth_subdir.join("file1.pth"))
+ pth1 = PthDistributions(pth_path)
+ pth2 = PthDistributions(pth_path)
+ assert pth1.paths == pth2.paths == [], (
+ "unless there would be some default added at some point"
+ )
+ # and so putting the src_subdir in folder distinct than the pth one,
+ # so to keep it absolute by PthDistributions
+ new_src_path = tmpdir.join("src_subdir")
+ new_src_path.mkdir() # must exist to be accounted
+ new_src_path_str = str(new_src_path)
+ pth1.paths.append(new_src_path_str)
+ pth1.save()
+ assert pth1.paths, (
+ "the new_src_path added must still be present/valid in pth1 after save"
+ )
+ # now,
+ assert new_src_path_str not in pth2.paths, (
+ "right before we save the entry should still not be present"
+ )
+ pth2.save()
+ assert new_src_path_str in pth2.paths, (
+ "the new_src_path entry should have been added by pth2 with its save() call"
+ )
+ assert pth2.paths[-1] == new_src_path, (
+ "and it should match exactly on the last entry actually "
+ "given we append to it in save()"
+ )
+ # finally,
+ assert PthDistributions(pth_path).paths == pth2.paths, (
+ "and we should have the exact same list at the end "
+ "with a fresh PthDistributions instance"
+ )
+
+
+@pytest.fixture
+def setup_context(tmpdir):
+ with (tmpdir / 'setup.py').open('w', encoding="utf-8") as f:
+ f.write(SETUP_PY)
+ with tmpdir.as_cwd():
+ yield tmpdir
+
+
+@pytest.mark.usefixtures("user_override")
+@pytest.mark.usefixtures("setup_context")
+class TestUserInstallTest:
+ # prevent check that site-packages is writable. easy_install
+ # shouldn't be writing to system site-packages during finalize
+ # options, but while it does, bypass the behavior.
+ prev_sp_write = mock.patch(
+ 'setuptools.command.easy_install.easy_install.check_site_dir',
+ mock.Mock(),
+ )
+
+ # simulate setuptools installed in user site packages
+ @mock.patch('setuptools.command.easy_install.__file__', site.USER_SITE)
+ @mock.patch('site.ENABLE_USER_SITE', True)
+ @prev_sp_write
+ def test_user_install_not_implied_user_site_enabled(self):
+ self.assert_not_user_site()
+
+ @mock.patch('site.ENABLE_USER_SITE', False)
+ @prev_sp_write
+ def test_user_install_not_implied_user_site_disabled(self):
+ self.assert_not_user_site()
+
+ @staticmethod
+ def assert_not_user_site():
+ # create a finalized easy_install command
+ dist = Distribution()
+ dist.script_name = 'setup.py'
+ cmd = ei.easy_install(dist)
+ cmd.args = ['py']
+ cmd.ensure_finalized()
+ assert not cmd.user, 'user should not be implied'
+
+ def test_multiproc_atexit(self):
+ pytest.importorskip('multiprocessing')
+
+ log = logging.getLogger('test_easy_install')
+ logging.basicConfig(level=logging.INFO, stream=sys.stderr)
+ log.info('this should not break')
+
+ @pytest.fixture
+ def foo_package(self, tmpdir):
+ egg_file = tmpdir / 'foo-1.0.egg-info'
+ with egg_file.open('w') as f:
+ f.write('Name: foo\n')
+ return str(tmpdir)
+
+ @pytest.fixture
+ def install_target(self, tmpdir):
+ target = str(tmpdir)
+ with mock.patch('sys.path', sys.path + [target]):
+ python_path = os.path.pathsep.join(sys.path)
+ with mock.patch.dict(os.environ, PYTHONPATH=python_path):
+ yield target
+
+ def test_local_index(self, foo_package, install_target):
+ """
+ The local index must be used when easy_install locates installed
+ packages.
+ """
+ dist = Distribution()
+ dist.script_name = 'setup.py'
+ cmd = ei.easy_install(dist)
+ cmd.install_dir = install_target
+ cmd.args = ['foo']
+ cmd.ensure_finalized()
+ cmd.local_index.scan([foo_package])
+ res = cmd.easy_install('foo')
+ actual = os.path.normcase(os.path.realpath(res.location))
+ expected = os.path.normcase(os.path.realpath(foo_package))
+ assert actual == expected
+
+ @contextlib.contextmanager
+ def user_install_setup_context(self, *args, **kwargs):
+ """
+ Wrap sandbox.setup_context to patch easy_install in that context to
+ appear as user-installed.
+ """
+ with self.orig_context(*args, **kwargs):
+ import setuptools.command.easy_install as ei
+
+ ei.__file__ = site.USER_SITE
+ yield
+
+ def patched_setup_context(self):
+ self.orig_context = sandbox.setup_context
+
+ return mock.patch(
+ 'setuptools.sandbox.setup_context',
+ self.user_install_setup_context,
+ )
+
+
+@pytest.fixture
+def distutils_package():
+ distutils_setup_py = SETUP_PY.replace(
+ 'from setuptools import setup',
+ 'from distutils.core import setup',
+ )
+ with contexts.tempdir(cd=os.chdir):
+ with open('setup.py', 'w', encoding="utf-8") as f:
+ f.write(distutils_setup_py)
+ yield
+
+
+@pytest.mark.usefixtures("distutils_package")
+class TestDistutilsPackage:
+ def test_bdist_egg_available_on_distutils_pkg(self):
+ run_setup('setup.py', ['bdist_egg'])
+
+
+@pytest.fixture
+def mock_index():
+ # set up a server which will simulate an alternate package index.
+ p_index = MockServer()
+ if p_index.server_port == 0:
+ # Some platforms (Jython) don't find a port to which to bind,
+ # so skip test for them.
+ pytest.skip("could not find a valid port")
+ p_index.start()
+ return p_index
+
+
+class TestInstallRequires:
+ def test_setup_install_includes_dependencies(self, tmp_path, mock_index):
+ """
+ When ``python setup.py install`` is called directly, it will use easy_install
+ to fetch dependencies.
+ """
+ # TODO: Remove these tests once `setup.py install` is completely removed
+ project_root = tmp_path / "project"
+ project_root.mkdir(exist_ok=True)
+ install_root = tmp_path / "install"
+ install_root.mkdir(exist_ok=True)
+
+ self.create_project(project_root)
+ cmd = [
+ sys.executable,
+ '-c',
+ '__import__("setuptools").setup()',
+ 'install',
+ '--install-base',
+ str(install_root),
+ '--install-lib',
+ str(install_root),
+ '--install-headers',
+ str(install_root),
+ '--install-scripts',
+ str(install_root),
+ '--install-data',
+ str(install_root),
+ '--install-purelib',
+ str(install_root),
+ '--install-platlib',
+ str(install_root),
+ ]
+ env = {**os.environ, "__EASYINSTALL_INDEX": mock_index.url}
+ cp = subprocess.run(
+ cmd,
+ cwd=str(project_root),
+ env=env,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ encoding="utf-8",
+ )
+ assert cp.returncode != 0
+ try:
+ assert '/does-not-exist/' in {r.path for r in mock_index.requests}
+ assert next(
+ line
+ for line in cp.stdout.splitlines()
+ if "not find suitable distribution for" in line
+ and "does-not-exist" in line
+ )
+ except Exception:
+ if "failed to get random numbers" in cp.stdout:
+ pytest.xfail(f"{sys.platform} failure - {cp.stdout}")
+ raise
+
+ def create_project(self, root):
+ config = """
+ [metadata]
+ name = project
+ version = 42
+
+ [options]
+ install_requires = does-not-exist
+ py_modules = mod
+ """
+ (root / 'setup.cfg').write_text(DALS(config), encoding="utf-8")
+ (root / 'mod.py').touch()
+
+
+class TestSetupRequires:
+ def test_setup_requires_honors_fetch_params(self, mock_index, monkeypatch):
+ """
+ When easy_install installs a source distribution which specifies
+ setup_requires, it should honor the fetch parameters (such as
+ index-url, and find-links).
+ """
+ monkeypatch.setenv('PIP_RETRIES', '0')
+ monkeypatch.setenv('PIP_TIMEOUT', '0')
+ monkeypatch.setenv('PIP_NO_INDEX', 'false')
+ with contexts.quiet():
+ # create an sdist that has a build-time dependency.
+ with TestSetupRequires.create_sdist() as dist_file:
+ with contexts.tempdir() as temp_install_dir:
+ with contexts.environment(PYTHONPATH=temp_install_dir):
+ cmd = [
+ sys.executable,
+ '-c',
+ '__import__("setuptools").setup()',
+ 'easy_install',
+ '--index-url',
+ mock_index.url,
+ '--exclude-scripts',
+ '--install-dir',
+ temp_install_dir,
+ dist_file,
+ ]
+ subprocess.Popen(cmd).wait()
+ # there should have been one requests to the server
+ assert [r.path for r in mock_index.requests] == ['/does-not-exist/']
+
+ @staticmethod
+ @contextlib.contextmanager
+ def create_sdist():
+ """
+ Return an sdist with a setup_requires dependency (of something that
+ doesn't exist)
+ """
+ with contexts.tempdir() as dir:
+ dist_path = os.path.join(dir, 'setuptools-test-fetcher-1.0.tar.gz')
+ make_sdist(
+ dist_path,
+ [
+ (
+ 'setup.py',
+ DALS(
+ """
+ import setuptools
+ setuptools.setup(
+ name="setuptools-test-fetcher",
+ version="1.0",
+ setup_requires = ['does-not-exist'],
+ )
+ """
+ ),
+ ),
+ ('setup.cfg', ''),
+ ],
+ )
+ yield dist_path
+
+ use_setup_cfg = (
+ (),
+ ('dependency_links',),
+ ('setup_requires',),
+ ('dependency_links', 'setup_requires'),
+ )
+
+ @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
+ def test_setup_requires_overrides_version_conflict(self, use_setup_cfg):
+ """
+ Regression test for distribution issue 323:
+ https://bitbucket.org/tarek/distribute/issues/323
+
+ Ensures that a distribution's setup_requires requirements can still be
+ installed and used locally even if a conflicting version of that
+ requirement is already on the path.
+ """
+
+ fake_dist = PRDistribution(
+ 'does-not-matter', project_name='foobar', version='0.0'
+ )
+ working_set.add(fake_dist)
+
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ test_pkg = create_setup_requires_package(
+ temp_dir, use_setup_cfg=use_setup_cfg
+ )
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+ with contexts.quiet() as (stdout, _stderr):
+ # Don't even need to install the package, just
+ # running the setup.py at all is sufficient
+ run_setup(test_setup_py, ['--name'])
+
+ lines = stdout.readlines()
+ assert len(lines) > 0
+ assert lines[-1].strip() == 'test_pkg'
+
+ @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
+ def test_setup_requires_override_nspkg(self, use_setup_cfg):
+ """
+ Like ``test_setup_requires_overrides_version_conflict`` but where the
+ ``setup_requires`` package is part of a namespace package that has
+ *already* been imported.
+ """
+
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ foobar_1_archive = os.path.join(temp_dir, 'foo_bar-0.1.tar.gz')
+ make_nspkg_sdist(foobar_1_archive, 'foo.bar', '0.1')
+ # Now actually go ahead an extract to the temp dir and add the
+ # extracted path to sys.path so foo.bar v0.1 is importable
+ foobar_1_dir = os.path.join(temp_dir, 'foo_bar-0.1')
+ os.mkdir(foobar_1_dir)
+ with tarfile.open(foobar_1_archive) as tf:
+ tf.extraction_filter = lambda member, path: member
+ tf.extractall(foobar_1_dir)
+ sys.path.insert(1, foobar_1_dir)
+
+ dist = PRDistribution(
+ foobar_1_dir, project_name='foo.bar', version='0.1'
+ )
+ working_set.add(dist)
+
+ template = DALS(
+ """\
+ import foo # Even with foo imported first the
+ # setup_requires package should override
+ import setuptools
+ setuptools.setup(**%r)
+
+ if not (hasattr(foo, '__path__') and
+ len(foo.__path__) == 2):
+ print('FAIL')
+
+ if 'foo_bar-0.2' not in foo.__path__[0]:
+ print('FAIL')
+ """
+ )
+
+ test_pkg = create_setup_requires_package(
+ temp_dir,
+ 'foo.bar',
+ '0.2',
+ make_nspkg_sdist,
+ template,
+ use_setup_cfg=use_setup_cfg,
+ )
+
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+
+ with contexts.quiet() as (stdout, _stderr):
+ try:
+ # Don't even need to install the package, just
+ # running the setup.py at all is sufficient
+ run_setup(test_setup_py, ['--name'])
+ except pkg_resources.VersionConflict: # pragma: nocover
+ pytest.fail(
+ 'Installing setup.py requirements caused a VersionConflict'
+ )
+
+ assert 'FAIL' not in stdout.getvalue()
+ lines = stdout.readlines()
+ assert len(lines) > 0
+ assert lines[-1].strip() == 'test_pkg'
+
+ @pytest.mark.parametrize('use_setup_cfg', use_setup_cfg)
+ def test_setup_requires_with_attr_version(self, use_setup_cfg):
+ def make_dependency_sdist(dist_path, distname, version):
+ files = [
+ (
+ 'setup.py',
+ DALS(
+ f"""
+ import setuptools
+ setuptools.setup(
+ name={distname!r},
+ version={version!r},
+ py_modules=[{distname!r}],
+ )
+ """
+ ),
+ ),
+ (
+ distname + '.py',
+ DALS(
+ """
+ version = 42
+ """
+ ),
+ ),
+ ]
+ make_sdist(dist_path, files)
+
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ test_pkg = create_setup_requires_package(
+ temp_dir,
+ setup_attrs=dict(version='attr: foobar.version'),
+ make_package=make_dependency_sdist,
+ use_setup_cfg=use_setup_cfg + ('version',),
+ )
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+ with contexts.quiet() as (stdout, _stderr):
+ run_setup(test_setup_py, ['--version'])
+ lines = stdout.readlines()
+ assert len(lines) > 0
+ assert lines[-1].strip() == '42'
+
+ def test_setup_requires_honors_pip_env(self, mock_index, monkeypatch):
+ monkeypatch.setenv('PIP_RETRIES', '0')
+ monkeypatch.setenv('PIP_TIMEOUT', '0')
+ monkeypatch.setenv('PIP_NO_INDEX', 'false')
+ monkeypatch.setenv('PIP_INDEX_URL', mock_index.url)
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ test_pkg = create_setup_requires_package(
+ temp_dir,
+ 'python-xlib',
+ '0.19',
+ setup_attrs=dict(dependency_links=[]),
+ )
+ test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
+ with open(test_setup_cfg, 'w', encoding="utf-8") as fp:
+ fp.write(
+ DALS(
+ """
+ [easy_install]
+ index_url = https://pypi.org/legacy/
+ """
+ )
+ )
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+ with pytest.raises(distutils.errors.DistutilsError):
+ run_setup(test_setup_py, ['--version'])
+ assert len(mock_index.requests) == 1
+ assert mock_index.requests[0].path == '/python-xlib/'
+
+ def test_setup_requires_with_pep508_url(self, mock_index, monkeypatch):
+ monkeypatch.setenv('PIP_RETRIES', '0')
+ monkeypatch.setenv('PIP_TIMEOUT', '0')
+ monkeypatch.setenv('PIP_INDEX_URL', mock_index.url)
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ dep_sdist = os.path.join(temp_dir, 'dep.tar.gz')
+ make_trivial_sdist(dep_sdist, 'dependency', '42')
+ dep_url = path_to_url(dep_sdist, authority='localhost')
+ test_pkg = create_setup_requires_package(
+ temp_dir,
+ # Ignored (overridden by setup_attrs)
+ 'python-xlib',
+ '0.19',
+ setup_attrs=dict(setup_requires=f'dependency @ {dep_url}'),
+ )
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+ run_setup(test_setup_py, ['--version'])
+ assert len(mock_index.requests) == 0
+
+ def test_setup_requires_with_allow_hosts(self, mock_index):
+ """The `allow-hosts` option in not supported anymore."""
+ files = {
+ 'test_pkg': {
+ 'setup.py': DALS(
+ """
+ from setuptools import setup
+ setup(setup_requires='python-xlib')
+ """
+ ),
+ 'setup.cfg': DALS(
+ """
+ [easy_install]
+ allow_hosts = *
+ """
+ ),
+ }
+ }
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ path.build(files, prefix=temp_dir)
+ setup_py = str(pathlib.Path(temp_dir, 'test_pkg', 'setup.py'))
+ with pytest.raises(distutils.errors.DistutilsError):
+ run_setup(setup_py, ['--version'])
+ assert len(mock_index.requests) == 0
+
+ def test_setup_requires_with_python_requires(self, monkeypatch, tmpdir):
+ """Check `python_requires` is honored."""
+ monkeypatch.setenv('PIP_RETRIES', '0')
+ monkeypatch.setenv('PIP_TIMEOUT', '0')
+ monkeypatch.setenv('PIP_NO_INDEX', '1')
+ monkeypatch.setenv('PIP_VERBOSE', '1')
+ dep_1_0_sdist = 'dep-1.0.tar.gz'
+ dep_1_0_url = path_to_url(str(tmpdir / dep_1_0_sdist))
+ dep_1_0_python_requires = '>=2.7'
+ make_python_requires_sdist(
+ str(tmpdir / dep_1_0_sdist), 'dep', '1.0', dep_1_0_python_requires
+ )
+ dep_2_0_sdist = 'dep-2.0.tar.gz'
+ dep_2_0_url = path_to_url(str(tmpdir / dep_2_0_sdist))
+ dep_2_0_python_requires = (
+ f'!={sys.version_info.major}.{sys.version_info.minor}.*'
+ )
+ make_python_requires_sdist(
+ str(tmpdir / dep_2_0_sdist), 'dep', '2.0', dep_2_0_python_requires
+ )
+ index = tmpdir / 'index.html'
+ index.write_text(
+ DALS(
+ """
+ <!DOCTYPE html>
+ <html><head><title>Links for dep</title></head>
+ <body>
+ <h1>Links for dep</h1>
+ <a href="{dep_1_0_url}"\
+data-requires-python="{dep_1_0_python_requires}">{dep_1_0_sdist}</a><br/>
+ <a href="{dep_2_0_url}"\
+data-requires-python="{dep_2_0_python_requires}">{dep_2_0_sdist}</a><br/>
+ </body>
+ </html>
+ """
+ ).format(
+ dep_1_0_url=dep_1_0_url,
+ dep_1_0_sdist=dep_1_0_sdist,
+ dep_1_0_python_requires=dep_1_0_python_requires,
+ dep_2_0_url=dep_2_0_url,
+ dep_2_0_sdist=dep_2_0_sdist,
+ dep_2_0_python_requires=dep_2_0_python_requires,
+ ),
+ 'utf-8',
+ )
+ index_url = path_to_url(str(index))
+ with contexts.save_pkg_resources_state():
+ test_pkg = create_setup_requires_package(
+ str(tmpdir),
+ 'python-xlib',
+ '0.19', # Ignored (overridden by setup_attrs).
+ setup_attrs=dict(setup_requires='dep', dependency_links=[index_url]),
+ )
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+ run_setup(test_setup_py, ['--version'])
+ eggs = list(
+ map(str, pkg_resources.find_distributions(os.path.join(test_pkg, '.eggs')))
+ )
+ assert eggs == ['dep 1.0']
+
+ @pytest.mark.parametrize('with_dependency_links_in_setup_py', (False, True))
+ def test_setup_requires_with_find_links_in_setup_cfg(
+ self, monkeypatch, with_dependency_links_in_setup_py
+ ):
+ monkeypatch.setenv('PIP_RETRIES', '0')
+ monkeypatch.setenv('PIP_TIMEOUT', '0')
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ make_trivial_sdist(
+ os.path.join(temp_dir, 'python-xlib-42.tar.gz'), 'python-xlib', '42'
+ )
+ test_pkg = os.path.join(temp_dir, 'test_pkg')
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+ test_setup_cfg = os.path.join(test_pkg, 'setup.cfg')
+ os.mkdir(test_pkg)
+ with open(test_setup_py, 'w', encoding="utf-8") as fp:
+ if with_dependency_links_in_setup_py:
+ dependency_links = [os.path.join(temp_dir, 'links')]
+ else:
+ dependency_links = []
+ fp.write(
+ DALS(
+ """
+ from setuptools import installer, setup
+ setup(setup_requires='python-xlib==42',
+ dependency_links={dependency_links!r})
+ """
+ ).format(dependency_links=dependency_links)
+ )
+ with open(test_setup_cfg, 'w', encoding="utf-8") as fp:
+ fp.write(
+ DALS(
+ """
+ [easy_install]
+ index_url = {index_url}
+ find_links = {find_links}
+ """
+ ).format(
+ index_url=os.path.join(temp_dir, 'index'),
+ find_links=temp_dir,
+ )
+ )
+ run_setup(test_setup_py, ['--version'])
+
+ def test_setup_requires_with_transitive_extra_dependency(self, monkeypatch):
+ """
+ Use case: installing a package with a build dependency on
+ an already installed `dep[extra]`, which in turn depends
+ on `extra_dep` (whose is not already installed).
+ """
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ # Create source distribution for `extra_dep`.
+ make_trivial_sdist(
+ os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'), 'extra_dep', '1.0'
+ )
+ # Create source tree for `dep`.
+ dep_pkg = os.path.join(temp_dir, 'dep')
+ os.mkdir(dep_pkg)
+ path.build(
+ {
+ 'setup.py': DALS(
+ """
+ import setuptools
+ setuptools.setup(
+ name='dep', version='2.0',
+ extras_require={'extra': ['extra_dep']},
+ )
+ """
+ ),
+ 'setup.cfg': '',
+ },
+ prefix=dep_pkg,
+ )
+ # "Install" dep.
+ run_setup(os.path.join(dep_pkg, 'setup.py'), ['dist_info'])
+ working_set.add_entry(dep_pkg)
+ # Create source tree for test package.
+ test_pkg = os.path.join(temp_dir, 'test_pkg')
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+ os.mkdir(test_pkg)
+ with open(test_setup_py, 'w', encoding="utf-8") as fp:
+ fp.write(
+ DALS(
+ """
+ from setuptools import installer, setup
+ setup(setup_requires='dep[extra]')
+ """
+ )
+ )
+ # Check...
+ monkeypatch.setenv('PIP_FIND_LINKS', str(temp_dir))
+ monkeypatch.setenv('PIP_NO_INDEX', '1')
+ monkeypatch.setenv('PIP_RETRIES', '0')
+ monkeypatch.setenv('PIP_TIMEOUT', '0')
+ run_setup(test_setup_py, ['--version'])
+
+ def test_setup_requires_with_distutils_command_dep(self, monkeypatch):
+ """
+ Use case: ensure build requirements' extras
+ are properly installed and activated.
+ """
+ with contexts.save_pkg_resources_state():
+ with contexts.tempdir() as temp_dir:
+ # Create source distribution for `extra_dep`.
+ make_sdist(
+ os.path.join(temp_dir, 'extra_dep-1.0.tar.gz'),
+ [
+ (
+ 'setup.py',
+ DALS(
+ """
+ import setuptools
+ setuptools.setup(
+ name='extra_dep',
+ version='1.0',
+ py_modules=['extra_dep'],
+ )
+ """
+ ),
+ ),
+ ('setup.cfg', ''),
+ ('extra_dep.py', ''),
+ ],
+ )
+ # Create source tree for `epdep`.
+ dep_pkg = os.path.join(temp_dir, 'epdep')
+ os.mkdir(dep_pkg)
+ path.build(
+ {
+ 'setup.py': DALS(
+ """
+ import setuptools
+ setuptools.setup(
+ name='dep', version='2.0',
+ py_modules=['epcmd'],
+ extras_require={'extra': ['extra_dep']},
+ entry_points='''
+ [distutils.commands]
+ epcmd = epcmd:epcmd [extra]
+ ''',
+ )
+ """
+ ),
+ 'setup.cfg': '',
+ 'epcmd.py': DALS(
+ """
+ from distutils.command.build_py import build_py
+
+ import extra_dep
+
+ class epcmd(build_py):
+ pass
+ """
+ ),
+ },
+ prefix=dep_pkg,
+ )
+ # "Install" dep.
+ run_setup(os.path.join(dep_pkg, 'setup.py'), ['dist_info'])
+ working_set.add_entry(dep_pkg)
+ # Create source tree for test package.
+ test_pkg = os.path.join(temp_dir, 'test_pkg')
+ test_setup_py = os.path.join(test_pkg, 'setup.py')
+ os.mkdir(test_pkg)
+ with open(test_setup_py, 'w', encoding="utf-8") as fp:
+ fp.write(
+ DALS(
+ """
+ from setuptools import installer, setup
+ setup(setup_requires='dep[extra]')
+ """
+ )
+ )
+ # Check...
+ monkeypatch.setenv('PIP_FIND_LINKS', str(temp_dir))
+ monkeypatch.setenv('PIP_NO_INDEX', '1')
+ monkeypatch.setenv('PIP_RETRIES', '0')
+ monkeypatch.setenv('PIP_TIMEOUT', '0')
+ run_setup(test_setup_py, ['epcmd'])
+
+
+def make_trivial_sdist(dist_path, distname, version):
+ """
+ Create a simple sdist tarball at dist_path, containing just a simple
+ setup.py.
+ """
+
+ make_sdist(
+ dist_path,
+ [
+ (
+ 'setup.py',
+ DALS(
+ f"""\
+ import setuptools
+ setuptools.setup(
+ name={distname!r},
+ version={version!r}
+ )
+ """
+ ),
+ ),
+ ('setup.cfg', ''),
+ ],
+ )
+
+
+def make_nspkg_sdist(dist_path, distname, version):
+ """
+ Make an sdist tarball with distname and version which also contains one
+ package with the same name as distname. The top-level package is
+ designated a namespace package).
+ """
+ # Assert that the distname contains at least one period
+ assert '.' in distname
+
+ parts = distname.split('.')
+ nspackage = parts[0]
+
+ packages = ['.'.join(parts[:idx]) for idx in range(1, len(parts) + 1)]
+
+ setup_py = DALS(
+ f"""\
+ import setuptools
+ setuptools.setup(
+ name={distname!r},
+ version={version!r},
+ packages={packages!r},
+ namespace_packages=[{nspackage!r}]
+ )
+ """
+ )
+
+ init = "__import__('pkg_resources').declare_namespace(__name__)"
+
+ files = [('setup.py', setup_py), (os.path.join(nspackage, '__init__.py'), init)]
+ for package in packages[1:]:
+ filename = os.path.join(*(package.split('.') + ['__init__.py']))
+ files.append((filename, ''))
+
+ make_sdist(dist_path, files)
+
+
+def make_python_requires_sdist(dist_path, distname, version, python_requires):
+ make_sdist(
+ dist_path,
+ [
+ (
+ 'setup.py',
+ DALS(
+ """\
+ import setuptools
+ setuptools.setup(
+ name={name!r},
+ version={version!r},
+ python_requires={python_requires!r},
+ )
+ """
+ ).format(
+ name=distname, version=version, python_requires=python_requires
+ ),
+ ),
+ ('setup.cfg', ''),
+ ],
+ )
+
+
+def make_sdist(dist_path, files):
+ """
+ Create a simple sdist tarball at dist_path, containing the files
+ listed in ``files`` as ``(filename, content)`` tuples.
+ """
+
+ # Distributions with only one file don't play well with pip.
+ assert len(files) > 1
+ with tarfile.open(dist_path, 'w:gz') as dist:
+ for filename, content in files:
+ file_bytes = io.BytesIO(content.encode('utf-8'))
+ file_info = tarfile.TarInfo(name=filename)
+ file_info.size = len(file_bytes.getvalue())
+ file_info.mtime = int(time.time())
+ dist.addfile(file_info, fileobj=file_bytes)
+
+
+def create_setup_requires_package(
+ path,
+ distname='foobar',
+ version='0.1',
+ make_package=make_trivial_sdist,
+ setup_py_template=None,
+ setup_attrs=None,
+ use_setup_cfg=(),
+):
+ """Creates a source tree under path for a trivial test package that has a
+ single requirement in setup_requires--a tarball for that requirement is
+ also created and added to the dependency_links argument.
+
+ ``distname`` and ``version`` refer to the name/version of the package that
+ the test package requires via ``setup_requires``. The name of the test
+ package itself is just 'test_pkg'.
+ """
+
+ normalized_distname = safer_name(distname)
+ test_setup_attrs = {
+ 'name': 'test_pkg',
+ 'version': '0.0',
+ 'setup_requires': [f'{normalized_distname}=={version}'],
+ 'dependency_links': [os.path.abspath(path)],
+ }
+ if setup_attrs:
+ test_setup_attrs.update(setup_attrs)
+
+ test_pkg = os.path.join(path, 'test_pkg')
+ os.mkdir(test_pkg)
+
+ # setup.cfg
+ if use_setup_cfg:
+ options = []
+ metadata = []
+ for name in use_setup_cfg:
+ value = test_setup_attrs.pop(name)
+ if name in 'name version'.split():
+ section = metadata
+ else:
+ section = options
+ if isinstance(value, (tuple, list)):
+ value = ';'.join(value)
+ section.append(f'{name}: {value}')
+ test_setup_cfg_contents = DALS(
+ """
+ [metadata]
+ {metadata}
+ [options]
+ {options}
+ """
+ ).format(
+ options='\n'.join(options),
+ metadata='\n'.join(metadata),
+ )
+ else:
+ test_setup_cfg_contents = ''
+ with open(os.path.join(test_pkg, 'setup.cfg'), 'w', encoding="utf-8") as f:
+ f.write(test_setup_cfg_contents)
+
+ # setup.py
+ if setup_py_template is None:
+ setup_py_template = DALS(
+ """\
+ import setuptools
+ setuptools.setup(**%r)
+ """
+ )
+ with open(os.path.join(test_pkg, 'setup.py'), 'w', encoding="utf-8") as f:
+ f.write(setup_py_template % test_setup_attrs)
+
+ foobar_path = os.path.join(path, f'{normalized_distname}-{version}.tar.gz')
+ make_package(foobar_path, distname, version)
+
+ return test_pkg
+
+
+@pytest.mark.skipif(
+ sys.platform.startswith('java') and ei.is_sh(sys.executable),
+ reason="Test cannot run under java when executable is sh",
+)
+class TestScriptHeader:
+ non_ascii_exe = '/Users/José/bin/python'
+ exe_with_spaces = r'C:\Program Files\Python36\python.exe'
+
+ def test_get_script_header(self):
+ expected = f'#!{ei.nt_quote_arg(os.path.normpath(sys.executable))}\n'
+ actual = ei.ScriptWriter.get_header('#!/usr/local/bin/python')
+ assert actual == expected
+
+ def test_get_script_header_args(self):
+ expected = f'#!{ei.nt_quote_arg(os.path.normpath(sys.executable))} -x\n'
+ actual = ei.ScriptWriter.get_header('#!/usr/bin/python -x')
+ assert actual == expected
+
+ def test_get_script_header_non_ascii_exe(self):
+ actual = ei.ScriptWriter.get_header(
+ '#!/usr/bin/python', executable=self.non_ascii_exe
+ )
+ expected = f'#!{self.non_ascii_exe} -x\n'
+ assert actual == expected
+
+ def test_get_script_header_exe_with_spaces(self):
+ actual = ei.ScriptWriter.get_header(
+ '#!/usr/bin/python', executable='"' + self.exe_with_spaces + '"'
+ )
+ expected = f'#!"{self.exe_with_spaces}"\n'
+ assert actual == expected
+
+
+class TestCommandSpec:
+ def test_custom_launch_command(self):
+ """
+ Show how a custom CommandSpec could be used to specify a #! executable
+ which takes parameters.
+ """
+ cmd = ei.CommandSpec(['/usr/bin/env', 'python3'])
+ assert cmd.as_header() == '#!/usr/bin/env python3\n'
+
+ def test_from_param_for_CommandSpec_is_passthrough(self):
+ """
+ from_param should return an instance of a CommandSpec
+ """
+ cmd = ei.CommandSpec(['python'])
+ cmd_new = ei.CommandSpec.from_param(cmd)
+ assert cmd is cmd_new
+
+ @mock.patch('sys.executable', TestScriptHeader.exe_with_spaces)
+ @mock.patch.dict(os.environ)
+ def test_from_environment_with_spaces_in_executable(self):
+ os.environ.pop('__PYVENV_LAUNCHER__', None)
+ cmd = ei.CommandSpec.from_environment()
+ assert len(cmd) == 1
+ assert cmd.as_header().startswith('#!"')
+
+ def test_from_simple_string_uses_shlex(self):
+ """
+ In order to support `executable = /usr/bin/env my-python`, make sure
+ from_param invokes shlex on that input.
+ """
+ cmd = ei.CommandSpec.from_param('/usr/bin/env my-python')
+ assert len(cmd) == 2
+ assert '"' not in cmd.as_header()
+
+ def test_from_param_raises_expected_error(self) -> None:
+ """
+ from_param should raise its own TypeError when the argument's type is unsupported
+ """
+ with pytest.raises(TypeError) as exc_info:
+ ei.CommandSpec.from_param(object()) # type: ignore[arg-type] # We want a type error here
+ assert (
+ str(exc_info.value) == "Argument has an unsupported type <class 'object'>"
+ ), exc_info.value
+
+
+class TestWindowsScriptWriter:
+ def test_header(self):
+ hdr = ei.WindowsScriptWriter.get_header('')
+ assert hdr.startswith('#!')
+ assert hdr.endswith('\n')
+ hdr = hdr.lstrip('#!')
+ hdr = hdr.rstrip('\n')
+ # header should not start with an escaped quote
+ assert not hdr.startswith('\\"')
+
+
+class VersionStub(NamedTuple):
+ major: int
+ minor: int
+ micro: int
+ releaselevel: str
+ serial: int
+
+
+def test_use_correct_python_version_string(tmpdir, tmpdir_cwd, monkeypatch):
+ # In issue #3001, easy_install wrongly uses the `python3.1` directory
+ # when the interpreter is `python3.10` and the `--user` option is given.
+ # See pypa/setuptools#3001.
+ dist = Distribution()
+ cmd = dist.get_command_obj('easy_install')
+ cmd.args = ['ok']
+ cmd.optimize = 0
+ cmd.user = True
+ cmd.install_userbase = str(tmpdir)
+ cmd.install_usersite = None
+ install_cmd = dist.get_command_obj('install')
+ install_cmd.install_userbase = str(tmpdir)
+ install_cmd.install_usersite = None
+
+ with monkeypatch.context() as patch, warnings.catch_warnings():
+ warnings.simplefilter("ignore")
+ version = '3.10.1 (main, Dec 21 2021, 09:17:12) [GCC 10.2.1 20210110]'
+ info = VersionStub(3, 10, 1, "final", 0)
+ patch.setattr('site.ENABLE_USER_SITE', True)
+ patch.setattr('sys.version', version)
+ patch.setattr('sys.version_info', info)
+ patch.setattr(cmd, 'create_home_path', mock.Mock())
+ cmd.finalize_options()
+
+ name = "pypy" if hasattr(sys, 'pypy_version_info') else "python"
+ install_dir = cmd.install_dir.lower()
+
+ # In some platforms (e.g. Windows), install_dir is mostly determined
+ # via `sysconfig`, which define constants eagerly at module creation.
+ # This means that monkeypatching `sys.version` to emulate 3.10 for testing
+ # may have no effect.
+ # The safest test here is to rely on the fact that 3.1 is no longer
+ # supported/tested, and make sure that if 'python3.1' ever appears in the string
+ # it is followed by another digit (e.g. 'python3.10').
+ if re.search(name + r'3\.?1', install_dir):
+ assert re.search(name + r'3\.?1\d', install_dir)
+
+ # The following "variables" are used for interpolation in distutils
+ # installation schemes, so it should be fair to treat them as "semi-public",
+ # or at least public enough so we can have a test to make sure they are correct
+ assert cmd.config_vars['py_version'] == '3.10.1'
+ assert cmd.config_vars['py_version_short'] == '3.10'
+ assert cmd.config_vars['py_version_nodot'] == '310'
+
+
+@pytest.mark.xfail(
+ sys.platform == "darwin",
+ reason="https://github.com/pypa/setuptools/pull/4716#issuecomment-2447624418",
+)
+def test_editable_user_and_build_isolation(setup_context, monkeypatch, tmp_path):
+ """`setup.py develop` should honor `--user` even under build isolation"""
+
+ # == Arrange ==
+ # Pretend that build isolation was enabled
+ # e.g pip sets the environment variable PYTHONNOUSERSITE=1
+ monkeypatch.setattr('site.ENABLE_USER_SITE', False)
+
+ # Patching $HOME for 2 reasons:
+ # 1. setuptools/command/easy_install.py:create_home_path
+ # tries creating directories in $HOME.
+ # Given::
+ # self.config_vars['DESTDIRS'] = (
+ # "/home/user/.pyenv/versions/3.9.10 "
+ # "/home/user/.pyenv/versions/3.9.10/lib "
+ # "/home/user/.pyenv/versions/3.9.10/lib/python3.9 "
+ # "/home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")
+ # `create_home_path` will::
+ # makedirs(
+ # "/home/user/.pyenv/versions/3.9.10 "
+ # "/home/user/.pyenv/versions/3.9.10/lib "
+ # "/home/user/.pyenv/versions/3.9.10/lib/python3.9 "
+ # "/home/user/.pyenv/versions/3.9.10/lib/python3.9/lib-dynload")
+ #
+ # 2. We are going to force `site` to update site.USER_BASE and site.USER_SITE
+ # To point inside our new home
+ monkeypatch.setenv('HOME', str(tmp_path / '.home'))
+ monkeypatch.setenv('USERPROFILE', str(tmp_path / '.home'))
+ monkeypatch.setenv('APPDATA', str(tmp_path / '.home'))
+ monkeypatch.setattr('site.USER_BASE', None)
+ monkeypatch.setattr('site.USER_SITE', None)
+ user_site = Path(site.getusersitepackages())
+ user_site.mkdir(parents=True, exist_ok=True)
+
+ sys_prefix = tmp_path / '.sys_prefix'
+ sys_prefix.mkdir(parents=True, exist_ok=True)
+ monkeypatch.setattr('sys.prefix', str(sys_prefix))
+
+ setup_script = (
+ "__import__('setuptools').setup(name='aproj', version=42, packages=[])\n"
+ )
+ (tmp_path / "setup.py").write_text(setup_script, encoding="utf-8")
+
+ # == Sanity check ==
+ assert list(sys_prefix.glob("*")) == []
+ assert list(user_site.glob("*")) == []
+
+ # == Act ==
+ run_setup('setup.py', ['develop', '--user'])
+
+ # == Assert ==
+ # Should not install to sys.prefix
+ assert list(sys_prefix.glob("*")) == []
+ # Should install to user site
+ installed = {f.name for f in user_site.glob("*")}
+ # sometimes easy-install.pth is created and sometimes not
+ installed = installed - {"easy-install.pth"}
+ assert installed == {'aproj.egg-link'}
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_editable_install.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_editable_install.py
new file mode 100644
index 00000000..038dcadf
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_editable_install.py
@@ -0,0 +1,1289 @@
+from __future__ import annotations
+
+import os
+import platform
+import stat
+import subprocess
+import sys
+from copy import deepcopy
+from importlib import import_module
+from importlib.machinery import EXTENSION_SUFFIXES
+from pathlib import Path
+from textwrap import dedent
+from typing import Any
+from unittest.mock import Mock
+from uuid import uuid4
+
+import jaraco.envs
+import jaraco.path
+import pytest
+from path import Path as _Path
+
+from setuptools._importlib import resources as importlib_resources
+from setuptools.command.editable_wheel import (
+ _DebuggingTips,
+ _encode_pth,
+ _find_namespaces,
+ _find_package_roots,
+ _find_virtual_namespaces,
+ _finder_template,
+ _LinkTree,
+ _TopLevelFinder,
+ editable_wheel,
+)
+from setuptools.dist import Distribution
+from setuptools.extension import Extension
+from setuptools.warnings import SetuptoolsDeprecationWarning
+
+from . import contexts, namespaces
+
+from distutils.core import run_setup
+
+
+@pytest.fixture(params=["strict", "lenient"])
+def editable_opts(request):
+ if request.param == "strict":
+ return ["--config-settings", "editable-mode=strict"]
+ return []
+
+
+EXAMPLE = {
+ 'pyproject.toml': dedent(
+ """\
+ [build-system]
+ requires = ["setuptools"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "mypkg"
+ version = "3.14159"
+ license = {text = "MIT"}
+ description = "This is a Python package"
+ dynamic = ["readme"]
+ classifiers = [
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers"
+ ]
+ urls = {Homepage = "https://github.com"}
+
+ [tool.setuptools]
+ package-dir = {"" = "src"}
+ packages = {find = {where = ["src"]}}
+ license-files = ["LICENSE*"]
+
+ [tool.setuptools.dynamic]
+ readme = {file = "README.rst"}
+
+ [tool.distutils.egg_info]
+ tag-build = ".post0"
+ """
+ ),
+ "MANIFEST.in": dedent(
+ """\
+ global-include *.py *.txt
+ global-exclude *.py[cod]
+ prune dist
+ prune build
+ """
+ ).strip(),
+ "README.rst": "This is a ``README``",
+ "LICENSE.txt": "---- placeholder MIT license ----",
+ "src": {
+ "mypkg": {
+ "__init__.py": dedent(
+ """\
+ import sys
+ from importlib.metadata import PackageNotFoundError, version
+
+ try:
+ __version__ = version(__name__)
+ except PackageNotFoundError:
+ __version__ = "unknown"
+ """
+ ),
+ "__main__.py": dedent(
+ """\
+ from importlib.resources import read_text
+ from . import __version__, __name__ as parent
+ from .mod import x
+
+ data = read_text(parent, "data.txt")
+ print(__version__, data, x)
+ """
+ ),
+ "mod.py": "x = ''",
+ "data.txt": "Hello World",
+ }
+ },
+}
+
+
+SETUP_SCRIPT_STUB = "__import__('setuptools').setup()"
+
+
+@pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328")
+@pytest.mark.parametrize(
+ "files",
+ [
+ {**EXAMPLE, "setup.py": SETUP_SCRIPT_STUB},
+ EXAMPLE, # No setup.py script
+ ],
+)
+def test_editable_with_pyproject(tmp_path, venv, files, editable_opts):
+ project = tmp_path / "mypkg"
+ project.mkdir()
+ jaraco.path.build(files, prefix=project)
+
+ cmd = [
+ "python",
+ "-m",
+ "pip",
+ "install",
+ "--no-build-isolation", # required to force current version of setuptools
+ "-e",
+ str(project),
+ *editable_opts,
+ ]
+ print(venv.run(cmd))
+
+ cmd = ["python", "-m", "mypkg"]
+ assert venv.run(cmd).strip() == "3.14159.post0 Hello World"
+
+ (project / "src/mypkg/data.txt").write_text("foobar", encoding="utf-8")
+ (project / "src/mypkg/mod.py").write_text("x = 42", encoding="utf-8")
+ assert venv.run(cmd).strip() == "3.14159.post0 foobar 42"
+
+
+def test_editable_with_flat_layout(tmp_path, venv, editable_opts):
+ files = {
+ "mypkg": {
+ "pyproject.toml": dedent(
+ """\
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "mypkg"
+ version = "3.14159"
+
+ [tool.setuptools]
+ packages = ["pkg"]
+ py-modules = ["mod"]
+ """
+ ),
+ "pkg": {"__init__.py": "a = 4"},
+ "mod.py": "b = 2",
+ },
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+ project = tmp_path / "mypkg"
+
+ cmd = [
+ "python",
+ "-m",
+ "pip",
+ "install",
+ "--no-build-isolation", # required to force current version of setuptools
+ "-e",
+ str(project),
+ *editable_opts,
+ ]
+ print(venv.run(cmd))
+ cmd = ["python", "-c", "import pkg, mod; print(pkg.a, mod.b)"]
+ assert venv.run(cmd).strip() == "4 2"
+
+
+def test_editable_with_single_module(tmp_path, venv, editable_opts):
+ files = {
+ "mypkg": {
+ "pyproject.toml": dedent(
+ """\
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "mod"
+ version = "3.14159"
+
+ [tool.setuptools]
+ py-modules = ["mod"]
+ """
+ ),
+ "mod.py": "b = 2",
+ },
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+ project = tmp_path / "mypkg"
+
+ cmd = [
+ "python",
+ "-m",
+ "pip",
+ "install",
+ "--no-build-isolation", # required to force current version of setuptools
+ "-e",
+ str(project),
+ *editable_opts,
+ ]
+ print(venv.run(cmd))
+ cmd = ["python", "-c", "import mod; print(mod.b)"]
+ assert venv.run(cmd).strip() == "2"
+
+
+class TestLegacyNamespaces:
+ # legacy => pkg_resources.declare_namespace(...) + setup(namespace_packages=...)
+
+ def test_nspkg_file_is_unique(self, tmp_path, monkeypatch):
+ deprecation = pytest.warns(
+ SetuptoolsDeprecationWarning, match=".*namespace_packages parameter.*"
+ )
+ installation_dir = tmp_path / ".installation_dir"
+ installation_dir.mkdir()
+ examples = (
+ "myns.pkgA",
+ "myns.pkgB",
+ "myns.n.pkgA",
+ "myns.n.pkgB",
+ )
+
+ for name in examples:
+ pkg = namespaces.build_namespace_package(tmp_path, name, version="42")
+ with deprecation, monkeypatch.context() as ctx:
+ ctx.chdir(pkg)
+ dist = run_setup("setup.py", stop_after="config")
+ cmd = editable_wheel(dist)
+ cmd.finalize_options()
+ editable_name = cmd.get_finalized_command("dist_info").name
+ cmd._install_namespaces(installation_dir, editable_name)
+
+ files = list(installation_dir.glob("*-nspkg.pth"))
+ assert len(files) == len(examples)
+
+ @pytest.mark.parametrize(
+ "impl",
+ (
+ "pkg_resources",
+ # "pkgutil", => does not work
+ ),
+ )
+ @pytest.mark.parametrize("ns", ("myns.n",))
+ def test_namespace_package_importable(
+ self, venv, tmp_path, ns, impl, editable_opts
+ ):
+ """
+ Installing two packages sharing the same namespace, one installed
+ naturally using pip or `--single-version-externally-managed`
+ and the other installed in editable mode should leave the namespace
+ intact and both packages reachable by import.
+ (Ported from test_develop).
+ """
+ build_system = """\
+ [build-system]
+ requires = ["setuptools"]
+ build-backend = "setuptools.build_meta"
+ """
+ pkg_A = namespaces.build_namespace_package(tmp_path, f"{ns}.pkgA", impl=impl)
+ pkg_B = namespaces.build_namespace_package(tmp_path, f"{ns}.pkgB", impl=impl)
+ (pkg_A / "pyproject.toml").write_text(build_system, encoding="utf-8")
+ (pkg_B / "pyproject.toml").write_text(build_system, encoding="utf-8")
+ # use pip to install to the target directory
+ opts = editable_opts[:]
+ opts.append("--no-build-isolation") # force current version of setuptools
+ venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts])
+ venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts])
+ venv.run(["python", "-c", f"import {ns}.pkgA; import {ns}.pkgB"])
+ # additionally ensure that pkg_resources import works
+ venv.run(["python", "-c", "import pkg_resources"])
+
+
+class TestPep420Namespaces:
+ def test_namespace_package_importable(self, venv, tmp_path, editable_opts):
+ """
+ Installing two packages sharing the same namespace, one installed
+ normally using pip and the other installed in editable mode
+ should allow importing both packages.
+ """
+ pkg_A = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgA')
+ pkg_B = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgB')
+ # use pip to install to the target directory
+ opts = editable_opts[:]
+ opts.append("--no-build-isolation") # force current version of setuptools
+ venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts])
+ venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts])
+ venv.run(["python", "-c", "import myns.n.pkgA; import myns.n.pkgB"])
+
+ def test_namespace_created_via_package_dir(self, venv, tmp_path, editable_opts):
+ """Currently users can create a namespace by tweaking `package_dir`"""
+ files = {
+ "pkgA": {
+ "pyproject.toml": dedent(
+ """\
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "pkgA"
+ version = "3.14159"
+
+ [tool.setuptools]
+ package-dir = {"myns.n.pkgA" = "src"}
+ """
+ ),
+ "src": {"__init__.py": "a = 1"},
+ },
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+ pkg_A = tmp_path / "pkgA"
+ pkg_B = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgB')
+ pkg_C = namespaces.build_pep420_namespace_package(tmp_path, 'myns.n.pkgC')
+
+ # use pip to install to the target directory
+ opts = editable_opts[:]
+ opts.append("--no-build-isolation") # force current version of setuptools
+ venv.run(["python", "-m", "pip", "install", str(pkg_A), *opts])
+ venv.run(["python", "-m", "pip", "install", "-e", str(pkg_B), *opts])
+ venv.run(["python", "-m", "pip", "install", "-e", str(pkg_C), *opts])
+ venv.run(["python", "-c", "from myns.n import pkgA, pkgB, pkgC"])
+
+ def test_namespace_accidental_config_in_lenient_mode(self, venv, tmp_path):
+ """Sometimes users might specify an ``include`` pattern that ignores parent
+ packages. In a normal installation this would ignore all modules inside the
+ parent packages, and make them namespaces (reported in issue #3504),
+ so the editable mode should preserve this behaviour.
+ """
+ files = {
+ "pkgA": {
+ "pyproject.toml": dedent(
+ """\
+ [build-system]
+ requires = ["setuptools", "wheel"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "pkgA"
+ version = "3.14159"
+
+ [tool.setuptools]
+ packages.find.include = ["mypkg.*"]
+ """
+ ),
+ "mypkg": {
+ "__init__.py": "",
+ "other.py": "b = 1",
+ "n": {
+ "__init__.py": "",
+ "pkgA.py": "a = 1",
+ },
+ },
+ "MANIFEST.in": EXAMPLE["MANIFEST.in"],
+ },
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+ pkg_A = tmp_path / "pkgA"
+
+ # use pip to install to the target directory
+ opts = ["--no-build-isolation"] # force current version of setuptools
+ venv.run(["python", "-m", "pip", "-v", "install", "-e", str(pkg_A), *opts])
+ out = venv.run(["python", "-c", "from mypkg.n import pkgA; print(pkgA.a)"])
+ assert out.strip() == "1"
+ cmd = """\
+ try:
+ import mypkg.other
+ except ImportError:
+ print("mypkg.other not defined")
+ """
+ out = venv.run(["python", "-c", dedent(cmd)])
+ assert "mypkg.other not defined" in out
+
+
+def test_editable_with_prefix(tmp_path, sample_project, editable_opts):
+ """
+ Editable install to a prefix should be discoverable.
+ """
+ prefix = tmp_path / 'prefix'
+
+ # figure out where pip will likely install the package
+ site_packages_all = [
+ prefix / Path(path).relative_to(sys.prefix)
+ for path in sys.path
+ if 'site-packages' in path and path.startswith(sys.prefix)
+ ]
+
+ for sp in site_packages_all:
+ sp.mkdir(parents=True)
+
+ # install workaround
+ _addsitedirs(site_packages_all)
+
+ env = dict(os.environ, PYTHONPATH=os.pathsep.join(map(str, site_packages_all)))
+ cmd = [
+ sys.executable,
+ '-m',
+ 'pip',
+ 'install',
+ '--editable',
+ str(sample_project),
+ '--prefix',
+ str(prefix),
+ '--no-build-isolation',
+ *editable_opts,
+ ]
+ subprocess.check_call(cmd, env=env)
+
+ # now run 'sample' with the prefix on the PYTHONPATH
+ bin = 'Scripts' if platform.system() == 'Windows' else 'bin'
+ exe = prefix / bin / 'sample'
+ subprocess.check_call([exe], env=env)
+
+
+class TestFinderTemplate:
+ """This test focus in getting a particular implementation detail right.
+ If at some point in time the implementation is changed for something different,
+ this test can be modified or even excluded.
+ """
+
+ def install_finder(self, finder):
+ loc = {}
+ exec(finder, loc, loc)
+ loc["install"]()
+
+ def test_packages(self, tmp_path):
+ files = {
+ "src1": {
+ "pkg1": {
+ "__init__.py": "",
+ "subpkg": {"mod1.py": "a = 42"},
+ },
+ },
+ "src2": {"mod2.py": "a = 43"},
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+
+ mapping = {
+ "pkg1": str(tmp_path / "src1/pkg1"),
+ "mod2": str(tmp_path / "src2/mod2"),
+ }
+ template = _finder_template(str(uuid4()), mapping, {})
+
+ with contexts.save_paths(), contexts.save_sys_modules():
+ for mod in ("pkg1", "pkg1.subpkg", "pkg1.subpkg.mod1", "mod2"):
+ sys.modules.pop(mod, None)
+
+ self.install_finder(template)
+ mod1 = import_module("pkg1.subpkg.mod1")
+ mod2 = import_module("mod2")
+ subpkg = import_module("pkg1.subpkg")
+
+ assert mod1.a == 42
+ assert mod2.a == 43
+ expected = str((tmp_path / "src1/pkg1/subpkg").resolve())
+ assert_path(subpkg, expected)
+
+ def test_namespace(self, tmp_path):
+ files = {"pkg": {"__init__.py": "a = 13", "text.txt": "abc"}}
+ jaraco.path.build(files, prefix=tmp_path)
+
+ mapping = {"ns.othername": str(tmp_path / "pkg")}
+ namespaces = {"ns": []}
+
+ template = _finder_template(str(uuid4()), mapping, namespaces)
+ with contexts.save_paths(), contexts.save_sys_modules():
+ for mod in ("ns", "ns.othername"):
+ sys.modules.pop(mod, None)
+
+ self.install_finder(template)
+ pkg = import_module("ns.othername")
+ text = importlib_resources.files(pkg) / "text.txt"
+
+ expected = str((tmp_path / "pkg").resolve())
+ assert_path(pkg, expected)
+ assert pkg.a == 13
+
+ # Make sure resources can also be found
+ assert text.read_text(encoding="utf-8") == "abc"
+
+ def test_combine_namespaces(self, tmp_path):
+ files = {
+ "src1": {"ns": {"pkg1": {"__init__.py": "a = 13"}}},
+ "src2": {"ns": {"mod2.py": "b = 37"}},
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+
+ mapping = {
+ "ns.pkgA": str(tmp_path / "src1/ns/pkg1"),
+ "ns": str(tmp_path / "src2/ns"),
+ }
+ namespaces_ = {"ns": [str(tmp_path / "src1"), str(tmp_path / "src2")]}
+ template = _finder_template(str(uuid4()), mapping, namespaces_)
+
+ with contexts.save_paths(), contexts.save_sys_modules():
+ for mod in ("ns", "ns.pkgA", "ns.mod2"):
+ sys.modules.pop(mod, None)
+
+ self.install_finder(template)
+ pkgA = import_module("ns.pkgA")
+ mod2 = import_module("ns.mod2")
+
+ expected = str((tmp_path / "src1/ns/pkg1").resolve())
+ assert_path(pkgA, expected)
+ assert pkgA.a == 13
+ assert mod2.b == 37
+
+ def test_combine_namespaces_nested(self, tmp_path):
+ """
+ Users may attempt to combine namespace packages in a nested way via
+ ``package_dir`` as shown in pypa/setuptools#4248.
+ """
+
+ files = {
+ "src": {"my_package": {"my_module.py": "a = 13"}},
+ "src2": {"my_package2": {"my_module2.py": "b = 37"}},
+ }
+
+ stack = jaraco.path.DirectoryStack()
+ with stack.context(tmp_path):
+ jaraco.path.build(files)
+ attrs = {
+ "script_name": "%PEP 517%",
+ "package_dir": {
+ "different_name": "src/my_package",
+ "different_name.subpkg": "src2/my_package2",
+ },
+ "packages": ["different_name", "different_name.subpkg"],
+ }
+ dist = Distribution(attrs)
+ finder = _TopLevelFinder(dist, str(uuid4()))
+ code = next(v for k, v in finder.get_implementation() if k.endswith(".py"))
+
+ with contexts.save_paths(), contexts.save_sys_modules():
+ for mod in attrs["packages"]:
+ sys.modules.pop(mod, None)
+
+ self.install_finder(code)
+ mod1 = import_module("different_name.my_module")
+ mod2 = import_module("different_name.subpkg.my_module2")
+
+ expected = str((tmp_path / "src/my_package/my_module.py").resolve())
+ assert str(Path(mod1.__file__).resolve()) == expected
+
+ expected = str((tmp_path / "src2/my_package2/my_module2.py").resolve())
+ assert str(Path(mod2.__file__).resolve()) == expected
+
+ assert mod1.a == 13
+ assert mod2.b == 37
+
+ def test_dynamic_path_computation(self, tmp_path):
+ # Follows the example in PEP 420
+ files = {
+ "project1": {"parent": {"child": {"one.py": "x = 1"}}},
+ "project2": {"parent": {"child": {"two.py": "x = 2"}}},
+ "project3": {"parent": {"child": {"three.py": "x = 3"}}},
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+ mapping = {}
+ namespaces_ = {"parent": [str(tmp_path / "project1/parent")]}
+ template = _finder_template(str(uuid4()), mapping, namespaces_)
+
+ mods = (f"parent.child.{name}" for name in ("one", "two", "three"))
+ with contexts.save_paths(), contexts.save_sys_modules():
+ for mod in ("parent", "parent.child", "parent.child", *mods):
+ sys.modules.pop(mod, None)
+
+ self.install_finder(template)
+
+ one = import_module("parent.child.one")
+ assert one.x == 1
+
+ with pytest.raises(ImportError):
+ import_module("parent.child.two")
+
+ sys.path.append(str(tmp_path / "project2"))
+ two = import_module("parent.child.two")
+ assert two.x == 2
+
+ with pytest.raises(ImportError):
+ import_module("parent.child.three")
+
+ sys.path.append(str(tmp_path / "project3"))
+ three = import_module("parent.child.three")
+ assert three.x == 3
+
+ def test_no_recursion(self, tmp_path):
+ # See issue #3550
+ files = {
+ "pkg": {
+ "__init__.py": "from . import pkg",
+ },
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+
+ mapping = {
+ "pkg": str(tmp_path / "pkg"),
+ }
+ template = _finder_template(str(uuid4()), mapping, {})
+
+ with contexts.save_paths(), contexts.save_sys_modules():
+ sys.modules.pop("pkg", None)
+
+ self.install_finder(template)
+ with pytest.raises(ImportError, match="pkg"):
+ import_module("pkg")
+
+ def test_similar_name(self, tmp_path):
+ files = {
+ "foo": {
+ "__init__.py": "",
+ "bar": {
+ "__init__.py": "",
+ },
+ },
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+
+ mapping = {
+ "foo": str(tmp_path / "foo"),
+ }
+ template = _finder_template(str(uuid4()), mapping, {})
+
+ with contexts.save_paths(), contexts.save_sys_modules():
+ sys.modules.pop("foo", None)
+ sys.modules.pop("foo.bar", None)
+
+ self.install_finder(template)
+ with pytest.raises(ImportError, match="foobar"):
+ import_module("foobar")
+
+ def test_case_sensitivity(self, tmp_path):
+ files = {
+ "foo": {
+ "__init__.py": "",
+ "lowercase.py": "x = 1",
+ "bar": {
+ "__init__.py": "",
+ "lowercase.py": "x = 2",
+ },
+ },
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+ mapping = {
+ "foo": str(tmp_path / "foo"),
+ }
+ template = _finder_template(str(uuid4()), mapping, {})
+ with contexts.save_paths(), contexts.save_sys_modules():
+ sys.modules.pop("foo", None)
+
+ self.install_finder(template)
+ with pytest.raises(ImportError, match="'FOO'"):
+ import_module("FOO")
+
+ with pytest.raises(ImportError, match="'foo\\.LOWERCASE'"):
+ import_module("foo.LOWERCASE")
+
+ with pytest.raises(ImportError, match="'foo\\.bar\\.Lowercase'"):
+ import_module("foo.bar.Lowercase")
+
+ with pytest.raises(ImportError, match="'foo\\.BAR'"):
+ import_module("foo.BAR.lowercase")
+
+ with pytest.raises(ImportError, match="'FOO'"):
+ import_module("FOO.bar.lowercase")
+
+ mod = import_module("foo.lowercase")
+ assert mod.x == 1
+
+ mod = import_module("foo.bar.lowercase")
+ assert mod.x == 2
+
+ def test_namespace_case_sensitivity(self, tmp_path):
+ files = {
+ "pkg": {
+ "__init__.py": "a = 13",
+ "foo": {
+ "__init__.py": "b = 37",
+ "bar.py": "c = 42",
+ },
+ },
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+
+ mapping = {"ns.othername": str(tmp_path / "pkg")}
+ namespaces = {"ns": []}
+
+ template = _finder_template(str(uuid4()), mapping, namespaces)
+ with contexts.save_paths(), contexts.save_sys_modules():
+ for mod in ("ns", "ns.othername"):
+ sys.modules.pop(mod, None)
+
+ self.install_finder(template)
+ pkg = import_module("ns.othername")
+ expected = str((tmp_path / "pkg").resolve())
+ assert_path(pkg, expected)
+ assert pkg.a == 13
+
+ foo = import_module("ns.othername.foo")
+ assert foo.b == 37
+
+ bar = import_module("ns.othername.foo.bar")
+ assert bar.c == 42
+
+ with pytest.raises(ImportError, match="'NS'"):
+ import_module("NS.othername.foo")
+
+ with pytest.raises(ImportError, match="'ns\\.othername\\.FOO\\'"):
+ import_module("ns.othername.FOO")
+
+ with pytest.raises(ImportError, match="'ns\\.othername\\.foo\\.BAR\\'"):
+ import_module("ns.othername.foo.BAR")
+
+ def test_intermediate_packages(self, tmp_path):
+ """
+ The finder should not import ``fullname`` if the intermediate segments
+ don't exist (see pypa/setuptools#4019).
+ """
+ files = {
+ "src": {
+ "mypkg": {
+ "__init__.py": "",
+ "config.py": "a = 13",
+ "helloworld.py": "b = 13",
+ "components": {
+ "config.py": "a = 37",
+ },
+ },
+ }
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+
+ mapping = {"mypkg": str(tmp_path / "src/mypkg")}
+ template = _finder_template(str(uuid4()), mapping, {})
+
+ with contexts.save_paths(), contexts.save_sys_modules():
+ for mod in (
+ "mypkg",
+ "mypkg.config",
+ "mypkg.helloworld",
+ "mypkg.components",
+ "mypkg.components.config",
+ "mypkg.components.helloworld",
+ ):
+ sys.modules.pop(mod, None)
+
+ self.install_finder(template)
+
+ config = import_module("mypkg.components.config")
+ assert config.a == 37
+
+ helloworld = import_module("mypkg.helloworld")
+ assert helloworld.b == 13
+
+ with pytest.raises(ImportError):
+ import_module("mypkg.components.helloworld")
+
+
+def test_pkg_roots(tmp_path):
+ """This test focus in getting a particular implementation detail right.
+ If at some point in time the implementation is changed for something different,
+ this test can be modified or even excluded.
+ """
+ files = {
+ "a": {"b": {"__init__.py": "ab = 1"}, "__init__.py": "a = 1"},
+ "d": {"__init__.py": "d = 1", "e": {"__init__.py": "de = 1"}},
+ "f": {"g": {"h": {"__init__.py": "fgh = 1"}}},
+ "other": {"__init__.py": "abc = 1"},
+ "another": {"__init__.py": "abcxyz = 1"},
+ "yet_another": {"__init__.py": "mnopq = 1"},
+ }
+ jaraco.path.build(files, prefix=tmp_path)
+ package_dir = {
+ "a.b.c": "other",
+ "a.b.c.x.y.z": "another",
+ "m.n.o.p.q": "yet_another",
+ }
+ packages = [
+ "a",
+ "a.b",
+ "a.b.c",
+ "a.b.c.x.y",
+ "a.b.c.x.y.z",
+ "d",
+ "d.e",
+ "f",
+ "f.g",
+ "f.g.h",
+ "m.n.o.p.q",
+ ]
+ roots = _find_package_roots(packages, package_dir, tmp_path)
+ assert roots == {
+ "a": str(tmp_path / "a"),
+ "a.b.c": str(tmp_path / "other"),
+ "a.b.c.x.y.z": str(tmp_path / "another"),
+ "d": str(tmp_path / "d"),
+ "f": str(tmp_path / "f"),
+ "m.n.o.p.q": str(tmp_path / "yet_another"),
+ }
+
+ ns = set(dict(_find_namespaces(packages, roots)))
+ assert ns == {"f", "f.g"}
+
+ ns = set(_find_virtual_namespaces(roots))
+ assert ns == {"a.b", "a.b.c.x", "a.b.c.x.y", "m", "m.n", "m.n.o", "m.n.o.p"}
+
+
+class TestOverallBehaviour:
+ PYPROJECT = """\
+ [build-system]
+ requires = ["setuptools"]
+ build-backend = "setuptools.build_meta"
+
+ [project]
+ name = "mypkg"
+ version = "3.14159"
+ """
+
+ # Any: Would need a TypedDict. Keep it simple for tests
+ FLAT_LAYOUT: dict[str, Any] = {
+ "pyproject.toml": dedent(PYPROJECT),
+ "MANIFEST.in": EXAMPLE["MANIFEST.in"],
+ "otherfile.py": "",
+ "mypkg": {
+ "__init__.py": "",
+ "mod1.py": "var = 42",
+ "subpackage": {
+ "__init__.py": "",
+ "mod2.py": "var = 13",
+ "resource_file.txt": "resource 39",
+ },
+ },
+ }
+
+ EXAMPLES = {
+ "flat-layout": FLAT_LAYOUT,
+ "src-layout": {
+ "pyproject.toml": dedent(PYPROJECT),
+ "MANIFEST.in": EXAMPLE["MANIFEST.in"],
+ "otherfile.py": "",
+ "src": {"mypkg": FLAT_LAYOUT["mypkg"]},
+ },
+ "custom-layout": {
+ "pyproject.toml": dedent(PYPROJECT)
+ + dedent(
+ """\
+ [tool.setuptools]
+ packages = ["mypkg", "mypkg.subpackage"]
+
+ [tool.setuptools.package-dir]
+ "mypkg.subpackage" = "other"
+ """
+ ),
+ "MANIFEST.in": EXAMPLE["MANIFEST.in"],
+ "otherfile.py": "",
+ "mypkg": {
+ "__init__.py": "",
+ "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"],
+ },
+ "other": FLAT_LAYOUT["mypkg"]["subpackage"],
+ },
+ "namespace": {
+ "pyproject.toml": dedent(PYPROJECT),
+ "MANIFEST.in": EXAMPLE["MANIFEST.in"],
+ "otherfile.py": "",
+ "src": {
+ "mypkg": {
+ "mod1.py": FLAT_LAYOUT["mypkg"]["mod1.py"],
+ "subpackage": FLAT_LAYOUT["mypkg"]["subpackage"],
+ },
+ },
+ },
+ }
+
+ @pytest.mark.xfail(sys.platform == "darwin", reason="pypa/setuptools#4328")
+ @pytest.mark.parametrize("layout", EXAMPLES.keys())
+ def test_editable_install(self, tmp_path, venv, layout, editable_opts):
+ project, _ = install_project(
+ "mypkg", venv, tmp_path, self.EXAMPLES[layout], *editable_opts
+ )
+
+ # Ensure stray files are not importable
+ cmd_import_error = """\
+ try:
+ import otherfile
+ except ImportError as ex:
+ print(ex)
+ """
+ out = venv.run(["python", "-c", dedent(cmd_import_error)])
+ assert "No module named 'otherfile'" in out
+
+ # Ensure the modules are importable
+ cmd_get_vars = """\
+ import mypkg, mypkg.mod1, mypkg.subpackage.mod2
+ print(mypkg.mod1.var, mypkg.subpackage.mod2.var)
+ """
+ out = venv.run(["python", "-c", dedent(cmd_get_vars)])
+ assert "42 13" in out
+
+ # Ensure resources are reachable
+ cmd_get_resource = """\
+ import mypkg.subpackage
+ from setuptools._importlib import resources as importlib_resources
+ text = importlib_resources.files(mypkg.subpackage) / "resource_file.txt"
+ print(text.read_text(encoding="utf-8"))
+ """
+ out = venv.run(["python", "-c", dedent(cmd_get_resource)])
+ assert "resource 39" in out
+
+ # Ensure files are editable
+ mod1 = next(project.glob("**/mod1.py"))
+ mod2 = next(project.glob("**/mod2.py"))
+ resource_file = next(project.glob("**/resource_file.txt"))
+
+ mod1.write_text("var = 17", encoding="utf-8")
+ mod2.write_text("var = 781", encoding="utf-8")
+ resource_file.write_text("resource 374", encoding="utf-8")
+
+ out = venv.run(["python", "-c", dedent(cmd_get_vars)])
+ assert "42 13" not in out
+ assert "17 781" in out
+
+ out = venv.run(["python", "-c", dedent(cmd_get_resource)])
+ assert "resource 39" not in out
+ assert "resource 374" in out
+
+
+class TestLinkTree:
+ FILES = deepcopy(TestOverallBehaviour.EXAMPLES["src-layout"])
+ FILES["pyproject.toml"] += dedent(
+ """\
+ [tool.setuptools]
+ # Temporary workaround: both `include-package-data` and `package-data` configs
+ # can be removed after #3260 is fixed.
+ include-package-data = false
+ package-data = {"*" = ["*.txt"]}
+
+ [tool.setuptools.packages.find]
+ where = ["src"]
+ exclude = ["*.subpackage*"]
+ """
+ )
+ FILES["src"]["mypkg"]["resource.not_in_manifest"] = "abc"
+
+ def test_generated_tree(self, tmp_path):
+ jaraco.path.build(self.FILES, prefix=tmp_path)
+
+ with _Path(tmp_path):
+ name = "mypkg-3.14159"
+ dist = Distribution({"script_name": "%PEP 517%"})
+ dist.parse_config_files()
+
+ wheel = Mock()
+ aux = tmp_path / ".aux"
+ build = tmp_path / ".build"
+ aux.mkdir()
+ build.mkdir()
+
+ build_py = dist.get_command_obj("build_py")
+ build_py.editable_mode = True
+ build_py.build_lib = str(build)
+ build_py.ensure_finalized()
+ outputs = build_py.get_outputs()
+ output_mapping = build_py.get_output_mapping()
+
+ make_tree = _LinkTree(dist, name, aux, build)
+ make_tree(wheel, outputs, output_mapping)
+
+ mod1 = next(aux.glob("**/mod1.py"))
+ expected = tmp_path / "src/mypkg/mod1.py"
+ assert_link_to(mod1, expected)
+
+ assert next(aux.glob("**/subpackage"), None) is None
+ assert next(aux.glob("**/mod2.py"), None) is None
+ assert next(aux.glob("**/resource_file.txt"), None) is None
+
+ assert next(aux.glob("**/resource.not_in_manifest"), None) is None
+
+ def test_strict_install(self, tmp_path, venv):
+ opts = ["--config-settings", "editable-mode=strict"]
+ install_project("mypkg", venv, tmp_path, self.FILES, *opts)
+
+ out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
+ assert "42" in out
+
+ # Ensure packages excluded from distribution are not importable
+ cmd_import_error = """\
+ try:
+ from mypkg import subpackage
+ except ImportError as ex:
+ print(ex)
+ """
+ out = venv.run(["python", "-c", dedent(cmd_import_error)])
+ assert "cannot import name 'subpackage'" in out
+
+ # Ensure resource files excluded from distribution are not reachable
+ cmd_get_resource = """\
+ import mypkg
+ from setuptools._importlib import resources as importlib_resources
+ try:
+ text = importlib_resources.files(mypkg) / "resource.not_in_manifest"
+ print(text.read_text(encoding="utf-8"))
+ except FileNotFoundError as ex:
+ print(ex)
+ """
+ out = venv.run(["python", "-c", dedent(cmd_get_resource)])
+ assert "No such file or directory" in out
+ assert "resource.not_in_manifest" in out
+
+
+@pytest.mark.filterwarnings("ignore:.*compat.*:setuptools.SetuptoolsDeprecationWarning")
+def test_compat_install(tmp_path, venv):
+ # TODO: Remove `compat` after Dec/2022.
+ opts = ["--config-settings", "editable-mode=compat"]
+ files = TestOverallBehaviour.EXAMPLES["custom-layout"]
+ install_project("mypkg", venv, tmp_path, files, *opts)
+
+ out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
+ assert "42" in out
+
+ expected_path = comparable_path(str(tmp_path))
+
+ # Compatible behaviour will make spurious modules and excluded
+ # files importable directly from the original path
+ for cmd in (
+ "import otherfile; print(otherfile)",
+ "import other; print(other)",
+ "import mypkg; print(mypkg)",
+ ):
+ out = comparable_path(venv.run(["python", "-c", cmd]))
+ assert expected_path in out
+
+ # Compatible behaviour will not consider custom mappings
+ cmd = """\
+ try:
+ from mypkg import subpackage;
+ except ImportError as ex:
+ print(ex)
+ """
+ out = venv.run(["python", "-c", dedent(cmd)])
+ assert "cannot import name 'subpackage'" in out
+
+
+def test_pbr_integration(tmp_path, venv, editable_opts):
+ """Ensure editable installs work with pbr, issue #3500"""
+ files = {
+ "pyproject.toml": dedent(
+ """\
+ [build-system]
+ requires = ["setuptools"]
+ build-backend = "setuptools.build_meta"
+ """
+ ),
+ "setup.py": dedent(
+ """\
+ __import__('setuptools').setup(
+ pbr=True,
+ setup_requires=["pbr"],
+ )
+ """
+ ),
+ "setup.cfg": dedent(
+ """\
+ [metadata]
+ name = mypkg
+
+ [files]
+ packages =
+ mypkg
+ """
+ ),
+ "mypkg": {
+ "__init__.py": "",
+ "hello.py": "print('Hello world!')",
+ },
+ "other": {"test.txt": "Another file in here."},
+ }
+ venv.run(["python", "-m", "pip", "install", "pbr"])
+
+ with contexts.environment(PBR_VERSION="0.42"):
+ install_project("mypkg", venv, tmp_path, files, *editable_opts)
+
+ out = venv.run(["python", "-c", "import mypkg.hello"])
+ assert "Hello world!" in out
+
+
+class TestCustomBuildPy:
+ """
+ Issue #3501 indicates that some plugins/customizations might rely on:
+
+ 1. ``build_py`` not running
+ 2. ``build_py`` always copying files to ``build_lib``
+
+ During the transition period setuptools should prevent potential errors from
+ happening due to those assumptions.
+ """
+
+ # TODO: Remove tests after _run_build_steps is removed.
+
+ FILES = {
+ **TestOverallBehaviour.EXAMPLES["flat-layout"],
+ "setup.py": dedent(
+ """\
+ import pathlib
+ from setuptools import setup
+ from setuptools.command.build_py import build_py as orig
+
+ class my_build_py(orig):
+ def run(self):
+ super().run()
+ raise ValueError("TEST_RAISE")
+
+ setup(cmdclass={"build_py": my_build_py})
+ """
+ ),
+ }
+
+ def test_safeguarded_from_errors(self, tmp_path, venv):
+ """Ensure that errors in custom build_py are reported as warnings"""
+ # Warnings should show up
+ _, out = install_project("mypkg", venv, tmp_path, self.FILES)
+ assert "SetuptoolsDeprecationWarning" in out
+ assert "ValueError: TEST_RAISE" in out
+ # but installation should be successful
+ out = venv.run(["python", "-c", "import mypkg.mod1; print(mypkg.mod1.var)"])
+ assert "42" in out
+
+
+class TestCustomBuildWheel:
+ def install_custom_build_wheel(self, dist):
+ bdist_wheel_cls = dist.get_command_class("bdist_wheel")
+
+ class MyBdistWheel(bdist_wheel_cls):
+ def get_tag(self):
+ # In issue #3513, we can see that some extensions may try to access
+ # the `plat_name` property in bdist_wheel
+ if self.plat_name.startswith("macosx-"):
+ _ = "macOS platform"
+ return super().get_tag()
+
+ dist.cmdclass["bdist_wheel"] = MyBdistWheel
+
+ def test_access_plat_name(self, tmpdir_cwd):
+ # Even when a custom bdist_wheel tries to access plat_name the build should
+ # be successful
+ jaraco.path.build({"module.py": "x = 42"})
+ dist = Distribution()
+ dist.script_name = "setup.py"
+ dist.set_defaults()
+ self.install_custom_build_wheel(dist)
+ cmd = editable_wheel(dist)
+ cmd.ensure_finalized()
+ cmd.run()
+ wheel_file = str(next(Path().glob('dist/*.whl')))
+ assert "editable" in wheel_file
+
+
+class TestCustomBuildExt:
+ def install_custom_build_ext_distutils(self, dist):
+ from distutils.command.build_ext import build_ext as build_ext_cls
+
+ class MyBuildExt(build_ext_cls):
+ pass
+
+ dist.cmdclass["build_ext"] = MyBuildExt
+
+ @pytest.mark.skipif(
+ sys.platform != "linux", reason="compilers may fail without correct setup"
+ )
+ def test_distutils_leave_inplace_files(self, tmpdir_cwd):
+ jaraco.path.build({"module.c": ""})
+ attrs = {
+ "ext_modules": [Extension("module", ["module.c"])],
+ }
+ dist = Distribution(attrs)
+ dist.script_name = "setup.py"
+ dist.set_defaults()
+ self.install_custom_build_ext_distutils(dist)
+ cmd = editable_wheel(dist)
+ cmd.ensure_finalized()
+ cmd.run()
+ wheel_file = str(next(Path().glob('dist/*.whl')))
+ assert "editable" in wheel_file
+ files = [p for p in Path().glob("module.*") if p.suffix != ".c"]
+ assert len(files) == 1
+ name = files[0].name
+ assert any(name.endswith(ext) for ext in EXTENSION_SUFFIXES)
+
+
+def test_debugging_tips(tmpdir_cwd, monkeypatch):
+ """Make sure to display useful debugging tips to the user."""
+ jaraco.path.build({"module.py": "x = 42"})
+ dist = Distribution()
+ dist.script_name = "setup.py"
+ dist.set_defaults()
+ cmd = editable_wheel(dist)
+ cmd.ensure_finalized()
+
+ SimulatedErr = type("SimulatedErr", (Exception,), {})
+ simulated_failure = Mock(side_effect=SimulatedErr())
+ monkeypatch.setattr(cmd, "get_finalized_command", simulated_failure)
+
+ expected_msg = "following steps are recommended to help debug"
+ with pytest.raises(SimulatedErr), pytest.warns(_DebuggingTips, match=expected_msg):
+ cmd.run()
+
+
+@pytest.mark.filterwarnings("error")
+def test_encode_pth():
+ """Ensure _encode_pth function does not produce encoding warnings"""
+ content = _encode_pth("tkmilan_ç_utf8") # no warnings (would be turned into errors)
+ assert isinstance(content, bytes)
+
+
+def install_project(name, venv, tmp_path, files, *opts):
+ project = tmp_path / name
+ project.mkdir()
+ jaraco.path.build(files, prefix=project)
+ opts = [*opts, "--no-build-isolation"] # force current version of setuptools
+ out = venv.run(
+ ["python", "-m", "pip", "-v", "install", "-e", str(project), *opts],
+ stderr=subprocess.STDOUT,
+ )
+ return project, out
+
+
+def _addsitedirs(new_dirs):
+ """To use this function, it is necessary to insert new_dir in front of sys.path.
+ The Python process will try to import a ``sitecustomize`` module on startup.
+ If we manipulate sys.path/PYTHONPATH, we can force it to run our code,
+ which invokes ``addsitedir`` and ensure ``.pth`` files are loaded.
+ """
+ content = '\n'.join(
+ ("import site",)
+ + tuple(f"site.addsitedir({os.fspath(new_dir)!r})" for new_dir in new_dirs)
+ )
+ (new_dirs[0] / "sitecustomize.py").write_text(content, encoding="utf-8")
+
+
+# ---- Assertion Helpers ----
+
+
+def assert_path(pkg, expected):
+ # __path__ is not guaranteed to exist, so we have to account for that
+ if pkg.__path__:
+ path = next(iter(pkg.__path__), None)
+ if path:
+ assert str(Path(path).resolve()) == expected
+
+
+def assert_link_to(file: Path, other: Path) -> None:
+ if file.is_symlink():
+ assert str(file.resolve()) == str(other.resolve())
+ else:
+ file_stat = file.stat()
+ other_stat = other.stat()
+ assert file_stat[stat.ST_INO] == other_stat[stat.ST_INO]
+ assert file_stat[stat.ST_DEV] == other_stat[stat.ST_DEV]
+
+
+def comparable_path(str_with_path: str) -> str:
+ return str_with_path.lower().replace(os.sep, "/").replace("//", "/")
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py
new file mode 100644
index 00000000..528e2c13
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_egg_info.py
@@ -0,0 +1,1305 @@
+from __future__ import annotations
+
+import ast
+import glob
+import os
+import re
+import stat
+import sys
+import time
+from pathlib import Path
+from unittest import mock
+
+import pytest
+from jaraco import path
+
+from setuptools import errors
+from setuptools.command.egg_info import egg_info, manifest_maker, write_entries
+from setuptools.dist import Distribution
+
+from . import contexts, environment
+from .textwrap import DALS
+
+
+class Environment(str):
+ pass
+
+
+@pytest.fixture
+def env():
+ with contexts.tempdir(prefix='setuptools-test.') as env_dir:
+ env = Environment(env_dir)
+ os.chmod(env_dir, stat.S_IRWXU)
+ subs = 'home', 'lib', 'scripts', 'data', 'egg-base'
+ env.paths = dict((dirname, os.path.join(env_dir, dirname)) for dirname in subs)
+ list(map(os.mkdir, env.paths.values()))
+ path.build({
+ env.paths['home']: {
+ '.pydistutils.cfg': DALS(
+ """
+ [egg_info]
+ egg-base = {egg-base}
+ """.format(**env.paths)
+ )
+ }
+ })
+ yield env
+
+
+class TestEggInfo:
+ setup_script = DALS(
+ """
+ from setuptools import setup
+
+ setup(
+ name='foo',
+ py_modules=['hello'],
+ entry_points={'console_scripts': ['hi = hello.run']},
+ zip_safe=False,
+ )
+ """
+ )
+
+ def _create_project(self):
+ path.build({
+ 'setup.py': self.setup_script,
+ 'hello.py': DALS(
+ """
+ def run():
+ print('hello')
+ """
+ ),
+ })
+
+ @staticmethod
+ def _extract_mv_version(pkg_info_lines: list[str]) -> tuple[int, int]:
+ version_str = pkg_info_lines[0].split(' ')[1]
+ major, minor = map(int, version_str.split('.')[:2])
+ return major, minor
+
+ def test_egg_info_save_version_info_setup_empty(self, tmpdir_cwd, env):
+ """
+ When the egg_info section is empty or not present, running
+ save_version_info should add the settings to the setup.cfg
+ in a deterministic order.
+ """
+ setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')
+ dist = Distribution()
+ ei = egg_info(dist)
+ ei.initialize_options()
+ ei.save_version_info(setup_cfg)
+
+ with open(setup_cfg, 'r', encoding="utf-8") as f:
+ content = f.read()
+
+ assert '[egg_info]' in content
+ assert 'tag_build =' in content
+ assert 'tag_date = 0' in content
+
+ expected_order = (
+ 'tag_build',
+ 'tag_date',
+ )
+
+ self._validate_content_order(content, expected_order)
+
+ @staticmethod
+ def _validate_content_order(content, expected):
+ """
+ Assert that the strings in expected appear in content
+ in order.
+ """
+ pattern = '.*'.join(expected)
+ flags = re.MULTILINE | re.DOTALL
+ assert re.search(pattern, content, flags)
+
+ def test_egg_info_save_version_info_setup_defaults(self, tmpdir_cwd, env):
+ """
+ When running save_version_info on an existing setup.cfg
+ with the 'default' values present from a previous run,
+ the file should remain unchanged.
+ """
+ setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')
+ path.build({
+ setup_cfg: DALS(
+ """
+ [egg_info]
+ tag_build =
+ tag_date = 0
+ """
+ ),
+ })
+ dist = Distribution()
+ ei = egg_info(dist)
+ ei.initialize_options()
+ ei.save_version_info(setup_cfg)
+
+ with open(setup_cfg, 'r', encoding="utf-8") as f:
+ content = f.read()
+
+ assert '[egg_info]' in content
+ assert 'tag_build =' in content
+ assert 'tag_date = 0' in content
+
+ expected_order = (
+ 'tag_build',
+ 'tag_date',
+ )
+
+ self._validate_content_order(content, expected_order)
+
+ def test_expected_files_produced(self, tmpdir_cwd, env):
+ self._create_project()
+
+ self._run_egg_info_command(tmpdir_cwd, env)
+ actual = os.listdir('foo.egg-info')
+
+ expected = [
+ 'PKG-INFO',
+ 'SOURCES.txt',
+ 'dependency_links.txt',
+ 'entry_points.txt',
+ 'not-zip-safe',
+ 'top_level.txt',
+ ]
+ assert sorted(actual) == expected
+
+ def test_handling_utime_error(self, tmpdir_cwd, env):
+ dist = Distribution()
+ ei = egg_info(dist)
+ utime_patch = mock.patch('os.utime', side_effect=OSError("TEST"))
+ mkpath_patch = mock.patch(
+ 'setuptools.command.egg_info.egg_info.mkpath', return_val=None
+ )
+
+ with utime_patch, mkpath_patch:
+ import distutils.errors
+
+ msg = r"Cannot update time stamp of directory 'None'"
+ with pytest.raises(distutils.errors.DistutilsFileError, match=msg):
+ ei.run()
+
+ def test_license_is_a_string(self, tmpdir_cwd, env):
+ setup_config = DALS(
+ """
+ [metadata]
+ name=foo
+ version=0.0.1
+ license=file:MIT
+ """
+ )
+
+ setup_script = DALS(
+ """
+ from setuptools import setup
+
+ setup()
+ """
+ )
+
+ path.build({
+ 'setup.py': setup_script,
+ 'setup.cfg': setup_config,
+ })
+
+ # This command should fail with a ValueError, but because it's
+ # currently configured to use a subprocess, the actual traceback
+ # object is lost and we need to parse it from stderr
+ with pytest.raises(AssertionError) as exc:
+ self._run_egg_info_command(tmpdir_cwd, env)
+
+ # The only argument to the assertion error should be a traceback
+ # containing a ValueError
+ assert 'ValueError' in exc.value.args[0]
+
+ def test_rebuilt(self, tmpdir_cwd, env):
+ """Ensure timestamps are updated when the command is re-run."""
+ self._create_project()
+
+ self._run_egg_info_command(tmpdir_cwd, env)
+ timestamp_a = os.path.getmtime('foo.egg-info')
+
+ # arbitrary sleep just to handle *really* fast systems
+ time.sleep(0.001)
+
+ self._run_egg_info_command(tmpdir_cwd, env)
+ timestamp_b = os.path.getmtime('foo.egg-info')
+
+ assert timestamp_a != timestamp_b
+
+ def test_manifest_template_is_read(self, tmpdir_cwd, env):
+ self._create_project()
+ path.build({
+ 'MANIFEST.in': DALS(
+ """
+ recursive-include docs *.rst
+ """
+ ),
+ 'docs': {
+ 'usage.rst': "Run 'hi'",
+ },
+ })
+ self._run_egg_info_command(tmpdir_cwd, env)
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt')
+ with open(sources_txt, encoding="utf-8") as f:
+ assert 'docs/usage.rst' in f.read().split('\n')
+
+ def _setup_script_with_requires(self, requires, use_setup_cfg=False):
+ setup_script = DALS(
+ """
+ from setuptools import setup
+
+ setup(name='foo', zip_safe=False, %s)
+ """
+ ) % ('' if use_setup_cfg else requires)
+ setup_config = requires if use_setup_cfg else ''
+ path.build({
+ 'setup.py': setup_script,
+ 'setup.cfg': setup_config,
+ })
+
+ mismatch_marker = f"python_version<'{sys.version_info[0]}'"
+ # Alternate equivalent syntax.
+ mismatch_marker_alternate = f'python_version < "{sys.version_info[0]}"'
+ invalid_marker = "<=>++"
+
+ class RequiresTestHelper:
+ @staticmethod
+ def parametrize(*test_list, **format_dict):
+ idlist = []
+ argvalues = []
+ for test in test_list:
+ test_params = test.lstrip().split('\n\n', 3)
+ name_kwargs = test_params.pop(0).split('\n')
+ if len(name_kwargs) > 1:
+ val = name_kwargs[1].strip()
+ install_cmd_kwargs = ast.literal_eval(val)
+ else:
+ install_cmd_kwargs = {}
+ name = name_kwargs[0].strip()
+ setup_py_requires, setup_cfg_requires, expected_requires = [
+ DALS(a).format(**format_dict) for a in test_params
+ ]
+ for id_, requires, use_cfg in (
+ (name, setup_py_requires, False),
+ (name + '_in_setup_cfg', setup_cfg_requires, True),
+ ):
+ idlist.append(id_)
+ marks = ()
+ if requires.startswith('@xfail\n'):
+ requires = requires[7:]
+ marks = pytest.mark.xfail
+ argvalues.append(
+ pytest.param(
+ requires,
+ use_cfg,
+ expected_requires,
+ install_cmd_kwargs,
+ marks=marks,
+ )
+ )
+ return pytest.mark.parametrize(
+ (
+ "requires",
+ "use_setup_cfg",
+ "expected_requires",
+ "install_cmd_kwargs",
+ ),
+ argvalues,
+ ids=idlist,
+ )
+
+ @RequiresTestHelper.parametrize(
+ # Format of a test:
+ #
+ # id
+ # install_cmd_kwargs [optional]
+ #
+ # requires block (when used in setup.py)
+ #
+ # requires block (when used in setup.cfg)
+ #
+ # expected contents of requires.txt
+ """
+ install_requires_deterministic
+
+ install_requires=["wheel>=0.5", "pytest"]
+
+ [options]
+ install_requires =
+ wheel>=0.5
+ pytest
+
+ wheel>=0.5
+ pytest
+ """,
+ """
+ install_requires_ordered
+
+ install_requires=["pytest>=3.0.2,!=10.9999"]
+
+ [options]
+ install_requires =
+ pytest>=3.0.2,!=10.9999
+
+ pytest!=10.9999,>=3.0.2
+ """,
+ """
+ install_requires_with_marker
+
+ install_requires=["barbazquux;{mismatch_marker}"],
+
+ [options]
+ install_requires =
+ barbazquux; {mismatch_marker}
+
+ [:{mismatch_marker_alternate}]
+ barbazquux
+ """,
+ """
+ install_requires_with_extra
+ {'cmd': ['egg_info']}
+
+ install_requires=["barbazquux [test]"],
+
+ [options]
+ install_requires =
+ barbazquux [test]
+
+ barbazquux[test]
+ """,
+ """
+ install_requires_with_extra_and_marker
+
+ install_requires=["barbazquux [test]; {mismatch_marker}"],
+
+ [options]
+ install_requires =
+ barbazquux [test]; {mismatch_marker}
+
+ [:{mismatch_marker_alternate}]
+ barbazquux[test]
+ """,
+ """
+ setup_requires_with_markers
+
+ setup_requires=["barbazquux;{mismatch_marker}"],
+
+ [options]
+ setup_requires =
+ barbazquux; {mismatch_marker}
+
+ """,
+ """
+ extras_require_with_extra
+ {'cmd': ['egg_info']}
+
+ extras_require={{"extra": ["barbazquux [test]"]}},
+
+ [options.extras_require]
+ extra = barbazquux [test]
+
+ [extra]
+ barbazquux[test]
+ """,
+ """
+ extras_require_with_extra_and_marker_in_req
+
+ extras_require={{"extra": ["barbazquux [test]; {mismatch_marker}"]}},
+
+ [options.extras_require]
+ extra =
+ barbazquux [test]; {mismatch_marker}
+
+ [extra]
+
+ [extra:{mismatch_marker_alternate}]
+ barbazquux[test]
+ """,
+ # FIXME: ConfigParser does not allow : in key names!
+ """
+ extras_require_with_marker
+
+ extras_require={{":{mismatch_marker}": ["barbazquux"]}},
+
+ @xfail
+ [options.extras_require]
+ :{mismatch_marker} = barbazquux
+
+ [:{mismatch_marker}]
+ barbazquux
+ """,
+ """
+ extras_require_with_marker_in_req
+
+ extras_require={{"extra": ["barbazquux; {mismatch_marker}"]}},
+
+ [options.extras_require]
+ extra =
+ barbazquux; {mismatch_marker}
+
+ [extra]
+
+ [extra:{mismatch_marker_alternate}]
+ barbazquux
+ """,
+ """
+ extras_require_with_empty_section
+
+ extras_require={{"empty": []}},
+
+ [options.extras_require]
+ empty =
+
+ [empty]
+ """,
+ # Format arguments.
+ invalid_marker=invalid_marker,
+ mismatch_marker=mismatch_marker,
+ mismatch_marker_alternate=mismatch_marker_alternate,
+ )
+ def test_requires(
+ self,
+ tmpdir_cwd,
+ env,
+ requires,
+ use_setup_cfg,
+ expected_requires,
+ install_cmd_kwargs,
+ ):
+ self._setup_script_with_requires(requires, use_setup_cfg)
+ self._run_egg_info_command(tmpdir_cwd, env, **install_cmd_kwargs)
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ requires_txt = os.path.join(egg_info_dir, 'requires.txt')
+ if os.path.exists(requires_txt):
+ with open(requires_txt, encoding="utf-8") as fp:
+ install_requires = fp.read()
+ else:
+ install_requires = ''
+ assert install_requires.lstrip() == expected_requires
+ assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
+
+ def test_install_requires_unordered_disallowed(self, tmpdir_cwd, env):
+ """
+ Packages that pass unordered install_requires sequences
+ should be rejected as they produce non-deterministic
+ builds. See #458.
+ """
+ req = 'install_requires={"fake-factory==0.5.2", "pytz"}'
+ self._setup_script_with_requires(req)
+ with pytest.raises(AssertionError):
+ self._run_egg_info_command(tmpdir_cwd, env)
+
+ def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env):
+ tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},'
+ req = tmpl.format(marker=self.invalid_marker)
+ self._setup_script_with_requires(req)
+ with pytest.raises(AssertionError):
+ self._run_egg_info_command(tmpdir_cwd, env)
+ assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
+
+ def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env):
+ tmpl = 'extras_require={{"extra": ["barbazquux; {marker}"]}},'
+ req = tmpl.format(marker=self.invalid_marker)
+ self._setup_script_with_requires(req)
+ with pytest.raises(AssertionError):
+ self._run_egg_info_command(tmpdir_cwd, env)
+ assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == []
+
+ def test_provides_extra(self, tmpdir_cwd, env):
+ self._setup_script_with_requires('extras_require={"foobar": ["barbazquux"]},')
+ environ = os.environ.copy().update(
+ HOME=env.paths['home'],
+ )
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ env=environ,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+ assert 'Provides-Extra: foobar' in pkg_info_lines
+ assert 'Metadata-Version: 2.4' in pkg_info_lines
+
+ def test_doesnt_provides_extra(self, tmpdir_cwd, env):
+ self._setup_script_with_requires(
+ """install_requires=["spam ; python_version<'3.6'"]"""
+ )
+ environ = os.environ.copy().update(
+ HOME=env.paths['home'],
+ )
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ env=environ,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_text = fp.read()
+ assert 'Provides-Extra:' not in pkg_info_text
+
+ @pytest.mark.parametrize(
+ ('files', 'license_in_sources'),
+ [
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICENSE
+ """
+ ),
+ 'LICENSE': "Test license",
+ },
+ True,
+ ), # with license
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = INVALID_LICENSE
+ """
+ ),
+ 'LICENSE': "Test license",
+ },
+ False,
+ ), # with an invalid license
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ """
+ ),
+ 'LICENSE': "Test license",
+ },
+ True,
+ ), # no license_file attribute, LICENSE auto-included
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICENSE
+ """
+ ),
+ 'MANIFEST.in': "exclude LICENSE",
+ 'LICENSE': "Test license",
+ },
+ True,
+ ), # manifest is overwritten by license_file
+ pytest.param(
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICEN[CS]E*
+ """
+ ),
+ 'LICENSE': "Test license",
+ },
+ True,
+ id="glob_pattern",
+ ),
+ ],
+ )
+ def test_setup_cfg_license_file(self, tmpdir_cwd, env, files, license_in_sources):
+ self._create_project()
+ path.build(files)
+
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+
+ sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")
+
+ if license_in_sources:
+ assert 'LICENSE' in sources_text
+ else:
+ assert 'LICENSE' not in sources_text
+ # for invalid license test
+ assert 'INVALID_LICENSE' not in sources_text
+
+ @pytest.mark.parametrize(
+ ('files', 'incl_licenses', 'excl_licenses'),
+ [
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files =
+ LICENSE-ABC
+ LICENSE-XYZ
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-XYZ': "XYZ license",
+ },
+ ['LICENSE-ABC', 'LICENSE-XYZ'],
+ [],
+ ), # with licenses
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files = LICENSE-ABC, LICENSE-XYZ
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-XYZ': "XYZ license",
+ },
+ ['LICENSE-ABC', 'LICENSE-XYZ'],
+ [],
+ ), # with commas
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files =
+ LICENSE-ABC
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-XYZ': "XYZ license",
+ },
+ ['LICENSE-ABC'],
+ ['LICENSE-XYZ'],
+ ), # with one license
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files =
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-XYZ': "XYZ license",
+ },
+ [],
+ ['LICENSE-ABC', 'LICENSE-XYZ'],
+ ), # empty
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files = LICENSE-XYZ
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-XYZ': "XYZ license",
+ },
+ ['LICENSE-XYZ'],
+ ['LICENSE-ABC'],
+ ), # on same line
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files =
+ LICENSE-ABC
+ INVALID_LICENSE
+ """
+ ),
+ 'LICENSE-ABC': "Test license",
+ },
+ ['LICENSE-ABC'],
+ ['INVALID_LICENSE'],
+ ), # with an invalid license
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ """
+ ),
+ 'LICENSE': "Test license",
+ },
+ ['LICENSE'],
+ [],
+ ), # no license_files attribute, LICENSE auto-included
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files = LICENSE
+ """
+ ),
+ 'MANIFEST.in': "exclude LICENSE",
+ 'LICENSE': "Test license",
+ },
+ ['LICENSE'],
+ [],
+ ), # manifest is overwritten by license_files
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files =
+ LICENSE-ABC
+ LICENSE-XYZ
+ """
+ ),
+ 'MANIFEST.in': "exclude LICENSE-XYZ",
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-XYZ': "XYZ license",
+ # manifest is overwritten by license_files
+ },
+ ['LICENSE-ABC', 'LICENSE-XYZ'],
+ [],
+ ),
+ pytest.param(
+ {
+ 'setup.cfg': "",
+ 'LICENSE-ABC': "ABC license",
+ 'COPYING-ABC': "ABC copying",
+ 'NOTICE-ABC': "ABC notice",
+ 'AUTHORS-ABC': "ABC authors",
+ 'LICENCE-XYZ': "XYZ license",
+ 'LICENSE': "License",
+ 'INVALID-LICENSE': "Invalid license",
+ },
+ [
+ 'LICENSE-ABC',
+ 'COPYING-ABC',
+ 'NOTICE-ABC',
+ 'AUTHORS-ABC',
+ 'LICENCE-XYZ',
+ 'LICENSE',
+ ],
+ ['INVALID-LICENSE'],
+ # ('LICEN[CS]E*', 'COPYING*', 'NOTICE*', 'AUTHORS*')
+ id="default_glob_patterns",
+ ),
+ pytest.param(
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files =
+ LICENSE*
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'NOTICE-XYZ': "XYZ notice",
+ },
+ ['LICENSE-ABC'],
+ ['NOTICE-XYZ'],
+ id="no_default_glob_patterns",
+ ),
+ pytest.param(
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files =
+ LICENSE-ABC
+ LICENSE*
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ },
+ ['LICENSE-ABC'],
+ [],
+ id="files_only_added_once",
+ ),
+ pytest.param(
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_files = **/LICENSE
+ """
+ ),
+ 'LICENSE': "ABC license",
+ 'LICENSE-OTHER': "Don't include",
+ 'vendor': {'LICENSE': "Vendor license"},
+ },
+ ['LICENSE', 'vendor/LICENSE'],
+ ['LICENSE-OTHER'],
+ id="recursive_glob",
+ ),
+ ],
+ )
+ def test_setup_cfg_license_files(
+ self, tmpdir_cwd, env, files, incl_licenses, excl_licenses
+ ):
+ self._create_project()
+ path.build(files)
+
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+
+ sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")
+ sources_lines = [line.strip() for line in sources_text.splitlines()]
+
+ for lf in incl_licenses:
+ assert sources_lines.count(lf) == 1
+
+ for lf in excl_licenses:
+ assert sources_lines.count(lf) == 0
+
+ @pytest.mark.parametrize(
+ ('files', 'incl_licenses', 'excl_licenses'),
+ [
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file =
+ license_files =
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-XYZ': "XYZ license",
+ },
+ [],
+ ['LICENSE-ABC', 'LICENSE-XYZ'],
+ ), # both empty
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file =
+ LICENSE-ABC
+ LICENSE-XYZ
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-XYZ': "XYZ license",
+ # license_file is still singular
+ },
+ [],
+ ['LICENSE-ABC', 'LICENSE-XYZ'],
+ ),
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICENSE-ABC
+ license_files =
+ LICENSE-XYZ
+ LICENSE-PQR
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-PQR': "PQR license",
+ 'LICENSE-XYZ': "XYZ license",
+ },
+ ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],
+ [],
+ ), # combined
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICENSE-ABC
+ license_files =
+ LICENSE-ABC
+ LICENSE-XYZ
+ LICENSE-PQR
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-PQR': "PQR license",
+ 'LICENSE-XYZ': "XYZ license",
+ # duplicate license
+ },
+ ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],
+ [],
+ ),
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICENSE-ABC
+ license_files =
+ LICENSE-XYZ
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-PQR': "PQR license",
+ 'LICENSE-XYZ': "XYZ license",
+ # combined subset
+ },
+ ['LICENSE-ABC', 'LICENSE-XYZ'],
+ ['LICENSE-PQR'],
+ ),
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICENSE-ABC
+ license_files =
+ LICENSE-XYZ
+ LICENSE-PQR
+ """
+ ),
+ 'LICENSE-PQR': "Test license",
+ # with invalid licenses
+ },
+ ['LICENSE-PQR'],
+ ['LICENSE-ABC', 'LICENSE-XYZ'],
+ ),
+ (
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICENSE-ABC
+ license_files =
+ LICENSE-PQR
+ LICENSE-XYZ
+ """
+ ),
+ 'MANIFEST.in': "exclude LICENSE-ABC\nexclude LICENSE-PQR",
+ 'LICENSE-ABC': "ABC license",
+ 'LICENSE-PQR': "PQR license",
+ 'LICENSE-XYZ': "XYZ license",
+ # manifest is overwritten
+ },
+ ['LICENSE-ABC', 'LICENSE-PQR', 'LICENSE-XYZ'],
+ [],
+ ),
+ pytest.param(
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICENSE*
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'NOTICE-XYZ': "XYZ notice",
+ },
+ ['LICENSE-ABC'],
+ ['NOTICE-XYZ'],
+ id="no_default_glob_patterns",
+ ),
+ pytest.param(
+ {
+ 'setup.cfg': DALS(
+ """
+ [metadata]
+ license_file = LICENSE*
+ license_files =
+ NOTICE*
+ """
+ ),
+ 'LICENSE-ABC': "ABC license",
+ 'NOTICE-ABC': "ABC notice",
+ 'AUTHORS-ABC': "ABC authors",
+ },
+ ['LICENSE-ABC', 'NOTICE-ABC'],
+ ['AUTHORS-ABC'],
+ id="combined_glob_patterrns",
+ ),
+ ],
+ )
+ def test_setup_cfg_license_file_license_files(
+ self, tmpdir_cwd, env, files, incl_licenses, excl_licenses
+ ):
+ self._create_project()
+ path.build(files)
+
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+
+ sources_text = Path(egg_info_dir, "SOURCES.txt").read_text(encoding="utf-8")
+ sources_lines = [line.strip() for line in sources_text.splitlines()]
+
+ for lf in incl_licenses:
+ assert sources_lines.count(lf) == 1
+
+ for lf in excl_licenses:
+ assert sources_lines.count(lf) == 0
+
+ def test_license_file_attr_pkg_info(self, tmpdir_cwd, env):
+ """All matched license files should have a corresponding License-File."""
+ self._create_project()
+ path.build({
+ "setup.cfg": DALS(
+ """
+ [metadata]
+ license_files =
+ NOTICE*
+ LICENSE*
+ **/LICENSE
+ """
+ ),
+ "LICENSE-ABC": "ABC license",
+ "LICENSE-XYZ": "XYZ license",
+ "NOTICE": "included",
+ "IGNORE": "not include",
+ "vendor": {'LICENSE': "Vendor license"},
+ })
+
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+ license_file_lines = [
+ line for line in pkg_info_lines if line.startswith('License-File:')
+ ]
+
+ # Only 'NOTICE', LICENSE-ABC', and 'LICENSE-XYZ' should have been matched
+ # Also assert that order from license_files is keeped
+ assert len(license_file_lines) == 4
+ assert "License-File: NOTICE" == license_file_lines[0]
+ assert "License-File: LICENSE-ABC" in license_file_lines[1:]
+ assert "License-File: LICENSE-XYZ" in license_file_lines[1:]
+ assert "License-File: vendor/LICENSE" in license_file_lines[3]
+
+ def test_metadata_version(self, tmpdir_cwd, env):
+ """Make sure latest metadata version is used by default."""
+ self._setup_script_with_requires("")
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+ # Update metadata version if changed
+ assert self._extract_mv_version(pkg_info_lines) == (2, 4)
+
+ def test_long_description_content_type(self, tmpdir_cwd, env):
+ # Test that specifying a `long_description_content_type` keyword arg to
+ # the `setup` function results in writing a `Description-Content-Type`
+ # line to the `PKG-INFO` file in the `<distribution>.egg-info`
+ # directory.
+ # `Description-Content-Type` is described at
+ # https://github.com/pypa/python-packaging-user-guide/pull/258
+
+ self._setup_script_with_requires(
+ """long_description_content_type='text/markdown',"""
+ )
+ environ = os.environ.copy().update(
+ HOME=env.paths['home'],
+ )
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ env=environ,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+ expected_line = 'Description-Content-Type: text/markdown'
+ assert expected_line in pkg_info_lines
+ assert 'Metadata-Version: 2.4' in pkg_info_lines
+
+ def test_long_description(self, tmpdir_cwd, env):
+ # Test that specifying `long_description` and `long_description_content_type`
+ # keyword args to the `setup` function results in writing
+ # the description in the message payload of the `PKG-INFO` file
+ # in the `<distribution>.egg-info` directory.
+ self._setup_script_with_requires(
+ "long_description='This is a long description\\nover multiple lines',"
+ "long_description_content_type='text/markdown',"
+ )
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+ assert 'Metadata-Version: 2.4' in pkg_info_lines
+ assert '' == pkg_info_lines[-1] # last line should be empty
+ long_desc_lines = pkg_info_lines[pkg_info_lines.index('') :]
+ assert 'This is a long description' in long_desc_lines
+ assert 'over multiple lines' in long_desc_lines
+
+ def test_project_urls(self, tmpdir_cwd, env):
+ # Test that specifying a `project_urls` dict to the `setup`
+ # function results in writing multiple `Project-URL` lines to
+ # the `PKG-INFO` file in the `<distribution>.egg-info`
+ # directory.
+ # `Project-URL` is described at https://packaging.python.org
+ # /specifications/core-metadata/#project-url-multiple-use
+
+ self._setup_script_with_requires(
+ """project_urls={
+ 'Link One': 'https://example.com/one/',
+ 'Link Two': 'https://example.com/two/',
+ },"""
+ )
+ environ = os.environ.copy().update(
+ HOME=env.paths['home'],
+ )
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ env=environ,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+ expected_line = 'Project-URL: Link One, https://example.com/one/'
+ assert expected_line in pkg_info_lines
+ expected_line = 'Project-URL: Link Two, https://example.com/two/'
+ assert expected_line in pkg_info_lines
+ assert self._extract_mv_version(pkg_info_lines) >= (1, 2)
+
+ def test_license(self, tmpdir_cwd, env):
+ """Test single line license."""
+ self._setup_script_with_requires("license='MIT',")
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+ assert 'License: MIT' in pkg_info_lines
+
+ def test_license_escape(self, tmpdir_cwd, env):
+ """Test license is escaped correctly if longer than one line."""
+ self._setup_script_with_requires(
+ "license='This is a long license text \\nover multiple lines',"
+ )
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+
+ assert 'License: This is a long license text ' in pkg_info_lines
+ assert ' over multiple lines' in pkg_info_lines
+ assert 'text \n over multiple' in '\n'.join(pkg_info_lines)
+
+ def test_python_requires_egg_info(self, tmpdir_cwd, env):
+ self._setup_script_with_requires("""python_requires='>=2.7.12',""")
+ environ = os.environ.copy().update(
+ HOME=env.paths['home'],
+ )
+ environment.run_setup_py(
+ cmd=['egg_info'],
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ env=environ,
+ )
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+ assert 'Requires-Python: >=2.7.12' in pkg_info_lines
+ assert self._extract_mv_version(pkg_info_lines) >= (1, 2)
+
+ def test_manifest_maker_warning_suppression(self):
+ fixtures = [
+ "standard file not found: should have one of foo.py, bar.py",
+ "standard file 'setup.py' not found",
+ ]
+
+ for msg in fixtures:
+ assert manifest_maker._should_suppress_warning(msg)
+
+ def test_egg_info_includes_setup_py(self, tmpdir_cwd):
+ self._create_project()
+ dist = Distribution({"name": "foo", "version": "0.0.1"})
+ dist.script_name = "non_setup.py"
+ egg_info_instance = egg_info(dist)
+ egg_info_instance.finalize_options()
+ egg_info_instance.run()
+
+ assert 'setup.py' in egg_info_instance.filelist.files
+
+ with open(egg_info_instance.egg_info + "/SOURCES.txt", encoding="utf-8") as f:
+ sources = f.read().split('\n')
+ assert 'setup.py' in sources
+
+ def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None):
+ environ = os.environ.copy().update(
+ HOME=env.paths['home'],
+ )
+ if cmd is None:
+ cmd = [
+ 'egg_info',
+ ]
+ code, data = environment.run_setup_py(
+ cmd=cmd,
+ pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]),
+ data_stream=1,
+ env=environ,
+ )
+ assert not code, data
+
+ if output:
+ assert output in data
+
+ def test_egg_info_tag_only_once(self, tmpdir_cwd, env):
+ self._create_project()
+ path.build({
+ 'setup.cfg': DALS(
+ """
+ [egg_info]
+ tag_build = dev
+ tag_date = 0
+ tag_svn_revision = 0
+ """
+ ),
+ })
+ self._run_egg_info_command(tmpdir_cwd, env)
+ egg_info_dir = os.path.join('.', 'foo.egg-info')
+ with open(os.path.join(egg_info_dir, 'PKG-INFO'), encoding="utf-8") as fp:
+ pkg_info_lines = fp.read().split('\n')
+ assert 'Version: 0.0.0.dev0' in pkg_info_lines
+
+
+class TestWriteEntries:
+ def test_invalid_entry_point(self, tmpdir_cwd, env):
+ dist = Distribution({"name": "foo", "version": "0.0.1"})
+ dist.entry_points = {"foo": "foo = invalid-identifier:foo"}
+ cmd = dist.get_command_obj("egg_info")
+ expected_msg = r"Problems to parse .*invalid-identifier.*"
+ with pytest.raises(errors.OptionError, match=expected_msg) as ex:
+ write_entries(cmd, "entry_points", "entry_points.txt")
+ assert "ensure entry-point follows the spec" in ex.value.args[0]
+
+ def test_valid_entry_point(self, tmpdir_cwd, env):
+ dist = Distribution({"name": "foo", "version": "0.0.1"})
+ dist.entry_points = {
+ "abc": "foo = bar:baz",
+ "def": ["faa = bor:boz"],
+ }
+ cmd = dist.get_command_obj("egg_info")
+ write_entries(cmd, "entry_points", "entry_points.txt")
+ content = Path("entry_points.txt").read_text(encoding="utf-8")
+ assert "[abc]\nfoo = bar:baz\n" in content
+ assert "[def]\nfaa = bor:boz\n" in content
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_extern.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_extern.py
new file mode 100644
index 00000000..d7eb3c62
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_extern.py
@@ -0,0 +1,15 @@
+import importlib
+import pickle
+
+import packaging
+
+from setuptools import Distribution
+
+
+def test_reimport_extern():
+ packaging2 = importlib.import_module(packaging.__name__)
+ assert packaging is packaging2
+
+
+def test_distribution_picklable():
+ pickle.loads(pickle.dumps(Distribution()))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_find_packages.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_find_packages.py
new file mode 100644
index 00000000..9fd9f8f6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_find_packages.py
@@ -0,0 +1,218 @@
+"""Tests for automatic package discovery"""
+
+import os
+import shutil
+import tempfile
+
+import pytest
+
+from setuptools import find_namespace_packages, find_packages
+from setuptools.discovery import FlatLayoutPackageFinder
+
+from .compat.py39 import os_helper
+
+
+class TestFindPackages:
+ def setup_method(self, method):
+ self.dist_dir = tempfile.mkdtemp()
+ self._make_pkg_structure()
+
+ def teardown_method(self, method):
+ shutil.rmtree(self.dist_dir)
+
+ def _make_pkg_structure(self):
+ """Make basic package structure.
+
+ dist/
+ docs/
+ conf.py
+ pkg/
+ __pycache__/
+ nspkg/
+ mod.py
+ subpkg/
+ assets/
+ asset
+ __init__.py
+ setup.py
+
+ """
+ self.docs_dir = self._mkdir('docs', self.dist_dir)
+ self._touch('conf.py', self.docs_dir)
+ self.pkg_dir = self._mkdir('pkg', self.dist_dir)
+ self._mkdir('__pycache__', self.pkg_dir)
+ self.ns_pkg_dir = self._mkdir('nspkg', self.pkg_dir)
+ self._touch('mod.py', self.ns_pkg_dir)
+ self.sub_pkg_dir = self._mkdir('subpkg', self.pkg_dir)
+ self.asset_dir = self._mkdir('assets', self.sub_pkg_dir)
+ self._touch('asset', self.asset_dir)
+ self._touch('__init__.py', self.sub_pkg_dir)
+ self._touch('setup.py', self.dist_dir)
+
+ def _mkdir(self, path, parent_dir=None):
+ if parent_dir:
+ path = os.path.join(parent_dir, path)
+ os.mkdir(path)
+ return path
+
+ def _touch(self, path, dir_=None):
+ if dir_:
+ path = os.path.join(dir_, path)
+ open(path, 'wb').close()
+ return path
+
+ def test_regular_package(self):
+ self._touch('__init__.py', self.pkg_dir)
+ packages = find_packages(self.dist_dir)
+ assert packages == ['pkg', 'pkg.subpkg']
+
+ def test_exclude(self):
+ self._touch('__init__.py', self.pkg_dir)
+ packages = find_packages(self.dist_dir, exclude=('pkg.*',))
+ assert packages == ['pkg']
+
+ def test_exclude_recursive(self):
+ """
+ Excluding a parent package should not exclude child packages as well.
+ """
+ self._touch('__init__.py', self.pkg_dir)
+ self._touch('__init__.py', self.sub_pkg_dir)
+ packages = find_packages(self.dist_dir, exclude=('pkg',))
+ assert packages == ['pkg.subpkg']
+
+ def test_include_excludes_other(self):
+ """
+ If include is specified, other packages should be excluded.
+ """
+ self._touch('__init__.py', self.pkg_dir)
+ alt_dir = self._mkdir('other_pkg', self.dist_dir)
+ self._touch('__init__.py', alt_dir)
+ packages = find_packages(self.dist_dir, include=['other_pkg'])
+ assert packages == ['other_pkg']
+
+ def test_dir_with_dot_is_skipped(self):
+ shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets'))
+ data_dir = self._mkdir('some.data', self.pkg_dir)
+ self._touch('__init__.py', data_dir)
+ self._touch('file.dat', data_dir)
+ packages = find_packages(self.dist_dir)
+ assert 'pkg.some.data' not in packages
+
+ def test_dir_with_packages_in_subdir_is_excluded(self):
+ """
+ Ensure that a package in a non-package such as build/pkg/__init__.py
+ is excluded.
+ """
+ build_dir = self._mkdir('build', self.dist_dir)
+ build_pkg_dir = self._mkdir('pkg', build_dir)
+ self._touch('__init__.py', build_pkg_dir)
+ packages = find_packages(self.dist_dir)
+ assert 'build.pkg' not in packages
+
+ @pytest.mark.skipif(not os_helper.can_symlink(), reason='Symlink support required')
+ def test_symlinked_packages_are_included(self):
+ """
+ A symbolically-linked directory should be treated like any other
+ directory when matched as a package.
+
+ Create a link from lpkg -> pkg.
+ """
+ self._touch('__init__.py', self.pkg_dir)
+ linked_pkg = os.path.join(self.dist_dir, 'lpkg')
+ os.symlink('pkg', linked_pkg)
+ assert os.path.isdir(linked_pkg)
+ packages = find_packages(self.dist_dir)
+ assert 'lpkg' in packages
+
+ def _assert_packages(self, actual, expected):
+ assert set(actual) == set(expected)
+
+ def test_pep420_ns_package(self):
+ packages = find_namespace_packages(
+ self.dist_dir, include=['pkg*'], exclude=['pkg.subpkg.assets']
+ )
+ self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
+
+ def test_pep420_ns_package_no_includes(self):
+ packages = find_namespace_packages(self.dist_dir, exclude=['pkg.subpkg.assets'])
+ self._assert_packages(packages, ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg'])
+
+ def test_pep420_ns_package_no_includes_or_excludes(self):
+ packages = find_namespace_packages(self.dist_dir)
+ expected = ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets']
+ self._assert_packages(packages, expected)
+
+ def test_regular_package_with_nested_pep420_ns_packages(self):
+ self._touch('__init__.py', self.pkg_dir)
+ packages = find_namespace_packages(
+ self.dist_dir, exclude=['docs', 'pkg.subpkg.assets']
+ )
+ self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
+
+ def test_pep420_ns_package_no_non_package_dirs(self):
+ shutil.rmtree(self.docs_dir)
+ shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets'))
+ packages = find_namespace_packages(self.dist_dir)
+ self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
+
+
+class TestFlatLayoutPackageFinder:
+ EXAMPLES = {
+ "hidden-folders": (
+ [".pkg/__init__.py", "pkg/__init__.py", "pkg/nested/file.txt"],
+ ["pkg", "pkg.nested"],
+ ),
+ "private-packages": (
+ ["_pkg/__init__.py", "pkg/_private/__init__.py"],
+ ["pkg", "pkg._private"],
+ ),
+ "invalid-name": (
+ ["invalid-pkg/__init__.py", "other.pkg/__init__.py", "yet,another/file.py"],
+ [],
+ ),
+ "docs": (["pkg/__init__.py", "docs/conf.py", "docs/readme.rst"], ["pkg"]),
+ "tests": (
+ ["pkg/__init__.py", "tests/test_pkg.py", "tests/__init__.py"],
+ ["pkg"],
+ ),
+ "examples": (
+ [
+ "pkg/__init__.py",
+ "examples/__init__.py",
+ "examples/file.py",
+ "example/other_file.py",
+ # Sub-packages should always be fine
+ "pkg/example/__init__.py",
+ "pkg/examples/__init__.py",
+ ],
+ ["pkg", "pkg.examples", "pkg.example"],
+ ),
+ "tool-specific": (
+ [
+ "htmlcov/index.html",
+ "pkg/__init__.py",
+ "tasks/__init__.py",
+ "tasks/subpackage/__init__.py",
+ "fabfile/__init__.py",
+ "fabfile/subpackage/__init__.py",
+ # Sub-packages should always be fine
+ "pkg/tasks/__init__.py",
+ "pkg/fabfile/__init__.py",
+ ],
+ ["pkg", "pkg.tasks", "pkg.fabfile"],
+ ),
+ }
+
+ @pytest.mark.parametrize("example", EXAMPLES.keys())
+ def test_unwanted_directories_not_included(self, tmp_path, example):
+ files, expected_packages = self.EXAMPLES[example]
+ ensure_files(tmp_path, files)
+ found_packages = FlatLayoutPackageFinder.find(str(tmp_path))
+ assert set(found_packages) == set(expected_packages)
+
+
+def ensure_files(root_path, files):
+ for file in files:
+ path = root_path / file
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.touch()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_find_py_modules.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_find_py_modules.py
new file mode 100644
index 00000000..8034b544
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_find_py_modules.py
@@ -0,0 +1,73 @@
+"""Tests for automatic discovery of modules"""
+
+import os
+
+import pytest
+
+from setuptools.discovery import FlatLayoutModuleFinder, ModuleFinder
+
+from .compat.py39 import os_helper
+from .test_find_packages import ensure_files
+
+
+class TestModuleFinder:
+ def find(self, path, *args, **kwargs):
+ return set(ModuleFinder.find(str(path), *args, **kwargs))
+
+ EXAMPLES = {
+ # circumstance: (files, kwargs, expected_modules)
+ "simple_folder": (
+ ["file.py", "other.py"],
+ {}, # kwargs
+ ["file", "other"],
+ ),
+ "exclude": (
+ ["file.py", "other.py"],
+ {"exclude": ["f*"]},
+ ["other"],
+ ),
+ "include": (
+ ["file.py", "fole.py", "other.py"],
+ {"include": ["f*"], "exclude": ["fo*"]},
+ ["file"],
+ ),
+ "invalid-name": (["my-file.py", "other.file.py"], {}, []),
+ }
+
+ @pytest.mark.parametrize("example", EXAMPLES.keys())
+ def test_finder(self, tmp_path, example):
+ files, kwargs, expected_modules = self.EXAMPLES[example]
+ ensure_files(tmp_path, files)
+ assert self.find(tmp_path, **kwargs) == set(expected_modules)
+
+ @pytest.mark.skipif(not os_helper.can_symlink(), reason='Symlink support required')
+ def test_symlinked_packages_are_included(self, tmp_path):
+ src = "_myfiles/file.py"
+ ensure_files(tmp_path, [src])
+ os.symlink(tmp_path / src, tmp_path / "link.py")
+ assert self.find(tmp_path) == {"link"}
+
+
+class TestFlatLayoutModuleFinder:
+ def find(self, path, *args, **kwargs):
+ return set(FlatLayoutModuleFinder.find(str(path)))
+
+ EXAMPLES = {
+ # circumstance: (files, expected_modules)
+ "hidden-files": ([".module.py"], []),
+ "private-modules": (["_module.py"], []),
+ "common-names": (
+ ["setup.py", "conftest.py", "test.py", "tests.py", "example.py", "mod.py"],
+ ["mod"],
+ ),
+ "tool-specific": (
+ ["tasks.py", "fabfile.py", "noxfile.py", "dodo.py", "manage.py", "mod.py"],
+ ["mod"],
+ ),
+ }
+
+ @pytest.mark.parametrize("example", EXAMPLES.keys())
+ def test_unwanted_files_not_included(self, tmp_path, example):
+ files, expected_modules = self.EXAMPLES[example]
+ ensure_files(tmp_path, files)
+ assert self.find(tmp_path) == set(expected_modules)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_glob.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_glob.py
new file mode 100644
index 00000000..8d225a44
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_glob.py
@@ -0,0 +1,45 @@
+import pytest
+from jaraco import path
+
+from setuptools.glob import glob
+
+
+@pytest.mark.parametrize(
+ ('tree', 'pattern', 'matches'),
+ (
+ ('', b'', []),
+ ('', '', []),
+ (
+ """
+ appveyor.yml
+ CHANGES.rst
+ LICENSE
+ MANIFEST.in
+ pyproject.toml
+ README.rst
+ setup.cfg
+ setup.py
+ """,
+ '*.rst',
+ ('CHANGES.rst', 'README.rst'),
+ ),
+ (
+ """
+ appveyor.yml
+ CHANGES.rst
+ LICENSE
+ MANIFEST.in
+ pyproject.toml
+ README.rst
+ setup.cfg
+ setup.py
+ """,
+ b'*.rst',
+ (b'CHANGES.rst', b'README.rst'),
+ ),
+ ),
+)
+def test_glob(monkeypatch, tmpdir, tree, pattern, matches):
+ monkeypatch.chdir(tmpdir)
+ path.build({name: '' for name in tree.split()})
+ assert list(sorted(glob(pattern))) == list(sorted(matches))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_install_scripts.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_install_scripts.py
new file mode 100644
index 00000000..e62a6b7f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_install_scripts.py
@@ -0,0 +1,89 @@
+"""install_scripts tests"""
+
+import sys
+
+import pytest
+
+from setuptools.command.install_scripts import install_scripts
+from setuptools.dist import Distribution
+
+from . import contexts
+
+
+class TestInstallScripts:
+ settings = dict(
+ name='foo',
+ entry_points={'console_scripts': ['foo=foo:foo']},
+ version='0.0',
+ )
+ unix_exe = '/usr/dummy-test-path/local/bin/python'
+ unix_spaces_exe = '/usr/bin/env dummy-test-python'
+ win32_exe = 'C:\\Dummy Test Path\\Program Files\\Python 3.6\\python.exe'
+
+ def _run_install_scripts(self, install_dir, executable=None):
+ dist = Distribution(self.settings)
+ dist.script_name = 'setup.py'
+ cmd = install_scripts(dist)
+ cmd.install_dir = install_dir
+ if executable is not None:
+ bs = cmd.get_finalized_command('build_scripts')
+ bs.executable = executable
+ cmd.ensure_finalized()
+ with contexts.quiet():
+ cmd.run()
+
+ @pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only')
+ def test_sys_executable_escaping_unix(self, tmpdir, monkeypatch):
+ """
+ Ensure that shebang is not quoted on Unix when getting the Python exe
+ from sys.executable.
+ """
+ expected = f'#!{self.unix_exe}\n'
+ monkeypatch.setattr('sys.executable', self.unix_exe)
+ with tmpdir.as_cwd():
+ self._run_install_scripts(str(tmpdir))
+ with open(str(tmpdir.join('foo')), 'r', encoding="utf-8") as f:
+ actual = f.readline()
+ assert actual == expected
+
+ @pytest.mark.skipif(sys.platform != 'win32', reason='Windows only')
+ def test_sys_executable_escaping_win32(self, tmpdir, monkeypatch):
+ """
+ Ensure that shebang is quoted on Windows when getting the Python exe
+ from sys.executable and it contains a space.
+ """
+ expected = f'#!"{self.win32_exe}"\n'
+ monkeypatch.setattr('sys.executable', self.win32_exe)
+ with tmpdir.as_cwd():
+ self._run_install_scripts(str(tmpdir))
+ with open(str(tmpdir.join('foo-script.py')), 'r', encoding="utf-8") as f:
+ actual = f.readline()
+ assert actual == expected
+
+ @pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only')
+ def test_executable_with_spaces_escaping_unix(self, tmpdir):
+ """
+ Ensure that shebang on Unix is not quoted, even when
+ a value with spaces
+ is specified using --executable.
+ """
+ expected = f'#!{self.unix_spaces_exe}\n'
+ with tmpdir.as_cwd():
+ self._run_install_scripts(str(tmpdir), self.unix_spaces_exe)
+ with open(str(tmpdir.join('foo')), 'r', encoding="utf-8") as f:
+ actual = f.readline()
+ assert actual == expected
+
+ @pytest.mark.skipif(sys.platform != 'win32', reason='Windows only')
+ def test_executable_arg_escaping_win32(self, tmpdir):
+ """
+ Ensure that shebang on Windows is quoted when
+ getting a path with spaces
+ from --executable, that is itself properly quoted.
+ """
+ expected = f'#!"{self.win32_exe}"\n'
+ with tmpdir.as_cwd():
+ self._run_install_scripts(str(tmpdir), '"' + self.win32_exe + '"')
+ with open(str(tmpdir.join('foo-script.py')), 'r', encoding="utf-8") as f:
+ actual = f.readline()
+ assert actual == expected
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_logging.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_logging.py
new file mode 100644
index 00000000..ea58001e
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_logging.py
@@ -0,0 +1,76 @@
+import functools
+import inspect
+import logging
+import sys
+
+import pytest
+
+IS_PYPY = '__pypy__' in sys.builtin_module_names
+
+
+setup_py = """\
+from setuptools import setup
+
+setup(
+ name="test_logging",
+ version="0.0"
+)
+"""
+
+
+@pytest.mark.parametrize(
+ ('flag', 'expected_level'), [("--dry-run", "INFO"), ("--verbose", "DEBUG")]
+)
+def test_verbosity_level(tmp_path, monkeypatch, flag, expected_level):
+ """Make sure the correct verbosity level is set (issue #3038)"""
+ import setuptools # noqa: F401 # import setuptools to monkeypatch distutils
+
+ import distutils # <- load distutils after all the patches take place
+
+ logger = logging.Logger(__name__)
+ monkeypatch.setattr(logging, "root", logger)
+ unset_log_level = logger.getEffectiveLevel()
+ assert logging.getLevelName(unset_log_level) == "NOTSET"
+
+ setup_script = tmp_path / "setup.py"
+ setup_script.write_text(setup_py, encoding="utf-8")
+ dist = distutils.core.run_setup(setup_script, stop_after="init")
+ dist.script_args = [flag, "sdist"]
+ dist.parse_command_line() # <- where the log level is set
+ log_level = logger.getEffectiveLevel()
+ log_level_name = logging.getLevelName(log_level)
+ assert log_level_name == expected_level
+
+
+def flaky_on_pypy(func):
+ @functools.wraps(func)
+ def _func():
+ try:
+ func()
+ except AssertionError: # pragma: no cover
+ if IS_PYPY:
+ msg = "Flaky monkeypatch on PyPy (#4124)"
+ pytest.xfail(f"{msg}. Original discussion in #3707, #3709.")
+ raise
+
+ return _func
+
+
+@flaky_on_pypy
+def test_patching_does_not_cause_problems():
+ # Ensure `dist.log` is only patched if necessary
+
+ import _distutils_hack
+
+ import setuptools.logging
+
+ from distutils import dist
+
+ setuptools.logging.configure()
+
+ if _distutils_hack.enabled():
+ # Modern logging infra, no problematic patching.
+ assert dist.__file__ is None or "setuptools" in dist.__file__
+ assert isinstance(dist.log, logging.Logger)
+ else:
+ assert inspect.ismodule(dist.log)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_manifest.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_manifest.py
new file mode 100644
index 00000000..903a528d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_manifest.py
@@ -0,0 +1,622 @@
+"""sdist tests"""
+
+from __future__ import annotations
+
+import contextlib
+import io
+import itertools
+import logging
+import os
+import shutil
+import sys
+import tempfile
+
+import pytest
+
+from setuptools.command.egg_info import FileList, egg_info, translate_pattern
+from setuptools.dist import Distribution
+from setuptools.tests.textwrap import DALS
+
+from distutils import log
+from distutils.errors import DistutilsTemplateError
+
+IS_PYPY = '__pypy__' in sys.builtin_module_names
+
+
+def make_local_path(s):
+ """Converts '/' in a string to os.sep"""
+ return s.replace('/', os.sep)
+
+
+SETUP_ATTRS = {
+ 'name': 'app',
+ 'version': '0.0',
+ 'packages': ['app'],
+}
+
+SETUP_PY = f"""\
+from setuptools import setup
+
+setup(**{SETUP_ATTRS!r})
+"""
+
+
+@contextlib.contextmanager
+def quiet():
+ old_stdout, old_stderr = sys.stdout, sys.stderr
+ sys.stdout, sys.stderr = io.StringIO(), io.StringIO()
+ try:
+ yield
+ finally:
+ sys.stdout, sys.stderr = old_stdout, old_stderr
+
+
+def touch(filename):
+ open(filename, 'wb').close()
+
+
+# The set of files always in the manifest, including all files in the
+# .egg-info directory
+default_files = frozenset(
+ map(
+ make_local_path,
+ [
+ 'README.rst',
+ 'MANIFEST.in',
+ 'setup.py',
+ 'app.egg-info/PKG-INFO',
+ 'app.egg-info/SOURCES.txt',
+ 'app.egg-info/dependency_links.txt',
+ 'app.egg-info/top_level.txt',
+ 'app/__init__.py',
+ ],
+ )
+)
+
+
+translate_specs: list[tuple[str, list[str], list[str]]] = [
+ ('foo', ['foo'], ['bar', 'foobar']),
+ ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']),
+ # Glob matching
+ ('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']),
+ ('dir/*.txt', ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']),
+ ('*/*.py', ['bin/start.py'], []),
+ ('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']),
+ # Globstars change what they mean depending upon where they are
+ (
+ 'foo/**/bar',
+ ['foo/bing/bar', 'foo/bing/bang/bar', 'foo/bar'],
+ ['foo/abar'],
+ ),
+ (
+ 'foo/**',
+ ['foo/bar/bing.py', 'foo/x'],
+ ['/foo/x'],
+ ),
+ (
+ '**',
+ ['x', 'abc/xyz', '@nything'],
+ [],
+ ),
+ # Character classes
+ (
+ 'pre[one]post',
+ ['preopost', 'prenpost', 'preepost'],
+ ['prepost', 'preonepost'],
+ ),
+ (
+ 'hello[!one]world',
+ ['helloxworld', 'helloyworld'],
+ ['hellooworld', 'helloworld', 'hellooneworld'],
+ ),
+ (
+ '[]one].txt',
+ ['o.txt', '].txt', 'e.txt'],
+ ['one].txt'],
+ ),
+ (
+ 'foo[!]one]bar',
+ ['fooybar'],
+ ['foo]bar', 'fooobar', 'fooebar'],
+ ),
+]
+"""
+A spec of inputs for 'translate_pattern' and matches and mismatches
+for that input.
+"""
+
+match_params = itertools.chain.from_iterable(
+ zip(itertools.repeat(pattern), matches)
+ for pattern, matches, mismatches in translate_specs
+)
+
+
+@pytest.fixture(params=match_params)
+def pattern_match(request):
+ return map(make_local_path, request.param)
+
+
+mismatch_params = itertools.chain.from_iterable(
+ zip(itertools.repeat(pattern), mismatches)
+ for pattern, matches, mismatches in translate_specs
+)
+
+
+@pytest.fixture(params=mismatch_params)
+def pattern_mismatch(request):
+ return map(make_local_path, request.param)
+
+
+def test_translated_pattern_match(pattern_match):
+ pattern, target = pattern_match
+ assert translate_pattern(pattern).match(target)
+
+
+def test_translated_pattern_mismatch(pattern_mismatch):
+ pattern, target = pattern_mismatch
+ assert not translate_pattern(pattern).match(target)
+
+
+class TempDirTestCase:
+ def setup_method(self, method):
+ self.temp_dir = tempfile.mkdtemp()
+ self.old_cwd = os.getcwd()
+ os.chdir(self.temp_dir)
+
+ def teardown_method(self, method):
+ os.chdir(self.old_cwd)
+ shutil.rmtree(self.temp_dir)
+
+
+class TestManifestTest(TempDirTestCase):
+ def setup_method(self, method):
+ super().setup_method(method)
+
+ f = open(os.path.join(self.temp_dir, 'setup.py'), 'w', encoding="utf-8")
+ f.write(SETUP_PY)
+ f.close()
+ """
+ Create a file tree like:
+ - LICENSE
+ - README.rst
+ - testing.rst
+ - .hidden.rst
+ - app/
+ - __init__.py
+ - a.txt
+ - b.txt
+ - c.rst
+ - static/
+ - app.js
+ - app.js.map
+ - app.css
+ - app.css.map
+ """
+
+ for fname in ['README.rst', '.hidden.rst', 'testing.rst', 'LICENSE']:
+ touch(os.path.join(self.temp_dir, fname))
+
+ # Set up the rest of the test package
+ test_pkg = os.path.join(self.temp_dir, 'app')
+ os.mkdir(test_pkg)
+ for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']:
+ touch(os.path.join(test_pkg, fname))
+
+ # Some compiled front-end assets to include
+ static = os.path.join(test_pkg, 'static')
+ os.mkdir(static)
+ for fname in ['app.js', 'app.js.map', 'app.css', 'app.css.map']:
+ touch(os.path.join(static, fname))
+
+ def make_manifest(self, contents):
+ """Write a MANIFEST.in."""
+ manifest = os.path.join(self.temp_dir, 'MANIFEST.in')
+ with open(manifest, 'w', encoding="utf-8") as f:
+ f.write(DALS(contents))
+
+ def get_files(self):
+ """Run egg_info and get all the files to include, as a set"""
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'setup.py'
+ cmd = egg_info(dist)
+ cmd.ensure_finalized()
+
+ cmd.run()
+
+ return set(cmd.filelist.files)
+
+ def test_no_manifest(self):
+ """Check a missing MANIFEST.in includes only the standard files."""
+ assert (default_files - set(['MANIFEST.in'])) == self.get_files()
+
+ def test_empty_files(self):
+ """Check an empty MANIFEST.in includes only the standard files."""
+ self.make_manifest("")
+ assert default_files == self.get_files()
+
+ def test_include(self):
+ """Include extra rst files in the project root."""
+ self.make_manifest("include *.rst")
+ files = default_files | set(['testing.rst', '.hidden.rst'])
+ assert files == self.get_files()
+
+ def test_exclude(self):
+ """Include everything in app/ except the text files"""
+ ml = make_local_path
+ self.make_manifest(
+ """
+ include app/*
+ exclude app/*.txt
+ """
+ )
+ files = default_files | set([ml('app/c.rst')])
+ assert files == self.get_files()
+
+ def test_include_multiple(self):
+ """Include with multiple patterns."""
+ ml = make_local_path
+ self.make_manifest("include app/*.txt app/static/*")
+ files = default_files | set([
+ ml('app/a.txt'),
+ ml('app/b.txt'),
+ ml('app/static/app.js'),
+ ml('app/static/app.js.map'),
+ ml('app/static/app.css'),
+ ml('app/static/app.css.map'),
+ ])
+ assert files == self.get_files()
+
+ def test_graft(self):
+ """Include the whole app/static/ directory."""
+ ml = make_local_path
+ self.make_manifest("graft app/static")
+ files = default_files | set([
+ ml('app/static/app.js'),
+ ml('app/static/app.js.map'),
+ ml('app/static/app.css'),
+ ml('app/static/app.css.map'),
+ ])
+ assert files == self.get_files()
+
+ def test_graft_glob_syntax(self):
+ """Include the whole app/static/ directory."""
+ ml = make_local_path
+ self.make_manifest("graft */static")
+ files = default_files | set([
+ ml('app/static/app.js'),
+ ml('app/static/app.js.map'),
+ ml('app/static/app.css'),
+ ml('app/static/app.css.map'),
+ ])
+ assert files == self.get_files()
+
+ def test_graft_global_exclude(self):
+ """Exclude all *.map files in the project."""
+ ml = make_local_path
+ self.make_manifest(
+ """
+ graft app/static
+ global-exclude *.map
+ """
+ )
+ files = default_files | set([ml('app/static/app.js'), ml('app/static/app.css')])
+ assert files == self.get_files()
+
+ def test_global_include(self):
+ """Include all *.rst, *.js, and *.css files in the whole tree."""
+ ml = make_local_path
+ self.make_manifest(
+ """
+ global-include *.rst *.js *.css
+ """
+ )
+ files = default_files | set([
+ '.hidden.rst',
+ 'testing.rst',
+ ml('app/c.rst'),
+ ml('app/static/app.js'),
+ ml('app/static/app.css'),
+ ])
+ assert files == self.get_files()
+
+ def test_graft_prune(self):
+ """Include all files in app/, except for the whole app/static/ dir."""
+ ml = make_local_path
+ self.make_manifest(
+ """
+ graft app
+ prune app/static
+ """
+ )
+ files = default_files | set([ml('app/a.txt'), ml('app/b.txt'), ml('app/c.rst')])
+ assert files == self.get_files()
+
+
+class TestFileListTest(TempDirTestCase):
+ """
+ A copy of the relevant bits of distutils/tests/test_filelist.py,
+ to ensure setuptools' version of FileList keeps parity with distutils.
+ """
+
+ @pytest.fixture(autouse=os.getenv("SETUPTOOLS_USE_DISTUTILS") == "stdlib")
+ def _compat_record_logs(self, monkeypatch, caplog):
+ """Account for stdlib compatibility"""
+
+ def _log(_logger, level, msg, args):
+ exc = sys.exc_info()
+ rec = logging.LogRecord("distutils", level, "", 0, msg, args, exc)
+ caplog.records.append(rec)
+
+ monkeypatch.setattr(log.Log, "_log", _log)
+
+ def get_records(self, caplog, *levels):
+ return [r for r in caplog.records if r.levelno in levels]
+
+ def assertNoWarnings(self, caplog):
+ assert self.get_records(caplog, log.WARN) == []
+ caplog.clear()
+
+ def assertWarnings(self, caplog):
+ if IS_PYPY and not caplog.records:
+ pytest.xfail("caplog checks may not work well in PyPy")
+ else:
+ assert len(self.get_records(caplog, log.WARN)) > 0
+ caplog.clear()
+
+ def make_files(self, files):
+ for file in files:
+ file = os.path.join(self.temp_dir, file)
+ dirname, _basename = os.path.split(file)
+ os.makedirs(dirname, exist_ok=True)
+ touch(file)
+
+ def test_process_template_line(self):
+ # testing all MANIFEST.in template patterns
+ file_list = FileList()
+ ml = make_local_path
+
+ # simulated file list
+ self.make_files([
+ 'foo.tmp',
+ 'ok',
+ 'xo',
+ 'four.txt',
+ 'buildout.cfg',
+ # filelist does not filter out VCS directories,
+ # it's sdist that does
+ ml('.hg/last-message.txt'),
+ ml('global/one.txt'),
+ ml('global/two.txt'),
+ ml('global/files.x'),
+ ml('global/here.tmp'),
+ ml('f/o/f.oo'),
+ ml('dir/graft-one'),
+ ml('dir/dir2/graft2'),
+ ml('dir3/ok'),
+ ml('dir3/sub/ok.txt'),
+ ])
+
+ MANIFEST_IN = DALS(
+ """\
+ include ok
+ include xo
+ exclude xo
+ include foo.tmp
+ include buildout.cfg
+ global-include *.x
+ global-include *.txt
+ global-exclude *.tmp
+ recursive-include f *.oo
+ recursive-exclude global *.x
+ graft dir
+ prune dir3
+ """
+ )
+
+ for line in MANIFEST_IN.split('\n'):
+ if not line:
+ continue
+ file_list.process_template_line(line)
+
+ wanted = [
+ 'buildout.cfg',
+ 'four.txt',
+ 'ok',
+ ml('.hg/last-message.txt'),
+ ml('dir/graft-one'),
+ ml('dir/dir2/graft2'),
+ ml('f/o/f.oo'),
+ ml('global/one.txt'),
+ ml('global/two.txt'),
+ ]
+
+ file_list.sort()
+ assert file_list.files == wanted
+
+ def test_exclude_pattern(self):
+ # return False if no match
+ file_list = FileList()
+ assert not file_list.exclude_pattern('*.py')
+
+ # return True if files match
+ file_list = FileList()
+ file_list.files = ['a.py', 'b.py']
+ assert file_list.exclude_pattern('*.py')
+
+ # test excludes
+ file_list = FileList()
+ file_list.files = ['a.py', 'a.txt']
+ file_list.exclude_pattern('*.py')
+ file_list.sort()
+ assert file_list.files == ['a.txt']
+
+ def test_include_pattern(self):
+ # return False if no match
+ file_list = FileList()
+ self.make_files([])
+ assert not file_list.include_pattern('*.py')
+
+ # return True if files match
+ file_list = FileList()
+ self.make_files(['a.py', 'b.txt'])
+ assert file_list.include_pattern('*.py')
+
+ # test * matches all files
+ file_list = FileList()
+ self.make_files(['a.py', 'b.txt'])
+ file_list.include_pattern('*')
+ file_list.sort()
+ assert file_list.files == ['a.py', 'b.txt']
+
+ def test_process_template_line_invalid(self):
+ # invalid lines
+ file_list = FileList()
+ for action in (
+ 'include',
+ 'exclude',
+ 'global-include',
+ 'global-exclude',
+ 'recursive-include',
+ 'recursive-exclude',
+ 'graft',
+ 'prune',
+ 'blarg',
+ ):
+ with pytest.raises(DistutilsTemplateError):
+ file_list.process_template_line(action)
+
+ def test_include(self, caplog):
+ caplog.set_level(logging.DEBUG)
+ ml = make_local_path
+ # include
+ file_list = FileList()
+ self.make_files(['a.py', 'b.txt', ml('d/c.py')])
+
+ file_list.process_template_line('include *.py')
+ file_list.sort()
+ assert file_list.files == ['a.py']
+ self.assertNoWarnings(caplog)
+
+ file_list.process_template_line('include *.rb')
+ file_list.sort()
+ assert file_list.files == ['a.py']
+ self.assertWarnings(caplog)
+
+ def test_exclude(self, caplog):
+ caplog.set_level(logging.DEBUG)
+ ml = make_local_path
+ # exclude
+ file_list = FileList()
+ file_list.files = ['a.py', 'b.txt', ml('d/c.py')]
+
+ file_list.process_template_line('exclude *.py')
+ file_list.sort()
+ assert file_list.files == ['b.txt', ml('d/c.py')]
+ self.assertNoWarnings(caplog)
+
+ file_list.process_template_line('exclude *.rb')
+ file_list.sort()
+ assert file_list.files == ['b.txt', ml('d/c.py')]
+ self.assertWarnings(caplog)
+
+ def test_global_include(self, caplog):
+ caplog.set_level(logging.DEBUG)
+ ml = make_local_path
+ # global-include
+ file_list = FileList()
+ self.make_files(['a.py', 'b.txt', ml('d/c.py')])
+
+ file_list.process_template_line('global-include *.py')
+ file_list.sort()
+ assert file_list.files == ['a.py', ml('d/c.py')]
+ self.assertNoWarnings(caplog)
+
+ file_list.process_template_line('global-include *.rb')
+ file_list.sort()
+ assert file_list.files == ['a.py', ml('d/c.py')]
+ self.assertWarnings(caplog)
+
+ def test_global_exclude(self, caplog):
+ caplog.set_level(logging.DEBUG)
+ ml = make_local_path
+ # global-exclude
+ file_list = FileList()
+ file_list.files = ['a.py', 'b.txt', ml('d/c.py')]
+
+ file_list.process_template_line('global-exclude *.py')
+ file_list.sort()
+ assert file_list.files == ['b.txt']
+ self.assertNoWarnings(caplog)
+
+ file_list.process_template_line('global-exclude *.rb')
+ file_list.sort()
+ assert file_list.files == ['b.txt']
+ self.assertWarnings(caplog)
+
+ def test_recursive_include(self, caplog):
+ caplog.set_level(logging.DEBUG)
+ ml = make_local_path
+ # recursive-include
+ file_list = FileList()
+ self.make_files(['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')])
+
+ file_list.process_template_line('recursive-include d *.py')
+ file_list.sort()
+ assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
+ self.assertNoWarnings(caplog)
+
+ file_list.process_template_line('recursive-include e *.py')
+ file_list.sort()
+ assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
+ self.assertWarnings(caplog)
+
+ def test_recursive_exclude(self, caplog):
+ caplog.set_level(logging.DEBUG)
+ ml = make_local_path
+ # recursive-exclude
+ file_list = FileList()
+ file_list.files = ['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')]
+
+ file_list.process_template_line('recursive-exclude d *.py')
+ file_list.sort()
+ assert file_list.files == ['a.py', ml('d/c.txt')]
+ self.assertNoWarnings(caplog)
+
+ file_list.process_template_line('recursive-exclude e *.py')
+ file_list.sort()
+ assert file_list.files == ['a.py', ml('d/c.txt')]
+ self.assertWarnings(caplog)
+
+ def test_graft(self, caplog):
+ caplog.set_level(logging.DEBUG)
+ ml = make_local_path
+ # graft
+ file_list = FileList()
+ self.make_files(['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')])
+
+ file_list.process_template_line('graft d')
+ file_list.sort()
+ assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
+ self.assertNoWarnings(caplog)
+
+ file_list.process_template_line('graft e')
+ file_list.sort()
+ assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
+ self.assertWarnings(caplog)
+
+ def test_prune(self, caplog):
+ caplog.set_level(logging.DEBUG)
+ ml = make_local_path
+ # prune
+ file_list = FileList()
+ file_list.files = ['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')]
+
+ file_list.process_template_line('prune d')
+ file_list.sort()
+ assert file_list.files == ['a.py', ml('f/f.py')]
+ self.assertNoWarnings(caplog)
+
+ file_list.process_template_line('prune e')
+ file_list.sort()
+ assert file_list.files == ['a.py', ml('f/f.py')]
+ self.assertWarnings(caplog)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_namespaces.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_namespaces.py
new file mode 100644
index 00000000..a0f4120b
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_namespaces.py
@@ -0,0 +1,138 @@
+import subprocess
+import sys
+
+from setuptools._path import paths_on_pythonpath
+
+from . import namespaces
+
+
+class TestNamespaces:
+ def test_mixed_site_and_non_site(self, tmpdir):
+ """
+ Installing two packages sharing the same namespace, one installed
+ to a site dir and the other installed just to a path on PYTHONPATH
+ should leave the namespace in tact and both packages reachable by
+ import.
+ """
+ pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA')
+ pkg_B = namespaces.build_namespace_package(tmpdir, 'myns.pkgB')
+ site_packages = tmpdir / 'site-packages'
+ path_packages = tmpdir / 'path-packages'
+ targets = site_packages, path_packages
+ # use pip to install to the target directory
+ install_cmd = [
+ sys.executable,
+ '-m',
+ 'pip.__main__',
+ 'install',
+ str(pkg_A),
+ '-t',
+ str(site_packages),
+ ]
+ subprocess.check_call(install_cmd)
+ namespaces.make_site_dir(site_packages)
+ install_cmd = [
+ sys.executable,
+ '-m',
+ 'pip.__main__',
+ 'install',
+ str(pkg_B),
+ '-t',
+ str(path_packages),
+ ]
+ subprocess.check_call(install_cmd)
+ try_import = [
+ sys.executable,
+ '-c',
+ 'import myns.pkgA; import myns.pkgB',
+ ]
+ with paths_on_pythonpath(map(str, targets)):
+ subprocess.check_call(try_import)
+
+ def test_pkg_resources_import(self, tmpdir):
+ """
+ Ensure that a namespace package doesn't break on import
+ of pkg_resources.
+ """
+ pkg = namespaces.build_namespace_package(tmpdir, 'myns.pkgA')
+ target = tmpdir / 'packages'
+ target.mkdir()
+ install_cmd = [
+ sys.executable,
+ '-m',
+ 'pip',
+ 'install',
+ '-t',
+ str(target),
+ str(pkg),
+ ]
+ with paths_on_pythonpath([str(target)]):
+ subprocess.check_call(install_cmd)
+ namespaces.make_site_dir(target)
+ try_import = [
+ sys.executable,
+ '-c',
+ 'import pkg_resources',
+ ]
+ with paths_on_pythonpath([str(target)]):
+ subprocess.check_call(try_import)
+
+ def test_namespace_package_installed_and_cwd(self, tmpdir):
+ """
+ Installing a namespace packages but also having it in the current
+ working directory, only one version should take precedence.
+ """
+ pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA')
+ target = tmpdir / 'packages'
+ # use pip to install to the target directory
+ install_cmd = [
+ sys.executable,
+ '-m',
+ 'pip.__main__',
+ 'install',
+ str(pkg_A),
+ '-t',
+ str(target),
+ ]
+ subprocess.check_call(install_cmd)
+ namespaces.make_site_dir(target)
+
+ # ensure that package imports and pkg_resources imports
+ pkg_resources_imp = [
+ sys.executable,
+ '-c',
+ 'import pkg_resources; import myns.pkgA',
+ ]
+ with paths_on_pythonpath([str(target)]):
+ subprocess.check_call(pkg_resources_imp, cwd=str(pkg_A))
+
+ def test_packages_in_the_same_namespace_installed_and_cwd(self, tmpdir):
+ """
+ Installing one namespace package and also have another in the same
+ namespace in the current working directory, both of them must be
+ importable.
+ """
+ pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA')
+ pkg_B = namespaces.build_namespace_package(tmpdir, 'myns.pkgB')
+ target = tmpdir / 'packages'
+ # use pip to install to the target directory
+ install_cmd = [
+ sys.executable,
+ '-m',
+ 'pip.__main__',
+ 'install',
+ str(pkg_A),
+ '-t',
+ str(target),
+ ]
+ subprocess.check_call(install_cmd)
+ namespaces.make_site_dir(target)
+
+ # ensure that all packages import and pkg_resources imports
+ pkg_resources_imp = [
+ sys.executable,
+ '-c',
+ 'import pkg_resources; import myns.pkgA; import myns.pkgB',
+ ]
+ with paths_on_pythonpath([str(target)]):
+ subprocess.check_call(pkg_resources_imp, cwd=str(pkg_B))
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_packageindex.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_packageindex.py
new file mode 100644
index 00000000..2a6e5917
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_packageindex.py
@@ -0,0 +1,267 @@
+import http.client
+import re
+import urllib.error
+import urllib.request
+from inspect import cleandoc
+
+import pytest
+
+import setuptools.package_index
+
+import distutils.errors
+
+
+class TestPackageIndex:
+ def test_regex(self):
+ hash_url = 'http://other_url?:action=show_md5&amp;'
+ hash_url += 'digest=0123456789abcdef0123456789abcdef'
+ doc = """
+ <a href="http://some_url">Name</a>
+ (<a title="MD5 hash"
+ href="{hash_url}">md5</a>)
+ """.lstrip().format(**locals())
+ assert setuptools.package_index.PYPI_MD5.match(doc)
+
+ def test_bad_url_bad_port(self):
+ index = setuptools.package_index.PackageIndex()
+ url = 'http://127.0.0.1:0/nonesuch/test_package_index'
+ with pytest.raises(Exception, match=re.escape(url)):
+ v = index.open_url(url)
+ assert isinstance(v, urllib.error.HTTPError)
+
+ def test_bad_url_typo(self):
+ # issue 16
+ # easy_install inquant.contentmirror.plone breaks because of a typo
+ # in its home URL
+ index = setuptools.package_index.PackageIndex(hosts=('www.example.com',))
+
+ url = 'url:%20https://svn.plone.org/svn/collective/inquant.contentmirror.plone/trunk'
+
+ with pytest.raises(Exception, match=re.escape(url)):
+ v = index.open_url(url)
+ assert isinstance(v, urllib.error.HTTPError)
+
+ def test_bad_url_bad_status_line(self):
+ index = setuptools.package_index.PackageIndex(hosts=('www.example.com',))
+
+ def _urlopen(*args):
+ raise http.client.BadStatusLine('line')
+
+ index.opener = _urlopen
+ url = 'http://example.com'
+ with pytest.raises(Exception, match=r'line'):
+ index.open_url(url)
+
+ def test_bad_url_double_scheme(self):
+ """
+ A bad URL with a double scheme should raise a DistutilsError.
+ """
+ index = setuptools.package_index.PackageIndex(hosts=('www.example.com',))
+
+ # issue 20
+ url = 'http://http://svn.pythonpaste.org/Paste/wphp/trunk'
+ try:
+ index.open_url(url)
+ except distutils.errors.DistutilsError as error:
+ msg = str(error)
+ assert (
+ 'nonnumeric port' in msg
+ or 'getaddrinfo failed' in msg
+ or 'Name or service not known' in msg
+ )
+ return
+ raise RuntimeError("Did not raise")
+
+ def test_url_ok(self):
+ index = setuptools.package_index.PackageIndex(hosts=('www.example.com',))
+ url = 'file:///tmp/test_package_index'
+ assert index.url_ok(url, True)
+
+ def test_parse_bdist_wininst(self):
+ parse = setuptools.package_index.parse_bdist_wininst
+
+ actual = parse('reportlab-2.5.win32-py2.4.exe')
+ expected = 'reportlab-2.5', '2.4', 'win32'
+ assert actual == expected
+
+ actual = parse('reportlab-2.5.win32.exe')
+ expected = 'reportlab-2.5', None, 'win32'
+ assert actual == expected
+
+ actual = parse('reportlab-2.5.win-amd64-py2.7.exe')
+ expected = 'reportlab-2.5', '2.7', 'win-amd64'
+ assert actual == expected
+
+ actual = parse('reportlab-2.5.win-amd64.exe')
+ expected = 'reportlab-2.5', None, 'win-amd64'
+ assert actual == expected
+
+ def test__vcs_split_rev_from_url(self):
+ """
+ Test the basic usage of _vcs_split_rev_from_url
+ """
+ vsrfu = setuptools.package_index.PackageIndex._vcs_split_rev_from_url
+ url, rev = vsrfu('https://example.com/bar@2995')
+ assert url == 'https://example.com/bar'
+ assert rev == '2995'
+
+ def test_local_index(self, tmpdir):
+ """
+ local_open should be able to read an index from the file system.
+ """
+ index_file = tmpdir / 'index.html'
+ with index_file.open('w') as f:
+ f.write('<div>content</div>')
+ url = 'file:' + urllib.request.pathname2url(str(tmpdir)) + '/'
+ res = setuptools.package_index.local_open(url)
+ assert 'content' in res.read()
+
+ def test_egg_fragment(self):
+ """
+ EGG fragments must comply to PEP 440
+ """
+ epoch = [
+ '',
+ '1!',
+ ]
+ releases = [
+ '0',
+ '0.0',
+ '0.0.0',
+ ]
+ pre = [
+ 'a0',
+ 'b0',
+ 'rc0',
+ ]
+ post = ['.post0']
+ dev = [
+ '.dev0',
+ ]
+ local = [
+ ('', ''),
+ ('+ubuntu.0', '+ubuntu.0'),
+ ('+ubuntu-0', '+ubuntu.0'),
+ ('+ubuntu_0', '+ubuntu.0'),
+ ]
+ versions = [
+ [''.join([e, r, p, loc]) for loc in locs]
+ for e in epoch
+ for r in releases
+ for p in sum([pre, post, dev], [''])
+ for locs in local
+ ]
+ for v, vc in versions:
+ dists = list(
+ setuptools.package_index.distros_for_url(
+ 'http://example.com/example-foo.zip#egg=example-foo-' + v
+ )
+ )
+ assert dists[0].version == ''
+ assert dists[1].version == vc
+
+ def test_download_git_with_rev(self, tmp_path, fp):
+ url = 'git+https://github.example/group/project@master#egg=foo'
+ index = setuptools.package_index.PackageIndex()
+
+ expected_dir = tmp_path / 'project@master'
+ fp.register([
+ 'git',
+ 'clone',
+ '--quiet',
+ 'https://github.example/group/project',
+ expected_dir,
+ ])
+ fp.register(['git', '-C', expected_dir, 'checkout', '--quiet', 'master'])
+
+ result = index.download(url, tmp_path)
+
+ assert result == str(expected_dir)
+ assert len(fp.calls) == 2
+
+ def test_download_git_no_rev(self, tmp_path, fp):
+ url = 'git+https://github.example/group/project#egg=foo'
+ index = setuptools.package_index.PackageIndex()
+
+ expected_dir = tmp_path / 'project'
+ fp.register([
+ 'git',
+ 'clone',
+ '--quiet',
+ 'https://github.example/group/project',
+ expected_dir,
+ ])
+ index.download(url, tmp_path)
+
+ def test_download_svn(self, tmp_path):
+ url = 'svn+https://svn.example/project#egg=foo'
+ index = setuptools.package_index.PackageIndex()
+
+ msg = r".*SVN download is not supported.*"
+ with pytest.raises(distutils.errors.DistutilsError, match=msg):
+ index.download(url, tmp_path)
+
+
+class TestContentCheckers:
+ def test_md5(self):
+ checker = setuptools.package_index.HashChecker.from_url(
+ 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478'
+ )
+ checker.feed('You should probably not be using MD5'.encode('ascii'))
+ assert checker.hash.hexdigest() == 'f12895fdffbd45007040d2e44df98478'
+ assert checker.is_valid()
+
+ def test_other_fragment(self):
+ "Content checks should succeed silently if no hash is present"
+ checker = setuptools.package_index.HashChecker.from_url(
+ 'http://foo/bar#something%20completely%20different'
+ )
+ checker.feed('anything'.encode('ascii'))
+ assert checker.is_valid()
+
+ def test_blank_md5(self):
+ "Content checks should succeed if a hash is empty"
+ checker = setuptools.package_index.HashChecker.from_url('http://foo/bar#md5=')
+ checker.feed('anything'.encode('ascii'))
+ assert checker.is_valid()
+
+ def test_get_hash_name_md5(self):
+ checker = setuptools.package_index.HashChecker.from_url(
+ 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478'
+ )
+ assert checker.hash_name == 'md5'
+
+ def test_report(self):
+ checker = setuptools.package_index.HashChecker.from_url(
+ 'http://foo/bar#md5=f12895fdffbd45007040d2e44df98478'
+ )
+ rep = checker.report(lambda x: x, 'My message about %s')
+ assert rep == 'My message about md5'
+
+
+class TestPyPIConfig:
+ def test_percent_in_password(self, tmp_home_dir):
+ pypirc = tmp_home_dir / '.pypirc'
+ pypirc.write_text(
+ cleandoc(
+ """
+ [pypi]
+ repository=https://pypi.org
+ username=jaraco
+ password=pity%
+ """
+ ),
+ encoding="utf-8",
+ )
+ cfg = setuptools.package_index.PyPIConfig()
+ cred = cfg.creds_by_repository['https://pypi.org']
+ assert cred.username == 'jaraco'
+ assert cred.password == 'pity%'
+
+
+@pytest.mark.timeout(1)
+def test_REL_DoS():
+ """
+ REL should not hang on a contrived attack string.
+ """
+ setuptools.package_index.REL.search('< rel=' + ' ' * 2**12)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_sandbox.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_sandbox.py
new file mode 100644
index 00000000..a476b7c9
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_sandbox.py
@@ -0,0 +1,134 @@
+"""develop tests"""
+
+import os
+import types
+
+import pytest
+
+import pkg_resources
+import setuptools.sandbox
+
+
+class TestSandbox:
+ def test_devnull(self, tmpdir):
+ with setuptools.sandbox.DirectorySandbox(str(tmpdir)):
+ self._file_writer(os.devnull)
+
+ @staticmethod
+ def _file_writer(path):
+ def do_write():
+ with open(path, 'w', encoding="utf-8") as f:
+ f.write('xxx')
+
+ return do_write
+
+ def test_setup_py_with_BOM(self):
+ """
+ It should be possible to execute a setup.py with a Byte Order Mark
+ """
+ target = pkg_resources.resource_filename(__name__, 'script-with-bom.py')
+ namespace = types.ModuleType('namespace')
+ setuptools.sandbox._execfile(target, vars(namespace))
+ assert namespace.result == 'passed'
+
+ def test_setup_py_with_CRLF(self, tmpdir):
+ setup_py = tmpdir / 'setup.py'
+ with setup_py.open('wb') as stream:
+ stream.write(b'"degenerate script"\r\n')
+ setuptools.sandbox._execfile(str(setup_py), globals())
+
+
+class TestExceptionSaver:
+ def test_exception_trapped(self):
+ with setuptools.sandbox.ExceptionSaver():
+ raise ValueError("details")
+
+ def test_exception_resumed(self):
+ with setuptools.sandbox.ExceptionSaver() as saved_exc:
+ raise ValueError("details")
+
+ with pytest.raises(ValueError) as caught:
+ saved_exc.resume()
+
+ assert isinstance(caught.value, ValueError)
+ assert str(caught.value) == 'details'
+
+ def test_exception_reconstructed(self):
+ orig_exc = ValueError("details")
+
+ with setuptools.sandbox.ExceptionSaver() as saved_exc:
+ raise orig_exc
+
+ with pytest.raises(ValueError) as caught:
+ saved_exc.resume()
+
+ assert isinstance(caught.value, ValueError)
+ assert caught.value is not orig_exc
+
+ def test_no_exception_passes_quietly(self):
+ with setuptools.sandbox.ExceptionSaver() as saved_exc:
+ pass
+
+ saved_exc.resume()
+
+ def test_unpickleable_exception(self):
+ class CantPickleThis(Exception):
+ "This Exception is unpickleable because it's not in globals"
+
+ def __repr__(self) -> str:
+ return f'CantPickleThis{self.args!r}'
+
+ with setuptools.sandbox.ExceptionSaver() as saved_exc:
+ raise CantPickleThis('detail')
+
+ with pytest.raises(setuptools.sandbox.UnpickleableException) as caught:
+ saved_exc.resume()
+
+ assert str(caught.value) == "CantPickleThis('detail',)"
+
+ def test_unpickleable_exception_when_hiding_setuptools(self):
+ """
+ As revealed in #440, an infinite recursion can occur if an unpickleable
+ exception while setuptools is hidden. Ensure this doesn't happen.
+ """
+
+ class ExceptionUnderTest(Exception):
+ """
+ An unpickleable exception (not in globals).
+ """
+
+ with pytest.raises(setuptools.sandbox.UnpickleableException) as caught:
+ with setuptools.sandbox.save_modules():
+ setuptools.sandbox.hide_setuptools()
+ raise ExceptionUnderTest
+
+ (msg,) = caught.value.args
+ assert msg == 'ExceptionUnderTest()'
+
+ def test_sandbox_violation_raised_hiding_setuptools(self, tmpdir):
+ """
+ When in a sandbox with setuptools hidden, a SandboxViolation
+ should reflect a proper exception and not be wrapped in
+ an UnpickleableException.
+ """
+
+ def write_file():
+ "Trigger a SandboxViolation by writing outside the sandbox"
+ with open('/etc/foo', 'w', encoding="utf-8"):
+ pass
+
+ with pytest.raises(setuptools.sandbox.SandboxViolation) as caught:
+ with setuptools.sandbox.save_modules():
+ setuptools.sandbox.hide_setuptools()
+ with setuptools.sandbox.DirectorySandbox(str(tmpdir)):
+ write_file()
+
+ cmd, args, kwargs = caught.value.args
+ assert cmd == 'open'
+ assert args == ('/etc/foo', 'w')
+ assert kwargs == {"encoding": "utf-8"}
+
+ msg = str(caught.value)
+ assert 'open' in msg
+ assert "('/etc/foo', 'w')" in msg
+ assert "{'encoding': 'utf-8'}" in msg
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_sdist.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_sdist.py
new file mode 100644
index 00000000..19d8ddf6
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_sdist.py
@@ -0,0 +1,984 @@
+"""sdist tests"""
+
+import contextlib
+import io
+import logging
+import os
+import pathlib
+import sys
+import tarfile
+import tempfile
+import unicodedata
+from inspect import cleandoc
+from pathlib import Path
+from unittest import mock
+
+import jaraco.path
+import pytest
+
+from setuptools import Command, SetuptoolsDeprecationWarning
+from setuptools._importlib import metadata
+from setuptools.command.egg_info import manifest_maker
+from setuptools.command.sdist import sdist
+from setuptools.dist import Distribution
+from setuptools.extension import Extension
+from setuptools.tests import fail_on_ascii
+
+from .text import Filenames
+
+import distutils
+from distutils.core import run_setup
+
+SETUP_ATTRS = {
+ 'name': 'sdist_test',
+ 'version': '0.0',
+ 'packages': ['sdist_test'],
+ 'package_data': {'sdist_test': ['*.txt']},
+ 'data_files': [("data", [os.path.join("d", "e.dat")])],
+}
+
+SETUP_PY = f"""\
+from setuptools import setup
+
+setup(**{SETUP_ATTRS!r})
+"""
+
+EXTENSION = Extension(
+ name="sdist_test.f",
+ sources=[os.path.join("sdist_test", "f.c")],
+ depends=[os.path.join("sdist_test", "f.h")],
+)
+EXTENSION_SOURCES = EXTENSION.sources + EXTENSION.depends
+
+
+@contextlib.contextmanager
+def quiet():
+ old_stdout, old_stderr = sys.stdout, sys.stderr
+ sys.stdout, sys.stderr = io.StringIO(), io.StringIO()
+ try:
+ yield
+ finally:
+ sys.stdout, sys.stderr = old_stdout, old_stderr
+
+
+# Convert to POSIX path
+def posix(path):
+ if not isinstance(path, str):
+ return path.replace(os.sep.encode('ascii'), b'/')
+ else:
+ return path.replace(os.sep, '/')
+
+
+# HFS Plus uses decomposed UTF-8
+def decompose(path):
+ if isinstance(path, str):
+ return unicodedata.normalize('NFD', path)
+ try:
+ path = path.decode('utf-8')
+ path = unicodedata.normalize('NFD', path)
+ path = path.encode('utf-8')
+ except UnicodeError:
+ pass # Not UTF-8
+ return path
+
+
+def read_all_bytes(filename):
+ with open(filename, 'rb') as fp:
+ return fp.read()
+
+
+def latin1_fail():
+ try:
+ desc, filename = tempfile.mkstemp(suffix=Filenames.latin_1)
+ os.close(desc)
+ os.remove(filename)
+ except Exception:
+ return True
+
+
+fail_on_latin1_encoded_filenames = pytest.mark.xfail(
+ latin1_fail(),
+ reason="System does not support latin-1 filenames",
+)
+
+
+skip_under_xdist = pytest.mark.skipif(
+ "os.environ.get('PYTEST_XDIST_WORKER')",
+ reason="pytest-dev/pytest-xdist#843",
+)
+skip_under_stdlib_distutils = pytest.mark.skipif(
+ not distutils.__package__.startswith('setuptools'),
+ reason="the test is not supported with stdlib distutils",
+)
+
+
+def touch(path):
+ open(path, 'wb').close()
+ return path
+
+
+def symlink_or_skip_test(src, dst):
+ try:
+ os.symlink(src, dst)
+ except (OSError, NotImplementedError):
+ pytest.skip("symlink not supported in OS")
+ return None
+ return dst
+
+
+class TestSdistTest:
+ @pytest.fixture(autouse=True)
+ def source_dir(self, tmpdir):
+ tmpdir = tmpdir / "project_root"
+ tmpdir.mkdir()
+
+ (tmpdir / 'setup.py').write_text(SETUP_PY, encoding='utf-8')
+
+ # Set up the rest of the test package
+ test_pkg = tmpdir / 'sdist_test'
+ test_pkg.mkdir()
+ data_folder = tmpdir / 'd'
+ data_folder.mkdir()
+ # *.rst was not included in package_data, so c.rst should not be
+ # automatically added to the manifest when not under version control
+ for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']:
+ touch(test_pkg / fname)
+ touch(data_folder / 'e.dat')
+ # C sources are not included by default, but they will be,
+ # if an extension module uses them as sources or depends
+ for fname in EXTENSION_SOURCES:
+ touch(tmpdir / fname)
+
+ with tmpdir.as_cwd():
+ yield tmpdir
+
+ def assert_package_data_in_manifest(self, cmd):
+ manifest = cmd.filelist.files
+ assert os.path.join('sdist_test', 'a.txt') in manifest
+ assert os.path.join('sdist_test', 'b.txt') in manifest
+ assert os.path.join('sdist_test', 'c.rst') not in manifest
+ assert os.path.join('d', 'e.dat') in manifest
+
+ def setup_with_extension(self):
+ setup_attrs = {**SETUP_ATTRS, 'ext_modules': [EXTENSION]}
+
+ dist = Distribution(setup_attrs)
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ with quiet():
+ cmd.run()
+
+ return cmd
+
+ def test_package_data_in_sdist(self):
+ """Regression test for pull request #4: ensures that files listed in
+ package_data are included in the manifest even if they're not added to
+ version control.
+ """
+
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ with quiet():
+ cmd.run()
+
+ self.assert_package_data_in_manifest(cmd)
+
+ def test_package_data_and_include_package_data_in_sdist(self):
+ """
+ Ensure package_data and include_package_data work
+ together.
+ """
+ setup_attrs = {**SETUP_ATTRS, 'include_package_data': True}
+ assert setup_attrs['package_data']
+
+ dist = Distribution(setup_attrs)
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ with quiet():
+ cmd.run()
+
+ self.assert_package_data_in_manifest(cmd)
+
+ def test_extension_sources_in_sdist(self):
+ """
+ Ensure that the files listed in Extension.sources and Extension.depends
+ are automatically included in the manifest.
+ """
+ cmd = self.setup_with_extension()
+ self.assert_package_data_in_manifest(cmd)
+ manifest = cmd.filelist.files
+ for path in EXTENSION_SOURCES:
+ assert path in manifest
+
+ def test_missing_extension_sources(self):
+ """
+ Similar to test_extension_sources_in_sdist but the referenced files don't exist.
+ Missing files should not be included in distribution (with no error raised).
+ """
+ for path in EXTENSION_SOURCES:
+ os.remove(path)
+
+ cmd = self.setup_with_extension()
+ self.assert_package_data_in_manifest(cmd)
+ manifest = cmd.filelist.files
+ for path in EXTENSION_SOURCES:
+ assert path not in manifest
+
+ def test_symlinked_extension_sources(self):
+ """
+ Similar to test_extension_sources_in_sdist but the referenced files are
+ instead symbolic links to project-local files. Referenced file paths
+ should be included. Symlink targets themselves should NOT be included.
+ """
+ symlinked = []
+ for path in EXTENSION_SOURCES:
+ base, ext = os.path.splitext(path)
+ target = base + "_target." + ext
+
+ os.rename(path, target)
+ symlink_or_skip_test(os.path.basename(target), path)
+ symlinked.append(target)
+
+ cmd = self.setup_with_extension()
+ self.assert_package_data_in_manifest(cmd)
+ manifest = cmd.filelist.files
+ for path in EXTENSION_SOURCES:
+ assert path in manifest
+ for path in symlinked:
+ assert path not in manifest
+
+ _INVALID_PATHS = {
+ "must be relative": lambda: (
+ os.path.abspath(os.path.join("sdist_test", "f.h"))
+ ),
+ "can't have `..` segments": lambda: (
+ os.path.join("sdist_test", "..", "sdist_test", "f.h")
+ ),
+ "doesn't exist": lambda: (
+ os.path.join("sdist_test", "this_file_does_not_exist.h")
+ ),
+ "must be inside the project root": lambda: (
+ symlink_or_skip_test(
+ touch(os.path.join("..", "outside_of_project_root.h")),
+ "symlink.h",
+ )
+ ),
+ }
+
+ @skip_under_stdlib_distutils
+ @pytest.mark.parametrize("reason", _INVALID_PATHS.keys())
+ def test_invalid_extension_depends(self, reason, caplog):
+ """
+ Due to backwards compatibility reasons, `Extension.depends` should accept
+ invalid/weird paths, but then ignore them when building a sdist.
+
+ This test verifies that the source distribution is still built
+ successfully with such paths, but that instead of adding these paths to
+ the manifest, we emit an informational message, notifying the user that
+ the invalid path won't be automatically included.
+ """
+ invalid_path = self._INVALID_PATHS[reason]()
+ extension = Extension(
+ name="sdist_test.f",
+ sources=[],
+ depends=[invalid_path],
+ )
+ setup_attrs = {**SETUP_ATTRS, 'ext_modules': [extension]}
+
+ dist = Distribution(setup_attrs)
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ with quiet(), caplog.at_level(logging.INFO):
+ cmd.run()
+
+ self.assert_package_data_in_manifest(cmd)
+ manifest = cmd.filelist.files
+ assert invalid_path not in manifest
+
+ expected_message = [
+ message
+ for (logger, level, message) in caplog.record_tuples
+ if (
+ logger == "root" #
+ and level == logging.INFO #
+ and invalid_path in message #
+ )
+ ]
+ assert len(expected_message) == 1
+ (expected_message,) = expected_message
+ assert reason in expected_message
+
+ def test_custom_build_py(self):
+ """
+ Ensure projects defining custom build_py don't break
+ when creating sdists (issue #2849)
+ """
+ from distutils.command.build_py import build_py as OrigBuildPy
+
+ using_custom_command_guard = mock.Mock()
+
+ class CustomBuildPy(OrigBuildPy):
+ """
+ Some projects have custom commands inheriting from `distutils`
+ """
+
+ def get_data_files(self):
+ using_custom_command_guard()
+ return super().get_data_files()
+
+ setup_attrs = {**SETUP_ATTRS, 'include_package_data': True}
+ assert setup_attrs['package_data']
+
+ dist = Distribution(setup_attrs)
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ # Make sure we use the custom command
+ cmd.cmdclass = {'build_py': CustomBuildPy}
+ cmd.distribution.cmdclass = {'build_py': CustomBuildPy}
+ assert cmd.distribution.get_command_class('build_py') == CustomBuildPy
+
+ msg = "setuptools instead of distutils"
+ with quiet(), pytest.warns(SetuptoolsDeprecationWarning, match=msg):
+ cmd.run()
+
+ using_custom_command_guard.assert_called()
+ self.assert_package_data_in_manifest(cmd)
+
+ def test_setup_py_exists(self):
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'foo.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ with quiet():
+ cmd.run()
+
+ manifest = cmd.filelist.files
+ assert 'setup.py' in manifest
+
+ def test_setup_py_missing(self):
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'foo.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ if os.path.exists("setup.py"):
+ os.remove("setup.py")
+ with quiet():
+ cmd.run()
+
+ manifest = cmd.filelist.files
+ assert 'setup.py' not in manifest
+
+ def test_setup_py_excluded(self):
+ with open("MANIFEST.in", "w", encoding="utf-8") as manifest_file:
+ manifest_file.write("exclude setup.py")
+
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'foo.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ with quiet():
+ cmd.run()
+
+ manifest = cmd.filelist.files
+ assert 'setup.py' not in manifest
+
+ def test_defaults_case_sensitivity(self, source_dir):
+ """
+ Make sure default files (README.*, etc.) are added in a case-sensitive
+ way to avoid problems with packages built on Windows.
+ """
+
+ touch(source_dir / 'readme.rst')
+ touch(source_dir / 'SETUP.cfg')
+
+ dist = Distribution(SETUP_ATTRS)
+ # the extension deliberately capitalized for this test
+ # to make sure the actual filename (not capitalized) gets added
+ # to the manifest
+ dist.script_name = 'setup.PY'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ with quiet():
+ cmd.run()
+
+ # lowercase all names so we can test in a
+ # case-insensitive way to make sure the files
+ # are not included.
+ manifest = map(lambda x: x.lower(), cmd.filelist.files)
+ assert 'readme.rst' not in manifest, manifest
+ assert 'setup.py' not in manifest, manifest
+ assert 'setup.cfg' not in manifest, manifest
+
+ def test_exclude_dev_only_cache_folders(self, source_dir):
+ included = {
+ # Emulate problem in https://github.com/pypa/setuptools/issues/4601
+ "MANIFEST.in": (
+ "global-include LICEN[CS]E* COPYING* NOTICE* AUTHORS*\n"
+ "global-include *.txt\n"
+ ),
+ # For the sake of being conservative and limiting unforeseen side-effects
+ # we just exclude dev-only cache folders at the root of the repository:
+ "test/.venv/lib/python3.9/site-packages/bar-2.dist-info/AUTHORS.rst": "",
+ "src/.nox/py/lib/python3.12/site-packages/bar-2.dist-info/COPYING.txt": "",
+ "doc/.tox/default/lib/python3.11/site-packages/foo-4.dist-info/LICENSE": "",
+ # Let's test against false positives with similarly named files:
+ ".venv-requirements.txt": "",
+ ".tox-coveragerc.txt": "",
+ ".noxy/coveragerc.txt": "",
+ }
+
+ excluded = {
+ # .tox/.nox/.venv are well-know folders present at the root of Python repos
+ # and therefore should be excluded
+ ".tox/release/lib/python3.11/site-packages/foo-4.dist-info/LICENSE": "",
+ ".nox/py/lib/python3.12/site-packages/bar-2.dist-info/COPYING.txt": "",
+ ".venv/lib/python3.9/site-packages/bar-2.dist-info/AUTHORS.rst": "",
+ }
+
+ for file, content in {**excluded, **included}.items():
+ Path(source_dir, file).parent.mkdir(parents=True, exist_ok=True)
+ Path(source_dir, file).write_text(content, encoding="utf-8")
+
+ cmd = self.setup_with_extension()
+ self.assert_package_data_in_manifest(cmd)
+ manifest = {f.replace(os.sep, '/') for f in cmd.filelist.files}
+ for path in excluded:
+ assert os.path.exists(path)
+ assert path not in manifest, (path, manifest)
+ for path in included:
+ assert os.path.exists(path)
+ assert path in manifest, (path, manifest)
+
+ @fail_on_ascii
+ def test_manifest_is_written_with_utf8_encoding(self):
+ # Test for #303.
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'setup.py'
+ mm = manifest_maker(dist)
+ mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
+ os.mkdir('sdist_test.egg-info')
+
+ # UTF-8 filename
+ filename = os.path.join('sdist_test', 'smörbröd.py')
+
+ # Must create the file or it will get stripped.
+ touch(filename)
+
+ # Add UTF-8 filename and write manifest
+ with quiet():
+ mm.run()
+ mm.filelist.append(filename)
+ mm.write_manifest()
+
+ contents = read_all_bytes(mm.manifest)
+
+ # The manifest should be UTF-8 encoded
+ u_contents = contents.decode('UTF-8')
+
+ # The manifest should contain the UTF-8 filename
+ assert posix(filename) in u_contents
+
+ @fail_on_ascii
+ def test_write_manifest_allows_utf8_filenames(self):
+ # Test for #303.
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'setup.py'
+ mm = manifest_maker(dist)
+ mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
+ os.mkdir('sdist_test.egg-info')
+
+ filename = os.path.join(b'sdist_test', Filenames.utf_8)
+
+ # Must touch the file or risk removal
+ touch(filename)
+
+ # Add filename and write manifest
+ with quiet():
+ mm.run()
+ u_filename = filename.decode('utf-8')
+ mm.filelist.files.append(u_filename)
+ # Re-write manifest
+ mm.write_manifest()
+
+ contents = read_all_bytes(mm.manifest)
+
+ # The manifest should be UTF-8 encoded
+ contents.decode('UTF-8')
+
+ # The manifest should contain the UTF-8 filename
+ assert posix(filename) in contents
+
+ # The filelist should have been updated as well
+ assert u_filename in mm.filelist.files
+
+ @skip_under_xdist
+ def test_write_manifest_skips_non_utf8_filenames(self):
+ """
+ Files that cannot be encoded to UTF-8 (specifically, those that
+ weren't originally successfully decoded and have surrogate
+ escapes) should be omitted from the manifest.
+ See https://bitbucket.org/tarek/distribute/issue/303 for history.
+ """
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'setup.py'
+ mm = manifest_maker(dist)
+ mm.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
+ os.mkdir('sdist_test.egg-info')
+
+ # Latin-1 filename
+ filename = os.path.join(b'sdist_test', Filenames.latin_1)
+
+ # Add filename with surrogates and write manifest
+ with quiet():
+ mm.run()
+ u_filename = filename.decode('utf-8', 'surrogateescape')
+ mm.filelist.append(u_filename)
+ # Re-write manifest
+ mm.write_manifest()
+
+ contents = read_all_bytes(mm.manifest)
+
+ # The manifest should be UTF-8 encoded
+ contents.decode('UTF-8')
+
+ # The Latin-1 filename should have been skipped
+ assert posix(filename) not in contents
+
+ # The filelist should have been updated as well
+ assert u_filename not in mm.filelist.files
+
+ @fail_on_ascii
+ def test_manifest_is_read_with_utf8_encoding(self):
+ # Test for #303.
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ # Create manifest
+ with quiet():
+ cmd.run()
+
+ # Add UTF-8 filename to manifest
+ filename = os.path.join(b'sdist_test', Filenames.utf_8)
+ cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
+ manifest = open(cmd.manifest, 'ab')
+ manifest.write(b'\n' + filename)
+ manifest.close()
+
+ # The file must exist to be included in the filelist
+ touch(filename)
+
+ # Re-read manifest
+ cmd.filelist.files = []
+ with quiet():
+ cmd.read_manifest()
+
+ # The filelist should contain the UTF-8 filename
+ filename = filename.decode('utf-8')
+ assert filename in cmd.filelist.files
+
+ @fail_on_latin1_encoded_filenames
+ def test_read_manifest_skips_non_utf8_filenames(self):
+ # Test for #303.
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ # Create manifest
+ with quiet():
+ cmd.run()
+
+ # Add Latin-1 filename to manifest
+ filename = os.path.join(b'sdist_test', Filenames.latin_1)
+ cmd.manifest = os.path.join('sdist_test.egg-info', 'SOURCES.txt')
+ manifest = open(cmd.manifest, 'ab')
+ manifest.write(b'\n' + filename)
+ manifest.close()
+
+ # The file must exist to be included in the filelist
+ touch(filename)
+
+ # Re-read manifest
+ cmd.filelist.files = []
+ with quiet():
+ cmd.read_manifest()
+
+ # The Latin-1 filename should have been skipped
+ filename = filename.decode('latin-1')
+ assert filename not in cmd.filelist.files
+
+ @fail_on_ascii
+ @fail_on_latin1_encoded_filenames
+ def test_sdist_with_utf8_encoded_filename(self):
+ # Test for #303.
+ dist = Distribution(self.make_strings(SETUP_ATTRS))
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ filename = os.path.join(b'sdist_test', Filenames.utf_8)
+ touch(filename)
+
+ with quiet():
+ cmd.run()
+
+ if sys.platform == 'darwin':
+ filename = decompose(filename)
+
+ fs_enc = sys.getfilesystemencoding()
+
+ if sys.platform == 'win32':
+ if fs_enc == 'cp1252':
+ # Python mangles the UTF-8 filename
+ filename = filename.decode('cp1252')
+ assert filename in cmd.filelist.files
+ else:
+ filename = filename.decode('mbcs')
+ assert filename in cmd.filelist.files
+ else:
+ filename = filename.decode('utf-8')
+ assert filename in cmd.filelist.files
+
+ @classmethod
+ def make_strings(cls, item):
+ if isinstance(item, dict):
+ return {key: cls.make_strings(value) for key, value in item.items()}
+ if isinstance(item, list):
+ return list(map(cls.make_strings, item))
+ return str(item)
+
+ @fail_on_latin1_encoded_filenames
+ @skip_under_xdist
+ def test_sdist_with_latin1_encoded_filename(self):
+ # Test for #303.
+ dist = Distribution(self.make_strings(SETUP_ATTRS))
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+
+ # Latin-1 filename
+ filename = os.path.join(b'sdist_test', Filenames.latin_1)
+ touch(filename)
+ assert os.path.isfile(filename)
+
+ with quiet():
+ cmd.run()
+
+ # not all windows systems have a default FS encoding of cp1252
+ if sys.platform == 'win32':
+ # Latin-1 is similar to Windows-1252 however
+ # on mbcs filesys it is not in latin-1 encoding
+ fs_enc = sys.getfilesystemencoding()
+ if fs_enc != 'mbcs':
+ fs_enc = 'latin-1'
+ filename = filename.decode(fs_enc)
+
+ assert filename in cmd.filelist.files
+ else:
+ # The Latin-1 filename should have been skipped
+ filename = filename.decode('latin-1')
+ assert filename not in cmd.filelist.files
+
+ _EXAMPLE_DIRECTIVES = {
+ "setup.cfg - long_description and version": """
+ [metadata]
+ name = testing
+ version = file: src/VERSION.txt
+ license_files = DOWHATYOUWANT
+ long_description = file: README.rst, USAGE.rst
+ """,
+ "pyproject.toml - static readme/license files and dynamic version": """
+ [project]
+ name = "testing"
+ readme = "USAGE.rst"
+ license-files = ["DOWHATYOUWANT"]
+ dynamic = ["version"]
+ [tool.setuptools.dynamic]
+ version = {file = ["src/VERSION.txt"]}
+ """,
+ "pyproject.toml - directive with str instead of list": """
+ [project]
+ name = "testing"
+ readme = "USAGE.rst"
+ license-files = ["DOWHATYOUWANT"]
+ dynamic = ["version"]
+ [tool.setuptools.dynamic]
+ version = {file = "src/VERSION.txt"}
+ """,
+ "pyproject.toml - deprecated license table with file entry": """
+ [project]
+ name = "testing"
+ readme = "USAGE.rst"
+ license = {file = "DOWHATYOUWANT"}
+ dynamic = ["version"]
+ [tool.setuptools.dynamic]
+ version = {file = "src/VERSION.txt"}
+ """,
+ }
+
+ @pytest.mark.parametrize("config", _EXAMPLE_DIRECTIVES.keys())
+ @pytest.mark.filterwarnings(
+ "ignore:.project.license. as a TOML table is deprecated"
+ )
+ def test_add_files_referenced_by_config_directives(self, source_dir, config):
+ config_file, _, _ = config.partition(" - ")
+ config_text = self._EXAMPLE_DIRECTIVES[config]
+ (source_dir / 'src').mkdir()
+ (source_dir / 'src/VERSION.txt').write_text("0.42", encoding="utf-8")
+ (source_dir / 'README.rst').write_text("hello world!", encoding="utf-8")
+ (source_dir / 'USAGE.rst').write_text("hello world!", encoding="utf-8")
+ (source_dir / 'DOWHATYOUWANT').write_text("hello world!", encoding="utf-8")
+ (source_dir / config_file).write_text(config_text, encoding="utf-8")
+
+ dist = Distribution({"packages": []})
+ dist.script_name = 'setup.py'
+ dist.parse_config_files()
+
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+ with quiet():
+ cmd.run()
+
+ assert (
+ 'src/VERSION.txt' in cmd.filelist.files
+ or 'src\\VERSION.txt' in cmd.filelist.files
+ )
+ assert 'USAGE.rst' in cmd.filelist.files
+ assert 'DOWHATYOUWANT' in cmd.filelist.files
+ assert '/' not in cmd.filelist.files
+ assert '\\' not in cmd.filelist.files
+
+ def test_pyproject_toml_in_sdist(self, source_dir):
+ """
+ Check if pyproject.toml is included in source distribution if present
+ """
+ touch(source_dir / 'pyproject.toml')
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+ with quiet():
+ cmd.run()
+ manifest = cmd.filelist.files
+ assert 'pyproject.toml' in manifest
+
+ def test_pyproject_toml_excluded(self, source_dir):
+ """
+ Check that pyproject.toml can excluded even if present
+ """
+ touch(source_dir / 'pyproject.toml')
+ with open('MANIFEST.in', 'w', encoding="utf-8") as mts:
+ print('exclude pyproject.toml', file=mts)
+ dist = Distribution(SETUP_ATTRS)
+ dist.script_name = 'setup.py'
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+ with quiet():
+ cmd.run()
+ manifest = cmd.filelist.files
+ assert 'pyproject.toml' not in manifest
+
+ def test_build_subcommand_source_files(self, source_dir):
+ touch(source_dir / '.myfile~')
+
+ # Sanity check: without custom commands file list should not be affected
+ dist = Distribution({**SETUP_ATTRS, "script_name": "setup.py"})
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+ with quiet():
+ cmd.run()
+ manifest = cmd.filelist.files
+ assert '.myfile~' not in manifest
+
+ # Test: custom command should be able to augment file list
+ dist = Distribution({**SETUP_ATTRS, "script_name": "setup.py"})
+ build = dist.get_command_obj("build")
+ build.sub_commands = [*build.sub_commands, ("build_custom", None)]
+
+ class build_custom(Command):
+ def initialize_options(self): ...
+
+ def finalize_options(self): ...
+
+ def run(self): ...
+
+ def get_source_files(self):
+ return ['.myfile~']
+
+ dist.cmdclass.update(build_custom=build_custom)
+
+ cmd = sdist(dist)
+ cmd.use_defaults = True
+ cmd.ensure_finalized()
+ with quiet():
+ cmd.run()
+ manifest = cmd.filelist.files
+ assert '.myfile~' in manifest
+
+ @pytest.mark.skipif("os.environ.get('SETUPTOOLS_USE_DISTUTILS') == 'stdlib'")
+ def test_build_base_pathlib(self, source_dir):
+ """
+ Ensure if build_base is a pathlib.Path, the build still succeeds.
+ """
+ dist = Distribution({
+ **SETUP_ATTRS,
+ "script_name": "setup.py",
+ "options": {"build": {"build_base": pathlib.Path('build')}},
+ })
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+ with quiet():
+ cmd.run()
+
+
+def test_default_revctrl():
+ """
+ When _default_revctrl was removed from the `setuptools.command.sdist`
+ module in 10.0, it broke some systems which keep an old install of
+ setuptools (Distribute) around. Those old versions require that the
+ setuptools package continue to implement that interface, so this
+ function provides that interface, stubbed. See #320 for details.
+
+ This interface must be maintained until Ubuntu 12.04 is no longer
+ supported (by Setuptools).
+ """
+ (ep,) = metadata.EntryPoints._from_text(
+ """
+ [setuptools.file_finders]
+ svn_cvs = setuptools.command.sdist:_default_revctrl
+ """
+ )
+ res = ep.load()
+ assert hasattr(res, '__iter__')
+
+
+class TestRegressions:
+ """
+ Can be removed/changed if the project decides to change how it handles symlinks
+ or external files.
+ """
+
+ @staticmethod
+ def files_for_symlink_in_extension_depends(tmp_path, dep_path):
+ return {
+ "external": {
+ "dir": {"file.h": ""},
+ },
+ "project": {
+ "setup.py": cleandoc(
+ f"""
+ from setuptools import Extension, setup
+ setup(
+ name="myproj",
+ version="42",
+ ext_modules=[
+ Extension(
+ "hello", sources=["hello.pyx"],
+ depends=[{dep_path!r}]
+ )
+ ],
+ )
+ """
+ ),
+ "hello.pyx": "",
+ "MANIFEST.in": "global-include *.h",
+ },
+ }
+
+ @pytest.mark.parametrize(
+ "dep_path", ("myheaders/dir/file.h", "myheaders/dir/../dir/file.h")
+ )
+ def test_symlink_in_extension_depends(self, monkeypatch, tmp_path, dep_path):
+ # Given a project with a symlinked dir and a "depends" targeting that dir
+ files = self.files_for_symlink_in_extension_depends(tmp_path, dep_path)
+ jaraco.path.build(files, prefix=str(tmp_path))
+ symlink_or_skip_test(tmp_path / "external", tmp_path / "project/myheaders")
+
+ # When `sdist` runs, there should be no error
+ members = run_sdist(monkeypatch, tmp_path / "project")
+ # and the sdist should contain the symlinked files
+ for expected in (
+ "myproj-42/hello.pyx",
+ "myproj-42/myheaders/dir/file.h",
+ ):
+ assert expected in members
+
+ @staticmethod
+ def files_for_external_path_in_extension_depends(tmp_path, dep_path):
+ head, _, tail = dep_path.partition("$tmp_path$/")
+ dep_path = tmp_path / tail if tail else head
+
+ return {
+ "external": {
+ "dir": {"file.h": ""},
+ },
+ "project": {
+ "setup.py": cleandoc(
+ f"""
+ from setuptools import Extension, setup
+ setup(
+ name="myproj",
+ version="42",
+ ext_modules=[
+ Extension(
+ "hello", sources=["hello.pyx"],
+ depends=[{str(dep_path)!r}]
+ )
+ ],
+ )
+ """
+ ),
+ "hello.pyx": "",
+ "MANIFEST.in": "global-include *.h",
+ },
+ }
+
+ @pytest.mark.parametrize(
+ "dep_path", ("$tmp_path$/external/dir/file.h", "../external/dir/file.h")
+ )
+ def test_external_path_in_extension_depends(self, monkeypatch, tmp_path, dep_path):
+ # Given a project with a "depends" targeting an external dir
+ files = self.files_for_external_path_in_extension_depends(tmp_path, dep_path)
+ jaraco.path.build(files, prefix=str(tmp_path))
+ # When `sdist` runs, there should be no error
+ members = run_sdist(monkeypatch, tmp_path / "project")
+ # and the sdist should not contain the external file
+ for name in members:
+ assert "file.h" not in name
+
+
+def run_sdist(monkeypatch, project):
+ """Given a project directory, run the sdist and return its contents"""
+ monkeypatch.chdir(project)
+ with quiet():
+ run_setup("setup.py", ["sdist"])
+
+ archive = next((project / "dist").glob("*.tar.gz"))
+ with tarfile.open(str(archive)) as tar:
+ return set(tar.getnames())
+
+
+def test_sanity_check_setuptools_own_sdist(setuptools_sdist):
+ with tarfile.open(setuptools_sdist) as tar:
+ files = tar.getnames()
+
+ # setuptools sdist should not include the .tox folder
+ tox_files = [name for name in files if ".tox" in name]
+ assert len(tox_files) == 0, f"not empty {tox_files}"
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_setopt.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_setopt.py
new file mode 100644
index 00000000..ccf25618
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_setopt.py
@@ -0,0 +1,40 @@
+import configparser
+
+from setuptools.command import setopt
+
+
+class TestEdit:
+ @staticmethod
+ def parse_config(filename):
+ parser = configparser.ConfigParser()
+ with open(filename, encoding='utf-8') as reader:
+ parser.read_file(reader)
+ return parser
+
+ @staticmethod
+ def write_text(file, content):
+ with open(file, 'wb') as strm:
+ strm.write(content.encode('utf-8'))
+
+ def test_utf8_encoding_retained(self, tmpdir):
+ """
+ When editing a file, non-ASCII characters encoded in
+ UTF-8 should be retained.
+ """
+ config = tmpdir.join('setup.cfg')
+ self.write_text(str(config), '[names]\njaraco=джарако')
+ setopt.edit_config(str(config), dict(names=dict(other='yes')))
+ parser = self.parse_config(str(config))
+ assert parser.get('names', 'jaraco') == 'джарако'
+ assert parser.get('names', 'other') == 'yes'
+
+ def test_case_retained(self, tmpdir):
+ """
+ When editing a file, case of keys should be retained.
+ """
+ config = tmpdir.join('setup.cfg')
+ self.write_text(str(config), '[names]\nFoO=bAr')
+ setopt.edit_config(str(config), dict(names=dict(oTher='yes')))
+ actual = config.read_text(encoding='ascii')
+ assert 'FoO' in actual
+ assert 'oTher' in actual
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_setuptools.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_setuptools.py
new file mode 100644
index 00000000..1d56e1a8
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_setuptools.py
@@ -0,0 +1,290 @@
+"""Tests for the 'setuptools' package"""
+
+import os
+import re
+import sys
+from zipfile import ZipFile
+
+import pytest
+from packaging.version import Version
+
+import setuptools
+import setuptools.depends as dep
+import setuptools.dist
+from setuptools.depends import Require
+
+import distutils.cmd
+import distutils.core
+from distutils.core import Extension
+from distutils.errors import DistutilsSetupError
+
+
+@pytest.fixture(autouse=True)
+def isolated_dir(tmpdir_cwd):
+ return
+
+
+def makeSetup(**args):
+ """Return distribution from 'setup(**args)', without executing commands"""
+
+ distutils.core._setup_stop_after = "commandline"
+
+ # Don't let system command line leak into tests!
+ args.setdefault('script_args', ['install'])
+
+ try:
+ return setuptools.setup(**args)
+ finally:
+ distutils.core._setup_stop_after = None
+
+
+needs_bytecode = pytest.mark.skipif(
+ not hasattr(dep, 'get_module_constant'),
+ reason="bytecode support not available",
+)
+
+
+class TestDepends:
+ def testExtractConst(self):
+ if not hasattr(dep, 'extract_constant'):
+ # skip on non-bytecode platforms
+ return
+
+ def f1():
+ global x, y, z
+ x = "test"
+ y = z # pyright: ignore[reportUnboundVariable] # Explicitly testing for this runtime issue
+
+ fc = f1.__code__
+
+ # unrecognized name
+ assert dep.extract_constant(fc, 'q', -1) is None
+
+ # constant assigned
+ assert dep.extract_constant(fc, 'x', -1) == "test"
+
+ # expression assigned
+ assert dep.extract_constant(fc, 'y', -1) == -1
+
+ # recognized name, not assigned
+ assert dep.extract_constant(fc, 'z', -1) is None
+
+ def testFindModule(self):
+ with pytest.raises(ImportError):
+ dep.find_module('no-such.-thing')
+ with pytest.raises(ImportError):
+ dep.find_module('setuptools.non-existent')
+ f, _p, _i = dep.find_module('setuptools.tests')
+ f.close()
+
+ @needs_bytecode
+ def testModuleExtract(self):
+ from json import __version__
+
+ assert dep.get_module_constant('json', '__version__') == __version__
+ assert dep.get_module_constant('sys', 'version') == sys.version
+ assert (
+ dep.get_module_constant('setuptools.tests.test_setuptools', '__doc__')
+ == __doc__
+ )
+
+ @needs_bytecode
+ def testRequire(self):
+ req = Require('Json', '1.0.3', 'json')
+
+ assert req.name == 'Json'
+ assert req.module == 'json'
+ assert req.requested_version == Version('1.0.3')
+ assert req.attribute == '__version__'
+ assert req.full_name() == 'Json-1.0.3'
+
+ from json import __version__
+
+ assert str(req.get_version()) == __version__
+ assert req.version_ok('1.0.9')
+ assert not req.version_ok('0.9.1')
+ assert not req.version_ok('unknown')
+
+ assert req.is_present()
+ assert req.is_current()
+
+ req = Require('Do-what-I-mean', '1.0', 'd-w-i-m')
+ assert not req.is_present()
+ assert not req.is_current()
+
+ @needs_bytecode
+ def test_require_present(self):
+ # In #1896, this test was failing for months with the only
+ # complaint coming from test runners (not end users).
+ # TODO: Evaluate if this code is needed at all.
+ req = Require('Tests', None, 'tests', homepage="http://example.com")
+ assert req.format is None
+ assert req.attribute is None
+ assert req.requested_version is None
+ assert req.full_name() == 'Tests'
+ assert req.homepage == 'http://example.com'
+
+ from setuptools.tests import __path__
+
+ paths = [os.path.dirname(p) for p in __path__]
+ assert req.is_present(paths)
+ assert req.is_current(paths)
+
+
+class TestDistro:
+ def setup_method(self, method):
+ self.e1 = Extension('bar.ext', ['bar.c'])
+ self.e2 = Extension('c.y', ['y.c'])
+
+ self.dist = makeSetup(
+ packages=['a', 'a.b', 'a.b.c', 'b', 'c'],
+ py_modules=['b.d', 'x'],
+ ext_modules=(self.e1, self.e2),
+ package_dir={},
+ )
+
+ def testDistroType(self):
+ assert isinstance(self.dist, setuptools.dist.Distribution)
+
+ def testExcludePackage(self):
+ self.dist.exclude_package('a')
+ assert self.dist.packages == ['b', 'c']
+
+ self.dist.exclude_package('b')
+ assert self.dist.packages == ['c']
+ assert self.dist.py_modules == ['x']
+ assert self.dist.ext_modules == [self.e1, self.e2]
+
+ self.dist.exclude_package('c')
+ assert self.dist.packages == []
+ assert self.dist.py_modules == ['x']
+ assert self.dist.ext_modules == [self.e1]
+
+ # test removals from unspecified options
+ makeSetup().exclude_package('x')
+
+ def testIncludeExclude(self):
+ # remove an extension
+ self.dist.exclude(ext_modules=[self.e1])
+ assert self.dist.ext_modules == [self.e2]
+
+ # add it back in
+ self.dist.include(ext_modules=[self.e1])
+ assert self.dist.ext_modules == [self.e2, self.e1]
+
+ # should not add duplicate
+ self.dist.include(ext_modules=[self.e1])
+ assert self.dist.ext_modules == [self.e2, self.e1]
+
+ def testExcludePackages(self):
+ self.dist.exclude(packages=['c', 'b', 'a'])
+ assert self.dist.packages == []
+ assert self.dist.py_modules == ['x']
+ assert self.dist.ext_modules == [self.e1]
+
+ def testEmpty(self):
+ dist = makeSetup()
+ dist.include(packages=['a'], py_modules=['b'], ext_modules=[self.e2])
+ dist = makeSetup()
+ dist.exclude(packages=['a'], py_modules=['b'], ext_modules=[self.e2])
+
+ def testContents(self):
+ assert self.dist.has_contents_for('a')
+ self.dist.exclude_package('a')
+ assert not self.dist.has_contents_for('a')
+
+ assert self.dist.has_contents_for('b')
+ self.dist.exclude_package('b')
+ assert not self.dist.has_contents_for('b')
+
+ assert self.dist.has_contents_for('c')
+ self.dist.exclude_package('c')
+ assert not self.dist.has_contents_for('c')
+
+ def testInvalidIncludeExclude(self):
+ with pytest.raises(DistutilsSetupError):
+ self.dist.include(nonexistent_option='x')
+ with pytest.raises(DistutilsSetupError):
+ self.dist.exclude(nonexistent_option='x')
+ with pytest.raises(DistutilsSetupError):
+ self.dist.include(packages={'x': 'y'})
+ with pytest.raises(DistutilsSetupError):
+ self.dist.exclude(packages={'x': 'y'})
+ with pytest.raises(DistutilsSetupError):
+ self.dist.include(ext_modules={'x': 'y'})
+ with pytest.raises(DistutilsSetupError):
+ self.dist.exclude(ext_modules={'x': 'y'})
+
+ with pytest.raises(DistutilsSetupError):
+ self.dist.include(package_dir=['q'])
+ with pytest.raises(DistutilsSetupError):
+ self.dist.exclude(package_dir=['q'])
+
+
+@pytest.fixture
+def example_source(tmpdir):
+ tmpdir.mkdir('foo')
+ (tmpdir / 'foo/bar.py').write('')
+ (tmpdir / 'readme.txt').write('')
+ return tmpdir
+
+
+def test_findall(example_source):
+ found = list(setuptools.findall(str(example_source)))
+ expected = ['readme.txt', 'foo/bar.py']
+ expected = [example_source.join(fn) for fn in expected]
+ assert found == expected
+
+
+def test_findall_curdir(example_source):
+ with example_source.as_cwd():
+ found = list(setuptools.findall())
+ expected = ['readme.txt', os.path.join('foo', 'bar.py')]
+ assert found == expected
+
+
+@pytest.fixture
+def can_symlink(tmpdir):
+ """
+ Skip if cannot create a symbolic link
+ """
+ link_fn = 'link'
+ target_fn = 'target'
+ try:
+ os.symlink(target_fn, link_fn)
+ except (OSError, NotImplementedError, AttributeError):
+ pytest.skip("Cannot create symbolic links")
+ os.remove(link_fn)
+
+
+@pytest.mark.usefixtures("can_symlink")
+def test_findall_missing_symlink(tmpdir):
+ with tmpdir.as_cwd():
+ os.symlink('foo', 'bar')
+ found = list(setuptools.findall())
+ assert found == []
+
+
+@pytest.mark.xfail(reason="unable to exclude tests; #4475 #3260")
+def test_its_own_wheel_does_not_contain_tests(setuptools_wheel):
+ with ZipFile(setuptools_wheel) as zipfile:
+ contents = [f.replace(os.sep, '/') for f in zipfile.namelist()]
+
+ for member in contents:
+ assert '/tests/' not in member
+
+
+def test_wheel_includes_cli_scripts(setuptools_wheel):
+ with ZipFile(setuptools_wheel) as zipfile:
+ contents = [f.replace(os.sep, '/') for f in zipfile.namelist()]
+
+ assert any('cli-64.exe' in member for member in contents)
+
+
+def test_wheel_includes_vendored_metadata(setuptools_wheel):
+ with ZipFile(setuptools_wheel) as zipfile:
+ contents = [f.replace(os.sep, '/') for f in zipfile.namelist()]
+
+ assert any(
+ re.search(r'_vendor/.*\.dist-info/METADATA', member) for member in contents
+ )
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_shutil_wrapper.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_shutil_wrapper.py
new file mode 100644
index 00000000..74ff7e9a
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_shutil_wrapper.py
@@ -0,0 +1,23 @@
+import stat
+import sys
+from unittest.mock import Mock
+
+from setuptools import _shutil
+
+
+def test_rmtree_readonly(monkeypatch, tmp_path):
+ """Verify onerr works as expected"""
+
+ tmp_dir = tmp_path / "with_readonly"
+ tmp_dir.mkdir()
+ some_file = tmp_dir.joinpath("file.txt")
+ some_file.touch()
+ some_file.chmod(stat.S_IREAD)
+
+ expected_count = 1 if sys.platform.startswith("win") else 0
+ chmod_fn = Mock(wraps=_shutil.attempt_chmod_verbose)
+ monkeypatch.setattr(_shutil, "attempt_chmod_verbose", chmod_fn)
+
+ _shutil.rmtree(tmp_dir)
+ assert chmod_fn.call_count == expected_count
+ assert not tmp_dir.is_dir()
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_unicode_utils.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_unicode_utils.py
new file mode 100644
index 00000000..a24a9bd5
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_unicode_utils.py
@@ -0,0 +1,10 @@
+from setuptools import unicode_utils
+
+
+def test_filesys_decode_fs_encoding_is_None(monkeypatch):
+ """
+ Test filesys_decode does not raise TypeError when
+ getfilesystemencoding returns None.
+ """
+ monkeypatch.setattr('sys.getfilesystemencoding', lambda: None)
+ unicode_utils.filesys_decode(b'test')
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_virtualenv.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_virtualenv.py
new file mode 100644
index 00000000..b02949ba
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_virtualenv.py
@@ -0,0 +1,113 @@
+import os
+import subprocess
+import sys
+from urllib.error import URLError
+from urllib.request import urlopen
+
+import pytest
+
+
+@pytest.fixture(autouse=True)
+def pytest_virtualenv_works(venv):
+ """
+ pytest_virtualenv may not work. if it doesn't, skip these
+ tests. See #1284.
+ """
+ venv_prefix = venv.run(["python", "-c", "import sys; print(sys.prefix)"]).strip()
+ if venv_prefix == sys.prefix:
+ pytest.skip("virtualenv is broken (see pypa/setuptools#1284)")
+
+
+def test_clean_env_install(venv_without_setuptools, setuptools_wheel):
+ """
+ Check setuptools can be installed in a clean environment.
+ """
+ cmd = ["python", "-m", "pip", "install", str(setuptools_wheel)]
+ venv_without_setuptools.run(cmd)
+
+
+def access_pypi():
+ # Detect if tests are being run without connectivity
+ if not os.environ.get('NETWORK_REQUIRED', False): # pragma: nocover
+ try:
+ urlopen('https://pypi.org', timeout=1)
+ except URLError:
+ # No network, disable most of these tests
+ return False
+
+ return True
+
+
+@pytest.mark.skipif(
+ 'platform.python_implementation() == "PyPy"',
+ reason="https://github.com/pypa/setuptools/pull/2865#issuecomment-965834995",
+)
+@pytest.mark.skipif(not access_pypi(), reason="no network")
+# ^-- Even when it is not necessary to install a different version of `pip`
+# the build process will still try to download `wheel`, see #3147 and #2986.
+@pytest.mark.parametrize(
+ 'pip_version',
+ [
+ None,
+ pytest.param(
+ 'pip<20.1',
+ marks=pytest.mark.xfail(
+ 'sys.version_info >= (3, 12)',
+ reason="pip 23.1.2 required for Python 3.12 and later",
+ ),
+ ),
+ pytest.param(
+ 'pip<21',
+ marks=pytest.mark.xfail(
+ 'sys.version_info >= (3, 12)',
+ reason="pip 23.1.2 required for Python 3.12 and later",
+ ),
+ ),
+ pytest.param(
+ 'pip<22',
+ marks=pytest.mark.xfail(
+ 'sys.version_info >= (3, 12)',
+ reason="pip 23.1.2 required for Python 3.12 and later",
+ ),
+ ),
+ pytest.param(
+ 'pip<23',
+ marks=pytest.mark.xfail(
+ 'sys.version_info >= (3, 12)',
+ reason="pip 23.1.2 required for Python 3.12 and later",
+ ),
+ ),
+ pytest.param(
+ 'https://github.com/pypa/pip/archive/main.zip',
+ marks=pytest.mark.xfail(reason='#2975'),
+ ),
+ ],
+)
+def test_pip_upgrade_from_source(
+ pip_version, venv_without_setuptools, setuptools_wheel, setuptools_sdist
+):
+ """
+ Check pip can upgrade setuptools from source.
+ """
+ # Install pip/wheel, in a venv without setuptools (as it
+ # should not be needed for bootstrapping from source)
+ venv = venv_without_setuptools
+ venv.run(["pip", "install", "-U", "wheel"])
+ if pip_version is not None:
+ venv.run(["python", "-m", "pip", "install", "-U", pip_version, "--retries=1"])
+ with pytest.raises(subprocess.CalledProcessError):
+ # Meta-test to make sure setuptools is not installed
+ venv.run(["python", "-c", "import setuptools"])
+
+ # Then install from wheel.
+ venv.run(["pip", "install", str(setuptools_wheel)])
+ # And finally try to upgrade from source.
+ venv.run(["pip", "install", "--no-cache-dir", "--upgrade", str(setuptools_sdist)])
+
+
+def test_no_missing_dependencies(bare_venv, request):
+ """
+ Quick and dirty test to ensure all external dependencies are vendored.
+ """
+ setuptools_dir = request.config.rootdir
+ bare_venv.run(['python', 'setup.py', '--help'], cwd=setuptools_dir)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_warnings.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_warnings.py
new file mode 100644
index 00000000..41193d4f
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_warnings.py
@@ -0,0 +1,106 @@
+from inspect import cleandoc
+
+import pytest
+
+from setuptools.warnings import SetuptoolsDeprecationWarning, SetuptoolsWarning
+
+_EXAMPLES = {
+ "default": dict(
+ args=("Hello {x}", "\n\t{target} {v:.1f}"),
+ kwargs={"x": 5, "v": 3, "target": "World"},
+ expected="""
+ Hello 5
+ !!
+
+ ********************************************************************************
+ World 3.0
+ ********************************************************************************
+
+ !!
+ """,
+ ),
+ "futue_due_date": dict(
+ args=("Summary", "Lorem ipsum"),
+ kwargs={"due_date": (9999, 11, 22)},
+ expected="""
+ Summary
+ !!
+
+ ********************************************************************************
+ Lorem ipsum
+
+ By 9999-Nov-22, you need to update your project and remove deprecated calls
+ or your builds will no longer be supported.
+ ********************************************************************************
+
+ !!
+ """,
+ ),
+ "past_due_date_with_docs": dict(
+ args=("Summary", "Lorem ipsum"),
+ kwargs={"due_date": (2000, 11, 22), "see_docs": "some_page.html"},
+ expected="""
+ Summary
+ !!
+
+ ********************************************************************************
+ Lorem ipsum
+
+ This deprecation is overdue, please update your project and remove deprecated
+ calls to avoid build errors in the future.
+
+ See https://setuptools.pypa.io/en/latest/some_page.html for details.
+ ********************************************************************************
+
+ !!
+ """,
+ ),
+}
+
+
+@pytest.mark.parametrize("example_name", _EXAMPLES.keys())
+def test_formatting(monkeypatch, example_name):
+ """
+ It should automatically handle indentation, interpolation and things like due date.
+ """
+ args = _EXAMPLES[example_name]["args"]
+ kwargs = _EXAMPLES[example_name]["kwargs"]
+ expected = _EXAMPLES[example_name]["expected"]
+
+ monkeypatch.setenv("SETUPTOOLS_ENFORCE_DEPRECATION", "false")
+ with pytest.warns(SetuptoolsWarning) as warn_info:
+ SetuptoolsWarning.emit(*args, **kwargs)
+ assert _get_message(warn_info) == cleandoc(expected)
+
+
+def test_due_date_enforcement(monkeypatch):
+ class _MyDeprecation(SetuptoolsDeprecationWarning):
+ _SUMMARY = "Summary"
+ _DETAILS = "Lorem ipsum"
+ _DUE_DATE = (2000, 11, 22)
+ _SEE_DOCS = "some_page.html"
+
+ monkeypatch.setenv("SETUPTOOLS_ENFORCE_DEPRECATION", "true")
+ with pytest.raises(SetuptoolsDeprecationWarning) as exc_info:
+ _MyDeprecation.emit()
+
+ expected = """
+ Summary
+ !!
+
+ ********************************************************************************
+ Lorem ipsum
+
+ This deprecation is overdue, please update your project and remove deprecated
+ calls to avoid build errors in the future.
+
+ See https://setuptools.pypa.io/en/latest/some_page.html for details.
+ ********************************************************************************
+
+ !!
+ """
+ assert str(exc_info.value) == cleandoc(expected)
+
+
+def _get_message(warn_info):
+ return next(warn.message.args[0] for warn in warn_info)
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_wheel.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_wheel.py
new file mode 100644
index 00000000..70165c60
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_wheel.py
@@ -0,0 +1,714 @@
+"""wheel tests"""
+
+from __future__ import annotations
+
+import contextlib
+import glob
+import inspect
+import os
+import pathlib
+import shutil
+import stat
+import subprocess
+import sys
+import zipfile
+from typing import Any
+
+import pytest
+from jaraco import path
+from packaging.tags import parse_tag
+from packaging.utils import canonicalize_name
+
+from pkg_resources import PY_MAJOR, Distribution, PathMetadata
+from setuptools.wheel import Wheel
+
+from .contexts import tempdir
+from .textwrap import DALS
+
+from distutils.sysconfig import get_config_var
+from distutils.util import get_platform
+
+WHEEL_INFO_TESTS = (
+ ('invalid.whl', ValueError),
+ (
+ 'simplewheel-2.0-1-py2.py3-none-any.whl',
+ {
+ 'project_name': 'simplewheel',
+ 'version': '2.0',
+ 'build': '1',
+ 'py_version': 'py2.py3',
+ 'abi': 'none',
+ 'platform': 'any',
+ },
+ ),
+ (
+ 'simple.dist-0.1-py2.py3-none-any.whl',
+ {
+ 'project_name': 'simple.dist',
+ 'version': '0.1',
+ 'build': None,
+ 'py_version': 'py2.py3',
+ 'abi': 'none',
+ 'platform': 'any',
+ },
+ ),
+ (
+ 'example_pkg_a-1-py3-none-any.whl',
+ {
+ 'project_name': 'example_pkg_a',
+ 'version': '1',
+ 'build': None,
+ 'py_version': 'py3',
+ 'abi': 'none',
+ 'platform': 'any',
+ },
+ ),
+ (
+ 'PyQt5-5.9-5.9.1-cp35.cp36.cp37-abi3-manylinux1_x86_64.whl',
+ {
+ 'project_name': 'PyQt5',
+ 'version': '5.9',
+ 'build': '5.9.1',
+ 'py_version': 'cp35.cp36.cp37',
+ 'abi': 'abi3',
+ 'platform': 'manylinux1_x86_64',
+ },
+ ),
+)
+
+
+@pytest.mark.parametrize(
+ ('filename', 'info'), WHEEL_INFO_TESTS, ids=[t[0] for t in WHEEL_INFO_TESTS]
+)
+def test_wheel_info(filename, info):
+ if inspect.isclass(info):
+ with pytest.raises(info):
+ Wheel(filename)
+ return
+ w = Wheel(filename)
+ assert {k: getattr(w, k) for k in info.keys()} == info
+
+
+@contextlib.contextmanager
+def build_wheel(extra_file_defs=None, **kwargs):
+ file_defs = {
+ 'setup.py': (
+ DALS(
+ """
+ # -*- coding: utf-8 -*-
+ from setuptools import setup
+ import setuptools
+ setup(**%r)
+ """
+ )
+ % kwargs
+ ).encode('utf-8'),
+ }
+ if extra_file_defs:
+ file_defs.update(extra_file_defs)
+ with tempdir() as source_dir:
+ path.build(file_defs, source_dir)
+ subprocess.check_call(
+ (sys.executable, 'setup.py', '-q', 'bdist_wheel'), cwd=source_dir
+ )
+ yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0]
+
+
+def tree_set(root):
+ contents = set()
+ for dirpath, dirnames, filenames in os.walk(root):
+ for filename in filenames:
+ contents.add(os.path.join(os.path.relpath(dirpath, root), filename))
+ return contents
+
+
+def flatten_tree(tree):
+ """Flatten nested dicts and lists into a full list of paths"""
+ output = set()
+ for node, contents in tree.items():
+ if isinstance(contents, dict):
+ contents = flatten_tree(contents)
+
+ for elem in contents:
+ if isinstance(elem, dict):
+ output |= {os.path.join(node, val) for val in flatten_tree(elem)}
+ else:
+ output.add(os.path.join(node, elem))
+ return output
+
+
+def format_install_tree(tree):
+ return {
+ x.format(
+ py_version=PY_MAJOR,
+ platform=get_platform(),
+ shlib_ext=get_config_var('EXT_SUFFIX') or get_config_var('SO'),
+ )
+ for x in tree
+ }
+
+
+def _check_wheel_install(
+ filename, install_dir, install_tree_includes, project_name, version, requires_txt
+):
+ w = Wheel(filename)
+ egg_path = os.path.join(install_dir, w.egg_name())
+ w.install_as_egg(egg_path)
+ if install_tree_includes is not None:
+ install_tree = format_install_tree(install_tree_includes)
+ exp = tree_set(install_dir)
+ assert install_tree.issubset(exp), install_tree - exp
+
+ metadata = PathMetadata(egg_path, os.path.join(egg_path, 'EGG-INFO'))
+ dist = Distribution.from_filename(egg_path, metadata=metadata)
+ assert dist.project_name == project_name
+ assert dist.version == version
+ if requires_txt is None:
+ assert not dist.has_metadata('requires.txt')
+ else:
+ # Order must match to ensure reproducibility.
+ assert requires_txt == dist.get_metadata('requires.txt').lstrip()
+
+
+class Record:
+ def __init__(self, id, **kwargs):
+ self._id = id
+ self._fields = kwargs
+
+ def __repr__(self) -> str:
+ return f'{self._id}(**{self._fields!r})'
+
+
+# Using Any to avoid possible type union issues later in test
+# making a TypedDict is not worth in a test and anonymous/inline TypedDict are experimental
+# https://github.com/python/mypy/issues/9884
+WHEEL_INSTALL_TESTS: tuple[dict[str, Any], ...] = (
+ dict(
+ id='basic',
+ file_defs={'foo': {'__init__.py': ''}},
+ setup_kwargs=dict(
+ packages=['foo'],
+ ),
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}.egg': {
+ 'EGG-INFO': ['PKG-INFO', 'RECORD', 'WHEEL', 'top_level.txt'],
+ 'foo': ['__init__.py'],
+ }
+ }),
+ ),
+ dict(
+ id='utf-8',
+ setup_kwargs=dict(
+ description='Description accentuée',
+ ),
+ ),
+ dict(
+ id='data',
+ file_defs={
+ 'data.txt': DALS(
+ """
+ Some data...
+ """
+ ),
+ },
+ setup_kwargs=dict(
+ data_files=[('data_dir', ['data.txt'])],
+ ),
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}.egg': {
+ 'EGG-INFO': ['PKG-INFO', 'RECORD', 'WHEEL', 'top_level.txt'],
+ 'data_dir': ['data.txt'],
+ }
+ }),
+ ),
+ dict(
+ id='extension',
+ file_defs={
+ 'extension.c': DALS(
+ """
+ #include "Python.h"
+
+ #if PY_MAJOR_VERSION >= 3
+
+ static struct PyModuleDef moduledef = {
+ PyModuleDef_HEAD_INIT,
+ "extension",
+ NULL,
+ 0,
+ NULL,
+ NULL,
+ NULL,
+ NULL,
+ NULL
+ };
+
+ #define INITERROR return NULL
+
+ PyMODINIT_FUNC PyInit_extension(void)
+
+ #else
+
+ #define INITERROR return
+
+ void initextension(void)
+
+ #endif
+ {
+ #if PY_MAJOR_VERSION >= 3
+ PyObject *module = PyModule_Create(&moduledef);
+ #else
+ PyObject *module = Py_InitModule("extension", NULL);
+ #endif
+ if (module == NULL)
+ INITERROR;
+ #if PY_MAJOR_VERSION >= 3
+ return module;
+ #endif
+ }
+ """
+ ),
+ },
+ setup_kwargs=dict(
+ ext_modules=[
+ Record(
+ 'setuptools.Extension', name='extension', sources=['extension.c']
+ )
+ ],
+ ),
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}-{platform}.egg': [
+ 'extension{shlib_ext}',
+ {
+ 'EGG-INFO': [
+ 'PKG-INFO',
+ 'RECORD',
+ 'WHEEL',
+ 'top_level.txt',
+ ]
+ },
+ ]
+ }),
+ ),
+ dict(
+ id='header',
+ file_defs={
+ 'header.h': DALS(
+ """
+ """
+ ),
+ },
+ setup_kwargs=dict(
+ headers=['header.h'],
+ ),
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}.egg': [
+ 'header.h',
+ {
+ 'EGG-INFO': [
+ 'PKG-INFO',
+ 'RECORD',
+ 'WHEEL',
+ 'top_level.txt',
+ ]
+ },
+ ]
+ }),
+ ),
+ dict(
+ id='script',
+ file_defs={
+ 'script.py': DALS(
+ """
+ #/usr/bin/python
+ print('hello world!')
+ """
+ ),
+ 'script.sh': DALS(
+ """
+ #/bin/sh
+ echo 'hello world!'
+ """
+ ),
+ },
+ setup_kwargs=dict(
+ scripts=['script.py', 'script.sh'],
+ ),
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}.egg': {
+ 'EGG-INFO': [
+ 'PKG-INFO',
+ 'RECORD',
+ 'WHEEL',
+ 'top_level.txt',
+ {'scripts': ['script.py', 'script.sh']},
+ ]
+ }
+ }),
+ ),
+ dict(
+ id='requires1',
+ install_requires='foobar==2.0',
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}.egg': {
+ 'EGG-INFO': [
+ 'PKG-INFO',
+ 'RECORD',
+ 'WHEEL',
+ 'requires.txt',
+ 'top_level.txt',
+ ]
+ }
+ }),
+ requires_txt=DALS(
+ """
+ foobar==2.0
+ """
+ ),
+ ),
+ dict(
+ id='requires2',
+ install_requires=f"""
+ bar
+ foo<=2.0; {sys.platform!r} in sys_platform
+ """,
+ requires_txt=DALS(
+ """
+ bar
+ foo<=2.0
+ """
+ ),
+ ),
+ dict(
+ id='requires3',
+ install_requires=f"""
+ bar; {sys.platform!r} != sys_platform
+ """,
+ ),
+ dict(
+ id='requires4',
+ install_requires="""
+ foo
+ """,
+ extras_require={
+ 'extra': 'foobar>3',
+ },
+ requires_txt=DALS(
+ """
+ foo
+
+ [extra]
+ foobar>3
+ """
+ ),
+ ),
+ dict(
+ id='requires5',
+ extras_require={
+ 'extra': f'foobar; {sys.platform!r} != sys_platform',
+ },
+ requires_txt=DALS(
+ """
+ [extra]
+ """
+ ),
+ ),
+ dict(
+ id='requires_ensure_order',
+ install_requires="""
+ foo
+ bar
+ baz
+ qux
+ """,
+ extras_require={
+ 'extra': """
+ foobar>3
+ barbaz>4
+ bazqux>5
+ quxzap>6
+ """,
+ },
+ requires_txt=DALS(
+ """
+ foo
+ bar
+ baz
+ qux
+
+ [extra]
+ foobar>3
+ barbaz>4
+ bazqux>5
+ quxzap>6
+ """
+ ),
+ ),
+ dict(
+ id='namespace_package',
+ file_defs={
+ 'foo': {
+ 'bar': {'__init__.py': ''},
+ },
+ },
+ setup_kwargs=dict(
+ namespace_packages=['foo'],
+ packages=['foo.bar'],
+ ),
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}.egg': [
+ 'foo-1.0-py{py_version}-nspkg.pth',
+ {
+ 'EGG-INFO': [
+ 'PKG-INFO',
+ 'RECORD',
+ 'WHEEL',
+ 'namespace_packages.txt',
+ 'top_level.txt',
+ ]
+ },
+ {
+ 'foo': [
+ '__init__.py',
+ {'bar': ['__init__.py']},
+ ]
+ },
+ ]
+ }),
+ ),
+ dict(
+ id='empty_namespace_package',
+ file_defs={
+ 'foobar': {
+ '__init__.py': (
+ "__import__('pkg_resources').declare_namespace(__name__)"
+ )
+ },
+ },
+ setup_kwargs=dict(
+ namespace_packages=['foobar'],
+ packages=['foobar'],
+ ),
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}.egg': [
+ 'foo-1.0-py{py_version}-nspkg.pth',
+ {
+ 'EGG-INFO': [
+ 'PKG-INFO',
+ 'RECORD',
+ 'WHEEL',
+ 'namespace_packages.txt',
+ 'top_level.txt',
+ ]
+ },
+ {
+ 'foobar': [
+ '__init__.py',
+ ]
+ },
+ ]
+ }),
+ ),
+ dict(
+ id='data_in_package',
+ file_defs={
+ 'foo': {
+ '__init__.py': '',
+ 'data_dir': {
+ 'data.txt': DALS(
+ """
+ Some data...
+ """
+ ),
+ },
+ }
+ },
+ setup_kwargs=dict(
+ packages=['foo'],
+ data_files=[('foo/data_dir', ['foo/data_dir/data.txt'])],
+ ),
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}.egg': {
+ 'EGG-INFO': [
+ 'PKG-INFO',
+ 'RECORD',
+ 'WHEEL',
+ 'top_level.txt',
+ ],
+ 'foo': [
+ '__init__.py',
+ {
+ 'data_dir': [
+ 'data.txt',
+ ]
+ },
+ ],
+ }
+ }),
+ ),
+)
+
+
+@pytest.mark.parametrize(
+ 'params',
+ WHEEL_INSTALL_TESTS,
+ ids=[params['id'] for params in WHEEL_INSTALL_TESTS],
+)
+def test_wheel_install(params):
+ project_name = params.get('name', 'foo')
+ version = params.get('version', '1.0')
+ install_requires = params.get('install_requires', [])
+ extras_require = params.get('extras_require', {})
+ requires_txt = params.get('requires_txt', None)
+ install_tree = params.get('install_tree')
+ file_defs = params.get('file_defs', {})
+ setup_kwargs = params.get('setup_kwargs', {})
+ with (
+ build_wheel(
+ name=project_name,
+ version=version,
+ install_requires=install_requires,
+ extras_require=extras_require,
+ extra_file_defs=file_defs,
+ **setup_kwargs,
+ ) as filename,
+ tempdir() as install_dir,
+ ):
+ _check_wheel_install(
+ filename, install_dir, install_tree, project_name, version, requires_txt
+ )
+
+
+def test_wheel_install_pep_503():
+ project_name = 'Foo_Bar' # PEP 503 canonicalized name is "foo-bar"
+ version = '1.0'
+ with (
+ build_wheel(
+ name=project_name,
+ version=version,
+ ) as filename,
+ tempdir() as install_dir,
+ ):
+ new_filename = filename.replace(project_name, canonicalize_name(project_name))
+ shutil.move(filename, new_filename)
+ _check_wheel_install(
+ new_filename,
+ install_dir,
+ None,
+ canonicalize_name(project_name),
+ version,
+ None,
+ )
+
+
+def test_wheel_no_dist_dir():
+ project_name = 'nodistinfo'
+ version = '1.0'
+ wheel_name = f'{project_name}-{version}-py2.py3-none-any.whl'
+ with tempdir() as source_dir:
+ wheel_path = os.path.join(source_dir, wheel_name)
+ # create an empty zip file
+ zipfile.ZipFile(wheel_path, 'w').close()
+ with tempdir() as install_dir:
+ with pytest.raises(ValueError):
+ _check_wheel_install(
+ wheel_path, install_dir, None, project_name, version, None
+ )
+
+
+def test_wheel_is_compatible(monkeypatch):
+ def sys_tags():
+ return {
+ (t.interpreter, t.abi, t.platform)
+ for t in parse_tag('cp36-cp36m-manylinux1_x86_64')
+ }
+
+ monkeypatch.setattr('setuptools.wheel._get_supported_tags', sys_tags)
+ assert Wheel('onnxruntime-0.1.2-cp36-cp36m-manylinux1_x86_64.whl').is_compatible()
+
+
+def test_wheel_mode():
+ @contextlib.contextmanager
+ def build_wheel(extra_file_defs=None, **kwargs):
+ file_defs = {
+ 'setup.py': (
+ DALS(
+ """
+ # -*- coding: utf-8 -*-
+ from setuptools import setup
+ import setuptools
+ setup(**%r)
+ """
+ )
+ % kwargs
+ ).encode('utf-8'),
+ }
+ if extra_file_defs:
+ file_defs.update(extra_file_defs)
+ with tempdir() as source_dir:
+ path.build(file_defs, source_dir)
+ runsh = pathlib.Path(source_dir) / "script.sh"
+ os.chmod(runsh, 0o777)
+ subprocess.check_call(
+ (sys.executable, 'setup.py', '-q', 'bdist_wheel'), cwd=source_dir
+ )
+ yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0]
+
+ params = dict(
+ id='script',
+ file_defs={
+ 'script.py': DALS(
+ """
+ #/usr/bin/python
+ print('hello world!')
+ """
+ ),
+ 'script.sh': DALS(
+ """
+ #/bin/sh
+ echo 'hello world!'
+ """
+ ),
+ },
+ setup_kwargs=dict(
+ scripts=['script.py', 'script.sh'],
+ ),
+ install_tree=flatten_tree({
+ 'foo-1.0-py{py_version}.egg': {
+ 'EGG-INFO': [
+ 'PKG-INFO',
+ 'RECORD',
+ 'WHEEL',
+ 'top_level.txt',
+ {'scripts': ['script.py', 'script.sh']},
+ ]
+ }
+ }),
+ )
+
+ project_name = params.get('name', 'foo')
+ version = params.get('version', '1.0')
+ install_tree = params.get('install_tree')
+ file_defs = params.get('file_defs', {})
+ setup_kwargs = params.get('setup_kwargs', {})
+
+ with (
+ build_wheel(
+ name=project_name,
+ version=version,
+ install_requires=[],
+ extras_require={},
+ extra_file_defs=file_defs,
+ **setup_kwargs,
+ ) as filename,
+ tempdir() as install_dir,
+ ):
+ _check_wheel_install(
+ filename, install_dir, install_tree, project_name, version, None
+ )
+ w = Wheel(filename)
+ base = pathlib.Path(install_dir) / w.egg_name()
+ script_sh = base / "EGG-INFO" / "scripts" / "script.sh"
+ assert script_sh.exists()
+ if sys.platform != 'win32':
+ # Editable file mode has no effect on Windows
+ assert oct(stat.S_IMODE(script_sh.stat().st_mode)) == "0o777"
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/test_windows_wrappers.py b/.venv/lib/python3.12/site-packages/setuptools/tests/test_windows_wrappers.py
new file mode 100644
index 00000000..f8954853
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/test_windows_wrappers.py
@@ -0,0 +1,259 @@
+"""
+Python Script Wrapper for Windows
+=================================
+
+setuptools includes wrappers for Python scripts that allows them to be
+executed like regular windows programs. There are 2 wrappers, one
+for command-line programs, cli.exe, and one for graphical programs,
+gui.exe. These programs are almost identical, function pretty much
+the same way, and are generated from the same source file. The
+wrapper programs are used by copying them to the directory containing
+the script they are to wrap and with the same name as the script they
+are to wrap.
+"""
+
+import pathlib
+import platform
+import subprocess
+import sys
+import textwrap
+
+import pytest
+
+import pkg_resources
+from setuptools.command.easy_install import nt_quote_arg
+
+pytestmark = pytest.mark.skipif(sys.platform != 'win32', reason="Windows only")
+
+
+class WrapperTester:
+ @classmethod
+ def prep_script(cls, template):
+ python_exe = nt_quote_arg(sys.executable)
+ return template % locals()
+
+ @classmethod
+ def create_script(cls, tmpdir):
+ """
+ Create a simple script, foo-script.py
+
+ Note that the script starts with a Unix-style '#!' line saying which
+ Python executable to run. The wrapper will use this line to find the
+ correct Python executable.
+ """
+
+ script = cls.prep_script(cls.script_tmpl)
+
+ with (tmpdir / cls.script_name).open('w') as f:
+ f.write(script)
+
+ # also copy cli.exe to the sample directory
+ with (tmpdir / cls.wrapper_name).open('wb') as f:
+ w = pkg_resources.resource_string('setuptools', cls.wrapper_source)
+ f.write(w)
+
+
+def win_launcher_exe(prefix):
+ """A simple routine to select launcher script based on platform."""
+ assert prefix in ('cli', 'gui')
+ if platform.machine() == "ARM64":
+ return f"{prefix}-arm64.exe"
+ else:
+ return f"{prefix}-32.exe"
+
+
+class TestCLI(WrapperTester):
+ script_name = 'foo-script.py'
+ wrapper_name = 'foo.exe'
+ wrapper_source = win_launcher_exe('cli')
+
+ script_tmpl = textwrap.dedent(
+ """
+ #!%(python_exe)s
+ import sys
+ input = repr(sys.stdin.read())
+ print(sys.argv[0][-14:])
+ print(sys.argv[1:])
+ print(input)
+ if __debug__:
+ print('non-optimized')
+ """
+ ).lstrip()
+
+ def test_basic(self, tmpdir):
+ """
+ When the copy of cli.exe, foo.exe in this example, runs, it examines
+ the path name it was run with and computes a Python script path name
+ by removing the '.exe' suffix and adding the '-script.py' suffix. (For
+ GUI programs, the suffix '-script.pyw' is added.) This is why we
+ named out script the way we did. Now we can run out script by running
+ the wrapper:
+
+ This example was a little pathological in that it exercised windows
+ (MS C runtime) quoting rules:
+
+ - Strings containing spaces are surrounded by double quotes.
+
+ - Double quotes in strings need to be escaped by preceding them with
+ back slashes.
+
+ - One or more backslashes preceding double quotes need to be escaped
+ by preceding each of them with back slashes.
+ """
+ self.create_script(tmpdir)
+ cmd = [
+ str(tmpdir / 'foo.exe'),
+ 'arg1',
+ 'arg 2',
+ 'arg "2\\"',
+ 'arg 4\\',
+ 'arg5 a\\\\b',
+ ]
+ proc = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ text=True,
+ encoding="utf-8",
+ )
+ stdout, _stderr = proc.communicate('hello\nworld\n')
+ actual = stdout.replace('\r\n', '\n')
+ expected = textwrap.dedent(
+ r"""
+ \foo-script.py
+ ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b']
+ 'hello\nworld\n'
+ non-optimized
+ """
+ ).lstrip()
+ assert actual == expected
+
+ def test_symlink(self, tmpdir):
+ """
+ Ensure that symlink for the foo.exe is working correctly.
+ """
+ script_dir = tmpdir / "script_dir"
+ script_dir.mkdir()
+ self.create_script(script_dir)
+ symlink = pathlib.Path(tmpdir / "foo.exe")
+ symlink.symlink_to(script_dir / "foo.exe")
+
+ cmd = [
+ str(tmpdir / 'foo.exe'),
+ 'arg1',
+ 'arg 2',
+ 'arg "2\\"',
+ 'arg 4\\',
+ 'arg5 a\\\\b',
+ ]
+ proc = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ text=True,
+ encoding="utf-8",
+ )
+ stdout, _stderr = proc.communicate('hello\nworld\n')
+ actual = stdout.replace('\r\n', '\n')
+ expected = textwrap.dedent(
+ r"""
+ \foo-script.py
+ ['arg1', 'arg 2', 'arg "2\\"', 'arg 4\\', 'arg5 a\\\\b']
+ 'hello\nworld\n'
+ non-optimized
+ """
+ ).lstrip()
+ assert actual == expected
+
+ def test_with_options(self, tmpdir):
+ """
+ Specifying Python Command-line Options
+ --------------------------------------
+
+ You can specify a single argument on the '#!' line. This can be used
+ to specify Python options like -O, to run in optimized mode or -i
+ to start the interactive interpreter. You can combine multiple
+ options as usual. For example, to run in optimized mode and
+ enter the interpreter after running the script, you could use -Oi:
+ """
+ self.create_script(tmpdir)
+ tmpl = textwrap.dedent(
+ """
+ #!%(python_exe)s -Oi
+ import sys
+ input = repr(sys.stdin.read())
+ print(sys.argv[0][-14:])
+ print(sys.argv[1:])
+ print(input)
+ if __debug__:
+ print('non-optimized')
+ sys.ps1 = '---'
+ """
+ ).lstrip()
+ with (tmpdir / 'foo-script.py').open('w') as f:
+ f.write(self.prep_script(tmpl))
+ cmd = [str(tmpdir / 'foo.exe')]
+ proc = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ encoding="utf-8",
+ )
+ stdout, _stderr = proc.communicate()
+ actual = stdout.replace('\r\n', '\n')
+ expected = textwrap.dedent(
+ r"""
+ \foo-script.py
+ []
+ ''
+ ---
+ """
+ ).lstrip()
+ assert actual == expected
+
+
+class TestGUI(WrapperTester):
+ """
+ Testing the GUI Version
+ -----------------------
+ """
+
+ script_name = 'bar-script.pyw'
+ wrapper_source = win_launcher_exe('gui')
+ wrapper_name = 'bar.exe'
+
+ script_tmpl = textwrap.dedent(
+ """
+ #!%(python_exe)s
+ import sys
+ f = open(sys.argv[1], 'wb')
+ bytes_written = f.write(repr(sys.argv[2]).encode('utf-8'))
+ f.close()
+ """
+ ).strip()
+
+ def test_basic(self, tmpdir):
+ """Test the GUI version with the simple script, bar-script.py"""
+ self.create_script(tmpdir)
+
+ cmd = [
+ str(tmpdir / 'bar.exe'),
+ str(tmpdir / 'test_output.txt'),
+ 'Test Argument',
+ ]
+ proc = subprocess.Popen(
+ cmd,
+ stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ stderr=subprocess.STDOUT,
+ text=True,
+ encoding="utf-8",
+ )
+ stdout, stderr = proc.communicate()
+ assert not stdout
+ assert not stderr
+ with (tmpdir / 'test_output.txt').open('rb') as f_out:
+ actual = f_out.read().decode('ascii')
+ assert actual == repr('Test Argument')
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/text.py b/.venv/lib/python3.12/site-packages/setuptools/tests/text.py
new file mode 100644
index 00000000..e05cc633
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/text.py
@@ -0,0 +1,4 @@
+class Filenames:
+ unicode = 'smörbröd.py'
+ latin_1 = unicode.encode('latin-1')
+ utf_8 = unicode.encode('utf-8')
diff --git a/.venv/lib/python3.12/site-packages/setuptools/tests/textwrap.py b/.venv/lib/python3.12/site-packages/setuptools/tests/textwrap.py
new file mode 100644
index 00000000..5e39618d
--- /dev/null
+++ b/.venv/lib/python3.12/site-packages/setuptools/tests/textwrap.py
@@ -0,0 +1,6 @@
+import textwrap
+
+
+def DALS(s):
+ "dedent and left-strip"
+ return textwrap.dedent(s).lstrip()