[bumpversion]
-current_version = 53.1.0
+current_version = 54.0.0
commit = True
tag = True
+v54.0.0
+-------
+
+
+Breaking Changes
+^^^^^^^^^^^^^^^^
+* #2582: Simplified build-from-source story by providing bootstrapping metadata in a separate egg-info directory. Build requirements no longer include setuptools itself. Sdist once again includes the pyproject.toml. Project can no longer be installed from source on pip 19.x, but install from source is still supported on pip < 19 and pip >= 20 and install from wheel is still supported with pip >= 9.
+
+Changes
+^^^^^^^
+* #1932: Handled :code:`AttributeError` by raising :code:`DistutilsSetupError` in :code:`dist.check_specifier()` when specifier is not a string -- by :user:`melissa-kun-li`
+* #2570: Correctly parse cmdclass in setup.cfg.
+
+Documentation changes
+^^^^^^^^^^^^^^^^^^^^^
+* #2553: Added userguide example for markers in extras_require -- by :user:`pwoolvett`
+
+
v53.1.0
-------
Changes
^^^^^^^
* #1937: Preserved case-sensitivity of keys in setup.cfg so that entry point names are case-sensitive. Changed sensitivity of configparser. NOTE: Any projects relying on case-insensitivity will need to adapt to accept the original case as published. -- by :user:`melissa-kun-li`
-* #2573: Fixed error in uploading a Sphinx doc with the :code:`upload_docs` command. An html builder will be used.
+* #2573: Fixed error in uploading a Sphinx doc with the :code:`upload_docs` command. An html builder will be used.
Note: :code:`upload_docs` is deprecated for PyPi, but is supported for other sites -- by :user:`melissa-kun-li`
include msvc-build-launcher.cmd
include pytest.ini
include tox.ini
-exclude pyproject.toml # Temporary workaround for #1644.
--- /dev/null
+Name: setuptools-bootstrap
+Version: 1.0
--- /dev/null
+[distutils.commands]
+egg_info = setuptools.command.egg_info:egg_info
+
+[distutils.setup_keywords]
+include_package_data = setuptools.dist:assert_bool
+install_requires = setuptools.dist:check_requirements
+extras_require = setuptools.dist:check_extras
+entry_points = setuptools.dist:check_entry_points
+
+[egg_info.writers]
+PKG-INFO = setuptools.command.egg_info:write_pkg_info
+dependency_links.txt = setuptools.command.egg_info:overwrite_arg
+entry_points.txt = setuptools.command.egg_info:write_entries
+requires.txt = setuptools.command.egg_info:write_requirements
**find_namespace directive** - The ``find_namespace:`` directive is supported since Python >=3.3.
+
Notes:
1. In the ``package_data`` section, a key named with a single asterisk (``*``)
refers to all packages, in lieu of the empty string used in ``setup.py``.
+
+2. In the ``extras_require`` section, values are parsed as ``list-semi``. This implies that in
+order to include markers, they **must** be *dangling*:
+
+.. code-block:: ini
+
+ [options.extras_require]
+ rest = docutils>=0.3; pack ==1.1, ==1.3
+ pdf =
+ ReportLab>=1.2
+ RXP
+ importlib-metadata; python_version < "3.8"
[build-system]
requires = [
- "setuptools >= 40.8; python_version > '3'",
"wheel",
]
build-backend = "setuptools.build_meta"
license_files =
LICENSE
name = setuptools
-version = 53.1.0
+version = 54.0.0
author = Python Packaging Authority
author_email = distutils-sig@python.org
description = Easily download, build, install, upgrade, and uninstall Python packages
testing =
# upstream
pytest >= 3.5, !=3.7.3
- pytest-checkdocs >= 1.2.3
+ pytest-checkdocs >= 2.4
pytest-flake8
pytest-black >= 0.3.7; python_implementation != "PyPy"
pytest-cov
jaraco.envs
pytest-xdist
sphinx
+ jaraco.path>=3.2.0
docs =
# Keep these in sync with docs/requirements.txt
parse_list_semicolon = partial(self._parse_list, separator=';')
parse_bool = self._parse_bool
parse_dict = self._parse_dict
+ parse_cmdclass = self._parse_cmdclass
return {
'zip_safe': parse_bool,
'entry_points': self._parse_file,
'py_modules': parse_list,
'python_requires': SpecifierSet,
+ 'cmdclass': parse_cmdclass,
+ }
+
+ def _parse_cmdclass(self, value):
+ def resolve_class(qualified_class_name):
+ idx = qualified_class_name.rfind('.')
+ class_name = qualified_class_name[idx+1:]
+ pkg_name = qualified_class_name[:idx]
+
+ module = __import__(pkg_name)
+
+ return getattr(module, class_name)
+
+ return {
+ k: resolve_class(v)
+ for k, v in self._parse_dict(value).items()
}
def _parse_packages(self, value):
"""Verify that value is a valid version specifier"""
try:
packaging.specifiers.SpecifierSet(value)
- except packaging.specifiers.InvalidSpecifier as error:
+ except (packaging.specifiers.InvalidSpecifier, AttributeError) as error:
tmpl = (
"{attr!r} must be a string "
"containing valid version specifiers; {error}"
+++ /dev/null
-import os
-
-
-def build_files(file_defs, prefix=""):
- """
- Build a set of files/directories, as described by the
- file_defs dictionary.
-
- Each key/value pair in the dictionary is interpreted as
- a filename/contents
- pair. If the contents value is a dictionary, a directory
- is created, and the
- dictionary interpreted as the files within it, recursively.
-
- For example:
-
- {"README.txt": "A README file",
- "foo": {
- "__init__.py": "",
- "bar": {
- "__init__.py": "",
- },
- "baz.py": "# Some code",
- }
- }
- """
- for name, contents in file_defs.items():
- full_name = os.path.join(prefix, name)
- if isinstance(contents, dict):
- os.makedirs(full_name, exist_ok=True)
- build_files(contents, prefix=full_name)
- else:
- if isinstance(contents, bytes):
- with open(full_name, 'wb') as f:
- f.write(contents)
- else:
- with open(full_name, 'w') as f:
- f.write(contents)
import distutils.command.build_ext as orig
from distutils.sysconfig import get_config_var
+from jaraco import path
+
from setuptools.command.build_ext import build_ext, get_abi3_suffix
from setuptools.dist import Distribution
from setuptools.extension import Extension
from . import environment
-from .files import build_files
from .textwrap import DALS
build-base = foo_build
"""),
}
- build_files(files)
+ path.build(files)
code, output = environment.run_setup_py(
cmd=['build'], data_stream=(0, 2),
)
from concurrent import futures
import pytest
+from jaraco import path
-from .files import build_files
from .textwrap import DALS
@pytest.fixture(params=defns)
def build_backend(self, tmpdir, request):
- build_files(request.param, prefix=str(tmpdir))
+ path.build(request.param, prefix=str(tmpdir))
with tmpdir.as_cwd():
yield self.get_build_backend()
"""),
}
- build_files(files)
+ path.build(files)
dist_dir = os.path.abspath('preexisting-' + build_type)
build-backend = "setuptools.build_meta
"""),
}
- build_files(files)
+ path.build(files)
build_backend = self.get_build_backend()
targz_path = build_backend.build_sdist("temp")
with tarfile.open(os.path.join("temp", targz_path)) as tar:
def test_build_sdist_setup_py_exists(self, tmpdir_cwd):
# If build_sdist is called from a script other than setup.py,
# ensure setup.py is included
- build_files(defns[0])
+ path.build(defns[0])
build_backend = self.get_build_backend()
targz_path = build_backend.build_sdist("temp")
""")
}
- build_files(files)
+ path.build(files)
build_backend = self.get_build_backend()
targz_path = build_backend.build_sdist("temp")
""")
}
- build_files(files)
+ path.build(files)
build_backend = self.get_build_backend()
build_backend.build_sdist("temp")
}
def test_build_sdist_relative_path_import(self, tmpdir_cwd):
- build_files(self._relative_path_import_files)
+ path.build(self._relative_path_import_files)
build_backend = self.get_build_backend()
with pytest.raises(ImportError, match="^No module named 'hello'$"):
build_backend.build_sdist("temp")
"""),
}
- build_files(files)
+ path.build(files)
build_backend = self.get_build_backend()
"""),
}
- build_files(files)
+ path.build(files)
build_backend = self.get_build_backend()
}
def test_sys_argv_passthrough(self, tmpdir_cwd):
- build_files(self._sys_argv_0_passthrough)
+ path.build(self._sys_argv_0_passthrough)
build_backend = self.get_build_backend()
with pytest.raises(AssertionError):
build_backend.build_sdist("temp")
# build_meta_legacy-specific tests
def test_build_sdist_relative_path_import(self, tmpdir_cwd):
# This must fail in build_meta, but must pass in build_meta_legacy
- build_files(self._relative_path_import_files)
+ path.build(self._relative_path_import_files)
build_backend = self.get_build_backend()
build_backend.build_sdist("temp")
def test_sys_argv_passthrough(self, tmpdir_cwd):
- build_files(self._sys_argv_0_passthrough)
+ path.build(self._sys_argv_0_passthrough)
build_backend = self.get_build_backend()
build_backend.build_sdist("temp")
+import types
+import sys
+
import contextlib
import configparser
from mock import patch
from setuptools.dist import Distribution, _Distribution
from setuptools.config import ConfigHandler, read_configuration
+from distutils.core import Command
from .textwrap import DALS
with get_dist(tmpdir) as dist:
dist.parse_config_files()
+ def test_cmdclass(self, tmpdir):
+ class CustomCmd(Command):
+ pass
+
+ m = types.ModuleType('custom_build', 'test package')
+
+ m.__dict__['CustomCmd'] = CustomCmd
+
+ sys.modules['custom_build'] = m
+
+ fake_env(
+ tmpdir,
+ '[options]\n'
+ 'cmdclass =\n'
+ ' customcmd = custom_build.CustomCmd\n'
+ )
+
+ with get_dist(tmpdir) as dist:
+ assert dist.cmdclass == {'customcmd': CustomCmd}
+
saved_dist_init = _Distribution.__init__
_get_unpatched,
check_package_data,
DistDeprecationWarning,
+ check_specifier,
)
from setuptools import sic
from setuptools import Distribution
with pytest.raises(
DistutilsSetupError, match=re.escape(expected_message)):
check_package_data(None, str('package_data'), package_data)
+
+
+def test_check_specifier():
+ # valid specifier value
+ attrs = {'name': 'foo', 'python_requires': '>=3.0, !=3.1'}
+ dist = Distribution(attrs)
+ check_specifier(dist, attrs, attrs['python_requires'])
+
+ # invalid specifier value
+ attrs = {'name': 'foo', 'python_requires': ['>=3.0', '!=3.1']}
+ with pytest.raises(DistutilsSetupError):
+ dist = Distribution(attrs)
import subprocess
import pytest
+from jaraco import path
from setuptools import sandbox
from setuptools.sandbox import run_setup
import pkg_resources
from . import contexts
-from .files import build_files
from .textwrap import DALS
# Create source tree for `dep`.
dep_pkg = os.path.join(temp_dir, 'dep')
os.mkdir(dep_pkg)
- build_files({
+ path.build({
'setup.py':
DALS("""
import setuptools
import stat
import time
+import pytest
+from jaraco import path
+
from setuptools.command.egg_info import (
egg_info, manifest_maker, EggInfoDeprecationWarning, get_pkg_info_revision,
)
from setuptools.dist import Distribution
-import pytest
-
from . import environment
-from .files import build_files
from .textwrap import DALS
from . import contexts
""")
def _create_project(self):
- build_files({
+ path.build({
'setup.py': self.setup_script,
'hello.py': DALS("""
def run():
for dirname in subs
)
list(map(os.mkdir, env.paths.values()))
- build_files({
+ path.build({
env.paths['home']: {
'.pydistutils.cfg': DALS("""
[egg_info]
the file should remain unchanged.
"""
setup_cfg = os.path.join(env.paths['home'], 'setup.cfg')
- build_files({
+ path.build({
setup_cfg: DALS("""
[egg_info]
tag_build =
setup()
""")
- build_files({'setup.py': setup_script,
- 'setup.cfg': setup_config})
+ path.build({
+ 'setup.py': setup_script,
+ 'setup.cfg': setup_config,
+ })
# This command should fail with a ValueError, but because it's
# currently configured to use a subprocess, the actual traceback
def test_manifest_template_is_read(self, tmpdir_cwd, env):
self._create_project()
- build_files({
+ path.build({
'MANIFEST.in': DALS("""
recursive-include docs *.rst
"""),
'''
) % ('' if use_setup_cfg else requires)
setup_config = requires if use_setup_cfg else ''
- build_files({'setup.py': setup_script,
- 'setup.cfg': setup_config})
+ path.build({
+ 'setup.py': setup_script,
+ 'setup.cfg': setup_config,
+ })
mismatch_marker = "python_version<'{this_ver}'".format(
this_ver=sys.version_info[0],
def test_setup_cfg_license_file(
self, tmpdir_cwd, env, files, license_in_sources):
self._create_project()
- build_files(files)
+ path.build(files)
environment.run_setup_py(
cmd=['egg_info'],
def test_setup_cfg_license_files(
self, tmpdir_cwd, env, files, incl_licenses, excl_licenses):
self._create_project()
- build_files(files)
+ path.build(files)
environment.run_setup_py(
cmd=['egg_info'],
def test_setup_cfg_license_file_license_files(
self, tmpdir_cwd, env, files, incl_licenses, excl_licenses):
self._create_project()
- build_files(files)
+ path.build(files)
environment.run_setup_py(
cmd=['egg_info'],
def test_egg_info_tag_only_once(self, tmpdir_cwd, env):
self._create_project()
- build_files({
+ path.build({
'setup.cfg': DALS("""
[egg_info]
tag_build = dev
import pytest
+from jaraco import path
from setuptools.glob import glob
-from .files import build_files
-
@pytest.mark.parametrize('tree, pattern, matches', (
('', b'', []),
))
def test_glob(monkeypatch, tmpdir, tree, pattern, matches):
monkeypatch.chdir(tmpdir)
- build_files({name: '' for name in tree.split()})
+ path.build({name: '' for name in tree.split()})
assert list(sorted(glob(pattern))) == list(sorted(matches))
import pytest
-import os
+
+from jaraco import path
from setuptools.command.upload_docs import upload_docs
from setuptools.dist import Distribution
@pytest.fixture
def sphinx_doc_sample_project(tmpdir_cwd):
- # setup.py
- with open('setup.py', 'wt') as f:
- f.write('from setuptools import setup; setup()\n')
-
- os.makedirs('build/docs')
-
- # A test conf.py for Sphinx
- with open('build/docs/conf.py', 'w') as f:
- f.write("project = 'test'")
-
- # A test index.rst for Sphinx
- with open('build/docs/index.rst', 'w') as f:
- f.write(".. toctree::\
+ path.build({
+ 'setup.py': 'from setuptools import setup; setup()',
+ 'build': {
+ 'docs': {
+ 'conf.py': 'project="test"',
+ 'index.rst': ".. toctree::\
:maxdepth: 2\
- :caption: Contents:")
+ :caption: Contents:",
+ },
+ },
+ })
@pytest.mark.usefixtures('sphinx_doc_sample_project')
import contextlib
import pytest
+from jaraco import path
from setuptools.command.upload_docs import upload_docs
from setuptools.dist import Distribution
from .textwrap import DALS
from . import contexts
-SETUP_PY = DALS(
- """
- from setuptools import setup
-
- setup(name='foo')
- """)
-
@pytest.fixture
def sample_project(tmpdir_cwd):
- # setup.py
- with open('setup.py', 'wt') as f:
- f.write(SETUP_PY)
-
- os.mkdir('build')
-
- # A test document.
- with open('build/index.html', 'w') as f:
- f.write("Hello world.")
-
- # An empty folder.
- os.mkdir('build/empty')
+ path.build({
+ 'setup.py': DALS("""
+ from setuptools import setup
+
+ setup(name='foo')
+ """),
+ 'build': {
+ 'index.html': 'Hello world.',
+ 'empty': {},
+ }
+ })
@pytest.mark.usefixtures('sample_project')
import glob
import os
import sys
+import itertools
import pathlib
# No network, disable most of these tests
network = False
+ def mark(param, *marks):
+ if not isinstance(param, type(pytest.param(''))):
+ param = pytest.param(param)
+ return param._replace(marks=param.marks + marks)
+
+ def skip_network(param):
+ return param if network else mark(param, pytest.mark.skip(reason="no network"))
+
network_versions = [
'pip==9.0.3',
'pip==10.0.1',
'pip==18.1',
- 'pip==19.0.1',
+ mark('pip==19.3.1', pytest.mark.xfail(reason='pypa/pip#6599')),
+ 'pip==20.0.2',
'https://github.com/pypa/pip/archive/master.zip',
]
- versions = [None] + [
- pytest.param(v, **({} if network else {'marks': pytest.mark.skip}))
- for v in network_versions
- ]
+ versions = itertools.chain(
+ [None],
+ map(skip_network, network_versions)
+ )
- return versions
+ return list(versions)
@pytest.mark.parametrize('pip_version', _get_pip_versions())
import zipfile
import pytest
+from jaraco import path
from pkg_resources import Distribution, PathMetadata, PY_MAJOR
from setuptools.extern.packaging.utils import canonicalize_name
from setuptools.wheel import Wheel
from .contexts import tempdir
-from .files import build_files
from .textwrap import DALS
if extra_file_defs:
file_defs.update(extra_file_defs)
with tempdir() as source_dir:
- build_files(file_defs, source_dir)
+ path.build(file_defs, source_dir)
subprocess.check_call((sys.executable, 'setup.py',
'-q', 'bdist_wheel'), cwd=source_dir)
yield glob.glob(os.path.join(source_dir, 'dist', '*.whl'))[0]