diff options
52 files changed, 2308 insertions, 401 deletions
diff --git a/.travis.yml b/.travis.yml index 006316d1..b402cf6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,7 +8,6 @@ python: - "3.6-dev" - nightly - pypy - - pypy3 env: - "" - LC_ALL=C LC_CTYPE=C @@ -24,8 +23,6 @@ script: #- python -m tox - tox -before_deploy: - - export SETUPTOOLS_INSTALL_WINDOWS_SPECIFIC_FILES=1 deploy: provider: pypi # Also update server in setup.cfg @@ -39,3 +36,4 @@ deploy: password: secure: tfWrsQMH2bHrWjqnP+08IX1WlkbW94Q30f4d7lCyhWS1FIf/jBDx4jrEILNfMxQ1NCwuBRje5sihj1Ow0BFf0vVrkaeff2IdvnNDEGFduMejaEQJL3s3QrLfpiAvUbtqwyWaHfAdGfk48PovDKTx0ZTvXZKYGXZhxGCYSlG2CE6Y6RDvnEl6Tk8e+LqUohkcSOwxrRwUoyxSnUaavdGohXxDT8MJlfWOXgr2u+KsRrriZqp3l6Fdsnk4IGvy6pXpy42L1HYQyyVu9XyJilR2JTbC6eCp5f8p26093m1Qas49+t6vYb0VLqQe12dO+Jm3v4uztSS5pPQzS7PFyjEYd2Rdb6ijsdbsy1074S4q7G9Sz+T3RsPUwYEJ07lzez8cxP64dtj5j94RL8m35A1Fb1OE8hHN+4c1yLG1gudfXbem+fUhi2eqhJrzQo5vsvDv1xS5x5GIS5ZHgKHCsWcW1Tv+dsFkrhaup3uU6VkOuc9UN+7VPsGEY7NvquGpTm8O1CnGJRzuJg6nbYRGj8ORwDpI0KmrExx6akV92P72fMC/I5TCgbSQSZn370H3Jj40gz1SM30WAli9M+wFHFd4ddMVY65yxj0NLmrP+m1tvnWdKtNh/RHuoW92d9/UFtiA5IhMf1/3djfsjBq6S9NT1uaLkVkTttqrPYJ7hOql8+g= distributions: release + skip_upload_docs: true diff --git a/CHANGES.rst b/CHANGES.rst index 400e9094..9d8e2e29 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,6 +1,182 @@ -======= -CHANGES -======= +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 + <https://setuptools.readthedocs.io/en/latest/setuptools.html#configuring-setup-using-setup-cfg-files>`_. + +v30.2.1 +------- + +* #850: In test command, invoke unittest.main with + indication not to exit the process. + +v30.2.0 +------- + +* #854: Bump to vendored Packaging 16.8. + +v30.1.0 +------- + +* #846: Also trap 'socket.error' when opening URLs in + package_index. + +* #849: Manifest processing now matches the filename + pattern anywhere in the filename and not just at the + start. Restores behavior found prior to 28.5.0. + +v30.0.0 +------- + +* #864: Drop support for Python 3.2. Systems requiring + Python 3.2 support must use 'setuptools < 30'. + +* #825: Suppress warnings for single files. + +* #830 via #843: Once again restored inclusion of data + files to sdists, but now trap TypeError caused by + techniques employed rjsmin and similar. + +v29.0.1 +------- + +* #861: Re-release of v29.0.1 with the executable script + launchers bundled. Now, launchers are included by default + and users that want to disable this behavior must set the + environment variable + 'SETUPTOOLS_INSTALL_WINDOWS_SPECIFIC_FILES' to + a false value like "false" or "0". + +v29.0.0 +------- + +* #841: Drop special exception for packages invoking + win32com during the build/install process. See + Distribute #118 for history. + +v28.8.0 +------- + +* #629: Per the discussion, refine the sorting to use version + value order for more accurate detection of the latest + available version when scanning for packages. See also + #829. + +* #837: Rely on the config var "SO" for Python 3.3.0 only + when determining the ext filename. + +v28.7.1 +------- + +* #827: Update PyPI root for dependency links. + +* #833: Backed out changes from #830 as the implementation + seems to have problems in some cases. + +v28.7.0 +------- + +* #832: Moved much of the namespace package handling + functionality into a separate module for re-use in something + like #789. +* #830: ``sdist`` command no longer suppresses the inclusion + of data files, re-aligning with the expectation of distutils + and addressing #274 and #521. v28.7.1 ------- @@ -27,7 +203,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 ------- @@ -534,7 +714,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 ---- @@ -614,7 +794,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 @@ -1368,7 +1548,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. @@ -2788,7 +2968,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 @@ -5,7 +5,7 @@ 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 ------------------------- @@ -126,8 +126,8 @@ 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 +.. _EasyInstall manual: https://setuptools.readthedocs.io/en/latest/easy_install.html +.. _the section on custom installation locations: https://setuptools.readthedocs.io/en/latest/easy_install.html#custom-installation-locations Downloads @@ -139,10 +139,10 @@ 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 +is available from the `GitHub 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 +.. _GitHub repo: https://github.com/pypa/setuptools/archive/master.tar.gz#egg=setuptools-dev .. _0.6 branch: http://svn.python.org/projects/sandbox/branches/setuptools-0.6/#egg=setuptools-dev06 Uninstalling diff --git a/appveyor.yml b/appveyor.yml index 299c35b7..9313a482 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,5 +1,7 @@ environment: + APPVEYOR: true + matrix: - PYTHON: "C:\\Python35-x64" - PYTHON: "C:\\Python27-x64" diff --git a/conftest.py b/conftest.py index a513bb9e..3cccfe1a 100644 --- a/conftest.py +++ b/conftest.py @@ -1 +1,8 @@ pytest_plugins = 'setuptools.tests.fixtures' + + +def pytest_addoption(parser): + parser.addoption( + "--package_name", action="append", default=[], + help="list of package_name to pass to test functions", + ) 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..c1854ed8 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -157,6 +157,10 @@ link_files = { url='https://www.python.org/dev/peps/pep-{pep_number:0>4}/', ), dict( + 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/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 7b979ec3..e8412b33 100644 --- a/docs/pkg_resources.txt +++ b/docs/pkg_resources.txt @@ -831,10 +831,9 @@ correspond exactly to the constructor argument names: ``name``, ``module_name``, ``attrs``, ``extras``, and ``dist`` are all available. In addition, the following methods are provided: -``load(require=True, env=None, installer=None)`` - Load the entry point, returning the advertised Python object, or raise - ``ImportError`` if it cannot be obtained. If `require` is a true value, - then ``require(env, installer)`` is called before attempting the import. +``load()`` + Load the entry point, returning the advertised Python object. Effectively + calls ``self.require()`` then returns ``self.resolve()``. ``require(env=None, installer=None)`` Ensure that any "extras" needed by the entry point are available on @@ -846,6 +845,10 @@ addition, the following methods are provided: taking a ``Requirement`` instance and returning a matching importable ``Distribution`` instance or None. +``resolve()`` + Resolve the entry point from its module and attrs, returning the advertised + Python object. Raises ``ImportError`` if it cannot be obtained. + ``__str__()`` The string form of an ``EntryPoint`` is a string that could be passed to ``EntryPoint.parse()`` to produce an equivalent ``EntryPoint``. diff --git a/docs/setuptools.txt b/docs/setuptools.txt index 84a9aa75..10bf7dc4 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 ==================== @@ -1428,10 +1454,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 @@ -1563,68 +1585,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 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -1638,18 +1598,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:: @@ -1709,8 +1669,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.:: @@ -1720,7 +1679,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 @@ -2007,27 +1966,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. @@ -2341,67 +2279,210 @@ password from the keyring. New in 20.1: Added keyring support. -.. _upload_docs: -``upload_docs`` - Upload package documentation to PyPI -====================================================== +----------------------------------------- +Configuring setup() using setup.cfg files +----------------------------------------- -PyPI now supports uploading project documentation to the dedicated URL -https://pythonhosted.org/<project>/. +``Setuptools`` allows using configuration files (usually `setup.cfg`) +to define package’s metadata and other options which are normally supplied +to ``setup()`` function. -The ``upload_docs`` command will create the necessary zip file out of a -documentation directory and will post to the repository. +This approach not only allows automation scenarios, but also reduces +boilerplate code in some cases. -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. +.. note:: + Implementation presents limited compatibility with distutils2-like + ``setup.cfg`` sections (used by ``pbr`` and ``d2to1`` packages). -Assuming there is an ``Example`` project with documentation in the -subdirectory ``docs``, e.g.:: + Namely: only metadata related keys from ``metadata`` section are supported + (except for ``description-file``); keys from ``files``, ``entry_points`` + and ``backwards_compat`` are not supported. - 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:: +.. code-block:: ini - python setup.py upload_docs --upload-dir=docs/build/html + [metadata] + name = my_package + version = attr: src.VERSION + description = My package description + long_description = file: README.rst + keywords = one, two + license = BSD 3-Clause License -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. + [metadata.classifiers] + Framework :: Django + Programming Language :: Python :: 3.5 -As with other ``setuptools``-based commands, you can define useful -defaults in the ``setup.cfg`` of your Python project, e.g.: + [options] + zip_safe = False + include_package_data = True + packages = find: + scripts = + bin/first.py + bin/second.py -.. code-block:: ini + [options.package_data] + * = *.txt, *.rst + hello = *.msg + + [options.extras_require] + 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. + +* Keys are the same as keyword arguments one provides to ``setup()`` function. + +* Complex values could be placed comma-separated or one per line + in *dangling* sections. The following are the same: + + .. code-block:: ini + + [metadata] + keywords = one, two + + [metadata] + keywords = + one + two + +* In some cases complex values could be provided in subsections for clarity. + +* Some keys allow ``file:``, ``attr:`` and ``find:`` directives to cover + common usecases. + +* Unknown keys are ignored. + + +Specifying values +================= + +Some values are treated as simple strings, some allow more logic. + +Type names used below: + +* ``str`` - simple string +* ``list-comma`` - dangling list or comma-separated values string +* ``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 + + +.. note:: + ``file:`` directive is sandboxed and won't reach anything outside + directory with ``setup.py``. + + +Metadata +-------- + +.. note:: + 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 +================= ================= ===== + +.. note:: + + **version** - ``attr:`` supports callables; supports iterables; + unsupported types are casted using ``str()``. + + +Options +------- + +======================= ===== +Key Accepted value type +======================= ===== +zip_safe bool +setup_requires list-semi +install_requires list-semi +extras_require section +entry_points file:, section +use_2to3 bool +use_2to3_fixers list-comma +use_2to3_exclude_fixers list-comma +convert_2to3_doctests list-comma +scripts list-comma +eager_resources list-comma +dependency_links list-comma +tests_require list-semi +include_package_data bool +packages find:, list-comma +package_dir dict +package_data section +exclude_package_data section +namespace_packages 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 +================= + +Some automation tools may wish to access data from a configuration file. + +``Setuptools`` exposes ``read_configuration()`` function allowing +parsing ``metadata`` and ``options`` sections into a dictionary. + + +.. code-block:: python - [upload_docs] - upload-dir = docs/build/html + from setuptools.config import read_configuration -The ``upload_docs`` command has the following options: + conf_dict = read_configuration('/home/user/dev/package/setup.cfg') -``--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. +By default ``read_configuration()`` will read only file provided +in the first argument. To include values from other configuration files +which could be in various places set `find_others` function argument +to ``True``. -``--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). +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. -------------------------------- diff --git a/pkg_resources/__init__.py b/pkg_resources/__init__.py index 12226d4b..4c9868c7 100644 --- a/pkg_resources/__init__.py +++ b/pkg_resources/__init__.py @@ -36,6 +36,7 @@ import plistlib import email.parser import tempfile import textwrap +import itertools from pkgutil import get_importer try: @@ -74,11 +75,7 @@ __import__('pkg_resources.extern.packaging.requirements') __import__('pkg_resources.extern.packaging.markers') if (3, 0) < sys.version_info < (3, 3): - msg = ( - "Support for Python 3.0-3.2 has been dropped. Future versions " - "will fail here." - ) - warnings.warn(msg) + raise RuntimeError("Python 3.3 or later is required") # declare some globals that will be defined later to # satisfy the linters. @@ -1966,6 +1963,32 @@ def find_nothing(importer, path_item, only=False): register_finder(object, find_nothing) +def _by_version_descending(names): + """ + Given a list of filenames, return them in descending order + by version number. + + >>> names = 'bar', 'foo', 'Python-2.7.10.egg', 'Python-2.7.2.egg' + >>> _by_version_descending(names) + ['Python-2.7.10.egg', 'Python-2.7.2.egg', 'foo', 'bar'] + >>> names = 'Setuptools-1.2.3b1.egg', 'Setuptools-1.2.3.egg' + >>> _by_version_descending(names) + ['Setuptools-1.2.3.egg', 'Setuptools-1.2.3b1.egg'] + >>> names = 'Setuptools-1.2.3b1.egg', 'Setuptools-1.2.3.post1.egg' + >>> _by_version_descending(names) + ['Setuptools-1.2.3.post1.egg', 'Setuptools-1.2.3b1.egg'] + """ + def _by_version(name): + """ + Parse each component of the filename + """ + name, ext = os.path.splitext(name) + parts = itertools.chain(name.split('-'), [ext]) + return [packaging.version.parse(part) for part in parts] + + return sorted(names, key=_by_version, reverse=True) + + def find_on_path(importer, path_item, only=False): """Yield distributions accessible on a sys.path directory""" path_item = _normalize_cached(path_item) @@ -1979,11 +2002,7 @@ def find_on_path(importer, path_item, only=False): ) else: # scan for .egg and .egg-info in directory - - path_item_entries = os.listdir(path_item) - # Reverse so we find the newest version of a distribution, - path_item_entries.sort() - path_item_entries.reverse() + 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'): @@ -2094,6 +2113,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] @@ -2986,9 +3009,11 @@ def _initialize(g=globals()): "Set up global resource manager (deliberately not state-saved)" manager = ResourceManager() g['_manager'] = manager - for name in dir(manager): - if not name.startswith('_'): - g[name] = getattr(manager, name) + g.update( + (name, getattr(manager, name)) + for name in dir(manager) + if not name.startswith('_') + ) @_call_aside @@ -3017,10 +3042,10 @@ def _initialize_master_working_set(): # ensure that all distributions added to the working set in the future # (e.g. by calling ``require()``) will get activated as well, # with higher priority (replace=True). - dist = None # ensure dist is defined for del dist below - for dist in working_set: + tuple( dist.activate(replace=False) - del dist + for dist in working_set + ) add_activation_listener(lambda dist: dist.activate(replace=True), existing=False) working_set.entries = [] # match order diff --git a/pkg_resources/_vendor/packaging/__about__.py b/pkg_resources/_vendor/packaging/__about__.py index c21a758b..95d330ef 100644 --- a/pkg_resources/_vendor/packaging/__about__.py +++ b/pkg_resources/_vendor/packaging/__about__.py @@ -12,7 +12,7 @@ __title__ = "packaging" __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "16.7" +__version__ = "16.8" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" diff --git a/pkg_resources/_vendor/packaging/markers.py b/pkg_resources/_vendor/packaging/markers.py index c5d29cd9..892e578e 100644 --- a/pkg_resources/_vendor/packaging/markers.py +++ b/pkg_resources/_vendor/packaging/markers.py @@ -52,13 +52,26 @@ class Node(object): def __repr__(self): return "<{0}({1!r})>".format(self.__class__.__name__, str(self)) + def serialize(self): + raise NotImplementedError + class Variable(Node): - pass + + def serialize(self): + return str(self) class Value(Node): - pass + + def serialize(self): + return '"{0}"'.format(self) + + +class Op(Node): + + def serialize(self): + return str(self) VARIABLE = ( @@ -103,6 +116,7 @@ VERSION_CMP = ( ) MARKER_OP = VERSION_CMP | L("not in") | L("in") +MARKER_OP.setParseAction(lambda s, l, t: Op(t[0])) MARKER_VALUE = QuotedString("'") | QuotedString('"') MARKER_VALUE.setParseAction(lambda s, l, t: Value(t[0])) @@ -149,7 +163,7 @@ def _format_marker(marker, first=True): else: return "(" + " ".join(inner) + ")" elif isinstance(marker, tuple): - return '{0} {1} "{2}"'.format(*marker) + return " ".join([m.serialize() for m in marker]) else: return marker @@ -168,13 +182,13 @@ _operators = { def _eval_op(lhs, op, rhs): try: - spec = Specifier("".join([op, rhs])) + spec = Specifier("".join([op.serialize(), rhs])) except InvalidSpecifier: pass else: return spec.contains(lhs) - oper = _operators.get(op) + oper = _operators.get(op.serialize()) if oper is None: raise UndefinedComparison( "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs) diff --git a/pkg_resources/_vendor/vendored.txt b/pkg_resources/_vendor/vendored.txt index 6b5eb450..9a94c5bc 100644 --- a/pkg_resources/_vendor/vendored.txt +++ b/pkg_resources/_vendor/vendored.txt @@ -1,4 +1,4 @@ -packaging==16.7 +packaging==16.8 pyparsing==2.1.10 six==1.10.0 appdirs==1.4.0 @@ -1,5 +1,5 @@ [pytest] -addopts=--doctest-modules --ignore release.py --ignore setuptools/lib2to3_ex.py --ignore tests/manual_test.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 norecursedirs=dist build *.egg setuptools/extern pkg_resources/extern .* flake8-ignore = setuptools/site-patch.py F821 @@ -1,5 +1,5 @@ [bumpversion] -current_version = 28.6.1 +current_version = 33.0.0 commit = True tag = True @@ -8,17 +8,16 @@ 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] universal = 1 @@ -54,8 +54,8 @@ package_data = dict( ) force_windows_specific_files = ( - os.environ.get("SETUPTOOLS_INSTALL_WINDOWS_SPECIFIC_FILES") - not in (None, "", "0") + os.environ.get("SETUPTOOLS_INSTALL_WINDOWS_SPECIFIC_FILES", "1").lower() + not in ("", "0", "false", "no") ) include_windows_files = ( @@ -77,7 +77,7 @@ def pypi_link(pkg_filename): Given the filename, including md5 fragment, construct the dependency link for PyPI. """ - root = 'https://pypi.python.org/packages/source' + root = 'https://files.pythonhosted.org/packages/source' name, sep, rest = pkg_filename.partition('-') parts = root, name[0], name, pkg_filename return '/'.join(parts) @@ -85,7 +85,7 @@ def pypi_link(pkg_filename): setup_params = dict( name="setuptools", - version="28.6.1", + version="33.0.0", description="Easily download, build, install, upgrade, and uninstall " "Python packages", author="Python Packaging Authority", @@ -156,6 +156,7 @@ setup_params = dict( Topic :: System :: Systems Administration Topic :: Utilities """).strip().splitlines(), + python_requires='>=2.6,!=3.0.*,!=3.1.*,!=3.2.*', extras_require={ "ssl:sys_platform=='win32'": "wincertstore==0.2", "certs": "certifi==2016.9.26", diff --git a/setuptools/command/build_ext.py b/setuptools/command/build_ext.py index 12dc88cd..36f53f0d 100644 --- a/setuptools/command/build_ext.py +++ b/setuptools/command/build_ext.py @@ -62,7 +62,7 @@ if_dl = lambda s: s if have_rtld else '' def get_abi3_suffix(): """Return the file extension for an abi3-compliant Extension()""" for suffix, _, _ in (s for s in imp.get_suffixes() if s[2] == imp.C_EXTENSION): - if '.abi3' in suffix: # Unix + if '.abi3' in suffix: # Unix return suffix elif suffix == '.pyd': # Windows return suffix @@ -109,7 +109,7 @@ class build_ext(_build_ext): and get_abi3_suffix() ) if use_abi3: - so_ext = get_config_var('EXT_SUFFIX') + so_ext = _get_config_var_837('EXT_SUFFIX') filename = filename[:-len(so_ext)] filename = filename + get_abi3_suffix() if isinstance(ext, Library): @@ -316,3 +316,13 @@ else: self.create_static_lib( objects, basename, output_dir, debug, target_lang ) + + +def _get_config_var_837(name): + """ + In https://github.com/pypa/setuptools/pull/837, we discovered + Python 3.3.0 exposes the extension suffix under the name 'SO'. + """ + if sys.version_info < (3, 3, 1): + name = 'SO' + return get_config_var(name) diff --git a/setuptools/command/build_py.py b/setuptools/command/build_py.py index 289e6fb8..b0314fd4 100644 --- a/setuptools/command/build_py.py +++ b/setuptools/command/build_py.py @@ -219,7 +219,7 @@ class build_py(orig.build_py, Mixin2to3): @staticmethod def _get_platform_patterns(spec, package, src_dir): """ - yield platfrom-specific path patterns (suitable for glob + yield platform-specific path patterns (suitable for glob or fn_match) from a glob-based spec (such as self.package_data or self.exclude_package_data) matching package in src_dir. diff --git a/setuptools/command/develop.py b/setuptools/command/develop.py index 3eb86120..aa82f959 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() @@ -123,6 +125,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..36e7f359 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 ( @@ -627,12 +628,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 +673,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 diff --git a/setuptools/command/egg_info.py b/setuptools/command/egg_info.py index 6cc8f4c4..ca6a4348 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): """ @@ -126,18 +121,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 +137,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. """ # python 2.6 compatibility @@ -165,7 +166,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): @@ -282,22 +282,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") @@ -554,10 +542,17 @@ class manifest_maker(sdist): msg = "writing manifest file '%s'" % self.manifest self.execute(write_file, (self.manifest, files), msg) - def warn(self, msg): # suppress missing-file warnings from sdist - if not msg.startswith("standard file not found:"): + def warn(self, msg): + if not self._should_suppress_warning(msg): sdist.warn(self, msg) + @staticmethod + def _should_suppress_warning(msg): + """ + suppress missing-file warnings from sdist + """ + return re.match(r"standard file .*not found", msg) + def add_defaults(self): sdist.add_defaults(self) self.filelist.append(self.template) diff --git a/setuptools/command/install_egg_info.py b/setuptools/command/install_egg_info.py index 7834e107..edc4718b 100755 --- a/setuptools/command/install_egg_info.py +++ b/setuptools/command/install_egg_info.py @@ -1,14 +1,13 @@ from distutils import log, dir_util import os -from setuptools.extern.six.moves import map - from setuptools import Command +from setuptools import namespaces from setuptools.archive_util import unpack_archive import pkg_resources -class install_egg_info(Command): +class install_egg_info(namespaces.Installer, Command): """Install an .egg-info directory for the package""" description = "Install an .egg-info directory for the package" @@ -61,59 +60,3 @@ class install_egg_info(Command): return dst unpack_archive(self.source, self.target, skimmer) - - def install_namespaces(self): - nsp = self._get_all_ns_packages() - if not nsp: - return - filename, ext = os.path.splitext(self.target) - filename += '-nspkg.pth' - self.outputs.append(filename) - log.info("Installing %s", filename) - lines = map(self._gen_nspkg_line, nsp) - - if self.dry_run: - # always generate the lines, even in dry run - list(lines) - return - - with open(filename, 'wt') as f: - f.writelines(lines) - - _nspkg_tmpl = ( - "import sys, types, os", - "pep420 = sys.version_info > (3, 3)", - "p = os.path.join(sys._getframe(1).f_locals['sitedir'], *%(pth)r)", - "ie = os.path.exists(os.path.join(p,'__init__.py'))", - "m = not ie and not pep420 and " - "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)", - ) - "lines for the namespace installer" - - _nspkg_tmpl_multi = ( - 'm and setattr(sys.modules[%(parent)r], %(child)r, m)', - ) - "additional line(s) when a parent package is indicated" - - @classmethod - def _gen_nspkg_line(cls, pkg): - # ensure pkg is not a unicode string under Python 2.7 - pkg = str(pkg) - pth = tuple(pkg.split('.')) - tmpl_lines = cls._nspkg_tmpl - parent, sep, child = pkg.rpartition('.') - if parent: - tmpl_lines += cls._nspkg_tmpl_multi - return ';'.join(tmpl_lines) % locals() + '\n' - - def _get_all_ns_packages(self): - """Return sorted list of all package namespaces""" - nsp = set() - for pkg in self.distribution.namespace_packages or []: - pkg = pkg.split('.') - while pkg: - nsp.add('.'.join(pkg)) - pkg.pop() - return sorted(nsp) diff --git a/setuptools/command/sdist.py b/setuptools/command/sdist.py index 9975753d..84e29a1b 100755 --- a/setuptools/command/sdist.py +++ b/setuptools/command/sdist.py @@ -142,9 +142,13 @@ class sdist(sdist_add_defaults, orig.sdist): for filename in filenames]) def _add_defaults_data_files(self): - """ - Don't add any data files, but why? - """ + try: + if six.PY2: + sdist_add_defaults._add_defaults_data_files(self) + else: + super()._add_defaults_data_files() + except TypeError: + log.warn("data_files contains unexpected objects") def check_readme(self): for f in self.READMES: diff --git a/setuptools/command/test.py b/setuptools/command/test.py index 270674e2..ef0af12f 100644 --- a/setuptools/command/test.py +++ b/setuptools/command/test.py @@ -3,7 +3,8 @@ import operator import sys import contextlib import itertools -from distutils.errors import DistutilsOptionError +from distutils.errors import DistutilsError, DistutilsOptionError +from distutils import log from unittest import TestLoader from setuptools.extern import six @@ -225,11 +226,17 @@ class test(Command): del_modules.append(name) list(map(sys.modules.__delitem__, del_modules)) - unittest_main( + exit_kwarg = {} if sys.version_info < (2, 7) else {"exit": False} + test = unittest_main( None, None, self._argv, testLoader=self._resolve_as_ep(self.test_loader), testRunner=self._resolve_as_ep(self.test_runner), + **exit_kwarg ) + 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/config.py b/setuptools/config.py new file mode 100644 index 00000000..d71ff028 --- /dev/null +++ b/setuptools/config.py @@ -0,0 +1,558 @@ +from __future__ import absolute_import, unicode_literals +import io +import os +import sys +from collections import defaultdict +from functools import partial + +from distutils.errors import DistutilsOptionError, DistutilsFileError +from setuptools.py26compat import import_module +from setuptools.extern.six import string_types + + +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 + to get options from. + + :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 + + filepath = os.path.abspath(filepath) + + if not os.path.isfile(filepath): + raise DistutilsFileError( + 'Configuration file %s does not exist.' % filepath) + + current_directory = os.getcwd() + os.chdir(os.path.dirname(filepath)) + + try: + dist = Distribution() + + filenames = dist.find_config_files() if find_others else [] + if filepath not in filenames: + filenames.append(filepath) + + _Distribution.parse_config_files(dist, filenames=filenames) + + handlers = parse_configuration( + dist, dist.command_options, + ignore_option_errors=ignore_option_errors) + + finally: + os.chdir(current_directory) + + return configuration_to_dict(handlers) + + +def configuration_to_dict(handlers): + """Returns configuration data gathered by given handlers as a dict. + + :param list[ConfigHandler] handlers: Handlers list, + usually from parse_configuration() + + :rtype: dict + """ + config_dict = defaultdict(dict) + + for handler in handlers: + + obj_alias = handler.section_prefix + target_obj = handler.target_obj + + for option in handler.set_options: + getter = getattr(target_obj, 'get_%s' % option, None) + + if getter is None: + value = getattr(target_obj, option) + + else: + value = getter() + + config_dict[obj_alias][option] = value + + return config_dict + + +def parse_configuration( + distribution, command_options, ignore_option_errors=False): + """Performs additional parsing of configuration options + for a distribution. + + Returns a list of used option handlers. + + :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, ignore_option_errors) + meta.parse() + + options = ConfigOptionsHandler( + distribution, command_options, ignore_option_errors) + options.parse() + + return [meta, options] + + +class ConfigHandler(object): + """Handles metadata supplied in configuration files.""" + + section_prefix = None + """Prefix for config sections handled by this handler. + Must be provided by class heirs. + + """ + + aliases = {} + """Options aliases. + For compatibility with various packages. E.g.: d2to1 and pbr. + Note: `-` in keys is replaced with `_` by config parser. + + """ + + def __init__(self, target_obj, options, ignore_option_errors=False): + sections = {} + + section_prefix = self.section_prefix + for section_name, section_options in options.items(): + if not section_name.startswith(section_prefix): + continue + + 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 = [] + + @property + def parsers(self): + """Metadata item name to parser function mapping.""" + raise NotImplementedError( + '%s must provide .parsers property' % self.__class__.__name__) + + def __setitem__(self, option_name, value): + unknown = tuple() + target_obj = self.target_obj + + # Translate alias into real name. + option_name = self.aliases.get(option_name, option_name) + + current_value = getattr(target_obj, option_name, unknown) + + if current_value is unknown: + raise KeyError(option_name) + + if current_value: + # Already inhabited. Skipping. + return + + skip_option = False + parser = self.parsers.get(option_name) + if parser: + 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: + setattr(target_obj, option_name, value) + else: + setter(value) + + self.set_options.append(option_name) + + @classmethod + def _parse_list(cls, value, separator=','): + """Represents value as a list. + + Value is split either by separator (defaults to comma) or by lines. + + :param value: + :param separator: List items separator character. + :rtype: list + """ + if isinstance(value, list): # _get_parser_compound case + return value + + if '\n' in value: + value = value.splitlines() + else: + value = value.split(separator) + + return [chunk.strip() for chunk in value if chunk.strip()] + + @classmethod + def _parse_dict(cls, value): + """Represents value as a dict. + + :param value: + :rtype: dict + """ + separator = '=' + result = {} + for line in cls._parse_list(value): + key, sep, val = line.partition(separator) + if sep != separator: + raise DistutilsOptionError( + 'Unable to parse option value to dict: %s' % value) + result[key.strip()] = val.strip() + + return result + + @classmethod + def _parse_bool(cls, value): + """Represents value as boolean. + + :param value: + :rtype: bool + """ + value = value.lower() + return value in ('1', 'true', 'yes') + + @classmethod + def _parse_file(cls, value): + """Represents value as a string, allowing including text + from nearest files using `file:` directive. + + Directive is sandboxed and won't reach anything outside + directory with setup.py. + + Examples: + include: LICENSE + include: src/file.txt + + :param str value: + :rtype: str + """ + 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): + 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 + + @classmethod + def _parse_attr(cls, value): + """Represents value as a module attribute. + + Examples: + attr: package.attr + attr: package.module.attr + + :param str value: + :rtype: str + """ + attr_directive = 'attr:' + if not value.startswith(attr_directive): + return value + + attrs_path = value.replace(attr_directive, '').strip().split('.') + attr_name = attrs_path.pop() + + module_name = '.'.join(attrs_path) + module_name = module_name or '__init__' + + sys.path.insert(0, os.getcwd()) + try: + module = import_module(module_name) + value = getattr(module, attr_name) + + finally: + sys.path = sys.path[1:] + + return value + + @classmethod + def _get_parser_compound(cls, *parse_methods): + """Returns parser function to represents value as a list. + + Parses a value applying given methods one after another. + + :param parse_methods: + :rtype: callable + """ + def parse(value): + parsed = value + + for method in parse_methods: + parsed = method(parsed) + + return parsed + + return parse + + @classmethod + def _parse_section_to_dict(cls, section_options, values_parser=None): + """Parses section options into a dictionary. + + Optionally applies a given parser to values. + + :param dict section_options: + :param callable values_parser: + :rtype: dict + """ + value = {} + values_parser = values_parser or (lambda val: val) + for key, (_, val) in section_options.items(): + value[key] = values_parser(val) + return value + + def parse_section(self, section_options): + """Parses configuration file section. + + :param dict section_options: + """ + for (name, (_, value)) in section_options.items(): + try: + self[name] = value + + except KeyError: + pass # Keep silent for a new option may appear anytime. + + def parse(self): + """Parses configuration file items from one + or more related sections. + + """ + for section_name, section_options in self.sections.items(): + + method_postfix = '' + if section_name: # [section.option] variant + method_postfix = '_%s' % section_name + + section_parser_method = getattr( + self, + # Dots in section names are tranlsated into dunderscores. + ('parse_section%s' % method_postfix).replace('.', '__'), + None) + + if section_parser_method is None: + raise DistutilsOptionError( + 'Unsupported distribution option section: [%s.%s]' % ( + self.section_prefix, section_name)) + + section_parser_method(section_options) + + +class ConfigMetadataHandler(ConfigHandler): + + section_prefix = 'metadata' + + aliases = { + 'home_page': 'url', + 'summary': 'description', + 'classifier': 'classifiers', + 'platform': 'platforms', + } + + strict_mode = False + """We need to keep it loose, to be partially compatible with + `pbr` and `d2to1` packages which also uses `metadata` section. + + """ + + @property + def parsers(self): + """Metadata item name to parser function mapping.""" + parse_list = self._parse_list + parse_file = self._parse_file + + return { + 'platforms': parse_list, + 'keywords': parse_list, + 'provides': parse_list, + 'requires': parse_list, + 'obsoletes': parse_list, + 'classifiers': self._get_parser_compound(parse_file, parse_list), + 'license': parse_file, + 'description': parse_file, + 'long_description': parse_file, + '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. + + :param value: + :rtype: str + + """ + version = self._parse_attr(value) + + if callable(version): + version = version() + + if not isinstance(version, string_types): + if hasattr(version, '__iter__'): + version = '.'.join(map(str, version)) + else: + version = '%s' % version + + return version + + +class ConfigOptionsHandler(ConfigHandler): + + section_prefix = 'options' + + @property + def parsers(self): + """Metadata item name to parser function mapping.""" + parse_list = self._parse_list + parse_list_semicolon = partial(self._parse_list, separator=';') + parse_bool = self._parse_bool + parse_dict = self._parse_dict + + return { + 'zip_safe': parse_bool, + 'use_2to3': parse_bool, + 'include_package_data': parse_bool, + 'package_dir': parse_dict, + 'use_2to3_fixers': parse_list, + 'use_2to3_exclude_fixers': parse_list, + 'convert_2to3_doctests': parse_list, + 'scripts': parse_list, + 'eager_resources': parse_list, + 'dependency_links': parse_list, + 'namespace_packages': parse_list, + 'install_requires': parse_list_semicolon, + 'setup_requires': parse_list_semicolon, + 'tests_require': parse_list_semicolon, + 'packages': self._parse_packages, + 'entry_points': self._parse_file, + } + + def _parse_packages(self, value): + """Parses `packages` option value. + + :param value: + :rtype: list + """ + find_directive = 'find:' + + 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(**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. + + :param dict section_options: + """ + parsed = self._parse_section_to_dict(section_options, self._parse_list) + self['entry_points'] = parsed + + def _parse_package_data(self, section_options): + parsed = self._parse_section_to_dict(section_options, self._parse_list) + + root = parsed.get('*') + if root: + parsed[''] = root + del parsed['*'] + + return parsed + + def parse_section_package_data(self, section_options): + """Parses `package_data` configuration file section. + + :param dict section_options: + """ + self['package_data'] = self._parse_package_data(section_options) + + def parse_section_exclude_package_data(self, section_options): + """Parses `exclude_package_data` configuration file section. + + :param dict section_options: + """ + self['exclude_package_data'] = self._parse_package_data( + section_options) + + def parse_section_extras_require(self, section_options): + """Parses `extras_require` configuration file section. + + :param dict section_options: + """ + parse_list = partial(self._parse_list, separator=';') + self['extras_require'] = self._parse_section_to_dict( + section_options, parse_list) 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 8058ee7c..60cf6d9d 100644 --- a/setuptools/dist.py +++ b/setuptools/dist.py @@ -19,7 +19,9 @@ from pkg_resources.extern import packaging from setuptools.depends import Require 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 def _get_unpatched(cls): @@ -220,7 +222,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 @@ -350,6 +352,15 @@ class Distribution(_Distribution): if getattr(self, 'python_requires', None): self.metadata.python_requires = self.python_requires + def parse_config_files(self, filenames=None): + """Parses configuration files from various levels + and loads configuration. + + """ + _Distribution.parse_config_files(self, filenames=filenames) + + parse_configuration(self, self.command_options) + def parse_command_line(self): """Process features after parsing command line options""" result = _Distribution.parse_command_line(self) diff --git a/setuptools/monkey.py b/setuptools/monkey.py index aabc280f..dbe9a617 100644 --- a/setuptools/monkey.py +++ b/setuptools/monkey.py @@ -7,6 +7,7 @@ import distutils.filelist import platform import types import functools +import inspect from .py26compat import import_module from setuptools.extern import six @@ -35,12 +36,16 @@ 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 inspect.getmro(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(): diff --git a/setuptools/namespaces.py b/setuptools/namespaces.py new file mode 100755 index 00000000..0a889f22 --- /dev/null +++ b/setuptools/namespaces.py @@ -0,0 +1,108 @@ +import os +import sys +from distutils import log +import itertools + +from setuptools.extern.six.moves import map + + +flatten = itertools.chain.from_iterable + + +class Installer: + + nspkg_ext = '-nspkg.pth' + + def install_namespaces(self): + nsp = self._get_all_ns_packages() + if not nsp: + return + filename, ext = os.path.splitext(self._get_target()) + filename += self.nspkg_ext + self.outputs.append(filename) + log.info("Installing %s", filename) + lines = map(self._gen_nspkg_line, nsp) + + if self.dry_run: + # always generate the lines, even in dry run + list(lines) + return + + 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", + "has_mfs = sys.version_info > (3, 5)", + "p = os.path.join(%(root)s, *%(pth)r)", + "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 not has_mfs and " + "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)", + ) + "lines for the namespace installer" + + _nspkg_tmpl_multi = ( + 'm and setattr(sys.modules[%(parent)r], %(child)r, m)', + ) + "additional line(s) when a parent package is indicated" + + def _get_root(self): + return "sys._getframe(1).f_locals['sitedir']" + + def _gen_nspkg_line(self, pkg): + # ensure pkg is not a unicode string under Python 2.7 + pkg = str(pkg) + pth = tuple(pkg.split('.')) + root = self._get_root() + tmpl_lines = self._nspkg_tmpl + parent, sep, child = pkg.rpartition('.') + if parent: + tmpl_lines += self._nspkg_tmpl_multi + return ';'.join(tmpl_lines) % locals() + '\n' + + def _get_all_ns_packages(self): + """Return sorted list of all package namespaces""" + pkgs = self.distribution.namespace_packages or [] + return sorted(flatten(map(self._pkg_names, pkgs))) + + @staticmethod + def _pkg_names(pkg): + """ + Given a namespace package, yield the components of that + package. + + >>> names = Installer._pkg_names('a.b.c') + >>> set(names) == set(['a', 'a.b', 'a.b.c']) + True + """ + parts = pkg.split('.') + while parts: + yield '.'.join(parts) + parts.pop() + + +class DevelopInstaller(Installer): + def _get_root(self): + return repr(str(self.egg_path)) + + def _get_target(self): + return self.egg_link diff --git a/setuptools/package_index.py b/setuptools/package_index.py index e5249b27..d80d43bc 100755 --- a/setuptools/package_index.py +++ b/setuptools/package_index.py @@ -515,10 +515,10 @@ class PackageIndex(Environment): """Add `urls` to the list that will be prescanned for searches""" for url in urls: if ( - self.to_scan is None # if we have already "gone online" - or not URL_SCHEME(url) # or it's a local file/directory + self.to_scan is None # if we have already "gone online" + or not URL_SCHEME(url) # or it's a local file/directory or url.startswith('file:') - or list(distros_for_url(url)) # or a direct package link + or list(distros_for_url(url)) # or a direct package link ): # then go ahead and process it now self.scan_url(url) @@ -768,7 +768,7 @@ class PackageIndex(Environment): 'down, %s' % (url, v.line) ) - except http_client.HTTPException as v: + except (http_client.HTTPException, socket.error) as v: if warning: self.warn(warning, v) else: diff --git a/setuptools/py27compat.py b/setuptools/py27compat.py index 4e3e4ab3..a71a936e 100644 --- a/setuptools/py27compat.py +++ b/setuptools/py27compat.py @@ -3,6 +3,7 @@ Compatibility Support for Python 2.7 and earlier """ import sys +import platform def get_all_headers(message, key): @@ -16,3 +17,13 @@ if sys.version_info < (3,): def get_all_headers(message, key): return message.getheaders(key) + + +linux_py2_ascii = ( + platform.system() == 'Linux' and + sys.getfilesystemencoding() == 'ascii' and + sys.version_info < (3,) +) + +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..2588d680 --- /dev/null +++ b/setuptools/py33compat.py @@ -0,0 +1,46 @@ +import dis +import code +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 39afd57e..817a3afa 100755 --- a/setuptools/sandbox.py +++ b/setuptools/sandbox.py @@ -241,8 +241,15 @@ def run_setup(setup_script, args): working_set.__init__() working_set.callbacks.append(lambda dist: dist.activate()) + # __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()) + ) + def runner(): - ns = dict(__file__=setup_script, __name__='__main__') + ns = dict(__file__=dunder_file, __name__='__main__') _execfile(setup_script, ns) DirectorySandbox(setup_dir).run(runner) @@ -373,14 +380,6 @@ if hasattr(os, 'devnull'): else: _EXCEPTIONS = [] -try: - from win32com.client.gencache import GetGeneratePath - _EXCEPTIONS.append(GetGeneratePath()) - del GetGeneratePath -except ImportError: - # it appears pywin32 is not installed, so no need to exclude. - pass - class DirectorySandbox(AbstractSandbox): """Restrict operations to a single subdirectory - pseudo-chroot""" diff --git a/setuptools/site-patch.py b/setuptools/site-patch.py index 92194abd..0d2d2ff8 100644 --- a/setuptools/site-patch.py +++ b/setuptools/site-patch.py @@ -13,7 +13,7 @@ def __boot(): for item in stdpath: if item == mydir or not item: - continue # skip if current dir. on Windows, or my own directory + continue # skip if current dir. on Windows, or my own directory importer = pic.get(item) if importer is not None: loader = importer.find_module('site') diff --git a/setuptools/tests/environment.py b/setuptools/tests/environment.py index b0e3bd36..c67898ca 100644 --- a/setuptools/tests/environment.py +++ b/setuptools/tests/environment.py @@ -56,5 +56,5 @@ def run_setup_py(cmd, pypath=None, path=None, data = data.decode() data = unicodedata.normalize('NFC', data) - # communciate calls wait() + # communicate calls wait() return proc.returncode, data 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/namespaces.py b/setuptools/tests/namespaces.py new file mode 100644 index 00000000..ef5ecdad --- /dev/null +++ b/setuptools/tests/namespaces.py @@ -0,0 +1,42 @@ +from __future__ import absolute_import, unicode_literals + +import textwrap + + +def build_namespace_package(tmpdir, name): + src_dir = tmpdir / name + src_dir.mkdir() + setup_py = src_dir / 'setup.py' + namespace, sep, rest = name.partition('.') + script = textwrap.dedent(""" + import setuptools + setuptools.setup( + name={name!r}, + version="1.0", + namespace_packages=[{namespace!r}], + packages=[{namespace!r}], + ) + """).format(**locals()) + setup_py.write_text(script, encoding='utf-8') + ns_pkg_dir = src_dir / namespace + ns_pkg_dir.mkdir() + pkg_init = ns_pkg_dir / '__init__.py' + tmpl = '__import__("pkg_resources").declare_namespace({namespace!r})' + decl = tmpl.format(**locals()) + pkg_init.write_text(decl, encoding='utf-8') + pkg_mod = ns_pkg_dir / (rest + '.py') + some_functionality = 'name = {rest!r}'.format(**locals()) + pkg_mod.write_text(some_functionality, encoding='utf-8') + return src_dir + + +def make_site_dir(target): + """ + Add a sitecustomize.py module in target to cause + target to be added to site dirs such that .pth files + are processed there. + """ + sc = target / 'sitecustomize.py' + target_str = str(target) + tmpl = '__import__("site").addsitedir({target_str!r})' + sc.write_text(tmpl.format(**locals()), encoding='utf-8') diff --git a/setuptools/tests/test_config.py b/setuptools/tests/test_config.py new file mode 100644 index 00000000..fa8d523b --- /dev/null +++ b/setuptools/tests/test_config.py @@ -0,0 +1,539 @@ +import contextlib +import pytest +from distutils.errors import DistutilsOptionError, DistutilsFileError +from setuptools.dist import Distribution +from setuptools.config import ConfigHandler, read_configuration + + +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: + setup_py = ( + 'from setuptools import setup\n' + 'setup()\n' + ) + + tmpdir.join('setup.py').write(setup_py) + config = tmpdir.join('setup.cfg') + config.write(setup_cfg) + + package_dir, init_file = make_package_dir('fake_package', tmpdir) + + init_file.write( + 'VERSION = (1, 2, 3)\n' + '\n' + 'VERSION_MAJOR = 1' + '\n' + 'def get_version():\n' + ' return [3, 4, 5, "dev"]\n' + '\n' + ) + return package_dir, config + + +@contextlib.contextmanager +def get_dist(tmpdir, kwargs_initial=None, parse=True): + kwargs_initial = kwargs_initial or {} + + with tmpdir.as_cwd(): + dist = Distribution(kwargs_initial) + dist.script_name = 'setup.py' + parse and dist.parse_config_files() + + yield dist + + +def test_parsers_implemented(): + + with pytest.raises(NotImplementedError): + handler = ErrConfigHandler(None, {}) + handler.parsers + + +class TestConfigurationReader: + + def test_basic(self, tmpdir): + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'version = 10.1.1\n' + 'keywords = one, two\n' + '\n' + '[options]\n' + 'scripts = bin/a.py, bin/b.py\n' + ) + 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'] + + def test_no_config(self, tmpdir): + 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: + + def test_basic(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'version = 10.1.1\n' + 'description = Some description\n' + 'long_description = file: README\n' + 'name = fake_name\n' + 'keywords = one, two\n' + 'provides = package, package.sub\n' + 'license = otherlic\n' + 'download_url = http://test.test.com/test/\n' + 'maintainer_email = test@test.com\n' + ) + + tmpdir.join('README').write('readme contents\nline2') + + meta_initial = { + # This will be used so `otherlic` won't replace it. + 'license': 'BSD 3-Clause License', + } + + with get_dist(tmpdir, meta_initial) as dist: + metadata = dist.metadata + + assert metadata.version == '10.1.1' + assert metadata.description == 'Some description' + assert metadata.long_description == 'readme contents\nline2' + assert metadata.provides == ['package', 'package.sub'] + assert metadata.license == 'BSD 3-Clause License' + assert metadata.name == 'fake_name' + assert metadata.keywords == ['one', 'two'] + assert metadata.download_url == 'http://test.test.com/test/' + assert metadata.maintainer_email == 'test@test.com' + + def test_file_sandboxed(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'long_description = file: ../../README\n' + ) + + with get_dist(tmpdir, parse=False) as dist: + with pytest.raises(DistutilsOptionError): + dist.parse_config_files() # file: out of sandbox + + def test_aliases(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'author-email = test@test.com\n' + 'home-page = http://test.test.com/test/\n' + 'summary = Short summary\n' + 'platform = a, b\n' + 'classifier =\n' + ' Framework :: Django\n' + ' Programming Language :: Python :: 3.5\n' + ) + + with get_dist(tmpdir) as dist: + metadata = dist.metadata + assert metadata.author_email == 'test@test.com' + assert metadata.url == 'http://test.test.com/test/' + assert metadata.description == 'Short summary' + assert metadata.platforms == ['a', 'b'] + assert metadata.classifiers == [ + 'Framework :: Django', + 'Programming Language :: Python :: 3.5', + ] + + def test_multiline(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'name = fake_name\n' + 'keywords =\n' + ' one\n' + ' two\n' + 'classifiers =\n' + ' Framework :: Django\n' + ' Programming Language :: Python :: 3.5\n' + ) + with get_dist(tmpdir) as dist: + metadata = dist.metadata + assert metadata.keywords == ['one', 'two'] + assert metadata.classifiers == [ + 'Framework :: Django', + 'Programming Language :: Python :: 3.5', + ] + + def test_version(self, tmpdir): + + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'version = attr: fake_package.VERSION\n' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1.2.3' + + 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' + + config.write( + '[metadata]\n' + 'version = attr: fake_package.VERSION_MAJOR\n' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '1' + + subpack = tmpdir.join('fake_package').mkdir('subpackage') + subpack.join('__init__.py').write('') + subpack.join('submodule.py').write('VERSION = (2016, 11, 26)') + + config.write( + '[metadata]\n' + 'version = attr: fake_package.subpackage.submodule.VERSION\n' + ) + with get_dist(tmpdir) as dist: + assert dist.metadata.version == '2016.11.26' + + def test_unknown_meta_item(self, tmpdir): + + fake_env( + tmpdir, + '[metadata]\n' + 'name = fake_name\n' + 'unknown = some\n' + ) + with get_dist(tmpdir, parse=False) as dist: + dist.parse_config_files() # Skip unknown. + + def test_usupported_section(self, tmpdir): + + fake_env( + tmpdir, + '[metadata.some]\n' + 'key = val\n' + ) + with get_dist(tmpdir, parse=False) as dist: + with pytest.raises(DistutilsOptionError): + dist.parse_config_files() + + def test_classifiers(self, tmpdir): + expected = set([ + 'Framework :: Django', + 'Programming Language :: Python :: 3.5', + ]) + + # From file. + _, config = fake_env( + tmpdir, + '[metadata]\n' + 'classifiers = file: classifiers\n' + ) + + tmpdir.join('classifiers').write( + 'Framework :: Django\n' + 'Programming Language :: Python :: 3.5\n' + ) + + with get_dist(tmpdir) as dist: + assert set(dist.metadata.classifiers) == expected + + # From section. + config.write( + '[metadata.classifiers]\n' + 'Framework :: Django\n' + 'Programming Language :: Python :: 3.5\n' + ) + + with get_dist(tmpdir) as dist: + assert set(dist.metadata.classifiers) == expected + + +class TestOptions: + + def test_basic(self, tmpdir): + + fake_env( + tmpdir, + '[options]\n' + 'zip_safe = True\n' + 'use_2to3 = 1\n' + 'include_package_data = yes\n' + 'package_dir = b=c, =src\n' + 'packages = pack_a, pack_b.subpack\n' + 'namespace_packages = pack1, pack2\n' + 'use_2to3_fixers = your.fixers, or.here\n' + 'use_2to3_exclude_fixers = one.here, two.there\n' + 'convert_2to3_doctests = src/tests/one.txt, src/two.txt\n' + 'scripts = bin/one.py, bin/two.py\n' + 'eager_resources = bin/one.py, bin/two.py\n' + 'install_requires = docutils>=0.3; pack ==1.1, ==1.3; hey\n' + 'tests_require = mock==0.7.2; pytest\n' + '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' + ) + with get_dist(tmpdir) as dist: + assert dist.zip_safe + assert dist.use_2to3 + assert dist.include_package_data + assert dist.package_dir == {'': 'src', 'b': 'c'} + assert dist.packages == ['pack_a', 'pack_b.subpack'] + assert dist.namespace_packages == ['pack1', 'pack2'] + assert dist.use_2to3_fixers == ['your.fixers', 'or.here'] + assert dist.use_2to3_exclude_fixers == ['one.here', 'two.there'] + assert dist.convert_2to3_doctests == ([ + 'src/tests/one.txt', 'src/two.txt']) + assert dist.scripts == ['bin/one.py', 'bin/two.py'] + assert dist.dependency_links == ([ + 'http://some.com/here/1', + 'http://some.com/there/2' + ]) + assert dist.install_requires == ([ + 'docutils>=0.3', + 'pack ==1.1, ==1.3', + 'hey' + ]) + assert dist.setup_requires == ([ + 'docutils>=0.3', + 'spack ==1.1, ==1.3', + 'there' + ]) + assert dist.tests_require == ['mock==0.7.2', 'pytest'] + + def test_multiline(self, tmpdir): + fake_env( + tmpdir, + '[options]\n' + 'package_dir = \n' + ' b=c\n' + ' =src\n' + 'packages = \n' + ' pack_a\n' + ' pack_b.subpack\n' + 'namespace_packages = \n' + ' pack1\n' + ' pack2\n' + 'use_2to3_fixers = \n' + ' your.fixers\n' + ' or.here\n' + 'use_2to3_exclude_fixers = \n' + ' one.here\n' + ' two.there\n' + 'convert_2to3_doctests = \n' + ' src/tests/one.txt\n' + ' src/two.txt\n' + 'scripts = \n' + ' bin/one.py\n' + ' bin/two.py\n' + 'eager_resources = \n' + ' bin/one.py\n' + ' bin/two.py\n' + 'install_requires = \n' + ' docutils>=0.3\n' + ' pack ==1.1, ==1.3\n' + ' hey\n' + 'tests_require = \n' + ' mock==0.7.2\n' + ' pytest\n' + 'setup_requires = \n' + ' docutils>=0.3\n' + ' spack ==1.1, ==1.3\n' + ' there\n' + 'dependency_links = \n' + ' http://some.com/here/1\n' + ' http://some.com/there/2\n' + ) + with get_dist(tmpdir) as dist: + assert dist.package_dir == {'': 'src', 'b': 'c'} + assert dist.packages == ['pack_a', 'pack_b.subpack'] + assert dist.namespace_packages == ['pack1', 'pack2'] + assert dist.use_2to3_fixers == ['your.fixers', 'or.here'] + assert dist.use_2to3_exclude_fixers == ['one.here', 'two.there'] + assert dist.convert_2to3_doctests == ( + ['src/tests/one.txt', 'src/two.txt']) + assert dist.scripts == ['bin/one.py', 'bin/two.py'] + assert dist.dependency_links == ([ + 'http://some.com/here/1', + 'http://some.com/there/2' + ]) + assert dist.install_requires == ([ + 'docutils>=0.3', + 'pack ==1.1, ==1.3', + 'hey' + ]) + assert dist.setup_requires == ([ + 'docutils>=0.3', + 'spack ==1.1, ==1.3', + 'there' + ]) + assert dist.tests_require == ['mock==0.7.2', 'pytest'] + + def test_package_dir_fail(self, tmpdir): + fake_env( + tmpdir, + '[options]\n' + 'package_dir = a b\n' + ) + with get_dist(tmpdir, parse=False) as dist: + with pytest.raises(DistutilsOptionError): + dist.parse_config_files() + + def test_package_data(self, tmpdir): + fake_env( + tmpdir, + '[options.package_data]\n' + '* = *.txt, *.rst\n' + 'hello = *.msg\n' + '\n' + '[options.exclude_package_data]\n' + '* = fake1.txt, fake2.txt\n' + 'hello = *.dat\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.package_data == { + '': ['*.txt', '*.rst'], + 'hello': ['*.msg'], + } + assert dist.exclude_package_data == { + '': ['fake1.txt', 'fake2.txt'], + 'hello': ['*.dat'], + } + + def test_packages(self, tmpdir): + fake_env( + tmpdir, + '[options]\n' + 'packages = find:\n' + ) + + 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, + '[options.extras_require]\n' + 'pdf = ReportLab>=1.2; RXP\n' + 'rest = \n' + ' docutils>=0.3\n' + ' pack ==1.1, ==1.3\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.extras_require == { + 'pdf': ['ReportLab>=1.2', 'RXP'], + 'rest': ['docutils>=0.3', 'pack ==1.1, ==1.3'] + } + + def test_entry_points(self, tmpdir): + _, config = fake_env( + tmpdir, + '[options.entry_points]\n' + 'group1 = point1 = pack.module:func, ' + '.point2 = pack.module2:func_rest [rest]\n' + 'group2 = point3 = pack.module:func2\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.entry_points == { + 'group1': [ + 'point1 = pack.module:func', + '.point2 = pack.module2:func_rest [rest]', + ], + 'group2': ['point3 = pack.module:func2'] + } + + expected = ( + '[blogtool.parsers]\n' + '.rst = some.nested.module:SomeClass.some_classmethod[reST]\n' + ) + + tmpdir.join('entry_points').write(expected) + + # From file. + config.write( + '[options]\n' + 'entry_points = file: entry_points\n' + ) + + with get_dist(tmpdir) as dist: + assert dist.entry_points == expected 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..5dd72aae 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,56 @@ class TestDevelop: cmd.install_dir = tmpdir cmd.run() # assert '0.0' not in foocmd_text + + +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_easy_install.py b/setuptools/tests/test_easy_install.py index 209e6b78..1ea33b08 100644 --- a/setuptools/tests/test_easy_install.py +++ b/setuptools/tests/test_easy_install.py @@ -169,10 +169,6 @@ 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") def test_unicode_filename_in_sdist(self, sdist_unicode, tmpdir, monkeypatch): """ The install command should execute correctly even if diff --git a/setuptools/tests/test_egg_info.py b/setuptools/tests/test_egg_info.py index 12c10497..a32b981d 100644 --- a/setuptools/tests/test_egg_info.py +++ b/setuptools/tests/test_egg_info.py @@ -4,7 +4,7 @@ import re import stat import sys -from setuptools.command.egg_info import egg_info +from setuptools.command.egg_info import egg_info, manifest_maker from setuptools.dist import Distribution from setuptools.extern.six.moves import map @@ -88,9 +88,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) @@ -117,7 +116,6 @@ class TestEggInfo(object): [egg_info] tag_build = tag_date = 0 - tag_svn_revision = 0 """), }) dist = Distribution() @@ -131,9 +129,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) @@ -237,6 +234,15 @@ class TestEggInfo(object): pkginfo = os.path.join(egg_info_dir, 'PKG-INFO') assert 'Requires-Python: >=1.2.3' in open(pkginfo).read().split('\n') + def test_manifest_maker_warning_suppression(self): + fixtures = [ + "standard file not found: should have one of foo.py, bar.py", + "standard file 'setup.py' not found" + ] + + for msg in fixtures: + assert manifest_maker._should_suppress_warning(msg) + def _run_install_command(self, tmpdir_cwd, env, cmd=None, output=None): environ = os.environ.copy().update( HOME=env.paths['home'], diff --git a/setuptools/tests/test_namespaces.py b/setuptools/tests/test_namespaces.py new file mode 100644 index 00000000..721cad1e --- /dev/null +++ b/setuptools/tests/test_namespaces.py @@ -0,0 +1,105 @@ +from __future__ import absolute_import, unicode_literals + +import os +import sys +import subprocess + +import pytest + +from . import namespaces +from setuptools.command import test + + +class TestNamespaces: + + @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): + """ + Installing two packages sharing the same namespace, one installed + to a site dir and the other installed just to a path on PYTHONPATH + 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') + site_packages = tmpdir / 'site-packages' + path_packages = tmpdir / 'path-packages' + targets = site_packages, path_packages + # use pip to install to the target directory + install_cmd = [ + 'pip', + 'install', + str(pkg_A), + '-t', str(site_packages), + ] + subprocess.check_call(install_cmd) + namespaces.make_site_dir(site_packages) + install_cmd = [ + 'pip', + 'install', + str(pkg_B), + '-t', str(path_packages), + ] + subprocess.check_call(install_cmd) + try_import = [ + sys.executable, + '-c', 'import myns.pkgA; import myns.pkgB', + ] + 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_sandbox.py b/setuptools/tests/test_sandbox.py index b92a477a..929f0a5b 100644 --- a/setuptools/tests/test_sandbox.py +++ b/setuptools/tests/test_sandbox.py @@ -23,22 +23,6 @@ class TestSandbox: return do_write - def test_win32com(self, tmpdir): - """ - win32com should not be prevented from caching COM interfaces - in gen_py. - """ - win32com = pytest.importorskip('win32com') - gen_py = win32com.__gen_path__ - target = os.path.join(gen_py, 'test_write') - sandbox = DirectorySandbox(str(tmpdir)) - try: - # attempt to create gen_py file - sandbox.run(self._file_writer(target)) - finally: - if os.path.exists(target): - os.remove(target) - def test_setup_py_with_BOM(self): """ It should be possible to execute a setup.py with a Byte Order Mark diff --git a/setuptools/tests/test_sdist.py b/setuptools/tests/test_sdist.py index 609c7830..f34068dc 100644 --- a/setuptools/tests/test_sdist.py +++ b/setuptools/tests/test_sdist.py @@ -26,7 +26,8 @@ SETUP_ATTRS = { 'name': 'sdist_test', 'version': '0.0', 'packages': ['sdist_test'], - 'package_data': {'sdist_test': ['*.txt']} + 'package_data': {'sdist_test': ['*.txt']}, + 'data_files': [("data", [os.path.join("d", "e.dat")])], } SETUP_PY = """\ @@ -95,9 +96,12 @@ class TestSdistTest: # Set up the rest of the test package test_pkg = os.path.join(self.temp_dir, 'sdist_test') os.mkdir(test_pkg) + data_folder = os.path.join(self.temp_dir, "d") + os.mkdir(data_folder) # *.rst was not included in package_data, so c.rst should not be # automatically added to the manifest when not under version control - for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst']: + for fname in ['__init__.py', 'a.txt', 'b.txt', 'c.rst', + os.path.join(data_folder, "e.dat")]: # Just touch the files; their contents are irrelevant open(os.path.join(test_pkg, fname), 'w').close() @@ -126,6 +130,7 @@ class TestSdistTest: assert os.path.join('sdist_test', 'a.txt') in manifest assert os.path.join('sdist_test', 'b.txt') in manifest assert os.path.join('sdist_test', 'c.rst') not in manifest + assert os.path.join('d', 'e.dat') in manifest def test_defaults_case_sensitivity(self): """ diff --git a/setuptools/version.py b/setuptools/version.py index f2b40722..95e18696 100644 --- a/setuptools/version.py +++ b/setuptools/version.py @@ -1,6 +1,6 @@ import pkg_resources try: - __version__ = pkg_resources.require('setuptools')[0].version + __version__ = pkg_resources.get_distribution('setuptools').version except Exception: __version__ = 'unknown' diff --git a/tests/requirements.txt b/tests/requirements.txt new file mode 100644 index 00000000..d07e9cde --- /dev/null +++ b/tests/requirements.txt @@ -0,0 +1,4 @@ +pytest-flake8 +pytest>=3.0.2 +setuptools[ssl] +backports.unittest_mock>=1.2 diff --git a/tests/test_pypi.py b/tests/test_pypi.py new file mode 100644 index 00000000..b3425e53 --- /dev/null +++ b/tests/test_pypi.py @@ -0,0 +1,82 @@ +import os +import subprocess + +import virtualenv +from setuptools.extern.six.moves import http_client +from setuptools.extern.six.moves import xmlrpc_client + +TOP = 200 +PYPI_HOSTNAME = 'pypi.python.org' + + +def rpc_pypi(method, *args): + """Call an XML-RPC method on the Pypi server.""" + conn = http_client.HTTPSConnection(PYPI_HOSTNAME) + headers = {'Content-Type': 'text/xml'} + payload = xmlrpc_client.dumps(args, method) + + conn.request("POST", "/pypi", payload, headers) + response = conn.getresponse() + if response.status == 200: + result = xmlrpc_client.loads(response.read())[0][0] + return result + else: + raise RuntimeError("Unable to download the list of top " + "packages from Pypi.") + + +def get_top_packages(limit): + """Collect the name of the top packages on Pypi.""" + packages = rpc_pypi('top_packages') + return packages[:limit] + + +def _package_install(package_name, tmp_dir=None, local_setuptools=True): + """Try to install a package and return the exit status. + + This function creates a virtual environment, install setuptools using pip + and then install the required package. If local_setuptools is True, it + will install the local version of setuptools. + """ + package_dir = os.path.join(tmp_dir, "test_%s" % package_name) + if not local_setuptools: + package_dir = package_dir + "_baseline" + + virtualenv.create_environment(package_dir) + + pip_path = os.path.join(package_dir, "bin", "pip") + if local_setuptools: + subprocess.check_call([pip_path, "install", "."]) + returncode = subprocess.call([pip_path, "install", package_name]) + return returncode + + +def test_package_install(package_name, tmpdir): + """Test to verify the outcome of installing a package. + + This test compare that the return code when installing a package is the + same as with the current stable version of setuptools. + """ + new_exit_status = _package_install(package_name, tmp_dir=str(tmpdir)) + if new_exit_status: + print("Installation failed, testing against stable setuptools", + package_name) + old_exit_status = _package_install(package_name, tmp_dir=str(tmpdir), + local_setuptools=False) + assert new_exit_status == old_exit_status + + +def pytest_generate_tests(metafunc): + """Generator function for test_package_install. + + This function will generate calls to test_package_install. If a package + list has been specified on the command line, it will be used. Otherwise, + Pypi will be queried to get the current list of top packages. + """ + if "package_name" in metafunc.fixturenames: + if not metafunc.config.option.package_name: + packages = get_top_packages(TOP) + packages = [name for name, downloads in packages] + else: + packages = metafunc.config.option.package_name + metafunc.parametrize("package_name", packages) @@ -1,9 +1,5 @@ [testenv] -deps= - pytest-flake8 - pytest>=3.0.2 - setuptools[ssl] - backports.unittest_mock>=1.2 - -passenv=APPDATA USERPROFILE HOMEDRIVE HOMEPATH windir -commands=python -m pytest {posargs:-rsx} +deps=-rtests/requirements.txt +passenv=APPDATA USERPROFILE HOMEDRIVE HOMEPATH windir APPVEYOR +commands=py.test {posargs:-rsx} +usedevelop=True |