diff options
-rw-r--r-- | .hgignore | 8 | ||||
-rw-r--r-- | README | 20 | ||||
-rw-r--r-- | blinker/base.py | 92 | ||||
-rw-r--r-- | docs/source/Makefile | 45 | ||||
-rw-r--r-- | docs/source/api.rst | 39 | ||||
-rw-r--r-- | docs/source/conf.py | 206 | ||||
-rw-r--r-- | docs/source/index.rst | 42 | ||||
-rw-r--r-- | docs/source/signals.rst | 233 |
8 files changed, 657 insertions, 28 deletions
@@ -7,3 +7,11 @@ bench build dist MANIFEST +docs/text +docs/html +docs/doctrees +docs/doctest +docs/pickles +docs/source/_static +docs/source/_template + @@ -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`. + |