From c46b7057d0ca0483ebf56d93ba3fab043e6365e3 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Feb 2019 10:22:04 -0600 Subject: [PATCH 01/63] Created a port for merge into CPython master branch. Removed Python 2 compability. Ref #47. --- importlib_metadata/_hooks.py | 24 ++------------------ importlib_metadata/api.py | 34 ++++------------------------ importlib_metadata/tests/test_api.py | 21 ++++------------- 3 files changed, 11 insertions(+), 68 deletions(-) diff --git a/importlib_metadata/_hooks.py b/importlib_metadata/_hooks.py index b819155b..bb2095e6 100644 --- a/importlib_metadata/_hooks.py +++ b/importlib_metadata/_hooks.py @@ -1,22 +1,11 @@ -from __future__ import unicode_literals, absolute_import - import re import sys import zipp import itertools from .api import Distribution - -if sys.version_info >= (3,): # pragma: nocover - from contextlib import suppress - from pathlib import Path -else: # pragma: nocover - from contextlib2 import suppress # noqa - from itertools import imap as map # type: ignore - from pathlib2 import Path - - FileNotFoundError = IOError, OSError - __metaclass__ = type +from contextlib import suppress +from pathlib import Path def install(cls): @@ -30,15 +19,6 @@ class NullFinder: def find_spec(*args, **kwargs): return None - # In Python 2, the import system requires finders - # to have a find_module() method, but this usage - # is deprecated in Python 3 in favor of find_spec(). - # For the purposes of this finder (i.e. being present - # on sys.meta_path but having no other import - # system functionality), the two methods are identical. - find_module = find_spec - - @install class MetadataPathFinder(NullFinder): """A degenerate finder for distribution packages on the file system. diff --git a/importlib_metadata/api.py b/importlib_metadata/api.py index b5cc7da4..384db023 100644 --- a/importlib_metadata/api.py +++ b/importlib_metadata/api.py @@ -12,24 +12,13 @@ from importlib import import_module from itertools import starmap -if sys.version_info > (3,): # pragma: nocover - import pathlib - from configparser import ConfigParser -else: # pragma: nocover - import pathlib2 as pathlib - from backports.configparser import ConfigParser - from itertools import imap as map # type: ignore - -try: - BaseClass = ModuleNotFoundError -except NameError: # pragma: nocover - BaseClass = ImportError # type: ignore - +import pathlib +from configparser import ConfigParser __metaclass__ = type -class PackageNotFoundError(BaseClass): +class PackageNotFoundError(ModuleNotFoundError): """The package was not found.""" @@ -83,11 +72,7 @@ def _from_config(cls, config): @classmethod def _from_text(cls, text): config = ConfigParser() - try: - config.read_string(text) - except AttributeError: # pragma: nocover - # Python 2 has no read_string - config.readfp(io.StringIO(text)) + config.read_string(text) return EntryPoint._from_config(config) def __iter__(self): @@ -194,7 +179,7 @@ def metadata(self): metadata. See PEP 566 for details. """ text = self.read_text('METADATA') or self.read_text('PKG-INFO') - return _email_message_from_string(text) + return email.message_from_string(text) @property def version(self): @@ -292,15 +277,6 @@ def parse_condition(section): yield dep + parse_condition(section) -def _email_message_from_string(text): - # Work around https://bugs.python.org/issue25545 where - # email.message_from_string cannot handle Unicode on Python 2. - if sys.version_info < (3,): # nocoverpy3 - io_buffer = io.StringIO(text) - return email.message_from_file(io_buffer) - return email.message_from_string(text) # nocoverpy2 - - def distribution(package): """Get the ``Distribution`` instance for the given package. diff --git a/importlib_metadata/tests/test_api.py b/importlib_metadata/tests/test_api.py index ccff2a05..1892a3c0 100644 --- a/importlib_metadata/tests/test_api.py +++ b/importlib_metadata/tests/test_api.py @@ -4,29 +4,20 @@ import importlib_metadata import packaging.requirements -try: - from collections.abc import Iterator -except ImportError: - from collections import Iterator # noqa: F401 - -try: - from builtins import str as text -except ImportError: - from __builtin__ import unicode as text - +from collections.abc import Iterator class APITests(unittest.TestCase): version_pattern = r'\d+\.\d+(\.\d)?' def test_retrieves_version_of_self(self): version = importlib_metadata.version('importlib_metadata') - assert isinstance(version, text) + assert isinstance(version, str) assert re.match(self.version_pattern, version) def test_retrieves_version_of_pip(self): # Assume pip is installed and retrieve the version of pip. version = importlib_metadata.version('pip') - assert isinstance(version, text) + assert isinstance(version, str) assert re.match(self.version_pattern, version) def test_for_name_does_not_exist(self): @@ -82,11 +73,7 @@ def _test_files(files_iter): file.read_text() def test_file_hash_repr(self): - try: - assertRegex = self.assertRegex - except AttributeError: - # Python 2 - assertRegex = self.assertRegexpMatches + assertRegex = self.assertRegex util = [ p for p in importlib_metadata.files('wheel') From 93731b5fe00a4da6abab85ca24d2c4fbd50d30be Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Feb 2019 10:42:29 -0600 Subject: [PATCH 02/63] Modified files in 3.8 port that were unnecessary in transition to CPython. Ref #47. --- .gitignore | 12 -- .gitlab-ci.yml | 44 ------ .readthedocs.yml | 5 - LICENSE | 13 -- MANIFEST.in | 5 - README.rst | 66 --------- coverage.ini | 23 --- coverplug.py | 21 --- importlib_metadata/docs/__init__.py | 0 importlib_metadata/docs/changelog.rst | 90 ------------ importlib_metadata/docs/conf.py | 196 -------------------------- pyproject.toml | 2 - setup.cfg | 56 -------- setup.py | 3 - tox.ini | 91 ------------ 15 files changed, 627 deletions(-) delete mode 100644 .gitignore delete mode 100644 .gitlab-ci.yml delete mode 100644 .readthedocs.yml delete mode 100644 LICENSE delete mode 100644 MANIFEST.in delete mode 100644 README.rst delete mode 100644 coverage.ini delete mode 100644 coverplug.py delete mode 100644 importlib_metadata/docs/__init__.py delete mode 100644 importlib_metadata/docs/changelog.rst delete mode 100644 importlib_metadata/docs/conf.py delete mode 100644 pyproject.toml delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 tox.ini diff --git a/.gitignore b/.gitignore deleted file mode 100644 index f4a46499..00000000 --- a/.gitignore +++ /dev/null @@ -1,12 +0,0 @@ -build -/coverage.xml -/diffcov.html -htmlcov -importlib_metadata.egg-info -.mypy_cache -/.coverage -/.DS_Store -artifacts -.eggs -.doctrees -dist diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 87e19ed3..00000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,44 +0,0 @@ -image: quay.io/python-devs/ci-image - -stages: - - test - - codecov - - deploy - -qa: - script: - - tox -e qa - -tests: - script: - - tox -e py34-nocov,py35-nocov,py36-nocov,py37-nocov,py38-nocov - -coverage: - script: - - tox -e py27-cov,py34-cov,py35-cov,py36-cov,py37-cov - artifacts: - paths: - - coverage.xml - -diffcov: - script: - - tox -e py27-diffcov,py34-diffcov,py35-diffcov,py36-diffcov,py37-diffcov - -docs: - script: - - tox -e docs - -codecov: - stage: codecov - dependencies: - - coverage - script: - - codecov -t $CODECOV_TOKEN - when: on_success - -release: - stage: deploy - only: - - /^\d+\.\d+(\.\d+)?([abc]\d*)?$/ - script: - - tox -e release diff --git a/.readthedocs.yml b/.readthedocs.yml deleted file mode 100644 index 8ae44684..00000000 --- a/.readthedocs.yml +++ /dev/null @@ -1,5 +0,0 @@ -python: - version: 3 - extra_requirements: - - docs - pip_install: true diff --git a/LICENSE b/LICENSE deleted file mode 100644 index be7e092b..00000000 --- a/LICENSE +++ /dev/null @@ -1,13 +0,0 @@ -Copyright 2017-2019 Jason R. Coombs, Barry Warsaw - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 750bdc73..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include *.py MANIFEST.in LICENSE README.rst -global-include *.txt *.rst *.ini *.cfg *.toml *.whl -exclude .gitignore -prune build -prune .tox diff --git a/README.rst b/README.rst deleted file mode 100644 index 9ca4421b..00000000 --- a/README.rst +++ /dev/null @@ -1,66 +0,0 @@ -========================= - ``importlib_metadata`` -========================= - -``importlib_metadata`` is a library to access the metadata for a Python -package. It is intended to be ported to Python 3.8. - - -Usage -===== - -See the `online documentation `_ -for usage details. - -`Finder authors -`_ can -also add support for custom package installers. See the above documentation -for details. - - -Caveats -======= - -This project primarily supports third-party packages installed by PyPA -tools (or other conforming packages). It does not support: - -- Packages in the stdlib. -- Packages installed without metadata. -- Packages installed as eggs. - -Eggs ----- - -Not only does ``importlib_metadata`` not support loading metadata -from eggs, it will crash when it attempts to load metadata for -any package that's an egg. - -``easy_install`` creates eggs when installing packages, which is why -you should use ``pip`` to install packages. ``pip`` never installs -eggs. There are some cases, however, where a project's usage -may not be able to avoid ``easy_install``. In particular, if a project -uses ``setup.py test``, any ``install_requires`` of that project that -aren't already installed will be installed using ``easy_install``. -Additionally, any project defining ``setup_requires`` may get those -dependencies installed as eggs if those dependencies aren't met before -setup.py is invoked (for any command). - -Because ``importlib_metadata`` doesn't support loading metadata from -eggs and because ``importlib_metadata`` calls itself to get its own version, -simply importing ``importlib_metadata`` will fail if it is installed as an -egg. Any package that incorporates ``importlib_metadata`` (directly -or indirectly) should be prepared to guide its users to tools that avoid -installing eggs (such as `pip `_ and -`tox `_). - -More detail and discussion can be found at -`issue 19 `_. - - -Project details -=============== - - * Project home: https://gitlab.com/python-devs/importlib_metadata - * Report bugs at: https://gitlab.com/python-devs/importlib_metadata/issues - * Code hosting: https://gitlab.com/python-devs/importlib_metadata.git - * Documentation: http://importlib_metadata.readthedocs.io/ diff --git a/coverage.ini b/coverage.ini deleted file mode 100644 index 36065351..00000000 --- a/coverage.ini +++ /dev/null @@ -1,23 +0,0 @@ -[run] -branch = true -parallel = true -omit = - setup* - .tox/*/lib/python* - */tests/*.py - */testing/*.py - /usr/local/* -plugins = - coverplug - -[report] -exclude_lines = - pragma: nocover - raise NotImplementedError - raise AssertionError - assert\s - nocoverpy${PYV} - -[paths] -source = - importlib_metadata diff --git a/coverplug.py b/coverplug.py deleted file mode 100644 index 0b0c7cb5..00000000 --- a/coverplug.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Coverage plugin to add exclude lines based on the Python version.""" - -import sys - -from coverage import CoveragePlugin - - -class MyConfigPlugin(CoveragePlugin): - def configure(self, config): - opt_name = 'report:exclude_lines' - exclude_lines = config.get_option(opt_name) - # Python >= 3.6 has os.PathLike. - if sys.version_info >= (3, 6): - exclude_lines.append('pragma: >=36') - else: - exclude_lines.append('pragma: <=35') - config.set_option(opt_name, exclude_lines) - - -def coverage_init(reg, options): - reg.add_configurer(MyConfigPlugin()) diff --git a/importlib_metadata/docs/__init__.py b/importlib_metadata/docs/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_metadata/docs/changelog.rst b/importlib_metadata/docs/changelog.rst deleted file mode 100644 index fe59144e..00000000 --- a/importlib_metadata/docs/changelog.rst +++ /dev/null @@ -1,90 +0,0 @@ -========================= - importlib_metadata NEWS -========================= - -0.9 -=== -* #40: Fixed issue where entry points without an attribute would raise an - Exception. - -0.8 -=== -* This library can now discover/enumerate all installed packages. **This - backward-incompatible change alters the protocol finders must - implement to support distribution package discovery.** Closes #24. -* The signature of ``find_distributions()`` on custom installer finders - should now accept two parameters, ``name`` and ``path`` and - these parameters must supply defaults. -* The ``entry_points()`` method no longer accepts a package name - but instead returns all entry points in a dictionary keyed by the - ``EntryPoint.group``. The ``resolve`` method has been removed. Instead, - call ``EntryPoint.load()``, which has the same semantics as - ``pkg_resources`` and ``entrypoints``. **This is a backward incompatible - change.** -* Metadata is now always returned as Unicode text regardless of - Python version. Closes #29. -* This library can now discover metadata for a 'local' package (found - in the current-working directory). Closes #27. -* Added ``files()`` function for resolving files from a distribution. -* Added a new ``requires()`` function, which returns the requirements - for a package suitable for parsing by - ``packaging.requirements.Requirement``. Closes #18. -* The top-level ``read_text()`` function has been removed. Use - ``PackagePath.read_text()`` on instances returned by the ``files()`` - function. **This is a backward incompatible change.** -* Release dates are now automatically injected into the changelog - based on SCM tags. - -0.7 -=== -* Fixed issue where packages with dashes in their names would - not be discovered. Closes #21. -* Distribution lookup is now case-insensitive. Closes #20. -* Wheel distributions can no longer be discovered by their module - name. Like Path distributions, they must be indicated by their - distribution package name. - -0.6 -=== -* Removed ``importlib_metadata.distribution`` function. Now - the public interface is primarily the utility functions exposed - in ``importlib_metadata.__all__``. Closes #14. -* Added two new utility functions ``read_text`` and - ``metadata``. - -0.5 -=== -* Updated README and removed details about Distribution - class, now considered private. Closes #15. -* Added test suite support for Python 3.4+. -* Fixed SyntaxErrors on Python 3.4 and 3.5. !12 -* Fixed errors on Windows joining Path elements. !15 - -0.4 -=== -* Housekeeping. - -0.3 -=== -* Added usage documentation. Closes #8 -* Add support for getting metadata from wheels on ``sys.path``. Closes #9 - -0.2 -=== -* Added ``importlib_metadata.entry_points()``. Closes #1 -* Added ``importlib_metadata.resolve()``. Closes #12 -* Add support for Python 2.7. Closes #4 - -0.1 -=== -* Initial release. - - -.. - Local Variables: - mode: change-log-mode - indent-tabs-mode: nil - sentence-end-double-space: t - fill-column: 78 - coding: utf-8 - End: diff --git a/importlib_metadata/docs/conf.py b/importlib_metadata/docs/conf.py deleted file mode 100644 index 6a925749..00000000 --- a/importlib_metadata/docs/conf.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# -# flake8: noqa -# -# importlib_metadata documentation build configuration file, created by -# sphinx-quickstart on Thu Nov 30 10:21:00 2017. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# 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. - -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) - - -# -- General configuration ------------------------------------------------ - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [ - 'rst.linker', - 'sphinx.ext.autodoc', - 'sphinx.ext.coverage', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.viewcode', - ] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'importlib_metadata' -copyright = '2017-2019, Jason R. Coombs, Barry Warsaw' -author = 'Jason R. Coombs, Barry Warsaw' - -# 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 = '0.1' -# The full version, including alpha/beta/rc tags. -release = '0.1' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = None - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - - -# -- Options for HTML output ---------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -# -html_theme = 'default' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# 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'] - -# Custom sidebar templates, must be a dictionary that maps document names -# to template names. -# -# This is required for the alabaster theme -# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars -html_sidebars = { - '**': [ - 'relations.html', # needs 'show_related': True theme option to display - 'searchbox.html', - ] -} - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'importlib_metadatadoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'importlib_metadata.tex', 'importlib\\_metadata Documentation', - 'Brett Cannon, Barry Warsaw', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - (master_doc, 'importlib_metadata', 'importlib_metadata Documentation', - [author], 1) -] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - (master_doc, 'importlib_metadata', 'importlib_metadata Documentation', - author, 'importlib_metadata', 'One line description of project.', - 'Miscellaneous'), -] - - - - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'python': ('https://docs.python.org/3', None), - } - - -# For rst.linker, inject release dates into changelog.rst -link_files = { - 'changelog.rst': dict( - replace=[ - dict( - pattern=r'^(?m)((?Pv?\d+(\.\d+){1,2}))\n[-=]+\n', - with_scm='{text}\n{rev[timestamp]:%Y-%m-%d}\n\n', - ), - ], - ), -} diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index e5c3a6a4..00000000 --- a/pyproject.toml +++ /dev/null @@ -1,2 +0,0 @@ -[build-system] -requires = ["setuptools>=30.3", "wheel", "setuptools_scm"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index aa38b6d9..00000000 --- a/setup.cfg +++ /dev/null @@ -1,56 +0,0 @@ -[metadata] -name = importlib_metadata -author = Barry Warsaw -author_email = barry@python.org -url = http://importlib-metadata.readthedocs.io/ -description = Read metadata from Python packages -long_description = file: README.rst -license = Apache Software License -classifiers = - Development Status :: 3 - Alpha - Intended Audience :: Developers - License :: OSI Approved :: Apache Software License - Topic :: Software Development :: Libraries - Programming Language :: Python :: 3 - Programming Language :: Python :: 2 - -[options] -python_requires = >=2.7,!=3.0,!=3.1,!=3.2,!=3.3 -install_requires = - zipp>=0.3.2 - pathlib2; python_version < '3' - contextlib2; python_version < '3' - configparser; python_version < '3' -packages = find: - -[options.package_data] -* = *.zip, *.file, *.txt, *.toml -importlib_metadata = - docs/* - docs/_static/* -importlib_metadata.tests.data03 = - namespace/* - -[mypy] -ignore_missing_imports = True -# XXX We really should use the default `True` value here, but it causes too -# many warnings, so for now just disable it. E.g. a package's __spec__ is -# defined as Optional[ModuleSpec] so we can't just blindly pull attributes off -# of that attribute. The real fix is to add conditionals or asserts proving -# that package.__spec__ is not None. -strict_optional = False - -[mypy-importlib_metadata.docs.*] -ignore_errors: True - -[mypy-importlib_metadata.tests.*] -ignore_errors: True - -[wheel] -universal=1 - -[options.extras_require] -docs = - sphinx - docutils==0.12 - rst.linker diff --git a/setup.py b/setup.py deleted file mode 100644 index d5d43d7c..00000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup(use_scm_version=True) diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 24ae7a2c..00000000 --- a/tox.ini +++ /dev/null @@ -1,91 +0,0 @@ -[tox] -# Don't cover Python 3.8 since this is just a shim for that version. Do at -# least make sure we don't regress! -envlist = {py27,py34,py35,py36,py37}-{nocov,cov,diffcov},py38-nocov,qa,docs -skip_missing_interpreters = True - - -[testenv] -commands = - nocov: python -m unittest discover - cov,diffcov: python -m coverage run {[coverage]rc} -m unittest discover {posargs} - cov,diffcov: python -m coverage combine {[coverage]rc} - cov: python -m coverage html {[coverage]rc} - cov: python -m coverage xml {[coverage]rc} - cov: python -m coverage report -m {[coverage]rc} --fail-under=100 - diffcov: python -m coverage xml {[coverage]rc} - diffcov: diff-cover coverage.xml --html-report diffcov.html - diffcov: diff-cover coverage.xml --fail-under=100 -usedevelop = True -passenv = - PYTHON* - LANG* - LC_* - PYV -deps = - cov,diffcov: coverage>=4.5 - diffcov: diff_cover - importlib_resources - py27: contextlib2 - pip >= 18 - packaging -setenv = - cov: COVERAGE_PROCESS_START={[coverage]rcfile} - cov: COVERAGE_OPTIONS="-p" - cov: COVERAGE_FILE={toxinidir}/.coverage - py27: PYV=2 - py34,py35,py36,py37,py38: PYV=3 - # workaround deprecation warnings in pip's vendored packages - PYTHONWARNINGS=ignore:Using or importing the ABCs:DeprecationWarning:pip._vendor - - -[testenv:qa] -basepython = python3.7 -commands = - python -m flake8 importlib_metadata - mypy importlib_metadata -deps = - mypy - flake8 - flufl.flake8 - - -[testenv:docs] -basepython = python3 -commands = - sphinx-build importlib_metadata/docs build/sphinx/html -extras = - docs - - -[testenv:release] -basepython = python3 -deps = - twine - wheel - setuptools - keyring - setuptools_scm -passenv = - TWINE_USERNAME - TWINE_PASSWORD -commands = - python setup.py sdist bdist_wheel - python -m twine {posargs} upload dist/* - - -[coverage] -rcfile = {toxinidir}/coverage.ini -rc = --rcfile={[coverage]rcfile} - - -[flake8] -hang-closing = True -jobs = 1 -max-line-length = 79 -exclude = - # Exclude the entire top-level __init__.py file since its only purpose is - # to expose the version string and to handle Python 2/3 compatibility. - importlib_metadata/__init__.py - importlib_metadata/docs/conf.py -enable-extensions = U4 From 5b37e57b7d1740529714e691fbe74edfde16b4e2 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Feb 2019 14:23:51 -0600 Subject: [PATCH 03/63] Modified repository structure for CPython. Changed some code to reflect this change. Ref #47. --- importlib_metadata/__init__.py | 17 - importlib_metadata/_hooks.py | 134 ------- importlib_metadata/api.py | 351 ------------------ importlib_metadata/docs/index.rst | 53 --- importlib_metadata/docs/using.rst | 256 ------------- importlib_metadata/tests/__init__.py | 0 importlib_metadata/tests/data/__init__.py | 0 .../tests/data/example-21.12-py3-none-any.whl | Bin 1453 -> 0 bytes importlib_metadata/tests/fixtures.py | 35 -- importlib_metadata/tests/test_api.py | 141 ------- importlib_metadata/tests/test_main.py | 163 -------- importlib_metadata/tests/test_zip.py | 48 --- prepare/example/example/__init__.py | 2 - prepare/example/setup.py | 10 - 14 files changed, 1210 deletions(-) delete mode 100644 importlib_metadata/__init__.py delete mode 100644 importlib_metadata/_hooks.py delete mode 100644 importlib_metadata/api.py delete mode 100644 importlib_metadata/docs/index.rst delete mode 100644 importlib_metadata/docs/using.rst delete mode 100644 importlib_metadata/tests/__init__.py delete mode 100644 importlib_metadata/tests/data/__init__.py delete mode 100644 importlib_metadata/tests/data/example-21.12-py3-none-any.whl delete mode 100644 importlib_metadata/tests/fixtures.py delete mode 100644 importlib_metadata/tests/test_api.py delete mode 100644 importlib_metadata/tests/test_main.py delete mode 100644 importlib_metadata/tests/test_zip.py delete mode 100644 prepare/example/example/__init__.py delete mode 100644 prepare/example/setup.py diff --git a/importlib_metadata/__init__.py b/importlib_metadata/__init__.py deleted file mode 100644 index aa8519fd..00000000 --- a/importlib_metadata/__init__.py +++ /dev/null @@ -1,17 +0,0 @@ -from .api import distribution, Distribution, PackageNotFoundError # noqa: F401 -from .api import metadata, entry_points, version, files, requires - -# Import for installation side-effects. -from . import _hooks # noqa: F401 - - -__all__ = [ - 'entry_points', - 'files', - 'metadata', - 'requires', - 'version', - ] - - -__version__ = version(__name__) diff --git a/importlib_metadata/_hooks.py b/importlib_metadata/_hooks.py deleted file mode 100644 index bb2095e6..00000000 --- a/importlib_metadata/_hooks.py +++ /dev/null @@ -1,134 +0,0 @@ -import re -import sys -import zipp -import itertools - -from .api import Distribution -from contextlib import suppress -from pathlib import Path - - -def install(cls): - """Class decorator for installation on sys.meta_path.""" - sys.meta_path.append(cls) - return cls - - -class NullFinder: - @staticmethod - def find_spec(*args, **kwargs): - return None - -@install -class MetadataPathFinder(NullFinder): - """A degenerate finder for distribution packages on the file system. - - This finder supplies only a find_distributions() method for versions - of Python that do not have a PathFinder find_distributions(). - """ - search_template = r'{pattern}(-.*)?\.(dist|egg)-info' - - @classmethod - def find_distributions(cls, name=None, path=None): - """Return an iterable of all Distribution instances capable of - loading the metadata for packages matching the name - (or all names if not supplied) along the paths in the list - of directories ``path`` (defaults to sys.path). - """ - if path is None: - path = sys.path - pattern = '.*' if name is None else re.escape(name) - found = cls._search_paths(pattern, path) - return map(PathDistribution, found) - - @classmethod - def _search_paths(cls, pattern, paths): - """ - Find metadata directories in paths heuristically. - """ - return itertools.chain.from_iterable( - cls._search_path(path, pattern) - for path in map(Path, paths) - ) - - @classmethod - def _search_path(cls, root, pattern): - if not root.is_dir(): - return () - normalized = pattern.replace('-', '_') - return ( - item - for item in root.iterdir() - if item.is_dir() - and re.match( - cls.search_template.format(pattern=normalized), - str(item.name), - flags=re.IGNORECASE, - ) - ) - - -class PathDistribution(Distribution): - def __init__(self, path): - """Construct a distribution from a path to the metadata directory.""" - self._path = path - - def read_text(self, filename): - with suppress(FileNotFoundError): - with self._path.joinpath(filename).open(encoding='utf-8') as fp: - return fp.read() - return None - read_text.__doc__ = Distribution.read_text.__doc__ - - def locate_file(self, path): - return self._path.parent / path - - -@install -class WheelMetadataFinder(NullFinder): - """A degenerate finder for distribution packages in wheels. - - This finder supplies only a find_distributions() method for versions - of Python that do not have a PathFinder find_distributions(). - """ - search_template = r'{pattern}(-.*)?\.whl' - - @classmethod - def find_distributions(cls, name=None, path=None): - """Return an iterable of all Distribution instances capable of - loading the metadata for packages matching the name - (or all names if not supplied) along the paths in the list - of directories ``path`` (defaults to sys.path). - """ - if path is None: - path = sys.path - pattern = '.*' if name is None else re.escape(name) - found = cls._search_paths(pattern, path) - return map(WheelDistribution, found) - - @classmethod - def _search_paths(cls, pattern, paths): - return ( - path - for path in map(Path, paths) - if re.match( - cls.search_template.format(pattern=pattern), - str(path.name), - flags=re.IGNORECASE, - ) - ) - - -class WheelDistribution(Distribution): - def __init__(self, archive): - self._archive = zipp.Path(archive) - name, version = archive.name.split('-')[0:2] - self._dist_info = '{}-{}.dist-info'.format(name, version) - - def read_text(self, filename): - target = self._archive / self._dist_info / filename - return target.read_text() if target.exists() else None - read_text.__doc__ = Distribution.read_text.__doc__ - - def locate_file(self, path): - return self._archive / path diff --git a/importlib_metadata/api.py b/importlib_metadata/api.py deleted file mode 100644 index 384db023..00000000 --- a/importlib_metadata/api.py +++ /dev/null @@ -1,351 +0,0 @@ -import io -import re -import abc -import csv -import sys -import email -import operator -import functools -import itertools -import collections - -from importlib import import_module -from itertools import starmap - -import pathlib -from configparser import ConfigParser - -__metaclass__ = type - - -class PackageNotFoundError(ModuleNotFoundError): - """The package was not found.""" - - -class EntryPoint(collections.namedtuple('EntryPointBase', 'name value group')): - """An entry point as defined by Python packaging conventions.""" - - pattern = re.compile( - r'(?P[\w.]+)\s*' - r'(:\s*(?P[\w.]+))?\s*' - r'(?P\[.*\])?\s*$' - ) - """ - A regular expression describing the syntax for an entry point, - which might look like: - - - module - - package.module - - package.module:attribute - - package.module:object.attribute - - package.module:attr [extra1, extra2] - - Other combinations are possible as well. - - The expression is lenient about whitespace around the ':', - following the attr, and following any extras. - """ - - def load(self): - """Load the entry point from its definition. If only a module - is indicated by the value, return that module. Otherwise, - return the named object. - """ - match = self.pattern.match(self.value) - module = import_module(match.group('module')) - attrs = filter(None, (match.group('attr') or '').split('.')) - return functools.reduce(getattr, attrs, module) - - @property - def extras(self): - match = self.pattern.match(self.value) - return list(re.finditer(r'\w+', match.group('extras') or '')) - - @classmethod - def _from_config(cls, config): - return [ - cls(name, value, group) - for group in config.sections() - for name, value in config.items(group) - ] - - @classmethod - def _from_text(cls, text): - config = ConfigParser() - config.read_string(text) - return EntryPoint._from_config(config) - - def __iter__(self): - """ - Supply iter so one may construct dicts of EntryPoints easily. - """ - return iter((self.name, self)) - - -class PackagePath(pathlib.PosixPath): - """A reference to a path in a package""" - - def read_text(self, encoding='utf-8'): - with self.locate().open(encoding=encoding) as stream: - return stream.read() - - def read_binary(self): - with self.locate().open('rb') as stream: - return stream.read() - - def locate(self): - """Return a path-like object for this path""" - return self.dist.locate_file(self) - - -class FileHash: - def __init__(self, spec): - self.mode, _, self.value = spec.partition('=') - - def __repr__(self): - return ''.format(self.mode, self.value) - - -class Distribution: - """A Python distribution package.""" - - @abc.abstractmethod - def read_text(self, filename): - """Attempt to load metadata file given by the name. - - :param filename: The name of the file in the distribution info. - :return: The text if found, otherwise None. - """ - - @abc.abstractmethod - def locate_file(self, path): - """ - Given a path to a file in this distribution, return a path - to it. - """ - - @classmethod - def from_name(cls, name): - """Return the Distribution for the given package name. - - :param name: The name of the distribution package to search for. - :return: The Distribution instance (or subclass thereof) for the named - package, if found. - :raises PackageNotFoundError: When the named package's distribution - metadata cannot be found. - """ - for resolver in cls._discover_resolvers(): - dists = resolver(name) - dist = next(dists, None) - if dist is not None: - return dist - else: - raise PackageNotFoundError(name) - - @classmethod - def discover(cls): - """Return an iterable of Distribution objects for all packages. - - :return: Iterable of Distribution objects for all packages. - """ - return itertools.chain.from_iterable( - resolver() - for resolver in cls._discover_resolvers() - ) - - @staticmethod - def _discover_resolvers(): - """Search the meta_path for resolvers.""" - declared = ( - getattr(finder, 'find_distributions', None) - for finder in sys.meta_path - ) - return filter(None, declared) - - @classmethod - def find_local(cls): - dists = itertools.chain.from_iterable( - resolver(path=['.']) - for resolver in cls._discover_resolvers() - ) - dist, = dists - return dist - - @property - def metadata(self): - """Return the parsed metadata for this Distribution. - - The returned object will have keys that name the various bits of - metadata. See PEP 566 for details. - """ - text = self.read_text('METADATA') or self.read_text('PKG-INFO') - return email.message_from_string(text) - - @property - def version(self): - """Return the 'Version' metadata for the distribution package.""" - return self.metadata['Version'] - - @property - def entry_points(self): - return EntryPoint._from_text(self.read_text('entry_points.txt')) - - @property - def files(self): - file_lines = self._read_files_distinfo() or self._read_files_egginfo() - - def make_file(name, hash=None, size_str=None): - result = PackagePath(name) - result.hash = FileHash(hash) if hash else None - result.size = int(size_str) if size_str else None - result.dist = self - return result - - return file_lines and starmap(make_file, csv.reader(file_lines)) - - def _read_files_distinfo(self): - """ - Read the lines of RECORD - """ - text = self.read_text('RECORD') - return text and text.splitlines() - - def _read_files_egginfo(self): - """ - SOURCES.txt might contain literal commas, so wrap each line - in quotes. - """ - text = self.read_text('SOURCES.txt') - return text and map('"{}"'.format, text.splitlines()) - - @property - def requires(self): - return self._read_dist_info_reqs() or self._read_egg_info_reqs() - - def _read_dist_info_reqs(self): - spec = self.metadata['Requires-Dist'] - return spec and filter(None, spec.splitlines()) - - def _read_egg_info_reqs(self): - source = self.read_text('requires.txt') - return self._deps_from_requires_text(source) - - @classmethod - def _deps_from_requires_text(cls, source): - section_pairs = cls._read_sections(source.splitlines()) - sections = { - section: list(map(operator.itemgetter('line'), results)) - for section, results in - itertools.groupby(section_pairs, operator.itemgetter('section')) - } - return cls._convert_egg_info_reqs_to_simple_reqs(sections) - - @staticmethod - def _read_sections(lines): - section = None - for line in filter(None, lines): - section_match = re.match(r'\[(.*)\]$', line) - if section_match: - section = section_match.group(1) - continue - yield locals() - - @staticmethod - def _convert_egg_info_reqs_to_simple_reqs(sections): - """ - Historically, setuptools would solicit and store 'extra' - requirements, including those with environment markers, - in separate sections. More modern tools expect each - dependency to be defined separately, with any relevant - extras and environment markers attached directly to that - requirement. This method converts the former to the - latter. See _test_deps_from_requires_text for an example. - """ - def make_condition(name): - return name and 'extra == "{name}"'.format(name=name) - - def parse_condition(section): - section = section or '' - extra, sep, markers = section.partition(':') - if extra and markers: - markers = '({markers})'.format(markers=markers) - conditions = list(filter(None, [markers, make_condition(extra)])) - return '; ' + ' and '.join(conditions) if conditions else '' - - for section, deps in sections.items(): - for dep in deps: - yield dep + parse_condition(section) - - -def distribution(package): - """Get the ``Distribution`` instance for the given package. - - :param package: The name of the package as a string. - :return: A ``Distribution`` instance (or subclass thereof). - """ - return Distribution.from_name(package) - - -def distributions(): - """Get all ``Distribution`` instances in the current environment. - - :return: An iterable of ``Distribution`` instances. - """ - return Distribution.discover() - - -def local_distribution(): - """Get the ``Distribution`` instance for the package in CWD. - - :return: A ``Distribution`` instance (or subclass thereof). - """ - return Distribution.find_local() - - -def metadata(package): - """Get the metadata for the package. - - :param package: The name of the distribution package to query. - :return: An email.Message containing the parsed metadata. - """ - return Distribution.from_name(package).metadata - - -def version(package): - """Get the version string for the named package. - - :param package: The name of the distribution package to query. - :return: The version string for the package as defined in the package's - "Version" metadata key. - """ - return distribution(package).version - - -def entry_points(name=None): - """Return EntryPoint objects for all installed packages. - - :return: EntryPoint objects for all installed packages. - """ - eps = itertools.chain.from_iterable( - dist.entry_points for dist in distributions()) - by_group = operator.attrgetter('group') - ordered = sorted(eps, key=by_group) - grouped = itertools.groupby(ordered, by_group) - return { - group: tuple(eps) - for group, eps in grouped - } - - -def files(package): - return distribution(package).files - - -def requires(package): - """ - Return a list of requirements for the indicated distribution. - - :return: An iterator of requirements, suitable for - packaging.requirement.Requirement. - """ - return distribution(package).requires diff --git a/importlib_metadata/docs/index.rst b/importlib_metadata/docs/index.rst deleted file mode 100644 index fb5047f4..00000000 --- a/importlib_metadata/docs/index.rst +++ /dev/null @@ -1,53 +0,0 @@ -=============================== - Welcome to importlib_metadata -=============================== - -``importlib_metadata`` is a library which provides an API for accessing an -installed package's `metadata`_, such as its entry points or its top-level -name. This functionality intends to replace most uses of ``pkg_resources`` -`entry point API`_ and `metadata API`_. Along with ``importlib.resources`` in -`Python 3.7 and newer`_ (backported as `importlib_resources`_ for older -versions of Python), this can eliminate the need to use the older and less -efficient ``pkg_resources`` package. - -``importlib_metadata`` is a backport of Python 3.8's standard library -`importlib.metadata`_ module for Python 2.7, and 3.4 through 3.7. Users of -Python 3.8 and beyond are encouraged to use the standard library module, and -in fact for these versions, ``importlib_metadata`` just shadows that module. -Developers looking for detailed API descriptions should refer to the Python -3.8 standard library documentation. - -The documentation here includes a general :ref:`usage ` guide. - - -.. toctree:: - :maxdepth: 2 - :caption: Contents: - - using.rst - changelog (links).rst - - -Project details -=============== - - * Project home: https://gitlab.com/python-devs/importlib_metadata - * Report bugs at: https://gitlab.com/python-devs/importlib_metadata/issues - * Code hosting: https://gitlab.com/python-devs/importlib_metadata.git - * Documentation: http://importlib_metadata.readthedocs.io/ - - -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` - - -.. _`metadata`: https://www.python.org/dev/peps/pep-0566/ -.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points -.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api -.. _`Python 3.7 and newer`: https://docs.python.org/3/library/importlib.html#module-importlib.resources -.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html -.. _`importlib.metadata`: TBD diff --git a/importlib_metadata/docs/using.rst b/importlib_metadata/docs/using.rst deleted file mode 100644 index 93dad3df..00000000 --- a/importlib_metadata/docs/using.rst +++ /dev/null @@ -1,256 +0,0 @@ -.. _using: - -========================== - Using importlib_metadata -========================== - -``importlib_metadata`` is a library that provides for access to installed -package metadata. Built in part on Python's import system, this library -intends to replace similar functionality in the `entry point -API`_ and `metadata API`_ of ``pkg_resources``. Along with -``importlib.resources`` in `Python 3.7 -and newer`_ (backported as `importlib_resources`_ for older versions of -Python), this can eliminate the need to use the older and less efficient -``pkg_resources`` package. - -By "installed package" we generally mean a third-party package installed into -Python's ``site-packages`` directory via tools such as `pip -`_. Specifically, -it means a package with either a discoverable ``dist-info`` or ``egg-info`` -directory, and metadata defined by `PEP 566`_ or its older specifications. -By default, package metadata can live on the file system or in wheels on -``sys.path``. Through an extension mechanism, the metadata can live almost -anywhere. - - -Overview -======== - -Let's say you wanted to get the version string for a package you've installed -using ``pip``. We start by creating a virtual environment and installing -something into it:: - - $ python3 -m venv example - $ source example/bin/activate - (example) $ pip install importlib_metadata - (example) $ pip install wheel - -You can get the version string for ``wheel`` by running the following:: - - (example) $ python - >>> from importlib_metadata import version - >>> version('wheel') - '0.32.3' - -You can also get the set of entry points keyed by group, such as -``console_scripts``, ``distutils.commands`` and others. Each group contains a -sequence of :ref:`EntryPoint ` objects. - -You can get the :ref:`metadata for a distribution `:: - - >>> list(metadata('wheel')) - ['Metadata-Version', 'Name', 'Version', 'Summary', 'Home-page', 'Author', 'Author-email', 'Maintainer', 'Maintainer-email', 'License', 'Project-URL', 'Project-URL', 'Project-URL', 'Keywords', 'Platform', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Requires-Python', 'Provides-Extra', 'Requires-Dist', 'Requires-Dist'] - -You can also get a :ref:`distribution's version number `, list its -:ref:`constituent files `_, and get a list of the distribution's -:ref:`requirements`_. - - -Distributions -============= - -.. CAUTION:: The ``Distribution`` class described here may or may not end up - in the final stable public API. Consider this class `provisional - `_ until the 1.0 - release. - -While the above API is the most common and convenient usage, you can get all -of that information from the ``Distribution`` class. A ``Distribution`` is an -abstract object that represents the metadata for a Python package. You can -get the ``Distribution`` instance:: - - >>> from importlib_metadata import distribution - >>> dist = distribution('wheel') - -Thus, an alternative way to get the version number is through the -``Distribution`` instance:: - - >>> dist.version - '0.32.3' - -There are all kinds of additional metadata available on the ``Distribution`` -instance:: - - >>> d.metadata['Requires-Python'] - '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' - >>> d.metadata['License'] - 'MIT' - -The full set of available metadata is not described here. See `PEP 566 -`_ for additional details. - - -Functional API -============== - -This package provides the following functionality via its public API. - - -.. _entry-points:: - -Entry points ------------- - -The ``entry_points()`` function returns a dictionary of all entry points, -keyed by group. Entry points are represented by ``EntryPoint`` instances; -each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and -a ``.load()`` method to resolve the value. - - >>> eps = entry_points() - >>> list(eps) - ['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation'] - >>> scripts = eps['console_scripts'] - >>> wheel = [ep for ep in scripts if ep.name == 'wheel'][0] - >>> wheel - EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts') - >>> main = wheel.load() - >>> main - - -The ``group`` and ``name`` are arbitrary values defined by the package author -and usually a client will wish to resolve all entry points for a particular -group. Read `the setuptools docs -`_ -for more information on entrypoints, their definition, and usage. - - -.. _metadata:: - -Distribution metadata ---------------------- - -Every distribution includes some metadata, which you can extract using the -``metadata()`` function:: - - >>> wheel_metadata = metadata('wheel') - -The keys of the returned data structure [#f1]_ name the metadata keywords, and -their values are returned unparsed from the distribution metadata:: - - >>> wheel_metadata['Requires-Python'] - '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' - - -.. _version:: - -Distribution versions ---------------------- - -The ``version()`` function is the quickest way to get a distribution's version -number, as a string:: - - >>> version('wheel') - '0.32.3' - - -.. _files:: - -Distribution files ------------------- - -You can also get the full set of files contained within a distribution. The -``files()`` function takes a distribution package name and returns all of the -files installed by this distribution. Each file object returned is a -``PackagePath``, a `pathlib.Path`_ derived object with additional ``dist``, -``size``, and ``hash`` properties as indicated by the metadata. For example:: - - >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0] - >>> util - PackagePath('wheel/util.py') - >>> util.size - 859 - >>> util.dist - - >>> util.hash - - -Once you have the file, you can also read its contents:: - - >>> print(util.read_text()) - import base64 - import sys - ... - def as_bytes(s): - if isinstance(s, text_type): - return s.encode('utf-8') - return s - - -.. _requirements:: - -Distribution requirements -------------------------- - -To get the full set of requirements for a distribution, use the ``requires()`` -function. Note that this returns an iterator:: - - >>> list(requires('wheel')) - ["pytest (>=3.0.0) ; extra == 'test'"] - - - -Extending the search algorithm -============================== - -Because package metadata is not available through ``sys.path`` searches, or -package loaders directly, the metadata for a package is found through import -system `finders`_. To find a distribution package's metadata, -``importlib_metadata`` queries the list of `meta path finders`_ on -`sys.meta_path`_. - -By default ``importlib_metadata`` installs a finder for distribution packages -found on the file system. This finder doesn't actually find any *packages*, -but it can find the packages' metadata. - -The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the -interface expected of finders by Python's import system. -``importlib_metadata`` extends this protocol by looking for an optional -``find_distributions`` callable on the finders from -``sys.meta_path``. If the finder has this method, it must return -an iterator over instances of the ``Distribution`` abstract class. This -method must have the signature:: - - def find_distributions(name=None, path=sys.path): - """Return an iterable of all Distribution instances capable of - loading the metadata for packages matching the name - (or all names if not supplied) along the paths in the list - of directories ``path`` (defaults to sys.path). - """ - -What this means in practice is that to support finding distribution package -metadata in locations other than the file system, you should derive from -``Distribution`` and implement the ``load_metadata()`` method. This takes a -single argument which is the name of the package whose metadata is being -found. This instance of the ``Distribution`` base abstract class is what your -finder's ``find_distributions()`` method should return. - - -.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points -.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api -.. _`Python 3.7 and newer`: https://docs.python.org/3/library/importlib.html#module-importlib.resources -.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html -.. _`PEP 566`: https://www.python.org/dev/peps/pep-0566/ -.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders -.. _`meta path finders`: https://docs.python.org/3/glossary.html#term-meta-path-finder -.. _`sys.meta_path`: https://docs.python.org/3/library/sys.html#sys.meta_path -.. _`pathlib.Path`: https://docs.python.org/3/library/pathlib.html#pathlib.Path - - -.. rubric:: Footnotes - -.. [#f1] Technically, the returned distribution metadata object is an - `email.message.Message - `_ - instance, but this is an implementation detail, and not part of the - stable API. You should only use dictionary-like methods and syntax - to access the metadata contents. diff --git a/importlib_metadata/tests/__init__.py b/importlib_metadata/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_metadata/tests/data/__init__.py b/importlib_metadata/tests/data/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/importlib_metadata/tests/data/example-21.12-py3-none-any.whl b/importlib_metadata/tests/data/example-21.12-py3-none-any.whl deleted file mode 100644 index f92f7716e3e613a2a9703be785135fd8b35cfb1c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1453 zcmWIWW@Zs#U|`^2$X%l2E2Vj`LJ`OVVPPOntw_u*$Vt_YkI&4@EQycTE2#AL^bJ1Y zd*;mL3tJuqF*Gf@GU?JH8`iH^x{lmwniEp0#}EKF@#|6@-~Vw}E~^1e(gI=)go(OF zhI)oZdMTO3CAyh;Y5Dr8c_l@a@df#rc_qbqB^4#ze&^0>pF8i_tM8|GN=HMp@2S^X zk2AU_JVQ5xHf&l`By9Y7#||{R?xt`CaRKe%0Af`#eJG?#%hkK?YZh9~AkY_15*$IjO%X$iwTT zj$Wre`^vxz1{aLYt{7i=!gcDr{>864*LXE_z0RKW*%YLqspb2W%hP9jkj4s=YiCcN z_rB_TX7!Ut=+0vKml077bk1%dR>0#dU)K;v7sn9C*zS#7hYUnqzke6~*(h?!FxuR) z*JA$j=D}6nJuk63>$g|^aHQ)0StmYywxSwA=est9~!zT(kqV|fK2BE1^mxK0q zD(qh)b?4sPb$w-l5$bn#F3Qs`KJ!0*^MQ6R{<p(1r$KgS)&i+9zwP$x1H90UiT)ZdwKjq+uV@j;~jI4*9 zFSbRdFl*>>tZs;(uk_eQ>hgU{@!k)&u4PPICc4gJ)}^;Bzl1h5{m``K-uhy9=9lTe z-NnTxRmGRCybveUZnWu;{lQCXJ}s6};eD{ z#=6?Q%UABG&=n2Q`J~J@GtS}8)q{&~{}TM{1aZ}u05ukm*h zl5ERPr=_}oV=S9Bfwe1=@2=aOxF-kSE;PEja34gdfE diff --git a/importlib_metadata/tests/fixtures.py b/importlib_metadata/tests/fixtures.py deleted file mode 100644 index c2c6bf27..00000000 --- a/importlib_metadata/tests/fixtures.py +++ /dev/null @@ -1,35 +0,0 @@ -import sys -import shutil -import tempfile -import contextlib - -try: - from contextlib import ExitStack -except ImportError: - from contextlib2 import ExitStack - -try: - import pathlib -except ImportError: - import pathlib2 as pathlib - - -__metaclass__ = type - - -class SiteDir: - @staticmethod - @contextlib.contextmanager - def site_dir(): - tmpdir = tempfile.mkdtemp() - sys.path[:0] = [tmpdir] - try: - yield pathlib.Path(tmpdir) - finally: - sys.path.remove(tmpdir) - shutil.rmtree(tmpdir) - - def setUp(self): - self.fixtures = ExitStack() - self.addCleanup(self.fixtures.close) - self.site_dir = self.fixtures.enter_context(self.site_dir()) diff --git a/importlib_metadata/tests/test_api.py b/importlib_metadata/tests/test_api.py deleted file mode 100644 index 1892a3c0..00000000 --- a/importlib_metadata/tests/test_api.py +++ /dev/null @@ -1,141 +0,0 @@ -import re -import textwrap -import unittest -import importlib_metadata -import packaging.requirements - -from collections.abc import Iterator - -class APITests(unittest.TestCase): - version_pattern = r'\d+\.\d+(\.\d)?' - - def test_retrieves_version_of_self(self): - version = importlib_metadata.version('importlib_metadata') - assert isinstance(version, str) - assert re.match(self.version_pattern, version) - - def test_retrieves_version_of_pip(self): - # Assume pip is installed and retrieve the version of pip. - version = importlib_metadata.version('pip') - assert isinstance(version, str) - assert re.match(self.version_pattern, version) - - def test_for_name_does_not_exist(self): - with self.assertRaises(importlib_metadata.PackageNotFoundError): - importlib_metadata.distribution('does-not-exist') - - def test_for_top_level(self): - distribution = importlib_metadata.distribution('importlib_metadata') - self.assertEqual( - distribution.read_text('top_level.txt').strip(), - 'importlib_metadata') - - def test_read_text(self): - top_level = [ - path for path in importlib_metadata.files('importlib_metadata') - if path.name == 'top_level.txt' - ][0] - self.assertEqual(top_level.read_text(), 'importlib_metadata\n') - - def test_entry_points(self): - scripts = importlib_metadata.entry_points()['console_scripts'] - scripts = dict(scripts) - pip_ep = scripts['pip'] - # We should probably not be dependent on a third party package's - # internal API staying stable. - self.assertEqual(pip_ep.value, 'pip._internal:main') - self.assertEqual(pip_ep.extras, []) - - def test_metadata_for_this_package(self): - md = importlib_metadata.metadata('importlib_metadata') - assert md['author'] == 'Barry Warsaw' - assert md['LICENSE'] == 'Apache Software License' - assert md['Name'] == 'importlib-metadata' - classifiers = md.get_all('Classifier') - assert 'Topic :: Software Development :: Libraries' in classifiers - - def test_importlib_metadata_version(self): - assert re.match(self.version_pattern, importlib_metadata.__version__) - - @staticmethod - def _test_files(files_iter): - assert isinstance(files_iter, Iterator) - files = list(files_iter) - root = files[0].root - for file in files: - assert file.root == root - assert not file.hash or file.hash.value - assert not file.hash or file.hash.mode == 'sha256' - assert not file.size or file.size >= 0 - assert file.locate().exists() - assert isinstance(file.read_binary(), bytes) - if file.name.endswith('.py'): - file.read_text() - - def test_file_hash_repr(self): - assertRegex = self.assertRegex - - util = [ - p for p in importlib_metadata.files('wheel') - if p.name == 'util.py' - ][0] - assertRegex( - repr(util.hash), - '') - - def test_files_dist_info(self): - self._test_files(importlib_metadata.files('pip')) - - def test_files_egg_info(self): - self._test_files(importlib_metadata.files('importlib_metadata')) - - def test_find_local(self): - dist = importlib_metadata.api.local_distribution() - assert dist.metadata['Name'] == 'importlib-metadata' - - def test_requires(self): - deps = importlib_metadata.requires('importlib_metadata') - parsed = list(map(packaging.requirements.Requirement, deps)) - assert all(parsed) - assert any( - dep.name == 'pathlib2' and dep.marker - for dep in parsed - ) - - def test_requires_dist_info(self): - # assume 'packaging' is installed as a wheel with dist-info - deps = importlib_metadata.requires('packaging') - parsed = list(map(packaging.requirements.Requirement, deps)) - assert parsed - - def test_more_complex_deps_requires_text(self): - requires = textwrap.dedent(""" - dep1 - dep2 - - [:python_version < "3"] - dep3 - - [extra1] - dep4 - - [extra2:python_version < "3"] - dep5 - """) - deps = sorted( - importlib_metadata.api.Distribution._deps_from_requires_text( - requires) - ) - expected = [ - 'dep1', - 'dep2', - 'dep3; python_version < "3"', - 'dep4; extra == "extra1"', - 'dep5; (python_version < "3") and extra == "extra2"', - ] - # It's important that the environment marker expression be - # wrapped in parentheses to avoid the following 'and' binding more - # tightly than some other part of the environment expression. - - assert deps == expected - assert all(map(packaging.requirements.Requirement, deps)) diff --git a/importlib_metadata/tests/test_main.py b/importlib_metadata/tests/test_main.py deleted file mode 100644 index c18f7de8..00000000 --- a/importlib_metadata/tests/test_main.py +++ /dev/null @@ -1,163 +0,0 @@ -# coding: utf-8 -from __future__ import unicode_literals - -import re -import textwrap -import unittest -import importlib -import importlib_metadata - -from . import fixtures -from importlib_metadata import _hooks - -try: - from builtins import str as text -except ImportError: - from __builtin__ import unicode as text - - -class BasicTests(unittest.TestCase): - version_pattern = r'\d+\.\d+(\.\d)?' - - def test_retrieves_version_of_pip(self): - # Assume pip is installed and retrieve the version of pip. - dist = importlib_metadata.Distribution.from_name('pip') - assert isinstance(dist.version, text) - assert re.match(self.version_pattern, dist.version) - - def test_for_name_does_not_exist(self): - with self.assertRaises(importlib_metadata.PackageNotFoundError): - importlib_metadata.Distribution.from_name('does-not-exist') - - def test_new_style_classes(self): - self.assertIsInstance(importlib_metadata.Distribution, type) - self.assertIsInstance(_hooks.MetadataPathFinder, type) - self.assertIsInstance(_hooks.WheelMetadataFinder, type) - self.assertIsInstance(_hooks.WheelDistribution, type) - - -class ImportTests(unittest.TestCase): - def test_import_nonexistent_module(self): - # Ensure that the MetadataPathFinder does not crash an import of a - # non-existant module. - with self.assertRaises(ImportError): - importlib.import_module('does_not_exist') - - def test_resolve(self): - scripts = dict(importlib_metadata.entry_points()['console_scripts']) - pip_ep = scripts['pip'] - import pip._internal - self.assertEqual(pip_ep.load(), pip._internal.main) - - def test_resolve_without_attr(self): - ep = importlib_metadata.api.EntryPoint( - name='ep', - value='importlib_metadata.api', - group='grp', - ) - assert ep.load() is importlib_metadata.api - - -class NameNormalizationTests(fixtures.SiteDir, unittest.TestCase): - @staticmethod - def pkg_with_dashes(site_dir): - """ - Create minimal metadata for a package with dashes - in the name (and thus underscores in the filename). - """ - metadata_dir = site_dir / 'my_pkg.dist-info' - metadata_dir.mkdir() - metadata = metadata_dir / 'METADATA' - with metadata.open('w') as strm: - strm.write('Version: 1.0\n') - return 'my-pkg' - - def test_dashes_in_dist_name_found_as_underscores(self): - """ - For a package with a dash in the name, the dist-info metadata - uses underscores in the name. Ensure the metadata loads. - """ - pkg_name = self.pkg_with_dashes(self.site_dir) - assert importlib_metadata.version(pkg_name) == '1.0' - - @staticmethod - def pkg_with_mixed_case(site_dir): - """ - Create minimal metadata for a package with mixed case - in the name. - """ - metadata_dir = site_dir / 'CherryPy.dist-info' - metadata_dir.mkdir() - metadata = metadata_dir / 'METADATA' - with metadata.open('w') as strm: - strm.write('Version: 1.0\n') - return 'CherryPy' - - def test_dist_name_found_as_any_case(self): - """ - Ensure the metadata loads when queried with any case. - """ - pkg_name = self.pkg_with_mixed_case(self.site_dir) - assert importlib_metadata.version(pkg_name) == '1.0' - assert importlib_metadata.version(pkg_name.lower()) == '1.0' - assert importlib_metadata.version(pkg_name.upper()) == '1.0' - - -class NonASCIITests(fixtures.SiteDir, unittest.TestCase): - @staticmethod - def pkg_with_non_ascii_description(site_dir): - """ - Create minimal metadata for a package with non-ASCII in - the description. - """ - metadata_dir = site_dir / 'portend.dist-info' - metadata_dir.mkdir() - metadata = metadata_dir / 'METADATA' - with metadata.open('w', encoding='utf-8') as fp: - fp.write('Description: pÎrˈtend\n') - return 'portend' - - @staticmethod - def pkg_with_non_ascii_description_egg_info(site_dir): - """ - Create minimal metadata for an egg-info package with - non-ASCII in the description. - """ - metadata_dir = site_dir / 'portend.dist-info' - metadata_dir.mkdir() - metadata = metadata_dir / 'METADATA' - with metadata.open('w', encoding='utf-8') as fp: - fp.write(textwrap.dedent(""" - Name: portend - - pÎrˈtend - """).lstrip()) - return 'portend' - - def test_metadata_loads(self): - pkg_name = self.pkg_with_non_ascii_description(self.site_dir) - meta = importlib_metadata.metadata(pkg_name) - assert meta['Description'] == 'pÎrˈtend' - - def test_metadata_loads_egg_info(self): - pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir) - meta = importlib_metadata.metadata(pkg_name) - assert meta.get_payload() == 'pÎrˈtend\n' - - -class DiscoveryTests(unittest.TestCase): - - def test_package_discovery(self): - dists = list(importlib_metadata.api.distributions()) - assert all( - isinstance(dist, importlib_metadata.Distribution) - for dist in dists - ) - assert any( - dist.metadata['Name'] == 'importlib-metadata' - for dist in dists - ) - assert any( - dist.metadata['Name'] == 'pip' - for dist in dists - ) diff --git a/importlib_metadata/tests/test_zip.py b/importlib_metadata/tests/test_zip.py deleted file mode 100644 index cf686db8..00000000 --- a/importlib_metadata/tests/test_zip.py +++ /dev/null @@ -1,48 +0,0 @@ -import sys -import unittest -import importlib_metadata - -from importlib_resources import path - -try: - from contextlib import ExitStack -except ImportError: - from contextlib2 import ExitStack - - -class BespokeLoader: - archive = 'bespoke' - - -class TestZip(unittest.TestCase): - def setUp(self): - # Find the path to the example.*.whl so we can add it to the front of - # sys.path, where we'll then try to find the metadata thereof. - self.resources = ExitStack() - self.addCleanup(self.resources.close) - wheel = self.resources.enter_context( - path('importlib_metadata.tests.data', - 'example-21.12-py3-none-any.whl')) - sys.path.insert(0, str(wheel)) - self.resources.callback(sys.path.pop, 0) - - def test_zip_version(self): - self.assertEqual(importlib_metadata.version('example'), '21.12') - - def test_zip_entry_points(self): - scripts = dict(importlib_metadata.entry_points()['console_scripts']) - entry_point = scripts['example'] - self.assertEqual(entry_point.value, 'example:main') - - def test_missing_metadata(self): - distribution = importlib_metadata.distribution('example') - self.assertIsNone(distribution.read_text('does not exist')) - - def test_case_insensitive(self): - self.assertEqual(importlib_metadata.version('Example'), '21.12') - - def test_files(self): - files = importlib_metadata.files('example') - for file in files: - path = str(file.dist.locate_file(file)) - assert '.whl/' in path, path diff --git a/prepare/example/example/__init__.py b/prepare/example/example/__init__.py deleted file mode 100644 index ba73b743..00000000 --- a/prepare/example/example/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -def main(): - return 'example' diff --git a/prepare/example/setup.py b/prepare/example/setup.py deleted file mode 100644 index 38f9a6a6..00000000 --- a/prepare/example/setup.py +++ /dev/null @@ -1,10 +0,0 @@ -from setuptools import setup -setup( - name='example', - version='21.12', - license='Apache Software License', - packages=['example'], - entry_points={ - 'console_scripts': ['example = example:main'], - }, - ) From 2c734fea304c54b64bd18e93f813bcbb7a2f5ff1 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Feb 2019 14:33:39 -0600 Subject: [PATCH 04/63] Copied files over into cpython. Still need to establish independence from zipp and pip. Ref #47. --- Doc/library/importlib_metadata.rst | 256 +++++++++++++ Doc/prepare/example/example/__init__.py | 2 + Doc/prepare/example/setup.py | 10 + Lib/importlib/metadata/__init__.py | 17 + Lib/importlib/metadata/_hooks.py | 134 +++++++ Lib/importlib/metadata/api.py | 351 ++++++++++++++++++ Lib/tests/test_importlib/__init__.py | 0 Lib/tests/test_importlib/data/__init__.py | 0 .../data/example-21.12-py3-none-any.whl | Bin 0 -> 1453 bytes Lib/tests/test_importlib/fixtures.py | 35 ++ Lib/tests/test_importlib/test_api.py | 141 +++++++ Lib/tests/test_importlib/test_main.py | 163 ++++++++ Lib/tests/test_importlib/test_zip.py | 48 +++ 13 files changed, 1157 insertions(+) create mode 100644 Doc/library/importlib_metadata.rst create mode 100644 Doc/prepare/example/example/__init__.py create mode 100644 Doc/prepare/example/setup.py create mode 100644 Lib/importlib/metadata/__init__.py create mode 100644 Lib/importlib/metadata/_hooks.py create mode 100644 Lib/importlib/metadata/api.py create mode 100644 Lib/tests/test_importlib/__init__.py create mode 100644 Lib/tests/test_importlib/data/__init__.py create mode 100644 Lib/tests/test_importlib/data/example-21.12-py3-none-any.whl create mode 100644 Lib/tests/test_importlib/fixtures.py create mode 100644 Lib/tests/test_importlib/test_api.py create mode 100644 Lib/tests/test_importlib/test_main.py create mode 100644 Lib/tests/test_importlib/test_zip.py diff --git a/Doc/library/importlib_metadata.rst b/Doc/library/importlib_metadata.rst new file mode 100644 index 00000000..93dad3df --- /dev/null +++ b/Doc/library/importlib_metadata.rst @@ -0,0 +1,256 @@ +.. _using: + +========================== + Using importlib_metadata +========================== + +``importlib_metadata`` is a library that provides for access to installed +package metadata. Built in part on Python's import system, this library +intends to replace similar functionality in the `entry point +API`_ and `metadata API`_ of ``pkg_resources``. Along with +``importlib.resources`` in `Python 3.7 +and newer`_ (backported as `importlib_resources`_ for older versions of +Python), this can eliminate the need to use the older and less efficient +``pkg_resources`` package. + +By "installed package" we generally mean a third-party package installed into +Python's ``site-packages`` directory via tools such as `pip +`_. Specifically, +it means a package with either a discoverable ``dist-info`` or ``egg-info`` +directory, and metadata defined by `PEP 566`_ or its older specifications. +By default, package metadata can live on the file system or in wheels on +``sys.path``. Through an extension mechanism, the metadata can live almost +anywhere. + + +Overview +======== + +Let's say you wanted to get the version string for a package you've installed +using ``pip``. We start by creating a virtual environment and installing +something into it:: + + $ python3 -m venv example + $ source example/bin/activate + (example) $ pip install importlib_metadata + (example) $ pip install wheel + +You can get the version string for ``wheel`` by running the following:: + + (example) $ python + >>> from importlib_metadata import version + >>> version('wheel') + '0.32.3' + +You can also get the set of entry points keyed by group, such as +``console_scripts``, ``distutils.commands`` and others. Each group contains a +sequence of :ref:`EntryPoint ` objects. + +You can get the :ref:`metadata for a distribution `:: + + >>> list(metadata('wheel')) + ['Metadata-Version', 'Name', 'Version', 'Summary', 'Home-page', 'Author', 'Author-email', 'Maintainer', 'Maintainer-email', 'License', 'Project-URL', 'Project-URL', 'Project-URL', 'Keywords', 'Platform', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Requires-Python', 'Provides-Extra', 'Requires-Dist', 'Requires-Dist'] + +You can also get a :ref:`distribution's version number `, list its +:ref:`constituent files `_, and get a list of the distribution's +:ref:`requirements`_. + + +Distributions +============= + +.. CAUTION:: The ``Distribution`` class described here may or may not end up + in the final stable public API. Consider this class `provisional + `_ until the 1.0 + release. + +While the above API is the most common and convenient usage, you can get all +of that information from the ``Distribution`` class. A ``Distribution`` is an +abstract object that represents the metadata for a Python package. You can +get the ``Distribution`` instance:: + + >>> from importlib_metadata import distribution + >>> dist = distribution('wheel') + +Thus, an alternative way to get the version number is through the +``Distribution`` instance:: + + >>> dist.version + '0.32.3' + +There are all kinds of additional metadata available on the ``Distribution`` +instance:: + + >>> d.metadata['Requires-Python'] + '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' + >>> d.metadata['License'] + 'MIT' + +The full set of available metadata is not described here. See `PEP 566 +`_ for additional details. + + +Functional API +============== + +This package provides the following functionality via its public API. + + +.. _entry-points:: + +Entry points +------------ + +The ``entry_points()`` function returns a dictionary of all entry points, +keyed by group. Entry points are represented by ``EntryPoint`` instances; +each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and +a ``.load()`` method to resolve the value. + + >>> eps = entry_points() + >>> list(eps) + ['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation'] + >>> scripts = eps['console_scripts'] + >>> wheel = [ep for ep in scripts if ep.name == 'wheel'][0] + >>> wheel + EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts') + >>> main = wheel.load() + >>> main + + +The ``group`` and ``name`` are arbitrary values defined by the package author +and usually a client will wish to resolve all entry points for a particular +group. Read `the setuptools docs +`_ +for more information on entrypoints, their definition, and usage. + + +.. _metadata:: + +Distribution metadata +--------------------- + +Every distribution includes some metadata, which you can extract using the +``metadata()`` function:: + + >>> wheel_metadata = metadata('wheel') + +The keys of the returned data structure [#f1]_ name the metadata keywords, and +their values are returned unparsed from the distribution metadata:: + + >>> wheel_metadata['Requires-Python'] + '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' + + +.. _version:: + +Distribution versions +--------------------- + +The ``version()`` function is the quickest way to get a distribution's version +number, as a string:: + + >>> version('wheel') + '0.32.3' + + +.. _files:: + +Distribution files +------------------ + +You can also get the full set of files contained within a distribution. The +``files()`` function takes a distribution package name and returns all of the +files installed by this distribution. Each file object returned is a +``PackagePath``, a `pathlib.Path`_ derived object with additional ``dist``, +``size``, and ``hash`` properties as indicated by the metadata. For example:: + + >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0] + >>> util + PackagePath('wheel/util.py') + >>> util.size + 859 + >>> util.dist + + >>> util.hash + + +Once you have the file, you can also read its contents:: + + >>> print(util.read_text()) + import base64 + import sys + ... + def as_bytes(s): + if isinstance(s, text_type): + return s.encode('utf-8') + return s + + +.. _requirements:: + +Distribution requirements +------------------------- + +To get the full set of requirements for a distribution, use the ``requires()`` +function. Note that this returns an iterator:: + + >>> list(requires('wheel')) + ["pytest (>=3.0.0) ; extra == 'test'"] + + + +Extending the search algorithm +============================== + +Because package metadata is not available through ``sys.path`` searches, or +package loaders directly, the metadata for a package is found through import +system `finders`_. To find a distribution package's metadata, +``importlib_metadata`` queries the list of `meta path finders`_ on +`sys.meta_path`_. + +By default ``importlib_metadata`` installs a finder for distribution packages +found on the file system. This finder doesn't actually find any *packages*, +but it can find the packages' metadata. + +The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the +interface expected of finders by Python's import system. +``importlib_metadata`` extends this protocol by looking for an optional +``find_distributions`` callable on the finders from +``sys.meta_path``. If the finder has this method, it must return +an iterator over instances of the ``Distribution`` abstract class. This +method must have the signature:: + + def find_distributions(name=None, path=sys.path): + """Return an iterable of all Distribution instances capable of + loading the metadata for packages matching the name + (or all names if not supplied) along the paths in the list + of directories ``path`` (defaults to sys.path). + """ + +What this means in practice is that to support finding distribution package +metadata in locations other than the file system, you should derive from +``Distribution`` and implement the ``load_metadata()`` method. This takes a +single argument which is the name of the package whose metadata is being +found. This instance of the ``Distribution`` base abstract class is what your +finder's ``find_distributions()`` method should return. + + +.. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points +.. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api +.. _`Python 3.7 and newer`: https://docs.python.org/3/library/importlib.html#module-importlib.resources +.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html +.. _`PEP 566`: https://www.python.org/dev/peps/pep-0566/ +.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders +.. _`meta path finders`: https://docs.python.org/3/glossary.html#term-meta-path-finder +.. _`sys.meta_path`: https://docs.python.org/3/library/sys.html#sys.meta_path +.. _`pathlib.Path`: https://docs.python.org/3/library/pathlib.html#pathlib.Path + + +.. rubric:: Footnotes + +.. [#f1] Technically, the returned distribution metadata object is an + `email.message.Message + `_ + instance, but this is an implementation detail, and not part of the + stable API. You should only use dictionary-like methods and syntax + to access the metadata contents. diff --git a/Doc/prepare/example/example/__init__.py b/Doc/prepare/example/example/__init__.py new file mode 100644 index 00000000..ba73b743 --- /dev/null +++ b/Doc/prepare/example/example/__init__.py @@ -0,0 +1,2 @@ +def main(): + return 'example' diff --git a/Doc/prepare/example/setup.py b/Doc/prepare/example/setup.py new file mode 100644 index 00000000..38f9a6a6 --- /dev/null +++ b/Doc/prepare/example/setup.py @@ -0,0 +1,10 @@ +from setuptools import setup +setup( + name='example', + version='21.12', + license='Apache Software License', + packages=['example'], + entry_points={ + 'console_scripts': ['example = example:main'], + }, + ) diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py new file mode 100644 index 00000000..aa8519fd --- /dev/null +++ b/Lib/importlib/metadata/__init__.py @@ -0,0 +1,17 @@ +from .api import distribution, Distribution, PackageNotFoundError # noqa: F401 +from .api import metadata, entry_points, version, files, requires + +# Import for installation side-effects. +from . import _hooks # noqa: F401 + + +__all__ = [ + 'entry_points', + 'files', + 'metadata', + 'requires', + 'version', + ] + + +__version__ = version(__name__) diff --git a/Lib/importlib/metadata/_hooks.py b/Lib/importlib/metadata/_hooks.py new file mode 100644 index 00000000..bb2095e6 --- /dev/null +++ b/Lib/importlib/metadata/_hooks.py @@ -0,0 +1,134 @@ +import re +import sys +import zipp +import itertools + +from .api import Distribution +from contextlib import suppress +from pathlib import Path + + +def install(cls): + """Class decorator for installation on sys.meta_path.""" + sys.meta_path.append(cls) + return cls + + +class NullFinder: + @staticmethod + def find_spec(*args, **kwargs): + return None + +@install +class MetadataPathFinder(NullFinder): + """A degenerate finder for distribution packages on the file system. + + This finder supplies only a find_distributions() method for versions + of Python that do not have a PathFinder find_distributions(). + """ + search_template = r'{pattern}(-.*)?\.(dist|egg)-info' + + @classmethod + def find_distributions(cls, name=None, path=None): + """Return an iterable of all Distribution instances capable of + loading the metadata for packages matching the name + (or all names if not supplied) along the paths in the list + of directories ``path`` (defaults to sys.path). + """ + if path is None: + path = sys.path + pattern = '.*' if name is None else re.escape(name) + found = cls._search_paths(pattern, path) + return map(PathDistribution, found) + + @classmethod + def _search_paths(cls, pattern, paths): + """ + Find metadata directories in paths heuristically. + """ + return itertools.chain.from_iterable( + cls._search_path(path, pattern) + for path in map(Path, paths) + ) + + @classmethod + def _search_path(cls, root, pattern): + if not root.is_dir(): + return () + normalized = pattern.replace('-', '_') + return ( + item + for item in root.iterdir() + if item.is_dir() + and re.match( + cls.search_template.format(pattern=normalized), + str(item.name), + flags=re.IGNORECASE, + ) + ) + + +class PathDistribution(Distribution): + def __init__(self, path): + """Construct a distribution from a path to the metadata directory.""" + self._path = path + + def read_text(self, filename): + with suppress(FileNotFoundError): + with self._path.joinpath(filename).open(encoding='utf-8') as fp: + return fp.read() + return None + read_text.__doc__ = Distribution.read_text.__doc__ + + def locate_file(self, path): + return self._path.parent / path + + +@install +class WheelMetadataFinder(NullFinder): + """A degenerate finder for distribution packages in wheels. + + This finder supplies only a find_distributions() method for versions + of Python that do not have a PathFinder find_distributions(). + """ + search_template = r'{pattern}(-.*)?\.whl' + + @classmethod + def find_distributions(cls, name=None, path=None): + """Return an iterable of all Distribution instances capable of + loading the metadata for packages matching the name + (or all names if not supplied) along the paths in the list + of directories ``path`` (defaults to sys.path). + """ + if path is None: + path = sys.path + pattern = '.*' if name is None else re.escape(name) + found = cls._search_paths(pattern, path) + return map(WheelDistribution, found) + + @classmethod + def _search_paths(cls, pattern, paths): + return ( + path + for path in map(Path, paths) + if re.match( + cls.search_template.format(pattern=pattern), + str(path.name), + flags=re.IGNORECASE, + ) + ) + + +class WheelDistribution(Distribution): + def __init__(self, archive): + self._archive = zipp.Path(archive) + name, version = archive.name.split('-')[0:2] + self._dist_info = '{}-{}.dist-info'.format(name, version) + + def read_text(self, filename): + target = self._archive / self._dist_info / filename + return target.read_text() if target.exists() else None + read_text.__doc__ = Distribution.read_text.__doc__ + + def locate_file(self, path): + return self._archive / path diff --git a/Lib/importlib/metadata/api.py b/Lib/importlib/metadata/api.py new file mode 100644 index 00000000..384db023 --- /dev/null +++ b/Lib/importlib/metadata/api.py @@ -0,0 +1,351 @@ +import io +import re +import abc +import csv +import sys +import email +import operator +import functools +import itertools +import collections + +from importlib import import_module +from itertools import starmap + +import pathlib +from configparser import ConfigParser + +__metaclass__ = type + + +class PackageNotFoundError(ModuleNotFoundError): + """The package was not found.""" + + +class EntryPoint(collections.namedtuple('EntryPointBase', 'name value group')): + """An entry point as defined by Python packaging conventions.""" + + pattern = re.compile( + r'(?P[\w.]+)\s*' + r'(:\s*(?P[\w.]+))?\s*' + r'(?P\[.*\])?\s*$' + ) + """ + A regular expression describing the syntax for an entry point, + which might look like: + + - module + - package.module + - package.module:attribute + - package.module:object.attribute + - package.module:attr [extra1, extra2] + + Other combinations are possible as well. + + The expression is lenient about whitespace around the ':', + following the attr, and following any extras. + """ + + def load(self): + """Load the entry point from its definition. If only a module + is indicated by the value, return that module. Otherwise, + return the named object. + """ + match = self.pattern.match(self.value) + module = import_module(match.group('module')) + attrs = filter(None, (match.group('attr') or '').split('.')) + return functools.reduce(getattr, attrs, module) + + @property + def extras(self): + match = self.pattern.match(self.value) + return list(re.finditer(r'\w+', match.group('extras') or '')) + + @classmethod + def _from_config(cls, config): + return [ + cls(name, value, group) + for group in config.sections() + for name, value in config.items(group) + ] + + @classmethod + def _from_text(cls, text): + config = ConfigParser() + config.read_string(text) + return EntryPoint._from_config(config) + + def __iter__(self): + """ + Supply iter so one may construct dicts of EntryPoints easily. + """ + return iter((self.name, self)) + + +class PackagePath(pathlib.PosixPath): + """A reference to a path in a package""" + + def read_text(self, encoding='utf-8'): + with self.locate().open(encoding=encoding) as stream: + return stream.read() + + def read_binary(self): + with self.locate().open('rb') as stream: + return stream.read() + + def locate(self): + """Return a path-like object for this path""" + return self.dist.locate_file(self) + + +class FileHash: + def __init__(self, spec): + self.mode, _, self.value = spec.partition('=') + + def __repr__(self): + return ''.format(self.mode, self.value) + + +class Distribution: + """A Python distribution package.""" + + @abc.abstractmethod + def read_text(self, filename): + """Attempt to load metadata file given by the name. + + :param filename: The name of the file in the distribution info. + :return: The text if found, otherwise None. + """ + + @abc.abstractmethod + def locate_file(self, path): + """ + Given a path to a file in this distribution, return a path + to it. + """ + + @classmethod + def from_name(cls, name): + """Return the Distribution for the given package name. + + :param name: The name of the distribution package to search for. + :return: The Distribution instance (or subclass thereof) for the named + package, if found. + :raises PackageNotFoundError: When the named package's distribution + metadata cannot be found. + """ + for resolver in cls._discover_resolvers(): + dists = resolver(name) + dist = next(dists, None) + if dist is not None: + return dist + else: + raise PackageNotFoundError(name) + + @classmethod + def discover(cls): + """Return an iterable of Distribution objects for all packages. + + :return: Iterable of Distribution objects for all packages. + """ + return itertools.chain.from_iterable( + resolver() + for resolver in cls._discover_resolvers() + ) + + @staticmethod + def _discover_resolvers(): + """Search the meta_path for resolvers.""" + declared = ( + getattr(finder, 'find_distributions', None) + for finder in sys.meta_path + ) + return filter(None, declared) + + @classmethod + def find_local(cls): + dists = itertools.chain.from_iterable( + resolver(path=['.']) + for resolver in cls._discover_resolvers() + ) + dist, = dists + return dist + + @property + def metadata(self): + """Return the parsed metadata for this Distribution. + + The returned object will have keys that name the various bits of + metadata. See PEP 566 for details. + """ + text = self.read_text('METADATA') or self.read_text('PKG-INFO') + return email.message_from_string(text) + + @property + def version(self): + """Return the 'Version' metadata for the distribution package.""" + return self.metadata['Version'] + + @property + def entry_points(self): + return EntryPoint._from_text(self.read_text('entry_points.txt')) + + @property + def files(self): + file_lines = self._read_files_distinfo() or self._read_files_egginfo() + + def make_file(name, hash=None, size_str=None): + result = PackagePath(name) + result.hash = FileHash(hash) if hash else None + result.size = int(size_str) if size_str else None + result.dist = self + return result + + return file_lines and starmap(make_file, csv.reader(file_lines)) + + def _read_files_distinfo(self): + """ + Read the lines of RECORD + """ + text = self.read_text('RECORD') + return text and text.splitlines() + + def _read_files_egginfo(self): + """ + SOURCES.txt might contain literal commas, so wrap each line + in quotes. + """ + text = self.read_text('SOURCES.txt') + return text and map('"{}"'.format, text.splitlines()) + + @property + def requires(self): + return self._read_dist_info_reqs() or self._read_egg_info_reqs() + + def _read_dist_info_reqs(self): + spec = self.metadata['Requires-Dist'] + return spec and filter(None, spec.splitlines()) + + def _read_egg_info_reqs(self): + source = self.read_text('requires.txt') + return self._deps_from_requires_text(source) + + @classmethod + def _deps_from_requires_text(cls, source): + section_pairs = cls._read_sections(source.splitlines()) + sections = { + section: list(map(operator.itemgetter('line'), results)) + for section, results in + itertools.groupby(section_pairs, operator.itemgetter('section')) + } + return cls._convert_egg_info_reqs_to_simple_reqs(sections) + + @staticmethod + def _read_sections(lines): + section = None + for line in filter(None, lines): + section_match = re.match(r'\[(.*)\]$', line) + if section_match: + section = section_match.group(1) + continue + yield locals() + + @staticmethod + def _convert_egg_info_reqs_to_simple_reqs(sections): + """ + Historically, setuptools would solicit and store 'extra' + requirements, including those with environment markers, + in separate sections. More modern tools expect each + dependency to be defined separately, with any relevant + extras and environment markers attached directly to that + requirement. This method converts the former to the + latter. See _test_deps_from_requires_text for an example. + """ + def make_condition(name): + return name and 'extra == "{name}"'.format(name=name) + + def parse_condition(section): + section = section or '' + extra, sep, markers = section.partition(':') + if extra and markers: + markers = '({markers})'.format(markers=markers) + conditions = list(filter(None, [markers, make_condition(extra)])) + return '; ' + ' and '.join(conditions) if conditions else '' + + for section, deps in sections.items(): + for dep in deps: + yield dep + parse_condition(section) + + +def distribution(package): + """Get the ``Distribution`` instance for the given package. + + :param package: The name of the package as a string. + :return: A ``Distribution`` instance (or subclass thereof). + """ + return Distribution.from_name(package) + + +def distributions(): + """Get all ``Distribution`` instances in the current environment. + + :return: An iterable of ``Distribution`` instances. + """ + return Distribution.discover() + + +def local_distribution(): + """Get the ``Distribution`` instance for the package in CWD. + + :return: A ``Distribution`` instance (or subclass thereof). + """ + return Distribution.find_local() + + +def metadata(package): + """Get the metadata for the package. + + :param package: The name of the distribution package to query. + :return: An email.Message containing the parsed metadata. + """ + return Distribution.from_name(package).metadata + + +def version(package): + """Get the version string for the named package. + + :param package: The name of the distribution package to query. + :return: The version string for the package as defined in the package's + "Version" metadata key. + """ + return distribution(package).version + + +def entry_points(name=None): + """Return EntryPoint objects for all installed packages. + + :return: EntryPoint objects for all installed packages. + """ + eps = itertools.chain.from_iterable( + dist.entry_points for dist in distributions()) + by_group = operator.attrgetter('group') + ordered = sorted(eps, key=by_group) + grouped = itertools.groupby(ordered, by_group) + return { + group: tuple(eps) + for group, eps in grouped + } + + +def files(package): + return distribution(package).files + + +def requires(package): + """ + Return a list of requirements for the indicated distribution. + + :return: An iterator of requirements, suitable for + packaging.requirement.Requirement. + """ + return distribution(package).requires diff --git a/Lib/tests/test_importlib/__init__.py b/Lib/tests/test_importlib/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Lib/tests/test_importlib/data/__init__.py b/Lib/tests/test_importlib/data/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/Lib/tests/test_importlib/data/example-21.12-py3-none-any.whl b/Lib/tests/test_importlib/data/example-21.12-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..f92f7716e3e613a2a9703be785135fd8b35cfb1c GIT binary patch literal 1453 zcmWIWW@Zs#U|`^2$X%l2E2Vj`LJ`OVVPPOntw_u*$Vt_YkI&4@EQycTE2#AL^bJ1Y zd*;mL3tJuqF*Gf@GU?JH8`iH^x{lmwniEp0#}EKF@#|6@-~Vw}E~^1e(gI=)go(OF zhI)oZdMTO3CAyh;Y5Dr8c_l@a@df#rc_qbqB^4#ze&^0>pF8i_tM8|GN=HMp@2S^X zk2AU_JVQ5xHf&l`By9Y7#||{R?xt`CaRKe%0Af`#eJG?#%hkK?YZh9~AkY_15*$IjO%X$iwTT zj$Wre`^vxz1{aLYt{7i=!gcDr{>864*LXE_z0RKW*%YLqspb2W%hP9jkj4s=YiCcN z_rB_TX7!Ut=+0vKml077bk1%dR>0#dU)K;v7sn9C*zS#7hYUnqzke6~*(h?!FxuR) z*JA$j=D}6nJuk63>$g|^aHQ)0StmYywxSwA=est9~!zT(kqV|fK2BE1^mxK0q zD(qh)b?4sPb$w-l5$bn#F3Qs`KJ!0*^MQ6R{<p(1r$KgS)&i+9zwP$x1H90UiT)ZdwKjq+uV@j;~jI4*9 zFSbRdFl*>>tZs;(uk_eQ>hgU{@!k)&u4PPICc4gJ)}^;Bzl1h5{m``K-uhy9=9lTe z-NnTxRmGRCybveUZnWu;{lQCXJ}s6};eD{ z#=6?Q%UABG&=n2Q`J~J@GtS}8)q{&~{}TM{1aZ}u05ukm*h zl5ERPr=_}oV=S9Bfwe1=@2=aOxF-kSE;PEja34gdfE literal 0 HcmV?d00001 diff --git a/Lib/tests/test_importlib/fixtures.py b/Lib/tests/test_importlib/fixtures.py new file mode 100644 index 00000000..c2c6bf27 --- /dev/null +++ b/Lib/tests/test_importlib/fixtures.py @@ -0,0 +1,35 @@ +import sys +import shutil +import tempfile +import contextlib + +try: + from contextlib import ExitStack +except ImportError: + from contextlib2 import ExitStack + +try: + import pathlib +except ImportError: + import pathlib2 as pathlib + + +__metaclass__ = type + + +class SiteDir: + @staticmethod + @contextlib.contextmanager + def site_dir(): + tmpdir = tempfile.mkdtemp() + sys.path[:0] = [tmpdir] + try: + yield pathlib.Path(tmpdir) + finally: + sys.path.remove(tmpdir) + shutil.rmtree(tmpdir) + + def setUp(self): + self.fixtures = ExitStack() + self.addCleanup(self.fixtures.close) + self.site_dir = self.fixtures.enter_context(self.site_dir()) diff --git a/Lib/tests/test_importlib/test_api.py b/Lib/tests/test_importlib/test_api.py new file mode 100644 index 00000000..89f53e20 --- /dev/null +++ b/Lib/tests/test_importlib/test_api.py @@ -0,0 +1,141 @@ +import re +import textwrap +import unittest +import importlib.metadata +import packaging.requirements + +from collections.abc import Iterator + +class APITests(unittest.TestCase): + version_pattern = r'\d+\.\d+(\.\d)?' + + def test_retrieves_version_of_self(self): + version = importlib.metadata.version('importlib.metadata') + assert isinstance(version, str) + assert re.match(self.version_pattern, version) + + def test_retrieves_version_of_pip(self): + # Assume pip is installed and retrieve the version of pip. + version = importlib.metadata.version('pip') + assert isinstance(version, str) + assert re.match(self.version_pattern, version) + + def test_for_name_does_not_exist(self): + with self.assertRaises(importlib.metadata.PackageNotFoundError): + importlib.metadata.distribution('does-not-exist') + + def test_for_top_level(self): + distribution = importlib.metadata.distribution('importlib.metadata') + self.assertEqual( + distribution.read_text('top_level.txt').strip(), + 'importlib.metadata') + + def test_read_text(self): + top_level = [ + path for path in importlib.metadata.files('importlib.metadata') + if path.name == 'top_level.txt' + ][0] + self.assertEqual(top_level.read_text(), 'importlib.metadata\n') + + def test_entry_points(self): + scripts = importlib.metadata.entry_points()['console_scripts'] + scripts = dict(scripts) + pip_ep = scripts['pip'] + # We should probably not be dependent on a third party package's + # internal API staying stable. + self.assertEqual(pip_ep.value, 'pip._internal:main') + self.assertEqual(pip_ep.extras, []) + + def test_metadata_for_this_package(self): + md = importlib.metadata.metadata('importlib.metadata') + assert md['author'] == 'Barry Warsaw' + assert md['LICENSE'] == 'Apache Software License' + assert md['Name'] == 'importlib-metadata' + classifiers = md.get_all('Classifier') + assert 'Topic :: Software Development :: Libraries' in classifiers + + def test_importlib.metadata_version(self): + assert re.match(self.version_pattern, importlib.metadata.__version__) + + @staticmethod + def _test_files(files_iter): + assert isinstance(files_iter, Iterator) + files = list(files_iter) + root = files[0].root + for file in files: + assert file.root == root + assert not file.hash or file.hash.value + assert not file.hash or file.hash.mode == 'sha256' + assert not file.size or file.size >= 0 + assert file.locate().exists() + assert isinstance(file.read_binary(), bytes) + if file.name.endswith('.py'): + file.read_text() + + def test_file_hash_repr(self): + assertRegex = self.assertRegex + + util = [ + p for p in importlib.metadata.files('wheel') + if p.name == 'util.py' + ][0] + assertRegex( + repr(util.hash), + '') + + def test_files_dist_info(self): + self._test_files(importlib.metadata.files('pip')) + + def test_files_egg_info(self): + self._test_files(importlib.metadata.files('importlib.metadata')) + + def test_find_local(self): + dist = importlib.metadata.api.local_distribution() + assert dist.metadata['Name'] == 'importlib-metadata' + + def test_requires(self): + deps = importlib.metadata.requires('importlib.metadata') + parsed = list(map(packaging.requirements.Requirement, deps)) + assert all(parsed) + assert any( + dep.name == 'pathlib2' and dep.marker + for dep in parsed + ) + + def test_requires_dist_info(self): + # assume 'packaging' is installed as a wheel with dist-info + deps = importlib.metadata.requires('packaging') + parsed = list(map(packaging.requirements.Requirement, deps)) + assert parsed + + def test_more_complex_deps_requires_text(self): + requires = textwrap.dedent(""" + dep1 + dep2 + + [:python_version < "3"] + dep3 + + [extra1] + dep4 + + [extra2:python_version < "3"] + dep5 + """) + deps = sorted( + importlib.metadata.api.Distribution._deps_from_requires_text( + requires) + ) + expected = [ + 'dep1', + 'dep2', + 'dep3; python_version < "3"', + 'dep4; extra == "extra1"', + 'dep5; (python_version < "3") and extra == "extra2"', + ] + # It's important that the environment marker expression be + # wrapped in parentheses to avoid the following 'and' binding more + # tightly than some other part of the environment expression. + + assert deps == expected + assert all(map(packaging.requirements.Requirement, deps)) diff --git a/Lib/tests/test_importlib/test_main.py b/Lib/tests/test_importlib/test_main.py new file mode 100644 index 00000000..7d51d757 --- /dev/null +++ b/Lib/tests/test_importlib/test_main.py @@ -0,0 +1,163 @@ +# coding: utf-8 +from __future__ import unicode_literals + +import re +import textwrap +import unittest +import importlib +import importlib.metadata + +from . import fixtures +from importlib.metadata import _hooks + +try: + from builtins import str as text +except ImportError: + from __builtin__ import unicode as text + + +class BasicTests(unittest.TestCase): + version_pattern = r'\d+\.\d+(\.\d)?' + + def test_retrieves_version_of_pip(self): + # Assume pip is installed and retrieve the version of pip. + dist = importlib.metadata.Distribution.from_name('pip') + assert isinstance(dist.version, text) + assert re.match(self.version_pattern, dist.version) + + def test_for_name_does_not_exist(self): + with self.assertRaises(importlib.metadata.PackageNotFoundError): + importlib.metadata.Distribution.from_name('does-not-exist') + + def test_new_style_classes(self): + self.assertIsInstance(importlib.metadata.Distribution, type) + self.assertIsInstance(_hooks.MetadataPathFinder, type) + self.assertIsInstance(_hooks.WheelMetadataFinder, type) + self.assertIsInstance(_hooks.WheelDistribution, type) + + +class ImportTests(unittest.TestCase): + def test_import_nonexistent_module(self): + # Ensure that the MetadataPathFinder does not crash an import of a + # non-existant module. + with self.assertRaises(ImportError): + importlib.import_module('does_not_exist') + + def test_resolve(self): + scripts = dict(importlib.metadata.entry_points()['console_scripts']) + pip_ep = scripts['pip'] + import pip._internal + self.assertEqual(pip_ep.load(), pip._internal.main) + + def test_resolve_without_attr(self): + ep = importlib.metadata.api.EntryPoint( + name='ep', + value='importlib.metadata.api', + group='grp', + ) + assert ep.load() is importlib.metadata.api + + +class NameNormalizationTests(fixtures.SiteDir, unittest.TestCase): + @staticmethod + def pkg_with_dashes(site_dir): + """ + Create minimal metadata for a package with dashes + in the name (and thus underscores in the filename). + """ + metadata_dir = site_dir / 'my_pkg.dist-info' + metadata_dir.mkdir() + metadata = metadata_dir / 'METADATA' + with metadata.open('w') as strm: + strm.write('Version: 1.0\n') + return 'my-pkg' + + def test_dashes_in_dist_name_found_as_underscores(self): + """ + For a package with a dash in the name, the dist-info metadata + uses underscores in the name. Ensure the metadata loads. + """ + pkg_name = self.pkg_with_dashes(self.site_dir) + assert importlib.metadata.version(pkg_name) == '1.0' + + @staticmethod + def pkg_with_mixed_case(site_dir): + """ + Create minimal metadata for a package with mixed case + in the name. + """ + metadata_dir = site_dir / 'CherryPy.dist-info' + metadata_dir.mkdir() + metadata = metadata_dir / 'METADATA' + with metadata.open('w') as strm: + strm.write('Version: 1.0\n') + return 'CherryPy' + + def test_dist_name_found_as_any_case(self): + """ + Ensure the metadata loads when queried with any case. + """ + pkg_name = self.pkg_with_mixed_case(self.site_dir) + assert importlib.metadata.version(pkg_name) == '1.0' + assert importlib.metadata.version(pkg_name.lower()) == '1.0' + assert importlib.metadata.version(pkg_name.upper()) == '1.0' + + +class NonASCIITests(fixtures.SiteDir, unittest.TestCase): + @staticmethod + def pkg_with_non_ascii_description(site_dir): + """ + Create minimal metadata for a package with non-ASCII in + the description. + """ + metadata_dir = site_dir / 'portend.dist-info' + metadata_dir.mkdir() + metadata = metadata_dir / 'METADATA' + with metadata.open('w', encoding='utf-8') as fp: + fp.write('Description: pÎrˈtend\n') + return 'portend' + + @staticmethod + def pkg_with_non_ascii_description_egg_info(site_dir): + """ + Create minimal metadata for an egg-info package with + non-ASCII in the description. + """ + metadata_dir = site_dir / 'portend.dist-info' + metadata_dir.mkdir() + metadata = metadata_dir / 'METADATA' + with metadata.open('w', encoding='utf-8') as fp: + fp.write(textwrap.dedent(""" + Name: portend + + pÎrˈtend + """).lstrip()) + return 'portend' + + def test_metadata_loads(self): + pkg_name = self.pkg_with_non_ascii_description(self.site_dir) + meta = importlib.metadata.metadata(pkg_name) + assert meta['Description'] == 'pÎrˈtend' + + def test_metadata_loads_egg_info(self): + pkg_name = self.pkg_with_non_ascii_description_egg_info(self.site_dir) + meta = importlib.metadata.metadata(pkg_name) + assert meta.get_payload() == 'pÎrˈtend\n' + + +class DiscoveryTests(unittest.TestCase): + + def test_package_discovery(self): + dists = list(importlib.metadata.api.distributions()) + assert all( + isinstance(dist, importlib.metadata.Distribution) + for dist in dists + ) + assert any( + dist.metadata['Name'] == 'importlib-metadata' + for dist in dists + ) + assert any( + dist.metadata['Name'] == 'pip' + for dist in dists + ) diff --git a/Lib/tests/test_importlib/test_zip.py b/Lib/tests/test_importlib/test_zip.py new file mode 100644 index 00000000..8fdd6108 --- /dev/null +++ b/Lib/tests/test_importlib/test_zip.py @@ -0,0 +1,48 @@ +import sys +import unittest +import importlib.metadata + +from importlib.resources import path + +try: + from contextlib import ExitStack +except ImportError: + from contextlib2 import ExitStack + + +class BespokeLoader: + archive = 'bespoke' + + +class TestZip(unittest.TestCase): + def setUp(self): + # Find the path to the example.*.whl so we can add it to the front of + # sys.path, where we'll then try to find the metadata thereof. + self.resources = ExitStack() + self.addCleanup(self.resources.close) + wheel = self.resources.enter_context( + path('tests.test_importlib.data', + 'example-21.12-py3-none-any.whl')) + sys.path.insert(0, str(wheel)) + self.resources.callback(sys.path.pop, 0) + + def test_zip_version(self): + self.assertEqual(importlib.metadata.version('example'), '21.12') + + def test_zip_entry_points(self): + scripts = dict(importlib.metadata.entry_points()['console_scripts']) + entry_point = scripts['example'] + self.assertEqual(entry_point.value, 'example:main') + + def test_missing_metadata(self): + distribution = importlib.metadata.distribution('example') + self.assertIsNone(distribution.read_text('does not exist')) + + def test_case_insensitive(self): + self.assertEqual(importlib.metadata.version('Example'), '21.12') + + def test_files(self): + files = importlib.metadata.files('example') + for file in files: + path = str(file.dist.locate_file(file)) + assert '.whl/' in path, path From 7926e6f4b128b9f60d46cc46320613ed9e6fe69f Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Feb 2019 14:49:24 -0600 Subject: [PATCH 05/63] Added zipp (Version 0.3.2). Ref #47 --- Lib/importlib/metadata/zipp.py | 110 +++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 Lib/importlib/metadata/zipp.py diff --git a/Lib/importlib/metadata/zipp.py b/Lib/importlib/metadata/zipp.py new file mode 100644 index 00000000..ffd129f6 --- /dev/null +++ b/Lib/importlib/metadata/zipp.py @@ -0,0 +1,110 @@ +""" +>>> root = Path(getfixture('zipfile_abcde')) +>>> a, b = root.iterdir() +>>> a +Path('abcde.zip', 'a.txt') +>>> b +Path('abcde.zip', 'b/') +>>> b.name +'b' +>>> c = b / 'c.txt' +>>> c +Path('abcde.zip', 'b/c.txt') +>>> c.name +'c.txt' +>>> c.read_text() +'content of c' +>>> c.exists() +True +>>> (b / 'missing.txt').exists() +False +>>> str(c) +'abcde.zip/b/c.txt' +""" + +from __future__ import division + +import io +import sys +import posixpath +import zipfile +import operator +import functools + +__metaclass__ = type + + +class Path: + __repr = '{self.__class__.__name__}({self.root.filename!r}, {self.at!r})' + + def __init__(self, root, at=''): + self.root = root if isinstance(root, zipfile.ZipFile) \ + else zipfile.ZipFile(self._pathlib_compat(root)) + self.at = at + + @staticmethod + def _pathlib_compat(path): + """ + For path-like objects, convert to a filename for compatibility + on Python 3.6.1 and earlier. + """ + try: + return path.__fspath__() + except AttributeError: + return str(path) + + @property + def open(self): + return functools.partial(self.root.open, self.at) + + @property + def name(self): + return posixpath.basename(self.at.rstrip('/')) + + def read_text(self, *args, **kwargs): + with self.open() as strm: + return io.TextIOWrapper(strm, *args, **kwargs).read() + + def read_bytes(self): + with self.open() as strm: + return strm.read() + + def _is_child(self, path): + return posixpath.dirname(path.at.rstrip('/')) == self.at.rstrip('/') + + def _next(self, at): + return Path(self.root, at) + + def is_dir(self): + return not self.at or self.at.endswith('/') + + def is_file(self): + return not self.is_dir() + + def exists(self): + return self.at in self.root.namelist() + + def iterdir(self): + if not self.is_dir(): + raise ValueError("Can't listdir a file") + names = map(operator.attrgetter('filename'), self.root.infolist()) + subs = map(self._next, names) + return filter(self._is_child, subs) + + def __str__(self): + return posixpath.join(self.root.filename, self.at) + + def __repr__(self): + return self.__repr.format(self=self) + + def __truediv__(self, add): + add = self._pathlib_compat(add) + next = posixpath.join(self.at, add) + next_dir = posixpath.join(self.at, add, '') + names = self.root.namelist() + return self._next( + next_dir if next not in names and next_dir in names else next + ) + + if sys.version_info < (3,): + __div__ = __truediv__ \ No newline at end of file From 44a29b75102441f9ff5c4c7af85cbdbfa7dadf8f Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Feb 2019 14:51:26 -0600 Subject: [PATCH 06/63] Added zipp (Version 0.3.2). Ref #47 --- Lib/importlib/metadata/_hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/importlib/metadata/_hooks.py b/Lib/importlib/metadata/_hooks.py index bb2095e6..21f9e3dc 100644 --- a/Lib/importlib/metadata/_hooks.py +++ b/Lib/importlib/metadata/_hooks.py @@ -1,8 +1,8 @@ import re import sys -import zipp import itertools +from . import zipp from .api import Distribution from contextlib import suppress from pathlib import Path From 1e134db749f673da32981da484356d63966b2657 Mon Sep 17 00:00:00 2001 From: Steven Date: Sat, 23 Feb 2019 15:13:49 -0600 Subject: [PATCH 07/63] Fixed failing tests in CPython. Still need to address pip and importlib.metadata tests. --- Lib/importlib/metadata/__init__.py | 3 --- .../test_importlib/data}/__init__.py | 0 .../data/example-21.12-py3-none-any.whl | Bin Lib/{tests => test}/test_importlib/fixtures.py | 3 ++- Lib/{tests => test}/test_importlib/test_api.py | 0 Lib/{tests => test}/test_importlib/test_main.py | 0 Lib/{tests => test}/test_importlib/test_zip.py | 2 +- Lib/tests/test_importlib/data/__init__.py | 0 8 files changed, 3 insertions(+), 5 deletions(-) rename Lib/{tests/test_importlib => test/test_importlib/data}/__init__.py (100%) rename Lib/{tests => test}/test_importlib/data/example-21.12-py3-none-any.whl (100%) rename Lib/{tests => test}/test_importlib/fixtures.py (87%) rename Lib/{tests => test}/test_importlib/test_api.py (100%) rename Lib/{tests => test}/test_importlib/test_main.py (100%) rename Lib/{tests => test}/test_importlib/test_zip.py (97%) delete mode 100644 Lib/tests/test_importlib/data/__init__.py diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index aa8519fd..a6b80ded 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -12,6 +12,3 @@ 'requires', 'version', ] - - -__version__ = version(__name__) diff --git a/Lib/tests/test_importlib/__init__.py b/Lib/test/test_importlib/data/__init__.py similarity index 100% rename from Lib/tests/test_importlib/__init__.py rename to Lib/test/test_importlib/data/__init__.py diff --git a/Lib/tests/test_importlib/data/example-21.12-py3-none-any.whl b/Lib/test/test_importlib/data/example-21.12-py3-none-any.whl similarity index 100% rename from Lib/tests/test_importlib/data/example-21.12-py3-none-any.whl rename to Lib/test/test_importlib/data/example-21.12-py3-none-any.whl diff --git a/Lib/tests/test_importlib/fixtures.py b/Lib/test/test_importlib/fixtures.py similarity index 87% rename from Lib/tests/test_importlib/fixtures.py rename to Lib/test/test_importlib/fixtures.py index c2c6bf27..c38873b1 100644 --- a/Lib/tests/test_importlib/fixtures.py +++ b/Lib/test/test_importlib/fixtures.py @@ -16,7 +16,7 @@ __metaclass__ = type - +# Have a distinfo_pkg and egginfo_pkg (which is corresponding to pip and importlib_metadata) class SiteDir: @staticmethod @contextlib.contextmanager @@ -33,3 +33,4 @@ def setUp(self): self.fixtures = ExitStack() self.addCleanup(self.fixtures.close) self.site_dir = self.fixtures.enter_context(self.site_dir()) + diff --git a/Lib/tests/test_importlib/test_api.py b/Lib/test/test_importlib/test_api.py similarity index 100% rename from Lib/tests/test_importlib/test_api.py rename to Lib/test/test_importlib/test_api.py diff --git a/Lib/tests/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py similarity index 100% rename from Lib/tests/test_importlib/test_main.py rename to Lib/test/test_importlib/test_main.py diff --git a/Lib/tests/test_importlib/test_zip.py b/Lib/test/test_importlib/test_zip.py similarity index 97% rename from Lib/tests/test_importlib/test_zip.py rename to Lib/test/test_importlib/test_zip.py index 8fdd6108..4af027a1 100644 --- a/Lib/tests/test_importlib/test_zip.py +++ b/Lib/test/test_importlib/test_zip.py @@ -21,7 +21,7 @@ def setUp(self): self.resources = ExitStack() self.addCleanup(self.resources.close) wheel = self.resources.enter_context( - path('tests.test_importlib.data', + path('test.test_importlib.data', 'example-21.12-py3-none-any.whl')) sys.path.insert(0, str(wheel)) self.resources.callback(sys.path.pop, 0) diff --git a/Lib/tests/test_importlib/data/__init__.py b/Lib/tests/test_importlib/data/__init__.py deleted file mode 100644 index e69de29b..00000000 From 3a6bacfd28ec0a88cb1a421127eeee871b473c7e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 25 Mar 2019 09:26:32 -0400 Subject: [PATCH 08/63] Remove more compatibility code --- Lib/test/test_importlib/test_main.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py index d96a527a..64270850 100644 --- a/Lib/test/test_importlib/test_main.py +++ b/Lib/test/test_importlib/test_main.py @@ -10,18 +10,13 @@ from . import fixtures from importlib.metadata import _hooks -try: - from builtins import str as text -except ImportError: - from __builtin__ import unicode as text - class BasicTests(fixtures.DistInfoPkg, unittest.TestCase): version_pattern = r'\d+\.\d+(\.\d)?' def test_retrieves_version_of_self(self): dist = importlib.metadata.Distribution.from_name('distinfo-pkg') - assert isinstance(dist.version, text) + assert isinstance(dist.version, str) assert re.match(self.version_pattern, dist.version) def test_for_name_does_not_exist(self): From 6c35d56afbcb25147e76bfba6e7e101febd816f5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 25 Mar 2019 09:28:37 -0400 Subject: [PATCH 09/63] Create a stub for when packaging.requirements isn't available. --- Lib/test/test_importlib/test_api.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Lib/test/test_importlib/test_api.py b/Lib/test/test_importlib/test_api.py index b15042dc..51be0d12 100644 --- a/Lib/test/test_importlib/test_api.py +++ b/Lib/test/test_importlib/test_api.py @@ -2,7 +2,14 @@ import textwrap import unittest import importlib.metadata -import packaging.requirements + +try: + import packaging.requirements +except ImportError: + # stub out requirements parsing when not available + class packaging: + class requirements: + Requirement = str from collections.abc import Iterator From 6b5ec23d252f1d7978d7a859c74dc8e324b69f4e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 25 Mar 2019 09:41:32 -0400 Subject: [PATCH 10/63] Fix two more substitutions --- Lib/test/test_importlib/test_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_importlib/test_api.py b/Lib/test/test_importlib/test_api.py index 51be0d12..8b6ae3e0 100644 --- a/Lib/test/test_importlib/test_api.py +++ b/Lib/test/test_importlib/test_api.py @@ -61,7 +61,7 @@ def test_metadata_for_this_package(self): classifiers = md.get_all('Classifier') assert 'Topic :: Software Development :: Libraries' in classifiers - def test_importlib.metadata_version(self): + def test_importlib_metadata_version(self): assert re.match(self.version_pattern, importlib.metadata.__version__) @staticmethod @@ -145,5 +145,5 @@ def test_more_complex_deps_requires_text(self): class LocalProjectTests(fixtures.LocalPackage, unittest.TestCase): def test_find_local(self): - dist = importlib_metadata.api.local_distribution() + dist = importlib.metadata.api.local_distribution() assert dist.metadata['Name'] == 'egginfo-pkg' From a8823e5fcf0a36a5f379ea1361e75fd2c63be56a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 25 Mar 2019 09:52:09 -0400 Subject: [PATCH 11/63] Remove long-line comment --- Lib/test/test_importlib/fixtures.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_importlib/fixtures.py b/Lib/test/test_importlib/fixtures.py index 8217c4e0..8a8b4add 100644 --- a/Lib/test/test_importlib/fixtures.py +++ b/Lib/test/test_importlib/fixtures.py @@ -49,7 +49,6 @@ def tempdir_as_cwd(): yield tmp -# Have a distinfo_pkg and egginfo_pkg (which is corresponding to pip and importlib_metadata) class SiteDir: @staticmethod @contextlib.contextmanager From 11d3e531baac49896de4cdb510b1a0826d90a326 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 25 Mar 2019 10:32:29 -0400 Subject: [PATCH 12/63] Rename 'test_api' to 'test_metadata_api' to avoid conflict with module of the same name in cpython. --- Lib/test/test_importlib/{test_api.py => test_metadata_api.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Lib/test/test_importlib/{test_api.py => test_metadata_api.py} (100%) diff --git a/Lib/test/test_importlib/test_api.py b/Lib/test/test_importlib/test_metadata_api.py similarity index 100% rename from Lib/test/test_importlib/test_api.py rename to Lib/test/test_importlib/test_metadata_api.py From 955ff5861929042925f8045295ef0460a1c56abc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 25 Mar 2019 10:38:46 -0400 Subject: [PATCH 13/63] Remove test for __version__, not relevant upstream --- Lib/test/test_importlib/test_metadata_api.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py index 8b6ae3e0..078f104b 100644 --- a/Lib/test/test_importlib/test_metadata_api.py +++ b/Lib/test/test_importlib/test_metadata_api.py @@ -61,9 +61,6 @@ def test_metadata_for_this_package(self): classifiers = md.get_all('Classifier') assert 'Topic :: Software Development :: Libraries' in classifiers - def test_importlib_metadata_version(self): - assert re.match(self.version_pattern, importlib.metadata.__version__) - @staticmethod def _test_files(files_iter): assert isinstance(files_iter, Iterator), files_iter From 6f8bd81d6d48a50841efd2fd05d48561d9a11e48 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 25 Mar 2019 22:19:32 -0400 Subject: [PATCH 14/63] Remove 'prepare' directory --- Doc/prepare/example/example/__init__.py | 2 -- Doc/prepare/example/setup.py | 10 ---------- 2 files changed, 12 deletions(-) delete mode 100644 Doc/prepare/example/example/__init__.py delete mode 100644 Doc/prepare/example/setup.py diff --git a/Doc/prepare/example/example/__init__.py b/Doc/prepare/example/example/__init__.py deleted file mode 100644 index ba73b743..00000000 --- a/Doc/prepare/example/example/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -def main(): - return 'example' diff --git a/Doc/prepare/example/setup.py b/Doc/prepare/example/setup.py deleted file mode 100644 index 38f9a6a6..00000000 --- a/Doc/prepare/example/setup.py +++ /dev/null @@ -1,10 +0,0 @@ -from setuptools import setup -setup( - name='example', - version='21.12', - license='Apache Software License', - packages=['example'], - entry_points={ - 'console_scripts': ['example = example:main'], - }, - ) From 50e14914326c16d86b7616f52750baee5dc6eb8e Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 8 May 2019 09:58:56 -0400 Subject: [PATCH 15/63] Rely on zipfile.Path as found in stdlib --- Lib/importlib/metadata/_hooks.py | 4 +- Lib/importlib/metadata/zipp.py | 110 ------------------------------- 2 files changed, 2 insertions(+), 112 deletions(-) delete mode 100644 Lib/importlib/metadata/zipp.py diff --git a/Lib/importlib/metadata/_hooks.py b/Lib/importlib/metadata/_hooks.py index 707d9f04..e6248442 100644 --- a/Lib/importlib/metadata/_hooks.py +++ b/Lib/importlib/metadata/_hooks.py @@ -1,8 +1,8 @@ import re import sys +import zipfile import itertools -from . import zipp from .api import Distribution from .abc import DistributionFinder from contextlib import suppress @@ -136,7 +136,7 @@ def _search_paths(cls, pattern, paths): class WheelDistribution(Distribution): def __init__(self, archive): - self._archive = zipp.Path(archive) + self._archive = zipfile.Path(archive) name, version = archive.name.split('-')[0:2] self._dist_info = '{}-{}.dist-info'.format(name, version) diff --git a/Lib/importlib/metadata/zipp.py b/Lib/importlib/metadata/zipp.py deleted file mode 100644 index ffd129f6..00000000 --- a/Lib/importlib/metadata/zipp.py +++ /dev/null @@ -1,110 +0,0 @@ -""" ->>> root = Path(getfixture('zipfile_abcde')) ->>> a, b = root.iterdir() ->>> a -Path('abcde.zip', 'a.txt') ->>> b -Path('abcde.zip', 'b/') ->>> b.name -'b' ->>> c = b / 'c.txt' ->>> c -Path('abcde.zip', 'b/c.txt') ->>> c.name -'c.txt' ->>> c.read_text() -'content of c' ->>> c.exists() -True ->>> (b / 'missing.txt').exists() -False ->>> str(c) -'abcde.zip/b/c.txt' -""" - -from __future__ import division - -import io -import sys -import posixpath -import zipfile -import operator -import functools - -__metaclass__ = type - - -class Path: - __repr = '{self.__class__.__name__}({self.root.filename!r}, {self.at!r})' - - def __init__(self, root, at=''): - self.root = root if isinstance(root, zipfile.ZipFile) \ - else zipfile.ZipFile(self._pathlib_compat(root)) - self.at = at - - @staticmethod - def _pathlib_compat(path): - """ - For path-like objects, convert to a filename for compatibility - on Python 3.6.1 and earlier. - """ - try: - return path.__fspath__() - except AttributeError: - return str(path) - - @property - def open(self): - return functools.partial(self.root.open, self.at) - - @property - def name(self): - return posixpath.basename(self.at.rstrip('/')) - - def read_text(self, *args, **kwargs): - with self.open() as strm: - return io.TextIOWrapper(strm, *args, **kwargs).read() - - def read_bytes(self): - with self.open() as strm: - return strm.read() - - def _is_child(self, path): - return posixpath.dirname(path.at.rstrip('/')) == self.at.rstrip('/') - - def _next(self, at): - return Path(self.root, at) - - def is_dir(self): - return not self.at or self.at.endswith('/') - - def is_file(self): - return not self.is_dir() - - def exists(self): - return self.at in self.root.namelist() - - def iterdir(self): - if not self.is_dir(): - raise ValueError("Can't listdir a file") - names = map(operator.attrgetter('filename'), self.root.infolist()) - subs = map(self._next, names) - return filter(self._is_child, subs) - - def __str__(self): - return posixpath.join(self.root.filename, self.at) - - def __repr__(self): - return self.__repr.format(self=self) - - def __truediv__(self, add): - add = self._pathlib_compat(add) - next = posixpath.join(self.at, add) - next_dir = posixpath.join(self.at, add, '') - names = self.root.namelist() - return self._next( - next_dir if next not in names and next_dir in names else next - ) - - if sys.version_info < (3,): - __div__ = __truediv__ \ No newline at end of file From 227a8385eab3d35217e9dd052ee08dd8ed90d9b5 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 8 May 2019 10:43:44 -0400 Subject: [PATCH 16/63] Rename doc to match module structure. --- Doc/library/{importlib_metadata.rst => importlib.metadata.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Doc/library/{importlib_metadata.rst => importlib.metadata.rst} (100%) diff --git a/Doc/library/importlib_metadata.rst b/Doc/library/importlib.metadata.rst similarity index 100% rename from Doc/library/importlib_metadata.rst rename to Doc/library/importlib.metadata.rst From 86affbdadbaaca85b412a53a2aba64f8fcc1374f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 8 May 2019 11:09:00 -0400 Subject: [PATCH 17/63] Disable implicit 'python3' highlight for shell examples --- Doc/library/importlib.metadata.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index ecf0986c..0f9b5bbf 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -37,6 +37,8 @@ Let's say you wanted to get the version string for a package you've installed using ``pip``. We start by creating a virtual environment and installing something into it:: +.. highlight:: none + $ python3 -m venv example $ source example/bin/activate (example) $ pip install importlib_metadata @@ -44,6 +46,8 @@ something into it:: You can get the version string for ``wheel`` by running the following:: +.. highlight:: none + (example) $ python >>> from importlib_metadata import version >>> version('wheel') From 10d4f28c0db562c1bc020054303a9d32d5beb626 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 8 May 2019 11:10:56 -0400 Subject: [PATCH 18/63] Rename importlib_metadata to importlib.metadata in docs --- Doc/library/importlib.metadata.rst | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 0f9b5bbf..ad17d939 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -1,10 +1,10 @@ .. _using: ========================== - Using importlib_metadata + Using importlib.metadata ========================== -``importlib_metadata`` is a library that provides for access to installed +``importlib.metadata`` is a library that provides for access to installed package metadata. Built in part on Python's import system, this library intends to replace similar functionality in the `entry point API`_ and `metadata API`_ of ``pkg_resources``. Along with @@ -41,7 +41,6 @@ something into it:: $ python3 -m venv example $ source example/bin/activate - (example) $ pip install importlib_metadata (example) $ pip install wheel You can get the version string for ``wheel`` by running the following:: @@ -49,7 +48,7 @@ You can get the version string for ``wheel`` by running the following:: .. highlight:: none (example) $ python - >>> from importlib_metadata import version + >>> from importlib.metadata import version >>> version('wheel') '0.32.3' @@ -147,7 +146,7 @@ files installed by this distribution. Each file object returned is a >>> util.size 859 >>> util.dist - + >>> util.hash @@ -183,7 +182,7 @@ of that information from the ``Distribution`` class. A ``Distribution`` is an abstract object that represents the metadata for a Python package. You can get the ``Distribution`` instance:: - >>> from importlib_metadata import distribution + >>> from importlib.metadata import distribution >>> dist = distribution('wheel') Thus, an alternative way to get the version number is through the @@ -210,16 +209,16 @@ Extending the search algorithm Because package metadata is not available through ``sys.path`` searches, or package loaders directly, the metadata for a package is found through import system `finders`_. To find a distribution package's metadata, -``importlib_metadata`` queries the list of `meta path finders`_ on +``importlib.metadata`` queries the list of `meta path finders`_ on `sys.meta_path`_. -By default ``importlib_metadata`` installs a finder for distribution packages +By default ``importlib.metadata`` installs a finder for distribution packages found on the file system. This finder doesn't actually find any *packages*, but it can find the packages' metadata. The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the interface expected of finders by Python's import system. -``importlib_metadata`` extends this protocol by looking for an optional +``importlib.metadata`` extends this protocol by looking for an optional ``find_distributions`` callable on the finders from ``sys.meta_path``. If the finder has this method, it must return an iterator over instances of the ``Distribution`` abstract class. This From 89d4ed8611257c6c628f63474f1c800fa3cec303 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 8 May 2019 12:41:23 -0400 Subject: [PATCH 19/63] Update references to importlib_metadata --- Lib/test/test_importlib/test_metadata_api.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py index 4aba1af7..8e4c95cc 100644 --- a/Lib/test/test_importlib/test_metadata_api.py +++ b/Lib/test/test_importlib/test_metadata_api.py @@ -91,11 +91,11 @@ def test_files_egg_info(self): self._test_files(importlib.metadata.files('egginfo-pkg')) def test_version_egg_info_file(self): - version = importlib_metadata.version('egginfo-file') + version = importlib.metadata.version('egginfo-file') self.assertEqual(version, '0.1') def test_requires_egg_info_file(self): - requirements = importlib_metadata.requires('egginfo-file') + requirements = importlib.metadata.requires('egginfo-file') self.assertIsNone(requirements) def test_requires(self): From 3e00db72041b50aa5cd8d061bd380a37527d3083 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 10 May 2019 10:01:35 -0400 Subject: [PATCH 20/63] Remove extraneous newline --- Lib/test/test_importlib/fixtures.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_importlib/fixtures.py b/Lib/test/test_importlib/fixtures.py index 737ea4be..b4f34157 100644 --- a/Lib/test/test_importlib/fixtures.py +++ b/Lib/test/test_importlib/fixtures.py @@ -193,4 +193,3 @@ def build_files(file_defs, prefix=pathlib.Path()): def DALS(str): "Dedent and left-strip" return textwrap.dedent(str).lstrip() - From 80d04fd334ab6f96df836f47bcf37b91f059ec4a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 10 May 2019 21:54:55 -0400 Subject: [PATCH 21/63] Move MetaPathFinder into PathFinder. Ref #57. --- Lib/importlib/_bootstrap_external.py | 55 ++++++++++++++++++++++++++++ Lib/importlib/metadata/_hooks.py | 55 ---------------------------- 2 files changed, 55 insertions(+), 55 deletions(-) create mode 100644 Lib/importlib/_bootstrap_external.py diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py new file mode 100644 index 00000000..a6275620 --- /dev/null +++ b/Lib/importlib/_bootstrap_external.py @@ -0,0 +1,55 @@ +import sys + + +# Merge the body of this class into _bootstrap_external: +class PathFinder: + search_template = r'{pattern}(-.*)?\.(dist|egg)-info' + + @classmethod + def find_distributions(cls, name=None, path=None): + """Return an iterable of all Distribution instances capable of + loading the metadata for packages matching the name + (or all names if not supplied) along the paths in the list + of directories ``path`` (defaults to sys.path). + """ + import re + from importlib.metadata._hooks import PathDistribution + if path is None: + path = sys.path + pattern = '.*' if name is None else re.escape(name) + found = cls._search_paths(pattern, path) + return map(PathDistribution, found) + + @classmethod + def _search_paths(cls, pattern, paths): + """ + Find metadata directories in paths heuristically. + """ + import itertools + return itertools.chain.from_iterable( + cls._search_path(path, pattern) + for path in map(cls._switch_path, paths) + ) + + @staticmethod + def _switch_path(path): + from contextlib import suppress + import zipfile + from pathlib import Path + with suppress(Exception): + return zipfile.Path(path) + return Path(path) + + @classmethod + def _predicate(cls, pattern, root, item): + import re + return re.match(pattern, str(item.name), flags=re.IGNORECASE) + + @classmethod + def _search_path(cls, root, pattern): + if not root.is_dir(): + return () + normalized = pattern.replace('-', '_') + matcher = cls.search_template.format(pattern=normalized) + return (item for item in root.iterdir() + if cls._predicate(matcher, root, item)) diff --git a/Lib/importlib/metadata/_hooks.py b/Lib/importlib/metadata/_hooks.py index f6bed1a6..11b94772 100644 --- a/Lib/importlib/metadata/_hooks.py +++ b/Lib/importlib/metadata/_hooks.py @@ -1,12 +1,8 @@ -import re import sys -import zipfile -import itertools from .api import Distribution from .abc import DistributionFinder from contextlib import suppress -from pathlib import Path def install(cls): @@ -25,57 +21,6 @@ def find_spec(*args, **kwargs): return None -@install -class MetadataPathFinder(NullFinder): - """A degenerate finder for distribution packages on the file system. - - This finder supplies only a find_distributions() method for versions - of Python that do not have a PathFinder find_distributions(). - """ - search_template = r'{pattern}(-.*)?\.(dist|egg)-info' - - def find_distributions(self, name=None, path=None): - """Return an iterable of all Distribution instances capable of - loading the metadata for packages matching the name - (or all names if not supplied) along the paths in the list - of directories ``path`` (defaults to sys.path). - """ - if path is None: - path = sys.path - pattern = '.*' if name is None else re.escape(name) - found = self._search_paths(pattern, path) - return map(PathDistribution, found) - - @classmethod - def _search_paths(cls, pattern, paths): - """ - Find metadata directories in paths heuristically. - """ - return itertools.chain.from_iterable( - cls._search_path(path, pattern) - for path in map(cls._switch_path, paths) - ) - - @staticmethod - def _switch_path(path): - with suppress(Exception): - return zipfile.Path(path) - return Path(path) - - @classmethod - def _predicate(cls, pattern, root, item): - return re.match(pattern, str(item.name), flags=re.IGNORECASE) - - @classmethod - def _search_path(cls, root, pattern): - if not root.is_dir(): - return () - normalized = pattern.replace('-', '_') - matcher = cls.search_template.format(pattern=normalized) - return (item for item in root.iterdir() - if cls._predicate(matcher, root, item)) - - class PathDistribution(Distribution): def __init__(self, path): """Construct a distribution from a path to the metadata directory.""" From be570a591ca57b4e7287fae992e2d6da92fab7ee Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 14 May 2019 09:56:32 -0400 Subject: [PATCH 22/63] Remove install and NullFinder, no longer needed. --- Lib/importlib/metadata/_hooks.py | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/Lib/importlib/metadata/_hooks.py b/Lib/importlib/metadata/_hooks.py index 11b94772..4c57f4d4 100644 --- a/Lib/importlib/metadata/_hooks.py +++ b/Lib/importlib/metadata/_hooks.py @@ -1,26 +1,7 @@ -import sys - from .api import Distribution -from .abc import DistributionFinder from contextlib import suppress -def install(cls): - """Class decorator for installation on sys.meta_path.""" - sys.meta_path.append(cls()) - return cls - - -class NullFinder(DistributionFinder): - """ - A "Finder" (aka "MetaClassFinder") that never finds any modules, - but may find distributions. - """ - @staticmethod - def find_spec(*args, **kwargs): - return None - - class PathDistribution(Distribution): def __init__(self, path): """Construct a distribution from a path to the metadata directory.""" From b980e975b97c64d3bd997e4d98526ff9b05e4c12 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 14 May 2019 09:59:38 -0400 Subject: [PATCH 23/63] Update docs from upstream (skip doctests) --- Doc/library/importlib.metadata.rst | 52 +++++++++++++++--------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 303c4c24..44185071 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -41,8 +41,8 @@ You can get the version string for ``wheel`` by running the following:: .. highlight:: none (example) $ python - >>> from importlib.metadata import version - >>> version('wheel') + >>> from importlib.metadata import version # doctest: +SKIP + >>> version('wheel') # doctest: +SKIP '0.32.3' You can also get the set of entry points keyed by group, such as @@ -51,7 +51,7 @@ sequence of :ref:`EntryPoint ` objects. You can get the :ref:`metadata for a distribution `:: - >>> list(metadata('wheel')) + >>> list(metadata('wheel')) # doctest: +SKIP ['Metadata-Version', 'Name', 'Version', 'Summary', 'Home-page', 'Author', 'Author-email', 'Maintainer', 'Maintainer-email', 'License', 'Project-URL', 'Project-URL', 'Project-URL', 'Keywords', 'Platform', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Classifier', 'Requires-Python', 'Provides-Extra', 'Requires-Dist', 'Requires-Dist'] You can also get a :ref:`distribution's version number `, list its @@ -73,17 +73,17 @@ Entry points The ``entry_points()`` function returns a dictionary of all entry points, keyed by group. Entry points are represented by ``EntryPoint`` instances; each ``EntryPoint`` has a ``.name``, ``.group``, and ``.value`` attributes and -a ``.load()`` method to resolve the value:: +a ``.load()`` method to resolve the value. - >>> eps = entry_points() - >>> list(eps) + >>> eps = entry_points() # doctest: +SKIP + >>> list(eps) # doctest: +SKIP ['console_scripts', 'distutils.commands', 'distutils.setup_keywords', 'egg_info.writers', 'setuptools.installation'] - >>> scripts = eps['console_scripts'] - >>> wheel = [ep for ep in scripts if ep.name == 'wheel'][0] - >>> wheel + >>> scripts = eps['console_scripts'] # doctest: +SKIP + >>> wheel = [ep for ep in scripts if ep.name == 'wheel'][0] # doctest: +SKIP + >>> wheel # doctest: +SKIP EntryPoint(name='wheel', value='wheel.cli:main', group='console_scripts') - >>> main = wheel.load() - >>> main + >>> main = wheel.load() # doctest: +SKIP + >>> main # doctest: +SKIP The ``group`` and ``name`` are arbitrary values defined by the package author @@ -101,12 +101,12 @@ Distribution metadata Every distribution includes some metadata, which you can extract using the ``metadata()`` function:: - >>> wheel_metadata = metadata('wheel') + >>> wheel_metadata = metadata('wheel') # doctest: +SKIP The keys of the returned data structure [#f1]_ name the metadata keywords, and their values are returned unparsed from the distribution metadata:: - >>> wheel_metadata['Requires-Python'] + >>> wheel_metadata['Requires-Python'] # doctest: +SKIP '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' @@ -118,7 +118,7 @@ Distribution versions The ``version()`` function is the quickest way to get a distribution's version number, as a string:: - >>> version('wheel') + >>> version('wheel') # doctest: +SKIP '0.32.3' @@ -133,19 +133,19 @@ files installed by this distribution. Each file object returned is a ``PackagePath``, a `pathlib.Path`_ derived object with additional ``dist``, ``size``, and ``hash`` properties as indicated by the metadata. For example:: - >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0] - >>> util + >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0] # doctest: +SKIP + >>> util # doctest: +SKIP PackagePath('wheel/util.py') - >>> util.size + >>> util.size # doctest: +SKIP 859 - >>> util.dist + >>> util.dist # doctest: +SKIP - >>> util.hash + >>> util.hash # doctest: +SKIP Once you have the file, you can also read its contents:: - >>> print(util.read_text()) + >>> print(util.read_text()) # doctest: +SKIP import base64 import sys ... @@ -163,7 +163,7 @@ Distribution requirements To get the full set of requirements for a distribution, use the ``requires()`` function. Note that this returns an iterator:: - >>> list(requires('wheel')) + >>> list(requires('wheel')) # doctest: +SKIP ["pytest (>=3.0.0) ; extra == 'test'"] @@ -175,21 +175,21 @@ of that information from the ``Distribution`` class. A ``Distribution`` is an abstract object that represents the metadata for a Python package. You can get the ``Distribution`` instance:: - >>> from importlib.metadata import distribution - >>> dist = distribution('wheel') + >>> from importlib.metadata import distribution # doctest: +SKIP + >>> dist = distribution('wheel') # doctest: +SKIP Thus, an alternative way to get the version number is through the ``Distribution`` instance:: - >>> dist.version + >>> dist.version # doctest: +SKIP '0.32.3' There are all kinds of additional metadata available on the ``Distribution`` instance:: - >>> d.metadata['Requires-Python'] + >>> d.metadata['Requires-Python'] # doctest: +SKIP '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' - >>> d.metadata['License'] + >>> d.metadata['License'] # doctest: +SKIP 'MIT' The full set of available metadata is not described here. See `PEP 566 From 3eba983db6dedcb8f24136d14a68b0c5bdad3937 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 14 May 2019 10:00:53 -0400 Subject: [PATCH 24/63] Remove reference to _hooks --- Lib/test/test_importlib/test_main.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py index 926064ed..22529b1c 100644 --- a/Lib/test/test_importlib/test_main.py +++ b/Lib/test/test_importlib/test_main.py @@ -7,7 +7,7 @@ from . import fixtures from importlib.metadata import ( - Distribution, PackageNotFoundError, _hooks, api, distributions, + Distribution, PackageNotFoundError, api, distributions, entry_points, metadata, version) @@ -25,7 +25,6 @@ def test_for_name_does_not_exist(self): def test_new_style_classes(self): self.assertIsInstance(Distribution, type) - self.assertIsInstance(_hooks.MetadataPathFinder, type) class ImportTests(fixtures.DistInfoPkg, unittest.TestCase): From cbfd3844b171ef07a3cc1833033bd978d8cec37d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 14 May 2019 10:36:46 -0400 Subject: [PATCH 25/63] Add 'provisional' disclaimer. --- Doc/library/importlib.metadata.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 44185071..416d8b6f 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -4,6 +4,10 @@ Using importlib.metadata ========================== +.. note:: + This functionality is provisional and may deviate from the usual + version semantics of the standard library. + ``importlib.metadata`` is a library that provides for access to installed package metadata. Built in part on Python's import system, this library intends to replace similar functionality in the `entry point From 8dd914a3085425c953551e6ff2ee9765fa425c07 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 14 May 2019 10:39:59 -0400 Subject: [PATCH 26/63] Update reference to test data --- Lib/test/test_importlib/test_zip.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_importlib/test_zip.py b/Lib/test/test_importlib/test_zip.py index 9b6bce0d..e92028cf 100644 --- a/Lib/test/test_importlib/test_zip.py +++ b/Lib/test/test_importlib/test_zip.py @@ -45,7 +45,7 @@ def setUp(self): self.resources = ExitStack() self.addCleanup(self.resources.close) egg = self.resources.enter_context( - path('importlib_metadata.tests.data', + path('test.test_importlib.data', 'example-21.12-py3.6.egg')) sys.path.insert(0, str(egg)) self.resources.callback(sys.path.pop, 0) From 72d70c977d09f29a4dc7fd617746f17a9a9602a0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 11 Jun 2019 14:15:03 -0400 Subject: [PATCH 27/63] Exclude PYPY_OPEN_BUG handling as discussed in !75. --- Lib/importlib/_bootstrap_external.py | 7 ++----- Lib/importlib/metadata/__init__.py | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py index cdabec15..5ee4b680 100644 --- a/Lib/importlib/_bootstrap_external.py +++ b/Lib/importlib/_bootstrap_external.py @@ -1,5 +1,4 @@ import sys -import os # Merge the body of this class into _bootstrap_external: @@ -38,10 +37,8 @@ def _switch_path(path): from contextlib import suppress import zipfile from pathlib import Path - PYPY_OPEN_BUG = False - if not PYPY_OPEN_BUG or os.path.isfile(path): # pragma: no branch - with suppress(Exception): - return zipfile.Path(path) + with suppress(Exception): + return zipfile.Path(path) return Path(path) @classmethod diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index e6f556a1..944dbb5f 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -1,5 +1,4 @@ import io -import os import re import abc import csv From 3c389f53131d66d3ef4f2ae08a561afe2745436a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 11 Jun 2019 14:54:33 -0400 Subject: [PATCH 28/63] Refresh docs from cpython --- Doc/library/importlib.metadata.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 3d10b5ae..2126498e 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -32,17 +32,17 @@ Overview Let's say you wanted to get the version string for a package you've installed using ``pip``. We start by creating a virtual environment and installing -something into it:: +something into it: -.. highlight:: none +.. code-block:: shell-session $ python3 -m venv example $ source example/bin/activate (example) $ pip install wheel -You can get the version string for ``wheel`` by running the following:: +You can get the version string for ``wheel`` by running the following: -.. highlight:: none +.. code-block:: pycon (example) $ python >>> from importlib.metadata import version # doctest: +SKIP From 1158760660d480ec9c3419a24f6d43f6be840db1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 9 Aug 2019 07:29:26 -0400 Subject: [PATCH 29/63] Move metadata to a simple module to match upstream --- Lib/importlib/{metadata/__init__.py => metadata.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename Lib/importlib/{metadata/__init__.py => metadata.py} (100%) diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata.py similarity index 100% rename from Lib/importlib/metadata/__init__.py rename to Lib/importlib/metadata.py From 83b48ec85f27b65b01969bfc453f30308ca9d138 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 11 Sep 2019 07:35:07 +0100 Subject: [PATCH 30/63] Move MetadataPathFinder back into the metadata package. Let importlib.PathFinder.find_distributions just call into MetadataPathFinder.find_distributions. Ref #88. --- Lib/importlib/_bootstrap_external.py | 63 ---------------------------- Lib/importlib/metadata.py | 54 ++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 63 deletions(-) delete mode 100644 Lib/importlib/_bootstrap_external.py diff --git a/Lib/importlib/_bootstrap_external.py b/Lib/importlib/_bootstrap_external.py deleted file mode 100644 index 88ea8574..00000000 --- a/Lib/importlib/_bootstrap_external.py +++ /dev/null @@ -1,63 +0,0 @@ -import os - - -# Merge the body of this class into _bootstrap_external: -class PathFinder: - @classmethod - def find_distributions(self, context=None): - """ - Find distributions. - - Return an iterable of all Distribution instances capable of - loading the metadata for packages matching ``context.name`` - (or all names if ``None`` indicated) along the paths in the list - of directories ``context.path``. - """ - from importlib.metadata import PathDistribution, DistributionFinder - if context is None: - context = DistributionFinder.Context() - found = self._search_paths(context.pattern, context.path) - return map(PathDistribution, found) - - @classmethod - def _search_paths(cls, pattern, paths): - """Find metadata directories in paths heuristically.""" - import itertools - return itertools.chain.from_iterable( - cls._search_path(path, pattern) - for path in map(cls._switch_path, paths) - ) - - @staticmethod - def _switch_path(path): - from contextlib import suppress - import zipfile - import pathlib - PYPY_OPEN_BUG = False - if not PYPY_OPEN_BUG or os.path.isfile(path): # pragma: no branch - with suppress(Exception): - return zipfile.Path(path) - return pathlib.Path(path) - - @classmethod - def _matches_info(cls, normalized, item): - import re - template = r'{pattern}(-.*)?\.(dist|egg)-info' - manifest = template.format(pattern=normalized) - return re.match(manifest, item.name, flags=re.IGNORECASE) - - @classmethod - def _matches_legacy(cls, normalized, item): - import re - template = r'{pattern}-.*\.egg[\\/]EGG-INFO' - manifest = template.format(pattern=normalized) - return re.search(manifest, str(item), flags=re.IGNORECASE) - - @classmethod - def _search_path(cls, root, pattern): - if not root.is_dir(): - return () - normalized = pattern.replace('-', '_') - return (item for item in root.iterdir() - if cls._matches_info(normalized, item) - or cls._matches_legacy(normalized, item)) diff --git a/Lib/importlib/metadata.py b/Lib/importlib/metadata.py index e2307665..612cb911 100644 --- a/Lib/importlib/metadata.py +++ b/Lib/importlib/metadata.py @@ -1,10 +1,12 @@ import io +import os import re import abc import csv import sys import email import pathlib +import zipfile import operator import functools import itertools @@ -363,6 +365,58 @@ def find_distributions(self, context=Context()): """ +class MetadataPathFinder(DistributionFinder): + @classmethod + def find_distributions(cls, context=DistributionFinder.Context()): + """ + Find distributions. + + Return an iterable of all Distribution instances capable of + loading the metadata for packages matching ``context.name`` + (or all names if ``None`` indicated) along the paths in the list + of directories ``context.path``. + """ + found = cls._search_paths(context.pattern, context.path) + return map(PathDistribution, found) + + @classmethod + def _search_paths(cls, pattern, paths): + """Find metadata directories in paths heuristically.""" + return itertools.chain.from_iterable( + cls._search_path(path, pattern) + for path in map(cls._switch_path, paths) + ) + + @staticmethod + def _switch_path(path): + PYPY_OPEN_BUG = False + if not PYPY_OPEN_BUG or os.path.isfile(path): # pragma: no branch + with suppress(Exception): + return zipfile.Path(path) + return pathlib.Path(path) + + @classmethod + def _matches_info(cls, normalized, item): + template = r'{pattern}(-.*)?\.(dist|egg)-info' + manifest = template.format(pattern=normalized) + return re.match(manifest, item.name, flags=re.IGNORECASE) + + @classmethod + def _matches_legacy(cls, normalized, item): + template = r'{pattern}-.*\.egg[\\/]EGG-INFO' + manifest = template.format(pattern=normalized) + return re.search(manifest, str(item), flags=re.IGNORECASE) + + @classmethod + def _search_path(cls, root, pattern): + if not root.is_dir(): + return () + normalized = pattern.replace('-', '_') + return (item for item in root.iterdir() + if cls._matches_info(normalized, item) + or cls._matches_legacy(normalized, item)) + + class PathDistribution(Distribution): def __init__(self, path): """Construct a distribution from a path to the metadata directory. From 4761c03c9d6c12a3da09268a854de87cbf119db9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 8 Dec 2019 16:59:43 -0500 Subject: [PATCH 31/63] Update documentation to reflect implementation in stdlib. --- Doc/library/importlib.metadata.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 4a4a8f7d..135020a5 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -216,9 +216,9 @@ system `finders`_. To find a distribution package's metadata, ``importlib.metadata`` queries the list of `meta path finders`_ on `sys.meta_path`_. -By default ``importlib.metadata`` installs a finder for distribution packages -found on the file system. This finder doesn't actually find any *packages*, -but it can find the packages' metadata. +The default ``PathFinder`` for Python includes a hook that calls into +``importlib.metadata.MetadataPathFinder`` for finding distributions +loaded from typical file-system-based paths. The abstract class :py:class:`importlib.abc.MetaPathFinder` defines the interface expected of finders by Python's import system. From fc4b9bd7bd24a0b6592c076a4ddc106abc28fd23 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 10 Dec 2019 19:15:47 -0500 Subject: [PATCH 32/63] Correct reference to module --- Lib/test/test_importlib/test_main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py index 3909f07d..c5f1dbba 100644 --- a/Lib/test/test_importlib/test_main.py +++ b/Lib/test/test_importlib/test_main.py @@ -188,7 +188,7 @@ def test_egg(self): class TestEntryPoints(unittest.TestCase): def __init__(self, *args): super(TestEntryPoints, self).__init__(*args) - self.ep = importlib_metadata.EntryPoint('name', 'value', 'group') + self.ep = importlib.metadata.EntryPoint('name', 'value', 'group') def test_entry_point_pickleable(self): revived = pickle.loads(pickle.dumps(self.ep)) From 9af4411f11ea38205b7753cc7368e5bc8458a9ed Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 29 Dec 2019 12:32:32 -0500 Subject: [PATCH 33/63] Apply changes from https://github.com/python/cpython/pull/17730 not included in the backport --- Doc/library/importlib.metadata.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index d232d4de..15e58b86 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -13,7 +13,7 @@ package metadata. Built in part on Python's import system, this library intends to replace similar functionality in the `entry point API`_ and `metadata API`_ of ``pkg_resources``. Along with :mod:`importlib.resources` in Python 3.7 -and newer (backported as :doc:`importlib_resources ` for older versions of +and newer (backported as `importlib_resources`_ for older versions of Python), this can eliminate the need to use the older and less efficient ``pkg_resources`` package. @@ -212,7 +212,7 @@ Extending the search algorithm Because package metadata is not available through :data:`sys.path` searches, or package loaders directly, the metadata for a package is found through import -system `finders`_. To find a distribution package's metadata, +system :ref:`finders `. To find a distribution package's metadata, ``importlib.metadata`` queries the list of :term:`meta path finders ` on :data:`sys.meta_path`. @@ -247,7 +247,7 @@ a custom finder, return instances of this derived ``Distribution`` in the .. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api -.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders +.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html .. rubric:: Footnotes From 80f2241fbe3f9a21cc4351f3d23a2d5a596d62ff Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 25 Mar 2020 12:54:18 -0400 Subject: [PATCH 34/63] Add decorator per bpo-40029 --- Lib/test/test_importlib/test_zip.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/test/test_importlib/test_zip.py b/Lib/test/test_importlib/test_zip.py index 9568c226..9466ca4a 100644 --- a/Lib/test/test_importlib/test_zip.py +++ b/Lib/test/test_importlib/test_zip.py @@ -7,7 +7,10 @@ ) from importlib.resources import path +from test.support import requires_zlib + +@requires_zlib class TestZip(unittest.TestCase): root = 'test.test_importlib.data' @@ -47,6 +50,7 @@ def test_files(self): assert '.whl/' in path, path +@requires_zlib class TestEgg(TestZip): def setUp(self): # Find the path to the example-*.egg so we can add it to the front of From ae58e94a45182e805e0263292fa3910d1dba8e39 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 5 Jun 2020 15:00:13 -0400 Subject: [PATCH 35/63] Fix decorator per bpo-40275. --- Lib/test/test_importlib/test_zip.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_importlib/test_zip.py b/Lib/test/test_importlib/test_zip.py index 9466ca4a..fa87cd7c 100644 --- a/Lib/test/test_importlib/test_zip.py +++ b/Lib/test/test_importlib/test_zip.py @@ -10,7 +10,7 @@ from test.support import requires_zlib -@requires_zlib +@requires_zlib() class TestZip(unittest.TestCase): root = 'test.test_importlib.data' @@ -50,7 +50,7 @@ def test_files(self): assert '.whl/' in path, path -@requires_zlib +@requires_zlib() class TestEgg(TestZip): def setUp(self): # Find the path to the example-*.egg so we can add it to the front of From 1a8f32b467b36945b09079e867163542ba6c881f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 19 Oct 2020 15:12:13 -0400 Subject: [PATCH 36/63] Relocate tests/docs for cpython merge --- .../using.rst => Doc/library/importlib.metadata.rst | 0 {tests => Lib/test/test_importlib}/__init__.py | 0 {tests => Lib/test/test_importlib}/data/__init__.py | 0 .../data/example-21.12-py3-none-any.whl | Bin .../test_importlib}/data/example-21.12-py3.6.egg | Bin {tests => Lib/test/test_importlib}/fixtures.py | 0 .../test/test_importlib}/test_integration.py | 0 {tests => Lib/test/test_importlib}/test_main.py | 0 .../test/test_importlib/test_metadata_api.py | 0 {tests => Lib/test/test_importlib}/test_zip.py | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename docs/using.rst => Doc/library/importlib.metadata.rst (100%) rename {tests => Lib/test/test_importlib}/__init__.py (100%) rename {tests => Lib/test/test_importlib}/data/__init__.py (100%) rename {tests => Lib/test/test_importlib}/data/example-21.12-py3-none-any.whl (100%) rename {tests => Lib/test/test_importlib}/data/example-21.12-py3.6.egg (100%) rename {tests => Lib/test/test_importlib}/fixtures.py (100%) rename {tests => Lib/test/test_importlib}/test_integration.py (100%) rename {tests => Lib/test/test_importlib}/test_main.py (100%) rename tests/test_api.py => Lib/test/test_importlib/test_metadata_api.py (100%) rename {tests => Lib/test/test_importlib}/test_zip.py (100%) diff --git a/docs/using.rst b/Doc/library/importlib.metadata.rst similarity index 100% rename from docs/using.rst rename to Doc/library/importlib.metadata.rst diff --git a/tests/__init__.py b/Lib/test/test_importlib/__init__.py similarity index 100% rename from tests/__init__.py rename to Lib/test/test_importlib/__init__.py diff --git a/tests/data/__init__.py b/Lib/test/test_importlib/data/__init__.py similarity index 100% rename from tests/data/__init__.py rename to Lib/test/test_importlib/data/__init__.py diff --git a/tests/data/example-21.12-py3-none-any.whl b/Lib/test/test_importlib/data/example-21.12-py3-none-any.whl similarity index 100% rename from tests/data/example-21.12-py3-none-any.whl rename to Lib/test/test_importlib/data/example-21.12-py3-none-any.whl diff --git a/tests/data/example-21.12-py3.6.egg b/Lib/test/test_importlib/data/example-21.12-py3.6.egg similarity index 100% rename from tests/data/example-21.12-py3.6.egg rename to Lib/test/test_importlib/data/example-21.12-py3.6.egg diff --git a/tests/fixtures.py b/Lib/test/test_importlib/fixtures.py similarity index 100% rename from tests/fixtures.py rename to Lib/test/test_importlib/fixtures.py diff --git a/tests/test_integration.py b/Lib/test/test_importlib/test_integration.py similarity index 100% rename from tests/test_integration.py rename to Lib/test/test_importlib/test_integration.py diff --git a/tests/test_main.py b/Lib/test/test_importlib/test_main.py similarity index 100% rename from tests/test_main.py rename to Lib/test/test_importlib/test_main.py diff --git a/tests/test_api.py b/Lib/test/test_importlib/test_metadata_api.py similarity index 100% rename from tests/test_api.py rename to Lib/test/test_importlib/test_metadata_api.py diff --git a/tests/test_zip.py b/Lib/test/test_importlib/test_zip.py similarity index 100% rename from tests/test_zip.py rename to Lib/test/test_importlib/test_zip.py From 05e3288ea3413f7ad9317b14a266c024cb6a2458 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 7 Mar 2021 17:40:04 -0500 Subject: [PATCH 37/63] CPython needs warning to be enabled explicitly. --- Lib/test/test_importlib/test_metadata_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py index eb2b5a52..a0f9d511 100644 --- a/Lib/test/test_importlib/test_metadata_api.py +++ b/Lib/test/test_importlib/test_metadata_api.py @@ -122,6 +122,7 @@ def test_entry_points_dict_construction(self): Capture this now deprecated use-case. """ with warnings.catch_warnings(record=True) as caught: + warnings.filterwarnings("default", category=DeprecationWarning) eps = dict(entry_points(group='entries')) assert 'main' in eps From 63e65f346453b74fe2de76ba9e766d73160610d0 Mon Sep 17 00:00:00 2001 From: Brett Cannon Date: Fri, 2 Apr 2021 12:35:32 -0700 Subject: [PATCH 38/63] bpo-43672: raise ImportWarning when calling find_loader() (GH-25119) --- Lib/test/test_importlib/test_main.py | 36 +++++++------------- Lib/test/test_importlib/test_metadata_api.py | 32 +++++++---------- 2 files changed, 24 insertions(+), 44 deletions(-) diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py index 02e8a573..db97e539 100644 --- a/Lib/test/test_importlib/test_main.py +++ b/Lib/test/test_importlib/test_main.py @@ -36,12 +36,10 @@ def test_for_name_does_not_exist(self): Distribution.from_name('does-not-exist') def test_package_not_found_mentions_metadata(self): - """ - When a package is not found, that could indicate that the - packgae is not installed or that it is installed without - metadata. Ensure the exception mentions metadata to help - guide users toward the cause. See #124. - """ + # When a package is not found, that could indicate that the + # packgae is not installed or that it is installed without + # metadata. Ensure the exception mentions metadata to help + # guide users toward the cause. See #124. with self.assertRaises(PackageNotFoundError) as ctx: Distribution.from_name('does-not-exist') @@ -90,10 +88,8 @@ def pkg_with_dashes(site_dir): return 'my-pkg' def test_dashes_in_dist_name_found_as_underscores(self): - """ - For a package with a dash in the name, the dist-info metadata - uses underscores in the name. Ensure the metadata loads. - """ + # For a package with a dash in the name, the dist-info metadata + # uses underscores in the name. Ensure the metadata loads. pkg_name = self.pkg_with_dashes(self.site_dir) assert version(pkg_name) == '1.0' @@ -111,9 +107,7 @@ def pkg_with_mixed_case(site_dir): return 'CherryPy' def test_dist_name_found_as_any_case(self): - """ - Ensure the metadata loads when queried with any case. - """ + # Ensure the metadata loads when queried with any case. pkg_name = self.pkg_with_mixed_case(self.site_dir) assert version(pkg_name) == '1.0' assert version(pkg_name.lower()) == '1.0' @@ -241,13 +235,11 @@ def test_repr(self): assert "'name'" in repr(self.ep) def test_hashable(self): - """EntryPoints should be hashable""" + # EntryPoints should be hashable. hash(self.ep) def test_json_dump(self): - """ - json should not expect to be able to dump an EntryPoint - """ + # json should not expect to be able to dump an EntryPoint. with self.assertRaises(Exception): with warnings.catch_warnings(record=True): json.dumps(self.ep) @@ -259,9 +251,7 @@ def test_attr(self): assert self.ep.attr is None def test_sortable(self): - """ - EntryPoint objects are sortable, but result is undefined. - """ + # EntryPoint objects are sortable, but result is undefined. sorted( [ EntryPoint('b', 'val', 'group'), @@ -274,10 +264,8 @@ class FileSystem( fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder, unittest.TestCase ): def test_unicode_dir_on_sys_path(self): - """ - Ensure a Unicode subdirectory of a directory on sys.path - does not crash. - """ + # Ensure a Unicode subdirectory of a directory on sys.path + # does not crash. fixtures.build_files( {self.unicode_filename(): {}}, prefix=self.site_dir, diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py index a0f9d511..b54c3bd0 100644 --- a/Lib/test/test_importlib/test_metadata_api.py +++ b/Lib/test/test_importlib/test_metadata_api.py @@ -81,10 +81,8 @@ def test_entry_points_distribution(self): self.assertEqual(ep.dist.version, "1.0.0") def test_entry_points_unique_packages(self): - """ - Entry points should only be exposed for the first package - on sys.path with a given name. - """ + # Entry points should only be exposed for the first package + # on sys.path with a given name. alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) alt_pkg = { @@ -116,11 +114,9 @@ def test_entry_points_missing_group(self): assert entry_points(group='missing') == () def test_entry_points_dict_construction(self): - """ - Prior versions of entry_points() returned simple lists and - allowed casting those lists into maps by name using ``dict()``. - Capture this now deprecated use-case. - """ + # Prior versions of entry_points() returned simple lists and + # allowed casting those lists into maps by name using ``dict()``. + # Capture this now deprecated use-case. with warnings.catch_warnings(record=True) as caught: warnings.filterwarnings("default", category=DeprecationWarning) eps = dict(entry_points(group='entries')) @@ -134,11 +130,9 @@ def test_entry_points_dict_construction(self): assert "Construction of dict of EntryPoints is deprecated" in str(expected) def test_entry_points_groups_getitem(self): - """ - Prior versions of entry_points() returned a dict. Ensure - that callers using '.__getitem__()' are supported but warned to - migrate. - """ + # Prior versions of entry_points() returned a dict. Ensure + # that callers using '.__getitem__()' are supported but warned to + # migrate. with warnings.catch_warnings(record=True): entry_points()['entries'] == entry_points(group='entries') @@ -146,11 +140,9 @@ def test_entry_points_groups_getitem(self): entry_points()['missing'] def test_entry_points_groups_get(self): - """ - Prior versions of entry_points() returned a dict. Ensure - that callers using '.get()' are supported but warned to - migrate. - """ + # Prior versions of entry_points() returned a dict. Ensure + # that callers using '.get()' are supported but warned to + # migrate. with warnings.catch_warnings(record=True): entry_points().get('missing', 'default') == 'default' entry_points().get('entries', 'default') == entry_points()['entries'] @@ -259,7 +251,7 @@ def test_find_distributions_specified_path(self): assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) def test_distribution_at_pathlib(self): - """Demonstrate how to load metadata direct from a directory.""" + # Demonstrate how to load metadata direct from a directory. dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info' dist = Distribution.at(dist_info_path) assert dist.version == '1.0.0' From 2ea4a5a4298bfe9acafa2064dbcc67f176271b50 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 12 Apr 2021 17:40:27 -0400 Subject: [PATCH 39/63] Sync docs with python/cpython --- Doc/library/importlib.metadata.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index bb3903fa..fee5e677 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -238,6 +238,8 @@ Python packages or modules:: >>> packages_distributions() {'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...} +.. versionadded:: 3.10 + Distributions ============= From 842fc9f8b22ab852254d320e3d8d5a4f8e19fad3 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 14 Apr 2021 20:56:21 -0400 Subject: [PATCH 40/63] bpo-37741: make importlib.metadata docs discoverable through a module directive. (GH-25415) Automerge-Triggered-By: GH:jaraco --- Doc/library/importlib.metadata.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index fee5e677..3cd200b7 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -4,6 +4,11 @@ Using :mod:`!importlib.metadata` ================================= +.. module:: importlib.metadata + :synopsis: The implementation of the importlib metadata. + +**Source code:** :source:`Lib/importlib/metadata.py` + .. note:: This functionality is provisional and may deviate from the usual version semantics of the standard library. From b4b3eff37c3d70732abdd060fab518727923273c Mon Sep 17 00:00:00 2001 From: Zackery Spytz Date: Fri, 16 Apr 2021 17:13:38 -0600 Subject: [PATCH 41/63] bpo-43856: Add a versionadded directive to the importlib.metadata docs (GH-25445) Use a versionadded directive to generate the text "New in version 3.8." (to match with the documentation of other modules). Automerge-Triggered-By: GH:jaraco --- Doc/library/importlib.metadata.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 3cd200b7..0f0a8dd9 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -7,6 +7,8 @@ .. module:: importlib.metadata :synopsis: The implementation of the importlib metadata. +.. versionadded:: 3.8 + **Source code:** :source:`Lib/importlib/metadata.py` .. note:: From 5a3ec5718efdef575753832294d0454234b89220 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Fri, 23 Apr 2021 17:07:40 -0400 Subject: [PATCH 42/63] Move importlib.metadata into its own package to avoid conflicts with other importlib functionality. --- Lib/importlib/{metadata.py => metadata/__init__.py} | 0 Lib/importlib/{ => metadata}/_adapters.py | 0 Lib/importlib/{ => metadata}/_collections.py | 0 Lib/importlib/{ => metadata}/_functools.py | 0 Lib/importlib/{ => metadata}/_itertools.py | 0 Lib/importlib/{ => metadata}/_meta.py | 3 +-- Lib/importlib/{ => metadata}/_text.py | 0 7 files changed, 1 insertion(+), 2 deletions(-) rename Lib/importlib/{metadata.py => metadata/__init__.py} (100%) rename Lib/importlib/{ => metadata}/_adapters.py (100%) rename Lib/importlib/{ => metadata}/_collections.py (100%) rename Lib/importlib/{ => metadata}/_functools.py (100%) rename Lib/importlib/{ => metadata}/_itertools.py (100%) rename Lib/importlib/{ => metadata}/_meta.py (87%) rename Lib/importlib/{ => metadata}/_text.py (100%) diff --git a/Lib/importlib/metadata.py b/Lib/importlib/metadata/__init__.py similarity index 100% rename from Lib/importlib/metadata.py rename to Lib/importlib/metadata/__init__.py diff --git a/Lib/importlib/_adapters.py b/Lib/importlib/metadata/_adapters.py similarity index 100% rename from Lib/importlib/_adapters.py rename to Lib/importlib/metadata/_adapters.py diff --git a/Lib/importlib/_collections.py b/Lib/importlib/metadata/_collections.py similarity index 100% rename from Lib/importlib/_collections.py rename to Lib/importlib/metadata/_collections.py diff --git a/Lib/importlib/_functools.py b/Lib/importlib/metadata/_functools.py similarity index 100% rename from Lib/importlib/_functools.py rename to Lib/importlib/metadata/_functools.py diff --git a/Lib/importlib/_itertools.py b/Lib/importlib/metadata/_itertools.py similarity index 100% rename from Lib/importlib/_itertools.py rename to Lib/importlib/metadata/_itertools.py diff --git a/Lib/importlib/_meta.py b/Lib/importlib/metadata/_meta.py similarity index 87% rename from Lib/importlib/_meta.py rename to Lib/importlib/metadata/_meta.py index 5cb690fe..04d9a023 100644 --- a/Lib/importlib/_meta.py +++ b/Lib/importlib/metadata/_meta.py @@ -1,5 +1,4 @@ -from ._compat import Protocol -from typing import Any, Dict, Iterator, List, TypeVar, Union +from typing import Any, Dict, Iterator, List, Protocol, TypeVar, Union _T = TypeVar("_T") diff --git a/Lib/importlib/_text.py b/Lib/importlib/metadata/_text.py similarity index 100% rename from Lib/importlib/_text.py rename to Lib/importlib/metadata/_text.py From a44c88068818a557cdf00878122ecaa0a64a8e3f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 26 May 2021 12:55:53 -0400 Subject: [PATCH 43/63] Apply changes from upstream. --- Doc/library/importlib.metadata.rst | 17 +++++++++++++++-- Lib/importlib/metadata/__init__.py | 2 ++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 41469761..9bedee5a 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -171,11 +171,18 @@ the values are returned unparsed from the distribution metadata:: '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' ``PackageMetadata`` also presents a ``json`` attribute that returns -all the metadata in a JSON-compatible form per PEP 566:: +all the metadata in a JSON-compatible form per :PEP:`566`:: >>> wheel_metadata.json['requires_python'] '>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*' +.. versionchanged:: 3.10 + The ``Description`` is now included in the metadata when presented + through the payload. Line continuation characters have been removed. + +.. versionadded:: 3.10 + The ``json`` attribute was added. + .. _version: @@ -197,7 +204,7 @@ Distribution files You can also get the full set of files contained within a distribution. The ``files()`` function takes a distribution package name and returns all of the files installed by this distribution. Each file object returned is a -``PackagePath``, a :class:`pathlib.Path` derived object with additional ``dist``, +``PackagePath``, a :class:`pathlib.PurePath` derived object with additional ``dist``, ``size``, and ``hash`` properties as indicated by the metadata. For example:: >>> util = [p for p in files('wheel') if 'util.py' in str(p)][0] # doctest: +SKIP @@ -221,6 +228,12 @@ Once you have the file, you can also read its contents:: return s.encode('utf-8') return s +You can also use the ``locate`` method to get a the absolute path to the +file:: + + >>> util.locate() # doctest: +SKIP + PosixPath('/home/gustav/example/lib/site-packages/wheel/util.py') + In the case where the metadata file listing files (RECORD or SOURCES.txt) is missing, ``files()`` will return ``None``. The caller may wish to wrap calls to diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index 14216219..e2f2e47f 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -15,6 +15,7 @@ import collections from . import _adapters, _meta +from ._meta import PackageMetadata from ._collections import FreezableDefaultDict, Pair from ._functools import method_cache from ._itertools import unique_everseen @@ -29,6 +30,7 @@ __all__ = [ 'Distribution', 'DistributionFinder', + 'PackageMetadata', 'PackageNotFoundError', 'distribution', 'distributions', From 0e9e17725e8dce102c7d7f211b8e03214f4055a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= Date: Fri, 18 Jun 2021 15:27:24 +0200 Subject: [PATCH 44/63] bpo-44451: Reset DeprecationWarning filters in test_api.test_entry_points_by_index This avoids the following error in CPython tests if DeprecationWarnings are ignored. ====================================================================== ERROR: test_entry_points_by_index (test.test_importlib.test_metadata_api.APITests) Prior versions of Distribution.entry_points would return a ---------------------------------------------------------------------- Traceback (most recent call last): File "/builddir/build/BUILD/Python-3.10.0b3/Lib/test/test_importlib/test_metadata_api.py", line 145, in test_entry_points_by_index expected = next(iter(caught)) StopIteration ---------------------------------------------------------------------- Ran 1402 tests in 2.125s FAILED (errors=1, skipped=18, expected failures=1) --- tests/test_api.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_api.py b/tests/test_api.py index 819d4841..544ab7f4 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -142,6 +142,7 @@ def test_entry_points_by_index(self): """ eps = distribution('distinfo-pkg').entry_points with warnings.catch_warnings(record=True) as caught: + warnings.filterwarnings("default", category=DeprecationWarning) eps[0] # check warning From d916be010b01b0bda2345019f1c78de12b18175c Mon Sep 17 00:00:00 2001 From: Barry Warsaw Date: Mon, 12 Jul 2021 16:56:40 -0700 Subject: [PATCH 45/63] bpo-44613: Make importlib.metadata non-provisional (#27101) * importlib.metadata is no longer provisional as of 3.10 * Add NEWS entry --- Doc/library/importlib.metadata.rst | 6 ++---- .../Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst | 1 + 2 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 Misc/NEWS.d/next/Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 9bedee5a..c43457a3 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -8,13 +8,11 @@ :synopsis: The implementation of the importlib metadata. .. versionadded:: 3.8 +.. versionchanged:: 3.10 + ``importlib.metadata`` is no longer provisional. **Source code:** :source:`Lib/importlib/metadata.py` -.. note:: - This functionality is provisional and may deviate from the usual - version semantics of the standard library. - ``importlib.metadata`` is a library that provides for access to installed package metadata. Built in part on Python's import system, this library intends to replace similar functionality in the `entry point diff --git a/Misc/NEWS.d/next/Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst b/Misc/NEWS.d/next/Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst new file mode 100644 index 00000000..baf59107 --- /dev/null +++ b/Misc/NEWS.d/next/Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst @@ -0,0 +1 @@ +importlib.metadata is no longer provisional. From 900837226489377dc9c0cb1635bd659cfa378b8e Mon Sep 17 00:00:00 2001 From: Philipp A Date: Mon, 18 Oct 2021 11:50:37 +0200 Subject: [PATCH 46/63] Fix docs source link for importlib.metadata --- Doc/library/importlib.metadata.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index c43457a3..1385615e 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -11,7 +11,7 @@ .. versionchanged:: 3.10 ``importlib.metadata`` is no longer provisional. -**Source code:** :source:`Lib/importlib/metadata.py` +**Source code:** :source:`Lib/importlib/metadata/__init__.py` ``importlib.metadata`` is a library that provides for access to installed package metadata. Built in part on Python's import system, this library From cec0421e8323f466785a4542eeec1a422a4f70aa Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Mon, 8 Nov 2021 19:52:26 -0500 Subject: [PATCH 47/63] Restore fallback when FastPath.root is empty. Fixes #353. --- Lib/importlib/metadata/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index f5172eef..b3e8fb05 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -748,7 +748,7 @@ def joinpath(self, child): def children(self): with suppress(Exception): - return os.listdir(self.root or '') + return os.listdir(self.root or '.') with suppress(Exception): return self.zip_children() return [] From 2f21af9672dc28928c0ee7b0134c0c10eee85801 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Nov 2021 08:58:46 -0500 Subject: [PATCH 48/63] Remove NEWS entry --- .../next/Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst | 1 - 1 file changed, 1 deletion(-) delete mode 100644 Misc/NEWS.d/next/Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst diff --git a/Misc/NEWS.d/next/Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst b/Misc/NEWS.d/next/Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst deleted file mode 100644 index baf59107..00000000 --- a/Misc/NEWS.d/next/Documentation/2021-07-12-11-39-20.bpo-44613.DIXNzc.rst +++ /dev/null @@ -1 +0,0 @@ -importlib.metadata is no longer provisional. From 052dc99655d0db784a97f72c8775a026d2470788 Mon Sep 17 00:00:00 2001 From: Christian Clauss Date: Tue, 5 Oct 2021 23:48:44 +0200 Subject: [PATCH 49/63] [doc] Fix typos found using codespell (GH-28744) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Ɓukasz Langa --- Doc/library/importlib.metadata.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 1385615e..99bcfeb2 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -255,7 +255,7 @@ function:: Package distributions --------------------- -A convience method to resolve the distribution or +A convenience method to resolve the distribution or distributions (in the case of a namespace package) for top-level Python packages or modules:: From 01dfc467a170acfc59d3f51d6f301a068b8a1e2f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Tue, 9 Nov 2021 09:13:32 -0500 Subject: [PATCH 50/63] Remove redundant import --- Lib/importlib/metadata/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index b3e8fb05..6347d538 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -15,7 +15,6 @@ import collections from . import _adapters, _meta -from ._meta import PackageMetadata from ._collections import FreezableDefaultDict, Pair from ._functools import method_cache from ._itertools import unique_everseen From f861da0a2fa4bfa63d4e94efe8b883ef74ff5399 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 3 Apr 2022 11:07:09 -0400 Subject: [PATCH 51/63] Revert "bpo-43672: raise ImportWarning when calling find_loader() (GH-25119)" This reverts commit 63e65f346453b74fe2de76ba9e766d73160610d0. Ref python/importlib_metadata#302. --- Lib/test/test_importlib/test_main.py | 36 +++++++++++++------- Lib/test/test_importlib/test_metadata_api.py | 32 ++++++++++------- 2 files changed, 44 insertions(+), 24 deletions(-) diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/test_main.py index 2e120f7a..c80a0e4a 100644 --- a/Lib/test/test_importlib/test_main.py +++ b/Lib/test/test_importlib/test_main.py @@ -37,10 +37,12 @@ def test_for_name_does_not_exist(self): Distribution.from_name('does-not-exist') def test_package_not_found_mentions_metadata(self): - # When a package is not found, that could indicate that the - # packgae is not installed or that it is installed without - # metadata. Ensure the exception mentions metadata to help - # guide users toward the cause. See #124. + """ + When a package is not found, that could indicate that the + packgae is not installed or that it is installed without + metadata. Ensure the exception mentions metadata to help + guide users toward the cause. See #124. + """ with self.assertRaises(PackageNotFoundError) as ctx: Distribution.from_name('does-not-exist') @@ -89,8 +91,10 @@ def pkg_with_dashes(site_dir): return 'my-pkg' def test_dashes_in_dist_name_found_as_underscores(self): - # For a package with a dash in the name, the dist-info metadata - # uses underscores in the name. Ensure the metadata loads. + """ + For a package with a dash in the name, the dist-info metadata + uses underscores in the name. Ensure the metadata loads. + """ pkg_name = self.pkg_with_dashes(self.site_dir) assert version(pkg_name) == '1.0' @@ -108,7 +112,9 @@ def pkg_with_mixed_case(site_dir): return 'CherryPy' def test_dist_name_found_as_any_case(self): - # Ensure the metadata loads when queried with any case. + """ + Ensure the metadata loads when queried with any case. + """ pkg_name = self.pkg_with_mixed_case(self.site_dir) assert version(pkg_name) == '1.0' assert version(pkg_name.lower()) == '1.0' @@ -244,11 +250,13 @@ def test_repr(self): assert "'name'" in repr(self.ep) def test_hashable(self): - # EntryPoints should be hashable. + """EntryPoints should be hashable""" hash(self.ep) def test_json_dump(self): - # json should not expect to be able to dump an EntryPoint. + """ + json should not expect to be able to dump an EntryPoint + """ with self.assertRaises(Exception): with warnings.catch_warnings(record=True): json.dumps(self.ep) @@ -260,7 +268,9 @@ def test_attr(self): assert self.ep.attr is None def test_sortable(self): - # EntryPoint objects are sortable, but result is undefined. + """ + EntryPoint objects are sortable, but result is undefined. + """ sorted( [ EntryPoint(name='b', value='val', group='group'), @@ -273,8 +283,10 @@ class FileSystem( fixtures.OnSysPath, fixtures.SiteDir, fixtures.FileBuilder, unittest.TestCase ): def test_unicode_dir_on_sys_path(self): - # Ensure a Unicode subdirectory of a directory on sys.path - # does not crash. + """ + Ensure a Unicode subdirectory of a directory on sys.path + does not crash. + """ fixtures.build_files( {self.unicode_filename(): {}}, prefix=self.site_dir, diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py index b3627cbb..c86bb4df 100644 --- a/Lib/test/test_importlib/test_metadata_api.py +++ b/Lib/test/test_importlib/test_metadata_api.py @@ -90,8 +90,10 @@ def test_entry_points_distribution(self): self.assertEqual(ep.dist.version, "1.0.0") def test_entry_points_unique_packages(self): - # Entry points should only be exposed for the first package - # on sys.path with a given name. + """ + Entry points should only be exposed for the first package + on sys.path with a given name. + """ alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) alt_pkg = { @@ -123,9 +125,11 @@ def test_entry_points_missing_group(self): assert entry_points(group='missing') == () def test_entry_points_dict_construction(self): - # Prior versions of entry_points() returned simple lists and - # allowed casting those lists into maps by name using ``dict()``. - # Capture this now deprecated use-case. + """ + Prior versions of entry_points() returned simple lists and + allowed casting those lists into maps by name using ``dict()``. + Capture this now deprecated use-case. + """ with suppress_known_deprecation() as caught: eps = dict(entry_points(group='entries')) @@ -154,9 +158,11 @@ def test_entry_points_by_index(self): assert "Accessing entry points by index is deprecated" in str(expected) def test_entry_points_groups_getitem(self): - # Prior versions of entry_points() returned a dict. Ensure - # that callers using '.__getitem__()' are supported but warned to - # migrate. + """ + Prior versions of entry_points() returned a dict. Ensure + that callers using '.__getitem__()' are supported but warned to + migrate. + """ with suppress_known_deprecation(): entry_points()['entries'] == entry_points(group='entries') @@ -164,9 +170,11 @@ def test_entry_points_groups_getitem(self): entry_points()['missing'] def test_entry_points_groups_get(self): - # Prior versions of entry_points() returned a dict. Ensure - # that callers using '.get()' are supported but warned to - # migrate. + """ + Prior versions of entry_points() returned a dict. Ensure + that callers using '.get()' are supported but warned to + migrate. + """ with suppress_known_deprecation(): entry_points().get('missing', 'default') == 'default' entry_points().get('entries', 'default') == entry_points()['entries'] @@ -313,7 +321,7 @@ def test_find_distributions_specified_path(self): assert any(dist.metadata['Name'] == 'distinfo-pkg' for dist in dists) def test_distribution_at_pathlib(self): - # Demonstrate how to load metadata direct from a directory. + """Demonstrate how to load metadata direct from a directory.""" dist_info_path = self.site_dir / 'distinfo_pkg-1.0.0.dist-info' dist = Distribution.at(dist_info_path) assert dist.version == '1.0.0' From 7ebf336c153ec914d9c676b2c5a31a8d31cd20a9 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 25 Jun 2022 13:43:21 -0400 Subject: [PATCH 52/63] Replace importlib_metadata with importlib.metadata. --- Doc/library/importlib.metadata.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 4452e281..3527115f 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -315,8 +315,8 @@ Distribution Discovery By default, this package provides built-in support for discovery of metadata for file system and zip file packages. This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular: -- ``importlib_metadata`` does not honor :class:`bytes` objects on ``sys.path``. -- ``importlib_metadata`` will incidentally honor :py:class:`pathlib.Path` objects on ``sys.path`` even though such values will be ignored for imports. +- ``importlib.metadata`` does not honor :class:`bytes` objects on ``sys.path``. +- ``importlib.metadata`` will incidentally honor :py:class:`pathlib.Path` objects on ``sys.path`` even though such values will be ignored for imports. Extending the search algorithm From aa2cbe438b1f8f18380a5a0ddf08dc8cb047a873 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Oct 2022 13:42:09 -0400 Subject: [PATCH 53/63] Update references to match upstream --- Doc/library/importlib.metadata.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index d7f6353a..27dfb439 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -396,4 +396,4 @@ a custom finder, return instances of this derived ``Distribution`` in the .. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api -.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html +.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders From 5cf4adbedc185bd4919e93cbd026419236f2f1fc Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Oct 2022 13:49:47 -0400 Subject: [PATCH 54/63] Hard-code links to packaging docs until intersphinx is supported. Ref python/cpython#97785. --- Doc/library/importlib.metadata.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 9dee6748..094c2688 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -14,9 +14,9 @@ **Source code:** :source:`Lib/importlib/metadata/__init__.py` ``importlib_metadata`` is a library that provides access to -the metadata of an installed :term:`packaging:Distribution Package`, +the metadata of an installed `Distribution Package `_, such as its entry points -or its top-level names (:term:`packaging:Import Package`\s, modules, if any). +or its top-level names (`Import Package `_\s, modules, if any). Built in part on Python's import system, this library intends to replace similar functionality in the `entry point API`_ and `metadata API`_ of ``pkg_resources``. Along with @@ -29,7 +29,7 @@ installed into Python's ``site-packages`` directory via tools such as `pip `_. Specifically, it works with distributions with discoverable ``dist-info`` or ``egg-info`` directories, -and metadata defined by the :ref:`packaging:core-metadata`. +and metadata defined by the `Core metadata specifications `_. .. important:: @@ -67,7 +67,7 @@ Overview ======== Let's say you wanted to get the version string for a -:term:`packaging:Distribution Package` you've installed +`Distribution Package `_ you've installed using ``pip``. We start by creating a virtual environment and installing something into it: @@ -187,7 +187,7 @@ for compatibility options. Distribution metadata --------------------- -Every :term:`packaging:Distribution Package` includes some metadata, +Every `Distribution Package `_ includes some metadata, which you can extract using the ``metadata()`` function:: @@ -227,7 +227,7 @@ Distribution versions --------------------- The ``version()`` function is the quickest way to get a -:term:`packaging:Distribution Package`'s version +`Distribution Package `_'s version number, as a string:: >>> version('wheel') # doctest: +SKIP @@ -240,7 +240,7 @@ Distribution files ------------------ You can also get the full set of files contained within a distribution. The -``files()`` function takes a :term:`packaging:Distribution Package` name +``files()`` function takes a `Distribution Package `_ name and returns all of the files installed by this distribution. Each file object returned is a ``PackagePath``, a :class:`pathlib.PurePath` derived object with additional ``dist``, @@ -286,7 +286,7 @@ distribution is not known to have the metadata present. Distribution requirements ------------------------- -To get the full set of requirements for a :term:`packaging:Distribution Package`, +To get the full set of requirements for a `Distribution Package `_, use the ``requires()`` function:: @@ -300,10 +300,10 @@ function:: Mapping import to distribution packages --------------------------------------- -A convenience method to resolve the :term:`packaging:Distribution Package` +A convenience method to resolve the `Distribution Package `_ name (or names, in the case of a namespace package) that provide each importable top-level -Python module or :term:`packaging:Import Package`:: +Python module or `Import Package `_:: >>> packages_distributions() {'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...} @@ -318,7 +318,7 @@ Distributions While the above API is the most common and convenient usage, you can get all of that information from the ``Distribution`` class. A ``Distribution`` is an abstract object that represents the metadata for -a Python :term:`packaging:Distribution Package`. You can +a Python `Distribution Package `_. You can get the ``Distribution`` instance:: >>> from importlib.metadata import distribution # doctest: +SKIP @@ -339,14 +339,14 @@ instance:: 'MIT' The full set of available metadata is not described here. -See the :ref:`packaging:core-metadata` for additional details. +See the `Core metadata specifications `_ for additional details. Distribution Discovery ====================== By default, this package provides built-in support for discovery of metadata -for file system and zip file :term:`packaging:Distribution Package`\s. +for file system and zip file `Distribution Package `_\s. This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular: - ``importlib.metadata`` does not honor :class:`bytes` objects on ``sys.path``. @@ -356,7 +356,7 @@ This metadata finder search defaults to ``sys.path``, but varies slightly in how Extending the search algorithm ============================== -Because :term:`packaging:Distribution Package` metadata +Because `Distribution Package `_ metadata is not available through :data:`sys.path` searches, or package loaders directly, the metadata for a distribution is found through import From 15e15e36bcbbf35e81b7691454d007590a9a6cb0 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 5 Oct 2022 15:08:07 -0400 Subject: [PATCH 55/63] Fix warning in plural of Import Package. --- Doc/library/importlib.metadata.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 1d3a2bef..150be816 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -16,7 +16,7 @@ ``importlib_metadata`` is a library that provides access to the metadata of an installed :term:`packaging:Distribution Package`, such as its entry points -or its top-level names (:term:`packaging:Import Package`s, modules, if any). +or its top-level names (:term:`packaging:Import Package`\ s, modules, if any). Built in part on Python's import system, this library intends to replace similar functionality in the `entry point API`_ and `metadata API`_ of ``pkg_resources``. Along with From 386479bfc4477f653df0aaa39b3d303417e8d719 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Oct 2022 13:46:44 -0400 Subject: [PATCH 56/63] Correct syntax is without the space. --- Doc/library/importlib.metadata.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 150be816..7c01cc55 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -16,7 +16,7 @@ ``importlib_metadata`` is a library that provides access to the metadata of an installed :term:`packaging:Distribution Package`, such as its entry points -or its top-level names (:term:`packaging:Import Package`\ s, modules, if any). +or its top-level names (:term:`packaging:Import Package`\s, modules, if any). Built in part on Python's import system, this library intends to replace similar functionality in the `entry point API`_ and `metadata API`_ of ``pkg_resources``. Along with From e8d8f9dcfbf14dac36358e54387f905c9f82c9be Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Oct 2022 13:49:47 -0400 Subject: [PATCH 57/63] Hard-code links to packaging docs until intersphinx is supported. Ref python/cpython#97785. --- Doc/library/importlib.metadata.rst | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 7c01cc55..0028d4b1 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -14,9 +14,9 @@ **Source code:** :source:`Lib/importlib/metadata/__init__.py` ``importlib_metadata`` is a library that provides access to -the metadata of an installed :term:`packaging:Distribution Package`, +the metadata of an installed `Distribution Package `_, such as its entry points -or its top-level names (:term:`packaging:Import Package`\s, modules, if any). +or its top-level names (`Import Package `_\s, modules, if any). Built in part on Python's import system, this library intends to replace similar functionality in the `entry point API`_ and `metadata API`_ of ``pkg_resources``. Along with @@ -29,7 +29,7 @@ installed into Python's ``site-packages`` directory via tools such as `pip `_. Specifically, it works with distributions with discoverable ``dist-info`` or ``egg-info`` directories, -and metadata defined by the :ref:`packaging:core-metadata`. +and metadata defined by the `Core metadata specifications `_. .. important:: @@ -67,7 +67,7 @@ Overview ======== Let's say you wanted to get the version string for a -:term:`packaging:Distribution Package` you've installed +`Distribution Package `_ you've installed using ``pip``. We start by creating a virtual environment and installing something into it: @@ -188,7 +188,7 @@ interface to retrieve entry points by group. Distribution metadata --------------------- -Every :term:`packaging:Distribution Package` includes some metadata, +Every `Distribution Package `_ includes some metadata, which you can extract using the ``metadata()`` function:: @@ -228,7 +228,7 @@ Distribution versions --------------------- The ``version()`` function is the quickest way to get a -:term:`packaging:Distribution Package`'s version +`Distribution Package `_'s version number, as a string:: >>> version('wheel') # doctest: +SKIP @@ -241,7 +241,7 @@ Distribution files ------------------ You can also get the full set of files contained within a distribution. The -``files()`` function takes a :term:`packaging:Distribution Package` name +``files()`` function takes a `Distribution Package `_ name and returns all of the files installed by this distribution. Each file object returned is a ``PackagePath``, a :class:`pathlib.PurePath` derived object with additional ``dist``, @@ -287,7 +287,7 @@ distribution is not known to have the metadata present. Distribution requirements ------------------------- -To get the full set of requirements for a :term:`packaging:Distribution Package`, +To get the full set of requirements for a `Distribution Package `_, use the ``requires()`` function:: @@ -301,10 +301,10 @@ function:: Mapping import to distribution packages --------------------------------------- -A convenience method to resolve the :term:`packaging:Distribution Package` +A convenience method to resolve the `Distribution Package `_ name (or names, in the case of a namespace package) that provide each importable top-level -Python module or :term:`packaging:Import Package`:: +Python module or `Import Package `_:: >>> packages_distributions() {'importlib_metadata': ['importlib-metadata'], 'yaml': ['PyYAML'], 'jaraco': ['jaraco.classes', 'jaraco.functools'], ...} @@ -319,7 +319,7 @@ Distributions While the above API is the most common and convenient usage, you can get all of that information from the ``Distribution`` class. A ``Distribution`` is an abstract object that represents the metadata for -a Python :term:`packaging:Distribution Package`. You can +a Python `Distribution Package `_. You can get the ``Distribution`` instance:: >>> from importlib.metadata import distribution # doctest: +SKIP @@ -340,14 +340,14 @@ instance:: 'MIT' The full set of available metadata is not described here. -See the :ref:`packaging:core-metadata` for additional details. +See the `Core metadata specifications `_ for additional details. Distribution Discovery ====================== By default, this package provides built-in support for discovery of metadata -for file system and zip file :term:`packaging:Distribution Package`\s. +for file system and zip file `Distribution Package `_\s. This metadata finder search defaults to ``sys.path``, but varies slightly in how it interprets those values from how other import machinery does. In particular: - ``importlib.metadata`` does not honor :class:`bytes` objects on ``sys.path``. @@ -357,7 +357,7 @@ This metadata finder search defaults to ``sys.path``, but varies slightly in how Extending the search algorithm ============================== -Because :term:`packaging:Distribution Package` metadata +Because `Distribution Package `_ metadata is not available through :data:`sys.path` searches, or package loaders directly, the metadata for a distribution is found through import From c3a60e65baca1e7f2a324272946f60f23614708a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Thu, 6 Oct 2022 13:42:09 -0400 Subject: [PATCH 58/63] Update references to match upstream --- Doc/library/importlib.metadata.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index 8ede0091..57991779 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -397,4 +397,4 @@ a custom finder, return instances of this derived ``Distribution`` in the .. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api -.. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html +.. _`finders`: https://docs.python.org/3/reference/import.html#finders-and-loaders From 996308821ba485d889146c1605a547de5505cc90 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 5 Nov 2022 11:13:43 -0400 Subject: [PATCH 59/63] Apply changes as found downstream in Python 3.10.8. --- Doc/library/importlib.metadata.rst | 10 ++-- Lib/importlib/metadata/__init__.py | 54 +++++++++++++++++--- Lib/test/test_importlib/test_metadata_api.py | 17 ++++++ 3 files changed, 67 insertions(+), 14 deletions(-) diff --git a/Doc/library/importlib.metadata.rst b/Doc/library/importlib.metadata.rst index c43457a3..c4c4a7db 100644 --- a/Doc/library/importlib.metadata.rst +++ b/Doc/library/importlib.metadata.rst @@ -11,7 +11,7 @@ .. versionchanged:: 3.10 ``importlib.metadata`` is no longer provisional. -**Source code:** :source:`Lib/importlib/metadata.py` +**Source code:** :source:`Lib/importlib/metadata/__init__.py` ``importlib.metadata`` is a library that provides for access to installed package metadata. Built in part on Python's import system, this library @@ -136,7 +136,7 @@ Inspect the resolved entry point:: The ``group`` and ``name`` are arbitrary values defined by the package author and usually a client will wish to resolve all entry points for a particular group. Read `the setuptools docs -`_ +`_ for more information on entry points, their definition, and usage. *Compatibility Note* @@ -255,7 +255,7 @@ function:: Package distributions --------------------- -A convience method to resolve the distribution or +A convenience method to resolve the distribution or distributions (in the case of a namespace package) for top-level Python packages or modules:: @@ -264,6 +264,7 @@ Python packages or modules:: .. versionadded:: 3.10 +.. _distributions: Distributions ============= @@ -335,6 +336,3 @@ a custom finder, return instances of this derived ``Distribution`` in the .. _`entry point API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#entry-points .. _`metadata API`: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#metadata-api .. _`importlib_resources`: https://importlib-resources.readthedocs.io/en/latest/index.html - - -.. rubric:: Footnotes diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index f5172eef..b3063cd9 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -128,12 +128,21 @@ class EntryPoint( See `the packaging docs on entry points `_ for more information. + + >>> ep = EntryPoint( + ... name=None, group=None, value='package.module:attr [extra1, extra2]') + >>> ep.module + 'package.module' + >>> ep.attr + 'attr' + >>> ep.extras + ['extra1', 'extra2'] """ pattern = re.compile( r'(?P[\w.]+)\s*' - r'(:\s*(?P[\w.]+))?\s*' - r'(?P\[.*\])?\s*$' + r'(:\s*(?P[\w.]+)\s*)?' + r'((?P\[.*\])\s*)?$' ) """ A regular expression describing the syntax for an entry point, @@ -176,7 +185,7 @@ def attr(self): @property def extras(self): match = self.pattern.match(self.value) - return list(re.finditer(r'\w+', match.group('extras') or '')) + return re.findall(r'\w+', match.group('extras') or '') def _for(self, dist): self.dist = dist @@ -200,6 +209,25 @@ def __reduce__(self): ) def matches(self, **params): + """ + EntryPoint matches the given parameters. + + >>> ep = EntryPoint(group='foo', name='bar', value='bing:bong [extra1, extra2]') + >>> ep.matches(group='foo') + True + >>> ep.matches(name='bar', value='bing:bong [extra1, extra2]') + True + >>> ep.matches(group='foo', name='other') + False + >>> ep.matches() + True + >>> ep.matches(extras=['extra1', 'extra2']) + True + >>> ep.matches(module='bing') + True + >>> ep.matches(attr='bong') + True + """ attrs = (getattr(self, param) for param in params) return all(map(operator.eq, params.values(), attrs)) @@ -236,6 +264,8 @@ class DeprecatedList(list): 1 """ + __slots__ = () + _warn = functools.partial( warnings.warn, "EntryPoints list interface is deprecated. Cast to list if needed.", @@ -648,7 +678,7 @@ def _read_dist_info_reqs(self): def _read_egg_info_reqs(self): source = self.read_text('requires.txt') - return source and self._deps_from_requires_text(source) + return None if source is None else self._deps_from_requires_text(source) @classmethod def _deps_from_requires_text(cls, source): @@ -669,7 +699,7 @@ def _convert_egg_info_reqs_to_simple_reqs(sections): def make_condition(name): return name and f'extra == "{name}"' - def parse_condition(section): + def quoted_marker(section): section = section or '' extra, sep, markers = section.partition(':') if extra and markers: @@ -677,8 +707,17 @@ def parse_condition(section): conditions = list(filter(None, [markers, make_condition(extra)])) return '; ' + ' and '.join(conditions) if conditions else '' + def url_req_space(req): + """ + PEP 508 requires a space between the url_spec and the quoted_marker. + Ref python/importlib_metadata#357. + """ + # '@' is uniquely indicative of a url_req. + return ' ' * ('@' in req) + for section in sections: - yield section.value + parse_condition(section.name) + space = url_req_space(section.value) + yield section.value + space + quoted_marker(section.name) class DistributionFinder(MetaPathFinder): @@ -741,14 +780,13 @@ def __new__(cls, root): def __init__(self, root): self.root = root - self.base = os.path.basename(self.root).lower() def joinpath(self, child): return pathlib.Path(self.root, child) def children(self): with suppress(Exception): - return os.listdir(self.root or '') + return os.listdir(self.root or '.') with suppress(Exception): return self.zip_children() return [] diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py index dc9c234d..799f0070 100644 --- a/Lib/test/test_importlib/test_metadata_api.py +++ b/Lib/test/test_importlib/test_metadata_api.py @@ -172,6 +172,11 @@ def test_entry_points_groups_get(self): entry_points().get('entries', 'default') == entry_points()['entries'] entry_points().get('missing', ()) == () + def test_entry_points_allows_no_attributes(self): + ep = entry_points().select(group='entries', name='main') + with self.assertRaises(AttributeError): + ep.foo = 4 + def test_metadata_for_this_package(self): md = metadata('egginfo-pkg') assert md['author'] == 'Steven Ma' @@ -217,6 +222,16 @@ def test_requires_egg_info(self): assert len(deps) == 2 assert any(dep == 'wheel >= 1.0; python_version >= "2.7"' for dep in deps) + def test_requires_egg_info_empty(self): + fixtures.build_files( + { + 'requires.txt': '', + }, + self.site_dir.joinpath('egginfo_pkg.egg-info'), + ) + deps = requires('egginfo-pkg') + assert deps == [] + def test_requires_dist_info(self): deps = requires('distinfo-pkg') assert len(deps) == 2 @@ -235,6 +250,7 @@ def test_more_complex_deps_requires_text(self): [extra1] dep4 + dep6@ git+https://example.com/python/dep.git@v1.0.0 [extra2:python_version < "3"] dep5 @@ -247,6 +263,7 @@ def test_more_complex_deps_requires_text(self): 'dep3; python_version < "3"', 'dep4; extra == "extra1"', 'dep5; (python_version < "3") and extra == "extra2"', + 'dep6@ git+https://example.com/python/dep.git@v1.0.0 ; extra == "extra1"', ] # It's important that the environment marker expression be # wrapped in parentheses to avoid the following 'and' binding more From 648c868e2bf83b6e23fb361108d3c796857c555d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:02:35 -0400 Subject: [PATCH 60/63] Merge pull request #379 from python/bugfix/egg-on-face Fix filesystem path distribution name inference parsing. --- Lib/importlib/metadata/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index b3063cd9..b70824e6 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -940,11 +940,20 @@ def _normalized_name(self): stem = os.path.basename(str(self._path)) return self._name_from_stem(stem) or super()._normalized_name - def _name_from_stem(self, stem): - name, ext = os.path.splitext(stem) + @staticmethod + def _name_from_stem(stem): + """ + >>> PathDistribution._name_from_stem('foo-3.0.egg-info') + 'foo' + >>> PathDistribution._name_from_stem('CherryPy-3.0.dist-info') + 'CherryPy' + >>> PathDistribution._name_from_stem('face.egg-info') + 'face' + """ + filename, ext = os.path.splitext(stem) if ext not in ('.dist-info', '.egg-info'): return - name, sep, rest = stem.partition('-') + name, sep, rest = filename.partition('-') return name From 1f46e76d95da380282032f88fe92e6e036767d3a Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sat, 21 May 2022 11:42:38 -0400 Subject: [PATCH 61/63] Merge pull request #381 from python/bugfix/entry-points-unique-normalized Fix expectation on unique packages to include normalization. --- Lib/importlib/metadata/__init__.py | 5 ++++- Lib/test/test_importlib/test_metadata_api.py | 10 ++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index b70824e6..44fe61f2 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -938,7 +938,10 @@ def _normalized_name(self): normalized name from the file system path. """ stem = os.path.basename(str(self._path)) - return self._name_from_stem(stem) or super()._normalized_name + return ( + pass_none(Prepared.normalize)(self._name_from_stem(stem)) + or super()._normalized_name + ) @staticmethod def _name_from_stem(stem): diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/test_metadata_api.py index 799f0070..ab482817 100644 --- a/Lib/test/test_importlib/test_metadata_api.py +++ b/Lib/test/test_importlib/test_metadata_api.py @@ -89,13 +89,15 @@ def test_entry_points_distribution(self): self.assertIn(ep.dist.name, ('distinfo-pkg', 'egginfo-pkg')) self.assertEqual(ep.dist.version, "1.0.0") - def test_entry_points_unique_packages(self): - # Entry points should only be exposed for the first package - # on sys.path with a given name. + def test_entry_points_unique_packages_normalized(self): + """ + Entry points should only be exposed for the first package + on sys.path with a given name (even when normalized). + """ alt_site_dir = self.fixtures.enter_context(fixtures.tempdir()) self.fixtures.enter_context(self.add_sys_path(alt_site_dir)) alt_pkg = { - "distinfo_pkg-1.1.0.dist-info": { + "DistInfo_pkg-1.1.0.dist-info": { "METADATA": """ Name: distinfo-pkg Version: 1.1.0 From ee91f897da4205630d73f77f31152682fc388e79 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 29 Aug 2021 14:31:15 -0400 Subject: [PATCH 62/63] Add pass_none from python/importlib_metadata@1e19fd2c52. --- Lib/importlib/metadata/__init__.py | 2 +- Lib/importlib/metadata/_functools.py | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/Lib/importlib/metadata/__init__.py b/Lib/importlib/metadata/__init__.py index 44fe61f2..682067d3 100644 --- a/Lib/importlib/metadata/__init__.py +++ b/Lib/importlib/metadata/__init__.py @@ -17,7 +17,7 @@ from . import _adapters, _meta from ._meta import PackageMetadata from ._collections import FreezableDefaultDict, Pair -from ._functools import method_cache +from ._functools import method_cache, pass_none from ._itertools import unique_everseen from ._meta import PackageMetadata, SimplePath diff --git a/Lib/importlib/metadata/_functools.py b/Lib/importlib/metadata/_functools.py index 73f50d00..71f66bd0 100644 --- a/Lib/importlib/metadata/_functools.py +++ b/Lib/importlib/metadata/_functools.py @@ -83,3 +83,22 @@ def wrapper(self, *args, **kwargs): wrapper.cache_clear = lambda: None return wrapper + + +# From jaraco.functools 3.3 +def pass_none(func): + """ + Wrap func so it's not called if its first param is None + + >>> print_text = pass_none(print) + >>> print_text('text') + text + >>> print_text(None) + """ + + @functools.wraps(func) + def wrapper(param, *args, **kwargs): + if param is not None: + return func(param, *args, **kwargs) + + return wrapper From 68c680122a86920e11e748a4fa476bf30f172277 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Wed, 20 Mar 2024 12:49:27 -0400 Subject: [PATCH 63/63] Move tests into their own package. --- Lib/test/test_importlib/{ => metadata}/_context.py | 0 Lib/test/test_importlib/{ => metadata}/_path.py | 0 .../test_importlib/{ => metadata}/data/__init__.py | 0 .../data/example-21.12-py3-none-any.whl | Bin .../{ => metadata}/data/example-21.12-py3.6.egg | Bin .../data/example2-1.0.0-py3-none-any.whl | Bin Lib/test/test_importlib/{ => metadata}/fixtures.py | 0 Lib/test/test_importlib/{ => metadata}/stubs.py | 0 .../{test_metadata_api.py => metadata/test_api.py} | 0 Lib/test/test_importlib/{ => metadata}/test_main.py | 0 Lib/test/test_importlib/{ => metadata}/test_zip.py | 0 11 files changed, 0 insertions(+), 0 deletions(-) rename Lib/test/test_importlib/{ => metadata}/_context.py (100%) rename Lib/test/test_importlib/{ => metadata}/_path.py (100%) rename Lib/test/test_importlib/{ => metadata}/data/__init__.py (100%) rename Lib/test/test_importlib/{ => metadata}/data/example-21.12-py3-none-any.whl (100%) rename Lib/test/test_importlib/{ => metadata}/data/example-21.12-py3.6.egg (100%) rename Lib/test/test_importlib/{ => metadata}/data/example2-1.0.0-py3-none-any.whl (100%) rename Lib/test/test_importlib/{ => metadata}/fixtures.py (100%) rename Lib/test/test_importlib/{ => metadata}/stubs.py (100%) rename Lib/test/test_importlib/{test_metadata_api.py => metadata/test_api.py} (100%) rename Lib/test/test_importlib/{ => metadata}/test_main.py (100%) rename Lib/test/test_importlib/{ => metadata}/test_zip.py (100%) diff --git a/Lib/test/test_importlib/_context.py b/Lib/test/test_importlib/metadata/_context.py similarity index 100% rename from Lib/test/test_importlib/_context.py rename to Lib/test/test_importlib/metadata/_context.py diff --git a/Lib/test/test_importlib/_path.py b/Lib/test/test_importlib/metadata/_path.py similarity index 100% rename from Lib/test/test_importlib/_path.py rename to Lib/test/test_importlib/metadata/_path.py diff --git a/Lib/test/test_importlib/data/__init__.py b/Lib/test/test_importlib/metadata/data/__init__.py similarity index 100% rename from Lib/test/test_importlib/data/__init__.py rename to Lib/test/test_importlib/metadata/data/__init__.py diff --git a/Lib/test/test_importlib/data/example-21.12-py3-none-any.whl b/Lib/test/test_importlib/metadata/data/example-21.12-py3-none-any.whl similarity index 100% rename from Lib/test/test_importlib/data/example-21.12-py3-none-any.whl rename to Lib/test/test_importlib/metadata/data/example-21.12-py3-none-any.whl diff --git a/Lib/test/test_importlib/data/example-21.12-py3.6.egg b/Lib/test/test_importlib/metadata/data/example-21.12-py3.6.egg similarity index 100% rename from Lib/test/test_importlib/data/example-21.12-py3.6.egg rename to Lib/test/test_importlib/metadata/data/example-21.12-py3.6.egg diff --git a/Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl b/Lib/test/test_importlib/metadata/data/example2-1.0.0-py3-none-any.whl similarity index 100% rename from Lib/test/test_importlib/data/example2-1.0.0-py3-none-any.whl rename to Lib/test/test_importlib/metadata/data/example2-1.0.0-py3-none-any.whl diff --git a/Lib/test/test_importlib/fixtures.py b/Lib/test/test_importlib/metadata/fixtures.py similarity index 100% rename from Lib/test/test_importlib/fixtures.py rename to Lib/test/test_importlib/metadata/fixtures.py diff --git a/Lib/test/test_importlib/stubs.py b/Lib/test/test_importlib/metadata/stubs.py similarity index 100% rename from Lib/test/test_importlib/stubs.py rename to Lib/test/test_importlib/metadata/stubs.py diff --git a/Lib/test/test_importlib/test_metadata_api.py b/Lib/test/test_importlib/metadata/test_api.py similarity index 100% rename from Lib/test/test_importlib/test_metadata_api.py rename to Lib/test/test_importlib/metadata/test_api.py diff --git a/Lib/test/test_importlib/test_main.py b/Lib/test/test_importlib/metadata/test_main.py similarity index 100% rename from Lib/test/test_importlib/test_main.py rename to Lib/test/test_importlib/metadata/test_main.py diff --git a/Lib/test/test_importlib/test_zip.py b/Lib/test/test_importlib/metadata/test_zip.py similarity index 100% rename from Lib/test/test_importlib/test_zip.py rename to Lib/test/test_importlib/metadata/test_zip.py