[bumpversion]
-current_version = 61.0.0
+current_version = 61.1.0
commit = True
tag = True
+v61.1.0
+-------
+
+
+Deprecations
+^^^^^^^^^^^^
+* #3206: Changed ``setuptools.convert_path`` to an internal function that is not exposed
+ as part of setuptools API.
+ Future releases of ``setuptools`` are likely to remove this function.
+
+Changes
+^^^^^^^
+* #3202: Changed behaviour of auto-discovery to not explicitly expand ``package_dir``
+ for flat-layouts and to not use relative paths starting with ``./``.
+* #3203: Prevented ``pyproject.toml`` parsing from overwriting
+ ``dist.include_package_data`` explicitly set in ``setup.py`` with default
+ value.
+* #3208: Added a warning for non existing files listed with the ``file`` directive in
+ ``setup.cfg`` and ``pyproject.toml``.
+* #3208: Added a default value for dynamic ``classifiers`` in ``pyproject.toml`` when
+ files are missing and errors being ignored.
+* #3211: Disabled auto-discovery when distribution class has a ``configuration``
+ attribute (e.g. when the ``setup.py`` script contains ``setup(...,
+ configuration=...)``). This is done to ensure extension-only packages created
+ with ``numpy.distutils.misc_util.Configuration`` are not broken by the safe
+ guard
+ behaviour to avoid accidental multiple top-level packages in a flat-layout.
+
+ .. note::
+ Users that don't set ``packages``, ``py_modules``, or ``configuration`` are
+ still likely to observe the auto-discovery behavior, which may halt the
+ build if the project contains multiple directories and/or multiple Python
+ files directly under the project root.
+
+ To disable auto-discovery please explicitly set either ``packages`` or
+ ``py_modules``. Alternatively you can also configure :ref:`custom-discovery`.
+
+
v61.0.0
-------
You can check details about the automatic discovery (and how to configure a
different behaviour) in :doc:`/userguide/package_discovery`.
+* #3067: If the file ``pyproject.toml`` exists and it includes project
+ metadata/config (via ``[project]`` table or ``[tool.setuptools]``),
+ a series of new behaviors that are not backward compatible may take place:
+
+ - The default value of ``include_package_data`` will be considered to be ``True``.
+ - Setuptools will attempt to validate the ``pyproject.toml`` file according
+ to PEP 621 specification.
+ - The values specified in ``pyproject.toml`` will take precedence over those
+ specified in ``setup.cfg`` or ``setup.py``.
Changes
^^^^^^^
* #3066: Added vendored dependencies for :pypi:`tomli`, :pypi:`validate-pyproject`.
These dependencies are used to read ``pyproject.toml`` files and validate them.
+* #3067: **[EXPERIMENTAL]** When using ``pyproject.toml`` metadata,
+ the default value of ``include_package_data`` is changed to ``True``.
* #3068: **[EXPERIMENTAL]** Add support for ``pyproject.toml`` configuration
(as introduced by :pep:`621`). Configuration parameters not covered by
standards are handled in the ``[tool.setuptools]`` sub-table.
Python requirement
==================
In some cases, you might need to specify the minimum required python version.
-This can be configured as shown in the example bellow.
+This can be configured as shown in the example below.
.. tab:: setup.cfg
``Setuptools`` has adopted this standard and will use the information contained
in this file as an input in the build process.
-The example bellow illustrates how to write a ``pyproject.toml`` file that can
+The example below illustrates how to write a ``pyproject.toml`` file that can
be used with ``setuptools``. It contains two TOML tables (identified by the
``[table-header]`` syntax): ``build-system`` and ``project``.
The ``build-system`` table is used to tell the build frontend (e.g.
``zip-safe`` boolean If not specified, ``setuptools`` will try to guess
a reasonable default for the package
``eager-resources`` array
-``py-modules`` array See tip bellow
-``packages`` array or ``find`` directive See tip bellow
+``py-modules`` array See tip below
+``packages`` array or ``find`` directive See tip below
``package-dir`` table/inline-table Used when explicitly listing ``packages``
``namespace-packages`` array Not necessary if you use :pep:`420`
``package-data`` table/inline-table See :doc:`/userguide/datafiles`
=====================
Packages built with ``setuptools`` can specify dependencies to be automatically
installed when the package itself is installed.
-The example bellow show how to configure this kind of dependencies:
+The example below show how to configure this kind of dependencies:
.. tab:: setup.cfg
[metadata]
name = setuptools
-version = 61.0.0
+version = 61.1.0
author = Python Packaging Authority
author_email = distutils-sig@python.org
description = Easily download, build, install, upgrade, and uninstall Python packages
import functools
import os
import re
+import warnings
import _distutils_hack.override # noqa: F401
import distutils.core
from distutils.errors import DistutilsOptionError
+from distutils.util import convert_path as _convert_path
from ._deprecation_warning import SetuptoolsDeprecationWarning
return list(files)
+@functools.wraps(_convert_path)
+def convert_path(pathname):
+ from inspect import cleandoc
+
+ msg = """
+ The function `convert_path` is considered internal and not part of the public API.
+ Its direct usage by 3rd-party packages is considered deprecated and the function
+ may be removed in the future.
+ """
+ warnings.warn(cleandoc(msg), SetuptoolsDeprecationWarning)
+ return _convert_path(pathname)
+
+
class sic(str):
"""Treat this string as-is (https://en.wikipedia.org/wiki/Sic)"""
- `__init__.py`
- `fastjsonschema_validations.py`
-The relevant copyright notes and licenses are included bellow.
+The relevant copyright notes and licenses are included below.
***
import io
import os
import sys
+import warnings
from glob import iglob
from configparser import ConfigParser
from importlib.machinery import ModuleSpec
(By default ``root_dir`` is the current directory).
"""
- if isinstance(filepaths, (str, bytes)):
- filepaths = [filepaths] # type: ignore
+ from setuptools.extern.more_itertools import always_iterable
root_dir = os.path.abspath(root_dir or os.getcwd())
- _filepaths = (os.path.join(root_dir, path) for path in filepaths)
+ _filepaths = (os.path.join(root_dir, path) for path in always_iterable(filepaths))
return '\n'.join(
_read_file(path)
- for path in _filepaths
- if _assert_local(path, root_dir) and os.path.isfile(path)
+ for path in _filter_existing_files(_filepaths)
+ if _assert_local(path, root_dir)
)
+def _filter_existing_files(filepaths: Iterable[_Path]) -> Iterator[_Path]:
+ for path in filepaths:
+ if os.path.isfile(path):
+ yield path
+ else:
+ warnings.warn(f"File {path!r} cannot be found")
+
+
def _read_file(filepath: Union[bytes, _Path]) -> str:
with io.open(filepath, encoding='utf-8') as f:
return f.read()
:rtype: list
"""
-
- from setuptools.discovery import remove_nested_packages
+ from setuptools.discovery import construct_package_dir
+ from setuptools.extern.more_itertools import unique_everseen, always_iterable
if namespaces:
from setuptools.discovery import PEP420PackageFinder as PackageFinder
root_dir = root_dir or os.curdir
where = kwargs.pop('where', ['.'])
- if isinstance(where, str):
- where = [where]
-
- packages = []
+ packages: List[str] = []
fill_package_dir = {} if fill_package_dir is None else fill_package_dir
- for path in where:
- pkgs = PackageFinder.find(_nest_path(root_dir, path), **kwargs)
+
+ for path in unique_everseen(always_iterable(where)):
+ package_path = _nest_path(root_dir, path)
+ pkgs = PackageFinder.find(package_path, **kwargs)
packages.extend(pkgs)
- if fill_package_dir.get("") != path:
- parent_pkgs = remove_nested_packages(pkgs)
- parent = {pkg: "/".join([path, *pkg.split(".")]) for pkg in parent_pkgs}
- fill_package_dir.update(parent)
+ if pkgs and not (
+ fill_package_dir.get("") == path
+ or os.path.samefile(package_path, root_dir)
+ ):
+ fill_package_dir.update(construct_package_dir(pkgs, path))
return packages
# the default would be an improvement.
# `ini2toml` backfills include_package_data=False when nothing is explicitly given,
# therefore setting a default here is backwards compatible.
- setuptools_table.setdefault("include-package-data", True)
+ if dist and getattr(dist, "include_package_data") is not None:
+ setuptools_table.setdefault("include-package-data", dist.include_package_data)
+ else:
+ setuptools_table.setdefault("include-package-data", True)
# Persist changes:
asdict["tool"] = tool_table
tool_table["setuptools"] = setuptools_table
if "classifiers" in dynamic:
value = _expand_dynamic(dynamic_cfg, "classifiers", pkg_dir, root_dir, ignore)
- project_cfg["classifiers"] = value.splitlines()
+ project_cfg["classifiers"] = (value or "").splitlines()
def _expand_dynamic(
import os
from fnmatch import fnmatchcase
from glob import glob
+from pathlib import Path
from typing import TYPE_CHECKING
from typing import Callable, Dict, Iterator, Iterable, List, Optional, Tuple, Union
self.dist.packages is not None
or self.dist.py_modules is not None
or ext_modules
+ or hasattr(self.dist, "configuration") and self.dist.configuration
+ # ^ Some projects use numpy.distutils.misc_util.Configuration
)
def _analyse_package_layout(self, ignore_ext_modules: bool) -> bool:
parent = package_dir.get("") or ""
return os.path.join(root_dir, *parent.split("/"), *parts)
+
+
+def construct_package_dir(packages: List[str], package_path: _Path) -> Dict[str, str]:
+ parent_pkgs = remove_nested_packages(packages)
+ prefix = Path(package_path).parts
+ return {pkg: "/".join([*prefix, *pkg.split(".")]) for pkg in parent_pkgs}
text = text.replace(orig, subst)
pyproject.write_text(text)
- (tmp_path / "README.rst").write_text("hello world")
+ (tmp_path / readme).write_text("hello world")
(tmp_path / "LICENSE.txt").write_text("--- LICENSE stub ---")
(tmp_path / "spam.py").write_text(PEP621_EXAMPLE_SCRIPT)
return pyproject
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, tmp_path)
+ write_files(files, dir_)
with monkeypatch.context() as m:
- m.chdir(tmp_path)
+ m.chdir(dir_)
assert expand.read_files(list(files)) == "a\nb\nc"
cannot_access_msg = r"Cannot access '.*\.\..a\.txt'"
expand.read_files(["../a.txt"])
# Make sure the same APIs work outside cwd
- assert expand.read_files(list(files), tmp_path) == "a\nb\nc"
+ assert expand.read_files(list(files), dir_) == "a\nb\nc"
with pytest.raises(DistutilsOptionError, match=cannot_access_msg):
- expand.read_files(["../a.txt"], tmp_path)
+ expand.read_files(["../a.txt"], dir_)
class TestReadAttr:
import pytest
import tomli_w
+from path import Path as _Path
from setuptools.config.pyprojecttoml import (
read_configuration,
expand_configuration,
validate,
)
+from setuptools.errors import OptionError
+
+
+import setuptools # noqa -- force distutils.core to be patched
+import distutils.core
EXAMPLE = """
[project]
assert "gui-scripts" not in expanded_project
-def test_dynamic_classifiers(tmp_path):
- # Let's create a project example that has dynamic classifiers
- # coming from a txt file.
- create_example(tmp_path, "src")
- classifiers = """\
- Framework :: Flask
- Programming Language :: Haskell
- """
- (tmp_path / "classifiers.txt").write_text(cleandoc(classifiers))
+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 = """\
+ Framework :: Flask
+ Programming Language :: Haskell
+ """
+ (tmp_path / "classifiers.txt").write_text(cleandoc(classifiers))
+
+ 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",
+ }
- 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"}
+ def test_dynamic_without_config(self, tmp_path):
+ config = """
+ [project]
+ name = "myproj"
+ version = '42'
+ dynamic = ["classifiers"]
+ """
- # When the configuration is expanded,
- # each line of the file should be an different classifier.
- validate(config, pyproject)
- expanded = expand_configuration(config, tmp_path)
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text(cleandoc(config))
+ with pytest.raises(OptionError, match="No configuration found"):
+ read_configuration(pyproject)
- assert set(expanded["project"]["classifiers"]) == {
- "Framework :: Flask",
- "Programming Language :: Haskell",
- }
+ 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))
+ with pytest.warns(UserWarning, match="File .*classifiers.txt. cannot be found"):
+ expanded = read_configuration(pyproject)
+ assert not expanded["project"]["classifiers"]
@pytest.mark.parametrize(
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
+ """
+ pyproject = tmp_path / "pyproject.toml"
+ pyproject.write_text("[project]\nname = 'myproj'\nversion='42'\n")
+ setuppy = tmp_path / "setup.py"
+ setuppy.write_text("__import__('setuptools').setup(include_package_data=False)")
+
+ 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_file_sandboxed(self, tmpdir):
- fake_env(tmpdir, '[metadata]\n' 'long_description = file: ../../README\n')
+ tmpdir.ensure("README")
+ project = tmpdir.join('depth1', 'depth2')
+ project.ensure(dir=True)
+ fake_env(project, '[metadata]\n' 'long_description = file: ../../README\n')
- with get_dist(tmpdir, parse=False) as dist:
+ with get_dist(project, parse=False) as dist:
with pytest.raises(DistutilsOptionError):
dist.parse_config_files() # file: out of sandbox
("brotli", LATEST), # not in the list but used by urllib3
# When adding packages to this list, make sure they expose a `__version__`
- # attribute, or modify the tests bellow
+ # attribute, or modify the tests below
]
import distutils.core
import pytest
+import jaraco.path
from path import Path as _Path
from .contexts import quiet
_get_dist(tmp_path, {})
+class TestWithPackageData:
+ def _simulate_package_with_data_files(self, tmp_path, src_root):
+ files = [
+ f"{src_root}/proj/__init__.py",
+ f"{src_root}/proj/file1.txt",
+ f"{src_root}/proj/nested/file2.txt",
+ ]
+ _populate_project_dir(tmp_path, files, {})
+
+ manifest = """
+ global-include *.py *.txt
+ """
+ (tmp_path / "MANIFEST.in").write_text(DALS(manifest))
+
+ EXAMPLE_SETUPCFG = """
+ [metadata]
+ name = proj
+ version = 42
+
+ [options]
+ include_package_data = True
+ """
+ EXAMPLE_PYPROJECT = """
+ [project]
+ name = "proj"
+ version = "42"
+ """
+
+ PYPROJECT_PACKAGE_DIR = """
+ [tool.setuptools]
+ package-dir = {"" = "src"}
+ """
+
+ @pytest.mark.parametrize(
+ "src_root, files",
+ [
+ (".", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}),
+ (".", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}),
+ ("src", {"setup.cfg": DALS(EXAMPLE_SETUPCFG)}),
+ ("src", {"pyproject.toml": DALS(EXAMPLE_PYPROJECT)}),
+ (
+ "src",
+ {
+ "setup.cfg": DALS(EXAMPLE_SETUPCFG) + DALS(
+ """
+ packages = find:
+ package_dir =
+ =src
+
+ [options.packages.find]
+ where = src
+ """
+ )
+ }
+ ),
+ (
+ "src",
+ {
+ "pyproject.toml": DALS(EXAMPLE_PYPROJECT) + DALS(
+ """
+ [tool.setuptools]
+ package-dir = {"" = "src"}
+ """
+ )
+ },
+ ),
+ ]
+ )
+ def test_include_package_data(self, tmp_path, src_root, files):
+ """
+ Make sure auto-discovery does not affect package include_package_data.
+ See issue #3196.
+ """
+ jaraco.path.build(files, prefix=str(tmp_path))
+ self._simulate_package_with_data_files(tmp_path, src_root)
+
+ expected = {
+ os.path.normpath(f"{src_root}/proj/file1.txt").replace(os.sep, "/"),
+ os.path.normpath(f"{src_root}/proj/nested/file2.txt").replace(os.sep, "/"),
+ }
+
+ _run_build(tmp_path)
+
+ sdist_files = get_sdist_members(next(tmp_path.glob("dist/*.tar.gz")))
+ print("~~~~~ sdist_members ~~~~~")
+ print('\n'.join(sdist_files))
+ assert sdist_files >= expected
+
+ wheel_files = get_wheel_members(next(tmp_path.glob("dist/*.whl")))
+ print("~~~~~ wheel_members ~~~~~")
+ print('\n'.join(wheel_files))
+ orig_files = {f.replace("src/", "").replace("lib/", "") for f in expected}
+ assert wheel_files >= orig_files
+
+
+def test_compatible_with_numpy_configuration(tmp_path):
+ files = [
+ "dir1/__init__.py",
+ "dir2/__init__.py",
+ "file.py",
+ ]
+ _populate_project_dir(tmp_path, files, {})
+ dist = Distribution({})
+ dist.configuration = object()
+ dist.set_defaults()
+ assert dist.py_modules is None
+ assert dist.packages is None
+
+
def _populate_project_dir(root, files, options):
# NOTE: Currently pypa/build will refuse to build the project if no
# `pyproject.toml` or `setup.py` is found. So it is impossible to do
def _run_build(path, *flags):
cmd = [sys.executable, "-m", "build", "--no-isolation", *flags, str(path)]
- return run(cmd, env={'DISTUTILS_DEBUG': '1'})
+ return run(cmd, env={'DISTUTILS_DEBUG': ''})
def _get_dist(dist_path, attrs):
for member in contents:
assert '/tests/' not in member
+
+
+def test_convert_path_deprecated():
+ with pytest.warns(setuptools.SetuptoolsDeprecationWarning):
+ setuptools.convert_path('setuptools/tests')