Imported Upstream version 40.6.0 upstream/40.6.0
authorDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 14 Jan 2019 01:44:12 +0000 (10:44 +0900)
committerDongHun Kwak <dh0128.kwak@samsung.com>
Mon, 14 Jan 2019 01:44:12 +0000 (10:44 +0900)
34 files changed:
.travis.yml
CHANGES.rst
MANIFEST.in
README.rst
docs/ez_setup.txt [new file with mode: 0644]
docs/index.txt
docs/python3.txt
docs/roadmap.txt
docs/setuptools.txt
pkg_resources/__init__.py
pkg_resources/tests/test_pkg_resources.py
pkg_resources/tests/test_resources.py
setup.cfg
setup.py
setuptools/__init__.py
setuptools/_deprecation_warning.py [new file with mode: 0644]
setuptools/command/easy_install.py
setuptools/command/egg_info.py
setuptools/command/upload.py
setuptools/config.py
setuptools/dist.py
setuptools/monkey.py
setuptools/tests/test_build_meta.py
setuptools/tests/test_config.py
setuptools/tests/test_dist.py
setuptools/tests/test_easy_install.py
setuptools/tests/test_egg_info.py
setuptools/tests/test_manifest.py
setuptools/tests/test_namespaces.py
setuptools/tests/test_sdist.py
setuptools/tests/test_test.py
setuptools/tests/test_upload.py
setuptools/tests/test_virtualenv.py
tests/requirements.txt

index 0e53bd2..d1febcc 100644 (file)
@@ -30,7 +30,9 @@ jobs:
     install: skip
     script: skip
     after_success: true
-    before_deploy: python bootstrap.py
+    before_deploy:
+      - python bootstrap.py
+      - "! grep pyc setuptools.egg-info/SOURCES.txt"
     deploy:
       provider: pypi
       on:
@@ -58,6 +60,7 @@ install:
 
 # update egg_info based on setup.py in checkout
 - python bootstrap.py
+- "! grep pyc setuptools.egg-info/SOURCES.txt"
 
 script:
   - |
index ca07119..3929703 100644 (file)
@@ -1,3 +1,23 @@
+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
 -------
 
index 325bbed..9cce3c9 100644 (file)
@@ -2,7 +2,7 @@ recursive-include setuptools *.py *.exe *.xml
 recursive-include tests *.py
 recursive-include setuptools/tests *.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
 include *.py
 include *.rst
index a9bed52..0454f2e 100644 (file)
@@ -23,10 +23,6 @@ See the `Installation Instructions
 User's Guide for instructions on installing, upgrading, and uninstalling
 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
 mailing list <http://mail.python.org/pipermail/distutils-sig/>`_.
 Bug reports and especially tested patches may be
diff --git a/docs/ez_setup.txt b/docs/ez_setup.txt
new file mode 100644 (file)
index 0000000..0126fee
--- /dev/null
@@ -0,0 +1,195 @@
+: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.
index 74aabb5..13a46e7 100644 (file)
@@ -17,9 +17,9 @@ Documentation content:
    :maxdepth: 2
 
    setuptools
-   easy_install
    pkg_resources
    python3
    development
    roadmap
+   Deprecated: Easy Install <easy_install>
    history
index c528fc3..6b55fe7 100644 (file)
@@ -9,7 +9,7 @@ code.
 
 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
-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/>`_,
 `future <https://pypi.org/project/future/>`_, or another compatibility
 library.
index 9bde493..147288f 100644 (file)
@@ -2,9 +2,6 @@
 Roadmap
 =======
 
-Setuptools has the following large-scale goals on the roadmap:
-
-- Harmonize declarative config 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.
+Setuptools maintains a series of `milestones
+<https://github.com/pypa/setuptools/milestones>`_ to track
+a roadmap of large-scale goals.
index da9b013..bca211b 100644 (file)
@@ -41,9 +41,9 @@ Feature Highlights:
   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.)
 
-* Transparent Pyrex 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
-  you include the Pyrex-generated C in your source distribution)
+* Transparent Cython support, so that your setup.py can list ``.pyx`` files and
+  still work even when the end-user doesn't have Cython installed (as long as
+  you include the Cython-generated C in your source distribution)
 
 * Command aliases - create project-specific, per-user, or site-wide shortcut
   names for commonly used commands and options
@@ -73,23 +73,17 @@ Developer's Guide
 Installing ``setuptools``
 =========================
 
-Please follow the `EasyInstall Installation Instructions`_ to install the
-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
 
-.. _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
-install a stable version, and then run::
+To install the latest version of setuptools, use::
 
-    ez_setup.py setuptools==dev
-
-This will download and install the latest development (i.e. unstable) version
-of setuptools from the Python Subversion sandbox.
+    pip install -U setuptools
 
+Refer to `Installing Packages`_ guide for more information.
 
 Basic Use
 =========
@@ -1229,121 +1223,53 @@ the quoted part.
 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
-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:
+Before you begin, make sure you have the latest versions of setuptools and wheel::
 
-.. code-block:: python
+    python3 -m pip install --user --upgrade setuptools wheel
 
-    import ez_setup
-    ez_setup.use_setuptools()
+To build a setuptools project, run this command from the same directory where
+setup.py is located::
 
-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).
+    python3 setup.py sdist bdist_wheel
 
-(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.)
+This will generate distribution archives in the `dist` directory.
 
+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.
+
+Distributing packages using the legacy ``ez_setup.py`` and ``easy_install`` is 
+deprecated in favor of PIP. Please consider migrating to using pip and twine based
+distribution.
 
-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.
-
-    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
 -----------------------------
@@ -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`` includes transparent support for building Pyrex extensions, as
-long as you define your extensions using ``setuptools.Extension``, *not*
-``distutils.Extension``.  You must also not import anything from Pyrex in
-your setup script.
+``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']
+
+Built with pip 10 or later, that declaration is sufficient to include Cython
+in the build. For broader compatibility, declare the dependency in your
+setup-requires of setup.cfg::
+
+    [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
-of your ``Extension`` objects in the setup script.  ``setuptools`` will detect
-at build time whether Pyrex is installed or not.  If it is, then ``setuptools``
-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 your ``Extension`` objects in the setup script.  If it is, then ``setuptools``
+will use it.
 
 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
 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
-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
-without Pyrex.
+without Cython.
 
 
 -----------------
@@ -2046,52 +1986,6 @@ specified in ``setup.cfg``::
 (Notice that ``egg_info`` must always appear on the command line *before* any
 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`` - Delete outdated distribution files
@@ -2400,6 +2294,35 @@ Metadata and options are set in the config sections of the same name.
 * 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
 =================
 
@@ -2434,14 +2357,14 @@ Metadata
     The aliases given below are supported for compatibility reasons,
     but their use is not advised.
 
-==============================  =================  =====
-Key                             Aliases            Type
-==============================  =================  =====
+==============================  =================  =================  =============== =====
+Key                             Aliases            Type               Minumum Version Notes
+==============================  =================  =================  =============== =====
 name                                               str
-version                                            attr:, file:, str
+version                                            attr:, file:, str  39.2.0          (1)
 url                             home-page          str
 download_url                    download-url       str
-project_urls                                       dict
+project_urls                                       dict               38.3.0
 author                                             str
 author_email                    author-email       str
 maintainer                                         str
@@ -2450,25 +2373,28 @@ classifiers                     classifier         file:, list-comma
 license                                            file:, str
 description                     summary            file:, str
 long_description                long-description   file:, str
-long_description_content_type                      str
+long_description_content_type                      str                38.6.0
 keywords                                           list-comma
 platforms                       platform           list-comma
 provides                                           list-comma
 requires                                           list-comma
 obsoletes                                          list-comma
-==============================  =================  =====
+==============================  =================  =================  =============== =====
 
 .. note::
     A version loaded using the ``file:`` directive must comply with PEP 440.
     It is easy to accidentally put something other than a valid version
     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
 -------
 
-=======================  =====
-Key                      Type
-=======================  =====
+=======================  ===================================  =============== =====
+Key                      Type                                 Minimum Version Notes
+=======================  ===================================  =============== =====
 zip_safe                 bool
 setup_requires           list-semi
 install_requires         list-semi
@@ -2490,7 +2416,8 @@ package_data             section
 exclude_package_data     section
 namespace_packages       list-comma
 py_modules               list-comma
-=======================  =====
+data_files               dict                                 40.6.0
+=======================  ===================================  =============== =====
 
 .. note::
 
@@ -2499,7 +2426,7 @@ py_modules               list-comma
     accepts the same keys as the `setuptools.find_packages` and the
     `setuptools.find_namespace_packages` function:
     ``where``, ``include``, and ``exclude``.
-    
+
     **find_namespace directive** - The ``find_namespace:`` directive is supported since Python >=3.3.
 
 
index 7413470..6ca68da 100644 (file)
@@ -238,6 +238,9 @@ __all__ = [
     'register_finder', 'register_namespace_handler', 'register_loader_type',
     'fixup_namespace_packages', 'get_importer',
 
+    # Warnings
+    'PkgResourcesDeprecationWarning',
+
     # Deprecated/backward compatibility only
     'run_main', 'AvailableDistributions',
 ]
@@ -2228,7 +2231,7 @@ register_namespace_handler(object, null_ns_handler)
 
 def normalize_path(filename):
     """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
@@ -2335,7 +2338,7 @@ class EntryPoint:
             warnings.warn(
                 "Parameters to load are deprecated.  Call .resolve and "
                 ".require separately.",
-                DeprecationWarning,
+                PkgResourcesDeprecationWarning,
                 stacklevel=2,
             )
         if require:
@@ -3158,3 +3161,11 @@ def _initialize_master_working_set():
     # match order
     list(map(working_set.add_entry, sys.path))
     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.
+    """
index 62a39b8..416f9af 100644 (file)
@@ -236,3 +236,56 @@ class TestDeepVersionLookupDistutils:
         req = pkg_resources.Requirement.parse('foo>=1.9')
         dist = pkg_resources.WorkingSet([env.paths['lib']]).find(req)
         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)
index 171ba2f..86afcf7 100644 (file)
@@ -15,7 +15,7 @@ import pkg_resources
 from pkg_resources import (
     parse_requirements, VersionConflict, parse_version,
     Distribution, EntryPoint, Requirement, safe_version, safe_name,
-    WorkingSet)
+    WorkingSet, PkgResourcesDeprecationWarning)
 
 
 # from Python 3.6 docs.
@@ -492,6 +492,15 @@ class TestEntryPoints:
         with pytest.raises(ValueError):
             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:
     def testBasics(self):
index 5f7abd3..fd7e8cc 100644 (file)
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,5 +1,5 @@
 [bumpversion]
-current_version = 40.5.0
+current_version = 40.6.0
 commit = True
 tag = True
 
index 2cac67a..569730d 100755 (executable)
--- a/setup.py
+++ b/setup.py
@@ -89,7 +89,7 @@ def pypi_link(pkg_filename):
 
 setup_params = dict(
     name="setuptools",
-    version="40.5.0",
+    version="40.6.0",
     description=(
         "Easily download, build, install, upgrade, and uninstall "
         "Python packages"
@@ -164,6 +164,7 @@ setup_params = dict(
         Programming Language :: Python :: 3.4
         Programming Language :: Python :: 3.5
         Programming Language :: Python :: 3.6
+        Programming Language :: Python :: 3.7
         Topic :: Software Development :: Libraries :: Python Modules
         Topic :: System :: Archiving :: Packaging
         Topic :: System :: Systems Administration
index 54309b5..e438036 100644 (file)
@@ -8,6 +8,8 @@ import distutils.filelist
 from distutils.util import convert_path
 from fnmatch import fnmatchcase
 
+from ._deprecation_warning import SetuptoolsDeprecationWarning
+
 from setuptools.extern.six import PY3
 from setuptools.extern.six.moves import filter, map
 
@@ -22,6 +24,7 @@ __metaclass__ = type
 
 __all__ = [
     'setup', 'Distribution', 'Feature', 'Command', 'Extension', 'Require',
+    'SetuptoolsDeprecationWarning',
     'find_packages'
 ]
 
@@ -188,4 +191,5 @@ def findall(dir=os.curdir):
     return list(files)
 
 
+# Apply monkey patches
 monkey.patch_all()
diff --git a/setuptools/_deprecation_warning.py b/setuptools/_deprecation_warning.py
new file mode 100644 (file)
index 0000000..086b64d
--- /dev/null
@@ -0,0 +1,7 @@
+class SetuptoolsDeprecationWarning(Warning):
+    """
+    Base class for warning deprecations in ``setuptools``
+
+    This class is not derived from ``DeprecationWarning``, and as such is
+    visible by default.
+    """
index c670a16..06c9827 100644 (file)
@@ -40,8 +40,11 @@ import subprocess
 import shlex
 import io
 
+
 from sysconfig import get_config_vars, get_path
 
+from setuptools import SetuptoolsDeprecationWarning
+
 from setuptools.extern import six
 from setuptools.extern.six.moves import configparser, map
 
@@ -2077,7 +2080,7 @@ class ScriptWriter:
     @classmethod
     def get_script_args(cls, dist, executable=None, wininst=False):
         # for backward compatibility
-        warnings.warn("Use get_args", DeprecationWarning)
+        warnings.warn("Use get_args", EasyInstallDeprecationWarning)
         writer = (WindowsScriptWriter if wininst else ScriptWriter).best()
         header = cls.get_script_header("", executable, wininst)
         return writer.get_args(dist, header)
@@ -2085,7 +2088,7 @@ class ScriptWriter:
     @classmethod
     def get_script_header(cls, script_text, executable=None, wininst=False):
         # for backward compatibility
-        warnings.warn("Use get_header", DeprecationWarning, stacklevel=2)
+        warnings.warn("Use get_header", EasyInstallDeprecationWarning, stacklevel=2)
         if wininst:
             executable = "python.exe"
         return cls.get_header(script_text, executable)
@@ -2120,7 +2123,7 @@ class ScriptWriter:
     @classmethod
     def get_writer(cls, force_windows):
         # for backward compatibility
-        warnings.warn("Use best", DeprecationWarning)
+        warnings.warn("Use best", EasyInstallDeprecationWarning)
         return WindowsScriptWriter.best() if force_windows else cls.best()
 
     @classmethod
@@ -2152,7 +2155,7 @@ class WindowsScriptWriter(ScriptWriter):
     @classmethod
     def get_writer(cls):
         # for backward compatibility
-        warnings.warn("Use best", DeprecationWarning)
+        warnings.warn("Use best", EasyInstallDeprecationWarning)
         return cls.best()
 
     @classmethod
@@ -2333,3 +2336,7 @@ def _patch_usage():
         yield
     finally:
         distutils.core.gen_usage = saved
+
+class EasyInstallDeprecationWarning(SetuptoolsDeprecationWarning):
+    """Class for warning about deprecations in EasyInstall in SetupTools. Not ignored by default, unlike DeprecationWarning."""
+    
index bd116e1..d9fe3da 100644 (file)
@@ -31,7 +31,7 @@ import setuptools.unicode_utils as unicode_utils
 from setuptools.glob import glob
 
 from setuptools.extern import packaging
-
+from setuptools import SetuptoolsDeprecationWarning
 
 def translate_pattern(glob):
     """
@@ -575,6 +575,12 @@ class manifest_maker(sdist):
             self.filelist.extend(rcfiles)
         elif os.path.exists(self.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')
         self.filelist.graft(ei_cmd.egg_info)
 
@@ -696,7 +702,7 @@ def get_pkg_info_revision():
     Get a -r### off of PKG-INFO Version in case this is an sdist of
     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'):
         with io.open('PKG-INFO') as f:
             for line in f:
@@ -704,3 +710,7 @@ def get_pkg_info_revision():
                 if match:
                     return int(match.group(1))
     return 0
+
+
+class EggInfoDeprecationWarning(SetuptoolsDeprecationWarning):
+    """Class for warning about deprecations in eggInfo in setupTools. Not ignored by default, unlike DeprecationWarning."""
index 72f24d8..3b8cab5 100644 (file)
@@ -1,14 +1,26 @@
+import io
+import os
+import hashlib
 import getpass
+import platform
+
+from base64 import standard_b64encode
+
 from distutils import log
 from distutils.command import upload as orig
+from distutils.spawn import spawn
+
+from distutils.errors import DistutilsError
 
+from six.moves.urllib.request import urlopen, Request
+from six.moves.urllib.error import HTTPError
+from six.moves.urllib.parse import urlparse
 
 class upload(orig.upload):
     """
     Override default upload behavior to obtain password
     in a variety of different ways.
     """
-
     def run(self):
         try:
             orig.upload.run(self)
@@ -33,6 +45,137 @@ class upload(orig.upload):
             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):
         """
         Attempt to load password from keyring. Suppress Exceptions.
index 73a3bf7..d1ac673 100644 (file)
@@ -2,8 +2,12 @@ from __future__ import absolute_import, unicode_literals
 import io
 import os
 import sys
+
+import warnings
+import functools
 from collections import defaultdict
 from functools import partial
+from functools import wraps
 from importlib import import_module
 
 from distutils.errors import DistutilsOptionError, DistutilsFileError
@@ -61,6 +65,18 @@ def read_configuration(
     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):
     """Returns configuration data gathered by given handlers as a dict.
 
@@ -72,20 +88,9 @@ def configuration_to_dict(handlers):
     config_dict = defaultdict(dict)
 
     for handler in handlers:
-
-        obj_alias = handler.section_prefix
-        target_obj = handler.target_obj
-
         for option in handler.set_options:
-            getter = getattr(target_obj, 'get_%s' % option, None)
-
-            if getter is None:
-                value = getattr(target_obj, option)
-
-            else:
-                value = getter()
-
-            config_dict[obj_alias][option] = value
+            value = _get_option(handler.target_obj, option)
+            config_dict[handler.section_prefix][option] = value
 
     return config_dict
 
@@ -110,7 +115,8 @@ def parse_configuration(
     options.parse()
 
     meta = ConfigMetadataHandler(
-        distribution.metadata, command_options, ignore_option_errors, distribution.package_dir)
+        distribution.metadata, command_options, ignore_option_errors,
+        distribution.package_dir)
     meta.parse()
 
     return meta, options
@@ -399,6 +405,20 @@ class ConfigHandler:
 
             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):
 
@@ -434,7 +454,10 @@ class ConfigMetadataHandler(ConfigHandler):
             'platforms': parse_list,
             'keywords': 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,
             'classifiers': self._get_parser_compound(parse_file, parse_list),
             'license': parse_file,
@@ -458,9 +481,12 @@ class ConfigMetadataHandler(ConfigHandler):
             # Be strict about versions loaded from file because it's easy to
             # accidentally include newlines and other unintended content
             if isinstance(parse(version), LegacyVersion):
-                raise DistutilsOptionError('Version loaded from %s does not comply with PEP 440: %s' % (
-                    value, version
-                ))
+                tmpl = (
+                    'Version loaded from {value} does not '
+                    'comply with PEP 440: {version}'
+                )
+                raise DistutilsOptionError(tmpl.format(**locals()))
+
             return version
 
         version = self._parse_attr(value, self.package_dir)
@@ -518,12 +544,13 @@ class ConfigOptionsHandler(ConfigHandler):
         find_directives = ['find:', 'find_namespace:']
         trimmed_value = value.strip()
 
-        if not trimmed_value in find_directives:
+        if trimmed_value not in find_directives:
             return self._parse_list(value)
 
         findns = trimmed_value == find_directives[1]
         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.
         find_kwargs = self.parse_section_packages__find(
index 6ee4a97..b741c64 100644 (file)
@@ -10,7 +10,11 @@ import distutils.core
 import distutils.cmd
 import distutils.dist
 import itertools
+
+
 from collections import defaultdict
+from email import message_from_file
+
 from distutils.errors import (
     DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError,
 )
@@ -21,6 +25,8 @@ from setuptools.extern import six
 from setuptools.extern import packaging
 from setuptools.extern.six.moves import map, filter, filterfalse
 
+from . import SetuptoolsDeprecationWarning
+
 from setuptools.depends import Require
 from setuptools import windows_support
 from setuptools.monkey import get_unpatched
@@ -33,39 +39,107 @@ __import__('setuptools.extern.packaging.version')
 
 
 def _get_unpatched(cls):
-    warnings.warn("Do not call this function", DeprecationWarning)
+    warnings.warn("Do not call this function", DistDeprecationWarning)
     return get_unpatched(cls)
 
 
-def get_metadata_version(dist_md):
-    if dist_md.long_description_content_type or dist_md.provides_extras:
-        return StrictVersion('2.1')
-    elif (dist_md.maintainer is not None or
-          dist_md.maintainer_email is not None or
-          getattr(dist_md, 'python_requires', None) is not None):
-        return StrictVersion('1.2')
-    elif (dist_md.provides or dist_md.requires or dist_md.obsoletes or
-            dist_md.classifiers or dist_md.download_url):
-        return StrictVersion('1.1')
+def get_metadata_version(self):
+    mv = getattr(self, 'metadata_version', None)
+
+    if mv is None:
+        if self.long_description_content_type or self.provides_extras:
+            mv = StrictVersion('2.1')
+        elif (self.maintainer is not None or
+              self.maintainer_email is not None or
+              getattr(self, 'python_requires', None) is not None):
+            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
+
+    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')
 
-    return StrictVersion('1.0')
+    # 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
 def write_pkg_file(self, file):
     """Write the PKG-INFO format data to a file object.
     """
-    version = get_metadata_version(self)
+    version = self.get_metadata_version()
+
+    if six.PY2:
+        def write_field(key, value):
+            file.write("%s: %s\n" % (key, self._encode_field(value)))
+    else:
+        def write_field(key, value):
+            file.write("%s: %s\n" % (key, value))
+
 
-    file.write('Metadata-Version: %s\n' % version)
-    file.write('Name: %s\n' % self.get_name())
-    file.write('Version: %s\n' % self.get_version())
-    file.write('Summary: %s\n' % self.get_description())
-    file.write('Home-page: %s\n' % self.get_url())
+    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'):
-        file.write('Author: %s\n' % self.get_contact())
-        file.write('Author-email: %s\n' % self.get_contact_email())
+        write_field('Author:', self.get_contact())
+        write_field('Author-email:', self.get_contact_email())
     else:
         optional_fields = (
             ('Author', 'author'),
@@ -76,28 +150,26 @@ def write_pkg_file(self, file):
 
         for field, attr in optional_fields:
             attr_val = getattr(self, attr)
-            if six.PY2:
-                attr_val = self._encode_field(attr_val)
 
             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:
-        file.write('Download-URL: %s\n' % self.download_url)
+        write_field('Download-URL', self.download_url)
     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())
-    file.write('Description: %s\n' % long_desc)
+    write_field('Description', long_desc)
 
     keywords = ','.join(self.get_keywords())
     if keywords:
-        file.write('Keywords: %s\n' % keywords)
+        write_field('Keywords', keywords)
 
     if version >= StrictVersion('1.2'):
         for platform in self.get_platforms():
-            file.write('Platform: %s\n' % platform)
+            write_field('Platform', platform)
     else:
         self._write_list(file, 'Platform', self.get_platforms())
 
@@ -110,17 +182,17 @@ def write_pkg_file(self, file):
 
     # Setuptools specific for PEP 345
     if hasattr(self, 'python_requires'):
-        file.write('Requires-Python: %s\n' % self.python_requires)
+        write_field('Requires-Python', self.python_requires)
 
     # PEP 566
     if self.long_description_content_type:
-        file.write(
-            'Description-Content-Type: %s\n' %
+        write_field(
+            'Description-Content-Type',
             self.long_description_content_type
         )
     if self.provides_extras:
         for extra in self.provides_extras:
-            file.write('Provides-Extra: %s\n' % extra)
+            write_field('Provides-Extra', extra)
 
 
 sequence = tuple, list
@@ -980,7 +1052,7 @@ class Feature:
             "Features are deprecated and will be removed in a future "
             "version. See https://github.com/pypa/setuptools/issues/65."
         )
-        warnings.warn(msg, DeprecationWarning, stacklevel=3)
+        warnings.warn(msg, DistDeprecationWarning, stacklevel=3)
 
     def __init__(
             self, description, standard=False, available=True,
@@ -1069,3 +1141,7 @@ class Feature:
                     " doesn't contain any packages or modules under %s"
                     % (self.description, item, item)
                 )
+
+
+class DistDeprecationWarning(SetuptoolsDeprecationWarning):
+    """Class for warning about deprecations in dist in setuptools. Not ignored by default, unlike DeprecationWarning."""
index 05a738b..3c77f8c 100644 (file)
@@ -84,7 +84,7 @@ def patch_all():
         warehouse = 'https://upload.pypi.org/legacy/'
         distutils.config.PyPIRCCommand.DEFAULT_REPOSITORY = warehouse
 
-    _patch_distribution_metadata_write_pkg_file()
+    _patch_distribution_metadata()
 
     # Install Distribution throughout the distutils
     for module in distutils.dist, distutils.core, distutils.cmd:
@@ -101,11 +101,11 @@ def patch_all():
     patch_for_msvc_specialized_compiler()
 
 
-def _patch_distribution_metadata_write_pkg_file():
-    """Patch write_pkg_file to also write Requires-Python/Requires-External"""
-    distutils.dist.DistributionMetadata.write_pkg_file = (
-        setuptools.dist.write_pkg_file
-    )
+def _patch_distribution_metadata():
+    """Patch write_pkg_file and read_pkg_file for higher metadata standards"""
+    for attr in ('write_pkg_file', 'read_pkg_file', 'get_metadata_version'):
+        new_val = getattr(setuptools.dist, attr)
+        setattr(distutils.dist.DistributionMetadata, attr, new_val)
 
 
 def patch_func(replacement, target_mod, func_name):
index 7b195e2..712e114 100644 (file)
@@ -2,16 +2,20 @@ from __future__ import unicode_literals
 
 import os
 import shutil
+import tarfile
 
 import pytest
 
+from setuptools.build_meta import build_sdist
 from .files import build_files
 from .textwrap import DALS
+from . import py2_only
 
 __metaclass__ = type
 
-futures = pytest.importorskip('concurrent.futures')
-importlib = pytest.importorskip('importlib')
+# Backports on Python 2.7
+import importlib
+from concurrent import futures
 
 
 class BuildBackendBase:
@@ -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'))
 
 
-@pytest.mark.skipif('sys.version_info > (3,)')
+@py2_only
 def test_prepare_metadata_for_build_wheel_with_str(build_backend):
     dist_dir = os.path.abspath(str('pip-dist-info'))
     os.makedirs(dist_dir)
@@ -180,3 +184,34 @@ def test_build_sdist_version_change(build_backend):
 
     sdist_name = build_backend.build_sdist("out_sdist")
     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())
+
index 76759ec..736c184 100644 (file)
@@ -391,6 +391,23 @@ class TestMetadata:
         with get_dist(tmpdir) as dist:
             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']
+
 
 class TestOptions:
 
index 5162e1c..a7f4452 100644 (file)
@@ -3,7 +3,7 @@
 from __future__ import unicode_literals
 
 import io
-
+from setuptools.dist import DistDeprecationWarning, _get_unpatched
 from setuptools import Distribution
 from setuptools.extern.six.moves.urllib.request import pathname2url
 from setuptools.extern.six.moves.urllib_parse import urljoin
@@ -12,6 +12,7 @@ from .textwrap import DALS
 from .test_easy_install import make_nspkg_sdist
 
 import pytest
+import six
 
 
 def test_dist_fetch_build_egg(tmpdir):
@@ -56,6 +57,107 @@ def test_dist_fetch_build_egg(tmpdir):
     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")),
+    ]
+
+    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),
+        ('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():
     attrs = {"name": "package",
              "version": "1.0",
index b0cc4c9..2cf65ae 100644 (file)
@@ -15,7 +15,7 @@ import distutils.errors
 import io
 import zipfile
 import mock
-
+from setuptools.command.easy_install import EasyInstallDeprecationWarning, ScriptWriter, WindowsScriptWriter
 import time
 from setuptools.extern import six
 from setuptools.extern.six.moves import urllib
@@ -288,6 +288,22 @@ class TestEasyInstallTest:
         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')
 class TestPTHFileWriter:
     def test_add_from_cwd_site_sets_dirty(self):
index 59ffb16..f97b3f1 100644 (file)
@@ -7,7 +7,7 @@ import re
 import stat
 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.extern.six.moves import map
 
@@ -569,6 +569,20 @@ class TestEggInfo:
         for msg in fixtures:
             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):
         environ = os.environ.copy().update(
             HOME=env.paths['home'],
@@ -603,3 +617,6 @@ class TestEggInfo:
         with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file:
             pkg_info_lines = pkginfo_file.read().split('\n')
         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)
index c9533dd..5edfbea 100644 (file)
@@ -15,13 +15,12 @@ from setuptools.command.egg_info import FileList, egg_info, translate_pattern
 from setuptools.dist import Distribution
 from setuptools.extern import six
 from setuptools.tests.textwrap import DALS
+from . import py3_only
 
 import pytest
 
 __metaclass__ = type
 
-py3_only = pytest.mark.xfail(six.PY2, reason="Test runs on Python 3 only")
-
 
 def make_local_path(s):
     """Converts '/' in a string to os.sep"""
index da19bd7..670ccee 100644 (file)
@@ -12,7 +12,7 @@ from setuptools.command import test
 
 class TestNamespaces:
 
-    @pytest.mark.xfail(
+    @pytest.mark.skipif(
         sys.version_info < (3, 5),
         reason="Requires importlib.util.module_from_spec",
     )
index 02222da..d2c4e0c 100644 (file)
@@ -20,8 +20,8 @@ from setuptools.command.egg_info import manifest_maker
 from setuptools.dist import Distribution
 from setuptools.tests import fail_on_ascii
 from .text import Filenames
+from . import py3_only
 
-py3_only = pytest.mark.xfail(six.PY2, reason="Test runs on Python 3 only")
 
 SETUP_ATTRS = {
     'name': 'sdist_test',
@@ -92,9 +92,8 @@ fail_on_latin1_encoded_filenames = pytest.mark.xfail(
 class TestSdistTest:
     def setup_method(self, method):
         self.temp_dir = tempfile.mkdtemp()
-        f = open(os.path.join(self.temp_dir, 'setup.py'), 'w')
-        f.write(SETUP_PY)
-        f.close()
+        with open(os.path.join(self.temp_dir, 'setup.py'), 'w') as f:
+            f.write(SETUP_PY)
 
         # Set up the rest of the test package
         test_pkg = os.path.join(self.temp_dir, 'sdist_test')
@@ -135,6 +134,47 @@ class TestSdistTest:
         assert os.path.join('sdist_test', 'c.rst') not 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):
         """
         Make sure default files (README.*, etc.) are added in a case-sensitive
index 960527b..8d1425e 100644 (file)
@@ -93,10 +93,6 @@ def test_test(capfd):
     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')
 def test_tests_are_run_once(capfd):
     params = dict(
index 95a8d16..9229bba 100644 (file)
 import mock
+import os
+import re
+
 from distutils import log
+from distutils.errors import DistutilsError
+from distutils.version import StrictVersion
 
 import pytest
+import six
 
 from setuptools.command.upload import upload
 from setuptools.dist import Distribution
 
 
+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:
+    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):
         dist = Distribution()
         dist.dist_files = [(mock.Mock(), mock.Mock(), mock.Mock())]
@@ -41,3 +129,86 @@ class TestUploadTest:
             "upload instead (https://pypi.org/p/twine/)",
             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()
index b66a311..e511c91 100644 (file)
@@ -57,9 +57,6 @@ def test_pip_upgrade_from_source(virtualenv):
     Check pip can upgrade setuptools from source.
     """
     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.
     virtualenv.run(' && '.join((
         'cd {source}',
index b38fcbf..0c6c3e5 100644 (file)
@@ -8,3 +8,4 @@ wheel
 coverage>=4.5.1
 pytest-cov>=2.5.1
 paver; python_version>="3.6"
+futures; python_version=="2.7"