--- /dev/null
+[bumpversion]
+current_version = 40.9.0
+commit = True
+tag = True
+
+[bumpversion:file:setup.cfg]
+
python: 2.7
- <<: *latest_py2
env: LANG=C
- - python: pypy
+ - python: pypy2.7-6.0.0
+ dist: xenial
env: DISABLE_COVERAGE=1 # Don't run coverage on pypy (too slow).
- python: pypy3
env: DISABLE_COVERAGE=1
- "! grep pyc setuptools.egg-info/SOURCES.txt"
script:
+ - export NETWORK_REQUIRED=1
- |
( # Run testsuite.
if [ -z "$DISABLE_COVERAGE" ]
+v40.9.0
+-------
+
+* #1675: Added support for ``setup.cfg``-only projects when using the ``setuptools.build_meta`` backend. Projects that have enabled PEP 517 no longer need to have a ``setup.py`` and can use the purely declarative ``setup.cfg`` configuration file instead.
+* #1720: Added support for ``pkg_resources.parse_requirements``-style requirements in ``setup_requires`` when ``setup.py`` is invoked from the ``setuptools.build_meta`` build backend.
+* #1664: Added the path to the ``PKG-INFO`` or ``METADATA`` file in the exception
+ text when the ``Version:`` header can't be found.
+* #1705: Removed some placeholder documentation sections referring to deprecated features.
+
+
v40.8.0
-------
.. image:: https://img.shields.io/pypi/v/setuptools.svg
:target: https://pypi.org/project/setuptools
-.. image:: https://readthedocs.org/projects/setuptools/badge/?version=latest
+.. image:: https://img.shields.io/readthedocs/setuptools/latest.svg
:target: https://setuptools.readthedocs.io
-.. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20build%20%40%20Travis%20CI
+.. image:: https://img.shields.io/travis/pypa/setuptools/master.svg?label=Linux%20CI&logo=travis&logoColor=white
:target: https://travis-ci.org/pypa/setuptools
-.. image:: https://img.shields.io/appveyor/ci/pypa/setuptools/master.svg?label=Windows%20build%20%40%20Appveyor
+.. image:: https://img.shields.io/appveyor/ci/pypa/setuptools/master.svg?label=Windows%20CI&logo=appveyor&logoColor=white
:target: https://ci.appveyor.com/project/pypa/setuptools/branch/master
-.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg
+.. image:: https://img.shields.io/codecov/c/github/pypa/setuptools/master.svg?logo=codecov&logoColor=white
:target: https://codecov.io/gh/pypa/setuptools
-.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg
-
-.. image:: https://tidelift.com/badges/github/pypa/setuptools
+.. image:: https://tidelift.com/badges/github/pypa/setuptools?style=flat
:target: https://tidelift.com/subscription/pkg/pypi-setuptools?utm_source=pypi-setuptools&utm_medium=readme
+.. image:: https://img.shields.io/pypi/pyversions/setuptools.svg
+
See the `Installation Instructions
<https://packaging.python.org/installing/>`_ in the Python Packaging
User's Guide for instructions on installing, upgrading, and uninstalling
submitted directly to the `bug tracker
<https://github.com/pypa/setuptools/issues>`_.
+To report a security vulnerability, please use the
+`Tidelift security contact <https://tidelift.com/security>`_.
+Tidelift will coordinate the fix and disclosure.
+
Code of Conduct
---------------
environment:
APPVEYOR: True
+ NETWORK_REQUIRED: True
CODECOV_ENV: APPVEYOR_JOB_NAME
matrix:
inform the user of the missing program(s).
-Subclassing ``Command``
------------------------
-
-Sorry, this section isn't written yet, and neither is a lot of what's below
-this point.
-
-XXX
-
-
-Reusing ``setuptools`` Code
-===========================
-
-``ez_setup``
-------------
-
-XXX
-
-
-``setuptools.archive_util``
----------------------------
-
-XXX
-
-
-``setuptools.sandbox``
-----------------------
-
-XXX
-
-
-``setuptools.package_index``
-----------------------------
-
-XXX
-
-
Mailing List and Bug Tracker
============================
def has_resource(self, resource_name):
return self._has(self._fn(self.module_path, resource_name))
+ def _get_metadata_path(self, name):
+ return self._fn(self.egg_info, name)
+
def has_metadata(self, name):
- return self.egg_info and self._has(self._fn(self.egg_info, name))
+ if not self.egg_info:
+ return self.egg_info
+
+ path = self._get_metadata_path(name)
+ return self._has(path)
def get_metadata(self, name):
if not self.egg_info:
...
ValueError: Use of .. or absolute path in a resource path \
is not allowed.
+
+ Blank values are allowed
+
+ >>> vrp('')
+ >>> bool(warned)
+ False
+
+ Non-string values are not.
+
+ >>> vrp(None)
+ Traceback (most recent call last):
+ ...
+ AttributeError: ...
"""
invalid = (
os.path.pardir in path.split(posixpath.sep) or
def __init__(self, path):
self.path = path
+ def _get_metadata_path(self, name):
+ return self.path
+
def has_metadata(self, name):
return name == 'PKG-INFO' and os.path.isfile(self.path)
try:
return self._version
except AttributeError:
- version = _version_from_file(self._get_metadata(self.PKG_INFO))
+ version = self._get_version()
if version is None:
- tmpl = "Missing 'Version:' header and/or %s file"
- raise ValueError(tmpl % self.PKG_INFO, self)
+ path = self._get_metadata_path_for_display(self.PKG_INFO)
+ msg = (
+ "Missing 'Version:' header and/or {} file at path: {}"
+ ).format(self.PKG_INFO, path)
+ raise ValueError(msg, self)
+
return version
@property
)
return deps
+ def _get_metadata_path_for_display(self, name):
+ """
+ Return the path to the given metadata file, if available.
+ """
+ try:
+ # We need to access _get_metadata_path() on the provider object
+ # directly rather than through this class's __getattr__()
+ # since _get_metadata_path() is marked private.
+ path = self._provider._get_metadata_path(name)
+
+ # Handle exceptions e.g. in case the distribution's metadata
+ # provider doesn't support _get_metadata_path().
+ except Exception:
+ return '[could not detect]'
+
+ return path
+
def _get_metadata(self, name):
if self.has_metadata(name):
for line in self.get_metadata_lines(name):
yield line
+ def _get_version(self):
+ lines = self._get_metadata(self.PKG_INFO)
+ version = _version_from_file(lines)
+
+ return version
+
def activate(self, path=None, replace=False):
"""Ensure distribution is importable on `path` (default=sys.path)"""
if path is None:
take an extra step and try to get the version number from
the metadata file itself instead of the filename.
"""
- md_version = _version_from_file(self._get_metadata(self.PKG_INFO))
+ md_version = self._get_version()
if md_version:
self._version = md_version
return self
except ImportError:
import mock
+from pkg_resources import DistInfoDistribution, Distribution, EggInfoDistribution
from pkg_resources.extern.six.moves import map
from pkg_resources.extern.six import text_type, string_types
subprocess.check_call(cmd)
+# TODO: remove this in favor of Path.touch() when Python 2 is dropped.
+def touch_file(path):
+ """
+ Create an empty file.
+ """
+ with open(path, 'w'):
+ pass
+
+
+def make_distribution_no_version(tmpdir, basename):
+ """
+ Create a distribution directory with no file containing the version.
+ """
+ # Convert the LocalPath object to a string before joining.
+ dist_dir = os.path.join(str(tmpdir), basename)
+ os.mkdir(dist_dir)
+ # Make the directory non-empty so distributions_from_metadata()
+ # will detect it and yield it.
+ touch_file(os.path.join(dist_dir, 'temp.txt'))
+
+ dists = list(pkg_resources.distributions_from_metadata(dist_dir))
+ assert len(dists) == 1
+ dist, = dists
+
+ return dist, dist_dir
+
+
+@pytest.mark.parametrize(
+ 'suffix, expected_filename, expected_dist_type',
+ [
+ ('egg-info', 'PKG-INFO', EggInfoDistribution),
+ ('dist-info', 'METADATA', DistInfoDistribution),
+ ],
+)
+def test_distribution_version_missing(tmpdir, suffix, expected_filename,
+ expected_dist_type):
+ """
+ Test Distribution.version when the "Version" header is missing.
+ """
+ basename = 'foo.{}'.format(suffix)
+ dist, dist_dir = make_distribution_no_version(tmpdir, basename)
+
+ expected_text = (
+ "Missing 'Version:' header and/or {} file at path: "
+ ).format(expected_filename)
+ metadata_path = os.path.join(dist_dir, expected_filename)
+
+ # Now check the exception raised when the "version" attribute is accessed.
+ with pytest.raises(ValueError) as excinfo:
+ dist.version
+
+ err = str(excinfo)
+ # Include a string expression after the assert so the full strings
+ # will be visible for inspection on failure.
+ assert expected_text in err, str((expected_text, err))
+
+ # Also check the args passed to the ValueError.
+ msg, dist = excinfo.value.args
+ assert expected_text in msg
+ # Check that the message portion contains the path.
+ assert metadata_path in msg, str((metadata_path, msg))
+ assert type(dist) == expected_dist_type
+
+
+def test_distribution_version_missing_undetected_path():
+ """
+ Test Distribution.version when the "Version" header is missing and
+ the path can't be detected.
+ """
+ # Create a Distribution object with no metadata argument, which results
+ # in an empty metadata provider.
+ dist = Distribution('/foo')
+ with pytest.raises(ValueError) as excinfo:
+ dist.version
+
+ msg, dist = excinfo.value.args
+ expected = (
+ "Missing 'Version:' header and/or PKG-INFO file at path: "
+ '[could not detect]'
+ )
+ assert msg == expected
+
+
class TestDeepVersionLookupDistutils:
@pytest.fixture
def env(self, tmpdir):
-[bumpversion]
-current_version = 40.8.0
-commit = True
-tag = True
-
[egg_info]
tag_build = .post
tag_date = 1
[metadata]
license_file = LICENSE
-
-[bumpversion:file:setup.py]
-
+version = 40.9.0
setup_params = dict(
name="setuptools",
- version="40.8.0",
description=(
"Easily download, build, install, upgrade, and uninstall "
"Python packages"
Again, this is not a formal definition! Just a "taste" of the module.
"""
+import io
import os
import sys
import tokenize
import setuptools
import distutils
+from pkg_resources import parse_requirements
+
__all__ = ['get_requires_for_build_sdist',
'get_requires_for_build_wheel',
'prepare_metadata_for_build_wheel',
class Distribution(setuptools.dist.Distribution):
def fetch_build_eggs(self, specifiers):
- raise SetupRequirementsError(specifiers)
+ specifier_list = list(map(str, parse_requirements(specifiers)))
+
+ raise SetupRequirementsError(specifier_list)
@classmethod
@contextlib.contextmanager
return file
+def _open_setup_script(setup_script):
+ if not os.path.exists(setup_script):
+ # Supply a default setup.py
+ return io.StringIO(u"from setuptools import setup; setup()")
+
+ return getattr(tokenize, 'open', open)(setup_script)
+
+
class _BuildMetaBackend(object):
def _fix_config(self, config_settings):
# Correctness comes first, then optimization later
__file__ = setup_script
__name__ = '__main__'
- f = getattr(tokenize, 'open', open)(__file__)
- code = f.read().replace('\\r\\n', '\\n')
- f.close()
+
+ with _open_setup_script(__file__) as f:
+ code = f.read().replace(r'\r\n', r'\n')
+
exec(compile(code, __file__, 'exec'), locals())
def get_requires_for_build_wheel(self, config_settings=None):
def include(self, **attrs):
"""Add items to distribution that are named in keyword arguments
- For example, 'dist.exclude(py_modules=["x"])' would add 'x' to
+ For example, 'dist.include(py_modules=["x"])' would add 'x' to
the distribution's 'py_modules' attribute, if it was not already
there.
return _Distribution.handle_display_options(self, option_order)
# Stdout may be StringIO (e.g. in tests)
- import io
if not isinstance(sys.stdout, io.TextIOWrapper):
return _Distribution.handle_display_options(self, option_order)
print('hello')
"""),
},
+ {
+ 'setup.cfg': DALS("""
+ [metadata]
+ name = foo
+ version='0.0.0'
+
+ [options]
+ py_modules=hello
+ setup_requires=six
+ """),
+ 'hello.py': DALS("""
+ def run():
+ print('hello')
+ """)
+ },
]
# if the setup.py changes subsequent call of the build meta
# should still succeed, given the
# sdist_directory the frontend specifies is empty
- with open(os.path.abspath("setup.py"), 'rt') as file_handler:
+ setup_loc = os.path.abspath("setup.py")
+ if not os.path.exists(setup_loc):
+ setup_loc = os.path.abspath("setup.cfg")
+
+ with open(setup_loc, 'rt') as file_handler:
content = file_handler.read()
- with open(os.path.abspath("setup.py"), 'wt') as file_handler:
+ with open(setup_loc, 'wt') as file_handler:
file_handler.write(
content.replace("version='0.0.0'", "version='0.0.1'"))
with pytest.raises(ImportError):
build_backend.build_sdist("temp")
+ @pytest.mark.parametrize('setup_literal, requirements', [
+ ("'foo'", ['foo']),
+ ("['foo']", ['foo']),
+ (r"'foo\n'", ['foo']),
+ (r"'foo\n\n'", ['foo']),
+ ("['foo', 'bar']", ['foo', 'bar']),
+ (r"'# Has a comment line\nfoo'", ['foo']),
+ (r"'foo # Has an inline comment'", ['foo']),
+ (r"'foo \\\n >=3.0'", ['foo>=3.0']),
+ (r"'foo\nbar'", ['foo', 'bar']),
+ (r"'foo\nbar\n'", ['foo', 'bar']),
+ (r"['foo\n', 'bar\n']", ['foo', 'bar']),
+ ])
+ @pytest.mark.parametrize('use_wheel', [True, False])
+ def test_setup_requires(self, setup_literal, requirements, use_wheel,
+ tmpdir_cwd):
+
+ files = {
+ 'setup.py': DALS("""
+ from setuptools import setup
+
+ setup(
+ name="qux",
+ version="0.0.0",
+ py_modules=["hello.py"],
+ setup_requires={setup_literal},
+ )
+ """).format(setup_literal=setup_literal),
+ 'hello.py': DALS("""
+ def run():
+ print('hello')
+ """),
+ }
+
+ build_files(files)
+
+ build_backend = self.get_build_backend()
+
+ if use_wheel:
+ base_requirements = ['wheel']
+ get_requires = build_backend.get_requires_for_build_wheel
+ else:
+ base_requirements = []
+ get_requires = build_backend.get_requires_for_build_sdist
+
+ # Ensure that the build requirements are properly parsed
+ expected = sorted(base_requirements + requirements)
+ actual = get_requires()
+
+ assert expected == sorted(actual)
+
class TestBuildMetaLegacyBackend(TestBuildMetaBackend):
backend_name = 'setuptools.build_meta:__legacy__'
)).format(source=SOURCE_DIR))
-def test_pip_upgrade_from_source(virtualenv):
+def _get_pip_versions():
+ # This fixture will attempt to detect if tests are being run without
+ # network connectivity and if so skip some tests
+
+ network = True
+ if not os.environ.get('NETWORK_REQUIRED', False): # pragma: nocover
+ try:
+ from urllib.request import urlopen
+ from urllib.error import URLError
+ except ImportError:
+ from urllib2 import urlopen, URLError # Python 2.7 compat
+
+ try:
+ urlopen('https://pypi.org', timeout=1)
+ except URLError:
+ # No network, disable most of these tests
+ network = False
+
+ network_versions = [
+ 'pip==9.0.3',
+ 'pip==10.0.1',
+ 'pip==18.1',
+ 'pip==19.0.1',
+ '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
+ ]
+
+ return versions
+
+
+@pytest.mark.parametrize('pip_version', _get_pip_versions())
+def test_pip_upgrade_from_source(pip_version, virtualenv):
"""
Check pip can upgrade setuptools from source.
"""
+ # Install pip/wheel, and remove setuptools (as it
+ # should not be needed for bootstraping from source)
+ if pip_version is None:
+ upgrade_pip = ()
+ else:
+ upgrade_pip = ('python -m pip install -U {pip_version} --retries=1',)
+ virtualenv.run(' && '.join((
+ 'pip uninstall -y setuptools',
+ 'pip install -U wheel',
+ ) + upgrade_pip).format(pip_version=pip_version))
dist_dir = virtualenv.workspace
# Generate source distribution / wheel.
virtualenv.run(' && '.join((
setenv=COVERAGE_FILE={toxworkdir}/.coverage.{envname}
# TODO: The passed environment variables came from copying other tox.ini files
# These should probably be individually annotated to explain what needs them.
-passenv=APPDATA HOMEDRIVE HOMEPATH windir APPVEYOR APPVEYOR_* CI CODECOV_* TRAVIS TRAVIS_*
+passenv=APPDATA HOMEDRIVE HOMEPATH windir APPVEYOR APPVEYOR_* CI CODECOV_* TRAVIS TRAVIS_* NETWORK_REQUIRED
commands=pytest --cov-config={toxinidir}/tox.ini --cov-report= {posargs}
usedevelop=True