commit cf42014c4843b41c39675a1ba2232120230b43f4 Author: su-fang Date: Tue Feb 21 09:29:01 2023 +0800 Import Upstream version 1.8.2 diff --git a/.bzrignore b/.bzrignore new file mode 100644 index 0000000..ceb3376 --- /dev/null +++ b/.bzrignore @@ -0,0 +1,9 @@ +*.egg-info +./build +./dist +.coverage +__pycache__ +.tox +coverage.xml +nosetests.xml +docs/_build diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..1b7036e --- /dev/null +++ b/.coveragerc @@ -0,0 +1,3 @@ +[report] +show_missing=1 + diff --git a/CHANGES.txt b/CHANGES.txt new file mode 100644 index 0000000..4a377e3 --- /dev/null +++ b/CHANGES.txt @@ -0,0 +1,316 @@ +``pkginfo`` Changelog +===================== + +1.8.2 (2021-12-01) +------------------ + +- Add fix for installed distributions with '__package__' set to an empty + string. LP #1952946. + +1.8.1 (2021-11-19) +------------------ + +- Add 'MANIFEST.in' to ensure example files used by tests are included + in source distributions. LP #1951553. + +1.8.0 (2021-11-18) +------------------ + +- Support new standard metadata location for installed dists. LP #1865286. + +- Don't overwrite header-based 'description' with empty payload. LP #1885458. + +- Add support for Metadata-Version 2.2. LP #1928729. + +- Add support for uncompressed tarballs for sdists. LP #1951457. + +- Add support for Python 3.10. + +1.7.1 (2021-07-09) +------------------ + +- Use Python3 to build docs, and fix doctest examples to use Python3- + compatible syntax. LP #1933322. + +1.7.0 (2021-01-16) +------------------ + +- Add support for Python 3.9. + +- Drop support for Python 3.5. + +1.6.1 (2020-10-26) +------------------ + +- Adjust test classifiers to match supported Python versions. LP #1901127. + +1.6.0 (2020-10-20) +------------------ + +- Add support for Python 3.8. + LP #1869854. + +- Drop support for Python 3.4. + +- Update tests to match setuptools' change, no longer reporting metadata + version for installed packages w/o explicit metadata. LP #1870197. + +1.5.0.1 (2019-01-08) +-------------------- + +- Fix broken 'sdist'. LP #1639585. + +1.5.0 (2019-01-07) +------------------ + +- Fix 'console_scripts' entry point syntax. LP #1810734. + +- Add support for JSON output from the CLI. LP #1700580. + +- Add support for installed wheels. E.g., 'dist-info/' dirs. LP #1700200. + +- Harden metadata extraction against unexpected encodings. LP #1780454. + +- Update tests to match pip/setuptools' use of new metadata version. + LP #1772274. + +- Add support for Python 3.6 and 3.7. + +- Drop support for Python 3.3. + +1.4.2 (2018-03-14) +------------------ + +- Use relative imports in pkginfo modules. Supports vendoring of the + package into setuptools. + +- Add support for ``Provides-Extra`` and ``Description-Content-Type`` fields. + Per https://packaging.python.org/specifications/. See: PEP 566. + +- Remove support for old setuptools leaving ``PKG-INFO`` in the root of + the project directory. + +1.4.1 (2016-11-07) +------------------ + +- Packaging only change (invalid sdist built for 1.4.0). + +1.4.0 (2016-11-04) +------------------ + +- Relicense under MIT license: the PSF license is not suitable for + third-party libraries. + +1.3.2 (2016-05-24) +------------------ + +- Packaging-only change (automate fix for wheel built for 1.3.1). + +1.3.1 (2016-05-24) +------------------ + +- Packaging-only change (invalid wheel built for 1.3.0). + +1.3.0 (2016-05-23) +------------------ + +- Update homepage URL to point to Launchpad, rather than PyPI. + +- Add support for building wheels. + +- Add support for Python 3.5. + +- Drop support for Python 2.6 and 3.2. + +1.2.1 (2014-01-02) +------------------ + +- Add overlooked Trove classifier for Python 3.4. + +1.2 (2014-01-02) +---------------- + +- Add support for Python 3.4, PyPy3. + +- Add 100% coverage for ``pkginfo.commandline`` module. + +1.2b1 (2013-12-05) +------------------ + +- Add support for the "wheel" distribution format, along with minimal + metadata 2.0 support (not including new PEP 426 JSON properties). + Code (re-)borrowed from Donald Stuft's ``twine`` package. + +1.1 (2013-10-09) +---------------- + +- Fix tests to pass with current PyPy releases. + +1.1b1 (2013-05-05) +------------------ + +- Support "develop" packages which keep their ``*.egg-info`` in a subdirectory. + See https://bugs.launchpad.net/pkginfo/+bug/919147. + +- Add support for "unpacked SDists" (thanks to Mike Lundy for the patch). + +1.0 (2013-05-05) +---------------- + +- No changes from 1.0b2. + +1.0b2 (2012-12-28) +------------------ + +- Suppress resource warning leaks reported against clients. + +- Fix 'commandline' module under Py3k. + +1.0b1 (2012-12-28) +------------------ + +- Add support for Python 3.2 and 3.3, including testing them under ``tox``. + +- Add support for PyPy, including testing it under ``tox``. + +- Test supported Python versions under ``tox``. + +- Drop support for Python 2.5. + +- Add a ``setup.py dev`` alias: runs ``setup.py develop`` and installs + testing extras (``nose`` and ``coverage``). + +0.9.1 (2012-10-22) +------------------ + +- Fix test failure under Python >= 2.7, which is enforcing + 'metadata_version == 1.1' because we have classifiers. + + +0.9 (2012-04-25) +---------------- + +- Fix introspection of installed namespace packages. + They may be installed as eggs or via dist-installed 'egg-info' files. + https://bugs.launchpad.net/pkginfo/+bug/934311 + +- Avoid a regression in 0.8 under Python 2.6 / 2.7 when parsing unicode. + https://bugs.launchpad.net/pkginfo/+bug/733827/comments/3 + + +0.8 (2011-03-12) +---------------- + +- Work around Python 2.7's breakage of StringIO. Fixes + https://bugs.launchpad.net/pkginfo/+bug/733827 + +- Fix bug in introspection of installed packages missing the + ``__package__`` attribute. + + +0.7 (2010-11-04) +---------------- + +- Preserve newlines in the ``description`` field. Thanks to Sridhar + Ratnakumar for the patch. + +- 100% test coverage. + + +0.6 (2010-06-01) +---------------- + +- Replace use of ``StringIO.StringIO`` with ``io.StringIO``, where available + (Python >= 2.6). + +- Replace use of ``rfc822`` stdlib module with ``email.parser``, when + available (Python >= 2.5). Ensured that distributions "unfold" wrapped + continuation lines, stripping any leading / trailing whitespace, no matter + which module was used for parsing. + +- Remove bogus testing dependency on ``zope.testing``. + +- Add tests that the "environment markers" spelled out in the approved + PEP 345 are captured. + +- Add ``Project-URL`` for ``1.2`` PKG-INFO metdata (defined in the accepted + version of PEP 345). + + +0.5 (2009-09-11) +---------------- + +- Marked package as non-zip-safe. + +- Fix Trove metadata misspelling. + +- Restore compatibility with Python 2.4. + +- Note that the introspection of installed packages / modules works only + in Python 2.6 or later. + +- Add ``Index`` class as an abstraction over a collection of distributions. + +- Add ``download_url_prefix`` argument to ``pkginfo`` script. If passed, + the script will use the prefix to synthesize a ``download_url`` for + distributions which do not supply that value directly. + + +0.4.1 (2009-05-07) +------------------ + +- Fix bugs in handling of installed packages which lack ``__file__`` + or ``PKG-INFO``. + + +0.4 (2009-05-07) +---------------- + +- Extend the console script to allow output as CSV or INI. Also, added + arguments to specify the metadata version and other parsing / output + policies. + +- Add support for the different metadata versions specified in PEPs + 241, 314, and 345. Distributions now parse and expose only the attributes + corresponding to their metadata version, which defaults to the version + parsed from the ``PKG-INFO`` file. The programmer can override that version + when creating the distribution object. + + +0.3 (2009-05-07) +---------------- + +- Add support for introspection of "development eggs" (checkouts with + ``PKG-INFO``, perhaps created via ``setup.py develop``). + +- Add a console script, ``pkginfo``, which takes one or more paths + on the command line and writes out the associated information. Thanks + to ``runeh`` for the patch! + +- Add ``get_metadata`` helper function, which dispatches a given path or + module across the available distribution types, and returns a distribution + object. Thanks to ``runeh`` for the patch! + +- Make distribution objects support iteration over the metadata fields. + Thanks to ``runeh`` for the patch! + +- Make ``Distribution`` and subclasses new-style classes. Thanks to ``runeh`` + for the patch! + + +0.2 (2009-04-14) +---------------- + +- Add support for introspection of ``bdist_egg`` binary distributions. + + +0.1.1 (2009-04-10) +------------------ + +- Fix packaging errors. + + +0.1 (2009-04-10) +---------------- + +- Initial release. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..68b8104 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2009 Agendaless Consulting, Inc. + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..aca2c6c --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1 @@ +graft docs/examples/ diff --git a/PKG-INFO b/PKG-INFO new file mode 100644 index 0000000..9081ec8 --- /dev/null +++ b/PKG-INFO @@ -0,0 +1,357 @@ +Metadata-Version: 2.1 +Name: pkginfo +Version: 1.8.2 +Summary: Query metadatdata from sdists / bdists / installed packages. +Home-page: https://code.launchpad.net/~tseaver/pkginfo/trunk +Author: Tres Seaver, Agendaless Consulting +Author-email: tseaver@agendaless.com +License: MIT +Description: ``pkginfo`` README + ================== + + This package provides an API for querying the distutils metadata written in + the ``PKG-INFO`` file inside a source distriubtion (an ``sdist``) or a + binary distribution (e.g., created by running ``bdist_egg``). It can + also query the ``EGG-INFO`` directory of an installed distribution, and + the ``*.egg-info`` stored in a "development checkout" + (e.g, created by running ``setup.py develop``). + + + Please see the `pkginfo docs `_ + for detailed documentation. + + + ``pkginfo`` Changelog + ===================== + + 1.8.2 (2021-12-01) + ------------------ + + - Add fix for installed distributions with '__package__' set to an empty + string. LP #1952946. + + 1.8.1 (2021-11-19) + ------------------ + + - Add 'MANIFEST.in' to ensure example files used by tests are included + in source distributions. LP #1951553. + + 1.8.0 (2021-11-18) + ------------------ + + - Support new standard metadata location for installed dists. LP #1865286. + + - Don't overwrite header-based 'description' with empty payload. LP #1885458. + + - Add support for Metadata-Version 2.2. LP #1928729. + + - Add support for uncompressed tarballs for sdists. LP #1951457. + + - Add support for Python 3.10. + + 1.7.1 (2021-07-09) + ------------------ + + - Use Python3 to build docs, and fix doctest examples to use Python3- + compatible syntax. LP #1933322. + + 1.7.0 (2021-01-16) + ------------------ + + - Add support for Python 3.9. + + - Drop support for Python 3.5. + + 1.6.1 (2020-10-26) + ------------------ + + - Adjust test classifiers to match supported Python versions. LP #1901127. + + 1.6.0 (2020-10-20) + ------------------ + + - Add support for Python 3.8. + LP #1869854. + + - Drop support for Python 3.4. + + - Update tests to match setuptools' change, no longer reporting metadata + version for installed packages w/o explicit metadata. LP #1870197. + + 1.5.0.1 (2019-01-08) + -------------------- + + - Fix broken 'sdist'. LP #1639585. + + 1.5.0 (2019-01-07) + ------------------ + + - Fix 'console_scripts' entry point syntax. LP #1810734. + + - Add support for JSON output from the CLI. LP #1700580. + + - Add support for installed wheels. E.g., 'dist-info/' dirs. LP #1700200. + + - Harden metadata extraction against unexpected encodings. LP #1780454. + + - Update tests to match pip/setuptools' use of new metadata version. + LP #1772274. + + - Add support for Python 3.6 and 3.7. + + - Drop support for Python 3.3. + + 1.4.2 (2018-03-14) + ------------------ + + - Use relative imports in pkginfo modules. Supports vendoring of the + package into setuptools. + + - Add support for ``Provides-Extra`` and ``Description-Content-Type`` fields. + Per https://packaging.python.org/specifications/. See: PEP 566. + + - Remove support for old setuptools leaving ``PKG-INFO`` in the root of + the project directory. + + 1.4.1 (2016-11-07) + ------------------ + + - Packaging only change (invalid sdist built for 1.4.0). + + 1.4.0 (2016-11-04) + ------------------ + + - Relicense under MIT license: the PSF license is not suitable for + third-party libraries. + + 1.3.2 (2016-05-24) + ------------------ + + - Packaging-only change (automate fix for wheel built for 1.3.1). + + 1.3.1 (2016-05-24) + ------------------ + + - Packaging-only change (invalid wheel built for 1.3.0). + + 1.3.0 (2016-05-23) + ------------------ + + - Update homepage URL to point to Launchpad, rather than PyPI. + + - Add support for building wheels. + + - Add support for Python 3.5. + + - Drop support for Python 2.6 and 3.2. + + 1.2.1 (2014-01-02) + ------------------ + + - Add overlooked Trove classifier for Python 3.4. + + 1.2 (2014-01-02) + ---------------- + + - Add support for Python 3.4, PyPy3. + + - Add 100% coverage for ``pkginfo.commandline`` module. + + 1.2b1 (2013-12-05) + ------------------ + + - Add support for the "wheel" distribution format, along with minimal + metadata 2.0 support (not including new PEP 426 JSON properties). + Code (re-)borrowed from Donald Stuft's ``twine`` package. + + 1.1 (2013-10-09) + ---------------- + + - Fix tests to pass with current PyPy releases. + + 1.1b1 (2013-05-05) + ------------------ + + - Support "develop" packages which keep their ``*.egg-info`` in a subdirectory. + See https://bugs.launchpad.net/pkginfo/+bug/919147. + + - Add support for "unpacked SDists" (thanks to Mike Lundy for the patch). + + 1.0 (2013-05-05) + ---------------- + + - No changes from 1.0b2. + + 1.0b2 (2012-12-28) + ------------------ + + - Suppress resource warning leaks reported against clients. + + - Fix 'commandline' module under Py3k. + + 1.0b1 (2012-12-28) + ------------------ + + - Add support for Python 3.2 and 3.3, including testing them under ``tox``. + + - Add support for PyPy, including testing it under ``tox``. + + - Test supported Python versions under ``tox``. + + - Drop support for Python 2.5. + + - Add a ``setup.py dev`` alias: runs ``setup.py develop`` and installs + testing extras (``nose`` and ``coverage``). + + 0.9.1 (2012-10-22) + ------------------ + + - Fix test failure under Python >= 2.7, which is enforcing + 'metadata_version == 1.1' because we have classifiers. + + + 0.9 (2012-04-25) + ---------------- + + - Fix introspection of installed namespace packages. + They may be installed as eggs or via dist-installed 'egg-info' files. + https://bugs.launchpad.net/pkginfo/+bug/934311 + + - Avoid a regression in 0.8 under Python 2.6 / 2.7 when parsing unicode. + https://bugs.launchpad.net/pkginfo/+bug/733827/comments/3 + + + 0.8 (2011-03-12) + ---------------- + + - Work around Python 2.7's breakage of StringIO. Fixes + https://bugs.launchpad.net/pkginfo/+bug/733827 + + - Fix bug in introspection of installed packages missing the + ``__package__`` attribute. + + + 0.7 (2010-11-04) + ---------------- + + - Preserve newlines in the ``description`` field. Thanks to Sridhar + Ratnakumar for the patch. + + - 100% test coverage. + + + 0.6 (2010-06-01) + ---------------- + + - Replace use of ``StringIO.StringIO`` with ``io.StringIO``, where available + (Python >= 2.6). + + - Replace use of ``rfc822`` stdlib module with ``email.parser``, when + available (Python >= 2.5). Ensured that distributions "unfold" wrapped + continuation lines, stripping any leading / trailing whitespace, no matter + which module was used for parsing. + + - Remove bogus testing dependency on ``zope.testing``. + + - Add tests that the "environment markers" spelled out in the approved + PEP 345 are captured. + + - Add ``Project-URL`` for ``1.2`` PKG-INFO metdata (defined in the accepted + version of PEP 345). + + + 0.5 (2009-09-11) + ---------------- + + - Marked package as non-zip-safe. + + - Fix Trove metadata misspelling. + + - Restore compatibility with Python 2.4. + + - Note that the introspection of installed packages / modules works only + in Python 2.6 or later. + + - Add ``Index`` class as an abstraction over a collection of distributions. + + - Add ``download_url_prefix`` argument to ``pkginfo`` script. If passed, + the script will use the prefix to synthesize a ``download_url`` for + distributions which do not supply that value directly. + + + 0.4.1 (2009-05-07) + ------------------ + + - Fix bugs in handling of installed packages which lack ``__file__`` + or ``PKG-INFO``. + + + 0.4 (2009-05-07) + ---------------- + + - Extend the console script to allow output as CSV or INI. Also, added + arguments to specify the metadata version and other parsing / output + policies. + + - Add support for the different metadata versions specified in PEPs + 241, 314, and 345. Distributions now parse and expose only the attributes + corresponding to their metadata version, which defaults to the version + parsed from the ``PKG-INFO`` file. The programmer can override that version + when creating the distribution object. + + + 0.3 (2009-05-07) + ---------------- + + - Add support for introspection of "development eggs" (checkouts with + ``PKG-INFO``, perhaps created via ``setup.py develop``). + + - Add a console script, ``pkginfo``, which takes one or more paths + on the command line and writes out the associated information. Thanks + to ``runeh`` for the patch! + + - Add ``get_metadata`` helper function, which dispatches a given path or + module across the available distribution types, and returns a distribution + object. Thanks to ``runeh`` for the patch! + + - Make distribution objects support iteration over the metadata fields. + Thanks to ``runeh`` for the patch! + + - Make ``Distribution`` and subclasses new-style classes. Thanks to ``runeh`` + for the patch! + + + 0.2 (2009-04-14) + ---------------- + + - Add support for introspection of ``bdist_egg`` binary distributions. + + + 0.1.1 (2009-04-10) + ------------------ + + - Fix packaging errors. + + + 0.1 (2009-04-10) + ---------------- + + - Initial release. + +Keywords: distribution sdist installed metadata +Platform: Unix +Platform: Windows +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Software Distribution +Provides-Extra: testing diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..91caaf6 --- /dev/null +++ b/README.txt @@ -0,0 +1,13 @@ +``pkginfo`` README +================== + +This package provides an API for querying the distutils metadata written in +the ``PKG-INFO`` file inside a source distriubtion (an ``sdist``) or a +binary distribution (e.g., created by running ``bdist_egg``). It can +also query the ``EGG-INFO`` directory of an installed distribution, and +the ``*.egg-info`` stored in a "development checkout" +(e.g, created by running ``setup.py develop``). + + +Please see the `pkginfo docs `_ +for detailed documentation. diff --git a/TODO.txt b/TODO.txt new file mode 100644 index 0000000..5d3b43e --- /dev/null +++ b/TODO.txt @@ -0,0 +1,15 @@ +TODOs +===== + +- [X] Catch up to latest changes in PEP345: + + * Project-URL header + + * "environment markers" + +- [_] Add APIs to ``Distribution`` which expose the semantics of the + requirement versions and environment markers. + +- [_] Allow the ``pkginfo`` script to process URLs. + +- [_] Allow the ``pkginfo`` script to process requirements specs. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..ef87680 --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,75 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d .build/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html web pickle htmlhelp latex changes linkcheck + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " changes to make an overview over all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + +clean: + -rm -rf .build/* + +html: + mkdir -p .build/html .build/doctrees + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) .build/html + @echo + @echo "Build finished. The HTML pages are in .build/html." + +pickle: + mkdir -p .build/pickle .build/doctrees + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) .build/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +web: pickle + +json: + mkdir -p .build/json .build/doctrees + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) .build/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + mkdir -p .build/htmlhelp .build/doctrees + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) .build/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in .build/htmlhelp." + +latex: + mkdir -p .build/latex .build/doctrees + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) .build/latex + @echo + @echo "Build finished; the LaTeX files are in .build/latex." + @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ + "run these through (pdf)latex." + +changes: + mkdir -p .build/changes .build/doctrees + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) .build/changes + @echo + @echo "The overview file is in .build/changes." + +linkcheck: + mkdir -p .build/linkcheck .build/doctrees + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) .build/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in .build/linkcheck/output.txt." diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..c69a453 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,194 @@ +# -*- coding: utf-8 -*- +# +# pkginfo documentation build configuration file, created by +# sphinx-quickstart on Wed Apr 8 19:26:04 2009. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# The contents of this file are pickled, so don't put values in the namespace +# that aren't pickleable (module imports are okay, they're removed automatically). +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If your extensions are in another directory, add it here. If the directory +# is relative to the documentation root, use os.path.abspath to make it +# absolute, like shown here. +#sys.path.append(os.path.abspath('.')) + +# General configuration +# --------------------- + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.doctest', +] + +doctest_path = [os.path.abspath('..')] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'pkginfo' +copyright = u'2009-2013, Tres Seaver' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '1.2' +# The full version, including alpha/beta/rc tags. +release = '1.2' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of documents that shouldn't be included in the build. +#unused_docs = [] + +# List of directories, relative to source directory, that shouldn't be searched +# for source files. +exclude_trees = ['.build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + + +# Options for HTML output +# ----------------------- + +# The style sheet to use for HTML and HTML Help pages. A file of that name +# must exist either in Sphinx' static/ path, or in one of the custom paths +# given in html_static_path. +#html_style = 'default.css' + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_use_modindex = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, the reST sources are included in the HTML build as _sources/. +#html_copy_source = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = '' + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pkginfodoc' + + +# Options for LaTeX output +# ------------------------ + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, document class [howto/manual]). +latex_documents = [ + ('index', 'pkginfo.tex', u'pkginfo Documentation', u'Tres Seaver', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_use_modindex = True diff --git a/docs/distributions.rst b/docs/distributions.rst new file mode 100644 index 0000000..5b9db9b --- /dev/null +++ b/docs/distributions.rst @@ -0,0 +1,164 @@ +Distribution Types +================== + +The fundamental abstraction provided by this pacakge is the ``Distribution`` +base class. Implementations exist for specific cases: source distributions, +binary distributions, installed pakcages, and development checkouts. + +.. doctest:: + + >>> from pkginfo import Distribution + >>> from pkginfo import SDist + >>> assert issubclass(SDist, Distribution) + >>> from pkginfo import UnpackedSDist + >>> assert issubclass(UnpackedSDist, SDist) + >>> from pkginfo import BDist + >>> assert issubclass(BDist, Distribution) + >>> from pkginfo import Wheel + >>> assert issubclass(Wheel, Distribution) + >>> from pkginfo import Installed + >>> assert issubclass(Installed, Distribution) + >>> from pkginfo import Develop + >>> assert issubclass(Develop, Distribution) + +Introspecting Source Distributions +---------------------------------- + +``SDist`` objects are created from a filesystem path to the corresponding +archive, which should have been created via the ``sdist`` command from +distutils: + +.. doctest:: + + >>> mypackage = SDist('docs/examples/mypackage-0.1.tar.gz') + +After creation, the ``SDist`` instance will have attributes corrsponding +the the fields defined in the PEP corresponding to the metadata version, +lower-cased and transliterated into valid Python identifiers by mapping +hyphens to underscores. E.g.: + +.. doctest:: + + >>> print(mypackage.metadata_version) + 1.0 + >>> print(mypackage.name) + mypackage + >>> print(mypackage.version) + 0.1 + +Fields which are optional under the PEP, and which have no value set +in their ``PKG-INFO``, will map to the value ``None``: + +.. doctest:: + + >>> print(mypackage.keywords) + None + +Fields which are marked "multiple use" under the PEP map onto sequences; +their names are pluralized to indicate the sequence. "Multiple use" fields +with no occurences in the ``PKG-INFO`` file will map onto an empty sequence: + +.. doctest:: + + >>> print(list(mypackage.supported_platforms)) + [] + +See `Metadata Versions `_ for an example with a non-empty, +"multiple-use" field. + +Introspecting Unpacked Source Distributions +------------------------------------------- + +You can also introspect a previously-unpacked package with ``UnpackedSDist`` +either by passing it the path to the unpacked package, or by passing it the +setup.py at the top level: + +.. doctest:: + + >>> mypackage = UnpackedSDist('docs/examples/mypackage-0.1') + >>> print(mypackage.name) + mypackage + >>> myotherpackage = UnpackedSDist('docs/examples/mypackage-0.1/setup.py') + >>> print(myotherpackage.name) + mypackage + +``UnpackedSDist`` objects are most useful in conjuction with distutils to +produce sdists that want complex behavior for determining what metadata to use; +these sdists normally break when installed with ``pip``, because metadata in an +sdist is regenerated when pip installed. You can achieve this in your +`setup.py` as follows: + +.. code:: + + >>> from setuptools import dist, setup + >>> dist.Distribution(dict(setup_requires='pkginfo')) + >>> from pkginfo import UnpackedSDist + + >>> try: + ... d = UnpackedSDist(__file__) + ... VERSION = d.version + ... except ValueError: + ... VERSION = (version_from_source_control() or + ... os.getenv('VERSION', '1.0')) + >>> setup(name='mypackage', version=VERSION) + +Introspecting Binary Distributions +---------------------------------- + +``BDist`` objects are created from the filename, which should have been +generated via ``setup.py bdist_egg``. + +.. doctest:: + + >>> mypackage = BDist('docs/examples/mypackage-0.1-py2.6.egg') + +After that, they have the same metadata as other ``Distribution`` objects, + +Introspecting Wheels +-------------------- + +``Wheel`` objects are created from the filename, which should have been +generated via ``setup.py bdist_wheel``. + +.. doctest:: + + >>> mypackage = Wheel('docs/examples/mypackage-0.1-cp26-none-linux_x86_64.whl') + +After that, they have the same metadata as other ``Distribution`` objects, + + +Introspecting Installed Packages +-------------------------------- + +``Installed`` objects are created from either a module object or its +dotted name. Note that this feature only works in Python 2.6 or later: +earlier Python versions did not record ``PKG-INFO`` for installed packages. + +.. doctest:: + + >>> import sys + >>> if sys.version_info >= (2,6): + ... dotted = Installed('pkginfo') + ... import pkginfo + ... direct = Installed(pkginfo) + +After that, they have the same metadata as other ``Distribution`` objects, +assuming that the package on which they were based has a discoverable +'.egg-info' file / directory. To be discoverable, the '.egg-info' must +either be located inside the package (e.g., created via ``setup.py develop`` +under setuptools), or adjacent to the package (e.g., created via +``setup.py instlall``). + + +Introspecting Development Checkouts +----------------------------------- + +``Develop`` objects are created from a path to a checkout containing +a ``PKG-iNFO`` file, e.g., created by running ``setup.py develop`` under +setuptools. + +.. doctest:: + + >>> develop = Develop('.') + +After that, they have the same metadata as other ``Distribution`` objects. diff --git a/docs/examples/distlib-0.3.1-py2.py3-none-any.whl b/docs/examples/distlib-0.3.1-py2.py3-none-any.whl new file mode 100644 index 0000000..d7755c5 Binary files /dev/null and b/docs/examples/distlib-0.3.1-py2.py3-none-any.whl differ diff --git a/docs/examples/mypackage-0.1-cp26-none-linux_x86_64.whl b/docs/examples/mypackage-0.1-cp26-none-linux_x86_64.whl new file mode 100644 index 0000000..271fca4 Binary files /dev/null and b/docs/examples/mypackage-0.1-cp26-none-linux_x86_64.whl differ diff --git a/docs/examples/mypackage-0.1-py2.6.egg b/docs/examples/mypackage-0.1-py2.6.egg new file mode 100644 index 0000000..01e55c6 Binary files /dev/null and b/docs/examples/mypackage-0.1-py2.6.egg differ diff --git a/docs/examples/mypackage-0.1.bogus b/docs/examples/mypackage-0.1.bogus new file mode 100644 index 0000000..e69de29 diff --git a/docs/examples/mypackage-0.1.dist-info/METADATA b/docs/examples/mypackage-0.1.dist-info/METADATA new file mode 100644 index 0000000..ed1d807 --- /dev/null +++ b/docs/examples/mypackage-0.1.dist-info/METADATA @@ -0,0 +1,15 @@ +Metadata-Version: 2.0 +Name: mypackage +Version: 0.1 +Summary: UNKNOWN +Home-page: http://pypi.python.org/pypi/pkginfo +Author: Tres Seaver +Author-email: tseaver@palladion.com +License: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console (Text Based) + +UNKNOWN + + diff --git a/docs/examples/mypackage-0.1.tar b/docs/examples/mypackage-0.1.tar new file mode 100644 index 0000000..eb6c576 Binary files /dev/null and b/docs/examples/mypackage-0.1.tar differ diff --git a/docs/examples/mypackage-0.1.tar.bz2 b/docs/examples/mypackage-0.1.tar.bz2 new file mode 100644 index 0000000..699651b Binary files /dev/null and b/docs/examples/mypackage-0.1.tar.bz2 differ diff --git a/docs/examples/mypackage-0.1.tar.gz b/docs/examples/mypackage-0.1.tar.gz new file mode 100644 index 0000000..bf1fde4 Binary files /dev/null and b/docs/examples/mypackage-0.1.tar.gz differ diff --git a/docs/examples/mypackage-0.1.zip b/docs/examples/mypackage-0.1.zip new file mode 100644 index 0000000..dd8355a Binary files /dev/null and b/docs/examples/mypackage-0.1.zip differ diff --git a/docs/examples/mypackage-0.1/PKG-INFO b/docs/examples/mypackage-0.1/PKG-INFO new file mode 100644 index 0000000..48ce6df --- /dev/null +++ b/docs/examples/mypackage-0.1/PKG-INFO @@ -0,0 +1,12 @@ +Metadata-Version: 1.0 +Name: mypackage +Version: 0.1 +Summary: UNKNOWN +Home-page: http://pypi.python.org/pypi/pkginfo +Author: Tres Seaver +Author-email: tseaver@palladion.com +License: UNKNOWN +Description: UNKNOWN +Platform: UNKNOWN +Classifier: Development Status :: 4 - Beta +Classifier: Environment :: Console (Text Based) diff --git a/docs/examples/mypackage-0.1/README.txt b/docs/examples/mypackage-0.1/README.txt new file mode 100644 index 0000000..253933c --- /dev/null +++ b/docs/examples/mypackage-0.1/README.txt @@ -0,0 +1,4 @@ +mypackage README +================ + +Dummy package for testing ``pkginfo``. diff --git a/docs/examples/mypackage-0.1/setup.cfg b/docs/examples/mypackage-0.1/setup.cfg new file mode 100644 index 0000000..861a9f5 --- /dev/null +++ b/docs/examples/mypackage-0.1/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/docs/examples/mypackage-0.1/setup.py b/docs/examples/mypackage-0.1/setup.py new file mode 100644 index 0000000..0f3abda --- /dev/null +++ b/docs/examples/mypackage-0.1/setup.py @@ -0,0 +1,13 @@ +from setuptools import setup + +setup( + name='mypackage', + version='0.1', + author='Tres Seaver', + author_email='tseaver@palladion.com', + url='http://pypi.python.org/pypi/pkginfo', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Environment :: Console (Text Based)', + ], +) diff --git a/docs/examples/nodistinfo-0.1-any.whl b/docs/examples/nodistinfo-0.1-any.whl new file mode 100644 index 0000000..2c503bd Binary files /dev/null and b/docs/examples/nodistinfo-0.1-any.whl differ diff --git a/docs/examples/nopkginfo-0.1.egg b/docs/examples/nopkginfo-0.1.egg new file mode 100644 index 0000000..2c503bd Binary files /dev/null and b/docs/examples/nopkginfo-0.1.egg differ diff --git a/docs/examples/nopkginfo-0.1.zip b/docs/examples/nopkginfo-0.1.zip new file mode 100644 index 0000000..2c503bd Binary files /dev/null and b/docs/examples/nopkginfo-0.1.zip differ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..d32523d --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,18 @@ +:mod:`pkginfo` documentation +============================ + +This package provides an API for querying the distutils metadata written in +the ``PKG-INFO`` file inside a source distriubtion (an ``sdist``) or a +binary distribution (e.g., created by running ``bdist_egg`` or +``bdist_wheel``). It can also query the ``EGG-INFO`` directory of an +installed distribution, and the ``*.egg-info`` stored in a "development +checkout" (e.g, created by running ``setup.py develop``). + +Contents: + +.. toctree:: + :maxdepth: 2 + + distributions + metadata + indexes diff --git a/docs/indexes.rst b/docs/indexes.rst new file mode 100644 index 0000000..085d5c5 --- /dev/null +++ b/docs/indexes.rst @@ -0,0 +1,26 @@ +Distribution Indexes +===================== + +An ``Index`` is conceptually a set of ``Distribution`` objects, with some +additional behavior for managing the set as a whole. + +.. doctest:: + + >>> from pkginfo import Distribution + >>> from pkginfo import Index + >>> index = Index() + >>> list(index) + [] + >>> d1 = Distribution() + >>> d1.name = 'foo' + >>> d1.version = '1.0' + >>> index.add(d1) + >>> list(index) + ['foo-1.0'] + >>> d2 = Distribution() + >>> d2.name = 'foo' + >>> d2.version = '1.1' + >>> index.add(d2) + >>> sorted(list(index)) + ['foo-1.0', 'foo-1.1'] + diff --git a/docs/metadata.rst b/docs/metadata.rst new file mode 100644 index 0000000..9a46c31 --- /dev/null +++ b/docs/metadata.rst @@ -0,0 +1,29 @@ +Metadata Versions +================= + +The allowed ``PKG-INFO`` fields and their semantics are defined in a series +of PEPs, each of which updates the metadata version field. + +- Metadata version 1.0 is specified in `PEP 241`_. +- Metadata version 1.1 is specified in `PEP 314`_. +- Metadata version 1.2 is specified in `PEP 345`_ (still in draft). + +A given ``Distribution`` object parses / exposes the attributes which +correspond to the metadata version specified in its ``PKG-INFO``. + +You can override the metadata version stored in a given distribution by +passing the specific version (as a string) to its constructor. E.g., +updating the metadata version here in order to expose the classifiers, +which were not defined under version '1.0': + +.. doctest:: + + >>> from pkginfo import SDist + >>> mypackage = SDist('docs/examples/mypackage-0.1.tar.gz', + ... metadata_version='1.1') + >>> print([str(x) for x in mypackage.classifiers]) + ['Development Status :: 4 - Beta', 'Environment :: Console (Text Based)'] + +.. _`PEP 241`: http://svn.python.org/projects/peps/trunk/pep-0241.txt +.. _`PEP 314`: http://svn.python.org/projects/peps/trunk/pep-0314.txt +.. _`PEP 345`: http://svn.python.org/projects/peps/trunk/pep-0345.txt diff --git a/pkginfo.egg-info/PKG-INFO b/pkginfo.egg-info/PKG-INFO new file mode 100644 index 0000000..9081ec8 --- /dev/null +++ b/pkginfo.egg-info/PKG-INFO @@ -0,0 +1,357 @@ +Metadata-Version: 2.1 +Name: pkginfo +Version: 1.8.2 +Summary: Query metadatdata from sdists / bdists / installed packages. +Home-page: https://code.launchpad.net/~tseaver/pkginfo/trunk +Author: Tres Seaver, Agendaless Consulting +Author-email: tseaver@agendaless.com +License: MIT +Description: ``pkginfo`` README + ================== + + This package provides an API for querying the distutils metadata written in + the ``PKG-INFO`` file inside a source distriubtion (an ``sdist``) or a + binary distribution (e.g., created by running ``bdist_egg``). It can + also query the ``EGG-INFO`` directory of an installed distribution, and + the ``*.egg-info`` stored in a "development checkout" + (e.g, created by running ``setup.py develop``). + + + Please see the `pkginfo docs `_ + for detailed documentation. + + + ``pkginfo`` Changelog + ===================== + + 1.8.2 (2021-12-01) + ------------------ + + - Add fix for installed distributions with '__package__' set to an empty + string. LP #1952946. + + 1.8.1 (2021-11-19) + ------------------ + + - Add 'MANIFEST.in' to ensure example files used by tests are included + in source distributions. LP #1951553. + + 1.8.0 (2021-11-18) + ------------------ + + - Support new standard metadata location for installed dists. LP #1865286. + + - Don't overwrite header-based 'description' with empty payload. LP #1885458. + + - Add support for Metadata-Version 2.2. LP #1928729. + + - Add support for uncompressed tarballs for sdists. LP #1951457. + + - Add support for Python 3.10. + + 1.7.1 (2021-07-09) + ------------------ + + - Use Python3 to build docs, and fix doctest examples to use Python3- + compatible syntax. LP #1933322. + + 1.7.0 (2021-01-16) + ------------------ + + - Add support for Python 3.9. + + - Drop support for Python 3.5. + + 1.6.1 (2020-10-26) + ------------------ + + - Adjust test classifiers to match supported Python versions. LP #1901127. + + 1.6.0 (2020-10-20) + ------------------ + + - Add support for Python 3.8. + LP #1869854. + + - Drop support for Python 3.4. + + - Update tests to match setuptools' change, no longer reporting metadata + version for installed packages w/o explicit metadata. LP #1870197. + + 1.5.0.1 (2019-01-08) + -------------------- + + - Fix broken 'sdist'. LP #1639585. + + 1.5.0 (2019-01-07) + ------------------ + + - Fix 'console_scripts' entry point syntax. LP #1810734. + + - Add support for JSON output from the CLI. LP #1700580. + + - Add support for installed wheels. E.g., 'dist-info/' dirs. LP #1700200. + + - Harden metadata extraction against unexpected encodings. LP #1780454. + + - Update tests to match pip/setuptools' use of new metadata version. + LP #1772274. + + - Add support for Python 3.6 and 3.7. + + - Drop support for Python 3.3. + + 1.4.2 (2018-03-14) + ------------------ + + - Use relative imports in pkginfo modules. Supports vendoring of the + package into setuptools. + + - Add support for ``Provides-Extra`` and ``Description-Content-Type`` fields. + Per https://packaging.python.org/specifications/. See: PEP 566. + + - Remove support for old setuptools leaving ``PKG-INFO`` in the root of + the project directory. + + 1.4.1 (2016-11-07) + ------------------ + + - Packaging only change (invalid sdist built for 1.4.0). + + 1.4.0 (2016-11-04) + ------------------ + + - Relicense under MIT license: the PSF license is not suitable for + third-party libraries. + + 1.3.2 (2016-05-24) + ------------------ + + - Packaging-only change (automate fix for wheel built for 1.3.1). + + 1.3.1 (2016-05-24) + ------------------ + + - Packaging-only change (invalid wheel built for 1.3.0). + + 1.3.0 (2016-05-23) + ------------------ + + - Update homepage URL to point to Launchpad, rather than PyPI. + + - Add support for building wheels. + + - Add support for Python 3.5. + + - Drop support for Python 2.6 and 3.2. + + 1.2.1 (2014-01-02) + ------------------ + + - Add overlooked Trove classifier for Python 3.4. + + 1.2 (2014-01-02) + ---------------- + + - Add support for Python 3.4, PyPy3. + + - Add 100% coverage for ``pkginfo.commandline`` module. + + 1.2b1 (2013-12-05) + ------------------ + + - Add support for the "wheel" distribution format, along with minimal + metadata 2.0 support (not including new PEP 426 JSON properties). + Code (re-)borrowed from Donald Stuft's ``twine`` package. + + 1.1 (2013-10-09) + ---------------- + + - Fix tests to pass with current PyPy releases. + + 1.1b1 (2013-05-05) + ------------------ + + - Support "develop" packages which keep their ``*.egg-info`` in a subdirectory. + See https://bugs.launchpad.net/pkginfo/+bug/919147. + + - Add support for "unpacked SDists" (thanks to Mike Lundy for the patch). + + 1.0 (2013-05-05) + ---------------- + + - No changes from 1.0b2. + + 1.0b2 (2012-12-28) + ------------------ + + - Suppress resource warning leaks reported against clients. + + - Fix 'commandline' module under Py3k. + + 1.0b1 (2012-12-28) + ------------------ + + - Add support for Python 3.2 and 3.3, including testing them under ``tox``. + + - Add support for PyPy, including testing it under ``tox``. + + - Test supported Python versions under ``tox``. + + - Drop support for Python 2.5. + + - Add a ``setup.py dev`` alias: runs ``setup.py develop`` and installs + testing extras (``nose`` and ``coverage``). + + 0.9.1 (2012-10-22) + ------------------ + + - Fix test failure under Python >= 2.7, which is enforcing + 'metadata_version == 1.1' because we have classifiers. + + + 0.9 (2012-04-25) + ---------------- + + - Fix introspection of installed namespace packages. + They may be installed as eggs or via dist-installed 'egg-info' files. + https://bugs.launchpad.net/pkginfo/+bug/934311 + + - Avoid a regression in 0.8 under Python 2.6 / 2.7 when parsing unicode. + https://bugs.launchpad.net/pkginfo/+bug/733827/comments/3 + + + 0.8 (2011-03-12) + ---------------- + + - Work around Python 2.7's breakage of StringIO. Fixes + https://bugs.launchpad.net/pkginfo/+bug/733827 + + - Fix bug in introspection of installed packages missing the + ``__package__`` attribute. + + + 0.7 (2010-11-04) + ---------------- + + - Preserve newlines in the ``description`` field. Thanks to Sridhar + Ratnakumar for the patch. + + - 100% test coverage. + + + 0.6 (2010-06-01) + ---------------- + + - Replace use of ``StringIO.StringIO`` with ``io.StringIO``, where available + (Python >= 2.6). + + - Replace use of ``rfc822`` stdlib module with ``email.parser``, when + available (Python >= 2.5). Ensured that distributions "unfold" wrapped + continuation lines, stripping any leading / trailing whitespace, no matter + which module was used for parsing. + + - Remove bogus testing dependency on ``zope.testing``. + + - Add tests that the "environment markers" spelled out in the approved + PEP 345 are captured. + + - Add ``Project-URL`` for ``1.2`` PKG-INFO metdata (defined in the accepted + version of PEP 345). + + + 0.5 (2009-09-11) + ---------------- + + - Marked package as non-zip-safe. + + - Fix Trove metadata misspelling. + + - Restore compatibility with Python 2.4. + + - Note that the introspection of installed packages / modules works only + in Python 2.6 or later. + + - Add ``Index`` class as an abstraction over a collection of distributions. + + - Add ``download_url_prefix`` argument to ``pkginfo`` script. If passed, + the script will use the prefix to synthesize a ``download_url`` for + distributions which do not supply that value directly. + + + 0.4.1 (2009-05-07) + ------------------ + + - Fix bugs in handling of installed packages which lack ``__file__`` + or ``PKG-INFO``. + + + 0.4 (2009-05-07) + ---------------- + + - Extend the console script to allow output as CSV or INI. Also, added + arguments to specify the metadata version and other parsing / output + policies. + + - Add support for the different metadata versions specified in PEPs + 241, 314, and 345. Distributions now parse and expose only the attributes + corresponding to their metadata version, which defaults to the version + parsed from the ``PKG-INFO`` file. The programmer can override that version + when creating the distribution object. + + + 0.3 (2009-05-07) + ---------------- + + - Add support for introspection of "development eggs" (checkouts with + ``PKG-INFO``, perhaps created via ``setup.py develop``). + + - Add a console script, ``pkginfo``, which takes one or more paths + on the command line and writes out the associated information. Thanks + to ``runeh`` for the patch! + + - Add ``get_metadata`` helper function, which dispatches a given path or + module across the available distribution types, and returns a distribution + object. Thanks to ``runeh`` for the patch! + + - Make distribution objects support iteration over the metadata fields. + Thanks to ``runeh`` for the patch! + + - Make ``Distribution`` and subclasses new-style classes. Thanks to ``runeh`` + for the patch! + + + 0.2 (2009-04-14) + ---------------- + + - Add support for introspection of ``bdist_egg`` binary distributions. + + + 0.1.1 (2009-04-10) + ------------------ + + - Fix packaging errors. + + + 0.1 (2009-04-10) + ---------------- + + - Initial release. + +Keywords: distribution sdist installed metadata +Platform: Unix +Platform: Windows +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 2.7 +Classifier: Programming Language :: Python :: 3.6 +Classifier: Programming Language :: Python :: 3.7 +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: System :: Software Distribution +Provides-Extra: testing diff --git a/pkginfo.egg-info/SOURCES.txt b/pkginfo.egg-info/SOURCES.txt new file mode 100644 index 0000000..2d0cf1e --- /dev/null +++ b/pkginfo.egg-info/SOURCES.txt @@ -0,0 +1,71 @@ +.bzrignore +.coveragerc +CHANGES.txt +LICENSE.txt +MANIFEST.in +README.txt +TODO.txt +setup.cfg +setup.py +tox.ini +docs/Makefile +docs/conf.py +docs/distributions.rst +docs/index.rst +docs/indexes.rst +docs/metadata.rst +docs/examples/distlib-0.3.1-py2.py3-none-any.whl +docs/examples/mypackage-0.1-cp26-none-linux_x86_64.whl +docs/examples/mypackage-0.1-py2.6.egg +docs/examples/mypackage-0.1.bogus +docs/examples/mypackage-0.1.tar +docs/examples/mypackage-0.1.tar.bz2 +docs/examples/mypackage-0.1.tar.gz +docs/examples/mypackage-0.1.zip +docs/examples/nodistinfo-0.1-any.whl +docs/examples/nopkginfo-0.1.egg +docs/examples/nopkginfo-0.1.zip +docs/examples/mypackage-0.1/PKG-INFO +docs/examples/mypackage-0.1/README.txt +docs/examples/mypackage-0.1/setup.cfg +docs/examples/mypackage-0.1/setup.py +docs/examples/mypackage-0.1.dist-info/METADATA +pkginfo/__init__.py +pkginfo/_compat.py +pkginfo/bdist.py +pkginfo/commandline.py +pkginfo/develop.py +pkginfo/distribution.py +pkginfo/index.py +pkginfo/installed.py +pkginfo/sdist.py +pkginfo/utils.py +pkginfo/wheel.py +pkginfo.egg-info/PKG-INFO +pkginfo.egg-info/SOURCES.txt +pkginfo.egg-info/dependency_links.txt +pkginfo.egg-info/entry_points.txt +pkginfo.egg-info/not-zip-safe +pkginfo.egg-info/requires.txt +pkginfo.egg-info/top_level.txt +pkginfo/tests/__init__.py +pkginfo/tests/test_bdist.py +pkginfo/tests/test_commandline.py +pkginfo/tests/test_develop.py +pkginfo/tests/test_distribution.py +pkginfo/tests/test_index.py +pkginfo/tests/test_installed.py +pkginfo/tests/test_sdist.py +pkginfo/tests/test_utils.py +pkginfo/tests/test_wheel.py +pkginfo/tests/funny/__init__.py +pkginfo/tests/funny/funny.egg-info +pkginfo/tests/manky/NOT-A-PACKAGE.txt +pkginfo/tests/manky/namespaced/__init__.py +pkginfo/tests/manky/namespaced.manky-0.1.egg-info/PKG-INFO +pkginfo/tests/manky/namespaced/manky/__init__.py +pkginfo/tests/silly/PKG-INFO +pkginfo/tests/wonky/NOT-A-PACKAGE.txt +pkginfo/tests/wonky/EGG-INFO/PKG-INFO +pkginfo/tests/wonky/namespaced/__init__.py +pkginfo/tests/wonky/namespaced/wonky/__init__.py \ No newline at end of file diff --git a/pkginfo.egg-info/dependency_links.txt b/pkginfo.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pkginfo.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/pkginfo.egg-info/entry_points.txt b/pkginfo.egg-info/entry_points.txt new file mode 100644 index 0000000..d919b6f --- /dev/null +++ b/pkginfo.egg-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +pkginfo = pkginfo.commandline:main + diff --git a/pkginfo.egg-info/not-zip-safe b/pkginfo.egg-info/not-zip-safe new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/pkginfo.egg-info/not-zip-safe @@ -0,0 +1 @@ + diff --git a/pkginfo.egg-info/requires.txt b/pkginfo.egg-info/requires.txt new file mode 100644 index 0000000..26a6d96 --- /dev/null +++ b/pkginfo.egg-info/requires.txt @@ -0,0 +1,4 @@ + +[testing] +coverage +nose diff --git a/pkginfo.egg-info/top_level.txt b/pkginfo.egg-info/top_level.txt new file mode 100644 index 0000000..23207d6 --- /dev/null +++ b/pkginfo.egg-info/top_level.txt @@ -0,0 +1 @@ +pkginfo diff --git a/pkginfo/__init__.py b/pkginfo/__init__.py new file mode 100644 index 0000000..849884c --- /dev/null +++ b/pkginfo/__init__.py @@ -0,0 +1,9 @@ +from .bdist import BDist +from .develop import Develop +from .distribution import Distribution +from .index import Index +from .installed import Installed +from .sdist import SDist +from .sdist import UnpackedSDist +from .utils import get_metadata +from .wheel import Wheel diff --git a/pkginfo/_compat.py b/pkginfo/_compat.py new file mode 100644 index 0000000..ff32652 --- /dev/null +++ b/pkginfo/_compat.py @@ -0,0 +1,34 @@ +try: + STRING_TYPES = (str, unicode) +except NameError: #pragma NO COVER Python >= 3.0 + STRING_TYPES = (str,) + +try: + u = unicode +except NameError: #pragma NO COVER Python >= 3.0 + u = str + b = bytes +else: #pragma NO COVER Python < 3.0 + b = str + +try: + from StringIO import StringIO +except ImportError: #pragma NO COVER Python >= 3.0 + from io import StringIO + from io import BytesIO +else: #pragma NO COVER Python < 3.0 + BytesIO = StringIO + + +def must_decode(value): #pragma NO COVER + if type(value) is bytes: + try: + return value.decode('utf-8') + except UnicodeDecodeError: + return value.decode('latin1') + return value + +def must_encode(value): #pragma NO COVER + if type(value) is u: + return value.encode('utf-8') + return value diff --git a/pkginfo/bdist.py b/pkginfo/bdist.py new file mode 100644 index 0000000..71ad9cf --- /dev/null +++ b/pkginfo/bdist.py @@ -0,0 +1,39 @@ +import os +import zipfile + +from .distribution import Distribution + +class BDist(Distribution): + + def __init__(self, filename, metadata_version=None): + self.filename = filename + self.metadata_version = metadata_version + self.extractMetadata() + + def read(self): + fqn = os.path.abspath( + os.path.normpath(self.filename)) + if not os.path.exists(fqn): + raise ValueError('No such file: %s' % fqn) + + if fqn.endswith('.egg'): + archive = zipfile.ZipFile(fqn) + names = archive.namelist() + def read_file(name): + return archive.read(name) + else: + raise ValueError('Not a known archive format: %s' % fqn) + + try: + tuples = [x.split('/') for x in names if 'PKG-INFO' in x] + schwarz = sorted([(len(x), x) for x in tuples]) + for path in [x[1] for x in schwarz]: + candidate = '/'.join(path) + data = read_file(candidate) + if b'Metadata-Version' in data: + return data + finally: + archive.close() + + raise ValueError('No PKG-INFO in archive: %s' % fqn) + diff --git a/pkginfo/commandline.py b/pkginfo/commandline.py new file mode 100644 index 0000000..f34a7b6 --- /dev/null +++ b/pkginfo/commandline.py @@ -0,0 +1,232 @@ +"""Print the metadata for one or more Python package distributions. + +Usage: %prog [options] path+ + +Each 'path' entry can be one of the following: + +o a source distribution: in this case, 'path' should point to an existing + archive file (.tar.gz, .tar.bz2, or .zip) as generated by 'setup.py sdist'. + +o a binary distribution: in this case, 'path' should point to an existing + archive file (.egg) + +o a "develop" checkout: in this case, 'path' should point to a directory + initialized via 'setup.py develop' (under setuptools). + +o an installed package: in this case, 'path' should be the importable name + of the package. +""" +try: + from configparser import ConfigParser +except ImportError: # pragma: NO COVER + from ConfigParser import ConfigParser +from collections import OrderedDict +from csv import writer +import json +import optparse +import os +import sys + +from .utils import get_metadata + + +def _parse_options(args=None): + parser = optparse.OptionParser(usage=__doc__) + + parser.add_option("-m", "--metadata-version", default=None, + help="Override metadata version") + + parser.add_option("-f", "--field", dest="fields", action="append", + help="Specify an output field (repeatable)", + ) + + parser.add_option("-d", "--download-url-prefix", + dest="download_url_prefix", + help="Download URL prefix", + ) + + parser.add_option("--simple", dest="output", action="store_const", + const='simple', default='simple', + help="Output as simple key-value pairs", + ) + + parser.add_option("-s", "--skip", dest="skip", action="store_true", + default=True, + help="Skip missing values in simple output", + ) + + parser.add_option("-S", "--no-skip", dest="skip", action="store_false", + help="Don't skip missing values in simple output", + ) + + parser.add_option("--single", dest="output", action="store_const", + const='single', + help="Output delimited values", + ) + + parser.add_option("--item-delim", dest="item_delim", action="store", + default=';', + help="Delimiter for fields in single-line output", + ) + + parser.add_option("--sequence-delim", dest="sequence_delim", + action="store", default=',', + help="Delimiter for multi-valued fields", + ) + + parser.add_option("--csv", dest="output", action="store_const", + const='csv', + help="Output as CSV", + ) + + parser.add_option("--ini", dest="output", action="store_const", + const='ini', + help="Output as INI", + ) + + parser.add_option("--json", dest="output", action="store_const", + const='json', + help="Output as JSON", + ) + + options, args = parser.parse_args(args) + + if len(args)==0: + parser.error("Pass one or more files or directories as arguments.") + else: + return options, args + +class Base(object): + _fields = None + def __init__(self, options): + if options.fields: + self._fields = options.fields + + def finish(self): # pragma: NO COVER + pass + +class Simple(Base): + def __init__(self, options): + super(Simple, self).__init__(options) + self._skip = options.skip + + def __call__(self, meta): + for field in self._fields or list(meta): + value = getattr(meta, field) + if (not self._skip) or (value is not None and value!=()): + print("%s: %s" % (field, value)) + +class SingleLine(Base): + _fields = None + def __init__(self, options): + super(SingleLine, self).__init__(options) + self._item_delim = options.item_delim + self._sequence_delim = options.sequence_delim + + def __call__(self, meta): + if self._fields is None: + self._fields = list(meta) + values = [] + for field in self._fields: + value = getattr(meta, field) + if isinstance(value, (tuple, list)): + value = self._sequence_delim.join(value) + else: + value = str(value) + values.append(value) + print(self._item_delim.join(values)) + +class CSV(Base): + _writer = None + def __init__(self, options): + super(CSV, self).__init__(options) + self._sequence_delim = options.sequence_delim + + def __call__(self, meta): + if self._fields is None: + self._fields = list(meta) # first dist wins + fields = self._fields + if self._writer is None: + self._writer = writer(sys.stdout) + self._writer.writerow(fields) + values = [] + for field in fields: + value = getattr(meta, field) + if isinstance(value, (tuple, list)): + value = self._sequence_delim.join(value) + else: + value = str(value) + values.append(value) + self._writer.writerow(values) + +class INI(Base): + _fields = None + def __init__(self, options): + super(INI, self).__init__(options) + self._parser = ConfigParser() + + def __call__(self, meta): + name = meta.name + version = meta.version + section = '%s-%s' % (name, version) + if self._parser.has_section(section): + raise ValueError('Duplicate distribution: %s' % section) + self._parser.add_section(section) + for field in self._fields or list(meta): + value = getattr(meta, field) + if isinstance(value, (tuple, list)): + value = '\n\t'.join(value) + self._parser.set(section, field, value) + + def finish(self): + self._parser.write(sys.stdout) # pragma: NO COVER + +class JSON(Base): + _fields = None + def __init__(self, options): + super(JSON, self).__init__(options) + self._mapping = OrderedDict() + + def __call__(self, meta): + if self._fields is None: + self._fields = list(meta) + for field in self._fields: + value = getattr(meta, field) + if value and not isinstance(value, (tuple, list)): + value = str(value) + if field in self._mapping: + raise ValueError('Duplicate field: %(field)r' % locals()) + self._mapping[field] = value + + def finish(self): + json.dump(self._mapping, sys.stdout, indent=2) + +_FORMATTERS = { + 'simple': Simple, + 'single': SingleLine, + 'csv': CSV, + 'ini': INI, + 'json': JSON, +} + +def main(args=None): + """Entry point for pkginfo tool + """ + options, paths = _parse_options(args) + format = getattr(options, 'output', 'simple') + formatter = _FORMATTERS[format](options) + + for path in paths: + meta = get_metadata(path, options.metadata_version) + if meta is None: + continue + + if options.download_url_prefix: + if meta.download_url is None: + filename = os.path.basename(path) + meta.download_url = '%s/%s' % (options.download_url_prefix, + filename) + + formatter(meta) + + formatter.finish() diff --git a/pkginfo/develop.py b/pkginfo/develop.py new file mode 100644 index 0000000..d0f53b4 --- /dev/null +++ b/pkginfo/develop.py @@ -0,0 +1,46 @@ +import io +import os +import sys +import warnings + +from .distribution import Distribution + +def _gather_py2(top, candidates): #pragma NO COVER Py3k + def _filter(candidates, dirname, fnames): + for fname in fnames: + fqn = os.path.join(dirname, fname) + if os.path.isdir(fqn): + if fname == 'EGG-INFO' or fname.endswith('.egg-info'): + candidates.append(fqn) + os.path.walk(top, _filter, candidates) + +def _gather_py3(top, candidates): #pragma NO COVER Python2 + for dirpath, dirnames, fnames in os.walk(top): + for dirname in dirnames: + fqn = os.path.join(dirpath, dirname) + if dirname == 'EGG-INFO' or dirname.endswith('.egg-info'): + candidates.append(fqn) + +if sys.version_info[0] < 3: #pragma NO COVER Python2 + _gather = _gather_py2 +else: #pragma NO COVER Py3k + _gather = _gather_py3 + +class Develop(Distribution): + + def __init__(self, path, metadata_version=None): + self.path = os.path.abspath( + os.path.normpath( + os.path.expanduser(path))) + self.metadata_version = metadata_version + self.extractMetadata() + + def read(self): + candidates = [self.path] + _gather(self.path, candidates) + for candidate in candidates: + path = os.path.join(candidate, 'PKG-INFO') + if os.path.exists(path): + with io.open(path, errors='ignore') as f: + return f.read() + warnings.warn('No PKG-INFO found for path: %s' % self.path) diff --git a/pkginfo/distribution.py b/pkginfo/distribution.py new file mode 100644 index 0000000..2ebefc9 --- /dev/null +++ b/pkginfo/distribution.py @@ -0,0 +1,154 @@ +from email.parser import Parser + +from ._compat import StringIO +from ._compat import must_decode + + +def parse(fp): + return Parser().parse(fp) +def get(msg, header): + return _collapse_leading_ws(header, msg.get(header)) +def get_all(msg, header): + return [_collapse_leading_ws(header, x) for x in msg.get_all(header)] + +def _collapse_leading_ws(header, txt): + """ + ``Description`` header must preserve newlines; all others need not + """ + if header.lower() == 'description': # preserve newlines + return '\n'.join([x[8:] if x.startswith(' ' * 8) else x + for x in txt.strip().splitlines()]) + else: + return ' '.join([x.strip() for x in txt.splitlines()]) + + +HEADER_ATTRS_1_0 = ( # PEP 241 + ('Metadata-Version', 'metadata_version', False), + ('Name', 'name', False), + ('Version', 'version', False), + ('Platform', 'platforms', True), + ('Supported-Platform', 'supported_platforms', True), + ('Summary', 'summary', False), + ('Description', 'description', False), + ('Keywords', 'keywords', False), + ('Home-Page', 'home_page', False), + ('Author', 'author', False), + ('Author-email', 'author_email', False), + ('License', 'license', False), +) + +HEADER_ATTRS_1_1 = HEADER_ATTRS_1_0 + ( # PEP 314 + ('Classifier', 'classifiers', True), + ('Download-URL', 'download_url', False), + ('Requires', 'requires', True), + ('Provides', 'provides', True), + ('Obsoletes', 'obsoletes', True), +) + +HEADER_ATTRS_1_2 = HEADER_ATTRS_1_1 + ( # PEP 345 + ('Maintainer', 'maintainer', False), + ('Maintainer-email', 'maintainer_email', False), + ('Requires-Python', 'requires_python', False), + ('Requires-External', 'requires_external', True), + ('Requires-Dist', 'requires_dist', True), + ('Provides-Dist', 'provides_dist', True), + ('Obsoletes-Dist', 'obsoletes_dist', True), + ('Project-URL', 'project_urls', True), +) + +HEADER_ATTRS_2_0 = HEADER_ATTRS_1_2 #XXX PEP 426? + +HEADER_ATTRS_2_1 = HEADER_ATTRS_1_2 + ( # PEP 566 + ('Provides-Extra', 'provides_extras', True), + ('Description-Content-Type', 'description_content_type', False) +) + +HEADER_ATTRS_2_2 = HEADER_ATTRS_2_1 + ( # PEP 643 + ('Dynamic', 'dynamic', True), +) + +HEADER_ATTRS = { + '1.0': HEADER_ATTRS_1_0, + '1.1': HEADER_ATTRS_1_1, + '1.2': HEADER_ATTRS_1_2, + '2.0': HEADER_ATTRS_2_0, + '2.1': HEADER_ATTRS_2_1, + '2.2': HEADER_ATTRS_2_2, +} + +class Distribution(object): + metadata_version = None + # version 1.0 + name = None + version = None + platforms = () + supported_platforms = () + summary = None + description = None + keywords = None + home_page = None + download_url = None + author = None + author_email = None + license = None + # version 1.1 + classifiers = () + requires = () + provides = () + obsoletes = () + # version 1.2 + maintainer = None + maintainer_email = None + requires_python = None + requires_external = () + requires_dist = () + provides_dist = () + obsoletes_dist = () + project_urls = () + # version 2.1 + provides_extras = () + description_content_type = None + # version 2.2 + dynamic = () + + def extractMetadata(self): + data = self.read() + self.parse(data) + + def read(self): + raise NotImplementedError + + def _getHeaderAttrs(self): + return HEADER_ATTRS.get(self.metadata_version, []) + + def parse(self, data): + fp = StringIO(must_decode(data)) + msg = parse(fp) + + if 'Metadata-Version' in msg and self.metadata_version is None: + value = get(msg, 'Metadata-Version') + metadata_version = self.metadata_version = value + + for header_name, attr_name, multiple in self._getHeaderAttrs(): + + if attr_name == 'metadata_version': + continue + + if header_name in msg: + if multiple: + values = get_all(msg, header_name) + setattr(self, attr_name, values) + else: + value = get(msg, header_name) + if value != 'UNKNOWN': + setattr(self, attr_name, value) + + body = msg.get_payload() + if body: + setattr(self, 'description', body) + + def __iter__(self): + for header_name, attr_name, multiple in self._getHeaderAttrs(): + yield attr_name + + iterkeys = __iter__ diff --git a/pkginfo/index.py b/pkginfo/index.py new file mode 100644 index 0000000..006f599 --- /dev/null +++ b/pkginfo/index.py @@ -0,0 +1,15 @@ +from .distribution import Distribution + +class Index(dict): + + def __setitem__(self, key, value): + if not isinstance(value, Distribution): + raise ValueError('Not a distribution: %r.' % value) + if key != '%s-%s' % (value.name, value.version): + raise ValueError('Key must match -.') + super(Index, self).__setitem__(key, value) + + def add(self, distribution): + key = '%s-%s' % (distribution.name, distribution.version) + self[key] = distribution + diff --git a/pkginfo/installed.py b/pkginfo/installed.py new file mode 100644 index 0000000..e37d70a --- /dev/null +++ b/pkginfo/installed.py @@ -0,0 +1,63 @@ +import glob +import io +import os +import sys +import warnings + +from .distribution import Distribution +from ._compat import STRING_TYPES + +class Installed(Distribution): + + def __init__(self, package, metadata_version=None): + if isinstance(package, STRING_TYPES): + self.package_name = package + try: + __import__(package) + except ImportError: + package = None + else: + package = sys.modules[package] + else: + self.package_name = package.__name__ + self.package = package + self.metadata_version = metadata_version + self.extractMetadata() + + def read(self): + opj = os.path.join + if self.package is not None: + package = self.package.__package__ + if package in ('', None): + package = self.package.__name__ + egg_pattern = '%s*.egg-info' % package + dist_pattern = '%s*.dist-info' % package + file = getattr(self.package, '__file__', None) + if file is not None: + candidates = [] + def _add_candidate(where): + candidates.extend(glob.glob(where)) + for entry in sys.path: + if file.startswith(entry): + _add_candidate(opj(entry, 'EGG-INFO')) # egg? + _add_candidate(opj(entry, egg_pattern)) + _add_candidate(opj(entry, dist_pattern)) + dir, name = os.path.split(self.package.__file__) + _add_candidate(opj(dir, egg_pattern)) + _add_candidate(opj(dir, '..', egg_pattern)) + _add_candidate(opj(dir, dist_pattern)) + _add_candidate(opj(dir, '..', dist_pattern)) + for candidate in candidates: + if os.path.isdir(candidate): + if candidate.lower().endswith("egg-info"): + path = opj(candidate, 'PKG-INFO') + elif candidate.endswith("dist-info"): + path = opj(candidate, 'METADATA') + else: # pragma: NO COVER + continue + else: + path = candidate + if os.path.exists(path): + with io.open(path, errors='ignore') as f: + return f.read() + warnings.warn('No PKG-INFO found for package: %s' % self.package_name) diff --git a/pkginfo/sdist.py b/pkginfo/sdist.py new file mode 100644 index 0000000..8c4e7cb --- /dev/null +++ b/pkginfo/sdist.py @@ -0,0 +1,75 @@ +import io +import os +import tarfile +import zipfile + +from .distribution import Distribution + +class SDist(Distribution): + + def __init__(self, filename, metadata_version=None): + self.filename = filename + self.metadata_version = metadata_version + self.extractMetadata() + + @classmethod + def _get_archive(cls, fqn): + if not os.path.exists(fqn): + raise ValueError('No such file: %s' % fqn) + + if zipfile.is_zipfile(fqn): + archive = zipfile.ZipFile(fqn) + names = archive.namelist() + def read_file(name): + return archive.read(name) + elif tarfile.is_tarfile(fqn): + archive = tarfile.TarFile.open(fqn) + names = archive.getnames() + def read_file(name): + return archive.extractfile(name).read() + else: + raise ValueError('Not a known archive format: %s' % fqn) + + return archive, names, read_file + + + def read(self): + fqn = os.path.abspath( + os.path.normpath(self.filename)) + + archive, names, read_file = self._get_archive(fqn) + + try: + tuples = [x.split('/') for x in names if 'PKG-INFO' in x] + schwarz = sorted([(len(x), x) for x in tuples]) + for path in [x[1] for x in schwarz]: + candidate = '/'.join(path) + data = read_file(candidate) + if b'Metadata-Version' in data: + return data + finally: + archive.close() + + raise ValueError('No PKG-INFO in archive: %s' % fqn) + + +class UnpackedSDist(SDist): + def __init__(self, filename, metadata_version=None): + if os.path.isdir(filename): + pass + elif os.path.isfile(filename): + filename = os.path.dirname(filename) + else: + raise ValueError('No such file: %s' % filename) + + super(UnpackedSDist, self).__init__( + filename, metadata_version=metadata_version) + + def read(self): + try: + pkg_info = os.path.join(self.filename, 'PKG-INFO') + with io.open(pkg_info, errors='ignore') as f: + return f.read() + except Exception as e: + raise ValueError('Could not load %s as an unpacked sdist: %s' + % (self.filename, e)) diff --git a/pkginfo/tests/__init__.py b/pkginfo/tests/__init__.py new file mode 100644 index 0000000..399240d --- /dev/null +++ b/pkginfo/tests/__init__.py @@ -0,0 +1,37 @@ +# requirements + + +def _checkSample(testcase, installed): + try: + import pkg_resources + except ImportError: # no setuptools :( + pass + else: + version = pkg_resources.require('pkginfo')[0].version + testcase.assertEqual(installed.version, version) + testcase.assertEqual(installed.name, 'pkginfo') + testcase.assertEqual(installed.keywords, + 'distribution sdist installed metadata' ) + testcase.assertEqual(list(installed.supported_platforms), []) + +def _checkClassifiers(testcase, installed): + testcase.assertEqual(list(installed.classifiers), + [ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: System :: Software Distribution', + ]) + + +def _defaultMetadataVersion(): + return '2.1' diff --git a/pkginfo/tests/funny/__init__.py b/pkginfo/tests/funny/__init__.py new file mode 100644 index 0000000..6012a75 --- /dev/null +++ b/pkginfo/tests/funny/__init__.py @@ -0,0 +1,2 @@ +# sample installed package w/ .egg-info file. +__package__ = 'funny' diff --git a/pkginfo/tests/funny/funny.egg-info b/pkginfo/tests/funny/funny.egg-info new file mode 100644 index 0000000..d3f79ef --- /dev/null +++ b/pkginfo/tests/funny/funny.egg-info @@ -0,0 +1,3 @@ +Metadata-Version: 1.0 +Name: funny +Version: 0.1 diff --git a/pkginfo/tests/manky/NOT-A-PACKAGE.txt b/pkginfo/tests/manky/NOT-A-PACKAGE.txt new file mode 100644 index 0000000..861b19e --- /dev/null +++ b/pkginfo/tests/manky/NOT-A-PACKAGE.txt @@ -0,0 +1,4 @@ +THIS IS NOT A PYTHON PACKAGE!!!! + +It is meant to be added to sys.path for testing introspection of namespace +packages installed via setuptools. diff --git a/pkginfo/tests/manky/namespaced.manky-0.1.egg-info/PKG-INFO b/pkginfo/tests/manky/namespaced.manky-0.1.egg-info/PKG-INFO new file mode 100644 index 0000000..ba75092 --- /dev/null +++ b/pkginfo/tests/manky/namespaced.manky-0.1.egg-info/PKG-INFO @@ -0,0 +1,2 @@ +Metadata-Version: 1.0 +Name: namespaced.wonky diff --git a/pkginfo/tests/manky/namespaced/__init__.py b/pkginfo/tests/manky/namespaced/__init__.py new file mode 100644 index 0000000..2e2033b --- /dev/null +++ b/pkginfo/tests/manky/namespaced/__init__.py @@ -0,0 +1,7 @@ +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/pkginfo/tests/manky/namespaced/manky/__init__.py b/pkginfo/tests/manky/namespaced/manky/__init__.py new file mode 100644 index 0000000..db26912 --- /dev/null +++ b/pkginfo/tests/manky/namespaced/manky/__init__.py @@ -0,0 +1 @@ +# Dummy package inside the 'namespaced' namespace. diff --git a/pkginfo/tests/silly/PKG-INFO b/pkginfo/tests/silly/PKG-INFO new file mode 100644 index 0000000..978e1b4 --- /dev/null +++ b/pkginfo/tests/silly/PKG-INFO @@ -0,0 +1,3 @@ +Metadata-Version: 1.0 +Name: silly +Version: 0.1 diff --git a/pkginfo/tests/test_bdist.py b/pkginfo/tests/test_bdist.py new file mode 100644 index 0000000..46e8a80 --- /dev/null +++ b/pkginfo/tests/test_bdist.py @@ -0,0 +1,60 @@ +import unittest + +class BDistTests(unittest.TestCase): + + def _getTargetClass(self): + from pkginfo.bdist import BDist + return BDist + + def _makeOne(self, filename=None, metadata_version=None): + if metadata_version is not None: + return self._getTargetClass()(filename, metadata_version) + return self._getTargetClass()(filename) + + def _checkSample(self, bdist, filename): + self.assertEqual(bdist.filename, filename) + self.assertEqual(bdist.name, 'mypackage') + self.assertEqual(bdist.version, '0.1') + self.assertEqual(bdist.keywords, None) + + def _checkClassifiers(self, bdist): + self.assertEqual(list(bdist.classifiers), + ['Development Status :: 4 - Beta', + 'Environment :: Console (Text Based)', + ]) + self.assertEqual(list(bdist.supported_platforms), []) + + def test_ctor_w_bogus_filename(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/nonesuch-0.1-py2.6.egg' % d + self.assertRaises(ValueError, self._makeOne, filename) + + def test_ctor_w_non_egg(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.zip' % d + self.assertRaises(ValueError, self._makeOne, filename) + + def test_ctor_wo_PKG_INFO(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/nopkginfo-0.1.egg' % d + self.assertRaises(ValueError, self._makeOne, filename) + + def test_ctor_w_egg(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1-py2.6.egg' % d + bdist = self._makeOne(filename) + self.assertEqual(bdist.metadata_version, '1.0') + self._checkSample(bdist, filename) + + def test_ctor_w_egg_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1-py2.6.egg' % d + bdist = self._makeOne(filename, metadata_version='1.1') + self.assertEqual(bdist.metadata_version, '1.1') + self._checkSample(bdist, filename) + self._checkClassifiers(bdist) diff --git a/pkginfo/tests/test_commandline.py b/pkginfo/tests/test_commandline.py new file mode 100644 index 0000000..804e4c3 --- /dev/null +++ b/pkginfo/tests/test_commandline.py @@ -0,0 +1,350 @@ +import unittest + +class Test__parse_options(unittest.TestCase): + + def _callFUT(self, args): + from pkginfo.commandline import _parse_options + return _parse_options(args) + + def test_empty(self): + import io + import sys + from pkginfo.commandline import __doc__ as usage + firstline = usage.splitlines()[0] + + # parse_args emits "native" error output. + if sys.version_info[0] < 3: + buf = io.BytesIO() + else: + buf = io.StringIO() + + with _Monkey(sys, stderr=buf): + self.assertRaises(SystemExit, self._callFUT, []) + self.assertTrue(firstline in buf.getvalue()) + + def test_nonempty(self): + options, args = self._callFUT(['foo']) + self.assertEqual(args, ['foo']) + +class BaseTests(unittest.TestCase): + + def _getTargetClass(self): + from pkginfo.commandline import Base + return Base + + def _makeOne(self, options): + return self._getTargetClass()(options) + + def test___init___defaults(self): + base = self._makeOne(_Options(fields=())) + self.assertTrue(base._fields is None) + + def test___init___w_fields(self): + fields = object() + base = self._makeOne(_Options(fields=fields)) + self.assertTrue(base._fields is fields) + +class _FormatterBase(object): + + def _capture_output(self, func, *args, **kw): + import io + import sys + # Emulate stdout as wanting "native" strings + if sys.version_info[0] < 3: + buf = io.BytesIO() + else: + buf = io.StringIO() + with _Monkey(sys, stdout=buf): + func(*args, **kw) + return buf.getvalue() + + def _no_output(self, simple, meta): + import sys + with _Monkey(sys, stdout=object()): # raise if write + simple(meta) + +class SimpleTests(unittest.TestCase, _FormatterBase): + + def _getTargetClass(self): + from pkginfo.commandline import Simple + return Simple + + def _makeOne(self, options): + return self._getTargetClass()(options) + + def test___init___(self): + simple = self._makeOne(_Options(fields=None, skip=True)) + self.assertTrue(simple._skip) + + def test___call___w_empty_fields(self): + simple = self._makeOne(_Options(fields=(), skip=False)) + meta = _Meta() + self._no_output(simple, meta) + + def test___call___w_skip_and_value_None_no_fields(self): + simple = self._makeOne(_Options(fields=(), skip=True)) + meta = _Meta(foo=None) + self._no_output(simple, meta) + + def test___call___w_skip_and_value_empty_tuple_explicit_fields(self): + simple = self._makeOne(_Options(fields=('foo',), skip=True)) + meta = _Meta(foo=(), bar='Bar') + self._no_output(simple, meta) + + def test___call___w_skip_but_values_explicit_fields(self): + simple = self._makeOne(_Options(fields=('foo',), skip=True)) + meta = _Meta(foo='Foo') + output = self._capture_output(simple, meta) + self.assertEqual(output, 'foo: Foo\n') + +class SingleLineTests(unittest.TestCase, _FormatterBase): + + def _getTargetClass(self): + from pkginfo.commandline import SingleLine + return SingleLine + + def _makeOne(self, options): + return self._getTargetClass()(options) + + def test___init___(self): + single = self._makeOne( + _Options(fields=None, item_delim='I', sequence_delim='S')) + self.assertEqual(single._item_delim, 'I') + self.assertEqual(single._sequence_delim, 'S') + + def test___call__wo_fields_wo_list(self): + single = self._makeOne( + _Options(fields=(), item_delim='|', + sequence_delim=object())) # raise if used + meta = _Meta(foo='Foo', bar='Bar') + output = self._capture_output(single, meta) + self.assertEqual(output, 'Bar|Foo\n') + + def test___call__w_fields_w_list(self): + single = self._makeOne( + _Options(fields=('foo', 'bar'), item_delim='|', + sequence_delim='*')) + meta = _Meta(foo='Foo', bar=['Bar1', 'Bar2'], baz='Baz') + output = self._capture_output(single, meta) + self.assertEqual(output, 'Foo|Bar1*Bar2\n') + +class CSVTests(unittest.TestCase, _FormatterBase): + + def _getTargetClass(self): + from pkginfo.commandline import CSV + return CSV + + def _makeOne(self, options): + return self._getTargetClass()(options) + + def test___init___(self): + csv = self._makeOne( + _Options(fields=None, sequence_delim='S')) + self.assertEqual(csv._sequence_delim, 'S') + + def test___call__wo_fields_wo_list(self): + meta = _Meta(foo='Foo', bar='Bar') + csv = self._makeOne( + _Options(fields=None, + sequence_delim=object())) # raise if used + output = self._capture_output(csv, meta) + self.assertEqual(output, 'bar,foo\r\nBar,Foo\r\n') + + def test___call__w_fields_w_list(self): + meta = _Meta(foo='Foo', bar=['Bar1', 'Bar2'], baz='Baz') + csv = self._makeOne( + _Options(fields=('foo', 'bar'), item_delim='|', + sequence_delim='*')) + output = self._capture_output(csv, meta) + self.assertEqual(output, 'foo,bar\r\nFoo,Bar1*Bar2\r\n') + +class INITests(unittest.TestCase, _FormatterBase): + + def _getTargetClass(self): + from pkginfo.commandline import INI + return INI + + def _makeOne(self, options): + return self._getTargetClass()(options) + + def test___call___duplicate(self): + ini = self._makeOne(_Options(fields=('foo',))) + meta = _Meta(name='foo', version='0.1', foo='Foo') + ini._parser.add_section('foo-0.1') + self.assertRaises(ValueError, ini, meta) + + def test___call___wo_fields_wo_list(self): + ini = self._makeOne(_Options(fields=None)) + meta = _Meta(name='foo', version='0.1', foo='Foo') + ini(meta) + cp = ini._parser + self.assertEqual(cp.sections(), ['foo-0.1']) + self.assertEqual(sorted(cp.options('foo-0.1')), + ['foo', 'name', 'version']) + self.assertEqual(cp.get('foo-0.1', 'name'), 'foo') + self.assertEqual(cp.get('foo-0.1', 'version'), '0.1') + self.assertEqual(cp.get('foo-0.1', 'foo'), 'Foo') + + def test___call___w_fields_w_list(self): + ini = self._makeOne(_Options(fields=('foo', 'bar'))) + meta = _Meta(name='foo', version='0.1', + foo='Foo', bar=['Bar1', 'Bar2'], baz='Baz') + ini(meta) + cp = ini._parser + self.assertEqual(cp.sections(), ['foo-0.1']) + self.assertEqual(sorted(cp.options('foo-0.1')), ['bar', 'foo']) + self.assertEqual(cp.get('foo-0.1', 'foo'), 'Foo') + self.assertEqual(cp.get('foo-0.1', 'bar'), 'Bar1\n\tBar2') + +class JSONtests(unittest.TestCase, _FormatterBase): + + def _getTargetClass(self): + from pkginfo.commandline import JSON + return JSON + + def _makeOne(self, options): + return self._getTargetClass()(options) + + def test___call___duplicate_with_meta_and_fields(self): + json = self._makeOne(_Options(fields=('name',))) + meta = _Meta(name='foo', version='0.1', foo='Foo') + json._mapping['name'] = 'foo' + self.assertRaises(ValueError, json, meta) + + def test___call___duplicate_with_meta_wo_fields(self): + json = self._makeOne(_Options(fields=None)) + meta = _Meta(name='foo', version='0.1', foo='Foo') + json._mapping['name'] = 'foo' + self.assertRaises(ValueError, json, meta) + + def test___call___wo_fields_wo_list(self): + from collections import OrderedDict + + json = self._makeOne(_Options(fields=None)) + meta = _Meta(name='foo', version='0.1', foo='Foo') + json(meta) + expected = OrderedDict([ + ('foo', 'Foo'), ('name', 'foo'), ('version', '0.1')]) + self.assertEqual(expected, json._mapping) + + def test___call___w_fields_w_list(self): + from collections import OrderedDict + + json = self._makeOne(_Options(fields=('foo', 'bar'))) + meta = _Meta(name='foo', version='0.1', + foo='Foo', bar=['Bar1', 'Bar2'], baz='Baz') + json(meta) + expected = OrderedDict([ + ('foo', 'Foo'), ('bar', ['Bar1', 'Bar2'])]) + self.assertEqual(expected, json._mapping) + + def test___call___output(self): + from collections import OrderedDict + import json as json_parser + + json = self._makeOne(_Options(fields=None)) + meta = _Meta(name='foo', version='0.1', foo='Foo') + json(meta) + output = self._capture_output(json.finish) + output = json_parser.loads( + output, object_pairs_hook=OrderedDict) + expected = OrderedDict([ + ('foo', 'Foo'), ('name', 'foo'), ('version', '0.1')]) + self.assertEqual(expected, output) + +class Test_main(unittest.TestCase): + + def _callFUT(self, args, monkey='simple'): + from pkginfo.commandline import main + from pkginfo.commandline import _FORMATTERS + before = _FORMATTERS[monkey] + dummy = _Formatter() + _FORMATTERS[monkey] = lambda *options: dummy + try: + main(args) + finally: + _FORMATTERS[monkey] = before + return dummy + + def test_w_mising_dist(self): + from pkginfo import commandline as MUT + def _get_metadata(path_or_module, md_version): + self.assertEqual(path_or_module, 'foo') + self.assertEqual(md_version, None) + return None + with _Monkey(MUT, get_metadata=_get_metadata): + formatter = self._callFUT(['foo']) + self.assertEqual(formatter._called_with, []) + self.assertTrue(formatter._finished) + + def test_w_dist_wo_download_url(self): + from pkginfo import commandline as MUT + meta = _Meta(download_url=None) + def _get_metadata(path_or_module, md_version): + self.assertEqual(path_or_module, '/path/to/foo') + self.assertEqual(md_version, None) + return meta + with _Monkey(MUT, get_metadata=_get_metadata): + formatter = self._callFUT( + ['-d', 'http://example.com', '/path/to/foo']) + self.assertEqual(formatter._called_with, [meta]) + self.assertTrue(formatter._finished) + self.assertEqual(meta.download_url, 'http://example.com/foo') + + def test_w_dist_w_download_url(self): + from pkginfo import commandline as MUT + meta = _Meta(download_url='http://example.com/dist/foo') + def _get_metadata(path_or_module, md_version): + self.assertEqual(path_or_module, '/path/to/foo') + self.assertEqual(md_version, None) + return meta + with _Monkey(MUT, get_metadata=_get_metadata): + formatter = self._callFUT( + ['-d', 'http://example.com', '/path/to/foo']) + self.assertEqual(formatter._called_with, [meta]) + self.assertTrue(formatter._finished) + self.assertEqual(meta.download_url, 'http://example.com/dist/foo') + +class _Options(object): + + def __init__(self, **kw): + for k in kw: + self.__dict__[k] = kw[k] + +class _Meta(object): + + def __init__(self, **kw): + for k in kw: + self.__dict__[k] = kw[k] + + def __iter__(self): + return iter(sorted(self.__dict__)) + +class _Monkey(object): + # context-manager for replacing module names in the scope of a test. + + def __init__(self, module, **kw): + self.module = module + self.to_restore = dict([(key, getattr(module, key)) for key in kw]) + for key, value in kw.items(): + setattr(module, key, value) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + for key, value in self.to_restore.items(): + setattr(self.module, key, value) + +class _Formatter(object): + + _finished = False + + def __init__(self): + self._called_with = [] + + def __call__(self, meta): + self._called_with.append(meta) + + def finish(self): + self._finished = True diff --git a/pkginfo/tests/test_develop.py b/pkginfo/tests/test_develop.py new file mode 100644 index 0000000..085cb66 --- /dev/null +++ b/pkginfo/tests/test_develop.py @@ -0,0 +1,27 @@ +import unittest + +class DevelopTests(unittest.TestCase): + + def _getTargetClass(self): + from pkginfo.develop import Develop + return Develop + + def _makeOne(self, dirname=None): + return self._getTargetClass()(dirname) + + def test_ctor_w_path(self): + from pkginfo.tests import _checkSample + develop = self._makeOne('.') + _checkSample(self, develop) + + def test_ctor_w_invalid_path(self): + import warnings + old_filters = warnings.filters[:] + warnings.filterwarnings('ignore') + try: + develop = self._makeOne('/nonesuch') + self.assertEqual(develop.metadata_version, None) + self.assertEqual(develop.name, None) + self.assertEqual(develop.version, None) + finally: + warnings.filters[:] = old_filters diff --git a/pkginfo/tests/test_distribution.py b/pkginfo/tests/test_distribution.py new file mode 100644 index 0000000..8f8dcae --- /dev/null +++ b/pkginfo/tests/test_distribution.py @@ -0,0 +1,450 @@ +import unittest + +class DistributionTests(unittest.TestCase): + + def _getTargetClass(self): + from pkginfo.distribution import Distribution + return Distribution + + def _makeOne(self, metadata_version='1.0'): + dist = self._getTargetClass()() + if metadata_version is not None: + dist.metadata_version = metadata_version + return dist + + def test_ctor_defaults(self): + sdist = self._makeOne(None) + self.assertEqual(sdist.metadata_version, None) + # version 1.0 + self.assertEqual(sdist.name, None) + self.assertEqual(sdist.version, None) + self.assertEqual(sdist.platforms, ()) + self.assertEqual(sdist.supported_platforms, ()) + self.assertEqual(sdist.summary, None) + self.assertEqual(sdist.description, None) + self.assertEqual(sdist.keywords, None) + self.assertEqual(sdist.home_page, None) + self.assertEqual(sdist.download_url, None) + self.assertEqual(sdist.author, None) + self.assertEqual(sdist.author_email, None) + self.assertEqual(sdist.license, None) + # version 1.1 + self.assertEqual(sdist.classifiers, ()) + self.assertEqual(sdist.requires, ()) + self.assertEqual(sdist.provides, ()) + self.assertEqual(sdist.obsoletes, ()) + # version 1.2 + self.assertEqual(sdist.maintainer, None) + self.assertEqual(sdist.maintainer_email, None) + self.assertEqual(sdist.requires_python, None) + self.assertEqual(sdist.requires_external, ()) + self.assertEqual(sdist.requires_dist, ()) + self.assertEqual(sdist.provides_dist, ()) + self.assertEqual(sdist.obsoletes_dist, ()) + self.assertEqual(sdist.project_urls, ()) + # version 2.1 + self.assertEqual(sdist.provides_extras, ()) + self.assertEqual(sdist.description_content_type, None) + # version 2.2 + self.assertEqual(sdist.dynamic, ()) + + def test_extractMetadata_raises_NotImplementedError(self): + # 'extractMetadata' calls 'read', which subclasses must override. + dist = self._makeOne(None) + self.assertRaises(NotImplementedError, dist.extractMetadata) + + def test_read_raises_NotImplementedError(self): + # Subclasses must override 'read'. + dist = self._makeOne(None) + self.assertRaises(NotImplementedError, dist.read) + + def test_parse_given_unicode(self): + from pkginfo._compat import u + dist = self._makeOne() + dist.parse(u('Metadata-Version: 1.0\nName: lp722928_c3')) # no raise + + def test_parse_Metadata_Version_1_0(self): + from pkginfo.distribution import HEADER_ATTRS_1_0 + dist = self._makeOne(None) + dist.parse('Metadata-Version: 1.0') + self.assertEqual(dist.metadata_version, '1.0') + self.assertEqual(list(dist), + [x[1] for x in HEADER_ATTRS_1_0]) + + def test_parse_Metadata_Version_1_1(self): + from pkginfo.distribution import HEADER_ATTRS_1_1 + dist = self._makeOne(None) + dist.parse('Metadata-Version: 1.1') + self.assertEqual(dist.metadata_version, '1.1') + self.assertEqual(list(dist), + [x[1] for x in HEADER_ATTRS_1_1]) + + def test_parse_Metadata_Version_1_2(self): + from pkginfo.distribution import HEADER_ATTRS_1_2 + dist = self._makeOne(None) + dist.parse('Metadata-Version: 1.2') + self.assertEqual(dist.metadata_version, '1.2') + self.assertEqual(list(dist), + [x[1] for x in HEADER_ATTRS_1_2]) + + def test_parse_Metadata_Version_2_1(self): + from pkginfo.distribution import HEADER_ATTRS_2_1 + dist = self._makeOne(None) + dist.parse('Metadata-Version: 2.1') + self.assertEqual(dist.metadata_version, '2.1') + self.assertEqual(list(dist), + [x[1] for x in HEADER_ATTRS_2_1]) + + def test_parse_Metadata_Version_2_2(self): + from pkginfo.distribution import HEADER_ATTRS_2_2 + dist = self._makeOne(None) + dist.parse('Metadata-Version: 2.2') + self.assertEqual(dist.metadata_version, '2.2') + self.assertEqual(list(dist), + [x[1] for x in HEADER_ATTRS_2_2]) + + def test_parse_Metadata_Version_unknown(self): + dist = self._makeOne(None) + dist.parse('Metadata-Version: 1.3') + self.assertEqual(dist.metadata_version, '1.3') + self.assertEqual(list(dist), []) + + def test_parse_Metadata_Version_override(self): + dist = self._makeOne('1.2') + dist.parse('Metadata-Version: 1.0') + self.assertEqual(dist.metadata_version, '1.2') + + def test_parse_Name(self): + dist = self._makeOne() + dist.parse('Name: foobar') + self.assertEqual(dist.name, 'foobar') + + def test_parse_Version(self): + dist = self._makeOne() + dist.parse('Version: 2.1.3b5') + self.assertEqual(dist.version, '2.1.3b5') + + def test_parse_Platform_single(self): + dist = self._makeOne() + dist.parse('Platform: Plan9') + self.assertEqual(list(dist.platforms), ['Plan9']) + + def test_parse_Platform_multiple(self): + dist = self._makeOne() + dist.parse('Platform: Plan9\nPlatform: AIX') + self.assertEqual(list(dist.platforms), ['Plan9', 'AIX']) + + def test_parse_Supported_Platform_single(self): + dist = self._makeOne() + dist.parse('Supported-Platform: Plan9') + self.assertEqual(list(dist.supported_platforms), ['Plan9']) + + def test_parse_Supported_Platform_multiple(self): + dist = self._makeOne() + dist.parse('Supported-Platform: i386-win32\n' + 'Supported-Platform: RedHat 7.2') + self.assertEqual(list(dist.supported_platforms), + ['i386-win32', 'RedHat 7.2']) + + def test_parse_Summary(self): + dist = self._makeOne() + dist.parse('Summary: Package for foo') + self.assertEqual(dist.summary, 'Package for foo') + + def test_parse_Description(self): + dist = self._makeOne() + dist.parse('Description: This package enables integration with ' + 'foo servers.') + self.assertEqual(dist.description, + 'This package enables integration with ' + 'foo servers.') + + def test_parse_Description_multiline(self): + dist = self._makeOne() + dist.parse('Description: This package enables integration with\n' + ' foo servers.') + self.assertEqual(dist.description, + 'This package enables integration with\n' + 'foo servers.') + + def test_parse_Description_in_payload(self): + dist = self._makeOne() + dist.parse('Foo: Bar\n' + '\n' + 'This package enables integration with\n' + 'foo servers.') + self.assertEqual(dist.description, + 'This package enables integration with\n' + 'foo servers.') + + def test_parse_Keywords(self): + dist = self._makeOne() + dist.parse('Keywords: bar foo qux') + self.assertEqual(dist.keywords, 'bar foo qux') + + def test_parse_Home_page(self): + dist = self._makeOne() + dist.parse('Home-page: http://example.com/package') + self.assertEqual(dist.home_page, 'http://example.com/package') + + def test_parse_Author(self): + dist = self._makeOne() + dist.parse('Author: J. Phredd Bloggs') + self.assertEqual(dist.author, 'J. Phredd Bloggs') + + def test_parse_Author_Email(self): + dist = self._makeOne() + dist.parse('Author-email: phreddy@example.com') + self.assertEqual(dist.author_email, 'phreddy@example.com') + + def test_parse_License(self): + dist = self._makeOne() + dist.parse('License: Poetic') + self.assertEqual(dist.license, 'Poetic') + + # Metadata version 1.1, defined in PEP 314. + def test_parse_Classifier_single(self): + dist = self._makeOne('1.1') + dist.parse('Classifier: Some :: Silly Thing') + self.assertEqual(list(dist.classifiers), ['Some :: Silly Thing']) + + def test_parse_Classifier_multiple(self): + dist = self._makeOne('1.1') + dist.parse('Classifier: Some :: Silly Thing\n' + 'Classifier: Or :: Other') + self.assertEqual(list(dist.classifiers), + ['Some :: Silly Thing', 'Or :: Other']) + + def test_parse_Download_URL(self): + dist = self._makeOne('1.1') + dist.parse('Download-URL: ' + 'http://example.com/package/mypackage-0.1.zip') + self.assertEqual(dist.download_url, + 'http://example.com/package/mypackage-0.1.zip') + + def test_parse_Requires_single_wo_version(self): + dist = self._makeOne('1.1') + dist.parse('Requires: SpanishInquisition') + self.assertEqual(list(dist.requires), ['SpanishInquisition']) + + def test_parse_Requires_single_w_version(self): + dist = self._makeOne('1.1') + dist.parse('Requires: SpanishInquisition (>=1.3)') + self.assertEqual(list(dist.requires), ['SpanishInquisition (>=1.3)']) + + def test_parse_Requires_multiple(self): + dist = self._makeOne('1.1') + dist.parse('Requires: SpanishInquisition\n' + 'Requires: SillyWalks (1.4)\n' + 'Requires: kniggits (>=2.3,<3.0)') + self.assertEqual(list(dist.requires), + ['SpanishInquisition', + 'SillyWalks (1.4)', + 'kniggits (>=2.3,<3.0)', + ]) + + def test_parse_Provides_single_wo_version(self): + dist = self._makeOne('1.1') + dist.parse('Provides: SillyWalks') + self.assertEqual(list(dist.provides), ['SillyWalks']) + + def test_parse_Provides_single_w_version(self): + dist = self._makeOne('1.1') + dist.parse('Provides: SillyWalks (1.4)') + self.assertEqual(list(dist.provides), ['SillyWalks (1.4)']) + + def test_parse_Provides_multiple(self): + dist = self._makeOne('1.1') + dist.parse('Provides: SillyWalks\n' + 'Provides: DeadlyJoke (3.1.4)') + self.assertEqual(list(dist.provides), + ['SillyWalks', + 'DeadlyJoke (3.1.4)', + ]) + + def test_parse_Obsoletes_single_no_version(self): + dist = self._makeOne('1.1') + dist.parse('Obsoletes: SillyWalks') + self.assertEqual(list(dist.obsoletes), ['SillyWalks']) + + def test_parse_Obsoletes_single_w_version(self): + dist = self._makeOne('1.1') + dist.parse('Obsoletes: SillyWalks (<=1.3)') + self.assertEqual(list(dist.obsoletes), ['SillyWalks (<=1.3)']) + + def test_parse_Obsoletes_multiple(self): + dist = self._makeOne('1.1') + dist.parse('Obsoletes: kniggits\n' + 'Obsoletes: SillyWalks (<=2.0)') + self.assertEqual(list(dist.obsoletes), + ['kniggits', + 'SillyWalks (<=2.0)', + ]) + + + # Metadata version 1.2, defined in PEP 345. + def test_parse_Maintainer(self): + dist = self._makeOne(metadata_version='1.2') + dist.parse('Maintainer: J. Phredd Bloggs') + self.assertEqual(dist.maintainer, 'J. Phredd Bloggs') + + def test_parse_Maintainer_Email(self): + dist = self._makeOne(metadata_version='1.2') + dist.parse('Maintainer-email: phreddy@example.com') + self.assertEqual(dist.maintainer_email, 'phreddy@example.com') + + def test_parse_Requires_Python_single_spec(self): + dist = self._makeOne('1.2') + dist.parse('Requires-Python: >2.4') + self.assertEqual(dist.requires_python, '>2.4') + + def test_parse_Requires_External_single_wo_version(self): + dist = self._makeOne('1.2') + dist.parse('Requires-External: libfoo') + self.assertEqual(list(dist.requires_external), ['libfoo']) + + def test_parse_Requires_External_single_w_version(self): + dist = self._makeOne('1.2') + dist.parse('Requires-External: libfoo (>=1.3)') + self.assertEqual(list(dist.requires_external), ['libfoo (>=1.3)']) + + def test_parse_Requires_External_multiple(self): + dist = self._makeOne('1.2') + dist.parse('Requires-External: libfoo\n' + 'Requires-External: libbar (1.4)\n' + 'Requires-External: libbaz (>=2.3,<3.0)') + self.assertEqual(list(dist.requires_external), + ['libfoo', + 'libbar (1.4)', + 'libbaz (>=2.3,<3.0)', + ]) + + + def test_parse_Requires_Dist_single_wo_version(self): + dist = self._makeOne('1.2') + dist.parse('Requires-Dist: SpanishInquisition') + self.assertEqual(list(dist.requires_dist), ['SpanishInquisition']) + + def test_parse_Requires_Dist_single_w_version(self): + dist = self._makeOne('1.2') + dist.parse('Requires-Dist: SpanishInquisition (>=1.3)') + self.assertEqual(list(dist.requires_dist), + ['SpanishInquisition (>=1.3)']) + + def test_parse_Requires_Dist_single_w_env_marker(self): + dist = self._makeOne('1.2') + dist.parse("Requires-Dist: SpanishInquisition; " + "python_version == '1.4'") + self.assertEqual(list(dist.requires_dist), + ["SpanishInquisition; python_version == '1.4'"]) + + def test_parse_Requires_Dist_multiple(self): + dist = self._makeOne('1.2') + dist.parse("Requires-Dist: SpanishInquisition\n" + "Requires-Dist: SillyWalks; python_version == '1.4'\n" + "Requires-Dist: kniggits (>=2.3,<3.0)") + self.assertEqual(list(dist.requires_dist), + ["SpanishInquisition", + "SillyWalks; python_version == '1.4'", + "kniggits (>=2.3,<3.0)", + ]) + + def test_parse_Provides_Dist_single_wo_version(self): + dist = self._makeOne('1.2') + dist.parse('Provides-Dist: SillyWalks') + self.assertEqual(list(dist.provides_dist), ['SillyWalks']) + + def test_parse_Provides_Dist_single_w_version(self): + dist = self._makeOne('1.2') + dist.parse('Provides-Dist: SillyWalks (1.4)') + self.assertEqual(list(dist.provides_dist), ['SillyWalks (1.4)']) + + def test_parse_Provides_Dist_single_w_env_marker(self): + dist = self._makeOne('1.2') + dist.parse("Provides-Dist: SillyWalks; sys.platform == 'os2'") + self.assertEqual(list(dist.provides_dist), + ["SillyWalks; sys.platform == 'os2'"]) + + def test_parse_Provides_Dist_multiple(self): + dist = self._makeOne('1.2') + dist.parse("Provides-Dist: SillyWalks\n" + "Provides-Dist: SpanishInquisition; sys.platform == 'os2'\n" + "Provides-Dist: DeadlyJoke (3.1.4)") + self.assertEqual(list(dist.provides_dist), + ["SillyWalks", + "SpanishInquisition; sys.platform == 'os2'", + "DeadlyJoke (3.1.4)", + ]) + + def test_parse_Obsoletes_Dist_single_no_version(self): + dist = self._makeOne('1.2') + dist.parse('Obsoletes-Dist: SillyWalks') + self.assertEqual(list(dist.obsoletes_dist), ['SillyWalks']) + + def test_parse_Obsoletes_Dist_single_w_version(self): + dist = self._makeOne('1.2') + dist.parse('Obsoletes-Dist: SillyWalks (<=1.3)') + self.assertEqual(list(dist.obsoletes_dist), ['SillyWalks (<=1.3)']) + + def test_parse_Obsoletes_Dist_single_w_env_marker(self): + dist = self._makeOne('1.2') + dist.parse("Obsoletes-Dist: SillyWalks; sys.platform == 'os2'") + self.assertEqual(list(dist.obsoletes_dist), + ["SillyWalks; sys.platform == 'os2'"]) + + def test_parse_Obsoletes_Dist_multiple(self): + dist = self._makeOne('1.2') + dist.parse("Obsoletes-Dist: kniggits\n" + "Obsoletes-Dist: SillyWalks; sys.platform == 'os2'\n" + "Obsoletes-Dist: DeadlyJoke (<=2.0)\n" + ) + self.assertEqual(list(dist.obsoletes_dist), + ["kniggits", + "SillyWalks; sys.platform == 'os2'", + "DeadlyJoke (<=2.0)", + ]) + + def test_parse_Project_URL_single_no_version(self): + dist = self._makeOne('1.2') + dist.parse('Project-URL: Bug tracker, http://bugs.example.com/grail') + self.assertEqual(list(dist.project_urls), + ['Bug tracker, http://bugs.example.com/grail']) + + def test_parse_Project_URL_multiple(self): + dist = self._makeOne('1.2') + dist.parse('Project-URL: Bug tracker, http://bugs.example.com/grail\n' + 'Project-URL: Repository, http://svn.example.com/grail') + self.assertEqual(list(dist.project_urls), + ['Bug tracker, http://bugs.example.com/grail', + 'Repository, http://svn.example.com/grail', + ]) + + # Metadata version 2.1, defined in PEP 566. + def test_parse_Provides_Extra_single(self): + dist = self._makeOne('2.1') + dist.parse('Provides-Extra: pdf') + self.assertEqual(list(dist.provides_extras), ['pdf']) + + def test_parse_Provides_Extra_multiple(self): + dist = self._makeOne('2.1') + dist.parse('Provides-Extra: pdf\n' + 'Provides-Extra: tex') + self.assertEqual(list(dist.provides_extras), ['pdf', 'tex']) + + def test_parse_Provides_Extra_single(self): + dist = self._makeOne('2.1') + dist.parse('Description-Content-Type: text/plain') + self.assertEqual(dist.description_content_type, 'text/plain') + + # Metadata version 2.2, defined in PEP 643. + def test_parse_Dynamic_single(self): + dist = self._makeOne('2.2') + dist.parse('Dynamic: Platforms') + self.assertEqual(list(dist.dynamic), ['Platforms']) + + def test_parse_Dynamic_multiple(self): + dist = self._makeOne('2.2') + dist.parse('Dynamic: Platforms\n' + 'Dynamic: Supported-Platforms') + self.assertEqual(list(dist.dynamic), + ['Platforms', 'Supported-Platforms']) diff --git a/pkginfo/tests/test_index.py b/pkginfo/tests/test_index.py new file mode 100644 index 0000000..bbff12c --- /dev/null +++ b/pkginfo/tests/test_index.py @@ -0,0 +1,76 @@ +import unittest + +class IndexTests(unittest.TestCase): + + def _getTargetClass(self): + from pkginfo.index import Index + return Index + + def _makeOne(self): + return self._getTargetClass()() + + def test_empty(self): + index = self._makeOne() + self.assertEqual(len(index), 0) + self.assertEqual(len(index.keys()), 0) + self.assertEqual(len(index.values()), 0) + self.assertEqual(len(index.items()), 0) + + def _makeDummy(self): + from pkginfo.distribution import Distribution + class DummyDistribution(Distribution): + name = 'dummy' + version = '1.0' + + return DummyDistribution() + + def test___getitem___miss(self): + index = self._makeOne() + self.assertRaises(KeyError, index.__getitem__, 'nonesuch') + + def test___setitem___value_not_dist(self): + class NotDistribution: + name = 'dummy' + version = '1.0' + dummy = NotDistribution() + index = self._makeOne() + self.assertRaises(ValueError, index.__setitem__, 'dummy-1.0', dummy) + + def test___setitem___bad_key(self): + index = self._makeOne() + dummy = self._makeDummy() + self.assertRaises(ValueError, index.__setitem__, 'nonesuch', dummy) + + def test___setitem___valid_key(self): + index = self._makeOne() + dummy = self._makeDummy() + index['dummy-1.0'] = dummy + self.assertTrue(index['dummy-1.0'] is dummy) + self.assertEqual(len(index), 1) + self.assertEqual(len(index.keys()), 1) + self.assertEqual(list(index.keys())[0], 'dummy-1.0') + self.assertEqual(len(index.values()), 1) + self.assertEqual(list(index.values())[0], dummy) + self.assertEqual(len(index.items()), 1) + self.assertEqual(list(index.items())[0], ('dummy-1.0', dummy)) + + def test_add_not_dist(self): + index = self._makeOne() + class NotDistribution: + name = 'dummy' + version = '1.0' + dummy = NotDistribution() + self.assertRaises(ValueError, index.add, dummy) + + def test_add_valid_dist(self): + index = self._makeOne() + dummy = self._makeDummy() + index.add(dummy) + self.assertTrue(index['dummy-1.0'] is dummy) + self.assertEqual(len(index), 1) + self.assertEqual(len(index.keys()), 1) + self.assertEqual(list(index.keys())[0], 'dummy-1.0') + self.assertEqual(len(index.values()), 1) + self.assertEqual(list(index.values())[0], dummy) + self.assertEqual(len(index.items()), 1) + self.assertEqual(list(index.items())[0], ('dummy-1.0', dummy)) diff --git a/pkginfo/tests/test_installed.py b/pkginfo/tests/test_installed.py new file mode 100644 index 0000000..934dadc --- /dev/null +++ b/pkginfo/tests/test_installed.py @@ -0,0 +1,142 @@ +import unittest + +class InstalledTests(unittest.TestCase): + + def _getTargetClass(self): + from pkginfo.installed import Installed + return Installed + + def _makeOne(self, filename=None, metadata_version=None): + if metadata_version is not None: + return self._getTargetClass()(filename, metadata_version) + return self._getTargetClass()(filename) + + def test_ctor_w_package_no___file__(self): + import sys + import warnings + with warnings.catch_warnings(record=True): + installed = self._makeOne(sys) + self.assertEqual(installed.package, sys) + self.assertEqual(installed.package_name, 'sys') + self.assertEqual(installed.metadata_version, None) + + def test_ctor_w_package(self): + import pkginfo + from pkginfo.tests import _checkSample + from pkginfo.tests import _defaultMetadataVersion + EXPECTED = _defaultMetadataVersion() + installed = self._makeOne(pkginfo) + self.assertEqual(installed.package, pkginfo) + self.assertEqual(installed.package_name, 'pkginfo') + self.assertEqual(installed.metadata_version, EXPECTED) + _checkSample(self, installed) + + def test_ctor_w_no___package___falls_back_to___name__(self): + import sys + import wsgiref + import warnings + with warnings.catch_warnings(record=True): + installed = self._makeOne(wsgiref) + self.assertEqual(installed.package, wsgiref) + self.assertEqual(installed.package_name, 'wsgiref') + if sys.version_info[:2] >= (3, 3): + self.assertEqual(installed.metadata_version, None) + else: + self.assertEqual(installed.metadata_version, '1.0') + + def test_ctor_w_package_no_PKG_INFO(self): + import sys + import types + import warnings + with warnings.catch_warnings(record=True): + installed = self._makeOne(types) + self.assertEqual(installed.package, types) + self.assertEqual(installed.package_name, 'types') + self.assertEqual(installed.metadata_version, None) + + def test_ctor_w_package_and_metadata_version(self): + import pkginfo + from pkginfo.tests import _checkSample + installed = self._makeOne(pkginfo, metadata_version='1.2') + self.assertEqual(installed.metadata_version, '1.2') + self.assertEqual(installed.package.__name__, 'pkginfo') + _checkSample(self, installed) + + def test_ctor_w_name(self): + import pkginfo + from pkginfo.tests import _checkSample + from pkginfo.tests import _defaultMetadataVersion + EXPECTED = _defaultMetadataVersion() + installed = self._makeOne('pkginfo') + self.assertEqual(installed.metadata_version, EXPECTED) + self.assertEqual(installed.package, pkginfo) + self.assertEqual(installed.package_name, 'pkginfo') + _checkSample(self, installed) + + def test_ctor_w_name_and_metadata_version(self): + import pkginfo + from pkginfo.tests import _checkSample + installed = self._makeOne('pkginfo', metadata_version='1.2') + self.assertEqual(installed.metadata_version, '1.2') + self.assertEqual(installed.package, pkginfo) + self.assertEqual(installed.package_name, 'pkginfo') + _checkSample(self, installed) + + def test_ctor_w_invalid_name(self): + import warnings + with warnings.catch_warnings(record=True): + installed = self._makeOne('nonesuch') + self.assertEqual(installed.package, None) + self.assertEqual(installed.package_name, 'nonesuch') + self.assertEqual(installed.metadata_version, None) + + def test_ctor_w_egg_info_as_file(self): + import pkginfo.tests.funny + installed = self._makeOne('pkginfo.tests.funny') + self.assertEqual(installed.metadata_version, '1.0') + self.assertEqual(installed.package, pkginfo.tests.funny) + self.assertEqual(installed.package_name, 'pkginfo.tests.funny') + + def test_ctor_w_dist_info(self): + import wheel + installed = self._makeOne('wheel') + self.assertEqual(installed.metadata_version, '2.1') + self.assertEqual(installed.package, wheel) + self.assertEqual(installed.package_name, 'wheel') + + def test_namespaced_pkg_installed_via_setuptools(self): + import os + import sys + where, _ = os.path.split(__file__) + wonky = os.path.join(where, 'wonky') + oldpath = sys.path[:] + try: + sys.path.append(wonky) + import namespaced.wonky + installed = self._makeOne('namespaced.wonky') + self.assertEqual(installed.metadata_version, '1.0') + self.assertEqual(installed.package, namespaced.wonky) + self.assertEqual(installed.package_name, 'namespaced.wonky') + finally: + sys.path[:] = oldpath + sys.modules.pop('namespaced.wonky', None) + sys.modules.pop('namespaced', None) + + def test_namespaced_pkg_installed_via_pth(self): + # E.g., installed by a Linux distro + import os + import sys + where, _ = os.path.split(__file__) + manky = os.path.join(where, 'manky') + oldpath = sys.path[:] + try: + sys.path.append(manky) + import namespaced.manky + installed = self._makeOne('namespaced.manky') + self.assertEqual(installed.metadata_version, '1.0') + self.assertEqual(installed.package, namespaced.manky) + self.assertEqual(installed.package_name, 'namespaced.manky') + finally: + sys.path[:] = oldpath + sys.modules.pop('namespaced.manky', None) + sys.modules.pop('namespaced', None) diff --git a/pkginfo/tests/test_sdist.py b/pkginfo/tests/test_sdist.py new file mode 100644 index 0000000..88e87d0 --- /dev/null +++ b/pkginfo/tests/test_sdist.py @@ -0,0 +1,158 @@ +import shutil +import tempfile +import unittest + +class SDistTests(unittest.TestCase): + + def _getTargetClass(self): + from pkginfo.sdist import SDist + return SDist + + def _makeOne(self, filename=None, metadata_version=None): + if metadata_version is not None: + return self._getTargetClass()(filename, metadata_version) + return self._getTargetClass()(filename) + + def _checkSample(self, sdist, filename): + self.assertEqual(sdist.filename, filename) + self.assertEqual(sdist.name, 'mypackage') + self.assertEqual(sdist.version, '0.1') + self.assertEqual(sdist.keywords, None) + self.assertEqual(list(sdist.supported_platforms), []) + + def _checkClassifiers(self, sdist): + self.assertEqual(list(sdist.classifiers), + ['Development Status :: 4 - Beta', + 'Environment :: Console (Text Based)', + ]) + + def test_ctor_w_invalid_filename(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/nonesuch-0.1.tar.gz' % d + self.assertRaises(ValueError, self._makeOne, filename) + + def test_ctor_wo_PKG_INFO(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/nopkginfo-0.1.zip' % d + self.assertRaises(ValueError, self._makeOne, filename) + + def test_ctor_w_tar(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.tar' % d + sdist = self._makeOne(filename) + self.assertEqual(sdist.metadata_version, '1.0') + self._checkSample(sdist, filename) + + def test_ctor_w_gztar(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.tar.gz' % d + sdist = self._makeOne(filename) + self.assertEqual(sdist.metadata_version, '1.0') + self._checkSample(sdist, filename) + + def test_ctor_w_gztar_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.tar.gz' % d + sdist = self._makeOne(filename, metadata_version='1.1') + self._checkSample(sdist, filename) + self.assertEqual(sdist.metadata_version, '1.1') + self._checkClassifiers(sdist) + + def test_ctor_w_bztar(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.tar.bz2' % d + sdist = self._makeOne(filename) + self.assertEqual(sdist.metadata_version, '1.0') + self._checkSample(sdist, filename) + + def test_ctor_w_bztar_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.tar.bz2' % d + sdist = self._makeOne(filename, metadata_version='1.1') + self.assertEqual(sdist.metadata_version, '1.1') + self._checkSample(sdist, filename) + self._checkClassifiers(sdist) + + def test_ctor_w_zip(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.zip' % d + sdist = self._makeOne(filename) + self.assertEqual(sdist.metadata_version, '1.0') + self._checkSample(sdist, filename) + + def test_ctor_w_zip_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.zip' % d + sdist = self._makeOne(filename, metadata_version='1.1') + self.assertEqual(sdist.metadata_version, '1.1') + self._checkSample(sdist, filename) + self._checkClassifiers(sdist) + + def test_ctor_w_bogus(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.bogus' % d + + with self.assertRaises(ValueError): + self._makeOne(filename, metadata_version='1.1') + + +class UnpackedMixin(object): + def setUp(self): + super(UnpackedMixin, self).setUp() + self.__tmpdir = tempfile.mkdtemp() + + def tearDown(self): + shutil.rmtree(self.__tmpdir) + super(UnpackedMixin, self).tearDown() + + def _getTargetClass(self): + from pkginfo.sdist import UnpackedSDist + return UnpackedSDist + + def _getTopDirectory(self): + import os + topnames = os.listdir(self.__tmpdir) + if len(topnames) == 1: + return os.path.join(self.__tmpdir, topnames[0]) + else: + return self.__tmpdir + + def _getLoadFilename(self): + return self._getTopDirectory() + + def _makeOne(self, filename=None, metadata_version=None): + + archive, _, _ = self._getTargetClass()._get_archive(filename) + try: + archive.extractall(self.__tmpdir) + finally: + archive.close() + + load_filename = self._getLoadFilename() + + if metadata_version is not None: + return self._getTargetClass()(load_filename, metadata_version) + return self._getTargetClass()(load_filename) + + def _checkSample(self, sdist, filename): + filename = self._getTopDirectory() + super(UnpackedMixin, self)._checkSample(sdist, filename) + + +class UnpackedSDistGivenDirectoryTests(UnpackedMixin, SDistTests): + pass + +class UnpackedSDistGivenFileSDistTests(UnpackedMixin, SDistTests): + def _getLoadFilename(self): + import os + return os.path.join(self._getTopDirectory(), 'setup.py') diff --git a/pkginfo/tests/test_utils.py b/pkginfo/tests/test_utils.py new file mode 100644 index 0000000..bd6f74a --- /dev/null +++ b/pkginfo/tests/test_utils.py @@ -0,0 +1,176 @@ +import unittest + +class Test_get_metadata(unittest.TestCase): + + def _callFUT(self, path, metadata_version=None): + from pkginfo.utils import get_metadata + if metadata_version is not None: + return get_metadata(path, metadata_version) + return get_metadata(path) + + def _checkMyPackage(self, dist, filename): + self.assertEqual(dist.filename, filename) + self.assertEqual(dist.name, 'mypackage') + self.assertEqual(dist.version, '0.1') + self.assertEqual(dist.keywords, None) + self.assertEqual(list(dist.supported_platforms), []) + + def _checkClassifiers(self, dist): + self.assertEqual(list(dist.classifiers), + ['Development Status :: 4 - Beta', + 'Environment :: Console (Text Based)', + ]) + + def test_w_gztar(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.tar.gz' % d + dist = self._callFUT(filename) + self.assertEqual(dist.metadata_version, '1.0') + self._checkMyPackage(dist, filename) + + def test_w_gztar_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.tar.gz' % d + dist = self._callFUT(filename, metadata_version='1.1') + self.assertEqual(dist.metadata_version, '1.1') + self._checkMyPackage(dist, filename) + self._checkClassifiers(dist) + + def test_w_bztar(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.tar.bz2' % d + dist = self._callFUT(filename) + self.assertEqual(dist.metadata_version, '1.0') + self._checkMyPackage(dist, filename) + + def test_w_bztar_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.tar.bz2' % d + dist = self._callFUT(filename, metadata_version='1.1') + self.assertEqual(dist.metadata_version, '1.1') + self._checkMyPackage(dist, filename) + self._checkClassifiers(dist) + + def test_w_zip(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.zip' % d + dist = self._callFUT(filename) + self.assertEqual(dist.metadata_version, '1.0') + self._checkMyPackage(dist, filename) + + def test_w_zip_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.zip' % d + dist = self._callFUT(filename, metadata_version='1.1') + self.assertEqual(dist.metadata_version, '1.1') + self._checkMyPackage(dist, filename) + self._checkClassifiers(dist) + + def test_w_egg(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1-py2.6.egg' % d + dist = self._callFUT(filename) + self.assertEqual(dist.metadata_version, '1.0') + self._checkMyPackage(dist, filename) + + def test_w_egg_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1-py2.6.egg' % d + dist = self._callFUT(filename, metadata_version='1.1') + self.assertEqual(dist.metadata_version, '1.1') + self._checkMyPackage(dist, filename) + self._checkClassifiers(dist) + + def test_w_wheel(self): + import os + d, _ = os.path.split(__file__) + filename = ('%s/../../docs/examples/' + 'mypackage-0.1-cp26-none-linux_x86_64.whl') % d + dist = self._callFUT(filename) + self.assertEqual(dist.metadata_version, '2.0') + self._checkMyPackage(dist, filename) + + def test_w_wheel_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = ('%s/../../docs/examples/' + 'mypackage-0.1-cp26-none-linux_x86_64.whl') % d + dist = self._callFUT(filename, metadata_version='1.1') + self.assertEqual(dist.metadata_version, '1.1') + self._checkMyPackage(dist, filename) + self._checkClassifiers(dist) + + def test_w_module(self): + from pkginfo.tests import _defaultMetadataVersion + EXPECTED = _defaultMetadataVersion() + import pkginfo + from pkginfo.tests import _checkSample + dist = self._callFUT(pkginfo) + self.assertEqual(dist.metadata_version, EXPECTED) + _checkSample(self, dist) + + def test_w_module_and_metadata_version(self): + import pkginfo + from pkginfo.tests import _checkSample + from pkginfo.tests import _checkClassifiers + dist = self._callFUT(pkginfo, metadata_version='1.2') + self.assertEqual(dist.metadata_version, '1.2') + _checkSample(self, dist) + _checkClassifiers(self, dist) + + def test_w_package_name(self): + from pkginfo.tests import _defaultMetadataVersion + EXPECTED = _defaultMetadataVersion() + from pkginfo.tests import _checkSample + dist = self._callFUT('pkginfo') + self.assertEqual(dist.metadata_version, EXPECTED) + _checkSample(self, dist) + + def test_w_package_name_and_metadata_version(self): + from pkginfo.tests import _checkSample + from pkginfo.tests import _checkClassifiers + dist = self._callFUT('pkginfo', metadata_version='1.2') + self.assertEqual(dist.metadata_version, '1.2') + _checkSample(self, dist) + _checkClassifiers(self, dist) + + def test_w_directory_no_EGG_INFO(self): + import os + import warnings + dir, name = os.path.split(__file__) + subdir = os.path.join(dir, 'funny') + old_filters = warnings.filters[:] + warnings.filterwarnings('ignore') + try: + dist = self._callFUT(subdir) + self.assertEqual(dist.path, subdir) + self.assertEqual(dist.name, None) + self.assertEqual(dist.version, None) + finally: + warnings.filters[:] = old_filters + + def test_w_directory(self): + import os + dir, name = os.path.split(__file__) + subdir = os.path.join(dir, 'silly') + dist = self._callFUT(subdir) + self.assertEqual(dist.metadata_version, '1.0') + self.assertEqual(dist.name, 'silly') + self.assertEqual(dist.version, '0.1') + + def test_w_directory_and_metadata_version(self): + import os + dir, name = os.path.split(__file__) + subdir = os.path.join(dir, 'silly') + dist = self._callFUT(subdir, metadata_version='1.2') + self.assertEqual(dist.metadata_version, '1.2') + self.assertEqual(dist.name, 'silly') + self.assertEqual(dist.version, '0.1') diff --git a/pkginfo/tests/test_wheel.py b/pkginfo/tests/test_wheel.py new file mode 100644 index 0000000..85dd254 --- /dev/null +++ b/pkginfo/tests/test_wheel.py @@ -0,0 +1,105 @@ +import unittest + +class WheelTests(unittest.TestCase): + + def _getTargetClass(self): + from pkginfo.wheel import Wheel + return Wheel + + def _makeOne(self, filename=None, metadata_version=None): + if metadata_version is not None: + return self._getTargetClass()(filename, metadata_version) + return self._getTargetClass()(filename) + + def _checkSample(self, wheel, filename): + self.assertEqual(wheel.filename, filename) + self.assertEqual(wheel.name, 'mypackage') + self.assertEqual(wheel.version, '0.1') + self.assertEqual(wheel.keywords, None) + + def _checkClassifiers(self, wheel): + self.assertEqual(list(wheel.classifiers), + ['Development Status :: 4 - Beta', + 'Environment :: Console (Text Based)', + ]) + self.assertEqual(list(wheel.supported_platforms), []) + + def test_ctor_w_bogus_filename(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/nonesuch-0.1-any.whl' % d + self.assertRaises(ValueError, self._makeOne, filename) + + def test_ctor_w_non_wheel(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/mypackage-0.1.zip' % d + self.assertRaises(ValueError, self._makeOne, filename) + + def test_ctor_wo_dist_info(self): + import os + d, _ = os.path.split(__file__) + filename = '%s/../../docs/examples/nodistinfo-0.1-any.whl' % d + self.assertRaises(ValueError, self._makeOne, filename) + + def test_ctor_w_valid_wheel(self): + import os + d, _ = os.path.split(__file__) + filename = ('%s/../../docs/examples/' + 'mypackage-0.1-cp26-none-linux_x86_64.whl') % d + wheel = self._makeOne(filename) + self.assertEqual(wheel.metadata_version, '2.0') + self._checkSample(wheel, filename) + self._checkClassifiers(wheel) + + def test_ctor_w_installed_wheel(self): + import os + d, _ = os.path.split(__file__) + filename = ( + '%s/../../docs/examples/mypackage-0.1.dist-info') % d + wheel = self._makeOne(filename) + self.assertEqual(wheel.metadata_version, '2.0') + self._checkSample(wheel, filename) + self._checkClassifiers(wheel) + + def test_ctor_w_valid_wheel_and_metadata_version(self): + import os + d, _ = os.path.split(__file__) + filename = ('%s/../../docs/examples/' + 'mypackage-0.1-cp26-none-linux_x86_64.whl') % d + wheel = self._makeOne(filename, metadata_version='1.1') + self.assertEqual(wheel.metadata_version, '1.1') + self._checkSample(wheel, filename) + self._checkClassifiers(wheel) + + def test_ctor_w_valid_wheel_w_description_header(self): + import os + d, _ = os.path.split(__file__) + filename = ('%s/../../docs/examples/' + 'distlib-0.3.1-py2.py3-none-any.whl') % d + wheel = self._makeOne(filename, metadata_version='1.1') + self.assertEqual(wheel.metadata_version, '1.1') + self.assertTrue(wheel.description) + + def test_ctor_w_valid_installed_wheel(self): + import os + import shutil + import tempfile + import zipfile + + d, _ = os.path.split(__file__) + filename = ('%s/../../docs/examples/' + 'mypackage-0.1-cp26-none-linux_x86_64.whl') % d + + try: + # note: we mock a wheel installation by unzipping + test_dir = tempfile.mkdtemp() + with zipfile.ZipFile(filename) as zipf: + zipf.extractall(test_dir) + wheel = self._makeOne(filename) + self.assertEqual(wheel.metadata_version, '2.0') + self._checkSample(wheel, filename) + self._checkClassifiers(wheel) + finally: + if os.path.exists(test_dir): + shutil.rmtree(test_dir) diff --git a/pkginfo/tests/wonky/EGG-INFO/PKG-INFO b/pkginfo/tests/wonky/EGG-INFO/PKG-INFO new file mode 100644 index 0000000..ba75092 --- /dev/null +++ b/pkginfo/tests/wonky/EGG-INFO/PKG-INFO @@ -0,0 +1,2 @@ +Metadata-Version: 1.0 +Name: namespaced.wonky diff --git a/pkginfo/tests/wonky/NOT-A-PACKAGE.txt b/pkginfo/tests/wonky/NOT-A-PACKAGE.txt new file mode 100644 index 0000000..861b19e --- /dev/null +++ b/pkginfo/tests/wonky/NOT-A-PACKAGE.txt @@ -0,0 +1,4 @@ +THIS IS NOT A PYTHON PACKAGE!!!! + +It is meant to be added to sys.path for testing introspection of namespace +packages installed via setuptools. diff --git a/pkginfo/tests/wonky/namespaced/__init__.py b/pkginfo/tests/wonky/namespaced/__init__.py new file mode 100644 index 0000000..2e2033b --- /dev/null +++ b/pkginfo/tests/wonky/namespaced/__init__.py @@ -0,0 +1,7 @@ +# this is a namespace package +try: + import pkg_resources + pkg_resources.declare_namespace(__name__) +except ImportError: + import pkgutil + __path__ = pkgutil.extend_path(__path__, __name__) diff --git a/pkginfo/tests/wonky/namespaced/wonky/__init__.py b/pkginfo/tests/wonky/namespaced/wonky/__init__.py new file mode 100644 index 0000000..db26912 --- /dev/null +++ b/pkginfo/tests/wonky/namespaced/wonky/__init__.py @@ -0,0 +1 @@ +# Dummy package inside the 'namespaced' namespace. diff --git a/pkginfo/utils.py b/pkginfo/utils.py new file mode 100644 index 0000000..306630a --- /dev/null +++ b/pkginfo/utils.py @@ -0,0 +1,62 @@ +import os +from types import ModuleType + +from .bdist import BDist +from .develop import Develop +from .installed import Installed +from .sdist import SDist +from .wheel import Wheel + +def get_metadata(path_or_module, metadata_version=None): + """ Try to create a Distribution 'path_or_module'. + + o 'path_or_module' may be a module object. + + o If a string, 'path_or_module' may point to an sdist file, a bdist + file, an installed package, or a working checkout (if it contains + PKG-INFO). + + o Return None if 'path_or_module' can't be parsed. + """ + if isinstance(path_or_module, ModuleType): + try: + return Installed(path_or_module, metadata_version) + except (ValueError, IOError): #pragma NO COVER + pass + + try: + __import__(path_or_module) + except ImportError: + pass + else: + try: + return Installed(path_or_module, metadata_version) + except (ValueError, IOError): #pragma NO COVER + pass + + if os.path.isfile(path_or_module): + try: + return SDist(path_or_module, metadata_version) + except (ValueError, IOError): + pass + + try: + return BDist(path_or_module, metadata_version) + except (ValueError, IOError): #pragma NO COVER + pass + + try: + return Wheel(path_or_module, metadata_version) + except (ValueError, IOError): #pragma NO COVER + pass + + if os.path.isdir(path_or_module): + try: + return Wheel(path_or_module, metadata_version) + except (ValueError, IOError): #pragma NO COVER + pass + + try: + return Develop(path_or_module, metadata_version) + except (ValueError, IOError): #pragma NO COVER + pass diff --git a/pkginfo/wheel.py b/pkginfo/wheel.py new file mode 100644 index 0000000..aa9e1ba --- /dev/null +++ b/pkginfo/wheel.py @@ -0,0 +1,63 @@ +import io +import os +import zipfile + + +from .distribution import Distribution +from .distribution import must_decode +from .distribution import parse + + +class Wheel(Distribution): + + def __init__(self, filename, metadata_version=None): + self.filename = filename + self.metadata_version = metadata_version + self.extractMetadata() + + def read(self): + fqn = os.path.abspath(os.path.normpath(self.filename)) + if not os.path.exists(fqn): + raise ValueError('No such file: %s' % fqn) + + if fqn.endswith('.whl'): + archive = zipfile.ZipFile(fqn) + names = archive.namelist() + + def read_file(name): + return archive.read(name) + + close = archive.close + + elif fqn.endswith('.dist-info'): + names = [os.path.join(fqn, p) for p in os.listdir(fqn)] + + def read_file(name): + with io.open(name, mode='rb') as inf: + return inf.read() + + close = lambda : None + + else: + raise ValueError('Not a known wheel archive format or ' + 'installed .dist-info: %s' % fqn) + + try: + tuples = [x.split('/') for x in names if 'METADATA' in x] + schwarz = sorted([(len(x), x) for x in tuples]) + for path in [x[1] for x in schwarz]: + candidate = '/'.join(path) + data = read_file(candidate) + if b'Metadata-Version' in data: + return data + finally: + close() + + raise ValueError('No METADATA in archive: %s' % fqn) + + def parse(self, data): + super(Wheel, self).parse(data) + fp = io.StringIO(must_decode(data)) + msg = parse(fp) + if self.description is None: + self.description = msg.get_payload() diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..12841f1 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,18 @@ +[bdist_wheel] +universal = 1 + +[easy_install] +zip_ok = false + +[nosetests] +nocapture = 1 +cover-package = pkginfo +cover-erase = 1 + +[aliases] +dev = develop easy_install pkginfo[testing] + +[egg_info] +tag_build = +tag_date = 0 + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6507e79 --- /dev/null +++ b/setup.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +import os + +try: + from setuptools import setup +except ImportError: + from distutils.core import setup + extras = {} +else: + extras = { + 'test_suite': 'pkginfo.tests', + 'zip_safe': False, + 'extras_require': { + 'testing': ['nose', 'coverage'], + }, + } + +here = os.path.abspath(os.path.dirname(__file__)) +README = open(os.path.join(here, 'README.txt')).read() +CHANGES = open(os.path.join(here, 'CHANGES.txt')).read() + +setup( + name='pkginfo', + version='1.8.2', + description='Query metadatdata from sdists / bdists / installed packages.', + platforms=['Unix', 'Windows'], + long_description='\n\n'.join([README, CHANGES]), + keywords='distribution sdist installed metadata', + url='https://code.launchpad.net/~tseaver/pkginfo/trunk', + author='Tres Seaver, Agendaless Consulting', + author_email='tseaver@agendaless.com', + license='MIT', + classifiers=[ + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: Implementation :: CPython', + 'Programming Language :: Python :: Implementation :: PyPy', + 'Topic :: Software Development :: Libraries :: Python Modules', + 'Topic :: System :: Software Distribution', + ], + entry_points={ + 'console_scripts': [ + 'pkginfo = pkginfo.commandline:main', + ] + }, + packages=['pkginfo', 'pkginfo.tests'], + **extras +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b13b252 --- /dev/null +++ b/tox.ini @@ -0,0 +1,37 @@ +[tox] +envlist = + py27,pypy,py36,py37,py38,py39,py310,pypy3,cover2,cover3,docs + +[testenv] +usedevelop = true +commands = + python setup.py test -q + +[testenv:cover2] +basepython = + python2.7 +commands = + python setup.py nosetests --with-xunit --with-xcoverage +deps = + nose + coverage + nosexcover + +[testenv:cover3] +basepython = + python3.7 +commands = + python setup.py nosetests --with-xunit --with-xcoverage +deps = + nose + coverage + nosexcover + +[testenv:docs] +basepython = + python3.7 +commands = + sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html + sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest +deps = + Sphinx