summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.hgignore8
-rw-r--r--README20
-rw-r--r--blinker/base.py92
-rw-r--r--docs/source/Makefile45
-rw-r--r--docs/source/api.rst39
-rw-r--r--docs/source/conf.py206
-rw-r--r--docs/source/index.rst42
-rw-r--r--docs/source/signals.rst233
8 files changed, 657 insertions, 28 deletions
diff --git a/.hgignore b/.hgignore
index ffcb4d6..1dfb19f 100644
--- a/.hgignore
+++ b/.hgignore
@@ -7,3 +7,11 @@ bench
build
dist
MANIFEST
+docs/text
+docs/html
+docs/doctrees
+docs/doctest
+docs/pickles
+docs/source/_static
+docs/source/_template
+
diff --git a/README b/README
index 2725785..707f634 100644
--- a/README
+++ b/README
@@ -7,22 +7,32 @@ interested parties to subscribe to events, or "signals".
Signal receivers can subscribe to specific senders or receive signals
sent by any sender.
- >>> from blinker import Signal
- >>> signal = Signal('round_started')
+ >>> from blinker import signal
+ >>> started = signal('round-started')
>>> def each(round):
... print "Round %s!" % round
...
- >>> signal.connect(each)
+ >>> started.connect(each)
+
>>> def round_two(round):
... print "This is round two."
...
- >>> signal.connect(round_two, sender=2)
+ >>> started.connect(round_two, sender=2)
>>> for round in range(1, 4):
- ... signal.send(round)
+ ... started.send(round)
...
Round 1!
Round 2!
This is round two.
Round 3!
+Changelog Summary
+-----------------
+
+0.9 (development)
+ - Sphinx docs
+
+0.8 (February 14, 2010)
+ - First independent release separate from flatland
+ - "sender" is now a positional, not keyword, argument
diff --git a/blinker/base.py b/blinker/base.py
index 56b1c54..c0a5e3f 100644
--- a/blinker/base.py
+++ b/blinker/base.py
@@ -21,35 +21,49 @@ from blinker._utilities import (
ANY = symbol('ANY')
+ANY.__doc__ = 'Token for "any sender".'
ANY_ID = 0
class Signal(object):
- """A generic notification emitter."""
+ """A notification emitter."""
- #: A convenience for importers, allows Signal.ANY
+ #: An :obj:`ANY` convenience synonym, allows ``Signal.ANY``
+ #: without an additional import.
ANY = ANY
def __init__(self, doc=None):
+ """
+ :param doc: optional. If provided, will be assigned to the signal's
+ __doc__ attribute.
+
+ """
if doc:
self.__doc__ = doc
+ #: A mapping of connected receivers.
+ #:
+ #: The values of this mapping are not meaningful outside of the
+ #: internal :class:`Signal` implementation, however the boolean value
+ #: of the mapping is useful as an extremely efficient check to see if
+ #: any receivers are connected to the signal.
self.receivers = {}
self._by_receiver = defaultdict(set)
self._by_sender = defaultdict(set)
self._weak_senders = {}
def connect(self, receiver, sender=ANY, weak=True):
- """Connect *receiver* to signal events send by *sender*.
+ """Connect *receiver* to signal events sent by *sender*.
- :param receiver: A callable. Will be invoked by :meth:`send`. Will
- be invoked with `sender=` as a named argument and any \*\*kwargs
- that were provided to a call to :meth:`send`.
+ :param receiver: A callable. Will be invoked by :meth:`send` with
+ `sender=` as a single positional argument and any \*\*kwargs that
+ were provided to a call to :meth:`send`.
- :param sender: Any object or :attr:`Signal.ANY`. Restricts
- notifications to *receiver* to only those :meth:`send` emissions
- sent by *sender*. If ``ANY``, the receiver will always be
- notified. A *receiver* may be connected to multiple *sender* on
- the same Signal. Defaults to ``ANY``.
+ :param sender: Any object or :obj:`ANY`, defaults to ``ANY``.
+ Restricts notifications delivered to *receiver* to only those
+ :meth:`send` emissions sent by *sender*. If ``ANY``, the receiver
+ will always be notified. A *receiver* may be connected to
+ multiple *sender* values on the same Signal through multiple calls
+ to :meth:`connect`.
:param weak: If true, the Signal will hold a weakref to *receiver*
and automatically disconnect when *receiver* goes out of scope or
@@ -99,17 +113,27 @@ class Signal(object):
def temporarily_connected_to(self, receiver, sender=ANY):
"""Execute a block with the signal connected *receiver*.
- This is a context manager for use in the ``with`` statement, and can
+ :param receiver: a receiver callable
+ :param sender: optional, a sender to filter on
+
+ This is a context manager for use in the ``with`` statement. It can
be useful in unit tests. *receiver* is connected to the signal for
the duration of the ``with`` block, and will be disconnected
- automatically when exiting the block::
+ automatically when exiting the block:
+
+ .. testsetup::
- ready = Signal()
- receiver = lambda sender: pass
+ from blinker import Signal
+ on_ready = Signal()
+ receiver = lambda sender: None
- with ready.temporarily_connected_to(receiver):
+ .. testcode::
+
+ with on_ready.temporarily_connected_to(receiver):
# do stuff
- ready.send(123)
+ on_ready.send(123)
+
+ .. versionadded:: 0.9
"""
self.connect(receiver, sender=sender, weak=False)
@@ -128,7 +152,7 @@ class Signal(object):
value. The ordering of receiver notification is undefined.
:param \*sender: Any object or ``None``. If omitted, synonymous
- with ``None``. Only accepts one positional argument.
+ with ``None``. Only accepts one positional argument.
:param \*\*kwargs: Data to be sent to receivers.
@@ -152,8 +176,8 @@ class Signal(object):
def has_receivers_for(self, sender):
"""True if there is probably a receiver for *sender*.
- Performs an optimistic check for receivers only. Does not guarantee
- that all weakly referenced receivers are still alive. See
+ Performs an optimistic check only. Does not guarantee that all
+ weakly referenced receivers are still alive. See
:meth:`receivers_for` for a stronger search.
"""
@@ -188,7 +212,14 @@ class Signal(object):
yield receiver
def disconnect(self, receiver, sender=ANY):
- """Disconnect *receiver* from this signal's events."""
+ """Disconnect *receiver* from this signal's events.
+
+ :param receiver: a previously :meth:`connected<connect>` callable
+
+ :param sender: a specific sender to disconnect from, or :obj:`ANY`
+ to disconnect from all senders. Defaults to ``ANY``.
+
+ """
if sender is ANY:
sender_id = ANY_ID
else:
@@ -225,7 +256,15 @@ class Signal(object):
self._by_receiver.clear()
-receiver_connected = Signal()
+receiver_connected = Signal("""\
+Sent by a :class:`Signal` after a receiver connects.
+
+:argument: the Signal that was connected to
+:keyword receiver_arg: the connected receiver
+:keyword sender_arg: the sender to connect to
+:keyword weak_arg: true if the connection to receiver_arg is a weak reference
+
+""")
class NamedSignal(Signal):
@@ -233,6 +272,8 @@ class NamedSignal(Signal):
def __init__(self, name, doc=None):
Signal.__init__(self, doc)
+
+ #: The name of this signal.
self.name = name
def __repr__(self):
@@ -241,9 +282,14 @@ class NamedSignal(Signal):
class Namespace(WeakValueDictionary):
+ """A mapping of signal names to signals."""
def signal(self, name, doc=None):
- """Return the :class:`NamedSignal` *name*, creating it if required."""
+ """Return the :class:`NamedSignal` *name*, creating it if required.
+
+ Repeated calls to this function will return the same signal object.
+
+ """
try:
return self[name]
except KeyError:
diff --git a/docs/source/Makefile b/docs/source/Makefile
new file mode 100644
index 0000000..d524eb7
--- /dev/null
+++ b/docs/source/Makefile
@@ -0,0 +1,45 @@
+# Makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS =
+SPHINXBUILD = sphinx-build
+
+# Internal variables.
+ALLSPHINXOPTS = -d ../doctrees $(SPHINXOPTS) .
+
+help:
+ @echo "Please use \`make <target>' where <target> is one of"
+ @echo " html to make standalone HTML files"
+ @echo " text to make standalone text files"
+ @echo " sdist to build documentation for release"
+ @echo " doctest to run doctests"
+ @echo " pickles to build pickles"
+ @echo " clean to remove generated artifacts"
+
+sdist: clean text html
+
+clean:
+ for i in doctrees html text doctest pickles; do \
+ rm -rf ../$$i; \
+ done
+
+html:
+ mkdir -p ../html ../doctrees _static _template
+ $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) ../html
+ @echo
+ @echo "Build finished. The HTML pages are in ../html."
+
+text:
+ mkdir -p ../text ../doctrees
+ $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) ../text
+ @echo
+ @echo "Build finished. The text pages are in ../text."
+
+doctest:
+ mkdir -p ../doctrees ../doctest
+ $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) ../doctest
+
+pickles:
+ mkdir -p ../pickles ../doctest
+ $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) ../pickles
diff --git a/docs/source/api.rst b/docs/source/api.rst
new file mode 100644
index 0000000..e80684d
--- /dev/null
+++ b/docs/source/api.rst
@@ -0,0 +1,39 @@
+.. _api:
+
+API Documentation
+=================
+
+All public API members can (and should) be imported from ``blinker``::
+
+ from blinker import ANY, signal
+
+.. currentmodule:: blinker.base
+
+Basic Signals
+-------------
+
+.. autoattribute:: blinker.base.ANY
+
+.. autoattribute:: blinker.base.receiver_connected
+
+.. autoclass:: Signal
+ :members:
+ :undoc-members:
+
+Named Signals
+-------------
+
+.. function:: signal(name, doc=None)
+
+ Return the :class:`NamedSignal` *name*, creating it if required.
+
+ Repeated calls to this function will return the same signal object.
+ Signals are created in a global :class:`Namespace`.
+
+.. autoclass:: NamedSignal
+ :show-inheritance:
+ :members:
+
+.. autoclass:: Namespace
+ :show-inheritance:
+ :members: signal
diff --git a/docs/source/conf.py b/docs/source/conf.py
new file mode 100644
index 0000000..c4adb72
--- /dev/null
+++ b/docs/source/conf.py
@@ -0,0 +1,206 @@
+# -*- coding: utf-8 -*-
+#
+# Blinker documentation build configuration file, created by
+# sphinx-quickstart on Mon Feb 15 10:54:13 2010.
+#
+# This file is execfile()d with the current directory set to its containing
+# dir.
+
+import os
+import sys
+
+# 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.append(os.path.abspath('../../'))
+
+# -- General configuration -----------------------------------------------------
+
+# Add any Sphinx extension module names here, as strings. They can be extensions
+# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
+extensions = ['sphinx.ext.autodoc',
+ 'sphinx.ext.doctest',
+ 'sphinx.ext.coverage']
+
+# 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'Blinker'
+copyright = u'2010, Jason Kirtland'
+
+# The version info for the project you're documenting, acts as replacement for
+# |version| and |release|, also used in various other places throughout the
+# built documents.
+#
+# The short X.Y version.
+version = '0.9'
+# The full version, including alpha/beta/rc tags.
+release = '0.9dev'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#language = None
+
+# There are two options for replacing |today|: either, you set today to some
+# non-false value, then it is used:
+#today = ''
+# Else, today_fmt is used as the format for a strftime call.
+#today_fmt = '%B %d, %Y'
+
+# List of documents that shouldn't be included in the build.
+#unused_docs = []
+
+# List of directories, relative to source directory, that shouldn't be searched
+# for source files.
+exclude_trees = []
+
+autoclass_content = "both"
+autodoc_member_order = "groupwise"
+import sphinx.ext.autodoc
+sphinx.ext.autodoc.AttributeDocumenter.member_order = 25
+sphinx.ext.autodoc.InstanceAttributeDocumenter.member_order = 26
+
+
+# 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. Major themes that come with
+# Sphinx are currently 'default' and 'sphinxdoc'.
+html_theme = 'default'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further. For a list of options available for each theme, see the
+# documentation.
+#html_theme_options = {}
+
+# Add any paths that contain custom 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_use_modindex = True
+
+# If false, no index is generated.
+#html_use_index = True
+
+# If true, the index is split into individual pages for each letter.
+#html_split_index = False
+
+# If true, 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 = ''
+
+# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
+#html_file_suffix = ''
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'Blinkerdoc'
+
+
+# -- Options for LaTeX output --------------------------------------------------
+
+# The paper size ('letter' or 'a4').
+#latex_paper_size = 'letter'
+
+# The font size ('10pt', '11pt' or '12pt').
+#latex_font_size = '10pt'
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title, author, documentclass [howto/manual]).
+latex_documents = [
+ ('index', 'Blinker.tex', u'Blinker Documentation',
+ u'Jason Kirtland', 'manual'),
+]
+
+# The name of an image file (relative to this directory) to place at the top of
+# the title page.
+#latex_logo = None
+
+# For "manual" documents, if this is true, then toplevel headings are parts,
+# not chapters.
+#latex_use_parts = False
+
+# Additional stuff for the LaTeX preamble.
+#latex_preamble = ''
+
+# Documents to append as an appendix to all manuals.
+#latex_appendices = []
+
+# If false, no module index is generated.
+#latex_use_modindex = True
diff --git a/docs/source/index.rst b/docs/source/index.rst
new file mode 100644
index 0000000..8a594bc
--- /dev/null
+++ b/docs/source/index.rst
@@ -0,0 +1,42 @@
+Blinker Documentation
+=====================
+
+Blinker provides fast & simple object-to-object and broadcast
+signaling for Python objects.
+
+The core of Blinker is quite small but provides powerful features:
+
+ - a global registry of named signals
+ - anonymous signals
+ - custom name registries
+ - permanently or temporarily connected receivers
+ - automatically disconnected receivers via weak referencing
+ - sending arbitrary data payloads
+ - collecting return values from signal receivers
+
+
+Requirements
+------------
+
+Python 2.4 or later. No other modules are required.
+
+As of Blinker 0.8, Python 3.1 passes all tests except for weakly
+binding to instance methods.
+
+
+License
+-------
+
+Blinker is provided under the MIT License.
+
+
+Contents
+--------
+
+.. toctree::
+ :maxdepth: 2
+
+ signals
+ api
+
+
diff --git a/docs/source/signals.rst b/docs/source/signals.rst
new file mode 100644
index 0000000..8bb967a
--- /dev/null
+++ b/docs/source/signals.rst
@@ -0,0 +1,233 @@
+=======
+Signals
+=======
+
+.. currentmodule:: blinker.base
+
+Decoupling With Named Signals
+-----------------------------
+
+Named signals are created with :func:`signal`:
+
+.. doctest::
+
+ >>> from blinker import signal
+ >>> initialized = signal('initialized')
+ >>> initialized is signal('initialized')
+ True
+
+Every call to ``signal('name')`` returns the same signal object,
+allowing unconnected parts of code (different modules, plugins,
+anything) to all use the same signal without requiring any code
+sharing or special imports.
+
+
+Subscribing to Signals
+----------------------
+
+:meth:`Signal.connect` registers a function to be invoked each time
+the signal is emitted. Connected functions are always passed the
+object that caused the signal to be emitted.
+
+.. doctest::
+
+ >>> def subscriber(sender):
+ ... print("Got a signal sent by %r" % sender)
+ ...
+ >>> ready = signal('ready')
+ >>> ready.connect(subscriber)
+ <function subscriber at 0x...>
+
+
+Emitting Signals
+----------------
+
+Code producing events of interest can :meth:`Signal.send`
+notifications to all connected receivers.
+
+Below, a simple ``Processor`` class emits a ``ready`` signal when it's
+about to process something, and ``complete`` when it is done. It
+passes ``self`` to the :meth:`~Signal.send` method, signifying that
+that particular instance was responsible for emitting the signal.
+
+.. doctest::
+
+ >>> class Processor:
+ ... def __init__(self, name):
+ ... self.name = name
+ ...
+ ... def go(self):
+ ... ready = signal('ready')
+ ... ready.send(self)
+ ... print("Processing.")
+ ... complete = signal('complete')
+ ... complete.send(self)
+ ...
+ ... def __repr__(self):
+ ... return '<Processor %s>' % self.name
+ ...
+ >>> processor_a = Processor('a')
+ >>> processor_a.go()
+ Got a signal sent by <Processor a>
+ Processing.
+
+Notice the ``complete`` signal in ``go()``? No receivers have
+connected to ``complete`` yet, and that's a-ok. Calling
+:meth:`~Signal.send` on a signal with no receivers will result in no
+notifications being sent, and these no-op sends are optimized to be as
+inexpensive as possible.
+
+
+Subscribing to Specific Senders
+-------------------------------
+
+The default connection to a signal invokes the receiver function when
+any sender emits it. The :meth:`Signal.connect` function accepts an
+optional argument to restrict the subscription to one specific sending
+object:
+
+.. doctest::
+
+ >>> def b_subscriber(sender):
+ ... print("Caught signal from processor_b.")
+ ... assert sender.name == 'b'
+ ...
+ >>> processor_b = Processor('b')
+ >>> ready.connect(b_subscriber, sender=processor_b)
+ <function b_subscriber at 0x...>
+
+This function has been subscribed to ``ready`` but only when sent by
+``processor_b``:
+
+.. doctest::
+
+ >>> processor_a.go()
+ Got a signal sent by <Processor a>
+ Processing.
+ >>> processor_b.go()
+ Got a signal sent by <Processor b>
+ Caught signal from processor_b.
+ Processing.
+
+
+Sending and Receiving Data Through Signals
+------------------------------------------
+
+Additional keyword arguments can be passed to :meth:`~Signal.send`.
+These will in turn be passed to the connected functions:
+
+.. doctest::
+
+ >>> send_data = signal('send-data')
+ >>> @send_data.connect
+ ... def receive_data(sender, **kw):
+ ... print("Caught signal from %r, data %r" % (sender, kw))
+ ... return 'received!'
+ ...
+ >>> result = send_data.send('anonymous', abc=123)
+ Caught signal from 'anonymous', data {'abc': 123}
+
+The return value of :meth:`~Signal.send` collects the return values of
+each connected function as a list of (``receiver function``, ``return
+value``) pairs:
+
+.. doctest::
+
+ >>> result
+ [(<function receive_data at 0x...>, 'received!')]
+
+
+Anonymous Signals
+-----------------
+
+Signals need not be named. The :class:`Signal` constructor creates a
+unique signal each time it is invoked. For example, an alternative
+implementation of the Processor from above might provide the
+processing signals as class attributes:
+
+.. doctest::
+
+ >>> from blinker import Signal
+ >>> class AltProcessor:
+ ... on_ready = Signal()
+ ... on_complete = Signal()
+ ...
+ ... def __init__(self, name):
+ ... self.name = name
+ ...
+ ... def go(self):
+ ... self.on_ready.send(self)
+ ... print("Alternate processing.")
+ ... self.on_complete.send(self)
+ ...
+ ... def __repr__(self):
+ ... return '<AltProcessor %s>' % self.name
+ ...
+
+``connect`` as a Decorator
+--------------------------
+
+You may have noticed the return value of :meth:`~Signal.connect` in
+the console output in the sections above. This allows ``connect`` to
+be used as a decorator on functions:
+
+.. doctest::
+
+ >>> apc = AltProcessor('c')
+ >>> @apc.on_complete.connect
+ ... def completed(sender):
+ ... print "AltProcessor %s completed!" % sender.name
+ ...
+ >>> apc.go()
+ Alternate processing.
+ AltProcessor c completed!
+
+While convenient, this form unfortunately does not allow the
+``sender`` or ``weak`` arguments to be customized for the connected
+function.
+
+
+Optimizing Signal Sending
+-------------------------
+
+Signals are optimized to send very quickly, whether receivers are
+connected or not. If the data to be sent down a signal is very
+expensive, it can be more efficient to check to see if any receivers
+are connected first by testing the :attr:`~Signal.receivers` property:
+
+.. doctest::
+
+ >>> bool(signal('ready').receivers)
+ True
+ >>> bool(signal('complete').receivers)
+ False
+ >>> bool(AltProcessor.on_complete.receivers)
+ True
+
+Checking for a receiver listening for a particular sender is also
+possible:
+
+.. doctest::
+
+ >>> signal('ready').has_receivers_for(processor_a)
+ True
+
+Documenting Signals
+-------------------
+
+Both named and anonymous signals can be passed a ``doc`` argument at
+construction to set the pydoc help text for the signal. This
+documentation will be picked up by most documentation generators (such
+as sphinx) and is nice for documenting any additional data parameters
+that will be sent down with the signal.
+
+See the documentation of the :obj:`receiver_connected` built-in signal
+for an example.
+
+More
+----
+
+Disconnecting receivers from signals, introspection of connected
+receivers, private namespaces for named signals and more are discussed
+in the :ref:`api`.
+