From: DongHun Kwak Date: Mon, 14 Jan 2019 01:44:12 +0000 (+0900) Subject: Imported Upstream version 40.6.0 X-Git-Tag: upstream/40.6.0^0 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=9783565b96cfb0b13fad1e2b689f9721edb6b2f3;p=platform%2Fupstream%2Fpython-setuptools.git Imported Upstream version 40.6.0 --- diff --git a/.travis.yml b/.travis.yml index 0e53bd2..d1febcc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -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: - | diff --git a/CHANGES.rst b/CHANGES.rst index ca07119..3929703 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -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 ------- diff --git a/MANIFEST.in b/MANIFEST.in index 325bbed..9cce3c9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -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 diff --git a/README.rst b/README.rst index a9bed52..0454f2e 100644 --- a/README.rst +++ b/README.rst @@ -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 `_ -by the `Setuptools Developers -`_. - Questions and comments should be directed to the `distutils-sig mailing list `_. Bug reports and especially tested patches may be diff --git a/docs/ez_setup.txt b/docs/ez_setup.txt new file mode 100644 index 0000000..0126fee --- /dev/null +++ b/docs/ez_setup.txt @@ -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. diff --git a/docs/index.txt b/docs/index.txt index 74aabb5..13a46e7 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -17,9 +17,9 @@ Documentation content: :maxdepth: 2 setuptools - easy_install pkg_resources python3 development roadmap + Deprecated: Easy Install history diff --git a/docs/python3.txt b/docs/python3.txt index c528fc3..6b55fe7 100644 --- a/docs/python3.txt +++ b/docs/python3.txt @@ -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 `_, `future `_, or another compatibility library. diff --git a/docs/roadmap.txt b/docs/roadmap.txt index 9bde493..147288f 100644 --- a/docs/roadmap.txt +++ b/docs/roadmap.txt @@ -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 +`_ to track +a roadmap of large-scale goals. diff --git a/docs/setuptools.txt b/docs/setuptools.txt index da9b013..bca211b 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -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. diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 7413470..6ca68da 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -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. + """ diff --git a/pkg_resources/tests/test_pkg_resources.py b/pkg_resources/tests/test_pkg_resources.py index 62a39b8..416f9af 100644 --- a/pkg_resources/tests/test_pkg_resources.py +++ b/pkg_resources/tests/test_pkg_resources.py @@ -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) diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py index 171ba2f..86afcf7 100644 --- a/pkg_resources/tests/test_resources.py +++ b/pkg_resources/tests/test_resources.py @@ -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): diff --git a/setup.cfg b/setup.cfg index 5f7abd3..fd7e8cc 100644 --- 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 diff --git a/setup.py b/setup.py index 2cac67a..569730d 100755 --- 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 diff --git a/setuptools/__init__.py b/setuptools/__init__.py index 54309b5..e438036 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -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 index 0000000..086b64d --- /dev/null +++ b/setuptools/_deprecation_warning.py @@ -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. + """ diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index c670a16..06c9827 100644 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -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.""" + diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index bd116e1..d9fe3da 100644 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -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.""" diff --git a/setuptools/command/upload.py b/setuptools/command/upload.py index 72f24d8..3b8cab5 100644 --- a/setuptools/command/upload.py +++ b/setuptools/command/upload.py @@ -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. diff --git a/setuptools/config.py b/setuptools/config.py index 73a3bf7..d1ac673 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -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( diff --git a/setuptools/dist.py b/setuptools/dist.py index 6ee4a97..b741c64 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -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.""" diff --git a/setuptools/monkey.py b/setuptools/monkey.py index 05a738b..3c77f8c 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -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): diff --git a/setuptools/tests/test_build_meta.py b/setuptools/tests/test_build_meta.py index 7b195e2..712e114 100644 --- a/setuptools/tests/test_build_meta.py +++ b/setuptools/tests/test_build_meta.py @@ -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()) + diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 76759ec..736c184 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -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: diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py index 5162e1c..a7f4452 100644 --- a/setuptools/tests/test_dist.py +++ b/setuptools/tests/test_dist.py @@ -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", diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index b0cc4c9..2cf65ae 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -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): diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 59ffb16..f97b3f1 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -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) diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index c9533dd..5edfbea 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -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""" diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py index da19bd7..670ccee 100644 --- a/setuptools/tests/test_namespaces.py +++ b/setuptools/tests/test_namespaces.py @@ -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", ) diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index 02222da..d2c4e0c 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -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 diff --git a/setuptools/tests/test_test.py b/setuptools/tests/test_test.py index 960527b..8d1425e 100644 --- a/setuptools/tests/test_test.py +++ b/setuptools/tests/test_test.py @@ -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( diff --git a/setuptools/tests/test_upload.py b/setuptools/tests/test_upload.py index 95a8d16..9229bba 100644 --- a/setuptools/tests/test_upload.py +++ b/setuptools/tests/test_upload.py @@ -1,13 +1,101 @@ 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() diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py index b66a311..e511c91 100644 --- a/setuptools/tests/test_virtualenv.py +++ b/setuptools/tests/test_virtualenv.py @@ -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}', diff --git a/tests/requirements.txt b/tests/requirements.txt index b38fcbf..0c6c3e5 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -8,3 +8,4 @@ wheel coverage>=4.5.1 pytest-cov>=2.5.1 paver; python_version>="3.6" +futures; python_version=="2.7"