[bumpversion]
-current_version = 66.0.0
+current_version = 66.1.0
commit = True
tag = True
+v66.1.0
+-------
+
+
+Changes
+^^^^^^^
+* #3685: Fix improper usage of deprecated/removed ``pkgutil`` APIs in Python 3.12+.
+* #3779: Files referenced by ``file:`` in ``setup.cfg`` and by ``project.readme.file``,
+ ``project.license.file`` or ``tool.setuptools.dynamic.*.file`` in
+ ``pyproject.toml`` are now automatically included in the generated sdists.
+
+Misc
+^^^^
+* #3776: Added note about using the ``--pep-517`` flag with ``pip`` to workaround
+ ``InvalidVersion`` errors for packages that are already installed in the system.
+
+
v66.0.0
-------
This design was inspired by :user:`cajhne`'s `original proposal`_ and the
ancient symbol of the ouroboros_.
It features a snake moving in a circular trajectory not only as a reference to
-the Python programming language but also to the `wheel package format`_ as one
+the Python programming language but also to the :pep:`wheel package format <427>` as one
of the distribution formats supported by setuptools.
The shape of the snake also resembles a cog, which together with the hammer is
a nod to the two words that compose the name of the project.
.. _setuptools repository: https://github.com/pypa/setuptools
.. _install the correct fonts: https://wiki.inkscape.org/wiki/Installing_fonts
.. _original proposal: https://github.com/pypa/setuptools/issues/2227#issuecomment-653628344
-.. _wheel package format: https://www.python.org/dev/peps/pep-0427/
.. _ouroboros: https://en.wikipedia.org/wiki/Ouroboros
is difficult to provide a reliable user experience across environments
and history.
-`PEP 517 <https://www.python.org/dev/peps/pep-0517/>`_ came to
+:pep:`517` came to
the rescue and specified a new standard for packaging and distributing Python
modules. Under PEP 517:
`Use "virtualenv"`_
-.. [1] There are older ways to achieve custom installation using various ``easy_install`` and ``setup.py install`` options, combined with ``PYTHONPATH`` and/or ``PYTHONUSERBASE`` alterations, but all of these are effectively deprecated by the User scheme brought in by `PEP-370`_.
-
-.. _PEP-370: http://www.python.org/dev/peps/pep-0370/
+.. [1] There are older ways to achieve custom installation using various ``easy_install`` and ``setup.py install`` options, combined with ``PYTHONPATH`` and/or ``PYTHONUSERBASE`` alterations, but all of these are effectively deprecated by the User scheme brought in by :pep:`370`.
Use the "--user" option
description = My package description
long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst
keywords = one, two
- python_requires = >=3.7
license = BSD-3-Clause
classifiers =
Framework :: Django
zip_safe = False
include_package_data = True
packages = find:
+ python_requires = >=3.7
install_requires =
requests
importlib-metadata; python_version<"3.8"
The ``file:`` directive is sandboxed and won't reach anything outside the
project directory (i.e. the directory containing ``setup.cfg``/``pyproject.toml``).
- .. attention::
- When using the ``file:`` directive, please make sure that all necessary
- files are included in the ``sdist``. You can do that via ``MANIFEST.in``
- or using plugins such as ``setuptools-scm``.
- Please have a look on :doc:`/userguide/miscellaneous` for more information.
+ .. note::
+ If you are using an old version of ``setuptools``, you might need to ensure
+ that all files referenced by the ``file:`` directive are included in the ``sdist``
+ (you can do that via ``MANIFEST.in`` or using plugins such as ``setuptools-scm``,
+ please have a look on :doc:`/userguide/miscellaneous` for more information).
+
+ .. versionchanged:: 66.1.0
+ Newer versions of ``setuptools`` will automatically add these files to the ``sdist``.
Metadata
Legacy Namespace Packages
=========================
The fact you can create namespace packages so effortlessly above is credited
-to `PEP 420 <https://www.python.org/dev/peps/pep-0420/>`_. It used to be more
+to :pep:`420`. It used to be more
cumbersome to accomplish the same result. Historically, there were two methods
to create namespace packages. One is the ``pkg_resources`` style supported by
``setuptools`` and the other one being ``pkgutils`` style offered by
(``pip``-specify syntaxes, e.g. ``-c/-r/-e`` flags, are not supported).
-.. attention::
- When using the ``file`` directive, please make sure that all necessary
- files are included in the ``sdist``. You can do that via ``MANIFEST.in``
- or using plugins such as ``setuptools-scm``.
- Please have a look on :doc:`/userguide/miscellaneous` for more information.
+.. note::
+ If you are using an old version of ``setuptools``, you might need to ensure
+ that all files referenced by the ``file`` directive are included in the ``sdist``
+ (you can do that via ``MANIFEST.in`` or using plugins such as ``setuptools-scm``,
+ please have a look on :doc:`/userguide/miscellaneous` for more information).
+
+ .. versionchanged:: 66.1.0
+ Newer versions of ``setuptools`` will automatically add these files to the ``sdist``.
----
return next(dist_groups, ())
-register_finder(pkgutil.ImpImporter, find_on_path)
+if hasattr(pkgutil, 'ImpImporter'):
+ register_finder(pkgutil.ImpImporter, find_on_path)
-if hasattr(importlib_machinery, 'FileFinder'):
- register_finder(importlib_machinery.FileFinder, find_on_path)
+register_finder(importlib_machinery.FileFinder, find_on_path)
_declare_state('dict', _namespace_handlers={})
_declare_state('dict', _namespace_packages={})
return subpath
-register_namespace_handler(pkgutil.ImpImporter, file_ns_handler)
-register_namespace_handler(zipimport.zipimporter, file_ns_handler)
+if hasattr(pkgutil, 'ImpImporter'):
+ register_namespace_handler(pkgutil.ImpImporter, file_ns_handler)
-if hasattr(importlib_machinery, 'FileFinder'):
- register_namespace_handler(importlib_machinery.FileFinder, file_ns_handler)
+register_namespace_handler(zipimport.zipimporter, file_ns_handler)
+register_namespace_handler(importlib_machinery.FileFinder, file_ns_handler)
def null_ns_handler(importer, path_item, packageName, module):
@property
def parsed_version(self):
if not hasattr(self, "_parsed_version"):
- self._parsed_version = parse_version(self.version)
+ try:
+ self._parsed_version = parse_version(self.version)
+ except packaging.version.InvalidVersion as ex:
+ info = f"(package: {self.project_name})"
+ if hasattr(ex, "add_note"):
+ ex.add_note(info) # PEP 678
+ raise
+ raise packaging.version.InvalidVersion(f"{str(ex)} {info}") from None
return self._parsed_version
[metadata]
name = setuptools
-version = 66.0.0
+version = 66.1.0
author = Python Packaging Authority
author_email = distutils-sig@python.org
description = Easily download, build, install, upgrade, and uninstall Python packages
# Honor setup.cfg's options.
dist.parse_config_files(ignore_option_errors=True)
if dist.setup_requires:
+ _fetch_build_eggs(dist)
+
+
+def _fetch_build_eggs(dist):
+ try:
dist.fetch_build_eggs(dist.setup_requires)
+ except Exception as ex:
+ msg = """
+ It is possible a package already installed in your system
+ contains an version that is invalid according to PEP 440.
+ You can try `pip install --use-pep517` as a workaround for this problem,
+ or rely on a new virtual environment.
+
+ If the problem refers to a package that is not installed yet,
+ please contact that package's maintainers or distributors.
+ """
+ if "InvalidVersion" in ex.__class__.__name__:
+ if hasattr(ex, "add_note"):
+ ex.add_note(msg) # PEP 678
+ else:
+ dist.announce(f"\n{msg}\n")
+ raise
def setup(**attrs):
if os.path.exists(self.template):
self.read_template()
self.add_license_files()
+ self._add_referenced_files()
self.prune_file_list()
self.filelist.sort()
self.filelist.remove_duplicates()
license_files = self.distribution.metadata.license_files or []
for lf in license_files:
log.info("adding license file '%s'", lf)
- pass
self.filelist.extend(license_files)
+ def _add_referenced_files(self):
+ """Add files referenced by the config (e.g. `file:` directive) to filelist"""
+ referenced = getattr(self.distribution, '_referenced_files', [])
+ # ^-- fallback if dist comes from distutils or is a custom class
+ for rf in referenced:
+ log.debug("adding file referenced by config '%s'", rf)
+ self.filelist.extend(referenced)
+
def prune_file_list(self):
build = self.get_finalized_command('build')
base_dir = self.distribution.get_fullname()
from itertools import chain
from types import MappingProxyType
from typing import (TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple,
- Type, Union)
+ Type, Union, cast)
from setuptools._deprecation_warning import SetuptoolsDeprecationWarning
from setuptools.config import expand
if isinstance(val, str):
- text = expand.read_files(val, root_dir)
+ file: Union[str, list] = val
+ text = expand.read_files(file, root_dir)
ctype = _guess_content_type(val)
else:
- text = val.get("text") or expand.read_files(val.get("file", []), root_dir)
+ file = val.get("file") or []
+ text = val.get("text") or expand.read_files(file, root_dir)
ctype = val["content-type"]
_set_config(dist, "long_description", text)
+
if ctype:
_set_config(dist, "long_description_content_type", ctype)
+ if file:
+ dist._referenced_files.add(cast(str, file))
+
def _license(dist: "Distribution", val: dict, root_dir: _Path):
from setuptools.config import expand
if "file" in val:
_set_config(dist, "license", expand.read_files([val["file"]], root_dir))
+ dist._referenced_files.add(val["file"])
else:
_set_config(dist, "license", val["text"])
import warnings
from contextlib import contextmanager
from functools import partial
-from typing import TYPE_CHECKING, Callable, Dict, Optional, Mapping, Union
+from typing import TYPE_CHECKING, Callable, Dict, Optional, Mapping, Set, Union
from setuptools.errors import FileError, OptionError
:param Distribution|None: Distribution object to which the configuration refers.
If not given a dummy object will be created and discarded after the
- configuration is read. This is used for auto-discovery of packages in the case
- a dynamic configuration (e.g. ``attr`` or ``cmdclass``) is expanded.
+ configuration is read. This is used for auto-discovery of packages and in the
+ case a dynamic configuration (e.g. ``attr`` or ``cmdclass``) is expanded.
When ``expand=False`` this object is simply ignored.
:rtype: dict
self.dynamic_cfg = self.setuptools_cfg.get("dynamic", {})
self.ignore_option_errors = ignore_option_errors
self._dist = dist
+ self._referenced_files: Set[str] = set()
def _ensure_dist(self) -> "Distribution":
from setuptools.dist import Distribution
self._expand_cmdclass(package_dir)
self._expand_all_dynamic(dist, package_dir)
+ dist._referenced_files.update(self._referenced_files)
return self.config
def _expand_packages(self):
with _ignore_errors(self.ignore_option_errors):
root_dir = self.root_dir
if "file" in directive:
+ self._referenced_files.update(directive["file"])
return _expand.read_files(directive["file"], root_dir)
if "attr" in directive:
return _expand.read_attr(directive["attr"], package_dir, root_dir)
from functools import partial
from functools import wraps
from typing import (TYPE_CHECKING, Callable, Any, Dict, Generic, Iterable, List,
- Optional, Tuple, TypeVar, Union)
+ Optional, Set, Tuple, TypeVar, Union)
from distutils.errors import DistutilsOptionError, DistutilsFileError
from setuptools.extern.packaging.requirements import Requirement, InvalidRequirement
distribution.src_root,
)
meta.parse()
+ distribution._referenced_files.update(
+ options._referenced_files, meta._referenced_files
+ )
return meta, options
self.sections = sections
self.set_options: List[str] = []
self.ensure_discovered = ensure_discovered
+ self._referenced_files: Set[str] = set()
+ """After parsing configurations, this property will enumerate
+ all files referenced by the "file:" directive. Private API for setuptools only.
+ """
@property
def parsers(self):
return parser
- @classmethod
- def _parse_file(cls, value, root_dir: _Path):
+ def _parse_file(self, value, root_dir: _Path):
"""Represents value as a string, allowing including text
from nearest files using `file:` directive.
return value
spec = value[len(include_directive) :]
- filepaths = (path.strip() for path in spec.split(','))
+ filepaths = [path.strip() for path in spec.split(',')]
+ self._referenced_files.update(filepaths)
return expand.read_files(filepaths, root_dir)
def _parse_attr(self, value, package_dir, root_dir: _Path):
from glob import iglob
import itertools
import textwrap
-from typing import List, Optional, TYPE_CHECKING
+from typing import List, Optional, Set, TYPE_CHECKING
from pathlib import Path
from collections import defaultdict
},
)
+ # Private API (setuptools-use only, not restricted to Distribution)
+ # Stores files that are referenced by the configuration and need to be in the
+ # sdist (e.g. `version = file: VERSION.txt`)
+ self._referenced_files: Set[str] = set()
+
# Save the original dependencies before they are processed into the egg format
self._orig_extras_require = {}
self._orig_install_requires = []
filename = filename.decode('latin-1')
filename not in cmd.filelist.files
+ _EXAMPLE_DIRECTIVES = {
+ "setup.cfg - long_description and version": """
+ [metadata]
+ name = testing
+ version = file: VERSION.txt
+ license_files = DOWHATYOUWANT
+ long_description = file: README.rst, USAGE.rst
+ """,
+ "pyproject.toml - static readme/license files and dynamic version": """
+ [project]
+ name = "testing"
+ readme = "USAGE.rst"
+ license = {file = "DOWHATYOUWANT"}
+ dynamic = ["version"]
+ [tool.setuptools.dynamic]
+ version = {file = ["VERSION.txt"]}
+ """
+ }
+
+ @pytest.mark.parametrize("config", _EXAMPLE_DIRECTIVES.keys())
+ def test_add_files_referenced_by_config_directives(self, tmp_path, config):
+ config_file, _, _ = config.partition(" - ")
+ config_text = self._EXAMPLE_DIRECTIVES[config]
+ (tmp_path / 'VERSION.txt').write_text("0.42", encoding="utf-8")
+ (tmp_path / 'README.rst').write_text("hello world!", encoding="utf-8")
+ (tmp_path / 'USAGE.rst').write_text("hello world!", encoding="utf-8")
+ (tmp_path / 'DOWHATYOUWANT').write_text("hello world!", encoding="utf-8")
+ (tmp_path / config_file).write_text(config_text, encoding="utf-8")
+
+ dist = Distribution({"packages": []})
+ dist.script_name = 'setup.py'
+ dist.parse_config_files()
+
+ cmd = sdist(dist)
+ cmd.ensure_finalized()
+ with quiet():
+ cmd.run()
+
+ assert 'VERSION.txt' in cmd.filelist.files
+ assert 'USAGE.rst' in cmd.filelist.files
+ assert 'DOWHATYOUWANT' in cmd.filelist.files
+
def test_pyproject_toml_in_sdist(self, tmpdir):
"""
Check if pyproject.toml is included in source distribution if present