diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 09:22:09 +0000 |
commit | 43a97878ce14b72f0981164f87f2e35e14151312 (patch) | |
tree | 620249daf56c0258faa40cbdcf9cfba06de2a846 /testing/web-platform/tests/tools/third_party/pluggy | |
parent | Initial commit. (diff) | |
download | firefox-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')
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 Binary files differnew file mode 100644 index 0000000000..3339f8a608 --- /dev/null +++ b/testing/web-platform/tests/tools/third_party/pluggy/docs/_static/img/plug.png 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} |