juc500 정상동작
This commit is contained in:
vendor/python-windows/Lib/site-packages/setuptools/tests/config/__pycache__/__init__.cpython-311.pyc
Vendored
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
+59
@@ -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()
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
+18
@@ -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))
|
||||
+22
@@ -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
|
||||
Vendored
+794
@@ -0,0 +1,794 @@
|
||||
"""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.IGNORECASE) 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 = "ankit@example.com"},
|
||||
]
|
||||
"""
|
||||
|
||||
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,
|
||||
'Ankit Ahlawat <अंकित@उदाहरण.भारत>',
|
||||
marks=pytest.mark.xfail(
|
||||
reason="CPython's `email.headerregistry.Address` only supports "
|
||||
'RFC 5322, as of Oct 20, 2025 and latest Python 3.13.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 make_dist(self, toml_config):
|
||||
pyproject = Path("pyproject.toml")
|
||||
pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
|
||||
with pytest.warns(pyprojecttoml._ExperimentalConfiguration):
|
||||
return pyprojecttoml.apply_configuration(Distribution({}), pyproject)
|
||||
|
||||
def test_pyproject_sets_attribute(self, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
toml_config = """
|
||||
[project]
|
||||
name = "test"
|
||||
version = "42.0"
|
||||
[tool.setuptools]
|
||||
ext-modules = [
|
||||
{name = "my.ext", sources = ["hello.c", "world.c"]}
|
||||
]
|
||||
"""
|
||||
dist = self.make_dist(toml_config)
|
||||
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"}
|
||||
|
||||
def test_pyproject_define_macros_as_tuples(self, tmp_path, monkeypatch):
|
||||
# https://github.com/pypa/setuptools/issues/4810
|
||||
monkeypatch.chdir(tmp_path)
|
||||
toml_config = """
|
||||
[project]
|
||||
name = "test"
|
||||
version = "42.0"
|
||||
[[tool.setuptools.ext-modules]]
|
||||
name = "my.ext"
|
||||
sources = ["hello.c", "world.c"]
|
||||
define-macros = [["FIRST_SINGLE"], ["SECOND_TWO", "1"]]
|
||||
"""
|
||||
dist = self.make_dist(toml_config)
|
||||
assert isinstance(dist.ext_modules[0].define_macros[0], tuple)
|
||||
assert dist.ext_modules[0].define_macros[0] == ("FIRST_SINGLE",)
|
||||
assert dist.ext_modules[0].define_macros[1] == ("SECOND_TWO", "1")
|
||||
|
||||
|
||||
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.DOTALL)
|
||||
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)
|
||||
+247
@@ -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)
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
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.DOTALL)):
|
||||
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"]
|
||||
|
||||
|
||||
class TestImportNames:
|
||||
EXAMPLES = [
|
||||
'import-names = ["hello", "world"]',
|
||||
'import-namespaces = ["hello", "world"]',
|
||||
'dynamic = ["import-names"]',
|
||||
'dynamic = ["import-namespaces"]',
|
||||
]
|
||||
|
||||
@pytest.mark.parametrize("example", EXAMPLES)
|
||||
def test_not_implemented(self, monkeypatch, tmp_path, example):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
pyproject = Path("pyproject.toml")
|
||||
toml_config = f"""
|
||||
[project]
|
||||
name = 'proj'
|
||||
version = '42'
|
||||
{example}
|
||||
"""
|
||||
pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
|
||||
with pytest.raises(NotImplementedError, match='import-names'):
|
||||
apply_configuration(Distribution({}), pyproject)
|
||||
|
||||
|
||||
@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.MULTILINE | re.DOTALL
|
||||
)
|
||||
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)
|
||||
Vendored
+111
@@ -0,0 +1,111 @@
|
||||
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"
|
||||
|
||||
dist = Distribution({"extras_require": {"hello": ["world"]}})
|
||||
|
||||
with pytest.warns(SetuptoolsWarning, match=".extras_require. overwritten"):
|
||||
dist = apply_configuration(dist, pyproject)
|
||||
|
||||
assert dist.extras_require == {"docs": ["sphinx"]}
|
||||
+987
@@ -0,0 +1,987 @@
|
||||
import configparser
|
||||
import contextlib
|
||||
import inspect
|
||||
import re
|
||||
import sys
|
||||
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.warnings import SetuptoolsDeprecationWarning
|
||||
|
||||
from ..textwrap import DALS
|
||||
|
||||
from distutils.errors import DistutilsFileError, DistutilsOptionError
|
||||
|
||||
IS_PYPY = '__pypy__' in sys.builtin_module_names
|
||||
|
||||
|
||||
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"),
|
||||
[
|
||||
(
|
||||
"Invalid dash-separated key 'author-email' in 'metadata' (setup.cfg)",
|
||||
DALS(
|
||||
"""
|
||||
[metadata]
|
||||
author-email = test@test.com
|
||||
maintainer_email = foo@foo.com
|
||||
"""
|
||||
),
|
||||
{"author-email": "test@test.com"},
|
||||
),
|
||||
(
|
||||
"Invalid uppercase key 'Name' in 'metadata' (setup.cfg)",
|
||||
DALS(
|
||||
"""
|
||||
[metadata]
|
||||
Name = foo
|
||||
description = Some description
|
||||
"""
|
||||
),
|
||||
{"Name": "foo"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_invalid_options_previously_deprecated(
|
||||
self, tmpdir, error_msg, config, invalid
|
||||
):
|
||||
# This test and related methods can be removed when no longer needed.
|
||||
# Deprecation postponed due to push-back from the community in
|
||||
# https://github.com/pypa/setuptools/issues/4910
|
||||
fake_env(tmpdir, config)
|
||||
with pytest.warns(SetuptoolsDeprecationWarning, match=re.escape(error_msg)):
|
||||
dist = get_dist(tmpdir).__enter__()
|
||||
|
||||
tmpdir.join('setup.cfg').remove()
|
||||
|
||||
for field, value in invalid.items():
|
||||
attr = field.replace("-", "_").lower()
|
||||
assert getattr(dist.metadata, attr) == value
|
||||
|
||||
|
||||
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",
|
||||
],
|
||||
)
|
||||
@pytest.mark.xfail(IS_PYPY, reason="Exceptions missing on PyPy")
|
||||
# TODO: investigate PyPy problem
|
||||
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",
|
||||
],
|
||||
)
|
||||
@pytest.mark.xfail(IS_PYPY, reason="Warnings missing on PyPy (minor issue)")
|
||||
# TODO: investigate PyPy problem
|
||||
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/',
|
||||
}
|
||||
Reference in New Issue
Block a user