diff options
| author | Jason R. Coombs <jaraco@jaraco.com> | 2017-09-03 19:57:54 -0400 |
|---|---|---|
| committer | Jason R. Coombs <jaraco@jaraco.com> | 2017-09-03 20:01:45 -0400 |
| commit | dcb24ad15465c266a3f258471766fbbe8fc8a42e (patch) | |
| tree | 13123440610d78e398476a8ce1e8cc3d9f9ec72e | |
| parent | f14930e66601b462699c44384c482cd966f53b8f (diff) | |
| parent | 1b192005562d5cf0de30c02154c58fd1dca577c8 (diff) | |
| download | python-setuptools-git-dcb24ad15465c266a3f258471766fbbe8fc8a42e.tar.gz | |
Merge branch 'master' into drop-py26
71 files changed, 3319 insertions, 1144 deletions
@@ -7,9 +7,11 @@ lib distribute.egg-info setuptools.egg-info .coverage +.eggs .tox *.egg *.py[cod] *.swp *~ .hg* +.cache diff --git a/.travis.yml b/.travis.yml index 9277af80..8558159f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,37 +1,48 @@ +dist: trusty +sudo: false language: python -python: - - 2.7 - - 3.3 - - 3.4 - - 3.5 - - "3.6-dev" - - nightly - - pypy -env: - - "" - - LC_ALL=C LC_CTYPE=C -script: - - pip install tox +jobs: + fast_finish: true + include: + - python: &latest_py2 2.7 + - python: 3.3 + - python: 3.4 + - python: 3.5 + - python: &latest_py3 3.6 + - python: nightly + - python: pypy + - python: pypy3 + - python: *latest_py3 + env: LANG=C + - python: *latest_py2 + env: LANG=C + - stage: deploy (to PyPI for tagged commits) + python: *latest_py3 + install: skip + script: skip + before_deploy: python bootstrap.py + deploy: + provider: pypi + on: + tags: true + all_branches: true + user: jaraco + password: + secure: tfWrsQMH2bHrWjqnP+08IX1WlkbW94Q30f4d7lCyhWS1FIf/jBDx4jrEILNfMxQ1NCwuBRje5sihj1Ow0BFf0vVrkaeff2IdvnNDEGFduMejaEQJL3s3QrLfpiAvUbtqwyWaHfAdGfk48PovDKTx0ZTvXZKYGXZhxGCYSlG2CE6Y6RDvnEl6Tk8e+LqUohkcSOwxrRwUoyxSnUaavdGohXxDT8MJlfWOXgr2u+KsRrriZqp3l6Fdsnk4IGvy6pXpy42L1HYQyyVu9XyJilR2JTbC6eCp5f8p26093m1Qas49+t6vYb0VLqQe12dO+Jm3v4uztSS5pPQzS7PFyjEYd2Rdb6ijsdbsy1074S4q7G9Sz+T3RsPUwYEJ07lzez8cxP64dtj5j94RL8m35A1Fb1OE8hHN+4c1yLG1gudfXbem+fUhi2eqhJrzQo5vsvDv1xS5x5GIS5ZHgKHCsWcW1Tv+dsFkrhaup3uU6VkOuc9UN+7VPsGEY7NvquGpTm8O1CnGJRzuJg6nbYRGj8ORwDpI0KmrExx6akV92P72fMC/I5TCgbSQSZn370H3Jj40gz1SM30WAli9M+wFHFd4ddMVY65yxj0NLmrP+m1tvnWdKtNh/RHuoW92d9/UFtiA5IhMf1/3djfsjBq6S9NT1uaLkVkTttqrPYJ7hOql8+g= + distributions: release + skip_cleanup: true + skip_upload_docs: true - # Output the env, to verify behavior - - env +cache: pip - # update egg_info based on setup.py in checkout - - python bootstrap.py +install: +# need tox to get started +- pip install tox - #- python -m tox - - tox +# Output the env, to verify behavior +- env -deploy: - provider: pypi - # Also update server in setup.cfg - server: https://upload.pypi.org/legacy/ - on: - tags: true - all_branches: true - python: 3.5 - condition: $LC_ALL != "C" - user: jaraco - password: - secure: tfWrsQMH2bHrWjqnP+08IX1WlkbW94Q30f4d7lCyhWS1FIf/jBDx4jrEILNfMxQ1NCwuBRje5sihj1Ow0BFf0vVrkaeff2IdvnNDEGFduMejaEQJL3s3QrLfpiAvUbtqwyWaHfAdGfk48PovDKTx0ZTvXZKYGXZhxGCYSlG2CE6Y6RDvnEl6Tk8e+LqUohkcSOwxrRwUoyxSnUaavdGohXxDT8MJlfWOXgr2u+KsRrriZqp3l6Fdsnk4IGvy6pXpy42L1HYQyyVu9XyJilR2JTbC6eCp5f8p26093m1Qas49+t6vYb0VLqQe12dO+Jm3v4uztSS5pPQzS7PFyjEYd2Rdb6ijsdbsy1074S4q7G9Sz+T3RsPUwYEJ07lzez8cxP64dtj5j94RL8m35A1Fb1OE8hHN+4c1yLG1gudfXbem+fUhi2eqhJrzQo5vsvDv1xS5x5GIS5ZHgKHCsWcW1Tv+dsFkrhaup3uU6VkOuc9UN+7VPsGEY7NvquGpTm8O1CnGJRzuJg6nbYRGj8ORwDpI0KmrExx6akV92P72fMC/I5TCgbSQSZn370H3Jj40gz1SM30WAli9M+wFHFd4ddMVY65yxj0NLmrP+m1tvnWdKtNh/RHuoW92d9/UFtiA5IhMf1/3djfsjBq6S9NT1uaLkVkTttqrPYJ7hOql8+g= - distributions: release +# update egg_info based on setup.py in checkout +- python bootstrap.py + +script: tox diff --git a/CHANGES.rst b/CHANGES.rst index 50bdd3ab..cf596cf2 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,15 +1,413 @@ -v31.0.0 +v37.0.0 ------- * #878: Drop support for Python 2.6. Python 2.6 users should - rely on 'setuptools < 31dev'. + rely on 'setuptools < 37dev'. + +v36.4.0 +------- + +* #1075: Add new ``Description-Content-Type`` metadata field. `See here for + documentation on how to use this field. + <https://packaging.python.org/specifications/#description-content-type>`_ + +* #1068: Sort files and directories when building eggs for + deterministic order. + +* #196: Remove caching of easy_install command in fetch_build_egg. + Fixes issue where ``pytest-runner-N.N`` would satisfy the installation + of ``pytest``. + +* #1129: Fix working set dependencies handling when replacing conflicting + distributions (e.g. when using ``setup_requires`` with a conflicting + transitive dependency, fix #1124). + +* #1133: Improved handling of README files extensions and added + Markdown to the list of searched READMES. + +* #1135: Improve performance of pkg_resources import by not invoking + ``access`` or ``stat`` and using ``os.listdir`` instead. + +v36.3.0 +------- + +* #1131: Make possible using several files within ``file:`` directive + in metadata.long_description in ``setup.cfg``. + +v36.2.7 +------- + +* fix #1105: Fix handling of requirements with environment + markers when declared in ``setup.cfg`` (same treatment as + for #1081). + +v36.2.6 +------- + +* #462: Don't assume a directory is an egg by the ``.egg`` + extension alone. + +v36.2.5 +------- + +* #1093: Fix test command handler with extras_require. +* #1112, #1091, #1115: Now using Trusty containers in + Travis for CI and CD. + +v36.2.4 +------- + +* #1092: ``pkg_resources`` now uses ``inspect.getmro`` to + resolve classes in method resolution order. + +v36.2.3 +------- + +* #1102: Restore behavior for empty extras. + +v36.2.2 +------- + +* #1099: Revert commit a3ec721, restoring intended purpose of + extras as part of a requirement declaration. + +v36.2.1 +------- + +* fix #1086 +* fix #1087 +* support extras specifiers in install_requires requirements + +v36.2.0 +------- + +* #1081: Environment markers indicated in ``install_requires`` + are now processed and treated as nameless ``extras_require`` + with markers, allowing their metadata in requires.txt to be + correctly generated. + +* #1053: Tagged commits are now released using Travis-CI + build stages, meaning releases depend on passing tests on + all supported Python versions (Linux) and not just the latest + Python version. + +v36.1.1 +------- + +* #1083: Correct ``py31compat.makedirs`` to correctly honor + ``exist_ok`` parameter. +* #1083: Also use makedirs compatibility throughout setuptools. + +v36.1.0 +------- + +* #1083: Avoid race condition on directory creation in + ``pkg_resources.ensure_directory``. + +* Removed deprecation of and restored support for + ``upload_docs`` command for sites other than PyPI. + Only warehouse is dropping support, but services like + `devpi <http://doc.devpi.net/latest/>`_ continue to + support docs built by setuptools' plugins. See + `this comment <https://bitbucket.org/hpk42/devpi/issues/388/support-rtd-model-for-building-uploading#comment-34292423>`_ + for more context on the motivation for this change. + +v36.0.1 +------- + +* #1042: Fix import in py27compat module that still + referenced six directly, rather than through the externs + module (vendored packages hook). + +v36.0.0 +------- + +* #980 and others: Once again, Setuptools vendors all + of its dependencies. It seems to be the case that in + the Python ecosystem, all build tools must run without + any dependencies (build, runtime, or otherwise). At + such a point that a mechanism exists that allows + build tools to have dependencies, Setuptools will adopt + it. + +v35.0.2 +------- + +* #1015: Fix test failures on Python 3.7. + +* #1024: Add workaround for Jython #2581 in monkey module. + +v35.0.1 +------- + +* #992: Revert change introduced in v34.4.1, now + considered invalid. + +* #1016: Revert change introduced in v35.0.0 per #1014, + referencing #436. The approach had unintended + consequences, causing sdist installs to be missing + files. + +v35.0.0 +------- + +* #436: In egg_info.manifest_maker, no longer read + the file list from the manifest file, and instead + re-build it on each build. In this way, files removed + from the specification will not linger in the manifest. + As a result, any files manually added to the manifest + will be removed on subsequent egg_info invocations. + No projects should be manually adding files to the + manifest and should instead use MANIFEST.in or SCM + file finders to force inclusion of files in the manifest. + +v34.4.1 +------- + +* #1008: In MSVC support, use always the last version available for Windows SDK and UCRT SDK. + +* #1008: In MSVC support, fix "vcruntime140.dll" returned path with Visual Studio 2017. + +* #992: In msvc.msvc9_query_vcvarsall, ensure the + returned dicts have str values and not Unicode for + compatibility with os.environ. + +v34.4.0 +------- + +* #995: In MSVC support, add support for "Microsoft Visual Studio 2017" and "Microsoft Visual Studio Build Tools 2017". + +* #999 via #1007: Extend support for declarative package + config in a setup.cfg file to include the options + ``python_requires`` and ``py_modules``. + +v34.3.3 +------- + +* #967 (and #997): Explicitly import submodules of + packaging to account for environments where the imports + of those submodules is not implied by other behavior. + +v34.3.2 +------- + +* #993: Fix documentation upload by correcting + rendering of content-type in _build_multipart + on Python 3. + +v34.3.1 +------- + +* #988: Trap ``os.unlink`` same as ``os.remove`` in + ``auto_chmod`` error handler. + +* #983: Fixes to invalid escape sequence deprecations on + Python 3.6. + +v34.3.0 +------- + +* #941: In the upload command, if the username is blank, + default to ``getpass.getuser()``. + +* #971: Correct distutils findall monkeypatch to match + appropriate versions (namely Python 3.4.6). + +v34.2.0 +------- + +* #966: Add support for reading dist-info metadata and + thus locating Distributions from zip files. + +* #968: Allow '+' and '!' in egg fragments + so that it can take package names that contain + PEP 440 conforming version specifiers. + +v34.1.1 +------- + +* #953: More aggressively employ the compatibility issue + originally added in #706. + +v34.1.0 +------- + +* #930: ``build_info`` now accepts two new parameters + to optimize and customize the building of C libraries. + +v34.0.3 +------- + +* #947: Loosen restriction on the version of six required, + restoring compatibility with environments relying on + six 1.6.0 and later. + +v34.0.2 +------- + +* #882: Ensure extras are honored when building the + working set. +* #913: Fix issue in develop if package directory has + a trailing slash. + +v34.0.1 +------- + +* #935: Fix glob syntax in graft. + +v34.0.0 +------- + +* #581: Instead of vendoring the growing list of + dependencies that Setuptools requires to function, + Setuptools now requires these dependencies just like + any other project. Unlike other projects, however, + Setuptools cannot rely on ``setup_requires`` to + demand the dependencies it needs to install because + its own machinery would be necessary to pull those + dependencies if not present (a bootstrapping problem). + As a result, Setuptools no longer supports self upgrade or + installation in the general case. Instead, users are + directed to use pip to install and upgrade using the + ``wheel`` distributions of setuptools. + + Users are welcome to contrive other means to install + or upgrade Setuptools using other means, such as + pre-installing the Setuptools dependencies with pip + or a bespoke bootstrap tool, but such usage is not + recommended and is not supported. + + As discovered in #940, not all versions of pip will + successfully install Setuptools from its pre-built + wheel. If you encounter issues with "No module named + six" or "No module named packaging", especially + following a line "Running setup.py egg_info for package + setuptools", then your pip is not new enough. + + There's an additional issue in pip where setuptools + is upgraded concurrently with other source packages, + described in pip #4253. The proposed workaround is to + always upgrade Setuptools first prior to upgrading + other packages that would upgrade Setuptools. + +v33.1.1 +------- + +* #921: Correct issue where certifi fallback not being + reached on Windows. + +v33.1.0 +------- + +Installation via pip, as indicated in the `Python Packaging +User's Guide <https://packaging.python.org/installing/>`_, +is the officially-supported mechanism for installing +Setuptools, and this recommendation is now explicit in the +much more concise README. + +Other edits and tweaks were made to the documentation. The +codebase is unchanged. + +v33.0.0 +------- + +* #619: Removed support for the ``tag_svn_revision`` + distribution option. If Subversion tagging support is + still desired, consider adding the functionality to + setuptools_svn in setuptools_svn #2. + +v32.3.1 +------- + +* #866: Use ``dis.Bytecode`` on Python 3.4 and later in + ``setuptools.depends``. + +v32.3.0 +------- + +* #889: Backport proposed fix for disabling interpolation in + distutils.Distribution.parse_config_files. + +v32.2.0 +------- + +* #884: Restore support for running the tests under + `pytest-runner <https://github.com/pytest-dev/pytest-runner>`_ + by ensuring that PYTHONPATH is honored in tests invoking + a subprocess. + +v32.1.3 +------- + +* #706: Add rmtree compatibility shim for environments where + rmtree fails when passed a unicode string. + +v32.1.2 +------- + +* #893: Only release sdist in zip format as warehouse now + disallows releasing two different formats. + +v32.1.1 +------- + +* #704: More selectively ensure that 'rmtree' is not invoked with + a byte string, enabling it to remove files that are non-ascii, + even on Python 2. + +* #712: In 'sandbox.run_setup', ensure that ``__file__`` is + always a ``str``, modeling the behavior observed by the + interpreter when invoking scripts and modules. + +v32.1.0 +------- + +* #891: In 'test' command on test failure, raise DistutilsError, + suppression invocation of subsequent commands. + +v32.0.0 +------- + +* #890: Revert #849. ``global-exclude .foo`` will not match all + ``*.foo`` files any more. Package authors must add an explicit + wildcard, such as ``global-exclude *.foo``, to match all + ``.foo`` files. See #886, #849. + +v31.0.1 +------- + +* #885: Fix regression where 'pkg_resources._rebuild_mod_path' + would fail when a namespace package's '__path__' was not + a list with a sort attribute. + +v31.0.0 +------- + +* #250: Install '-nspkg.pth' files for packages installed + with 'setup.py develop'. These .pth files allow + namespace packages installed by pip or develop to + co-mingle. This change required the removal of the + change for #805 and pip #1924, introduced in 28.3.0 and implicated + in #870, but means that namespace packages not in a + site packages directory will no longer work on Python + earlier than 3.5, whereas before they would work on + Python not earlier than 3.3. + +v30.4.0 +------- + +* #879: For declarative config: + + - read_configuration() now accepts ignore_option_errors argument. This allows scraping tools to read metadata without a need to download entire packages. E.g. we can gather some stats right from GitHub repos just by downloading setup.cfg. + + - packages find: directive now supports fine tuning from a subsection. The same arguments as for find() are accepted. v30.3.0 ------- * #394 via #862: Added support for `declarative package config in a setup.cfg file - <http://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_. + <https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_. v30.2.1 ------- @@ -109,7 +507,11 @@ v28.5.0 * #810: Tests are now invoked with tox and not setup.py test. * #249 and #450 via #764: Avoid scanning the whole tree - when building the manifest. + when building the manifest. Also fixes a long-standing bug + where patterns in ``MANIFEST.in`` had implicit wildcard + matching. This caused ``global-exclude .foo`` to exclude + all ``*.foo`` files, but also ``global-exclude bar.py`` to + exclude ``foo_bar.py``. v28.4.0 ------- @@ -616,7 +1018,7 @@ v20.6.0 `semver <https://semver.org>`_ precisely. The 'v' prefix on version numbers now also allows version numbers to be referenced in the changelog, - e.g. https://pythonhosted.org/setuptools/history.html#v20-6-0. + e.g. http://setuptools.readthedocs.io/en/latest/history.html#v20-6-0. 20.5 ---- @@ -696,7 +1098,7 @@ v20.6.0 * Added support for using passwords from keyring in the upload command. See `the upload docs - <http://pythonhosted.org/setuptools/setuptools.html#upload-upload-source-and-or-egg-distributions-to-pypi>`_ + <https://setuptools.readthedocs.io/en/latest/setuptools.html#upload-upload-source-and-or-egg-distributions-to-pypi>`_ for details. 20.0 @@ -1450,7 +1852,7 @@ process to fail and PyPI uploads no longer accept files for 13.0. --- * Added a `Developer Guide - <https://pythonhosted.org/setuptools/developer-guide.html>`_ to the official + <https://setuptools.readthedocs.io/en/latest/developer-guide.html>`_ to the official documentation. * Some code refactoring and cleanup was done with no intended behavioral changes. @@ -2870,7 +3272,7 @@ easy_install * ``setuptools`` now finds its commands, ``setup()`` argument validators, and metadata writers using entry points, so that they can be extended by third-party packages. See `Creating distutils Extensions - <http://pythonhosted.org/setuptools/setuptools.html#creating-distutils-extensions>`_ + <https://setuptools.readthedocs.io/en/latest/setuptools.html#creating-distutils-extensions>`_ for more details. * The vestigial ``depends`` command has been removed. It was never finished diff --git a/MANIFEST.in b/MANIFEST.in index e25a5ea5..325bbed8 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -11,3 +11,4 @@ include LICENSE include launcher.c include msvc-build-launcher.cmd include pytest.ini +include tox.ini @@ -1,239 +1,23 @@ -=============================== -Installing and Using Setuptools -=============================== - -.. contents:: **Table of Contents** - - -.. image:: https://setuptools.readthedocs.io/en/latest/?badge=latest +.. image:: https://readthedocs.org/projects/setuptools/badge/?version=latest :target: https://setuptools.readthedocs.io -------------------------- -Installation Instructions -------------------------- - -The recommended way to bootstrap setuptools on any system is to download -`ez_setup.py`_ and run it using the target Python environment. Different -operating systems have different recommended techniques to accomplish this -basic routine, so below are some examples to get you started. - -Setuptools requires Python 3.3 or later (or Python 2.7). - -The link provided to ez_setup.py is a bookmark to bootstrap script for the -latest known stable release. - -.. _ez_setup.py: https://bootstrap.pypa.io/ez_setup.py - -Windows (Powershell 3 or later) -=============================== - -For best results, uninstall previous versions FIRST (see `Uninstalling`_). - -Using Windows 8 (which includes PowerShell 3) or earlier versions of Windows -with PowerShell 3 installed, it's possible to install with one simple -Powershell command. Start up Powershell and paste this command:: - - > (Invoke-WebRequest https://bootstrap.pypa.io/ez_setup.py).Content | python - - -.. image:: https://badges.gitter.im/pypa/setuptools.svg - :alt: Join the chat at https://gitter.im/pypa/setuptools - :target: https://gitter.im/pypa/setuptools?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge - -You must start the Powershell with Administrative privileges or you may choose -to install a user-local installation:: - - > (Invoke-WebRequest https://bootstrap.pypa.io/ez_setup.py).Content | python - --user - -If you have Python 3.3 or later, you can use the ``py`` command to install to -different Python versions. For example, to install to Python 3.3 if you have -Python 2.7 installed:: - - > (Invoke-WebRequest https://bootstrap.pypa.io/ez_setup.py).Content | py -3 - - -The recommended way to install setuptools on Windows is to download -`ez_setup.py`_ and run it. The script will download the appropriate -distribution file and install it for you. - -Once installation is complete, you will find an ``easy_install`` program in -your Python ``Scripts`` subdirectory. For simple invocation and best results, -add this directory to your ``PATH`` environment variable, if it is not already -present. If you did a user-local install, the ``Scripts`` subdirectory is -``$env:APPDATA\Python\Scripts``. - - -Windows (simplified) -==================== - -For Windows without PowerShell 3 or for installation without a command-line, -download `ez_setup.py`_ using your preferred web browser or other technique -and "run" that file. - - -Unix (wget) -=========== - -Most Linux distributions come with wget. - -Download `ez_setup.py`_ and run it using the target Python version. The script -will download the appropriate version and install it for you:: - - > wget https://bootstrap.pypa.io/ez_setup.py -O - | python - -Note that you will may need to invoke the command with superuser privileges to -install to the system Python:: - - > wget https://bootstrap.pypa.io/ez_setup.py -O - | sudo python - -Alternatively, Setuptools may be installed to a user-local path:: - - > wget https://bootstrap.pypa.io/ez_setup.py -O - | python - --user - -Note that on some older systems (noted on Debian 6 and CentOS 5 installations), -`wget` may refuse to download `ez_setup.py`, complaining that the certificate common name `*.c.ssl.fastly.net` -does not match the host name `bootstrap.pypa.io`. In addition, the `ez_setup.py` script may then encounter similar problems using -`wget` internally to download `setuptools-x.y.zip`, complaining that the certificate common name of `www.python.org` does not match the -host name `pypi.python.org`. Those are known issues, related to a bug in the older versions of `wget` -(see `Issue 59 <https://bitbucket.org/pypa/pypi/issue/59#comment-5881915>`_). If you happen to encounter them, -install Setuptools as follows:: - - > wget --no-check-certificate https://bootstrap.pypa.io/ez_setup.py - > python ez_setup.py --insecure - - -Unix including Mac OS X (curl) -============================== +See the `Installation Instructions +<https://packaging.python.org/installing/>`_ in the Python Packaging +User's Guide for instructions on installing, upgrading, and uninstalling +Setuptools. -If your system has curl installed, follow the ``wget`` instructions but -replace ``wget`` with ``curl`` and ``-O`` with ``-o``. For example:: +The project is `maintained at GitHub <https://github.com/pypa/setuptools>`_. - > curl https://bootstrap.pypa.io/ez_setup.py -o - | python +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 +submitted directly to the `bug tracker +<https://github.com/pypa/setuptools/issues>`_. -Advanced Installation -===================== - -For more advanced installation options, such as installing to custom -locations or prefixes, download and extract the source -tarball from `Setuptools on PyPI <https://pypi.python.org/pypi/setuptools>`_ -and run setup.py with any supported distutils and Setuptools options. -For example:: - - setuptools-x.x$ python setup.py install --prefix=/opt/setuptools - -Use ``--help`` to get a full options list, but we recommend consulting -the `EasyInstall manual`_ for detailed instructions, especially `the section -on custom installation locations`_. - -.. _EasyInstall manual: https://pythonhosted.org/setuptools/EasyInstall -.. _the section on custom installation locations: https://pythonhosted.org/setuptools/EasyInstall#custom-installation-locations - - -Downloads -========= - -All setuptools downloads can be found at `the project's home page in the Python -Package Index`_. Scroll to the very bottom of the page to find the links. - -.. _the project's home page in the Python Package Index: https://pypi.python.org/pypi/setuptools - -In addition to the PyPI downloads, the development version of ``setuptools`` -is available from the `Bitbucket repo`_, and in-development versions of the -`0.6 branch`_ are available as well. - -.. _Bitbucket repo: https://bitbucket.org/pypa/setuptools/get/default.tar.gz#egg=setuptools-dev -.. _0.6 branch: http://svn.python.org/projects/sandbox/branches/setuptools-0.6/#egg=setuptools-dev06 - -Uninstalling -============ - -On Windows, if Setuptools was installed using an ``.exe`` or ``.msi`` -installer, simply use the uninstall feature of "Add/Remove Programs" in the -Control Panel. - -Otherwise, to uninstall Setuptools or Distribute, regardless of the Python -version, delete all ``setuptools*`` and ``distribute*`` files and -directories from your system's ``site-packages`` directory -(and any other ``sys.path`` directories) FIRST. - -If you are upgrading or otherwise plan to re-install Setuptools or Distribute, -nothing further needs to be done. If you want to completely remove Setuptools, -you may also want to remove the 'easy_install' and 'easy_install-x.x' scripts -and associated executables installed to the Python scripts directory. - --------------------------------- -Using Setuptools and EasyInstall --------------------------------- - -Here are some of the available manuals, tutorials, and other resources for -learning about Setuptools, Python Eggs, and EasyInstall: - -* `The EasyInstall user's guide and reference manual`_ -* `The setuptools Developer's Guide`_ -* `The pkg_resources API reference`_ -* `The Internal Structure of Python Eggs`_ - -Questions, comments, and bug reports should be directed to the `distutils-sig -mailing list`_. If you have written (or know of) any tutorials, documentation, -plug-ins, or other resources for setuptools users, please let us know about -them there, so this reference list can be updated. If you have working, -*tested* patches to correct problems or add features, you may submit them to -the `setuptools bug tracker`_. - -.. _setuptools bug tracker: https://github.com/pypa/setuptools/issues -.. _The Internal Structure of Python Eggs: https://setuptools.readthedocs.io/en/latest/formats.html -.. _The setuptools Developer's Guide: https://setuptools.readthedocs.io/en/latest/developer-guide.html -.. _The pkg_resources API reference: https://setuptools.readthedocs.io/en/latest/pkg_resources.html -.. _The EasyInstall user's guide and reference manual: https://setuptools.readthedocs.io/en/latest/easy_install.html -.. _distutils-sig mailing list: http://mail.python.org/pipermail/distutils-sig/ - - -------- -Credits -------- - -* The original design for the ``.egg`` format and the ``pkg_resources`` API was - co-created by Phillip Eby and Bob Ippolito. Bob also implemented the first - version of ``pkg_resources``, and supplied the OS X operating system version - compatibility algorithm. - -* Ian Bicking implemented many early "creature comfort" features of - easy_install, including support for downloading via Sourceforge and - Subversion repositories. Ian's comments on the Web-SIG about WSGI - application deployment also inspired the concept of "entry points" in eggs, - and he has given talks at PyCon and elsewhere to inform and educate the - community about eggs and setuptools. - -* Jim Fulton contributed time and effort to build automated tests of various - aspects of ``easy_install``, and supplied the doctests for the command-line - ``.exe`` wrappers on Windows. - -* Phillip J. Eby is the seminal author of setuptools, and - first proposed the idea of an importable binary distribution format for - Python application plug-ins. - -* Significant parts of the implementation of setuptools were funded by the Open - Source Applications Foundation, to provide a plug-in infrastructure for the - Chandler PIM application. In addition, many OSAF staffers (such as Mike - "Code Bear" Taylor) contributed their time and stress as guinea pigs for the - use of eggs and setuptools, even before eggs were "cool". (Thanks, guys!) - -* Tarek Ziadé is the principal author of the Distribute fork, which - re-invigorated the community on the project, encouraged renewed innovation, - and addressed many defects. - -* Since the merge with Distribute, Jason R. Coombs is the - maintainer of setuptools. The project is maintained in coordination with - the Python Packaging Authority (PyPA) and the larger Python community. - -.. _files: - - ---------------- Code of Conduct --------------- Everyone interacting in the setuptools project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the -`PyPA Code of Conduct`_. - -.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ +`PyPA Code of Conduct <https://www.pypa.io/en/latest/code-of-conduct/>`_. diff --git a/bootstrap.py b/bootstrap.py index c5f470a4..8c7d7fc3 100644 --- a/bootstrap.py +++ b/bootstrap.py @@ -5,10 +5,14 @@ environment by creating a minimal egg-info directory and then invoking the egg-info command to flesh out the egg-info directory. """ +from __future__ import unicode_literals + import os import sys import textwrap import subprocess +import io + minimal_egg_info = textwrap.dedent(""" [distutils.commands] @@ -40,7 +44,7 @@ def build_egg_info(): """ os.mkdir('setuptools.egg-info') - with open('setuptools.egg-info/entry_points.txt', 'w') as ep: + with io.open('setuptools.egg-info/entry_points.txt', 'w') as ep: ep.write(minimal_egg_info) @@ -52,6 +56,9 @@ def run_egg_info(): subprocess.check_call(cmd) -if __name__ == '__main__': +def main(): ensure_egg_info() run_egg_info() + + +__name__ == '__main__' and main() diff --git a/conftest.py b/conftest.py index 0da92be9..3cccfe1a 100644 --- a/conftest.py +++ b/conftest.py @@ -1,6 +1,3 @@ -import os - - pytest_plugins = 'setuptools.tests.fixtures' @@ -9,17 +6,3 @@ def pytest_addoption(parser): "--package_name", action="append", default=[], help="list of package_name to pass to test functions", ) - - -def pytest_configure(): - _issue_852_workaround() - - -def _issue_852_workaround(): - """ - Patch 'setuptools.__file__' with an absolute path - for forward compatibility with Python 3. - Workaround for https://github.com/pypa/setuptools/issues/852 - """ - setuptools = __import__('setuptools') - setuptools.__file__ = os.path.abspath(setuptools.__file__) diff --git a/docs/_templates/indexsidebar.html b/docs/_templates/indexsidebar.html index a27c85fe..3b127602 100644 --- a/docs/_templates/indexsidebar.html +++ b/docs/_templates/indexsidebar.html @@ -5,4 +5,4 @@ <h3>Questions? Suggestions? Contributions?</h3> -<p>Visit the <a href="https://bitbucket.org/pypa/setuptools">Setuptools project page</a> </p> +<p>Visit the <a href="https://github.com/pypa/setuptools">Setuptools project page</a> </p> diff --git a/docs/conf.py b/docs/conf.py index fae8e632..f7d02303 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -18,18 +18,23 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# Allow Sphinx to find the setup command that is imported below, as referenced above. -import os +import subprocess import sys -sys.path.append(os.path.abspath('..')) +import os + -import setup as setup_script +# hack to run the bootstrap script so that jaraco.packaging.sphinx +# can invoke setup.py +'READTHEDOCS' in os.environ and subprocess.check_call( + [sys.executable, 'bootstrap.py'], + cwd=os.path.join(os.path.dirname(__file__), os.path.pardir), +) # -- General configuration ----------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['rst.linker'] +extensions = ['jaraco.packaging.sphinx', 'rst.linker', 'sphinx.ext.autosectionlabel'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -40,19 +45,6 @@ source_suffix = '.txt' # The master toctree document. master_doc = 'index' -# General information about the project. -project = 'Setuptools' -copyright = '2009-2014, The fellowship of the packaging' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = setup_script.setup_params['version'] -# The full version, including alpha/beta/rc tags. -release = setup_script.setup_params['version'] - # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] @@ -69,13 +61,6 @@ html_theme = 'nature' # Add any paths that contain custom themes here, relative to this directory. html_theme_path = ['_theme'] -# The name for this set of Sphinx documents. If None, it defaults to -# "<project> v<release> documentation". -html_title = "Setuptools documentation" - -# A shorter title for the navigation bar. Default is the same as html_title. -html_short_title = "Setuptools" - # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. html_use_smartypants = True @@ -89,9 +74,6 @@ html_use_modindex = False # If false, no index is generated. html_use_index = False -# Output file base name for HTML help builder. -htmlhelp_basename = 'Setuptoolsdoc' - # -- Options for LaTeX output -------------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples @@ -109,56 +91,60 @@ link_files = { ), replace=[ dict( - pattern=r"(Issue )?#(?P<issue>\d+)", - url='{GH}/pypa/setuptools/issues/{issue}', + pattern=r'(Issue )?#(?P<issue>\d+)', + url='{package_url}/issues/{issue}', ), dict( - pattern=r"BB Pull Request ?#(?P<bb_pull_request>\d+)", + pattern=r'BB Pull Request ?#(?P<bb_pull_request>\d+)', url='{BB}/pypa/setuptools/pull-request/{bb_pull_request}', ), dict( - pattern=r"Distribute #(?P<distribute>\d+)", + pattern=r'Distribute #(?P<distribute>\d+)', url='{BB}/tarek/distribute/issue/{distribute}', ), dict( - pattern=r"Buildout #(?P<buildout>\d+)", + pattern=r'Buildout #(?P<buildout>\d+)', url='{GH}/buildout/buildout/issues/{buildout}', ), dict( - pattern=r"Old Setuptools #(?P<old_setuptools>\d+)", + pattern=r'Old Setuptools #(?P<old_setuptools>\d+)', url='http://bugs.python.org/setuptools/issue{old_setuptools}', ), dict( - pattern=r"Jython #(?P<jython>\d+)", + pattern=r'Jython #(?P<jython>\d+)', url='http://bugs.jython.org/issue{jython}', ), dict( - pattern=r"Python #(?P<python>\d+)", + pattern=r'Python #(?P<python>\d+)', url='http://bugs.python.org/issue{python}', ), dict( - pattern=r"Interop #(?P<interop>\d+)", + pattern=r'Interop #(?P<interop>\d+)', url='{GH}/pypa/interoperability-peps/issues/{interop}', ), dict( - pattern=r"Pip #(?P<pip>\d+)", + pattern=r'Pip #(?P<pip>\d+)', url='{GH}/pypa/pip/issues/{pip}', ), dict( - pattern=r"Packaging #(?P<packaging>\d+)", + pattern=r'Packaging #(?P<packaging>\d+)', url='{GH}/pypa/packaging/issues/{packaging}', ), dict( - pattern=r"[Pp]ackaging (?P<packaging_ver>\d+(\.\d+)+)", + pattern=r'[Pp]ackaging (?P<packaging_ver>\d+(\.\d+)+)', url='{GH}/pypa/packaging/blob/{packaging_ver}/CHANGELOG.rst', ), dict( - pattern=r"PEP[- ](?P<pep_number>\d+)", + pattern=r'PEP[- ](?P<pep_number>\d+)', url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', ), dict( - pattern=r"^(?m)((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n", - with_scm="{text}\n{rev[timestamp]:%d %b %Y}\n", + pattern=r'setuptools_svn #(?P<setuptools_svn>\d+)', + url='{GH}/jaraco/setuptools_svn/issues/{setuptools_svn}', + ), + dict( + pattern=r'^(?m)((?P<scm_version>v?\d+(\.\d+){1,2}))\n[-=]+\n', + with_scm='{text}\n{rev[timestamp]:%d %b %Y}\n', ), ], ), diff --git a/docs/developer-guide.txt b/docs/developer-guide.txt index c8f51b02..8a136380 100644 --- a/docs/developer-guide.txt +++ b/docs/developer-guide.txt @@ -13,11 +13,11 @@ Recommended Reading ------------------- Please read `How to write the perfect pull request -<http://blog.jaraco.com/2014/04/how-to-write-perfect-pull-request.html>`_ -for some tips on contributing to open source projects. Although the article -is not authoritative, it was authored by the maintainer of Setuptools, so -reflects his opinions and will improve the likelihood of acceptance and -quality of contribution. +<https://blog.jaraco.com/how-to-write-perfect-pull-request/>`_ for some tips +on contributing to open source projects. Although the article is not +authoritative, it was authored by the maintainer of Setuptools, so reflects +his opinions and will improve the likelihood of acceptance and quality of +contribution. ------------------ Project Management diff --git a/docs/history.txt b/docs/history.txt index 8e217503..8fd1dc65 100644 --- a/docs/history.txt +++ b/docs/history.txt @@ -6,3 +6,41 @@ History ******* .. include:: ../CHANGES (links).rst + +Credits +******* + +* The original design for the ``.egg`` format and the ``pkg_resources`` API was + co-created by Phillip Eby and Bob Ippolito. Bob also implemented the first + version of ``pkg_resources``, and supplied the OS X operating system version + compatibility algorithm. + +* Ian Bicking implemented many early "creature comfort" features of + easy_install, including support for downloading via Sourceforge and + Subversion repositories. Ian's comments on the Web-SIG about WSGI + application deployment also inspired the concept of "entry points" in eggs, + and he has given talks at PyCon and elsewhere to inform and educate the + community about eggs and setuptools. + +* Jim Fulton contributed time and effort to build automated tests of various + aspects of ``easy_install``, and supplied the doctests for the command-line + ``.exe`` wrappers on Windows. + +* Phillip J. Eby is the seminal author of setuptools, and + first proposed the idea of an importable binary distribution format for + Python application plug-ins. + +* Significant parts of the implementation of setuptools were funded by the Open + Source Applications Foundation, to provide a plug-in infrastructure for the + Chandler PIM application. In addition, many OSAF staffers (such as Mike + "Code Bear" Taylor) contributed their time and stress as guinea pigs for the + use of eggs and setuptools, even before eggs were "cool". (Thanks, guys!) + +* Tarek Ziadé is the principal author of the Distribute fork, which + re-invigorated the community on the project, encouraged renewed innovation, + and addressed many defects. + +* Since the merge with Distribute, Jason R. Coombs is the + maintainer of setuptools. The project is maintained in coordination with + the Python Packaging Authority (PyPA) and the larger Python community. + diff --git a/docs/index.txt b/docs/index.txt index 6ac37252..74aabb5e 100644 --- a/docs/index.txt +++ b/docs/index.txt @@ -16,10 +16,10 @@ Documentation content: .. toctree:: :maxdepth: 2 - history - roadmap - python3 setuptools easy_install pkg_resources + python3 development + roadmap + history diff --git a/docs/pkg_resources.txt b/docs/pkg_resources.txt index c504412d..8d337cb2 100644 --- a/docs/pkg_resources.txt +++ b/docs/pkg_resources.txt @@ -143,10 +143,10 @@ namespace package for Zope Corporation packages, and the ``peak`` namespace package for the Python Enterprise Application Kit. To create a namespace package, you list it in the ``namespace_packages`` -argument to ``setup()``, in your project's ``setup.py``. (See the `setuptools -documentation on namespace packages`_ for more information on this.) Also, -you must add a ``declare_namespace()`` call in the package's ``__init__.py`` -file(s): +argument to ``setup()``, in your project's ``setup.py``. (See the +:ref:`setuptools documentation on namespace packages <Namespace Packages>` for +more information on this.) Also, you must add a ``declare_namespace()`` call +in the package's ``__init__.py`` file(s): ``declare_namespace(name)`` Declare that the dotted package name `name` is a "namespace package" whose @@ -175,8 +175,6 @@ filesystem and zip importers, you can extend its support to other "importers" compatible with PEP 302 using the ``register_namespace_handler()`` function. See the section below on `Supporting Custom Importers`_ for details. -.. _setuptools documentation on namespace packages: http://peak.telecommunity.com/DevCenter/setuptools#namespace-packages - ``WorkingSet`` Objects ====================== diff --git a/docs/releases.txt b/docs/releases.txt index c84ddd75..30ea084f 100644 --- a/docs/releases.txt +++ b/docs/releases.txt @@ -7,25 +7,20 @@ mechanical technique for releases, enacted by Travis following a successful build of a tagged release per `PyPI deployment <https://docs.travis-ci.com/user/deployment/pypi>`_. -To cut a release, install and run ``bumpversion {part}`` where ``part`` +Prior to cutting a release, please check that the CHANGES.rst reflects +the summary of changes since the last release. +Ideally, these changelog entries would have been added +along with the changes, but it's always good to check. +Think about it from the +perspective of a user not involved with the development--what would +that person want to know about what has changed--or from the +perspective of your future self wanting to know when a particular +change landed. + +To cut a release, install and run ``bump2version {part}`` where ``part`` is major, minor, or patch based on the scope of the changes in the release. Then, push the commits to the master branch. If tests pass, -the release will be uploaded to PyPI (from the Python 3.5 tests). - -Bootstrap Branch ----------------- - -Setuptools has a bootstrap script (ez_setup.py), which is hosted in the -repository in the ``bootstrap`` branch. - -Therefore, the latest bootstrap script can be retrieved by checking out -that branch. - -The officially-published location of the bootstrap script is hosted on Python -infrastructure (#python-infra on freenode) at https://bootstrap.pypa.io and -is updated every fifteen minutes from the bootstrap branch. Sometimes, -especially when the bootstrap script is rolled back, this -process doesn't work as expected and requires manual intervention. +the release will be uploaded to PyPI (from the Python 3.6 tests). Release Frequency ----------------- diff --git a/docs/requirements.txt b/docs/requirements.txt index 0871ed76..2138c884 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1 +1,5 @@ -rst.linker>=1.6.1 +sphinx +rst.linker>=1.9 +jaraco.packaging>=3.2 + +setuptools>=34 diff --git a/docs/setuptools.txt b/docs/setuptools.txt index 1b0be77d..a9242a51 100644 --- a/docs/setuptools.txt +++ b/docs/setuptools.txt @@ -59,14 +59,6 @@ Feature Highlights: * Create extensible applications and frameworks that automatically discover extensions, using simple "entry points" declared in a project's setup script. -In addition to the PyPI downloads, the development version of ``setuptools`` -is available from the `Python SVN sandbox`_, and in-development versions of the -`0.6 branch`_ are available as well. - -.. _0.6 branch: http://svn.python.org/projects/sandbox/branches/setuptools-0.6/#egg=setuptools-dev06 - -.. _Python SVN sandbox: http://svn.python.org/projects/sandbox/trunk/setuptools/#egg=setuptools-dev - .. contents:: **Table of Contents** .. _ez_setup.py: `bootstrap module`_ @@ -769,6 +761,40 @@ so that Package B doesn't have to remove the ``[PDF]`` from its requirement specifier. +.. _Platform Specific Dependencies: + + +Declaring platform specific dependencies +---------------------------------------- + +Sometimes a project might require a dependency to run on a specific platform. +This could to a package that back ports a module so that it can be used in +older python versions. Or it could be a package that is required to run on a +specific operating system. This will allow a project to work on multiple +different platforms without installing dependencies that are not required for +a platform that is installing the project. + +For example, here is a project that uses the ``enum`` module and ``pywin32``:: + + setup( + name="Project", + ... + install_requires=[ + 'enum34;python_version<"3.4"', + 'pywin32 >= 1.0;platform_system=="Windows"' + ] + ) + +Since the ``enum`` module was added in Python 3.4, it should only be installed +if the python version is earlier. Since ``pywin32`` will only be used on +windows, it should only be installed when the operating system is Windows. +Specifying version requirements for the dependencies is supported as normal. + +The environmental markers that may be used for testing platform types are +detailed in `PEP 508`_. + +.. _PEP 508: https://www.python.org/dev/peps/pep-0508/ + Including Data Files ==================== @@ -914,14 +940,13 @@ Typically, existing programs manipulate a package's ``__file__`` attribute in order to find the location of data files. However, this manipulation isn't compatible with PEP 302-based import hooks, including importing from zip files and Python Eggs. It is strongly recommended that, if you are using data files, -you should use the `Resource Management API`_ of ``pkg_resources`` to access +you should use the :ref:`ResourceManager API` of ``pkg_resources`` to access them. The ``pkg_resources`` module is distributed as part of setuptools, so if you're using setuptools to distribute your package, there is no reason not to use its resource management API. See also `Accessing Package Resources`_ for a quick example of converting code that uses ``__file__`` to use ``pkg_resources`` instead. -.. _Resource Management API: http://peak.telecommunity.com/DevCenter/PkgResources#resourcemanager-api .. _Accessing Package Resources: http://peak.telecommunity.com/DevCenter/PythonEggs#accessing-package-resources @@ -933,8 +958,8 @@ location (e.g. ``/usr/share``). This feature intended to be used for things like documentation, example configuration files, and the like. ``setuptools`` does not install these data files in a separate location, however. They are bundled inside the egg file or directory, alongside the Python modules and -packages. The data files can also be accessed using the `Resource Management -API`_, by specifying a ``Requirement`` instead of a package name:: +packages. The data files can also be accessed using the :ref:`ResourceManager +API`, by specifying a ``Requirement`` instead of a package name:: from pkg_resources import Requirement, resource_filename filename = resource_filename(Requirement.parse("MyProject"),"sample.conf") @@ -1151,6 +1176,8 @@ Distributing a ``setuptools``-based project Using ``setuptools``... Without bundling it! --------------------------------------------- +.. warning:: **ez_setup** is deprecated in favor of PIP with **PEP-518** support. + 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`` @@ -1422,10 +1449,6 @@ egg distributions by adding one or more of the following to the project's manually-specified post-release tag, such as a build or revision number (``--tag-build=STRING, -bSTRING``) -* A "last-modified revision number" string generated automatically from - Subversion's metadata (assuming your project is being built from a Subversion - "working copy") (``--tag-svn-revision, -r``) - * An 8-character representation of the build date (``--tag-date, -d``), as a postrelease tag @@ -1557,68 +1580,6 @@ this:: in order to check out the in-development version of ``projectname``. -Managing "Continuous Releases" Using Subversion ------------------------------------------------ - -If you expect your users to track in-development versions of your project via -Subversion, there are a few additional steps you should take to ensure that -things work smoothly with EasyInstall. First, you should add the following -to your project's ``setup.cfg`` file: - -.. code-block:: ini - - [egg_info] - tag_build = .dev - tag_svn_revision = 1 - -This will tell ``setuptools`` to generate package version numbers like -``1.0a1.dev-r1263``, which will be considered to be an *older* release than -``1.0a1``. Thus, when you actually release ``1.0a1``, the entire egg -infrastructure (including ``setuptools``, ``pkg_resources`` and EasyInstall) -will know that ``1.0a1`` supersedes any interim snapshots from Subversion, and -handle upgrades accordingly. - -(Note: the project version number you specify in ``setup.py`` should always be -the *next* version of your software, not the last released version. -Alternately, you can leave out the ``tag_build=.dev``, and always use the -*last* release as a version number, so that your post-1.0 builds are labelled -``1.0-r1263``, indicating a post-1.0 patchlevel. Most projects so far, -however, seem to prefer to think of their project as being a future version -still under development, rather than a past version being patched. It is of -course possible for a single project to have both situations, using -post-release numbering on release branches, and pre-release numbering on the -trunk. But you don't have to make things this complex if you don't want to.) - -Commonly, projects releasing code from Subversion will include a PyPI link to -their checkout URL (as described in the previous section) with an -``#egg=projectname-dev`` suffix. This allows users to request EasyInstall -to download ``projectname==dev`` in order to get the latest in-development -code. Note that if your project depends on such in-progress code, you may wish -to specify your ``install_requires`` (or other requirements) to include -``==dev``, e.g.: - -.. code-block:: python - - install_requires=["OtherProject>=0.2a1.dev-r143,==dev"] - -The above example says, "I really want at least this particular development -revision number, but feel free to follow and use an ``#egg=OtherProject-dev`` -link if you find one". This avoids the need to have actual source or binary -distribution snapshots of in-development code available, just to be able to -depend on the latest and greatest a project has to offer. - -A final note for Subversion development: if you are using SVN revision tags -as described in this section, it's a good idea to run ``setup.py develop`` -after each Subversion checkin or update, because your project's version number -will be changing, and your script wrappers need to be updated accordingly. - -Also, if the project's requirements have changed, the ``develop`` command will -take care of fetching the updated dependencies, building changed extensions, -etc. Be sure to also remind any of your users who check out your project -from Subversion that they need to run ``setup.py develop`` after every update -in order to keep their checkout completely in sync. - - Making "Official" (Non-Snapshot) Releases ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1632,18 +1593,18 @@ tagging the release, so the trunk will still produce development snapshots. Alternately, if you are not branching for releases, you can override the default version options on the command line, using something like:: - python setup.py egg_info -RDb "" sdist bdist_egg register upload + python setup.py egg_info -Db "" sdist bdist_egg register upload -The first part of this command (``egg_info -RDb ""``) will override the +The first part of this command (``egg_info -Db ""``) will override the configured tag information, before creating source and binary eggs, registering the project with PyPI, and uploading the files. Thus, these commands will use -the plain version from your ``setup.py``, without adding the Subversion -revision number or build designation string. +the plain version from your ``setup.py``, without adding the build designation +string. Of course, if you will be doing this a lot, you may wish to create a personal alias for this operation, e.g.:: - python setup.py alias -u release egg_info -RDb "" + python setup.py alias -u release egg_info -Db "" You can then use it like this:: @@ -1703,8 +1664,7 @@ the command line supplies its expansion. For example, this command defines a sitewide alias called "daily", that sets various ``egg_info`` tagging options:: - setup.py alias --global-config daily egg_info --tag-svn-revision \ - --tag-build=development + setup.py alias --global-config daily egg_info --tag-build=development Once the alias is defined, it can then be used with other setup commands, e.g.:: @@ -1714,7 +1674,7 @@ e.g.:: setup.py daily sdist bdist_egg # generate both The above commands are interpreted as if the word ``daily`` were replaced with -``egg_info --tag-svn-revision --tag-build=development``. +``egg_info --tag-build=development``. Note that setuptools will expand each alias *at most once* in a given command line. This serves two purposes. First, if you accidentally create an alias @@ -2001,27 +1961,6 @@ added in the following order: it on the command line using ``-b ""`` or ``--tag-build=""`` as an argument to the ``egg_info`` command. -``--tag-svn-revision, -r`` - If the current directory is a Subversion checkout (i.e. has a ``.svn`` - subdirectory, this appends a string of the form "-rNNNN" to the project's - version string, where NNNN is the revision number of the most recent - modification to the current directory, as obtained from the ``svn info`` - command. - - If the current directory is not a Subversion checkout, the command will - look for a ``PKG-INFO`` file instead, and try to find the revision number - from that, by looking for a "-rNNNN" string at the end of the version - number. (This is so that building a package from a source distribution of - a Subversion snapshot will produce a binary with the correct version - number.) - - If there is no ``PKG-INFO`` file, or the version number contained therein - does not end with ``-r`` and a number, then ``-r0`` is used. - -``--no-svn-revision, -R`` - Don't include the Subversion revision in the version number. This option - is included so you can override a default setting put in ``setup.cfg``. - ``--tag-date, -d`` Add a date stamp of the form "-YYYYMMDD" (e.g. "-20050528") to the project's version number. @@ -2335,73 +2274,16 @@ password from the keyring. New in 20.1: Added keyring support. -.. _upload_docs: - -``upload_docs`` - Upload package documentation to PyPI -====================================================== - -PyPI now supports uploading project documentation to the dedicated URL -https://pythonhosted.org/<project>/. - -The ``upload_docs`` command will create the necessary zip file out of a -documentation directory and will post to the repository. - -Note that to upload the documentation of a project, the corresponding version -must already be registered with PyPI, using the distutils ``register`` -command -- just like the ``upload`` command. - -Assuming there is an ``Example`` project with documentation in the -subdirectory ``docs``, e.g.:: - - Example/ - |-- example.py - |-- setup.cfg - |-- setup.py - |-- docs - | |-- build - | | `-- html - | | | |-- index.html - | | | `-- tips_tricks.html - | |-- conf.py - | |-- index.txt - | `-- tips_tricks.txt - -You can simply pass the documentation directory path to the ``upload_docs`` -command:: - - python setup.py upload_docs --upload-dir=docs/build/html - -If no ``--upload-dir`` is given, ``upload_docs`` will attempt to run the -``build_sphinx`` command to generate uploadable documentation. -For the command to become available, `Sphinx <http://sphinx.pocoo.org/>`_ -must be installed in the same environment as distribute. - -As with other ``setuptools``-based commands, you can define useful -defaults in the ``setup.cfg`` of your Python project, e.g.: - -.. code-block:: ini - - [upload_docs] - upload-dir = docs/build/html - -The ``upload_docs`` command has the following options: - -``--upload-dir`` - The directory to be uploaded to the repository. - -``--show-response`` - Display the full response text from server; this is useful for debugging - PyPI problems. - -``--repository=URL, -r URL`` - The URL of the repository to upload to. Defaults to - https://pypi.python.org/pypi (i.e., the main PyPI installation). - ----------------------------------------- Configuring setup() using setup.cfg files ----------------------------------------- +.. note:: New in 30.3.0 (8 Dec 2016). + +.. important:: ``setup.py`` with ``setup()`` function call is still required even + if your configuration resides in ``setup.cfg``. + ``Setuptools`` allows using configuration files (usually `setup.cfg`) to define package’s metadata and other options which are normally supplied to ``setup()`` function. @@ -2424,13 +2306,13 @@ boilerplate code in some cases. name = my_package version = attr: src.VERSION description = My package description - long_description = file: README.rst + long_description = file: README.rst, CHANGELOG.rst, LICENSE.rst keywords = one, two license = BSD 3-Clause License - - [metadata.classifiers] - Framework :: Django - Programming Language :: Python :: 3.5 + classifiers = + Framework :: Django + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.5 [options] zip_safe = False @@ -2448,6 +2330,11 @@ boilerplate code in some cases. pdf = ReportLab>=1.2; RXP rest = docutils>=0.3; pack ==1.1, ==1.3 + [options.packages.find] + exclude = + src.subpackage1 + src.subpackage2 + Metadata and options could be set in sections with the same names. @@ -2486,13 +2373,13 @@ Type names used below: * ``list-semi`` - dangling list or semicolon-separated values string * ``bool`` - ``True`` is 1, yes, true * ``dict`` - list-comma where keys from values are separated by = +* ``section`` - values could be read from a dedicated (sub)section Special directives: * ``attr:`` - value could be read from module attribute -* ``file:`` - value could be read from a file -* ``section:`` - values could be read from a dedicated (sub)section +* ``file:`` - value could be read from a list of files and then concatenated .. note:: @@ -2507,30 +2394,33 @@ Metadata Aliases given below are supported for compatibility reasons, but not advised. -================= ================= ===== -Key Aliases Accepted value type -================= ================= ===== -name str -version attr:, str -url home-page str -download_url download-url str -author str -author_email author-email str -maintainer str -maintainer_email maintainer-email str -classifiers classifier file:, section, list-comma -license file:, str -description summary file:, str -long_description long-description file:, str -keywords list-comma -platforms platform list-comma -provides list-comma -requires list-comma -obsoletes list-comma -================= ================= ===== - -**version** - ``attr:`` supports callables; supports iterables; -unsupported types are casted using ``str()``. +============================== ================= ===== +Key Aliases Accepted value type +============================== ================= ===== +name str +version attr:, str +url home-page str +download_url download-url str +author str +author_email author-email str +maintainer str +maintainer_email maintainer-email str +classifiers classifier file:, list-comma +license file:, str +description summary file:, str +long_description long-description file:, str +long_description_content_type str +keywords list-comma +platforms platform list-comma +provides list-comma +requires list-comma +obsoletes list-comma +============================== ================= ===== + +.. note:: + + **version** - ``attr:`` supports callables; supports iterables; + unsupported types are casted using ``str()``. Options @@ -2543,7 +2433,8 @@ zip_safe bool setup_requires list-semi install_requires list-semi extras_require section -entry_points file, section +python_requires str +entry_points file:, section use_2to3 bool use_2to3_fixers list-comma use_2to3_exclude_fixers list-comma @@ -2558,8 +2449,16 @@ package_dir dict package_data section exclude_package_data section namespace_packages list-comma +py_modules list-comma ======================= ===== +.. note:: + + **packages** - ``find:`` directive can be further configured + in a dedicated subsection `options.packages.find`. This subsection + accepts the same keys as `setuptools.find` function: + `where`, `include`, `exclude`. + Configuration API ================= @@ -2582,6 +2481,12 @@ in the first argument. To include values from other configuration files which could be in various places set `find_others` function argument to ``True``. +If you have only a configuration file but not the whole package you can still +try to get data out of it with the help of `ignore_option_errors` function +argument. When it is set to ``True`` all options with errors possibly produced +by directives, such as ``attr:`` and others will be silently ignored. +As a consequence the resulting dictionary will include no such options. + -------------------------------- Extending and Reusing Setuptools diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index d8000b00..497448de 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -34,9 +34,11 @@ import platform import collections import plistlib import email.parser +import errno import tempfile import textwrap import itertools +import inspect from pkgutil import get_importer try: @@ -67,6 +69,7 @@ try: except ImportError: importlib_machinery = None +from . import py31compat from pkg_resources.extern import appdirs from pkg_resources.extern import packaging __import__('pkg_resources.extern.packaging.version') @@ -74,9 +77,15 @@ __import__('pkg_resources.extern.packaging.specifiers') __import__('pkg_resources.extern.packaging.requirements') __import__('pkg_resources.extern.packaging.markers') + if (3, 0) < sys.version_info < (3, 3): raise RuntimeError("Python 3.3 or later is required") +if six.PY2: + # Those builtin exceptions are only defined in Python 3 + PermissionError = None + NotADirectoryError = None + # declare some globals that will be defined later to # satisfy the linters. require = None @@ -786,7 +795,7 @@ class WorkingSet(object): self._added_new(dist) def resolve(self, requirements, env=None, installer=None, - replace_conflicting=False): + replace_conflicting=False, extras=None): """List all distributions needed to (recursively) meet `requirements` `requirements` must be a sequence of ``Requirement`` objects. `env`, @@ -802,6 +811,12 @@ class WorkingSet(object): the wrong version. Otherwise, if an `installer` is supplied it will be invoked to obtain the correct version of the requirement and activate it. + + `extras` is a list of the extras to be used with these requirements. + This is important because extra requirements may look like `my_req; + extra = "my_extra"`, which would otherwise be interpreted as a purely + optional requirement. Instead, we want to be able to assert that these + requirements are truly required. """ # set up the stack @@ -825,7 +840,7 @@ class WorkingSet(object): # Ignore cyclic or redundant dependencies continue - if not req_extras.markers_pass(req): + if not req_extras.markers_pass(req, extras): continue dist = best.get(req.key) @@ -843,7 +858,10 @@ class WorkingSet(object): # distribution env = Environment([]) ws = WorkingSet([]) - dist = best[req.key] = env.best_match(req, ws, installer) + dist = best[req.key] = env.best_match( + req, ws, installer, + replace_conflicting=replace_conflicting + ) if dist is None: requirers = required_by.get(req, None) raise DistributionNotFound(req, requirers) @@ -1004,7 +1022,7 @@ class _ReqExtras(dict): Map each requirement to the extras that demanded it. """ - def markers_pass(self, req): + def markers_pass(self, req, extras=None): """ Evaluate markers for req against each extra that demanded it. @@ -1014,7 +1032,7 @@ class _ReqExtras(dict): """ extra_evals = ( req.marker.evaluate({'extra': extra}) - for extra in self.get(req, ()) + (None,) + for extra in self.get(req, ()) + (extras or (None,)) ) return not req.marker or any(extra_evals) @@ -1095,7 +1113,7 @@ class Environment(object): dists.append(dist) dists.sort(key=operator.attrgetter('hashcmp'), reverse=True) - def best_match(self, req, working_set, installer=None): + def best_match(self, req, working_set, installer=None, replace_conflicting=False): """Find distribution best matching `req` and usable on `working_set` This calls the ``find(req)`` method of the `working_set` to see if a @@ -1108,7 +1126,12 @@ class Environment(object): calling the environment's ``obtain(req, installer)`` method will be returned. """ - dist = working_set.find(req) + try: + dist = working_set.find(req) + except VersionConflict: + if not replace_conflicting: + raise + dist = None if dist is not None: return dist for dist in self[req.key]: @@ -1544,7 +1567,7 @@ class EggProvider(NullProvider): path = self.module_path old = None while path != old: - if _is_unpacked_egg(path): + if _is_egg_path(path): self.egg_name = os.path.basename(path) self.egg_info = os.path.join(path, 'EGG-INFO') self.egg_root = path @@ -1927,10 +1950,16 @@ def find_eggs_in_zip(importer, path_item, only=False): # don't yield nested distros return for subitem in metadata.resource_listdir('/'): - if _is_unpacked_egg(subitem): + if _is_egg_path(subitem): subpath = os.path.join(path_item, subitem) for dist in find_eggs_in_zip(zipimport.zipimporter(subpath), subpath): yield dist + elif subitem.lower().endswith('.dist-info'): + subpath = os.path.join(path_item, subitem) + submeta = EggMetadata(zipimport.zipimporter(subpath)) + submeta.egg_info = subpath + yield Distribution.from_location(path_item, subitem, submeta) + register_finder(zipimport.zipimporter, find_eggs_in_zip) @@ -1973,46 +2002,57 @@ def find_on_path(importer, path_item, only=False): """Yield distributions accessible on a sys.path directory""" path_item = _normalize_cached(path_item) - if os.path.isdir(path_item) and os.access(path_item, os.R_OK): - if _is_unpacked_egg(path_item): - yield Distribution.from_filename( - path_item, metadata=PathMetadata( - path_item, os.path.join(path_item, 'EGG-INFO') - ) + if _is_unpacked_egg(path_item): + yield Distribution.from_filename( + path_item, metadata=PathMetadata( + path_item, os.path.join(path_item, 'EGG-INFO') ) - else: - # scan for .egg and .egg-info in directory - path_item_entries = _by_version_descending(os.listdir(path_item)) - for entry in path_item_entries: - lower = entry.lower() - if lower.endswith('.egg-info') or lower.endswith('.dist-info'): - fullpath = os.path.join(path_item, entry) - if os.path.isdir(fullpath): - # egg-info directory, allow getting metadata - if len(os.listdir(fullpath)) == 0: - # Empty egg directory, skip. - continue - metadata = PathMetadata(path_item, fullpath) - else: - metadata = FileMetadata(fullpath) - yield Distribution.from_location( - path_item, entry, metadata, precedence=DEVELOP_DIST - ) - elif not only and _is_unpacked_egg(entry): - dists = find_distributions(os.path.join(path_item, entry)) - for dist in dists: - yield dist - elif not only and lower.endswith('.egg-link'): - with open(os.path.join(path_item, entry)) as entry_file: - entry_lines = entry_file.readlines() - for line in entry_lines: - if not line.strip(): - continue - path = os.path.join(path_item, line.rstrip()) - dists = find_distributions(path) - for item in dists: - yield item - break + ) + else: + try: + entries = os.listdir(path_item) + except (PermissionError, NotADirectoryError): + return + except OSError as e: + # Ignore the directory if does not exist, not a directory or we + # don't have permissions + if (e.errno in (errno.ENOTDIR, errno.EACCES, errno.ENOENT) + # Python 2 on Windows needs to be handled this way :( + or hasattr(e, "winerror") and e.winerror == 267): + return + raise + # scan for .egg and .egg-info in directory + path_item_entries = _by_version_descending(entries) + for entry in path_item_entries: + lower = entry.lower() + if lower.endswith('.egg-info') or lower.endswith('.dist-info'): + fullpath = os.path.join(path_item, entry) + if os.path.isdir(fullpath): + # egg-info directory, allow getting metadata + if len(os.listdir(fullpath)) == 0: + # Empty egg directory, skip. + continue + metadata = PathMetadata(path_item, fullpath) + else: + metadata = FileMetadata(fullpath) + yield Distribution.from_location( + path_item, entry, metadata, precedence=DEVELOP_DIST + ) + elif not only and _is_egg_path(entry): + dists = find_distributions(os.path.join(path_item, entry)) + for dist in dists: + yield dist + elif not only and lower.endswith('.egg-link'): + with open(os.path.join(path_item, entry)) as entry_file: + entry_lines = entry_file.readlines() + for line in entry_lines: + if not line.strip(): + continue + path = os.path.join(path_item, line.rstrip()) + dists = find_distributions(path) + for item in dists: + yield item + break register_finder(pkgutil.ImpImporter, find_on_path) @@ -2093,6 +2133,10 @@ def _rebuild_mod_path(orig_path, package_name, module): parts = path_parts[:-module_parts] return safe_sys_path_index(_normalize_cached(os.sep.join(parts))) + if not isinstance(orig_path, list): + # Is this behavior useful when module.__path__ is not a list? + return + orig_path.sort(key=position_in_sys_path) module.__path__[:] = [_normalize_cached(p) for p in orig_path] @@ -2182,12 +2226,22 @@ def _normalize_cached(filename, _cache={}): return result +def _is_egg_path(path): + """ + Determine if given path appears to be an egg. + """ + return ( + path.lower().endswith('.egg') + ) + + def _is_unpacked_egg(path): """ Determine if given path appears to be an unpacked egg. """ return ( - path.lower().endswith('.egg') + _is_egg_path(path) and + os.path.isfile(os.path.join(path, 'EGG-INFO', 'PKG-INFO')) ) @@ -2279,8 +2333,14 @@ class EntryPoint(object): def require(self, env=None, installer=None): if self.extras and not self.dist: raise UnknownExtra("Can't require() without a distribution", self) + + # Get the requirements for this entry point with all its extras and + # then resolve them. We have to pass `extras` along when resolving so + # that the working set knows what extras we want. Otherwise, for + # dist-info distributions, the working set will assume that the + # requirements for that extra are purely optional and skip over them. reqs = self.dist.requires(self.extras) - items = working_set.resolve(reqs, env, installer) + items = working_set.resolve(reqs, env, installer, extras=self.extras) list(map(working_set.add, items)) pattern = re.compile( @@ -2895,20 +2955,20 @@ class Requirement(packaging.requirements.Requirement): return req -def _get_mro(cls): - """Get an mro for a type or classic class""" - if not isinstance(cls, type): - - class cls(cls, object): - pass - - return cls.__mro__[1:] - return cls.__mro__ +def _always_object(classes): + """ + Ensure object appears in the mro even + for old-style classes. + """ + if object not in classes: + return classes + (object,) + return classes def _find_adapter(registry, ob): """Return an adapter factory for `ob` from `registry`""" - for t in _get_mro(getattr(ob, '__class__', type(ob))): + types = _always_object(inspect.getmro(getattr(ob, '__class__', type(ob)))) + for t in types: if t in registry: return registry[t] @@ -2916,8 +2976,7 @@ def _find_adapter(registry, ob): def ensure_directory(path): """Ensure that the parent directory of `path` exists""" dirname = os.path.dirname(path) - if not os.path.isdir(dirname): - os.makedirs(dirname) + py31compat.makedirs(dirname, exist_ok=True) def _bypass_ensure_directory(path): diff --git a/pkg_resources/py31compat.py b/pkg_resources/py31compat.py new file mode 100644 index 00000000..331a51bb --- /dev/null +++ b/pkg_resources/py31compat.py @@ -0,0 +1,22 @@ +import os +import errno +import sys + + +def _makedirs_31(path, exist_ok=False): + try: + os.makedirs(path) + except OSError as exc: + if not exist_ok or exc.errno != errno.EEXIST: + raise + + +# rely on compatibility behavior until mode considerations +# and exists_ok considerations are disentangled. +# See https://github.com/pypa/setuptools/pull/1083#issuecomment-315168663 +needs_makedirs = ( + sys.version_info < (3, 2, 5) or + (3, 3) <= sys.version_info < (3, 3, 6) or + (3, 4) <= sys.version_info < (3, 4, 1) +) +makedirs = _makedirs_31 if needs_makedirs else os.makedirs diff --git a/pkg_resources/tests/test_find_distributions.py b/pkg_resources/tests/test_find_distributions.py new file mode 100644 index 00000000..97999b33 --- /dev/null +++ b/pkg_resources/tests/test_find_distributions.py @@ -0,0 +1,65 @@ +import subprocess +import sys + +import pytest +import pkg_resources + +SETUP_TEMPLATE = """ +import setuptools +setuptools.setup( + name="my-test-package", + version="1.0", + zip_safe=True, +) +""".lstrip() + +class TestFindDistributions: + + @pytest.fixture + def target_dir(self, tmpdir): + target_dir = tmpdir.mkdir('target') + # place a .egg named directory in the target that is not an egg: + target_dir.mkdir('not.an.egg') + return str(target_dir) + + @pytest.fixture + def project_dir(self, tmpdir): + project_dir = tmpdir.mkdir('my-test-package') + (project_dir / "setup.py").write(SETUP_TEMPLATE) + return str(project_dir) + + def test_non_egg_dir_named_egg(self, target_dir): + dists = pkg_resources.find_distributions(target_dir) + assert not list(dists) + + def test_standalone_egg_directory(self, project_dir, target_dir): + # install this distro as an unpacked egg: + args = [ + sys.executable, + '-c', 'from setuptools.command.easy_install import main; main()', + '-mNx', + '-d', target_dir, + '--always-unzip', + project_dir, + ] + subprocess.check_call(args) + dists = pkg_resources.find_distributions(target_dir) + assert [dist.project_name for dist in dists] == ['my-test-package'] + dists = pkg_resources.find_distributions(target_dir, only=True) + assert not list(dists) + + def test_zipped_egg(self, project_dir, target_dir): + # install this distro as an unpacked egg: + args = [ + sys.executable, + '-c', 'from setuptools.command.easy_install import main; main()', + '-mNx', + '-d', target_dir, + '--zip-ok', + project_dir, + ] + subprocess.check_call(args) + dists = pkg_resources.find_distributions(target_dir) + assert [dist.project_name for dist in dists] == ['my-test-package'] + dists = pkg_resources.find_distributions(target_dir, only=True) + assert not list(dists) diff --git a/pkg_resources/tests/test_markers.py b/pkg_resources/tests/test_markers.py index 78810b6e..15a3b499 100644 --- a/pkg_resources/tests/test_markers.py +++ b/pkg_resources/tests/test_markers.py @@ -1,7 +1,4 @@ -try: - import unittest.mock as mock -except ImportError: - import mock +import mock from pkg_resources import evaluate_marker diff --git a/pkg_resources/tests/test_resources.py b/pkg_resources/tests/test_resources.py index 8223963c..dcd2f42c 100644 --- a/pkg_resources/tests/test_resources.py +++ b/pkg_resources/tests/test_resources.py @@ -585,7 +585,7 @@ class TestParsing: [Requirement('Twis-Ted>=1.2-1')] ) assert ( - list(parse_requirements('Twisted >=1.2, \ # more\n<2.0')) + list(parse_requirements('Twisted >=1.2, \\ # more\n<2.0')) == [Requirement('Twisted>=1.2,<2.0')] ) diff --git a/pkg_resources/tests/test_working_set.py b/pkg_resources/tests/test_working_set.py new file mode 100644 index 00000000..422a7283 --- /dev/null +++ b/pkg_resources/tests/test_working_set.py @@ -0,0 +1,478 @@ +import inspect +import re +import textwrap + +import pytest + +import pkg_resources + +from .test_resources import Metadata + + +def strip_comments(s): + return '\n'.join( + l for l in s.split('\n') + if l.strip() and not l.strip().startswith('#') + ) + +def parse_distributions(s): + ''' + Parse a series of distribution specs of the form: + {project_name}-{version} + [optional, indented requirements specification] + + Example: + + foo-0.2 + bar-1.0 + foo>=3.0 + [feature] + baz + + yield 2 distributions: + - project_name=foo, version=0.2 + - project_name=bar, version=1.0, requires=['foo>=3.0', 'baz; extra=="feature"'] + ''' + s = s.strip() + for spec in re.split('\n(?=[^\s])', s): + if not spec: + continue + fields = spec.split('\n', 1) + assert 1 <= len(fields) <= 2 + name, version = fields.pop(0).split('-') + if fields: + requires = textwrap.dedent(fields.pop(0)) + metadata=Metadata(('requires.txt', requires)) + else: + metadata = None + dist = pkg_resources.Distribution(project_name=name, + version=version, + metadata=metadata) + yield dist + + +class FakeInstaller(object): + + def __init__(self, installable_dists): + self._installable_dists = installable_dists + + def __call__(self, req): + return next(iter(filter(lambda dist: dist in req, + self._installable_dists)), None) + + +def parametrize_test_working_set_resolve(*test_list): + idlist = [] + argvalues = [] + for test in test_list: + ( + name, + installed_dists, + installable_dists, + requirements, + expected1, expected2 + ) = [ + strip_comments(s.lstrip()) for s in + textwrap.dedent(test).lstrip().split('\n\n', 5) + ] + installed_dists = list(parse_distributions(installed_dists)) + installable_dists = list(parse_distributions(installable_dists)) + requirements = list(pkg_resources.parse_requirements(requirements)) + for id_, replace_conflicting, expected in ( + (name, False, expected1), + (name + '_replace_conflicting', True, expected2), + ): + idlist.append(id_) + expected = strip_comments(expected.strip()) + if re.match('\w+$', expected): + expected = getattr(pkg_resources, expected) + assert issubclass(expected, Exception) + else: + expected = list(parse_distributions(expected)) + argvalues.append(pytest.param(installed_dists, installable_dists, + requirements, replace_conflicting, + expected)) + return pytest.mark.parametrize('installed_dists,installable_dists,' + 'requirements,replace_conflicting,' + 'resolved_dists_or_exception', + argvalues, ids=idlist) + + +@parametrize_test_working_set_resolve( + ''' + # id + noop + + # installed + + # installable + + # wanted + + # resolved + + # resolved [replace conflicting] + ''', + + ''' + # id + already_installed + + # installed + foo-3.0 + + # installable + + # wanted + foo>=2.1,!=3.1,<4 + + # resolved + foo-3.0 + + # resolved [replace conflicting] + foo-3.0 + ''', + + ''' + # id + installable_not_installed + + # installed + + # installable + foo-3.0 + foo-4.0 + + # wanted + foo>=2.1,!=3.1,<4 + + # resolved + foo-3.0 + + # resolved [replace conflicting] + foo-3.0 + ''', + + ''' + # id + not_installable + + # installed + + # installable + + # wanted + foo>=2.1,!=3.1,<4 + + # resolved + DistributionNotFound + + # resolved [replace conflicting] + DistributionNotFound + ''', + + ''' + # id + no_matching_version + + # installed + + # installable + foo-3.1 + + # wanted + foo>=2.1,!=3.1,<4 + + # resolved + DistributionNotFound + + # resolved [replace conflicting] + DistributionNotFound + ''', + + ''' + # id + installable_with_installed_conflict + + # installed + foo-3.1 + + # installable + foo-3.5 + + # wanted + foo>=2.1,!=3.1,<4 + + # resolved + VersionConflict + + # resolved [replace conflicting] + foo-3.5 + ''', + + ''' + # id + not_installable_with_installed_conflict + + # installed + foo-3.1 + + # installable + + # wanted + foo>=2.1,!=3.1,<4 + + # resolved + VersionConflict + + # resolved [replace conflicting] + DistributionNotFound + ''', + + ''' + # id + installed_with_installed_require + + # installed + foo-3.9 + baz-0.1 + foo>=2.1,!=3.1,<4 + + # installable + + # wanted + baz + + # resolved + foo-3.9 + baz-0.1 + + # resolved [replace conflicting] + foo-3.9 + baz-0.1 + ''', + + ''' + # id + installed_with_conflicting_installed_require + + # installed + foo-5 + baz-0.1 + foo>=2.1,!=3.1,<4 + + # installable + + # wanted + baz + + # resolved + VersionConflict + + # resolved [replace conflicting] + DistributionNotFound + ''', + + ''' + # id + installed_with_installable_conflicting_require + + # installed + foo-5 + baz-0.1 + foo>=2.1,!=3.1,<4 + + # installable + foo-2.9 + + # wanted + baz + + # resolved + VersionConflict + + # resolved [replace conflicting] + baz-0.1 + foo-2.9 + ''', + + ''' + # id + installed_with_installable_require + + # installed + baz-0.1 + foo>=2.1,!=3.1,<4 + + # installable + foo-3.9 + + # wanted + baz + + # resolved + foo-3.9 + baz-0.1 + + # resolved [replace conflicting] + foo-3.9 + baz-0.1 + ''', + + ''' + # id + installable_with_installed_require + + # installed + foo-3.9 + + # installable + baz-0.1 + foo>=2.1,!=3.1,<4 + + # wanted + baz + + # resolved + foo-3.9 + baz-0.1 + + # resolved [replace conflicting] + foo-3.9 + baz-0.1 + ''', + + ''' + # id + installable_with_installable_require + + # installed + + # installable + foo-3.9 + baz-0.1 + foo>=2.1,!=3.1,<4 + + # wanted + baz + + # resolved + foo-3.9 + baz-0.1 + + # resolved [replace conflicting] + foo-3.9 + baz-0.1 + ''', + + ''' + # id + installable_with_conflicting_installable_require + + # installed + foo-5 + + # installable + foo-2.9 + baz-0.1 + foo>=2.1,!=3.1,<4 + + # wanted + baz + + # resolved + VersionConflict + + # resolved [replace conflicting] + baz-0.1 + foo-2.9 + ''', + + ''' + # id + conflicting_installables + + # installed + + # installable + foo-2.9 + foo-5.0 + + # wanted + foo>=2.1,!=3.1,<4 + foo>=4 + + # resolved + VersionConflict + + # resolved [replace conflicting] + VersionConflict + ''', + + ''' + # id + installables_with_conflicting_requires + + # installed + + # installable + foo-2.9 + dep==1.0 + baz-5.0 + dep==2.0 + dep-1.0 + dep-2.0 + + # wanted + foo + baz + + # resolved + VersionConflict + + # resolved [replace conflicting] + VersionConflict + ''', + + ''' + # id + installables_with_conflicting_nested_requires + + # installed + + # installable + foo-2.9 + dep1 + dep1-1.0 + subdep<1.0 + baz-5.0 + dep2 + dep2-1.0 + subdep>1.0 + subdep-0.9 + subdep-1.1 + + # wanted + foo + baz + + # resolved + VersionConflict + + # resolved [replace conflicting] + VersionConflict + ''', +) +def test_working_set_resolve(installed_dists, installable_dists, requirements, + replace_conflicting, resolved_dists_or_exception): + ws = pkg_resources.WorkingSet([]) + list(map(ws.add, installed_dists)) + resolve_call = lambda: ws.resolve( + requirements, installer=FakeInstaller(installable_dists), + replace_conflicting=replace_conflicting, + ) + if inspect.isclass(resolved_dists_or_exception): + with pytest.raises(resolved_dists_or_exception): + resolve_call() + else: + assert sorted(resolve_call()) == sorted(resolved_dists_or_exception) @@ -1,5 +1,5 @@ [pytest] -addopts=--doctest-modules --ignore release.py --ignore setuptools/lib2to3_ex.py --ignore tests/manual_test.py --ignore tests/test_pypi.py --ignore tests/shlib_test --doctest-glob=pkg_resources/api_tests.txt --ignore scripts/upload-old-releases-as-zip.py --ignore pavement.py +addopts=--doctest-modules --ignore release.py --ignore setuptools/lib2to3_ex.py --ignore tests/manual_test.py --ignore tests/test_pypi.py --ignore tests/shlib_test --doctest-glob=pkg_resources/api_tests.txt --ignore scripts/upload-old-releases-as-zip.py --ignore pavement.py --ignore setuptools/tests/mod_with_constant.py -rsxX norecursedirs=dist build *.egg setuptools/extern pkg_resources/extern .* flake8-ignore = setuptools/site-patch.py F821 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 30.3.0 +current_version = 36.4.0 commit = True tag = True @@ -8,19 +8,18 @@ tag_build = .post tag_date = 1 [aliases] -clean_egg_info = egg_info -RDb '' +clean_egg_info = egg_info -Db '' release = clean_egg_info sdist bdist_wheel source = register sdist binary binary = bdist_egg upload --show-response -test = pytest [upload] repository = https://upload.pypi.org/legacy/ [sdist] -formats = gztar zip +formats = zip -[wheel] +[bdist_wheel] universal = 1 [bumpversion:file:setup.py] @@ -15,8 +15,12 @@ here = os.path.dirname(__file__) def require_metadata(): "Prevent improper installs without necessary metadata. See #659" - if not os.path.exists('setuptools.egg-info'): - msg = "Cannot build setuptools without metadata. Run bootstrap.py" + egg_info_dir = os.path.join(here, 'setuptools.egg-info') + if not os.path.exists(egg_info_dir): + msg = ( + "Cannot build setuptools without metadata. " + "Run `bootstrap.py`." + ) raise RuntimeError(msg) @@ -85,12 +89,13 @@ def pypi_link(pkg_filename): setup_params = dict( name="setuptools", - version="30.3.0", + version="36.4.0", description="Easily download, build, install, upgrade, and uninstall " "Python packages", author="Python Packaging Authority", author_email="distutils-sig@python.org", long_description=long_description, + long_description_content_type='text/x-rst; charset=UTF-8', keywords="CPAN PyPI distutils eggs package management", url="https://github.com/pypa/setuptools", src_root=None, @@ -145,11 +150,13 @@ setup_params = dict( Intended Audience :: Developers License :: OSI Approved :: MIT License Operating System :: OS Independent + Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 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 54577ced..04f76740 100644 --- a/setuptools/__init__.py +++ b/setuptools/__init__.py @@ -7,7 +7,7 @@ import distutils.filelist from distutils.util import convert_path from fnmatch import fnmatchcase -from setuptools.extern.six.moves import filter, filterfalse, map +from setuptools.extern.six.moves import filter, map import setuptools.version from setuptools.extension import Extension diff --git a/setuptools/command/__init__.py b/setuptools/command/__init__.py index efbe9411..c96d33c2 100644 --- a/setuptools/command/__init__.py +++ b/setuptools/command/__init__.py @@ -2,7 +2,7 @@ __all__ = [ 'alias', 'bdist_egg', 'bdist_rpm', 'build_ext', 'build_py', 'develop', 'easy_install', 'egg_info', 'install', 'install_lib', 'rotate', 'saveopts', 'sdist', 'setopt', 'test', 'install_egg_info', 'install_scripts', - 'register', 'bdist_wininst', 'upload_docs', 'upload', + 'register', 'bdist_wininst', 'upload_docs', 'upload', 'build_clib', ] from distutils.command.bdist import bdist diff --git a/setuptools/command/bdist_egg.py b/setuptools/command/bdist_egg.py index 8cd9dfef..51755d52 100644 --- a/setuptools/command/bdist_egg.py +++ b/setuptools/command/bdist_egg.py @@ -38,6 +38,14 @@ def strip_module(filename): filename = filename[:-6] return filename +def sorted_walk(dir): + """Do os.walk in a reproducible way, + independent of indeterministic filesystem readdir order + """ + for base, dirs, files in os.walk(dir): + dirs.sort() + files.sort() + yield base, dirs, files def write_stub(resource, pyfile): _stub_template = textwrap.dedent(""" @@ -302,7 +310,7 @@ class bdist_egg(Command): ext_outputs = [] paths = {self.bdist_dir: ''} - for base, dirs, files in os.walk(self.bdist_dir): + for base, dirs, files in sorted_walk(self.bdist_dir): for filename in files: if os.path.splitext(filename)[1].lower() in NATIVE_EXTENSIONS: all_outputs.append(paths[base] + filename) @@ -329,7 +337,7 @@ NATIVE_EXTENSIONS = dict.fromkeys('.dll .so .dylib .pyd'.split()) def walk_egg(egg_dir): """Walk an unpacked egg's contents, skipping the metadata directory""" - walker = os.walk(egg_dir) + walker = sorted_walk(egg_dir) base, dirs, files = next(walker) if 'EGG-INFO' in dirs: dirs.remove('EGG-INFO') @@ -463,10 +471,10 @@ def make_zipfile(zip_filename, base_dir, verbose=0, dry_run=0, compress=True, compression = zipfile.ZIP_DEFLATED if compress else zipfile.ZIP_STORED if not dry_run: z = zipfile.ZipFile(zip_filename, mode, compression=compression) - for dirname, dirs, files in os.walk(base_dir): + for dirname, dirs, files in sorted_walk(base_dir): visit(z, dirname, files) z.close() else: - for dirname, dirs, files in os.walk(base_dir): + for dirname, dirs, files in sorted_walk(base_dir): visit(None, dirname, files) return zip_filename diff --git a/setuptools/command/build_clib.py b/setuptools/command/build_clib.py new file mode 100644 index 00000000..09caff6f --- /dev/null +++ b/setuptools/command/build_clib.py @@ -0,0 +1,98 @@ +import distutils.command.build_clib as orig +from distutils.errors import DistutilsSetupError +from distutils import log +from setuptools.dep_util import newer_pairwise_group + + +class build_clib(orig.build_clib): + """ + Override the default build_clib behaviour to do the following: + + 1. Implement a rudimentary timestamp-based dependency system + so 'compile()' doesn't run every time. + 2. Add more keys to the 'build_info' dictionary: + * obj_deps - specify dependencies for each object compiled. + this should be a dictionary mapping a key + with the source filename to a list of + dependencies. Use an empty string for global + dependencies. + * cflags - specify a list of additional flags to pass to + the compiler. + """ + + def build_libraries(self, libraries): + for (lib_name, build_info) in libraries: + sources = build_info.get('sources') + if sources is None or not isinstance(sources, (list, tuple)): + raise DistutilsSetupError( + "in 'libraries' option (library '%s'), " + "'sources' must be present and must be " + "a list of source filenames" % lib_name) + sources = list(sources) + + log.info("building '%s' library", lib_name) + + # Make sure everything is the correct type. + # obj_deps should be a dictionary of keys as sources + # and a list/tuple of files that are its dependencies. + obj_deps = build_info.get('obj_deps', dict()) + if not isinstance(obj_deps, dict): + raise DistutilsSetupError( + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name) + dependencies = [] + + # Get the global dependencies that are specified by the '' key. + # These will go into every source's dependency list. + global_deps = obj_deps.get('', list()) + if not isinstance(global_deps, (list, tuple)): + raise DistutilsSetupError( + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name) + + # Build the list to be used by newer_pairwise_group + # each source will be auto-added to its dependencies. + for source in sources: + src_deps = [source] + src_deps.extend(global_deps) + extra_deps = obj_deps.get(source, list()) + if not isinstance(extra_deps, (list, tuple)): + raise DistutilsSetupError( + "in 'libraries' option (library '%s'), " + "'obj_deps' must be a dictionary of " + "type 'source: list'" % lib_name) + src_deps.extend(extra_deps) + dependencies.append(src_deps) + + expected_objects = self.compiler.object_filenames( + sources, + output_dir=self.build_temp + ) + + if newer_pairwise_group(dependencies, expected_objects) != ([], []): + # First, compile the source code to object files in the library + # directory. (This should probably change to putting object + # files in a temporary build directory.) + macros = build_info.get('macros') + include_dirs = build_info.get('include_dirs') + cflags = build_info.get('cflags') + objects = self.compiler.compile( + sources, + output_dir=self.build_temp, + macros=macros, + include_dirs=include_dirs, + extra_postargs=cflags, + debug=self.debug + ) + + # Now "link" the object files together into a static library. + # (On Unix at least, this isn't really linking -- it just + # builds an archive. Whatever.) + self.compiler.create_static_lib( + expected_objects, + lib_name, + output_dir=self.build_clib, + debug=self.debug + ) diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py index 3eb86120..85b23c60 100755 --- a/setuptools/command/develop.py +++ b/setuptools/command/develop.py @@ -9,10 +9,11 @@ from setuptools.extern import six from pkg_resources import Distribution, PathMetadata, normalize_path from setuptools.command.easy_install import easy_install +from setuptools import namespaces import setuptools -class develop(easy_install): +class develop(namespaces.DevelopInstaller, easy_install): """Set up package for development""" description = "install package in 'development mode'" @@ -30,6 +31,7 @@ class develop(easy_install): if self.uninstall: self.multi_version = True self.uninstall_link() + self.uninstall_namespaces() else: self.install_for_development() self.warn_deprecated_options() @@ -77,15 +79,28 @@ class develop(easy_install): project_name=ei.egg_name ) - p = self.egg_base.replace(os.sep, '/') - if p != os.curdir: - p = '../' * (p.count('/') + 1) - self.setup_path = p - p = normalize_path(os.path.join(self.install_dir, self.egg_path, p)) - if p != normalize_path(os.curdir): + self.setup_path = self._resolve_setup_path( + self.egg_base, + self.install_dir, + self.egg_path, + ) + + @staticmethod + def _resolve_setup_path(egg_base, install_dir, egg_path): + """ + Generate a path from egg_base back to '.' where the + setup script resides and ensure that path points to the + setup path from $install_dir/$egg_path. + """ + path_to_setup = egg_base.replace(os.sep, '/').rstrip('/') + if path_to_setup != os.curdir: + path_to_setup = '../' * (path_to_setup.count('/') + 1) + resolved = normalize_path(os.path.join(install_dir, egg_path, path_to_setup)) + if resolved != normalize_path(os.curdir): raise DistutilsOptionError( "Can't get a consistent path to setup script from" - " installation directory", p, normalize_path(os.curdir)) + " installation directory", resolved, normalize_path(os.curdir)) + return path_to_setup def install_for_development(self): if six.PY3 and getattr(self.distribution, 'use_2to3', False): @@ -123,6 +138,8 @@ class develop(easy_install): self.easy_install(setuptools.bootstrap_install_from) setuptools.bootstrap_install_from = None + self.install_namespaces() + # create an .egg-link in the installation dir, pointing to our egg log.info("Creating %s (link to %s)", self.egg_link, self.egg_base) if not self.dry_run: diff --git a/setuptools/command/easy_install.py b/setuptools/command/easy_install.py index 03dd6768..8fba7b41 100755 --- a/setuptools/command/easy_install.py +++ b/setuptools/command/easy_install.py @@ -46,6 +46,7 @@ from setuptools.extern.six.moves import configparser, map from setuptools import Command from setuptools.sandbox import run_setup from setuptools.py31compat import get_path, get_config_vars +from setuptools.py27compat import rmtree_safe from setuptools.command import setopt from setuptools.archive_util import unpack_archive from setuptools.package_index import ( @@ -58,7 +59,7 @@ from pkg_resources import ( Distribution, PathMetadata, EggMetadata, WorkingSet, DistributionNotFound, VersionConflict, DEVELOP_DIST, ) -import pkg_resources +import pkg_resources.py31compat # Turn on PEP440Warnings warnings.filterwarnings("default", category=pkg_resources.PEP440Warning) @@ -473,8 +474,7 @@ class easy_install(Command): else: self.pth_file = None - PYTHONPATH = os.environ.get('PYTHONPATH', '').split(os.pathsep) - if instdir not in map(normalize_path, filter(None, PYTHONPATH)): + if instdir not in map(normalize_path, _pythonpath()): # only PYTHONPATH dirs need a site.py, so pretend it's there self.sitepy_installed = True elif self.multi_version and not os.path.exists(pth_file): @@ -544,8 +544,7 @@ class easy_install(Command): if ok_exists: os.unlink(ok_file) dirname = os.path.dirname(ok_file) - if not os.path.exists(dirname): - os.makedirs(dirname) + pkg_resources.py31compat.makedirs(dirname, exist_ok=True) f = open(pth_file, 'w') except (OSError, IOError): self.cant_write_to_target() @@ -627,12 +626,20 @@ class easy_install(Command): (spec.key, self.build_directory) ) + @contextlib.contextmanager + def _tmpdir(self): + tmpdir = tempfile.mkdtemp(prefix=six.u("easy_install-")) + try: + # cast to str as workaround for #709 and #710 and #712 + yield str(tmpdir) + finally: + os.path.exists(tmpdir) and rmtree(rmtree_safe(tmpdir)) + def easy_install(self, spec, deps=False): - tmpdir = tempfile.mkdtemp(prefix="easy_install-") if not self.editable: self.install_site_py() - try: + with self._tmpdir() as tmpdir: if not isinstance(spec, Requirement): if URL_SCHEME(spec): # It's a url, download it to tmpdir and process @@ -664,10 +671,6 @@ class easy_install(Command): else: return self.install_item(spec, dist.location, tmpdir, deps) - finally: - if os.path.exists(tmpdir): - rmtree(tmpdir) - def install_item(self, spec, download, tmpdir, deps, install_needed=False): # Installation is also needed if file in tmpdir or is not an egg @@ -1343,10 +1346,21 @@ class easy_install(Command): setattr(self, attr, val) +def _pythonpath(): + items = os.environ.get('PYTHONPATH', '').split(os.pathsep) + return filter(None, items) + + def get_site_dirs(): - # return a list of 'site' dirs - sitedirs = [_f for _f in os.environ.get('PYTHONPATH', - '').split(os.pathsep) if _f] + """ + Return a list of 'site' dirs + """ + + sitedirs = [] + + # start with PYTHONPATH + sitedirs.extend(_pythonpath()) + prefixes = [sys.prefix] if sys.exec_prefix != sys.prefix: prefixes.append(sys.exec_prefix) @@ -1670,7 +1684,7 @@ def _first_line_re(): def auto_chmod(func, arg, exc): - if func is os.remove and os.name == 'nt': + if func in [os.unlink, os.remove] and os.name == 'nt': chmod(arg, stat.S_IWRITE) return func(arg) et, ev, _ = sys.exc_info() @@ -2008,7 +2022,7 @@ class ScriptWriter(object): gui apps. """ - template = textwrap.dedent(""" + template = textwrap.dedent(r""" # EASY-INSTALL-ENTRY-SCRIPT: %(spec)r,%(group)r,%(name)r __requires__ = %(spec)r import re diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 40cea9bf..a1d41b27 100755 --- a/setuptools/command/egg_info.py +++ b/setuptools/command/egg_info.py @@ -32,11 +32,6 @@ from setuptools.glob import glob from pkg_resources.extern import packaging -try: - from setuptools_svn import svn_utils -except ImportError: - pass - def translate_pattern(glob): """ @@ -117,7 +112,8 @@ def translate_pattern(glob): if not last_chunk: pat += sep - return re.compile(pat + r'\Z(?ms)') + pat += r'\Z' + return re.compile(pat, flags=re.MULTILINE|re.DOTALL) class egg_info(Command): @@ -126,18 +122,13 @@ class egg_info(Command): user_options = [ ('egg-base=', 'e', "directory containing .egg-info directories" " (default: top of the source tree)"), - ('tag-svn-revision', 'r', - "Add subversion revision ID to version number"), ('tag-date', 'd', "Add date stamp (e.g. 20050528) to version number"), ('tag-build=', 'b', "Specify explicit tag to add to version number"), - ('no-svn-revision', 'R', - "Don't add subversion revision ID [default]"), ('no-date', 'D', "Don't include date stamp [default]"), ] - boolean_options = ['tag-date', 'tag-svn-revision'] + boolean_options = ['tag-date'] negative_opt = { - 'no-svn-revision': 'tag-svn-revision', 'no-date': 'tag-date', } @@ -147,15 +138,26 @@ class egg_info(Command): self.egg_base = None self.egg_info = None self.tag_build = None - self.tag_svn_revision = 0 self.tag_date = 0 self.broken_egg_info = False self.vtags = None + #################################### + # allow the 'tag_svn_revision' to be detected and + # set, supporting sdists built on older Setuptools. + @property + def tag_svn_revision(self): + pass + + @tag_svn_revision.setter + def tag_svn_revision(self, value): + pass + #################################### + def save_version_info(self, filename): """ - Materialize the values of svn_revision and date into the - build tag. Install these keys in a deterministic order + Materialize the value of date into the + build tag. Install build keys in a deterministic order to avoid arbitrary reordering on subsequent builds. """ egg_info = collections.OrderedDict() @@ -163,7 +165,6 @@ class egg_info(Command): # when PYTHONHASHSEED=0 egg_info['tag_build'] = self.tags() egg_info['tag_date'] = 0 - egg_info['tag_svn_revision'] = 0 edit_config(filename, dict(egg_info=egg_info)) def finalize_options(self): @@ -280,22 +281,10 @@ class egg_info(Command): version = '' if self.tag_build: version += self.tag_build - if self.tag_svn_revision: - warnings.warn( - "tag_svn_revision is deprecated and will not be honored " - "in a future release" - ) - version += '-r%s' % self.get_svn_revision() if self.tag_date: version += time.strftime("-%Y%m%d") return version - @staticmethod - def get_svn_revision(): - if 'svn_utils' not in globals(): - return "0" - return str(svn_utils.SvnInfo.load(os.curdir).get_revision()) - def find_sources(self): """Generate SOURCES.txt manifest file""" manifest_filename = os.path.join(self.egg_info, "SOURCES.txt") @@ -439,7 +428,11 @@ class FileList(_FileList): def graft(self, dir): """Include all files from 'dir/'.""" - found = distutils.filelist.findall(dir) + found = [ + item + for match_dir in glob(dir) + for item in distutils.filelist.findall(match_dir) + ] self.extend(found) return bool(found) @@ -455,7 +448,7 @@ class FileList(_FileList): """ if self.allfiles is None: self.findall() - match = translate_pattern(os.path.join('**', '*' + pattern)) + match = translate_pattern(os.path.join('**', pattern)) found = [f for f in self.allfiles if match.match(f)] self.extend(found) return bool(found) @@ -464,7 +457,7 @@ class FileList(_FileList): """ Exclude all files anywhere that match the pattern. """ - match = translate_pattern(os.path.join('**', '*' + pattern)) + match = translate_pattern(os.path.join('**', pattern)) return self._remove_files(match.match) def append(self, item): @@ -604,6 +597,10 @@ def write_pkg_info(cmd, basename, filename): metadata = cmd.distribution.metadata metadata.version, oldver = cmd.egg_version, metadata.version metadata.name, oldname = cmd.egg_name, metadata.name + metadata.long_description_content_type = getattr( + cmd.distribution, + 'long_description_content_type' + ) try: # write unescaped data to PKG-INFO, so older pkg_resources # can still parse it diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 39e29d73..bcfae4d8 100755 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -37,7 +37,8 @@ class sdist(sdist_add_defaults, orig.sdist): negative_opt = {} - READMES = 'README', 'README.rst', 'README.txt' + README_EXTENSIONS = ['', '.rst', '.txt', '.md'] + READMES = tuple('README{0}'.format(ext) for ext in README_EXTENSIONS) def run(self): self.run_command('egg_info') diff --git a/setuptools/command/test.py b/setuptools/command/test.py index 9931565b..f00d6794 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -4,14 +4,15 @@ import sys import contextlib import itertools import unittest -from distutils.errors import DistutilsOptionError +from distutils.errors import DistutilsError, DistutilsOptionError +from distutils import log from unittest import TestLoader from setuptools.extern import six from setuptools.extern.six.moves import map, filter from pkg_resources import (resource_listdir, resource_exists, normalize_path, - working_set, _namespace_packages, + working_set, _namespace_packages, evaluate_marker, add_activation_listener, require, EntryPoint) from setuptools import Command @@ -66,7 +67,7 @@ class test(Command): user_options = [ ('test-module=', 'm', "Run 'test_suite' in specified module"), ('test-suite=', 's', - "Test suite to run (e.g. 'some_module.test_suite')"), + "Run single test, case or suite (e.g. 'module.test_suite')"), ('test-runner=', 'r', "Test runner to use"), ] @@ -190,9 +191,13 @@ class test(Command): Install the requirements indicated by self.distribution and return an iterable of the dists that were built. """ - ir_d = dist.fetch_build_eggs(dist.install_requires or []) + ir_d = dist.fetch_build_eggs(dist.install_requires) tr_d = dist.fetch_build_eggs(dist.tests_require or []) - return itertools.chain(ir_d, tr_d) + er_d = dist.fetch_build_eggs( + v for k, v in dist.extras_require.items() + if k.startswith(':') and evaluate_marker(k[1:]) + ) + return itertools.chain(ir_d, tr_d, er_d) def run(self): installed_dists = self.install_dists(self.distribution) @@ -225,12 +230,16 @@ class test(Command): del_modules.append(name) list(map(sys.modules.__delitem__, del_modules)) - unittest.main( + test = unittest.main( None, None, self._argv, testLoader=self._resolve_as_ep(self.test_loader), testRunner=self._resolve_as_ep(self.test_runner), exit=False, ) + if not test.result.wasSuccessful(): + msg = 'Test failed: %s' % test.result + self.announce(msg, log.ERROR) + raise DistutilsError(msg) @property def _argv(self): diff --git a/setuptools/command/upload.py b/setuptools/command/upload.py index 484baa5a..a44173a9 100644 --- a/setuptools/command/upload.py +++ b/setuptools/command/upload.py @@ -10,6 +10,10 @@ class upload(orig.upload): def finalize_options(self): orig.upload.finalize_options(self) + self.username = ( + self.username or + getpass.getuser() + ) # Attempt to obtain password. Short circuit evaluation at the first # sign of success. self.password = ( diff --git a/setuptools/command/upload_docs.py b/setuptools/command/upload_docs.py index 269dc2d5..07aa564a 100644 --- a/setuptools/command/upload_docs.py +++ b/setuptools/command/upload_docs.py @@ -57,7 +57,6 @@ class upload_docs(upload): self.target_dir = None def finalize_options(self): - log.warn("Upload_docs command is deprecated. Use RTD instead.") upload.finalize_options(self) if self.upload_dir is None: if self.has_sphinx(): @@ -69,6 +68,8 @@ class upload_docs(upload): else: self.ensure_dirname('upload_dir') self.target_dir = self.upload_dir + if 'pypi.python.org' in self.repository: + log.warn("Upload_docs command is deprecated. Use RTD instead.") self.announce('Using upload directory %s' % self.target_dir) def create_zipfile(self, filename): @@ -77,9 +78,8 @@ class upload_docs(upload): self.mkpath(self.target_dir) # just in case for root, dirs, files in os.walk(self.target_dir): if root == self.target_dir and not files: - raise DistutilsOptionError( - "no files found in upload directory '%s'" - % self.target_dir) + tmpl = "no files found in upload directory '%s'" + raise DistutilsOptionError(tmpl % self.target_dir) for name in files: full = os.path.join(root, name) relative = root[len(self.target_dir):].lstrip(os.path.sep) @@ -138,7 +138,7 @@ class upload_docs(upload): part_groups = map(builder, data.items()) parts = itertools.chain.from_iterable(part_groups) body_items = itertools.chain(parts, end_items) - content_type = 'multipart/form-data; boundary=%s' % boundary + content_type = 'multipart/form-data; boundary=%s' % boundary.decode('ascii') return b''.join(body_items), content_type def upload_file(self, filename): @@ -159,8 +159,8 @@ class upload_docs(upload): body, ct = self._build_multipart(data) - self.announce("Submitting documentation to %s" % (self.repository), - log.INFO) + msg = "Submitting documentation to %s" % (self.repository) + self.announce(msg, log.INFO) # build the Request # We can't use urllib2 since we need to send the Basic @@ -191,16 +191,16 @@ class upload_docs(upload): r = conn.getresponse() if r.status == 200: - self.announce('Server response (%s): %s' % (r.status, r.reason), - log.INFO) + msg = 'Server response (%s): %s' % (r.status, r.reason) + self.announce(msg, log.INFO) elif r.status == 301: location = r.getheader('Location') if location is None: location = 'https://pythonhosted.org/%s/' % meta.get_name() - self.announce('Upload successful. Visit %s' % location, - log.INFO) + msg = 'Upload successful. Visit %s' % location + self.announce(msg, log.INFO) else: - self.announce('Upload failed (%s): %s' % (r.status, r.reason), - log.ERROR) + msg = 'Upload failed (%s): %s' % (r.status, r.reason) + self.announce(msg, log.ERROR) if self.show_response: print('-' * 75, r.read(), '-' * 75) diff --git a/setuptools/config.py b/setuptools/config.py index eb19c895..53828447 100644 --- a/setuptools/config.py +++ b/setuptools/config.py @@ -10,7 +10,8 @@ from distutils.errors import DistutilsOptionError, DistutilsFileError from setuptools.extern.six import string_types -def read_configuration(filepath, find_others=False): +def read_configuration( + filepath, find_others=False, ignore_option_errors=False): """Read given configuration file and returns options from it as a dict. :param str|unicode filepath: Path to configuration file @@ -19,6 +20,11 @@ def read_configuration(filepath, find_others=False): :param bool find_others: Whether to search for other configuration files which could be on in various places. + :param bool ignore_option_errors: Whether to silently ignore + options, values of which could not be resolved (e.g. due to exceptions + in directives such as file:, attr:, etc.). + If False exceptions are propagated as expected. + :rtype: dict """ from setuptools.dist import Distribution, _Distribution @@ -32,17 +38,21 @@ def read_configuration(filepath, find_others=False): current_directory = os.getcwd() os.chdir(os.path.dirname(filepath)) - dist = Distribution() + try: + dist = Distribution() - filenames = dist.find_config_files() if find_others else [] - if filepath not in filenames: - filenames.append(filepath) + filenames = dist.find_config_files() if find_others else [] + if filepath not in filenames: + filenames.append(filepath) - _Distribution.parse_config_files(dist, filenames=filenames) + _Distribution.parse_config_files(dist, filenames=filenames) - handlers = parse_configuration(dist, dist.command_options) + handlers = parse_configuration( + dist, dist.command_options, + ignore_option_errors=ignore_option_errors) - os.chdir(current_directory) + finally: + os.chdir(current_directory) return configuration_to_dict(handlers) @@ -76,7 +86,8 @@ def configuration_to_dict(handlers): return config_dict -def parse_configuration(distribution, command_options): +def parse_configuration( + distribution, command_options, ignore_option_errors=False): """Performs additional parsing of configuration options for a distribution. @@ -84,12 +95,18 @@ def parse_configuration(distribution, command_options): :param Distribution distribution: :param dict command_options: + :param bool ignore_option_errors: Whether to silently ignore + options, values of which could not be resolved (e.g. due to exceptions + in directives such as file:, attr:, etc.). + If False exceptions are propagated as expected. :rtype: list """ - meta = ConfigMetadataHandler(distribution.metadata, command_options) + meta = ConfigMetadataHandler( + distribution.metadata, command_options, ignore_option_errors) meta.parse() - options = ConfigOptionsHandler(distribution, command_options) + options = ConfigOptionsHandler( + distribution, command_options, ignore_option_errors) options.parse() return [meta, options] @@ -111,7 +128,7 @@ class ConfigHandler(object): """ - def __init__(self, target_obj, options): + def __init__(self, target_obj, options, ignore_option_errors=False): sections = {} section_prefix = self.section_prefix @@ -122,6 +139,7 @@ class ConfigHandler(object): section_name = section_name.replace(section_prefix, '').strip('.') sections[section_name] = section_options + self.ignore_option_errors = ignore_option_errors self.target_obj = target_obj self.sections = sections self.set_options = [] @@ -148,9 +166,19 @@ class ConfigHandler(object): # Already inhabited. Skipping. return + skip_option = False parser = self.parsers.get(option_name) if parser: - value = parser(value) + try: + value = parser(value) + + except Exception: + skip_option = True + if not self.ignore_option_errors: + raise + + if skip_option: + return setter = getattr(target_obj, 'set_%s' % option_name, None) if setter is None: @@ -217,33 +245,39 @@ class ConfigHandler(object): directory with setup.py. Examples: - include: LICENSE - include: src/file.txt + file: LICENSE + file: README.rst, CHANGELOG.md, src/file.txt :param str value: :rtype: str """ + include_directive = 'file:' + if not isinstance(value, string_types): return value - include_directive = 'file:' if not value.startswith(include_directive): return value - current_directory = os.getcwd() - - filepath = value.replace(include_directive, '').strip() - filepath = os.path.abspath(filepath) - - if not filepath.startswith(current_directory): + spec = value[len(include_directive):] + filepaths = (os.path.abspath(path.strip()) for path in spec.split(',')) + return '\n'.join( + cls._read_file(path) + for path in filepaths + if (cls._assert_local(path) or True) + and os.path.isfile(path) + ) + + @staticmethod + def _assert_local(filepath): + if not filepath.startswith(os.getcwd()): raise DistutilsOptionError( '`file:` directive can not access %s' % filepath) - if os.path.isfile(filepath): - with io.open(filepath, encoding='utf-8') as f: - value = f.read() - - return value + @staticmethod + def _read_file(filepath): + with io.open(filepath, encoding='utf-8') as f: + return f.read() @classmethod def _parse_attr(cls, value): @@ -335,7 +369,10 @@ class ConfigHandler(object): method_postfix = '_%s' % section_name section_parser_method = getattr( - self, 'parse_section%s' % method_postfix, None) + self, + # Dots in section names are tranlsated into dunderscores. + ('parse_section%s' % method_postfix).replace('.', '__'), + None) if section_parser_method is None: raise DistutilsOptionError( @@ -381,17 +418,6 @@ class ConfigMetadataHandler(ConfigHandler): 'version': self._parse_version, } - def parse_section_classifiers(self, section_options): - """Parses configuration file section. - - :param dict section_options: - """ - classifiers = [] - for begin, (_, rest) in section_options.items(): - classifiers.append('%s :%s' % (begin.title(), rest)) - - self['classifiers'] = classifiers - def _parse_version(self, value): """Parses `version` option value. @@ -442,6 +468,7 @@ class ConfigOptionsHandler(ConfigHandler): 'tests_require': parse_list_semicolon, 'packages': self._parse_packages, 'entry_points': self._parse_file, + 'py_modules': parse_list, } def _parse_packages(self, value): @@ -455,8 +482,34 @@ class ConfigOptionsHandler(ConfigHandler): if not value.startswith(find_directive): return self._parse_list(value) + # Read function arguments from a dedicated section. + find_kwargs = self.parse_section_packages__find( + self.sections.get('packages.find', {})) + from setuptools import find_packages - return find_packages() + + return find_packages(**find_kwargs) + + def parse_section_packages__find(self, section_options): + """Parses `packages.find` configuration file section. + + To be used in conjunction with _parse_packages(). + + :param dict section_options: + """ + section_data = self._parse_section_to_dict( + section_options, self._parse_list) + + valid_keys = ['where', 'include', 'exclude'] + + find_kwargs = dict( + [(k, v) for k, v in section_data.items() if k in valid_keys and v]) + + where = find_kwargs.get('where') + if where is not None: + find_kwargs['where'] = where[0] # cast list to single val + + return find_kwargs def parse_section_entry_points(self, section_options): """Parses `entry_points` configuration file section. diff --git a/setuptools/dep_util.py b/setuptools/dep_util.py new file mode 100644 index 00000000..2931c13e --- /dev/null +++ b/setuptools/dep_util.py @@ -0,0 +1,23 @@ +from distutils.dep_util import newer_group + +# yes, this is was almost entirely copy-pasted from +# 'newer_pairwise()', this is just another convenience +# function. +def newer_pairwise_group(sources_groups, targets): + """Walk both arguments in parallel, testing if each source group is newer + than its corresponding target. Returns a pair of lists (sources_groups, + targets) where sources is newer than target, according to the semantics + of 'newer_group()'. + """ + if len(sources_groups) != len(targets): + raise ValueError("'sources_group' and 'targets' must be the same length") + + # build a pair of lists (sources_groups, targets) where source is newer + n_sources = [] + n_targets = [] + for i in range(len(sources_groups)): + if newer_group(sources_groups[i], targets[i]): + n_sources.append(sources_groups[i]) + n_targets.append(targets[i]) + + return n_sources, n_targets diff --git a/setuptools/depends.py b/setuptools/depends.py index 89d39a50..45e7052d 100644 --- a/setuptools/depends.py +++ b/setuptools/depends.py @@ -4,7 +4,8 @@ import marshal from distutils.version import StrictVersion from imp import PKG_DIRECTORY, PY_COMPILED, PY_SOURCE, PY_FROZEN -from setuptools.extern import six +from .py33compat import Bytecode + __all__ = [ 'Require', 'find_module', 'get_module_constant', 'extract_constant' @@ -78,39 +79,6 @@ class Require: return self.version_ok(version) -def _iter_code(code): - """Yield '(op,arg)' pair for each operation in code object 'code'""" - - from array import array - from dis import HAVE_ARGUMENT, EXTENDED_ARG - - bytes = array('b', code.co_code) - eof = len(code.co_code) - - ptr = 0 - extended_arg = 0 - - while ptr < eof: - - op = bytes[ptr] - - if op >= HAVE_ARGUMENT: - - arg = bytes[ptr + 1] + bytes[ptr + 2] * 256 + extended_arg - ptr += 3 - - if op == EXTENDED_ARG: - long_type = six.integer_types[-1] - extended_arg = arg * long_type(65536) - continue - - else: - arg = None - ptr += 1 - - yield op, arg - - def find_module(module, paths=None): """Just like 'imp.find_module()', but with package support""" @@ -176,9 +144,8 @@ def extract_constant(code, symbol, default=-1): only 'STORE_NAME' and 'STORE_GLOBAL' opcodes are checked, and 'symbol' must be present in 'code.co_names'. """ - if symbol not in code.co_names: - # name's not there, can't possibly be an assigment + # name's not there, can't possibly be an assignment return None name_idx = list(code.co_names).index(symbol) @@ -189,7 +156,9 @@ def extract_constant(code, symbol, default=-1): const = default - for op, arg in _iter_code(code): + for byte_code in Bytecode(code): + op = byte_code.opcode + arg = byte_code.arg if op == LOAD_CONST: const = code.co_consts[arg] diff --git a/setuptools/dist.py b/setuptools/dist.py index c04e6426..a2ca8795 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -8,12 +8,15 @@ import distutils.log import distutils.core import distutils.cmd import distutils.dist -from distutils.errors import (DistutilsOptionError, DistutilsPlatformError, - DistutilsSetupError) +import itertools +from collections import defaultdict +from distutils.errors import ( + DistutilsOptionError, DistutilsPlatformError, DistutilsSetupError, +) from distutils.util import rfc822_escape from setuptools.extern import six -from setuptools.extern.six.moves import map +from setuptools.extern.six.moves import map, filter, filterfalse from pkg_resources.extern import packaging from setuptools.depends import Require @@ -21,6 +24,10 @@ from setuptools import windows_support from setuptools.monkey import get_unpatched from setuptools.config import parse_configuration import pkg_resources +from .py36compat import Distribution_parse_config_files + +__import__('pkg_resources.extern.packaging.specifiers') +__import__('pkg_resources.extern.packaging.version') def _get_unpatched(cls): @@ -51,6 +58,13 @@ def write_pkg_file(self, file): if self.download_url: file.write('Download-URL: %s\n' % self.download_url) + long_desc_content_type = getattr( + self, + 'long_description_content_type', + None + ) or 'UNKNOWN' + file.write('Description-Content-Type: %s\n' % long_desc_content_type) + long_desc = rfc822_escape(self.get_long_description()) file.write('Description: %s\n' % long_desc) @@ -125,12 +139,7 @@ def check_nsp(dist, attr, value): def check_extras(dist, attr, value): """Verify that extras_require mapping is valid""" try: - for k, v in value.items(): - if ':' in k: - k, m = k.split(':', 1) - if pkg_resources.invalid_marker(m): - raise DistutilsSetupError("Invalid environment marker: " + m) - list(pkg_resources.parse_requirements(v)) + list(itertools.starmap(_check_extra, value.items())) except (TypeError, ValueError, AttributeError): raise DistutilsSetupError( "'extras_require' must be a dictionary whose values are " @@ -139,6 +148,13 @@ def check_extras(dist, attr, value): ) +def _check_extra(extra, reqs): + name, sep, marker = extra.partition(':') + if marker and pkg_resources.invalid_marker(marker): + raise DistutilsSetupError("Invalid environment marker: " + marker) + list(pkg_resources.parse_requirements(reqs)) + + def assert_bool(dist, attr, value): """Verify that value is True, False, 0, or 1""" if bool(value) != value: @@ -164,7 +180,7 @@ def check_specifier(dist, attr, value): packaging.specifiers.SpecifierSet(value) except packaging.specifiers.InvalidSpecifier as error: tmpl = ( - "{attr!r} must be a string or list of strings " + "{attr!r} must be a string " "containing valid version specifiers; {error}" ) raise DistutilsSetupError(tmpl.format(attr=attr, error=error)) @@ -213,7 +229,7 @@ def check_packages(dist, attr, value): _Distribution = get_unpatched(distutils.core.Distribution) -class Distribution(_Distribution): +class Distribution(Distribution_parse_config_files, _Distribution): """Distribution with support for features, tests, and package data This is an enhanced version of 'distutils.dist.Distribution' that @@ -308,6 +324,9 @@ class Distribution(_Distribution): self.dist_files = [] self.src_root = attrs and attrs.pop("src_root", None) self.patch_missing_pkg_info(attrs) + self.long_description_content_type = _attrs_dict.get( + 'long_description_content_type' + ) # Make sure we have any eggs needed to interpret 'attrs' if attrs is not None: self.dependency_links = attrs.pop('dependency_links', []) @@ -340,8 +359,73 @@ class Distribution(_Distribution): "setuptools, pip, and PyPI. Please see PEP 440 for more " "details." % self.metadata.version ) + self._finalize_requires() + + def _finalize_requires(self): + """ + Set `metadata.python_requires` and fix environment markers + in `install_requires` and `extras_require`. + """ if getattr(self, 'python_requires', None): self.metadata.python_requires = self.python_requires + self._convert_extras_requirements() + self._move_install_requirements_markers() + + def _convert_extras_requirements(self): + """ + Convert requirements in `extras_require` of the form + `"extra": ["barbazquux; {marker}"]` to + `"extra:{marker}": ["barbazquux"]`. + """ + spec_ext_reqs = getattr(self, 'extras_require', None) or {} + self._tmp_extras_require = defaultdict(list) + for section, v in spec_ext_reqs.items(): + # Do not strip empty sections. + self._tmp_extras_require[section] + for r in pkg_resources.parse_requirements(v): + suffix = self._suffix_for(r) + self._tmp_extras_require[section + suffix].append(r) + + @staticmethod + def _suffix_for(req): + """ + For a requirement, return the 'extras_require' suffix for + that requirement. + """ + return ':' + str(req.marker) if req.marker else '' + + def _move_install_requirements_markers(self): + """ + Move requirements in `install_requires` that are using environment + markers `extras_require`. + """ + + # divide the install_requires into two sets, simple ones still + # handled by install_requires and more complex ones handled + # by extras_require. + + def is_simple_req(req): + return not req.marker + + spec_inst_reqs = getattr(self, 'install_requires', None) or () + inst_reqs = list(pkg_resources.parse_requirements(spec_inst_reqs)) + simple_reqs = filter(is_simple_req, inst_reqs) + complex_reqs = filterfalse(is_simple_req, inst_reqs) + self.install_requires = list(map(str, simple_reqs)) + + for r in complex_reqs: + self._tmp_extras_require[':' + str(r.marker)].append(r) + self.extras_require = dict( + (k, [str(r) for r in map(self._clean_req, v)]) + for k, v in self._tmp_extras_require.items() + ) + + def _clean_req(self, req): + """ + Given a Requirement, remove environment markers and return it. + """ + req.marker = None + return req def parse_config_files(self, filenames=None): """Parses configuration files from various levels @@ -351,6 +435,7 @@ class Distribution(_Distribution): _Distribution.parse_config_files(self, filenames=filenames) parse_configuration(self, self.command_options) + self._finalize_requires() def parse_command_line(self): """Process features after parsing command line options""" @@ -386,7 +471,10 @@ class Distribution(_Distribution): ep.load()(self, ep.name, value) if getattr(self, 'convert_2to3_doctests', None): # XXX may convert to set here when we can rely on set being builtin - self.convert_2to3_doctests = [os.path.abspath(p) for p in self.convert_2to3_doctests] + self.convert_2to3_doctests = [ + os.path.abspath(p) + for p in self.convert_2to3_doctests + ] else: self.convert_2to3_doctests = [] @@ -407,35 +495,30 @@ class Distribution(_Distribution): def fetch_build_egg(self, req): """Fetch an egg needed for building""" - - try: - cmd = self._egg_fetcher - cmd.package_index.to_scan = [] - except AttributeError: - from setuptools.command.easy_install import easy_install - dist = self.__class__({'script_args': ['easy_install']}) - dist.parse_config_files() - opts = dist.get_option_dict('easy_install') - keep = ( - 'find_links', 'site_dirs', 'index_url', 'optimize', - 'site_dirs', 'allow_hosts' - ) - for key in list(opts): - if key not in keep: - del opts[key] # don't use any other settings - if self.dependency_links: - links = self.dependency_links[:] - if 'find_links' in opts: - links = opts['find_links'][1].split() + links - opts['find_links'] = ('setup', links) - install_dir = self.get_egg_cache_dir() - cmd = easy_install( - dist, args=["x"], install_dir=install_dir, exclude_scripts=True, - always_copy=False, build_directory=None, editable=False, - upgrade=False, multi_version=True, no_report=True, user=False - ) - cmd.ensure_finalized() - self._egg_fetcher = cmd + from setuptools.command.easy_install import easy_install + dist = self.__class__({'script_args': ['easy_install']}) + dist.parse_config_files() + opts = dist.get_option_dict('easy_install') + keep = ( + 'find_links', 'site_dirs', 'index_url', 'optimize', + 'site_dirs', 'allow_hosts' + ) + for key in list(opts): + if key not in keep: + del opts[key] # don't use any other settings + if self.dependency_links: + links = self.dependency_links[:] + if 'find_links' in opts: + links = opts['find_links'][1].split() + links + opts['find_links'] = ('setup', links) + install_dir = self.get_egg_cache_dir() + cmd = easy_install( + dist, args=["x"], install_dir=install_dir, + exclude_scripts=True, + always_copy=False, build_directory=None, editable=False, + upgrade=False, multi_version=True, no_report=True, user=False + ) + cmd.ensure_finalized() return cmd.easy_install(req) def _set_global_opts_from_features(self): @@ -455,8 +538,11 @@ class Distribution(_Distribution): if not feature.include_by_default(): excdef, incdef = incdef, excdef - go.append(('with-' + name, None, 'include ' + descr + incdef)) - go.append(('without-' + name, None, 'exclude ' + descr + excdef)) + new = ( + ('with-' + name, None, 'include ' + descr + incdef), + ('without-' + name, None, 'exclude ' + descr + excdef), + ) + go.extend(new) no['without-' + name] = 'with-' + name self.global_options = self.feature_options = go + self.global_options @@ -484,7 +570,8 @@ class Distribution(_Distribution): if command in self.cmdclass: return self.cmdclass[command] - for ep in pkg_resources.iter_entry_points('distutils.commands', command): + eps = pkg_resources.iter_entry_points('distutils.commands', command) + for ep in eps: ep.require(installer=self.fetch_build_egg) self.cmdclass[command] = cmdclass = ep.load() return cmdclass @@ -618,7 +705,8 @@ class Distribution(_Distribution): name + ": this setting cannot be changed via include/exclude" ) else: - setattr(self, name, old + [item for item in value if item not in old]) + new = [item for item in value if item not in old] + setattr(self, name, old + new) def exclude(self, **attrs): """Remove items from distribution that are named in keyword arguments @@ -829,14 +917,14 @@ class Feature: @staticmethod def warn_deprecated(): - warnings.warn( + msg = ( "Features are deprecated and will be removed in a future " - "version. See https://github.com/pypa/setuptools/issues/65.", - DeprecationWarning, - stacklevel=3, + "version. See https://github.com/pypa/setuptools/issues/65." ) + warnings.warn(msg, DeprecationWarning, stacklevel=3) - def __init__(self, description, standard=False, available=True, + def __init__( + self, description, standard=False, available=True, optional=True, require_features=(), remove=(), **extras): self.warn_deprecated() @@ -861,8 +949,8 @@ class Feature: if not remove and not require_features and not extras: raise DistutilsSetupError( - "Feature %s: must define 'require_features', 'remove', or at least one" - " of 'packages', 'py_modules', etc." + "Feature %s: must define 'require_features', 'remove', or " + "at least one of 'packages', 'py_modules', etc." ) def include_by_default(self): diff --git a/setuptools/monkey.py b/setuptools/monkey.py index 09f208b1..d9eb7d7b 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -8,6 +8,7 @@ import platform import types import functools from importlib import import_module +import inspect from setuptools.extern import six @@ -20,6 +21,20 @@ if you think you need this functionality. """ +def _get_mro(cls): + """ + Returns the bases classes for cls sorted by the MRO. + + Works around an issue on Jython where inspect.getmro will not return all + base classes if multiple classes share the same name. Instead, this + function will return a tuple containing the class itself, and the contents + of cls.__bases__. See https://github.com/pypa/setuptools/issues/1024. + """ + if platform.python_implementation() == "Jython": + return (cls,) + cls.__bases__ + return inspect.getmro(cls) + + def get_unpatched(item): lookup = ( get_unpatched_class if isinstance(item, six.class_types) else @@ -35,25 +50,23 @@ def get_unpatched_class(cls): Also ensures that no other distutils extension monkeypatched the distutils first. """ - while cls.__module__.startswith('setuptools'): - cls, = cls.__bases__ - if not cls.__module__.startswith('distutils'): + external_bases = ( + cls + for cls in _get_mro(cls) + if not cls.__module__.startswith('setuptools') + ) + base = next(external_bases) + if not base.__module__.startswith('distutils'): msg = "distutils has already been patched by %r" % cls raise AssertionError(msg) - return cls + return base def patch_all(): # we can't patch distutils.cmd, alas distutils.core.Command = setuptools.Command - has_issue_12885 = ( - sys.version_info < (3, 4, 6) - or - (3, 5) < sys.version_info <= (3, 5, 3) - or - (3, 6) < sys.version_info - ) + has_issue_12885 = sys.version_info <= (3, 5, 3) if has_issue_12885: # fix findall bug in distutils (http://bugs.python.org/issue12885) @@ -67,8 +80,6 @@ def patch_all(): (3, 4) < sys.version_info < (3, 4, 6) or (3, 5) < sys.version_info <= (3, 5, 3) - or - (3, 6) < sys.version_info ) if needs_warehouse: diff --git a/setuptools/msvc.py b/setuptools/msvc.py index 447ddb38..8e3b638f 100644 --- a/setuptools/msvc.py +++ b/setuptools/msvc.py @@ -4,15 +4,17 @@ Improved support for Microsoft Visual C++ compilers. Known supported compilers: -------------------------- Microsoft Visual C++ 9.0: - Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64); - Microsoft Windows SDK 7.0 (x86, x64, ia64); + Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64) Microsoft Windows SDK 6.1 (x86, x64, ia64) + Microsoft Windows SDK 7.0 (x86, x64, ia64) Microsoft Visual C++ 10.0: Microsoft Windows SDK 7.1 (x86, x64, ia64) Microsoft Visual C++ 14.0: Microsoft Visual C++ Build Tools 2015 (x86, x64, arm) + Microsoft Visual Studio 2017 (x86, x64, arm, arm64) + Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64) """ import os @@ -43,9 +45,18 @@ else: safe_env = dict() +_msvc9_suppress_errors = ( + # msvc9compiler isn't available on some platforms + ImportError, + + # msvc9compiler raises DistutilsPlatformError in some + # environments. See #1118. + distutils.errors.DistutilsPlatformError, +) + try: from distutils.msvc9compiler import Reg -except ImportError: +except _msvc9_suppress_errors: pass @@ -94,7 +105,7 @@ def msvc9_find_vcvarsall(version): def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): """ - Patched "distutils.msvc9compiler.query_vcvarsall" for support standalones + Patched "distutils.msvc9compiler.query_vcvarsall" for support extra compilers. Set environment without use of "vcvarsall.bat". @@ -102,9 +113,9 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): Known supported compilers ------------------------- Microsoft Visual C++ 9.0: - Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64); - Microsoft Windows SDK 7.0 (x86, x64, ia64); + Microsoft Visual C++ Compiler for Python 2.7 (x86, amd64) Microsoft Windows SDK 6.1 (x86, x64, ia64) + Microsoft Windows SDK 7.0 (x86, x64, ia64) Microsoft Visual C++ 10.0: Microsoft Windows SDK 7.1 (x86, x64, ia64) @@ -141,7 +152,7 @@ def msvc9_query_vcvarsall(ver, arch='x86', *args, **kwargs): def msvc14_get_vc_env(plat_spec): """ - Patched "distutils._msvccompiler._get_vc_env" for support standalones + Patched "distutils._msvccompiler._get_vc_env" for support extra compilers. Set environment without use of "vcvarsall.bat". @@ -150,6 +161,8 @@ def msvc14_get_vc_env(plat_spec): ------------------------- Microsoft Visual C++ 14.0: Microsoft Visual C++ Build Tools 2015 (x86, x64, arm) + Microsoft Visual Studio 2017 (x86, x64, arm, arm64) + Microsoft Visual Studio Build Tools 2017 (x86, x64, arm, arm64) Parameters ---------- @@ -272,7 +285,7 @@ class PlatformInfo: ) def target_dir(self, hidex86=False, x64=False): - """ + r""" Target platform specific subfolder. Parameters @@ -294,7 +307,7 @@ class PlatformInfo: ) def cross_dir(self, forcex86=False): - """ + r""" Cross platform specific subfolder. Parameters @@ -411,7 +424,7 @@ class RegistryInfo: ------ str: value """ - node64 = '' if self.pi.current_is_x86() or x86 else r'\Wow6432Node' + node64 = '' if self.pi.current_is_x86() or x86 else 'Wow6432Node' return os.path.join('Software', node64, 'Microsoft', key) def lookup(self, key, name): @@ -470,25 +483,26 @@ class SystemInfo: def __init__(self, registry_info, vc_ver=None): self.ri = registry_info self.pi = self.ri.pi - if vc_ver: - self.vc_ver = vc_ver - else: - try: - self.vc_ver = self.find_available_vc_vers()[-1] - except IndexError: - err = 'No Microsoft Visual C++ version found' - raise distutils.errors.DistutilsPlatformError(err) + self.vc_ver = vc_ver or self._find_latest_available_vc_ver() + + def _find_latest_available_vc_ver(self): + try: + return self.find_available_vc_vers()[-1] + except IndexError: + err = 'No Microsoft Visual C++ version found' + raise distutils.errors.DistutilsPlatformError(err) def find_available_vc_vers(self): """ Find all available Microsoft Visual C++ versions. """ - vckeys = (self.ri.vc, self.ri.vc_for_python) + ms = self.ri.microsoft + vckeys = (self.ri.vc, self.ri.vc_for_python, self.ri.vs) vc_vers = [] for hkey in self.ri.HKEYS: for key in vckeys: try: - bkey = winreg.OpenKey(hkey, key, 0, winreg.KEY_READ) + bkey = winreg.OpenKey(hkey, ms(key), 0, winreg.KEY_READ) except (OSError, IOError): continue subkeys, values, _ = winreg.QueryInfoKey(bkey) @@ -525,9 +539,9 @@ class SystemInfo: """ Microsoft Visual C++ directory. """ - # Default path - default = r'Microsoft Visual Studio %0.1f\VC' % self.vc_ver - guess_vc = os.path.join(self.ProgramFilesx86, default) + self.VSInstallDir + + guess_vc = self._guess_vc() or self._guess_vc_legacy() # Try to get "VC++ for Python" path from registry as default path reg_path = os.path.join(self.ri.vc_for_python, '%0.1f' % self.vc_ver) @@ -543,12 +557,34 @@ class SystemInfo: return path + def _guess_vc(self): + """ + Locate Visual C for 2017 + """ + if self.vc_ver <= 14.0: + return + + default = r'VC\Tools\MSVC' + guess_vc = os.path.join(self.VSInstallDir, default) + # Subdir with VC exact version as name + try: + vc_exact_ver = os.listdir(guess_vc)[-1] + return os.path.join(guess_vc, vc_exact_ver) + except (OSError, IOError, IndexError): + pass + + def _guess_vc_legacy(self): + """ + Locate Visual C for versions prior to 2017 + """ + default = r'Microsoft Visual Studio %0.1f\VC' % self.vc_ver + return os.path.join(self.ProgramFilesx86, default) + @property def WindowsSdkVersion(self): """ - Microsoft Windows SDK versions. + Microsoft Windows SDK versions for specified MSVC++ version. """ - # Set Windows SDK versions for specified MSVC++ version if self.vc_ver <= 9.0: return ('7.0', '6.1', '6.0a') elif self.vc_ver == 10.0: @@ -561,6 +597,14 @@ class SystemInfo: return ('10.0', '8.1') @property + def WindowsSdkLastVersion(self): + """ + Microsoft Windows SDK last version + """ + return self._use_last_dir_name(os.path.join( + self.WindowsSdkDir, 'lib')) + + @property def WindowsSdkDir(self): """ Microsoft Windows SDK directory. @@ -658,6 +702,14 @@ class SystemInfo: return sdkdir or '' @property + def UniversalCRTSdkLastVersion(self): + """ + Microsoft Universal C Runtime SDK last version + """ + return self._use_last_dir_name(os.path.join( + self.UniversalCRTSdkDir, 'lib')) + + @property def NetFxSdkVersion(self): """ Microsoft .NET Framework SDK versions. @@ -716,7 +768,7 @@ class SystemInfo: """ return self._find_dot_net_versions(64) - def _find_dot_net_versions(self, bits=32): + def _find_dot_net_versions(self, bits): """ Find Microsoft .NET Framework versions. @@ -725,8 +777,10 @@ class SystemInfo: bits: int Platform number of bits: 32 or 64. """ - # Find actual .NET version - ver = self.ri.lookup(self.ri.vc, 'frameworkver%d' % bits) or '' + # Find actual .NET version in registry + reg_ver = self.ri.lookup(self.ri.vc, 'frameworkver%d' % bits) + dot_net_dir = getattr(self, 'FrameworkDir%d' % bits) + ver = reg_ver or self._use_last_dir_name(dot_net_dir, 'v') or '' # Set .NET versions for specified MSVC++ version if self.vc_ver >= 12.0: @@ -740,6 +794,25 @@ class SystemInfo: frameworkver = ('v3.0', 'v2.0.50727') return frameworkver + def _use_last_dir_name(self, path, prefix=''): + """ + Return name of the last dir in path or '' if no dir found. + + Parameters + ---------- + path: str + Use dirs in this path + prefix: str + Use only dirs startings by this prefix + """ + matching_dirs = ( + dir_name + for dir_name in reversed(os.listdir(path)) + if os.path.isdir(os.path.join(path, dir_name)) and + dir_name.startswith(prefix) + ) + return next(matching_dirs, None) or '' + class EnvironmentInfo: """ @@ -765,15 +838,14 @@ class EnvironmentInfo: # Variables and properties in this class use originals CamelCase variables # names from Microsoft source files for more easy comparaison. - def __init__(self, arch, vc_ver=None, vc_min_ver=None): + def __init__(self, arch, vc_ver=None, vc_min_ver=0): self.pi = PlatformInfo(arch) self.ri = RegistryInfo(self.pi) self.si = SystemInfo(self.ri, vc_ver) - if vc_min_ver: - if self.vc_ver < vc_min_ver: - err = 'No suitable Microsoft Visual C++ version found' - raise distutils.errors.DistutilsPlatformError(err) + if self.vc_ver < vc_min_ver: + err = 'No suitable Microsoft Visual C++ version found' + raise distutils.errors.DistutilsPlatformError(err) @property def vc_ver(self): @@ -810,7 +882,10 @@ class EnvironmentInfo: """ Microsoft Visual C++ & Microsoft Foundation Class Libraries """ - arch_subdir = self.pi.target_dir(hidex86=True) + if self.vc_ver >= 15.0: + arch_subdir = self.pi.target_dir(x64=True) + else: + arch_subdir = self.pi.target_dir(hidex86=True) paths = ['Lib%s' % arch_subdir, r'ATLMFC\Lib%s' % arch_subdir] if self.vc_ver >= 14.0: @@ -840,10 +915,20 @@ class EnvironmentInfo: if arch_subdir: tools += [os.path.join(si.VCInstallDir, 'Bin%s' % arch_subdir)] - if self.vc_ver >= 14.0: + if self.vc_ver == 14.0: path = 'Bin%s' % self.pi.current_dir(hidex86=True) tools += [os.path.join(si.VCInstallDir, path)] + elif self.vc_ver >= 15.0: + host_dir = (r'bin\HostX86%s' if self.pi.current_is_x86() else + r'bin\HostX64%s') + tools += [os.path.join( + si.VCInstallDir, host_dir % self.pi.target_dir(x64=True))] + + if self.pi.current_cpu != self.pi.target_cpu: + tools += [os.path.join( + si.VCInstallDir, host_dir % self.pi.current_dir(x64=True))] + else: tools += [os.path.join(si.VCInstallDir, 'Bin')] @@ -861,8 +946,8 @@ class EnvironmentInfo: else: arch_subdir = self.pi.target_dir(x64=True) lib = os.path.join(self.si.WindowsSdkDir, 'lib') - libver = self._get_content_dirname(lib) - return [os.path.join(lib, '%sum%s' % (libver, arch_subdir))] + libver = self._sdk_subdir + return [os.path.join(lib, '%sum%s' % (libver , arch_subdir))] @property def OSIncludes(self): @@ -876,7 +961,7 @@ class EnvironmentInfo: else: if self.vc_ver >= 14.0: - sdkver = self._get_content_dirname(include) + sdkver = self._sdk_subdir else: sdkver = '' return [os.path.join(include, '%sshared' % sdkver), @@ -933,13 +1018,20 @@ class EnvironmentInfo: """ Microsoft Windows SDK Tools """ - bin_dir = 'Bin' if self.vc_ver <= 11.0 else r'Bin\x86' - tools = [os.path.join(self.si.WindowsSdkDir, bin_dir)] + return list(self._sdk_tools()) + + def _sdk_tools(self): + """ + Microsoft Windows SDK Tools paths generator + """ + if self.vc_ver < 15.0: + bin_dir = 'Bin' if self.vc_ver <= 11.0 else r'Bin\x86' + yield os.path.join(self.si.WindowsSdkDir, bin_dir) if not self.pi.current_is_x86(): arch_subdir = self.pi.current_dir(x64=True) path = 'Bin%s' % arch_subdir - tools += [os.path.join(self.si.WindowsSdkDir, path)] + yield os.path.join(self.si.WindowsSdkDir, path) if self.vc_ver == 10.0 or self.vc_ver == 11.0: if self.pi.target_is_x86(): @@ -947,12 +1039,24 @@ class EnvironmentInfo: else: arch_subdir = self.pi.current_dir(hidex86=True, x64=True) path = r'Bin\NETFX 4.0 Tools%s' % arch_subdir - tools += [os.path.join(self.si.WindowsSdkDir, path)] + yield os.path.join(self.si.WindowsSdkDir, path) + + elif self.vc_ver >= 15.0: + path = os.path.join(self.si.WindowsSdkDir, 'Bin') + arch_subdir = self.pi.current_dir(x64=True) + sdkver = self.si.WindowsSdkLastVersion + yield os.path.join(path, '%s%s' % (sdkver, arch_subdir)) if self.si.WindowsSDKExecutablePath: - tools += [self.si.WindowsSDKExecutablePath] + yield self.si.WindowsSDKExecutablePath - return tools + @property + def _sdk_subdir(self): + """ + Microsoft Windows SDK version subdir + """ + ucrtver = self.si.WindowsSdkLastVersion + return ('%s\\' % ucrtver) if ucrtver else '' @property def SdkSetup(self): @@ -1023,10 +1127,21 @@ class EnvironmentInfo: """ if self.vc_ver < 12.0: return [] + elif self.vc_ver < 15.0: + base_path = self.si.ProgramFilesx86 + arch_subdir = self.pi.current_dir(hidex86=True) + else: + base_path = self.si.VSInstallDir + arch_subdir = '' - arch_subdir = self.pi.current_dir(hidex86=True) path = r'MSBuild\%0.1f\bin%s' % (self.vc_ver, arch_subdir) - return [os.path.join(self.si.ProgramFilesx86, path)] + build = [os.path.join(base_path, path)] + + if self.vc_ver >= 15.0: + # Add Roslyn C# & Visual Basic Compiler + build += [os.path.join(base_path, path, 'Roslyn')] + + return build @property def HTMLHelpWorkshop(self): @@ -1041,27 +1156,34 @@ class EnvironmentInfo: @property def UCRTLibraries(self): """ - Microsoft Universal CRT Libraries + Microsoft Universal C Runtime SDK Libraries """ if self.vc_ver < 14.0: return [] arch_subdir = self.pi.target_dir(x64=True) lib = os.path.join(self.si.UniversalCRTSdkDir, 'lib') - ucrtver = self._get_content_dirname(lib) + ucrtver = self._ucrt_subdir return [os.path.join(lib, '%sucrt%s' % (ucrtver, arch_subdir))] @property def UCRTIncludes(self): """ - Microsoft Universal CRT Include + Microsoft Universal C Runtime SDK Include """ if self.vc_ver < 14.0: return [] include = os.path.join(self.si.UniversalCRTSdkDir, 'include') - ucrtver = self._get_content_dirname(include) - return [os.path.join(include, '%sucrt' % ucrtver)] + return [os.path.join(include, '%sucrt' % self._ucrt_subdir)] + + @property + def _ucrt_subdir(self): + """ + Microsoft Universal C Runtime SDK version subdir + """ + ucrtver = self.si.UniversalCRTSdkLastVersion + return ('%s\\' % ucrtver) if ucrtver else '' @property def FSharp(self): @@ -1079,9 +1201,18 @@ class EnvironmentInfo: Microsoft Visual C++ runtime redistribuable dll """ arch_subdir = self.pi.target_dir(x64=True) - vcruntime = 'redist%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll' - vcruntime = vcruntime % (arch_subdir, self.vc_ver, self.vc_ver) - return os.path.join(self.si.VCInstallDir, vcruntime) + if self.vc_ver < 15: + redist_path = self.si.VCInstallDir + vcruntime = 'redist%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll' + else: + redist_path = self.si.VCInstallDir.replace('\\Tools', '\\Redist') + vcruntime = 'onecore%s\\Microsoft.VC%d0.CRT\\vcruntime%d0.dll' + + # Visual Studio 2017 is still Visual C++ 14.0 + dll_ver = 14.0 if self.vc_ver == 15 else self.vc_ver + + vcruntime = vcruntime % (arch_subdir, self.vc_ver, dll_ver) + return os.path.join(redist_path, vcruntime) def return_env(self, exists=True): """ @@ -1169,25 +1300,3 @@ class EnvironmentInfo: if k not in seen: seen_add(k) yield element - - def _get_content_dirname(self, path): - """ - Return name of the first dir in path or '' if no dir found. - - Parameters - ---------- - path: str - Path where search dir. - - Return - ------ - foldername: str - "name\" or "" - """ - try: - name = os.listdir(path) - if name: - return '%s\\' % name[0] - return '' - except (OSError, IOError): - return '' diff --git a/setuptools/namespaces.py b/setuptools/namespaces.py index cc934b7e..dc16106d 100755 --- a/setuptools/namespaces.py +++ b/setuptools/namespaces.py @@ -30,15 +30,29 @@ class Installer: with open(filename, 'wt') as f: f.writelines(lines) + def uninstall_namespaces(self): + filename, ext = os.path.splitext(self._get_target()) + filename += self.nspkg_ext + if not os.path.exists(filename): + return + log.info("Removing %s", filename) + os.remove(filename) + def _get_target(self): return self.target _nspkg_tmpl = ( "import sys, types, os", - "pep420 = sys.version_info > (3, 3)", + "has_mfs = sys.version_info > (3, 5)", "p = os.path.join(%(root)s, *%(pth)r)", - "ie = os.path.exists(os.path.join(p,'__init__.py'))", - "m = not ie and not pep420 and " + "importlib = has_mfs and __import__('importlib.util')", + "has_mfs and __import__('importlib.machinery')", + "m = has_mfs and " + "sys.modules.setdefault(%(pkg)r, " + "importlib.util.module_from_spec(" + "importlib.machinery.PathFinder.find_spec(%(pkg)r, " + "[os.path.dirname(p)])))", + "m = m or " "sys.modules.setdefault(%(pkg)r, types.ModuleType(%(pkg)r))", "mp = (m or []) and m.__dict__.setdefault('__path__',[])", "(p not in mp) and mp.append(p)", diff --git a/setuptools/package_index.py b/setuptools/package_index.py index d2f27ca6..4f610e0e 100755 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -20,7 +20,7 @@ from setuptools.extern.six.moves import urllib, http_client, configparser, map import setuptools from pkg_resources import ( CHECKOUT_DIST, Distribution, BINARY_DIST, normalize_path, SOURCE_DIST, - require, Environment, find_distributions, safe_name, safe_version, + Environment, find_distributions, safe_name, safe_version, to_filename, Requirement, DEVELOP_DIST, ) from setuptools import ssl_support @@ -29,12 +29,12 @@ from distutils.errors import DistutilsError from fnmatch import translate from setuptools.py27compat import get_all_headers -EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.]+)$') +EGG_FRAGMENT = re.compile(r'^egg=([-A-Za-z0-9_.+!]+)$') HREF = re.compile("""href\\s*=\\s*['"]?([^'"> ]+)""", re.I) # this is here to fix emacs' cruddy broken syntax highlighting PYPI_MD5 = re.compile( - '<a href="([^"#]+)">([^<]+)</a>\n\s+\\(<a (?:title="MD5 hash"\n\s+)' - 'href="[^?]+\?:action=show_md5&digest=([0-9a-f]{32})">md5</a>\\)' + '<a href="([^"#]+)">([^<]+)</a>\n\\s+\\(<a (?:title="MD5 hash"\n\\s+)' + 'href="[^?]+\\?:action=show_md5&digest=([0-9a-f]{32})">md5</a>\\)' ) URL_SCHEME = re.compile('([-+.a-z0-9]{2,}):', re.I).match EXTENSIONS = ".tar.gz .tar.bz2 .tar .zip .tgz".split() @@ -47,7 +47,7 @@ __all__ = [ _SOCKET_TIMEOUT = 15 _tmpl = "setuptools/{setuptools.__version__} Python-urllib/{py_major}" -user_agent = _tmpl.format(py_major=sys.version[:3], **globals()) +user_agent = _tmpl.format(py_major=sys.version[:3], setuptools=setuptools) def parse_requirement_arg(spec): @@ -160,7 +160,7 @@ def interpret_distro_name( # versions in distribution archive names (sdist and bdist). parts = basename.split('-') - if not py_version and any(re.match('py\d\.\d$', p) for p in parts[2:]): + if not py_version and any(re.match(r'py\d\.\d$', p) for p in parts[2:]): # it is a bdist_dumb, not an sdist -- bail out return @@ -204,7 +204,7 @@ def unique_values(func): return wrapper -REL = re.compile("""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I) +REL = re.compile(r"""<([^>]*\srel\s*=\s*['"]?([^'">]+)[^>]*)>""", re.I) # this line is here to fix emacs' cruddy broken syntax highlighting diff --git a/setuptools/py27compat.py b/setuptools/py27compat.py index 4e3e4ab3..2985011b 100644 --- a/setuptools/py27compat.py +++ b/setuptools/py27compat.py @@ -2,7 +2,9 @@ Compatibility Support for Python 2.7 and earlier """ -import sys +import platform + +from setuptools.extern import six def get_all_headers(message, key): @@ -12,7 +14,15 @@ def get_all_headers(message, key): return message.get_all(key) -if sys.version_info < (3,): - +if six.PY2: def get_all_headers(message, key): return message.getheaders(key) + + +linux_py2_ascii = ( + platform.system() == 'Linux' and + six.PY2 +) + +rmtree_safe = str if linux_py2_ascii else lambda x: x +"""Workaround for http://bugs.python.org/issue24672""" diff --git a/setuptools/py33compat.py b/setuptools/py33compat.py new file mode 100644 index 00000000..af64d5d1 --- /dev/null +++ b/setuptools/py33compat.py @@ -0,0 +1,45 @@ +import dis +import array +import collections + +from setuptools.extern import six + + +OpArg = collections.namedtuple('OpArg', 'opcode arg') + + +class Bytecode_compat(object): + def __init__(self, code): + self.code = code + + def __iter__(self): + """Yield '(op,arg)' pair for each operation in code object 'code'""" + + bytes = array.array('b', self.code.co_code) + eof = len(self.code.co_code) + + ptr = 0 + extended_arg = 0 + + while ptr < eof: + + op = bytes[ptr] + + if op >= dis.HAVE_ARGUMENT: + + arg = bytes[ptr + 1] + bytes[ptr + 2] * 256 + extended_arg + ptr += 3 + + if op == dis.EXTENDED_ARG: + long_type = six.integer_types[-1] + extended_arg = arg * long_type(65536) + continue + + else: + arg = None + ptr += 1 + + yield OpArg(op, arg) + + +Bytecode = getattr(dis, 'Bytecode', Bytecode_compat) diff --git a/setuptools/py36compat.py b/setuptools/py36compat.py new file mode 100644 index 00000000..f5279696 --- /dev/null +++ b/setuptools/py36compat.py @@ -0,0 +1,82 @@ +import sys +from distutils.errors import DistutilsOptionError +from distutils.util import strtobool +from distutils.debug import DEBUG + + +class Distribution_parse_config_files: + """ + Mix-in providing forward-compatibility for functionality to be + included by default on Python 3.7. + + Do not edit the code in this class except to update functionality + as implemented in distutils. + """ + def parse_config_files(self, filenames=None): + from configparser import ConfigParser + + # Ignore install directory options if we have a venv + if sys.prefix != sys.base_prefix: + ignore_options = [ + 'install-base', 'install-platbase', 'install-lib', + 'install-platlib', 'install-purelib', 'install-headers', + 'install-scripts', 'install-data', 'prefix', 'exec-prefix', + 'home', 'user', 'root'] + else: + ignore_options = [] + + ignore_options = frozenset(ignore_options) + + if filenames is None: + filenames = self.find_config_files() + + if DEBUG: + self.announce("Distribution.parse_config_files():") + + parser = ConfigParser(interpolation=None) + for filename in filenames: + if DEBUG: + self.announce(" reading %s" % filename) + parser.read(filename) + for section in parser.sections(): + options = parser.options(section) + opt_dict = self.get_option_dict(section) + + for opt in options: + if opt != '__name__' and opt not in ignore_options: + val = parser.get(section,opt) + opt = opt.replace('-', '_') + opt_dict[opt] = (filename, val) + + # Make the ConfigParser forget everything (so we retain + # the original filenames that options come from) + parser.__init__() + + # If there was a "global" section in the config file, use it + # to set Distribution options. + + if 'global' in self.command_options: + for (opt, (src, val)) in self.command_options['global'].items(): + alias = self.negative_opt.get(opt) + try: + if alias: + setattr(self, alias, not strtobool(val)) + elif opt in ('verbose', 'dry_run'): # ugh! + setattr(self, opt, strtobool(val)) + else: + setattr(self, opt, val) + except ValueError as msg: + raise DistutilsOptionError(msg) + + +if sys.version_info < (3,): + # Python 2 behavior is sufficient + class Distribution_parse_config_files: + pass + + +if False: + # When updated behavior is available upstream, + # disable override here. + class Distribution_parse_config_files: + pass diff --git a/setuptools/sandbox.py b/setuptools/sandbox.py index 640691d8..685f3f72 100755 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -7,11 +7,12 @@ import itertools import re import contextlib import pickle +import textwrap from setuptools.extern import six from setuptools.extern.six.moves import builtins, map -import pkg_resources +import pkg_resources.py31compat if sys.platform.startswith('java'): import org.python.modules.posix.PosixModule as _os @@ -25,6 +26,7 @@ _open = open from distutils.errors import DistutilsError from pkg_resources import working_set + __all__ = [ "AbstractSandbox", "DirectorySandbox", "SandboxViolation", "run_setup", ] @@ -68,8 +70,7 @@ def override_temp(replacement): """ Monkey-patch tempfile.tempdir with replacement, ensuring it exists """ - if not os.path.isdir(replacement): - os.makedirs(replacement) + pkg_resources.py31compat.makedirs(replacement, exist_ok=True) saved = tempfile.tempdir @@ -211,7 +212,7 @@ def _needs_hiding(mod_name): >>> _needs_hiding('Cython') True """ - pattern = re.compile('(setuptools|pkg_resources|distutils|Cython)(\.|$)') + pattern = re.compile(r'(setuptools|pkg_resources|distutils|Cython)(\.|$)') return bool(pattern.match(mod_name)) @@ -237,11 +238,16 @@ def run_setup(setup_script, args): working_set.__init__() working_set.callbacks.append(lambda dist: dist.activate()) - def runner(): - ns = dict(__file__=setup_script, __name__='__main__') - _execfile(setup_script, ns) + # __file__ should be a byte string on Python 2 (#712) + dunder_file = ( + setup_script + if isinstance(setup_script, str) else + setup_script.encode(sys.getfilesystemencoding()) + ) - DirectorySandbox(setup_dir).run(runner) + with DirectorySandbox(setup_dir): + ns = dict(__file__=dunder_file, __name__='__main__') + _execfile(setup_script, ns) except SystemExit as v: if v.args and v.args[0]: raise @@ -263,21 +269,24 @@ class AbstractSandbox: for name in self._attrs: setattr(os, name, getattr(source, name)) + def __enter__(self): + self._copy(self) + if _file: + builtins.file = self._file + builtins.open = self._open + self._active = True + + def __exit__(self, exc_type, exc_value, traceback): + self._active = False + if _file: + builtins.file = _file + builtins.open = _open + self._copy(_os) + def run(self, func): """Run 'func' under os sandboxing""" - try: - self._copy(self) - if _file: - builtins.file = self._file - builtins.open = self._open - self._active = True + with self: return func() - finally: - self._active = False - if _file: - builtins.file = _file - builtins.open = _open - self._copy(_os) def _mk_dual_path_wrapper(name): original = getattr(_os, name) @@ -380,7 +389,7 @@ class DirectorySandbox(AbstractSandbox): _exception_patterns = [ # Allow lib2to3 to attempt to save a pickled grammar object (#121) - '.*lib2to3.*\.pickle$', + r'.*lib2to3.*\.pickle$', ] "exempt writing to paths that match the pattern" @@ -465,16 +474,18 @@ WRITE_FLAGS = functools.reduce( class SandboxViolation(DistutilsError): """A setup script attempted to modify the filesystem outside the sandbox""" - def __str__(self): - return """SandboxViolation: %s%r %s - -The package setup script has attempted to modify files on your system -that are not within the EasyInstall build area, and has been aborted. + tmpl = textwrap.dedent(""" + SandboxViolation: {cmd}{args!r} {kwargs} -This package cannot be safely installed by EasyInstall, and may not -support alternate installation locations even if you run its setup -script by hand. Please inform the package's author and the EasyInstall -maintainers to find out if a fix or workaround is available.""" % self.args + The package setup script has attempted to modify files on your system + that are not within the EasyInstall build area, and has been aborted. + This package cannot be safely installed by EasyInstall, and may not + support alternate installation locations even if you run its setup + script by hand. Please inform the package's author and the EasyInstall + maintainers to find out if a fix or workaround is available. + """).lstrip() -# + def __str__(self): + cmd, args, kwargs = self.args + return self.tmpl.format(**locals()) diff --git a/setuptools/ssl_support.py b/setuptools/ssl_support.py index 82f8870a..72b18ef2 100644 --- a/setuptools/ssl_support.py +++ b/setuptools/ssl_support.py @@ -2,10 +2,10 @@ import os import socket import atexit import re +import functools -from setuptools.extern.six.moves import urllib, http_client, map +from setuptools.extern.six.moves import urllib, http_client, map, filter -import pkg_resources from pkg_resources import ResolutionError, ExtractionError try: @@ -204,47 +204,52 @@ def opener_for(ca_bundle=None): ).open -_wincerts = None +# from jaraco.functools +def once(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + if not hasattr(func, 'always_returns'): + func.always_returns = func(*args, **kwargs) + return func.always_returns + return wrapper +@once def get_win_certfile(): - global _wincerts - if _wincerts is not None: - return _wincerts.name - try: - from wincertstore import CertFile + import wincertstore except ImportError: return None - class MyCertFile(CertFile): - def __init__(self, stores=(), certs=()): - CertFile.__init__(self) - for store in stores: - self.addstore(store) - self.addcerts(certs) + class CertFile(wincertstore.CertFile): + def __init__(self): + super(CertFile, self).__init__() atexit.register(self.close) def close(self): try: - super(MyCertFile, self).close() + super(CertFile, self).close() except OSError: pass - _wincerts = MyCertFile(stores=['CA', 'ROOT']) + _wincerts = CertFile() + _wincerts.addstore('CA') + _wincerts.addstore('ROOT') return _wincerts.name def find_ca_bundle(): """Return an existing CA bundle path, or None""" - if os.name == 'nt': - return get_win_certfile() - else: - for cert_path in cert_paths: - if os.path.isfile(cert_path): - return cert_path + extant_cert_paths = filter(os.path.isfile, cert_paths) + return ( + get_win_certfile() + or next(extant_cert_paths, None) + or _certifi_where() + ) + + +def _certifi_where(): try: - import certifi - return certifi.where() + return __import__('certifi').where() except (ImportError, ResolutionError, ExtractionError): - return None + pass diff --git a/setuptools/tests/__init__.py b/setuptools/tests/__init__.py index dbf16201..8ae4402d 100644 --- a/setuptools/tests/__init__.py +++ b/setuptools/tests/__init__.py @@ -1,4 +1,5 @@ """Tests for the 'setuptools' package""" +import locale import sys import os import distutils.core @@ -16,8 +17,7 @@ import setuptools.depends as dep from setuptools import Feature from setuptools.depends import Require -c_type = os.environ.get("LC_CTYPE", os.environ.get("LC_ALL")) -is_ascii = c_type in ("C", "POSIX") +is_ascii = locale.getpreferredencoding() == 'ANSI_X3.4-1968' fail_on_ascii = pytest.mark.xfail(is_ascii, reason="Test fails in this locale") diff --git a/setuptools/tests/files.py b/setuptools/tests/files.py index 4364241b..98de9fc3 100644 --- a/setuptools/tests/files.py +++ b/setuptools/tests/files.py @@ -1,6 +1,9 @@ import os +import pkg_resources.py31compat + + def build_files(file_defs, prefix=""): """ Build a set of files/directories, as described by the file_defs dictionary. @@ -24,8 +27,7 @@ def build_files(file_defs, prefix=""): for name, contents in file_defs.items(): full_name = os.path.join(prefix, name) if isinstance(contents, dict): - if not os.path.exists(full_name): - os.makedirs(full_name) + pkg_resources.py31compat.makedirs(full_name, exist_ok=True) build_files(contents, prefix=full_name) else: with open(full_name, 'w') as f: diff --git a/setuptools/tests/mod_with_constant.py b/setuptools/tests/mod_with_constant.py new file mode 100644 index 00000000..ef755dd1 --- /dev/null +++ b/setuptools/tests/mod_with_constant.py @@ -0,0 +1 @@ +value = 'three, sir!' diff --git a/setuptools/tests/test_bdist_egg.py b/setuptools/tests/test_bdist_egg.py index 5aabf404..d24aa366 100644 --- a/setuptools/tests/test_bdist_egg.py +++ b/setuptools/tests/test_bdist_egg.py @@ -41,4 +41,4 @@ class Test: # let's see if we got our egg link at the right place [content] = os.listdir('dist') - assert re.match('foo-0.0.0-py[23].\d.egg$', content) + assert re.match(r'foo-0.0.0-py[23].\d.egg$', content) diff --git a/setuptools/tests/test_build_clib.py b/setuptools/tests/test_build_clib.py new file mode 100644 index 00000000..aebcc350 --- /dev/null +++ b/setuptools/tests/test_build_clib.py @@ -0,0 +1,59 @@ +import pytest +import os +import shutil + +import mock +from distutils.errors import DistutilsSetupError +from setuptools.command.build_clib import build_clib +from setuptools.dist import Distribution + + +class TestBuildCLib: + @mock.patch( + 'setuptools.command.build_clib.newer_pairwise_group' + ) + def test_build_libraries(self, mock_newer): + dist = Distribution() + cmd = build_clib(dist) + + # this will be a long section, just making sure all + # exceptions are properly raised + libs = [('example', {'sources': 'broken.c'})] + with pytest.raises(DistutilsSetupError): + cmd.build_libraries(libs) + + obj_deps = 'some_string' + libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})] + with pytest.raises(DistutilsSetupError): + cmd.build_libraries(libs) + + obj_deps = {'': ''} + libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})] + with pytest.raises(DistutilsSetupError): + cmd.build_libraries(libs) + + obj_deps = {'source.c': ''} + libs = [('example', {'sources': ['source.c'], 'obj_deps': obj_deps})] + with pytest.raises(DistutilsSetupError): + cmd.build_libraries(libs) + + # with that out of the way, let's see if the crude dependency + # system works + cmd.compiler = mock.MagicMock(spec=cmd.compiler) + mock_newer.return_value = ([],[]) + + obj_deps = {'': ('global.h',), 'example.c': ('example.h',)} + libs = [('example', {'sources': ['example.c'] ,'obj_deps': obj_deps})] + + cmd.build_libraries(libs) + assert [['example.c', 'global.h', 'example.h']] in mock_newer.call_args[0] + assert not cmd.compiler.compile.called + assert cmd.compiler.create_static_lib.call_count == 1 + + # reset the call numbers so we can test again + cmd.compiler.reset_mock() + + mock_newer.return_value = '' # anything as long as it's not ([],[]) + cmd.build_libraries(libs) + assert cmd.compiler.compile.call_count == 1 + assert cmd.compiler.create_static_lib.call_count == 1 diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py index 21487720..cdfa5af4 100644 --- a/setuptools/tests/test_config.py +++ b/setuptools/tests/test_config.py @@ -9,6 +9,13 @@ class ErrConfigHandler(ConfigHandler): """Erroneous handler. Fails to implement required methods.""" +def make_package_dir(name, base_dir): + dir_package = base_dir.mkdir(name) + init_file = dir_package.join('__init__.py') + init_file.write('') + return dir_package, init_file + + def fake_env(tmpdir, setup_cfg, setup_py=None): if setup_py is None: @@ -18,11 +25,12 @@ def fake_env(tmpdir, setup_cfg, setup_py=None): ) tmpdir.join('setup.py').write(setup_py) - tmpdir.join('setup.cfg').write(setup_cfg) + config = tmpdir.join('setup.cfg') + config.write(setup_cfg) - package_name = 'fake_package' - dir_package = tmpdir.mkdir(package_name) - dir_package.join('__init__.py').write( + package_dir, init_file = make_package_dir('fake_package', tmpdir) + + init_file.write( 'VERSION = (1, 2, 3)\n' '\n' 'VERSION_MAJOR = 1' @@ -31,6 +39,7 @@ def fake_env(tmpdir, setup_cfg, setup_py=None): ' return [3, 4, 5, "dev"]\n' '\n' ) + return package_dir, config @contextlib.contextmanager @@ -55,7 +64,7 @@ def test_parsers_implemented(): class TestConfigurationReader: def test_basic(self, tmpdir): - fake_env( + _, config = fake_env( tmpdir, '[metadata]\n' 'version = 10.1.1\n' @@ -64,7 +73,7 @@ class TestConfigurationReader: '[options]\n' 'scripts = bin/a.py, bin/b.py\n' ) - config_dict = read_configuration('%s' % tmpdir.join('setup.cfg')) + config_dict = read_configuration('%s' % config) assert config_dict['metadata']['version'] == '10.1.1' assert config_dict['metadata']['keywords'] == ['one', 'two'] assert config_dict['options']['scripts'] == ['bin/a.py', 'bin/b.py'] @@ -73,6 +82,24 @@ class TestConfigurationReader: with pytest.raises(DistutilsFileError): read_configuration('%s' % tmpdir.join('setup.cfg')) + def test_ignore_errors(self, tmpdir): + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'version = attr: none.VERSION\n' + 'keywords = one, two\n' + ) + with pytest.raises(ImportError): + read_configuration('%s' % config) + + config_dict = read_configuration( + '%s' % config, ignore_option_errors=True) + + assert config_dict['metadata']['keywords'] == ['one', 'two'] + assert 'version' not in config_dict['metadata'] + + config.remove() + class TestMetadata: @@ -112,6 +139,24 @@ class TestMetadata: assert metadata.download_url == 'http://test.test.com/test/' assert metadata.maintainer_email == 'test@test.com' + def test_file_mixed(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'long_description = file: README.rst, CHANGES.rst\n' + '\n' + ) + + tmpdir.join('README.rst').write('readme contents\nline2') + tmpdir.join('CHANGES.rst').write('changelog contents\nand stuff') + + with get_dist(tmpdir) as dist: + assert dist.metadata.long_description == ( + 'readme contents\nline2\n' + 'changelog contents\nand stuff' + ) + def test_file_sandboxed(self, tmpdir): fake_env( @@ -172,7 +217,7 @@ class TestMetadata: def test_version(self, tmpdir): - fake_env( + _, config = fake_env( tmpdir, '[metadata]\n' 'version = attr: fake_package.VERSION\n' @@ -180,14 +225,14 @@ class TestMetadata: with get_dist(tmpdir) as dist: assert dist.metadata.version == '1.2.3' - tmpdir.join('setup.cfg').write( + config.write( '[metadata]\n' 'version = attr: fake_package.get_version\n' ) with get_dist(tmpdir) as dist: assert dist.metadata.version == '3.4.5.dev' - tmpdir.join('setup.cfg').write( + config.write( '[metadata]\n' 'version = attr: fake_package.VERSION_MAJOR\n' ) @@ -198,7 +243,7 @@ class TestMetadata: subpack.join('__init__.py').write('') subpack.join('submodule.py').write('VERSION = (2016, 11, 26)') - tmpdir.join('setup.cfg').write( + config.write( '[metadata]\n' 'version = attr: fake_package.subpackage.submodule.VERSION\n' ) @@ -230,11 +275,12 @@ class TestMetadata: def test_classifiers(self, tmpdir): expected = set([ 'Framework :: Django', + 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', ]) # From file. - fake_env( + _, config = fake_env( tmpdir, '[metadata]\n' 'classifiers = file: classifiers\n' @@ -242,19 +288,21 @@ class TestMetadata: tmpdir.join('classifiers').write( 'Framework :: Django\n' + 'Programming Language :: Python :: 3\n' 'Programming Language :: Python :: 3.5\n' ) with get_dist(tmpdir) as dist: assert set(dist.metadata.classifiers) == expected - # From section. - tmpdir.join('setup.cfg').write( - '[metadata.classifiers]\n' - 'Framework :: Django\n' - 'Programming Language :: Python :: 3.5\n' + # From list notation + config.write( + '[metadata]\n' + 'classifiers =\n' + ' Framework :: Django\n' + ' Programming Language :: Python :: 3\n' + ' Programming Language :: Python :: 3.5\n' ) - with get_dist(tmpdir) as dist: assert set(dist.metadata.classifiers) == expected @@ -282,6 +330,8 @@ class TestOptions: 'setup_requires = docutils>=0.3; spack ==1.1, ==1.3; there\n' 'dependency_links = http://some.com/here/1, ' 'http://some.com/there/2\n' + 'python_requires = >=1.0, !=2.8\n' + 'py_modules = module1, module2\n' ) with get_dist(tmpdir) as dist: assert dist.zip_safe @@ -301,7 +351,7 @@ class TestOptions: ]) assert dist.install_requires == ([ 'docutils>=0.3', - 'pack ==1.1, ==1.3', + 'pack==1.1,==1.3', 'hey' ]) assert dist.setup_requires == ([ @@ -310,6 +360,8 @@ class TestOptions: 'there' ]) assert dist.tests_require == ['mock==0.7.2', 'pytest'] + assert dist.python_requires == '>=1.0, !=2.8' + assert dist.py_modules == ['module1', 'module2'] def test_multiline(self, tmpdir): fake_env( @@ -369,7 +421,7 @@ class TestOptions: ]) assert dist.install_requires == ([ 'docutils>=0.3', - 'pack ==1.1, ==1.3', + 'pack==1.1,==1.3', 'hey' ]) assert dist.setup_requires == ([ @@ -421,6 +473,46 @@ class TestOptions: with get_dist(tmpdir) as dist: assert dist.packages == ['fake_package'] + def test_find_directive(self, tmpdir): + dir_package, config = fake_env( + tmpdir, + '[options]\n' + 'packages = find:\n' + ) + + dir_sub_one, _ = make_package_dir('sub_one', dir_package) + dir_sub_two, _ = make_package_dir('sub_two', dir_package) + + with get_dist(tmpdir) as dist: + assert set(dist.packages) == set([ + 'fake_package', 'fake_package.sub_two', 'fake_package.sub_one' + ]) + + config.write( + '[options]\n' + 'packages = find:\n' + '\n' + '[options.packages.find]\n' + 'where = .\n' + 'include =\n' + ' fake_package.sub_one\n' + ' two\n' + ) + with get_dist(tmpdir) as dist: + assert dist.packages == ['fake_package.sub_one'] + + config.write( + '[options]\n' + 'packages = find:\n' + '\n' + '[options.packages.find]\n' + 'exclude =\n' + ' fake_package.sub_one\n' + ) + with get_dist(tmpdir) as dist: + assert set(dist.packages) == set( + ['fake_package', 'fake_package.sub_two']) + def test_extras_require(self, tmpdir): fake_env( tmpdir, @@ -434,11 +526,11 @@ class TestOptions: with get_dist(tmpdir) as dist: assert dist.extras_require == { 'pdf': ['ReportLab>=1.2', 'RXP'], - 'rest': ['docutils>=0.3', 'pack ==1.1, ==1.3'] + 'rest': ['docutils>=0.3', 'pack==1.1,==1.3'] } def test_entry_points(self, tmpdir): - fake_env( + _, config = fake_env( tmpdir, '[options.entry_points]\n' 'group1 = point1 = pack.module:func, ' @@ -463,7 +555,7 @@ class TestOptions: tmpdir.join('entry_points').write(expected) # From file. - tmpdir.join('setup.cfg').write( + config.write( '[options]\n' 'entry_points = file: entry_points\n' ) diff --git a/setuptools/tests/test_dep_util.py b/setuptools/tests/test_dep_util.py new file mode 100644 index 00000000..e5027c10 --- /dev/null +++ b/setuptools/tests/test_dep_util.py @@ -0,0 +1,30 @@ +from setuptools.dep_util import newer_pairwise_group +import os +import pytest + + +@pytest.fixture +def groups_target(tmpdir): + """Sets up some older sources, a target and newer sources. + Returns a 3-tuple in this order. + """ + creation_order = ['older.c', 'older.h', 'target.o', 'newer.c', 'newer.h'] + mtime = 0 + + for i in range(len(creation_order)): + creation_order[i] = os.path.join(str(tmpdir), creation_order[i]) + with open(creation_order[i], 'w'): + pass + + # make sure modification times are sequential + os.utime(creation_order[i], (mtime, mtime)) + mtime += 1 + + return creation_order[:2], creation_order[2], creation_order[3:] + + +def test_newer_pairwise_group(groups_target): + older = newer_pairwise_group([groups_target[0]], [groups_target[1]]) + newer = newer_pairwise_group([groups_target[2]], [groups_target[1]]) + assert older == ([], []) + assert newer == ([groups_target[2]], [groups_target[1]]) diff --git a/setuptools/tests/test_depends.py b/setuptools/tests/test_depends.py new file mode 100644 index 00000000..e0cfa880 --- /dev/null +++ b/setuptools/tests/test_depends.py @@ -0,0 +1,16 @@ +import sys + +from setuptools import depends + + +class TestGetModuleConstant: + + def test_basic(self): + """ + Invoke get_module_constant on a module in + the test package. + """ + mod_name = 'setuptools.tests.mod_with_constant' + val = depends.get_module_constant(mod_name, 'value') + assert val == 'three, sir!' + assert 'setuptools.tests.mod_with_constant' not in sys.modules diff --git a/setuptools/tests/test_develop.py b/setuptools/tests/test_develop.py index 4cf483f2..ad7cfa05 100644 --- a/setuptools/tests/test_develop.py +++ b/setuptools/tests/test_develop.py @@ -1,17 +1,23 @@ """develop tests """ + +from __future__ import absolute_import, unicode_literals + import os import site import sys import io +import subprocess from setuptools.extern import six +from setuptools.command import test import pytest from setuptools.command.develop import develop from setuptools.dist import Distribution from . import contexts +from . import namespaces SETUP_PY = """\ from setuptools import setup @@ -114,3 +120,72 @@ class TestDevelop: cmd.install_dir = tmpdir cmd.run() # assert '0.0' not in foocmd_text + + +class TestResolver: + """ + TODO: These tests were written with a minimal understanding + of what _resolve_setup_path is intending to do. Come up with + more meaningful cases that look like real-world scenarios. + """ + def test_resolve_setup_path_cwd(self): + assert develop._resolve_setup_path('.', '.', '.') == '.' + + def test_resolve_setup_path_one_dir(self): + assert develop._resolve_setup_path('pkgs', '.', 'pkgs') == '../' + + def test_resolve_setup_path_one_dir_trailing_slash(self): + assert develop._resolve_setup_path('pkgs/', '.', 'pkgs') == '../' + + +class TestNamespaces: + + @staticmethod + def install_develop(src_dir, target): + + develop_cmd = [ + sys.executable, + 'setup.py', + 'develop', + '--install-dir', str(target), + ] + with src_dir.as_cwd(): + with test.test.paths_on_pythonpath([str(target)]): + subprocess.check_call(develop_cmd) + + @pytest.mark.skipif(bool(os.environ.get("APPVEYOR")), + reason="https://github.com/pypa/setuptools/issues/851") + def test_namespace_package_importable(self, tmpdir): + """ + Installing two packages sharing the same namespace, one installed + naturally using pip or `--single-version-externally-managed` + and the other installed using `develop` should leave the namespace + in tact and both packages reachable by import. + """ + pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA') + pkg_B = namespaces.build_namespace_package(tmpdir, 'myns.pkgB') + target = tmpdir / 'packages' + # use pip to install to the target directory + install_cmd = [ + 'pip', + 'install', + str(pkg_A), + '-t', str(target), + ] + subprocess.check_call(install_cmd) + self.install_develop(pkg_B, target) + namespaces.make_site_dir(target) + try_import = [ + sys.executable, + '-c', 'import myns.pkgA; import myns.pkgB', + ] + with test.test.paths_on_pythonpath([str(target)]): + subprocess.check_call(try_import) + + # additionally ensure that pkg_resources import works + pkg_resources_imp = [ + sys.executable, + '-c', 'import pkg_resources', + ] + with test.test.paths_on_pythonpath([str(target)]): + subprocess.check_call(pkg_resources_imp) diff --git a/setuptools/tests/test_dist.py b/setuptools/tests/test_dist.py new file mode 100644 index 00000000..435ffec0 --- /dev/null +++ b/setuptools/tests/test_dist.py @@ -0,0 +1,46 @@ +from setuptools import Distribution +from setuptools.extern.six.moves.urllib.request import pathname2url +from setuptools.extern.six.moves.urllib_parse import urljoin + +from .textwrap import DALS +from .test_easy_install import make_nspkg_sdist + + +def test_dist_fetch_build_egg(tmpdir): + """ + Check multiple calls to `Distribution.fetch_build_egg` work as expected. + """ + index = tmpdir.mkdir('index') + index_url = urljoin('file://', pathname2url(str(index))) + def sdist_with_index(distname, version): + dist_dir = index.mkdir(distname) + dist_sdist = '%s-%s.tar.gz' % (distname, version) + make_nspkg_sdist(str(dist_dir.join(dist_sdist)), distname, version) + with dist_dir.join('index.html').open('w') as fp: + fp.write(DALS( + ''' + <!DOCTYPE html><html><body> + <a href="{dist_sdist}" rel="internal">{dist_sdist}</a><br/> + </body></html> + ''' + ).format(dist_sdist=dist_sdist)) + sdist_with_index('barbazquux', '3.2.0') + sdist_with_index('barbazquux-runner', '2.11.1') + with tmpdir.join('setup.cfg').open('w') as fp: + fp.write(DALS( + ''' + [easy_install] + index_url = {index_url} + ''' + ).format(index_url=index_url)) + reqs = ''' + barbazquux-runner + barbazquux + '''.split() + with tmpdir.as_cwd(): + dist = Distribution() + resolved_dists = [ + dist.fetch_build_egg(r) + for r in reqs + ] + assert [dist.key for dist in resolved_dists if dist] == reqs diff --git a/setuptools/tests/test_easy_install.py b/setuptools/tests/test_easy_install.py index 6f9bc8e1..e4ed556f 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -14,15 +14,12 @@ import itertools import distutils.errors import io import zipfile +import mock import time from setuptools.extern.six.moves import urllib import pytest -try: - from unittest import mock -except ImportError: - import mock from setuptools import sandbox from setuptools.sandbox import run_setup @@ -33,6 +30,7 @@ from setuptools.dist import Distribution from pkg_resources import normalize_path, working_set from pkg_resources import Distribution as PRDistribution import setuptools.tests.server +from setuptools.tests import fail_on_ascii import pkg_resources from . import contexts @@ -67,7 +65,7 @@ class TestEasyInstallTest: def test_get_script_args(self): header = ei.CommandSpec.best().from_environment().as_header() - expected = header + DALS(""" + expected = header + DALS(r""" # EASY-INSTALL-ENTRY-SCRIPT: 'spec','console_scripts','name' __requires__ = 'spec' import re @@ -168,10 +166,7 @@ class TestEasyInstallTest: sdist_zip.close() return str(sdist) - @pytest.mark.xfail(reason="#709 and #710") - # also - #@pytest.mark.xfail(setuptools.tests.is_ascii, - # reason="https://github.com/pypa/setuptools/issues/706") + @fail_on_ascii def test_unicode_filename_in_sdist(self, sdist_unicode, tmpdir, monkeypatch): """ The install command should execute correctly even if @@ -571,18 +566,6 @@ def create_setup_requires_package(path, distname='foobar', version='0.1', return test_pkg -def make_trivial_sdist(dist_path, setup_py): - """Create a simple sdist tarball at dist_path, containing just a - setup.py, the contents of which are provided by the setup_py string. - """ - - setup_py_file = tarfile.TarInfo(name='setup.py') - setup_py_bytes = io.BytesIO(setup_py.encode('utf-8')) - setup_py_file.size = len(setup_py_bytes.getvalue()) - with tarfile.open(dist_path, 'w:gz') as dist: - dist.addfile(setup_py_file, fileobj=setup_py_bytes) - - @pytest.mark.skipif( sys.platform.startswith('java') and ei.is_sh(sys.executable), reason="Test cannot run under java when executable is sh" diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 7bf6b68a..4c04d298 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -1,3 +1,5 @@ +import sys +import ast import os import glob import re @@ -81,9 +83,8 @@ class TestEggInfo(object): assert '[egg_info]' in content assert 'tag_build =' in content assert 'tag_date = 0' in content - assert 'tag_svn_revision = 0' in content - expected_order = 'tag_build', 'tag_date', 'tag_svn_revision' + expected_order = 'tag_build', 'tag_date', self._validate_content_order(content, expected_order) @@ -109,7 +110,6 @@ class TestEggInfo(object): [egg_info] tag_build = tag_date = 0 - tag_svn_revision = 0 """), }) dist = Distribution() @@ -123,9 +123,8 @@ class TestEggInfo(object): assert '[egg_info]' in content assert 'tag_build =' in content assert 'tag_date = 0' in content - assert 'tag_svn_revision = 0' in content - expected_order = 'tag_build', 'tag_date', 'tag_svn_revision' + expected_order = 'tag_build', 'tag_date', self._validate_content_order(content, expected_order) @@ -160,48 +159,262 @@ class TestEggInfo(object): sources_txt = os.path.join(egg_info_dir, 'SOURCES.txt') assert 'docs/usage.rst' in open(sources_txt).read().split('\n') - def _setup_script_with_requires(self, requires_line): - setup_script = DALS(""" + def _setup_script_with_requires(self, requires, use_setup_cfg=False): + setup_script = DALS( + ''' from setuptools import setup - setup( - name='foo', - %s - zip_safe=False, - ) - """ % requires_line) - build_files({ - 'setup.py': setup_script, - }) + setup(name='foo', zip_safe=False, %s) + ''' + ) % ('' if use_setup_cfg else requires) + setup_config = requires if use_setup_cfg else '' + build_files({'setup.py': setup_script, + 'setup.cfg': setup_config}) + + mismatch_marker = "python_version<'{this_ver}'".format( + this_ver=sys.version_info[0], + ) + # Alternate equivalent syntax. + mismatch_marker_alternate = 'python_version < "{this_ver}"'.format( + this_ver=sys.version_info[0], + ) + invalid_marker = "<=>++" + + class RequiresTestHelper(object): + + @staticmethod + def parametrize(*test_list, **format_dict): + idlist = [] + argvalues = [] + for test in test_list: + test_params = test.lstrip().split('\n\n', 3) + name_kwargs = test_params.pop(0).split('\n') + if len(name_kwargs) > 1: + install_cmd_kwargs = ast.literal_eval(name_kwargs[1].strip()) + else: + install_cmd_kwargs = {} + name = name_kwargs[0].strip() + setup_py_requires, setup_cfg_requires, expected_requires = ( + DALS(a).format(**format_dict) for a in test_params + ) + for id_, requires, use_cfg in ( + (name, setup_py_requires, False), + (name + '_in_setup_cfg', setup_cfg_requires, True), + ): + idlist.append(id_) + marks = () + if requires.startswith('@xfail\n'): + requires = requires[7:] + marks = pytest.mark.xfail + argvalues.append(pytest.param(requires, use_cfg, + expected_requires, + install_cmd_kwargs, + marks=marks)) + return pytest.mark.parametrize('requires,use_setup_cfg,' + 'expected_requires,install_cmd_kwargs', + argvalues, ids=idlist) + + @RequiresTestHelper.parametrize( + # Format of a test: + # + # id + # install_cmd_kwargs [optional] + # + # requires block (when used in setup.py) + # + # requires block (when used in setup.cfg) + # + # expected contents of requires.txt + + ''' + install_requires_with_marker + + install_requires=["barbazquux;{mismatch_marker}"], + + [options] + install_requires = + barbazquux; {mismatch_marker} + + [:{mismatch_marker_alternate}] + barbazquux + ''', + + ''' + install_requires_with_extra + {'cmd': ['egg_info']} + + install_requires=["barbazquux [test]"], + + [options] + install_requires = + barbazquux [test] + + barbazquux[test] + ''', + + ''' + install_requires_with_extra_and_marker + + install_requires=["barbazquux [test]; {mismatch_marker}"], + + [options] + install_requires = + barbazquux [test]; {mismatch_marker} + + [:{mismatch_marker_alternate}] + barbazquux[test] + ''', + + ''' + setup_requires_with_markers + + setup_requires=["barbazquux;{mismatch_marker}"], + + [options] + setup_requires = + barbazquux; {mismatch_marker} + + ''', + + ''' + tests_require_with_markers + {'cmd': ['test'], 'output': "Ran 0 tests in"} + + tests_require=["barbazquux;{mismatch_marker}"], + + [options] + tests_require = + barbazquux; {mismatch_marker} + + ''', + + ''' + extras_require_with_extra + {'cmd': ['egg_info']} - def test_install_requires_with_markers(self, tmpdir_cwd, env): - self._setup_script_with_requires( - """install_requires=["barbazquux;python_version<'2'"],""") - self._run_install_command(tmpdir_cwd, env) - egg_info_dir = self._find_egg_info_files(env.paths['lib']).base + extras_require={{"extra": ["barbazquux [test]"]}}, + + [options.extras_require] + extra = barbazquux [test] + + [extra] + barbazquux[test] + ''', + + ''' + extras_require_with_extra_and_marker_in_req + + extras_require={{"extra": ["barbazquux [test]; {mismatch_marker}"]}}, + + [options.extras_require] + extra = + barbazquux [test]; {mismatch_marker} + + [extra] + + [extra:{mismatch_marker_alternate}] + barbazquux[test] + ''', + + # FIXME: ConfigParser does not allow : in key names! + ''' + extras_require_with_marker + + extras_require={{":{mismatch_marker}": ["barbazquux"]}}, + + @xfail + [options.extras_require] + :{mismatch_marker} = barbazquux + + [:{mismatch_marker}] + barbazquux + ''', + + ''' + extras_require_with_marker_in_req + + extras_require={{"extra": ["barbazquux; {mismatch_marker}"]}}, + + [options.extras_require] + extra = + barbazquux; {mismatch_marker} + + [extra] + + [extra:{mismatch_marker_alternate}] + barbazquux + ''', + + ''' + extras_require_with_empty_section + + extras_require={{"empty": []}}, + + [options.extras_require] + empty = + + [empty] + ''', + # Format arguments. + invalid_marker=invalid_marker, + mismatch_marker=mismatch_marker, + mismatch_marker_alternate=mismatch_marker_alternate, + ) + def test_requires(self, tmpdir_cwd, env, + requires, use_setup_cfg, + expected_requires, install_cmd_kwargs): + self._setup_script_with_requires(requires, use_setup_cfg) + self._run_install_command(tmpdir_cwd, env, **install_cmd_kwargs) + egg_info_dir = os.path.join('.', 'foo.egg-info') requires_txt = os.path.join(egg_info_dir, 'requires.txt') - assert "barbazquux;python_version<'2'" in open( - requires_txt).read().split('\n') + if os.path.exists(requires_txt): + with open(requires_txt) as fp: + install_requires = fp.read() + else: + install_requires = '' + assert install_requires.lstrip() == expected_requires assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] - def test_setup_requires_with_markers(self, tmpdir_cwd, env): - self._setup_script_with_requires( - """setup_requires=["barbazquux;python_version<'2'"],""") - self._run_install_command(tmpdir_cwd, env) + def test_extras_require_with_invalid_marker(self, tmpdir_cwd, env): + tmpl = 'extras_require={{":{marker}": ["barbazquux"]}},' + req = tmpl.format(marker=self.invalid_marker) + self._setup_script_with_requires(req) + with pytest.raises(AssertionError): + self._run_install_command(tmpdir_cwd, env) assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] - def test_tests_require_with_markers(self, tmpdir_cwd, env): - self._setup_script_with_requires( - """tests_require=["barbazquux;python_version<'2'"],""") - self._run_install_command( - tmpdir_cwd, env, cmd=['test'], output="Ran 0 tests in") + def test_extras_require_with_invalid_marker_in_req(self, tmpdir_cwd, env): + tmpl = 'extras_require={{"extra": ["barbazquux; {marker}"]}},' + req = tmpl.format(marker=self.invalid_marker) + self._setup_script_with_requires(req) + with pytest.raises(AssertionError): + self._run_install_command(tmpdir_cwd, env) assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] - def test_extra_requires_with_markers(self, tmpdir_cwd, env): + def test_long_description_content_type(self, tmpdir_cwd, env): + # Test that specifying a `long_description_content_type` keyword arg to + # the `setup` function results in writing a `Description-Content-Type` + # line to the `PKG-INFO` file in the `<distribution>.egg-info` + # directory. + # `Description-Content-Type` is described at + # https://github.com/pypa/python-packaging-user-guide/pull/258 + self._setup_script_with_requires( - """extra_requires={":python_version<'2'": ["barbazquux"]},""") - self._run_install_command(tmpdir_cwd, env) - assert glob.glob(os.path.join(env.paths['lib'], 'barbazquux*')) == [] + """long_description_content_type='text/markdown',""") + environ = os.environ.copy().update( + HOME=env.paths['home'], + ) + code, data = environment.run_setup_py( + cmd=['egg_info'], + pypath=os.pathsep.join([env.paths['lib'], str(tmpdir_cwd)]), + data_stream=1, + env=environ, + ) + egg_info_dir = os.path.join('.', 'foo.egg-info') + with open(os.path.join(egg_info_dir, 'PKG-INFO')) as pkginfo_file: + pkg_info_lines = pkginfo_file.read().split('\n') + expected_line = 'Description-Content-Type: text/markdown' + assert expected_line in pkg_info_lines def test_python_requires_egg_info(self, tmpdir_cwd, env): self._setup_script_with_requires( diff --git a/setuptools/tests/test_integration.py b/setuptools/tests/test_integration.py index 78fb0627..3a9a6c50 100644 --- a/setuptools/tests/test_integration.py +++ b/setuptools/tests/test_integration.py @@ -98,3 +98,68 @@ def test_pbr(install_context): def test_python_novaclient(install_context): _install_one('python-novaclient', install_context, 'novaclient', 'base.py') + + +def test_pyuri(install_context): + """ + Install the pyuri package (version 0.3.1 at the time of writing). + + This is also a regression test for issue #1016. + """ + _install_one('pyuri', install_context, 'pyuri', 'uri.py') + + pyuri = install_context.installed_projects['pyuri'] + + # The package data should be installed. + assert os.path.exists(os.path.join(pyuri.location, 'pyuri', 'uri.regex')) + + +import re +import subprocess +import functools +import tarfile, zipfile + + +build_deps = ['appdirs', 'packaging', 'pyparsing', 'six'] +@pytest.mark.parametrize("build_dep", build_deps) +@pytest.mark.skipif(sys.version_info < (3, 6), reason='run only on late versions') +def test_build_deps_on_distutils(request, tmpdir_factory, build_dep): + """ + All setuptools build dependencies must build without + setuptools. + """ + if 'pyparsing' in build_dep: + pytest.xfail(reason="Project imports setuptools unconditionally") + build_target = tmpdir_factory.mktemp('source') + build_dir = download_and_extract(request, build_dep, build_target) + install_target = tmpdir_factory.mktemp('target') + output = install(build_dir, install_target) + for line in output.splitlines(): + match = re.search('Unknown distribution option: (.*)', line) + allowed_unknowns = [ + 'test_suite', + 'tests_require', + 'install_requires', + ] + assert not match or match.group(1).strip('"\'') in allowed_unknowns + + +def install(pkg_dir, install_dir): + with open(os.path.join(pkg_dir, 'setuptools.py'), 'w') as breaker: + breaker.write('raise ImportError()') + cmd = [sys.executable, 'setup.py', 'install', '--prefix', install_dir] + env = dict(os.environ, PYTHONPATH=pkg_dir) + output = subprocess.check_output(cmd, cwd=pkg_dir, env=env, stderr=subprocess.STDOUT) + return output.decode('utf-8') + + +def download_and_extract(request, req, target): + cmd = [sys.executable, '-m', 'pip', 'download', '--no-deps', + '--no-binary', ':all:', req] + output = subprocess.check_output(cmd, encoding='utf-8') + filename = re.search('Saved (.*)', output).group(1) + request.addfinalizer(functools.partial(os.remove, filename)) + opener = zipfile.ZipFile if filename.endswith('.zip') else tarfile.open + with opener(filename) as archive: + archive.extractall(target) + return os.path.join(target, os.listdir(target)[0]) diff --git a/setuptools/tests/test_manifest.py b/setuptools/tests/test_manifest.py index 62b6d708..65eec7d9 100644 --- a/setuptools/tests/test_manifest.py +++ b/setuptools/tests/test_manifest.py @@ -6,9 +6,11 @@ import os import shutil import sys import tempfile +import itertools from distutils import log from distutils.errors import DistutilsTemplateError +import pkg_resources.py31compat from setuptools.command.egg_info import FileList, egg_info, translate_pattern from setuptools.dist import Distribution from setuptools.extern import six @@ -65,32 +67,94 @@ default_files = frozenset(map(make_local_path, [ ])) -def get_pattern(glob): - return translate_pattern(make_local_path(glob)).pattern - - -def test_translated_pattern_test(): - l = make_local_path - assert get_pattern('foo') == r'foo\Z(?ms)' - assert get_pattern(l('foo/bar')) == l(r'foo\/bar\Z(?ms)') +translate_specs = [ + ('foo', ['foo'], ['bar', 'foobar']), + ('foo/bar', ['foo/bar'], ['foo/bar/baz', './foo/bar', 'foo']), # Glob matching - assert get_pattern('*.txt') == l(r'[^\/]*\.txt\Z(?ms)') - assert get_pattern('dir/*.txt') == l(r'dir\/[^\/]*\.txt\Z(?ms)') - assert get_pattern('*/*.py') == l(r'[^\/]*\/[^\/]*\.py\Z(?ms)') - assert get_pattern('docs/page-?.txt') \ - == l(r'docs\/page\-[^\/]\.txt\Z(?ms)') + ('*.txt', ['foo.txt', 'bar.txt'], ['foo/foo.txt']), + ('dir/*.txt', ['dir/foo.txt', 'dir/bar.txt', 'dir/.txt'], ['notdir/foo.txt']), + ('*/*.py', ['bin/start.py'], []), + ('docs/page-?.txt', ['docs/page-9.txt'], ['docs/page-10.txt']), # Globstars change what they mean depending upon where they are - assert get_pattern(l('foo/**/bar')) == l(r'foo\/(?:[^\/]+\/)*bar\Z(?ms)') - assert get_pattern(l('foo/**')) == l(r'foo\/.*\Z(?ms)') - assert get_pattern(l('**')) == r'.*\Z(?ms)' + ( + 'foo/**/bar', + ['foo/bing/bar', 'foo/bing/bang/bar', 'foo/bar'], + ['foo/abar'], + ), + ( + 'foo/**', + ['foo/bar/bing.py', 'foo/x'], + ['/foo/x'], + ), + ( + '**', + ['x', 'abc/xyz', '@nything'], + [], + ), # Character classes - assert get_pattern('pre[one]post') == r'pre[one]post\Z(?ms)' - assert get_pattern('hello[!one]world') == r'hello[^one]world\Z(?ms)' - assert get_pattern('[]one].txt') == r'[\]one]\.txt\Z(?ms)' - assert get_pattern('foo[!]one]bar') == r'foo[^\]one]bar\Z(?ms)' + ( + 'pre[one]post', + ['preopost', 'prenpost', 'preepost'], + ['prepost', 'preonepost'], + ), + + ( + 'hello[!one]world', + ['helloxworld', 'helloyworld'], + ['hellooworld', 'helloworld', 'hellooneworld'], + ), + + ( + '[]one].txt', + ['o.txt', '].txt', 'e.txt'], + ['one].txt'], + ), + + ( + 'foo[!]one]bar', + ['fooybar'], + ['foo]bar', 'fooobar', 'fooebar'], + ), + +] +""" +A spec of inputs for 'translate_pattern' and matches and mismatches +for that input. +""" + +match_params = itertools.chain.from_iterable( + zip(itertools.repeat(pattern), matches) + for pattern, matches, mismatches in translate_specs +) + + +@pytest.fixture(params=match_params) +def pattern_match(request): + return map(make_local_path, request.param) + + +mismatch_params = itertools.chain.from_iterable( + zip(itertools.repeat(pattern), mismatches) + for pattern, matches, mismatches in translate_specs +) + + +@pytest.fixture(params=mismatch_params) +def pattern_mismatch(request): + return map(make_local_path, request.param) + + +def test_translated_pattern_match(pattern_match): + pattern, target = pattern_match + assert translate_pattern(pattern).match(target) + + +def test_translated_pattern_mismatch(pattern_mismatch): + pattern, target = pattern_mismatch + assert not translate_pattern(pattern).match(target) class TempDirTestCase(object): @@ -206,6 +270,15 @@ class TestManifestTest(TempDirTestCase): l('app/static/app.css'), l('app/static/app.css.map')]) assert files == self.get_files() + def test_graft_glob_syntax(self): + """Include the whole app/static/ directory.""" + l = make_local_path + self.make_manifest("graft */static") + files = default_files | set([ + l('app/static/app.js'), l('app/static/app.js.map'), + l('app/static/app.css'), l('app/static/app.css.map')]) + assert files == self.get_files() + def test_graft_global_exclude(self): """Exclude all *.map files in the project.""" l = make_local_path @@ -289,8 +362,7 @@ class TestFileListTest(TempDirTestCase): for file in files: file = os.path.join(self.temp_dir, file) dirname, basename = os.path.split(file) - if not os.path.exists(dirname): - os.makedirs(dirname) + pkg_resources.py31compat.makedirs(dirname, exist_ok=True) open(file, 'w').close() def test_process_template_line(self): @@ -449,11 +521,6 @@ class TestFileListTest(TempDirTestCase): assert file_list.files == ['a.py', l('d/c.py')] self.assertWarnings() - file_list.process_template_line('global-include .txt') - file_list.sort() - assert file_list.files == ['a.py', 'b.txt', l('d/c.py')] - self.assertNoWarnings() - def test_global_exclude(self): l = make_local_path # global-exclude @@ -470,13 +537,6 @@ class TestFileListTest(TempDirTestCase): assert file_list.files == ['b.txt'] self.assertWarnings() - file_list = FileList() - file_list.files = ['a.py', 'b.txt', l('d/c.pyc'), 'e.pyo'] - file_list.process_template_line('global-exclude .py[co]') - file_list.sort() - assert file_list.files == ['a.py', 'b.txt'] - self.assertNoWarnings() - def test_recursive_include(self): l = make_local_path # recursive-include diff --git a/setuptools/tests/test_msvc.py b/setuptools/tests/test_msvc.py index a0c76ea0..32d7a907 100644 --- a/setuptools/tests/test_msvc.py +++ b/setuptools/tests/test_msvc.py @@ -5,12 +5,9 @@ Tests for msvc support module. import os import contextlib import distutils.errors +import mock import pytest -try: - from unittest import mock -except ImportError: - import mock from . import contexts diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py index 28c5e9de..721cad1e 100644 --- a/setuptools/tests/test_namespaces.py +++ b/setuptools/tests/test_namespaces.py @@ -7,12 +7,13 @@ import subprocess import pytest from . import namespaces +from setuptools.command import test class TestNamespaces: - @pytest.mark.xfail(sys.version_info < (3, 3), - reason="Requires PEP 420") + @pytest.mark.xfail(sys.version_info < (3, 5), + reason="Requires importlib.util.module_from_spec") @pytest.mark.skipif(bool(os.environ.get("APPVEYOR")), reason="https://github.com/pypa/setuptools/issues/851") def test_mixed_site_and_non_site(self, tmpdir): @@ -27,7 +28,6 @@ class TestNamespaces: site_packages = tmpdir / 'site-packages' path_packages = tmpdir / 'path-packages' targets = site_packages, path_packages - python_path = os.pathsep.join(map(str, targets)) # use pip to install to the target directory install_cmd = [ 'pip', @@ -48,5 +48,58 @@ class TestNamespaces: sys.executable, '-c', 'import myns.pkgA; import myns.pkgB', ] - env = dict(PYTHONPATH=python_path) - subprocess.check_call(try_import, env=env) + with test.test.paths_on_pythonpath(map(str, targets)): + subprocess.check_call(try_import) + + @pytest.mark.skipif(bool(os.environ.get("APPVEYOR")), + reason="https://github.com/pypa/setuptools/issues/851") + def test_pkg_resources_import(self, tmpdir): + """ + Ensure that a namespace package doesn't break on import + of pkg_resources. + """ + pkg = namespaces.build_namespace_package(tmpdir, 'myns.pkgA') + target = tmpdir / 'packages' + target.mkdir() + install_cmd = [ + sys.executable, + '-m', 'easy_install', + '-d', str(target), + str(pkg), + ] + with test.test.paths_on_pythonpath([str(target)]): + subprocess.check_call(install_cmd) + namespaces.make_site_dir(target) + try_import = [ + sys.executable, + '-c', 'import pkg_resources', + ] + with test.test.paths_on_pythonpath([str(target)]): + subprocess.check_call(try_import) + + @pytest.mark.skipif(bool(os.environ.get("APPVEYOR")), + reason="https://github.com/pypa/setuptools/issues/851") + def test_namespace_package_installed_and_cwd(self, tmpdir): + """ + Installing a namespace packages but also having it in the current + working directory, only one version should take precedence. + """ + pkg_A = namespaces.build_namespace_package(tmpdir, 'myns.pkgA') + target = tmpdir / 'packages' + # use pip to install to the target directory + install_cmd = [ + 'pip', + 'install', + str(pkg_A), + '-t', str(target), + ] + subprocess.check_call(install_cmd) + namespaces.make_site_dir(target) + + # ensure that package imports and pkg_resources imports + pkg_resources_imp = [ + sys.executable, + '-c', 'import pkg_resources; import myns.pkgA', + ] + with test.test.paths_on_pythonpath([str(target)]): + subprocess.check_call(pkg_resources_imp, cwd=str(pkg_A)) diff --git a/setuptools/tests/test_packageindex.py b/setuptools/tests/test_packageindex.py index f09dd78c..53e20d44 100644 --- a/setuptools/tests/test_packageindex.py +++ b/setuptools/tests/test_packageindex.py @@ -181,6 +181,48 @@ class TestPackageIndex: res = setuptools.package_index.local_open(url) assert 'content' in res.read() + def test_egg_fragment(self): + """ + EGG fragments must comply to PEP 440 + """ + epoch = [ + '', + '1!', + ] + releases = [ + '0', + '0.0', + '0.0.0', + ] + pre = [ + 'a0', + 'b0', + 'rc0', + ] + post = [ + '.post0' + ] + dev = [ + '.dev0', + ] + local = [ + ('', ''), + ('+ubuntu.0', '+ubuntu.0'), + ('+ubuntu-0', '+ubuntu.0'), + ('+ubuntu_0', '+ubuntu.0'), + ] + versions = [ + [''.join([e, r, p, l]) for l in ll] + for e in epoch + for r in releases + for p in sum([pre, post, dev], ['']) + for ll in local] + for v, vc in versions: + dists = list(setuptools.package_index.distros_for_url( + 'http://example.com/example.zip#egg=example-' + v)) + assert dists[0].version == '' + assert dists[1].version == vc + class TestContentCheckers: def test_md5(self): diff --git a/setuptools/tests/test_sandbox.py b/setuptools/tests/test_sandbox.py index 929f0a5b..a3f1206d 100644 --- a/setuptools/tests/test_sandbox.py +++ b/setuptools/tests/test_sandbox.py @@ -7,13 +7,12 @@ import pytest import pkg_resources import setuptools.sandbox -from setuptools.sandbox import DirectorySandbox class TestSandbox: def test_devnull(self, tmpdir): - sandbox = DirectorySandbox(str(tmpdir)) - sandbox.run(self._file_writer(os.devnull)) + with setuptools.sandbox.DirectorySandbox(str(tmpdir)): + self._file_writer(os.devnull) @staticmethod def _file_writer(path): @@ -116,13 +115,17 @@ class TestExceptionSaver: with open('/etc/foo', 'w'): pass - sandbox = DirectorySandbox(str(tmpdir)) with pytest.raises(setuptools.sandbox.SandboxViolation) as caught: with setuptools.sandbox.save_modules(): setuptools.sandbox.hide_setuptools() - sandbox.run(write_file) + with setuptools.sandbox.DirectorySandbox(str(tmpdir)): + write_file() cmd, args, kwargs = caught.value.args assert cmd == 'open' assert args == ('/etc/foo', 'w') assert kwargs == {} + + msg = str(caught.value) + assert 'open' in msg + assert "('/etc/foo', 'w')" in msg diff --git a/setuptools/tests/test_upload_docs.py b/setuptools/tests/test_upload_docs.py index 5d50bb0b..a26e32a6 100644 --- a/setuptools/tests/test_upload_docs.py +++ b/setuptools/tests/test_upload_docs.py @@ -64,6 +64,8 @@ class TestUploadDocsTest: ) body, content_type = upload_docs._build_multipart(data) assert 'form-data' in content_type + assert "b'" not in content_type + assert 'b"' not in content_type assert isinstance(body, bytes) assert b'foo' in body assert b'content' in body diff --git a/setuptools/tests/test_virtualenv.py b/setuptools/tests/test_virtualenv.py new file mode 100644 index 00000000..17b8793c --- /dev/null +++ b/setuptools/tests/test_virtualenv.py @@ -0,0 +1,116 @@ +import glob +import os + +from pytest import yield_fixture +from pytest_fixture_config import yield_requires_config + +import pytest_virtualenv + +from .textwrap import DALS +from .test_easy_install import make_nspkg_sdist + + +@yield_requires_config(pytest_virtualenv.CONFIG, ['virtualenv_executable']) +@yield_fixture(scope='function') +def bare_virtualenv(): + """ Bare virtualenv (no pip/setuptools/wheel). + """ + with pytest_virtualenv.VirtualEnv(args=( + '--no-wheel', + '--no-pip', + '--no-setuptools', + )) as venv: + yield venv + + +SOURCE_DIR = os.path.join(os.path.dirname(__file__), '../..') + +def test_clean_env_install(bare_virtualenv): + """ + Check setuptools can be installed in a clean environment. + """ + bare_virtualenv.run(' && '.join(( + 'cd {source}', + 'python setup.py install', + )).format(source=SOURCE_DIR)) + +def test_pip_upgrade_from_source(virtualenv): + """ + Check pip can upgrade setuptools from source. + """ + dist_dir = virtualenv.workspace + # Generate source distribution / wheel. + virtualenv.run(' && '.join(( + 'cd {source}', + 'python setup.py -q sdist -d {dist}', + 'python setup.py -q bdist_wheel -d {dist}', + )).format(source=SOURCE_DIR, dist=dist_dir)) + sdist = glob.glob(os.path.join(dist_dir, '*.zip'))[0] + wheel = glob.glob(os.path.join(dist_dir, '*.whl'))[0] + # Then update from wheel. + virtualenv.run('pip install ' + wheel) + # And finally try to upgrade from source. + virtualenv.run('pip install --no-cache-dir --upgrade ' + sdist) + +def test_test_command_install_requirements(bare_virtualenv, tmpdir): + """ + Check the test command will install all required dependencies. + """ + bare_virtualenv.run(' && '.join(( + 'cd {source}', + 'python setup.py develop', + )).format(source=SOURCE_DIR)) + def sdist(distname, version): + dist_path = tmpdir.join('%s-%s.tar.gz' % (distname, version)) + make_nspkg_sdist(str(dist_path), distname, version) + return dist_path + dependency_links = [ + str(dist_path) + for dist_path in ( + sdist('foobar', '2.4'), + sdist('bits', '4.2'), + sdist('bobs', '6.0'), + sdist('pieces', '0.6'), + ) + ] + with tmpdir.join('setup.py').open('w') as fp: + fp.write(DALS( + ''' + from setuptools import setup + + setup( + dependency_links={dependency_links!r}, + install_requires=[ + 'barbazquux1; sys_platform in ""', + 'foobar==2.4', + ], + setup_requires='bits==4.2', + tests_require=""" + bobs==6.0 + """, + extras_require={{ + 'test': ['barbazquux2'], + ':"" in sys_platform': 'pieces==0.6', + ':python_version > "1"': """ + pieces + foobar + """, + }} + ) + '''.format(dependency_links=dependency_links))) + with tmpdir.join('test.py').open('w') as fp: + fp.write(DALS( + ''' + import foobar + import bits + import bobs + import pieces + + open('success', 'w').close() + ''')) + # Run test command for test package. + bare_virtualenv.run(' && '.join(( + 'cd {tmpdir}', + 'python setup.py test -s test', + )).format(tmpdir=tmpdir)) + assert tmpdir.join('success').check() diff --git a/tests/requirements.txt b/tests/requirements.txt index d07e9cde..4761505f 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -1,4 +1,6 @@ -pytest-flake8 +importlib; python_version<"2.7" +mock +pytest-flake8; python_version>="2.7" +virtualenv>=13.0.0 +pytest-virtualenv>=1.2.7 pytest>=3.0.2 -setuptools[ssl] -backports.unittest_mock>=1.2 @@ -1,4 +1,11 @@ +# Note: Run "python bootstrap.py" before running Tox, to generate metadata. +# +# To run Tox against all supported Python interpreters, you can set: +# +# export TOXENV='py2{6,7},py3{3,4,5,6},pypy' + [testenv] deps=-rtests/requirements.txt passenv=APPDATA USERPROFILE HOMEDRIVE HOMEPATH windir APPVEYOR -commands=python -m pytest {posargs:-rsx} +commands=py.test {posargs} +usedevelop=True |
