Commit 0551421f authored by Jason R. Coombs's avatar Jason R. Coombs Committed by GitHub

Merge branch 'master' into license-fix-357

parents 28872fc9 78fd7302
pull_request_rules:
- name: auto-merge
conditions:
- base=master
- label=auto-merge
- status-success=continuous-integration/appveyor/pr
- status-success=continuous-integration/travis-ci/pr
- status-success=deploy/netlify
actions:
merge:
method: merge
...@@ -30,7 +30,9 @@ jobs: ...@@ -30,7 +30,9 @@ jobs:
install: skip install: skip
script: skip script: skip
after_success: true after_success: true
before_deploy: python bootstrap.py before_deploy:
- python bootstrap.py
- "! grep pyc setuptools.egg-info/SOURCES.txt"
deploy: deploy:
provider: pypi provider: pypi
on: on:
...@@ -58,6 +60,7 @@ install: ...@@ -58,6 +60,7 @@ install:
# update egg_info based on setup.py in checkout # update egg_info based on setup.py in checkout
- python bootstrap.py - python bootstrap.py
- "! grep pyc setuptools.egg-info/SOURCES.txt"
script: script:
- | - |
......
v40.7.0
-------
* #1551: File inputs for the `license` field in `setup.cfg` files now explicitly raise an error.
* #1180: Add support for non-ASCII in setup.cfg (#1062). Add support for native strings on some parameters (#1136).
* #1499: ``setuptools.package_index`` no longer relies on the deprecated ``urllib.parse.splituser`` per Python #27485.
* #1544: Added tests for PackageIndex.download (for git URLs).
* #1625: In PEP 517 build_meta builder, ensure that sdists are built as gztar per the spec.
v40.6.3
-------
* #1594: PEP 517 backend no longer declares setuptools as a dependency as it can be assumed.
v40.6.2
-------
* #1592: Fix invalid dependency on external six module (instead of vendored version).
v40.6.1
-------
* #1590: Fixed regression where packages without ``author`` or ``author_email`` fields generated malformed package metadata.
v40.6.0
-------
* #1541: Officially deprecated the ``requires`` parameter in ``setup()``.
* #1519: In ``pkg_resources.normalize_path``, additional path normalization is now performed to ensure path values to a directory is always the same, preventing false positives when checking scripts have a consistent prefix to set up on Windows.
* #1545: Changed the warning class of all deprecation warnings; deprecation warning classes are no longer derived from ``DeprecationWarning`` and are thus visible by default.
* #1554: ``build_meta.build_sdist`` now includes ``setup.py`` in source distributions by default.
* #1576: Started monkey-patching ``get_metadata_version`` and ``read_pkg_file`` onto ``distutils.DistributionMetadata`` to retain the correct version on the ``PKG-INFO`` file in the (deprecated) ``upload`` command.
* #1533: Restricted the ``recursive-include setuptools/_vendor`` to contain only .py and .txt files.
* #1395: Changed Pyrex references to Cython in the documentation.
* #1456: Documented that the ``rpmbuild`` packages is required for the ``bdist_rpm`` command.
* #1537: Documented how to use ``setup.cfg`` for ``src/ layouts``
* #1539: Added minimum version column in ``setup.cfg`` metadata table.
* #1552: Fixed a minor typo in the python 2/3 compatibility documentation.
* #1553: Updated installation instructions to point to ``pip install`` instead of ``ez_setup.py``.
* #1560: Updated ``setuptools`` distribution documentation to remove some outdated information.
* #1564: Documented ``setup.cfg`` minimum version for version and project_urls.
* #1572: Added the ``concurrent.futures`` backport ``futures`` to the Python 2.7 test suite requirements.
v40.5.0 v40.5.0
------- -------
......
...@@ -2,7 +2,7 @@ recursive-include setuptools *.py *.exe *.xml ...@@ -2,7 +2,7 @@ recursive-include setuptools *.py *.exe *.xml
recursive-include tests *.py recursive-include tests *.py
recursive-include setuptools/tests *.html recursive-include setuptools/tests *.html
recursive-include docs *.py *.txt *.conf *.css *.css_t Makefile indexsidebar.html recursive-include docs *.py *.txt *.conf *.css *.css_t Makefile indexsidebar.html
recursive-include setuptools/_vendor * recursive-include setuptools/_vendor *.py *.txt
recursive-include pkg_resources *.py *.txt recursive-include pkg_resources *.py *.txt
include *.py include *.py
include *.rst include *.rst
......
...@@ -23,10 +23,6 @@ See the `Installation Instructions ...@@ -23,10 +23,6 @@ See the `Installation Instructions
User's Guide for instructions on installing, upgrading, and uninstalling User's Guide for instructions on installing, upgrading, and uninstalling
Setuptools. Setuptools.
The project is `maintained at GitHub <https://github.com/pypa/setuptools>`_
by the `Setuptools Developers
<https://github.com/orgs/pypa/teams/setuptools-developers>`_.
Questions and comments should be directed to the `distutils-sig Questions and comments should be directed to the `distutils-sig
mailing list <http://mail.python.org/pipermail/distutils-sig/>`_. mailing list <http://mail.python.org/pipermail/distutils-sig/>`_.
Bug reports and especially tested patches may be Bug reports and especially tested patches may be
......
...@@ -5,4 +5,11 @@ ...@@ -5,4 +5,11 @@
<h3>Questions? Suggestions? Contributions?</h3> <h3>Questions? Suggestions? Contributions?</h3>
<p>Visit the <a href="https://github.com/pypa/setuptools">Setuptools project page</a> </p> <p>Visit the <a href="{{ package_url }}">Project page</a> </p>
<h3 class="donation">Professional support</h3>
<p>
Professionally-supported {{ project }} is available with the
<a href="https://tidelift.com/subscription/pkg/pypi-{{ project }}?utm_source=pypi-{{ project }}&utm_medium=readme">Tidelift Subscription</a>.
</p>
...@@ -67,8 +67,8 @@ All PRs with code changes should include tests. All changes should include a ...@@ -67,8 +67,8 @@ All PRs with code changes should include tests. All changes should include a
changelog entry. changelog entry.
``setuptools`` uses `towncrier <https://pypi.org/project/towncrier/>`_ ``setuptools`` uses `towncrier <https://pypi.org/project/towncrier/>`_
for changelog managment, so when making a PR, please add a news fragment in the for changelog management, so when making a PR, please add a news fragment in the
``changelog.d/`` folder. Changelog files are written in Restructured Text and ``changelog.d/`` folder. Changelog files are written in reStructuredText and
should be a 1 or 2 sentence description of the substantive changes in the PR. should be a 1 or 2 sentence description of the substantive changes in the PR.
They should be named ``<pr_number>.<category>.rst``, where the categories are: They should be named ``<pr_number>.<category>.rst``, where the categories are:
...@@ -76,7 +76,7 @@ They should be named ``<pr_number>.<category>.rst``, where the categories are: ...@@ -76,7 +76,7 @@ They should be named ``<pr_number>.<category>.rst``, where the categories are:
- ``breaking``: Any backwards-compatibility breaking change - ``breaking``: Any backwards-compatibility breaking change
- ``doc``: A change to the documentation - ``doc``: A change to the documentation
- ``misc``: Changes internal to the repo like CI, test and build changes - ``misc``: Changes internal to the repo like CI, test and build changes
- ``deprecation``: For deprecations of an existing feature of behavior - ``deprecation``: For deprecations of an existing feature or behavior
A pull request may have more than one of these components, for example a code A pull request may have more than one of these components, for example a code
change may introduce a new feature that deprecates an old feature, in which change may introduce a new feature that deprecates an old feature, in which
...@@ -89,6 +89,17 @@ code changes. See the following for an example news fragment: ...@@ -89,6 +89,17 @@ code changes. See the following for an example news fragment:
$ cat changelog.d/1288.change.rst $ cat changelog.d/1288.change.rst
Add support for maintainer in PKG-INFO Add support for maintainer in PKG-INFO
-------------------
Auto-Merge Requests
-------------------
To support running all code through CI, even lightweight contributions,
the project employs Mergify to auto-merge pull requests tagged as
auto-merge.
Use ``hub pull-request -l auto-merge`` to create such a pull request
from the command line after pushing a new branch.
------- -------
Testing Testing
------- -------
......
:orphan:
``ez_setup`` distribution guide
===============================
Using ``setuptools``... Without bundling it!
---------------------------------------------
.. warning:: **ez_setup** is deprecated in favor of PIP with **PEP-518** support.
.. _ez_setup.py: https://bootstrap.pypa.io/ez_setup.py
.. _EasyInstall Installation Instructions: easy_install.html
.. _Custom Installation Locations: easy_install.html
Your users might not have ``setuptools`` installed on their machines, or even
if they do, it might not be the right version. Fixing this is easy; just
download `ez_setup.py`_, and put it in the same directory as your ``setup.py``
script. (Be sure to add it to your revision control system, too.) Then add
these two lines to the very top of your setup script, before the script imports
anything from setuptools:
.. code-block:: python
import ez_setup
ez_setup.use_setuptools()
That's it. The ``ez_setup`` module will automatically download a matching
version of ``setuptools`` from PyPI, if it isn't present on the target system.
Whenever you install an updated version of setuptools, you should also update
your projects' ``ez_setup.py`` files, so that a matching version gets installed
on the target machine(s).
(By the way, if you need to distribute a specific version of ``setuptools``,
you can specify the exact version and base download URL as parameters to the
``use_setuptools()`` function. See the function's docstring for details.)
What Your Users Should Know
---------------------------
In general, a setuptools-based project looks just like any distutils-based
project -- as long as your users have an internet connection and are installing
to ``site-packages``, that is. But for some users, these conditions don't
apply, and they may become frustrated if this is their first encounter with
a setuptools-based project. To keep these users happy, you should review the
following topics in your project's installation instructions, if they are
relevant to your project and your target audience isn't already familiar with
setuptools and ``easy_install``.
Network Access
If your project is using ``ez_setup``, you should inform users of the
need to either have network access, or to preinstall the correct version of
setuptools using the `EasyInstall installation instructions`_. Those
instructions also have tips for dealing with firewalls as well as how to
manually download and install setuptools.
Custom Installation Locations
You should inform your users that if they are installing your project to
somewhere other than the main ``site-packages`` directory, they should
first install setuptools using the instructions for `Custom Installation
Locations`_, before installing your project.
Your Project's Dependencies
If your project depends on other projects that may need to be downloaded
from PyPI or elsewhere, you should list them in your installation
instructions, or tell users how to find out what they are. While most
users will not need this information, any users who don't have unrestricted
internet access may have to find, download, and install the other projects
manually. (Note, however, that they must still install those projects
using ``easy_install``, or your project will not know they are installed,
and your setup script will try to download them again.)
If you want to be especially friendly to users with limited network access,
you may wish to build eggs for your project and its dependencies, making
them all available for download from your site, or at least create a page
with links to all of the needed eggs. In this way, users with limited
network access can manually download all the eggs to a single directory,
then use the ``-f`` option of ``easy_install`` to specify the directory
to find eggs in. Users who have full network access can just use ``-f``
with the URL of your download page, and ``easy_install`` will find all the
needed eggs using your links directly. This is also useful when your
target audience isn't able to compile packages (e.g. most Windows users)
and your package or some of its dependencies include C code.
Revision Control System Users and Co-Developers
Users and co-developers who are tracking your in-development code using
a revision control system should probably read this manual's sections
regarding such development. Alternately, you may wish to create a
quick-reference guide containing the tips from this manual that apply to
your particular situation. For example, if you recommend that people use
``setup.py develop`` when tracking your in-development code, you should let
them know that this needs to be run after every update or commit.
Similarly, if you remove modules or data files from your project, you
should remind them to run ``setup.py clean --all`` and delete any obsolete
``.pyc`` or ``.pyo``. (This tip applies to the distutils in general, not
just setuptools, but not everybody knows about them; be kind to your users
by spelling out your project's best practices rather than leaving them
guessing.)
Creating System Packages
Some users want to manage all Python packages using a single package
manager, and sometimes that package manager isn't ``easy_install``!
Setuptools currently supports ``bdist_rpm``, ``bdist_wininst``, and
``bdist_dumb`` formats for system packaging. If a user has a locally-
installed "bdist" packaging tool that internally uses the distutils
``install`` command, it should be able to work with ``setuptools``. Some
examples of "bdist" formats that this should work with include the
``bdist_nsi`` and ``bdist_msi`` formats for Windows.
However, packaging tools that build binary distributions by running
``setup.py install`` on the command line or as a subprocess will require
modification to work with setuptools. They should use the
``--single-version-externally-managed`` option to the ``install`` command,
combined with the standard ``--root`` or ``--record`` options.
See the `install command`_ documentation below for more details. The
``bdist_deb`` command is an example of a command that currently requires
this kind of patching to work with setuptools.
Please note that building system packages may require you to install
some system software, for example ``bdist_rpm`` requires the ``rpmbuild``
command to be installed.
If you or your users have a problem building a usable system package for
your project, please report the problem via the mailing list so that
either the "bdist" tool in question or setuptools can be modified to
resolve the issue.
Your users might not have ``setuptools`` installed on their machines, or even
if they do, it might not be the right version. Fixing this is easy; just
download `ez_setup.py`_, and put it in the same directory as your ``setup.py``
script. (Be sure to add it to your revision control system, too.) Then add
these two lines to the very top of your setup script, before the script imports
anything from setuptools:
.. code-block:: python
import ez_setup
ez_setup.use_setuptools()
That's it. The ``ez_setup`` module will automatically download a matching
version of ``setuptools`` from PyPI, if it isn't present on the target system.
Whenever you install an updated version of setuptools, you should also update
your projects' ``ez_setup.py`` files, so that a matching version gets installed
on the target machine(s).
(By the way, if you need to distribute a specific version of ``setuptools``,
you can specify the exact version and base download URL as parameters to the
``use_setuptools()`` function. See the function's docstring for details.)
.. _install command:
``install`` - Run ``easy_install`` or old-style installation
============================================================
The setuptools ``install`` command is basically a shortcut to run the
``easy_install`` command on the current project. However, for convenience
in creating "system packages" of setuptools-based projects, you can also
use this option:
``--single-version-externally-managed``
This boolean option tells the ``install`` command to perform an "old style"
installation, with the addition of an ``.egg-info`` directory so that the
installed project will still have its metadata available and operate
normally. If you use this option, you *must* also specify the ``--root``
or ``--record`` options (or both), because otherwise you will have no way
to identify and remove the installed files.
This option is automatically in effect when ``install`` is invoked by another
distutils command, so that commands like ``bdist_wininst`` and ``bdist_rpm``
will create system packages of eggs. It is also automatically in effect if
you specify the ``--root`` option.
``install_egg_info`` - Install an ``.egg-info`` directory in ``site-packages``
==============================================================================
Setuptools runs this command as part of ``install`` operations that use the
``--single-version-externally-managed`` options. You should not invoke it
directly; it is documented here for completeness and so that distutils
extensions such as system package builders can make use of it. This command
has only one option:
``--install-dir=DIR, -d DIR``
The parent directory where the ``.egg-info`` directory will be placed.
Defaults to the same as the ``--install-dir`` option specified for the
``install_lib`` command, which is usually the system ``site-packages``
directory.
This command assumes that the ``egg_info`` command has been given valid options
via the command line or ``setup.cfg``, as it will invoke the ``egg_info``
command and use its options to locate the project's source ``.egg-info``
directory.
...@@ -17,9 +17,9 @@ Documentation content: ...@@ -17,9 +17,9 @@ Documentation content:
:maxdepth: 2 :maxdepth: 2
setuptools setuptools
easy_install
pkg_resources pkg_resources
python3 python3
development development
roadmap roadmap
Deprecated: Easy Install <easy_install>
history history
...@@ -9,7 +9,7 @@ code. ...@@ -9,7 +9,7 @@ code.
Setuptools provides a facility to invoke 2to3 on the code as a part of the Setuptools provides a facility to invoke 2to3 on the code as a part of the
build process, by setting the keyword parameter ``use_2to3`` to True, but build process, by setting the keyword parameter ``use_2to3`` to True, but
the Setuptools strongly recommends instead developing a unified codebase the Setuptools project strongly recommends instead developing a unified codebase
using `six <https://pypi.org/project/six/>`_, using `six <https://pypi.org/project/six/>`_,
`future <https://pypi.org/project/future/>`_, or another compatibility `future <https://pypi.org/project/future/>`_, or another compatibility
library. library.
......
sphinx!=1.8.0 sphinx!=1.8.0
rst.linker>=1.9 rst.linker>=1.9
jaraco.packaging>=3.2 jaraco.packaging>=6.1
setuptools>=34 setuptools>=34
...@@ -2,11 +2,6 @@ ...@@ -2,11 +2,6 @@
Roadmap Roadmap
======= =======
Setuptools has the following large-scale goals on the roadmap: Setuptools maintains a series of `milestones
<https://github.com/pypa/setuptools/milestones>`_ to track
- Mature declarative config to supersede imperative config in a roadmap of large-scale goals.
all supported use-cases and harmonize with pyproject.toml
syntax.
- Deprecate and remove setup_requires and easy_install in
favor of PEP 518 build requirements and pip install.
- Adopt the Distutils package and stop monkeypatching stdlib.
This diff is collapsed.
...@@ -238,6 +238,9 @@ __all__ = [ ...@@ -238,6 +238,9 @@ __all__ = [
'register_finder', 'register_namespace_handler', 'register_loader_type', 'register_finder', 'register_namespace_handler', 'register_loader_type',
'fixup_namespace_packages', 'get_importer', 'fixup_namespace_packages', 'get_importer',
# Warnings
'PkgResourcesDeprecationWarning',
# Deprecated/backward compatibility only # Deprecated/backward compatibility only
'run_main', 'AvailableDistributions', 'run_main', 'AvailableDistributions',
] ]
...@@ -2228,7 +2231,7 @@ register_namespace_handler(object, null_ns_handler) ...@@ -2228,7 +2231,7 @@ register_namespace_handler(object, null_ns_handler)
def normalize_path(filename): def normalize_path(filename):
"""Normalize a file/dir name for comparison purposes""" """Normalize a file/dir name for comparison purposes"""
return os.path.normcase(os.path.realpath(_cygwin_patch(filename))) return os.path.normcase(os.path.realpath(os.path.normpath(_cygwin_patch(filename))))
def _cygwin_patch(filename): # pragma: nocover def _cygwin_patch(filename): # pragma: nocover
...@@ -2335,7 +2338,7 @@ class EntryPoint: ...@@ -2335,7 +2338,7 @@ class EntryPoint:
warnings.warn( warnings.warn(
"Parameters to load are deprecated. Call .resolve and " "Parameters to load are deprecated. Call .resolve and "
".require separately.", ".require separately.",
DeprecationWarning, PkgResourcesDeprecationWarning,
stacklevel=2, stacklevel=2,
) )
if require: if require:
...@@ -3158,3 +3161,11 @@ def _initialize_master_working_set(): ...@@ -3158,3 +3161,11 @@ def _initialize_master_working_set():
# match order # match order
list(map(working_set.add_entry, sys.path)) list(map(working_set.add_entry, sys.path))
globals().update(locals()) globals().update(locals())
class PkgResourcesDeprecationWarning(Warning):
"""
Base class for warning about deprecations in ``pkg_resources``
This class is not derived from ``DeprecationWarning``, and as such is
visible by default.
"""
...@@ -236,3 +236,56 @@ class TestDeepVersionLookupDistutils: ...@@ -236,3 +236,56 @@ class TestDeepVersionLookupDistutils:
req = pkg_resources.Requirement.parse('foo>=1.9') req = pkg_resources.Requirement.parse('foo>=1.9')
dist = pkg_resources.WorkingSet([env.paths['lib']]).find(req) dist = pkg_resources.WorkingSet([env.paths['lib']]).find(req)
assert dist.version == version assert dist.version == version
@pytest.mark.parametrize(
'unnormalized, normalized',
[
('foo', 'foo'),
('foo/', 'foo'),
('foo/bar', 'foo/bar'),
('foo/bar/', 'foo/bar'),
],
)
def test_normalize_path_trailing_sep(self, unnormalized, normalized):
"""Ensure the trailing slash is cleaned for path comparison.
See pypa/setuptools#1519.
"""
result_from_unnormalized = pkg_resources.normalize_path(unnormalized)
result_from_normalized = pkg_resources.normalize_path(normalized)
assert result_from_unnormalized == result_from_normalized
@pytest.mark.skipif(
os.path.normcase('A') != os.path.normcase('a'),
reason='Testing case-insensitive filesystems.',
)
@pytest.mark.parametrize(
'unnormalized, normalized',
[
('MiXeD/CasE', 'mixed/case'),
],
)
def test_normalize_path_normcase(self, unnormalized, normalized):
"""Ensure mixed case is normalized on case-insensitive filesystems.
"""
result_from_unnormalized = pkg_resources.normalize_path(unnormalized)
result_from_normalized = pkg_resources.normalize_path(normalized)
assert result_from_unnormalized == result_from_normalized
@pytest.mark.skipif(
os.path.sep != '\\',
reason='Testing systems using backslashes as path separators.',
)
@pytest.mark.parametrize(
'unnormalized, expected',
[
('forward/slash', 'forward\\slash'),
('forward/slash/', 'forward\\slash'),
('backward\\slash\\', 'backward\\slash'),
],
)
def test_normalize_path_backslash_sep(self, unnormalized, expected):
"""Ensure path seps are cleaned on backslash path sep systems.
"""
result = pkg_resources.normalize_path(unnormalized)
assert result.endswith(expected)
...@@ -15,7 +15,7 @@ import pkg_resources ...@@ -15,7 +15,7 @@ import pkg_resources
from pkg_resources import ( from pkg_resources import (
parse_requirements, VersionConflict, parse_version, parse_requirements, VersionConflict, parse_version,
Distribution, EntryPoint, Requirement, safe_version, safe_name, Distribution, EntryPoint, Requirement, safe_version, safe_name,
WorkingSet) WorkingSet, PkgResourcesDeprecationWarning)
# from Python 3.6 docs. # from Python 3.6 docs.
...@@ -492,6 +492,15 @@ class TestEntryPoints: ...@@ -492,6 +492,15 @@ class TestEntryPoints:
with pytest.raises(ValueError): with pytest.raises(ValueError):
EntryPoint.parse_map(self.submap_str) EntryPoint.parse_map(self.submap_str)
def testDeprecationWarnings(self):
ep = EntryPoint(
"foo", "pkg_resources.tests.test_resources", ["TestEntryPoints"],
["x"]
)
with pytest.warns(pkg_resources.PkgResourcesDeprecationWarning):
ep.load(require=False)
class TestRequirements: class TestRequirements:
def testBasics(self): def testBasics(self):
......
[build-system] [build-system]
requires = ["wheel"] requires = ["wheel"]
build-backend = "setuptools.build_meta"
[tool.towncrier] [tool.towncrier]
package = "setuptools" package = "setuptools"
......
[bumpversion] [bumpversion]
current_version = 40.5.0 current_version = 40.7.0
commit = True commit = True
tag = True tag = True
......
...@@ -89,7 +89,7 @@ def pypi_link(pkg_filename): ...@@ -89,7 +89,7 @@ def pypi_link(pkg_filename):
setup_params = dict( setup_params = dict(
name="setuptools", name="setuptools",
version="40.5.0", version="40.7.0",
description=( description=(
"Easily download, build, install, upgrade, and uninstall " "Easily download, build, install, upgrade, and uninstall "
"Python packages" "Python packages"
...@@ -164,6 +164,7 @@ setup_params = dict( ...@@ -164,6 +164,7 @@ setup_params = dict(
Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.4
Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.5
Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.6
Programming Language :: Python :: 3.7
Topic :: Software Development :: Libraries :: Python Modules Topic :: Software Development :: Libraries :: Python Modules
Topic :: System :: Archiving :: Packaging Topic :: System :: Archiving :: Packaging
Topic :: System :: Systems Administration Topic :: System :: Systems Administration
......
...@@ -5,10 +5,14 @@ import sys ...@@ -5,10 +5,14 @@ import sys
import functools import functools
import distutils.core import distutils.core
import distutils.filelist import distutils.filelist
import re
from distutils.errors import DistutilsOptionError
from distutils.util import convert_path from distutils.util import convert_path
from fnmatch import fnmatchcase from fnmatch import fnmatchcase
from setuptools.extern.six import PY3 from ._deprecation_warning import SetuptoolsDeprecationWarning
from setuptools.extern.six import PY3, string_types
from setuptools.extern.six.moves import filter, map from setuptools.extern.six.moves import filter, map
import setuptools.version import setuptools.version
...@@ -22,6 +26,7 @@ __metaclass__ = type ...@@ -22,6 +26,7 @@ __metaclass__ = type
__all__ = [ __all__ = [
'setup', 'Distribution', 'Feature', 'Command', 'Extension', 'Require', 'setup', 'Distribution', 'Feature', 'Command', 'Extension', 'Require',
'SetuptoolsDeprecationWarning',
'find_packages' 'find_packages'
] ]
...@@ -158,6 +163,37 @@ class Command(_Command): ...@@ -158,6 +163,37 @@ class Command(_Command):
_Command.__init__(self, dist) _Command.__init__(self, dist)
vars(self).update(kw) vars(self).update(kw)
def _ensure_stringlike(self, option, what, default=None):
val = getattr(self, option)
if val is None:
setattr(self, option, default)
return default
elif not isinstance(val, string_types):
raise DistutilsOptionError("'%s' must be a %s (got `%s`)"
% (option, what, val))
return val
def ensure_string_list(self, option):
r"""Ensure that 'option' is a list of strings. If 'option' is
currently a string, we split it either on /,\s*/ or /\s+/, so
"foo bar baz", "foo,bar,baz", and "foo, bar baz" all become
["foo", "bar", "baz"].
"""
val = getattr(self, option)
if val is None:
return
elif isinstance(val, string_types):
setattr(self, option, re.split(r',\s*|\s+', val))
else:
if isinstance(val, list):
ok = all(isinstance(v, string_types) for v in val)
else:
ok = False
if not ok:
raise DistutilsOptionError(
"'%s' must be a list of strings (got %r)"
% (option, val))
def reinitialize_command(self, command, reinit_subcommands=0, **kw): def reinitialize_command(self, command, reinit_subcommands=0, **kw):
cmd = _Command.reinitialize_command(self, command, reinit_subcommands) cmd = _Command.reinitialize_command(self, command, reinit_subcommands)
vars(cmd).update(kw) vars(cmd).update(kw)
...@@ -188,4 +224,5 @@ def findall(dir=os.curdir): ...@@ -188,4 +224,5 @@ def findall(dir=os.curdir):
return list(files) return list(files)
# Apply monkey patches
monkey.patch_all() monkey.patch_all()
class SetuptoolsDeprecationWarning(Warning):
"""
Base class for warning deprecations in ``setuptools``
This class is not derived from ``DeprecationWarning``, and as such is
visible by default.
"""
...@@ -112,12 +112,12 @@ def _get_immediate_subdirectories(a_dir): ...@@ -112,12 +112,12 @@ def _get_immediate_subdirectories(a_dir):
def get_requires_for_build_wheel(config_settings=None): def get_requires_for_build_wheel(config_settings=None):
config_settings = _fix_config(config_settings) config_settings = _fix_config(config_settings)
return _get_build_requires(config_settings, requirements=['setuptools', 'wheel']) return _get_build_requires(config_settings, requirements=['wheel'])
def get_requires_for_build_sdist(config_settings=None): def get_requires_for_build_sdist(config_settings=None):
config_settings = _fix_config(config_settings) config_settings = _fix_config(config_settings)
return _get_build_requires(config_settings, requirements=['setuptools']) return _get_build_requires(config_settings, requirements=[])
def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
...@@ -149,6 +149,15 @@ def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None): ...@@ -149,6 +149,15 @@ def prepare_metadata_for_build_wheel(metadata_directory, config_settings=None):
return dist_infos[0] return dist_infos[0]
def _file_with_extension(directory, extension):
matching = (
f for f in os.listdir(directory)
if f.endswith(extension)
)
file, = matching
return file
def build_wheel(wheel_directory, config_settings=None, def build_wheel(wheel_directory, config_settings=None,
metadata_directory=None): metadata_directory=None):
config_settings = _fix_config(config_settings) config_settings = _fix_config(config_settings)
...@@ -160,23 +169,15 @@ def build_wheel(wheel_directory, config_settings=None, ...@@ -160,23 +169,15 @@ def build_wheel(wheel_directory, config_settings=None,
shutil.rmtree(wheel_directory) shutil.rmtree(wheel_directory)
shutil.copytree('dist', wheel_directory) shutil.copytree('dist', wheel_directory)
wheels = [f for f in os.listdir(wheel_directory) return _file_with_extension(wheel_directory, '.whl')
if f.endswith('.whl')]
assert len(wheels) == 1
return wheels[0]
def build_sdist(sdist_directory, config_settings=None): def build_sdist(sdist_directory, config_settings=None):
config_settings = _fix_config(config_settings) config_settings = _fix_config(config_settings)
sdist_directory = os.path.abspath(sdist_directory) sdist_directory = os.path.abspath(sdist_directory)
sys.argv = sys.argv[:1] + ['sdist'] + \ sys.argv = sys.argv[:1] + ['sdist', '--formats', 'gztar'] + \
config_settings["--global-option"] + \ config_settings["--global-option"] + \
["--dist-dir", sdist_directory] ["--dist-dir", sdist_directory]
_run_setup() _run_setup()
sdists = [f for f in os.listdir(sdist_directory) return _file_with_extension(sdist_directory, '.tar.gz')
if f.endswith('.tar.gz')]
assert len(sdists) == 1
return sdists[0]
...@@ -7,7 +7,7 @@ import io ...@@ -7,7 +7,7 @@ import io
from setuptools.extern import six from setuptools.extern import six
from pkg_resources import Distribution, PathMetadata, normalize_path import pkg_resources
from setuptools.command.easy_install import easy_install from setuptools.command.easy_install import easy_install
from setuptools import namespaces from setuptools import namespaces
import setuptools import setuptools
...@@ -65,9 +65,9 @@ class develop(namespaces.DevelopInstaller, easy_install): ...@@ -65,9 +65,9 @@ class develop(namespaces.DevelopInstaller, easy_install):
if self.egg_path is None: if self.egg_path is None:
self.egg_path = os.path.abspath(ei.egg_base) self.egg_path = os.path.abspath(ei.egg_base)
target = normalize_path(self.egg_base) target = pkg_resources.normalize_path(self.egg_base)
egg_path = normalize_path(os.path.join(self.install_dir, egg_path = pkg_resources.normalize_path(
self.egg_path)) os.path.join(self.install_dir, self.egg_path))
if egg_path != target: if egg_path != target:
raise DistutilsOptionError( raise DistutilsOptionError(
"--egg-path must be a relative path from the install" "--egg-path must be a relative path from the install"
...@@ -75,9 +75,9 @@ class develop(namespaces.DevelopInstaller, easy_install): ...@@ -75,9 +75,9 @@ class develop(namespaces.DevelopInstaller, easy_install):
) )
# Make a distribution for the package's source # Make a distribution for the package's source
self.dist = Distribution( self.dist = pkg_resources.Distribution(
target, target,
PathMetadata(target, os.path.abspath(ei.egg_info)), pkg_resources.PathMetadata(target, os.path.abspath(ei.egg_info)),
project_name=ei.egg_name project_name=ei.egg_name
) )
...@@ -97,13 +97,14 @@ class develop(namespaces.DevelopInstaller, easy_install): ...@@ -97,13 +97,14 @@ class develop(namespaces.DevelopInstaller, easy_install):
path_to_setup = egg_base.replace(os.sep, '/').rstrip('/') path_to_setup = egg_base.replace(os.sep, '/').rstrip('/')
if path_to_setup != os.curdir: if path_to_setup != os.curdir:
path_to_setup = '../' * (path_to_setup.count('/') + 1) path_to_setup = '../' * (path_to_setup.count('/') + 1)
resolved = normalize_path( resolved = pkg_resources.normalize_path(
os.path.join(install_dir, egg_path, path_to_setup) os.path.join(install_dir, egg_path, path_to_setup)
) )
if resolved != normalize_path(os.curdir): if resolved != pkg_resources.normalize_path(os.curdir):
raise DistutilsOptionError( raise DistutilsOptionError(
"Can't get a consistent path to setup script from" "Can't get a consistent path to setup script from"
" installation directory", resolved, normalize_path(os.curdir)) " installation directory", resolved,
pkg_resources.normalize_path(os.curdir))
return path_to_setup return path_to_setup
def install_for_development(self): def install_for_development(self):
...@@ -114,7 +115,7 @@ class develop(namespaces.DevelopInstaller, easy_install): ...@@ -114,7 +115,7 @@ class develop(namespaces.DevelopInstaller, easy_install):
self.reinitialize_command('build_py', inplace=0) self.reinitialize_command('build_py', inplace=0)
self.run_command('build_py') self.run_command('build_py')
bpy_cmd = self.get_finalized_command("build_py") bpy_cmd = self.get_finalized_command("build_py")
build_path = normalize_path(bpy_cmd.build_lib) build_path = pkg_resources.normalize_path(bpy_cmd.build_lib)
# Build extensions # Build extensions
self.reinitialize_command('egg_info', egg_base=build_path) self.reinitialize_command('egg_info', egg_base=build_path)
...@@ -128,7 +129,8 @@ class develop(namespaces.DevelopInstaller, easy_install): ...@@ -128,7 +129,8 @@ class develop(namespaces.DevelopInstaller, easy_install):
self.egg_path = build_path self.egg_path = build_path
self.dist.location = build_path self.dist.location = build_path
# XXX # XXX
self.dist._provider = PathMetadata(build_path, ei_cmd.egg_info) self.dist._provider = pkg_resources.PathMetadata(
build_path, ei_cmd.egg_info)
else: else:
# Without 2to3 inplace works fine: # Without 2to3 inplace works fine:
self.run_command('egg_info') self.run_command('egg_info')
...@@ -200,6 +202,7 @@ class VersionlessRequirement: ...@@ -200,6 +202,7 @@ class VersionlessRequirement:
name as the 'requirement' so that scripts will work across name as the 'requirement' so that scripts will work across
multiple versions. multiple versions.
>>> from pkg_resources import Distribution
>>> dist = Distribution(project_name='foo', version='1.0') >>> dist = Distribution(project_name='foo', version='1.0')
>>> str(dist.as_requirement()) >>> str(dist.as_requirement())
'foo==1.0' 'foo==1.0'
......
...@@ -40,8 +40,11 @@ import subprocess ...@@ -40,8 +40,11 @@ import subprocess
import shlex import shlex
import io import io
from sysconfig import get_config_vars, get_path from sysconfig import get_config_vars, get_path
from setuptools import SetuptoolsDeprecationWarning
from setuptools.extern import six from setuptools.extern import six
from setuptools.extern.six.moves import configparser, map from setuptools.extern.six.moves import configparser, map
...@@ -2077,7 +2080,7 @@ class ScriptWriter: ...@@ -2077,7 +2080,7 @@ class ScriptWriter:
@classmethod @classmethod
def get_script_args(cls, dist, executable=None, wininst=False): def get_script_args(cls, dist, executable=None, wininst=False):
# for backward compatibility # for backward compatibility
warnings.warn("Use get_args", DeprecationWarning) warnings.warn("Use get_args", EasyInstallDeprecationWarning)
writer = (WindowsScriptWriter if wininst else ScriptWriter).best() writer = (WindowsScriptWriter if wininst else ScriptWriter).best()
header = cls.get_script_header("", executable, wininst) header = cls.get_script_header("", executable, wininst)
return writer.get_args(dist, header) return writer.get_args(dist, header)
...@@ -2085,7 +2088,7 @@ class ScriptWriter: ...@@ -2085,7 +2088,7 @@ class ScriptWriter:
@classmethod @classmethod
def get_script_header(cls, script_text, executable=None, wininst=False): def get_script_header(cls, script_text, executable=None, wininst=False):
# for backward compatibility # for backward compatibility
warnings.warn("Use get_header", DeprecationWarning, stacklevel=2) warnings.warn("Use get_header", EasyInstallDeprecationWarning, stacklevel=2)
if wininst: if wininst:
executable = "python.exe" executable = "python.exe"
return cls.get_header(script_text, executable) return cls.get_header(script_text, executable)
...@@ -2120,7 +2123,7 @@ class ScriptWriter: ...@@ -2120,7 +2123,7 @@ class ScriptWriter:
@classmethod @classmethod
def get_writer(cls, force_windows): def get_writer(cls, force_windows):
# for backward compatibility # for backward compatibility
warnings.warn("Use best", DeprecationWarning) warnings.warn("Use best", EasyInstallDeprecationWarning)
return WindowsScriptWriter.best() if force_windows else cls.best() return WindowsScriptWriter.best() if force_windows else cls.best()
@classmethod @classmethod
...@@ -2152,7 +2155,7 @@ class WindowsScriptWriter(ScriptWriter): ...@@ -2152,7 +2155,7 @@ class WindowsScriptWriter(ScriptWriter):
@classmethod @classmethod
def get_writer(cls): def get_writer(cls):
# for backward compatibility # for backward compatibility
warnings.warn("Use best", DeprecationWarning) warnings.warn("Use best", EasyInstallDeprecationWarning)
return cls.best() return cls.best()
@classmethod @classmethod
...@@ -2333,3 +2336,7 @@ def _patch_usage(): ...@@ -2333,3 +2336,7 @@ def _patch_usage():
yield yield
finally: finally:
distutils.core.gen_usage = saved distutils.core.gen_usage = saved
class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning):
"""Class for warning about deprecations in EasyInstall in SetupTools. Not ignored by default, unlike DeprecationWarning."""
...@@ -31,7 +31,7 @@ import setuptools.unicode_utils as unicode_utils ...@@ -31,7 +31,7 @@ import setuptools.unicode_utils as unicode_utils
from setuptools.glob import glob from setuptools.glob import glob
from setuptools.extern import packaging from setuptools.extern import packaging
from setuptools import SetuptoolsDeprecationWarning
def translate_pattern(glob): def translate_pattern(glob):
""" """
...@@ -576,6 +576,12 @@ class manifest_maker(sdist): ...@@ -576,6 +576,12 @@ class manifest_maker(sdist):
self.filelist.extend(rcfiles) self.filelist.extend(rcfiles)
elif os.path.exists(self.manifest): elif os.path.exists(self.manifest):
self.read_manifest() self.read_manifest()
if os.path.exists("setup.py"):
# setup.py should be included by default, even if it's not
# the script called to create the sdist
self.filelist.append("setup.py")
ei_cmd = self.get_finalized_command('egg_info') ei_cmd = self.get_finalized_command('egg_info')
self.filelist.graft(ei_cmd.egg_info) self.filelist.graft(ei_cmd.egg_info)
...@@ -697,7 +703,7 @@ def get_pkg_info_revision(): ...@@ -697,7 +703,7 @@ def get_pkg_info_revision():
Get a -r### off of PKG-INFO Version in case this is an sdist of Get a -r### off of PKG-INFO Version in case this is an sdist of
a subversion revision. a subversion revision.
""" """
warnings.warn("get_pkg_info_revision is deprecated.", DeprecationWarning) warnings.warn("get_pkg_info_revision is deprecated.", EggInfoDeprecationWarning)
if os.path.exists('PKG-INFO'): if os.path.exists('PKG-INFO'):
with io.open('PKG-INFO') as f: with io.open('PKG-INFO') as f:
for line in f: for line in f:
...@@ -705,3 +711,7 @@ def get_pkg_info_revision(): ...@@ -705,3 +711,7 @@ def get_pkg_info_revision():
if match: if match:
return int(match.group(1)) return int(match.group(1))
return 0 return 0
class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):
"""Class for warning about deprecations in eggInfo in setupTools. Not ignored by default, unlike DeprecationWarning."""
import io
import os
import hashlib
import getpass import getpass
from base64 import standard_b64encode
from distutils import log from distutils import log
from distutils.command import upload as orig from distutils.command import upload as orig
from distutils.spawn import spawn
from distutils.errors import DistutilsError
from setuptools.extern.six.moves.urllib.request import urlopen, Request
from setuptools.extern.six.moves.urllib.error import HTTPError
from setuptools.extern.six.moves.urllib.parse import urlparse
class upload(orig.upload): class upload(orig.upload):
...@@ -8,7 +21,6 @@ class upload(orig.upload): ...@@ -8,7 +21,6 @@ class upload(orig.upload):
Override default upload behavior to obtain password Override default upload behavior to obtain password
in a variety of different ways. in a variety of different ways.
""" """
def run(self): def run(self):
try: try:
orig.upload.run(self) orig.upload.run(self)
...@@ -33,6 +45,137 @@ class upload(orig.upload): ...@@ -33,6 +45,137 @@ class upload(orig.upload):
self._prompt_for_password() self._prompt_for_password()
) )
def upload_file(self, command, pyversion, filename):
# Makes sure the repository URL is compliant
schema, netloc, url, params, query, fragments = \
urlparse(self.repository)
if params or query or fragments:
raise AssertionError("Incompatible url %s" % self.repository)
if schema not in ('http', 'https'):
raise AssertionError("unsupported schema " + schema)
# Sign if requested
if self.sign:
gpg_args = ["gpg", "--detach-sign", "-a", filename]
if self.identity:
gpg_args[2:2] = ["--local-user", self.identity]
spawn(gpg_args,
dry_run=self.dry_run)
# Fill in the data - send all the meta-data in case we need to
# register a new release
with open(filename, 'rb') as f:
content = f.read()
meta = self.distribution.metadata
data = {
# action
':action': 'file_upload',
'protocol_version': '1',
# identify release
'name': meta.get_name(),
'version': meta.get_version(),
# file content
'content': (os.path.basename(filename), content),
'filetype': command,
'pyversion': pyversion,
'md5_digest': hashlib.md5(content).hexdigest(),
# additional meta-data
'metadata_version': str(meta.get_metadata_version()),
'summary': meta.get_description(),
'home_page': meta.get_url(),
'author': meta.get_contact(),
'author_email': meta.get_contact_email(),
'license': meta.get_licence(),
'description': meta.get_long_description(),
'keywords': meta.get_keywords(),
'platform': meta.get_platforms(),
'classifiers': meta.get_classifiers(),
'download_url': meta.get_download_url(),
# PEP 314
'provides': meta.get_provides(),
'requires': meta.get_requires(),
'obsoletes': meta.get_obsoletes(),
}
data['comment'] = ''
if self.sign:
data['gpg_signature'] = (os.path.basename(filename) + ".asc",
open(filename+".asc", "rb").read())
# set up the authentication
user_pass = (self.username + ":" + self.password).encode('ascii')
# The exact encoding of the authentication string is debated.
# Anyway PyPI only accepts ascii for both username or password.
auth = "Basic " + standard_b64encode(user_pass).decode('ascii')
# Build up the MIME payload for the POST data
boundary = '--------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
sep_boundary = b'\r\n--' + boundary.encode('ascii')
end_boundary = sep_boundary + b'--\r\n'
body = io.BytesIO()
for key, value in data.items():
title = '\r\nContent-Disposition: form-data; name="%s"' % key
# handle multiple entries for the same name
if not isinstance(value, list):
value = [value]
for value in value:
if type(value) is tuple:
title += '; filename="%s"' % value[0]
value = value[1]
else:
value = str(value).encode('utf-8')
body.write(sep_boundary)
body.write(title.encode('utf-8'))
body.write(b"\r\n\r\n")
body.write(value)
body.write(end_boundary)
body = body.getvalue()
msg = "Submitting %s to %s" % (filename, self.repository)
self.announce(msg, log.INFO)
# build the Request
headers = {
'Content-type': 'multipart/form-data; boundary=%s' % boundary,
'Content-length': str(len(body)),
'Authorization': auth,
}
request = Request(self.repository, data=body,
headers=headers)
# send the data
try:
result = urlopen(request)
status = result.getcode()
reason = result.msg
except HTTPError as e:
status = e.code
reason = e.msg
except OSError as e:
self.announce(str(e), log.ERROR)
raise
if status == 200:
self.announce('Server response (%s): %s' % (status, reason),
log.INFO)
if self.show_response:
text = getattr(self, '_read_pypi_response',
lambda x: None)(result)
if text is not None:
msg = '\n'.join(('-' * 75, text, '-' * 75))
self.announce(msg, log.INFO)
else:
msg = 'Upload failed (%s): %s' % (status, reason)
self.announce(msg, log.ERROR)
raise DistutilsError(msg)
def _load_password_from_keyring(self): def _load_password_from_keyring(self):
""" """
Attempt to load password from keyring. Suppress Exceptions. Attempt to load password from keyring. Suppress Exceptions.
......
...@@ -2,8 +2,12 @@ from __future__ import absolute_import, unicode_literals ...@@ -2,8 +2,12 @@ from __future__ import absolute_import, unicode_literals
import io import io
import os import os
import sys import sys
import warnings
import functools
from collections import defaultdict from collections import defaultdict
from functools import partial from functools import partial
from functools import wraps
from importlib import import_module from importlib import import_module
from distutils.errors import DistutilsOptionError, DistutilsFileError from distutils.errors import DistutilsOptionError, DistutilsFileError
...@@ -61,6 +65,18 @@ def read_configuration( ...@@ -61,6 +65,18 @@ def read_configuration(
return configuration_to_dict(handlers) return configuration_to_dict(handlers)
def _get_option(target_obj, key):
"""
Given a target object and option key, get that option from
the target object, either through a get_{key} method or
from an attribute directly.
"""
getter_name = 'get_{key}'.format(**locals())
by_attribute = functools.partial(getattr, target_obj, key)
getter = getattr(target_obj, getter_name, by_attribute)
return getter()
def configuration_to_dict(handlers): def configuration_to_dict(handlers):
"""Returns configuration data gathered by given handlers as a dict. """Returns configuration data gathered by given handlers as a dict.
...@@ -72,20 +88,9 @@ def configuration_to_dict(handlers): ...@@ -72,20 +88,9 @@ def configuration_to_dict(handlers):
config_dict = defaultdict(dict) config_dict = defaultdict(dict)
for handler in handlers: for handler in handlers:
obj_alias = handler.section_prefix
target_obj = handler.target_obj
for option in handler.set_options: for option in handler.set_options:
getter = getattr(target_obj, 'get_%s' % option, None) value = _get_option(handler.target_obj, option)
config_dict[handler.section_prefix][option] = value
if getter is None:
value = getattr(target_obj, option)
else:
value = getter()
config_dict[obj_alias][option] = value
return config_dict return config_dict
...@@ -110,7 +115,8 @@ def parse_configuration( ...@@ -110,7 +115,8 @@ def parse_configuration(
options.parse() options.parse()
meta = ConfigMetadataHandler( meta = ConfigMetadataHandler(
distribution.metadata, command_options, ignore_option_errors, distribution.package_dir) distribution.metadata, command_options, ignore_option_errors,
distribution.package_dir)
meta.parse() meta.parse()
return meta, options return meta, options
...@@ -240,6 +246,26 @@ class ConfigHandler: ...@@ -240,6 +246,26 @@ class ConfigHandler:
value = value.lower() value = value.lower()
return value in ('1', 'true', 'yes') return value in ('1', 'true', 'yes')
@classmethod
def _exclude_files_parser(cls, key):
"""Returns a parser function to make sure field inputs
are not files.
Parses a value after getting the key so error messages are
more informative.
:param key:
:rtype: callable
"""
def parser(value):
exclude_directive = 'file:'
if value.startswith(exclude_directive):
raise ValueError(
'Only strings are accepted for the {0} field, '
'files are not accepted'.format(key))
return value
return parser
@classmethod @classmethod
def _parse_file(cls, value): def _parse_file(cls, value):
"""Represents value as a string, allowing including text """Represents value as a string, allowing including text
...@@ -249,7 +275,6 @@ class ConfigHandler: ...@@ -249,7 +275,6 @@ class ConfigHandler:
directory with setup.py. directory with setup.py.
Examples: Examples:
file: LICENSE
file: README.rst, CHANGELOG.md, src/file.txt file: README.rst, CHANGELOG.md, src/file.txt
:param str value: :param str value:
...@@ -388,7 +413,7 @@ class ConfigHandler: ...@@ -388,7 +413,7 @@ class ConfigHandler:
section_parser_method = getattr( section_parser_method = getattr(
self, self,
# Dots in section names are tranlsated into dunderscores. # Dots in section names are translated into dunderscores.
('parse_section%s' % method_postfix).replace('.', '__'), ('parse_section%s' % method_postfix).replace('.', '__'),
None) None)
...@@ -399,6 +424,20 @@ class ConfigHandler: ...@@ -399,6 +424,20 @@ class ConfigHandler:
section_parser_method(section_options) section_parser_method(section_options)
def _deprecated_config_handler(self, func, msg, warning_class):
""" this function will wrap around parameters that are deprecated
:param msg: deprecation message
:param warning_class: class of warning exception to be raised
:param func: function to be wrapped around
"""
@wraps(func)
def config_handler(*args, **kwargs):
warnings.warn(msg, warning_class)
return func(*args, **kwargs)
return config_handler
class ConfigMetadataHandler(ConfigHandler): class ConfigMetadataHandler(ConfigHandler):
...@@ -429,15 +468,20 @@ class ConfigMetadataHandler(ConfigHandler): ...@@ -429,15 +468,20 @@ class ConfigMetadataHandler(ConfigHandler):
parse_list = self._parse_list parse_list = self._parse_list
parse_file = self._parse_file parse_file = self._parse_file
parse_dict = self._parse_dict parse_dict = self._parse_dict
exclude_files_parser = self._exclude_files_parser
return { return {
'platforms': parse_list, 'platforms': parse_list,
'keywords': parse_list, 'keywords': parse_list,
'provides': parse_list, 'provides': parse_list,
'requires': parse_list, 'requires': self._deprecated_config_handler(
parse_list,
"The requires parameter is deprecated, please use "
"install_requires for runtime dependencies.",
DeprecationWarning),
'obsoletes': parse_list, 'obsoletes': parse_list,
'classifiers': self._get_parser_compound(parse_file, parse_list), 'classifiers': self._get_parser_compound(parse_file, parse_list),
'license': parse_file, 'license': exclude_files_parser('license'),
'description': parse_file, 'description': parse_file,
'long_description': parse_file, 'long_description': parse_file,
'version': self._parse_version, 'version': self._parse_version,
...@@ -458,9 +502,12 @@ class ConfigMetadataHandler(ConfigHandler): ...@@ -458,9 +502,12 @@ class ConfigMetadataHandler(ConfigHandler):
# Be strict about versions loaded from file because it's easy to # Be strict about versions loaded from file because it's easy to
# accidentally include newlines and other unintended content # accidentally include newlines and other unintended content
if isinstance(parse(version), LegacyVersion): if isinstance(parse(version), LegacyVersion):
raise DistutilsOptionError('Version loaded from %s does not comply with PEP 440: %s' % ( tmpl = (
value, version 'Version loaded from {value} does not '
)) 'comply with PEP 440: {version}'
)
raise DistutilsOptionError(tmpl.format(**locals()))
return version return version
version = self._parse_attr(value, self.package_dir) version = self._parse_attr(value, self.package_dir)
...@@ -518,12 +565,13 @@ class ConfigOptionsHandler(ConfigHandler): ...@@ -518,12 +565,13 @@ class ConfigOptionsHandler(ConfigHandler):
find_directives = ['find:', 'find_namespace:'] find_directives = ['find:', 'find_namespace:']
trimmed_value = value.strip() trimmed_value = value.strip()
if not trimmed_value in find_directives: if trimmed_value not in find_directives:
return self._parse_list(value) return self._parse_list(value)
findns = trimmed_value == find_directives[1] findns = trimmed_value == find_directives[1]
if findns and not PY3: if findns and not PY3:
raise DistutilsOptionError('find_namespace: directive is unsupported on Python < 3.3') raise DistutilsOptionError(
'find_namespace: directive is unsupported on Python < 3.3')
# Read function arguments from a dedicated section. # Read function arguments from a dedicated section.
find_kwargs = self.parse_section_packages__find( find_kwargs = self.parse_section_packages__find(
......
This diff is collapsed.
...@@ -84,7 +84,7 @@ def patch_all(): ...@@ -84,7 +84,7 @@ def patch_all():
warehouse = 'https://upload.pypi.org/legacy/' warehouse = 'https://upload.pypi.org/legacy/'
distutils.config.PyPIRCCommand.DEFAULT_REPOSITORY = warehouse distutils.config.PyPIRCCommand.DEFAULT_REPOSITORY = warehouse
_patch_distribution_metadata_write_pkg_file() _patch_distribution_metadata()
# Install Distribution throughout the distutils # Install Distribution throughout the distutils
for module in distutils.dist, distutils.core, distutils.cmd: for module in distutils.dist, distutils.core, distutils.cmd:
...@@ -101,11 +101,11 @@ def patch_all(): ...@@ -101,11 +101,11 @@ def patch_all():
patch_for_msvc_specialized_compiler() patch_for_msvc_specialized_compiler()
def _patch_distribution_metadata_write_pkg_file(): def _patch_distribution_metadata():
"""Patch write_pkg_file to also write Requires-Python/Requires-External""" """Patch write_pkg_file and read_pkg_file for higher metadata standards"""
distutils.dist.DistributionMetadata.write_pkg_file = ( for attr in ('write_pkg_file', 'read_pkg_file', 'get_metadata_version'):
setuptools.dist.write_pkg_file new_val = getattr(setuptools.dist, attr)
) setattr(distutils.dist.DistributionMetadata, attr, new_val)
def patch_func(replacement, target_mod, func_name): def patch_func(replacement, target_mod, func_name):
......
...@@ -850,13 +850,16 @@ class PackageIndex(Environment): ...@@ -850,13 +850,16 @@ class PackageIndex(Environment):
def _download_svn(self, url, filename): def _download_svn(self, url, filename):
warnings.warn("SVN download support is deprecated", UserWarning) warnings.warn("SVN download support is deprecated", UserWarning)
def splituser(host):
user, delim, host = host.rpartition('@')
return user, host
url = url.split('#', 1)[0] # remove any fragment for svn's sake url = url.split('#', 1)[0] # remove any fragment for svn's sake
creds = '' creds = ''
if url.lower().startswith('svn:') and '@' in url: if url.lower().startswith('svn:') and '@' in url:
scheme, netloc, path, p, q, f = urllib.parse.urlparse(url) scheme, netloc, path, p, q, f = urllib.parse.urlparse(url)
if not netloc and path.startswith('//') and '/' in path[2:]: if not netloc and path.startswith('//') and '/' in path[2:]:
netloc, path = path[2:].split('/', 1) netloc, path = path[2:].split('/', 1)
auth, host = urllib.parse.splituser(netloc) auth, host = splituser(netloc)
if auth: if auth:
if ':' in auth: if ':' in auth:
user, pw = auth.split(':', 1) user, pw = auth.split(':', 1)
...@@ -1047,15 +1050,16 @@ class PyPIConfig(configparser.RawConfigParser): ...@@ -1047,15 +1050,16 @@ class PyPIConfig(configparser.RawConfigParser):
def open_with_auth(url, opener=urllib.request.urlopen): def open_with_auth(url, opener=urllib.request.urlopen):
"""Open a urllib2 request, handling HTTP authentication""" """Open a urllib2 request, handling HTTP authentication"""
scheme, netloc, path, params, query, frag = urllib.parse.urlparse(url) parsed = urllib.parse.urlparse(url)
scheme, netloc, path, params, query, frag = parsed
# Double scheme does not raise on Mac OS X as revealed by a # Double scheme does not raise on Mac OS X as revealed by a
# failing test. We would expect "nonnumeric port". Refs #20. # failing test. We would expect "nonnumeric port". Refs #20.
if netloc.endswith(':'): if netloc.endswith(':'):
raise http_client.InvalidURL("nonnumeric port: ''") raise http_client.InvalidURL("nonnumeric port: ''")
if scheme in ('http', 'https'): if scheme in ('http', 'https') and parsed.username:
auth, host = urllib.parse.splituser(netloc) auth = ':'.join((parsed.username, parsed.password))
else: else:
auth = None auth = None
...@@ -1068,7 +1072,7 @@ def open_with_auth(url, opener=urllib.request.urlopen): ...@@ -1068,7 +1072,7 @@ def open_with_auth(url, opener=urllib.request.urlopen):
if auth: if auth:
auth = "Basic " + _encode_auth(auth) auth = "Basic " + _encode_auth(auth)
parts = scheme, host, path, params, query, frag parts = scheme, parsed.hostname, path, params, query, frag
new_url = urllib.parse.urlunparse(parts) new_url = urllib.parse.urlunparse(parts)
request = urllib.request.Request(new_url) request = urllib.request.Request(new_url)
request.add_header("Authorization", auth) request.add_header("Authorization", auth)
...@@ -1082,7 +1086,7 @@ def open_with_auth(url, opener=urllib.request.urlopen): ...@@ -1082,7 +1086,7 @@ def open_with_auth(url, opener=urllib.request.urlopen):
# Put authentication info back into request URL if same host, # Put authentication info back into request URL if same host,
# so that links found on the page will work # so that links found on the page will work
s2, h2, path2, param2, query2, frag2 = urllib.parse.urlparse(fp.url) s2, h2, path2, param2, query2, frag2 = urllib.parse.urlparse(fp.url)
if s2 == scheme and h2 == host: if s2 == scheme and h2 == parsed.hostname:
parts = s2, netloc, path2, param2, query2, frag2 parts = s2, netloc, path2, param2, query2, frag2
fp.url = urllib.parse.urlunparse(parts) fp.url = urllib.parse.urlunparse(parts)
......
...@@ -161,7 +161,7 @@ def is_manylinux1_compatible(): ...@@ -161,7 +161,7 @@ def is_manylinux1_compatible():
def get_darwin_arches(major, minor, machine): def get_darwin_arches(major, minor, machine):
"""Return a list of supported arches (including group arches) for """Return a list of supported arches (including group arches) for
the given major, minor and machine architecture of an macOS machine. the given major, minor and machine architecture of a macOS machine.
""" """
arches = [] arches = []
......
import sys
from distutils.errors import DistutilsOptionError
from distutils.util import strtobool
from distutils.debug import DEBUG
class Distribution_parse_config_files:
"""
Mix-in providing forward-compatibility for functionality to be
included by default on Python 3.7.
Do not edit the code in this class except to update functionality
as implemented in distutils.
"""
def parse_config_files(self, filenames=None):
from configparser import ConfigParser
# Ignore install directory options if we have a venv
if sys.prefix != sys.base_prefix:
ignore_options = [
'install-base', 'install-platbase', 'install-lib',
'install-platlib', 'install-purelib', 'install-headers',
'install-scripts', 'install-data', 'prefix', 'exec-prefix',
'home', 'user', 'root']
else:
ignore_options = []
ignore_options = frozenset(ignore_options)
if filenames is None:
filenames = self.find_config_files()
if DEBUG:
self.announce("Distribution.parse_config_files():")
parser = ConfigParser(interpolation=None)
for filename in filenames:
if DEBUG:
self.announce(" reading %s" % filename)
parser.read(filename)
for section in parser.sections():
options = parser.options(section)
opt_dict = self.get_option_dict(section)
for opt in options:
if opt != '__name__' and opt not in ignore_options:
val = parser.get(section,opt)
opt = opt.replace('-', '_')
opt_dict[opt] = (filename, val)
# Make the ConfigParser forget everything (so we retain
# the original filenames that options come from)
parser.__init__()
# If there was a "global" section in the config file, use it
# to set Distribution options.
if 'global' in self.command_options:
for (opt, (src, val)) in self.command_options['global'].items():
alias = self.negative_opt.get(opt)
try:
if alias:
setattr(self, alias, not strtobool(val))
elif opt in ('verbose', 'dry_run'): # ugh!
setattr(self, opt, strtobool(val))
else:
setattr(self, opt, val)
except ValueError as msg:
raise DistutilsOptionError(msg)
if sys.version_info < (3,):
# Python 2 behavior is sufficient
class Distribution_parse_config_files:
pass
if False:
# When updated behavior is available upstream,
# disable override here.
class Distribution_parse_config_files:
pass
...@@ -59,7 +59,7 @@ if not match_hostname: ...@@ -59,7 +59,7 @@ if not match_hostname:
def _dnsname_match(dn, hostname, max_wildcards=1): def _dnsname_match(dn, hostname, max_wildcards=1):
"""Matching according to RFC 6125, section 6.4.3 """Matching according to RFC 6125, section 6.4.3
http://tools.ietf.org/html/rfc6125#section-6.4.3 https://tools.ietf.org/html/rfc6125#section-6.4.3
""" """
pats = [] pats = []
if not dn: if not dn:
......
...@@ -6,10 +6,13 @@ import pkg_resources.py31compat ...@@ -6,10 +6,13 @@ import pkg_resources.py31compat
def build_files(file_defs, prefix=""): def build_files(file_defs, prefix=""):
""" """
Build a set of files/directories, as described by the file_defs dictionary. 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 Each key/value pair in the dictionary is interpreted as
pair. If the contents value is a dictionary, a directory is created, and the 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. dictionary interpreted as the files within it, recursively.
For example: For example:
......
...@@ -19,10 +19,11 @@ class IndexServer(BaseHTTPServer.HTTPServer): ...@@ -19,10 +19,11 @@ class IndexServer(BaseHTTPServer.HTTPServer):
s.stop() s.stop()
""" """
def __init__(self, server_address=('', 0), def __init__(
self, server_address=('', 0),
RequestHandlerClass=SimpleHTTPServer.SimpleHTTPRequestHandler): RequestHandlerClass=SimpleHTTPServer.SimpleHTTPRequestHandler):
BaseHTTPServer.HTTPServer.__init__(self, server_address, BaseHTTPServer.HTTPServer.__init__(
RequestHandlerClass) self, server_address, RequestHandlerClass)
self._run = True self._run = True
def start(self): def start(self):
...@@ -56,10 +57,11 @@ class MockServer(BaseHTTPServer.HTTPServer, threading.Thread): ...@@ -56,10 +57,11 @@ class MockServer(BaseHTTPServer.HTTPServer, threading.Thread):
A simple HTTP Server that records the requests made to it. A simple HTTP Server that records the requests made to it.
""" """
def __init__(self, server_address=('', 0), def __init__(
self, server_address=('', 0),
RequestHandlerClass=RequestRecorder): RequestHandlerClass=RequestRecorder):
BaseHTTPServer.HTTPServer.__init__(self, server_address, BaseHTTPServer.HTTPServer.__init__(
RequestHandlerClass) self, server_address, RequestHandlerClass)
threading.Thread.__init__(self) threading.Thread.__init__(self)
self.setDaemon(True) self.setDaemon(True)
self.requests = [] self.requests = []
......
import pytest import pytest
import os
import shutil
import mock import mock
from distutils.errors import DistutilsSetupError from distutils.errors import DistutilsSetupError
...@@ -40,13 +38,14 @@ class TestBuildCLib: ...@@ -40,13 +38,14 @@ class TestBuildCLib:
# with that out of the way, let's see if the crude dependency # with that out of the way, let's see if the crude dependency
# system works # system works
cmd.compiler = mock.MagicMock(spec=cmd.compiler) cmd.compiler = mock.MagicMock(spec=cmd.compiler)
mock_newer.return_value = ([],[]) mock_newer.return_value = ([], [])
obj_deps = {'': ('global.h',), 'example.c': ('example.h',)} obj_deps = {'': ('global.h',), 'example.c': ('example.h',)}
libs = [('example', {'sources': ['example.c'] ,'obj_deps': obj_deps})] libs = [('example', {'sources': ['example.c'], 'obj_deps': obj_deps})]
cmd.build_libraries(libs) cmd.build_libraries(libs)
assert [['example.c', 'global.h', 'example.h']] in mock_newer.call_args[0] assert [['example.c', 'global.h', 'example.h']] in \
mock_newer.call_args[0]
assert not cmd.compiler.compile.called assert not cmd.compiler.compile.called
assert cmd.compiler.create_static_lib.call_count == 1 assert cmd.compiler.create_static_lib.call_count == 1
......
...@@ -2,16 +2,20 @@ from __future__ import unicode_literals ...@@ -2,16 +2,20 @@ from __future__ import unicode_literals
import os import os
import shutil import shutil
import tarfile
import pytest import pytest
from setuptools.build_meta import build_sdist
from .files import build_files from .files import build_files
from .textwrap import DALS from .textwrap import DALS
from . import py2_only
__metaclass__ = type __metaclass__ = type
futures = pytest.importorskip('concurrent.futures') # Backports on Python 2.7
importlib = pytest.importorskip('importlib') import importlib
from concurrent import futures
class BuildBackendBase: class BuildBackendBase:
...@@ -108,13 +112,13 @@ def build_backend(tmpdir, request): ...@@ -108,13 +112,13 @@ def build_backend(tmpdir, request):
def test_get_requires_for_build_wheel(build_backend): def test_get_requires_for_build_wheel(build_backend):
actual = build_backend.get_requires_for_build_wheel() actual = build_backend.get_requires_for_build_wheel()
expected = ['six', 'setuptools', 'wheel'] expected = ['six', 'wheel']
assert sorted(actual) == sorted(expected) assert sorted(actual) == sorted(expected)
def test_get_requires_for_build_sdist(build_backend): def test_get_requires_for_build_sdist(build_backend):
actual = build_backend.get_requires_for_build_sdist() actual = build_backend.get_requires_for_build_sdist()
expected = ['six', 'setuptools'] expected = ['six']
assert sorted(actual) == sorted(expected) assert sorted(actual) == sorted(expected)
...@@ -143,7 +147,7 @@ def test_prepare_metadata_for_build_wheel(build_backend): ...@@ -143,7 +147,7 @@ def test_prepare_metadata_for_build_wheel(build_backend):
assert os.path.isfile(os.path.join(dist_dir, dist_info, 'METADATA')) assert os.path.isfile(os.path.join(dist_dir, dist_info, 'METADATA'))
@pytest.mark.skipif('sys.version_info > (3,)') @py2_only
def test_prepare_metadata_for_build_wheel_with_str(build_backend): def test_prepare_metadata_for_build_wheel_with_str(build_backend):
dist_dir = os.path.abspath(str('pip-dist-info')) dist_dir = os.path.abspath(str('pip-dist-info'))
os.makedirs(dist_dir) os.makedirs(dist_dir)
...@@ -168,15 +172,67 @@ def test_build_sdist_version_change(build_backend): ...@@ -168,15 +172,67 @@ def test_build_sdist_version_change(build_backend):
sdist_name = build_backend.build_sdist(sdist_into_directory) sdist_name = build_backend.build_sdist(sdist_into_directory)
assert os.path.isfile(os.path.join(sdist_into_directory, sdist_name)) assert os.path.isfile(os.path.join(sdist_into_directory, sdist_name))
# if the setup.py changes subsequent call of the build meta should still succeed, given the # if the setup.py changes subsequent call of the build meta
# should still succeed, given the
# sdist_directory the frontend specifies is empty # sdist_directory the frontend specifies is empty
with open(os.path.abspath("setup.py"), 'rt') as file_handler: with open(os.path.abspath("setup.py"), 'rt') as file_handler:
content = file_handler.read() content = file_handler.read()
with open(os.path.abspath("setup.py"), 'wt') as file_handler: with open(os.path.abspath("setup.py"), 'wt') as file_handler:
file_handler.write(content.replace("version='0.0.0'", "version='0.0.1'")) file_handler.write(
content.replace("version='0.0.0'", "version='0.0.1'"))
shutil.rmtree(sdist_into_directory) shutil.rmtree(sdist_into_directory)
os.makedirs(sdist_into_directory) os.makedirs(sdist_into_directory)
sdist_name = build_backend.build_sdist("out_sdist") sdist_name = build_backend.build_sdist("out_sdist")
assert os.path.isfile(os.path.join(os.path.abspath("out_sdist"), sdist_name)) assert os.path.isfile(
os.path.join(os.path.abspath("out_sdist"), sdist_name))
def test_build_sdist_setup_py_exists(tmpdir_cwd):
# If build_sdist is called from a script other than setup.py,
# ensure setup.py is include
build_files(defns[0])
targz_path = build_sdist("temp")
with tarfile.open(os.path.join("temp", targz_path)) as tar:
assert any('setup.py' in name for name in tar.getnames())
def test_build_sdist_setup_py_manifest_excluded(tmpdir_cwd):
# Ensure that MANIFEST.in can exclude setup.py
files = {
'setup.py': DALS("""
__import__('setuptools').setup(
name='foo',
version='0.0.0',
py_modules=['hello']
)"""),
'hello.py': '',
'MANIFEST.in': DALS("""
exclude setup.py
""")
}
build_files(files)
targz_path = build_sdist("temp")
with tarfile.open(os.path.join("temp", targz_path)) as tar:
assert not any('setup.py' in name for name in tar.getnames())
def test_build_sdist_builds_targz_even_if_zip_indicated(tmpdir_cwd):
files = {
'setup.py': DALS("""
__import__('setuptools').setup(
name='foo',
version='0.0.0',
py_modules=['hello']
)"""),
'hello.py': '',
'setup.cfg': DALS("""
[sdist]
formats=zip
""")
}
build_files(files)
build_sdist("temp")
# -*- coding: UTF-8 -*-
from __future__ import unicode_literals
import contextlib import contextlib
import pytest import pytest
from distutils.errors import DistutilsOptionError, DistutilsFileError from distutils.errors import DistutilsOptionError, DistutilsFileError
from mock import patch from mock import patch
from setuptools.dist import Distribution, _Distribution from setuptools.dist import Distribution, _Distribution
from setuptools.config import ConfigHandler, read_configuration from setuptools.config import ConfigHandler, read_configuration
from setuptools.extern.six.moves.configparser import InterpolationMissingOptionError
from setuptools.tests import is_ascii
from . import py2_only, py3_only from . import py2_only, py3_only
from .textwrap import DALS
class ErrConfigHandler(ConfigHandler): class ErrConfigHandler(ConfigHandler):
"""Erroneous handler. Fails to implement required methods.""" """Erroneous handler. Fails to implement required methods."""
...@@ -21,7 +29,7 @@ def make_package_dir(name, base_dir, ns=False): ...@@ -21,7 +29,7 @@ def make_package_dir(name, base_dir, ns=False):
return dir_package, init_file return dir_package, init_file
def fake_env(tmpdir, setup_cfg, setup_py=None, package_path='fake_package'): def fake_env(tmpdir, setup_cfg, setup_py=None, encoding='ascii', package_path='fake_package'):
if setup_py is None: if setup_py is None:
setup_py = ( setup_py = (
...@@ -31,7 +39,7 @@ def fake_env(tmpdir, setup_cfg, setup_py=None, package_path='fake_package'): ...@@ -31,7 +39,7 @@ def fake_env(tmpdir, setup_cfg, setup_py=None, package_path='fake_package'):
tmpdir.join('setup.py').write(setup_py) tmpdir.join('setup.py').write(setup_py)
config = tmpdir.join('setup.cfg') config = tmpdir.join('setup.cfg')
config.write(setup_cfg) config.write(setup_cfg.encode(encoding), mode='wb')
package_dir, init_file = make_package_dir(package_path, tmpdir) package_dir, init_file = make_package_dir(package_path, tmpdir)
...@@ -146,6 +154,24 @@ class TestMetadata: ...@@ -146,6 +154,24 @@ class TestMetadata:
assert metadata.download_url == 'http://test.test.com/test/' assert metadata.download_url == 'http://test.test.com/test/'
assert metadata.maintainer_email == 'test@test.com' 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): def test_file_mixed(self, tmpdir):
fake_env( fake_env(
...@@ -288,7 +314,7 @@ class TestMetadata: ...@@ -288,7 +314,7 @@ class TestMetadata:
tmpdir.join('fake_package', 'version.txt').write('1.2.3\n4.5.6\n') tmpdir.join('fake_package', 'version.txt').write('1.2.3\n4.5.6\n')
with pytest.raises(DistutilsOptionError): with pytest.raises(DistutilsOptionError):
with get_dist(tmpdir) as dist: with get_dist(tmpdir) as dist:
_ = dist.metadata.version dist.metadata.version
def test_version_with_package_dir_simple(self, tmpdir): def test_version_with_package_dir_simple(self, tmpdir):
...@@ -391,6 +417,89 @@ class TestMetadata: ...@@ -391,6 +417,89 @@ class TestMetadata:
with get_dist(tmpdir) as dist: with get_dist(tmpdir) as dist:
assert set(dist.metadata.classifiers) == expected assert set(dist.metadata.classifiers) == expected
def test_deprecated_config_handlers(self, tmpdir):
fake_env(
tmpdir,
'[metadata]\n'
'version = 10.1.1\n'
'description = Some description\n'
'requires = some, requirement\n'
)
with pytest.deprecated_call():
with get_dist(tmpdir) as dist:
metadata = dist.metadata
assert metadata.version == '10.1.1'
assert metadata.description == 'Some description'
assert metadata.requires == ['some', 'requirement']
def test_interpolation(self, tmpdir):
fake_env(
tmpdir,
'[metadata]\n'
'description = %(message)s\n'
)
with pytest.raises(InterpolationMissingOptionError):
with get_dist(tmpdir):
pass
skip_if_not_ascii = pytest.mark.skipif(not is_ascii, reason='Test not supported with this locale')
@skip_if_not_ascii
def test_non_ascii_1(self, tmpdir):
fake_env(
tmpdir,
'[metadata]\n'
'description = éàïôñ\n',
encoding='utf-8'
)
with pytest.raises(UnicodeDecodeError):
with get_dist(tmpdir):
pass
def test_non_ascii_2(self, tmpdir):
fake_env(
tmpdir,
'# -*- coding: invalid\n'
)
with pytest.raises(LookupError):
with get_dist(tmpdir):
pass
def test_non_ascii_3(self, tmpdir):
fake_env(
tmpdir,
'\n'
'# -*- coding: invalid\n'
)
with get_dist(tmpdir):
pass
@skip_if_not_ascii
def test_non_ascii_4(self, tmpdir):
fake_env(
tmpdir,
'# -*- coding: utf-8\n'
'[metadata]\n'
'description = éàïôñ\n',
encoding='utf-8'
)
with get_dist(tmpdir) as dist:
assert dist.metadata.description == 'éàïôñ'
@skip_if_not_ascii
def test_non_ascii_5(self, tmpdir):
fake_env(
tmpdir,
'# vim: set fileencoding=iso-8859-15 :\n'
'[metadata]\n'
'description = éàïôñ\n',
encoding='iso-8859-15'
)
with get_dist(tmpdir) as dist:
assert dist.metadata.description == 'éàïôñ'
class TestOptions: class TestOptions:
...@@ -720,7 +829,10 @@ class TestOptions: ...@@ -720,7 +829,10 @@ class TestOptions:
] ]
assert sorted(dist.data_files) == sorted(expected) assert sorted(dist.data_files) == sorted(expected)
saved_dist_init = _Distribution.__init__ saved_dist_init = _Distribution.__init__
class TestExternalSetters: class TestExternalSetters:
# During creation of the setuptools Distribution() object, we call # During creation of the setuptools Distribution() object, we call
# the init of the parent distutils Distribution object via # the init of the parent distutils Distribution object via
......
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import io import io
from setuptools.dist import DistDeprecationWarning, _get_unpatched
from setuptools import Distribution from setuptools import Distribution
from setuptools.extern.six.moves.urllib.request import pathname2url from setuptools.extern.six.moves.urllib.request import pathname2url
from setuptools.extern.six.moves.urllib_parse import urljoin from setuptools.extern.six.moves.urllib_parse import urljoin
from setuptools.extern import six
from .textwrap import DALS from .textwrap import DALS
from .test_easy_install import make_nspkg_sdist from .test_easy_install import make_nspkg_sdist
...@@ -56,6 +57,125 @@ def test_dist_fetch_build_egg(tmpdir): ...@@ -56,6 +57,125 @@ def test_dist_fetch_build_egg(tmpdir):
assert [dist.key for dist in resolved_dists if dist] == reqs assert [dist.key for dist in resolved_dists if dist] == reqs
def test_dist__get_unpatched_deprecated():
pytest.warns(DistDeprecationWarning, _get_unpatched, [""])
def __read_test_cases():
# Metadata version 1.0
base_attrs = {
"name": "package",
"version": "0.0.1",
"author": "Foo Bar",
"author_email": "foo@bar.net",
"long_description": "Long\ndescription",
"description": "Short description",
"keywords": ["one", "two"]
}
def merge_dicts(d1, d2):
d1 = d1.copy()
d1.update(d2)
return d1
test_cases = [
('Metadata version 1.0', base_attrs.copy()),
('Metadata version 1.1: Provides', merge_dicts(base_attrs, {
'provides': ['package']
})),
('Metadata version 1.1: Obsoletes', merge_dicts(base_attrs, {
'obsoletes': ['foo']
})),
('Metadata version 1.1: Classifiers', merge_dicts(base_attrs, {
'classifiers': [
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.7',
'License :: OSI Approved :: MIT License',
]})),
('Metadata version 1.1: Download URL', merge_dicts(base_attrs, {
'download_url': 'https://example.com'
})),
('Metadata Version 1.2: Requires-Python', merge_dicts(base_attrs, {
'python_requires': '>=3.7'
})),
pytest.param(
'Metadata Version 1.2: Project-Url',
merge_dicts(base_attrs, {
'project_urls': {
'Foo': 'https://example.bar'
}
}), marks=pytest.mark.xfail(
reason="Issue #1578: project_urls not read"
)),
('Metadata Version 2.1: Long Description Content Type',
merge_dicts(base_attrs, {
'long_description_content_type': 'text/x-rst; charset=UTF-8'
})),
pytest.param(
'Metadata Version 2.1: Provides Extra',
merge_dicts(base_attrs, {
'provides_extras': ['foo', 'bar']
}), marks=pytest.mark.xfail(reason="provides_extras not read")),
('Missing author, missing author e-mail',
{'name': 'foo', 'version': '1.0.0'}),
('Missing author',
{'name': 'foo',
'version': '1.0.0',
'author_email': 'snorri@sturluson.name'}),
('Missing author e-mail',
{'name': 'foo',
'version': '1.0.0',
'author': 'Snorri Sturluson'}),
('Missing author',
{'name': 'foo',
'version': '1.0.0',
'author': 'Snorri Sturluson'}),
]
return test_cases
@pytest.mark.parametrize('name,attrs', __read_test_cases())
def test_read_metadata(name, attrs):
dist = Distribution(attrs)
metadata_out = dist.metadata
dist_class = metadata_out.__class__
# Write to PKG_INFO and then load into a new metadata object
if six.PY2:
PKG_INFO = io.BytesIO()
else:
PKG_INFO = io.StringIO()
metadata_out.write_pkg_file(PKG_INFO)
PKG_INFO.seek(0)
metadata_in = dist_class()
metadata_in.read_pkg_file(PKG_INFO)
tested_attrs = [
('name', dist_class.get_name),
('version', dist_class.get_version),
('author', dist_class.get_contact),
('author_email', dist_class.get_contact_email),
('metadata_version', dist_class.get_metadata_version),
('provides', dist_class.get_provides),
('description', dist_class.get_description),
('download_url', dist_class.get_download_url),
('keywords', dist_class.get_keywords),
('platforms', dist_class.get_platforms),
('obsoletes', dist_class.get_obsoletes),
('requires', dist_class.get_requires),
('classifiers', dist_class.get_classifiers),
('project_urls', lambda s: getattr(s, 'project_urls', {})),
('provides_extras', lambda s: getattr(s, 'provides_extras', set())),
]
for attr, getter in tested_attrs:
assert getter(metadata_in) == getter(metadata_out)
def __maintainer_test_cases(): def __maintainer_test_cases():
attrs = {"name": "package", attrs = {"name": "package",
"version": "1.0", "version": "1.0",
......
...@@ -15,7 +15,9 @@ import distutils.errors ...@@ -15,7 +15,9 @@ import distutils.errors
import io import io
import zipfile import zipfile
import mock import mock
from setuptools.command.easy_install import (
EasyInstallDeprecationWarning, ScriptWriter, WindowsScriptWriter,
)
import time import time
from setuptools.extern import six from setuptools.extern import six
from setuptools.extern.six.moves import urllib from setuptools.extern.six.moves import urllib
...@@ -287,6 +289,22 @@ class TestEasyInstallTest: ...@@ -287,6 +289,22 @@ class TestEasyInstallTest:
cmd.easy_install(sdist_script) cmd.easy_install(sdist_script)
assert (target / 'mypkg_script').exists() assert (target / 'mypkg_script').exists()
def test_dist_get_script_args_deprecated(self):
with pytest.warns(EasyInstallDeprecationWarning):
ScriptWriter.get_script_args(None, None)
def test_dist_get_script_header_deprecated(self):
with pytest.warns(EasyInstallDeprecationWarning):
ScriptWriter.get_script_header("")
def test_dist_get_writer_deprecated(self):
with pytest.warns(EasyInstallDeprecationWarning):
ScriptWriter.get_writer(None)
def test_dist_WindowsScriptWriter_get_writer_deprecated(self):
with pytest.warns(EasyInstallDeprecationWarning):
WindowsScriptWriter.get_writer()
@pytest.mark.filterwarnings('ignore:Unbuilt egg') @pytest.mark.filterwarnings('ignore:Unbuilt egg')
class TestPTHFileWriter: class TestPTHFileWriter:
......
import datetime
import sys import sys
import ast import ast
import os import os
...@@ -7,7 +6,9 @@ import re ...@@ -7,7 +6,9 @@ import re
import stat import stat
import time import time
from setuptools.command.egg_info import egg_info, manifest_maker from setuptools.command.egg_info import (
egg_info, manifest_maker, EggInfoDeprecationWarning, get_pkg_info_revision,
)
from setuptools.dist import Distribution from setuptools.dist import Distribution
from setuptools.extern.six.moves import map from setuptools.extern.six.moves import map
...@@ -148,6 +149,37 @@ class TestEggInfo: ...@@ -148,6 +149,37 @@ class TestEggInfo:
] ]
assert sorted(actual) == expected assert sorted(actual) == expected
def test_license_is_a_string(self, tmpdir_cwd, env):
setup_config = DALS("""
[metadata]
name=foo
version=0.0.1
license=file:MIT
""")
setup_script = DALS("""
from setuptools import setup
setup()
""")
build_files({'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
# object is lost and we need to parse it from stderr
with pytest.raises(AssertionError) as exc:
self._run_egg_info_command(tmpdir_cwd, env)
# Hopefully this is not too fragile: the only argument to the
# assertion error should be a traceback, ending with:
# ValueError: ....
#
# assert not 1
tb = exc.value.args[0].split('\n')
assert tb[-3].lstrip().startswith('ValueError')
def test_rebuilt(self, tmpdir_cwd, env): def test_rebuilt(self, tmpdir_cwd, env):
"""Ensure timestamps are updated when the command is re-run.""" """Ensure timestamps are updated when the command is re-run."""
self._create_project() self._create_project()
...@@ -618,6 +650,20 @@ class TestEggInfo: ...@@ -618,6 +650,20 @@ class TestEggInfo:
for msg in fixtures: for msg in fixtures:
assert manifest_maker._should_suppress_warning(msg) assert manifest_maker._should_suppress_warning(msg)
def test_egg_info_includes_setup_py(self, tmpdir_cwd):
self._create_project()
dist = Distribution({"name": "foo", "version": "0.0.1"})
dist.script_name = "non_setup.py"
egg_info_instance = egg_info(dist)
egg_info_instance.finalize_options()
egg_info_instance.run()
assert 'setup.py' in egg_info_instance.filelist.files
with open(egg_info_instance.egg_info + "/SOURCES.txt") as f:
sources = f.read().split('\n')
assert 'setup.py' in sources
def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None): def _run_egg_info_command(self, tmpdir_cwd, env, cmd=None, output=None):
environ = os.environ.copy().update( environ = os.environ.copy().update(
HOME=env.paths['home'], HOME=env.paths['home'],
...@@ -632,8 +678,8 @@ class TestEggInfo: ...@@ -632,8 +678,8 @@ class TestEggInfo:
data_stream=1, data_stream=1,
env=environ, env=environ,
) )
if code: assert not code, data
raise AssertionError(data)
if output: if output:
assert output in data assert output in data
...@@ -652,3 +698,52 @@ class TestEggInfo: ...@@ -652,3 +698,52 @@ class TestEggInfo:
with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
pkg_info_lines = pkginfo_file.read().split('\n') pkg_info_lines = pkginfo_file.read().split('\n')
assert 'Version: 0.0.0.dev0' in pkg_info_lines assert 'Version: 0.0.0.dev0' in pkg_info_lines
def test_get_pkg_info_revision_deprecated(self):
pytest.warns(EggInfoDeprecationWarning, get_pkg_info_revision)
EGG_INFO_TESTS = (
# Check for issue #1136: invalid string type when
# reading declarative `setup.cfg` under Python 2.
{
'setup.py': DALS(
"""
from setuptools import setup
setup(
name="foo",
)
"""),
'setup.cfg': DALS(
"""
[options]
package_dir =
= src
"""),
'src': {},
},
# Check Unicode can be used in `setup.py` under Python 2.
{
'setup.py': DALS(
"""
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from setuptools import setup, find_packages
setup(
name="foo",
package_dir={'': 'src'},
)
"""),
'src': {},
}
)
@pytest.mark.parametrize('package_files', EGG_INFO_TESTS)
def test_egg_info(self, tmpdir_cwd, env, package_files):
"""
"""
build_files(package_files)
code, data = environment.run_setup_py(
cmd=['egg_info'],
data_stream=1,
)
assert not code, data
...@@ -14,8 +14,8 @@ from setuptools import find_packages ...@@ -14,8 +14,8 @@ from setuptools import find_packages
if PY3: if PY3:
from setuptools import find_namespace_packages from setuptools import find_namespace_packages
# modeled after CPython's test.support.can_symlink
# modeled after CPython's test.support.can_symlink
def can_symlink(): def can_symlink():
TESTFN = tempfile.mktemp() TESTFN = tempfile.mktemp()
symlink_path = TESTFN + "can_symlink" symlink_path = TESTFN + "can_symlink"
...@@ -164,12 +164,14 @@ class TestFindPackages: ...@@ -164,12 +164,14 @@ class TestFindPackages:
def test_pep420_ns_package_no_includes(self): def test_pep420_ns_package_no_includes(self):
packages = find_namespace_packages( packages = find_namespace_packages(
self.dist_dir, exclude=['pkg.subpkg.assets']) self.dist_dir, exclude=['pkg.subpkg.assets'])
self._assert_packages(packages, ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg']) self._assert_packages(
packages, ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg'])
@py3_only @py3_only
def test_pep420_ns_package_no_includes_or_excludes(self): def test_pep420_ns_package_no_includes_or_excludes(self):
packages = find_namespace_packages(self.dist_dir) packages = find_namespace_packages(self.dist_dir)
expected = ['docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets'] expected = [
'docs', 'pkg', 'pkg.nspkg', 'pkg.subpkg', 'pkg.subpkg.assets']
self._assert_packages(packages, expected) self._assert_packages(packages, expected)
@py3_only @py3_only
...@@ -185,4 +187,3 @@ class TestFindPackages: ...@@ -185,4 +187,3 @@ class TestFindPackages:
shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets')) shutil.rmtree(os.path.join(self.dist_dir, 'pkg/subpkg/assets'))
packages = find_namespace_packages(self.dist_dir) packages = find_namespace_packages(self.dist_dir)
self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg']) self._assert_packages(packages, ['pkg', 'pkg.nspkg', 'pkg.subpkg'])
...@@ -64,7 +64,8 @@ class TestInstallScripts: ...@@ -64,7 +64,8 @@ class TestInstallScripts:
@pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only') @pytest.mark.skipif(sys.platform == 'win32', reason='non-Windows only')
def test_executable_with_spaces_escaping_unix(self, tmpdir): def test_executable_with_spaces_escaping_unix(self, tmpdir):
""" """
Ensure that shebang on Unix is not quoted, even when a value with spaces Ensure that shebang on Unix is not quoted, even when
a value with spaces
is specified using --executable. is specified using --executable.
""" """
expected = '#!%s\n' % self.unix_spaces_exe expected = '#!%s\n' % self.unix_spaces_exe
...@@ -77,7 +78,8 @@ class TestInstallScripts: ...@@ -77,7 +78,8 @@ class TestInstallScripts:
@pytest.mark.skipif(sys.platform != 'win32', reason='Windows only') @pytest.mark.skipif(sys.platform != 'win32', reason='Windows only')
def test_executable_arg_escaping_win32(self, tmpdir): def test_executable_arg_escaping_win32(self, tmpdir):
""" """
Ensure that shebang on Windows is quoted when getting a path with spaces Ensure that shebang on Windows is quoted when
getting a path with spaces
from --executable, that is itself properly quoted. from --executable, that is itself properly quoted.
""" """
expected = '#!"%s"\n' % self.win32_exe expected = '#!"%s"\n' % self.win32_exe
......
...@@ -6,6 +6,11 @@ Try to install a few packages. ...@@ -6,6 +6,11 @@ Try to install a few packages.
import glob import glob
import os import os
import sys import sys
import re
import subprocess
import functools
import tarfile
import zipfile
from setuptools.extern.six.moves import urllib from setuptools.extern.six.moves import urllib
import pytest import pytest
...@@ -114,15 +119,12 @@ def test_pyuri(install_context): ...@@ -114,15 +119,12 @@ def test_pyuri(install_context):
assert os.path.exists(os.path.join(pyuri.location, 'pyuri', 'uri.regex')) assert os.path.exists(os.path.join(pyuri.location, 'pyuri', 'uri.regex'))
import re build_deps = ['appdirs', 'packaging', 'pyparsing', 'six']
import subprocess
import functools
import tarfile, zipfile
build_deps = ['appdirs', 'packaging', 'pyparsing', 'six']
@pytest.mark.parametrize("build_dep", build_deps) @pytest.mark.parametrize("build_dep", build_deps)
@pytest.mark.skipif(sys.version_info < (3, 6), reason='run only on late versions') @pytest.mark.skipif(
sys.version_info < (3, 6), reason='run only on late versions')
def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): def test_build_deps_on_distutils(request, tmpdir_factory, build_dep):
""" """
All setuptools build dependencies must build without All setuptools build dependencies must build without
...@@ -149,13 +151,16 @@ def install(pkg_dir, install_dir): ...@@ -149,13 +151,16 @@ def install(pkg_dir, install_dir):
breaker.write('raise ImportError()') breaker.write('raise ImportError()')
cmd = [sys.executable, 'setup.py', 'install', '--prefix', install_dir] cmd = [sys.executable, 'setup.py', 'install', '--prefix', install_dir]
env = dict(os.environ, PYTHONPATH=pkg_dir) env = dict(os.environ, PYTHONPATH=pkg_dir)
output = subprocess.check_output(cmd, cwd=pkg_dir, env=env, stderr=subprocess.STDOUT) output = subprocess.check_output(
cmd, cwd=pkg_dir, env=env, stderr=subprocess.STDOUT)
return output.decode('utf-8') return output.decode('utf-8')
def download_and_extract(request, req, target): def download_and_extract(request, req, target):
cmd = [sys.executable, '-m', 'pip', 'download', '--no-deps', cmd = [
'--no-binary', ':all:', req] sys.executable, '-m', 'pip', 'download', '--no-deps',
'--no-binary', ':all:', req,
]
output = subprocess.check_output(cmd, encoding='utf-8') output = subprocess.check_output(cmd, encoding='utf-8')
filename = re.search('Saved (.*)', output).group(1) filename = re.search('Saved (.*)', output).group(1)
request.addfinalizer(functools.partial(os.remove, filename)) request.addfinalizer(functools.partial(os.remove, filename))
......
This diff is collapsed.
...@@ -49,7 +49,8 @@ def mock_reg(hkcu=None, hklm=None): ...@@ -49,7 +49,8 @@ def mock_reg(hkcu=None, hklm=None):
for k in hive if k.startswith(key.lower()) for k in hive if k.startswith(key.lower())
) )
return mock.patch.multiple(distutils.msvc9compiler.Reg, return mock.patch.multiple(
distutils.msvc9compiler.Reg,
read_keys=read_keys, read_values=read_values) read_keys=read_keys, read_values=read_values)
...@@ -61,7 +62,7 @@ class TestModulePatch: ...@@ -61,7 +62,7 @@ class TestModulePatch:
""" """
key_32 = r'software\microsoft\devdiv\vcforpython\9.0\installdir' key_32 = r'software\microsoft\devdiv\vcforpython\9.0\installdir'
key_64 = r'software\wow6432node\microsoft\devdiv\vcforpython\9.0\installdir' key_64 = key_32.replace(r'\microsoft', r'\wow6432node\microsoft')
def test_patched(self): def test_patched(self):
"Test the module is actually patched" "Test the module is actually patched"
......
from __future__ import absolute_import, unicode_literals from __future__ import absolute_import, unicode_literals
import os
import sys import sys
import subprocess import subprocess
...@@ -12,7 +11,7 @@ from setuptools.command import test ...@@ -12,7 +11,7 @@ from setuptools.command import test
class TestNamespaces: class TestNamespaces:
@pytest.mark.xfail( @pytest.mark.skipif(
sys.version_info < (3, 5), sys.version_info < (3, 5),
reason="Requires importlib.util.module_from_spec", reason="Requires importlib.util.module_from_spec",
) )
......
...@@ -6,6 +6,8 @@ import distutils.errors ...@@ -6,6 +6,8 @@ import distutils.errors
from setuptools.extern import six from setuptools.extern import six
from setuptools.extern.six.moves import urllib, http_client from setuptools.extern.six.moves import urllib, http_client
import mock
import pytest
import pkg_resources import pkg_resources
import setuptools.package_index import setuptools.package_index
...@@ -42,7 +44,10 @@ class TestPackageIndex: ...@@ -42,7 +44,10 @@ class TestPackageIndex:
hosts=('www.example.com',) hosts=('www.example.com',)
) )
url = 'url:%20https://svn.plone.org/svn/collective/inquant.contentmirror.plone/trunk' url = (
'url:%20https://svn.plone.org/svn'
'/collective/inquant.contentmirror.plone/trunk'
)
try: try:
v = index.open_url(url) v = index.open_url(url)
except Exception as v: except Exception as v:
...@@ -61,9 +66,9 @@ class TestPackageIndex: ...@@ -61,9 +66,9 @@ class TestPackageIndex:
index.opener = _urlopen index.opener = _urlopen
url = 'http://example.com' url = 'http://example.com'
try: try:
v = index.open_url(url) index.open_url(url)
except Exception as v: except Exception as exc:
assert 'line' in str(v) assert 'line' in str(exc)
else: else:
raise AssertionError('Should have raise here!') raise AssertionError('Should have raise here!')
...@@ -81,7 +86,11 @@ class TestPackageIndex: ...@@ -81,7 +86,11 @@ class TestPackageIndex:
index.open_url(url) index.open_url(url)
except distutils.errors.DistutilsError as error: except distutils.errors.DistutilsError as error:
msg = six.text_type(error) msg = six.text_type(error)
assert 'nonnumeric port' in msg or 'getaddrinfo failed' in msg or 'Name or service not known' in msg assert (
'nonnumeric port' in msg
or 'getaddrinfo failed' in msg
or 'Name or service not known' in msg
)
return return
raise RuntimeError("Did not raise") raise RuntimeError("Did not raise")
...@@ -223,6 +232,61 @@ class TestPackageIndex: ...@@ -223,6 +232,61 @@ class TestPackageIndex:
assert dists[0].version == '' assert dists[0].version == ''
assert dists[1].version == vc assert dists[1].version == vc
def test_download_git_with_rev(self, tmpdir):
url = 'git+https://github.example/group/project@master#egg=foo'
index = setuptools.package_index.PackageIndex()
with mock.patch("os.system") as os_system_mock:
result = index.download(url, str(tmpdir))
os_system_mock.assert_called()
expected_dir = str(tmpdir / 'project@master')
expected = (
'git clone --quiet '
'https://github.example/group/project {expected_dir}'
).format(**locals())
first_call_args = os_system_mock.call_args_list[0][0]
assert first_call_args == (expected,)
tmpl = '(cd {expected_dir} && git checkout --quiet master)'
expected = tmpl.format(**locals())
assert os_system_mock.call_args_list[1][0] == (expected,)
assert result == expected_dir
def test_download_git_no_rev(self, tmpdir):
url = 'git+https://github.example/group/project#egg=foo'
index = setuptools.package_index.PackageIndex()
with mock.patch("os.system") as os_system_mock:
result = index.download(url, str(tmpdir))
os_system_mock.assert_called()
expected_dir = str(tmpdir / 'project')
expected = (
'git clone --quiet '
'https://github.example/group/project {expected_dir}'
).format(**locals())
os_system_mock.assert_called_once_with(expected)
def test_download_svn(self, tmpdir):
url = 'svn+https://svn.example/project#egg=foo'
index = setuptools.package_index.PackageIndex()
with pytest.warns(UserWarning):
with mock.patch("os.system") as os_system_mock:
result = index.download(url, str(tmpdir))
os_system_mock.assert_called()
expected_dir = str(tmpdir / 'project')
expected = (
'svn checkout -q '
'svn+https://svn.example/project {expected_dir}'
).format(**locals())
os_system_mock.assert_called_once_with(expected)
class TestContentCheckers: class TestContentCheckers:
def test_md5(self): def test_md5(self):
......
...@@ -32,7 +32,9 @@ class TestPEP425Tags: ...@@ -32,7 +32,9 @@ class TestPEP425Tags:
if sys.version_info < (3, 3): if sys.version_info < (3, 3):
config_vars.update({'Py_UNICODE_SIZE': 2}) config_vars.update({'Py_UNICODE_SIZE': 2})
mock_gcf = self.mock_get_config_var(**config_vars) mock_gcf = self.mock_get_config_var(**config_vars)
with patch('setuptools.pep425tags.sysconfig.get_config_var', mock_gcf): with patch(
'setuptools.pep425tags.sysconfig.get_config_var',
mock_gcf):
abi_tag = pep425tags.get_abi_tag() abi_tag = pep425tags.get_abi_tag()
assert abi_tag == base + flags assert abi_tag == base + flags
......
...@@ -26,7 +26,8 @@ class TestSandbox: ...@@ -26,7 +26,8 @@ class TestSandbox:
""" """
It should be possible to execute a setup.py with a Byte Order Mark It should be possible to execute a setup.py with a Byte Order Mark
""" """
target = pkg_resources.resource_filename(__name__, target = pkg_resources.resource_filename(
__name__,
'script-with-bom.py') 'script-with-bom.py')
namespace = types.ModuleType('namespace') namespace = types.ModuleType('namespace')
setuptools.sandbox._execfile(target, vars(namespace)) setuptools.sandbox._execfile(target, vars(namespace))
......
...@@ -20,8 +20,8 @@ from setuptools.command.egg_info import manifest_maker ...@@ -20,8 +20,8 @@ from setuptools.command.egg_info import manifest_maker
from setuptools.dist import Distribution from setuptools.dist import Distribution
from setuptools.tests import fail_on_ascii from setuptools.tests import fail_on_ascii
from .text import Filenames from .text import Filenames
from . import py3_only
py3_only = pytest.mark.xfail(six.PY2, reason="Test runs on Python 3 only")
SETUP_ATTRS = { SETUP_ATTRS = {
'name': 'sdist_test', 'name': 'sdist_test',
...@@ -92,9 +92,8 @@ fail_on_latin1_encoded_filenames = pytest.mark.xfail( ...@@ -92,9 +92,8 @@ fail_on_latin1_encoded_filenames = pytest.mark.xfail(
class TestSdistTest: class TestSdistTest:
def setup_method(self, method): def setup_method(self, method):
self.temp_dir = tempfile.mkdtemp() self.temp_dir = tempfile.mkdtemp()
f = open(os.path.join(self.temp_dir, 'setup.py'), 'w') with open(os.path.join(self.temp_dir, 'setup.py'), 'w') as f:
f.write(SETUP_PY) f.write(SETUP_PY)
f.close()
# Set up the rest of the test package # Set up the rest of the test package
test_pkg = os.path.join(self.temp_dir, 'sdist_test') test_pkg = os.path.join(self.temp_dir, 'sdist_test')
...@@ -135,6 +134,47 @@ class TestSdistTest: ...@@ -135,6 +134,47 @@ class TestSdistTest:
assert os.path.join('sdist_test', 'c.rst') not in manifest assert os.path.join('sdist_test', 'c.rst') not in manifest
assert os.path.join('d', 'e.dat') in manifest assert os.path.join('d', 'e.dat') in manifest
def test_setup_py_exists(self):
dist = Distribution(SETUP_ATTRS)
dist.script_name = 'foo.py'
cmd = sdist(dist)
cmd.ensure_finalized()
with quiet():
cmd.run()
manifest = cmd.filelist.files
assert 'setup.py' in manifest
def test_setup_py_missing(self):
dist = Distribution(SETUP_ATTRS)
dist.script_name = 'foo.py'
cmd = sdist(dist)
cmd.ensure_finalized()
if os.path.exists("setup.py"):
os.remove("setup.py")
with quiet():
cmd.run()
manifest = cmd.filelist.files
assert 'setup.py' not in manifest
def test_setup_py_excluded(self):
with open("MANIFEST.in", "w") as manifest_file:
manifest_file.write("exclude setup.py")
dist = Distribution(SETUP_ATTRS)
dist.script_name = 'foo.py'
cmd = sdist(dist)
cmd.ensure_finalized()
with quiet():
cmd.run()
manifest = cmd.filelist.files
assert 'setup.py' not in manifest
def test_defaults_case_sensitivity(self): def test_defaults_case_sensitivity(self):
""" """
Make sure default files (README.*, etc.) are added in a case-sensitive Make sure default files (README.*, etc.) are added in a case-sensitive
......
...@@ -77,7 +77,8 @@ class TestDepends: ...@@ -77,7 +77,8 @@ class TestDepends:
from json import __version__ from json import __version__
assert dep.get_module_constant('json', '__version__') == __version__ assert dep.get_module_constant('json', '__version__') == __version__
assert dep.get_module_constant('sys', 'version') == sys.version assert dep.get_module_constant('sys', 'version') == sys.version
assert dep.get_module_constant('setuptools.tests.test_setuptools', '__doc__') == __doc__ assert dep.get_module_constant(
'setuptools.tests.test_setuptools', '__doc__') == __doc__
@needs_bytecode @needs_bytecode
def testRequire(self): def testRequire(self):
...@@ -216,7 +217,8 @@ class TestFeatures: ...@@ -216,7 +217,8 @@ class TestFeatures:
self.req = Require('Distutils', '1.0.3', 'distutils') self.req = Require('Distutils', '1.0.3', 'distutils')
self.dist = makeSetup( self.dist = makeSetup(
features={ features={
'foo': Feature("foo", standard=True, require_features=['baz', self.req]), 'foo': Feature(
"foo", standard=True, require_features=['baz', self.req]),
'bar': Feature("bar", standard=True, packages=['pkg.bar'], 'bar': Feature("bar", standard=True, packages=['pkg.bar'],
py_modules=['bar_et'], remove=['bar.ext'], py_modules=['bar_et'], remove=['bar.ext'],
), ),
...@@ -252,7 +254,8 @@ class TestFeatures: ...@@ -252,7 +254,8 @@ class TestFeatures:
('with-dwim', None, 'include DWIM') in dist.feature_options ('with-dwim', None, 'include DWIM') in dist.feature_options
) )
assert ( assert (
('without-dwim', None, 'exclude DWIM (default)') in dist.feature_options ('without-dwim', None, 'exclude DWIM (default)')
in dist.feature_options
) )
assert ( assert (
('with-bar', None, 'include bar (default)') in dist.feature_options ('with-bar', None, 'include bar (default)') in dist.feature_options
......
...@@ -4,7 +4,6 @@ from __future__ import unicode_literals ...@@ -4,7 +4,6 @@ from __future__ import unicode_literals
from distutils import log from distutils import log
import os import os
import sys
import pytest import pytest
...@@ -93,10 +92,6 @@ def test_test(capfd): ...@@ -93,10 +92,6 @@ def test_test(capfd):
assert out == 'Foo\n' assert out == 'Foo\n'
@pytest.mark.xfail(
sys.version_info < (2, 7),
reason="No discover support for unittest on Python 2.6",
)
@pytest.mark.usefixtures('tmpdir_cwd', 'quiet_log') @pytest.mark.usefixtures('tmpdir_cwd', 'quiet_log')
def test_tests_are_run_once(capfd): def test_tests_are_run_once(capfd):
params = dict( params = dict(
......
import mock import mock
import os
import re
from distutils import log from distutils import log
from distutils.errors import DistutilsError
import pytest import pytest
from setuptools.command.upload import upload from setuptools.command.upload import upload
from setuptools.dist import Distribution from setuptools.dist import Distribution
from setuptools.extern import six
def _parse_upload_body(body):
boundary = u'\r\n----------------GHSKFJDLGDS7543FJKLFHRE75642756743254'
entries = []
name_re = re.compile(u'^Content-Disposition: form-data; name="([^\"]+)"')
for entry in body.split(boundary):
pair = entry.split(u'\r\n\r\n')
if not len(pair) == 2:
continue
key, value = map(six.text_type.strip, pair)
m = name_re.match(key)
if m is not None:
key = m.group(1)
entries.append((key, value))
return entries
@pytest.fixture
def patched_upload(tmpdir):
class Fix:
def __init__(self, cmd, urlopen):
self.cmd = cmd
self.urlopen = urlopen
def __iter__(self):
return iter((self.cmd, self.urlopen))
def get_uploaded_metadata(self):
request = self.urlopen.call_args_list[0][0][0]
body = request.data.decode('utf-8')
entries = dict(_parse_upload_body(body))
return entries
class ResponseMock(mock.Mock):
def getheader(self, name, default=None):
"""Mocked getheader method for response object"""
return {
'content-type': 'text/plain; charset=utf-8',
}.get(name.lower(), default)
with mock.patch('setuptools.command.upload.urlopen') as urlopen:
urlopen.return_value = ResponseMock()
urlopen.return_value.getcode.return_value = 200
urlopen.return_value.read.return_value = b''
content = os.path.join(str(tmpdir), "content_data")
with open(content, 'w') as f:
f.write("Some content")
dist = Distribution()
dist.dist_files = [('sdist', '3.7.0', content)]
cmd = upload(dist)
cmd.announce = mock.Mock()
cmd.username = 'user'
cmd.password = 'hunter2'
yield Fix(cmd, urlopen)
class TestUploadTest: class TestUploadTest:
def test_upload_metadata(self, patched_upload):
cmd, patch = patched_upload
# Set the metadata version to 2.1
cmd.distribution.metadata.metadata_version = '2.1'
# Run the command
cmd.ensure_finalized()
cmd.run()
# Make sure we did the upload
patch.assert_called_once()
# Make sure the metadata version is correct in the headers
entries = patched_upload.get_uploaded_metadata()
assert entries['metadata_version'] == '2.1'
def test_warns_deprecation(self): def test_warns_deprecation(self):
dist = Distribution() dist = Distribution()
dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())] dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())]
...@@ -41,3 +128,86 @@ class TestUploadTest: ...@@ -41,3 +128,86 @@ class TestUploadTest:
"upload instead (https://pypi.org/p/twine/)", "upload instead (https://pypi.org/p/twine/)",
log.WARN log.WARN
) )
@pytest.mark.parametrize('url', [
'https://example.com/a;parameter', # Has parameters
'https://example.com/a?query', # Has query
'https://example.com/a#fragment', # Has fragment
'ftp://example.com', # Invalid scheme
])
def test_upload_file_invalid_url(self, url, patched_upload):
patched_upload.urlopen.side_effect = Exception("Should not be reached")
cmd = patched_upload.cmd
cmd.repository = url
cmd.ensure_finalized()
with pytest.raises(AssertionError):
cmd.run()
def test_upload_file_http_error(self, patched_upload):
patched_upload.urlopen.side_effect = six.moves.urllib.error.HTTPError(
'https://example.com',
404,
'File not found',
None,
None
)
cmd = patched_upload.cmd
cmd.ensure_finalized()
with pytest.raises(DistutilsError):
cmd.run()
cmd.announce.assert_any_call(
'Upload failed (404): File not found',
log.ERROR)
def test_upload_file_os_error(self, patched_upload):
patched_upload.urlopen.side_effect = OSError("Invalid")
cmd = patched_upload.cmd
cmd.ensure_finalized()
with pytest.raises(OSError):
cmd.run()
cmd.announce.assert_any_call('Invalid', log.ERROR)
@mock.patch('setuptools.command.upload.spawn')
def test_upload_file_gpg(self, spawn, patched_upload):
cmd, urlopen = patched_upload
cmd.sign = True
cmd.identity = "Alice"
cmd.dry_run = True
content_fname = cmd.distribution.dist_files[0][2]
signed_file = content_fname + '.asc'
with open(signed_file, 'wb') as f:
f.write("signed-data".encode('utf-8'))
cmd.ensure_finalized()
cmd.run()
# Make sure that GPG was called
spawn.assert_called_once_with([
"gpg", "--detach-sign", "--local-user", "Alice", "-a",
content_fname
], dry_run=True)
# Read the 'signed' data that was transmitted
entries = patched_upload.get_uploaded_metadata()
assert entries['gpg_signature'] == 'signed-data'
def test_show_response_no_error(self, patched_upload):
# This test is just that show_response doesn't throw an error
# It is not really important what the printed response looks like
# in a deprecated command, but we don't want to introduce new
# errors when importing this function from distutils
patched_upload.cmd.show_response = True
patched_upload.cmd.ensure_finalized()
patched_upload.cmd.run()
...@@ -57,9 +57,6 @@ def test_pip_upgrade_from_source(virtualenv): ...@@ -57,9 +57,6 @@ def test_pip_upgrade_from_source(virtualenv):
Check pip can upgrade setuptools from source. Check pip can upgrade setuptools from source.
""" """
dist_dir = virtualenv.workspace dist_dir = virtualenv.workspace
if sys.version_info < (2, 7):
# Python 2.6 support was dropped in wheel 0.30.0.
virtualenv.run('pip install -U "wheel<0.30.0"')
# Generate source distribution / wheel. # Generate source distribution / wheel.
virtualenv.run(' && '.join(( virtualenv.run(' && '.join((
'cd {source}', 'cd {source}',
...@@ -137,3 +134,14 @@ def test_test_command_install_requirements(bare_virtualenv, tmpdir): ...@@ -137,3 +134,14 @@ def test_test_command_install_requirements(bare_virtualenv, tmpdir):
'python setup.py test -s test', 'python setup.py test -s test',
)).format(tmpdir=tmpdir)) )).format(tmpdir=tmpdir))
assert tmpdir.join('success').check() assert tmpdir.join('success').check()
def test_no_missing_dependencies(bare_virtualenv):
"""
Quick and dirty test to ensure all external dependencies are vendored.
"""
for command in ('upload',): # sorted(distutils.command.__all__):
bare_virtualenv.run(' && '.join((
'cd {source}',
'python setup.py {command} -h',
)).format(command=command, source=SOURCE_DIR))
...@@ -63,6 +63,7 @@ WHEEL_INFO_TESTS = ( ...@@ -63,6 +63,7 @@ WHEEL_INFO_TESTS = (
}), }),
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
('filename', 'info'), WHEEL_INFO_TESTS, ('filename', 'info'), WHEEL_INFO_TESTS,
ids=[t[0] for t in WHEEL_INFO_TESTS] ids=[t[0] for t in WHEEL_INFO_TESTS]
...@@ -487,6 +488,7 @@ WHEEL_INSTALL_TESTS = ( ...@@ -487,6 +488,7 @@ WHEEL_INSTALL_TESTS = (
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
'params', WHEEL_INSTALL_TESTS, 'params', WHEEL_INSTALL_TESTS,
ids=list(params['id'] for params in WHEEL_INSTALL_TESTS), ids=list(params['id'] for params in WHEEL_INSTALL_TESTS),
......
...@@ -97,7 +97,8 @@ class TestCLI(WrapperTester): ...@@ -97,7 +97,8 @@ class TestCLI(WrapperTester):
'arg 4\\', 'arg 4\\',
'arg5 a\\\\b', 'arg5 a\\\\b',
] ]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE) proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE)
stdout, stderr = proc.communicate('hello\nworld\n'.encode('ascii')) stdout, stderr = proc.communicate('hello\nworld\n'.encode('ascii'))
actual = stdout.decode('ascii').replace('\r\n', '\n') actual = stdout.decode('ascii').replace('\r\n', '\n')
expected = textwrap.dedent(r""" expected = textwrap.dedent(r"""
...@@ -134,7 +135,11 @@ class TestCLI(WrapperTester): ...@@ -134,7 +135,11 @@ class TestCLI(WrapperTester):
with (tmpdir / 'foo-script.py').open('w') as f: with (tmpdir / 'foo-script.py').open('w') as f:
f.write(self.prep_script(tmpl)) f.write(self.prep_script(tmpl))
cmd = [str(tmpdir / 'foo.exe')] cmd = [str(tmpdir / 'foo.exe')]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) proc = subprocess.Popen(
cmd,
stdout=subprocess.PIPE,
stdin=subprocess.PIPE,
stderr=subprocess.STDOUT)
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
actual = stdout.decode('ascii').replace('\r\n', '\n') actual = stdout.decode('ascii').replace('\r\n', '\n')
expected = textwrap.dedent(r""" expected = textwrap.dedent(r"""
...@@ -172,7 +177,9 @@ class TestGUI(WrapperTester): ...@@ -172,7 +177,9 @@ class TestGUI(WrapperTester):
str(tmpdir / 'test_output.txt'), str(tmpdir / 'test_output.txt'),
'Test Argument', 'Test Argument',
] ]
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.STDOUT) proc = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stdin=subprocess.PIPE,
stderr=subprocess.STDOUT)
stdout, stderr = proc.communicate() stdout, stderr = proc.communicate()
assert not stdout assert not stdout
assert not stderr assert not stderr
......
import unicodedata import unicodedata
import sys import sys
import re
from setuptools.extern import six from setuptools.extern import six
...@@ -42,3 +43,15 @@ def try_encode(string, enc): ...@@ -42,3 +43,15 @@ def try_encode(string, enc):
return string.encode(enc) return string.encode(enc)
except UnicodeEncodeError: except UnicodeEncodeError:
return None return None
CODING_RE = re.compile(br'^[ \t\f]*#.*?coding[:=][ \t]*([-\w.]+)')
def detect_encoding(fp):
first_line = fp.readline()
fp.seek(0)
m = CODING_RE.match(first_line)
if m is None:
return None
return m.group(1).decode('ascii')
...@@ -8,10 +8,11 @@ import posixpath ...@@ -8,10 +8,11 @@ import posixpath
import re import re
import zipfile import zipfile
from pkg_resources import Distribution, PathMetadata, parse_version import pkg_resources
import setuptools
from pkg_resources import parse_version
from setuptools.extern.packaging.utils import canonicalize_name from setuptools.extern.packaging.utils import canonicalize_name
from setuptools.extern.six import PY3 from setuptools.extern.six import PY3
from setuptools import Distribution as SetuptoolsDistribution
from setuptools import pep425tags from setuptools import pep425tags
from setuptools.command.egg_info import write_requirements from setuptools.command.egg_info import write_requirements
...@@ -79,7 +80,7 @@ class Wheel: ...@@ -79,7 +80,7 @@ class Wheel:
return next((True for t in self.tags() if t in supported_tags), False) return next((True for t in self.tags() if t in supported_tags), False)
def egg_name(self): def egg_name(self):
return Distribution( return pkg_resources.Distribution(
project_name=self.project_name, version=self.version, project_name=self.project_name, version=self.version,
platform=(None if self.platform == 'any' else get_platform()), platform=(None if self.platform == 'any' else get_platform()),
).egg_name() + '.egg' ).egg_name() + '.egg'
...@@ -130,9 +131,9 @@ class Wheel: ...@@ -130,9 +131,9 @@ class Wheel:
zf.extractall(destination_eggdir) zf.extractall(destination_eggdir)
# Convert metadata. # Convert metadata.
dist_info = os.path.join(destination_eggdir, dist_info) dist_info = os.path.join(destination_eggdir, dist_info)
dist = Distribution.from_location( dist = pkg_resources.Distribution.from_location(
destination_eggdir, dist_info, destination_eggdir, dist_info,
metadata=PathMetadata(destination_eggdir, dist_info), metadata=pkg_resources.PathMetadata(destination_eggdir, dist_info),
) )
# Note: Evaluate and strip markers now, # Note: Evaluate and strip markers now,
...@@ -155,7 +156,7 @@ class Wheel: ...@@ -155,7 +156,7 @@ class Wheel:
os.path.join(egg_info, 'METADATA'), os.path.join(egg_info, 'METADATA'),
os.path.join(egg_info, 'PKG-INFO'), os.path.join(egg_info, 'PKG-INFO'),
) )
setup_dist = SetuptoolsDistribution( setup_dist = setuptools.Distribution(
attrs=dict( attrs=dict(
install_requires=install_requires, install_requires=install_requires,
extras_require=extras_require, extras_require=extras_require,
......
...@@ -8,7 +8,7 @@ import subprocess ...@@ -8,7 +8,7 @@ import subprocess
from distutils.command.install import INSTALL_SCHEMES from distutils.command.install import INSTALL_SCHEMES
from string import Template from string import Template
from six.moves import urllib from setuptools.extern.six.moves import urllib
def _system_call(*args): def _system_call(*args):
......
...@@ -3,8 +3,11 @@ pytest-flake8; python_version!="3.4" ...@@ -3,8 +3,11 @@ pytest-flake8; python_version!="3.4"
pytest-flake8<=1.0.0; python_version=="3.4" pytest-flake8<=1.0.0; python_version=="3.4"
virtualenv>=13.0.0 virtualenv>=13.0.0
pytest-virtualenv>=1.2.7 pytest-virtualenv>=1.2.7
pytest>=3.0.2 # pytest pinned to <4 due to #1638
pytest>=3.7,<4
wheel wheel
coverage>=4.5.1 coverage>=4.5.1
pytest-cov>=2.5.1 pytest-cov>=2.5.1
paver; python_version>="3.6" paver; python_version>="3.6"
futures; python_version=="2.7"
pip==18.1 # Temporary workaround for #1644.
...@@ -12,7 +12,7 @@ deps=-rtests/requirements.txt ...@@ -12,7 +12,7 @@ deps=-rtests/requirements.txt
# Changed from default (`python -m pip ...`) # Changed from default (`python -m pip ...`)
# to prevent the current working directory # to prevent the current working directory
# from being added to `sys.path`. # from being added to `sys.path`.
install_command={envbindir}/pip install {opts} {packages} install_command=python -c 'import sys; sys.path.remove(""); from pkg_resources import load_entry_point; load_entry_point("pip", "console_scripts", "pip")()' install {opts} {packages}
# Same as above. # Same as above.
list_dependencies_command={envbindir}/pip freeze list_dependencies_command={envbindir}/pip freeze
setenv=COVERAGE_FILE={toxworkdir}/.coverage.{envname} setenv=COVERAGE_FILE={toxworkdir}/.coverage.{envname}
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment