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.
...@@ -41,9 +41,9 @@ Feature Highlights: ...@@ -41,9 +41,9 @@ Feature Highlights:
files for any number of "main" functions in your project. (Note: this is not files for any number of "main" functions in your project. (Note: this is not
a py2exe replacement; the .exe files rely on the local Python installation.) a py2exe replacement; the .exe files rely on the local Python installation.)
* Transparent Pyrex support, so that your setup.py can list ``.pyx`` files and * Transparent Cython support, so that your setup.py can list ``.pyx`` files and
still work even when the end-user doesn't have Pyrex installed (as long as still work even when the end-user doesn't have Cython installed (as long as
you include the Pyrex-generated C in your source distribution) you include the Cython-generated C in your source distribution)
* Command aliases - create project-specific, per-user, or site-wide shortcut * Command aliases - create project-specific, per-user, or site-wide shortcut
names for commonly used commands and options names for commonly used commands and options
...@@ -73,23 +73,17 @@ Developer's Guide ...@@ -73,23 +73,17 @@ Developer's Guide
Installing ``setuptools`` Installing ``setuptools``
========================= =========================
Please follow the `EasyInstall Installation Instructions`_ to install the .. _EasyInstall Installation Instructions: easy_install.html
current stable version of setuptools. In particular, be sure to read the
section on `Custom Installation Locations`_ if you are installing anywhere
other than Python's ``site-packages`` directory.
.. _EasyInstall Installation Instructions: easy_install.html#installation-instructions .. _Custom Installation Locations: easy_install.html
.. _Custom Installation Locations: easy_install.html#custom-installation-locations .. _Installing Packages: https://packaging.python.org/tutorials/installing-packages/
If you want the current in-development version of setuptools, you should first To install the latest version of setuptools, use::
install a stable version, and then run::
ez_setup.py setuptools==dev pip install -U setuptools
This will download and install the latest development (i.e. unstable) version
of setuptools from the Python Subversion sandbox.
Refer to `Installing Packages`_ guide for more information.
Basic Use Basic Use
========= =========
...@@ -1229,121 +1223,53 @@ the quoted part. ...@@ -1229,121 +1223,53 @@ the quoted part.
Distributing a ``setuptools``-based project Distributing a ``setuptools``-based project
=========================================== ===========================================
Using ``setuptools``... Without bundling it! Detailed instructions to distribute a setuptools project can be found at
--------------------------------------------- `Packaging project tutorials`_.
.. warning:: **ez_setup** is deprecated in favor of PIP with **PEP-518** support. .. _Packaging project tutorials: https://packaging.python.org/tutorials/packaging-projects/#generating-distribution-archives
Your users might not have ``setuptools`` installed on their machines, or even Before you begin, make sure you have the latest versions of setuptools and wheel::
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 python3 -m pip install --user --upgrade setuptools wheel
import ez_setup To build a setuptools project, run this command from the same directory where
ez_setup.use_setuptools() setup.py is located::
That's it. The ``ez_setup`` module will automatically download a matching python3 setup.py sdist bdist_wheel
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``, This will generate distribution archives in the `dist` directory.
you can specify the exact version and base download URL as parameters to the
``use_setuptools()`` function. See the function's docstring for details.)
Before you upload the generated archives make sure you're registered on
https://test.pypi.org/account/register/. You will also need to verify your email
to be able to upload any packages.
You should install twine to be able to upload packages::
What Your Users Should Know python3 -m pip install --user --upgrade setuptools wheel
---------------------------
Now, to upload these archives, run::
twine upload --repository-url https://test.pypi.org/legacy/ dist/*
To install your newly uploaded package ``example_pkg``, you can use pip::
python3 -m pip install --index-url https://test.pypi.org/simple/ example_pkg
If you have issues at any point, please refer to `Packaging project tutorials`_
for clarification.
Distributing legacy ``setuptools`` projects using ez_setup.py
-------------------------------------------------------------
.. warning:: **ez_setup** is deprecated in favor of PIP with **PEP-518** support.
In general, a setuptools-based project looks just like any distutils-based Distributing packages using the legacy ``ez_setup.py`` and ``easy_install`` is
project -- as long as your users have an internet connection and are installing deprecated in favor of PIP. Please consider migrating to using pip and twine based
to ``site-packages``, that is. But for some users, these conditions don't distribution.
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.
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.
However, if you still have any ``ez_setup`` based packages, documentation for
ez_setup based distributions can be found at `ez_setup distribution guide`_.
.. _ez_setup distribution guide: ez_setup.html
Setting the ``zip_safe`` flag Setting the ``zip_safe`` flag
----------------------------- -----------------------------
...@@ -1651,29 +1577,43 @@ See the sections below on the `egg_info`_ and `alias`_ commands for more ideas. ...@@ -1651,29 +1577,43 @@ See the sections below on the `egg_info`_ and `alias`_ commands for more ideas.
Distributing Extensions compiled with Pyrex Distributing Extensions compiled with Cython
------------------------------------------- --------------------------------------------
``setuptools`` will detect at build time whether Cython is installed or not.
If Cython is not found ``setuptools`` will ignore pyx files.
To ensure Cython is available, include Cython in the build-requires section
of your pyproject.toml::
[build-system]
requires=[..., 'cython']
``setuptools`` includes transparent support for building Pyrex extensions, as Built with pip 10 or later, that declaration is sufficient to include Cython
long as you define your extensions using ``setuptools.Extension``, *not* in the build. For broader compatibility, declare the dependency in your
``distutils.Extension``. You must also not import anything from Pyrex in setup-requires of setup.cfg::
your setup script.
[options]
setup_requires =
...
cython
As long as Cython is present in the build environment, ``setuptools`` includes
transparent support for building Cython extensions, as
long as extensions are defined using ``setuptools.Extension``.
If you follow these rules, you can safely list ``.pyx`` files as the source If you follow these rules, you can safely list ``.pyx`` files as the source
of your ``Extension`` objects in the setup script. ``setuptools`` will detect of your ``Extension`` objects in the setup script. If it is, then ``setuptools``
at build time whether Pyrex is installed or not. If it is, then ``setuptools`` will use it.
will use it. If not, then ``setuptools`` will silently change the
``Extension`` objects to refer to the ``.c`` counterparts of the ``.pyx``
files, so that the normal distutils C compilation process will occur.
Of course, for this to work, your source distributions must include the C Of course, for this to work, your source distributions must include the C
code generated by Pyrex, as well as your original ``.pyx`` files. This means code generated by Cython, as well as your original ``.pyx`` files. This means
that you will probably want to include current ``.c`` files in your revision that you will probably want to include current ``.c`` files in your revision
control system, rebuilding them whenever you check changes in for the ``.pyx`` control system, rebuilding them whenever you check changes in for the ``.pyx``
source files. This will ensure that people tracking your project in a revision source files. This will ensure that people tracking your project in a revision
control system will be able to build it even if they don't have Pyrex control system will be able to build it even if they don't have Cython
installed, and that your source releases will be similarly usable with or installed, and that your source releases will be similarly usable with or
without Pyrex. without Cython.
----------------- -----------------
...@@ -2046,52 +1986,6 @@ specified in ``setup.cfg``:: ...@@ -2046,52 +1986,6 @@ specified in ``setup.cfg``::
(Notice that ``egg_info`` must always appear on the command line *before* any (Notice that ``egg_info`` must always appear on the command line *before* any
commands that you want the version changes to apply to.) commands that you want the version changes to apply to.)
.. _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.
.. _rotate: .. _rotate:
``rotate`` - Delete outdated distribution files ``rotate`` - Delete outdated distribution files
...@@ -2400,6 +2294,35 @@ Metadata and options are set in the config sections of the same name. ...@@ -2400,6 +2294,35 @@ Metadata and options are set in the config sections of the same name.
* Unknown keys are ignored. * Unknown keys are ignored.
Using a ``src/`` layout
=======================
One commonly used package configuration has all the module source code in a
subdirectory (often called the ``src/`` layout), like this::
├── src
│   └── mypackage
│   ├── __init__.py
│   └── mod1.py
├── setup.py
└── setup.cfg
You can set up your ``setup.cfg`` to automatically find all your packages in
the subdirectory like this:
.. code-block:: ini
# This example contains just the necessary options for a src-layout, set up
# the rest of the file as described above.
[options]
package_dir=
=src
packages=find:
[options.packages.find]
where=src
Specifying values Specifying values
================= =================
...@@ -2434,42 +2357,45 @@ Metadata ...@@ -2434,42 +2357,45 @@ Metadata
The aliases given below are supported for compatibility reasons, The aliases given below are supported for compatibility reasons,
but their use is not advised. but their use is not advised.
============================== ================= ===== ============================== ================= ================= =============== =====
Key Aliases Type Key Aliases Type Minimum Version Notes
============================== ================= ===== ============================== ================= ================= =============== =====
name str name str
version attr:, file:, str version attr:, file:, str 39.2.0 (1)
url home-page str url home-page str
download_url download-url str download_url download-url str
project_urls dict project_urls dict 38.3.0
author str author str
author_email author-email str author_email author-email str
maintainer str maintainer str
maintainer_email maintainer-email str maintainer_email maintainer-email str
classifiers classifier file:, list-comma classifiers classifier file:, list-comma
license file:, str license str
license_file str license_file str
description summary file:, str description summary file:, str
long_description long-description file:, str long_description long-description file:, str
long_description_content_type str long_description_content_type str 38.6.0
keywords list-comma keywords list-comma
platforms platform list-comma platforms platform list-comma
provides list-comma provides list-comma
requires list-comma requires list-comma
obsoletes list-comma obsoletes list-comma
============================== ================= ===== ============================== ================= ================= =============== =====
.. note:: .. note::
A version loaded using the ``file:`` directive must comply with PEP 440. A version loaded using the ``file:`` directive must comply with PEP 440.
It is easy to accidentally put something other than a valid version It is easy to accidentally put something other than a valid version
string in such a file, so validation is stricter in this case. string in such a file, so validation is stricter in this case.
Notes:
1. The `version` file attribute has only been supported since 39.2.0.
Options Options
------- -------
======================= ===== ======================= =================================== =============== =====
Key Type Key Type Minimum Version Notes
======================= ===== ======================= =================================== =============== =====
zip_safe bool zip_safe bool
setup_requires list-semi setup_requires list-semi
install_requires list-semi install_requires list-semi
...@@ -2491,7 +2417,8 @@ package_data section ...@@ -2491,7 +2417,8 @@ package_data section
exclude_package_data section exclude_package_data section
namespace_packages list-comma namespace_packages list-comma
py_modules list-comma py_modules list-comma
======================= ===== data_files dict 40.6.0
======================= =================================== =============== =====
.. note:: .. note::
......
...@@ -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(
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
__all__ = ['Distribution'] __all__ = ['Distribution']
import io
import sys
import re import re
import os import os
import warnings import warnings
...@@ -9,8 +11,15 @@ import distutils.log ...@@ -9,8 +11,15 @@ import distutils.log
import distutils.core import distutils.core
import distutils.cmd import distutils.cmd
import distutils.dist import distutils.dist
from distutils.errors import DistutilsOptionError
from distutils.util import strtobool
from distutils.debug import DEBUG
from distutils.fancy_getopt import translate_longopt
import itertools import itertools
from collections import defaultdict from collections import defaultdict
from email import message_from_file
from distutils.errors import ( from distutils.errors import (
DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError, DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError,
) )
...@@ -21,51 +30,121 @@ from setuptools.extern import six ...@@ -21,51 +30,121 @@ from setuptools.extern import six
from setuptools.extern import packaging from setuptools.extern import packaging
from setuptools.extern.six.moves import map, filter, filterfalse from setuptools.extern.six.moves import map, filter, filterfalse
from . import SetuptoolsDeprecationWarning
from setuptools.depends import Require from setuptools.depends import Require
from setuptools import windows_support from setuptools import windows_support
from setuptools.monkey import get_unpatched from setuptools.monkey import get_unpatched
from setuptools.config import parse_configuration from setuptools.config import parse_configuration
from .unicode_utils import detect_encoding
import pkg_resources import pkg_resources
from .py36compat import Distribution_parse_config_files
__import__('setuptools.extern.packaging.specifiers') __import__('setuptools.extern.packaging.specifiers')
__import__('setuptools.extern.packaging.version') __import__('setuptools.extern.packaging.version')
def _get_unpatched(cls): def _get_unpatched(cls):
warnings.warn("Do not call this function", DeprecationWarning) warnings.warn("Do not call this function", DistDeprecationWarning)
return get_unpatched(cls) return get_unpatched(cls)
def get_metadata_version(dist_md): def get_metadata_version(self):
if dist_md.long_description_content_type or dist_md.provides_extras: mv = getattr(self, 'metadata_version', None)
return StrictVersion('2.1')
elif (dist_md.maintainer is not None or if mv is None:
dist_md.maintainer_email is not None or if self.long_description_content_type or self.provides_extras:
getattr(dist_md, 'python_requires', None) is not None): mv = StrictVersion('2.1')
return StrictVersion('1.2') elif (self.maintainer is not None or
elif (dist_md.provides or dist_md.requires or dist_md.obsoletes or self.maintainer_email is not None or
dist_md.classifiers or dist_md.download_url): getattr(self, 'python_requires', None) is not None):
return StrictVersion('1.1') mv = StrictVersion('1.2')
elif (self.provides or self.requires or self.obsoletes or
self.classifiers or self.download_url):
mv = StrictVersion('1.1')
else:
mv = StrictVersion('1.0')
self.metadata_version = mv
return mv
def read_pkg_file(self, file):
"""Reads the metadata values from a file object."""
msg = message_from_file(file)
def _read_field(name):
value = msg[name]
if value == 'UNKNOWN':
return None
return value
def _read_list(name):
values = msg.get_all(name, None)
if values == []:
return None
return values
self.metadata_version = StrictVersion(msg['metadata-version'])
self.name = _read_field('name')
self.version = _read_field('version')
self.description = _read_field('summary')
# we are filling author only.
self.author = _read_field('author')
self.maintainer = None
self.author_email = _read_field('author-email')
self.maintainer_email = None
self.url = _read_field('home-page')
self.license = _read_field('license')
if 'download-url' in msg:
self.download_url = _read_field('download-url')
else:
self.download_url = None
return StrictVersion('1.0') self.long_description = _read_field('description')
self.description = _read_field('summary')
if 'keywords' in msg:
self.keywords = _read_field('keywords').split(',')
self.platforms = _read_list('platform')
self.classifiers = _read_list('classifier')
# PEP 314 - these fields only exist in 1.1
if self.metadata_version == StrictVersion('1.1'):
self.requires = _read_list('requires')
self.provides = _read_list('provides')
self.obsoletes = _read_list('obsoletes')
else:
self.requires = None
self.provides = None
self.obsoletes = None
# Based on Python 3.5 version # Based on Python 3.5 version
def write_pkg_file(self, file): def write_pkg_file(self, file):
"""Write the PKG-INFO format data to a file object. """Write the PKG-INFO format data to a file object.
""" """
version = get_metadata_version(self) version = self.get_metadata_version()
file.write('Metadata-Version: %s\n' % version) if six.PY2:
file.write('Name: %s\n' % self.get_name()) def write_field(key, value):
file.write('Version: %s\n' % self.get_version()) file.write("%s: %s\n" % (key, self._encode_field(value)))
file.write('Summary: %s\n' % self.get_description()) else:
file.write('Home-page: %s\n' % self.get_url()) def write_field(key, value):
file.write("%s: %s\n" % (key, value))
write_field('Metadata-Version', str(version))
write_field('Name', self.get_name())
write_field('Version', self.get_version())
write_field('Summary', self.get_description())
write_field('Home-page', self.get_url())
if version < StrictVersion('1.2'): if version < StrictVersion('1.2'):
file.write('Author: %s\n' % self.get_contact()) write_field('Author', self.get_contact())
file.write('Author-email: %s\n' % self.get_contact_email()) write_field('Author-email', self.get_contact_email())
else: else:
optional_fields = ( optional_fields = (
('Author', 'author'), ('Author', 'author'),
...@@ -76,28 +155,26 @@ def write_pkg_file(self, file): ...@@ -76,28 +155,26 @@ def write_pkg_file(self, file):
for field, attr in optional_fields: for field, attr in optional_fields:
attr_val = getattr(self, attr) attr_val = getattr(self, attr)
if six.PY2:
attr_val = self._encode_field(attr_val)
if attr_val is not None: if attr_val is not None:
file.write('%s: %s\n' % (field, attr_val)) write_field(field, attr_val)
file.write('License: %s\n' % self.get_license()) write_field('License', self.get_license())
if self.download_url: if self.download_url:
file.write('Download-URL: %s\n' % self.download_url) write_field('Download-URL', self.download_url)
for project_url in self.project_urls.items(): for project_url in self.project_urls.items():
file.write('Project-URL: %s, %s\n' % project_url) write_field('Project-URL', '%s, %s' % project_url)
long_desc = rfc822_escape(self.get_long_description()) long_desc = rfc822_escape(self.get_long_description())
file.write('Description: %s\n' % long_desc) write_field('Description', long_desc)
keywords = ','.join(self.get_keywords()) keywords = ','.join(self.get_keywords())
if keywords: if keywords:
file.write('Keywords: %s\n' % keywords) write_field('Keywords', keywords)
if version >= StrictVersion('1.2'): if version >= StrictVersion('1.2'):
for platform in self.get_platforms(): for platform in self.get_platforms():
file.write('Platform: %s\n' % platform) write_field('Platform', platform)
else: else:
self._write_list(file, 'Platform', self.get_platforms()) self._write_list(file, 'Platform', self.get_platforms())
...@@ -110,17 +187,17 @@ def write_pkg_file(self, file): ...@@ -110,17 +187,17 @@ def write_pkg_file(self, file):
# Setuptools specific for PEP 345 # Setuptools specific for PEP 345
if hasattr(self, 'python_requires'): if hasattr(self, 'python_requires'):
file.write('Requires-Python: %s\n' % self.python_requires) write_field('Requires-Python', self.python_requires)
# PEP 566 # PEP 566
if self.long_description_content_type: if self.long_description_content_type:
file.write( write_field(
'Description-Content-Type: %s\n' % 'Description-Content-Type',
self.long_description_content_type self.long_description_content_type
) )
if self.provides_extras: if self.provides_extras:
for extra in self.provides_extras: for extra in self.provides_extras:
file.write('Provides-Extra: %s\n' % extra) write_field('Provides-Extra', extra)
sequence = tuple, list sequence = tuple, list
...@@ -260,7 +337,7 @@ def check_packages(dist, attr, value): ...@@ -260,7 +337,7 @@ def check_packages(dist, attr, value):
_Distribution = get_unpatched(distutils.core.Distribution) _Distribution = get_unpatched(distutils.core.Distribution)
class Distribution(Distribution_parse_config_files, _Distribution): class Distribution(_Distribution):
"""Distribution with support for features, tests, and package data """Distribution with support for features, tests, and package data
This is an enhanced version of 'distutils.dist.Distribution' that This is an enhanced version of 'distutils.dist.Distribution' that
...@@ -484,12 +561,125 @@ class Distribution(Distribution_parse_config_files, _Distribution): ...@@ -484,12 +561,125 @@ class Distribution(Distribution_parse_config_files, _Distribution):
req.marker = None req.marker = None
return req return req
def _parse_config_files(self, filenames=None):
"""
Adapted from distutils.dist.Distribution.parse_config_files,
this method provides the same functionality in subtly-improved
ways.
"""
from setuptools.extern.six.moves.configparser import ConfigParser
# Ignore install directory options if we have a venv
if six.PY3 and 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()
for filename in filenames:
with io.open(filename, 'rb') as fp:
encoding = detect_encoding(fp)
if DEBUG:
self.announce(" reading %s [%s]" % (
filename, encoding or 'locale')
)
reader = io.TextIOWrapper(fp, encoding=encoding)
(parser.read_file if six.PY3 else parser.readfp)(reader)
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)
def _set_command_options(self, command_obj, option_dict=None):
"""
Set the options for 'command_obj' from 'option_dict'. Basically
this means copying elements of a dictionary ('option_dict') to
attributes of an instance ('command').
'command_obj' must be a Command instance. If 'option_dict' is not
supplied, uses the standard option dictionary for this command
(from 'self.command_options').
(Adopted from distutils.dist.Distribution._set_command_options)
"""
command_name = command_obj.get_command_name()
if option_dict is None:
option_dict = self.get_option_dict(command_name)
if DEBUG:
self.announce(" setting options for '%s' command:" % command_name)
for (option, (source, value)) in option_dict.items():
if DEBUG:
self.announce(" %s = %s (from %s)" % (option, value,
source))
try:
bool_opts = [translate_longopt(o)
for o in command_obj.boolean_options]
except AttributeError:
bool_opts = []
try:
neg_opt = command_obj.negative_opt
except AttributeError:
neg_opt = {}
try:
is_string = isinstance(value, six.string_types)
if option in neg_opt and is_string:
setattr(command_obj, neg_opt[option], not strtobool(value))
elif option in bool_opts and is_string:
setattr(command_obj, option, strtobool(value))
elif hasattr(command_obj, option):
setattr(command_obj, option, value)
else:
raise DistutilsOptionError(
"error in %s: command '%s' has no such option '%s'"
% (source, command_name, option))
except ValueError as msg:
raise DistutilsOptionError(msg)
def parse_config_files(self, filenames=None, ignore_option_errors=False): def parse_config_files(self, filenames=None, ignore_option_errors=False):
"""Parses configuration files from various levels """Parses configuration files from various levels
and loads configuration. and loads configuration.
""" """
_Distribution.parse_config_files(self, filenames=filenames) self._parse_config_files(filenames=filenames)
parse_configuration(self, self.command_options, parse_configuration(self, self.command_options,
ignore_option_errors=ignore_option_errors) ignore_option_errors=ignore_option_errors)
...@@ -980,7 +1170,7 @@ class Feature: ...@@ -980,7 +1170,7 @@ class Feature:
"Features are deprecated and will be removed in a future " "Features are deprecated and will be removed in a future "
"version. See https://github.com/pypa/setuptools/issues/65." "version. See https://github.com/pypa/setuptools/issues/65."
) )
warnings.warn(msg, DeprecationWarning, stacklevel=3) warnings.warn(msg, DistDeprecationWarning, stacklevel=3)
def __init__( def __init__(
self, description, standard=False, available=True, self, description, standard=False, available=True,
...@@ -1069,3 +1259,7 @@ class Feature: ...@@ -1069,3 +1259,7 @@ class Feature:
" doesn't contain any packages or modules under %s" " doesn't contain any packages or modules under %s"
% (self.description, item, item) % (self.description, item, item)
) )
class DistDeprecationWarning(SetuptoolsDeprecationWarning):
"""Class for warning about deprecations in dist in setuptools. Not ignored by default, unlike DeprecationWarning."""
...@@ -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))
......
...@@ -20,8 +20,6 @@ import pytest ...@@ -20,8 +20,6 @@ import pytest
__metaclass__ = type __metaclass__ = type
py3_only = pytest.mark.xfail(six.PY2, reason="Test runs on Python 3 only")
def make_local_path(s): def make_local_path(s):
"""Converts '/' in a string to os.sep""" """Converts '/' in a string to os.sep"""
...@@ -75,7 +73,9 @@ translate_specs = [ ...@@ -75,7 +73,9 @@ translate_specs = [
# Glob matching # Glob matching
('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']), ('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']),
('dir/*.txt', ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']), (
'dir/*.txt',
['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']),
('*/*.py', ['bin/start.py'], []), ('*/*.py', ['bin/start.py'], []),
('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']), ('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']),
...@@ -244,77 +244,77 @@ class TestManifestTest(TempDirTestCase): ...@@ -244,77 +244,77 @@ class TestManifestTest(TempDirTestCase):
def test_exclude(self): def test_exclude(self):
"""Include everything in app/ except the text files""" """Include everything in app/ except the text files"""
l = make_local_path ml = make_local_path
self.make_manifest( self.make_manifest(
""" """
include app/* include app/*
exclude app/*.txt exclude app/*.txt
""") """)
files = default_files | set([l('app/c.rst')]) files = default_files | set([ml('app/c.rst')])
assert files == self.get_files() assert files == self.get_files()
def test_include_multiple(self): def test_include_multiple(self):
"""Include with multiple patterns.""" """Include with multiple patterns."""
l = make_local_path ml = make_local_path
self.make_manifest("include app/*.txt app/static/*") self.make_manifest("include app/*.txt app/static/*")
files = default_files | set([ files = default_files | set([
l('app/a.txt'), l('app/b.txt'), ml('app/a.txt'), ml('app/b.txt'),
l('app/static/app.js'), l('app/static/app.js.map'), ml('app/static/app.js'), ml('app/static/app.js.map'),
l('app/static/app.css'), l('app/static/app.css.map')]) ml('app/static/app.css'), ml('app/static/app.css.map')])
assert files == self.get_files() assert files == self.get_files()
def test_graft(self): def test_graft(self):
"""Include the whole app/static/ directory.""" """Include the whole app/static/ directory."""
l = make_local_path ml = make_local_path
self.make_manifest("graft app/static") self.make_manifest("graft app/static")
files = default_files | set([ files = default_files | set([
l('app/static/app.js'), l('app/static/app.js.map'), ml('app/static/app.js'), ml('app/static/app.js.map'),
l('app/static/app.css'), l('app/static/app.css.map')]) ml('app/static/app.css'), ml('app/static/app.css.map')])
assert files == self.get_files() assert files == self.get_files()
def test_graft_glob_syntax(self): def test_graft_glob_syntax(self):
"""Include the whole app/static/ directory.""" """Include the whole app/static/ directory."""
l = make_local_path ml = make_local_path
self.make_manifest("graft */static") self.make_manifest("graft */static")
files = default_files | set([ files = default_files | set([
l('app/static/app.js'), l('app/static/app.js.map'), ml('app/static/app.js'), ml('app/static/app.js.map'),
l('app/static/app.css'), l('app/static/app.css.map')]) ml('app/static/app.css'), ml('app/static/app.css.map')])
assert files == self.get_files() assert files == self.get_files()
def test_graft_global_exclude(self): def test_graft_global_exclude(self):
"""Exclude all *.map files in the project.""" """Exclude all *.map files in the project."""
l = make_local_path ml = make_local_path
self.make_manifest( self.make_manifest(
""" """
graft app/static graft app/static
global-exclude *.map global-exclude *.map
""") """)
files = default_files | set([ files = default_files | set([
l('app/static/app.js'), l('app/static/app.css')]) ml('app/static/app.js'), ml('app/static/app.css')])
assert files == self.get_files() assert files == self.get_files()
def test_global_include(self): def test_global_include(self):
"""Include all *.rst, *.js, and *.css files in the whole tree.""" """Include all *.rst, *.js, and *.css files in the whole tree."""
l = make_local_path ml = make_local_path
self.make_manifest( self.make_manifest(
""" """
global-include *.rst *.js *.css global-include *.rst *.js *.css
""") """)
files = default_files | set([ files = default_files | set([
'.hidden.rst', 'testing.rst', l('app/c.rst'), '.hidden.rst', 'testing.rst', ml('app/c.rst'),
l('app/static/app.js'), l('app/static/app.css')]) ml('app/static/app.js'), ml('app/static/app.css')])
assert files == self.get_files() assert files == self.get_files()
def test_graft_prune(self): def test_graft_prune(self):
"""Include all files in app/, except for the whole app/static/ dir.""" """Include all files in app/, except for the whole app/static/ dir."""
l = make_local_path ml = make_local_path
self.make_manifest( self.make_manifest(
""" """
graft app graft app
prune app/static prune app/static
""") """)
files = default_files | set([ files = default_files | set([
l('app/a.txt'), l('app/b.txt'), l('app/c.rst')]) ml('app/a.txt'), ml('app/b.txt'), ml('app/c.rst')])
assert files == self.get_files() assert files == self.get_files()
...@@ -370,7 +370,7 @@ class TestFileListTest(TempDirTestCase): ...@@ -370,7 +370,7 @@ class TestFileListTest(TempDirTestCase):
def test_process_template_line(self): def test_process_template_line(self):
# testing all MANIFEST.in template patterns # testing all MANIFEST.in template patterns
file_list = FileList() file_list = FileList()
l = make_local_path ml = make_local_path
# simulated file list # simulated file list
self.make_files([ self.make_files([
...@@ -378,16 +378,16 @@ class TestFileListTest(TempDirTestCase): ...@@ -378,16 +378,16 @@ class TestFileListTest(TempDirTestCase):
'buildout.cfg', 'buildout.cfg',
# filelist does not filter out VCS directories, # filelist does not filter out VCS directories,
# it's sdist that does # it's sdist that does
l('.hg/last-message.txt'), ml('.hg/last-message.txt'),
l('global/one.txt'), ml('global/one.txt'),
l('global/two.txt'), ml('global/two.txt'),
l('global/files.x'), ml('global/files.x'),
l('global/here.tmp'), ml('global/here.tmp'),
l('f/o/f.oo'), ml('f/o/f.oo'),
l('dir/graft-one'), ml('dir/graft-one'),
l('dir/dir2/graft2'), ml('dir/dir2/graft2'),
l('dir3/ok'), ml('dir3/ok'),
l('dir3/sub/ok.txt'), ml('dir3/sub/ok.txt'),
]) ])
MANIFEST_IN = DALS("""\ MANIFEST_IN = DALS("""\
...@@ -414,12 +414,12 @@ class TestFileListTest(TempDirTestCase): ...@@ -414,12 +414,12 @@ class TestFileListTest(TempDirTestCase):
'buildout.cfg', 'buildout.cfg',
'four.txt', 'four.txt',
'ok', 'ok',
l('.hg/last-message.txt'), ml('.hg/last-message.txt'),
l('dir/graft-one'), ml('dir/graft-one'),
l('dir/dir2/graft2'), ml('dir/dir2/graft2'),
l('f/o/f.oo'), ml('f/o/f.oo'),
l('global/one.txt'), ml('global/one.txt'),
l('global/two.txt'), ml('global/two.txt'),
] ]
file_list.sort() file_list.sort()
...@@ -476,10 +476,10 @@ class TestFileListTest(TempDirTestCase): ...@@ -476,10 +476,10 @@ class TestFileListTest(TempDirTestCase):
assert False, "Should have thrown an error" assert False, "Should have thrown an error"
def test_include(self): def test_include(self):
l = make_local_path ml = make_local_path
# include # include
file_list = FileList() file_list = FileList()
self.make_files(['a.py', 'b.txt', l('d/c.py')]) self.make_files(['a.py', 'b.txt', ml('d/c.py')])
file_list.process_template_line('include *.py') file_list.process_template_line('include *.py')
file_list.sort() file_list.sort()
...@@ -492,42 +492,42 @@ class TestFileListTest(TempDirTestCase): ...@@ -492,42 +492,42 @@ class TestFileListTest(TempDirTestCase):
self.assertWarnings() self.assertWarnings()
def test_exclude(self): def test_exclude(self):
l = make_local_path ml = make_local_path
# exclude # exclude
file_list = FileList() file_list = FileList()
file_list.files = ['a.py', 'b.txt', l('d/c.py')] file_list.files = ['a.py', 'b.txt', ml('d/c.py')]
file_list.process_template_line('exclude *.py') file_list.process_template_line('exclude *.py')
file_list.sort() file_list.sort()
assert file_list.files == ['b.txt', l('d/c.py')] assert file_list.files == ['b.txt', ml('d/c.py')]
self.assertNoWarnings() self.assertNoWarnings()
file_list.process_template_line('exclude *.rb') file_list.process_template_line('exclude *.rb')
file_list.sort() file_list.sort()
assert file_list.files == ['b.txt', l('d/c.py')] assert file_list.files == ['b.txt', ml('d/c.py')]
self.assertWarnings() self.assertWarnings()
def test_global_include(self): def test_global_include(self):
l = make_local_path ml = make_local_path
# global-include # global-include
file_list = FileList() file_list = FileList()
self.make_files(['a.py', 'b.txt', l('d/c.py')]) self.make_files(['a.py', 'b.txt', ml('d/c.py')])
file_list.process_template_line('global-include *.py') file_list.process_template_line('global-include *.py')
file_list.sort() file_list.sort()
assert file_list.files == ['a.py', l('d/c.py')] assert file_list.files == ['a.py', ml('d/c.py')]
self.assertNoWarnings() self.assertNoWarnings()
file_list.process_template_line('global-include *.rb') file_list.process_template_line('global-include *.rb')
file_list.sort() file_list.sort()
assert file_list.files == ['a.py', l('d/c.py')] assert file_list.files == ['a.py', ml('d/c.py')]
self.assertWarnings() self.assertWarnings()
def test_global_exclude(self): def test_global_exclude(self):
l = make_local_path ml = make_local_path
# global-exclude # global-exclude
file_list = FileList() file_list = FileList()
file_list.files = ['a.py', 'b.txt', l('d/c.py')] file_list.files = ['a.py', 'b.txt', ml('d/c.py')]
file_list.process_template_line('global-exclude *.py') file_list.process_template_line('global-exclude *.py')
file_list.sort() file_list.sort()
...@@ -540,65 +540,65 @@ class TestFileListTest(TempDirTestCase): ...@@ -540,65 +540,65 @@ class TestFileListTest(TempDirTestCase):
self.assertWarnings() self.assertWarnings()
def test_recursive_include(self): def test_recursive_include(self):
l = make_local_path ml = make_local_path
# recursive-include # recursive-include
file_list = FileList() file_list = FileList()
self.make_files(['a.py', l('d/b.py'), l('d/c.txt'), l('d/d/e.py')]) self.make_files(['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')])
file_list.process_template_line('recursive-include d *.py') file_list.process_template_line('recursive-include d *.py')
file_list.sort() file_list.sort()
assert file_list.files == [l('d/b.py'), l('d/d/e.py')] assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
self.assertNoWarnings() self.assertNoWarnings()
file_list.process_template_line('recursive-include e *.py') file_list.process_template_line('recursive-include e *.py')
file_list.sort() file_list.sort()
assert file_list.files == [l('d/b.py'), l('d/d/e.py')] assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
self.assertWarnings() self.assertWarnings()
def test_recursive_exclude(self): def test_recursive_exclude(self):
l = make_local_path ml = make_local_path
# recursive-exclude # recursive-exclude
file_list = FileList() file_list = FileList()
file_list.files = ['a.py', l('d/b.py'), l('d/c.txt'), l('d/d/e.py')] file_list.files = ['a.py', ml('d/b.py'), ml('d/c.txt'), ml('d/d/e.py')]
file_list.process_template_line('recursive-exclude d *.py') file_list.process_template_line('recursive-exclude d *.py')
file_list.sort() file_list.sort()
assert file_list.files == ['a.py', l('d/c.txt')] assert file_list.files == ['a.py', ml('d/c.txt')]
self.assertNoWarnings() self.assertNoWarnings()
file_list.process_template_line('recursive-exclude e *.py') file_list.process_template_line('recursive-exclude e *.py')
file_list.sort() file_list.sort()
assert file_list.files == ['a.py', l('d/c.txt')] assert file_list.files == ['a.py', ml('d/c.txt')]
self.assertWarnings() self.assertWarnings()
def test_graft(self): def test_graft(self):
l = make_local_path ml = make_local_path
# graft # graft
file_list = FileList() file_list = FileList()
self.make_files(['a.py', l('d/b.py'), l('d/d/e.py'), l('f/f.py')]) self.make_files(['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')])
file_list.process_template_line('graft d') file_list.process_template_line('graft d')
file_list.sort() file_list.sort()
assert file_list.files == [l('d/b.py'), l('d/d/e.py')] assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
self.assertNoWarnings() self.assertNoWarnings()
file_list.process_template_line('graft e') file_list.process_template_line('graft e')
file_list.sort() file_list.sort()
assert file_list.files == [l('d/b.py'), l('d/d/e.py')] assert file_list.files == [ml('d/b.py'), ml('d/d/e.py')]
self.assertWarnings() self.assertWarnings()
def test_prune(self): def test_prune(self):
l = make_local_path ml = make_local_path
# prune # prune
file_list = FileList() file_list = FileList()
file_list.files = ['a.py', l('d/b.py'), l('d/d/e.py'), l('f/f.py')] file_list.files = ['a.py', ml('d/b.py'), ml('d/d/e.py'), ml('f/f.py')]
file_list.process_template_line('prune d') file_list.process_template_line('prune d')
file_list.sort() file_list.sort()
assert file_list.files == ['a.py', l('f/f.py')] assert file_list.files == ['a.py', ml('f/f.py')]
self.assertNoWarnings() self.assertNoWarnings()
file_list.process_template_line('prune e') file_list.process_template_line('prune e')
file_list.sort() file_list.sort()
assert file_list.files == ['a.py', l('f/f.py')] assert file_list.files == ['a.py', ml('f/f.py')]
self.assertWarnings() self.assertWarnings()
...@@ -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