Retire Packaging Deb project repos
This commit is part of a series to retire the Packaging Deb project. Step 2 is to remove all content from the project repos, replacing it with a README notification where to find ongoing work, and how to recover the repo if needed at some future point (as in https://docs.openstack.org/infra/manual/drivers.html#retiring-a-project). Change-Id: I079aae2ad46fe5c5352bcfd5b9122c0b352a44fb
38
.gitignore
vendored
@@ -1,38 +0,0 @@
|
||||
*.py[co]
|
||||
|
||||
# Packages
|
||||
*.egg
|
||||
*.egg-info
|
||||
dist
|
||||
build
|
||||
eggs
|
||||
parts
|
||||
bin
|
||||
var
|
||||
sdist
|
||||
develop-eggs
|
||||
.installed.cfg
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
.coverage
|
||||
.tox
|
||||
|
||||
#Translations
|
||||
*.mo
|
||||
|
||||
#Mr Developer
|
||||
.mr.developer.cfg
|
||||
|
||||
# Editors
|
||||
tags
|
||||
|
||||
# pbr generated files
|
||||
AUTHORS
|
||||
ChangeLog
|
||||
/.testrepository/
|
||||
|
||||
# reno build
|
||||
releasenotes/build
|
@@ -1,4 +0,0 @@
|
||||
[gerrit]
|
||||
host=review.openstack.org
|
||||
port=29418
|
||||
project=openstack/stevedore.git
|
@@ -1,4 +0,0 @@
|
||||
[DEFAULT]
|
||||
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} ${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION
|
||||
test_id_option=--load-list $IDFILE
|
||||
test_list_option=--list
|
13
.travis.yml
@@ -1,13 +0,0 @@
|
||||
language: python
|
||||
python:
|
||||
- "2.7"
|
||||
- "3.2"
|
||||
- "3.3"
|
||||
- "pypy"
|
||||
install:
|
||||
- pip install flake8 --use-mirrors
|
||||
- pip install nose mock --use-mirrors
|
||||
- pip install -q . --use-mirrors
|
||||
before_script:
|
||||
- flake8 stevedore setup.py
|
||||
script: nosetests stevedore
|
@@ -1,10 +0,0 @@
|
||||
Changes should be submitted for review via the Gerrit tool,
|
||||
following the workflow documented at:
|
||||
|
||||
http://docs.openstack.org/infra/manual/developers.html#development-workflow
|
||||
|
||||
Pull requests submitted through GitHub will be ignored.
|
||||
|
||||
Bugs should be filed on Launchpad, not GitHub:
|
||||
|
||||
https://bugs.launchpad.net/python-stevedore
|
202
LICENSE
@@ -1,202 +0,0 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
@@ -1,5 +0,0 @@
|
||||
include setup.py
|
||||
recursive-include docs *.rst *.py *.html *.css *.js *.png *.txt *.jpg
|
||||
recursive-include tests *.py
|
||||
include tox.ini
|
||||
include LICENSE
|
14
README
Normal file
@@ -0,0 +1,14 @@
|
||||
This project is no longer maintained.
|
||||
|
||||
The contents of this repository are still available in the Git
|
||||
source code management system. To see the contents of this
|
||||
repository before it reached its end of life, please check out the
|
||||
previous commit with "git checkout HEAD^1".
|
||||
|
||||
For ongoing work on maintaining OpenStack packages in the Debian
|
||||
distribution, please see the Debian OpenStack packaging team at
|
||||
https://wiki.debian.org/OpenStack/.
|
||||
|
||||
For any further questions, please email
|
||||
openstack-dev@lists.openstack.org or join #openstack-dev on
|
||||
Freenode.
|
32
README.rst
@@ -1,32 +0,0 @@
|
||||
===========================================================
|
||||
stevedore -- Manage dynamic plugins for Python applications
|
||||
===========================================================
|
||||
|
||||
.. image:: https://img.shields.io/pypi/v/stevedore.svg
|
||||
:target: https://pypi.python.org/pypi/stevedore/
|
||||
:alt: Latest Version
|
||||
|
||||
.. image:: https://img.shields.io/pypi/dm/stevedore.svg
|
||||
:target: https://pypi.python.org/pypi/stevedore/
|
||||
:alt: Downloads
|
||||
|
||||
.. image:: http://governance.openstack.org/badges/stevedore.svg
|
||||
:target: http://governance.openstack.org/reference/tags/index.html
|
||||
|
||||
Python makes loading code dynamically easy, allowing you to configure
|
||||
and extend your application by discovering and loading extensions
|
||||
("*plugins*") at runtime. Many applications implement their own
|
||||
library for doing this, using ``__import__`` or ``importlib``.
|
||||
stevedore avoids creating yet another extension
|
||||
mechanism by building on top of `setuptools entry points`_. The code
|
||||
for managing entry points tends to be repetitive, though, so stevedore
|
||||
provides manager classes for implementing common patterns for using
|
||||
dynamically loaded extensions.
|
||||
|
||||
.. _setuptools entry points: http://setuptools.readthedocs.io/en/latest/pkg_resources.html?#entry-points
|
||||
|
||||
* Free software: Apache license
|
||||
* Documentation: https://docs.openstack.org/stevedore/latest
|
||||
* Source: https://git.openstack.org/cgit/openstack/stevedore
|
||||
* Bugs: https://bugs.launchpad.net/python-stevedore
|
||||
|
38
announce.rst
@@ -1,38 +0,0 @@
|
||||
================
|
||||
stevedore 0.14
|
||||
================
|
||||
|
||||
.. tags:: stevedore release python
|
||||
|
||||
What is stevedore?
|
||||
==================
|
||||
|
||||
Python makes loading code dynamically easy, allowing you to configure
|
||||
and extend your application by discovering and loading extensions
|
||||
("*plugins*") at runtime. Many applications implement their own
|
||||
library for doing this, using ``__import__`` or
|
||||
``importlib``. stevedore_ avoids creating yet another extension
|
||||
mechanism by building on top of `setuptools entry points`_. The code
|
||||
for managing entry points tends to be repetitive, though, so stevedore
|
||||
provides manager classes for implementing common patterns for using
|
||||
dynamically loaded extensions.
|
||||
|
||||
.. _stevedore: http://stevedore.readthedocs.org
|
||||
|
||||
.. _setuptools entry points: https://setuptools.readthedocs.io/en/latest/pkg_resources.html#convenience-api
|
||||
|
||||
|
||||
What's New?
|
||||
===========
|
||||
|
||||
- Provide an option to control requirements checking when loading
|
||||
plugins, and disable it by default. This removes protection against
|
||||
loading the wrong version of a plugin, or that plugin's
|
||||
dependencies.
|
||||
|
||||
|
||||
Installing
|
||||
==========
|
||||
|
||||
Visit the stevedore_ project page for download links and installation
|
||||
instructions.
|
153
doc/Makefile
@@ -1,153 +0,0 @@
|
||||
# Makefile for Sphinx documentation
|
||||
#
|
||||
|
||||
# You can set these variables from the command line.
|
||||
SPHINXOPTS =
|
||||
SPHINXBUILD = sphinx-build
|
||||
PAPER =
|
||||
BUILDDIR = build
|
||||
|
||||
# Internal variables.
|
||||
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||
PAPEROPT_letter = -D latex_paper_size=letter
|
||||
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
# the i18n builder cannot share the environment and doctrees with the others
|
||||
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
|
||||
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext
|
||||
|
||||
help:
|
||||
@echo "Please use \`make <target>' where <target> is one of"
|
||||
@echo " html to make standalone HTML files"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@echo " singlehtml to make a single large HTML file"
|
||||
@echo " pickle to make pickle files"
|
||||
@echo " json to make JSON files"
|
||||
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||
@echo " qthelp to make HTML files and a qthelp project"
|
||||
@echo " devhelp to make HTML files and a Devhelp project"
|
||||
@echo " epub to make an epub"
|
||||
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||
@echo " text to make text files"
|
||||
@echo " man to make manual pages"
|
||||
@echo " texinfo to make Texinfo files"
|
||||
@echo " info to make Texinfo files and run them through makeinfo"
|
||||
@echo " gettext to make PO message catalogs"
|
||||
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||
@echo " linkcheck to check all external links for integrity"
|
||||
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||
|
||||
dirhtml:
|
||||
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||
|
||||
singlehtml:
|
||||
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||
@echo
|
||||
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||
|
||||
pickle:
|
||||
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||
@echo
|
||||
@echo "Build finished; now you can process the pickle files."
|
||||
|
||||
json:
|
||||
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||
@echo
|
||||
@echo "Build finished; now you can process the JSON files."
|
||||
|
||||
htmlhelp:
|
||||
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||
|
||||
qthelp:
|
||||
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/stevedore.qhcp"
|
||||
@echo "To view the help file:"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/stevedore.qhc"
|
||||
|
||||
devhelp:
|
||||
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/stevedore"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/stevedore"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub:
|
||||
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||
@echo
|
||||
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||
|
||||
latex:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo
|
||||
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||
"(use \`make latexpdf' here to do that automatically)."
|
||||
|
||||
latexpdf:
|
||||
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||
@echo "Running LaTeX files through pdflatex..."
|
||||
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||
|
||||
text:
|
||||
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||
@echo
|
||||
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||
|
||||
man:
|
||||
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||
@echo
|
||||
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||
|
||||
texinfo:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo
|
||||
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||
"(use \`make info' here to do that automatically)."
|
||||
|
||||
info:
|
||||
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||
@echo "Running Texinfo files through makeinfo..."
|
||||
make -C $(BUILDDIR)/texinfo info
|
||||
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||
|
||||
gettext:
|
||||
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||
@echo
|
||||
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||
|
||||
changes:
|
||||
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||
@echo
|
||||
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||
|
||||
linkcheck:
|
||||
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||
@echo
|
||||
@echo "Link check complete; look for any errors in the above output " \
|
||||
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||
|
||||
doctest:
|
||||
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||
@echo "Testing of doctests in the sources finished, look at the " \
|
||||
"results in $(BUILDDIR)/doctest/output.txt."
|
@@ -1,265 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# stevedore documentation build configuration file, created by
|
||||
# sphinx-quickstart on Sun Jul 22 14:01:09 2012.
|
||||
#
|
||||
# This file is execfile()d with the current directory set to its containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
import datetime
|
||||
import subprocess
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
#sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration -----------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
#needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||
extensions = [
|
||||
'sphinx.ext.autodoc',
|
||||
'sphinx.ext.todo',
|
||||
'sphinx.ext.graphviz',
|
||||
'sphinx.ext.extlinks',
|
||||
'openstackdocstheme',
|
||||
'stevedore.sphinxext',
|
||||
]
|
||||
|
||||
# openstackdocstheme options
|
||||
repository_name = 'openstack/stevedore'
|
||||
bug_project = 'python-stevedore'
|
||||
bug_tag = ''
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'stevedore'
|
||||
copyright = u'2016, DreamHost'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = subprocess.Popen(['sh', '-c', 'cd ../..; python setup.py --version'],
|
||||
stdout=subprocess.PIPE).stdout.read()
|
||||
version = version.strip()
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
release = version
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
#language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
#today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
#today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
#add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
#add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
#show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
#modindex_common_prefix = []
|
||||
|
||||
|
||||
# -- Options for HTML output ---------------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
#html_theme = 'default'
|
||||
html_theme = 'openstackdocs'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
#html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
#html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
#html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
#html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
#html_static_path = ['_static']
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
#html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
#html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
#html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
#html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
#html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
#html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'stevedoredoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output --------------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
#'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
#'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
#'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'stevedore.tex', u'stevedore Documentation',
|
||||
u'DreamHost', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
#latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
#latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
#latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
# man_pages = [
|
||||
# ('index', 'stevedore', u'stevedore Documentation',
|
||||
# [u'DreamHost'], 1)
|
||||
# ]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
#man_show_urls = False
|
||||
|
||||
|
||||
# -- 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 = [
|
||||
('index', 'stevedore', u'stevedore Documentation',
|
||||
u'DreamHost', 'stevedore', 'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
#texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
#texinfo_show_urls = 'footnote'
|
||||
|
||||
extlinks = {
|
||||
'issue': ('https://github.com/dreamhost/stevedore/issues/%s', 'issue '),
|
||||
}
|
||||
|
||||
autodoc_default_flags = ['members', 'special-members', 'show-inheritance']
|
@@ -1,29 +0,0 @@
|
||||
=============================================================
|
||||
stevedore -- Manage Dynamic Plugins for Python Applications
|
||||
=============================================================
|
||||
|
||||
Python makes loading code dynamically easy, allowing you to configure
|
||||
and extend your application by discovering and loading extensions
|
||||
("*plugins*") at runtime. Many applications implement their own
|
||||
library for doing this, using ``__import__`` or
|
||||
:mod:`importlib`. stevedore avoids creating yet another extension
|
||||
mechanism by building on top of `setuptools entry points`_. The code
|
||||
for managing entry points tends to be repetitive, though, so stevedore
|
||||
provides manager classes for implementing common patterns for using
|
||||
dynamically loaded extensions.
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:maxdepth: 2
|
||||
|
||||
user/index
|
||||
reference/index
|
||||
install/index
|
||||
|
||||
|
||||
.. _setuptools entry points: http://setuptools.readthedocs.io/en/latest/pkg_resources.html?#entry-points
|
||||
|
||||
.. rubric:: Indices and tables
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`search`
|
@@ -1,49 +0,0 @@
|
||||
============
|
||||
Installation
|
||||
============
|
||||
|
||||
Python Versions
|
||||
===============
|
||||
|
||||
stevedore is tested under Python 2.7 and 3.4.
|
||||
|
||||
.. _install-basic:
|
||||
|
||||
Basic Installation
|
||||
==================
|
||||
|
||||
stevedore should be installed into the same site-packages area where
|
||||
the application and extensions are installed (either a virtualenv or
|
||||
the global site-packages). You may need administrative privileges to
|
||||
do that. The easiest way to install it is using pip_::
|
||||
|
||||
$ pip install stevedore
|
||||
|
||||
or::
|
||||
|
||||
$ sudo pip install stevedore
|
||||
|
||||
.. _pip: http://pypi.python.org/pypi/pip
|
||||
|
||||
Download
|
||||
========
|
||||
|
||||
stevedore releases are hosted on PyPI and can be downloaded from:
|
||||
http://pypi.python.org/pypi/stevedore
|
||||
|
||||
Source Code
|
||||
===========
|
||||
|
||||
The source is hosted on the OpenStack infrastructure: https://git.openstack.org/cgit/openstack/stevedore/
|
||||
|
||||
Entry point inspector
|
||||
=====================
|
||||
|
||||
To list entrypoints and registered plugins this tool can be also very useful:
|
||||
https://pypi.python.org/pypi/entry_point_inspector
|
||||
|
||||
Reporting Bugs
|
||||
==============
|
||||
|
||||
Please report bugs through the launchpad project:
|
||||
https://launchpad.net/python-stevedore
|
@@ -1,51 +0,0 @@
|
||||
===========================
|
||||
Extension Manager Classes
|
||||
===========================
|
||||
|
||||
DriverManager
|
||||
=============
|
||||
|
||||
.. autoclass:: stevedore.driver.DriverManager
|
||||
|
||||
HookManager
|
||||
===========
|
||||
|
||||
.. autoclass:: stevedore.hook.HookManager
|
||||
|
||||
NamedExtensionManager
|
||||
=====================
|
||||
|
||||
.. autoclass:: stevedore.named.NamedExtensionManager
|
||||
|
||||
EnabledExtensionManager
|
||||
=======================
|
||||
|
||||
.. autoclass:: stevedore.enabled.EnabledExtensionManager
|
||||
|
||||
DispatchExtensionManager
|
||||
========================
|
||||
|
||||
.. autoclass:: stevedore.dispatch.DispatchExtensionManager
|
||||
|
||||
NameDispatchExtensionManager
|
||||
============================
|
||||
|
||||
.. autoclass:: stevedore.dispatch.NameDispatchExtensionManager
|
||||
|
||||
ExtensionManager
|
||||
================
|
||||
|
||||
.. autoclass:: stevedore.extension.ExtensionManager
|
||||
|
||||
Extension
|
||||
=========
|
||||
|
||||
.. autoclass:: stevedore.extension.Extension
|
||||
:members:
|
||||
:show-inheritance:
|
||||
:no-special-members:
|
||||
|
||||
TestExtensionManager
|
||||
====================
|
||||
|
||||
.. autoclass:: stevedore.tests.manager.TestExtensionManager
|
Before Width: | Height: | Size: 92 KiB |
Before Width: | Height: | Size: 84 KiB |
Before Width: | Height: | Size: 88 KiB |
Before Width: | Height: | Size: 70 KiB |
Before Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 80 KiB |
Before Width: | Height: | Size: 76 KiB |
@@ -1,480 +0,0 @@
|
||||
=================================================================
|
||||
Dynamic Code Patterns: Extending Your Applications with Plugins
|
||||
=================================================================
|
||||
|
||||
This essay is based on my PyCon 2013 presentation of the same
|
||||
title. The presentation was recorded and the video is `available
|
||||
online
|
||||
<http://pyvideo.org/video/1789/dynamic-code-patterns-extending-your-application>`__,
|
||||
as are `the slides
|
||||
<http://www.slideshare.net/doughellmann/dynamic-codepatterns>`__.
|
||||
|
||||
Over the past few years I have been doing a lot of work on
|
||||
applications that make heavy use of Python’s ability to load code
|
||||
dynamically at runtime. This essay includes the results of some
|
||||
focused research that I did into patterns for using dynamic code, and
|
||||
the impact that research had on the design of stevedore and
|
||||
ceilometer, an application that uses it
|
||||
|
||||
For my analysis, I counted any application or framework that loads
|
||||
code dynamically at runtime to be using plugins. I did not consider
|
||||
delayed execution of hard-coded import statements as plugins, but
|
||||
restricted the research to true dynamic loading. In most cases, the
|
||||
name or location of the code is given through some external mechanism
|
||||
like a configuration file.
|
||||
|
||||
Why Use Plugins?
|
||||
================
|
||||
|
||||
Before examining patterns for using plugins, we should talk about why
|
||||
to use plugins in an application at all.
|
||||
|
||||
One important benefit is improved design. Keeping a separation between
|
||||
core and extension code encourages you to think more about
|
||||
abstractions in your design. Building an extensible system can take
|
||||
more work than hardwiring everything, but the results tend to be more
|
||||
flexible and maintainable over the long term.
|
||||
|
||||
Plugins are a good way to implement device drivers and other versions
|
||||
of the `Strategy pattern`_. The application can maintain generic core
|
||||
logic, and the plugin can handle the details for interfacing with an
|
||||
outside system or device.
|
||||
|
||||
.. _Strategy pattern: http://en.wikipedia.org/wiki/Strategy_pattern
|
||||
|
||||
Packaging extensions separately reduces dependency bloat and makes
|
||||
deployment easier to manage and install. Users who do not need some
|
||||
drivers or features can avoid deploying dependencies that are only
|
||||
used by those plugins.
|
||||
|
||||
Plugins also provide a convenient way to extend the feature set of an
|
||||
application by hooking new code into well-defined extension points.
|
||||
And having such an extensible system makes it easier for other
|
||||
developers to contribute to your project indirectly by providing
|
||||
add-on packages that are released separately.
|
||||
|
||||
Requirements for Ceilometer
|
||||
===========================
|
||||
|
||||
I have spent a fair bit of time studying plugin-based architectures
|
||||
over the last year while helping to create ceilometer_, the new
|
||||
metering component in OpenStack_. Ceilometer measures the resources
|
||||
being used in a cloud deployment so we can bill the tenants for those
|
||||
resources. We collect data like the lifetimes of servers, along with
|
||||
bandwidth and storage consumed.
|
||||
|
||||
.. _OpenStack: http://openstack.org
|
||||
.. _ceilometer: https://launchpad.net/ceilometer
|
||||
|
||||
However, the type and number of things a given cloud deployer will
|
||||
want to charge for will vary, so we wanted a flexible system for
|
||||
taking those measurements. We need to allow deployers to write their
|
||||
own plugins to measure things we haven’t thought of, or that may need
|
||||
to be measured in a way that is private to their configuration (a case
|
||||
we have at DreamHost).
|
||||
|
||||
We are expecting a lot of developers who don’t interact with us
|
||||
directly to be trying to write extensions to ceilometer for use in
|
||||
private cloud deployments, so it is also important to clearly document
|
||||
how to create new plugins.
|
||||
|
||||
With these things in mind, we designed ceilometer to be flexible in
|
||||
several different areas.
|
||||
|
||||
.. image:: ceilometer-design.jpg
|
||||
:scale: 50
|
||||
:align: center
|
||||
|
||||
OpenStack is a collection of components that cooperate to provide
|
||||
`Infrastructure as a Service`_ features. Each component manages a
|
||||
different aspect of the cloud and uses a message bus to communicate
|
||||
with the other components.
|
||||
|
||||
All of the components generate notification messages when events
|
||||
happen (like instances being created or destroyed). Capturing those
|
||||
messages was the first source of data for ceilometer. The
|
||||
notifications contain different metadata depending on the resource
|
||||
that triggered the event, so we needed plugins to translate the
|
||||
notification messages into a standard format for metering.
|
||||
|
||||
.. _Infrastructure as a Service: http://en.wikipedia.org/wiki/Cloud_computing
|
||||
|
||||
There aren’t events for all of the things we want to measure for
|
||||
billing, so we also had to create some agents to poll for data. For
|
||||
example, we want to check periodically to see how much CPU capacity
|
||||
each instance has consumed. Some pollsters run on the hypervisor
|
||||
server, and others run on a management server where they can
|
||||
communicate with the other OpenStack components via their APIs.
|
||||
|
||||
All of the ceilometer services use another message bus to deliver data
|
||||
to a collector process which uses a storage driver to write data to a
|
||||
database. We support relational and non-relational databases,
|
||||
depending on the choice of the deployer.
|
||||
|
||||
This architecture resulted in five sets of plugins for ceilometer.
|
||||
OpenStack includes a message bus abstraction layer and a set of
|
||||
drivers for using RabbitMQ, Qpid, and ZMQ. Since that was already
|
||||
implemented for us, we didn’t have to touch it.
|
||||
|
||||
The other 4 sets we created from scratch:
|
||||
|
||||
* The plugins for processing notification messages
|
||||
* The pollsters for the compute nodes
|
||||
* The central pollsters
|
||||
* And the storage driver
|
||||
|
||||
The resulting designs use patterns found in other applications and
|
||||
frameworks that use plugins.
|
||||
|
||||
Other Plugin-based Applications
|
||||
===============================
|
||||
|
||||
During my research, I looked at a few projects that I was already
|
||||
familiar with, either as a user or a developer, and some I had not
|
||||
used before. There are plenty of other examples, but this list was
|
||||
long enough to identify some common patterns and help us with our
|
||||
design.
|
||||
|
||||
Blogofile_ and Sphinx_ are two apps for working with different forms
|
||||
of text for publishing. They use extensions to add new content
|
||||
processing features.
|
||||
|
||||
.. _Blogofile: http://blogofile.com/
|
||||
.. _Sphinx: http://sphinx-doc.org/
|
||||
|
||||
Mercurial_ is a command line app that can be extended with new
|
||||
subcommands. Cliff_ is a library I created for building apps like
|
||||
Mercurial.
|
||||
|
||||
.. _Mercurial: http://mercurial.selenic.com/
|
||||
.. _Cliff: http://cliff.readthedocs.org
|
||||
|
||||
Virtualenvwrapper_ is a command line tool that uses hooks in a
|
||||
different way, to extend existing commands but not necessarily add new
|
||||
ones.
|
||||
|
||||
.. _Virtualenvwrapper: http://virtualenvwrapper.readthedocs.org
|
||||
|
||||
Nose_ and Trac_ are common developer tools. You’re more likely to have
|
||||
used them than written extensions for them, but they do both use
|
||||
plugins.
|
||||
|
||||
.. _Nose: https://nose.readthedocs.org/
|
||||
.. _Trac: http://trac.edgewall.org/
|
||||
|
||||
Django_, Pyramid_, and SQLAlchemy_ are developer libraries that use
|
||||
plugins.
|
||||
|
||||
.. _Django: https://www.djangoproject.com
|
||||
.. _Pyramid: http://docs.pylonsproject.org/projects/pyramid/en/latest/
|
||||
.. _SQLAlchemy: http://www.sqlalchemy.org
|
||||
|
||||
Diamond_ is a monitoring app with an extensive plugin set, similar to
|
||||
the system we were planning to build for OpenStack.
|
||||
|
||||
.. _Diamond: https://github.com/BrightcoveOS/Diamond
|
||||
|
||||
Nova_ is the primary component of the OpenStack cloud system. It relies
|
||||
on a large number of drivers for managing different aspects of the
|
||||
computing environment.
|
||||
|
||||
.. _Nova: https://launchpad.net/nova
|
||||
|
||||
I looked at all of this code in an effort to derive ideas about the
|
||||
"right way" to handle plugins for ceilometer. While some of what I
|
||||
will say today may sound critical of the choices made by the
|
||||
developers of the other code, I do have the benefit of hindsight and a
|
||||
different perspective based on looking at the examples all together,
|
||||
as well as different requirements for our project.
|
||||
|
||||
Discovery
|
||||
=========
|
||||
|
||||
The first thing an application has to do with a plugin is find it.
|
||||
The tools I looked at are split between some form of explicit
|
||||
definition of plugins and a scanner that looked for the plugins.
|
||||
|
||||
.. image:: discovery.jpg
|
||||
:scale: 50
|
||||
:align: center
|
||||
|
||||
Each of those sets is then further divided between what was being
|
||||
listed or scanned -- files on the filesystem, or python import
|
||||
references (either a module, or something inside of a module).
|
||||
|
||||
The "Explicit import reference" category means there is a
|
||||
configuration file somewhere and a user lists an importable object in
|
||||
that file.
|
||||
|
||||
The "Scan import reference" category means a registry of import
|
||||
strings is being scanned. All of these examples use setuptools and
|
||||
pkg_resources to manage entry points.
|
||||
|
||||
Enabling
|
||||
========
|
||||
|
||||
After the app finds a plugin, the next step is to decide whether to
|
||||
load it and use it. Most applications require an explicit step to
|
||||
configure extensions. There are times when this makes
|
||||
sense. Developer tools like Django are right to ask the developer to
|
||||
list the desired extensions explicitly, since you’re really bringing
|
||||
that code in statically. The extensions to SQLAlchemy are all
|
||||
enabled, but only one is really used at a time and that is chosen by
|
||||
the database connection string.
|
||||
|
||||
.. image:: enabling.jpg
|
||||
:scale: 50
|
||||
:align: center
|
||||
|
||||
However, some of the user applications like Blogofile, Mercurial, and
|
||||
Trac ask the user to explicitly enable extensions through a
|
||||
configuration step that seems like it could be skipped. When I
|
||||
created virtualenvwrapper and cliff, I decided to use installation as
|
||||
a trigger for activation because I wanted to avoid an extra
|
||||
opportunity for misconfiguration. In both of those cases, installing
|
||||
an extension makes it available, so the user can start taking
|
||||
advantage of it immediately.
|
||||
|
||||
That’s also true for Nose extensions, although whether or not they are
|
||||
used for a given test suite or test run depends on the options you
|
||||
give nose.
|
||||
|
||||
Importing
|
||||
=========
|
||||
|
||||
After the app decides whether to load a plugin, the next step is to
|
||||
actually get the code. All of the examples I looked at used two
|
||||
techniques, either calling import explicitly (by using the builtin
|
||||
function, the imp module, or some other variation), or using
|
||||
pkg_resources.
|
||||
|
||||
.. image:: importing.jpg
|
||||
:scale: 50
|
||||
:align: center
|
||||
|
||||
Nose, SQLAlchemy, and Blogofile all use both techniques. Nose falls
|
||||
back to a custom importer if pkg_resources is not installed.
|
||||
SQLAlchemy uses a custom importer for "extensions" distributed with
|
||||
the core but pkg_resources to find separate packages. Blogofile uses
|
||||
pkg_resources to find plugins, coupled with manual scanning and
|
||||
importing of the directories containing those plugins to load their
|
||||
parts.
|
||||
|
||||
If I discount the packages I created myself, shown here in italics,
|
||||
there seems to be a clear bias towards creating custom wrappers around
|
||||
import. That route seems easy at first, but all of the
|
||||
implementations I found exhibited some problems with tricky corner
|
||||
cases.
|
||||
|
||||
Application/Plugin Integration
|
||||
==============================
|
||||
|
||||
After the code for the extension is imported, the next step is to
|
||||
integrate it with the rest of the app. That is, to configure any
|
||||
hooks that need to call into the plugin, pass the plugin any state it
|
||||
needs, etc. I looked at this step along two axes.
|
||||
|
||||
.. image:: integration.jpg
|
||||
:scale: 50
|
||||
:align: center
|
||||
|
||||
First, I considered the granularity of the plugin interface. For
|
||||
"fine" grained plugins, the extension is treated as a standalone
|
||||
object to be called on as needed. In these cases, the code object
|
||||
being loaded is usually a function or a class.
|
||||
|
||||
For more "coarse" grained cases, a single plugin will include hooks
|
||||
that are referenced from multiple places in the application. There may
|
||||
be several classes inside the plugin, for example, or templates that
|
||||
are accessed directly by the application, not through a plugin API.
|
||||
|
||||
The other axis related to integration looks at how the code provided
|
||||
by the plugin is brought into the application. I found two techniques
|
||||
for doing that.
|
||||
|
||||
First, the application can instruct the plugin to integrate
|
||||
itself. That prompting usually takes the form of a setup() or
|
||||
initialization function implemented by the plugin author that calls
|
||||
back to an application context object, registering parts of the plugin
|
||||
explicitly. That registration could also be handled implicitly using
|
||||
an interface library such as the way Trac uses zope.interface.
|
||||
|
||||
Second, the application itself can interrogate or inspect the plugin,
|
||||
and make decisions based on the result. This usually means that part
|
||||
of the plugin API is responsible for providing metadata about the
|
||||
plugin itself, not just taking action.
|
||||
|
||||
API Enforcement
|
||||
===============
|
||||
|
||||
One common issue with dynamically loaded code is enforcing the plugin
|
||||
API at runtime. This is always a potential issue in dynamic
|
||||
languages, but it comes up frequently with plugins because the code is
|
||||
often written by someone other than the core developer for the
|
||||
application. I saw two basic techniques to help developers get their
|
||||
plugins right: convention and interfaces.
|
||||
|
||||
.. image:: api-enforcement.jpg
|
||||
:scale: 50
|
||||
:align: center
|
||||
|
||||
Many of the applications that used convention also had coarse-grained
|
||||
plugin APIs, and so while they may use classes to provide their
|
||||
features, the plugin relies on convention for discovering its
|
||||
configuration.
|
||||
|
||||
On the right are applications for which the plugin uses a class
|
||||
hierarchy. In the case of Nose, using the base class is optional, so
|
||||
that’s a quasi-interface. Trac, on the other hand, uses formal
|
||||
interfaces through `zope.interface`_.
|
||||
|
||||
.. _zope.interface: http://docs.zope.org/zope.interface
|
||||
|
||||
Diamond enforces a strict subclassing of its Collector base class.
|
||||
|
||||
For cliff I chose to use the abc_ module to define an abstract base
|
||||
class, but stick with `"duck typing"`_ in the actual application. The
|
||||
developer doesn’t have to inherit from the base class, but doing so
|
||||
helps ensure that the implementation is complete.
|
||||
|
||||
.. _abc: http://docs.python.org/2/library/abc.html
|
||||
.. _"duck typing": http://en.wikipedia.org/wiki/Duck_typing
|
||||
|
||||
Invocation
|
||||
==========
|
||||
|
||||
And the final dimension I looked at was how the plugin code was used
|
||||
at runtime. There were three primary patterns here.
|
||||
|
||||
.. image:: invocation.jpg
|
||||
:scale: 50
|
||||
:align: center
|
||||
|
||||
1. "Drivers" are loaded one at a time, and used directly.
|
||||
|
||||
2. The apps using the "Dispatcher" pattern load all of the extensions,
|
||||
and then make calls to the appropriate one based on name or some
|
||||
other selection criteria when an event happens.
|
||||
|
||||
3. The apps that use the "Iterator" pattern call each extension in turn,
|
||||
so that all of the plugins have a chance to participate in the
|
||||
processing.
|
||||
|
||||
Ceilometer Design
|
||||
=================
|
||||
|
||||
This analysis had a direct influence on the choices we made while
|
||||
implementing ceilometer.
|
||||
|
||||
Discovery and Import
|
||||
--------------------
|
||||
|
||||
For finding and loading, we chose to use entry points because they
|
||||
were the simplest solution. All of the apps that work on files
|
||||
instead of import references had issues, ranging from poorly
|
||||
implemented import path munging to packaging and distribution
|
||||
challenges. Even some of the code for working with import references
|
||||
directly was a little hairy. Leaving that to a library that handles
|
||||
the different cases transparently made our life a lot easier.
|
||||
|
||||
They are easier for users to install and configure because they don’t
|
||||
have to understand how your code is laid out. That also makes them
|
||||
more resilient in the face of code changes.
|
||||
|
||||
Entry points also support different package formats (egg, sdist,
|
||||
operating system packages), so it doesn’t matter how extensions are
|
||||
distributed.
|
||||
|
||||
They also make it easier for code to be packaged by the Linux
|
||||
distributions, since the packages don’t have to share overlapping
|
||||
installation directories.
|
||||
|
||||
There are alternate implementations of entry-point like systems, but
|
||||
none are so widely used or tested as pkg_resources.
|
||||
|
||||
And to further simplify, we always use entry points, even for the
|
||||
plugins we distribute with our core. That eliminates any special
|
||||
cases.
|
||||
|
||||
Enabling
|
||||
--------
|
||||
|
||||
We came up with a somewhat novel solution to manage which plugins are
|
||||
enabled. For Ceilometer we wanted to default to collecting data, but
|
||||
allow deployers to disable certain meters to save storage space if
|
||||
they knew they did not need the data. The solution was to use explicit
|
||||
configuration, but invert it from the normal implementation.
|
||||
|
||||
We assume that all of the extensions found should be loaded and used,
|
||||
unless they are explicitly *disabled* in the configuration file. We did
|
||||
that in the first version to simplify the configuration process,
|
||||
because we assumed most users would want most of the plugins to be
|
||||
used. Defaulting to enabled means users only have to provide a short
|
||||
list of the meters to turn off.
|
||||
|
||||
Ceilometer plugins also have a chance, when they are being loaded, to
|
||||
disable themselves automatically. This is especially useful in the
|
||||
polling plugins, which can tell the app to ignore them if the resource
|
||||
they use for collecting measurements is not present (like they work
|
||||
with a different hypervisor, or external service that is not
|
||||
configured).
|
||||
|
||||
Letting the plugin disable itself avoids repeated warning messages in
|
||||
the log file as a plugin is asked to poll for data that it cannot
|
||||
retrieve.
|
||||
|
||||
Integration
|
||||
-----------
|
||||
|
||||
For our integration pattern we went with a fine-grained API using
|
||||
inspection. There is a separate namespace for each type of plugin,
|
||||
and each plugin instance refers to a single class. The application
|
||||
loads and instantiates that class, then calls methods on it it to
|
||||
determine what it provides and wants (which notifications to subscribe
|
||||
to and which meters are produced).
|
||||
|
||||
This design lets us avoid having repetitious setup or configuration
|
||||
code in each plugin, since they provide data to the application on
|
||||
demand and the application configures itself. The instances don’t
|
||||
know about the application, or each other. They only run when the
|
||||
application calls them, never independently.
|
||||
|
||||
API Enforcement
|
||||
---------------
|
||||
|
||||
To define the API for each set of plugins we created a separate
|
||||
abstract base class using the abc module. This gives us a way to
|
||||
document each plugin API and Developers who use the base class get
|
||||
some help for free.
|
||||
|
||||
Since we don’t enforce the class hierarchy we also watch for
|
||||
unexpected errors from the plugins any time we call into them.
|
||||
|
||||
Invocation
|
||||
----------
|
||||
|
||||
We used all three invocation patterns, in different places.
|
||||
|
||||
1. We only use one storage system at a time, so we treat the storage
|
||||
plugin like a driver.
|
||||
|
||||
2. We load all of the notification plugins, and then dispatch incoming
|
||||
messages to them based on the message content.
|
||||
|
||||
3. We load all of the polling plugins and iterate through them on a
|
||||
regular schedule.
|
||||
|
||||
Conclusions
|
||||
===========
|
||||
|
||||
After we had all of this working in ceilometer, I extracted some of
|
||||
the code into stevedore. It wraps pkg_resources with a series of
|
||||
manager classes that implement the loading, enabling, and invocation
|
||||
patterns.
|
||||
|
||||
.. seealso::
|
||||
|
||||
* PyCon 2013 Video: http://pyvideo.org/video/1789/dynamic-code-patterns-extending-your-application
|
||||
* Slides: http://www.slideshare.net/doughellmann/dynamic-codepatterns
|
||||
* :doc:`/user/patterns_loading`
|
||||
* :doc:`/user/patterns_enabling`
|
@@ -1,5 +0,0 @@
|
||||
===========
|
||||
ChangeLog
|
||||
===========
|
||||
|
||||
.. include:: ../../../ChangeLog
|
@@ -1,14 +0,0 @@
|
||||
======================
|
||||
stevedore User Guide
|
||||
======================
|
||||
|
||||
.. toctree::
|
||||
:glob:
|
||||
:maxdepth: 2
|
||||
|
||||
patterns_loading
|
||||
patterns_enabling
|
||||
tutorial/index
|
||||
sphinxext
|
||||
essays/*
|
||||
history
|
@@ -1,73 +0,0 @@
|
||||
=======================
|
||||
Patterns for Enabling
|
||||
=======================
|
||||
|
||||
The entry point registry maintained by setuptools lists the available
|
||||
plugins, but does not provide a way for the end-user to control which
|
||||
are used or enabled. The common patterns for managing the set of
|
||||
extensions to be used are described below.
|
||||
|
||||
Enabled Through Installation
|
||||
============================
|
||||
|
||||
For many applications, simply installing an extension is enough of an
|
||||
indication that the extension should be used. No explicit
|
||||
configuration is required on the part of the user to either discover
|
||||
or enable the extension, since its entry point can be discovered when
|
||||
all of the plugins are loaded at runtime.
|
||||
|
||||
Examples of enabling through installation include:
|
||||
|
||||
* `python-openstackclient`_
|
||||
* virtualenvwrapper_
|
||||
|
||||
.. _python-openstackclient: https://github.com/openstack/python-openstackclient
|
||||
.. _virtualenvwrapper: http://pypi.python.org/pypi/virtualenvwrapper
|
||||
|
||||
Enabled Explicitly
|
||||
==================
|
||||
|
||||
In other cases, the extensions may be installed system-wide but should
|
||||
not all be enabled for a given application or instance of an
|
||||
application. In these situations, the person deploying or using the
|
||||
application will want to select the extensions to be used through an
|
||||
explicit configuration step.
|
||||
|
||||
Examples of explicitly enabled extensions include:
|
||||
|
||||
* `Django apps`_
|
||||
* `Sphinx extensions`_
|
||||
* `Trac Plugins`_
|
||||
|
||||
.. _Trac Plugins: http://trac.edgewall.org/wiki/TracPlugins
|
||||
|
||||
.. _Sphinx extensions: http://sphinx.pocoo.org/extensions.html
|
||||
|
||||
.. _Django apps: https://docs.djangoproject.com/en/dev/intro/tutorial01/
|
||||
|
||||
.. seealso::
|
||||
|
||||
:class:`stevedore.named.NamedExtensionManager`
|
||||
|
||||
|
||||
Self-Enabled
|
||||
============
|
||||
|
||||
Finally, some applications ask their extensions whether they should be
|
||||
enabled. The extension may look at other libraries installed on the
|
||||
system, check an external configuration setting, or examine a resource
|
||||
to see if it can be managed by the plugin. These checks are usually at
|
||||
runtime, either when the extension is loaded or when the user tries to
|
||||
access a specific resource.
|
||||
|
||||
Examples of self-enabled extensions include:
|
||||
|
||||
* anydbm_
|
||||
* PIL_
|
||||
|
||||
.. _anydbm: http://docs.python.org/library/anydbm.html
|
||||
.. _PIL: http://www.pythonware.com/products/pil/
|
||||
|
||||
.. seealso::
|
||||
|
||||
:class:`stevedore.enabled.EnabledExtensionManager`
|
@@ -1,124 +0,0 @@
|
||||
======================
|
||||
Patterns for Loading
|
||||
======================
|
||||
|
||||
Setuptools entry points are registered as within a namespace that
|
||||
defines the API expected by the plugin code. Each entry point has a
|
||||
name, which does not have to be unique within a given namespace. The
|
||||
flexibility of this name management system makes it possible to use
|
||||
plugins in a variety of ways. The manager classes in stevedore wrap
|
||||
:mod:`pkg_resources` to apply different rules matching the patterns
|
||||
described here.
|
||||
|
||||
Drivers -- Single Name, Single Entry Point
|
||||
==========================================
|
||||
|
||||
Specifying a *driver* for communicating with an external resource
|
||||
(database, device, or remote application) is perhaps the most common
|
||||
use of dynamically loaded libraries. Drivers support the abstracted
|
||||
view of the resource so an application can work with different types
|
||||
of resources. For example, drivers may connect to database engines,
|
||||
load different file formats, or communicate with similar web services
|
||||
from different providers. Many drivers may be available for a given
|
||||
application, but it is implied in the interface between the
|
||||
application and the driver that only one driver will be used to manage
|
||||
a given resource.
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph drivers {
|
||||
app [label="namespace",shape="record"];
|
||||
d1 [style=filled,color=".7 .3 1.0",label="driver 1"];
|
||||
d2 [style=dotted,label="driver 2"];
|
||||
d3 [style=dotted,label="driver 3"];
|
||||
app -> d1;
|
||||
app -> d2 [style=dotted];
|
||||
app -> d3 [style=dotted];
|
||||
}
|
||||
|
||||
Examples of the *drivers* pattern include:
|
||||
|
||||
* database client libraries used by SQLAlchemy_
|
||||
* cloud vendor API clients used by libcloud_
|
||||
|
||||
.. _SQLAlchemy: http://sqlalchemy.org/
|
||||
|
||||
.. _libcloud: http://libcloud.apache.org/
|
||||
|
||||
.. seealso::
|
||||
|
||||
:class:`stevedore.driver.DriverManager`
|
||||
|
||||
Hooks -- Single Name, Many Entry Points
|
||||
=======================================
|
||||
|
||||
*Hooks*, *signals*, or *callbacks* are invoked based on an event
|
||||
occurring within an application. All of the hooks for an application
|
||||
may share a single namespace (e.g., ``my.application.hooks``) and use
|
||||
a different name for the triggered event (e.g., ``startup`` and
|
||||
``precommit``). Multiple entry points can share the same name within
|
||||
the namespace, so that multiple hooks can be invoked when an event
|
||||
occurs.
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph drivers {
|
||||
app [label="namespace::event_name",shape="record"];
|
||||
l1 [style=filled,color=".7 .3 1.0",label="event_name (lib1)"];
|
||||
l2 [style=filled,color=".7 .3 1.0",label="event_name (lib2)"];
|
||||
l3 [style=filled,color=".7 .3 1.0",label="event_name (lib3)"];
|
||||
app -> l1;
|
||||
app -> l2;
|
||||
app -> l3;
|
||||
}
|
||||
|
||||
Examples of the *hooks* pattern include:
|
||||
|
||||
* Emacs `mode hook functions`_
|
||||
* `Django signals`_
|
||||
|
||||
.. _Django signals: https://docs.djangoproject.com/en/dev/topics/signals/
|
||||
|
||||
.. _mode hook functions: http://www.gnu.org/software/emacs/manual/html_node/emacs/Hooks.html
|
||||
|
||||
.. seealso::
|
||||
|
||||
:class:`stevedore.hook.HookManager`
|
||||
|
||||
Extensions -- Many Names, Many Entry Points
|
||||
===========================================
|
||||
|
||||
The more general form of extending an application is to load
|
||||
additional functionality by discovering add-on modules that use a
|
||||
minimal API to inject themselves at runtime. Extensions typically want
|
||||
to be notified that they have been loaded and are being used so they
|
||||
can perform initialization or setup steps. An extension may replace
|
||||
core functionality or add to it.
|
||||
|
||||
.. graphviz::
|
||||
|
||||
digraph drivers {
|
||||
app [label="application",shape="record"];
|
||||
e1 [style=filled,color=".7 .3 1.0",label="extension 1"];
|
||||
e2 [style=filled,color=".7 .3 1.0",label="extension 2"];
|
||||
e3 [style=filled,color=".7 .3 1.0",label="extension 3"];
|
||||
app -> e1;
|
||||
app -> e2;
|
||||
app -> e3;
|
||||
}
|
||||
|
||||
Examples of the *extensions* pattern include:
|
||||
|
||||
* `Django apps`_
|
||||
* `Sphinx extensions`_
|
||||
* `Trac Plugins`_
|
||||
|
||||
.. _Trac Plugins: http://trac.edgewall.org/wiki/TracPlugins
|
||||
|
||||
.. _Sphinx extensions: http://sphinx.pocoo.org/extensions.html
|
||||
|
||||
.. _Django apps: https://docs.djangoproject.com/en/dev/intro/tutorial01/
|
||||
|
||||
.. seealso::
|
||||
|
||||
:class:`stevedore.extension.ExtensionManager`
|
@@ -1,73 +0,0 @@
|
||||
====================
|
||||
Sphinx Integration
|
||||
====================
|
||||
|
||||
Stevedore includes an extension for integrating with Sphinx to
|
||||
automatically produce documentation about the supported plugins. To
|
||||
activate the plugin add ``stevedore.sphinxext`` to the list of
|
||||
extensions in your ``conf.py``.
|
||||
|
||||
.. rst:directive:: .. list-plugins:: namespace
|
||||
|
||||
List the plugins in a namespace.
|
||||
|
||||
Options:
|
||||
|
||||
``detailed``
|
||||
Flag to switch between simple and detailed output (see
|
||||
below).
|
||||
``overline-style``
|
||||
Character to use to draw line above header,
|
||||
defaults to none.
|
||||
``underline-style``
|
||||
Character to use to draw line below header,
|
||||
defaults to ``=``.
|
||||
|
||||
Simple List
|
||||
===========
|
||||
|
||||
By default, the ``list-plugins`` directive produces a simple list of
|
||||
plugins in a given namespace including the name and the first line of
|
||||
the docstring. For example:
|
||||
|
||||
::
|
||||
|
||||
.. list-plugins:: stevedore.example.formatter
|
||||
|
||||
produces
|
||||
|
||||
------
|
||||
|
||||
.. list-plugins:: stevedore.example.formatter
|
||||
|
||||
------
|
||||
|
||||
Detailed Lists
|
||||
==============
|
||||
|
||||
Adding the ``detailed`` flag to the directive causes the output to
|
||||
include a separate subsection for each plugin, with the full docstring
|
||||
rendered. The section heading level can be controlled using the
|
||||
``underline-style`` and ``overline-style`` options to fit the results
|
||||
into the structure of your existing document.
|
||||
|
||||
::
|
||||
|
||||
.. list-plugins:: stevedore.example.formatter
|
||||
:detailed:
|
||||
|
||||
produces
|
||||
|
||||
------
|
||||
|
||||
.. list-plugins:: stevedore.example.formatter
|
||||
:detailed:
|
||||
:underline-style: -
|
||||
|
||||
------
|
||||
|
||||
.. note::
|
||||
|
||||
Depending on how Sphinx is configured, bad reStructuredText syntax in
|
||||
the docstrings of the plugins may cause the documentation build to
|
||||
fail completely when detailed mode is enabled.
|
@@ -1,160 +0,0 @@
|
||||
==================
|
||||
Creating Plugins
|
||||
==================
|
||||
|
||||
After a lot of trial and error, the easiest way I have found to define
|
||||
an API is to follow these steps:
|
||||
|
||||
#. Use the `abc module`_ to create a base abstract class to define the
|
||||
behaviors required of plugins of the API. Developers don't have to
|
||||
subclass from the base class, but it provides a convenient way to
|
||||
document the API, and using an abstract base class keeps you
|
||||
honest.
|
||||
#. Create plugins by subclassing the base class and implementing the
|
||||
required methods.
|
||||
#. Define a unique namespace for each API by combining the name of the
|
||||
application (or library) and a name of the API. Keep it
|
||||
shallow. For example, "cliff.formatters" or
|
||||
"ceilometer.pollsters.compute".
|
||||
|
||||
Example Plugin Set
|
||||
==================
|
||||
|
||||
The example program in this tutorial will create a plugin set with
|
||||
several data formatters, like what might be used by a command line
|
||||
program to prepare data to be printed to the console. Each formatter
|
||||
will take as input a dictionary with string keys and built-in data
|
||||
types as values. It will return as output an iterator that produces
|
||||
the string with the data structure formatted based on the rules of the
|
||||
specific formatter being used. The formatter's constructor lets the
|
||||
caller specify the maximum width the output should have.
|
||||
|
||||
A Plugin Base Class
|
||||
===================
|
||||
|
||||
Step 1 above is to define an abstract base class for the API that
|
||||
needs to be implemented by each plugin.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/base.py
|
||||
:language: python
|
||||
:prepend: # stevedore/example/base.py
|
||||
|
||||
The constructor is a concrete method because subclasses do not need to
|
||||
override it, but the :func:`format` method does not do anything useful
|
||||
because there is no "default" implementation available.
|
||||
|
||||
Concrete Plugins
|
||||
================
|
||||
|
||||
The next step is to create a couple of plugin classes with concrete
|
||||
implementations of :func:`format`. A simple example formatter produces
|
||||
output with each variable name and value on a single line.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/simple.py
|
||||
:language: python
|
||||
:prepend: # stevedore/example/simple.py
|
||||
|
||||
There are plenty of other formatting options, but this example will
|
||||
give us enough to work with to demonstrate registering and using
|
||||
plugins.
|
||||
|
||||
Registering the Plugins
|
||||
=======================
|
||||
|
||||
To use setuptools entry points, you must package your application or
|
||||
library using setuptools. The build and packaging process generates
|
||||
metadata which is available after installation to find the plugins
|
||||
provided by each python distribution.
|
||||
|
||||
The entry points must be declared as belonging to a specific
|
||||
namespace, so we need to pick one before going any further. These
|
||||
plugins are formatters from the stevedore examples, so I will use the
|
||||
namespace "stevedore.example.formatter". Now it is possible to provide
|
||||
all of the necessary information in the packaging instructions:
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/setup.py
|
||||
:language: python
|
||||
:prepend: # stevedore/example/setup.py
|
||||
|
||||
The important lines are near the bottom where the ``entry_points``
|
||||
argument to :func:`setup` is set. The value is a dictionary mapping
|
||||
the namespace for the plugins to a list of their definitions. Each
|
||||
item in the list should be a string with ``name = module:importable``
|
||||
where *name* is the user-visible name for the plugin, *module* is the
|
||||
Python import reference for the module, and *importable* is the name
|
||||
of something that can be imported from inside the module.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/setup.py
|
||||
:language: python
|
||||
:lines: 37-43
|
||||
|
||||
In this case, there are two plugins registered. The "simple" plugin
|
||||
defined above, and a "plain" plugin, which is just an alias for the
|
||||
simple plugin.
|
||||
|
||||
setuptools Metadata
|
||||
===================
|
||||
|
||||
During the build, setuptools copies entry point definitions to a file
|
||||
in the ".egg-info" directory for the package. For example, the file
|
||||
for stevedore is located in ``stevedore.egg-info/entry_points.txt``:
|
||||
|
||||
::
|
||||
|
||||
[stevedore.example.formatter]
|
||||
simple = stevedore.example.simple:Simple
|
||||
plain = stevedore.example.simple:Simple
|
||||
|
||||
[stevedore.test.extension]
|
||||
t2 = stevedore.tests.test_extension:FauxExtension
|
||||
t1 = stevedore.tests.test_extension:FauxExtension
|
||||
|
||||
:mod:`pkg_resources` uses the ``entry_points.txt`` file from all of
|
||||
the installed packages on the import path to find plugins. You should
|
||||
not modify these files, except by changing the list of entry points in
|
||||
``setup.py``.
|
||||
|
||||
.. _abc module: http://docs.python.org/2/library/abc.html
|
||||
.. _field list: http://docutils.sourceforge.net/docs/ref/rst/restructuredtext.html#field-lists
|
||||
|
||||
Adding Plugins in Other Packages
|
||||
================================
|
||||
|
||||
Part of the appeal of using entry points for plugins is that they can
|
||||
be distributed independently of an application. The namespace
|
||||
setuptools uses to find the plugins is different from the Python
|
||||
source code namespace. It is common to use a plugin namespace prefixed
|
||||
with the name of the application or library that loads the plugins, to
|
||||
ensure it is unique, but that name has no bearing on what Python
|
||||
package the code for the plugin should live in.
|
||||
|
||||
For example, we can add an alternate implementation of a formatter
|
||||
plugin that produces a reStructuredText `field list`_.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example2/fields.py
|
||||
:language: python
|
||||
:prepend: # stevedore/example2/fields.py
|
||||
|
||||
The new plugin can then be packaged using a ``setup.py`` containing
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example2/setup.py
|
||||
:language: python
|
||||
:prepend: # stevedore/example2/setup.py
|
||||
|
||||
The new plugin is in a separate ``stevedore-examples2`` package.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example2/setup.py
|
||||
:language: python
|
||||
:lines: 3-4
|
||||
|
||||
However, the plugin is registered as part of the
|
||||
``stevedore.example.formatter`` namespace.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example2/setup.py
|
||||
:language: python
|
||||
:lines: 36-40
|
||||
|
||||
When the plugin namespace is scanned, all packages on the current
|
||||
``PYTHONPATH`` are examined and the entry point from the second
|
||||
package is found and can be loaded without the application having to
|
||||
know where the plugin is actually installed.
|
@@ -1,36 +0,0 @@
|
||||
$ python -m stevedore.example.load_as_driver a = A
|
||||
b = B
|
||||
long = word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word
|
||||
|
||||
$ python -m stevedore.example.load_as_driver field
|
||||
: a : A
|
||||
: b : B
|
||||
: long : word word word word word word word word word word
|
||||
word word word word word word word word word word word
|
||||
word word word word word word word word word word word
|
||||
word word word word word word word word word word word
|
||||
word word word word word word word word word word word
|
||||
word word word word word word word word word word word
|
||||
word word word word word word word word word word word
|
||||
word word word word
|
||||
|
||||
$ python -m stevedore.example.load_as_driver field --width 30
|
||||
: a : A
|
||||
: b : B
|
||||
: long : word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word
|
@@ -1,31 +0,0 @@
|
||||
$ python -m stevedore.example.load_as_extension --width 30
|
||||
Formatter: simple
|
||||
a = A
|
||||
b = B
|
||||
long = word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word
|
||||
|
||||
Formatter: field
|
||||
: a : A
|
||||
: b : B
|
||||
: long : word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word word word word word
|
||||
word
|
||||
|
||||
Formatter: plain
|
||||
a = A
|
||||
b = B
|
||||
long = word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word word
|
@@ -1,26 +0,0 @@
|
||||
=====================================
|
||||
Using Stevedore in Your Application
|
||||
=====================================
|
||||
|
||||
This tutorial is a step-by-step walk-through demonstrating how to
|
||||
define plugins and then use stevedore to load and use them in your
|
||||
application.
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
naming
|
||||
creating_plugins
|
||||
loading
|
||||
testing
|
||||
|
||||
.. seealso::
|
||||
|
||||
* :doc:`../essays/pycon2013`
|
||||
* `Using setuptools entry points`_
|
||||
* `Package Discovery and Resource Access using pkg_resources`_
|
||||
* `Using Entry Points to Write Plugins | Pylons`_
|
||||
|
||||
.. _Using setuptools entry points: http://reinout.vanrees.org/weblog/2010/01/06/zest-releaser-entry-points.html
|
||||
.. _Package Discovery and Resource Access using pkg_resources: http://pythonhosted.org/distribute/pkg_resources.html
|
||||
.. _Using Entry Points to Write Plugins | Pylons: http://docs.pylonsproject.org/projects/pylons-webframework/en/latest/advanced_pylons/entry_points_and_plugins.html
|
@@ -1,126 +0,0 @@
|
||||
=====================
|
||||
Loading the Plugins
|
||||
=====================
|
||||
|
||||
There are several different enabling and invocation patterns for
|
||||
consumers of plugins, depending on your needs.
|
||||
|
||||
Loading Drivers
|
||||
===============
|
||||
|
||||
The most common way plugins are used is as individual drivers. In this
|
||||
case, there may be many plugin options to choose from, but only one
|
||||
needs to be loaded and called. The
|
||||
:class:`~stevedore.driver.DriverManager` class supports this pattern.
|
||||
|
||||
This example program uses a :class:`DriverManager` to load a formatter
|
||||
defined in the examples for stevedore. It then uses the formatter to
|
||||
convert a data structure to a text format, which it can print.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/load_as_driver.py
|
||||
:language: python
|
||||
:prepend: # stevedore/example/load_as_driver.py
|
||||
|
||||
The manager takes the plugin namespace and name as arguments, and uses
|
||||
them to find the plugin. Then, because ``invoke_on_load`` is true, it
|
||||
calls the object loaded. In this case that object is the plugin class
|
||||
registered as a formatter. The ``invoke_args`` are positional
|
||||
arguments passed to the class constructor, and are used to set the
|
||||
maximum width parameter.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/load_as_driver.py
|
||||
:language: python
|
||||
:lines: 30-35
|
||||
|
||||
After the manager is created, it holds a reference to a single object
|
||||
returned by calling the code registered for the plugin. That object is
|
||||
the actual driver, in this case an instance of the formatter class
|
||||
from the plugin. The single driver can be accessed via the
|
||||
:attr:`driver` property of the manager, and then its methods can be
|
||||
called directly.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/load_as_driver.py
|
||||
:language: python
|
||||
:lines: 36-37
|
||||
|
||||
Running the example program produces this output:
|
||||
|
||||
.. literalinclude:: driver_output.txt
|
||||
|
||||
Loading Extensions
|
||||
==================
|
||||
|
||||
Another common use case is to load several extensions at one time, and
|
||||
do something with all of them. Several of the other manager classes
|
||||
support this invocation pattern, including
|
||||
:class:`~stevedore.extension.ExtensionManager`,
|
||||
:class:`~stevedore.named.NamedExtensionManager`, and
|
||||
:class:`~stevedore.enabled.EnabledExtensionManager`.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/load_as_extension.py
|
||||
:language: python
|
||||
:prepend: # stevedore/example/load_as_extension.py
|
||||
|
||||
The :class:`ExtensionManager` is created slightly differently from the
|
||||
:class:`DriverManager` because it does not need to know in advance
|
||||
which plugin to load. It loads all of the plugins it finds.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/load_as_extension.py
|
||||
:language: python
|
||||
:lines: 24-28
|
||||
|
||||
To call the plugins, use the :meth:`map` method, passing a callable to
|
||||
be invoked for each extension. The :func:`format_data` function used
|
||||
with :meth:`map` in this example takes two arguments, the
|
||||
:class:`~stevedore.extension.Extension` and the data argument given to
|
||||
:meth:`map`.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/load_as_extension.py
|
||||
:language: python
|
||||
:lines: 30-33
|
||||
|
||||
The :class:`Extension` passed :func:`format_data` is a class defined
|
||||
by stevedore that wraps the plugin. It includes the name of the
|
||||
plugin, the :class:`EntryPoint` returned by :mod:`pkg_resources`, and
|
||||
the plugin itself (the named object referenced by the plugin
|
||||
definition). When ``invoke_on_load`` is true, the :class:`Extension`
|
||||
will also have an :attr:`obj` attribute containing the value returned
|
||||
when the plugin was invoked.
|
||||
|
||||
:meth:`map` returns a sequence of the values returned by the callback
|
||||
function. In this case, :func:`format_data` returns a tuple containing
|
||||
the extension name and the iterable that produces the text to
|
||||
print. As the results are processed, the name of each plugin is
|
||||
printed and then the formatted data.
|
||||
|
||||
.. literalinclude:: ../../../../stevedore/example/load_as_extension.py
|
||||
:language: python
|
||||
:lines: 35-39
|
||||
|
||||
The order the plugins are loaded is undefined, and depends on the
|
||||
order packages are found on the import path as well as the way the
|
||||
metadata files are read. If the order extensions are used matters, try
|
||||
the :class:`~stevedore.named.NamedExtensionManager`.
|
||||
|
||||
.. literalinclude:: extension_output.txt
|
||||
|
||||
Why Not Call Plugins Directly?
|
||||
==============================
|
||||
|
||||
Using a separate callable argument to :meth:`map`, rather than just
|
||||
invoking the plugin directly introduces a separation between your
|
||||
application code and the plugins. The benefits of this separation
|
||||
manifest in the application code design and in the plugin API design.
|
||||
|
||||
If :meth:`map` called the plugin directly, each plugin would have to
|
||||
be a callable. That would mean a separate namespace for what is really
|
||||
just a method of the plugin. By using a separate callable argument,
|
||||
the plugin API does not need to match exactly any particular use case
|
||||
in the application. This frees you to create a finer-grained API, with
|
||||
more individual methods that can be called in different ways to
|
||||
achieve different goals.
|
||||
|
||||
.. seealso::
|
||||
|
||||
* :doc:`../patterns_loading`
|
||||
* :doc:`../patterns_enabling`
|
@@ -1,38 +0,0 @@
|
||||
===============================
|
||||
Guidelines for Naming Plugins
|
||||
===============================
|
||||
|
||||
Stevedore uses setuptools entry points to define and load plugins. An
|
||||
entry point is standard way to refer to a named object defined inside
|
||||
a Python module or package. The name can be a reference to any class,
|
||||
function, or instance, as long as it is created when the containing
|
||||
module is imported (i.e., it needs to be a module-level global).
|
||||
|
||||
Names and Namespaces
|
||||
====================
|
||||
|
||||
Entry points are registered using a *name* in a *namespace*.
|
||||
|
||||
Entry point names are usually considered user-visible. For example,
|
||||
they frequently appear in configuration files where a driver is being
|
||||
enabled. Because they are public, names are typically as short as
|
||||
possible while remaining descriptive. For example, database driver
|
||||
plugin names might be "mysql", "postgresql", "sqlite", etc.
|
||||
|
||||
Namespaces, on the other hand, are an implementation detail, and while
|
||||
they are known to developers they are not usually exposed to users.
|
||||
The namespace naming syntax looks a lot like Python's package syntax
|
||||
(``a.b.c``) but *namespaces do not correspond to Python
|
||||
packages*. Using a Python package name for an entry point namespace is
|
||||
an easy way to ensure a unique name, but it's not required at all.
|
||||
The main feature of entry points is that they can be discovered
|
||||
*across* packages. That means that a plugin can be developed and
|
||||
installed completely separately from the application that uses it, as
|
||||
long as they agree on the namespace and API.
|
||||
|
||||
Each namespace is owned by the code that consumes the plugins and is
|
||||
used to search for entry points. The entry point names are typically
|
||||
owned by the plugin, but they can also be defined by the consuming
|
||||
code for named hooks (see :class:`~stevedore.hook.HookManager`). The
|
||||
names of entry points must be unique within a given distribution, but
|
||||
are not necessarily unique in a namespace.
|
@@ -1,9 +0,0 @@
|
||||
=========
|
||||
Testing
|
||||
=========
|
||||
|
||||
.. describe using the TestManager for setting up application tests
|
||||
|
||||
.. seealso::
|
||||
|
||||
* :class:`~stevedore.tests.manager.TestExtensionManager`
|
@@ -1,3 +0,0 @@
|
||||
---
|
||||
other:
|
||||
- Introduce reno for deployer release notes.
|
@@ -1,282 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# This file is execfile()d with the current directory set to its
|
||||
# containing dir.
|
||||
#
|
||||
# Note that not all possible configuration values are present in this
|
||||
# autogenerated file.
|
||||
#
|
||||
# All configuration values have a default; values that are commented out
|
||||
# serve to show the default.
|
||||
|
||||
# If extensions (or modules to document with autodoc) are in another directory,
|
||||
# add these directories to sys.path here. If the directory is relative to the
|
||||
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||
# sys.path.insert(0, os.path.abspath('.'))
|
||||
|
||||
# -- General configuration ------------------------------------------------
|
||||
|
||||
# If your documentation needs a minimal Sphinx version, state it here.
|
||||
# needs_sphinx = '1.0'
|
||||
|
||||
# Add any Sphinx extension module names here, as strings. They can be
|
||||
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
|
||||
# ones.
|
||||
extensions = [
|
||||
'openstackdocstheme',
|
||||
'reno.sphinxext',
|
||||
]
|
||||
|
||||
# openstackdocstheme options
|
||||
repository_name = 'openstack/stevedore'
|
||||
bug_project = 'python-stevedore'
|
||||
bug_tag = ''
|
||||
html_last_updated_fmt = '%Y-%m-%d %H:%M'
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['_templates']
|
||||
|
||||
# The suffix of source filenames.
|
||||
source_suffix = '.rst'
|
||||
|
||||
# The encoding of source files.
|
||||
# source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
project = u'stevedore Release Notes'
|
||||
copyright = u'2016, stevedore Developers'
|
||||
|
||||
# The version info for the project you're documenting, acts as replacement for
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
import pkg_resources
|
||||
release = pkg_resources.get_distribution('stevedore').version
|
||||
# The short X.Y version.
|
||||
version = release
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
# language = None
|
||||
|
||||
# There are two options for replacing |today|: either, you set today to some
|
||||
# non-false value, then it is used:
|
||||
# today = ''
|
||||
# Else, today_fmt is used as the format for a strftime call.
|
||||
# today_fmt = '%B %d, %Y'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = []
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all
|
||||
# documents.
|
||||
# default_role = None
|
||||
|
||||
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||
# add_function_parentheses = True
|
||||
|
||||
# If true, the current module name will be prepended to all description
|
||||
# unit titles (such as .. function::).
|
||||
# add_module_names = True
|
||||
|
||||
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||
# output. They are ignored by default.
|
||||
# show_authors = False
|
||||
|
||||
# The name of the Pygments (syntax highlighting) style to use.
|
||||
pygments_style = 'sphinx'
|
||||
|
||||
# A list of ignored prefixes for module index sorting.
|
||||
# modindex_common_prefix = []
|
||||
|
||||
# If true, keep warnings as "system message" paragraphs in the built documents.
|
||||
# keep_warnings = False
|
||||
|
||||
|
||||
# -- Options for HTML output ----------------------------------------------
|
||||
|
||||
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||
# a list of builtin themes.
|
||||
html_theme = 'openstackdocs'
|
||||
|
||||
# Theme options are theme-specific and customize the look and feel of a theme
|
||||
# further. For a list of options available for each theme, see the
|
||||
# documentation.
|
||||
# html_theme_options = {}
|
||||
|
||||
# Add any paths that contain custom themes here, relative to this directory.
|
||||
# html_theme_path = []
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
# html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
# html_short_title = None
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
# html_logo = None
|
||||
|
||||
# The name of an image file (within the static path) to use as favicon of the
|
||||
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||
# pixels large.
|
||||
# html_favicon = None
|
||||
|
||||
# Add any paths that contain custom static files (such as style sheets) here,
|
||||
# relative to this directory. They are copied after the builtin static files,
|
||||
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||
html_static_path = ['_static']
|
||||
|
||||
# Add any extra paths that contain custom files (such as robots.txt or
|
||||
# .htaccess) here, relative to this directory. These files are copied
|
||||
# directly to the root of the documentation.
|
||||
# html_extra_path = []
|
||||
|
||||
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||
# using the given strftime format.
|
||||
# html_last_updated_fmt = '%b %d, %Y'
|
||||
|
||||
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||
# typographically correct entities.
|
||||
# html_use_smartypants = True
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
# html_sidebars = {}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
# html_additional_pages = {}
|
||||
|
||||
# If false, no module index is generated.
|
||||
# html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
# html_use_index = True
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
# html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
# html_show_sourcelink = True
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
# html_show_sphinx = True
|
||||
|
||||
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||
# html_show_copyright = True
|
||||
|
||||
# If true, an OpenSearch description file will be output, and all pages will
|
||||
# contain a <link> tag referring to it. The value of this option must be the
|
||||
# base URL from which the finished HTML is served.
|
||||
# html_use_opensearch = ''
|
||||
|
||||
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||
# html_file_suffix = None
|
||||
|
||||
# Output file base name for HTML help builder.
|
||||
htmlhelp_basename = 'stevedoreReleaseNotesDoc'
|
||||
|
||||
|
||||
# -- Options for LaTeX output ---------------------------------------------
|
||||
|
||||
latex_elements = {
|
||||
# The paper size ('letterpaper' or 'a4paper').
|
||||
# 'papersize': 'letterpaper',
|
||||
|
||||
# The font size ('10pt', '11pt' or '12pt').
|
||||
# 'pointsize': '10pt',
|
||||
|
||||
# Additional stuff for the LaTeX preamble.
|
||||
# 'preamble': '',
|
||||
}
|
||||
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title,
|
||||
# author, documentclass [howto, manual, or own class]).
|
||||
latex_documents = [
|
||||
('index', 'stevedoreReleaseNotes.tex',
|
||||
u'stevedore Release Notes Documentation',
|
||||
u'stevedore Developers', 'manual'),
|
||||
]
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top of
|
||||
# the title page.
|
||||
# latex_logo = None
|
||||
|
||||
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||
# not chapters.
|
||||
# latex_use_parts = False
|
||||
|
||||
# If true, show page references after internal links.
|
||||
# latex_show_pagerefs = False
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# latex_show_urls = False
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# latex_domain_indices = True
|
||||
|
||||
|
||||
# -- Options for manual page output ---------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'stevedoreReleaseNotes',
|
||||
u'stevedore Release Notes Documentation',
|
||||
[u'stevedore Developers'], 1)
|
||||
]
|
||||
|
||||
# If true, show URL addresses after external links.
|
||||
# man_show_urls = False
|
||||
|
||||
|
||||
# -- 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 = [
|
||||
('index', 'stevedoreReleaseNotes',
|
||||
u'stevedore Release Notes Documentation',
|
||||
u'stevedore Developers', 'stevedoreReleaseNotes',
|
||||
'One line description of project.',
|
||||
'Miscellaneous'),
|
||||
]
|
||||
|
||||
# Documents to append as an appendix to all manuals.
|
||||
# texinfo_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
# texinfo_domain_indices = True
|
||||
|
||||
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||
# texinfo_show_urls = 'footnote'
|
||||
|
||||
# If true, do not generate a @detailmenu in the "Top" node's menu.
|
||||
# texinfo_no_detailmenu = False
|
||||
|
||||
# -- Options for Internationalization output ------------------------------
|
||||
locale_dirs = ['locale/']
|
@@ -1,9 +0,0 @@
|
||||
=========================
|
||||
stevedore Release Notes
|
||||
=========================
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 1
|
||||
|
||||
unreleased
|
||||
ocata
|
@@ -1,6 +0,0 @@
|
||||
===================================
|
||||
Ocata Series Release Notes
|
||||
===================================
|
||||
|
||||
.. release-notes::
|
||||
:branch: origin/stable/ocata
|
@@ -1,5 +0,0 @@
|
||||
==============================
|
||||
Current Series Release Notes
|
||||
==============================
|
||||
|
||||
.. release-notes::
|
@@ -1,6 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
pbr!=2.1.0,>=2.0.0 # Apache-2.0
|
||||
six>=1.9.0 # MIT
|
50
setup.cfg
@@ -1,50 +0,0 @@
|
||||
[metadata]
|
||||
name = stevedore
|
||||
description-file = README.rst
|
||||
author = OpenStack
|
||||
author-email = openstack-dev@lists.openstack.org
|
||||
summary = Manage dynamic plugins for Python applications
|
||||
home-page = https://docs.openstack.org/stevedore/latest/
|
||||
classifier =
|
||||
Development Status :: 5 - Production/Stable
|
||||
License :: OSI Approved :: Apache Software License
|
||||
Programming Language :: Python
|
||||
Programming Language :: Python :: 2
|
||||
Programming Language :: Python :: 2.7
|
||||
Programming Language :: Python :: 3
|
||||
Programming Language :: Python :: 3.5
|
||||
Intended Audience :: Developers
|
||||
Environment :: Console
|
||||
|
||||
[global]
|
||||
setup-hooks =
|
||||
pbr.hooks.setup_hook
|
||||
|
||||
[files]
|
||||
packages =
|
||||
stevedore
|
||||
|
||||
[entry_points]
|
||||
stevedore.example.formatter =
|
||||
simple = stevedore.example.simple:Simple
|
||||
field = stevedore.example2.fields:FieldList
|
||||
plain = stevedore.example.simple:Simple
|
||||
|
||||
stevedore.test.extension =
|
||||
t1 = stevedore.tests.test_extension:FauxExtension
|
||||
t2 = stevedore.tests.test_extension:FauxExtension
|
||||
e1 = stevedore.tests.test_extension:BrokenExtension
|
||||
e2 = stevedore.tests.notfound:UnimportableExtension
|
||||
|
||||
|
||||
[build_sphinx]
|
||||
all_files = 1
|
||||
build-dir = doc/build
|
||||
source-dir = doc/source
|
||||
warning-is-error = 1
|
||||
|
||||
[pbr]
|
||||
warnerrors = True
|
||||
|
||||
[wheel]
|
||||
universal = true
|
29
setup.py
@@ -1,29 +0,0 @@
|
||||
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
# implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
|
||||
import setuptools
|
||||
|
||||
# In python < 2.7.4, a lazy loading of package `pbr` will break
|
||||
# setuptools if some other modules registered functions in `atexit`.
|
||||
# solution from: http://bugs.python.org/issue15881#msg170215
|
||||
try:
|
||||
import multiprocessing # noqa
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
setuptools.setup(
|
||||
setup_requires=['pbr>=2.0.0'],
|
||||
pbr=True)
|
@@ -1,24 +0,0 @@
|
||||
# flake8: noqa
|
||||
|
||||
__all__ = [
|
||||
'ExtensionManager',
|
||||
'EnabledExtensionManager',
|
||||
'NamedExtensionManager',
|
||||
'HookManager',
|
||||
'DriverManager',
|
||||
]
|
||||
|
||||
from .extension import ExtensionManager
|
||||
from .enabled import EnabledExtensionManager
|
||||
from .named import NamedExtensionManager
|
||||
from .hook import HookManager
|
||||
from .driver import DriverManager
|
||||
|
||||
import logging
|
||||
|
||||
# Configure a NullHandler for our log messages in case
|
||||
# the app we're used from does not set up logging.
|
||||
LOG = logging.getLogger('stevedore')
|
||||
|
||||
LOG.addHandler(logging.NullHandler())
|
||||
|
@@ -1,229 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from .enabled import EnabledExtensionManager
|
||||
from .exception import NoMatches
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DispatchExtensionManager(EnabledExtensionManager):
|
||||
"""Loads all plugins and filters on execution.
|
||||
|
||||
This is useful for long-running processes that need to pass
|
||||
different inputs to different extensions.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:type namespace: str
|
||||
:param check_func: Function to determine which extensions to load.
|
||||
:type check_func: callable
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:type invoke_on_load: bool
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_args: tuple
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_kwds: dict
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
:type invoke_on_load: bool
|
||||
"""
|
||||
|
||||
def map(self, filter_func, func, *args, **kwds):
|
||||
"""Iterate over the extensions invoking func() for any where
|
||||
filter_func() returns True.
|
||||
|
||||
The signature of filter_func() should be::
|
||||
|
||||
def filter_func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to filter_func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension`
|
||||
instance. filter_func() should return True if the extension
|
||||
should be invoked for the input arguments.
|
||||
|
||||
The signature for func() should be::
|
||||
|
||||
def func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension` instance.
|
||||
|
||||
Exceptions raised from within func() are propagated up and
|
||||
processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
:param filter_func: Callable to test each extension.
|
||||
:param func: Callable to invoke for each extension.
|
||||
:param args: Variable arguments to pass to func()
|
||||
:param kwds: Keyword arguments to pass to func()
|
||||
:returns: List of values returned from func()
|
||||
"""
|
||||
if not self.extensions:
|
||||
# FIXME: Use a more specific exception class here.
|
||||
raise NoMatches('No %s extensions found' % self.namespace)
|
||||
response = []
|
||||
for e in self.extensions:
|
||||
if filter_func(e, *args, **kwds):
|
||||
self._invoke_one_plugin(response.append, func, e, args, kwds)
|
||||
return response
|
||||
|
||||
def map_method(self, filter_func, method_name, *args, **kwds):
|
||||
"""Iterate over the extensions invoking each one's object method called
|
||||
`method_name` for any where filter_func() returns True.
|
||||
|
||||
This is equivalent of using :meth:`map` with func set to
|
||||
`lambda x: x.obj.method_name()`
|
||||
while being more convenient.
|
||||
|
||||
Exceptions raised from within the called method are propagated up
|
||||
and processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
.. versionadded:: 0.12
|
||||
|
||||
:param filter_func: Callable to test each extension.
|
||||
:param method_name: The extension method name to call
|
||||
for each extension.
|
||||
:param args: Variable arguments to pass to method
|
||||
:param kwds: Keyword arguments to pass to method
|
||||
:returns: List of values returned from methods
|
||||
"""
|
||||
return self.map(filter_func, self._call_extension_method,
|
||||
method_name, *args, **kwds)
|
||||
|
||||
|
||||
class NameDispatchExtensionManager(DispatchExtensionManager):
|
||||
"""Loads all plugins and filters on execution.
|
||||
|
||||
This is useful for long-running processes that need to pass
|
||||
different inputs to different extensions and can predict the name
|
||||
of the extensions before calling them.
|
||||
|
||||
The check_func argument should return a boolean, with ``True``
|
||||
indicating that the extension should be loaded and made available
|
||||
and ``False`` indicating that the extension should be ignored.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:type namespace: str
|
||||
:param check_func: Function to determine which extensions to load.
|
||||
:type check_func: callable
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:type invoke_on_load: bool
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_args: tuple
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_kwds: dict
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
:type invoke_on_load: bool
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
a entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:type on_load_failure_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:type verify_requirements: bool
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, check_func, invoke_on_load=False,
|
||||
invoke_args=(), invoke_kwds={},
|
||||
propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None,
|
||||
verify_requirements=False):
|
||||
super(NameDispatchExtensionManager, self).__init__(
|
||||
namespace=namespace,
|
||||
check_func=check_func,
|
||||
invoke_on_load=invoke_on_load,
|
||||
invoke_args=invoke_args,
|
||||
invoke_kwds=invoke_kwds,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
)
|
||||
|
||||
def _init_plugins(self, extensions):
|
||||
super(NameDispatchExtensionManager, self)._init_plugins(extensions)
|
||||
self.by_name = dict((e.name, e) for e in self.extensions)
|
||||
|
||||
def map(self, names, func, *args, **kwds):
|
||||
"""Iterate over the extensions invoking func() for any where
|
||||
the name is in the given list of names.
|
||||
|
||||
The signature for func() should be::
|
||||
|
||||
def func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension` instance.
|
||||
|
||||
Exceptions raised from within func() are propagated up and
|
||||
processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
:param names: List or set of name(s) of extension(s) to invoke.
|
||||
:param func: Callable to invoke for each extension.
|
||||
:param args: Variable arguments to pass to func()
|
||||
:param kwds: Keyword arguments to pass to func()
|
||||
:returns: List of values returned from func()
|
||||
"""
|
||||
response = []
|
||||
for name in names:
|
||||
try:
|
||||
e = self.by_name[name]
|
||||
except KeyError:
|
||||
LOG.debug('Missing extension %r being ignored', name)
|
||||
else:
|
||||
self._invoke_one_plugin(response.append, func, e, args, kwds)
|
||||
return response
|
||||
|
||||
def map_method(self, names, method_name, *args, **kwds):
|
||||
"""Iterate over the extensions invoking each one's object method called
|
||||
`method_name` for any where the name is in the given list of names.
|
||||
|
||||
This is equivalent of using :meth:`map` with func set to
|
||||
`lambda x: x.obj.method_name()`
|
||||
while being more convenient.
|
||||
|
||||
Exceptions raised from within the called method are propagated up
|
||||
and processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
.. versionadded:: 0.12
|
||||
|
||||
:param names: List or set of name(s) of extension(s) to invoke.
|
||||
:param method_name: The extension method name
|
||||
to call for each extension.
|
||||
:param args: Variable arguments to pass to method
|
||||
:param kwds: Keyword arguments to pass to method
|
||||
:returns: List of values returned from methods
|
||||
"""
|
||||
return self.map(names, self._call_extension_method,
|
||||
method_name, *args, **kwds)
|
@@ -1,148 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from .exception import NoMatches, MultipleMatches
|
||||
from .named import NamedExtensionManager
|
||||
|
||||
|
||||
class DriverManager(NamedExtensionManager):
|
||||
"""Load a single plugin with a given name from the namespace.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:type namespace: str
|
||||
:param name: The name of the driver to load.
|
||||
:type name: str
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:type invoke_on_load: bool
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_args: tuple
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_kwds: dict
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
a entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:type on_load_failure_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:type verify_requirements: bool
|
||||
:type warn_on_missing_entrypoint: bool
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, name,
|
||||
invoke_on_load=False, invoke_args=(), invoke_kwds={},
|
||||
on_load_failure_callback=None,
|
||||
verify_requirements=False,
|
||||
warn_on_missing_entrypoint=True):
|
||||
on_load_failure_callback = on_load_failure_callback \
|
||||
or self._default_on_load_failure
|
||||
super(DriverManager, self).__init__(
|
||||
namespace=namespace,
|
||||
names=[name],
|
||||
invoke_on_load=invoke_on_load,
|
||||
invoke_args=invoke_args,
|
||||
invoke_kwds=invoke_kwds,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
warn_on_missing_entrypoint=warn_on_missing_entrypoint
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _default_on_load_failure(drivermanager, ep, err):
|
||||
raise
|
||||
|
||||
@classmethod
|
||||
def make_test_instance(cls, extension, namespace='TESTING',
|
||||
propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None,
|
||||
verify_requirements=False):
|
||||
"""Construct a test DriverManager
|
||||
|
||||
Test instances are passed a list of extensions to work from rather
|
||||
than loading them from entry points.
|
||||
|
||||
:param extension: Pre-configured Extension instance
|
||||
:type extension: :class:`~stevedore.extension.Extension`
|
||||
:param namespace: The namespace for the manager; used only for
|
||||
identification since the extensions are passed in.
|
||||
:type namespace: str
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged
|
||||
and then ignored
|
||||
:type propagate_map_exceptions: bool
|
||||
:param on_load_failure_callback: Callback function that will
|
||||
be called when a entrypoint can not be loaded. The
|
||||
arguments that will be provided when this is called (when
|
||||
an entrypoint fails to load) are (manager, entrypoint,
|
||||
exception)
|
||||
:type on_load_failure_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:type verify_requirements: bool
|
||||
:return: The manager instance, initialized for testing
|
||||
|
||||
"""
|
||||
|
||||
o = super(DriverManager, cls).make_test_instance(
|
||||
[extension], namespace=namespace,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
verify_requirements=verify_requirements)
|
||||
return o
|
||||
|
||||
def _init_plugins(self, extensions):
|
||||
super(DriverManager, self)._init_plugins(extensions)
|
||||
|
||||
if not self.extensions:
|
||||
name = self._names[0]
|
||||
raise NoMatches('No %r driver found, looking for %r' %
|
||||
(self.namespace, name))
|
||||
if len(self.extensions) > 1:
|
||||
discovered_drivers = ','.join(e.entry_point_target
|
||||
for e in self.extensions)
|
||||
|
||||
raise MultipleMatches('Multiple %r drivers found: %s' %
|
||||
(self.namespace, discovered_drivers))
|
||||
|
||||
def __call__(self, func, *args, **kwds):
|
||||
"""Invokes func() for the single loaded extension.
|
||||
|
||||
The signature for func() should be::
|
||||
|
||||
def func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension` instance.
|
||||
|
||||
Exceptions raised from within func() are logged and ignored.
|
||||
|
||||
:param func: Callable to invoke for each extension.
|
||||
:param args: Variable arguments to pass to func()
|
||||
:param kwds: Keyword arguments to pass to func()
|
||||
:returns: List of values returned from func()
|
||||
"""
|
||||
results = self.map(func, *args, **kwds)
|
||||
if results:
|
||||
return results[0]
|
||||
|
||||
@property
|
||||
def driver(self):
|
||||
"""Returns the driver being used by this manager.
|
||||
"""
|
||||
ext = self.extensions[0]
|
||||
return ext.obj if ext.obj else ext.plugin
|
@@ -1,84 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from .extension import ExtensionManager
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EnabledExtensionManager(ExtensionManager):
|
||||
"""Loads only plugins that pass a check function.
|
||||
|
||||
The check_func argument should return a boolean, with ``True``
|
||||
indicating that the extension should be loaded and made available
|
||||
and ``False`` indicating that the extension should be ignored.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:type namespace: str
|
||||
:param check_func: Function to determine which extensions to load.
|
||||
:type check_func: callable, taking an :class:`Extension`
|
||||
instance as argument
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:type invoke_on_load: bool
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_args: tuple
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_kwds: dict
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
:type propagate_map_exceptions: bool
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
a entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:type on_load_failure_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:type verify_requirements: bool
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, check_func, invoke_on_load=False,
|
||||
invoke_args=(), invoke_kwds={},
|
||||
propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None,
|
||||
verify_requirements=False,):
|
||||
self.check_func = check_func
|
||||
super(EnabledExtensionManager, self).__init__(
|
||||
namespace,
|
||||
invoke_on_load=invoke_on_load,
|
||||
invoke_args=invoke_args,
|
||||
invoke_kwds=invoke_kwds,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
)
|
||||
|
||||
def _load_one_plugin(self, ep, invoke_on_load, invoke_args, invoke_kwds,
|
||||
verify_requirements):
|
||||
ext = super(EnabledExtensionManager, self)._load_one_plugin(
|
||||
ep, invoke_on_load, invoke_args, invoke_kwds,
|
||||
verify_requirements,
|
||||
)
|
||||
if ext and not self.check_func(ext):
|
||||
LOG.debug('ignoring extension %r', ep.name)
|
||||
return None
|
||||
return ext
|
@@ -1,22 +0,0 @@
|
||||
import abc
|
||||
|
||||
import six
|
||||
|
||||
|
||||
@six.add_metaclass(abc.ABCMeta)
|
||||
class FormatterBase(object):
|
||||
"""Base class for example plugin used in the tutorial.
|
||||
"""
|
||||
|
||||
def __init__(self, max_width=60):
|
||||
self.max_width = max_width
|
||||
|
||||
@abc.abstractmethod
|
||||
def format(self, data):
|
||||
"""Format the data and return unicode text.
|
||||
|
||||
:param data: A dictionary with string keys and simple types as
|
||||
values.
|
||||
:type data: dict(str:?)
|
||||
:returns: Iterable producing the formatted text.
|
||||
"""
|
@@ -1,37 +0,0 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
|
||||
from stevedore import driver
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'format',
|
||||
nargs='?',
|
||||
default='simple',
|
||||
help='the output format',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--width',
|
||||
default=60,
|
||||
type=int,
|
||||
help='maximum output width for text',
|
||||
)
|
||||
parsed_args = parser.parse_args()
|
||||
|
||||
data = {
|
||||
'a': 'A',
|
||||
'b': 'B',
|
||||
'long': 'word ' * 80,
|
||||
}
|
||||
|
||||
mgr = driver.DriverManager(
|
||||
namespace='stevedore.example.formatter',
|
||||
name=parsed_args.format,
|
||||
invoke_on_load=True,
|
||||
invoke_args=(parsed_args.width,),
|
||||
)
|
||||
for chunk in mgr.driver.format(data):
|
||||
print(chunk, end='')
|
@@ -1,39 +0,0 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import argparse
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
'--width',
|
||||
default=60,
|
||||
type=int,
|
||||
help='maximum output width for text',
|
||||
)
|
||||
parsed_args = parser.parse_args()
|
||||
|
||||
data = {
|
||||
'a': 'A',
|
||||
'b': 'B',
|
||||
'long': 'word ' * 80,
|
||||
}
|
||||
|
||||
mgr = extension.ExtensionManager(
|
||||
namespace='stevedore.example.formatter',
|
||||
invoke_on_load=True,
|
||||
invoke_args=(parsed_args.width,),
|
||||
)
|
||||
|
||||
def format_data(ext, data):
|
||||
return (ext.name, ext.obj.format(data))
|
||||
|
||||
results = mgr.map(format_data, data)
|
||||
|
||||
for name, result in results:
|
||||
print('Formatter: {0}'.format(name))
|
||||
for chunk in result:
|
||||
print(chunk, end='')
|
||||
print('')
|
@@ -1,43 +0,0 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='stevedore-examples',
|
||||
version='1.0',
|
||||
|
||||
description='Demonstration package for stevedore',
|
||||
|
||||
author='Doug Hellmann',
|
||||
author_email='doug@doughellmann.com',
|
||||
|
||||
url='http://git.openstack.org/cgit/openstack/stevedore',
|
||||
|
||||
classifiers=['Development Status :: 3 - Alpha',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Intended Audience :: Developers',
|
||||
'Environment :: Console',
|
||||
],
|
||||
|
||||
platforms=['Any'],
|
||||
|
||||
scripts=[],
|
||||
|
||||
provides=['stevedore.examples',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
|
||||
entry_points={
|
||||
'stevedore.example.formatter': [
|
||||
'simple = stevedore.example.simple:Simple',
|
||||
'plain = stevedore.example.simple:Simple',
|
||||
],
|
||||
},
|
||||
|
||||
zip_safe=False,
|
||||
)
|
@@ -1,20 +0,0 @@
|
||||
from stevedore.example import base
|
||||
|
||||
|
||||
class Simple(base.FormatterBase):
|
||||
"""A very basic formatter.
|
||||
"""
|
||||
|
||||
def format(self, data):
|
||||
"""Format the data and return unicode text.
|
||||
|
||||
:param data: A dictionary with string keys and simple types as
|
||||
values.
|
||||
:type data: dict(str:?)
|
||||
"""
|
||||
for name, value in sorted(data.items()):
|
||||
line = '{name} = {value}\n'.format(
|
||||
name=name,
|
||||
value=value,
|
||||
)
|
||||
yield line
|
@@ -1,36 +0,0 @@
|
||||
import textwrap
|
||||
|
||||
from stevedore.example import base
|
||||
|
||||
|
||||
class FieldList(base.FormatterBase):
|
||||
"""Format values as a reStructuredText field list.
|
||||
|
||||
For example::
|
||||
|
||||
: name1 : value
|
||||
: name2 : value
|
||||
: name3 : a long value
|
||||
will be wrapped with
|
||||
a hanging indent
|
||||
"""
|
||||
|
||||
def format(self, data):
|
||||
"""Format the data and return unicode text.
|
||||
|
||||
:param data: A dictionary with string keys and simple types as
|
||||
values.
|
||||
:type data: dict(str:?)
|
||||
"""
|
||||
for name, value in sorted(data.items()):
|
||||
full_text = ': {name} : {value}'.format(
|
||||
name=name,
|
||||
value=value,
|
||||
)
|
||||
wrapped_text = textwrap.fill(
|
||||
full_text,
|
||||
initial_indent='',
|
||||
subsequent_indent=' ',
|
||||
width=self.max_width,
|
||||
)
|
||||
yield wrapped_text + '\n'
|
@@ -1,42 +0,0 @@
|
||||
from setuptools import setup, find_packages
|
||||
|
||||
setup(
|
||||
name='stevedore-examples2',
|
||||
version='1.0',
|
||||
|
||||
description='Demonstration package for stevedore',
|
||||
|
||||
author='Doug Hellmann',
|
||||
author_email='doug@doughellmann.com',
|
||||
|
||||
url='http://git.openstack.org/cgit/openstack/stevedore',
|
||||
|
||||
classifiers=['Development Status :: 3 - Alpha',
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 2',
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Intended Audience :: Developers',
|
||||
'Environment :: Console',
|
||||
],
|
||||
|
||||
platforms=['Any'],
|
||||
|
||||
scripts=[],
|
||||
|
||||
provides=['stevedore.examples2',
|
||||
],
|
||||
|
||||
packages=find_packages(),
|
||||
include_package_data=True,
|
||||
|
||||
entry_points={
|
||||
'stevedore.example.formatter': [
|
||||
'field = stevedore.example2.fields:FieldList',
|
||||
],
|
||||
},
|
||||
|
||||
zip_safe=False,
|
||||
)
|
@@ -1,23 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
|
||||
class NoUniqueMatch(RuntimeError):
|
||||
"""There was more than one extension, or none, that matched the query."""
|
||||
|
||||
|
||||
class NoMatches(NoUniqueMatch):
|
||||
"""There were no extensions with the driver name found."""
|
||||
|
||||
|
||||
class MultipleMatches(NoUniqueMatch):
|
||||
"""There were multiple matches for the given name."""
|
@@ -1,319 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""ExtensionManager
|
||||
"""
|
||||
|
||||
import operator
|
||||
import pkg_resources
|
||||
|
||||
import logging
|
||||
|
||||
from .exception import NoMatches
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class Extension(object):
|
||||
"""Book-keeping object for tracking extensions.
|
||||
|
||||
The arguments passed to the constructor are saved as attributes of
|
||||
the instance using the same names, and can be accessed by the
|
||||
callables passed to :meth:`map` or when iterating over an
|
||||
:class:`ExtensionManager` directly.
|
||||
|
||||
:param name: The entry point name.
|
||||
:type name: str
|
||||
:param entry_point: The EntryPoint instance returned by
|
||||
:mod:`pkg_resources`.
|
||||
:type entry_point: EntryPoint
|
||||
:param plugin: The value returned by entry_point.load()
|
||||
:param obj: The object returned by ``plugin(*args, **kwds)`` if the
|
||||
manager invoked the extension on load.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, entry_point, plugin, obj):
|
||||
self.name = name
|
||||
self.entry_point = entry_point
|
||||
self.plugin = plugin
|
||||
self.obj = obj
|
||||
|
||||
@property
|
||||
def entry_point_target(self):
|
||||
"""The module and attribute referenced by this extension's entry_point.
|
||||
|
||||
:return: A string representation of the target of the entry point in
|
||||
'dotted.module:object' format.
|
||||
"""
|
||||
return '%s:%s' % (self.entry_point.module_name,
|
||||
self.entry_point.attrs[0])
|
||||
|
||||
|
||||
class ExtensionManager(object):
|
||||
"""Base class for all of the other managers.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:type namespace: str
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:type invoke_on_load: bool
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_args: tuple
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_kwds: dict
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
:type propagate_map_exceptions: bool
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
a entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:type on_load_failure_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:type verify_requirements: bool
|
||||
"""
|
||||
|
||||
def __init__(self, namespace,
|
||||
invoke_on_load=False,
|
||||
invoke_args=(),
|
||||
invoke_kwds={},
|
||||
propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None,
|
||||
verify_requirements=False):
|
||||
self._init_attributes(
|
||||
namespace,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback)
|
||||
extensions = self._load_plugins(invoke_on_load,
|
||||
invoke_args,
|
||||
invoke_kwds,
|
||||
verify_requirements)
|
||||
self._init_plugins(extensions)
|
||||
|
||||
@classmethod
|
||||
def make_test_instance(cls, extensions, namespace='TESTING',
|
||||
propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None,
|
||||
verify_requirements=False):
|
||||
"""Construct a test ExtensionManager
|
||||
|
||||
Test instances are passed a list of extensions to work from rather
|
||||
than loading them from entry points.
|
||||
|
||||
:param extensions: Pre-configured Extension instances to use
|
||||
:type extensions: list of :class:`~stevedore.extension.Extension`
|
||||
:param namespace: The namespace for the manager; used only for
|
||||
identification since the extensions are passed in.
|
||||
:type namespace: str
|
||||
:param propagate_map_exceptions: When calling map, controls whether
|
||||
exceptions are propagated up through the map call or whether they
|
||||
are logged and then ignored
|
||||
:type propagate_map_exceptions: bool
|
||||
:param on_load_failure_callback: Callback function that will
|
||||
be called when a entrypoint can not be loaded. The
|
||||
arguments that will be provided when this is called (when
|
||||
an entrypoint fails to load) are (manager, entrypoint,
|
||||
exception)
|
||||
:type on_load_failure_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:type verify_requirements: bool
|
||||
:return: The manager instance, initialized for testing
|
||||
|
||||
"""
|
||||
|
||||
o = cls.__new__(cls)
|
||||
o._init_attributes(namespace,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback)
|
||||
o._init_plugins(extensions)
|
||||
return o
|
||||
|
||||
def _init_attributes(self, namespace, propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None):
|
||||
self.namespace = namespace
|
||||
self.propagate_map_exceptions = propagate_map_exceptions
|
||||
self._on_load_failure_callback = on_load_failure_callback
|
||||
|
||||
def _init_plugins(self, extensions):
|
||||
self.extensions = extensions
|
||||
self._extensions_by_name = None
|
||||
|
||||
ENTRY_POINT_CACHE = {}
|
||||
|
||||
def list_entry_points(self):
|
||||
"""Return the list of entry points for this namespace.
|
||||
|
||||
The entry points are not actually loaded, their list is just read and
|
||||
returned.
|
||||
|
||||
"""
|
||||
if self.namespace not in self.ENTRY_POINT_CACHE:
|
||||
eps = list(pkg_resources.iter_entry_points(self.namespace))
|
||||
self.ENTRY_POINT_CACHE[self.namespace] = eps
|
||||
return self.ENTRY_POINT_CACHE[self.namespace]
|
||||
|
||||
def entry_points_names(self):
|
||||
"""Return the list of entry points names for this namespace."""
|
||||
return list(map(operator.attrgetter("name"), self.list_entry_points()))
|
||||
|
||||
def _load_plugins(self, invoke_on_load, invoke_args, invoke_kwds,
|
||||
verify_requirements):
|
||||
extensions = []
|
||||
for ep in self.list_entry_points():
|
||||
LOG.debug('found extension %r', ep)
|
||||
try:
|
||||
ext = self._load_one_plugin(ep,
|
||||
invoke_on_load,
|
||||
invoke_args,
|
||||
invoke_kwds,
|
||||
verify_requirements,
|
||||
)
|
||||
if ext:
|
||||
extensions.append(ext)
|
||||
except (KeyboardInterrupt, AssertionError):
|
||||
raise
|
||||
except Exception as err:
|
||||
if self._on_load_failure_callback is not None:
|
||||
self._on_load_failure_callback(self, ep, err)
|
||||
else:
|
||||
# Log the reason we couldn't import the module,
|
||||
# usually without a traceback. The most common
|
||||
# reason is an ImportError due to a missing
|
||||
# dependency, and the error message should be
|
||||
# enough to debug that. If debug logging is
|
||||
# enabled for our logger, provide the full
|
||||
# traceback.
|
||||
LOG.error('Could not load %r: %s', ep.name, err,
|
||||
exc_info=LOG.isEnabledFor(logging.DEBUG))
|
||||
return extensions
|
||||
|
||||
def _load_one_plugin(self, ep, invoke_on_load, invoke_args, invoke_kwds,
|
||||
verify_requirements):
|
||||
# NOTE(dhellmann): Using require=False is deprecated in
|
||||
# setuptools 11.3.
|
||||
if hasattr(ep, 'resolve') and hasattr(ep, 'require'):
|
||||
if verify_requirements:
|
||||
ep.require()
|
||||
plugin = ep.resolve()
|
||||
else:
|
||||
plugin = ep.load(require=verify_requirements)
|
||||
if invoke_on_load:
|
||||
obj = plugin(*invoke_args, **invoke_kwds)
|
||||
else:
|
||||
obj = None
|
||||
return Extension(ep.name, ep, plugin, obj)
|
||||
|
||||
def names(self):
|
||||
"Returns the names of the discovered extensions"
|
||||
# We want to return the names of the extensions in the order
|
||||
# they would be used by map(), since some subclasses change
|
||||
# that order.
|
||||
return [e.name for e in self.extensions]
|
||||
|
||||
def map(self, func, *args, **kwds):
|
||||
"""Iterate over the extensions invoking func() for each.
|
||||
|
||||
The signature for func() should be::
|
||||
|
||||
def func(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
The first argument to func(), 'ext', is the
|
||||
:class:`~stevedore.extension.Extension` instance.
|
||||
|
||||
Exceptions raised from within func() are propagated up and
|
||||
processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
:param func: Callable to invoke for each extension.
|
||||
:param args: Variable arguments to pass to func()
|
||||
:param kwds: Keyword arguments to pass to func()
|
||||
:returns: List of values returned from func()
|
||||
"""
|
||||
if not self.extensions:
|
||||
# FIXME: Use a more specific exception class here.
|
||||
raise NoMatches('No %s extensions found' % self.namespace)
|
||||
response = []
|
||||
for e in self.extensions:
|
||||
self._invoke_one_plugin(response.append, func, e, args, kwds)
|
||||
return response
|
||||
|
||||
@staticmethod
|
||||
def _call_extension_method(extension, method_name, *args, **kwds):
|
||||
return getattr(extension.obj, method_name)(*args, **kwds)
|
||||
|
||||
def map_method(self, method_name, *args, **kwds):
|
||||
"""Iterate over the extensions invoking a method by name.
|
||||
|
||||
This is equivalent of using :meth:`map` with func set to
|
||||
`lambda x: x.obj.method_name()`
|
||||
while being more convenient.
|
||||
|
||||
Exceptions raised from within the called method are propagated up
|
||||
and processing stopped if self.propagate_map_exceptions is True,
|
||||
otherwise they are logged and ignored.
|
||||
|
||||
.. versionadded:: 0.12
|
||||
|
||||
:param method_name: The extension method name
|
||||
to call for each extension.
|
||||
:param args: Variable arguments to pass to method
|
||||
:param kwds: Keyword arguments to pass to method
|
||||
:returns: List of values returned from methods
|
||||
"""
|
||||
return self.map(self._call_extension_method,
|
||||
method_name, *args, **kwds)
|
||||
|
||||
def _invoke_one_plugin(self, response_callback, func, e, args, kwds):
|
||||
try:
|
||||
response_callback(func(e, *args, **kwds))
|
||||
except Exception as err:
|
||||
if self.propagate_map_exceptions:
|
||||
raise
|
||||
else:
|
||||
LOG.error('error calling %r: %s', e.name, err)
|
||||
LOG.exception(err)
|
||||
|
||||
def __iter__(self):
|
||||
"""Produce iterator for the manager.
|
||||
|
||||
Iterating over an ExtensionManager produces the :class:`Extension`
|
||||
instances in the order they would be invoked.
|
||||
"""
|
||||
return iter(self.extensions)
|
||||
|
||||
def __getitem__(self, name):
|
||||
"""Return the named extension.
|
||||
|
||||
Accessing an ExtensionManager as a dictionary (``em['name']``)
|
||||
produces the :class:`Extension` instance with the
|
||||
specified name.
|
||||
"""
|
||||
if self._extensions_by_name is None:
|
||||
d = {}
|
||||
for e in self.extensions:
|
||||
d[e.name] = e
|
||||
self._extensions_by_name = d
|
||||
return self._extensions_by_name[name]
|
||||
|
||||
def __contains__(self, name):
|
||||
"""Return true if name is in list of enabled extensions.
|
||||
"""
|
||||
return any(extension.name == name for extension in self.extensions)
|
@@ -1,91 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from .named import NamedExtensionManager
|
||||
|
||||
|
||||
class HookManager(NamedExtensionManager):
|
||||
"""Coordinate execution of multiple extensions using a common name.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:type namespace: str
|
||||
:param name: The name of the hooks to load.
|
||||
:type name: str
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:type invoke_on_load: bool
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_args: tuple
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_kwds: dict
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
a entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:type on_load_failure_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:type verify_requirements: bool
|
||||
:type on_missing_entrypoints_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:param warn_on_missing_entrypoint: Flag to control whether failing
|
||||
to load a plugin is reported via a log mess. Only applies if
|
||||
on_missing_entrypoints_callback is None.
|
||||
:type warn_on_missing_entrypoint: bool
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, name,
|
||||
invoke_on_load=False, invoke_args=(), invoke_kwds={},
|
||||
on_load_failure_callback=None,
|
||||
verify_requirements=False,
|
||||
on_missing_entrypoints_callback=None,
|
||||
# NOTE(dhellmann): This default is different from the
|
||||
# base class because for hooks it is less likely to
|
||||
# be an error to have no entry points present.
|
||||
warn_on_missing_entrypoint=False):
|
||||
super(HookManager, self).__init__(
|
||||
namespace,
|
||||
[name],
|
||||
invoke_on_load=invoke_on_load,
|
||||
invoke_args=invoke_args,
|
||||
invoke_kwds=invoke_kwds,
|
||||
on_load_failure_callback=on_load_failure_callback,
|
||||
on_missing_entrypoints_callback=on_missing_entrypoints_callback,
|
||||
verify_requirements=verify_requirements,
|
||||
warn_on_missing_entrypoint=warn_on_missing_entrypoint,
|
||||
)
|
||||
|
||||
def _init_attributes(self, namespace, names, name_order=False,
|
||||
propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None):
|
||||
super(HookManager, self)._init_attributes(
|
||||
namespace, names,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback)
|
||||
self._name = names[0]
|
||||
|
||||
def __getitem__(self, name):
|
||||
"""Return the named extensions.
|
||||
|
||||
Accessing a HookManager as a dictionary (``em['name']``)
|
||||
produces a list of the :class:`Extension` instance(s) with the
|
||||
specified name, in the order they would be invoked by map().
|
||||
"""
|
||||
if name != self._name:
|
||||
raise KeyError(name)
|
||||
return self.extensions
|
@@ -1,159 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import logging
|
||||
|
||||
from .extension import ExtensionManager
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class NamedExtensionManager(ExtensionManager):
|
||||
"""Loads only the named extensions.
|
||||
|
||||
This is useful for explicitly enabling extensions in a
|
||||
configuration file, for example.
|
||||
|
||||
:param namespace: The namespace for the entry points.
|
||||
:type namespace: str
|
||||
:param names: The names of the extensions to load.
|
||||
:type names: list(str)
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:type invoke_on_load: bool
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_args: tuple
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_kwds: dict
|
||||
:param name_order: If true, sort the loaded extensions to match the
|
||||
order used in ``names``.
|
||||
:type name_order: bool
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged and
|
||||
then ignored
|
||||
:type propagate_map_exceptions: bool
|
||||
:param on_load_failure_callback: Callback function that will be called when
|
||||
a entrypoint can not be loaded. The arguments that will be provided
|
||||
when this is called (when an entrypoint fails to load) are
|
||||
(manager, entrypoint, exception)
|
||||
:type on_load_failure_callback: function
|
||||
:param on_missing_entrypoints_callback: Callback function that will be
|
||||
called when one or more names cannot be found. The provided argument
|
||||
will be a subset of the 'names' parameter.
|
||||
:type on_missing_entrypoints_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:type verify_requirements: bool
|
||||
:param warn_on_missing_entrypoint: Flag to control whether failing
|
||||
to load a plugin is reported via a log mess. Only applies if
|
||||
on_missing_entrypoints_callback is None.
|
||||
:type warn_on_missing_entrypoint: bool
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, namespace, names,
|
||||
invoke_on_load=False, invoke_args=(), invoke_kwds={},
|
||||
name_order=False, propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None,
|
||||
on_missing_entrypoints_callback=None,
|
||||
verify_requirements=False,
|
||||
warn_on_missing_entrypoint=True):
|
||||
self._init_attributes(
|
||||
namespace, names, name_order=name_order,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback)
|
||||
extensions = self._load_plugins(invoke_on_load,
|
||||
invoke_args,
|
||||
invoke_kwds,
|
||||
verify_requirements)
|
||||
self._missing_names = set(names) - set([e.name for e in extensions])
|
||||
if self._missing_names:
|
||||
if on_missing_entrypoints_callback:
|
||||
on_missing_entrypoints_callback(self._missing_names)
|
||||
elif warn_on_missing_entrypoint:
|
||||
LOG.warning('Could not load %s' %
|
||||
', '.join(self._missing_names))
|
||||
self._init_plugins(extensions)
|
||||
|
||||
@classmethod
|
||||
def make_test_instance(cls, extensions, namespace='TESTING',
|
||||
propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None,
|
||||
verify_requirements=False):
|
||||
"""Construct a test NamedExtensionManager
|
||||
|
||||
Test instances are passed a list of extensions to use rather than
|
||||
loading them from entry points.
|
||||
|
||||
:param extensions: Pre-configured Extension instances
|
||||
:type extensions: list of :class:`~stevedore.extension.Extension`
|
||||
:param namespace: The namespace for the manager; used only for
|
||||
identification since the extensions are passed in.
|
||||
:type namespace: str
|
||||
:param propagate_map_exceptions: Boolean controlling whether exceptions
|
||||
are propagated up through the map call or whether they are logged
|
||||
and then ignored
|
||||
:type propagate_map_exceptions: bool
|
||||
:param on_load_failure_callback: Callback function that will
|
||||
be called when a entrypoint can not be loaded. The
|
||||
arguments that will be provided when this is called (when
|
||||
an entrypoint fails to load) are (manager, entrypoint,
|
||||
exception)
|
||||
:type on_load_failure_callback: function
|
||||
:param verify_requirements: Use setuptools to enforce the
|
||||
dependencies of the plugin(s) being loaded. Defaults to False.
|
||||
:type verify_requirements: bool
|
||||
:return: The manager instance, initialized for testing
|
||||
|
||||
"""
|
||||
|
||||
o = cls.__new__(cls)
|
||||
names = [e.name for e in extensions]
|
||||
o._init_attributes(namespace, names,
|
||||
propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback)
|
||||
o._init_plugins(extensions)
|
||||
return o
|
||||
|
||||
def _init_attributes(self, namespace, names, name_order=False,
|
||||
propagate_map_exceptions=False,
|
||||
on_load_failure_callback=None):
|
||||
super(NamedExtensionManager, self)._init_attributes(
|
||||
namespace, propagate_map_exceptions=propagate_map_exceptions,
|
||||
on_load_failure_callback=on_load_failure_callback)
|
||||
|
||||
self._names = names
|
||||
self._missing_names = set()
|
||||
self._name_order = name_order
|
||||
|
||||
def _init_plugins(self, extensions):
|
||||
super(NamedExtensionManager, self)._init_plugins(extensions)
|
||||
|
||||
if self._name_order:
|
||||
self.extensions = [self[n] for n in self._names
|
||||
if n not in self._missing_names]
|
||||
|
||||
def _load_one_plugin(self, ep, invoke_on_load, invoke_args, invoke_kwds,
|
||||
verify_requirements):
|
||||
# Check the name before going any further to prevent
|
||||
# undesirable code from being loaded at all if we are not
|
||||
# going to use it.
|
||||
if ep.name not in self._names:
|
||||
return None
|
||||
return super(NamedExtensionManager, self)._load_one_plugin(
|
||||
ep, invoke_on_load, invoke_args, invoke_kwds,
|
||||
verify_requirements,
|
||||
)
|
@@ -1,115 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import inspect
|
||||
|
||||
from docutils import nodes
|
||||
from docutils.parsers import rst
|
||||
from docutils.parsers.rst import directives
|
||||
from docutils.statemachine import ViewList
|
||||
from sphinx.util.nodes import nested_parse_with_titles
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
|
||||
def _get_docstring(plugin):
|
||||
return inspect.getdoc(plugin) or ''
|
||||
|
||||
|
||||
def _simple_list(mgr):
|
||||
for name in sorted(mgr.names()):
|
||||
ext = mgr[name]
|
||||
doc = _get_docstring(ext.plugin) or '\n'
|
||||
summary = doc.splitlines()[0].strip()
|
||||
yield('* %s -- %s' % (ext.name, summary),
|
||||
ext.entry_point.module_name)
|
||||
|
||||
|
||||
def _detailed_list(mgr, over='', under='-', titlecase=False):
|
||||
for name in sorted(mgr.names()):
|
||||
ext = mgr[name]
|
||||
if over:
|
||||
yield (over * len(ext.name), ext.entry_point.module_name)
|
||||
if titlecase:
|
||||
yield (ext.name.title(), ext.entry_point.module_name)
|
||||
else:
|
||||
yield (ext.name, ext.entry_point.module_name)
|
||||
if under:
|
||||
yield (under * len(ext.name), ext.entry_point.module_name)
|
||||
yield ('\n', ext.entry_point.module_name)
|
||||
doc = _get_docstring(ext.plugin)
|
||||
if doc:
|
||||
yield (doc, ext.entry_point.module_name)
|
||||
else:
|
||||
yield ('.. warning:: No documentation found in %s'
|
||||
% ext.entry_point,
|
||||
ext.entry_point.module_name)
|
||||
yield ('\n', ext.entry_point.module_name)
|
||||
|
||||
|
||||
class ListPluginsDirective(rst.Directive):
|
||||
"""Present a simple list of the plugins in a namespace."""
|
||||
|
||||
option_spec = {
|
||||
'class': directives.class_option,
|
||||
'detailed': directives.flag,
|
||||
'titlecase': directives.flag,
|
||||
'overline-style': directives.single_char_or_unicode,
|
||||
'underline-style': directives.single_char_or_unicode,
|
||||
}
|
||||
|
||||
has_content = True
|
||||
|
||||
def run(self):
|
||||
env = self.state.document.settings.env
|
||||
app = env.app
|
||||
|
||||
namespace = ' '.join(self.content).strip()
|
||||
app.info('documenting plugins from %r' % namespace)
|
||||
overline_style = self.options.get('overline-style', '')
|
||||
underline_style = self.options.get('underline-style', '=')
|
||||
|
||||
def report_load_failure(mgr, ep, err):
|
||||
app.warn(u'Failed to load %s: %s' % (ep.module_name, err))
|
||||
|
||||
mgr = extension.ExtensionManager(
|
||||
namespace,
|
||||
on_load_failure_callback=report_load_failure,
|
||||
)
|
||||
|
||||
result = ViewList()
|
||||
|
||||
titlecase = 'titlecase' in self.options
|
||||
|
||||
if 'detailed' in self.options:
|
||||
data = _detailed_list(
|
||||
mgr, over=overline_style, under=underline_style,
|
||||
titlecase=titlecase)
|
||||
else:
|
||||
data = _simple_list(mgr)
|
||||
for text, source in data:
|
||||
for line in text.splitlines():
|
||||
result.append(line, source)
|
||||
|
||||
# Parse what we have into a new section.
|
||||
node = nodes.section()
|
||||
node.document = self.state.document
|
||||
nested_parse_with_titles(self.state, result, node)
|
||||
|
||||
return node.children
|
||||
|
||||
|
||||
def setup(app):
|
||||
app.info('loading stevedore.sphinxext')
|
||||
app.add_directive('list-plugins', ListPluginsDirective)
|
@@ -1,67 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""TestExtensionManager
|
||||
|
||||
Extension manager used only for testing.
|
||||
"""
|
||||
|
||||
import warnings
|
||||
|
||||
from stevedore import extension
|
||||
|
||||
|
||||
class TestExtensionManager(extension.ExtensionManager):
|
||||
"""ExtensionManager that is explicitly initialized for tests.
|
||||
|
||||
.. deprecated:: 0.13
|
||||
|
||||
Use the :func:`make_test_instance` class method of the class
|
||||
being replaced by the test instance instead of using this class
|
||||
directly.
|
||||
|
||||
:param extensions: Pre-configured Extension instances to use
|
||||
instead of loading them from entry points.
|
||||
:type extensions: list of :class:`~stevedore.extension.Extension`
|
||||
:param namespace: The namespace for the entry points.
|
||||
:type namespace: str
|
||||
:param invoke_on_load: Boolean controlling whether to invoke the
|
||||
object returned by the entry point after the driver is loaded.
|
||||
:type invoke_on_load: bool
|
||||
:param invoke_args: Positional arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_args: tuple
|
||||
:param invoke_kwds: Named arguments to pass when invoking
|
||||
the object returned by the entry point. Only used if invoke_on_load
|
||||
is True.
|
||||
:type invoke_kwds: dict
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, extensions,
|
||||
namespace='test',
|
||||
invoke_on_load=False,
|
||||
invoke_args=(),
|
||||
invoke_kwds={}):
|
||||
super(TestExtensionManager, self).__init__(namespace,
|
||||
invoke_on_load,
|
||||
invoke_args,
|
||||
invoke_kwds,
|
||||
)
|
||||
self.extensions = extensions
|
||||
warnings.warn(
|
||||
'TestExtesionManager has been replaced by make_test_instance()',
|
||||
DeprecationWarning)
|
||||
|
||||
def _load_plugins(self, *args, **kwds):
|
||||
return []
|
@@ -1,55 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for failure loading callback
|
||||
"""
|
||||
from testtools.matchers import GreaterThan
|
||||
import mock
|
||||
|
||||
from stevedore import extension
|
||||
from stevedore import named
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestCallback(utils.TestCase):
|
||||
def test_extension_failure_custom_callback(self):
|
||||
errors = []
|
||||
|
||||
def failure_callback(manager, entrypoint, error):
|
||||
errors.append((manager, entrypoint, error))
|
||||
|
||||
em = extension.ExtensionManager('stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
on_load_failure_callback=
|
||||
failure_callback)
|
||||
extensions = list(em.extensions)
|
||||
self.assertTrue(len(extensions), GreaterThan(0))
|
||||
self.assertEqual(len(errors), 2)
|
||||
for manager, entrypoint, error in errors:
|
||||
self.assertIs(manager, em)
|
||||
self.assertIsInstance(error, (IOError, ImportError))
|
||||
|
||||
@mock.patch('stevedore.named.NamedExtensionManager._load_plugins')
|
||||
def test_missing_entrypoints_callback(self, load_fn):
|
||||
errors = set()
|
||||
|
||||
def callback(names):
|
||||
errors.update(names)
|
||||
|
||||
load_fn.return_value = [
|
||||
extension.Extension('foo', None, None, None)
|
||||
]
|
||||
named.NamedExtensionManager('stevedore.test.extension',
|
||||
names=['foo', 'bar'],
|
||||
invoke_on_load=True,
|
||||
on_missing_entrypoints_callback=callback)
|
||||
self.assertEqual(errors, {'bar'})
|
@@ -1,103 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from stevedore.tests import utils
|
||||
from stevedore import dispatch
|
||||
|
||||
|
||||
def check_dispatch(ep, *args, **kwds):
|
||||
return ep.name == 't2'
|
||||
|
||||
|
||||
class TestDispatch(utils.TestCase):
|
||||
def check_dispatch(ep, *args, **kwds):
|
||||
return ep.name == 't2'
|
||||
|
||||
def test_dispatch(self):
|
||||
|
||||
def invoke(ep, *args, **kwds):
|
||||
return (ep.name, args, kwds)
|
||||
|
||||
em = dispatch.DispatchExtensionManager('stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 2)
|
||||
self.assertEqual(set(em.names()), set(['t1', 't2']))
|
||||
|
||||
results = em.map(check_dispatch,
|
||||
invoke,
|
||||
'first',
|
||||
named='named value',
|
||||
)
|
||||
expected = [('t2', ('first',), {'named': 'named value'})]
|
||||
self.assertEqual(results, expected)
|
||||
|
||||
def test_dispatch_map_method(self):
|
||||
em = dispatch.DispatchExtensionManager('stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
|
||||
results = em.map_method(check_dispatch, 'get_args_and_data', 'first')
|
||||
self.assertEqual(results, [(('a',), {'b': 'B'}, 'first')])
|
||||
|
||||
def test_name_dispatch(self):
|
||||
|
||||
def invoke(ep, *args, **kwds):
|
||||
return (ep.name, args, kwds)
|
||||
|
||||
em = dispatch.NameDispatchExtensionManager('stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 2)
|
||||
self.assertEqual(set(em.names()), set(['t1', 't2']))
|
||||
|
||||
results = em.map(['t2'], invoke, 'first', named='named value',)
|
||||
expected = [('t2', ('first',), {'named': 'named value'})]
|
||||
self.assertEqual(results, expected)
|
||||
|
||||
def test_name_dispatch_ignore_missing(self):
|
||||
|
||||
def invoke(ep, *args, **kwds):
|
||||
return (ep.name, args, kwds)
|
||||
|
||||
em = dispatch.NameDispatchExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
|
||||
results = em.map(['t3', 't1'], invoke, 'first', named='named value',)
|
||||
expected = [('t1', ('first',), {'named': 'named value'})]
|
||||
self.assertEqual(results, expected)
|
||||
|
||||
def test_name_dispatch_map_method(self):
|
||||
em = dispatch.NameDispatchExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
lambda *args, **kwds: True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
|
||||
results = em.map_method(['t3', 't1'], 'get_args_and_data', 'first')
|
||||
self.assertEqual(results, [(('a',), {'b': 'B'}, 'first')])
|
@@ -1,89 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for stevedore.extension
|
||||
"""
|
||||
|
||||
import pkg_resources
|
||||
|
||||
from stevedore import driver
|
||||
from stevedore import exception
|
||||
from stevedore import extension
|
||||
from stevedore.tests import test_extension
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestCallback(utils.TestCase):
|
||||
def test_detect_plugins(self):
|
||||
em = driver.DriverManager('stevedore.test.extension', 't1')
|
||||
names = sorted(em.names())
|
||||
self.assertEqual(names, ['t1'])
|
||||
|
||||
def test_call(self):
|
||||
def invoke(ext, *args, **kwds):
|
||||
return (ext.name, args, kwds)
|
||||
em = driver.DriverManager('stevedore.test.extension', 't1')
|
||||
result = em(invoke, 'a', b='C')
|
||||
self.assertEqual(result, ('t1', ('a',), {'b': 'C'}))
|
||||
|
||||
def test_driver_property_not_invoked_on_load(self):
|
||||
em = driver.DriverManager('stevedore.test.extension', 't1',
|
||||
invoke_on_load=False)
|
||||
d = em.driver
|
||||
self.assertIs(d, test_extension.FauxExtension)
|
||||
|
||||
def test_driver_property_invoked_on_load(self):
|
||||
em = driver.DriverManager('stevedore.test.extension', 't1',
|
||||
invoke_on_load=True)
|
||||
d = em.driver
|
||||
self.assertIsInstance(d, test_extension.FauxExtension)
|
||||
|
||||
def test_no_drivers(self):
|
||||
try:
|
||||
driver.DriverManager('stevedore.test.extension.none', 't1')
|
||||
except exception.NoMatches as err:
|
||||
self.assertIn("No 'stevedore.test.extension.none' driver found",
|
||||
str(err))
|
||||
|
||||
def test_bad_driver(self):
|
||||
try:
|
||||
driver.DriverManager('stevedore.test.extension', 'e2')
|
||||
except ImportError:
|
||||
pass
|
||||
else:
|
||||
self.assertEqual(False, "No error raised")
|
||||
|
||||
def test_multiple_drivers(self):
|
||||
# The idea for this test was contributed by clayg:
|
||||
# https://gist.github.com/clayg/6311348
|
||||
extensions = [
|
||||
extension.Extension(
|
||||
'backend',
|
||||
pkg_resources.EntryPoint.parse('backend = pkg1:driver'),
|
||||
'pkg backend',
|
||||
None,
|
||||
),
|
||||
extension.Extension(
|
||||
'backend',
|
||||
pkg_resources.EntryPoint.parse('backend = pkg2:driver'),
|
||||
'pkg backend',
|
||||
None,
|
||||
),
|
||||
]
|
||||
try:
|
||||
dm = driver.DriverManager.make_test_instance(extensions[0])
|
||||
# Call the initialization code that verifies the extension
|
||||
dm._init_plugins(extensions)
|
||||
except exception.MultipleMatches as err:
|
||||
self.assertIn("Multiple", str(err))
|
||||
else:
|
||||
self.fail('Should have had an error')
|
@@ -1,42 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from stevedore import enabled
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestEnabled(utils.TestCase):
|
||||
def test_enabled(self):
|
||||
def check_enabled(ep):
|
||||
return ep.name == 't2'
|
||||
em = enabled.EnabledExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
check_enabled,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 1)
|
||||
self.assertEqual(em.names(), ['t2'])
|
||||
|
||||
def test_enabled_after_load(self):
|
||||
def check_enabled(ext):
|
||||
return ext.obj and ext.name == 't2'
|
||||
em = enabled.EnabledExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
check_enabled,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 1)
|
||||
self.assertEqual(em.names(), ['t2'])
|
@@ -1,41 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for stevedore.example2.fields
|
||||
"""
|
||||
|
||||
from stevedore.example2 import fields
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestExampleFields(utils.TestCase):
|
||||
def test_simple_items(self):
|
||||
f = fields.FieldList(100)
|
||||
text = ''.join(f.format({'a': 'A', 'b': 'B'}))
|
||||
expected = '\n'.join([
|
||||
': a : A',
|
||||
': b : B',
|
||||
'',
|
||||
])
|
||||
self.assertEqual(text, expected)
|
||||
|
||||
def test_long_item(self):
|
||||
f = fields.FieldList(25)
|
||||
text = ''.join(f.format({'name':
|
||||
'a value longer than the allowed width'}))
|
||||
expected = '\n'.join([
|
||||
': name : a value longer',
|
||||
' than the allowed',
|
||||
' width',
|
||||
'',
|
||||
])
|
||||
self.assertEqual(text, expected)
|
@@ -1,29 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for stevedore.example.simple
|
||||
"""
|
||||
|
||||
from stevedore.example import simple
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestExampleSimple(utils.TestCase):
|
||||
def test_simple_items(self):
|
||||
f = simple.Simple(100)
|
||||
text = ''.join(f.format({'a': 'A', 'b': 'B'}))
|
||||
expected = '\n'.join([
|
||||
'a = A',
|
||||
'b = B',
|
||||
'',
|
||||
])
|
||||
self.assertEqual(text, expected)
|
@@ -1,239 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
"""Tests for stevedore.extension
|
||||
"""
|
||||
|
||||
import operator
|
||||
|
||||
import mock
|
||||
|
||||
from stevedore import exception
|
||||
from stevedore import extension
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
ALL_NAMES = ['e1', 't1', 't2']
|
||||
WORKING_NAMES = ['t1', 't2']
|
||||
|
||||
|
||||
class FauxExtension(object):
|
||||
def __init__(self, *args, **kwds):
|
||||
self.args = args
|
||||
self.kwds = kwds
|
||||
|
||||
def get_args_and_data(self, data):
|
||||
return self.args, self.kwds, data
|
||||
|
||||
|
||||
class BrokenExtension(object):
|
||||
def __init__(self, *args, **kwds):
|
||||
raise IOError("Did not create")
|
||||
|
||||
|
||||
class TestCallback(utils.TestCase):
|
||||
def test_detect_plugins(self):
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
names = sorted(em.names())
|
||||
self.assertEqual(names, ALL_NAMES)
|
||||
|
||||
def test_get_by_name(self):
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
e = em['t1']
|
||||
self.assertEqual(e.name, 't1')
|
||||
|
||||
def test_list_entry_points(self):
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
n = em.list_entry_points()
|
||||
self.assertEqual(set(['e1', 'e2', 't1', 't2']),
|
||||
set(map(operator.attrgetter("name"), n)))
|
||||
self.assertEqual(4, len(n))
|
||||
|
||||
def test_list_entry_points_names(self):
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
names = em.entry_points_names()
|
||||
self.assertEqual(set(['e1', 'e2', 't1', 't2']), set(names))
|
||||
self.assertEqual(4, len(names))
|
||||
|
||||
def test_contains_by_name(self):
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
self.assertEqual('t1' in em, True)
|
||||
|
||||
def test_get_by_name_missing(self):
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
try:
|
||||
em['t3']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
assert False, 'Failed to raise KeyError'
|
||||
|
||||
def test_load_multiple_times_entry_points(self):
|
||||
# We expect to get the same EntryPoint object because we save them
|
||||
# in the cache.
|
||||
em1 = extension.ExtensionManager('stevedore.test.extension')
|
||||
eps1 = [ext.entry_point for ext in em1]
|
||||
em2 = extension.ExtensionManager('stevedore.test.extension')
|
||||
eps2 = [ext.entry_point for ext in em2]
|
||||
self.assertIs(eps1[0], eps2[0])
|
||||
|
||||
def test_load_multiple_times_plugins(self):
|
||||
# We expect to get the same plugin object (module or class)
|
||||
# because the underlying import machinery will cache the values.
|
||||
em1 = extension.ExtensionManager('stevedore.test.extension')
|
||||
plugins1 = [ext.plugin for ext in em1]
|
||||
em2 = extension.ExtensionManager('stevedore.test.extension')
|
||||
plugins2 = [ext.plugin for ext in em2]
|
||||
self.assertIs(plugins1[0], plugins2[0])
|
||||
|
||||
def test_use_cache(self):
|
||||
# If we insert something into the cache of entry points,
|
||||
# the manager should not have to call into pkg_resources
|
||||
# to find the plugins.
|
||||
cache = extension.ExtensionManager.ENTRY_POINT_CACHE
|
||||
cache['stevedore.test.faux'] = []
|
||||
with mock.patch('pkg_resources.iter_entry_points',
|
||||
side_effect=
|
||||
AssertionError('called iter_entry_points')):
|
||||
em = extension.ExtensionManager('stevedore.test.faux')
|
||||
names = em.names()
|
||||
self.assertEqual(names, [])
|
||||
|
||||
def test_iterable(self):
|
||||
em = extension.ExtensionManager('stevedore.test.extension')
|
||||
names = sorted(e.name for e in em)
|
||||
self.assertEqual(names, ALL_NAMES)
|
||||
|
||||
def test_invoke_on_load(self):
|
||||
em = extension.ExtensionManager('stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 2)
|
||||
for e in em.extensions:
|
||||
self.assertEqual(e.obj.args, ('a',))
|
||||
self.assertEqual(e.obj.kwds, {'b': 'B'})
|
||||
|
||||
def test_map_return_values(self):
|
||||
def mapped(ext, *args, **kwds):
|
||||
return ext.name
|
||||
|
||||
em = extension.ExtensionManager('stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
)
|
||||
results = em.map(mapped)
|
||||
self.assertEqual(sorted(results), WORKING_NAMES)
|
||||
|
||||
def test_map_arguments(self):
|
||||
objs = []
|
||||
|
||||
def mapped(ext, *args, **kwds):
|
||||
objs.append((ext, args, kwds))
|
||||
|
||||
em = extension.ExtensionManager('stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
)
|
||||
em.map(mapped, 1, 2, a='A', b='B')
|
||||
self.assertEqual(len(objs), 2)
|
||||
names = sorted([o[0].name for o in objs])
|
||||
self.assertEqual(names, WORKING_NAMES)
|
||||
for o in objs:
|
||||
self.assertEqual(o[1], (1, 2))
|
||||
self.assertEqual(o[2], {'a': 'A', 'b': 'B'})
|
||||
|
||||
def test_map_eats_errors(self):
|
||||
def mapped(ext, *args, **kwds):
|
||||
raise RuntimeError('hard coded error')
|
||||
|
||||
em = extension.ExtensionManager('stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
)
|
||||
results = em.map(mapped, 1, 2, a='A', b='B')
|
||||
self.assertEqual(results, [])
|
||||
|
||||
def test_map_propagate_exceptions(self):
|
||||
def mapped(ext, *args, **kwds):
|
||||
raise RuntimeError('hard coded error')
|
||||
|
||||
em = extension.ExtensionManager('stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
propagate_map_exceptions=True
|
||||
)
|
||||
|
||||
try:
|
||||
em.map(mapped, 1, 2, a='A', b='B')
|
||||
assert False
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
def test_map_errors_when_no_plugins(self):
|
||||
expected_str = 'No stevedore.test.extension.none extensions found'
|
||||
|
||||
def mapped(ext, *args, **kwds):
|
||||
pass
|
||||
|
||||
em = extension.ExtensionManager('stevedore.test.extension.none',
|
||||
invoke_on_load=True,
|
||||
)
|
||||
try:
|
||||
em.map(mapped, 1, 2, a='A', b='B')
|
||||
except exception.NoMatches as err:
|
||||
self.assertEqual(expected_str, str(err))
|
||||
|
||||
def test_map_method(self):
|
||||
em = extension.ExtensionManager('stevedore.test.extension',
|
||||
invoke_on_load=True,
|
||||
)
|
||||
|
||||
result = em.map_method('get_args_and_data', 42)
|
||||
self.assertEqual(set(r[2] for r in result), set([42]))
|
||||
|
||||
|
||||
class TestLoadRequirementsNewSetuptools(utils.TestCase):
|
||||
# setuptools 11.3 and later
|
||||
|
||||
def setUp(self):
|
||||
super(TestLoadRequirementsNewSetuptools, self).setUp()
|
||||
self.mock_ep = mock.Mock(spec=['require', 'resolve', 'load', 'name'])
|
||||
self.em = extension.ExtensionManager.make_test_instance([])
|
||||
|
||||
def test_verify_requirements(self):
|
||||
self.em._load_one_plugin(self.mock_ep, False, (), {},
|
||||
verify_requirements=True)
|
||||
self.mock_ep.require.assert_called_once_with()
|
||||
self.mock_ep.resolve.assert_called_once_with()
|
||||
|
||||
def test_no_verify_requirements(self):
|
||||
self.em._load_one_plugin(self.mock_ep, False, (), {},
|
||||
verify_requirements=False)
|
||||
self.assertEqual(0, self.mock_ep.require.call_count)
|
||||
self.mock_ep.resolve.assert_called_once_with()
|
||||
|
||||
|
||||
class TestLoadRequirementsOldSetuptools(utils.TestCase):
|
||||
# Before setuptools 11.3
|
||||
|
||||
def setUp(self):
|
||||
super(TestLoadRequirementsOldSetuptools, self).setUp()
|
||||
self.mock_ep = mock.Mock(spec=['load', 'name'])
|
||||
self.em = extension.ExtensionManager.make_test_instance([])
|
||||
|
||||
def test_verify_requirements(self):
|
||||
self.em._load_one_plugin(self.mock_ep, False, (), {},
|
||||
verify_requirements=True)
|
||||
self.mock_ep.load.assert_called_once_with(require=True)
|
||||
|
||||
def test_no_verify_requirements(self):
|
||||
self.em._load_one_plugin(self.mock_ep, False, (), {},
|
||||
verify_requirements=False)
|
||||
self.mock_ep.load.assert_called_once_with(require=False)
|
@@ -1,55 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from stevedore import hook
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
class TestHook(utils.TestCase):
|
||||
def test_hook(self):
|
||||
em = hook.HookManager(
|
||||
'stevedore.test.extension',
|
||||
't1',
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
self.assertEqual(len(em.extensions), 1)
|
||||
self.assertEqual(em.names(), ['t1'])
|
||||
|
||||
def test_get_by_name(self):
|
||||
em = hook.HookManager(
|
||||
'stevedore.test.extension',
|
||||
't1',
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
e_list = em['t1']
|
||||
self.assertEqual(len(e_list), 1)
|
||||
e = e_list[0]
|
||||
self.assertEqual(e.name, 't1')
|
||||
|
||||
def test_get_by_name_missing(self):
|
||||
em = hook.HookManager(
|
||||
'stevedore.test.extension',
|
||||
't1',
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
try:
|
||||
em['t2']
|
||||
except KeyError:
|
||||
pass
|
||||
else:
|
||||
assert False, 'Failed to raise KeyError'
|
@@ -1,93 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from stevedore import named
|
||||
from stevedore.tests import utils
|
||||
|
||||
import mock
|
||||
|
||||
|
||||
class TestNamed(utils.TestCase):
|
||||
def test_named(self):
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
names=['t1'],
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(actual, ['t1'])
|
||||
|
||||
def test_enabled_before_load(self):
|
||||
# Set up the constructor for the FauxExtension to cause an
|
||||
# AssertionError so the test fails if the class is instantiated,
|
||||
# which should only happen if it is loaded before the name of the
|
||||
# extension is compared against the names that should be loaded by
|
||||
# the manager.
|
||||
init_name = 'stevedore.tests.test_extension.FauxExtension.__init__'
|
||||
with mock.patch(init_name) as m:
|
||||
m.side_effect = AssertionError
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
# Look for an extension that does not exist so the
|
||||
# __init__ we mocked should never be invoked.
|
||||
names=['no-such-extension'],
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(actual, [])
|
||||
|
||||
def test_extensions_listed_in_name_order(self):
|
||||
# Since we don't know the "natural" order of the extensions, run
|
||||
# the test both ways: if the sorting is broken, one of them will
|
||||
# fail
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
names=['t1', 't2'],
|
||||
name_order=True
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(actual, ['t1', 't2'])
|
||||
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
names=['t2', 't1'],
|
||||
name_order=True
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(actual, ['t2', 't1'])
|
||||
|
||||
def test_load_fail_ignored_when_sorted(self):
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
names=['e1', 't2', 'e2', 't1'],
|
||||
name_order=True,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(['t2', 't1'], actual)
|
||||
|
||||
em = named.NamedExtensionManager(
|
||||
'stevedore.test.extension',
|
||||
names=['e1', 't1'],
|
||||
name_order=False,
|
||||
invoke_on_load=True,
|
||||
invoke_args=('a',),
|
||||
invoke_kwds={'b': 'B'},
|
||||
)
|
||||
actual = em.names()
|
||||
self.assertEqual(['t1'], actual)
|
@@ -1,120 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
"""Tests for the sphinx extension
|
||||
"""
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from stevedore import extension
|
||||
from stevedore import sphinxext
|
||||
from stevedore.tests import utils
|
||||
|
||||
import mock
|
||||
import pkg_resources
|
||||
|
||||
|
||||
def _make_ext(name, docstring):
|
||||
def inner():
|
||||
pass
|
||||
|
||||
inner.__doc__ = docstring
|
||||
m1 = mock.Mock(spec=pkg_resources.EntryPoint)
|
||||
m1.module_name = '%s_module' % name
|
||||
s = mock.Mock(return_value='ENTRY_POINT(%s)' % name)
|
||||
m1.__str__ = s
|
||||
return extension.Extension(name, m1, inner, None)
|
||||
|
||||
|
||||
class TestSphinxExt(utils.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
super(TestSphinxExt, self).setUp()
|
||||
self.exts = [
|
||||
_make_ext('test1', 'One-line docstring'),
|
||||
_make_ext('test2', 'Multi-line docstring\n\nAnother para'),
|
||||
]
|
||||
self.em = extension.ExtensionManager.make_test_instance(self.exts)
|
||||
|
||||
def test_simple_list(self):
|
||||
results = list(sphinxext._simple_list(self.em))
|
||||
self.assertEqual(
|
||||
[
|
||||
('* test1 -- One-line docstring', 'test1_module'),
|
||||
('* test2 -- Multi-line docstring', 'test2_module'),
|
||||
],
|
||||
results,
|
||||
)
|
||||
|
||||
def test_simple_list_no_docstring(self):
|
||||
ext = [_make_ext('nodoc', None)]
|
||||
em = extension.ExtensionManager.make_test_instance(ext)
|
||||
results = list(sphinxext._simple_list(em))
|
||||
self.assertEqual(
|
||||
[
|
||||
('* nodoc -- ', 'nodoc_module'),
|
||||
],
|
||||
results,
|
||||
)
|
||||
|
||||
def test_detailed_list(self):
|
||||
results = list(sphinxext._detailed_list(self.em))
|
||||
self.assertEqual(
|
||||
[
|
||||
('test1', 'test1_module'),
|
||||
('-----', 'test1_module'),
|
||||
('\n', 'test1_module'),
|
||||
('One-line docstring', 'test1_module'),
|
||||
('\n', 'test1_module'),
|
||||
('test2', 'test2_module'),
|
||||
('-----', 'test2_module'),
|
||||
('\n', 'test2_module'),
|
||||
('Multi-line docstring\n\nAnother para', 'test2_module'),
|
||||
('\n', 'test2_module'),
|
||||
],
|
||||
results,
|
||||
)
|
||||
|
||||
def test_detailed_list_format(self):
|
||||
results = list(sphinxext._detailed_list(self.em, over='+', under='+'))
|
||||
self.assertEqual(
|
||||
[
|
||||
('+++++', 'test1_module'),
|
||||
('test1', 'test1_module'),
|
||||
('+++++', 'test1_module'),
|
||||
('\n', 'test1_module'),
|
||||
('One-line docstring', 'test1_module'),
|
||||
('\n', 'test1_module'),
|
||||
('+++++', 'test2_module'),
|
||||
('test2', 'test2_module'),
|
||||
('+++++', 'test2_module'),
|
||||
('\n', 'test2_module'),
|
||||
('Multi-line docstring\n\nAnother para', 'test2_module'),
|
||||
('\n', 'test2_module'),
|
||||
],
|
||||
results,
|
||||
)
|
||||
|
||||
def test_detailed_list_no_docstring(self):
|
||||
ext = [_make_ext('nodoc', None)]
|
||||
em = extension.ExtensionManager.make_test_instance(ext)
|
||||
results = list(sphinxext._detailed_list(em))
|
||||
self.assertEqual(
|
||||
[
|
||||
('nodoc', 'nodoc_module'),
|
||||
('-----', 'nodoc_module'),
|
||||
('\n', 'nodoc_module'),
|
||||
('.. warning:: No documentation found in ENTRY_POINT(nodoc)',
|
||||
'nodoc_module'),
|
||||
('\n', 'nodoc_module'),
|
||||
],
|
||||
results,
|
||||
)
|
@@ -1,216 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
from mock import Mock, sentinel
|
||||
from stevedore import (ExtensionManager, NamedExtensionManager, HookManager,
|
||||
DriverManager, EnabledExtensionManager)
|
||||
from stevedore.dispatch import (DispatchExtensionManager,
|
||||
NameDispatchExtensionManager)
|
||||
from stevedore.extension import Extension
|
||||
from stevedore.tests import utils
|
||||
|
||||
|
||||
test_extension = Extension('test_extension', None, None, None)
|
||||
test_extension2 = Extension('another_one', None, None, None)
|
||||
|
||||
mock_entry_point = Mock(module_name='test.extension', attrs=['obj'])
|
||||
a_driver = Extension('test_driver', mock_entry_point, sentinel.driver_plugin,
|
||||
sentinel.driver_obj)
|
||||
|
||||
|
||||
# base ExtensionManager
|
||||
class TestTestManager(utils.TestCase):
|
||||
def test_instance_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = ExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_instance_should_have_default_namespace(self):
|
||||
em = ExtensionManager.make_test_instance([])
|
||||
self.assertEqual(em.namespace, 'TESTING')
|
||||
|
||||
def test_instance_should_use_supplied_namespace(self):
|
||||
namespace = 'testing.1.2.3'
|
||||
em = ExtensionManager.make_test_instance([], namespace=namespace)
|
||||
self.assertEqual(namespace, em.namespace)
|
||||
|
||||
def test_extension_name_should_be_listed(self):
|
||||
em = ExtensionManager.make_test_instance([test_extension])
|
||||
self.assertIn(test_extension.name, em.names())
|
||||
|
||||
def test_iterator_should_yield_extension(self):
|
||||
em = ExtensionManager.make_test_instance([test_extension])
|
||||
self.assertEqual(test_extension, next(iter(em)))
|
||||
|
||||
def test_manager_should_allow_name_access(self):
|
||||
em = ExtensionManager.make_test_instance([test_extension])
|
||||
self.assertEqual(test_extension, em[test_extension.name])
|
||||
|
||||
def test_manager_should_call(self):
|
||||
em = ExtensionManager.make_test_instance([test_extension])
|
||||
func = Mock()
|
||||
em.map(func)
|
||||
func.assert_called_once_with(test_extension)
|
||||
|
||||
def test_manager_should_call_all(self):
|
||||
em = ExtensionManager.make_test_instance([test_extension2,
|
||||
test_extension])
|
||||
func = Mock()
|
||||
em.map(func)
|
||||
func.assert_any_call(test_extension2)
|
||||
func.assert_any_call(test_extension)
|
||||
|
||||
def test_manager_return_values(self):
|
||||
def mapped(ext, *args, **kwds):
|
||||
return ext.name
|
||||
|
||||
em = ExtensionManager.make_test_instance([test_extension2,
|
||||
test_extension])
|
||||
results = em.map(mapped)
|
||||
self.assertEqual(sorted(results), ['another_one', 'test_extension'])
|
||||
|
||||
def test_manager_should_eat_exceptions(self):
|
||||
em = ExtensionManager.make_test_instance([test_extension])
|
||||
|
||||
func = Mock(side_effect=RuntimeError('hard coded error'))
|
||||
|
||||
results = em.map(func, 1, 2, a='A', b='B')
|
||||
self.assertEqual(results, [])
|
||||
|
||||
def test_manager_should_propagate_exceptions(self):
|
||||
em = ExtensionManager.make_test_instance([test_extension],
|
||||
propagate_map_exceptions=True)
|
||||
self.skipTest('Skipping temporarily')
|
||||
func = Mock(side_effect=RuntimeError('hard coded error'))
|
||||
em.map(func, 1, 2, a='A', b='B')
|
||||
|
||||
# NamedExtensionManager
|
||||
def test_named_manager_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = NamedExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_named_manager_should_have_default_namespace(self):
|
||||
em = NamedExtensionManager.make_test_instance([])
|
||||
self.assertEqual(em.namespace, 'TESTING')
|
||||
|
||||
def test_named_manager_should_use_supplied_namespace(self):
|
||||
namespace = 'testing.1.2.3'
|
||||
em = NamedExtensionManager.make_test_instance([], namespace=namespace)
|
||||
self.assertEqual(namespace, em.namespace)
|
||||
|
||||
def test_named_manager_should_populate_names(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = NamedExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(em.names(), ['test_extension', 'another_one'])
|
||||
|
||||
# HookManager
|
||||
def test_hook_manager_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = HookManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_hook_manager_should_be_first_extension_name(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = HookManager.make_test_instance(extensions)
|
||||
# This will raise KeyError if the names don't match
|
||||
assert(em[test_extension.name])
|
||||
|
||||
def test_hook_manager_should_have_default_namespace(self):
|
||||
em = HookManager.make_test_instance([test_extension])
|
||||
self.assertEqual(em.namespace, 'TESTING')
|
||||
|
||||
def test_hook_manager_should_use_supplied_namespace(self):
|
||||
namespace = 'testing.1.2.3'
|
||||
em = HookManager.make_test_instance([test_extension],
|
||||
namespace=namespace)
|
||||
self.assertEqual(namespace, em.namespace)
|
||||
|
||||
def test_hook_manager_should_return_named_extensions(self):
|
||||
hook1 = Extension('captain', None, None, None)
|
||||
hook2 = Extension('captain', None, None, None)
|
||||
em = HookManager.make_test_instance([hook1, hook2])
|
||||
self.assertEqual([hook1, hook2], em['captain'])
|
||||
|
||||
# DriverManager
|
||||
def test_driver_manager_should_use_supplied_extension(self):
|
||||
em = DriverManager.make_test_instance(a_driver)
|
||||
self.assertEqual([a_driver], em.extensions)
|
||||
|
||||
def test_driver_manager_should_have_default_namespace(self):
|
||||
em = DriverManager.make_test_instance(a_driver)
|
||||
self.assertEqual(em.namespace, 'TESTING')
|
||||
|
||||
def test_driver_manager_should_use_supplied_namespace(self):
|
||||
namespace = 'testing.1.2.3'
|
||||
em = DriverManager.make_test_instance(a_driver, namespace=namespace)
|
||||
self.assertEqual(namespace, em.namespace)
|
||||
|
||||
def test_instance_should_use_driver_name(self):
|
||||
em = DriverManager.make_test_instance(a_driver)
|
||||
self.assertEqual(['test_driver'], em.names())
|
||||
|
||||
def test_instance_call(self):
|
||||
def invoke(ext, *args, **kwds):
|
||||
return ext.name, args, kwds
|
||||
|
||||
em = DriverManager.make_test_instance(a_driver)
|
||||
result = em(invoke, 'a', b='C')
|
||||
self.assertEqual(result, ('test_driver', ('a',), {'b': 'C'}))
|
||||
|
||||
def test_instance_driver_property(self):
|
||||
em = DriverManager.make_test_instance(a_driver)
|
||||
self.assertEqual(sentinel.driver_obj, em.driver)
|
||||
|
||||
# EnabledExtensionManager
|
||||
def test_enabled_instance_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = EnabledExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
# DispatchExtensionManager
|
||||
def test_dispatch_instance_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = DispatchExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_dispatch_map_should_invoke_filter_for_extensions(self):
|
||||
em = DispatchExtensionManager.make_test_instance([test_extension,
|
||||
test_extension2])
|
||||
filter_func = Mock(return_value=False)
|
||||
args = ('A',)
|
||||
kw = {'big': 'Cheese'}
|
||||
em.map(filter_func, None, *args, **kw)
|
||||
filter_func.assert_any_call(test_extension, *args, **kw)
|
||||
filter_func.assert_any_call(test_extension2, *args, **kw)
|
||||
|
||||
# NameDispatchExtensionManager
|
||||
def test_name_dispatch_instance_should_use_supplied_extensions(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = NameDispatchExtensionManager.make_test_instance(extensions)
|
||||
|
||||
self.assertEqual(extensions, em.extensions)
|
||||
|
||||
def test_name_dispatch_instance_should_build_extension_name_map(self):
|
||||
extensions = [test_extension, test_extension2]
|
||||
em = NameDispatchExtensionManager.make_test_instance(extensions)
|
||||
self.assertEqual(test_extension, em.by_name[test_extension.name])
|
||||
self.assertEqual(test_extension2, em.by_name[test_extension2.name])
|
||||
|
||||
def test_named_dispatch_map_should_invoke_filter_for_extensions(self):
|
||||
em = NameDispatchExtensionManager.make_test_instance([test_extension,
|
||||
test_extension2])
|
||||
func = Mock()
|
||||
args = ('A',)
|
||||
kw = {'BIGGER': 'Cheese'}
|
||||
em.map(['test_extension'], func, *args, **kw)
|
||||
func.assert_called_once_with(test_extension, *args, **kw)
|
@@ -1,17 +0,0 @@
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
# not use this file except in compliance with the License. You may obtain
|
||||
# a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
import unittest
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
pass
|
@@ -1,11 +0,0 @@
|
||||
# The order of packages is significant, because pip processes them in the order
|
||||
# of appearance. Changing the order has an impact on the overall integration
|
||||
# process, which may cause wedges in the gate later.
|
||||
|
||||
Pillow>=2.4.0 # PIL License
|
||||
sphinx>=1.6.2 # BSD
|
||||
mock>=2.0 # BSD
|
||||
coverage!=4.4,>=4.0 # Apache-2.0
|
||||
testrepository>=0.0.18 # Apache-2.0/BSD
|
||||
openstackdocstheme>=1.11.0 # Apache-2.0
|
||||
reno!=2.3.1,>=1.8.0 # Apache-2.0
|
@@ -1,30 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Client constraint file contains this client version pin that is in conflict
|
||||
# with installing the client from source. We should remove the version pin in
|
||||
# the constraints file before applying it for from-source installation.
|
||||
|
||||
CONSTRAINTS_FILE="$1"
|
||||
shift 1
|
||||
|
||||
set -e
|
||||
|
||||
# NOTE(tonyb): Place this in the tox enviroment's log dir so it will get
|
||||
# published to logs.openstack.org for easy debugging.
|
||||
localfile="$VIRTUAL_ENV/log/upper-constraints.txt"
|
||||
|
||||
if [[ "$CONSTRAINTS_FILE" != http* ]]; then
|
||||
CONSTRAINTS_FILE="file://$CONSTRAINTS_FILE"
|
||||
fi
|
||||
# NOTE(tonyb): need to add curl to bindep.txt if the project supports bindep
|
||||
curl "$CONSTRAINTS_FILE" --insecure --progress-bar --output "$localfile"
|
||||
|
||||
pip install -c"$localfile" openstack-requirements
|
||||
|
||||
# This is the main purpose of the script: Allow local installation of
|
||||
# the current repo. It is listed in constraints file and thus any
|
||||
# install will be constrained and we need to unconstrain it.
|
||||
edit-constraints "$localfile" -- "$CLIENT_NAME"
|
||||
|
||||
pip install -c"$localfile" -U "$@"
|
||||
exit $?
|
33
tox.ini
@@ -1,33 +0,0 @@
|
||||
[tox]
|
||||
minversion = 2.0
|
||||
envlist = py35,py27,pep8,docs
|
||||
|
||||
[testenv]
|
||||
setenv =
|
||||
VIRTUAL_ENV={envdir}
|
||||
BRANCH_NAME=master
|
||||
CLIENT_NAME=stevedore
|
||||
install_command = {toxinidir}/tools/tox_install.sh {env:UPPER_CONSTRAINTS_FILE:https://git.openstack.org/cgit/openstack/requirements/plain/upper-constraints.txt} {opts} {packages}
|
||||
deps =
|
||||
-r{toxinidir}/test-requirements.txt
|
||||
distribute = False
|
||||
commands = python setup.py testr --testr-args='{posargs}'
|
||||
|
||||
[testenv:venv]
|
||||
commands = {posargs}
|
||||
|
||||
[testenv:pep8]
|
||||
deps = flake8
|
||||
ignore = E251
|
||||
commands = flake8 stevedore setup.py
|
||||
|
||||
[testenv:docs]
|
||||
commands = python setup.py build_sphinx
|
||||
|
||||
[flake8]
|
||||
ignore = E251
|
||||
show-source = True
|
||||
exclude=.venv,.git,.tox,dist,*lib/python*,*egg,build
|
||||
|
||||
[testenv:releasenotes]
|
||||
commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html
|