fix
This commit is contained in:
@@ -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
|
||||
@@ -0,0 +1,512 @@
|
||||
"""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.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 RemovedConfigError
|
||||
|
||||
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})
|
||||
|
||||
|
||||
@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"))
|
||||
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 = {file = "LICENSE.txt"}
|
||||
keywords = ["egg", "bacon", "sausage", "tomatoes", "Lobster Thermidor"]
|
||||
authors = [
|
||||
{email = "hi@pradyunsg.me"},
|
||||
{name = "Tzu-Ping Chung"}
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Brett Cannon", email = "brett@python.org"},
|
||||
{name = "John X. Ãørçeč", email = "john@utf8.org"},
|
||||
{name = "Γαμα קּ 東", email = "gama@utf8.org"},
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Programming Language :: Python"
|
||||
]
|
||||
|
||||
dependencies = [
|
||||
"httpx",
|
||||
"gidgethub[httpx]>4.0.0",
|
||||
"django>2.1; os_name != 'nt'",
|
||||
"django>2.0; os_name == 'nt'"
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
test = [
|
||||
"pytest < 5.0.0",
|
||||
"pytest-cov[all]"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "http://example.com"
|
||||
documentation = "http://readthedocs.org"
|
||||
repository = "http://github.com"
|
||||
changelog = "http://github.com/me/spam/blob/master/CHANGELOG.md"
|
||||
|
||||
[project.scripts]
|
||||
spam-cli = "spam:main_cli"
|
||||
|
||||
[project.gui-scripts]
|
||||
spam-gui = "spam:main_gui"
|
||||
|
||||
[project.entry-points."spam.magical"]
|
||||
tomatoes = "spam:main_tomatoes"
|
||||
"""
|
||||
|
||||
PEP621_INTERNATIONAL_EMAIL_EXAMPLE = """\
|
||||
[project]
|
||||
name = "spam"
|
||||
version = "2020.0.0"
|
||||
authors = [
|
||||
{email = "hi@pradyunsg.me"},
|
||||
{name = "Tzu-Ping Chung"}
|
||||
]
|
||||
maintainers = [
|
||||
{name = "Степан Бандера", email = "криївка@оун-упа.укр"},
|
||||
]
|
||||
"""
|
||||
|
||||
PEP621_EXAMPLE_SCRIPT = """
|
||||
def main_cli(): pass
|
||||
def main_gui(): pass
|
||||
def main_tomatoes(): pass
|
||||
"""
|
||||
|
||||
|
||||
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 dist.metadata.license == "--- LICENSE stub ---"
|
||||
assert set(dist.metadata.license_files) == {"LICENSE.txt"}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("readme", "ctype"),
|
||||
[
|
||||
("Readme.txt", "text/plain"),
|
||||
("readme.md", "text/markdown"),
|
||||
("text.rst", "text/x-rst"),
|
||||
],
|
||||
)
|
||||
def test_readme_content_type(tmp_path, readme, ctype):
|
||||
pyproject = _pep621_example_project(tmp_path, readme)
|
||||
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
|
||||
assert dist.metadata.long_description_content_type == ctype
|
||||
|
||||
|
||||
def test_undefined_content_type(tmp_path):
|
||||
pyproject = _pep621_example_project(tmp_path, "README.tex")
|
||||
with pytest.raises(ValueError, match="Undefined content type for README.tex"):
|
||||
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
|
||||
|
||||
|
||||
def test_no_explicit_content_type_for_missing_extension(tmp_path):
|
||||
pyproject = _pep621_example_project(tmp_path, "README")
|
||||
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
|
||||
assert dist.metadata.long_description_content_type is None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("pyproject_text", "expected_maintainers_meta_value"),
|
||||
(
|
||||
pytest.param(
|
||||
PEP621_EXAMPLE,
|
||||
(
|
||||
'Brett Cannon <brett@python.org>, "John X. Ãørçeč" <john@utf8.org>, '
|
||||
'Γαμα קּ 東 <gama@utf8.org>'
|
||||
),
|
||||
id='non-international-emails',
|
||||
),
|
||||
pytest.param(
|
||||
PEP621_INTERNATIONAL_EMAIL_EXAMPLE,
|
||||
'Степан Бандера <криївка@оун-упа.укр>',
|
||||
marks=pytest.mark.xfail(
|
||||
reason="CPython's `email.headerregistry.Address` only supports "
|
||||
'RFC 5322, as of Nov 10, 2022 and latest Python 3.11.0',
|
||||
strict=True,
|
||||
),
|
||||
id='international-email',
|
||||
),
|
||||
),
|
||||
)
|
||||
def test_utf8_maintainer_in_metadata( # issue-3663
|
||||
expected_maintainers_meta_value,
|
||||
pyproject_text,
|
||||
tmp_path,
|
||||
):
|
||||
pyproject = _pep621_example_project(
|
||||
tmp_path,
|
||||
"README",
|
||||
pyproject_text=pyproject_text,
|
||||
)
|
||||
dist = pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
|
||||
assert dist.metadata.maintainer_email == expected_maintainers_meta_value
|
||||
pkg_file = tmp_path / "PKG-FILE"
|
||||
with open(pkg_file, "w", encoding="utf-8") as fh:
|
||||
dist.metadata.write_pkg_file(fh)
|
||||
content = pkg_file.read_text(encoding="utf-8")
|
||||
assert f"Maintainer-email: {expected_maintainers_meta_value}" in content
|
||||
|
||||
|
||||
class TestLicenseFiles:
|
||||
# TODO: After PEP 639 is accepted, we have to move the license-files
|
||||
# to the `project` table instead of `tool.setuptools`
|
||||
|
||||
def base_pyproject(self, tmp_path, additional_text):
|
||||
pyproject = _pep621_example_project(tmp_path, "README")
|
||||
text = pyproject.read_text(encoding="utf-8")
|
||||
|
||||
# Sanity-check
|
||||
assert 'license = {file = "LICENSE.txt"}' in text
|
||||
assert "[tool.setuptools]" not in text
|
||||
|
||||
text = f"{text}\n{additional_text}\n"
|
||||
pyproject.write_text(text, encoding="utf-8")
|
||||
return pyproject
|
||||
|
||||
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")
|
||||
|
||||
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_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_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"}
|
||||
|
||||
|
||||
class TestPyModules:
|
||||
# https://github.com/pypa/setuptools/issues/4316
|
||||
|
||||
def dist(self, name):
|
||||
toml_config = f"""
|
||||
[project]
|
||||
name = "test"
|
||||
version = "42.0"
|
||||
[tool.setuptools]
|
||||
py-modules = [{name!r}]
|
||||
"""
|
||||
pyproject = Path("pyproject.toml")
|
||||
pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
|
||||
return pyprojecttoml.apply_configuration(Distribution({}), pyproject)
|
||||
|
||||
@pytest.mark.parametrize("module", ["pip-run", "abc-d.λ-xyz-e"])
|
||||
def test_valid_module_name(self, tmp_path, monkeypatch, module):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
assert module in self.dist(module).py_modules
|
||||
|
||||
@pytest.mark.parametrize("module", ["pip run", "-pip-run", "pip-run-stubs"])
|
||||
def test_invalid_module_name(self, tmp_path, monkeypatch, module):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
with pytest.raises(ValueError, match="py-modules"):
|
||||
self.dist(module).py_modules
|
||||
|
||||
|
||||
class TestExtModules:
|
||||
def test_pyproject_sets_attribute(self, tmp_path, monkeypatch):
|
||||
monkeypatch.chdir(tmp_path)
|
||||
pyproject = Path("pyproject.toml")
|
||||
toml_config = """
|
||||
[project]
|
||||
name = "test"
|
||||
version = "42.0"
|
||||
[tool.setuptools]
|
||||
ext-modules = [
|
||||
{name = "my.ext", sources = ["hello.c", "world.c"]}
|
||||
]
|
||||
"""
|
||||
pyproject.write_text(cleandoc(toml_config), encoding="utf-8")
|
||||
with pytest.warns(pyprojecttoml._ExperimentalConfiguration):
|
||||
dist = pyprojecttoml.apply_configuration(Distribution({}), pyproject)
|
||||
assert len(dist.ext_modules) == 1
|
||||
assert dist.ext_modules[0].name == "my.ext"
|
||||
assert set(dist.ext_modules[0].sources) == {"hello.c", "world.c"}
|
||||
|
||||
|
||||
class TestDeprecatedFields:
|
||||
def test_namespace_packages(self, tmp_path):
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
config = """
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = "42"
|
||||
[tool.setuptools]
|
||||
namespace-packages = ["myproj.pkg"]
|
||||
"""
|
||||
pyproject.write_text(cleandoc(config), encoding="utf-8")
|
||||
with pytest.raises(RemovedConfigError, match="namespace-packages"):
|
||||
pyprojecttoml.apply_configuration(makedist(tmp_path), pyproject)
|
||||
|
||||
|
||||
class TestPresetField:
|
||||
def pyproject(self, tmp_path, dynamic, extra_content=""):
|
||||
content = f"[project]\nname = 'proj'\ndynamic = {dynamic!r}\n"
|
||||
if "version" not in dynamic:
|
||||
content += "version = '42'\n"
|
||||
file = tmp_path / "pyproject.toml"
|
||||
file.write_text(content + extra_content, encoding="utf-8")
|
||||
return file
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("attr", "field", "value"),
|
||||
[
|
||||
("classifiers", "classifiers", ["Private :: Classifier"]),
|
||||
("entry_points", "scripts", {"console_scripts": ["foobar=foobar:main"]}),
|
||||
("entry_points", "gui-scripts", {"gui_scripts": ["bazquux=bazquux:main"]}),
|
||||
pytest.param(
|
||||
*("install_requires", "dependencies", ["six"]),
|
||||
marks=[
|
||||
pytest.mark.filterwarnings("ignore:.*install_requires. overwritten")
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_not_listed_in_dynamic(self, tmp_path, attr, field, value):
|
||||
"""Setuptools cannot set a field if not listed in ``dynamic``"""
|
||||
pyproject = self.pyproject(tmp_path, [])
|
||||
dist = makedist(tmp_path, **{attr: value})
|
||||
msg = re.compile(f"defined outside of `pyproject.toml`:.*{field}", re.S)
|
||||
with pytest.warns(_MissingDynamic, match=msg):
|
||||
dist = pyprojecttoml.apply_configuration(dist, pyproject)
|
||||
|
||||
dist_value = _some_attrgetter(f"metadata.{attr}", attr)(dist)
|
||||
assert not dist_value
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("attr", "field", "value"),
|
||||
[
|
||||
("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_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
|
||||
|
||||
|
||||
# --- 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)
|
||||
@@ -0,0 +1,221 @@
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
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
|
||||
assert expand.read_attr('pkg.sub.VERSION') == '0.1.1'
|
||||
values = expand.read_attr('lib.mod.VALUES', {'lib': 'pkg/sub'})
|
||||
|
||||
assert values['a'] == 0
|
||||
assert values['b'] == {42}
|
||||
|
||||
# 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
|
||||
assert expand.read_attr('pkg.sub.VERSION', root_dir=tmp_path) == '0.1.1'
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,396 @@
|
||||
import re
|
||||
from configparser import ConfigParser
|
||||
from inspect import cleandoc
|
||||
|
||||
import jaraco.path
|
||||
import pytest
|
||||
import tomli_w
|
||||
from path import Path
|
||||
|
||||
import setuptools # noqa: F401 # force distutils.core to be patched
|
||||
from setuptools.config.pyprojecttoml import (
|
||||
_ToolsTypoInMetadata,
|
||||
apply_configuration,
|
||||
expand_configuration,
|
||||
read_configuration,
|
||||
validate,
|
||||
)
|
||||
from setuptools.dist import Distribution
|
||||
from setuptools.errors import OptionError
|
||||
|
||||
import distutils.core
|
||||
|
||||
EXAMPLE = """
|
||||
[project]
|
||||
name = "myproj"
|
||||
keywords = ["some", "key", "words"]
|
||||
dynamic = ["version", "readme"]
|
||||
requires-python = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||
dependencies = [
|
||||
'importlib-metadata>=0.12;python_version<"3.8"',
|
||||
'importlib-resources>=1.0;python_version<"3.7"',
|
||||
'pathlib2>=2.3.3,<3;python_version < "3.4" and sys.platform != "win32"',
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
docs = [
|
||||
"sphinx>=3",
|
||||
"sphinx-argparse>=0.2.5",
|
||||
"sphinx-rtd-theme>=0.4.3",
|
||||
]
|
||||
testing = [
|
||||
"pytest>=1",
|
||||
"coverage>=3,<5",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
exec = "pkg.__main__:exec"
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools]
|
||||
package-dir = {"" = "src"}
|
||||
zip-safe = true
|
||||
platforms = ["any"]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["src"]
|
||||
|
||||
[tool.setuptools.cmdclass]
|
||||
sdist = "pkg.mod.CustomSdist"
|
||||
|
||||
[tool.setuptools.dynamic.version]
|
||||
attr = "pkg.__version__.VERSION"
|
||||
|
||||
[tool.setuptools.dynamic.readme]
|
||||
file = ["README.md"]
|
||||
content-type = "text/markdown"
|
||||
|
||||
[tool.setuptools.package-data]
|
||||
"*" = ["*.txt"]
|
||||
|
||||
[tool.setuptools.data-files]
|
||||
"data" = ["_files/*.txt"]
|
||||
|
||||
[tool.distutils.sdist]
|
||||
formats = "gztar"
|
||||
|
||||
[tool.distutils.bdist_wheel]
|
||||
universal = true
|
||||
"""
|
||||
|
||||
|
||||
def create_example(path, pkg_root):
|
||||
files = {
|
||||
"pyproject.toml": EXAMPLE,
|
||||
"README.md": "hello world",
|
||||
"_files": {
|
||||
"file.txt": "",
|
||||
},
|
||||
}
|
||||
packages = {
|
||||
"pkg": {
|
||||
"__init__.py": "",
|
||||
"mod.py": "class CustomSdist: pass",
|
||||
"__version__.py": "VERSION = (3, 10)",
|
||||
"__main__.py": "def exec(): print('hello')",
|
||||
},
|
||||
}
|
||||
|
||||
assert pkg_root # Meta-test: cannot be empty string.
|
||||
|
||||
if pkg_root == ".":
|
||||
files = {**files, **packages}
|
||||
# skip other files: flat-layout will raise error for multi-package dist
|
||||
else:
|
||||
# Use this opportunity to ensure namespaces are discovered
|
||||
files[pkg_root] = {**packages, "other": {"nested": {"__init__.py": ""}}}
|
||||
|
||||
jaraco.path.build(files, prefix=path)
|
||||
|
||||
|
||||
def verify_example(config, path, pkg_root):
|
||||
pyproject = path / "pyproject.toml"
|
||||
pyproject.write_text(tomli_w.dumps(config), encoding="utf-8")
|
||||
expanded = expand_configuration(config, path)
|
||||
expanded_project = expanded["project"]
|
||||
assert read_configuration(pyproject, expand=True) == expanded
|
||||
assert expanded_project["version"] == "3.10"
|
||||
assert expanded_project["readme"]["text"] == "hello world"
|
||||
assert "packages" in expanded["tool"]["setuptools"]
|
||||
if pkg_root == ".":
|
||||
# Auto-discovery will raise error for multi-package dist
|
||||
assert set(expanded["tool"]["setuptools"]["packages"]) == {"pkg"}
|
||||
else:
|
||||
assert set(expanded["tool"]["setuptools"]["packages"]) == {
|
||||
"pkg",
|
||||
"other",
|
||||
"other.nested",
|
||||
}
|
||||
assert expanded["tool"]["setuptools"]["include-package-data"] is True
|
||||
assert "" in expanded["tool"]["setuptools"]["package-data"]
|
||||
assert "*" not in expanded["tool"]["setuptools"]["package-data"]
|
||||
assert expanded["tool"]["setuptools"]["data-files"] == [
|
||||
("data", ["_files/file.txt"])
|
||||
]
|
||||
|
||||
|
||||
def test_read_configuration(tmp_path):
|
||||
create_example(tmp_path, "src")
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
|
||||
config = read_configuration(pyproject, expand=False)
|
||||
assert config["project"].get("version") is None
|
||||
assert config["project"].get("readme") is None
|
||||
|
||||
verify_example(config, tmp_path, "src")
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("pkg_root", "opts"),
|
||||
[
|
||||
(".", {}),
|
||||
("src", {}),
|
||||
("lib", {"packages": {"find": {"where": ["lib"]}}}),
|
||||
],
|
||||
)
|
||||
def test_discovered_package_dir_with_attr_directive_in_config(tmp_path, pkg_root, opts):
|
||||
create_example(tmp_path, pkg_root)
|
||||
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
|
||||
config = read_configuration(pyproject, expand=False)
|
||||
assert config["project"].get("version") is None
|
||||
assert config["project"].get("readme") is None
|
||||
config["tool"]["setuptools"].pop("packages", None)
|
||||
config["tool"]["setuptools"].pop("package-dir", None)
|
||||
|
||||
config["tool"]["setuptools"].update(opts)
|
||||
verify_example(config, tmp_path, pkg_root)
|
||||
|
||||
|
||||
ENTRY_POINTS = {
|
||||
"console_scripts": {"a": "mod.a:func"},
|
||||
"gui_scripts": {"b": "mod.b:func"},
|
||||
"other": {"c": "mod.c:func [extra]"},
|
||||
}
|
||||
|
||||
|
||||
class TestEntryPoints:
|
||||
def write_entry_points(self, tmp_path):
|
||||
entry_points = ConfigParser()
|
||||
entry_points.read_dict(ENTRY_POINTS)
|
||||
with open(tmp_path / "entry-points.txt", "w", encoding="utf-8") as f:
|
||||
entry_points.write(f)
|
||||
|
||||
def pyproject(self, dynamic=None):
|
||||
project = {"dynamic": dynamic or ["scripts", "gui-scripts", "entry-points"]}
|
||||
tool = {"dynamic": {"entry-points": {"file": "entry-points.txt"}}}
|
||||
return {"project": project, "tool": {"setuptools": tool}}
|
||||
|
||||
def test_all_listed_in_dynamic(self, tmp_path):
|
||||
self.write_entry_points(tmp_path)
|
||||
expanded = expand_configuration(self.pyproject(), tmp_path)
|
||||
expanded_project = expanded["project"]
|
||||
assert len(expanded_project["scripts"]) == 1
|
||||
assert expanded_project["scripts"]["a"] == "mod.a:func"
|
||||
assert len(expanded_project["gui-scripts"]) == 1
|
||||
assert expanded_project["gui-scripts"]["b"] == "mod.b:func"
|
||||
assert len(expanded_project["entry-points"]) == 1
|
||||
assert expanded_project["entry-points"]["other"]["c"] == "mod.c:func [extra]"
|
||||
|
||||
@pytest.mark.parametrize("missing_dynamic", ("scripts", "gui-scripts"))
|
||||
def test_scripts_not_listed_in_dynamic(self, tmp_path, missing_dynamic):
|
||||
self.write_entry_points(tmp_path)
|
||||
dynamic = {"scripts", "gui-scripts", "entry-points"} - {missing_dynamic}
|
||||
|
||||
msg = f"defined outside of `pyproject.toml`:.*{missing_dynamic}"
|
||||
with pytest.raises(OptionError, match=re.compile(msg, re.S)):
|
||||
expand_configuration(self.pyproject(dynamic), tmp_path)
|
||||
|
||||
|
||||
class TestClassifiers:
|
||||
def test_dynamic(self, tmp_path):
|
||||
# Let's create a project example that has dynamic classifiers
|
||||
# coming from a txt file.
|
||||
create_example(tmp_path, "src")
|
||||
classifiers = cleandoc(
|
||||
"""
|
||||
Framework :: Flask
|
||||
Programming Language :: Haskell
|
||||
"""
|
||||
)
|
||||
(tmp_path / "classifiers.txt").write_text(classifiers, encoding="utf-8")
|
||||
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
config = read_configuration(pyproject, expand=False)
|
||||
dynamic = config["project"]["dynamic"]
|
||||
config["project"]["dynamic"] = list({*dynamic, "classifiers"})
|
||||
dynamic_config = config["tool"]["setuptools"]["dynamic"]
|
||||
dynamic_config["classifiers"] = {"file": "classifiers.txt"}
|
||||
|
||||
# When the configuration is expanded,
|
||||
# each line of the file should be an different classifier.
|
||||
validate(config, pyproject)
|
||||
expanded = expand_configuration(config, tmp_path)
|
||||
|
||||
assert set(expanded["project"]["classifiers"]) == {
|
||||
"Framework :: Flask",
|
||||
"Programming Language :: Haskell",
|
||||
}
|
||||
|
||||
def test_dynamic_without_config(self, tmp_path):
|
||||
config = """
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = '42'
|
||||
dynamic = ["classifiers"]
|
||||
"""
|
||||
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(cleandoc(config), encoding="utf-8")
|
||||
with pytest.raises(OptionError, match="No configuration .* .classifiers."):
|
||||
read_configuration(pyproject)
|
||||
|
||||
def test_dynamic_readme_from_setup_script_args(self, tmp_path):
|
||||
config = """
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = '42'
|
||||
dynamic = ["readme"]
|
||||
"""
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(cleandoc(config), encoding="utf-8")
|
||||
dist = Distribution(attrs={"long_description": "42"})
|
||||
# No error should occur because of missing `readme`
|
||||
dist = apply_configuration(dist, pyproject)
|
||||
assert dist.metadata.long_description == "42"
|
||||
|
||||
def test_dynamic_without_file(self, tmp_path):
|
||||
config = """
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = '42'
|
||||
dynamic = ["classifiers"]
|
||||
|
||||
[tool.setuptools.dynamic]
|
||||
classifiers = {file = ["classifiers.txt"]}
|
||||
"""
|
||||
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(cleandoc(config), encoding="utf-8")
|
||||
with pytest.warns(UserWarning, match="File .*classifiers.txt. cannot be found"):
|
||||
expanded = read_configuration(pyproject)
|
||||
assert "classifiers" not in expanded["project"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"example",
|
||||
(
|
||||
"""
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = "1.2"
|
||||
|
||||
[my-tool.that-disrespect.pep518]
|
||||
value = 42
|
||||
""",
|
||||
),
|
||||
)
|
||||
def test_ignore_unrelated_config(tmp_path, example):
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(cleandoc(example), encoding="utf-8")
|
||||
|
||||
# Make sure no error is raised due to 3rd party configs in pyproject.toml
|
||||
assert read_configuration(pyproject) is not None
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("example", "error_msg"),
|
||||
[
|
||||
(
|
||||
"""
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = "1.2"
|
||||
requires = ['pywin32; platform_system=="Windows"' ]
|
||||
""",
|
||||
"configuration error: .project. must not contain ..requires.. properties",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_invalid_example(tmp_path, example, error_msg):
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(cleandoc(example), encoding="utf-8")
|
||||
|
||||
pattern = re.compile(f"invalid pyproject.toml.*{error_msg}.*", re.M | re.S)
|
||||
with pytest.raises(ValueError, match=pattern):
|
||||
read_configuration(pyproject)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", ("", "[tool.something]\nvalue = 42"))
|
||||
def test_empty(tmp_path, config):
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(config, encoding="utf-8")
|
||||
|
||||
# Make sure no error is raised
|
||||
assert read_configuration(pyproject) == {}
|
||||
|
||||
|
||||
@pytest.mark.parametrize("config", ("[project]\nname = 'myproj'\nversion='42'\n",))
|
||||
def test_include_package_data_by_default(tmp_path, config):
|
||||
"""Builds with ``pyproject.toml`` should consider ``include-package-data=True`` as
|
||||
default.
|
||||
"""
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(config, encoding="utf-8")
|
||||
|
||||
config = read_configuration(pyproject)
|
||||
assert config["tool"]["setuptools"]["include-package-data"] is True
|
||||
|
||||
|
||||
def test_include_package_data_in_setuppy(tmp_path):
|
||||
"""Builds with ``pyproject.toml`` should consider ``include_package_data`` set in
|
||||
``setup.py``.
|
||||
|
||||
See https://github.com/pypa/setuptools/issues/3197#issuecomment-1079023889
|
||||
"""
|
||||
files = {
|
||||
"pyproject.toml": "[project]\nname = 'myproj'\nversion='42'\n",
|
||||
"setup.py": "__import__('setuptools').setup(include_package_data=False)",
|
||||
}
|
||||
jaraco.path.build(files, prefix=tmp_path)
|
||||
|
||||
with Path(tmp_path):
|
||||
dist = distutils.core.run_setup("setup.py", {}, stop_after="config")
|
||||
|
||||
assert dist.get_name() == "myproj"
|
||||
assert dist.get_version() == "42"
|
||||
assert dist.include_package_data is False
|
||||
|
||||
|
||||
def test_warn_tools_typo(tmp_path):
|
||||
"""Test that the common ``tools.setuptools`` typo in ``pyproject.toml`` issues a warning
|
||||
|
||||
See https://github.com/pypa/setuptools/issues/4150
|
||||
"""
|
||||
config = """
|
||||
[build-system]
|
||||
requires = ["setuptools"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = '42'
|
||||
|
||||
[tools.setuptools]
|
||||
packages = ["package"]
|
||||
"""
|
||||
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
pyproject.write_text(cleandoc(config), encoding="utf-8")
|
||||
|
||||
with pytest.warns(_ToolsTypoInMetadata):
|
||||
read_configuration(pyproject)
|
||||
@@ -0,0 +1,109 @@
|
||||
from inspect import cleandoc
|
||||
|
||||
import pytest
|
||||
from jaraco import path
|
||||
|
||||
from setuptools.config.pyprojecttoml import apply_configuration
|
||||
from setuptools.dist import Distribution
|
||||
from setuptools.warnings import SetuptoolsWarning
|
||||
|
||||
|
||||
def test_dynamic_dependencies(tmp_path):
|
||||
files = {
|
||||
"requirements.txt": "six\n # comment\n",
|
||||
"pyproject.toml": cleandoc(
|
||||
"""
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = "1.0"
|
||||
dynamic = ["dependencies"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools.dynamic.dependencies]
|
||||
file = ["requirements.txt"]
|
||||
"""
|
||||
),
|
||||
}
|
||||
path.build(files, prefix=tmp_path)
|
||||
dist = Distribution()
|
||||
dist = apply_configuration(dist, tmp_path / "pyproject.toml")
|
||||
assert dist.install_requires == ["six"]
|
||||
|
||||
|
||||
def test_dynamic_optional_dependencies(tmp_path):
|
||||
files = {
|
||||
"requirements-docs.txt": "sphinx\n # comment\n",
|
||||
"pyproject.toml": cleandoc(
|
||||
"""
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = "1.0"
|
||||
dynamic = ["optional-dependencies"]
|
||||
|
||||
[tool.setuptools.dynamic.optional-dependencies.docs]
|
||||
file = ["requirements-docs.txt"]
|
||||
|
||||
[build-system]
|
||||
requires = ["setuptools", "wheel"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
"""
|
||||
),
|
||||
}
|
||||
path.build(files, prefix=tmp_path)
|
||||
dist = Distribution()
|
||||
dist = apply_configuration(dist, tmp_path / "pyproject.toml")
|
||||
assert dist.extras_require == {"docs": ["sphinx"]}
|
||||
|
||||
|
||||
def test_mixed_dynamic_optional_dependencies(tmp_path):
|
||||
"""
|
||||
Test that if PEP 621 was loosened to allow mixing of dynamic and static
|
||||
configurations in the case of fields containing sub-fields (groups),
|
||||
things would work out.
|
||||
"""
|
||||
files = {
|
||||
"requirements-images.txt": "pillow~=42.0\n # comment\n",
|
||||
"pyproject.toml": cleandoc(
|
||||
"""
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = "1.0"
|
||||
dynamic = ["optional-dependencies"]
|
||||
|
||||
[project.optional-dependencies]
|
||||
docs = ["sphinx"]
|
||||
|
||||
[tool.setuptools.dynamic.optional-dependencies.images]
|
||||
file = ["requirements-images.txt"]
|
||||
"""
|
||||
),
|
||||
}
|
||||
|
||||
path.build(files, prefix=tmp_path)
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
with pytest.raises(ValueError, match="project.optional-dependencies"):
|
||||
apply_configuration(Distribution(), pyproject)
|
||||
|
||||
|
||||
def test_mixed_extras_require_optional_dependencies(tmp_path):
|
||||
files = {
|
||||
"pyproject.toml": cleandoc(
|
||||
"""
|
||||
[project]
|
||||
name = "myproj"
|
||||
version = "1.0"
|
||||
optional-dependencies.docs = ["sphinx"]
|
||||
"""
|
||||
),
|
||||
}
|
||||
|
||||
path.build(files, prefix=tmp_path)
|
||||
pyproject = tmp_path / "pyproject.toml"
|
||||
|
||||
with pytest.warns(SetuptoolsWarning, match=".extras_require. overwritten"):
|
||||
dist = Distribution({"extras_require": {"hello": ["world"]}})
|
||||
dist = apply_configuration(dist, pyproject)
|
||||
assert dist.extras_require == {"docs": ["sphinx"]}
|
||||
@@ -0,0 +1,965 @@
|
||||
import configparser
|
||||
import contextlib
|
||||
import inspect
|
||||
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
|
||||
|
||||
|
||||
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('%s' % 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('%s' % 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('%s' % config)
|
||||
|
||||
config_dict = read_configuration('%s' % 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
|
||||
|
||||
def test_warn_dash_deprecation(self, tmpdir):
|
||||
# warn_dash_deprecation() is a method in setuptools.dist
|
||||
# remove this test and the method when no longer needed
|
||||
fake_env(
|
||||
tmpdir,
|
||||
'[metadata]\n'
|
||||
'author-email = test@test.com\n'
|
||||
'maintainer_email = foo@foo.com\n',
|
||||
)
|
||||
msg = "Usage of dash-separated 'author-email' will not be supported"
|
||||
with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
|
||||
with get_dist(tmpdir) as dist:
|
||||
metadata = dist.metadata
|
||||
|
||||
assert metadata.author_email == 'test@test.com'
|
||||
assert metadata.maintainer_email == 'foo@foo.com'
|
||||
|
||||
def test_make_option_lowercase(self, tmpdir):
|
||||
# remove this test and the method make_option_lowercase() in setuptools.dist
|
||||
# when no longer needed
|
||||
fake_env(tmpdir, '[metadata]\nName = foo\ndescription = Some description\n')
|
||||
msg = "Usage of uppercase key 'Name' in 'metadata' will not be supported"
|
||||
with pytest.warns(SetuptoolsDeprecationWarning, match=msg):
|
||||
with get_dist(tmpdir) as dist:
|
||||
metadata = dist.metadata
|
||||
|
||||
assert metadata.name == 'foo'
|
||||
assert metadata.description == 'Some description'
|
||||
|
||||
|
||||
class TestOptions:
|
||||
def test_basic(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
'[options]\n'
|
||||
'zip_safe = True\n'
|
||||
'include_package_data = yes\n'
|
||||
'package_dir = b=c, =src\n'
|
||||
'packages = pack_a, pack_b.subpack\n'
|
||||
'namespace_packages = pack1, pack2\n'
|
||||
'scripts = bin/one.py, bin/two.py\n'
|
||||
'eager_resources = bin/one.py, bin/two.py\n'
|
||||
'install_requires = docutils>=0.3; pack ==1.1, ==1.3; hey\n'
|
||||
'setup_requires = docutils>=0.3; spack ==1.1, ==1.3; there\n'
|
||||
'dependency_links = http://some.com/here/1, '
|
||||
'http://some.com/there/2\n'
|
||||
'python_requires = >=1.0, !=2.8\n'
|
||||
'py_modules = module1, module2\n',
|
||||
)
|
||||
deprec = pytest.warns(SetuptoolsDeprecationWarning, match="namespace_packages")
|
||||
with deprec, get_dist(tmpdir) as dist:
|
||||
assert dist.zip_safe
|
||||
assert dist.include_package_data
|
||||
assert dist.package_dir == {'': 'src', 'b': 'c'}
|
||||
assert dist.packages == ['pack_a', 'pack_b.subpack']
|
||||
assert dist.namespace_packages == ['pack1', 'pack2']
|
||||
assert dist.scripts == ['bin/one.py', 'bin/two.py']
|
||||
assert dist.dependency_links == ([
|
||||
'http://some.com/here/1',
|
||||
'http://some.com/there/2',
|
||||
])
|
||||
assert dist.install_requires == ([
|
||||
'docutils>=0.3',
|
||||
'pack==1.1,==1.3',
|
||||
'hey',
|
||||
])
|
||||
assert dist.setup_requires == ([
|
||||
'docutils>=0.3',
|
||||
'spack ==1.1, ==1.3',
|
||||
'there',
|
||||
])
|
||||
assert dist.python_requires == '>=1.0, !=2.8'
|
||||
assert dist.py_modules == ['module1', 'module2']
|
||||
|
||||
def test_multiline(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
'[options]\n'
|
||||
'package_dir = \n'
|
||||
' b=c\n'
|
||||
' =src\n'
|
||||
'packages = \n'
|
||||
' pack_a\n'
|
||||
' pack_b.subpack\n'
|
||||
'namespace_packages = \n'
|
||||
' pack1\n'
|
||||
' pack2\n'
|
||||
'scripts = \n'
|
||||
' bin/one.py\n'
|
||||
' bin/two.py\n'
|
||||
'eager_resources = \n'
|
||||
' bin/one.py\n'
|
||||
' bin/two.py\n'
|
||||
'install_requires = \n'
|
||||
' docutils>=0.3\n'
|
||||
' pack ==1.1, ==1.3\n'
|
||||
' hey\n'
|
||||
'setup_requires = \n'
|
||||
' docutils>=0.3\n'
|
||||
' spack ==1.1, ==1.3\n'
|
||||
' there\n'
|
||||
'dependency_links = \n'
|
||||
' http://some.com/here/1\n'
|
||||
' http://some.com/there/2\n',
|
||||
)
|
||||
deprec = pytest.warns(SetuptoolsDeprecationWarning, match="namespace_packages")
|
||||
with deprec, get_dist(tmpdir) as dist:
|
||||
assert dist.package_dir == {'': 'src', 'b': 'c'}
|
||||
assert dist.packages == ['pack_a', 'pack_b.subpack']
|
||||
assert dist.namespace_packages == ['pack1', 'pack2']
|
||||
assert dist.scripts == ['bin/one.py', 'bin/two.py']
|
||||
assert dist.dependency_links == ([
|
||||
'http://some.com/here/1',
|
||||
'http://some.com/there/2',
|
||||
])
|
||||
assert dist.install_requires == ([
|
||||
'docutils>=0.3',
|
||||
'pack==1.1,==1.3',
|
||||
'hey',
|
||||
])
|
||||
assert dist.setup_requires == ([
|
||||
'docutils>=0.3',
|
||||
'spack ==1.1, ==1.3',
|
||||
'there',
|
||||
])
|
||||
|
||||
def test_package_dir_fail(self, tmpdir):
|
||||
fake_env(tmpdir, '[options]\npackage_dir = a b\n')
|
||||
with get_dist(tmpdir, parse=False) as dist:
|
||||
with pytest.raises(DistutilsOptionError):
|
||||
dist.parse_config_files()
|
||||
|
||||
def test_package_data(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
'[options.package_data]\n'
|
||||
'* = *.txt, *.rst\n'
|
||||
'hello = *.msg\n'
|
||||
'\n'
|
||||
'[options.exclude_package_data]\n'
|
||||
'* = fake1.txt, fake2.txt\n'
|
||||
'hello = *.dat\n',
|
||||
)
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.package_data == {
|
||||
'': ['*.txt', '*.rst'],
|
||||
'hello': ['*.msg'],
|
||||
}
|
||||
assert dist.exclude_package_data == {
|
||||
'': ['fake1.txt', 'fake2.txt'],
|
||||
'hello': ['*.dat'],
|
||||
}
|
||||
|
||||
def test_packages(self, tmpdir):
|
||||
fake_env(tmpdir, '[options]\npackages = find:\n')
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.packages == ['fake_package']
|
||||
|
||||
def test_find_directive(self, tmpdir):
|
||||
dir_package, config = fake_env(tmpdir, '[options]\npackages = find:\n')
|
||||
|
||||
make_package_dir('sub_one', dir_package)
|
||||
make_package_dir('sub_two', dir_package)
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert set(dist.packages) == set([
|
||||
'fake_package',
|
||||
'fake_package.sub_two',
|
||||
'fake_package.sub_one',
|
||||
])
|
||||
|
||||
config.write(
|
||||
'[options]\n'
|
||||
'packages = find:\n'
|
||||
'\n'
|
||||
'[options.packages.find]\n'
|
||||
'where = .\n'
|
||||
'include =\n'
|
||||
' fake_package.sub_one\n'
|
||||
' two\n'
|
||||
)
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.packages == ['fake_package.sub_one']
|
||||
|
||||
config.write(
|
||||
'[options]\n'
|
||||
'packages = find:\n'
|
||||
'\n'
|
||||
'[options.packages.find]\n'
|
||||
'exclude =\n'
|
||||
' fake_package.sub_one\n'
|
||||
)
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert set(dist.packages) == set(['fake_package', 'fake_package.sub_two'])
|
||||
|
||||
def test_find_namespace_directive(self, tmpdir):
|
||||
dir_package, config = fake_env(
|
||||
tmpdir, '[options]\npackages = find_namespace:\n'
|
||||
)
|
||||
|
||||
make_package_dir('sub_one', dir_package)
|
||||
make_package_dir('sub_two', dir_package, ns=True)
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert set(dist.packages) == {
|
||||
'fake_package',
|
||||
'fake_package.sub_two',
|
||||
'fake_package.sub_one',
|
||||
}
|
||||
|
||||
config.write(
|
||||
'[options]\n'
|
||||
'packages = find_namespace:\n'
|
||||
'\n'
|
||||
'[options.packages.find]\n'
|
||||
'where = .\n'
|
||||
'include =\n'
|
||||
' fake_package.sub_one\n'
|
||||
' two\n'
|
||||
)
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.packages == ['fake_package.sub_one']
|
||||
|
||||
config.write(
|
||||
'[options]\n'
|
||||
'packages = find_namespace:\n'
|
||||
'\n'
|
||||
'[options.packages.find]\n'
|
||||
'exclude =\n'
|
||||
' fake_package.sub_one\n'
|
||||
)
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert set(dist.packages) == {'fake_package', 'fake_package.sub_two'}
|
||||
|
||||
def test_extras_require(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
'[options.extras_require]\n'
|
||||
'pdf = ReportLab>=1.2; RXP\n'
|
||||
'rest = \n'
|
||||
' docutils>=0.3\n'
|
||||
' pack ==1.1, ==1.3\n',
|
||||
)
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.extras_require == {
|
||||
'pdf': ['ReportLab>=1.2', 'RXP'],
|
||||
'rest': ['docutils>=0.3', 'pack==1.1,==1.3'],
|
||||
}
|
||||
assert set(dist.metadata.provides_extras) == {'pdf', 'rest'}
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
"[options.extras_require]\nfoo = bar;python_version<'3'",
|
||||
"[options.extras_require]\nfoo = bar;os_name=='linux'",
|
||||
"[options.extras_require]\nfoo = bar;python_version<'3'\n",
|
||||
"[options.extras_require]\nfoo = bar;os_name=='linux'\n",
|
||||
"[options]\ninstall_requires = bar;python_version<'3'",
|
||||
"[options]\ninstall_requires = bar;os_name=='linux'",
|
||||
"[options]\ninstall_requires = bar;python_version<'3'\n",
|
||||
"[options]\ninstall_requires = bar;os_name=='linux'\n",
|
||||
],
|
||||
)
|
||||
def test_raises_accidental_env_marker_misconfig(self, config, tmpdir):
|
||||
fake_env(tmpdir, config)
|
||||
match = (
|
||||
r"One of the parsed requirements in `(install_requires|extras_require.+)` "
|
||||
"looks like a valid environment marker.*"
|
||||
)
|
||||
with pytest.raises(InvalidRequirement, match=match):
|
||||
with get_dist(tmpdir) as _:
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
"[options.extras_require]\nfoo = bar;python_version<3",
|
||||
"[options.extras_require]\nfoo = bar;python_version<3\n",
|
||||
"[options]\ninstall_requires = bar;python_version<3",
|
||||
"[options]\ninstall_requires = bar;python_version<3\n",
|
||||
],
|
||||
)
|
||||
def test_warn_accidental_env_marker_misconfig(self, config, tmpdir):
|
||||
fake_env(tmpdir, config)
|
||||
match = (
|
||||
r"One of the parsed requirements in `(install_requires|extras_require.+)` "
|
||||
"looks like a valid environment marker.*"
|
||||
)
|
||||
with pytest.warns(SetuptoolsDeprecationWarning, match=match):
|
||||
with get_dist(tmpdir) as _:
|
||||
pass
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"config",
|
||||
[
|
||||
"[options.extras_require]\nfoo =\n bar;python_version<'3'",
|
||||
"[options.extras_require]\nfoo = bar;baz\nboo = xxx;yyy",
|
||||
"[options.extras_require]\nfoo =\n bar;python_version<'3'\n",
|
||||
"[options.extras_require]\nfoo = bar;baz\nboo = xxx;yyy\n",
|
||||
"[options.extras_require]\nfoo =\n bar\n python_version<3\n",
|
||||
"[options]\ninstall_requires =\n bar;python_version<'3'",
|
||||
"[options]\ninstall_requires = bar;baz\nboo = xxx;yyy",
|
||||
"[options]\ninstall_requires =\n bar;python_version<'3'\n",
|
||||
"[options]\ninstall_requires = bar;baz\nboo = xxx;yyy\n",
|
||||
"[options]\ninstall_requires =\n bar\n python_version<3\n",
|
||||
],
|
||||
)
|
||||
@pytest.mark.filterwarnings("error::setuptools.SetuptoolsDeprecationWarning")
|
||||
def test_nowarn_accidental_env_marker_misconfig(self, config, tmpdir, recwarn):
|
||||
fake_env(tmpdir, config)
|
||||
num_warnings = len(recwarn)
|
||||
with get_dist(tmpdir) as _:
|
||||
pass
|
||||
# The examples are valid, no warnings shown
|
||||
assert len(recwarn) == num_warnings
|
||||
|
||||
def test_dash_preserved_extras_require(self, tmpdir):
|
||||
fake_env(tmpdir, '[options.extras_require]\nfoo-a = foo\nfoo_b = test\n')
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.extras_require == {'foo-a': ['foo'], 'foo_b': ['test']}
|
||||
|
||||
def test_entry_points(self, tmpdir):
|
||||
_, config = fake_env(
|
||||
tmpdir,
|
||||
'[options.entry_points]\n'
|
||||
'group1 = point1 = pack.module:func, '
|
||||
'.point2 = pack.module2:func_rest [rest]\n'
|
||||
'group2 = point3 = pack.module:func2\n',
|
||||
)
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.entry_points == {
|
||||
'group1': [
|
||||
'point1 = pack.module:func',
|
||||
'.point2 = pack.module2:func_rest [rest]',
|
||||
],
|
||||
'group2': ['point3 = pack.module:func2'],
|
||||
}
|
||||
|
||||
expected = (
|
||||
'[blogtool.parsers]\n'
|
||||
'.rst = some.nested.module:SomeClass.some_classmethod[reST]\n'
|
||||
)
|
||||
|
||||
tmpdir.join('entry_points').write(expected)
|
||||
|
||||
# From file.
|
||||
config.write('[options]\nentry_points = file: entry_points\n')
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.entry_points == expected
|
||||
|
||||
def test_case_sensitive_entry_points(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
'[options.entry_points]\n'
|
||||
'GROUP1 = point1 = pack.module:func, '
|
||||
'.point2 = pack.module2:func_rest [rest]\n'
|
||||
'group2 = point3 = pack.module:func2\n',
|
||||
)
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.entry_points == {
|
||||
'GROUP1': [
|
||||
'point1 = pack.module:func',
|
||||
'.point2 = pack.module2:func_rest [rest]',
|
||||
],
|
||||
'group2': ['point3 = pack.module:func2'],
|
||||
}
|
||||
|
||||
def test_data_files(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
'[options.data_files]\n'
|
||||
'cfg =\n'
|
||||
' a/b.conf\n'
|
||||
' c/d.conf\n'
|
||||
'data = e/f.dat, g/h.dat\n',
|
||||
)
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
expected = [
|
||||
('cfg', ['a/b.conf', 'c/d.conf']),
|
||||
('data', ['e/f.dat', 'g/h.dat']),
|
||||
]
|
||||
assert sorted(dist.data_files) == sorted(expected)
|
||||
|
||||
def test_data_files_globby(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
'[options.data_files]\n'
|
||||
'cfg =\n'
|
||||
' a/b.conf\n'
|
||||
' c/d.conf\n'
|
||||
'data = *.dat\n'
|
||||
'icons = \n'
|
||||
' *.ico\n'
|
||||
'audio = \n'
|
||||
' *.wav\n'
|
||||
' sounds.db\n',
|
||||
)
|
||||
|
||||
# Create dummy files for glob()'s sake:
|
||||
tmpdir.join('a.dat').write('')
|
||||
tmpdir.join('b.dat').write('')
|
||||
tmpdir.join('c.dat').write('')
|
||||
tmpdir.join('a.ico').write('')
|
||||
tmpdir.join('b.ico').write('')
|
||||
tmpdir.join('c.ico').write('')
|
||||
tmpdir.join('beep.wav').write('')
|
||||
tmpdir.join('boop.wav').write('')
|
||||
tmpdir.join('sounds.db').write('')
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
expected = [
|
||||
('cfg', ['a/b.conf', 'c/d.conf']),
|
||||
('data', ['a.dat', 'b.dat', 'c.dat']),
|
||||
('icons', ['a.ico', 'b.ico', 'c.ico']),
|
||||
('audio', ['beep.wav', 'boop.wav', 'sounds.db']),
|
||||
]
|
||||
assert sorted(dist.data_files) == sorted(expected)
|
||||
|
||||
def test_python_requires_simple(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
DALS(
|
||||
"""
|
||||
[options]
|
||||
python_requires=>=2.7
|
||||
"""
|
||||
),
|
||||
)
|
||||
with get_dist(tmpdir) as dist:
|
||||
dist.parse_config_files()
|
||||
|
||||
def test_python_requires_compound(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
DALS(
|
||||
"""
|
||||
[options]
|
||||
python_requires=>=2.7,!=3.0.*
|
||||
"""
|
||||
),
|
||||
)
|
||||
with get_dist(tmpdir) as dist:
|
||||
dist.parse_config_files()
|
||||
|
||||
def test_python_requires_invalid(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
DALS(
|
||||
"""
|
||||
[options]
|
||||
python_requires=invalid
|
||||
"""
|
||||
),
|
||||
)
|
||||
with pytest.raises(Exception):
|
||||
with get_dist(tmpdir) as dist:
|
||||
dist.parse_config_files()
|
||||
|
||||
def test_cmdclass(self, tmpdir):
|
||||
module_path = Path(tmpdir, "src/custom_build.py") # auto discovery for src
|
||||
module_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
module_path.write_text(
|
||||
"from distutils.core import Command\nclass CustomCmd(Command): pass\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
setup_cfg = """
|
||||
[options]
|
||||
cmdclass =
|
||||
customcmd = custom_build.CustomCmd
|
||||
"""
|
||||
fake_env(tmpdir, inspect.cleandoc(setup_cfg))
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
cmdclass = dist.cmdclass['customcmd']
|
||||
assert cmdclass.__name__ == "CustomCmd"
|
||||
assert cmdclass.__module__ == "custom_build"
|
||||
assert module_path.samefile(inspect.getfile(cmdclass))
|
||||
|
||||
def test_requirements_file(self, tmpdir):
|
||||
fake_env(
|
||||
tmpdir,
|
||||
DALS(
|
||||
"""
|
||||
[options]
|
||||
install_requires = file:requirements.txt
|
||||
[options.extras_require]
|
||||
colors = file:requirements-extra.txt
|
||||
"""
|
||||
),
|
||||
)
|
||||
|
||||
tmpdir.join('requirements.txt').write('\ndocutils>=0.3\n\n')
|
||||
tmpdir.join('requirements-extra.txt').write('colorama')
|
||||
|
||||
with get_dist(tmpdir) as dist:
|
||||
assert dist.install_requires == ['docutils>=0.3']
|
||||
assert dist.extras_require == {'colors': ['colorama']}
|
||||
|
||||
|
||||
saved_dist_init = _Distribution.__init__
|
||||
|
||||
|
||||
class TestExternalSetters:
|
||||
# During creation of the setuptools Distribution() object, we call
|
||||
# the init of the parent distutils Distribution object via
|
||||
# _Distribution.__init__ ().
|
||||
#
|
||||
# It's possible distutils calls out to various keyword
|
||||
# implementations (i.e. distutils.setup_keywords entry points)
|
||||
# that may set a range of variables.
|
||||
#
|
||||
# This wraps distutil's Distribution.__init__ and simulates
|
||||
# pbr or something else setting these values.
|
||||
def _fake_distribution_init(self, dist, attrs):
|
||||
saved_dist_init(dist, attrs)
|
||||
# see self._DISTUTILS_UNSUPPORTED_METADATA
|
||||
dist.metadata.long_description_content_type = 'text/something'
|
||||
# Test overwrite setup() args
|
||||
dist.metadata.project_urls = {
|
||||
'Link One': 'https://example.com/one/',
|
||||
'Link Two': 'https://example.com/two/',
|
||||
}
|
||||
|
||||
@patch.object(_Distribution, '__init__', autospec=True)
|
||||
def test_external_setters(self, mock_parent_init, tmpdir):
|
||||
mock_parent_init.side_effect = self._fake_distribution_init
|
||||
|
||||
dist = Distribution(attrs={'project_urls': {'will_be': 'ignored'}})
|
||||
|
||||
assert dist.metadata.long_description_content_type == 'text/something'
|
||||
assert dist.metadata.project_urls == {
|
||||
'Link One': 'https://example.com/one/',
|
||||
'Link Two': 'https://example.com/two/',
|
||||
}
|
||||
Reference in New Issue
Block a user