summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/tools/third_party/pluggy
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 09:22:09 +0000
commit43a97878ce14b72f0981164f87f2e35e14151312 (patch)
tree620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/tools/third_party/pluggy
parentInitial commit. (diff)
downloadfirefox-upstream.tar.xz
firefox-upstream.zip
Adding upstream version 110.0.1.upstream/110.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'testing/web-platform/tests/tools/third_party/pluggy')
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/.coveragerc14
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/.github/workflows/main.yml148
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/.gitignore64
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/.pre-commit-config.yaml34
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/CHANGELOG.rst409
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/LICENSE21
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/MANIFEST.in7
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/README.rst101
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/RELEASING.rst23
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/changelog/README.rst32
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/changelog/_template.rst40
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/codecov.yml7
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/_static/img/plug.pngbin0 -> 9350 bytes
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/api_reference.rst19
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/changelog.rst1
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/conf.py87
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/eggsample_spam.py22
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py4
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py21
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py57
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/lib.py14
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py8
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/examples/toy-example.py41
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/docs/index.rst957
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/pyproject.toml47
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/scripts/release.py69
-rwxr-xr-xtesting/web-platform/tests/tools/third_party/pluggy/scripts/upload-coverage.sh16
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/setup.cfg52
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/setup.py5
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/__init__.py18
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_callers.py60
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_hooks.py325
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_manager.py373
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_result.py60
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_tracing.py62
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/benchmark.py102
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/conftest.py26
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_details.py135
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_helpers.py84
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_hookcaller.py272
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_invocations.py215
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_multicall.py147
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_pluginmanager.py544
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/testing/test_tracer.py78
-rw-r--r--testing/web-platform/tests/tools/third_party/pluggy/tox.ini57
46 files changed, 4886 insertions, 0 deletions
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/.coveragerc b/testing/web-platform/tests/tools/third_party/pluggy/.coveragerc
new file mode 100644
index 0000000000..1b1de1cd24
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/.coveragerc
@@ -0,0 +1,14 @@
+[run]
+include =
+ pluggy/*
+ testing/*
+ */lib/python*/site-packages/pluggy/*
+ */pypy*/site-packages/pluggy/*
+ *\Lib\site-packages\pluggy\*
+branch = 1
+
+[paths]
+source = pluggy/
+ */lib/python*/site-packages/pluggy/
+ */pypy*/site-packages/pluggy/
+ *\Lib\site-packages\pluggy\
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/.github/workflows/main.yml b/testing/web-platform/tests/tools/third_party/pluggy/.github/workflows/main.yml
new file mode 100644
index 0000000000..e1022ca96d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/.github/workflows/main.yml
@@ -0,0 +1,148 @@
+name: main
+
+on:
+ push:
+ branches:
+ - main
+ tags:
+ - "*"
+
+ pull_request:
+ branches:
+ - main
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+
+ strategy:
+ fail-fast: false
+ matrix:
+ name: [
+ "windows-py36",
+ "windows-py39",
+ "windows-pypy3",
+
+ "ubuntu-py36",
+ "ubuntu-py36-pytestmain",
+ "ubuntu-py37",
+ "ubuntu-py38",
+ "ubuntu-py39",
+ "ubuntu-pypy3",
+ "ubuntu-benchmark",
+
+ "linting",
+ "docs",
+ ]
+
+ include:
+ - name: "windows-py36"
+ python: "3.6"
+ os: windows-latest
+ tox_env: "py36"
+ - name: "windows-py39"
+ python: "3.9"
+ os: windows-latest
+ tox_env: "py39"
+ - name: "windows-pypy3"
+ python: "pypy3"
+ os: windows-latest
+ tox_env: "pypy3"
+ - name: "ubuntu-py36"
+ python: "3.6"
+ os: ubuntu-latest
+ tox_env: "py36"
+ use_coverage: true
+ - name: "ubuntu-py36-pytestmain"
+ python: "3.6"
+ os: ubuntu-latest
+ tox_env: "py36-pytestmain"
+ use_coverage: true
+ - name: "ubuntu-py37"
+ python: "3.7"
+ os: ubuntu-latest
+ tox_env: "py37"
+ use_coverage: true
+ - name: "ubuntu-py38"
+ python: "3.8"
+ os: ubuntu-latest
+ tox_env: "py38"
+ use_coverage: true
+ - name: "ubuntu-py39"
+ python: "3.9"
+ os: ubuntu-latest
+ tox_env: "py39"
+ use_coverage: true
+ - name: "ubuntu-pypy3"
+ python: "pypy3"
+ os: ubuntu-latest
+ tox_env: "pypy3"
+ use_coverage: true
+ - name: "ubuntu-benchmark"
+ python: "3.8"
+ os: ubuntu-latest
+ tox_env: "benchmark"
+ - name: "linting"
+ python: "3.8"
+ os: ubuntu-latest
+ tox_env: "linting"
+ - name: "docs"
+ python: "3.8"
+ os: ubuntu-latest
+ tox_env: "docs"
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - name: Set up Python ${{ matrix.python }}
+ uses: actions/setup-python@v2
+ with:
+ python-version: ${{ matrix.python }}
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip setuptools
+ python -m pip install tox coverage
+
+ - name: Test without coverage
+ if: "! matrix.use_coverage"
+ run: "tox -e ${{ matrix.tox_env }}"
+
+ - name: Test with coverage
+ if: "matrix.use_coverage"
+ run: "tox -e ${{ matrix.tox_env }}-coverage"
+
+ - name: Upload coverage
+ if: matrix.use_coverage && github.repository == 'pytest-dev/pluggy'
+ env:
+ CODECOV_NAME: ${{ matrix.name }}
+ run: bash scripts/upload-coverage.sh -F GHA,${{ runner.os }}
+
+ deploy:
+ if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags') && github.repository == 'pytest-dev/pluggy'
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+
+ - uses: actions/setup-python@v2
+ with:
+ python-version: "3.8"
+
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip
+ pip install --upgrade wheel setuptools setuptools_scm
+
+ - name: Build package
+ run: python setup.py sdist bdist_wheel
+
+ - name: Publish package
+ uses: pypa/gh-action-pypi-publish@v1.4.1
+ with:
+ user: __token__
+ password: ${{ secrets.pypi_token }}
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/.gitignore b/testing/web-platform/tests/tools/third_party/pluggy/.gitignore
new file mode 100644
index 0000000000..4580536c7a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/.gitignore
@@ -0,0 +1,64 @@
+# Byte-compiled / optimized / DLL files
+__pycache__/
+*.py[cod]
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env/
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+*.egg-info/
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+target/
+*.swp
+
+# generated by setuptools_scm
+src/pluggy/_version.py
+
+# generated by pip
+pip-wheel-metadata/
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/.pre-commit-config.yaml b/testing/web-platform/tests/tools/third_party/pluggy/.pre-commit-config.yaml
new file mode 100644
index 0000000000..d919ffeb2f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/.pre-commit-config.yaml
@@ -0,0 +1,34 @@
+repos:
+- repo: https://github.com/ambv/black
+ rev: 21.7b0
+ hooks:
+ - id: black
+ args: [--safe, --quiet]
+- repo: https://github.com/asottile/blacken-docs
+ rev: v1.10.0
+ hooks:
+ - id: blacken-docs
+ additional_dependencies: [black==21.7b0]
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v2.1.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: flake8
+- repo: local
+ hooks:
+ - id: rst
+ name: rst
+ entry: rst-lint --encoding utf-8
+ files: ^(CHANGELOG.rst|HOWTORELEASE.rst|README.rst|changelog/.*)$
+ language: python
+ additional_dependencies: [pygments, restructuredtext_lint]
+- repo: https://github.com/pre-commit/pygrep-hooks
+ rev: v1.9.0
+ hooks:
+ - id: rst-backticks
+- repo: https://github.com/asottile/pyupgrade
+ rev: v2.23.3
+ hooks:
+ - id: pyupgrade
+ args: [--py36-plus]
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/CHANGELOG.rst b/testing/web-platform/tests/tools/third_party/pluggy/CHANGELOG.rst
new file mode 100644
index 0000000000..13a388c435
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/CHANGELOG.rst
@@ -0,0 +1,409 @@
+=========
+Changelog
+=========
+
+.. towncrier release notes start
+
+pluggy 1.0.0 (2021-08-25)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- `#116 <https://github.com/pytest-dev/pluggy/issues/116>`_: Remove deprecated ``implprefix`` support.
+ Decorate hook implementations using an instance of HookimplMarker instead.
+ The deprecation was announced in release ``0.7.0``.
+
+
+- `#120 <https://github.com/pytest-dev/pluggy/issues/120>`_: Remove the deprecated ``proc`` argument to ``call_historic``.
+ Use ``result_callback`` instead, which has the same behavior.
+ The deprecation was announced in release ``0.7.0``.
+
+
+- `#265 <https://github.com/pytest-dev/pluggy/issues/265>`_: Remove the ``_Result.result`` property. Use ``_Result.get_result()`` instead.
+ Note that unlike ``result``, ``get_result()`` raises the exception if the hook raised.
+ The deprecation was announced in release ``0.6.0``.
+
+
+- `#267 <https://github.com/pytest-dev/pluggy/issues/267>`_: Remove official support for Python 3.4.
+
+
+- `#272 <https://github.com/pytest-dev/pluggy/issues/272>`_: Dropped support for Python 2.
+ Continue to use pluggy 0.13.x for Python 2 support.
+
+
+- `#308 <https://github.com/pytest-dev/pluggy/issues/308>`_: Remove official support for Python 3.5.
+
+
+- `#313 <https://github.com/pytest-dev/pluggy/issues/313>`_: The internal ``pluggy.callers``, ``pluggy.manager`` and ``pluggy.hooks`` are now explicitly marked private by a ``_`` prefix (e.g. ``pluggy._callers``).
+ Only API exported by the top-level ``pluggy`` module is considered public.
+
+
+- `#59 <https://github.com/pytest-dev/pluggy/issues/59>`_: Remove legacy ``__multicall__`` recursive hook calling system.
+ The deprecation was announced in release ``0.5.0``.
+
+
+
+Features
+--------
+
+- `#282 <https://github.com/pytest-dev/pluggy/issues/282>`_: When registering a hookimpl which is declared as ``hookwrapper=True`` but whose
+ function is not a generator function, a ``PluggyValidationError`` exception is
+ now raised.
+
+ Previously this problem would cause an error only later, when calling the hook.
+
+ In the unlikely case that you have a hookwrapper that *returns* a generator
+ instead of yielding directly, for example:
+
+ .. code-block:: python
+
+ def my_hook_real_implementation(arg):
+ print("before")
+ yield
+ print("after")
+
+
+ @hookimpl(hookwrapper=True)
+ def my_hook(arg):
+ return my_hook_implementation(arg)
+
+ change it to use ``yield from`` instead:
+
+ .. code-block:: python
+
+ @hookimpl(hookwrapper=True)
+ def my_hook(arg):
+ yield from my_hook_implementation(arg)
+
+
+- `#309 <https://github.com/pytest-dev/pluggy/issues/309>`_: Add official support for Python 3.9.
+
+
+pluggy 0.13.1 (2019-11-21)
+==========================
+
+Trivial/Internal Changes
+------------------------
+
+- `#236 <https://github.com/pytest-dev/pluggy/pull/236>`_: Improved documentation, especially with regard to references.
+
+
+pluggy 0.13.0 (2019-09-10)
+==========================
+
+Trivial/Internal Changes
+------------------------
+
+- `#222 <https://github.com/pytest-dev/pluggy/issues/222>`_: Replace ``importlib_metadata`` backport with ``importlib.metadata`` from the
+ standard library on Python 3.8+.
+
+
+pluggy 0.12.0 (2019-05-27)
+==========================
+
+Features
+--------
+
+- `#215 <https://github.com/pytest-dev/pluggy/issues/215>`_: Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time. This time with ``.egg`` support.
+
+
+pluggy 0.11.0 (2019-05-07)
+==========================
+
+Bug Fixes
+---------
+
+- `#205 <https://github.com/pytest-dev/pluggy/issues/205>`_: Revert changes made in 0.10.0 release breaking ``.egg`` installs.
+
+
+pluggy 0.10.0 (2019-05-07)
+==========================
+
+Features
+--------
+
+- `#199 <https://github.com/pytest-dev/pluggy/issues/199>`_: Switch from ``pkg_resources`` to ``importlib-metadata`` for entrypoint detection for improved performance and import time.
+
+
+pluggy 0.9.0 (2019-02-21)
+=========================
+
+Features
+--------
+
+- `#189 <https://github.com/pytest-dev/pluggy/issues/189>`_: ``PluginManager.load_setuptools_entrypoints`` now accepts a ``name`` parameter that when given will
+ load only entry points with that name.
+
+ ``PluginManager.load_setuptools_entrypoints`` also now returns the number of plugins loaded by the
+ call, as opposed to the number of all plugins loaded by all calls to this method.
+
+
+
+Bug Fixes
+---------
+
+- `#187 <https://github.com/pytest-dev/pluggy/issues/187>`_: Fix internal ``varnames`` function for PyPy3.
+
+
+pluggy 0.8.1 (2018-11-09)
+=========================
+
+Trivial/Internal Changes
+------------------------
+
+- `#166 <https://github.com/pytest-dev/pluggy/issues/166>`_: Add ``stacklevel=2`` to implprefix warning so that the reported location of warning is the caller of PluginManager.
+
+
+pluggy 0.8.0 (2018-10-15)
+=========================
+
+Features
+--------
+
+- `#177 <https://github.com/pytest-dev/pluggy/issues/177>`_: Add ``get_hookimpls()`` method to hook callers.
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#165 <https://github.com/pytest-dev/pluggy/issues/165>`_: Add changelog in long package description and documentation.
+
+
+- `#172 <https://github.com/pytest-dev/pluggy/issues/172>`_: Add a test exemplifying the opt-in nature of spec defined args.
+
+
+- `#57 <https://github.com/pytest-dev/pluggy/issues/57>`_: Encapsulate hook specifications in a type for easier introspection.
+
+
+pluggy 0.7.1 (2018-07-28)
+=========================
+
+Deprecations and Removals
+-------------------------
+
+- `#116 <https://github.com/pytest-dev/pluggy/issues/116>`_: Deprecate the ``implprefix`` kwarg to ``PluginManager`` and instead
+ expect users to start using explicit ``HookimplMarker`` everywhere.
+
+
+
+Features
+--------
+
+- `#122 <https://github.com/pytest-dev/pluggy/issues/122>`_: Add ``.plugin`` member to ``PluginValidationError`` to access failing plugin during post-mortem.
+
+
+- `#138 <https://github.com/pytest-dev/pluggy/issues/138>`_: Add per implementation warnings support for hookspecs allowing for both
+ deprecation and future warnings of legacy and (future) experimental hooks
+ respectively.
+
+
+
+Bug Fixes
+---------
+
+- `#110 <https://github.com/pytest-dev/pluggy/issues/110>`_: Fix a bug where ``_HookCaller.call_historic()`` would call the ``proc``
+ arg even when the default is ``None`` resulting in a ``TypeError``.
+
+- `#160 <https://github.com/pytest-dev/pluggy/issues/160>`_: Fix problem when handling ``VersionConflict`` errors when loading setuptools plugins.
+
+
+
+Improved Documentation
+----------------------
+
+- `#123 <https://github.com/pytest-dev/pluggy/issues/123>`_: Document how exceptions are handled and how the hook call loop
+ terminates immediately on the first error which is then delivered
+ to any surrounding wrappers.
+
+
+- `#136 <https://github.com/pytest-dev/pluggy/issues/136>`_: Docs rework including a much better introduction and comprehensive example
+ set for new users. A big thanks goes out to @obestwalter for the great work!
+
+
+
+Trivial/Internal Changes
+------------------------
+
+- `#117 <https://github.com/pytest-dev/pluggy/issues/117>`_: Break up the main monolithic package modules into separate modules by concern
+
+
+- `#131 <https://github.com/pytest-dev/pluggy/issues/131>`_: Automate ``setuptools`` wheels building and PyPi upload using TravisCI.
+
+
+- `#153 <https://github.com/pytest-dev/pluggy/issues/153>`_: Reorganize tests more appropriately by modules relating to each
+ internal component/feature. This is in an effort to avoid (future)
+ duplication and better separation of concerns in the test set.
+
+
+- `#156 <https://github.com/pytest-dev/pluggy/issues/156>`_: Add ``HookImpl.__repr__()`` for better debugging.
+
+
+- `#66 <https://github.com/pytest-dev/pluggy/issues/66>`_: Start using ``towncrier`` and a custom ``tox`` environment to prepare releases!
+
+
+pluggy 0.7.0 (Unreleased)
+=========================
+
+* `#160 <https://github.com/pytest-dev/pluggy/issues/160>`_: We discovered a deployment issue so this version was never released to PyPI, only the tag exists.
+
+pluggy 0.6.0 (2017-11-24)
+=========================
+
+- Add CI testing for the features, release, and master
+ branches of ``pytest`` (PR `#79`_).
+- Document public API for ``_Result`` objects passed to wrappers
+ (PR `#85`_).
+- Document and test hook LIFO ordering (PR `#85`_).
+- Turn warnings into errors in test suite (PR `#89`_).
+- Deprecate ``_Result.result`` (PR `#88`_).
+- Convert ``_Multicall`` to a simple function distinguishing it from
+ the legacy version (PR `#90`_).
+- Resolve E741 errors (PR `#96`_).
+- Test and bug fix for unmarked hook collection (PRs `#97`_ and
+ `#102`_).
+- Drop support for EOL Python 2.6 and 3.3 (PR `#103`_).
+- Fix ``inspect`` based arg introspection on py3.6 (PR `#94`_).
+
+.. _#79: https://github.com/pytest-dev/pluggy/pull/79
+.. _#85: https://github.com/pytest-dev/pluggy/pull/85
+.. _#88: https://github.com/pytest-dev/pluggy/pull/88
+.. _#89: https://github.com/pytest-dev/pluggy/pull/89
+.. _#90: https://github.com/pytest-dev/pluggy/pull/90
+.. _#94: https://github.com/pytest-dev/pluggy/pull/94
+.. _#96: https://github.com/pytest-dev/pluggy/pull/96
+.. _#97: https://github.com/pytest-dev/pluggy/pull/97
+.. _#102: https://github.com/pytest-dev/pluggy/pull/102
+.. _#103: https://github.com/pytest-dev/pluggy/pull/103
+
+
+pluggy 0.5.2 (2017-09-06)
+=========================
+
+- fix bug where ``firstresult`` wrappers were being sent an incorrectly configured
+ ``_Result`` (a list was set instead of a single value). Add tests to check for
+ this as well as ``_Result.force_result()`` behaviour. Thanks to `@tgoodlet`_
+ for the PR `#72`_.
+
+- fix incorrect ``getattr`` of ``DeprecationWarning`` from the ``warnings``
+ module. Thanks to `@nicoddemus`_ for the PR `#77`_.
+
+- hide ``pytest`` tracebacks in certain core routines. Thanks to
+ `@nicoddemus`_ for the PR `#80`_.
+
+.. _#72: https://github.com/pytest-dev/pluggy/pull/72
+.. _#77: https://github.com/pytest-dev/pluggy/pull/77
+.. _#80: https://github.com/pytest-dev/pluggy/pull/80
+
+
+pluggy 0.5.1 (2017-08-29)
+=========================
+
+- fix a bug and add tests for case where ``firstresult`` hooks return
+ ``None`` results. Thanks to `@RonnyPfannschmidt`_ and `@tgoodlet`_
+ for the issue (`#68`_) and PR (`#69`_) respectively.
+
+.. _#69: https://github.com/pytest-dev/pluggy/pull/69
+.. _#68: https://github.com/pytest-dev/pluggy/issues/68
+
+
+pluggy 0.5.0 (2017-08-28)
+=========================
+
+- fix bug where callbacks for historic hooks would not be called for
+ already registered plugins. Thanks `@vodik`_ for the PR
+ and `@hpk42`_ for further fixes.
+
+- fix `#17`_ by considering only actual functions for hooks
+ this removes the ability to register arbitrary callable objects
+ which at first glance is a reasonable simplification,
+ thanks `@RonnyPfannschmidt`_ for report and pr.
+
+- fix `#19`_: allow registering hookspecs from instances. The PR from
+ `@tgoodlet`_ also modernized the varnames implementation.
+
+- resolve `#32`_: split up the test set into multiple modules.
+ Thanks to `@RonnyPfannschmidt`_ for the PR and `@tgoodlet`_ for
+ the initial request.
+
+- resolve `#14`_: add full sphinx docs. Thanks to `@tgoodlet`_ for
+ PR `#39`_.
+
+- add hook call mismatch warnings. Thanks to `@tgoodlet`_ for the
+ PR `#42`_.
+
+- resolve `#44`_: move to new-style classes. Thanks to `@MichalTHEDUDE`_
+ for PR `#46`_.
+
+- add baseline benchmarking/speed tests using ``pytest-benchmark``
+ in PR `#54`_. Thanks to `@tgoodlet`_.
+
+- update the README to showcase the API. Thanks to `@tgoodlet`_ for the
+ issue and PR `#55`_.
+
+- deprecate ``__multicall__`` and add a faster call loop implementation.
+ Thanks to `@tgoodlet`_ for PR `#58`_.
+
+- raise a comprehensible error when a ``hookimpl`` is called with positional
+ args. Thanks to `@RonnyPfannschmidt`_ for the issue and `@tgoodlet`_ for
+ PR `#60`_.
+
+- fix the ``firstresult`` test making it more complete
+ and remove a duplicate of that test. Thanks to `@tgoodlet`_
+ for PR `#62`_.
+
+.. _#62: https://github.com/pytest-dev/pluggy/pull/62
+.. _#60: https://github.com/pytest-dev/pluggy/pull/60
+.. _#58: https://github.com/pytest-dev/pluggy/pull/58
+.. _#55: https://github.com/pytest-dev/pluggy/pull/55
+.. _#54: https://github.com/pytest-dev/pluggy/pull/54
+.. _#46: https://github.com/pytest-dev/pluggy/pull/46
+.. _#44: https://github.com/pytest-dev/pluggy/issues/44
+.. _#42: https://github.com/pytest-dev/pluggy/pull/42
+.. _#39: https://github.com/pytest-dev/pluggy/pull/39
+.. _#32: https://github.com/pytest-dev/pluggy/pull/32
+.. _#19: https://github.com/pytest-dev/pluggy/issues/19
+.. _#17: https://github.com/pytest-dev/pluggy/issues/17
+.. _#14: https://github.com/pytest-dev/pluggy/issues/14
+
+
+pluggy 0.4.0 (2016-09-25)
+=========================
+
+- add ``has_plugin(name)`` method to pluginmanager. thanks `@nicoddemus`_.
+
+- fix `#11`_: make plugin parsing more resilient against exceptions
+ from ``__getattr__`` functions. Thanks `@nicoddemus`_.
+
+- fix issue `#4`_: specific ``HookCallError`` exception for when a hook call
+ provides not enough arguments.
+
+- better error message when loading setuptools entrypoints fails
+ due to a ``VersionConflict``. Thanks `@blueyed`_.
+
+.. _#11: https://github.com/pytest-dev/pluggy/issues/11
+.. _#4: https://github.com/pytest-dev/pluggy/issues/4
+
+
+pluggy 0.3.1 (2015-09-17)
+=========================
+
+- avoid using deprecated-in-python3.5 getargspec method. Thanks
+ `@mdboom`_.
+
+
+pluggy 0.3.0 (2015-05-07)
+=========================
+
+initial release
+
+.. contributors
+.. _@hpk42: https://github.com/hpk42
+.. _@tgoodlet: https://github.com/goodboy
+.. _@MichalTHEDUDE: https://github.com/MichalTHEDUDE
+.. _@vodik: https://github.com/vodik
+.. _@RonnyPfannschmidt: https://github.com/RonnyPfannschmidt
+.. _@blueyed: https://github.com/blueyed
+.. _@nicoddemus: https://github.com/nicoddemus
+.. _@mdboom: https://github.com/mdboom
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/LICENSE b/testing/web-platform/tests/tools/third_party/pluggy/LICENSE
new file mode 100644
index 0000000000..85f4dd63d2
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2015 holger krekel (rather uses bitbucket/hpk42)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/MANIFEST.in b/testing/web-platform/tests/tools/third_party/pluggy/MANIFEST.in
new file mode 100644
index 0000000000..0cf8f3e088
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/MANIFEST.in
@@ -0,0 +1,7 @@
+include CHANGELOG
+include README.rst
+include setup.py
+include tox.ini
+include LICENSE
+graft testing
+recursive-exclude * *.pyc *.pyo
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/README.rst b/testing/web-platform/tests/tools/third_party/pluggy/README.rst
new file mode 100644
index 0000000000..3496617e1e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/README.rst
@@ -0,0 +1,101 @@
+====================================================
+pluggy - A minimalist production ready plugin system
+====================================================
+
+|pypi| |conda-forge| |versions| |github-actions| |gitter| |black| |codecov|
+
+This is the core framework used by the `pytest`_, `tox`_, and `devpi`_ projects.
+
+Please `read the docs`_ to learn more!
+
+A definitive example
+====================
+.. code-block:: python
+
+ import pluggy
+
+ hookspec = pluggy.HookspecMarker("myproject")
+ hookimpl = pluggy.HookimplMarker("myproject")
+
+
+ class MySpec:
+ """A hook specification namespace."""
+
+ @hookspec
+ def myhook(self, arg1, arg2):
+ """My special little hook that you can customize."""
+
+
+ class Plugin_1:
+ """A hook implementation namespace."""
+
+ @hookimpl
+ def myhook(self, arg1, arg2):
+ print("inside Plugin_1.myhook()")
+ return arg1 + arg2
+
+
+ class Plugin_2:
+ """A 2nd hook implementation namespace."""
+
+ @hookimpl
+ def myhook(self, arg1, arg2):
+ print("inside Plugin_2.myhook()")
+ return arg1 - arg2
+
+
+ # create a manager and add the spec
+ pm = pluggy.PluginManager("myproject")
+ pm.add_hookspecs(MySpec)
+
+ # register plugins
+ pm.register(Plugin_1())
+ pm.register(Plugin_2())
+
+ # call our ``myhook`` hook
+ results = pm.hook.myhook(arg1=1, arg2=2)
+ print(results)
+
+
+Running this directly gets us::
+
+ $ python docs/examples/toy-example.py
+ inside Plugin_2.myhook()
+ inside Plugin_1.myhook()
+ [-1, 3]
+
+
+.. badges
+
+.. |pypi| image:: https://img.shields.io/pypi/v/pluggy.svg
+ :target: https://pypi.org/pypi/pluggy
+
+.. |versions| image:: https://img.shields.io/pypi/pyversions/pluggy.svg
+ :target: https://pypi.org/pypi/pluggy
+
+.. |github-actions| image:: https://github.com/pytest-dev/pluggy/workflows/main/badge.svg
+ :target: https://github.com/pytest-dev/pluggy/actions
+
+.. |conda-forge| image:: https://img.shields.io/conda/vn/conda-forge/pluggy.svg
+ :target: https://anaconda.org/conda-forge/pytest
+
+.. |gitter| image:: https://badges.gitter.im/pytest-dev/pluggy.svg
+ :alt: Join the chat at https://gitter.im/pytest-dev/pluggy
+ :target: https://gitter.im/pytest-dev/pluggy?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
+
+.. |black| image:: https://img.shields.io/badge/code%20style-black-000000.svg
+ :target: https://github.com/ambv/black
+
+.. |codecov| image:: https://codecov.io/gh/pytest-dev/pluggy/branch/master/graph/badge.svg
+ :target: https://codecov.io/gh/pytest-dev/pluggy
+ :alt: Code coverage Status
+
+.. links
+.. _pytest:
+ http://pytest.org
+.. _tox:
+ https://tox.readthedocs.org
+.. _devpi:
+ http://doc.devpi.net
+.. _read the docs:
+ https://pluggy.readthedocs.io/en/latest/
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/RELEASING.rst b/testing/web-platform/tests/tools/third_party/pluggy/RELEASING.rst
new file mode 100644
index 0000000000..ee0d1331e0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/RELEASING.rst
@@ -0,0 +1,23 @@
+Release Procedure
+-----------------
+
+#. From a clean work tree, execute::
+
+ tox -e release -- VERSION
+
+ This will create the branch ready to be pushed.
+
+#. Open a PR targeting ``main``.
+
+#. All tests must pass and the PR must be approved by at least another maintainer.
+
+#. Publish to PyPI by pushing a tag::
+
+ git tag X.Y.Z release-X.Y.Z
+ git push git@github.com:pytest-dev/pluggy.git X.Y.Z
+
+ The tag will trigger a new build, which will deploy to PyPI.
+
+#. Make sure it is `available on PyPI <https://pypi.org/project/pluggy>`_.
+
+#. Merge the PR into ``main``, either manually or using GitHub's web interface.
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/changelog/README.rst b/testing/web-platform/tests/tools/third_party/pluggy/changelog/README.rst
new file mode 100644
index 0000000000..47e21fb33f
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/changelog/README.rst
@@ -0,0 +1,32 @@
+This directory contains "newsfragments" which are short files that contain a small **ReST**-formatted
+text that will be added to the next ``CHANGELOG``.
+
+The ``CHANGELOG`` will be read by users, so this description should be aimed to pytest users
+instead of describing internal changes which are only relevant to the developers.
+
+Make sure to use full sentences with correct case and punctuation, for example::
+
+ Fix issue with non-ascii messages from the ``warnings`` module.
+
+Each file should be named like ``<ISSUE>.<TYPE>.rst``, where
+``<ISSUE>`` is an issue number, and ``<TYPE>`` is one of:
+
+* ``feature``: new user facing features, like new command-line options and new behavior.
+* ``bugfix``: fixes a reported bug.
+* ``doc``: documentation improvement, like rewording an entire session or adding missing docs.
+* ``removal``: feature deprecation or removal.
+* ``vendor``: changes in packages vendored in pytest.
+* ``trivial``: fixing a small typo or internal change that might be noteworthy.
+
+So for example: ``123.feature.rst``, ``456.bugfix.rst``.
+
+If your PR fixes an issue, use that number here. If there is no issue,
+then after you submit the PR and get the PR number you can add a
+changelog using that instead.
+
+If you are not sure what issue type to use, don't hesitate to ask in your PR.
+
+``towncrier`` preserves multiple paragraphs and formatting (code blocks, lists, and so on), but for entries
+other than ``features`` it is usually better to stick to a single paragraph to keep it concise. You can install
+``towncrier`` and then run ``towncrier --draft``
+if you want to get a preview of how your change will look in the final release notes.
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/changelog/_template.rst b/testing/web-platform/tests/tools/third_party/pluggy/changelog/_template.rst
new file mode 100644
index 0000000000..974e5c1b2d
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/changelog/_template.rst
@@ -0,0 +1,40 @@
+{% for section in sections %}
+{% set underline = "-" %}
+{% if section %}
+{{section}}
+{{ underline * section|length }}{% set underline = "~" %}
+
+{% endif %}
+{% if sections[section] %}
+{% for category, val in definitions.items() if category in sections[section] %}
+
+{{ definitions[category]['name'] }}
+{{ underline * definitions[category]['name']|length }}
+
+{% if definitions[category]['showcontent'] %}
+{% for text, values in sections[section][category]|dictsort(by='value') %}
+{% set issue_joiner = joiner(', ') %}
+- {% for value in values|sort %}{{ issue_joiner() }}`{{ value }} <https://github.com/pytest-dev/pluggy/issues/{{ value[1:] }}>`_{% endfor %}: {{ text }}
+
+
+{% endfor %}
+{% else %}
+- {{ sections[section][category]['']|sort|join(', ') }}
+
+
+{% endif %}
+{% if sections[section][category]|length == 0 %}
+
+No significant changes.
+
+
+{% else %}
+{% endif %}
+{% endfor %}
+{% else %}
+
+No significant changes.
+
+
+{% endif %}
+{% endfor %}
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/codecov.yml b/testing/web-platform/tests/tools/third_party/pluggy/codecov.yml
new file mode 100644
index 0000000000..a0a308588e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/codecov.yml
@@ -0,0 +1,7 @@
+coverage:
+ status:
+ project: true
+ patch: true
+ changes: true
+
+comment: off
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/_static/img/plug.png b/testing/web-platform/tests/tools/third_party/pluggy/docs/_static/img/plug.png
new file mode 100644
index 0000000000..3339f8a608
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/_static/img/plug.png
Binary files differ
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/api_reference.rst b/testing/web-platform/tests/tools/third_party/pluggy/docs/api_reference.rst
new file mode 100644
index 0000000000..d9552d4485
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/api_reference.rst
@@ -0,0 +1,19 @@
+:orphan:
+
+Api Reference
+=============
+
+.. automodule:: pluggy
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
+.. autoclass:: pluggy._callers._Result
+.. automethod:: pluggy._callers._Result.get_result
+.. automethod:: pluggy._callers._Result.force_result
+
+.. autoclass:: pluggy._hooks._HookCaller
+.. automethod:: pluggy._hooks._HookCaller.call_extra
+.. automethod:: pluggy._hooks._HookCaller.call_historic
+
+.. autoclass:: pluggy._hooks._HookRelay
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/changelog.rst b/testing/web-platform/tests/tools/third_party/pluggy/docs/changelog.rst
new file mode 100644
index 0000000000..565b0521d0
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/changelog.rst
@@ -0,0 +1 @@
+.. include:: ../CHANGELOG.rst
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/conf.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/conf.py
new file mode 100644
index 0000000000..f8e70c88bf
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/conf.py
@@ -0,0 +1,87 @@
+import sys
+
+if sys.version_info >= (3, 8):
+ from importlib import metadata
+else:
+ import importlib_metadata as metadata
+
+
+extensions = [
+ "sphinx.ext.autodoc",
+ "sphinx.ext.doctest",
+ "sphinx.ext.intersphinx",
+ "sphinx.ext.coverage",
+ "sphinx.ext.viewcode",
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ["_templates"]
+
+source_suffix = ".rst"
+
+# The master toctree document.
+master_doc = "index"
+
+# General information about the project.
+
+project = "pluggy"
+copyright = "2016, Holger Krekel"
+author = "Holger Krekel"
+
+release = metadata.version(project)
+# The short X.Y version.
+version = ".".join(release.split(".")[:2])
+
+
+language = None
+
+pygments_style = "sphinx"
+# html_logo = "_static/img/plug.png"
+html_theme = "alabaster"
+html_theme_options = {
+ "logo": "img/plug.png",
+ "description": "The pytest plugin system",
+ "github_user": "pytest-dev",
+ "github_repo": "pluggy",
+ "github_button": "true",
+ "github_banner": "true",
+ "github_type": "star",
+ "badge_branch": "master",
+ "page_width": "1080px",
+ "fixed_sidebar": "false",
+}
+html_sidebars = {
+ "**": ["about.html", "localtoc.html", "relations.html", "searchbox.html"]
+}
+html_static_path = ["_static"]
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [(master_doc, "pluggy", "pluggy 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,
+ "pluggy",
+ "pluggy Documentation",
+ author,
+ "pluggy",
+ "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),
+ "pytest": ("https://docs.pytest.org/en/latest", None),
+ "setuptools": ("https://setuptools.readthedocs.io/en/latest", None),
+ "tox": ("https://tox.readthedocs.io/en/latest", None),
+ "devpi": ("https://devpi.net/docs/devpi/devpi/stable/+doc/", None),
+}
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/eggsample_spam.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/eggsample_spam.py
new file mode 100644
index 0000000000..500d885d55
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/eggsample_spam.py
@@ -0,0 +1,22 @@
+import eggsample
+
+
+@eggsample.hookimpl
+def eggsample_add_ingredients(ingredients):
+ """Here the caller expects us to return a list."""
+ if "egg" in ingredients:
+ spam = ["lovely spam", "wonderous spam"]
+ else:
+ spam = ["splendiferous spam", "magnificent spam"]
+ return spam
+
+
+@eggsample.hookimpl
+def eggsample_prep_condiments(condiments):
+ """Here the caller passes a mutable object, so we mess with it directly."""
+ try:
+ del condiments["steak sauce"]
+ except KeyError:
+ pass
+ condiments["spam sauce"] = 42
+ return "Now this is what I call a condiments tray!"
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py
new file mode 100644
index 0000000000..f81a8eb403
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample-spam/setup.py
@@ -0,0 +1,8 @@
+from setuptools import setup
+
+setup(
+ name="eggsample-spam",
+ install_requires="eggsample",
+ entry_points={"eggsample": ["spam = eggsample_spam"]},
+ py_modules=["eggsample_spam"],
+)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py
new file mode 100644
index 0000000000..4dc4b36dec
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/__init__.py
@@ -0,0 +1,4 @@
+import pluggy
+
+hookimpl = pluggy.HookimplMarker("eggsample")
+"""Marker to be imported and used in plugins (and for own implementations)"""
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py
new file mode 100644
index 0000000000..48866b2491
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/hookspecs.py
@@ -0,0 +1,21 @@
+import pluggy
+
+hookspec = pluggy.HookspecMarker("eggsample")
+
+
+@hookspec
+def eggsample_add_ingredients(ingredients: tuple):
+ """Have a look at the ingredients and offer your own.
+
+ :param ingredients: the ingredients, don't touch them!
+ :return: a list of ingredients
+ """
+
+
+@hookspec
+def eggsample_prep_condiments(condiments: dict):
+ """Reorganize the condiments tray to your heart's content.
+
+ :param condiments: some sauces and stuff
+ :return: a witty comment about your activity
+ """
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py
new file mode 100644
index 0000000000..ac1d33b453
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/host.py
@@ -0,0 +1,57 @@
+import itertools
+import random
+
+import pluggy
+
+from eggsample import hookspecs, lib
+
+condiments_tray = {"pickled walnuts": 13, "steak sauce": 4, "mushy peas": 2}
+
+
+def main():
+ pm = get_plugin_manager()
+ cook = EggsellentCook(pm.hook)
+ cook.add_ingredients()
+ cook.prepare_the_food()
+ cook.serve_the_food()
+
+
+def get_plugin_manager():
+ pm = pluggy.PluginManager("eggsample")
+ pm.add_hookspecs(hookspecs)
+ pm.load_setuptools_entrypoints("eggsample")
+ pm.register(lib)
+ return pm
+
+
+class EggsellentCook:
+ FAVORITE_INGREDIENTS = ("egg", "egg", "egg")
+
+ def __init__(self, hook):
+ self.hook = hook
+ self.ingredients = None
+
+ def add_ingredients(self):
+ results = self.hook.eggsample_add_ingredients(
+ ingredients=self.FAVORITE_INGREDIENTS
+ )
+ my_ingredients = list(self.FAVORITE_INGREDIENTS)
+ # Each hook returns a list - so we chain this list of lists
+ other_ingredients = list(itertools.chain(*results))
+ self.ingredients = my_ingredients + other_ingredients
+
+ def prepare_the_food(self):
+ random.shuffle(self.ingredients)
+
+ def serve_the_food(self):
+ condiment_comments = self.hook.eggsample_prep_condiments(
+ condiments=condiments_tray
+ )
+ print(f"Your food. Enjoy some {', '.join(self.ingredients)}")
+ print(f"Some condiments? We have {', '.join(condiments_tray.keys())}")
+ if any(condiment_comments):
+ print("\n".join(condiment_comments))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/lib.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/lib.py
new file mode 100644
index 0000000000..62cea7458e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/eggsample/lib.py
@@ -0,0 +1,14 @@
+import eggsample
+
+
+@eggsample.hookimpl
+def eggsample_add_ingredients():
+ spices = ["salt", "pepper"]
+ you_can_never_have_enough_eggs = ["egg", "egg"]
+ ingredients = spices + you_can_never_have_enough_eggs
+ return ingredients
+
+
+@eggsample.hookimpl
+def eggsample_prep_condiments(condiments):
+ condiments["mint sauce"] = 1
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py
new file mode 100644
index 0000000000..8b3facb3b6
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/eggsample/setup.py
@@ -0,0 +1,8 @@
+from setuptools import setup, find_packages
+
+setup(
+ name="eggsample",
+ install_requires="pluggy>=0.3,<1.0",
+ entry_points={"console_scripts": ["eggsample=eggsample.host:main"]},
+ packages=find_packages(),
+)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/toy-example.py b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/toy-example.py
new file mode 100644
index 0000000000..6d2086f9ba
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/examples/toy-example.py
@@ -0,0 +1,41 @@
+import pluggy
+
+hookspec = pluggy.HookspecMarker("myproject")
+hookimpl = pluggy.HookimplMarker("myproject")
+
+
+class MySpec:
+ """A hook specification namespace."""
+
+ @hookspec
+ def myhook(self, arg1, arg2):
+ """My special little hook that you can customize."""
+
+
+class Plugin_1:
+ """A hook implementation namespace."""
+
+ @hookimpl
+ def myhook(self, arg1, arg2):
+ print("inside Plugin_1.myhook()")
+ return arg1 + arg2
+
+
+class Plugin_2:
+ """A 2nd hook implementation namespace."""
+
+ @hookimpl
+ def myhook(self, arg1, arg2):
+ print("inside Plugin_2.myhook()")
+ return arg1 - arg2
+
+
+# create a manager and add the spec
+pm = pluggy.PluginManager("myproject")
+pm.add_hookspecs(MySpec)
+# register plugins
+pm.register(Plugin_1())
+pm.register(Plugin_2())
+# call our `myhook` hook
+results = pm.hook.myhook(arg1=1, arg2=2)
+print(results)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/docs/index.rst b/testing/web-platform/tests/tools/third_party/pluggy/docs/index.rst
new file mode 100644
index 0000000000..eab08fcbbd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/index.rst
@@ -0,0 +1,957 @@
+``pluggy``
+==========
+**The pytest plugin system**
+
+What is it?
+***********
+``pluggy`` is the crystallized core of :ref:`plugin management and hook
+calling <pytest:writing-plugins>` for :std:doc:`pytest <pytest:index>`.
+It enables `500+ plugins`_ to extend and customize ``pytest``'s default
+behaviour. Even ``pytest`` itself is composed as a set of ``pluggy`` plugins
+which are invoked in sequence according to a well defined set of protocols.
+
+It gives users the ability to extend or modify the behaviour of a
+``host program`` by installing a ``plugin`` for that program.
+The plugin code will run as part of normal program execution, changing or
+enhancing certain aspects of it.
+
+In essence, ``pluggy`` enables function `hooking`_ so you can build
+"pluggable" systems.
+
+Why is it useful?
+*****************
+There are some established mechanisms for modifying the behavior of other
+programs/libraries in Python like
+`method overriding <https://en.wikipedia.org/wiki/Method_overriding>`_
+(e.g. Jinja2) or
+`monkey patching <https://en.wikipedia.org/wiki/Monkey_patch>`_ (e.g. gevent
+or for :std:doc:`writing tests <pytest:how-to/monkeypatch>`).
+These strategies become problematic though when several parties want to
+participate in the modification of the same program. Therefore ``pluggy``
+does not rely on these mechanisms to enable a more structured approach and
+avoid unnecessary exposure of state and behaviour. This leads to a more
+`loosely coupled <https://en.wikipedia.org/wiki/Loose_coupling>`_ relationship
+between ``host`` and ``plugins``.
+
+The ``pluggy`` approach puts the burden on the designer of the
+``host program`` to think carefully about which objects are really
+needed in a hook implementation. This gives ``plugin`` creators a clear
+framework for how to extend the ``host`` via a well defined set of functions
+and objects to work with.
+
+How does it work?
+*****************
+Let us start with a short overview of what is involved:
+
+* ``host`` or ``host program``: the program offering extensibility
+ by specifying ``hook functions`` and invoking their implementation(s) as
+ part of program execution
+* ``plugin``: the program implementing (a subset of) the specified hooks and
+ participating in program execution when the implementations are invoked
+ by the ``host``
+* ``pluggy``: connects ``host`` and ``plugins`` by using ...
+
+ - the hook :ref:`specifications <specs>` defining call signatures
+ provided by the ``host`` (a.k.a ``hookspecs`` - see :ref:`marking_hooks`)
+ - the hook :ref:`implementations <impls>` provided by registered
+ ``plugins`` (a.k.a ``hookimpl`` - see `callbacks`_)
+ - the hook :ref:`caller <calling>` - a call loop triggered at appropriate
+ program positions in the ``host`` invoking the implementations and
+ collecting the results
+
+ ... where for each registered hook *specification*, a hook *call* will
+ invoke up to ``N`` registered hook *implementations*.
+* ``user``: the person who installed the ``host program`` and wants to
+ extend its functionality with ``plugins``. In the simplest case they install
+ the ``plugin`` in the same environment as the ``host`` and the magic will
+ happen when the ``host program`` is run the next time. Depending on
+ the ``plugin``, there might be other things they need to do. For example,
+ they might have to call the host with an additional commandline parameter
+ to the host that the ``plugin`` added.
+
+A toy example
+-------------
+Let us demonstrate the core functionality in one module and show how you can
+start experimenting with pluggy functionality.
+
+.. literalinclude:: examples/toy-example.py
+
+Running this directly gets us::
+
+ $ python docs/examples/toy-example.py
+
+ inside Plugin_2.myhook()
+ inside Plugin_1.myhook()
+ [-1, 3]
+
+A complete example
+------------------
+Now let us demonstrate how this plays together in a vaguely real world scenario.
+
+Let's assume our ``host program`` is called **eggsample** where some eggs will
+be prepared and served with a tray containing condiments. As everybody knows:
+the more cooks are involved the better the food, so let us make the process
+pluggable and write a plugin that improves the meal with some spam and replaces
+the steak sauce (nobody likes that anyway) with spam sauce (it's a thing - trust me).
+
+.. note::
+
+ **naming markers**: ``HookSpecMarker`` and ``HookImplMarker`` must be
+ initialized with the name of the ``host`` project (the ``name``
+ parameter in ``setup()``) - so **eggsample** in our case.
+
+ **naming plugin projects**: they should be named in the form of
+ ``<host>-<plugin>`` (e.g. ``pytest-xdist``), therefore we call our
+ plugin *eggsample-spam*.
+
+The host
+^^^^^^^^
+``eggsample/eggsample/__init__.py``
+
+.. literalinclude:: examples/eggsample/eggsample/__init__.py
+
+``eggsample/eggsample/hookspecs.py``
+
+.. literalinclude:: examples/eggsample/eggsample/hookspecs.py
+
+``eggsample/eggsample/lib.py``
+
+.. literalinclude:: examples/eggsample/eggsample/lib.py
+
+``eggsample/eggsample/host.py``
+
+.. literalinclude:: examples/eggsample/eggsample/host.py
+
+``eggsample/setup.py``
+
+.. literalinclude:: examples/eggsample/setup.py
+
+Let's get cooking - we install the host and see what a program run looks like::
+
+ $ pip install --editable pluggy/docs/examples/eggsample
+ $ eggsample
+
+ Your food. Enjoy some egg, egg, salt, egg, egg, pepper, egg
+ Some condiments? We have pickled walnuts, steak sauce, mushy peas, mint sauce
+
+The plugin
+^^^^^^^^^^
+``eggsample-spam/eggsample_spam.py``
+
+.. literalinclude:: examples/eggsample-spam/eggsample_spam.py
+
+``eggsample-spam/setup.py``
+
+.. literalinclude:: examples/eggsample-spam/setup.py
+
+Let's get cooking with more cooks - we install the plugin and and see what
+we get::
+
+ $ pip install --editable pluggy/docs/examples/eggsample-spam
+ $ eggsample
+
+ Your food. Enjoy some egg, lovely spam, salt, egg, egg, egg, wonderous spam, egg, pepper
+ Some condiments? We have pickled walnuts, mushy peas, mint sauce, spam sauce
+ Now this is what I call a condiments tray!
+
+More real world examples
+------------------------
+To see how ``pluggy`` is used in the real world, have a look at these projects
+documentation and source code:
+
+* :ref:`pytest <pytest:writing-plugins>`
+* :std:doc:`tox <tox:plugins>`
+* :std:doc:`devpi <devpi:devguide/index>`
+
+For more details and advanced usage please read on.
+
+.. _define:
+
+Define and collect hooks
+************************
+A *plugin* is a :ref:`namespace <python:tut-scopes>` type (currently one of a
+``class`` or module) which defines a set of *hook* functions.
+
+As mentioned in :ref:`manage`, all *plugins* which specify *hooks*
+are managed by an instance of a :py:class:`pluggy.PluginManager` which
+defines the primary ``pluggy`` API.
+
+In order for a :py:class:`~pluggy.PluginManager` to detect functions in a namespace
+intended to be *hooks*, they must be decorated using special ``pluggy`` *marks*.
+
+.. _marking_hooks:
+
+Marking hooks
+-------------
+The :py:class:`~pluggy.HookspecMarker` and :py:class:`~pluggy.HookimplMarker`
+decorators are used to *mark* functions for detection by a
+:py:class:`~pluggy.PluginManager`:
+
+.. code-block:: python
+
+ from pluggy import HookspecMarker, HookimplMarker
+
+ hookspec = HookspecMarker("project_name")
+ hookimpl = HookimplMarker("project_name")
+
+
+Each decorator type takes a single ``project_name`` string as its
+lone argument the value of which is used to mark hooks for detection by
+a similarly configured :py:class:`~pluggy.PluginManager` instance.
+
+That is, a *mark* type called with ``project_name`` returns an object which
+can be used to decorate functions which will then be detected by a
+:py:class:`~pluggy.PluginManager` which was instantiated with the same
+``project_name`` value.
+
+Furthermore, each *hookimpl* or *hookspec* decorator can configure the
+underlying call-time behavior of each *hook* object by providing special
+*options* passed as keyword arguments.
+
+
+.. note::
+ The following sections correspond to similar documentation in
+ ``pytest`` for :ref:`pytest:writinghooks` and can be used as
+ a supplementary resource.
+
+.. _impls:
+
+Implementations
+---------------
+A hook *implementation* (*hookimpl*) is just a (callback) function
+which has been appropriately marked.
+
+*hookimpls* are loaded from a plugin using the
+:py:meth:`~pluggy.PluginManager.register()` method:
+
+.. code-block:: python
+
+ import sys
+ from pluggy import PluginManager, HookimplMarker
+
+ hookimpl = HookimplMarker("myproject")
+
+
+ @hookimpl
+ def setup_project(config, args):
+ """This hook is used to process the initial config
+ and possibly input arguments.
+ """
+ if args:
+ config.process_args(args)
+
+ return config
+
+
+ pm = PluginManager("myproject")
+
+ # load all hookimpls from the local module's namespace
+ plugin_name = pm.register(sys.modules[__name__])
+
+.. _optionalhook:
+
+Optional validation
+^^^^^^^^^^^^^^^^^^^
+Normally each *hookimpl* should be validated against a corresponding
+hook :ref:`specification <specs>`. If you want to make an exception
+then the *hookimpl* should be marked with the ``"optionalhook"`` option:
+
+.. code-block:: python
+
+ @hookimpl(optionalhook=True)
+ def setup_project(config, args):
+ """This hook is used to process the initial config
+ and possibly input arguments.
+ """
+ if args:
+ config.process_args(args)
+
+ return config
+
+.. _specname:
+
+Hookspec name matching
+^^^^^^^^^^^^^^^^^^^^^^
+
+During plugin :ref:`registration <registration>`, pluggy attempts to match each
+hook implementation declared by the *plugin* to a hook
+:ref:`specification <specs>` in the *host* program with the **same name** as
+the function being decorated by ``@hookimpl`` (e.g. ``setup_project`` in the
+example above). Note: there is *no* strict requirement that each *hookimpl*
+has a corresponding *hookspec* (see
+:ref:`enforcing spec validation <enforcing>`).
+
+*new in version 0.13.2:*
+
+To override the default behavior, a *hookimpl* may also be matched to a
+*hookspec* in the *host* program with a non-matching function name by using
+the ``specname`` option. Continuing the example above, the *hookimpl* function
+does not need to be named ``setup_project``, but if the argument
+``specname="setup_project"`` is provided to the ``hookimpl`` decorator, it will
+be matched and checked against the ``setup_project`` hookspec:
+
+.. code-block:: python
+
+ @hookimpl(specname="setup_project")
+ def any_plugin_function(config, args):
+ """This hook is used to process the initial config
+ and possibly input arguments.
+ """
+ if args:
+ config.process_args(args)
+
+ return config
+
+Call time order
+^^^^^^^^^^^^^^^
+By default hooks are :ref:`called <calling>` in LIFO registered order, however,
+a *hookimpl* can influence its call-time invocation position using special
+attributes. If marked with a ``"tryfirst"`` or ``"trylast"`` option it
+will be executed *first* or *last* respectively in the hook call loop:
+
+.. code-block:: python
+
+ import sys
+ from pluggy import PluginManager, HookimplMarker
+
+ hookimpl = HookimplMarker("myproject")
+
+
+ @hookimpl(trylast=True)
+ def setup_project(config, args):
+ """Default implementation."""
+ if args:
+ config.process_args(args)
+
+ return config
+
+
+ class SomeOtherPlugin:
+ """Some other plugin defining the same hook."""
+
+ @hookimpl(tryfirst=True)
+ def setup_project(self, config, args):
+ """Report what args were passed before calling
+ downstream hooks.
+ """
+ if args:
+ print("Got args: {}".format(args))
+
+ return config
+
+
+ pm = PluginManager("myproject")
+
+ # load from the local module's namespace
+ pm.register(sys.modules[__name__])
+ # load a plugin defined on a class
+ pm.register(SomeOtherPlugin())
+
+For another example see the :ref:`pytest:plugin-hookorder` section of the
+``pytest`` docs.
+
+.. note::
+ ``tryfirst`` and ``trylast`` hooks are still invoked in LIFO order within
+ each category.
+
+
+.. _hookwrappers:
+
+Wrappers
+^^^^^^^^
+A *hookimpl* can be marked with a ``"hookwrapper"`` option which indicates that
+the function will be called to *wrap* (or surround) all other normal *hookimpl*
+calls. A *hookwrapper* can thus execute some code ahead and after the execution
+of all corresponding non-wrappper *hookimpls*.
+
+Much in the same way as a :py:func:`@contextlib.contextmanager <python:contextlib.contextmanager>`, *hookwrappers* must
+be implemented as generator function with a single ``yield`` in its body:
+
+
+.. code-block:: python
+
+ @hookimpl(hookwrapper=True)
+ def setup_project(config, args):
+ """Wrap calls to ``setup_project()`` implementations which
+ should return json encoded config options.
+ """
+ if config.debug:
+ print("Pre-hook config is {}".format(config.tojson()))
+
+ # get initial default config
+ defaults = config.tojson()
+
+ # all corresponding hookimpls are invoked here
+ outcome = yield
+
+ for item in outcome.get_result():
+ print("JSON config override is {}".format(item))
+
+ if config.debug:
+ print("Post-hook config is {}".format(config.tojson()))
+
+ if config.use_defaults:
+ outcome.force_result(defaults)
+
+The generator is :py:meth:`sent <python:generator.send>` a :py:class:`pluggy._callers._Result` object which can
+be assigned in the ``yield`` expression and used to override or inspect
+the final result(s) returned back to the caller using the
+:py:meth:`~pluggy._callers._Result.force_result` or
+:py:meth:`~pluggy._callers._Result.get_result` methods.
+
+.. note::
+ Hook wrappers can **not** return results (as per generator function
+ semantics); they can only modify them using the ``_Result`` API.
+
+Also see the :ref:`pytest:hookwrapper` section in the ``pytest`` docs.
+
+.. _specs:
+
+Specifications
+--------------
+A hook *specification* (*hookspec*) is a definition used to validate each
+*hookimpl* ensuring that an extension writer has correctly defined their
+callback function *implementation* .
+
+*hookspecs* are defined using similarly marked functions however only the
+function *signature* (its name and names of all its arguments) is analyzed
+and stored. As such, often you will see a *hookspec* defined with only
+a docstring in its body.
+
+*hookspecs* are loaded using the
+:py:meth:`~pluggy.PluginManager.add_hookspecs()` method and normally
+should be added before registering corresponding *hookimpls*:
+
+.. code-block:: python
+
+ import sys
+ from pluggy import PluginManager, HookspecMarker
+
+ hookspec = HookspecMarker("myproject")
+
+
+ @hookspec
+ def setup_project(config, args):
+ """This hook is used to process the initial config and input
+ arguments.
+ """
+
+
+ pm = PluginManager("myproject")
+
+ # load from the local module's namespace
+ pm.add_hookspecs(sys.modules[__name__])
+
+
+Registering a *hookimpl* which does not meet the constraints of its
+corresponding *hookspec* will result in an error.
+
+A *hookspec* can also be added **after** some *hookimpls* have been
+registered however this is not normally recommended as it results in
+delayed hook validation.
+
+.. note::
+ The term *hookspec* can sometimes refer to the plugin-namespace
+ which defines ``hookspec`` decorated functions as in the case of
+ ``pytest``'s `hookspec module`_
+
+.. _enforcing:
+
+Enforcing spec validation
+^^^^^^^^^^^^^^^^^^^^^^^^^
+By default there is no strict requirement that each *hookimpl* has
+a corresponding *hookspec*. However, if you'd like you enforce this
+behavior you can run a check with the
+:py:meth:`~pluggy.PluginManager.check_pending()` method. If you'd like
+to enforce requisite *hookspecs* but with certain exceptions for some hooks
+then make sure to mark those hooks as :ref:`optional <optionalhook>`.
+
+Opt-in arguments
+^^^^^^^^^^^^^^^^
+To allow for *hookspecs* to evolve over the lifetime of a project,
+*hookimpls* can accept **less** arguments then defined in the spec.
+This allows for extending hook arguments (and thus semantics) without
+breaking existing *hookimpls*.
+
+In other words this is ok:
+
+.. code-block:: python
+
+ @hookspec
+ def myhook(config, args):
+ pass
+
+
+ @hookimpl
+ def myhook(args):
+ print(args)
+
+
+whereas this is not:
+
+.. code-block:: python
+
+ @hookspec
+ def myhook(config, args):
+ pass
+
+
+ @hookimpl
+ def myhook(config, args, extra_arg):
+ print(args)
+
+.. note::
+ The one exception to this rule (that a *hookspec* must have as least as
+ many arguments as its *hookimpls*) is the conventional :ref:`self <python:tut-remarks>` arg; this
+ is always ignored when *hookimpls* are defined as :ref:`methods <python:tut-methodobjects>`.
+
+.. _firstresult:
+
+First result only
+^^^^^^^^^^^^^^^^^
+A *hookspec* can be marked such that when the *hook* is called the call loop
+will only invoke up to the first *hookimpl* which returns a result other
+then ``None``.
+
+.. code-block:: python
+
+ @hookspec(firstresult=True)
+ def myhook(config, args):
+ pass
+
+This can be useful for optimizing a call loop for which you are only
+interested in a single core *hookimpl*. An example is the
+:func:`~_pytest.hookspec.pytest_cmdline_main` central routine of ``pytest``.
+Note that all ``hookwrappers`` are still invoked with the first result.
+
+Also see the :ref:`pytest:firstresult` section in the ``pytest`` docs.
+
+.. _historic:
+
+Historic hooks
+^^^^^^^^^^^^^^
+You can mark a *hookspec* as being *historic* meaning that the hook
+can be called with :py:meth:`~pluggy._hooks._HookCaller.call_historic()` **before**
+having been registered:
+
+.. code-block:: python
+
+ @hookspec(historic=True)
+ def myhook(config, args):
+ pass
+
+The implication is that late registered *hookimpls* will be called back
+immediately at register time and **can not** return a result to the caller.
+
+This turns out to be particularly useful when dealing with lazy or
+dynamically loaded plugins.
+
+For more info see :ref:`call_historic`.
+
+
+Warnings on hook implementation
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+As projects evolve new hooks may be introduced and/or deprecated.
+
+if a hookspec specifies a ``warn_on_impl``, pluggy will trigger it for any plugin implementing the hook.
+
+
+.. code-block:: python
+
+ @hookspec(
+ warn_on_impl=DeprecationWarning("oldhook is deprecated and will be removed soon")
+ )
+ def oldhook():
+ pass
+
+.. _manage:
+
+The Plugin registry
+*******************
+``pluggy`` manages plugins using instances of the
+:py:class:`pluggy.PluginManager`.
+
+A :py:class:`~pluggy.PluginManager` is instantiated with a single
+``str`` argument, the ``project_name``:
+
+.. code-block:: python
+
+ import pluggy
+
+ pm = pluggy.PluginManager("my_project_name")
+
+
+The ``project_name`` value is used when a :py:class:`~pluggy.PluginManager`
+scans for *hook* functions :ref:`defined on a plugin <define>`.
+This allows for multiple plugin managers from multiple projects
+to define hooks alongside each other.
+
+.. _registration:
+
+Registration
+------------
+Each :py:class:`~pluggy.PluginManager` maintains a *plugin* registry where each *plugin*
+contains a set of *hookimpl* definitions. Loading *hookimpl* and *hookspec*
+definitions to populate the registry is described in detail in the section on
+:ref:`define`.
+
+In summary, you pass a plugin namespace object to the
+:py:meth:`~pluggy.PluginManager.register()` and
+:py:meth:`~pluggy.PluginManager.add_hookspecs()` methods to collect
+hook *implementations* and *specifications* from *plugin* namespaces respectively.
+
+You can unregister any *plugin*'s hooks using
+:py:meth:`~pluggy.PluginManager.unregister()` and check if a plugin is
+registered by passing its name to the
+:py:meth:`~pluggy.PluginManager.is_registered()` method.
+
+Loading ``setuptools`` entry points
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+You can automatically load plugins registered through
+:ref:`setuptools entry points <setuptools:entry_points>`
+with the :py:meth:`~pluggy.PluginManager.load_setuptools_entrypoints()`
+method.
+
+An example use of this is the :ref:`pytest entry point <pytest:pip-installable plugins>`.
+
+
+Blocking
+--------
+You can block any plugin from being registered using
+:py:meth:`~pluggy.PluginManager.set_blocked()` and check if a given
+*plugin* is blocked by name using :py:meth:`~pluggy.PluginManager.is_blocked()`.
+
+
+Inspection
+----------
+You can use a variety of methods to inspect both the registry
+and particular plugins in it:
+
+- :py:meth:`~pluggy.PluginManager.list_name_plugin()` -
+ return a list of name-plugin pairs
+- :py:meth:`~pluggy.PluginManager.get_plugins()` - retrieve all plugins
+- :py:meth:`~pluggy.PluginManager.get_canonical_name()`- get a *plugin*'s
+ canonical name (the name it was registered with)
+- :py:meth:`~pluggy.PluginManager.get_plugin()` - retrieve a plugin by its
+ canonical name
+
+
+Parsing mark options
+^^^^^^^^^^^^^^^^^^^^
+You can retrieve the *options* applied to a particular
+*hookspec* or *hookimpl* as per :ref:`marking_hooks` using the
+:py:meth:`~pluggy.PluginManager.parse_hookspec_opts()` and
+:py:meth:`~pluggy.PluginManager.parse_hookimpl_opts()` respectively.
+
+
+.. _calling:
+
+Calling hooks
+*************
+The core functionality of ``pluggy`` enables an extension provider
+to override function calls made at certain points throughout a program.
+
+A particular *hook* is invoked by calling an instance of
+a :py:class:`pluggy._hooks._HookCaller` which in turn *loops* through the
+``1:N`` registered *hookimpls* and calls them in sequence.
+
+Every :py:class:`~pluggy.PluginManager` has a ``hook`` attribute
+which is an instance of this :py:class:`pluggy._hooks._HookRelay`.
+The :py:class:`~pluggy._hooks._HookRelay` itself contains references
+(by hook name) to each registered *hookimpl*'s :py:class:`~pluggy._hooks._HookCaller` instance.
+
+More practically you call a *hook* like so:
+
+.. code-block:: python
+
+ import sys
+ import pluggy
+ import mypluginspec
+ import myplugin
+ from configuration import config
+
+ pm = pluggy.PluginManager("myproject")
+ pm.add_hookspecs(mypluginspec)
+ pm.register(myplugin)
+
+ # we invoke the _HookCaller and thus all underlying hookimpls
+ result_list = pm.hook.myhook(config=config, args=sys.argv)
+
+Note that you **must** call hooks using keyword :std:term:`python:argument` syntax!
+
+Hook implementations are called in LIFO registered order: *the last
+registered plugin's hooks are called first*. As an example, the below
+assertion should not error:
+
+.. code-block:: python
+
+ from pluggy import PluginManager, HookimplMarker
+
+ hookimpl = HookimplMarker("myproject")
+
+
+ class Plugin1:
+ @hookimpl
+ def myhook(self, args):
+ """Default implementation."""
+ return 1
+
+
+ class Plugin2:
+ @hookimpl
+ def myhook(self, args):
+ """Default implementation."""
+ return 2
+
+
+ class Plugin3:
+ @hookimpl
+ def myhook(self, args):
+ """Default implementation."""
+ return 3
+
+
+ pm = PluginManager("myproject")
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+ pm.register(Plugin3())
+
+ assert pm.hook.myhook(args=()) == [3, 2, 1]
+
+Collecting results
+------------------
+By default calling a hook results in all underlying :ref:`hookimpls
+<impls>` functions to be invoked in sequence via a loop. Any function
+which returns a value other then a ``None`` result will have that result
+appended to a :py:class:`list` which is returned by the call.
+
+The only exception to this behaviour is if the hook has been marked to return
+its :ref:`first result only <firstresult>` in which case only the first
+single value (which is not ``None``) will be returned.
+
+.. _call_historic:
+
+Exception handling
+------------------
+If any *hookimpl* errors with an exception no further callbacks
+are invoked and the exception is packaged up and delivered to
+any :ref:`wrappers <hookwrappers>` before being re-raised at the
+hook invocation point:
+
+.. code-block:: python
+
+ from pluggy import PluginManager, HookimplMarker
+
+ hookimpl = HookimplMarker("myproject")
+
+
+ class Plugin1:
+ @hookimpl
+ def myhook(self, args):
+ return 1
+
+
+ class Plugin2:
+ @hookimpl
+ def myhook(self, args):
+ raise RuntimeError
+
+
+ class Plugin3:
+ @hookimpl
+ def myhook(self, args):
+ return 3
+
+
+ @hookimpl(hookwrapper=True)
+ def myhook(self, args):
+ outcome = yield
+
+ try:
+ outcome.get_result()
+ except RuntimeError:
+ # log the error details
+ print(outcome.excinfo)
+
+
+ pm = PluginManager("myproject")
+
+ # register plugins
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+ pm.register(Plugin3())
+
+ # register wrapper
+ pm.register(sys.modules[__name__])
+
+ # this raises RuntimeError due to Plugin2
+ pm.hook.myhook(args=())
+
+Historic calls
+--------------
+A *historic call* allows for all newly registered functions to receive all hook
+calls that happened before their registration. The implication is that this is
+only useful if you expect that some *hookimpls* may be registered **after** the
+hook is initially invoked.
+
+Historic hooks must be :ref:`specially marked <historic>` and called
+using the :py:meth:`~pluggy._hooks._HookCaller.call_historic()` method:
+
+.. code-block:: python
+
+ def callback(result):
+ print("historic call result is {result}".format(result=result))
+
+
+ # call with history; no results returned
+ pm.hook.myhook.call_historic(
+ kwargs={"config": config, "args": sys.argv}, result_callback=callback
+ )
+
+ # ... more of our program ...
+
+ # late loading of some plugin
+ import mylateplugin
+
+ # historic callback is invoked here
+ pm.register(mylateplugin)
+
+Note that if you :py:meth:`~pluggy._hooks._HookCaller.call_historic()`
+the :py:class:`~pluggy._hooks._HookCaller` (and thus your calling code)
+can not receive results back from the underlying *hookimpl* functions.
+Instead you can provide a *callback* for processing results (like the
+``callback`` function above) which will be called as each new plugin
+is registered.
+
+.. note::
+ *historic* calls are incompatible with :ref:`firstresult` marked
+ hooks since only the first registered plugin's hook(s) would
+ ever be called.
+
+Calling with extras
+-------------------
+You can call a hook with temporarily participating *implementation* functions
+(that aren't in the registry) using the
+:py:meth:`pluggy._hooks._HookCaller.call_extra()` method.
+
+
+Calling with a subset of registered plugins
+-------------------------------------------
+You can make a call using a subset of plugins by asking the
+:py:class:`~pluggy.PluginManager` first for a
+:py:class:`~pluggy._hooks._HookCaller` with those plugins removed
+using the :py:meth:`pluggy.PluginManager.subset_hook_caller()` method.
+
+You then can use that :py:class:`_HookCaller <pluggy._hooks._HookCaller>`
+to make normal, :py:meth:`~pluggy._hooks._HookCaller.call_historic`, or
+:py:meth:`~pluggy._hooks._HookCaller.call_extra` calls as necessary.
+
+Built-in tracing
+****************
+``pluggy`` comes with some batteries included hook tracing for your
+debugging needs.
+
+
+Call tracing
+------------
+To enable tracing use the
+:py:meth:`pluggy.PluginManager.enable_tracing()` method which returns an
+undo function to disable the behaviour.
+
+.. code-block:: python
+
+ pm = PluginManager("myproject")
+ # magic line to set a writer function
+ pm.trace.root.setwriter(print)
+ undo = pm.enable_tracing()
+
+
+Call monitoring
+---------------
+Instead of using the built-in tracing mechanism you can also add your
+own ``before`` and ``after`` monitoring functions using
+:py:class:`pluggy.PluginManager.add_hookcall_monitoring()`.
+
+The expected signature and default implementations for these functions is:
+
+.. code-block:: python
+
+ def before(hook_name, methods, kwargs):
+ pass
+
+
+ def after(outcome, hook_name, methods, kwargs):
+ pass
+
+Public API
+**********
+Please see the :doc:`api_reference`.
+
+Development
+***********
+Great care must taken when hacking on ``pluggy`` since multiple mature
+projects rely on it. Our Github integrated CI process runs the full
+`tox test suite`_ on each commit so be sure your changes can run on
+all required `Python interpreters`_ and ``pytest`` versions.
+
+For development, we suggest to create a virtual environment and install ``pluggy`` in
+editable mode and ``dev`` dependencies::
+
+ $ python3 -m venv .env
+ $ source .env/bin/activate
+ $ pip install -e .[dev]
+
+To make sure you follow the code style used in the project, install pre-commit_ which
+will run style checks before each commit::
+
+ $ pre-commit install
+
+
+Release Policy
+**************
+Pluggy uses `Semantic Versioning`_. Breaking changes are only foreseen for
+Major releases (incremented X in "X.Y.Z"). If you want to use ``pluggy``
+in your project you should thus use a dependency restriction like
+``"pluggy>=0.1.0,<1.0"`` to avoid surprises.
+
+
+Table of contents
+*****************
+
+.. toctree::
+ :maxdepth: 2
+
+ api_reference
+ changelog
+
+
+
+.. hyperlinks
+.. _hookspec module:
+ https://docs.pytest.org/en/latest/_modules/_pytest/hookspec.html
+.. _request-response pattern:
+ https://en.wikipedia.org/wiki/Request%E2%80%93response
+.. _publish-subscribe:
+ https://en.wikipedia.org/wiki/Publish%E2%80%93subscribe_pattern
+.. _hooking:
+ https://en.wikipedia.org/wiki/Hooking
+.. _callbacks:
+ https://en.wikipedia.org/wiki/Callback_(computer_programming)
+.. _tox test suite:
+ https://github.com/pytest-dev/pluggy/blob/master/tox.ini
+.. _Semantic Versioning:
+ https://semver.org/
+.. _Python interpreters:
+ https://github.com/pytest-dev/pluggy/blob/master/tox.ini#L2
+.. _500+ plugins:
+ http://plugincompat.herokuapp.com/
+.. _pre-commit:
+ https://pre-commit.com/
+
+
+.. Indices and tables
+.. ==================
+.. * :ref:`genindex`
+.. * :ref:`modindex`
+.. * :ref:`search`
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/pyproject.toml b/testing/web-platform/tests/tools/third_party/pluggy/pyproject.toml
new file mode 100644
index 0000000000..15eba26898
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/pyproject.toml
@@ -0,0 +1,47 @@
+[build-system]
+requires = [
+ "setuptools",
+ "setuptools-scm",
+ "wheel",
+]
+
+[tool.setuptools_scm]
+write_to = "src/pluggy/_version.py"
+
+[tool.towncrier]
+package = "pluggy"
+package_dir = "src/pluggy"
+filename = "CHANGELOG.rst"
+directory = "changelog/"
+title_format = "pluggy {version} ({project_date})"
+template = "changelog/_template.rst"
+
+ [[tool.towncrier.type]]
+ directory = "removal"
+ name = "Deprecations and Removals"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "feature"
+ name = "Features"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "bugfix"
+ name = "Bug Fixes"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "vendor"
+ name = "Vendored Libraries"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "doc"
+ name = "Improved Documentation"
+ showcontent = true
+
+ [[tool.towncrier.type]]
+ directory = "trivial"
+ name = "Trivial/Internal Changes"
+ showcontent = true
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/scripts/release.py b/testing/web-platform/tests/tools/third_party/pluggy/scripts/release.py
new file mode 100644
index 0000000000..e09b8c77b1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/scripts/release.py
@@ -0,0 +1,69 @@
+"""
+Release script.
+"""
+import argparse
+import sys
+from subprocess import check_call
+
+from colorama import init, Fore
+from git import Repo, Remote
+
+
+def create_branch(version):
+ """Create a fresh branch from upstream/main"""
+ repo = Repo.init(".")
+ if repo.is_dirty(untracked_files=True):
+ raise RuntimeError("Repository is dirty, please commit/stash your changes.")
+
+ branch_name = f"release-{version}"
+ print(f"{Fore.CYAN}Create {branch_name} branch from upstream main")
+ upstream = get_upstream(repo)
+ upstream.fetch()
+ release_branch = repo.create_head(branch_name, upstream.refs.main, force=True)
+ release_branch.checkout()
+ return repo
+
+
+def get_upstream(repo: Repo) -> Remote:
+ """Find upstream repository for pluggy on the remotes"""
+ for remote in repo.remotes:
+ for url in remote.urls:
+ if url.endswith(("pytest-dev/pluggy.git", "pytest-dev/pluggy")):
+ return remote
+ raise RuntimeError("could not find pytest-dev/pluggy remote")
+
+
+def pre_release(version):
+ """Generates new docs, release announcements and creates a local tag."""
+ create_branch(version)
+ changelog(version, write_out=True)
+
+ check_call(["git", "commit", "-a", "-m", f"Preparing release {version}"])
+
+ print()
+ print(f"{Fore.GREEN}Please push your branch to your fork and open a PR.")
+
+
+def changelog(version, write_out=False):
+ if write_out:
+ addopts = []
+ else:
+ addopts = ["--draft"]
+ print(f"{Fore.CYAN}Generating CHANGELOG")
+ check_call(["towncrier", "--yes", "--version", version] + addopts)
+
+
+def main():
+ init(autoreset=True)
+ parser = argparse.ArgumentParser()
+ parser.add_argument("version", help="Release version")
+ options = parser.parse_args()
+ try:
+ pre_release(options.version)
+ except RuntimeError as e:
+ print(f"{Fore.RED}ERROR: {e}")
+ return 1
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/scripts/upload-coverage.sh b/testing/web-platform/tests/tools/third_party/pluggy/scripts/upload-coverage.sh
new file mode 100755
index 0000000000..ad3dd48281
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/scripts/upload-coverage.sh
@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+set -e
+set -x
+
+if [ -z "$TOXENV" ]; then
+ python -m pip install coverage
+else
+ # Add last TOXENV to $PATH.
+ PATH="$PWD/.tox/${TOXENV##*,}/bin:$PATH"
+fi
+
+python -m coverage xml
+# Set --connect-timeout to work around https://github.com/curl/curl/issues/4461
+curl -S -L --connect-timeout 5 --retry 6 -s https://codecov.io/bash -o codecov-upload.sh
+bash codecov-upload.sh -Z -X fix -f coverage.xml "$@"
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/setup.cfg b/testing/web-platform/tests/tools/third_party/pluggy/setup.cfg
new file mode 100644
index 0000000000..7040bcb83b
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/setup.cfg
@@ -0,0 +1,52 @@
+[bdist_wheel]
+universal=1
+
+[metadata]
+name = pluggy
+description = plugin and hook calling mechanisms for python
+long_description = file: README.rst
+long_description_content_type = text/x-rst
+license = MIT
+platforms = unix, linux, osx, win32
+author = Holger Krekel
+author_email = holger@merlinux.eu
+url = https://github.com/pytest-dev/pluggy
+classifiers =
+ Development Status :: 6 - Mature
+ Intended Audience :: Developers
+ License :: OSI Approved :: MIT License
+ Operating System :: POSIX
+ Operating System :: Microsoft :: Windows
+ Operating System :: MacOS :: MacOS X
+ Topic :: Software Development :: Testing
+ Topic :: Software Development :: Libraries
+ Topic :: Utilities
+ Programming Language :: Python :: Implementation :: CPython
+ Programming Language :: Python :: Implementation :: PyPy
+ Programming Language :: Python :: 3
+ Programming Language :: Python :: 3 :: Only
+ Programming Language :: Python :: 3.6
+ Programming Language :: Python :: 3.7
+ Programming Language :: Python :: 3.8
+ Programming Language :: Python :: 3.9
+
+[options]
+packages =
+ pluggy
+install_requires =
+ importlib-metadata>=0.12;python_version<"3.8"
+python_requires = >=3.6
+package_dir =
+ =src
+setup_requires =
+ setuptools-scm
+[options.extras_require]
+dev =
+ pre-commit
+ tox
+testing =
+ pytest
+ pytest-benchmark
+
+[devpi:upload]
+formats=sdist.tgz,bdist_wheel
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/setup.py b/testing/web-platform/tests/tools/third_party/pluggy/setup.py
new file mode 100644
index 0000000000..ed442375f7
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/setup.py
@@ -0,0 +1,5 @@
+from setuptools import setup
+
+
+if __name__ == "__main__":
+ setup(use_scm_version={"write_to": "src/pluggy/_version.py"})
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/__init__.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/__init__.py
new file mode 100644
index 0000000000..979028f759
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/__init__.py
@@ -0,0 +1,18 @@
+try:
+ from ._version import version as __version__
+except ImportError:
+ # broken installation, we don't even try
+ # unknown only works because we do poor mans version compare
+ __version__ = "unknown"
+
+__all__ = [
+ "PluginManager",
+ "PluginValidationError",
+ "HookCallError",
+ "HookspecMarker",
+ "HookimplMarker",
+]
+
+from ._manager import PluginManager, PluginValidationError
+from ._callers import HookCallError
+from ._hooks import HookspecMarker, HookimplMarker
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_callers.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_callers.py
new file mode 100644
index 0000000000..7a16f3bdd4
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_callers.py
@@ -0,0 +1,60 @@
+"""
+Call loop machinery
+"""
+import sys
+
+from ._result import HookCallError, _Result, _raise_wrapfail
+
+
+def _multicall(hook_name, hook_impls, caller_kwargs, firstresult):
+ """Execute a call into multiple python functions/methods and return the
+ result(s).
+
+ ``caller_kwargs`` comes from _HookCaller.__call__().
+ """
+ __tracebackhide__ = True
+ results = []
+ excinfo = None
+ try: # run impl and wrapper setup functions in a loop
+ teardowns = []
+ try:
+ for hook_impl in reversed(hook_impls):
+ try:
+ args = [caller_kwargs[argname] for argname in hook_impl.argnames]
+ except KeyError:
+ for argname in hook_impl.argnames:
+ if argname not in caller_kwargs:
+ raise HookCallError(
+ f"hook call must provide argument {argname!r}"
+ )
+
+ if hook_impl.hookwrapper:
+ try:
+ gen = hook_impl.function(*args)
+ next(gen) # first yield
+ teardowns.append(gen)
+ except StopIteration:
+ _raise_wrapfail(gen, "did not yield")
+ else:
+ res = hook_impl.function(*args)
+ if res is not None:
+ results.append(res)
+ if firstresult: # halt further impl calls
+ break
+ except BaseException:
+ excinfo = sys.exc_info()
+ finally:
+ if firstresult: # first result hooks return a single value
+ outcome = _Result(results[0] if results else None, excinfo)
+ else:
+ outcome = _Result(results, excinfo)
+
+ # run all wrapper post-yield blocks
+ for gen in reversed(teardowns):
+ try:
+ gen.send(outcome)
+ _raise_wrapfail(gen, "has second yield")
+ except StopIteration:
+ pass
+
+ return outcome.get_result()
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_hooks.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_hooks.py
new file mode 100644
index 0000000000..1e5fbb7595
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_hooks.py
@@ -0,0 +1,325 @@
+"""
+Internal hook annotation, representation and calling machinery.
+"""
+import inspect
+import sys
+import warnings
+
+
+class HookspecMarker:
+ """Decorator helper class for marking functions as hook specifications.
+
+ You can instantiate it with a project_name to get a decorator.
+ Calling :py:meth:`.PluginManager.add_hookspecs` later will discover all marked functions
+ if the :py:class:`.PluginManager` uses the same project_name.
+ """
+
+ def __init__(self, project_name):
+ self.project_name = project_name
+
+ def __call__(
+ self, function=None, firstresult=False, historic=False, warn_on_impl=None
+ ):
+ """if passed a function, directly sets attributes on the function
+ which will make it discoverable to :py:meth:`.PluginManager.add_hookspecs`.
+ If passed no function, returns a decorator which can be applied to a function
+ later using the attributes supplied.
+
+ If ``firstresult`` is ``True`` the 1:N hook call (N being the number of registered
+ hook implementation functions) will stop at I<=N when the I'th function
+ returns a non-``None`` result.
+
+ If ``historic`` is ``True`` calls to a hook will be memorized and replayed
+ on later registered plugins.
+
+ """
+
+ def setattr_hookspec_opts(func):
+ if historic and firstresult:
+ raise ValueError("cannot have a historic firstresult hook")
+ setattr(
+ func,
+ self.project_name + "_spec",
+ dict(
+ firstresult=firstresult,
+ historic=historic,
+ warn_on_impl=warn_on_impl,
+ ),
+ )
+ return func
+
+ if function is not None:
+ return setattr_hookspec_opts(function)
+ else:
+ return setattr_hookspec_opts
+
+
+class HookimplMarker:
+ """Decorator helper class for marking functions as hook implementations.
+
+ You can instantiate with a ``project_name`` to get a decorator.
+ Calling :py:meth:`.PluginManager.register` later will discover all marked functions
+ if the :py:class:`.PluginManager` uses the same project_name.
+ """
+
+ def __init__(self, project_name):
+ self.project_name = project_name
+
+ def __call__(
+ self,
+ function=None,
+ hookwrapper=False,
+ optionalhook=False,
+ tryfirst=False,
+ trylast=False,
+ specname=None,
+ ):
+
+ """if passed a function, directly sets attributes on the function
+ which will make it discoverable to :py:meth:`.PluginManager.register`.
+ If passed no function, returns a decorator which can be applied to a
+ function later using the attributes supplied.
+
+ If ``optionalhook`` is ``True`` a missing matching hook specification will not result
+ in an error (by default it is an error if no matching spec is found).
+
+ If ``tryfirst`` is ``True`` this hook implementation will run as early as possible
+ in the chain of N hook implementations for a specification.
+
+ If ``trylast`` is ``True`` this hook implementation will run as late as possible
+ in the chain of N hook implementations.
+
+ If ``hookwrapper`` is ``True`` the hook implementations needs to execute exactly
+ one ``yield``. The code before the ``yield`` is run early before any non-hookwrapper
+ function is run. The code after the ``yield`` is run after all non-hookwrapper
+ function have run. The ``yield`` receives a :py:class:`.callers._Result` object
+ representing the exception or result outcome of the inner calls (including other
+ hookwrapper calls).
+
+ If ``specname`` is provided, it will be used instead of the function name when
+ matching this hook implementation to a hook specification during registration.
+
+ """
+
+ def setattr_hookimpl_opts(func):
+ setattr(
+ func,
+ self.project_name + "_impl",
+ dict(
+ hookwrapper=hookwrapper,
+ optionalhook=optionalhook,
+ tryfirst=tryfirst,
+ trylast=trylast,
+ specname=specname,
+ ),
+ )
+ return func
+
+ if function is None:
+ return setattr_hookimpl_opts
+ else:
+ return setattr_hookimpl_opts(function)
+
+
+def normalize_hookimpl_opts(opts):
+ opts.setdefault("tryfirst", False)
+ opts.setdefault("trylast", False)
+ opts.setdefault("hookwrapper", False)
+ opts.setdefault("optionalhook", False)
+ opts.setdefault("specname", None)
+
+
+_PYPY = hasattr(sys, "pypy_version_info")
+
+
+def varnames(func):
+ """Return tuple of positional and keywrord argument names for a function,
+ method, class or callable.
+
+ In case of a class, its ``__init__`` method is considered.
+ For methods the ``self`` parameter is not included.
+ """
+ if inspect.isclass(func):
+ try:
+ func = func.__init__
+ except AttributeError:
+ return (), ()
+ elif not inspect.isroutine(func): # callable object?
+ try:
+ func = getattr(func, "__call__", func)
+ except Exception:
+ return (), ()
+
+ try: # func MUST be a function or method here or we won't parse any args
+ spec = inspect.getfullargspec(func)
+ except TypeError:
+ return (), ()
+
+ args, defaults = tuple(spec.args), spec.defaults
+ if defaults:
+ index = -len(defaults)
+ args, kwargs = args[:index], tuple(args[index:])
+ else:
+ kwargs = ()
+
+ # strip any implicit instance arg
+ # pypy3 uses "obj" instead of "self" for default dunder methods
+ implicit_names = ("self",) if not _PYPY else ("self", "obj")
+ if args:
+ if inspect.ismethod(func) or (
+ "." in getattr(func, "__qualname__", ()) and args[0] in implicit_names
+ ):
+ args = args[1:]
+
+ return args, kwargs
+
+
+class _HookRelay:
+ """hook holder object for performing 1:N hook calls where N is the number
+ of registered plugins.
+
+ """
+
+
+class _HookCaller:
+ def __init__(self, name, hook_execute, specmodule_or_class=None, spec_opts=None):
+ self.name = name
+ self._wrappers = []
+ self._nonwrappers = []
+ self._hookexec = hook_execute
+ self._call_history = None
+ self.spec = None
+ if specmodule_or_class is not None:
+ assert spec_opts is not None
+ self.set_specification(specmodule_or_class, spec_opts)
+
+ def has_spec(self):
+ return self.spec is not None
+
+ def set_specification(self, specmodule_or_class, spec_opts):
+ assert not self.has_spec()
+ self.spec = HookSpec(specmodule_or_class, self.name, spec_opts)
+ if spec_opts.get("historic"):
+ self._call_history = []
+
+ def is_historic(self):
+ return self._call_history is not None
+
+ def _remove_plugin(self, plugin):
+ def remove(wrappers):
+ for i, method in enumerate(wrappers):
+ if method.plugin == plugin:
+ del wrappers[i]
+ return True
+
+ if remove(self._wrappers) is None:
+ if remove(self._nonwrappers) is None:
+ raise ValueError(f"plugin {plugin!r} not found")
+
+ def get_hookimpls(self):
+ # Order is important for _hookexec
+ return self._nonwrappers + self._wrappers
+
+ def _add_hookimpl(self, hookimpl):
+ """Add an implementation to the callback chain."""
+ if hookimpl.hookwrapper:
+ methods = self._wrappers
+ else:
+ methods = self._nonwrappers
+
+ if hookimpl.trylast:
+ methods.insert(0, hookimpl)
+ elif hookimpl.tryfirst:
+ methods.append(hookimpl)
+ else:
+ # find last non-tryfirst method
+ i = len(methods) - 1
+ while i >= 0 and methods[i].tryfirst:
+ i -= 1
+ methods.insert(i + 1, hookimpl)
+
+ def __repr__(self):
+ return f"<_HookCaller {self.name!r}>"
+
+ def __call__(self, *args, **kwargs):
+ if args:
+ raise TypeError("hook calling supports only keyword arguments")
+ assert not self.is_historic()
+
+ # This is written to avoid expensive operations when not needed.
+ if self.spec:
+ for argname in self.spec.argnames:
+ if argname not in kwargs:
+ notincall = tuple(set(self.spec.argnames) - kwargs.keys())
+ warnings.warn(
+ "Argument(s) {} which are declared in the hookspec "
+ "can not be found in this hook call".format(notincall),
+ stacklevel=2,
+ )
+ break
+
+ firstresult = self.spec.opts.get("firstresult")
+ else:
+ firstresult = False
+
+ return self._hookexec(self.name, self.get_hookimpls(), kwargs, firstresult)
+
+ def call_historic(self, result_callback=None, kwargs=None):
+ """Call the hook with given ``kwargs`` for all registered plugins and
+ for all plugins which will be registered afterwards.
+
+ If ``result_callback`` is not ``None`` it will be called for for each
+ non-``None`` result obtained from a hook implementation.
+ """
+ self._call_history.append((kwargs or {}, result_callback))
+ # Historizing hooks don't return results.
+ # Remember firstresult isn't compatible with historic.
+ res = self._hookexec(self.name, self.get_hookimpls(), kwargs, False)
+ if result_callback is None:
+ return
+ for x in res or []:
+ result_callback(x)
+
+ def call_extra(self, methods, kwargs):
+ """Call the hook with some additional temporarily participating
+ methods using the specified ``kwargs`` as call parameters."""
+ old = list(self._nonwrappers), list(self._wrappers)
+ for method in methods:
+ opts = dict(hookwrapper=False, trylast=False, tryfirst=False)
+ hookimpl = HookImpl(None, "<temp>", method, opts)
+ self._add_hookimpl(hookimpl)
+ try:
+ return self(**kwargs)
+ finally:
+ self._nonwrappers, self._wrappers = old
+
+ def _maybe_apply_history(self, method):
+ """Apply call history to a new hookimpl if it is marked as historic."""
+ if self.is_historic():
+ for kwargs, result_callback in self._call_history:
+ res = self._hookexec(self.name, [method], kwargs, False)
+ if res and result_callback is not None:
+ result_callback(res[0])
+
+
+class HookImpl:
+ def __init__(self, plugin, plugin_name, function, hook_impl_opts):
+ self.function = function
+ self.argnames, self.kwargnames = varnames(self.function)
+ self.plugin = plugin
+ self.opts = hook_impl_opts
+ self.plugin_name = plugin_name
+ self.__dict__.update(hook_impl_opts)
+
+ def __repr__(self):
+ return f"<HookImpl plugin_name={self.plugin_name!r}, plugin={self.plugin!r}>"
+
+
+class HookSpec:
+ def __init__(self, namespace, name, opts):
+ self.namespace = namespace
+ self.function = function = getattr(namespace, name)
+ self.name = name
+ self.argnames, self.kwargnames = varnames(function)
+ self.opts = opts
+ self.warn_on_impl = opts.get("warn_on_impl")
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_manager.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_manager.py
new file mode 100644
index 0000000000..65f4e50842
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_manager.py
@@ -0,0 +1,373 @@
+import inspect
+import sys
+import warnings
+
+from . import _tracing
+from ._callers import _Result, _multicall
+from ._hooks import HookImpl, _HookRelay, _HookCaller, normalize_hookimpl_opts
+
+if sys.version_info >= (3, 8):
+ from importlib import metadata as importlib_metadata
+else:
+ import importlib_metadata
+
+
+def _warn_for_function(warning, function):
+ warnings.warn_explicit(
+ warning,
+ type(warning),
+ lineno=function.__code__.co_firstlineno,
+ filename=function.__code__.co_filename,
+ )
+
+
+class PluginValidationError(Exception):
+ """plugin failed validation.
+
+ :param object plugin: the plugin which failed validation,
+ may be a module or an arbitrary object.
+ """
+
+ def __init__(self, plugin, message):
+ self.plugin = plugin
+ super(Exception, self).__init__(message)
+
+
+class DistFacade:
+ """Emulate a pkg_resources Distribution"""
+
+ def __init__(self, dist):
+ self._dist = dist
+
+ @property
+ def project_name(self):
+ return self.metadata["name"]
+
+ def __getattr__(self, attr, default=None):
+ return getattr(self._dist, attr, default)
+
+ def __dir__(self):
+ return sorted(dir(self._dist) + ["_dist", "project_name"])
+
+
+class PluginManager:
+ """Core :py:class:`.PluginManager` class which manages registration
+ of plugin objects and 1:N hook calling.
+
+ You can register new hooks by calling :py:meth:`add_hookspecs(module_or_class)
+ <.PluginManager.add_hookspecs>`.
+ You can register plugin objects (which contain hooks) by calling
+ :py:meth:`register(plugin) <.PluginManager.register>`. The :py:class:`.PluginManager`
+ is initialized with a prefix that is searched for in the names of the dict
+ of registered plugin objects.
+
+ For debugging purposes you can call :py:meth:`.PluginManager.enable_tracing`
+ which will subsequently send debug information to the trace helper.
+ """
+
+ def __init__(self, project_name):
+ self.project_name = project_name
+ self._name2plugin = {}
+ self._plugin2hookcallers = {}
+ self._plugin_distinfo = []
+ self.trace = _tracing.TagTracer().get("pluginmanage")
+ self.hook = _HookRelay()
+ self._inner_hookexec = _multicall
+
+ def _hookexec(self, hook_name, methods, kwargs, firstresult):
+ # called from all hookcaller instances.
+ # enable_tracing will set its own wrapping function at self._inner_hookexec
+ return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
+
+ def register(self, plugin, name=None):
+ """Register a plugin and return its canonical name or ``None`` if the name
+ is blocked from registering. Raise a :py:class:`ValueError` if the plugin
+ is already registered."""
+ plugin_name = name or self.get_canonical_name(plugin)
+
+ if plugin_name in self._name2plugin or plugin in self._plugin2hookcallers:
+ if self._name2plugin.get(plugin_name, -1) is None:
+ return # blocked plugin, return None to indicate no registration
+ raise ValueError(
+ "Plugin already registered: %s=%s\n%s"
+ % (plugin_name, plugin, self._name2plugin)
+ )
+
+ # XXX if an error happens we should make sure no state has been
+ # changed at point of return
+ self._name2plugin[plugin_name] = plugin
+
+ # register matching hook implementations of the plugin
+ self._plugin2hookcallers[plugin] = hookcallers = []
+ for name in dir(plugin):
+ hookimpl_opts = self.parse_hookimpl_opts(plugin, name)
+ if hookimpl_opts is not None:
+ normalize_hookimpl_opts(hookimpl_opts)
+ method = getattr(plugin, name)
+ hookimpl = HookImpl(plugin, plugin_name, method, hookimpl_opts)
+ name = hookimpl_opts.get("specname") or name
+ hook = getattr(self.hook, name, None)
+ if hook is None:
+ hook = _HookCaller(name, self._hookexec)
+ setattr(self.hook, name, hook)
+ elif hook.has_spec():
+ self._verify_hook(hook, hookimpl)
+ hook._maybe_apply_history(hookimpl)
+ hook._add_hookimpl(hookimpl)
+ hookcallers.append(hook)
+ return plugin_name
+
+ def parse_hookimpl_opts(self, plugin, name):
+ method = getattr(plugin, name)
+ if not inspect.isroutine(method):
+ return
+ try:
+ res = getattr(method, self.project_name + "_impl", None)
+ except Exception:
+ res = {}
+ if res is not None and not isinstance(res, dict):
+ # false positive
+ res = None
+ return res
+
+ def unregister(self, plugin=None, name=None):
+ """unregister a plugin object and all its contained hook implementations
+ from internal data structures."""
+ if name is None:
+ assert plugin is not None, "one of name or plugin needs to be specified"
+ name = self.get_name(plugin)
+
+ if plugin is None:
+ plugin = self.get_plugin(name)
+
+ # if self._name2plugin[name] == None registration was blocked: ignore
+ if self._name2plugin.get(name):
+ del self._name2plugin[name]
+
+ for hookcaller in self._plugin2hookcallers.pop(plugin, []):
+ hookcaller._remove_plugin(plugin)
+
+ return plugin
+
+ def set_blocked(self, name):
+ """block registrations of the given name, unregister if already registered."""
+ self.unregister(name=name)
+ self._name2plugin[name] = None
+
+ def is_blocked(self, name):
+ """return ``True`` if the given plugin name is blocked."""
+ return name in self._name2plugin and self._name2plugin[name] is None
+
+ def add_hookspecs(self, module_or_class):
+ """add new hook specifications defined in the given ``module_or_class``.
+ Functions are recognized if they have been decorated accordingly."""
+ names = []
+ for name in dir(module_or_class):
+ spec_opts = self.parse_hookspec_opts(module_or_class, name)
+ if spec_opts is not None:
+ hc = getattr(self.hook, name, None)
+ if hc is None:
+ hc = _HookCaller(name, self._hookexec, module_or_class, spec_opts)
+ setattr(self.hook, name, hc)
+ else:
+ # plugins registered this hook without knowing the spec
+ hc.set_specification(module_or_class, spec_opts)
+ for hookfunction in hc.get_hookimpls():
+ self._verify_hook(hc, hookfunction)
+ names.append(name)
+
+ if not names:
+ raise ValueError(
+ f"did not find any {self.project_name!r} hooks in {module_or_class!r}"
+ )
+
+ def parse_hookspec_opts(self, module_or_class, name):
+ method = getattr(module_or_class, name)
+ return getattr(method, self.project_name + "_spec", None)
+
+ def get_plugins(self):
+ """return the set of registered plugins."""
+ return set(self._plugin2hookcallers)
+
+ def is_registered(self, plugin):
+ """Return ``True`` if the plugin is already registered."""
+ return plugin in self._plugin2hookcallers
+
+ def get_canonical_name(self, plugin):
+ """Return canonical name for a plugin object. Note that a plugin
+ may be registered under a different name which was specified
+ by the caller of :py:meth:`register(plugin, name) <.PluginManager.register>`.
+ To obtain the name of an registered plugin use :py:meth:`get_name(plugin)
+ <.PluginManager.get_name>` instead."""
+ return getattr(plugin, "__name__", None) or str(id(plugin))
+
+ def get_plugin(self, name):
+ """Return a plugin or ``None`` for the given name."""
+ return self._name2plugin.get(name)
+
+ def has_plugin(self, name):
+ """Return ``True`` if a plugin with the given name is registered."""
+ return self.get_plugin(name) is not None
+
+ def get_name(self, plugin):
+ """Return name for registered plugin or ``None`` if not registered."""
+ for name, val in self._name2plugin.items():
+ if plugin == val:
+ return name
+
+ def _verify_hook(self, hook, hookimpl):
+ if hook.is_historic() and hookimpl.hookwrapper:
+ raise PluginValidationError(
+ hookimpl.plugin,
+ "Plugin %r\nhook %r\nhistoric incompatible to hookwrapper"
+ % (hookimpl.plugin_name, hook.name),
+ )
+
+ if hook.spec.warn_on_impl:
+ _warn_for_function(hook.spec.warn_on_impl, hookimpl.function)
+
+ # positional arg checking
+ notinspec = set(hookimpl.argnames) - set(hook.spec.argnames)
+ if notinspec:
+ raise PluginValidationError(
+ hookimpl.plugin,
+ "Plugin %r for hook %r\nhookimpl definition: %s\n"
+ "Argument(s) %s are declared in the hookimpl but "
+ "can not be found in the hookspec"
+ % (
+ hookimpl.plugin_name,
+ hook.name,
+ _formatdef(hookimpl.function),
+ notinspec,
+ ),
+ )
+
+ if hookimpl.hookwrapper and not inspect.isgeneratorfunction(hookimpl.function):
+ raise PluginValidationError(
+ hookimpl.plugin,
+ "Plugin %r for hook %r\nhookimpl definition: %s\n"
+ "Declared as hookwrapper=True but function is not a generator function"
+ % (hookimpl.plugin_name, hook.name, _formatdef(hookimpl.function)),
+ )
+
+ def check_pending(self):
+ """Verify that all hooks which have not been verified against
+ a hook specification are optional, otherwise raise :py:class:`.PluginValidationError`."""
+ for name in self.hook.__dict__:
+ if name[0] != "_":
+ hook = getattr(self.hook, name)
+ if not hook.has_spec():
+ for hookimpl in hook.get_hookimpls():
+ if not hookimpl.optionalhook:
+ raise PluginValidationError(
+ hookimpl.plugin,
+ "unknown hook %r in plugin %r"
+ % (name, hookimpl.plugin),
+ )
+
+ def load_setuptools_entrypoints(self, group, name=None):
+ """Load modules from querying the specified setuptools ``group``.
+
+ :param str group: entry point group to load plugins
+ :param str name: if given, loads only plugins with the given ``name``.
+ :rtype: int
+ :return: return the number of loaded plugins by this call.
+ """
+ count = 0
+ for dist in list(importlib_metadata.distributions()):
+ for ep in dist.entry_points:
+ if (
+ ep.group != group
+ or (name is not None and ep.name != name)
+ # already registered
+ or self.get_plugin(ep.name)
+ or self.is_blocked(ep.name)
+ ):
+ continue
+ plugin = ep.load()
+ self.register(plugin, name=ep.name)
+ self._plugin_distinfo.append((plugin, DistFacade(dist)))
+ count += 1
+ return count
+
+ def list_plugin_distinfo(self):
+ """return list of distinfo/plugin tuples for all setuptools registered
+ plugins."""
+ return list(self._plugin_distinfo)
+
+ def list_name_plugin(self):
+ """return list of name/plugin pairs."""
+ return list(self._name2plugin.items())
+
+ def get_hookcallers(self, plugin):
+ """get all hook callers for the specified plugin."""
+ return self._plugin2hookcallers.get(plugin)
+
+ def add_hookcall_monitoring(self, before, after):
+ """add before/after tracing functions for all hooks
+ and return an undo function which, when called,
+ will remove the added tracers.
+
+ ``before(hook_name, hook_impls, kwargs)`` will be called ahead
+ of all hook calls and receive a hookcaller instance, a list
+ of HookImpl instances and the keyword arguments for the hook call.
+
+ ``after(outcome, hook_name, hook_impls, kwargs)`` receives the
+ same arguments as ``before`` but also a :py:class:`pluggy._callers._Result` object
+ which represents the result of the overall hook call.
+ """
+ oldcall = self._inner_hookexec
+
+ def traced_hookexec(hook_name, hook_impls, kwargs, firstresult):
+ before(hook_name, hook_impls, kwargs)
+ outcome = _Result.from_call(
+ lambda: oldcall(hook_name, hook_impls, kwargs, firstresult)
+ )
+ after(outcome, hook_name, hook_impls, kwargs)
+ return outcome.get_result()
+
+ self._inner_hookexec = traced_hookexec
+
+ def undo():
+ self._inner_hookexec = oldcall
+
+ return undo
+
+ def enable_tracing(self):
+ """enable tracing of hook calls and return an undo function."""
+ hooktrace = self.trace.root.get("hook")
+
+ def before(hook_name, methods, kwargs):
+ hooktrace.root.indent += 1
+ hooktrace(hook_name, kwargs)
+
+ def after(outcome, hook_name, methods, kwargs):
+ if outcome.excinfo is None:
+ hooktrace("finish", hook_name, "-->", outcome.get_result())
+ hooktrace.root.indent -= 1
+
+ return self.add_hookcall_monitoring(before, after)
+
+ def subset_hook_caller(self, name, remove_plugins):
+ """Return a new :py:class:`._hooks._HookCaller` instance for the named method
+ which manages calls to all registered plugins except the
+ ones from remove_plugins."""
+ orig = getattr(self.hook, name)
+ plugins_to_remove = [plug for plug in remove_plugins if hasattr(plug, name)]
+ if plugins_to_remove:
+ hc = _HookCaller(
+ orig.name, orig._hookexec, orig.spec.namespace, orig.spec.opts
+ )
+ for hookimpl in orig.get_hookimpls():
+ plugin = hookimpl.plugin
+ if plugin not in plugins_to_remove:
+ hc._add_hookimpl(hookimpl)
+ # we also keep track of this hook caller so it
+ # gets properly removed on plugin unregistration
+ self._plugin2hookcallers.setdefault(plugin, []).append(hc)
+ return hc
+ return orig
+
+
+def _formatdef(func):
+ return f"{func.__name__}{inspect.signature(func)}"
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_result.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_result.py
new file mode 100644
index 0000000000..4c1f7f1f3c
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_result.py
@@ -0,0 +1,60 @@
+"""
+Hook wrapper "result" utilities.
+"""
+import sys
+
+
+def _raise_wrapfail(wrap_controller, msg):
+ co = wrap_controller.gi_code
+ raise RuntimeError(
+ "wrap_controller at %r %s:%d %s"
+ % (co.co_name, co.co_filename, co.co_firstlineno, msg)
+ )
+
+
+class HookCallError(Exception):
+ """Hook was called wrongly."""
+
+
+class _Result:
+ def __init__(self, result, excinfo):
+ self._result = result
+ self._excinfo = excinfo
+
+ @property
+ def excinfo(self):
+ return self._excinfo
+
+ @classmethod
+ def from_call(cls, func):
+ __tracebackhide__ = True
+ result = excinfo = None
+ try:
+ result = func()
+ except BaseException:
+ excinfo = sys.exc_info()
+
+ return cls(result, excinfo)
+
+ def force_result(self, result):
+ """Force the result(s) to ``result``.
+
+ If the hook was marked as a ``firstresult`` a single value should
+ be set otherwise set a (modified) list of results. Any exceptions
+ found during invocation will be deleted.
+ """
+ self._result = result
+ self._excinfo = None
+
+ def get_result(self):
+ """Get the result(s) for this hook call.
+
+ If the hook was marked as a ``firstresult`` only a single value
+ will be returned otherwise a list of results.
+ """
+ __tracebackhide__ = True
+ if self._excinfo is None:
+ return self._result
+ else:
+ ex = self._excinfo
+ raise ex[1].with_traceback(ex[2])
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_tracing.py b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_tracing.py
new file mode 100644
index 0000000000..82c016271e
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/src/pluggy/_tracing.py
@@ -0,0 +1,62 @@
+"""
+Tracing utils
+"""
+
+
+class TagTracer:
+ def __init__(self):
+ self._tags2proc = {}
+ self._writer = None
+ self.indent = 0
+
+ def get(self, name):
+ return TagTracerSub(self, (name,))
+
+ def _format_message(self, tags, args):
+ if isinstance(args[-1], dict):
+ extra = args[-1]
+ args = args[:-1]
+ else:
+ extra = {}
+
+ content = " ".join(map(str, args))
+ indent = " " * self.indent
+
+ lines = ["{}{} [{}]\n".format(indent, content, ":".join(tags))]
+
+ for name, value in extra.items():
+ lines.append(f"{indent} {name}: {value}\n")
+
+ return "".join(lines)
+
+ def _processmessage(self, tags, args):
+ if self._writer is not None and args:
+ self._writer(self._format_message(tags, args))
+ try:
+ processor = self._tags2proc[tags]
+ except KeyError:
+ pass
+ else:
+ processor(tags, args)
+
+ def setwriter(self, writer):
+ self._writer = writer
+
+ def setprocessor(self, tags, processor):
+ if isinstance(tags, str):
+ tags = tuple(tags.split(":"))
+ else:
+ assert isinstance(tags, tuple)
+ self._tags2proc[tags] = processor
+
+
+class TagTracerSub:
+ def __init__(self, root, tags):
+ self.root = root
+ self.tags = tags
+
+ def __call__(self, *args):
+ self.root._processmessage(self.tags, args)
+
+ def get(self, name):
+ return self.__class__(self.root, self.tags + (name,))
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/benchmark.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/benchmark.py
new file mode 100644
index 0000000000..b0d4b9536a
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/benchmark.py
@@ -0,0 +1,102 @@
+"""
+Benchmarking and performance tests.
+"""
+import pytest
+from pluggy import HookspecMarker, HookimplMarker, PluginManager
+from pluggy._hooks import HookImpl
+from pluggy._callers import _multicall
+
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+@hookimpl
+def hook(arg1, arg2, arg3):
+ return arg1, arg2, arg3
+
+
+@hookimpl(hookwrapper=True)
+def wrapper(arg1, arg2, arg3):
+ yield
+
+
+@pytest.fixture(params=[10, 100], ids="hooks={}".format)
+def hooks(request):
+ return [hook for i in range(request.param)]
+
+
+@pytest.fixture(params=[10, 100], ids="wrappers={}".format)
+def wrappers(request):
+ return [wrapper for i in range(request.param)]
+
+
+def test_hook_and_wrappers_speed(benchmark, hooks, wrappers):
+ def setup():
+ hook_name = "foo"
+ hook_impls = []
+ for method in hooks + wrappers:
+ f = HookImpl(None, "<temp>", method, method.example_impl)
+ hook_impls.append(f)
+ caller_kwargs = {"arg1": 1, "arg2": 2, "arg3": 3}
+ firstresult = False
+ return (hook_name, hook_impls, caller_kwargs, firstresult), {}
+
+ benchmark.pedantic(_multicall, setup=setup)
+
+
+@pytest.mark.parametrize(
+ ("plugins, wrappers, nesting"),
+ [
+ (1, 1, 0),
+ (1, 1, 1),
+ (1, 1, 5),
+ (1, 5, 1),
+ (1, 5, 5),
+ (5, 1, 1),
+ (5, 1, 5),
+ (5, 5, 1),
+ (5, 5, 5),
+ (20, 20, 0),
+ (100, 100, 0),
+ ],
+)
+def test_call_hook(benchmark, plugins, wrappers, nesting):
+ pm = PluginManager("example")
+
+ class HookSpec:
+ @hookspec
+ def fun(self, hooks, nesting: int):
+ yield
+
+ class Plugin:
+ def __init__(self, num):
+ self.num = num
+
+ def __repr__(self):
+ return f"<Plugin {self.num}>"
+
+ @hookimpl
+ def fun(self, hooks, nesting: int):
+ if nesting:
+ hooks.fun(hooks=hooks, nesting=nesting - 1)
+
+ class PluginWrap:
+ def __init__(self, num):
+ self.num = num
+
+ def __repr__(self):
+ return f"<PluginWrap {self.num}>"
+
+ @hookimpl(hookwrapper=True)
+ def fun(self):
+ yield
+
+ pm.add_hookspecs(HookSpec)
+
+ for i in range(plugins):
+ pm.register(Plugin(i), name=f"plug_{i}")
+ for i in range(wrappers):
+ pm.register(PluginWrap(i), name=f"wrap_plug_{i}")
+
+ benchmark(pm.hook.fun, hooks=pm.hook, nesting=nesting)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/conftest.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/conftest.py
new file mode 100644
index 0000000000..1fd4ecd5bd
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/conftest.py
@@ -0,0 +1,26 @@
+import pytest
+
+
+@pytest.fixture(
+ params=[lambda spec: spec, lambda spec: spec()],
+ ids=["spec-is-class", "spec-is-instance"],
+)
+def he_pm(request, pm):
+ from pluggy import HookspecMarker
+
+ hookspec = HookspecMarker("example")
+
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ return arg + 1
+
+ pm.add_hookspecs(request.param(Hooks))
+ return pm
+
+
+@pytest.fixture
+def pm():
+ from pluggy import PluginManager
+
+ return PluginManager("example")
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_details.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_details.py
new file mode 100644
index 0000000000..0ceb3b3eb1
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_details.py
@@ -0,0 +1,135 @@
+import warnings
+import pytest
+from pluggy import PluginManager, HookimplMarker, HookspecMarker
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+def test_parse_hookimpl_override():
+ class MyPluginManager(PluginManager):
+ def parse_hookimpl_opts(self, module_or_class, name):
+ opts = PluginManager.parse_hookimpl_opts(self, module_or_class, name)
+ if opts is None:
+ if name.startswith("x1"):
+ opts = {}
+ return opts
+
+ class Plugin:
+ def x1meth(self):
+ pass
+
+ @hookimpl(hookwrapper=True, tryfirst=True)
+ def x1meth2(self):
+ yield # pragma: no cover
+
+ class Spec:
+ @hookspec
+ def x1meth(self):
+ pass
+
+ @hookspec
+ def x1meth2(self):
+ pass
+
+ pm = MyPluginManager(hookspec.project_name)
+ pm.register(Plugin())
+ pm.add_hookspecs(Spec)
+ assert not pm.hook.x1meth._nonwrappers[0].hookwrapper
+ assert not pm.hook.x1meth._nonwrappers[0].tryfirst
+ assert not pm.hook.x1meth._nonwrappers[0].trylast
+ assert not pm.hook.x1meth._nonwrappers[0].optionalhook
+
+ assert pm.hook.x1meth2._wrappers[0].tryfirst
+ assert pm.hook.x1meth2._wrappers[0].hookwrapper
+
+
+def test_warn_when_deprecated_specified(recwarn):
+ warning = DeprecationWarning("foo is deprecated")
+
+ class Spec:
+ @hookspec(warn_on_impl=warning)
+ def foo(self):
+ pass
+
+ class Plugin:
+ @hookimpl
+ def foo(self):
+ pass
+
+ pm = PluginManager(hookspec.project_name)
+ pm.add_hookspecs(Spec)
+
+ with pytest.warns(DeprecationWarning) as records:
+ pm.register(Plugin())
+ (record,) = records
+ assert record.message is warning
+ assert record.filename == Plugin.foo.__code__.co_filename
+ assert record.lineno == Plugin.foo.__code__.co_firstlineno
+
+
+def test_plugin_getattr_raises_errors():
+ """Pluggy must be able to handle plugins which raise weird exceptions
+ when getattr() gets called (#11).
+ """
+
+ class DontTouchMe:
+ def __getattr__(self, x):
+ raise Exception("cant touch me")
+
+ class Module:
+ pass
+
+ module = Module()
+ module.x = DontTouchMe()
+
+ pm = PluginManager(hookspec.project_name)
+ # register() would raise an error
+ pm.register(module, "donttouch")
+ assert pm.get_plugin("donttouch") is module
+
+
+def test_warning_on_call_vs_hookspec_arg_mismatch():
+ """Verify that is a hook is called with less arguments then defined in the
+ spec that a warning is emitted.
+ """
+
+ class Spec:
+ @hookspec
+ def myhook(self, arg1, arg2):
+ pass
+
+ class Plugin:
+ @hookimpl
+ def myhook(self, arg1):
+ pass
+
+ pm = PluginManager(hookspec.project_name)
+ pm.register(Plugin())
+ pm.add_hookspecs(Spec())
+
+ with warnings.catch_warnings(record=True) as warns:
+ warnings.simplefilter("always")
+
+ # calling should trigger a warning
+ pm.hook.myhook(arg1=1)
+
+ assert len(warns) == 1
+ warning = warns[-1]
+ assert issubclass(warning.category, Warning)
+ assert "Argument(s) ('arg2',)" in str(warning.message)
+
+
+def test_repr():
+ class Plugin:
+ @hookimpl
+ def myhook(self):
+ raise NotImplementedError()
+
+ pm = PluginManager(hookspec.project_name)
+
+ plugin = Plugin()
+ pname = pm.register(plugin)
+ assert repr(pm.hook.myhook._nonwrappers[0]) == (
+ f"<HookImpl plugin_name={pname!r}, plugin={plugin!r}>"
+ )
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_helpers.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_helpers.py
new file mode 100644
index 0000000000..465858c499
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_helpers.py
@@ -0,0 +1,84 @@
+from pluggy._hooks import varnames
+from pluggy._manager import _formatdef
+
+
+def test_varnames():
+ def f(x):
+ i = 3 # noqa
+
+ class A:
+ def f(self, y):
+ pass
+
+ class B:
+ def __call__(self, z):
+ pass
+
+ assert varnames(f) == (("x",), ())
+ assert varnames(A().f) == (("y",), ())
+ assert varnames(B()) == (("z",), ())
+
+
+def test_varnames_default():
+ def f(x, y=3):
+ pass
+
+ assert varnames(f) == (("x",), ("y",))
+
+
+def test_varnames_class():
+ class C:
+ def __init__(self, x):
+ pass
+
+ class D:
+ pass
+
+ class E:
+ def __init__(self, x):
+ pass
+
+ class F:
+ pass
+
+ assert varnames(C) == (("x",), ())
+ assert varnames(D) == ((), ())
+ assert varnames(E) == (("x",), ())
+ assert varnames(F) == ((), ())
+
+
+def test_varnames_keyword_only():
+ def f1(x, *, y):
+ pass
+
+ def f2(x, *, y=3):
+ pass
+
+ def f3(x=1, *, y=3):
+ pass
+
+ assert varnames(f1) == (("x",), ())
+ assert varnames(f2) == (("x",), ())
+ assert varnames(f3) == ((), ("x",))
+
+
+def test_formatdef():
+ def function1():
+ pass
+
+ assert _formatdef(function1) == "function1()"
+
+ def function2(arg1):
+ pass
+
+ assert _formatdef(function2) == "function2(arg1)"
+
+ def function3(arg1, arg2="qwe"):
+ pass
+
+ assert _formatdef(function3) == "function3(arg1, arg2='qwe')"
+
+ def function4(arg1, *args, **kwargs):
+ pass
+
+ assert _formatdef(function4) == "function4(arg1, *args, **kwargs)"
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_hookcaller.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_hookcaller.py
new file mode 100644
index 0000000000..9eeaef8666
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_hookcaller.py
@@ -0,0 +1,272 @@
+import pytest
+
+from pluggy import HookimplMarker, HookspecMarker, PluginValidationError
+from pluggy._hooks import HookImpl
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+@pytest.fixture
+def hc(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+ return pm.hook.he_method1
+
+
+@pytest.fixture
+def addmeth(hc):
+ def addmeth(tryfirst=False, trylast=False, hookwrapper=False):
+ def wrap(func):
+ hookimpl(tryfirst=tryfirst, trylast=trylast, hookwrapper=hookwrapper)(func)
+ hc._add_hookimpl(HookImpl(None, "<temp>", func, func.example_impl))
+ return func
+
+ return wrap
+
+ return addmeth
+
+
+def funcs(hookmethods):
+ return [hookmethod.function for hookmethod in hookmethods]
+
+
+def test_adding_nonwrappers(hc, addmeth):
+ @addmeth()
+ def he_method1():
+ pass
+
+ @addmeth()
+ def he_method2():
+ pass
+
+ @addmeth()
+ def he_method3():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1, he_method2, he_method3]
+
+
+def test_adding_nonwrappers_trylast(hc, addmeth):
+ @addmeth()
+ def he_method1_middle():
+ pass
+
+ @addmeth(trylast=True)
+ def he_method1():
+ pass
+
+ @addmeth()
+ def he_method1_b():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b]
+
+
+def test_adding_nonwrappers_trylast3(hc, addmeth):
+ @addmeth()
+ def he_method1_a():
+ pass
+
+ @addmeth(trylast=True)
+ def he_method1_b():
+ pass
+
+ @addmeth()
+ def he_method1_c():
+ pass
+
+ @addmeth(trylast=True)
+ def he_method1_d():
+ pass
+
+ assert funcs(hc._nonwrappers) == [
+ he_method1_d,
+ he_method1_b,
+ he_method1_a,
+ he_method1_c,
+ ]
+
+
+def test_adding_nonwrappers_trylast2(hc, addmeth):
+ @addmeth()
+ def he_method1_middle():
+ pass
+
+ @addmeth()
+ def he_method1_b():
+ pass
+
+ @addmeth(trylast=True)
+ def he_method1():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1, he_method1_middle, he_method1_b]
+
+
+def test_adding_nonwrappers_tryfirst(hc, addmeth):
+ @addmeth(tryfirst=True)
+ def he_method1():
+ pass
+
+ @addmeth()
+ def he_method1_middle():
+ pass
+
+ @addmeth()
+ def he_method1_b():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1_middle, he_method1_b, he_method1]
+
+
+def test_adding_wrappers_ordering(hc, addmeth):
+ @addmeth(hookwrapper=True)
+ def he_method1():
+ pass
+
+ @addmeth()
+ def he_method1_middle():
+ pass
+
+ @addmeth(hookwrapper=True)
+ def he_method3():
+ pass
+
+ assert funcs(hc._nonwrappers) == [he_method1_middle]
+ assert funcs(hc._wrappers) == [he_method1, he_method3]
+
+
+def test_adding_wrappers_ordering_tryfirst(hc, addmeth):
+ @addmeth(hookwrapper=True, tryfirst=True)
+ def he_method1():
+ pass
+
+ @addmeth(hookwrapper=True)
+ def he_method2():
+ pass
+
+ assert hc._nonwrappers == []
+ assert funcs(hc._wrappers) == [he_method2, he_method1]
+
+
+def test_hookspec(pm):
+ class HookSpec:
+ @hookspec()
+ def he_myhook1(arg1):
+ pass
+
+ @hookspec(firstresult=True)
+ def he_myhook2(arg1):
+ pass
+
+ @hookspec(firstresult=False)
+ def he_myhook3(arg1):
+ pass
+
+ pm.add_hookspecs(HookSpec)
+ assert not pm.hook.he_myhook1.spec.opts["firstresult"]
+ assert pm.hook.he_myhook2.spec.opts["firstresult"]
+ assert not pm.hook.he_myhook3.spec.opts["firstresult"]
+
+
+@pytest.mark.parametrize("name", ["hookwrapper", "optionalhook", "tryfirst", "trylast"])
+@pytest.mark.parametrize("val", [True, False])
+def test_hookimpl(name, val):
+ @hookimpl(**{name: val})
+ def he_myhook1(arg1):
+ pass
+
+ if val:
+ assert he_myhook1.example_impl.get(name)
+ else:
+ assert not hasattr(he_myhook1, name)
+
+
+def test_hookrelay_registry(pm):
+ """Verify hook caller instances are registered by name onto the relay
+ and can be likewise unregistered."""
+
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+ hook = pm.hook
+ assert hasattr(hook, "hello")
+ assert repr(hook.hello).find("hello") != -1
+
+ class Plugin:
+ @hookimpl
+ def hello(self, arg):
+ return arg + 1
+
+ plugin = Plugin()
+ pm.register(plugin)
+ out = hook.hello(arg=3)
+ assert out == [4]
+ assert not hasattr(hook, "world")
+ pm.unregister(plugin)
+ assert hook.hello(arg=3) == []
+
+
+def test_hookrelay_registration_by_specname(pm):
+ """Verify hook caller instances may also be registered by specifying a
+ specname option to the hookimpl"""
+
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+ hook = pm.hook
+ assert hasattr(hook, "hello")
+ assert len(pm.hook.hello.get_hookimpls()) == 0
+
+ class Plugin:
+ @hookimpl(specname="hello")
+ def foo(self, arg):
+ return arg + 1
+
+ plugin = Plugin()
+ pm.register(plugin)
+ out = hook.hello(arg=3)
+ assert out == [4]
+
+
+def test_hookrelay_registration_by_specname_raises(pm):
+ """Verify using specname still raises the types of errors during registration as it
+ would have without using specname."""
+
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ # make sure a bad signature still raises an error when using specname
+ class Plugin:
+ @hookimpl(specname="hello")
+ def foo(self, arg, too, many, args):
+ return arg + 1
+
+ with pytest.raises(PluginValidationError):
+ pm.register(Plugin())
+
+ # make sure check_pending still fails if specname doesn't have a
+ # corresponding spec. EVEN if the function name matches one.
+ class Plugin2:
+ @hookimpl(specname="bar")
+ def hello(self, arg):
+ return arg + 1
+
+ pm.register(Plugin2())
+ with pytest.raises(PluginValidationError):
+ pm.check_pending()
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_invocations.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_invocations.py
new file mode 100644
index 0000000000..323b9b21e8
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_invocations.py
@@ -0,0 +1,215 @@
+import pytest
+from pluggy import PluginValidationError, HookimplMarker, HookspecMarker
+
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+def test_argmismatch(pm):
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ class Plugin:
+ @hookimpl
+ def hello(self, argwrong):
+ pass
+
+ with pytest.raises(PluginValidationError) as exc:
+ pm.register(Plugin())
+
+ assert "argwrong" in str(exc.value)
+
+
+def test_only_kwargs(pm):
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+ with pytest.raises(TypeError) as exc:
+ pm.hook.hello(3)
+
+ comprehensible = "hook calling supports only keyword arguments"
+ assert comprehensible in str(exc.value)
+
+
+def test_opt_in_args(pm):
+ """Verfiy that two hookimpls with mutex args can serve
+ under the same spec.
+ """
+
+ class Api:
+ @hookspec
+ def hello(self, arg1, arg2, common_arg):
+ "api hook 1"
+
+ class Plugin1:
+ @hookimpl
+ def hello(self, arg1, common_arg):
+ return arg1 + common_arg
+
+ class Plugin2:
+ @hookimpl
+ def hello(self, arg2, common_arg):
+ return arg2 + common_arg
+
+ pm.add_hookspecs(Api)
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+
+ results = pm.hook.hello(arg1=1, arg2=2, common_arg=0)
+ assert results == [2, 1]
+
+
+def test_call_order(pm):
+ class Api:
+ @hookspec
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ class Plugin1:
+ @hookimpl
+ def hello(self, arg):
+ return 1
+
+ class Plugin2:
+ @hookimpl
+ def hello(self, arg):
+ return 2
+
+ class Plugin3:
+ @hookimpl
+ def hello(self, arg):
+ return 3
+
+ class Plugin4:
+ @hookimpl(hookwrapper=True)
+ def hello(self, arg):
+ assert arg == 0
+ outcome = yield
+ assert outcome.get_result() == [3, 2, 1]
+
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+ pm.register(Plugin3())
+ pm.register(Plugin4()) # hookwrapper should get same list result
+ res = pm.hook.hello(arg=0)
+ assert res == [3, 2, 1]
+
+
+def test_firstresult_definition(pm):
+ class Api:
+ @hookspec(firstresult=True)
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ class Plugin1:
+ @hookimpl
+ def hello(self, arg):
+ return arg + 1
+
+ class Plugin2:
+ @hookimpl
+ def hello(self, arg):
+ return arg - 1
+
+ class Plugin3:
+ @hookimpl
+ def hello(self, arg):
+ return None
+
+ class Plugin4:
+ @hookimpl(hookwrapper=True)
+ def hello(self, arg):
+ assert arg == 3
+ outcome = yield
+ assert outcome.get_result() == 2
+
+ pm.register(Plugin1()) # discarded - not the last registered plugin
+ pm.register(Plugin2()) # used as result
+ pm.register(Plugin3()) # None result is ignored
+ pm.register(Plugin4()) # hookwrapper should get same non-list result
+ res = pm.hook.hello(arg=3)
+ assert res == 2
+
+
+def test_firstresult_force_result(pm):
+ """Verify forcing a result in a wrapper."""
+
+ class Api:
+ @hookspec(firstresult=True)
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ class Plugin1:
+ @hookimpl
+ def hello(self, arg):
+ return arg + 1
+
+ class Plugin2:
+ @hookimpl(hookwrapper=True)
+ def hello(self, arg):
+ assert arg == 3
+ outcome = yield
+ assert outcome.get_result() == 4
+ outcome.force_result(0)
+
+ class Plugin3:
+ @hookimpl
+ def hello(self, arg):
+ return None
+
+ pm.register(Plugin1())
+ pm.register(Plugin2()) # wrapper
+ pm.register(Plugin3()) # ignored since returns None
+ res = pm.hook.hello(arg=3)
+ assert res == 0 # this result is forced and not a list
+
+
+def test_firstresult_returns_none(pm):
+ """If None results are returned by underlying implementations ensure
+ the multi-call loop returns a None value.
+ """
+
+ class Api:
+ @hookspec(firstresult=True)
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+
+ class Plugin1:
+ @hookimpl
+ def hello(self, arg):
+ return None
+
+ pm.register(Plugin1())
+ res = pm.hook.hello(arg=3)
+ assert res is None
+
+
+def test_firstresult_no_plugin(pm):
+ """If no implementations/plugins have been registered for a firstresult
+ hook the multi-call loop should return a None value.
+ """
+
+ class Api:
+ @hookspec(firstresult=True)
+ def hello(self, arg):
+ "api hook 1"
+
+ pm.add_hookspecs(Api)
+ res = pm.hook.hello(arg=3)
+ assert res is None
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_multicall.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_multicall.py
new file mode 100644
index 0000000000..8ffb452f69
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_multicall.py
@@ -0,0 +1,147 @@
+import pytest
+from pluggy import HookCallError, HookspecMarker, HookimplMarker
+from pluggy._hooks import HookImpl
+from pluggy._callers import _multicall
+
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+def MC(methods, kwargs, firstresult=False):
+ caller = _multicall
+ hookfuncs = []
+ for method in methods:
+ f = HookImpl(None, "<temp>", method, method.example_impl)
+ hookfuncs.append(f)
+ return caller("foo", hookfuncs, kwargs, firstresult)
+
+
+def test_keyword_args():
+ @hookimpl
+ def f(x):
+ return x + 1
+
+ class A:
+ @hookimpl
+ def f(self, x, y):
+ return x + y
+
+ reslist = MC([f, A().f], dict(x=23, y=24))
+ assert reslist == [24 + 23, 24]
+
+
+def test_keyword_args_with_defaultargs():
+ @hookimpl
+ def f(x, z=1):
+ return x + z
+
+ reslist = MC([f], dict(x=23, y=24))
+ assert reslist == [24]
+
+
+def test_tags_call_error():
+ @hookimpl
+ def f(x):
+ return x
+
+ with pytest.raises(HookCallError):
+ MC([f], {})
+
+
+def test_call_none_is_no_result():
+ @hookimpl
+ def m1():
+ return 1
+
+ @hookimpl
+ def m2():
+ return None
+
+ res = MC([m1, m2], {}, firstresult=True)
+ assert res == 1
+ res = MC([m1, m2], {}, {})
+ assert res == [1]
+
+
+def test_hookwrapper():
+ out = []
+
+ @hookimpl(hookwrapper=True)
+ def m1():
+ out.append("m1 init")
+ yield None
+ out.append("m1 finish")
+
+ @hookimpl
+ def m2():
+ out.append("m2")
+ return 2
+
+ res = MC([m2, m1], {})
+ assert res == [2]
+ assert out == ["m1 init", "m2", "m1 finish"]
+ out[:] = []
+ res = MC([m2, m1], {}, firstresult=True)
+ assert res == 2
+ assert out == ["m1 init", "m2", "m1 finish"]
+
+
+def test_hookwrapper_order():
+ out = []
+
+ @hookimpl(hookwrapper=True)
+ def m1():
+ out.append("m1 init")
+ yield 1
+ out.append("m1 finish")
+
+ @hookimpl(hookwrapper=True)
+ def m2():
+ out.append("m2 init")
+ yield 2
+ out.append("m2 finish")
+
+ res = MC([m2, m1], {})
+ assert res == []
+ assert out == ["m1 init", "m2 init", "m2 finish", "m1 finish"]
+
+
+def test_hookwrapper_not_yield():
+ @hookimpl(hookwrapper=True)
+ def m1():
+ pass
+
+ with pytest.raises(TypeError):
+ MC([m1], {})
+
+
+def test_hookwrapper_too_many_yield():
+ @hookimpl(hookwrapper=True)
+ def m1():
+ yield 1
+ yield 2
+
+ with pytest.raises(RuntimeError) as ex:
+ MC([m1], {})
+ assert "m1" in str(ex.value)
+ assert (__file__ + ":") in str(ex.value)
+
+
+@pytest.mark.parametrize("exc", [ValueError, SystemExit])
+def test_hookwrapper_exception(exc):
+ out = []
+
+ @hookimpl(hookwrapper=True)
+ def m1():
+ out.append("m1 init")
+ yield None
+ out.append("m1 finish")
+
+ @hookimpl
+ def m2():
+ raise exc
+
+ with pytest.raises(exc):
+ MC([m2, m1], {})
+ assert out == ["m1 init", "m1 finish"]
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_pluginmanager.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_pluginmanager.py
new file mode 100644
index 0000000000..304a007a58
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_pluginmanager.py
@@ -0,0 +1,544 @@
+"""
+``PluginManager`` unit and public API testing.
+"""
+import pytest
+
+from pluggy import (
+ PluginValidationError,
+ HookCallError,
+ HookimplMarker,
+ HookspecMarker,
+)
+from pluggy._manager import importlib_metadata
+
+
+hookspec = HookspecMarker("example")
+hookimpl = HookimplMarker("example")
+
+
+def test_plugin_double_register(pm):
+ """Registering the same plugin more then once isn't allowed"""
+ pm.register(42, name="abc")
+ with pytest.raises(ValueError):
+ pm.register(42, name="abc")
+ with pytest.raises(ValueError):
+ pm.register(42, name="def")
+
+
+def test_pm(pm):
+ """Basic registration with objects"""
+
+ class A:
+ pass
+
+ a1, a2 = A(), A()
+ pm.register(a1)
+ assert pm.is_registered(a1)
+ pm.register(a2, "hello")
+ assert pm.is_registered(a2)
+ out = pm.get_plugins()
+ assert a1 in out
+ assert a2 in out
+ assert pm.get_plugin("hello") == a2
+ assert pm.unregister(a1) == a1
+ assert not pm.is_registered(a1)
+
+ out = pm.list_name_plugin()
+ assert len(out) == 1
+ assert out == [("hello", a2)]
+
+
+def test_has_plugin(pm):
+ class A:
+ pass
+
+ a1 = A()
+ pm.register(a1, "hello")
+ assert pm.is_registered(a1)
+ assert pm.has_plugin("hello")
+
+
+def test_register_dynamic_attr(he_pm):
+ class A:
+ def __getattr__(self, name):
+ if name[0] != "_":
+ return 42
+ raise AttributeError()
+
+ a = A()
+ he_pm.register(a)
+ assert not he_pm.get_hookcallers(a)
+
+
+def test_pm_name(pm):
+ class A:
+ pass
+
+ a1 = A()
+ name = pm.register(a1, name="hello")
+ assert name == "hello"
+ pm.unregister(a1)
+ assert pm.get_plugin(a1) is None
+ assert not pm.is_registered(a1)
+ assert not pm.get_plugins()
+ name2 = pm.register(a1, name="hello")
+ assert name2 == name
+ pm.unregister(name="hello")
+ assert pm.get_plugin(a1) is None
+ assert not pm.is_registered(a1)
+ assert not pm.get_plugins()
+
+
+def test_set_blocked(pm):
+ class A:
+ pass
+
+ a1 = A()
+ name = pm.register(a1)
+ assert pm.is_registered(a1)
+ assert not pm.is_blocked(name)
+ pm.set_blocked(name)
+ assert pm.is_blocked(name)
+ assert not pm.is_registered(a1)
+
+ pm.set_blocked("somename")
+ assert pm.is_blocked("somename")
+ assert not pm.register(A(), "somename")
+ pm.unregister(name="somename")
+ assert pm.is_blocked("somename")
+
+
+def test_register_mismatch_method(he_pm):
+ class hello:
+ @hookimpl
+ def he_method_notexists(self):
+ pass
+
+ plugin = hello()
+
+ he_pm.register(plugin)
+ with pytest.raises(PluginValidationError) as excinfo:
+ he_pm.check_pending()
+ assert excinfo.value.plugin is plugin
+
+
+def test_register_mismatch_arg(he_pm):
+ class hello:
+ @hookimpl
+ def he_method1(self, qlwkje):
+ pass
+
+ plugin = hello()
+
+ with pytest.raises(PluginValidationError) as excinfo:
+ he_pm.register(plugin)
+ assert excinfo.value.plugin is plugin
+
+
+def test_register_hookwrapper_not_a_generator_function(he_pm):
+ class hello:
+ @hookimpl(hookwrapper=True)
+ def he_method1(self):
+ pass # pragma: no cover
+
+ plugin = hello()
+
+ with pytest.raises(PluginValidationError, match="generator function") as excinfo:
+ he_pm.register(plugin)
+ assert excinfo.value.plugin is plugin
+
+
+def test_register(pm):
+ class MyPlugin:
+ pass
+
+ my = MyPlugin()
+ pm.register(my)
+ assert my in pm.get_plugins()
+ my2 = MyPlugin()
+ pm.register(my2)
+ assert {my, my2}.issubset(pm.get_plugins())
+
+ assert pm.is_registered(my)
+ assert pm.is_registered(my2)
+ pm.unregister(my)
+ assert not pm.is_registered(my)
+ assert my not in pm.get_plugins()
+
+
+def test_register_unknown_hooks(pm):
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg + 1
+
+ pname = pm.register(Plugin1())
+
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+ # assert not pm._unverified_hooks
+ assert pm.hook.he_method1(arg=1) == [2]
+ assert len(pm.get_hookcallers(pm.get_plugin(pname))) == 1
+
+
+def test_register_historic(pm):
+ class Hooks:
+ @hookspec(historic=True)
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ pm.hook.he_method1.call_historic(kwargs=dict(arg=1))
+ out = []
+
+ class Plugin:
+ @hookimpl
+ def he_method1(self, arg):
+ out.append(arg)
+
+ pm.register(Plugin())
+ assert out == [1]
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ out.append(arg * 10)
+
+ pm.register(Plugin2())
+ assert out == [1, 10]
+ pm.hook.he_method1.call_historic(kwargs=dict(arg=12))
+ assert out == [1, 10, 120, 12]
+
+
+@pytest.mark.parametrize("result_callback", [True, False])
+def test_with_result_memorized(pm, result_callback):
+ """Verify that ``_HookCaller._maybe_apply_history()`
+ correctly applies the ``result_callback`` function, when provided,
+ to the result from calling each newly registered hook.
+ """
+ out = []
+ if result_callback:
+
+ def callback(res):
+ out.append(res)
+
+ else:
+ callback = None
+
+ class Hooks:
+ @hookspec(historic=True)
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 10
+
+ pm.register(Plugin1())
+
+ he_method1 = pm.hook.he_method1
+ he_method1.call_historic(result_callback=callback, kwargs=dict(arg=1))
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 10
+
+ pm.register(Plugin2())
+ if result_callback:
+ assert out == [10, 10]
+ else:
+ assert out == []
+
+
+def test_with_callbacks_immediately_executed(pm):
+ class Hooks:
+ @hookspec(historic=True)
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 10
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 20
+
+ class Plugin3:
+ @hookimpl
+ def he_method1(self, arg):
+ return arg * 30
+
+ out = []
+ pm.register(Plugin1())
+ pm.register(Plugin2())
+
+ he_method1 = pm.hook.he_method1
+ he_method1.call_historic(lambda res: out.append(res), dict(arg=1))
+ assert out == [20, 10]
+ pm.register(Plugin3())
+ assert out == [20, 10, 30]
+
+
+def test_register_historic_incompat_hookwrapper(pm):
+ class Hooks:
+ @hookspec(historic=True)
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ out = []
+
+ class Plugin:
+ @hookimpl(hookwrapper=True)
+ def he_method1(self, arg):
+ out.append(arg)
+
+ with pytest.raises(PluginValidationError):
+ pm.register(Plugin())
+
+
+def test_call_extra(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ def he_method1(arg):
+ return arg * 10
+
+ out = pm.hook.he_method1.call_extra([he_method1], dict(arg=1))
+ assert out == [10]
+
+
+def test_call_with_too_few_args(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ 0 / 0
+
+ pm.register(Plugin1())
+ with pytest.raises(HookCallError):
+ with pytest.warns(UserWarning):
+ pm.hook.he_method1()
+
+
+def test_subset_hook_caller(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+
+ out = []
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ out.append(arg)
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ out.append(arg * 10)
+
+ class PluginNo:
+ pass
+
+ plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo()
+ pm.register(plugin1)
+ pm.register(plugin2)
+ pm.register(plugin3)
+ pm.hook.he_method1(arg=1)
+ assert out == [10, 1]
+ out[:] = []
+
+ hc = pm.subset_hook_caller("he_method1", [plugin1])
+ hc(arg=2)
+ assert out == [20]
+ out[:] = []
+
+ hc = pm.subset_hook_caller("he_method1", [plugin2])
+ hc(arg=2)
+ assert out == [2]
+ out[:] = []
+
+ pm.unregister(plugin1)
+ hc(arg=2)
+ assert out == []
+ out[:] = []
+
+ pm.hook.he_method1(arg=1)
+ assert out == [10]
+
+
+def test_get_hookimpls(pm):
+ class Hooks:
+ @hookspec
+ def he_method1(self, arg):
+ pass
+
+ pm.add_hookspecs(Hooks)
+ assert pm.hook.he_method1.get_hookimpls() == []
+
+ class Plugin1:
+ @hookimpl
+ def he_method1(self, arg):
+ pass
+
+ class Plugin2:
+ @hookimpl
+ def he_method1(self, arg):
+ pass
+
+ class PluginNo:
+ pass
+
+ plugin1, plugin2, plugin3 = Plugin1(), Plugin2(), PluginNo()
+ pm.register(plugin1)
+ pm.register(plugin2)
+ pm.register(plugin3)
+
+ hookimpls = pm.hook.he_method1.get_hookimpls()
+ hook_plugins = [item.plugin for item in hookimpls]
+ assert hook_plugins == [plugin1, plugin2]
+
+
+def test_add_hookspecs_nohooks(pm):
+ with pytest.raises(ValueError):
+ pm.add_hookspecs(10)
+
+
+def test_load_setuptools_instantiation(monkeypatch, pm):
+ class EntryPoint:
+ name = "myname"
+ group = "hello"
+ value = "myname:foo"
+
+ def load(self):
+ class PseudoPlugin:
+ x = 42
+
+ return PseudoPlugin()
+
+ class Distribution:
+ entry_points = (EntryPoint(),)
+
+ dist = Distribution()
+
+ def my_distributions():
+ return (dist,)
+
+ monkeypatch.setattr(importlib_metadata, "distributions", my_distributions)
+ num = pm.load_setuptools_entrypoints("hello")
+ assert num == 1
+ plugin = pm.get_plugin("myname")
+ assert plugin.x == 42
+ ret = pm.list_plugin_distinfo()
+ # poor man's `assert ret == [(plugin, mock.ANY)]`
+ assert len(ret) == 1
+ assert len(ret[0]) == 2
+ assert ret[0][0] == plugin
+ assert ret[0][1]._dist == dist
+ num = pm.load_setuptools_entrypoints("hello")
+ assert num == 0 # no plugin loaded by this call
+
+
+def test_add_tracefuncs(he_pm):
+ out = []
+
+ class api1:
+ @hookimpl
+ def he_method1(self):
+ out.append("he_method1-api1")
+
+ class api2:
+ @hookimpl
+ def he_method1(self):
+ out.append("he_method1-api2")
+
+ he_pm.register(api1())
+ he_pm.register(api2())
+
+ def before(hook_name, hook_impls, kwargs):
+ out.append((hook_name, list(hook_impls), kwargs))
+
+ def after(outcome, hook_name, hook_impls, kwargs):
+ out.append((outcome, hook_name, list(hook_impls), kwargs))
+
+ undo = he_pm.add_hookcall_monitoring(before, after)
+
+ he_pm.hook.he_method1(arg=1)
+ assert len(out) == 4
+ assert out[0][0] == "he_method1"
+ assert len(out[0][1]) == 2
+ assert isinstance(out[0][2], dict)
+ assert out[1] == "he_method1-api2"
+ assert out[2] == "he_method1-api1"
+ assert len(out[3]) == 4
+ assert out[3][1] == out[0][0]
+
+ undo()
+ he_pm.hook.he_method1(arg=1)
+ assert len(out) == 4 + 2
+
+
+def test_hook_tracing(he_pm):
+ saveindent = []
+
+ class api1:
+ @hookimpl
+ def he_method1(self):
+ saveindent.append(he_pm.trace.root.indent)
+
+ class api2:
+ @hookimpl
+ def he_method1(self):
+ saveindent.append(he_pm.trace.root.indent)
+ raise ValueError()
+
+ he_pm.register(api1())
+ out = []
+ he_pm.trace.root.setwriter(out.append)
+ undo = he_pm.enable_tracing()
+ try:
+ indent = he_pm.trace.root.indent
+ he_pm.hook.he_method1(arg=1)
+ assert indent == he_pm.trace.root.indent
+ assert len(out) == 2
+ assert "he_method1" in out[0]
+ assert "finish" in out[1]
+
+ out[:] = []
+ he_pm.register(api2())
+
+ with pytest.raises(ValueError):
+ he_pm.hook.he_method1(arg=1)
+ assert he_pm.trace.root.indent == indent
+ assert saveindent[0] > indent
+ finally:
+ undo()
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/testing/test_tracer.py b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_tracer.py
new file mode 100644
index 0000000000..992ec67914
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/testing/test_tracer.py
@@ -0,0 +1,78 @@
+from pluggy._tracing import TagTracer
+
+import pytest
+
+
+@pytest.fixture
+def rootlogger():
+ return TagTracer()
+
+
+def test_simple(rootlogger):
+ log = rootlogger.get("pytest")
+ log("hello")
+ out = []
+ rootlogger.setwriter(out.append)
+ log("world")
+ assert len(out) == 1
+ assert out[0] == "world [pytest]\n"
+ sublog = log.get("collection")
+ sublog("hello")
+ assert out[1] == "hello [pytest:collection]\n"
+
+
+def test_indent(rootlogger):
+ log = rootlogger.get("1")
+ out = []
+ log.root.setwriter(lambda arg: out.append(arg))
+ log("hello")
+ log.root.indent += 1
+ log("line1")
+ log("line2")
+ log.root.indent += 1
+ log("line3")
+ log("line4")
+ log.root.indent -= 1
+ log("line5")
+ log.root.indent -= 1
+ log("last")
+ assert len(out) == 7
+ names = [x[: x.rfind(" [")] for x in out]
+ assert names == [
+ "hello",
+ " line1",
+ " line2",
+ " line3",
+ " line4",
+ " line5",
+ "last",
+ ]
+
+
+def test_readable_output_dictargs(rootlogger):
+
+ out = rootlogger._format_message(["test"], [1])
+ assert out == "1 [test]\n"
+
+ out2 = rootlogger._format_message(["test"], ["test", {"a": 1}])
+ assert out2 == "test [test]\n a: 1\n"
+
+
+def test_setprocessor(rootlogger):
+ log = rootlogger.get("1")
+ log2 = log.get("2")
+ assert log2.tags == tuple("12")
+ out = []
+ rootlogger.setprocessor(tuple("12"), lambda *args: out.append(args))
+ log("not seen")
+ log2("seen")
+ assert len(out) == 1
+ tags, args = out[0]
+ assert "1" in tags
+ assert "2" in tags
+ assert args == ("seen",)
+ l2 = []
+ rootlogger.setprocessor("1:2", lambda *args: l2.append(args))
+ log2("seen")
+ tags, args = l2[0]
+ assert args == ("seen",)
diff --git a/testing/web-platform/tests/tools/third_party/pluggy/tox.ini b/testing/web-platform/tests/tools/third_party/pluggy/tox.ini
new file mode 100644
index 0000000000..97b3eb7792
--- /dev/null
+++ b/testing/web-platform/tests/tools/third_party/pluggy/tox.ini
@@ -0,0 +1,57 @@
+[tox]
+envlist=linting,docs,py{36,37,38,39,py3},py{36,37}-pytest{main}
+
+[testenv]
+commands=
+ {env:_PLUGGY_TOX_CMD:pytest} {posargs}
+ coverage: coverage report -m
+ coverage: coverage xml
+setenv=
+ _PYTEST_SETUP_SKIP_PLUGGY_DEP=1
+ coverage: _PLUGGY_TOX_CMD=coverage run -m pytest
+extras=testing
+deps=
+ coverage: coverage
+ pytestmain: git+https://github.com/pytest-dev/pytest.git@main
+
+[testenv:benchmark]
+commands=pytest {posargs:testing/benchmark.py}
+deps=
+ pytest
+ pytest-benchmark
+
+[testenv:linting]
+skip_install = true
+basepython = python3
+deps = pre-commit
+commands = pre-commit run --all-files --show-diff-on-failure
+
+[testenv:docs]
+deps =
+ sphinx
+ pygments
+commands =
+ sphinx-build -W -b html {toxinidir}/docs {toxinidir}/build/html-docs
+
+[pytest]
+minversion=2.0
+testpaths = testing
+#--pyargs --doctest-modules --ignore=.tox
+addopts=-r a
+filterwarnings =
+ error
+
+[flake8]
+max-line-length=99
+
+[testenv:release]
+decription = do a release, required posarg of the version number
+basepython = python3
+skipsdist = True
+usedevelop = True
+passenv = *
+deps =
+ colorama
+ gitpython
+ towncrier
+commands = python scripts/release.py {posargs}