diff options
70 files changed, 2560 insertions, 2228 deletions
@@ -151,7 +151,7 @@ SleekXMPP projects:: format='%(levelname)-8s %(message)s') xmpp = EchoBot('somejid@example.com', 'use_getpass') - xmpp.connect(): + xmpp.connect() xmpp.process(block=True) diff --git a/conn_tests/test_pubsubjobs.py b/conn_tests/test_pubsubjobs.py index edf22ccc..ec2a2716 100644 --- a/conn_tests/test_pubsubjobs.py +++ b/conn_tests/test_pubsubjobs.py @@ -122,14 +122,14 @@ if __name__ == '__main__': logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') #load xml config - logging.info("Loading config file: %s" % opts.configfile) + logging.info("Loading config file: %s" , opts.configfile) config = configparser.RawConfigParser() config.read(opts.configfile) #init - logging.info("Account 1 is %s" % config.get('account1', 'jid')) + logging.info("Account 1 is %s" , config.get('account1', 'jid')) xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass')) - logging.info("Account 2 is %s" % config.get('account2', 'jid')) + logging.info("Account 2 is %s" , config.get('account2', 'jid')) xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass')) xmpp1.registerPlugin('xep_0004') diff --git a/conn_tests/test_pubsubserver.py b/conn_tests/test_pubsubserver.py index 15635b4b..aae77dd3 100644 --- a/conn_tests/test_pubsubserver.py +++ b/conn_tests/test_pubsubserver.py @@ -186,14 +186,14 @@ if __name__ == '__main__': logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') #load xml config - logging.info("Loading config file: %s" % opts.configfile) + logging.info("Loading config file: %s" , opts.configfile) config = configparser.RawConfigParser() config.read(opts.configfile) #init - logging.info("Account 1 is %s" % config.get('account1', 'jid')) + logging.info("Account 1 is %s" , config.get('account1', 'jid')) xmpp1 = TestClient(config.get('account1','jid'), config.get('account1','pass')) - logging.info("Account 2 is %s" % config.get('account2', 'jid')) + logging.info("Account 2 is %s" , config.get('account2', 'jid')) xmpp2 = TestClient(config.get('account2','jid'), config.get('account2','pass')) xmpp1.registerPlugin('xep_0004') diff --git a/conn_tests/testpubsub.py b/conn_tests/testpubsub.py index 3aa7200e..0f46524e 100755 --- a/conn_tests/testpubsub.py +++ b/conn_tests/testpubsub.py @@ -329,11 +329,11 @@ if __name__ == '__main__': logging.basicConfig(level=opts.loglevel, format='%(levelname)-8s %(message)s') #load xml config - logging.info("Loading config file: %s" % opts.configfile) + logging.info("Loading config file: %s" , opts.configfile) config = ET.parse(os.path.expanduser(opts.configfile)).find('auth') #init - logging.info("Logging in as %s" % config.attrib['jid']) + logging.info("Logging in as %s" , config.attrib['jid']) plugin_config = {} diff --git a/docs/_static/pygments.css b/docs/_static/pygments.css deleted file mode 100644 index f04bc738..00000000 --- a/docs/_static/pygments.css +++ /dev/null @@ -1,70 +0,0 @@ -.highlight .hll { background-color: #ffffcc } -.highlight { background: #000000; color: #f6f3e8; } -.highlight .c { color: #7C7C7C; } /* Comment */ -.highlight .err { color: #f6f3e8; } /* Error */ -.highlight .g { color: #f6f3e8; } /* Generic */ -.highlight .k { color: #00ADEE; } /* Keyword */ -.highlight .l { color: #f6f3e8; } /* Literal */ -.highlight .n { color: #f6f3e8; } /* Name */ -.highlight .o { color: #f6f3e8; } /* Operator */ -.highlight .x { color: #f6f3e8; } /* Other */ -.highlight .p { color: #f6f3e8; } /* Punctuation */ -.highlight .cm { color: #7C7C7C; } /* Comment.Multiline */ -.highlight .cp { color: #96CBFE; } /* Comment.Preproc */ -.highlight .c1 { color: #7C7C7C; } /* Comment.Single */ -.highlight .cs { color: #7C7C7C; } /* Comment.Special */ -.highlight .gd { color: #f6f3e8; } /* Generic.Deleted */ -.highlight .ge { color: #f6f3e8; } /* Generic.Emph */ -.highlight .gr { color: #ffffff; background-color: #ff0000 } /* Generic.Error */ -.highlight .gh { color: #f6f3e8; font-weight: bold; } /* Generic.Heading */ -.highlight .gi { color: #f6f3e8; } /* Generic.Inserted */ -.highlight .go { color: #070707; } /* Generic.Output */ -.highlight .gp { color: #f6f3e8; } /* Generic.Prompt */ -.highlight .gs { color: #f6f3e8; } /* Generic.Strong */ -.highlight .gu { color: #f6f3e8; font-weight: bold; } /* Generic.Subheading */ -.highlight .gt { color: #ffffff; font-weight: bold; background-color: #FF6C60 } /* Generic.Traceback */ -.highlight .kc { color: #6699CC; } /* Keyword.Constant */ -.highlight .kd { color: #6699CC; } /* Keyword.Declaration */ -.highlight .kn { color: #6699CC; } /* Keyword.Namespace */ -.highlight .kp { color: #6699CC; } /* Keyword.Pseudo */ -.highlight .kr { color: #6699CC; } /* Keyword.Reserved */ -.highlight .kt { color: #FFFFB6; } /* Keyword.Type */ -.highlight .ld { color: #f6f3e8; } /* Literal.Date */ -.highlight .m { color: #FF73FD; } /* Literal.Number */ -.highlight .s { color: #F46DBA;/*#A8FF60;*/ } /* Literal.String */ -.highlight .na { color: #f6f3e8; } /* Name.Attribute */ -.highlight .nb { color: #f6f3e8; } /* Name.Builtin */ -.highlight .nc { color: #f6f3e8; } /* Name.Class */ -.highlight .no { color: #99CC99; } /* Name.Constant */ -.highlight .nd { color: #f6f3e8; } /* Name.Decorator */ -.highlight .ni { color: #E18964; } /* Name.Entity */ -.highlight .ne { color: #f6f3e8; } /* Name.Exception */ -.highlight .nf { color: #F64DBA; } /* Name.Function */ -.highlight .nl { color: #f6f3e8; } /* Name.Label */ -.highlight .nn { color: #f6f3e8; } /* Name.Namespace */ -.highlight .nx { color: #f6f3e8; } /* Name.Other */ -.highlight .py { color: #f6f3e8; } /* Name.Property */ -.highlight .nt { color: #00ADEE; } /* Name.Tag */ -.highlight .nv { color: #C6C5FE; } /* Name.Variable */ -.highlight .ow { color: #ffffff; } /* Operator.Word */ -.highlight .w { color: #f6f3e8; } /* Text.Whitespace */ -.highlight .mf { color: #FF73FD; } /* Literal.Number.Float */ -.highlight .mh { color: #FF73FD; } /* Literal.Number.Hex */ -.highlight .mi { color: #FF73FD; } /* Literal.Number.Integer */ -.highlight .mo { color: #FF73FD; } /* Literal.Number.Oct */ -.highlight .sb { color: #A8FF60; } /* Literal.String.Backtick */ -.highlight .sc { color: #A8FF60; } /* Literal.String.Char */ -.highlight .sd { color: #A8FF60; } /* Literal.String.Doc */ -.highlight .s2 { color: #A8FF60; } /* Literal.String.Double */ -.highlight .se { color: #A8FF60; } /* Literal.String.Escape */ -.highlight .sh { color: #A8FF60; } /* Literal.String.Heredoc */ -.highlight .si { color: #A8FF60; } /* Literal.String.Interpol */ -.highlight .sx { color: #A8FF60; } /* Literal.String.Other */ -.highlight .sr { color: #A8FF60; } /* Literal.String.Regex */ -.highlight .s1 { color: #A8FF60; } /* Literal.String.Single */ -.highlight .ss { color: #A8FF60; } /* Literal.String.Symbol */ -.highlight .bp { color: #f6f3e8; } /* Name.Builtin.Pseudo */ -.highlight .vc { color: #C6C5FE; } /* Name.Variable.Class */ -.highlight .vg { color: #C6C5FE; } /* Name.Variable.Global */ -.highlight .vi { color: #C6C5FE; } /* Name.Variable.Instance */ -.highlight .il { color: #FF73FD; } /* Literal.Number.Integer.Long */ diff --git a/docs/_templates/defindex.html b/docs/_templates/defindex.html deleted file mode 100644 index ce8d3af6..00000000 --- a/docs/_templates/defindex.html +++ /dev/null @@ -1,35 +0,0 @@ -{# - basic/defindex.html - ~~~~~~~~~~~~~~~~~~~ - - Default template for the "index" page. - - :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} -{% extends "layout.html" %} -{% set title = _('Overview') %} -{% block body %} - <h1>{{ docstitle|e }}</h1> - <p> - Welcome! This is - {% block description %}the documentation for {{ project|e }} - {{ release|e }}{% if last_updated %}, last updated {{ last_updated|e }}{% endif %}{% endblock %}. - </p> - {% block tables %} - <p><strong>{{ _('Indices and tables:') }}</strong></p> - <table class="contentstable" align="center"><tr> - <td width="50%"> - <p class="biglink"><a class="biglink" href="{{ pathto("contents") }}">{{ _('Complete Table of Contents') }}</a><br> - <span class="linkdescr">{{ _('lists all sections and subsections') }}</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("search") }}">{{ _('Search Page') }}</a><br> - <span class="linkdescr">{{ _('search this documentation') }}</span></p> - </td><td width="50%"> - <p class="biglink"><a class="biglink" href="{{ pathto("modindex") }}">{{ _('Global Module Index') }}</a><br> - <span class="linkdescr">{{ _('quick access to all modules') }}</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("genindex") }}">{{ _('General Index') }}</a><br> - <span class="linkdescr">{{ _('all functions, classes, terms') }}</span></p> - </td></tr> - </table> - {% endblock %} -{% endblock %} diff --git a/docs/_templates/indexcontent.html b/docs/_templates/indexcontent.html deleted file mode 100644 index d5e17cd6..00000000 --- a/docs/_templates/indexcontent.html +++ /dev/null @@ -1,61 +0,0 @@ -{% extends "defindex.html" %} -{% block tables %} - <p><strong>Parts of the documentation:</strong></p> - <table class="contentstable" align="center"><tr> - <td width="50%"> - <p class="biglink"><a class="biglink" href="{{ pathto("whatsnew/" + version) }}">What's new in Python {{ version }}?</a><br/> - <span class="linkdescr">or <a href="{{ pathto("whatsnew/index") }}">all "What's new" documents</a> since 2.0</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("tutorial/index") }}">Tutorial</a><br/> - <span class="linkdescr">start here</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("library/index") }}">Library Reference</a><br/> - <span class="linkdescr">keep this under your pillow</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("reference/index") }}">Language Reference</a><br/> - <span class="linkdescr">describes syntax and language elements</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("using/index") }}">Python Setup and Usage</a><br/> - <span class="linkdescr">how to use Python on different platforms</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("howto/index") }}">Python HOWTOs</a><br/> - <span class="linkdescr">in-depth documents on specific topics</span></p> - </td><td width="50%"> - <p class="biglink"><a class="biglink" href="{{ pathto("extending/index") }}">Extending and Embedding</a><br/> - <span class="linkdescr">tutorial for C/C++ programmers</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("c-api/index") }}">Python/C API</a><br/> - <span class="linkdescr">reference for C/C++ programmers</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("install/index") }}">Installing Python Modules</a><br/> - <span class="linkdescr">information for installers & sys-admins</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("distutils/index") }}">Distributing Python Modules</a><br/> - <span class="linkdescr">sharing modules with others</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("documenting/index") }}">Documenting Python</a><br/> - <span class="linkdescr">guide for documentation authors</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("faq/index") }}">FAQs</a><br/> - <span class="linkdescr">frequently asked questions (with answers!)</span></p> - </td></tr> - </table> - - <p><strong>Indices and tables:</strong></p> - <table class="contentstable" align="center"><tr> - <td width="50%"> - <p class="biglink"><a class="biglink" href="{{ pathto("py-modindex") }}">Global Module Index</a><br/> - <span class="linkdescr">quick access to all modules</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("genindex") }}">General Index</a><br/> - <span class="linkdescr">all functions, classes, terms</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("glossary") }}">Glossary</a><br/> - <span class="linkdescr">the most important terms explained</span></p> - </td><td width="50%"> - <p class="biglink"><a class="biglink" href="{{ pathto("search") }}">Search page</a><br/> - <span class="linkdescr">search this documentation</span></p> - <p class="biglink"><a class="biglink" href="{{ pathto("contents") }}">Complete Table of Contents</a><br/> - <span class="linkdescr">lists all sections and subsections</span></p> - </td></tr> - </table> - - <p><strong>Meta information:</strong></p> - <table class="contentstable" align="center"><tr> - <td width="50%"> - <p class="biglink"><a class="biglink" href="{{ pathto("bugs") }}">Reporting bugs</a></p> - <p class="biglink"><a class="biglink" href="{{ pathto("about") }}">About the documentation</a></p> - </td><td width="50%"> - <p class="biglink"><a class="biglink" href="{{ pathto("license") }}">History and License of Python</a></p> - <p class="biglink"><a class="biglink" href="{{ pathto("copyright") }}">Copyright</a></p> - </td></tr> - </table> -{% endblock %} diff --git a/docs/_templates/layout.html b/docs/_templates/layout.html deleted file mode 100644 index a5dd7c82..00000000 --- a/docs/_templates/layout.html +++ /dev/null @@ -1,69 +0,0 @@ -{# - haiku/layout.html - ~~~~~~~~~~~~~~~~~ - - Sphinx layout template for the haiku theme. - - :copyright: Copyright 2007-2011 by the Sphinx team, see AUTHORS. - :license: BSD, see LICENSE for details. -#} -{% extends "basic/layout.html" %} -{% set script_files = script_files + ['_static/theme_extras.js'] %} -{% set css_files = css_files + ['_static/print.css'] %} - -{# do not display relbars #} -{% block relbar1 %}{% endblock %} -{% block relbar2 %}{% endblock %} - -{% macro nav() %} - <p> - {%- block haikurel1 %} - {%- endblock %} - {%- if prev %} - «  <a href="{{ prev.link|e }}">{{ prev.title }}</a> -   ::   - {%- endif %} - <a class="uplink" href="{{ pathto(master_doc) }}">{{ _('Contents') }}</a> - {%- if next %} -   ::   - <a href="{{ next.link|e }}">{{ next.title }}</a>  » - {%- endif %} - {%- block haikurel2 %} - {%- endblock %} - </p> -{% endmacro %} - -{% block content %} - <div class="header"> - {%- block haikuheader %} - {%- if theme_full_logo != "false" %} - <a href="{{ pathto('index') }}"> - <img class="logo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/> - </a> - {%- else %} - {%- if logo -%} - <img class="rightlogo" src="{{ pathto('_static/' + logo, 1) }}" alt="Logo"/> - {%- endif -%} - <h1 class="heading"> - <a href="{{ pathto('index') }}"><span>{{ project|e }}</span></a> - </h1> - <h2 class="heading"><span>{{ shorttitle|e }}</span></h2> - {%- endif %} - {%- endblock %} - </div> - <div class="topnav"> - {{ nav() }} - </div> - <div class="content"> - {#{%- if display_toc %} - <div id="toc"> - <h3>Table Of Contents</h3> - {{ toc }} - </div> - {%- endif %}#} - {% block body %}{% endblock %} - </div> - <div class="bottomnav"> - {{ nav() }} - </div> -{% endblock %} diff --git a/docs/api/basexmpp.rst b/docs/api/basexmpp.rst index 841df3db..fa96322e 100644 --- a/docs/api/basexmpp.rst +++ b/docs/api/basexmpp.rst @@ -1,5 +1,5 @@ ======== -basexmpp +BaseXMPP ======== .. module:: sleekxmpp.basexmpp diff --git a/docs/api/clientxmpp.rst b/docs/api/clientxmpp.rst index 8f87664e..a6f32c43 100644 --- a/docs/api/clientxmpp.rst +++ b/docs/api/clientxmpp.rst @@ -1,17 +1,8 @@ ========== -clientxmpp +ClientXMPP ========== .. module:: sleekxmpp.clientxmpp .. autoclass:: ClientXMPP - - .. automethod:: connect - - .. automethod:: register_feature - - .. automethod:: get_roster - - .. automethod:: update_roster - - .. automethod:: del_roster_item + :members: diff --git a/docs/api/componentxmpp.rst b/docs/api/componentxmpp.rst new file mode 100644 index 00000000..989120c2 --- /dev/null +++ b/docs/api/componentxmpp.rst @@ -0,0 +1,8 @@ +============= +ComponentXMPP +============= + +.. module:: sleekxmpp.componentxmpp + +.. autoclass:: ComponentXMPP + :members: diff --git a/docs/api/xmlstream.rst b/docs/api/xmlstream.rst deleted file mode 100644 index 7835bf57..00000000 --- a/docs/api/xmlstream.rst +++ /dev/null @@ -1,8 +0,0 @@ -========= -xmlstream -========= - -.. module:: sleekxmpp.xmlstream - -.. autoclass:: XMLStream - :members: diff --git a/docs/api/xmlstream/filesocket.rst b/docs/api/xmlstream/filesocket.rst new file mode 100644 index 00000000..35f44019 --- /dev/null +++ b/docs/api/xmlstream/filesocket.rst @@ -0,0 +1,12 @@ +.. module:: sleekxmpp.xmlstream.filesocket + +.. _filesocket: + +Python 2.6 File Socket Shims +============================ + +.. autoclass:: FileSocket + :members: + +.. autoclass:: Socket26 + :members: diff --git a/docs/api/xmlstream/handler.rst b/docs/api/xmlstream/handler.rst new file mode 100644 index 00000000..33c0bf42 --- /dev/null +++ b/docs/api/xmlstream/handler.rst @@ -0,0 +1,24 @@ +Stanza Handlers +=============== + +The Basic Handler +----------------- +.. module:: sleekxmpp.xmlstream.handler.base + +.. autoclass:: BaseHandler + :members: + +Callback +-------- +.. module:: sleekxmpp.xmlstream.handler.callback + +.. autoclass:: Callback + :members: + + +Waiter +------ +.. module:: sleekxmpp.xmlstream.handler.waiter + +.. autoclass:: Waiter + :members: diff --git a/docs/api/xmlstream/jid.rst b/docs/api/xmlstream/jid.rst new file mode 100644 index 00000000..22a2db45 --- /dev/null +++ b/docs/api/xmlstream/jid.rst @@ -0,0 +1,7 @@ +Jabber IDs (JID) +================= + +.. module:: sleekxmpp.xmlstream.jid + +.. autoclass:: JID + :members: diff --git a/docs/api/xmlstream/matcher.rst b/docs/api/xmlstream/matcher.rst new file mode 100644 index 00000000..df3591bc --- /dev/null +++ b/docs/api/xmlstream/matcher.rst @@ -0,0 +1,41 @@ +Stanza Matchers +=============== + +The Basic Matcher +----------------- +.. module:: sleekxmpp.xmlstream.matcher.base + +.. autoclass:: MatcherBase + :members: + + +ID Matching +----------- +.. module:: sleekxmpp.xmlstream.matcher.id + +.. autoclass:: MatcherId + :members: + + +Stanza Path Matching +-------------------- +.. module:: sleekxmpp.xmlstream.matcher.stanzapath + +.. autoclass:: StanzaPath + :members: + + +XPath +----- +.. module:: sleekxmpp.xmlstream.matcher.xpath + +.. autoclass:: MatchXPath + :members: + + +XMLMask +------- +.. module:: sleekxmpp.xmlstream.matcher.xmlmask + +.. autoclass:: MatchXMLMask + :members: diff --git a/docs/api/xmlstream/scheduler.rst b/docs/api/xmlstream/scheduler.rst new file mode 100644 index 00000000..ff91701e --- /dev/null +++ b/docs/api/xmlstream/scheduler.rst @@ -0,0 +1,11 @@ +========= +Scheduler +========= + +.. module:: sleekxmpp.xmlstream.scheduler + +.. autoclass:: Task + :members: + +.. autoclass:: Scheduler + :members: diff --git a/docs/api/xmlstream/stanzabase.rst b/docs/api/xmlstream/stanzabase.rst new file mode 100644 index 00000000..f575299e --- /dev/null +++ b/docs/api/xmlstream/stanzabase.rst @@ -0,0 +1,123 @@ +.. _stanzabase: + +============== +Stanza Objects +============== + +.. module:: sleekxmpp.xmlstream.stanzabase + +The :mod:`~sleekmxpp.xmlstream.stanzabase` module provides a wrapper for the +standard :mod:`~xml.etree.ElementTree` module that makes working with XML +less painful. Instead of having to manually move up and down an element +tree and insert subelements and attributes, you can interact with an object +that behaves like a normal dictionary or JSON object, which silently maps +keys to XML attributes and elements behind the scenes. + +Overview +-------- + +The usefulness of this layer grows as the XML you have to work with +becomes nested. The base unit here, :class:`ElementBase`, can map to a +single XML element, or several depending on how advanced of a mapping +is desired from interface keys to XML structures. For example, a single +:class:`ElementBase` derived class could easily describe: + +.. code-block:: xml + + <message to="user@example.com" from="friend@example.com"> + <body>Hi!</body> + <x:extra> + <x:item>Custom item 1</x:item> + <x:item>Custom item 2</x:item> + <x:item>Custom item 3</x:item> + </x:extra> + </message> + +If that chunk of XML were put in the :class:`ElementBase` instance +``msg``, we could extract the data from the XML using:: + + >>> msg['extra'] + ['Custom item 1', 'Custom item 2', 'Custom item 3'] + +Provided we set up the handler for the ``'extra'`` interface to load the +``<x:item>`` element content into a list. + +The key concept is that given an XML structure that will be repeatedly +used, we can define a set of :term:`interfaces` which when we read from, +write to, or delete, will automatically manipulate the underlying XML +as needed. In addition, some of these interfaces may in turn reference +child objects which expose interfaces for particularly complex child +elements of the original XML chunk. + +.. seealso:: + :ref:`create-stanza-interfaces`. + +Because the :mod:`~sleekxmpp.xmlstream.stanzabase` module was developed +as part of an `XMPP <http://xmpp.org>`_ library, these chunks of XML are +referred to as :term:`stanzas <stanza>`, and in SleekXMPP we refer to a +subclass of :class:`ElementBase` which defines the interfaces needed for +interacting with a given :term:`stanza` a :term:`stanza object`. + +To make dealing with more complicated and nested :term:`stanzas <stanza>` +or XML chunks easier, :term:`stanza objects <stanza object>` can be +composed in two ways: as iterable child objects or as plugins. Iterable +child stanzas, or :term:`substanzas`, are accessible through a special +``'substanzas'`` interface. This option is useful for stanzas which +may contain more than one of the same kind of element. When there is +only one child element, the plugin method is more useful. For plugins, +a parent stanza object delegates one of its XML child elements to the +plugin stanza object. Here is an example: + +.. code-block:: xml + + <iq type="result"> + <query xmlns="http://jabber.org/protocol/disco#info"> + <identity category="client" type="bot" name="SleekXMPP Bot" /> + </query> + </iq> + +We can can arrange this stanza into two objects: an outer, wrapper object for +dealing with the ``<iq />`` element and its attributes, and a plugin object to +control the ``<query />`` payload element. If we give the plugin object the +name ``'disco_info'`` (using its :attr:`ElementBase.plugin_attrib` value), then +we can access the plugin as so:: + + >>> iq['disco_info'] + '<query xmlns="http://jabber.org/protocol/disco#info"> + <identity category="client" type="bot" name="SleekXMPP Bot" /> + </query>' + +We can then drill down through the plugin object's interfaces as desired:: + + >>> iq['disco_info']['identities'] + [('client', 'bot', 'SleekXMPP Bot')] + +Plugins may also add new interfaces to the parent stanza object as if they +had been defined by the parent directly, and can also override the behaviour +of an interface defined by the parent. + +.. seealso:: + + - :ref:`create-stanza-plugins` + - :ref:`create-extension-plugins` + - :ref:`override-parent-interfaces` + + +Registering Stanza Plugins +-------------------------- + +.. autofunction:: register_stanza_plugin + +ElementBase +----------- + +.. autoclass:: ElementBase + :members: + :private-members: + :special-members: + +StanzaBase +---------- + +.. autoclass:: StanzaBase + :members: diff --git a/docs/api/xmlstream/tostring.rst b/docs/api/xmlstream/tostring.rst new file mode 100644 index 00000000..82a8c2a5 --- /dev/null +++ b/docs/api/xmlstream/tostring.rst @@ -0,0 +1,46 @@ +.. module:: sleekxmpp.xmlstream.tostring + +.. _tostring: + +XML Serialization +================= + +Since the XML layer of SleekXMPP is based on :mod:`~xml.etree.ElementTree`, +why not just use the built-in :func:`~xml.etree.ElementTree.tostring` +method? The answer is that using that method produces ugly results when +using namespaces. The :func:`tostring()` method used here intelligently +hides namespaces when able and does not introduce excessive namespace +prefixes:: + + >>> from sleekxmpp.xmlstream.tostring import tostring + >>> from xml.etree import cElementTree as ET + >>> xml = ET.fromstring('<foo xmlns="bar"><baz /></foo>') + >>> ET.tostring(xml) + '<ns0:foo xmlns:ns0="bar"><ns0:baz /></foo>' + >>> tostring(xml) + '<foo xmlns="bar"><baz /></foo>' + +As a side effect of this namespace hiding, using :func:`tostring()` may +produce unexpected results depending on how the :func:`tostring()` method +is invoked. For example, when sending XML on the wire, the main XMPP +stanzas with their namespace of ``jabber:client`` will not include the +namespace because that is already declared by the stream header. But, if +you create a :class:`~sleekxmpp.stanza.message.Message` instance and dump +it to the terminal, the ``jabber:client`` namespace will appear. + +.. autofunction:: tostring + +Escaping Special Characters +--------------------------- + +In order to prevent errors when sending arbitrary text as the textual +content of an XML element, certain characters must be escaped. These +are: ``&``, ``<``, ``>``, ``"``, and ``'``. The default escaping +mechanism is to replace those characters with their equivalent escape +entities: ``&``, ``<``, ``>``, ``'``, and ``"``. + +In the future, the use of CDATA sections may be allowed to reduce the +size of escaped text or for when other XMPP processing agents do not +undertand these entities. + +.. autofunction:: xml_escape diff --git a/docs/api/xmlstream/xmlstream.rst b/docs/api/xmlstream/xmlstream.rst new file mode 100644 index 00000000..90a7a6af --- /dev/null +++ b/docs/api/xmlstream/xmlstream.rst @@ -0,0 +1,10 @@ +========== +XML Stream +========== + +.. module:: sleekxmpp.xmlstream.xmlstream + +.. autoexception:: RestartStream + +.. autoclass:: XMLStream + :members: diff --git a/docs/architecture.rst b/docs/architecture.rst index 53c326e1..a2e0a27d 100644 --- a/docs/architecture.rst +++ b/docs/architecture.rst @@ -17,21 +17,21 @@ of the tedium of creating/manipulating XML. The Foundation: XMLStream ------------------------- -``XMLStream`` is a mostly XMPP-agnostic class whose purpose is to read -and write from a bi-directional XML stream. It also allows for callback -functions to execute when XML matching given patterns is received; these -callbacks are also referred to as :term:`stream handlers <stream handler>`. -The class also provides a basic eventing system which can be triggered -either manually or on a timed schedule. +:class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` is a mostly XMPP-agnostic +class whose purpose is to read and write from a bi-directional XML stream. +It also allows for callback functions to execute when XML matching given +patterns is received; these callbacks are also referred to as :term:`stream +handlers <stream handler>`. The class also provides a basic eventing system +which can be triggered either manually or on a timed schedule. The Main Threads ~~~~~~~~~~~~~~~~ -``XMLStream`` instances run using at least three background threads: the -send thread, the read thread, and the scheduler thread. The send thread is -in charge of monitoring the send queue and writing text to the outgoing -XML stream. The read thread pulls text off of the incoming XML stream and -stores the results in an event queue. The scheduler thread is used to emit -events after a given period of time. +:class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` instances run using at +least three background threads: the send thread, the read thread, and the +scheduler thread. The send thread is in charge of monitoring the send queue +and writing text to the outgoing XML stream. The read thread pulls text off +of the incoming XML stream and stores the results in an event queue. The +scheduler thread is used to emit events after a given period of time. Additionally, the main event processing loop may be executed in its own thread if SleekXMPP is being used in the background for another @@ -61,9 +61,10 @@ when this bit of XML is received (with an assumed namespace of new object is determined using a map of namespaced element names to classes. - Our incoming XML is thus turned into a ``Message`` :term:`stanza object` - because the namespaced element name ``{jabber:client}message`` is - associated with the class ``sleekxmpp.stanza.Message``. + Our incoming XML is thus turned into a :class:`~sleekxmpp.stanza.Message` + :term:`stanza object` because the namespaced element name + ``{jabber:client}message`` is associated with the class + :class:`~sleekxmpp.stanza.Message`. 2. **Match stanza objects to callbacks.** @@ -72,8 +73,8 @@ when this bit of XML is received (with an assumed namespace of :term:`stanza object` is paired with a reference to the handler and placed into the event queue. - Our ``Message`` object is thus paired with the message stanza handler - ``BaseXMPP._handle_message`` to create the tuple:: + Our :class:`~sleekxmpp.stanza.Message` object is thus paired with the message stanza handler + :meth:`BaseXMPP._handle_message` to create the tuple:: ('stanza', stanza_obj, handler) @@ -88,7 +89,7 @@ when this bit of XML is received (with an assumed namespace of parameter. .. warning:: - The callback, aka :term:`stream handler`, is executed in the main + The callback, aka :term:`stream handler`, is executed in the main event processing thread. If the handler blocks, event processing will also block. @@ -96,20 +97,22 @@ when this bit of XML is received (with an assumed namespace of Since a :term:`stream handler` shouldn't block, if extensive processing for a stanza is required (such as needing to send and receive an - ``Iq`` stanza), then custom events must be used. These events are not - explicitly tied to the incoming XML stream and may be raised at any - time. Importantly, these events may be handled in their own thread. + :class:`~sleekxmpp.stanza.Iq` stanza), then custom events must be used. + These events are not explicitly tied to the incoming XML stream and may + be raised at any time. Importantly, these events may be handled in their + own thread. When the event is raised, a copy of the stanza is created for each - handler registered for the event. In contrast to :term:`stream handlers <stream handler>`, - these functions are referred to as :term:`event handlers <event handler>`. - Each stanza/handler pair is then put into the event queue. + handler registered for the event. In contrast to :term:`stream handlers + <stream handler>`, these functions are referred to as :term:`event + handlers <event handler>`. Each stanza/handler pair is then put into the + event queue. .. note:: It is possible to skip the event queue and process an event immediately by using ``direct=True`` when raising the event. - The code for ``BaseXMPP._handle_message`` follows this pattern, and + The code for :meth:`BaseXMPP._handle_message` follows this pattern, and raises a ``'message'`` event:: self.event('message', msg) @@ -145,125 +148,30 @@ when this bit of XML is received (with an assumed namespace of Raising XMPP Awareness: BaseXMPP -------------------------------- -While ``XMLStream`` attempts to shy away from anything too XMPP specific, -``BaseXMPP``'s sole purpose is to provide foundational support for sending -and receiving XMPP stanzas. This support includes registering the basic -message, presence, and iq stanzas, methods for creating and sending -stanzas, and default handlers for incoming messages and keeping track of -presence notifications. +While :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` attempts to shy away +from anything too XMPP specific, :class:`~sleekxmpp.basexmpp.BaseXMPP`'s +sole purpose is to provide foundational support for sending and receiving +XMPP stanzas. This support includes registering the basic message, +presence, and iq stanzas, methods for creating and sending stanzas, and +default handlers for incoming messages and keeping track of presence +notifications. The plugin system for adding new XEP support is also maintained by -``BaseXMPP``. +:class:`~sleekxmpp.basexmpp.BaseXMPP`. .. index:: ClientXMPP, BaseXMPP ClientXMPP ---------- -``ClientXMPP`` extends ``BaseXMPP`` with additional logic for connecting to -an XMPP server by performing DNS lookups. It also adds support for stream +:class:`~sleekxmpp.clientxmpp.ClientXMPP` extends +:class:`~sleekxmpp.clientxmpp.BaseXMPP` with additional logic for connecting +to an XMPP server by performing DNS lookups. It also adds support for stream features such as STARTTLS and SASL. .. index:: ComponentXMPP, BaseXMPP ComponentXMPP ------------- -``ComponentXMPP`` is only a thin layer on top of ``BaseXMPP`` that -implements the component handshake protocol. - -.. index:: - double: object; stanza - -Stanza Objects: A Brief Look ----------------------------- -.. seealso:: - See :ref:`api-stanza-objects` for a more detailed overview. - -Almost worthy of their own standalone library, :term:`stanza objects <stanza object>` -are wrappers for XML objects which expose dictionary like interfaces -for manipulating their XML content. For example, consider the XML: - -.. code-block:: xml - - <message /> - -A very plain element to start with, but we can create a :term:`stanza object` -using ``sleekxmpp.stanza.Message`` as so:: - - msg = Message(xml=ET.fromstring("<message />")) - -The ``Message`` stanza class defines interfaces such as ``'body'`` and -``'to'``, so we can assign values to those interfaces to include new XML -content:: - - msg['body'] = "Following so far?" - msg['to'] = 'user@example.com' - -Dumping the XML content of ``msg`` (using ``msg.xml``), we find: - -.. code-block:: xml - - <message to="user@example.com"> - <body>Following so far?</body> - </message> - -The process is similar for reading from interfaces and deleting interface -contents. A :term:`stanza object` behaves very similarly to a regular -``dict`` object: you may assign to keys, read from keys, and ``del`` keys. - -Stanza interfaces come with built-in behaviours such as adding/removing -attribute and sub element values. However, a lot of the time more custom -logic is needed. This can be provided by defining methods of the form -``get_*``, ``set_*``, and ``del_*`` for any interface which requires custom -behaviour. - -Stanza Plugins -~~~~~~~~~~~~~~ -Since it is generally possible to embed one XML element inside another, -:term:`stanza objects <stanza object>` may be nested. Nested -:term:`stanza objects <stanza object>` are referred to as :term:`stanza plugins <stanza plugin>` -or :term:`substanzas <substanza>`. - -A :term:`stanza plugin` exposes its own interfaces by adding a new -interface to its parent stanza. To demonstrate, consider these two stanza -class definitions using ``sleekxmpp.xmlstream.ElementBase``: - - -.. code-block:: python - - class Parent(ElementBase): - name = "the-parent-xml-element-name" - namespace = "the-parent-namespace" - interfaces = set(('foo', 'bar')) - - class Child(ElementBase): - name = "the-child-xml-element-name" - namespace = "the-child-namespace" - plugin_attrib = 'child' - interfaces = set(('baz',)) - - -If we register the ``Child`` stanza as a plugin of the ``Parent`` stanza as -so, using ``sleekxmpp.xmlstream.register_stanza_plugin``:: - - register_stanza_plugin(Parent, Child) - -Then we can access content in the child stanza through the parent. -Note that the interface used to access the child stanza is the same as -``Child.plugin_attrib``:: - - parent = Parent() - parent['foo'] = 'a' - parent['child']['baz'] = 'b' - -The above code would produce: - -.. code-block:: xml - - <the-parent-xml-element xmlns="the-parent-namespace" foo="a"> - <the-child-xml-element xmlsn="the-child-namespace" baz="b" /> - </the-parent-xml-element> - -It is also possible to allow a :term:`substanza` to appear multiple times -by using ``iterable=True`` in the ``register_stanza_plugin`` call. All -iterable :term:`substanzas <substanza>` can be accessed using a standard -``substanzas`` interface. +:class:`~sleekxmpp.componentxmpp.ComponentXMPP` is only a thin layer on top of +:class:`~sleekxmpp.basexmpp.BaseXMPP` that implements the component handshake +protocol. diff --git a/docs/conf.py b/docs/conf.py index 8a165872..dd83f243 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -16,7 +16,7 @@ import sys, os # 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('.')) +sys.path.insert(0, os.path.abspath('..')) # -- General configuration ----------------------------------------------------- @@ -25,7 +25,7 @@ import sys, os # 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.viewcode'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.viewcode', 'sphinx.ext.intersphinx'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] @@ -50,7 +50,7 @@ copyright = u'2011, Nathan Fritz, Lance Stout' # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. -release = '1.0RC1' +release = '1.0RC3' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -81,7 +81,7 @@ exclude_patterns = ['_build'] #show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'default' +pygments_style = 'tango' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] @@ -91,7 +91,7 @@ pygments_style = 'default' # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'haiku' +html_theme = 'nature' # 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 @@ -218,3 +218,5 @@ man_pages = [ ('index', 'sleekxmpp', u'SleekXMPP Documentation', [u'Nathan Fritz, Lance Stout'], 1) ] + +intersphinx_mapping = {'python': ('http://docs.python.org/3.2', 'python-objects.inv')} diff --git a/docs/howto/stanzas.rst b/docs/howto/stanzas.rst new file mode 100644 index 00000000..7ca7bbfd --- /dev/null +++ b/docs/howto/stanzas.rst @@ -0,0 +1,28 @@ +How to Work with Stanza Objects +=============================== + + +.. _create-stanza-interfaces: + +Defining Stanza Interfaces +-------------------------- + + +.. _create-stanza-plugins: + +Creating Stanza Plugins +----------------------- + + + +.. _create-extension-plugins: + +Creating a Stanza Extension +--------------------------- + + + +.. _override-parent-interfaces: + +Overriding a Parent Stanza +-------------------------- diff --git a/docs/index.rst b/docs/index.rst index 5da389b9..fc6541d6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,13 +12,8 @@ SleekXMPP ``master`` branch, while the latest development version is in the ``develop`` branch. - **Stable Releases** - - `1.0 Beta6.1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta6.1>`_ - - `1.0 Beta5 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta5>`_ - - `1.0 Beta4 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta4>`_ - - `1.0 Beta3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta3>`_ - - `1.0 Beta2 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta2>`_ - - `1.0 Beta1 <http://github.com/fritzy/SleekXMPP/zipball/1.0-Beta1>`_ + **Latest Stable Release** + - `1.0 RC3 <http://github.com/fritzy/SleekXMPP/zipball/1.0-RC3>`_ **Develop Releases** - `Latest Develop Version <http://github.com/fritzy/SleekXMPP/zipball/develop>`_ @@ -84,8 +79,10 @@ Tutorials, FAQs, and How To Guides .. toctree:: :maxdepth: 1 + faq xeps xmpp_tdg + howto/stanzas create_plugin features sasl @@ -113,8 +110,35 @@ API Reference event_index api/clientxmpp + api/componentxmpp api/basexmpp - api/xmlstream + api/exceptions + api/xmlstream/jid + api/xmlstream/stanzabase + api/xmlstream/handler + api/xmlstream/matcher + api/xmlstream/xmlstream + api/xmlstream/scheduler + api/xmlstream/tostring + api/xmlstream/filesocket + +Core Stanzas +~~~~~~~~~~~~ +.. toctree:: + :maxdepth: 2 + + api/stanza/rootstanza + api/stanza/message + api/stanza/presence + api/stanza/iq + api/stanza/error + api/stanza/stream_error + +Plugins +~~~~~~~ +.. toctree:: + :maxdepth: 2 + Additional Info --------------- diff --git a/docs/python-objects.inv b/docs/python-objects.inv Binary files differnew file mode 100644 index 00000000..b7afc07e --- /dev/null +++ b/docs/python-objects.inv diff --git a/docs/xeps.rst b/docs/xeps.rst index a541a586..3653d10a 100644 --- a/docs/xeps.rst +++ b/docs/xeps.rst @@ -1,2 +1,50 @@ Supported XEPS ============== + +======= ============================= ================ +XEP Description Notes +======= ============================= ================ +`0004`_ Data forms +`0009`_ Jabber RPC +`0012`_ Last Activity +`0030`_ Service Discovery +`0033`_ Extended Stanza Addressing +`0045`_ Multi-User Chat (MUC) Client-side only +`0050`_ Ad-hoc Commands +`0059`_ Result Set Management +`0060`_ Publish/Subscribe (PubSub) Client-side only +`0066`_ Out-of-band Data +`0078`_ Non-SASL Authentication +`0082`_ XMPP Date and Time Profiles +`0085`_ Chat-State Notifications +`0086`_ Error Condition Mappings +`0092`_ Software Version +`0128`_ Service Discovery Extensions +`0202`_ Entity Time +`0203`_ Delayed Delivery +`0224`_ Attention +`0249`_ Direct MUC Invitations +======= ============================= ================ + + +.. _0004: http://xmpp.org/extensions/xep-0004.html +.. _0009: http://xmpp.org/extensions/xep-0009.html +.. _0012: http://xmpp.org/extensions/xep-0012.html +.. _0030: http://xmpp.org/extensions/xep-0030.html +.. _0033: http://xmpp.org/extensions/xep-0033.html +.. _0045: http://xmpp.org/extensions/xep-0045.html +.. _0050: http://xmpp.org/extensions/xep-0050.html +.. _0059: http://xmpp.org/extensions/xep-0059.html +.. _0060: http://xmpp.org/extensions/xep-0060.html +.. _0066: http://xmpp.org/extensions/xep-0066.html +.. _0078: http://xmpp.org/extensions/xep-0078.html +.. _0082: http://xmpp.org/extensions/xep-0082.html +.. _0085: http://xmpp.org/extensions/xep-0085.html +.. _0086: http://xmpp.org/extensions/xep-0086.html +.. _0092: http://xmpp.org/extensions/xep-0092.html +.. _0128: http://xmpp.org/extensions/xep-0128.html +.. _0199: http://xmpp.org/extensions/xep-0199.html +.. _0202: http://xmpp.org/extensions/xep-0202.html +.. _0203: http://xmpp.org/extensions/xep-0203.html +.. _0224: http://xmpp.org/extensions/xep-0224.html +.. _0249: http://xmpp.org/extensions/xep-0249.html diff --git a/examples/ping.py b/examples/ping.py index d5622ffd..81194eef 100755 --- a/examples/ping.py +++ b/examples/ping.py @@ -72,7 +72,7 @@ class PingTest(sleekxmpp.ClientXMPP): self.disconnect() sys.exit(1) else: - logging.info("Success! RTT: %s" % str(result)) + logging.info("Success! RTT: %s", str(result)) self.disconnect() diff --git a/sleekxmpp/basexmpp.py b/sleekxmpp/basexmpp.py index 8f11eefa..11e787ad 100644 --- a/sleekxmpp/basexmpp.py +++ b/sleekxmpp/basexmpp.py @@ -1,9 +1,15 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.basexmpp + ~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + This module provides the common XMPP functionality + for both clients and components. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from __future__ import with_statement, unicode_literals @@ -43,71 +49,59 @@ class BaseXMPP(XMLStream): with XMPP. It also provides a plugin mechanism to easily extend and add support for new XMPP features. - Attributes: - auto_authorize -- Manage automatically accepting roster - subscriptions. - auto_subscribe -- Manage automatically requesting mutual - subscriptions. - is_component -- Indicates if this stream is for an XMPP component. - jid -- The XMPP JID for this stream. - plugin -- A dictionary of loaded plugins. - plugin_config -- A dictionary of plugin configurations. - plugin_whitelist -- A list of approved plugins. - sentpresence -- Indicates if an initial presence has been sent. - roster -- A dictionary containing subscribed JIDs and - their presence statuses. - - Methods: - Iq -- Factory for creating an Iq stanzas. - Message -- Factory for creating Message stanzas. - Presence -- Factory for creating Presence stanzas. - get -- Return a plugin given its name. - make_iq -- Create and initialize an Iq stanza. - make_iq_error -- Create an Iq stanza of type 'error'. - make_iq_get -- Create an Iq stanza of type 'get'. - make_iq_query -- Create an Iq stanza with a given query. - make_iq_result -- Create an Iq stanza of type 'result'. - make_iq_set -- Create an Iq stanza of type 'set'. - make_message -- Create and initialize a Message stanza. - make_presence -- Create and initialize a Presence stanza. - make_query_roster -- Create a roster query. - process -- Overrides XMLStream.process. - register_plugin -- Load and configure a plugin. - register_plugins -- Load and configure multiple plugins. - send_message -- Create and send a Message stanza. - send_presence -- Create and send a Presence stanza. - send_presence_subscribe -- Send a subscription request. + :param default_ns: Ensure that the correct default XML namespace + is used during initialization. """ def __init__(self, jid='', default_ns='jabber:client'): - """ - Adapt an XML stream for use with XMPP. - - Arguments: - default_ns -- Ensure that the correct default XML namespace - is used during initialization. - """ XMLStream.__init__(self) - # To comply with PEP8, method names now use underscores. - # Deprecated method names are re-mapped for backwards compatibility. self.default_ns = default_ns self.stream_ns = 'http://etherx.jabber.org/streams' self.namespace_map[self.stream_ns] = 'stream' + #: An identifier for the stream as given by the server. + self.stream_id = None + + #: The JabberID (JID) used by this connection. self.boundjid = JID(jid) + #: A dictionary mapping plugin names to plugins. self.plugin = {} + + #: Configuration options for whitelisted plugins. + #: If a plugin is registered without any configuration, + #: and there is an entry here, it will be used. self.plugin_config = {} + + #: A list of plugins that will be loaded if + #: :meth:`register_plugins` is called. self.plugin_whitelist = [] + #: The main roster object. This roster supports multiple + #: owner JIDs, as in the case for components. For clients + #: which only have a single JID, see :attr:`client_roster`. self.roster = roster.Roster(self) self.roster.add(self.boundjid.bare) + + #: The single roster for the bound JID. This is the + #: equivalent of:: + #: + #: self.roster[self.boundjid.bare] self.client_roster = self.roster[self.boundjid.bare] + #: The distinction between clients and components can be + #: important, primarily for choosing how to handle the + #: ``'to'`` and ``'from'`` JIDs of stanzas. self.is_component = False + + #: Flag indicating that the initial presence broadcast has + #: been sent. Until this happens, some servers may not + #: behave as expected when sending stanzas. self.sentpresence = False + #: A reference to :mod:`sleekxmpp.stanza` to make accessing + #: stanza classes easier. self.stanza = sleekxmpp.stanza self.register_handler( @@ -161,40 +155,36 @@ class BaseXMPP(XMLStream): register_stanza_plugin(Message, HTMLIM) def start_stream_handler(self, xml): - """ - Save the stream ID once the streams have been established. - - Overrides XMLStream.start_stream_handler. + """Save the stream ID once the streams have been established. - Arguments: - xml -- The incoming stream's root element. + :param xml: The incoming stream's root element. """ self.stream_id = xml.get('id', '') def process(self, *args, **kwargs): - """ - Overrides XMLStream.process. - - Initialize the XML streams and begin processing events. + """Initialize plugins and begin processing the XML stream. The number of threads used for processing stream events is determined - by HANDLER_THREADS. - - Arguments: - block -- If block=False then event dispatcher will run - in a separate thread, allowing for the stream to be - used in the background for another application. - Otherwise, process(block=True) blocks the current thread. - Defaults to False. - - **threaded is deprecated and included for API compatibility** - threaded -- If threaded=True then event dispatcher will run - in a separate thread, allowing for the stream to be - used in the background for another application. - Defaults to True. - - Event handlers and the send queue will be threaded - regardless of these parameters. + by :data:`HANDLER_THREADS`. + + :param bool block: If ``False``, then event dispatcher will run + in a separate thread, allowing for the stream to be + used in the background for another application. + Otherwise, ``process(block=True)`` blocks the current + thread. Defaults to ``False``. + :param bool threaded: **DEPRECATED** + If ``True``, then event dispatcher will run + in a separate thread, allowing for the stream to be + used in the background for another application. + Defaults to ``True``. This does **not** mean that no + threads are used at all if ``threaded=False``. + + Regardless of these threading options, these threads will + always exist: + + - The event queue processor + - The send queue processor + - The scheduler """ for name in self.plugin: if not self.plugin[name].post_inited: @@ -202,15 +192,13 @@ class BaseXMPP(XMLStream): return XMLStream.process(self, *args, **kwargs) def register_plugin(self, plugin, pconfig={}, module=None): - """ - Register and configure a plugin for use in this stream. + """Register and configure a plugin for use in this stream. - Arguments: - plugin -- The name of the plugin class. Plugin names must + :param plugin: The name of the plugin class. Plugin names must be unique. - pconfig -- A dictionary of configuration data for the plugin. - Defaults to an empty dictionary. - module -- Optional refence to the module containing the plugin + :param pconfig: A dictionary of configuration data for the plugin. + Defaults to an empty dictionary. + :param module: Optional refence to the module containing the plugin class if using custom plugins. """ try: @@ -239,25 +227,24 @@ class BaseXMPP(XMLStream): self.plugin[plugin] = getattr(module, plugin)(self, pconfig) # Let XEP/RFC implementing plugins have some extra logging info. - spec = '(CUSTOM) ' + spec = '(CUSTOM) %s' if self.plugin[plugin].xep: spec = "(XEP-%s) " % self.plugin[plugin].xep elif self.plugin[plugin].rfc: spec = "(RFC-%s) " % self.plugin[plugin].rfc desc = (spec, self.plugin[plugin].description) - log.debug("Loaded Plugin %s%s" % desc) + log.debug("Loaded Plugin %s %s" % desc) except: log.exception("Unable to load plugin: %s", plugin) def register_plugins(self): - """ - Register and initialize all built-in plugins. + """Register and initialize all built-in plugins. Optionally, the list of plugins loaded may be limited to those - contained in self.plugin_whitelist. + contained in :attr:`plugin_whitelist`. - Plugin configurations stored in self.plugin_config will be used. + Plugin configurations stored in :attr:`plugin_config` will be used. """ if self.plugin_whitelist: plugin_list = self.plugin_whitelist @@ -276,19 +263,15 @@ class BaseXMPP(XMLStream): self.plugin[plugin].post_init() def __getitem__(self, key): - """ - Return a plugin given its name, if it has been registered. - """ + """Return a plugin given its name, if it has been registered.""" if key in self.plugin: return self.plugin[key] else: - log.warning("""Plugin "%s" is not loaded.""" % key) + log.warning("Plugin '%s' is not loaded.", key) return False def get(self, key, default): - """ - Return a plugin given its name, if it has been registered. - """ + """Return a plugin given its name, if it has been registered.""" return self.plugin.get(key, default) def Message(self, *args, **kwargs): @@ -304,16 +287,18 @@ class BaseXMPP(XMLStream): return Presence(self, *args, **kwargs) def make_iq(self, id=0, ifrom=None, ito=None, itype=None, iquery=None): - """ - Create a new Iq stanza with a given Id and from JID. - - Arguments: - id -- An ideally unique ID value for this stanza thread. - Defaults to 0. - ifrom -- The from JID to use for this stanza. - ito -- The destination JID for this stanza. - itype -- The Iq's type, one of: get, set, result, or error. - iquery -- Optional namespace for adding a query element. + """Create a new Iq stanza with a given Id and from JID. + + :param id: An ideally unique ID value for this stanza thread. + Defaults to 0. + :param ifrom: The from :class:`~sleekxmpp.xmlstream.jid.JID` + to use for this stanza. + :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID` + for this stanza. + :param itype: The :class:`~sleekxmpp.stanza.iq.Iq`'s type, + one of: ``'get'``, ``'set'``, ``'result'``, + or ``'error'``. + :param iquery: Optional namespace for adding a query element. """ iq = self.Iq() iq['id'] = str(id) @@ -324,17 +309,17 @@ class BaseXMPP(XMLStream): return iq def make_iq_get(self, queryxmlns=None, ito=None, ifrom=None, iq=None): - """ - Create an Iq stanza of type 'get'. + """Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'get'``. Optionally, a query element may be added. - Arguments: - queryxmlns -- The namespace of the query to use. - ito -- The destination JID for this stanza. - ifrom -- The from JID to use for this stanza. - iq -- Optionally use an existing stanza instead - of generating a new one. + :param queryxmlns: The namespace of the query to use. + :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID` + for this stanza. + :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID` + to use for this stanza. + :param iq: Optionally use an existing stanza instead + of generating a new one. """ if not iq: iq = self.Iq() @@ -348,14 +333,16 @@ class BaseXMPP(XMLStream): def make_iq_result(self, id=None, ito=None, ifrom=None, iq=None): """ - Create an Iq stanza of type 'result' with the given ID value. - - Arguments: - id -- An ideally unique ID value. May use self.new_id(). - ito -- The destination JID for this stanza. - ifrom -- The from JID to use for this stanza. - iq -- Optionally use an existing stanza instead - of generating a new one. + Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type + ``'result'`` with the given ID value. + + :param id: An ideally unique ID value. May use :meth:`new_id()`. + :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID` + for this stanza. + :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID` + to use for this stanza. + :param iq: Optionally use an existing stanza instead + of generating a new one. """ if not iq: iq = self.Iq() @@ -371,17 +358,22 @@ class BaseXMPP(XMLStream): def make_iq_set(self, sub=None, ito=None, ifrom=None, iq=None): """ - Create an Iq stanza of type 'set'. + Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'set'``. Optionally, a substanza may be given to use as the stanza's payload. - Arguments: - sub -- A stanza or XML object to use as the Iq's payload. - ito -- The destination JID for this stanza. - ifrom -- The from JID to use for this stanza. - iq -- Optionally use an existing stanza instead - of generating a new one. + :param sub: Either an + :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + stanza object or an + :class:`~xml.etree.ElementTree.Element` XML object + to use as the :class:`~sleekxmpp.stanza.iq.Iq`'s payload. + :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID` + for this stanza. + :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID` + to use for this stanza. + :param iq: Optionally use an existing stanza instead + of generating a new one. """ if not iq: iq = self.Iq() @@ -398,19 +390,20 @@ class BaseXMPP(XMLStream): condition='feature-not-implemented', text=None, ito=None, ifrom=None, iq=None): """ - Create an Iq stanza of type 'error'. - - Arguments: - id -- An ideally unique ID value. May use self.new_id(). - type -- The type of the error, such as 'cancel' or 'modify'. - Defaults to 'cancel'. - condition -- The error condition. - Defaults to 'feature-not-implemented'. - text -- A message describing the cause of the error. - ito -- The destination JID for this stanza. - ifrom -- The from JID to use for this stanza. - iq -- Optionally use an existing stanza instead - of generating a new one. + Create an :class:`~sleekxmpp.stanza.iq.Iq` stanza of type ``'error'``. + + :param id: An ideally unique ID value. May use :meth:`new_id()`. + :param type: The type of the error, such as ``'cancel'`` or + ``'modify'``. Defaults to ``'cancel'``. + :param condition: The error condition. Defaults to + ``'feature-not-implemented'``. + :param text: A message describing the cause of the error. + :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID` + for this stanza. + :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID` + to use for this stanza. + :param iq: Optionally use an existing stanza instead + of generating a new one. """ if not iq: iq = self.Iq() @@ -426,15 +419,16 @@ class BaseXMPP(XMLStream): def make_iq_query(self, iq=None, xmlns='', ito=None, ifrom=None): """ - Create or modify an Iq stanza to use the given - query namespace. - - Arguments: - iq -- Optional Iq stanza to modify. A new - stanza is created otherwise. - xmlns -- The query's namespace. - ito -- The destination JID for this stanza. - ifrom -- The from JID to use for this stanza. + Create or modify an :class:`~sleekxmpp.stanza.iq.Iq` stanza + to use the given query namespace. + + :param iq: Optionally use an existing stanza instead + of generating a new one. + :param xmlns: The query's namespace. + :param ito: The destination :class:`~sleekxmpp.xmlstream.jid.JID` + for this stanza. + :param ifrom: The ``'from'`` :class:`~sleekxmpp.xmlstream.jid.JID` + to use for this stanza. """ if not iq: iq = self.Iq() @@ -446,12 +440,10 @@ class BaseXMPP(XMLStream): return iq def make_query_roster(self, iq=None): - """ - Create a roster query element. + """Create a roster query element. - Arguments: - iq -- Optional Iq stanza to modify. A new stanza - is created otherwise. + :param iq: Optionally use an existing stanza instead + of generating a new one. """ if iq: iq['query'] = 'jabber:iq:roster' @@ -460,18 +452,19 @@ class BaseXMPP(XMLStream): def make_message(self, mto, mbody=None, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): """ - Create and initialize a new Message stanza. - - Arguments: - mto -- The recipient of the message. - mbody -- The main contents of the message. - msubject -- Optional subject for the message. - mtype -- The message's type, such as 'chat' or 'groupchat'. - mhtml -- Optional HTML body content. - mfrom -- The sender of the message. if sending from a client, - be aware that some servers require that the full JID - of the sender be used. - mnick -- Optional nickname of the sender. + Create and initialize a new + :class:`~sleekxmpp.stanza.message.Message` stanza. + + :param mto: The recipient of the message. + :param mbody: The main contents of the message. + :param msubject: Optional subject for the message. + :param mtype: The message's type, such as ``'chat'`` or + ``'groupchat'``. + :param mhtml: Optional HTML body content in the form of a string. + :param mfrom: The sender of the message. if sending from a client, + be aware that some servers require that the full JID + of the sender be used. + :param mnick: Optional nickname of the sender. """ message = self.Message(sto=mto, stype=mtype, sfrom=mfrom) message['body'] = mbody @@ -485,16 +478,16 @@ class BaseXMPP(XMLStream): def make_presence(self, pshow=None, pstatus=None, ppriority=None, pto=None, ptype=None, pfrom=None, pnick=None): """ - Create and initialize a new Presence stanza. - - Arguments: - pshow -- The presence's show value. - pstatus -- The presence's status message. - ppriority -- This connections' priority. - pto -- The recipient of a directed presence. - ptype -- The type of presence, such as 'subscribe'. - pfrom -- The sender of the presence. - pnick -- Optional nickname of the presence's sender. + Create and initialize a new + :class:`~sleekxmpp.stanza.presence.Presence` stanza. + + :param pshow: The presence's show value. + :param pstatus: The presence's status message. + :param ppriority: This connection's priority. + :param pto: The recipient of a directed presence. + :param ptype: The type of presence, such as ``'subscribe'``. + :param pfrom: The sender of the presence. + :param pnick: Optional nickname of the presence's sender. """ presence = self.Presence(stype=ptype, sfrom=pfrom, sto=pto) if pshow is not None: @@ -509,18 +502,19 @@ class BaseXMPP(XMLStream): def send_message(self, mto, mbody, msubject=None, mtype=None, mhtml=None, mfrom=None, mnick=None): """ - Create, initialize, and send a Message stanza. - - Arguments: - mto -- The recipient of the message. - mbody -- The main contents of the message. - msubject -- Optional subject for the message. - mtype -- The message's type, such as 'chat' or 'groupchat'. - mhtml -- Optional HTML body content. - mfrom -- The sender of the message. if sending from a client, - be aware that some servers require that the full JID - of the sender be used. - mnick -- Optional nickname of the sender. + Create, initialize, and send a new + :class:`~sleekxmpp.stanza.message.Message` stanza. + + :param mto: The recipient of the message. + :param mbody: The main contents of the message. + :param msubject: Optional subject for the message. + :param mtype: The message's type, such as ``'chat'`` or + ``'groupchat'``. + :param mhtml: Optional HTML body content in the form of a string. + :param mfrom: The sender of the message. if sending from a client, + be aware that some servers require that the full JID + of the sender be used. + :param mnick: Optional nickname of the sender. """ self.make_message(mto, mbody, msubject, mtype, mhtml, mfrom, mnick).send() @@ -528,16 +522,16 @@ class BaseXMPP(XMLStream): def send_presence(self, pshow=None, pstatus=None, ppriority=None, pto=None, pfrom=None, ptype=None, pnick=None): """ - Create, initialize, and send a Presence stanza. - - Arguments: - pshow -- The presence's show value. - pstatus -- The presence's status message. - ppriority -- This connections' priority. - pto -- The recipient of a directed presence. - ptype -- The type of presence, such as 'subscribe'. - pfrom -- The sender of the presence. - pnick -- Optional nickname of the presence's sender. + Create, initialize, and send a new + :class:`~sleekxmpp.stanza.presence.Presence` stanza. + + :param pshow: The presence's show value. + :param pstatus: The presence's status message. + :param ppriority: This connection's priority. + :param pto: The recipient of a directed presence. + :param ptype: The type of presence, such as ``'subscribe'``. + :param pfrom: The sender of the presence. + :param pnick: Optional nickname of the presence's sender. """ # Python2.6 chokes on Unicode strings for dict keys. args = {str('pto'): pto, @@ -555,13 +549,14 @@ class BaseXMPP(XMLStream): def send_presence_subscription(self, pto, pfrom=None, ptype='subscribe', pnick=None): """ - Create, initialize, and send a Presence stanza of type 'subscribe'. + Create, initialize, and send a new + :class:`~sleekxmpp.stanza.presence.Presence` stanza of + type ``'subscribe'``. - Arguments: - pto -- The recipient of a directed presence. - pfrom -- The sender of the presence. - ptype -- The type of presence. Defaults to 'subscribe'. - pnick -- Nickname of the presence's sender. + :param pto: The recipient of a directed presence. + :param pfrom: The sender of the presence. + :param ptype: The type of presence, such as ``'subscribe'``. + :param pnick: Optional nickname of the presence's sender. """ presence = self.makePresence(ptype=ptype, pfrom=pfrom, @@ -574,9 +569,7 @@ class BaseXMPP(XMLStream): @property def jid(self): - """ - Attribute accessor for bare jid - """ + """Attribute accessor for bare jid""" log.warning("jid property deprecated. Use boundjid.bare") return self.boundjid.bare @@ -587,9 +580,7 @@ class BaseXMPP(XMLStream): @property def fulljid(self): - """ - Attribute accessor for full jid - """ + """Attribute accessor for full jid""" log.warning("fulljid property deprecated. Use boundjid.full") return self.boundjid.full @@ -600,9 +591,7 @@ class BaseXMPP(XMLStream): @property def resource(self): - """ - Attribute accessor for jid resource - """ + """Attribute accessor for jid resource""" log.warning("resource property deprecated. Use boundjid.resource") return self.boundjid.resource @@ -613,9 +602,7 @@ class BaseXMPP(XMLStream): @property def username(self): - """ - Attribute accessor for jid usernode - """ + """Attribute accessor for jid usernode""" log.warning("username property deprecated. Use boundjid.user") return self.boundjid.user @@ -626,9 +613,7 @@ class BaseXMPP(XMLStream): @property def server(self): - """ - Attribute accessor for jid host - """ + """Attribute accessor for jid host""" log.warning("server property deprecated. Use boundjid.host") return self.boundjid.server @@ -639,47 +624,33 @@ class BaseXMPP(XMLStream): @property def auto_authorize(self): - """ - Auto accept or deny subscription requests. + """Auto accept or deny subscription requests. - If True, auto accept subscription requests. - If False, auto deny subscription requests. - If None, don't automatically respond. + If ``True``, auto accept subscription requests. + If ``False``, auto deny subscription requests. + If ``None``, don't automatically respond. """ return self.roster.auto_authorize @auto_authorize.setter def auto_authorize(self, value): - """ - Auto accept or deny subscription requests. - - If True, auto accept subscription requests. - If False, auto deny subscription requests. - If None, don't automatically respond. - """ self.roster.auto_authorize = value @property def auto_subscribe(self): - """ - Auto send requests for mutual subscriptions. + """Auto send requests for mutual subscriptions. - If True, auto send mutual subscription requests. + If ``True``, auto send mutual subscription requests. """ return self.roster.auto_subscribe @auto_subscribe.setter def auto_subscribe(self, value): - """ - Auto send requests for mutual subscriptions. - - If True, auto send mutual subscription requests. - """ self.roster.auto_subscribe = value def set_jid(self, jid): """Rip a JID apart and claim it as our own.""" - log.debug("setting jid to %s" % jid) + log.debug("setting jid to %s", jid) self.boundjid.full = jid def getjidresource(self, fulljid): @@ -713,17 +684,16 @@ class BaseXMPP(XMLStream): self.roster[pto][pfrom].handle_unavailable(presence) def _handle_new_subscription(self, stanza): - """ - Attempt to automatically handle subscription requests. + """Attempt to automatically handle subscription requests. Subscriptions will be approved if the request is from - a whitelisted JID, of self.auto_authorize is True. They - will be rejected if self.auto_authorize is False. Setting - self.auto_authorize to None will disable automatic + a whitelisted JID, of :attr:`auto_authorize` is True. They + will be rejected if :attr:`auto_authorize` is False. Setting + :attr:`auto_authorize` to ``None`` will disable automatic subscription handling (except for whitelisted JIDs). If a subscription is accepted, a request for a mutual - subscription will be sent if self.auto_subscribe is True. + subscription will be sent if :attr:`auto_subscribe` is ``True``. """ roster = self.roster[stanza['to'].bare] item = self.roster[stanza['to'].bare][stanza['from'].bare] @@ -762,8 +732,7 @@ class BaseXMPP(XMLStream): self.roster[pto][pfrom].handle_unsubscribed(presence) def _handle_presence(self, presence): - """ - Process incoming presence stanzas. + """Process incoming presence stanzas. Update the roster with presence information. """ @@ -779,23 +748,20 @@ class BaseXMPP(XMLStream): return def exception(self, exception): - """ - Process any uncaught exceptions, notably IqError and - IqTimeout exceptions. - - Overrides XMLStream.exception. + """Process any uncaught exceptions, notably + :class:`~sleekxmpp.exceptions.IqError` and + :class:`~sleekxmpp.exceptions.IqTimeout` exceptions. - Arguments: - exception -- An unhandled exception object. + :param exception: An unhandled :class:`Exception` object. """ if isinstance(exception, IqError): iq = exception.iq - log.error('%s: %s' % (iq['error']['condition'], - iq['error']['text'])) + log.error('%s: %s', iq['error']['condition'], + iq['error']['text']) log.warning('You should catch IqError exceptions') elif isinstance(exception, IqTimeout): iq = exception.iq - log.error('Request timed out: %s' % iq) + log.error('Request timed out: %s', iq) log.warning('You should catch IqTimeout exceptions') else: log.exception(exception) diff --git a/sleekxmpp/clientxmpp.py b/sleekxmpp/clientxmpp.py index fb3551f2..20012b5f 100644 --- a/sleekxmpp/clientxmpp.py +++ b/sleekxmpp/clientxmpp.py @@ -1,9 +1,15 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.clientxmpp + ~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + This module provides XMPP functionality that + is specific to client connections. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from __future__ import absolute_import, unicode_literals @@ -41,37 +47,30 @@ log = logging.getLogger(__name__) class ClientXMPP(BaseXMPP): """ - SleekXMPP's client class. ( Use only for good, not for evil.) - - Typical Use: - xmpp = ClientXMPP('user@server.tld/resource', 'password') - xmpp.process(block=False) // when block is True, it blocks the current - // thread. False by default. - - Attributes: - - Methods: - connect -- Overrides XMLStream.connect. - del_roster_item -- Delete a roster item. - get_roster -- Retrieve the roster from the server. - register_feature -- Register a stream feature. - update_roster -- Update a roster item. + SleekXMPP's client class. (Use only for good, not for evil.) + + Typical use pattern: + + .. code-block:: python + + xmpp = ClientXMPP('user@server.tld/resource', 'password') + # ... Register plugins and event handlers ... + xmpp.connect() + xmpp.process(block=False) # block=True will block the current + # thread. By default, block=False + + :param jid: The JID of the XMPP user account. + :param password: The password for the XMPP user account. + :param ssl: **Deprecated.** + :param plugin_config: A dictionary of plugin configurations. + :param plugin_whitelist: A list of approved plugins that + will be loaded when calling + :meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`. + :param escape_quotes: **Deprecated.** """ def __init__(self, jid, password, ssl=False, plugin_config={}, plugin_whitelist=[], escape_quotes=True, sasl_mech=None): - """ - Create a new SleekXMPP client. - - Arguments: - jid -- The JID of the XMPP user account. - password -- The password for the XMPP user account. - ssl -- Deprecated. - plugin_config -- A dictionary of plugin configurations. - plugin_whitelist -- A list of approved plugins that will be loaded - when calling register_plugins. - escape_quotes -- Deprecated. - """ BaseXMPP.__init__(self, jid, 'jabber:client') self.set_jid(jid) @@ -121,21 +120,19 @@ class ClientXMPP(BaseXMPP): def connect(self, address=tuple(), reattempt=True, use_tls=True, use_ssl=False): - """ - Connect to the XMPP server. + """Connect to the XMPP server. When no address is given, a SRV lookup for the server will be attempted. If that fails, the server user in the JID will be used. - Arguments: - address -- A tuple containing the server's host and port. - reattempt -- If True, reattempt the connection if an - error occurs. Defaults to True. - use_tls -- Indicates if TLS should be used for the - connection. Defaults to True. - use_ssl -- Indicates if the older SSL connection method - should be used. Defaults to False. + :param address -- A tuple containing the server's host and port. + :param reattempt: If ``True``, repeat attempting to connect if an + error occurs. Defaults to ``True``. + :param use_tls: Indicates if TLS should be used for the + connection. Defaults to ``True``. + :param use_ssl: Indicates if the older SSL connection method + should be used. Defaults to ``False``. """ self.session_started_event.clear() if not address: @@ -146,13 +143,10 @@ class ClientXMPP(BaseXMPP): reattempt=reattempt) def get_dns_records(self, domain, port=None): - """ - Get the DNS records for a domain. - Overriddes XMLStream.get_dns_records to use SRV. + """Get the DNS records for a domain, including SRV records. - Arguments: - domain -- The domain in question. - port -- If the results don't include a port, use this one. + :param domain: The domain in question. + :param port: If the results don't include a port, use this one. """ if port is None: port = self.default_port @@ -164,11 +158,11 @@ class ClientXMPP(BaseXMPP): address = (answer.target.to_text()[:-1], answer.port) answers.append((address, answer.priority, answer.weight)) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - log.warning("No SRV records for %s" % domain) + log.warning("No SRV records for %s", domain) answers = super(ClientXMPP, self).get_dns_records(domain, port) except dns.exception.Timeout: log.warning("DNS resolution timed out " + \ - "for SRV record of %s" % domain) + "for SRV record of %s", domain) answers = super(ClientXMPP, self).get_dns_records(domain, port) return answers else: @@ -177,17 +171,15 @@ class ClientXMPP(BaseXMPP): return [((domain, port), 0, 0)] def register_feature(self, name, handler, restart=False, order=5000): - """ - Register a stream feature. - - Arguments: - name -- The name of the stream feature. - handler -- The function to execute if the feature is received. - restart -- Indicates if feature processing should halt with - this feature. Defaults to False. - order -- The relative ordering in which the feature should - be negotiated. Lower values will be attempted - earlier when available. + """Register a stream feature handler. + + :param name: The name of the stream feature. + :param handler: The function to execute if the feature is received. + :param restart: Indicates if feature processing should halt with + this feature. Defaults to ``False``. + :param order: The relative ordering in which the feature should + be negotiated. Lower values will be attempted + earlier when available. """ self._stream_feature_handlers[name] = (handler, restart) self._stream_feature_order.append((order, name)) @@ -195,53 +187,51 @@ class ClientXMPP(BaseXMPP): def update_roster(self, jid, name=None, subscription=None, groups=[], block=True, timeout=None, callback=None): - """ - Add or change a roster item. - - Arguments: - jid -- The JID of the entry to modify. - name -- The user's nickname for this JID. - subscription -- The subscription status. May be one of - 'to', 'from', 'both', or 'none'. If set - to 'remove', the entry will be deleted. - groups -- The roster groups that contain this item. - block -- Specify if the roster request will block - until a response is received, or a timeout - occurs. Defaults to True. - timeout -- The length of time (in seconds) to wait - for a response before continuing if blocking - is used. Defaults to self.response_timeout. - callback -- Optional reference to a stream handler function. - Will be executed when the roster is received. - Implies block=False. + """Add or change a roster item. + + :param jid: The JID of the entry to modify. + :param name: The user's nickname for this JID. + :param subscription: The subscription status. May be one of + ``'to'``, ``'from'``, ``'both'``, or + ``'none'``. If set to ``'remove'``, + the entry will be deleted. + :param groups: The roster groups that contain this item. + :param block: Specify if the roster request will block + until a response is received, or a timeout + occurs. Defaults to ``True``. + :param timeout: The length of time (in seconds) to wait + for a response before continuing if blocking + is used. Defaults to + :attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`. + :param callback: Optional reference to a stream handler function. + Will be executed when the roster is received. + Implies ``block=False``. """ return self.client_roster.update(jid, name, subscription, groups, block, timeout, callback) def del_roster_item(self, jid): - """ - Remove an item from the roster by setting its subscription - status to 'remove'. + """Remove an item from the roster. + + This is done by setting its subscription status to ``'remove'``. - Arguments: - jid -- The JID of the item to remove. + :param jid: The JID of the item to remove. """ return self.client_roster.remove(jid) def get_roster(self, block=True, timeout=None, callback=None): - """ - Request the roster from the server. + """Request the roster from the server. - Arguments: - block -- Specify if the roster request will block until a - response is received, or a timeout occurs. - Defaults to True. - timeout -- The length of time (in seconds) to wait for a response + :param block: Specify if the roster request will block until a + response is received, or a timeout occurs. + Defaults to ``True``. + :param timeout: The length of time (in seconds) to wait for a response before continuing if blocking is used. - Defaults to self.response_timeout. - callback -- Optional reference to a stream handler function. Will - be executed when the roster is received. - Implies block=False. + Defaults to + :attr:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout`. + :param callback: Optional reference to a stream handler function. Will + be executed when the roster is received. + Implies ``block=False``. """ iq = self.Iq() iq['type'] = 'get' @@ -260,11 +250,9 @@ class ClientXMPP(BaseXMPP): self.features = set() def _handle_stream_features(self, features): - """ - Process the received stream features. + """Process the received stream features. - Arguments: - features -- The features stanza. + :param features: The features stanza. """ for order, name in self._stream_feature_order: if name in features['features']: @@ -275,13 +263,12 @@ class ClientXMPP(BaseXMPP): return True def _handle_roster(self, iq, request=False): - """ - Update the roster after receiving a roster stanza. + """Update the roster after receiving a roster stanza. - Arguments: - iq -- The roster stanza. - request -- Indicates if this stanza is a response - to a request for the roster. + :param iq: The roster stanza. + :param request: Indicates if this stanza is a response + to a request for the roster, and not an + empty acknowledgement from the server. """ if iq['type'] == 'set' or (iq['type'] == 'result' and request): for jid in iq['roster']['items']: diff --git a/sleekxmpp/componentxmpp.py b/sleekxmpp/componentxmpp.py index 6c15986d..5b16c5ef 100644 --- a/sleekxmpp/componentxmpp.py +++ b/sleekxmpp/componentxmpp.py @@ -1,9 +1,15 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.clientxmpp + ~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + This module provides XMPP functionality that + is specific to external server component connections. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from __future__ import absolute_import @@ -32,28 +38,22 @@ class ComponentXMPP(BaseXMPP): Use only for good, not for evil. - Methods: - connect -- Overrides XMLStream.connect. - incoming_filter -- Overrides XMLStream.incoming_filter. - start_stream_handler -- Overrides XMLStream.start_stream_handler. + :param jid: The JID of the component. + :param secret: The secret or password for the component. + :param host: The server accepting the component. + :param port: The port used to connect to the server. + :param plugin_config: A dictionary of plugin configurations. + :param plugin_whitelist: A list of approved plugins that + will be loaded when calling + :meth:`~sleekxmpp.basexmpp.BaseXMPP.register_plugins()`. + :param use_jc_ns: Indicates if the ``'jabber:client'`` namespace + should be used instead of the standard + ``'jabber:component:accept'`` namespace. + Defaults to ``False``. """ - def __init__(self, jid, secret, host, port, + def __init__(self, jid, secret, host=None, port=None, plugin_config={}, plugin_whitelist=[], use_jc_ns=False): - """ - Arguments: - jid -- The JID of the component. - secret -- The secret or password for the component. - host -- The server accepting the component. - port -- The port used to connect to the server. - plugin_config -- A dictionary of plugin configurations. - plugin_whitelist -- A list of desired plugins to load - when using register_plugins. - use_js_ns -- Indicates if the 'jabber:client' namespace - should be used instead of the standard - 'jabber:component:accept' namespace. - Defaults to False. - """ if use_jc_ns: default_ns = 'jabber:client' else: @@ -81,26 +81,42 @@ class ComponentXMPP(BaseXMPP): self.add_event_handler('presence_probe', self._handle_probe) - def connect(self): + def connect(self, host=None, port=None, use_ssl=False, + use_tls=True, reattempt=True): + """Connect to the server. + + Setting ``reattempt`` to ``True`` will cause connection attempts to + be made every second until a successful connection is established. + + :param host: The name of the desired server for the connection. + Defaults to :attr:`server_host`. + :param port: Port to connect to on the server. + Defauts to :attr:`server_port`. + :param use_ssl: Flag indicating if SSL should be used by connecting + directly to a port using SSL. + :param use_tls: Flag indicating if TLS should be used, allowing for + connecting to a port without using SSL immediately and + later upgrading the connection. + :param reattempt: Flag indicating if the socket should reconnect + after disconnections. """ - Connect to the server. - - Overrides XMLStream.connect. - """ - log.debug("Connecting to %s:%s" % (self.server_host, - self.server_port)) - return XMLStream.connect(self, self.server_host, - self.server_port) + if host is None: + host = self.server_host + if port is None: + port = self.server_port + log.debug("Connecting to %s:%s", host, port) + return XMLStream.connect(self, host=host, port=port, + use_ssl=use_ssl, + use_tls=use_tls, + reattempt=reattempt) def incoming_filter(self, xml): """ - Pre-process incoming XML stanzas by converting any 'jabber:client' - namespaced elements to the component's default namespace. - - Overrides XMLStream.incoming_filter. + Pre-process incoming XML stanzas by converting any + ``'jabber:client'`` namespaced elements to the component's + default namespace. - Arguments: - xml -- The XML stanza to pre-process. + :param xml: The XML stanza to pre-process. """ if xml.tag.startswith('{jabber:client}'): xml.tag = xml.tag.replace('jabber:client', self.default_ns) @@ -117,10 +133,7 @@ class ComponentXMPP(BaseXMPP): Once the streams are established, attempt to handshake with the server to be accepted as a component. - Overrides BaseXMPP.start_stream_handler. - - Arguments: - xml -- The incoming stream's root element. + :param xml: The incoming stream's root element. """ BaseXMPP.start_stream_handler(self, xml) @@ -136,11 +149,9 @@ class ComponentXMPP(BaseXMPP): self.send_xml(handshake, now=True) def _handle_handshake(self, xml): - """ - The handshake has been accepted. + """The handshake has been accepted. - Arguments: - xml -- The reply handshake stanza. + :param xml: The reply handshake stanza. """ self.session_started_event.set() self.event("session_start") diff --git a/sleekxmpp/exceptions.py b/sleekxmpp/exceptions.py index 61d24f6b..6bac1e40 100644 --- a/sleekxmpp/exceptions.py +++ b/sleekxmpp/exceptions.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.exceptions + ~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ @@ -13,37 +16,35 @@ class XMPPError(Exception): A generic exception that may be raised while processing an XMPP stanza to indicate that an error response stanza should be sent. - The exception method for stanza objects extending RootStanza will create - an error stanza and initialize any additional substanzas using the - extension information included in the exception. + The exception method for stanza objects extending + :class:`~sleekxmpp.stanza.rootstanza.RootStanza` will create an error + stanza and initialize any additional substanzas using the extension + information included in the exception. Meant for use in SleekXMPP plugins and applications using SleekXMPP. + + Extension information can be included to add additional XML elements + to the generated error stanza. + + :param condition: The XMPP defined error condition. + Defaults to ``'undefined-condition'``. + :param text: Human readable text describing the error. + :param etype: The XMPP error type, such as ``'cancel'`` or ``'modify'``. + Defaults to ``'cancel'``. + :param extension: Tag name of the extension's XML content. + :param extension_ns: XML namespace of the extensions' XML content. + :param extension_args: Content and attributes for the extension + element. Same as the additional arguments to + the :class:`~xml.etree.ElementTree.Element` + constructor. + :param clear: Indicates if the stanza's contents should be + removed before replying with an error. + Defaults to ``True``. """ def __init__(self, condition='undefined-condition', text=None, etype='cancel', extension=None, extension_ns=None, extension_args=None, clear=True): - """ - Create a new XMPPError exception. - - Extension information can be included to add additional XML elements - to the generated error stanza. - - Arguments: - condition -- The XMPP defined error condition. - Defaults to 'undefined-condition'. - text -- Human readable text describing the error. - etype -- The XMPP error type, such as cancel or modify. - Defaults to 'cancel'. - extension -- Tag name of the extension's XML content. - extension_ns -- XML namespace of the extensions' XML content. - extension_args -- Content and attributes for the extension - element. Same as the additional arguments to - the ET.Element constructor. - clear -- Indicates if the stanza's contents should be - removed before replying with an error. - Defaults to True. - """ if extension_args is None: extension_args = {} @@ -68,6 +69,8 @@ class IqTimeout(XMPPError): condition='remote-server-timeout', etype='cancel') + #: The :class:`~sleekxmpp.stanza.iq.Iq` stanza whose response + #: did not arrive before the timeout expired. self.iq = iq class IqError(XMPPError): @@ -83,4 +86,5 @@ class IqError(XMPPError): text=iq['error']['text'], etype=iq['error']['type']) + #: The :class:`~sleekxmpp.stanza.iq.Iq` error result stanza. self.iq = iq diff --git a/sleekxmpp/features/feature_bind/bind.py b/sleekxmpp/features/feature_bind/bind.py index de03192c..d3b2b737 100644 --- a/sleekxmpp/features/feature_bind/bind.py +++ b/sleekxmpp/features/feature_bind/bind.py @@ -42,7 +42,7 @@ class feature_bind(base_plugin): Arguments: features -- The stream features stanza. """ - log.debug("Requesting resource: %s" % self.xmpp.boundjid.resource) + log.debug("Requesting resource: %s", self.xmpp.boundjid.resource) iq = self.xmpp.Iq() iq['type'] = 'set' iq.enable('bind') @@ -55,7 +55,7 @@ class feature_bind(base_plugin): self.xmpp.features.add('bind') - log.info("Node set to: %s" % self.xmpp.boundjid.full) + log.info("Node set to: %s", self.xmpp.boundjid.full) if 'session' not in features['features']: log.debug("Established Session") diff --git a/sleekxmpp/features/feature_mechanisms/mechanisms.py b/sleekxmpp/features/feature_mechanisms/mechanisms.py index a6cff0a0..2b8321c2 100644 --- a/sleekxmpp/features/feature_mechanisms/mechanisms.py +++ b/sleekxmpp/features/feature_mechanisms/mechanisms.py @@ -123,7 +123,7 @@ class feature_mechanisms(base_plugin): def _handle_fail(self, stanza): """SASL authentication failed. Disconnect and shutdown.""" - log.info("Authentication failed: %s" % stanza['condition']) + log.info("Authentication failed: %s", stanza['condition']) self.xmpp.event("failed_auth", stanza, direct=True) self.xmpp.disconnect() return True diff --git a/sleekxmpp/features/feature_starttls/starttls.py b/sleekxmpp/features/feature_starttls/starttls.py index 639788a0..4e2b6621 100644 --- a/sleekxmpp/features/feature_starttls/starttls.py +++ b/sleekxmpp/features/feature_starttls/starttls.py @@ -58,8 +58,8 @@ class feature_starttls(base_plugin): self.xmpp.send(features['starttls'], now=True) return True else: - log.warning("The module tlslite is required to log in" +\ - " to some servers, and has not been found.") + log.warning("The module tlslite is required to log in" + \ + " to some servers, and has not been found.") return False def _handle_starttls_proceed(self, proceed): diff --git a/sleekxmpp/plugins/gmail_notify.py b/sleekxmpp/plugins/gmail_notify.py index 9a94a413..fc97a2ab 100644 --- a/sleekxmpp/plugins/gmail_notify.py +++ b/sleekxmpp/plugins/gmail_notify.py @@ -121,7 +121,7 @@ class gmail_notify(base.base_plugin): def handle_gmail(self, iq): mailbox = iq['mailbox'] approx = ' approximately' if mailbox['estimated'] else '' - log.info('Gmail: Received%s %s emails' % (approx, mailbox['total-matched'])) + log.info('Gmail: Received%s %s emails', approx, mailbox['total-matched']) self.last_result_time = mailbox['result-time'] self.xmpp.event('gmail_messages', iq) @@ -140,7 +140,7 @@ class gmail_notify(base.base_plugin): if query is None: log.info("Gmail: Checking for new emails") else: - log.info('Gmail: Searching for emails matching: "%s"' % query) + log.info('Gmail: Searching for emails matching: "%s"', query) iq = self.xmpp.Iq() iq['type'] = 'get' iq['to'] = self.xmpp.boundjid.bare diff --git a/sleekxmpp/plugins/jobs.py b/sleekxmpp/plugins/jobs.py index 0f1f7fb1..cb9deba8 100644 --- a/sleekxmpp/plugins/jobs.py +++ b/sleekxmpp/plugins/jobs.py @@ -43,7 +43,7 @@ class jobs(base.base_plugin): iq['psstate']['payload'] = state result = iq.send() if result is None or type(result) == bool or result['type'] != 'result': - log.error("Unable to change %s:%s to %s" % (node, jobid, state)) + log.error("Unable to change %s:%s to %s", node, jobid, state) return False return True diff --git a/sleekxmpp/plugins/xep_0004/stanza/form.py b/sleekxmpp/plugins/xep_0004/stanza/form.py index 993f7b12..bbf0ee7d 100644 --- a/sleekxmpp/plugins/xep_0004/stanza/form.py +++ b/sleekxmpp/plugins/xep_0004/stanza/form.py @@ -96,11 +96,11 @@ class Form(ElementBase): self.xml.append(itemXML) reported_vars = self['reported'].keys() for var in reported_vars: - fieldXML = ET.Element('{%s}field' % FormField.namespace) - itemXML.append(fieldXML) - field = FormField(xml=fieldXML) + field = FormField() + field._type = self['reported'][var]['type'] field['var'] = var field['value'] = values.get(var, None) + itemXML.append(field.xml) def add_reported(self, var, ftype=None, label='', desc='', **kwargs): kwtype = kwargs.get('type', None) @@ -159,7 +159,7 @@ class Form(ElementBase): items = [] itemsXML = self.xml.findall('{%s}item' % self.namespace) for itemXML in itemsXML: - item = {} + item = OrderedDict() fieldsXML = itemXML.findall('{%s}field' % FormField.namespace) for fieldXML in fieldsXML: field = FormField(xml=fieldXML) @@ -168,7 +168,7 @@ class Form(ElementBase): return items def get_reported(self): - fields = {} + fields = OrderedDict() xml = self.xml.findall('{%s}reported/{%s}field' % (self.namespace, FormField.namespace)) for field in xml: @@ -177,7 +177,7 @@ class Form(ElementBase): return fields def get_values(self): - values = {} + values = OrderedDict() fields = self['fields'] for var in fields: values[var] = fields[var]['value'] diff --git a/sleekxmpp/plugins/xep_0009/binding.py b/sleekxmpp/plugins/xep_0009/binding.py index ef34b580..b4395707 100644 --- a/sleekxmpp/plugins/xep_0009/binding.py +++ b/sleekxmpp/plugins/xep_0009/binding.py @@ -42,46 +42,46 @@ def py2xml(*args): def _py2xml(*args): for x in args: - val = ET.Element("value") + val = ET.Element("{%s}value" % _namespace) if x is None: - nil = ET.Element("nil") + nil = ET.Element("{%s}nil" % _namespace) val.append(nil) elif type(x) is int: - i4 = ET.Element("i4") + i4 = ET.Element("{%s}i4" % _namespace) i4.text = str(x) val.append(i4) elif type(x) is bool: - boolean = ET.Element("boolean") + boolean = ET.Element("{%s}boolean" % _namespace) boolean.text = str(int(x)) val.append(boolean) elif type(x) is str: - string = ET.Element("string") + string = ET.Element("{%s}string" % _namespace) string.text = x val.append(string) elif type(x) is float: - double = ET.Element("double") + double = ET.Element("{%s}double" % _namespace) double.text = str(x) val.append(double) elif type(x) is rpcbase64: - b64 = ET.Element("base64") + b64 = ET.Element("{%s}base64" % _namespace) b64.text = x.encoded() val.append(b64) elif type(x) is rpctime: - iso = ET.Element("dateTime.iso8601") + iso = ET.Element("{%s}dateTime.iso8601" % _namespace) iso.text = str(x) val.append(iso) elif type(x) in (list, tuple): - array = ET.Element("array") - data = ET.Element("data") + array = ET.Element("{%s}array" % _namespace) + data = ET.Element("{%s}data" % _namespace) for y in x: data.append(_py2xml(y)) array.append(data) val.append(array) elif type(x) is dict: - struct = ET.Element("struct") + struct = ET.Element("{%s}struct" % _namespace) for y in x.keys(): - member = ET.Element("member") - name = ET.Element("name") + member = ET.Element("{%s}member" % _namespace) + name = ET.Element("{%s}name" % _namespace) name.text = y member.append(name) member.append(_py2xml(x[y])) @@ -105,18 +105,18 @@ def _xml2py(value): if value.find('{%s}int' % namespace) is not None: return int(value.find('{%s}int' % namespace).text) if value.find('{%s}boolean' % namespace) is not None: - return bool(value.find('{%s}boolean' % namespace).text) + return bool(int(value.find('{%s}boolean' % namespace).text)) if value.find('{%s}string' % namespace) is not None: return value.find('{%s}string' % namespace).text if value.find('{%s}double' % namespace) is not None: return float(value.find('{%s}double' % namespace).text) - if value.find('{%s}base64') is not None: - return rpcbase64(value.find('base64' % namespace).text) - if value.find('{%s}Base64') is not None: + if value.find('{%s}base64' % namespace) is not None: + return rpcbase64(value.find('{%s}base64' % namespace).text.encode()) + if value.find('{%s}Base64' % namespace) is not None: # Older versions of XEP-0009 used Base64 - return rpcbase64(value.find('Base64' % namespace).text) - if value.find('{%s}dateTime.iso8601') is not None: - return rpctime(value.find('{%s}dateTime.iso8601')) + return rpcbase64(value.find('{%s}Base64' % namespace).text.encode()) + if value.find('{%s}dateTime.iso8601' % namespace) is not None: + return rpctime(value.find('{%s}dateTime.iso8601' % namespace).text) if value.find('{%s}struct' % namespace) is not None: struct = {} for member in value.find('{%s}struct' % namespace).findall('{%s}member' % namespace): @@ -138,13 +138,13 @@ class rpcbase64(object): self.data = data def decode(self): - return base64.decodestring(self.data) + return base64.b64decode(self.data) def __str__(self): - return self.decode() + return self.decode().decode() def encoded(self): - return self.data + return self.data.decode() diff --git a/sleekxmpp/plugins/xep_0009/remote.py b/sleekxmpp/plugins/xep_0009/remote.py index 3cc0f520..8c08e8f3 100644 --- a/sleekxmpp/plugins/xep_0009/remote.py +++ b/sleekxmpp/plugins/xep_0009/remote.py @@ -20,7 +20,7 @@ log = logging.getLogger(__name__) def _intercept(method, name, public): def _resolver(instance, *args, **kwargs): - log.debug("Locally calling %s.%s with arguments %s." % (instance.FQN(), method.__name__, args)) + log.debug("Locally calling %s.%s with arguments %s.", instance.FQN(), method.__name__, args) try: value = method(instance, *args, **kwargs) if value == NotImplemented: @@ -113,6 +113,9 @@ class ACL: def check(cls, rules, jid, resource): if rules is None: return cls.DENY # No rules means no access! + jid = str(jid) # Check the string representation of the JID. + if not jid: + return cls.DENY # Can't check an empty JID. for rule in rules: policy = cls._check(rule, jid, resource) if policy is not None: @@ -381,7 +384,7 @@ class Proxy(Endpoint): try: if attribute._rpc: def _remote_call(*args, **kwargs): - log.debug("Remotely calling '%s.%s' with arguments %s." % (self._endpoint.FQN(), attribute._rpc_name, args)) + log.debug("Remotely calling '%s.%s' with arguments %s.", self._endpoint.FQN(), attribute._rpc_name, args) return self._endpoint.session._call_remote(self._endpoint.target_jid, "%s.%s" % (self._endpoint.FQN(), attribute._rpc_name), self._callback, *args, **kwargs) return _remote_call except: @@ -449,7 +452,7 @@ class RemoteSession(object): self._event.wait() def _notify(self, event): - log.debug("RPC Session as %s started." % self._client.boundjid.full) + log.debug("RPC Session as %s started.", self._client.boundjid.full) self._client.sendPresence() self._event.set() pass @@ -461,7 +464,7 @@ class RemoteSession(object): if name is None: name = method.__name__ key = "%s.%s" % (endpoint, name) - log.debug("Registering call handler for %s (%s)." % (key, method)) + log.debug("Registering call handler for %s (%s).", key, method) with self._lock: if key in self._entries: raise KeyError("A handler for %s has already been regisered!" % endpoint) @@ -469,7 +472,7 @@ class RemoteSession(object): return key def _register_acl(self, endpoint, acl): - log.debug("Registering ACL %s for endpoint %s." % (repr(acl), endpoint)) + log.debug("Registering ACL %s for endpoint %s.", repr(acl), endpoint) with self._lock: self._acls[endpoint] = acl @@ -562,7 +565,7 @@ class RemoteSession(object): iq.send() return future.get_value(30) else: - log.debug("[RemoteSession] _call_remote %s" % callback) + log.debug("[RemoteSession] _call_remote %s", callback) self._register_callback(pid, callback) iq.send() @@ -601,11 +604,11 @@ class RemoteSession(object): error.send() except Exception as e: if isinstance(e, KeyError): - log.error("No handler available for %s!" % pmethod) + log.error("No handler available for %s!", pmethod) error = self._client.plugin['xep_0009']._item_not_found(iq) else: traceback.print_exc(file=sys.stderr) - log.error("An unexpected problem occurred invoking method %s!" % pmethod) + log.error("An unexpected problem occurred invoking method %s!", pmethod) error = self._client.plugin['xep_0009']._undefined_condition(iq) #! print "[REMOTE.PY] _handle_remote_procedure_call AN ERROR SHOULD BE SENT NOW %s " % e error.send() diff --git a/sleekxmpp/plugins/xep_0009/rpc.py b/sleekxmpp/plugins/xep_0009/rpc.py index fc306d31..4f749f30 100644 --- a/sleekxmpp/plugins/xep_0009/rpc.py +++ b/sleekxmpp/plugins/xep_0009/rpc.py @@ -128,22 +128,22 @@ class xep_0009(base.base_plugin): def _handle_method_call(self, iq):
type = iq['type']
if type == 'set':
- log.debug("Incoming Jabber-RPC call from %s" % iq['from'])
+ log.debug("Incoming Jabber-RPC call from %s", iq['from'])
self.xmpp.event('jabber_rpc_method_call', iq)
else:
if type == 'error' and ['rpc_query'] is None:
self.handle_error(iq)
else:
- log.debug("Incoming Jabber-RPC error from %s" % iq['from'])
+ log.debug("Incoming Jabber-RPC error from %s", iq['from'])
self.xmpp.event('jabber_rpc_error', iq)
def _handle_method_response(self, iq):
if iq['rpc_query']['method_response']['fault'] is not None:
- log.debug("Incoming Jabber-RPC fault from %s" % iq['from'])
+ log.debug("Incoming Jabber-RPC fault from %s", iq['from'])
#self._on_jabber_rpc_method_fault(iq)
self.xmpp.event('jabber_rpc_method_fault', iq)
else:
- log.debug("Incoming Jabber-RPC response from %s" % iq['from'])
+ log.debug("Incoming Jabber-RPC response from %s", iq['from'])
self.xmpp.event('jabber_rpc_method_response', iq)
def _handle_error(self, iq):
diff --git a/sleekxmpp/plugins/xep_0012.py b/sleekxmpp/plugins/xep_0012.py index 8fe818b8..c5532bd4 100644 --- a/sleekxmpp/plugins/xep_0012.py +++ b/sleekxmpp/plugins/xep_0012.py @@ -71,10 +71,10 @@ class xep_0012(base.base_plugin): def handle_last_activity_query(self, iq):
if iq['type'] == 'get':
- log.debug("Last activity requested by %s" % iq['from'])
+ log.debug("Last activity requested by %s", iq['from'])
self.xmpp.event('last_activity_request', iq)
elif iq['type'] == 'result':
- log.debug("Last activity result from %s" % iq['from'])
+ log.debug("Last activity result from %s", iq['from'])
self.xmpp.event('last_activity', iq)
def handle_last_activity(self, iq):
diff --git a/sleekxmpp/plugins/xep_0030/disco.py b/sleekxmpp/plugins/xep_0030/disco.py index 83d7a9c0..53086d4e 100644 --- a/sleekxmpp/plugins/xep_0030/disco.py +++ b/sleekxmpp/plugins/xep_0030/disco.py @@ -268,7 +268,7 @@ class xep_0030(base_plugin): """ if local or jid is None: log.debug("Looking up local disco#info data " + \ - "for %s, node %s." % (jid, node)) + "for %s, node %s.", jid, node) info = self._run_node_handler('get_info', jid, node, kwargs) return self._fix_default_info(info) @@ -542,7 +542,7 @@ class xep_0030(base_plugin): """ if iq['type'] == 'get': log.debug("Received disco info query from " + \ - "<%s> to <%s>." % (iq['from'], iq['to'])) + "<%s> to <%s>.", iq['from'], iq['to']) if self.xmpp.is_component: jid = iq['to'].full else: @@ -551,14 +551,17 @@ class xep_0030(base_plugin): jid, iq['disco_info']['node'], iq) - iq.reply() - if info: - info = self._fix_default_info(info) - iq.set_payload(info.xml) - iq.send() + if isinstance(info, Iq): + info.send() + else: + iq.reply() + if info: + info = self._fix_default_info(info) + iq.set_payload(info.xml) + iq.send() elif iq['type'] == 'result': log.debug("Received disco info result from" + \ - "%s to %s." % (iq['from'], iq['to'])) + "%s to %s.", iq['from'], iq['to']) self.xmpp.event('disco_info', iq) def _handle_disco_items(self, iq): @@ -572,21 +575,25 @@ class xep_0030(base_plugin): """ if iq['type'] == 'get': log.debug("Received disco items query from " + \ - "<%s> to <%s>." % (iq['from'], iq['to'])) + "<%s> to <%s>.", iq['from'], iq['to']) if self.xmpp.is_component: jid = iq['to'].full else: jid = iq['to'].bare items = self._run_node_handler('get_items', jid, - iq['disco_items']['node']) - iq.reply() - if items: - iq.set_payload(items.xml) - iq.send() + iq['disco_items']['node'], + iq) + if isinstance(items, Iq): + items.send() + else: + iq.reply() + if items: + iq.set_payload(items.xml) + iq.send() elif iq['type'] == 'result': log.debug("Received disco items result from" + \ - "%s to %s." % (iq['from'], iq['to'])) + "%s to %s.", iq['from'], iq['to']) self.xmpp.event('disco_items', iq) def _fix_default_info(self, info): diff --git a/sleekxmpp/plugins/xep_0045.py b/sleekxmpp/plugins/xep_0045.py index 45f16110..ab3f750a 100644 --- a/sleekxmpp/plugins/xep_0045.py +++ b/sleekxmpp/plugins/xep_0045.py @@ -127,7 +127,7 @@ class xep_0045(base.base_plugin): def handle_groupchat_invite(self, inv): """ Handle an invite into a muc. """ - logging.debug("MUC invite to %s from %s: %s" % (inv['from'], inv["from"], inv)) + logging.debug("MUC invite to %s from %s: %s", inv['from'], inv["from"], inv) if inv['from'] not in self.rooms.keys(): self.xmpp.event("groupchat_invite", inv) @@ -149,7 +149,7 @@ class xep_0045(base.base_plugin): if entry['nick'] not in self.rooms[entry['room']]: got_online = True self.rooms[entry['room']][entry['nick']] = entry - log.debug("MUC presence from %s/%s : %s" % (entry['room'],entry['nick'], entry)) + log.debug("MUC presence from %s/%s : %s", entry['room'],entry['nick'], entry) self.xmpp.event("groupchat_presence", pr) self.xmpp.event("muc::%s::presence" % entry['room'], pr) if got_offline: diff --git a/sleekxmpp/plugins/xep_0050/adhoc.py b/sleekxmpp/plugins/xep_0050/adhoc.py index 5095f874..7dbef31c 100644 --- a/sleekxmpp/plugins/xep_0050/adhoc.py +++ b/sleekxmpp/plugins/xep_0050/adhoc.py @@ -17,6 +17,7 @@ from sleekxmpp.xmlstream import register_stanza_plugin, JID from sleekxmpp.plugins.base import base_plugin from sleekxmpp.plugins.xep_0050 import stanza from sleekxmpp.plugins.xep_0050 import Command +from sleekxmpp.plugins.xep_0004 import Form log = logging.getLogger(__name__) @@ -92,7 +93,8 @@ class xep_0050(base_plugin): StanzaPath('iq@type=set/command'), self._handle_command)) - register_stanza_plugin(Iq, stanza.Command) + register_stanza_plugin(Iq, Command) + register_stanza_plugin(Command, Form) self.xmpp.add_event_handler('command_execute', self._handle_command_start, @@ -211,8 +213,7 @@ class xep_0050(base_plugin): key = (iq['to'].full, node) name, handler = self.commands.get(key, ('Not found', None)) if not handler: - log.debug('Command not found: %s, %s' % (key, self.commands)) - + log.debug('Command not found: %s, %s', key, self.commands) initial_session = {'id': sessionid, 'from': iq['from'], 'to': iq['to'], diff --git a/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py b/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py index 46374a35..aeaeefe0 100644 --- a/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py +++ b/sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py @@ -22,7 +22,7 @@ class PubsubErrorCondition(ElementBase): 'max-items-exceeded', 'max-nodes-exceeded', 'nodeid-required', 'not-in-roster-group', 'not-subscribed', 'payload-too-big', - 'payload-required' 'pending-subscription', + 'payload-required', 'pending-subscription', 'presence-subscription-required', 'subid-required', 'too-many-subscriptions', 'unsupported')) condition_ns = 'http://jabber.org/protocol/pubsub#errors' diff --git a/sleekxmpp/plugins/xep_0078/legacyauth.py b/sleekxmpp/plugins/xep_0078/legacyauth.py index edb8f314..dec775a3 100644 --- a/sleekxmpp/plugins/xep_0078/legacyauth.py +++ b/sleekxmpp/plugins/xep_0078/legacyauth.py @@ -60,12 +60,12 @@ class xep_0078(base_plugin): try: resp = iq.send(now=True) except IqError: - log.info("Authentication failed: %s" % resp['error']['condition']) + log.info("Authentication failed: %s", resp['error']['condition']) self.xmpp.event('failed_auth', direct=True) self.xmpp.disconnect() return True except IqTimeout: - log.info("Authentication failed: %s" % 'timeout') + log.info("Authentication failed: %s", 'timeout') self.xmpp.event('failed_auth', direct=True) self.xmpp.disconnect() return True diff --git a/sleekxmpp/plugins/xep_0082.py b/sleekxmpp/plugins/xep_0082.py index d3c4cc56..25c80fd0 100644 --- a/sleekxmpp/plugins/xep_0082.py +++ b/sleekxmpp/plugins/xep_0082.py @@ -76,7 +76,7 @@ def format_datetime(time_obj): return '%sZ' % timestamp return timestamp -def date(year=None, month=None, day=None): +def date(year=None, month=None, day=None, obj=False): """ Create a date only timestamp for the given instant. @@ -86,17 +86,22 @@ def date(year=None, month=None, day=None): year -- Integer value of the year (4 digits) month -- Integer value of the month day -- Integer value of the day of the month. + obj -- If True, return the date object instead + of a formatted string. Defaults to False. """ - today = dt.datetime.today() + today = dt.datetime.utcnow() if year is None: year = today.year if month is None: month = today.month if day is None: day = today.day - return format_date(dt.date(year, month, day)) + value = dt.date(year, month, day) + if obj: + return value + return format_date(value) -def time(hour=None, min=None, sec=None, micro=None, offset=None): +def time(hour=None, min=None, sec=None, micro=None, offset=None, obj=False): """ Create a time only timestamp for the given instant. @@ -110,6 +115,8 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None): offset -- Either a positive or negative number of seconds to offset from UTC to match a desired timezone, or a tzinfo object. + obj -- If True, return the time object instead + of a formatted string. Defaults to False. """ now = dt.datetime.utcnow() if hour is None: @@ -124,12 +131,14 @@ def time(hour=None, min=None, sec=None, micro=None, offset=None): offset = tzutc() elif not isinstance(offset, dt.tzinfo): offset = tzoffset(None, offset) - time = dt.time(hour, min, sec, micro, offset) - return format_time(time) + value = dt.time(hour, min, sec, micro, offset) + if obj: + return value + return format_time(value) def datetime(year=None, month=None, day=None, hour=None, min=None, sec=None, micro=None, offset=None, - separators=True): + separators=True, obj=False): """ Create a datetime timestamp for the given instant. @@ -146,6 +155,8 @@ def datetime(year=None, month=None, day=None, hour=None, offset -- Either a positive or negative number of seconds to offset from UTC to match a desired timezone, or a tzinfo object. + obj -- If True, return the datetime object instead + of a formatted string. Defaults to False. """ now = dt.datetime.utcnow() if year is None: @@ -167,9 +178,11 @@ def datetime(year=None, month=None, day=None, hour=None, elif not isinstance(offset, dt.tzinfo): offset = tzoffset(None, offset) - date = dt.datetime(year, month, day, hour, + value = dt.datetime(year, month, day, hour, min, sec, micro, offset) - return format_datetime(date) + if obj: + return value + return format_datetime(value) class xep_0082(base_plugin): diff --git a/sleekxmpp/plugins/xep_0085/chat_states.py b/sleekxmpp/plugins/xep_0085/chat_states.py index 4fb21ba0..e95434d2 100644 --- a/sleekxmpp/plugins/xep_0085/chat_states.py +++ b/sleekxmpp/plugins/xep_0085/chat_states.py @@ -45,5 +45,5 @@ class xep_0085(base_plugin): def _handle_chat_state(self, msg): state = msg['chat_state'] - log.debug("Chat State: %s, %s" % (state, msg['from'].jid)) + log.debug("Chat State: %s, %s", state, msg['from'].jid) self.xmpp.event('chatstate_%s' % state, msg) diff --git a/sleekxmpp/plugins/xep_0199/ping.py b/sleekxmpp/plugins/xep_0199/ping.py index de7f5688..a0f60532 100644 --- a/sleekxmpp/plugins/xep_0199/ping.py +++ b/sleekxmpp/plugins/xep_0199/ping.py @@ -118,7 +118,7 @@ class xep_0199(base_plugin): Arguments: iq -- The ping request. """ - log.debug("Pinged by %s" % iq['from']) + log.debug("Pinged by %s", iq['from']) iq.reply().send() def send_ping(self, jid, timeout=None, errorfalse=False, @@ -141,7 +141,7 @@ class xep_0199(base_plugin): is received. Useful in conjunction with the option block=False. """ - log.debug("Pinging %s" % jid) + log.debug("Pinging %s", jid) if timeout is None: timeout = self.timeout @@ -167,7 +167,7 @@ class xep_0199(base_plugin): if not block: return None - log.debug("Pong: %s %f" % (jid, delay)) + log.debug("Pong: %s %f", jid, delay) return delay diff --git a/sleekxmpp/plugins/xep_0224/attention.py b/sleekxmpp/plugins/xep_0224/attention.py index 41d7a0f1..4a3ff368 100644 --- a/sleekxmpp/plugins/xep_0224/attention.py +++ b/sleekxmpp/plugins/xep_0224/attention.py @@ -68,5 +68,5 @@ class xep_0224(base_plugin): Arguments: msg -- A message stanza with an attention element. """ - log.debug("Received attention request from: %s" % msg['from']) + log.debug("Received attention request from: %s", msg['from']) self.xmpp.event('attention', msg) diff --git a/sleekxmpp/roster/item.py b/sleekxmpp/roster/item.py index 4f4663d3..6f956b31 100644 --- a/sleekxmpp/roster/item.py +++ b/sleekxmpp/roster/item.py @@ -172,6 +172,7 @@ class RosterItem(object): Save the item's state information to an external datastore, if one has been provided. """ + self['subscription'] = self._subscription() if self.db: self.db.save(self.owner, self.jid, self._state, self._db_state) diff --git a/sleekxmpp/stanza/rootstanza.py b/sleekxmpp/stanza/rootstanza.py index 470a1225..2ac47d8b 100644 --- a/sleekxmpp/stanza/rootstanza.py +++ b/sleekxmpp/stanza/rootstanza.py @@ -80,8 +80,7 @@ class RootStanza(StanzaBase): self['error']['type'] = 'cancel' self.send() # log the error - log.exception('Error handling {%s}%s stanza' % - (self.namespace, self.name)) + log.exception('Error handling {%s}%s stanza' , self.namespace, self.name) # Finally raise the exception to a global exception handler self.stream.exception(e) diff --git a/sleekxmpp/version.py b/sleekxmpp/version.py index 610614e9..24bee091 100644 --- a/sleekxmpp/version.py +++ b/sleekxmpp/version.py @@ -9,5 +9,5 @@ # We don't want to have to import the entire library # just to get the version info for setup.py -__version__ = '1.0rc3' -__version_info__ = (1, 0, 0, 'rc3', 0) +__version__ = '1.0rc4' +__version_info__ = (1, 0, 0, 'rc4', 0) diff --git a/sleekxmpp/xmlstream/filesocket.py b/sleekxmpp/xmlstream/filesocket.py index fd81864b..56554c73 100644 --- a/sleekxmpp/xmlstream/filesocket.py +++ b/sleekxmpp/xmlstream/filesocket.py @@ -1,9 +1,15 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.filesocket + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + This module is a shim for correcting deficiencies in the file + socket implementation of Python2.6. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from socket import _fileobject @@ -12,12 +18,11 @@ import socket class FileSocket(_fileobject): - """ - Create a file object wrapper for a socket to work around + """Create a file object wrapper for a socket to work around issues present in Python 2.6 when using sockets as file objects. - The parser for xml.etree.cElementTree requires a file, but we will - be reading from the XMPP connection socket instead. + The parser for :class:`~xml.etree.cElementTree` requires a file, but + we will be reading from the XMPP connection socket instead. """ def read(self, size=4096): @@ -31,8 +36,7 @@ class FileSocket(_fileobject): class Socket26(socket._socketobject): - """ - A custom socket implementation that uses our own FileSocket class + """A custom socket implementation that uses our own FileSocket class to work around issues in Python 2.6 when using sockets as files. """ diff --git a/sleekxmpp/xmlstream/handler/base.py b/sleekxmpp/xmlstream/handler/base.py index 7f05c757..59dcb306 100644 --- a/sleekxmpp/xmlstream/handler/base.py +++ b/sleekxmpp/xmlstream/handler/base.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.handler.base + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ import weakref @@ -16,78 +19,62 @@ class BaseHandler(object): incoming stanzas so that the stanza may be processed in some way. Stanzas may be matched with multiple handlers. - Handler execution may take place in two phases. The first is during - the stream processing itself. The second is after stream processing - and during SleekXMPP's main event loop. The prerun method is used - for execution during stream processing, and the run method is used - during the main event loop. - - Attributes: - name -- The name of the handler. - stream -- The stream this handler is assigned to. - - Methods: - match -- Compare a stanza with the handler's matcher. - prerun -- Handler execution during stream processing. - run -- Handler execution during the main event loop. - check_delete -- Indicate if the handler may be removed from use. + Handler execution may take place in two phases: during the incoming + stream processing, and in the main event loop. The :meth:`prerun()` + method is executed in the first case, and :meth:`run()` is called + during the second. + + :param string name: The name of the handler. + :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase` + derived object that will be used to determine if a + stanza should be accepted by this handler. + :param stream: The :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` + instance that the handle will respond to. """ def __init__(self, name, matcher, stream=None): - """ - Create a new stream handler. - - Arguments: - name -- The name of the handler. - matcher -- A matcher object from xmlstream.matcher that will be - used to determine if a stanza should be accepted by - this handler. - stream -- The XMLStream instance the handler should monitor. - """ + #: The name of the handler self.name = name + + #: The XML stream this handler is assigned to + self.stream = None if stream is not None: self.stream = weakref.ref(stream) - else: - self.stream = None + stream.register_handler(self) + self._destroy = False self._payload = None self._matcher = matcher - if stream is not None: - stream.registerHandler(self) def match(self, xml): - """ - Compare a stanza or XML object with the handler's matcher. + """Compare a stanza or XML object with the handler's matcher. - Arguments - xml -- An XML or stanza object. + :param xml: An XML or + :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object """ return self._matcher.match(xml) def prerun(self, payload): - """ - Prepare the handler for execution while the XML stream is being - processed. + """Prepare the handler for execution while the XML + stream is being processed. - Arguments: - payload -- A stanza object. + :param payload: A :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + object. """ self._payload = payload def run(self, payload): - """ - Execute the handler after XML stream processing and during the + """Execute the handler after XML stream processing and during the main event loop. - Arguments: - payload -- A stanza object. + :param payload: A :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + object. """ self._payload = payload def check_delete(self): - """ - Check if the handler should be removed from the list of stream - handlers. + """Check if the handler should be removed from the list + of stream handlers. """ return self._destroy diff --git a/sleekxmpp/xmlstream/handler/callback.py b/sleekxmpp/xmlstream/handler/callback.py index 7fadab43..37f53335 100644 --- a/sleekxmpp/xmlstream/handler/callback.py +++ b/sleekxmpp/xmlstream/handler/callback.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.handler.callback + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from sleekxmpp.xmlstream.handler.base import BaseHandler @@ -18,48 +21,42 @@ class Callback(BaseHandler): The handler may execute the callback either during stream processing or during the main event loop. - Callback functions are all executed in the same thread, so be - aware if you are executing functions that will block for extended - periods of time. Typically, you should signal your own events using the - SleekXMPP object's event() method to pass the stanza off to a threaded - event handler for further processing. - - Methods: - prerun -- Overrides BaseHandler.prerun - run -- Overrides BaseHandler.run + Callback functions are all executed in the same thread, so be aware if + you are executing functions that will block for extended periods of + time. Typically, you should signal your own events using the SleekXMPP + object's :meth:`~sleekxmpp.xmlstream.xmlstream.XMLStream.event()` + method to pass the stanza off to a threaded event handler for further + processing. + + + :param string name: The name of the handler. + :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase` + derived object for matching stanza objects. + :param pointer: The function to execute during callback. + :param bool thread: **DEPRECATED.** Remains only for + backwards compatibility. + :param bool once: Indicates if the handler should be used only + once. Defaults to False. + :param bool instream: Indicates if the callback should be executed + during stream processing instead of in the + main event loop. + :param stream: The :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` + instance this handler should monitor. """ def __init__(self, name, matcher, pointer, thread=False, once=False, instream=False, stream=None): - """ - Create a new callback handler. - - Arguments: - name -- The name of the handler. - matcher -- A matcher object for matching stanza objects. - pointer -- The function to execute during callback. - thread -- DEPRECATED. Remains only for backwards compatibility. - once -- Indicates if the handler should be used only - once. Defaults to False. - instream -- Indicates if the callback should be executed - during stream processing instead of in the - main event loop. - stream -- The XMLStream instance this handler should monitor. - """ BaseHandler.__init__(self, name, matcher, stream) self._pointer = pointer self._once = once self._instream = instream def prerun(self, payload): - """ - Execute the callback during stream processing, if - the callback was created with instream=True. - - Overrides BaseHandler.prerun + """Execute the callback during stream processing, if + the callback was created with ``instream=True``. - Arguments: - payload -- The matched stanza object. + :param payload: The matched + :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object. """ if self._once: self._destroy = True @@ -67,16 +64,13 @@ class Callback(BaseHandler): self.run(payload, True) def run(self, payload, instream=False): - """ - Execute the callback function with the matched stanza payload. - - Overrides BaseHandler.run + """Execute the callback function with the matched stanza payload. - Arguments: - payload -- The matched stanza object. - instream -- Force the handler to execute during - stream processing. Used only by prerun. - Defaults to False. + :param payload: The matched + :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object. + :param bool instream: Force the handler to execute during stream + processing. This should only be used by + :meth:`prerun()`. Defaults to ``False``. """ if not self._instream or instream: self._pointer(payload) diff --git a/sleekxmpp/xmlstream/handler/waiter.py b/sleekxmpp/xmlstream/handler/waiter.py index 25dc161c..01ff5d67 100644 --- a/sleekxmpp/xmlstream/handler/waiter.py +++ b/sleekxmpp/xmlstream/handler/waiter.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.handler.waiter + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ import logging @@ -22,83 +25,63 @@ log = logging.getLogger(__name__) class Waiter(BaseHandler): """ - The Waiter handler allows an event handler to block - until a particular stanza has been received. The handler - will either be given the matched stanza, or False if the - waiter has timed out. - - Methods: - check_delete -- Overrides BaseHandler.check_delete - prerun -- Overrides BaseHandler.prerun - run -- Overrides BaseHandler.run - wait -- Wait for a stanza to arrive and return it to - an event handler. + The Waiter handler allows an event handler to block until a + particular stanza has been received. The handler will either be + given the matched stanza, or ``False`` if the waiter has timed out. + + :param string name: The name of the handler. + :param matcher: A :class:`~sleekxmpp.xmlstream.matcher.base.MatcherBase` + derived object for matching stanza objects. + :param stream: The :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` + instance this handler should monitor. """ def __init__(self, name, matcher, stream=None): - """ - Create a new Waiter. - - Arguments: - name -- The name of the waiter. - matcher -- A matcher object to detect the desired stanza. - stream -- Optional XMLStream instance to monitor. - """ BaseHandler.__init__(self, name, matcher, stream=stream) self._payload = queue.Queue() def prerun(self, payload): - """ - Store the matched stanza. - - Overrides BaseHandler.prerun + """Store the matched stanza when received during processing. - Arguments: - payload -- The matched stanza object. + :param payload: The matched + :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` object. """ self._payload.put(payload) def run(self, payload): - """ - Do not process this handler during the main event loop. - - Overrides BaseHandler.run - - Arguments: - payload -- The matched stanza object. - """ + """Do not process this handler during the main event loop.""" pass def wait(self, timeout=None): - """ - Block an event handler while waiting for a stanza to arrive. + """Block an event handler while waiting for a stanza to arrive. Be aware that this will impact performance if called from a non-threaded event handler. - Will return either the received stanza, or False if the waiter - timed out. + Will return either the received stanza, or ``False`` if the + waiter timed out. - Arguments: - timeout -- The number of seconds to wait for the stanza to - arrive. Defaults to the global default timeout - value sleekxmpp.xmlstream.RESPONSE_TIMEOUT. + :param int timeout: The number of seconds to wait for the stanza + to arrive. Defaults to the the stream's + :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream.response_timeout` + value. """ if timeout is None: timeout = self.stream().response_timeout - try: - stanza = self._payload.get(True, timeout) - except queue.Empty: - stanza = False - log.warning("Timed out waiting for %s" % self.name) + elapsed_time = 0 + stanza = False + while elapsed_time < timeout and not self.stream().stop.is_set(): + try: + stanza = self._payload.get(True, 1) + break + except queue.Empty: + elapsed_time += 1 + if elapsed_time >= timeout: + log.warning("Timed out waiting for %s", self.name) self.stream().remove_handler(self.name) return stanza def check_delete(self): - """ - Always remove waiters after use. - - Overrides BaseHandler.check_delete - """ + """Always remove waiters after use.""" return True diff --git a/sleekxmpp/xmlstream/jid.py b/sleekxmpp/xmlstream/jid.py index 3d617f5a..c91c8fb3 100644 --- a/sleekxmpp/xmlstream/jid.py +++ b/sleekxmpp/xmlstream/jid.py @@ -1,15 +1,22 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.jid + ~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + This module allows for working with Jabber IDs (JIDs) by + providing accessors for the various components of a JID. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from __future__ import unicode_literals class JID(object): + """ A representation of a Jabber ID, or JID. @@ -19,18 +26,16 @@ class JID(object): When a resource is not used, the JID is called a bare JID. The JID is a full JID otherwise. - Attributes: - jid -- Alias for 'full'. - full -- The value of the full JID. - bare -- The value of the bare JID. - user -- The username portion of the JID. - domain -- The domain name portion of the JID. - server -- Alias for 'domain'. - resource -- The resource portion of the JID. - - Methods: - reset -- Use a new JID value. - regenerate -- Recreate the JID from its components. + **JID Properties:** + :jid: Alias for ``full``. + :full: The value of the full JID. + :bare: The value of the bare JID. + :user: The username portion of the JID. + :domain: The domain name portion of the JID. + :server: Alias for ``domain``. + :resource: The resource portion of the JID. + + :param string jid: A string of the form ``'[user@]domain[/resource]'``. """ def __init__(self, jid): @@ -38,11 +43,9 @@ class JID(object): self.reset(jid) def reset(self, jid): - """ - Start fresh from a new JID string. + """Start fresh from a new JID string. - Arguments: - jid - The new JID value. + :param string jid: A string of the form ``'[user@]domain[/resource]'``. """ if isinstance(jid, JID): jid = jid.full @@ -53,12 +56,10 @@ class JID(object): self._bare = None def __getattr__(self, name): - """ - Handle getting the JID values, using cache if available. + """Handle getting the JID values, using cache if available. - Arguments: - name -- One of: user, server, domain, resource, - full, or bare. + :param name: One of: user, server, domain, resource, + full, or bare. """ if name == 'resource': if self._resource is None and '/' in self._jid: @@ -83,8 +84,7 @@ class JID(object): return self._bare or "" def __setattr__(self, name, value): - """ - Edit a JID by updating it's individual values, resetting the + """Edit a JID by updating it's individual values, resetting the generated JID in the end. Arguments: @@ -137,7 +137,5 @@ class JID(object): return self.full == other.full def __ne__(self, other): - """ - Two JIDs are considered unequal if they are not equal. - """ + """Two JIDs are considered unequal if they are not equal.""" return not self == other diff --git a/sleekxmpp/xmlstream/matcher/base.py b/sleekxmpp/xmlstream/matcher/base.py index 701ab32f..83c26688 100644 --- a/sleekxmpp/xmlstream/matcher/base.py +++ b/sleekxmpp/xmlstream/matcher/base.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.matcher.base + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ @@ -13,21 +16,15 @@ class MatcherBase(object): Base class for stanza matchers. Stanza matchers are used to pick stanzas out of the XML stream and pass them to the appropriate stream handlers. + + :param criteria: Object to compare some aspect of a stanza against. """ def __init__(self, criteria): - """ - Create a new stanza matcher. - - Arguments: - criteria -- Object to compare some aspect of a stanza - against. - """ self._criteria = criteria def match(self, xml): - """ - Check if a stanza matches the stored criteria. + """Check if a stanza matches the stored criteria. Meant to be overridden. """ diff --git a/sleekxmpp/xmlstream/matcher/id.py b/sleekxmpp/xmlstream/matcher/id.py index 0c8ce2d8..11ab70bb 100644 --- a/sleekxmpp/xmlstream/matcher/id.py +++ b/sleekxmpp/xmlstream/matcher/id.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.matcher.id + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from sleekxmpp.xmlstream.matcher.base import MatcherBase @@ -14,19 +17,13 @@ class MatcherId(MatcherBase): """ The ID matcher selects stanzas that have the same stanza 'id' interface value as the desired ID. - - Methods: - match -- Overrides MatcherBase.match. """ def match(self, xml): - """ - Compare the given stanza's 'id' attribute to the stored - id value. - - Overrides MatcherBase.match. + """Compare the given stanza's ``'id'`` attribute to the stored + ``id`` value. - Arguments: - xml -- The stanza to compare against. + :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + stanza to compare against. """ return xml['id'] == self._criteria diff --git a/sleekxmpp/xmlstream/matcher/stanzapath.py b/sleekxmpp/xmlstream/matcher/stanzapath.py index f8ff283d..61c5332c 100644 --- a/sleekxmpp/xmlstream/matcher/stanzapath.py +++ b/sleekxmpp/xmlstream/matcher/stanzapath.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.matcher.stanzapath + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from sleekxmpp.xmlstream.matcher.base import MatcherBase @@ -15,24 +18,17 @@ class StanzaPath(MatcherBase): The StanzaPath matcher selects stanzas that match a given "stanza path", which is similar to a normal XPath except that it uses the interfaces and plugins of the stanza instead of the actual, underlying XML. - - In most cases, the stanza path and XPath should be identical, but be - aware that differences may occur. - - Methods: - match -- Overrides MatcherBase.match. """ def match(self, stanza): """ Compare a stanza against a "stanza path". A stanza path is similar to an XPath expression, but uses the stanza's interfaces and plugins - instead of the underlying XML. For most cases, the stanza path and - XPath should be identical, but be aware that differences may occur. - - Overrides MatcherBase.match. + instead of the underlying XML. See the documentation for the stanza + :meth:`~sleekxmpp.xmlstream.stanzabase.ElementBase.match()` method + for more information. - Arguments: - stanza -- The stanza object to compare against. + :param stanza: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + stanza to compare against. """ return stanza.match(self._criteria) diff --git a/sleekxmpp/xmlstream/matcher/xmlmask.py b/sleekxmpp/xmlstream/matcher/xmlmask.py index 53ccc9ba..7977e767 100644 --- a/sleekxmpp/xmlstream/matcher/xmlmask.py +++ b/sleekxmpp/xmlstream/matcher/xmlmask.py @@ -30,66 +30,59 @@ class MatchXMLMask(MatcherBase): XML pattern, or mask. For example, message stanzas with body elements could be matched using the mask: + .. code-block:: xml + <message xmlns="jabber:client"><body /></message> - Use of XMLMask is discouraged, and XPath or StanzaPath should be used - instead. + Use of XMLMask is discouraged, and + :class:`~sleekxmpp.xmlstream.matcher.xpath.MatchXPath` or + :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` + should be used instead. The use of namespaces in the mask comparison is controlled by - IGNORE_NS. Setting IGNORE_NS to True will disable namespace based matching - for ALL XMLMask matchers. + ``IGNORE_NS``. Setting ``IGNORE_NS`` to ``True`` will disable namespace + based matching for ALL XMLMask matchers. - Methods: - match -- Overrides MatcherBase.match. - setDefaultNS -- Set the default namespace for the mask. + :param criteria: Either an :class:`~xml.etree.ElementTree.Element` XML + object or XML string to use as a mask. """ def __init__(self, criteria): - """ - Create a new XMLMask matcher. - - Arguments: - criteria -- Either an XML object or XML string to use as a mask. - """ MatcherBase.__init__(self, criteria) if isinstance(criteria, str): self._criteria = ET.fromstring(self._criteria) self.default_ns = 'jabber:client' def setDefaultNS(self, ns): - """ - Set the default namespace to use during comparisons. + """Set the default namespace to use during comparisons. - Arguments: - ns -- The new namespace to use as the default. + :param ns: The new namespace to use as the default. """ self.default_ns = ns def match(self, xml): - """ - Compare a stanza object or XML object against the stored XML mask. + """Compare a stanza object or XML object against the stored XML mask. Overrides MatcherBase.match. - Arguments: - xml -- The stanza object or XML object to compare against. + :param xml: The stanza object or XML object to compare against. """ if hasattr(xml, 'xml'): xml = xml.xml return self._mask_cmp(xml, self._criteria, True) def _mask_cmp(self, source, mask, use_ns=False, default_ns='__no_ns__'): - """ - Compare an XML object against an XML mask. - - Arguments: - source -- The XML object to compare against the mask. - mask -- The XML object serving as the mask. - use_ns -- Indicates if namespaces should be respected during - the comparison. - default_ns -- The default namespace to apply to elements that - do not have a specified namespace. - Defaults to "__no_ns__". + """Compare an XML object against an XML mask. + + :param source: The :class:`~xml.etree.ElementTree.Element` XML object + to compare against the mask. + :param mask: The :class:`~xml.etree.ElementTree.Element` XML object + serving as the mask. + :param use_ns: Indicates if namespaces should be respected during + the comparison. + :default_ns: The default namespace to apply to elements that + do not have a specified namespace. + Defaults to ``"__no_ns__"``. """ use_ns = not IGNORE_NS @@ -102,8 +95,7 @@ class MatchXMLMask(MatcherBase): try: mask = ET.fromstring(mask) except ExpatError: - log.warning("Expat error: %s\nIn parsing: %s" % ('', mask)) - + log.warning("Expat error: %s\nIn parsing: %s", '', mask) if not use_ns: # Compare the element without using namespaces. source_tag = source.tag.split('}', 1)[-1] @@ -149,14 +141,13 @@ class MatchXMLMask(MatcherBase): return True def _get_child(self, xml, tag): - """ - Return a child element given its tag, ignoring namespace values. + """Return a child element given its tag, ignoring namespace values. - Returns None if the child was not found. + Returns ``None`` if the child was not found. - Arguments: - xml -- The XML object to search for the given child tag. - tag -- The name of the subelement to find. + :param xml: The :class:`~xml.etree.ElementTree.Element` XML object + to search for the given child tag. + :param tag: The name of the subelement to find. """ tag = tag.split('}')[-1] try: diff --git a/sleekxmpp/xmlstream/matcher/xpath.py b/sleekxmpp/xmlstream/matcher/xpath.py index 669c9f16..b6af0609 100644 --- a/sleekxmpp/xmlstream/matcher/xpath.py +++ b/sleekxmpp/xmlstream/matcher/xpath.py @@ -1,9 +1,12 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.matcher.xpath + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from sleekxmpp.xmlstream.stanzabase import ET @@ -22,30 +25,34 @@ class MatchXPath(MatcherBase): The XPath matcher selects stanzas whose XML contents matches a given XPath expression. - Note that using this matcher may not produce expected behavior when using - attribute selectors. For Python 2.6 and 3.1, the ElementTree find method - does not support the use of attribute selectors. If you need to support - Python 2.6 or 3.1, it might be more useful to use a StanzaPath matcher. + .. warning:: - If the value of IGNORE_NS is set to true, then XPath expressions will - be matched without using namespaces. + Using this matcher may not produce expected behavior when using + attribute selectors. For Python 2.6 and 3.1, the ElementTree + :meth:`~xml.etree.ElementTree.Element.find()` method does + not support the use of attribute selectors. If you need to + support Python 2.6 or 3.1, it might be more useful to use a + :class:`~sleekxmpp.xmlstream.matcher.stanzapath.StanzaPath` matcher. - Methods: - match -- Overrides MatcherBase.match. + If the value of :data:`IGNORE_NS` is set to ``True``, then XPath + expressions will be matched without using namespaces. """ def match(self, xml): """ Compare a stanza's XML contents to an XPath expression. - If the value of IGNORE_NS is set to true, then XPath expressions - will be matched without using namespaces. + If the value of :data:`IGNORE_NS` is set to ``True``, then XPath + expressions will be matched without using namespaces. + + .. warning:: - Note that in Python 2.6 and 3.1 the ElementTree find method does - not support attribute selectors in the XPath expression. + In Python 2.6 and 3.1 the ElementTree + :meth:`~xml.etree.ElementTree.Element.find()` method does not + support attribute selectors in the XPath expression. - Arguments: - xml -- The stanza object to compare against. + :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + stanza to compare against. """ if hasattr(xml, 'xml'): xml = xml.xml diff --git a/sleekxmpp/xmlstream/scheduler.py b/sleekxmpp/xmlstream/scheduler.py index 58219257..4a6f073f 100644 --- a/sleekxmpp/xmlstream/scheduler.py +++ b/sleekxmpp/xmlstream/scheduler.py @@ -1,9 +1,15 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.scheduler + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + This module provides a task scheduler that works better + with SleekXMPP's threading usage than the stock version. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ import time @@ -24,50 +30,47 @@ class Task(object): A scheduled task that will be executed by the scheduler after a given time interval has passed. - Attributes: - name -- The name of the task. - seconds -- The number of seconds to wait before executing. - callback -- The function to execute. - args -- The arguments to pass to the callback. - kwargs -- The keyword arguments to pass to the callback. - repeat -- Indicates if the task should repeat. - Defaults to False. - qpointer -- A pointer to an event queue for queuing callback + :param string name: The name of the task. + :param int seconds: The number of seconds to wait before executing. + :param callback: The function to execute. + :param tuple args: The arguments to pass to the callback. + :param dict kwargs: The keyword arguments to pass to the callback. + :param bool repeat: Indicates if the task should repeat. + Defaults to ``False``. + :param pointer: A pointer to an event queue for queuing callback execution instead of executing immediately. - - Methods: - run -- Either queue or execute the callback. - reset -- Reset the task's timer. """ def __init__(self, name, seconds, callback, args=None, kwargs=None, repeat=False, qpointer=None): - """ - Create a new task. - - Arguments: - name -- The name of the task. - seconds -- The number of seconds to wait before executing. - callback -- The function to execute. - args -- The arguments to pass to the callback. - kwargs -- The keyword arguments to pass to the callback. - repeat -- Indicates if the task should repeat. - Defaults to False. - qpointer -- A pointer to an event queue for queuing callback - execution instead of executing immediately. - """ + #: The name of the task. self.name = name + + #: The number of seconds to wait before executing. self.seconds = seconds + + #: The function to execute once enough time has passed. self.callback = callback + + #: The arguments to pass to :attr:`callback`. self.args = args or tuple() + + #: The keyword arguments to pass to :attr:`callback`. self.kwargs = kwargs or {} + + #: Indicates if the task should repeat after executing, + #: using the same :attr:`seconds` delay. self.repeat = repeat + + #: The time when the task should execute next. self.next = time.time() + self.seconds + + #: The main event queue, which allows for callbacks to + #: be queued for execution instead of executing immediately. self.qpointer = qpointer def run(self): - """ - Execute the task's callback. + """Execute the task's callback. If an event queue was supplied, place the callback in the queue; otherwise, execute the callback immediately. @@ -81,9 +84,7 @@ class Task(object): return self.repeat def reset(self): - """ - Reset the task's timer so that it will repeat. - """ + """Reset the task's timer so that it will repeat.""" self.next = time.time() + self.seconds @@ -93,48 +94,42 @@ class Scheduler(object): A threaded scheduler that allows for updates mid-execution unlike the scheduler in the standard library. - http://docs.python.org/library/sched.html#module-sched - - Attributes: - addq -- A queue storing added tasks. - schedule -- A list of tasks in order of execution times. - thread -- If threaded, the thread processing the schedule. - run -- Indicates if the scheduler is running. - stop -- Threading event indicating if the main process - has been stopped. - Methods: - add -- Add a new task to the schedule. - process -- Process and schedule tasks. - quit -- Stop the scheduler. + Based on: http://docs.python.org/library/sched.html#module-sched + + :param parentstop: An :class:`~threading.Event` to signal stopping + the scheduler. """ def __init__(self, parentstop=None): - """ - Create a new scheduler. - - Arguments: - parentstop -- A threading event indicating if the main process has - been stopped. - """ + #: A queue for storing tasks self.addq = queue.Queue() + + #: A list of tasks in order of execution time. self.schedule = [] + + #: If running in threaded mode, this will be the thread processing + #: the schedule. self.thread = None + + #: A flag indicating that the scheduler is running. self.run = False + + #: An :class:`~threading.Event` instance for signalling to stop + #: the scheduler. self.stop = parentstop + + #: Lock for accessing the task queue. self.schedule_lock = threading.RLock() def process(self, threaded=True): - """ - Begin accepting and processing scheduled tasks. + """Begin accepting and processing scheduled tasks. - Arguments: - threaded -- Indicates if the scheduler should execute in its own - thread. Defaults to True. + :param bool threaded: Indicates if the scheduler should execute + in its own thread. Defaults to ``True``. """ if threaded: - self.thread = threading.Thread(name='sheduler_process', + self.thread = threading.Thread(name='scheduler_process', target=self._process) - self.thread.daemon = True self.thread.start() else: self._process() @@ -184,18 +179,16 @@ class Scheduler(object): def add(self, name, seconds, callback, args=None, kwargs=None, repeat=False, qpointer=None): - """ - Schedule a new task. - - Arguments: - name -- The name of the task. - seconds -- The number of seconds to wait before executing. - callback -- The function to execute. - args -- The arguments to pass to the callback. - kwargs -- The keyword arguments to pass to the callback. - repeat -- Indicates if the task should repeat. - Defaults to False. - qpointer -- A pointer to an event queue for queuing callback + """Schedule a new task. + + :param string name: The name of the task. + :param int seconds: The number of seconds to wait before executing. + :param callback: The function to execute. + :param tuple args: The arguments to pass to the callback. + :param dict kwargs: The keyword arguments to pass to the callback. + :param bool repeat: Indicates if the task should repeat. + Defaults to ``False``. + :param pointer: A pointer to an event queue for queuing callback execution instead of executing immediately. """ try: @@ -212,12 +205,10 @@ class Scheduler(object): self.schedule_lock.release() def remove(self, name): - """ - Remove a scheduled task ahead of schedule, and without + """Remove a scheduled task ahead of schedule, and without executing it. - Arguments: - name -- The name of the task to remove. + :param string name: The name of the task to remove. """ try: self.schedule_lock.acquire() diff --git a/sleekxmpp/xmlstream/stanzabase.py b/sleekxmpp/xmlstream/stanzabase.py index 1ff89554..389fe20c 100644 --- a/sleekxmpp/xmlstream/stanzabase.py +++ b/sleekxmpp/xmlstream/stanzabase.py @@ -1,9 +1,15 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.stanzabase + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + This module implements a wrapper layer for XML objects + that allows them to be treated like dictionaries. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ import copy @@ -28,15 +34,22 @@ def register_stanza_plugin(stanza, plugin, iterable=False, overrides=False): """ Associate a stanza object as a plugin for another stanza. - Arguments: - stanza -- The class of the parent stanza. - plugin -- The class of the plugin stanza. - iterable -- Indicates if the plugin stanza should be - included in the parent stanza's iterable - 'substanzas' interface results. - overrides -- Indicates if the plugin should be allowed - to override the interface handlers for - the parent stanza. + >>> from sleekxmpp.xmlstream import register_stanza_plugin + >>> register_stanza_plugin(Iq, CustomStanza) + + :param class stanza: The class of the parent stanza. + :param class plugin: The class of the plugin stanza. + :param bool iterable: Indicates if the plugin stanza should be + included in the parent stanza's iterable + ``'substanzas'`` interface results. + :param bool overrides: Indicates if the plugin should be allowed + to override the interface handlers for + the parent stanza, based on the plugin's + ``overrides`` field. + + .. versionadded:: 1.0-Beta1 + Made ``register_stanza_plugin`` the default name. The prior + ``registerStanzaPlugin`` function name remains as an alias. """ tag = "{%s}%s" % (plugin.namespace, plugin.name) @@ -73,23 +86,23 @@ class ElementBase(object): to the Ruby XMPP library Blather's stanza implementation. Stanzas are defined by their name, namespace, and interfaces. For - example, a simplistic Message stanza could be defined as: + example, a simplistic Message stanza could be defined as:: - >>> class Message(ElementBase): - ... name = "message" - ... namespace = "jabber:client" - ... interfaces = set(('to', 'from', 'type', 'body')) - ... sub_interfaces = set(('body',)) + >>> class Message(ElementBase): + ... name = "message" + ... namespace = "jabber:client" + ... interfaces = set(('to', 'from', 'type', 'body')) + ... sub_interfaces = set(('body',)) - The resulting Message stanza's contents may be accessed as so: + The resulting Message stanza's contents may be accessed as so:: - >>> message['to'] = "user@example.com" - >>> message['body'] = "Hi!" - >>> message['body'] - "Hi!" - >>> del message['body'] - >>> message['body'] - "" + >>> message['to'] = "user@example.com" + >>> message['body'] = "Hi!" + >>> message['body'] + "Hi!" + >>> del message['body'] + >>> message['body'] + "" The interface values map to either custom access methods, stanza XML attributes, or (if the interface is also in sub_interfaces) the @@ -100,164 +113,171 @@ class ElementBase(object): "Interface" is the titlecase version of the interface name. Stanzas may be extended through the use of plugins. A plugin - is simply a stanza that has a plugin_attrib value. For example: + is simply a stanza that has a plugin_attrib value. For example:: - >>> class MessagePlugin(ElementBase): - ... name = "custom_plugin" - ... namespace = "custom" - ... interfaces = set(('useful_thing', 'custom')) - ... plugin_attrib = "custom" + >>> class MessagePlugin(ElementBase): + ... name = "custom_plugin" + ... namespace = "custom" + ... interfaces = set(('useful_thing', 'custom')) + ... plugin_attrib = "custom" The plugin stanza class must be associated with its intended - container stanza by using register_stanza_plugin as so: + container stanza by using register_stanza_plugin as so:: - >>> register_stanza_plugin(Message, MessagePlugin) + >>> register_stanza_plugin(Message, MessagePlugin) The plugin may then be accessed as if it were built-in to the parent - stanza. + stanza:: - >>> message['custom']['useful_thing'] = 'foo' + >>> message['custom']['useful_thing'] = 'foo' If a plugin provides an interface that is the same as the plugin's plugin_attrib value, then the plugin's interface may be assigned directly from the parent stanza, as shown below, but retrieving - information will require all interfaces to be used, as so: + information will require all interfaces to be used, as so:: - >>> message['custom'] = 'bar' # Same as using message['custom']['custom'] - >>> message['custom']['custom'] # Must use all interfaces - 'bar' + >>> message['custom'] = 'bar' # Same as using message['custom']['custom'] + >>> message['custom']['custom'] # Must use all interfaces + 'bar' - If the plugin sets the value is_extension = True, then both setting + If the plugin sets :attr:`is_extension` to ``True``, then both setting and getting an interface value that is the same as the plugin's - plugin_attrib value will work, as so: - - >>> message['custom'] = 'bar' # Using is_extension=True - >>> message['custom'] - 'bar' - - - Class Attributes: - name -- The name of the stanza's main element. - namespace -- The namespace of the stanza's main element. - interfaces -- A set of attribute and element names that may - be accessed using dictionary syntax. - sub_interfaces -- A subset of the set of interfaces which map - to subelements instead of attributes. - subitem -- A set of stanza classes which are allowed to - be added as substanzas. Deprecated version - of plugin_iterables. - overrides -- A list of interfaces prepended with 'get_', - 'set_', or 'del_'. If the stanza is registered - as a plugin with overrides=True, then the - parent's interface handlers will be - overridden by the plugin's matching handler. - types -- A set of generic type attribute values. - tag -- The namespaced name of the stanza's root - element. Example: "{foo_ns}bar" - plugin_attrib -- The interface name that the stanza uses to be - accessed as a plugin from another stanza. - plugin_attrib_map -- A mapping of plugin attribute names with the - associated plugin stanza classes. - plugin_iterables -- A set of stanza classes which are allowed to - be added as substanzas. - plugin_overrides -- A mapping of interfaces prepended with 'get_', - 'set_' or 'del_' to plugin attrib names. Allows - a plugin to override the behaviour of a parent - stanza's interface handlers. - plugin_tag_map -- A mapping of plugin stanza tag names with - the associated plugin stanza classes. - is_extension -- When True, allows the stanza to provide one - additional interface to the parent stanza, - extending the interfaces supported by the - parent. Defaults to False. - xml_ns -- The XML namespace, - http://www.w3.org/XML/1998/namespace, - for use with xml:lang values. - - Instance Attributes: - xml -- The stanza's XML contents. - parent -- The parent stanza of this stanza. - plugins -- A map of enabled plugin names with the - initialized plugin stanza objects. - values -- A dictionary of the stanza's interfaces - and interface values, including plugins. - - Class Methods - tag_name -- Return the namespaced version of the stanza's - root element's name. - - Methods: - setup -- Initialize the stanza's XML contents. - enable -- Instantiate a stanza plugin. - Alias for init_plugin. - init_plugin -- Instantiate a stanza plugin. - _get_stanza_values -- Return a dictionary of stanza interfaces and - their values. - _set_stanza_values -- Set stanza interface values given a dictionary - of interfaces and values. - __getitem__ -- Return the value of a stanza interface. - __setitem__ -- Set the value of a stanza interface. - __delitem__ -- Remove the value of a stanza interface. - _set_attr -- Set an attribute value of the main - stanza element. - _del_attr -- Remove an attribute from the main - stanza element. - _get_attr -- Return an attribute's value from the main - stanza element. - _get_sub_text -- Return the text contents of a subelement. - _set_sub_text -- Set the text contents of a subelement. - _del_sub -- Remove a subelement. - match -- Compare the stanza against an XPath expression. - find -- Return subelement matching an XPath expression. - findall -- Return subelements matching an XPath expression. - get -- Return the value of a stanza interface, with an - optional default value. - keys -- Return the set of interface names accepted by - the stanza. - append -- Add XML content or a substanza to the stanza. - appendxml -- Add XML content to the stanza. - pop -- Remove a substanza. - next -- Return the next iterable substanza. - clear -- Reset the stanza's XML contents. - _fix_ns -- Apply the stanza's namespace to non-namespaced - elements in an XPath expression. + plugin_attrib value will work, as so:: + + >>> message['custom'] = 'bar' # Using is_extension=True + >>> message['custom'] + 'bar' + + + :param xml: Initialize the stanza object with an existing XML object. + :param parent: Optionally specify a parent stanza object will will + contain this substanza. """ + #: The XML tag name of the element, not including any namespace + #: prefixes. For example, an :class:`ElementBase` object for ``<message />`` + #: would use ``name = 'message'``. name = 'stanza' - plugin_attrib = 'plugin' + + #: The XML namespace for the element. Given ``<foo xmlns="bar" />``, + #: then ``namespace = "bar"`` should be used. The default namespace + #: is ``jabber:client`` since this is being used in an XMPP library. namespace = 'jabber:client' + + #: For :class:`ElementBase` subclasses which are intended to be used + #: as plugins, the ``plugin_attrib`` value defines the plugin name. + #: Plugins may be accessed by using the ``plugin_attrib`` value as + #: the interface. An example using ``plugin_attrib = 'foo'``: + #: + #: register_stanza_plugin(Message, FooPlugin) + #: msg = Message() + #: msg['foo']['an_interface_from_the_foo_plugin'] + plugin_attrib = 'plugin' + + #: The set of keys that the stanza provides for accessing and + #: manipulating the underlying XML object. This set may be augmented + #: with the :attr:`plugin_attrib` value of any registered + #: stanza plugins. interfaces = set(('type', 'to', 'from', 'id', 'payload')) - types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) + + #: A subset of :attr:`interfaces` which maps interfaces to direct + #: subelements of the underlying XML object. Using this set, the text + #: of these subelements may be set, retrieved, or removed without + #: needing to define custom methods. sub_interfaces = tuple() - overrides = {} - plugin_attrib_map = {} + + #: In some cases you may wish to override the behaviour of one of the + #: parent stanza's interfaces. The ``overrides`` list specifies the + #: interface name and access method to be overridden. For example, + #: to override setting the parent's ``'condition'`` interface you + #: would use:: + #: + #: overrides = ['set_condition'] + #: + #: Getting and deleting the ``'condition'`` interface would not + #: be affected. + #: + #: .. versionadded:: 1.0-Beta5 + overrides = [] + + #: If you need to add a new interface to an existing stanza, you + #: can create a plugin and set ``is_extension = True``. Be sure + #: to set the :attr:`plugin_attrib` value to the desired interface + #: name, and that it is the only interface listed in + #: :attr:`interfaces`. Requests for the new interface from the + #: parent stanza will be passed to the plugin directly. + #: + #: .. versionadded:: 1.0-Beta5 + is_extension = False + + #: A map of interface operations to the overriding functions. + #: For example, after overriding the ``set`` operation for + #: the interface ``body``, :attr:`plugin_overrides` would be:: + #: + #: {'set_body': <some function>} + #: + #: .. versionadded: 1.0-Beta5 plugin_overrides = {} - plugin_iterables = set() + + #: A mapping of the :attr:`plugin_attrib` values of registered + #: plugins to their respective classes. + plugin_attrib_map = {} + + #: A mapping of root element tag names (in ``'{namespace}elementname'`` + #: format) to the plugin classes responsible for them. plugin_tag_map = {} + + #: The set of stanza classes that can be iterated over using + #: the 'substanzas' interface. Classes are added to this set + #: when registering a plugin with ``iterable=True``:: + #: + #: register_stanza_plugin(DiscoInfo, DiscoItem, iterable=True) + #: + #: .. versionadded:: 1.0-Beta5 + plugin_iterables = set() + + #: A deprecated version of :attr:`plugin_iterables` that remains + #: for backward compatibility. It required a parent stanza to + #: know beforehand what stanza classes would be iterable:: + #: + #: class DiscoItem(ElementBase): + #: ... + #: + #: class DiscoInfo(ElementBase): + #: subitem = (DiscoItem, ) + #: ... + #: + #: .. deprecated:: 1.0-Beta5 subitem = set() - is_extension = False + + #: The default XML namespace: ``http://www.w3.org/XML/1998/namespace``. xml_ns = 'http://www.w3.org/XML/1998/namespace' def __init__(self, xml=None, parent=None): - """ - Create a new stanza object. + self._index = 0 - Arguments: - xml -- Initialize the stanza with optional existing XML. - parent -- Optional stanza object that contains this stanza. - """ + #: The underlying XML object for the stanza. It is a standard + #: :class:`xml.etree.cElementTree` object. self.xml = xml + + #: An ordered dictionary of plugin stanzas, mapped by their + #: :attr:`plugin_attrib` value. self.plugins = OrderedDict() + + #: A list of child stanzas whose class is included in + #: :attr:`plugin_iterables`. self.iterables = [] - self._index = 0 + + #: The name of the tag for the stanza's root element. It is the + #: same as calling :meth:`tag_name()` and is formatted as + #: ``'{namespace}elementname'``. self.tag = self.tag_name() - if parent is None: - self.parent = None - else: - self.parent = weakref.ref(parent) - ElementBase.values = property(ElementBase._get_stanza_values, - ElementBase._set_stanza_values) + #: A :class:`weakref.weakref` to the parent stanza, if there is one. + #: If not, then :attr:`parent` is ``None``. + self.parent = None + if parent is not None: + self.parent = weakref.ref(parent) if self.subitem is not None: for sub in self.subitem: @@ -270,23 +290,21 @@ class ElementBase(object): # Initialize values using provided XML for child in self.xml.getchildren(): if child.tag in self.plugin_tag_map: - plugin = self.plugin_tag_map[child.tag] - self.plugins[plugin.plugin_attrib] = plugin(child, self) - for sub in self.plugin_iterables: - if child.tag == "{%s}%s" % (sub.namespace, sub.name): - self.iterables.append(sub(child, self)) - break + plugin_class = self.plugin_tag_map[child.tag] + plugin = plugin_class(child, self) + self.plugins[plugin.plugin_attrib] = plugin + if plugin_class in self.plugin_iterables: + self.iterables.append(plugin) def setup(self, xml=None): - """ - Initialize the stanza's XML contents. + """Initialize the stanza's XML contents. - Will return True if XML was generated according to the stanza's - definition. + Will return ``True`` if XML was generated according to the stanza's + definition instead of building a stanza object from an existing + XML object. - Arguments: - xml -- Optional XML object to use for the stanza's content - instead of generating XML. + :param xml: An existing XML object to use for the stanza's content + instead of generating new XML. """ if self.xml is None: self.xml = xml @@ -310,33 +328,47 @@ class ElementBase(object): return False def enable(self, attrib): - """ - Enable and initialize a stanza plugin. + """Enable and initialize a stanza plugin. - Alias for init_plugin. + Alias for :meth:`init_plugin`. - Arguments: - attrib -- The stanza interface for the plugin. + :param string attrib: The :attr:`plugin_attrib` value of the + plugin to enable. """ return self.init_plugin(attrib) def init_plugin(self, attrib): - """ - Enable and initialize a stanza plugin. + """Enable and initialize a stanza plugin. - Arguments: - attrib -- The stanza interface for the plugin. + :param string attrib: The :attr:`plugin_attrib` value of the + plugin to enable. """ if attrib not in self.plugins: plugin_class = self.plugin_attrib_map[attrib] - self.plugins[attrib] = plugin_class(parent=self) + plugin = plugin_class(parent=self) + self.plugins[attrib] = plugin + if plugin_class in self.plugin_iterables: + self.iterables.append(plugin) return self def _get_stanza_values(self): - """ - Return a dictionary of the stanza's interface values. + """Return A JSON/dictionary version of the XML content + exposed through the stanza's interfaces:: - Stanza plugin values are included as nested dictionaries. + >>> msg = Message() + >>> msg.values + {'body': '', 'from': , 'mucnick': '', 'mucroom': '', + 'to': , 'type': 'normal', 'id': '', 'subject': ''} + + Likewise, assigning to :attr:`values` will change the XML + content:: + + >>> msg = Message() + >>> msg.values = {'body': 'Hi!', 'to': 'user@example.com'} + >>> msg + '<message to="user@example.com"><body>Hi!</body></message>' + + .. versionadded:: 1.0-Beta1 """ values = {} for interface in self.interfaces: @@ -352,15 +384,15 @@ class ElementBase(object): return values def _set_stanza_values(self, values): - """ - Set multiple stanza interface values using a dictionary. + """Set multiple stanza interface values using a dictionary. Stanza plugin values may be set using nested dictionaries. - Arguments: - values -- A dictionary mapping stanza interface with values. - Plugin interfaces may accept a nested dictionary that - will be used recursively. + :param values: A dictionary mapping stanza interface with values. + Plugin interfaces may accept a nested dictionary that + will be used recursively. + + .. versionadded:: 1.0-Beta1 """ iterable_interfaces = [p.plugin_attrib for \ p in self.plugin_iterables] @@ -393,30 +425,31 @@ class ElementBase(object): return self def __getitem__(self, attrib): - """ - Return the value of a stanza interface using dictionary-like syntax. + """Return the value of a stanza interface using dict-like syntax. + + Example:: - Example: >>> msg['body'] 'Message contents' Stanza interfaces are typically mapped directly to the underlying XML - object, but can be overridden by the presence of a get_attrib method - (or get_foo where the interface is named foo, etc). + object, but can be overridden by the presence of a ``get_attrib`` + method (or ``get_foo`` where the interface is named ``'foo'``, etc). The search order for interface value retrieval for an interface - named 'foo' is: - 1. The list of substanzas. - 2. The result of calling the get_foo override handler. - 3. The result of calling get_foo. - 4. The result of calling getFoo. - 5. The contents of the foo subelement, if foo is a sub interface. - 6. The value of the foo attribute of the XML object. - 7. The plugin named 'foo' + named ``'foo'`` is: + + 1. The list of substanzas (``'substanzas'``) + 2. The result of calling the ``get_foo`` override handler. + 3. The result of calling ``get_foo``. + 4. The result of calling ``getFoo``. + 5. The contents of the ``foo`` subelement, if ``foo`` is listed + in :attr:`sub_interfaces`. + 6. The value of the ``foo`` attribute of the XML object. + 7. The plugin named ``'foo'`` 8. An empty string. - Arguments: - attrib -- The name of the requested stanza interface. + :param string attrib: The name of the requested stanza interface. """ if attrib == 'substanzas': return self.iterables @@ -452,33 +485,34 @@ class ElementBase(object): return '' def __setitem__(self, attrib, value): - """ - Set the value of a stanza interface using dictionary-like syntax. + """Set the value of a stanza interface using dictionary-like syntax. + + Example:: - Example: >>> msg['body'] = "Hi!" >>> msg['body'] 'Hi!' Stanza interfaces are typically mapped directly to the underlying XML - object, but can be overridden by the presence of a set_attrib method - (or set_foo where the interface is named foo, etc). + object, but can be overridden by the presence of a ``set_attrib`` + method (or ``set_foo`` where the interface is named ``'foo'``, etc). The effect of interface value assignment for an interface - named 'foo' will be one of: + named ``'foo'`` will be one of: + 1. Delete the interface's contents if the value is None. - 2. Call the set_foo override handler, if it exists. - 3. Call set_foo, if it exists. - 4. Call setFoo, if it exists. - 5. Set the text of a foo element, if foo is in sub_interfaces. - 6. Set the value of a top level XML attribute name foo. - 7. Attempt to pass value to a plugin named foo using the plugin's - foo interface. + 2. Call the ``set_foo`` override handler, if it exists. + 3. Call ``set_foo``, if it exists. + 4. Call ``setFoo``, if it exists. + 5. Set the text of a ``foo`` element, if ``'foo'`` is + in :attr:`sub_interfaces`. + 6. Set the value of a top level XML attribute named ``foo``. + 7. Attempt to pass the value to a plugin named ``'foo'`` using + the plugin's ``'foo'`` interface. 8. Do nothing. - Arguments: - attrib -- The name of the stanza interface to modify. - value -- The new value of the stanza interface. + :param string attrib: The name of the stanza interface to modify. + :param value: The new value of the stanza interface. """ if attrib in self.interfaces: if value is not None: @@ -513,10 +547,10 @@ class ElementBase(object): return self def __delitem__(self, attrib): - """ - Delete the value of a stanza interface using dictionary-like syntax. + """Delete the value of a stanza interface using dict-like syntax. + + Example:: - Example: >>> msg['body'] = "Hi!" >>> msg['body'] 'Hi!' @@ -525,21 +559,22 @@ class ElementBase(object): '' Stanza interfaces are typically mapped directly to the underlyig XML - object, but can be overridden by the presence of a del_attrib method - (or del_foo where the interface is named foo, etc). + object, but can be overridden by the presence of a ``del_attrib`` + method (or ``del_foo`` where the interface is named ``'foo'``, etc). - The effect of deleting a stanza interface value named foo will be + The effect of deleting a stanza interface value named ``foo`` will be one of: - 1. Call del_foo override handler, if it exists. - 2. Call del_foo, if it exists. - 3. Call delFoo, if it exists. - 4. Delete foo element, if foo is in sub_interfaces. - 5. Delete top level XML attribute named foo. - 6. Remove the foo plugin, if it was loaded. + + 1. Call ``del_foo`` override handler, if it exists. + 2. Call ``del_foo``, if it exists. + 3. Call ``delFoo``, if it exists. + 4. Delete ``foo`` element, if ``'foo'`` is in + :attr:`sub_interfaces`. + 5. Delete top level XML attribute named ``foo``. + 6. Remove the ``foo`` plugin, if it was loaded. 7. Do nothing. - Arguments: - attrib -- The name of the affected stanza interface. + :param attrib: The name of the affected stanza interface. """ if attrib in self.interfaces: del_method = "del_%s" % attrib.lower() @@ -576,16 +611,14 @@ class ElementBase(object): return self def _set_attr(self, name, value): - """ - Set the value of a top level attribute of the underlying XML object. + """Set the value of a top level attribute of the XML object. If the new value is None or an empty string, then the attribute will be removed. - Arguments: - name -- The name of the attribute. - value -- The new value of the attribute, or None or '' to - remove it. + :param name: The name of the attribute. + :param value: The new value of the attribute, or None or '' to + remove it. """ if value is None or value == '': self.__delitem__(name) @@ -593,43 +626,36 @@ class ElementBase(object): self.xml.attrib[name] = value def _del_attr(self, name): - """ - Remove a top level attribute of the underlying XML object. + """Remove a top level attribute of the XML object. - Arguments: - name -- The name of the attribute. + :param name: The name of the attribute. """ if name in self.xml.attrib: del self.xml.attrib[name] def _get_attr(self, name, default=''): - """ - Return the value of a top level attribute of the underlying - XML object. + """Return the value of a top level attribute of the XML object. In case the attribute has not been set, a default value can be returned instead. An empty string is returned if no other default is supplied. - Arguments: - name -- The name of the attribute. - default -- Optional value to return if the attribute has not - been set. An empty string is returned otherwise. + :param name: The name of the attribute. + :param default: Optional value to return if the attribute has not + been set. An empty string is returned otherwise. """ return self.xml.attrib.get(name, default) def _get_sub_text(self, name, default=''): - """ - Return the text contents of a sub element. + """Return the text contents of a sub element. In case the element does not exist, or it has no textual content, a default value can be returned instead. An empty string is returned if no other default is supplied. - Arguments: - name -- The name or XPath expression of the element. - default -- Optional default to return if the element does - not exists. An empty string is returned otherwise. + :param name: The name or XPath expression of the element. + :param default: Optional default to return if the element does + not exists. An empty string is returned otherwise. """ name = self._fix_ns(name) stanza = self.xml.find(name) @@ -639,8 +665,7 @@ class ElementBase(object): return stanza.text def _set_sub_text(self, name, text=None, keep=False): - """ - Set the text contents of a sub element. + """Set the text contents of a sub element. In case the element does not exist, a element will be created, and its text contents will be set. @@ -648,13 +673,12 @@ class ElementBase(object): If the text is set to an empty string, or None, then the element will be removed, unless keep is set to True. - Arguments: - name -- The name or XPath expression of the element. - text -- The new textual content of the element. If the text - is an empty string or None, the element will be removed - unless the parameter keep is True. - keep -- Indicates if the element should be kept if its text is - removed. Defaults to False. + :param name: The name or XPath expression of the element. + :param text: The new textual content of the element. If the text + is an empty string or None, the element will be removed + unless the parameter keep is True. + :param keep: Indicates if the element should be kept if its text is + removed. Defaults to False. """ path = self._fix_ns(name, split=True) element = self.xml.find(name) @@ -682,17 +706,15 @@ class ElementBase(object): return element def _del_sub(self, name, all=False): - """ - Remove sub elements that match the given name or XPath. + """Remove sub elements that match the given name or XPath. If the element is in a path, then any parent elements that become empty after deleting the element may also be deleted if requested by setting all=True. - Arguments: - name -- The name or XPath expression for the element(s) to remove. - all -- If True, remove all empty elements in the path to the - deleted element. Defaults to False. + :param name: The name or XPath expression for the element(s) to remove. + :param bool all: If True, remove all empty elements in the path to the + deleted element. Defaults to False. """ path = self._fix_ns(name, split=True) original_target = path[-1] @@ -720,19 +742,22 @@ class ElementBase(object): return def match(self, xpath): - """ - Compare a stanza object with an XPath expression. If the XPath matches - the contents of the stanza object, the match is successful. + """Compare a stanza object with an XPath-like expression. + + If the XPath matches the contents of the stanza object, the match + is successful. The XPath expression may include checks for stanza attributes. - For example: - presence@show=xa@priority=2/status - Would match a presence stanza whose show value is set to 'xa', has a - priority value of '2', and has a status element. + For example:: - Arguments: - xpath -- The XPath expression to check against. It may be either a - string or a list of element names with attribute checks. + 'presence@show=xa@priority=2/status' + + Would match a presence stanza whose show value is set to ``'xa'``, + has a priority value of ``'2'``, and has a status element. + + :param string xpath: The XPath expression to check against. It + may be either a string or a list of element + names with attribute checks. """ if isinstance(xpath, str): xpath = self._fix_ns(xpath, split=True, propagate_ns=False) @@ -781,44 +806,46 @@ class ElementBase(object): return True def find(self, xpath): - """ - Find an XML object in this stanza given an XPath expression. + """Find an XML object in this stanza given an XPath expression. Exposes ElementTree interface for backwards compatibility. - Note that matching on attribute values is not supported in Python 2.6 - or Python 3.1 + .. note:: - Arguments: - xpath -- An XPath expression matching a single desired element. + Matching on attribute values is not supported in Python 2.6 + or Python 3.1 + + :param string xpath: An XPath expression matching a single + desired element. """ return self.xml.find(xpath) def findall(self, xpath): - """ - Find multiple XML objects in this stanza given an XPath expression. + """Find multiple XML objects in this stanza given an XPath expression. Exposes ElementTree interface for backwards compatibility. - Note that matching on attribute values is not supported in Python 2.6 - or Python 3.1. + .. note:: - Arguments: - xpath -- An XPath expression matching multiple desired elements. + Matching on attribute values is not supported in Python 2.6 + or Python 3.1. + + :param string xpath: An XPath expression matching multiple + desired elements. """ return self.xml.findall(xpath) def get(self, key, default=None): - """ - Return the value of a stanza interface. If the found value is None - or an empty string, return the supplied default value. + """Return the value of a stanza interface. + + If the found value is None or an empty string, return the supplied + default value. Allows stanza objects to be used like dictionaries. - Arguments: - key -- The name of the stanza interface to check. - default -- Value to return if the stanza interface has a value - of None or "". Will default to returning None. + :param string key: The name of the stanza interface to check. + :param default: Value to return if the stanza interface has a value + of ``None`` or ``""``. Will default to returning None. """ value = self[key] if value is None or value == '': @@ -826,8 +853,7 @@ class ElementBase(object): return value def keys(self): - """ - Return the names of all stanza interfaces provided by the + """Return the names of all stanza interfaces provided by the stanza object. Allows stanza objects to be used like dictionaries. @@ -840,17 +866,15 @@ class ElementBase(object): return out def append(self, item): - """ - Append either an XML object or a substanza to this stanza object. + """Append either an XML object or a substanza to this stanza object. If a substanza object is appended, it will be added to the list of iterable stanzas. Allows stanza objects to be used like lists. - Arguments: - item -- Either an XML object or a stanza object to add to - this stanza's contents. + :param item: Either an XML object or a stanza object to add to + this stanza's contents. """ if not isinstance(item, ElementBase): if type(item) == XML_TYPE: @@ -862,41 +886,34 @@ class ElementBase(object): return self def appendxml(self, xml): - """ - Append an XML object to the stanza's XML. + """Append an XML object to the stanza's XML. The added XML will not be included in the list of iterable substanzas. - Arguments: - xml -- The XML object to add to the stanza. + :param XML xml: The XML object to add to the stanza. """ self.xml.append(xml) return self def pop(self, index=0): - """ - Remove and return the last substanza in the list of + """Remove and return the last substanza in the list of iterable substanzas. Allows stanza objects to be used like lists. - Arguments: - index -- The index of the substanza to remove. + :param int index: The index of the substanza to remove. """ substanza = self.iterables.pop(index) self.xml.remove(substanza.xml) return substanza def next(self): - """ - Return the next iterable substanza. - """ + """Return the next iterable substanza.""" return self.__next__() def clear(self): - """ - Remove all XML element contents and plugins. + """Remove all XML element contents and plugins. Any attribute values will be preserved. """ @@ -908,43 +925,44 @@ class ElementBase(object): @classmethod def tag_name(cls): - """ - Return the namespaced name of the stanza's root element. + """Return the namespaced name of the stanza's root element. + + The format for the tag name is:: + + '{namespace}elementname' - For example, for the stanza <foo xmlns="bar" />, - stanza.tag would return "{bar}foo". + For example, for the stanza ``<foo xmlns="bar" />``, + ``stanza.tag_name()`` would return ``"{bar}foo"``. """ return "{%s}%s" % (cls.namespace, cls.name) @property def attrib(self): - """ - DEPRECATED - - For backwards compatibility, stanza.attrib returns the stanza itself. + """Return the stanza object itself. Older implementations of stanza objects used XML objects directly, - requiring the use of .attrib to access attribute values. + requiring the use of ``.attrib`` to access attribute values. Use of the dictionary syntax with the stanza object itself for accessing stanza interfaces is preferred. + + .. deprecated:: 1.0 """ return self def _fix_ns(self, xpath, split=False, propagate_ns=True): - """ - Apply the stanza's namespace to elements in an XPath expression. - - Arguments: - xpath -- The XPath expression to fix with namespaces. - split -- Indicates if the fixed XPath should be left as a - list of element names with namespaces. Defaults to - False, which returns a flat string path. - propagate_ns -- Overrides propagating parent element namespaces - to child elements. Useful if you wish to simply - split an XPath that has non-specified namespaces, - and child and parent namespaces are known not to - always match. Defaults to True. + """Apply the stanza's namespace to elements in an XPath expression. + + :param string xpath: The XPath expression to fix with namespaces. + :param bool split: Indicates if the fixed XPath should be left as a + list of element names with namespaces. Defaults to + False, which returns a flat string path. + :param bool propagate_ns: Overrides propagating parent element + namespaces to child elements. Useful if + you wish to simply split an XPath that has + non-specified namespaces, and child and + parent namespaces are known not to always + match. Defaults to True. """ fixed = [] # Split the XPath into a series of blocks, where a block @@ -975,14 +993,12 @@ class ElementBase(object): return '/'.join(fixed) def __eq__(self, other): - """ - Compare the stanza object with another to test for equality. + """Compare the stanza object with another to test for equality. Stanzas are equal if their interfaces return the same values, and if they are both instances of ElementBase. - Arguments: - other -- The stanza object to compare against. + :param ElementBase other: The stanza object to compare against. """ if not isinstance(other, ElementBase): return False @@ -1004,42 +1020,35 @@ class ElementBase(object): return True def __ne__(self, other): - """ - Compare the stanza object with another to test for inequality. + """Compare the stanza object with another to test for inequality. Stanzas are not equal if their interfaces return different values, or if they are not both instances of ElementBase. - Arguments: - other -- The stanza object to compare against. + :param ElementBase other: The stanza object to compare against. """ return not self.__eq__(other) def __bool__(self): - """ - Stanza objects should be treated as True in boolean contexts. + """Stanza objects should be treated as True in boolean contexts. Python 3.x version. """ return True def __nonzero__(self): - """ - Stanza objects should be treated as True in boolean contexts. + """Stanza objects should be treated as True in boolean contexts. Python 2.x version. """ return True def __len__(self): - """ - Return the number of iterable substanzas contained in this stanza. - """ + """Return the number of iterable substanzas in this stanza.""" return len(self.iterables) def __iter__(self): - """ - Return an iterator object for iterating over the stanza's substanzas. + """Return an iterator object for the stanza's substanzas. The iterator is the stanza object itself. Attempting to use two iterators on the same stanza at the same time is discouraged. @@ -1048,9 +1057,7 @@ class ElementBase(object): return self def __next__(self): - """ - Return the next iterable substanza. - """ + """Return the next iterable substanza.""" self._index += 1 if self._index > len(self.iterables): self._index = 0 @@ -1058,19 +1065,18 @@ class ElementBase(object): return self.iterables[self._index - 1] def __copy__(self): - """ - Return a copy of the stanza object that does not share the same + """Return a copy of the stanza object that does not share the same underlying XML object. """ return self.__class__(xml=copy.deepcopy(self.xml), parent=self.parent) def __str__(self, top_level_ns=True): - """ - Return a string serialization of the underlying XML object. + """Return a string serialization of the underlying XML object. - Arguments: - top_level_ns -- Display the top-most namespace. - Defaults to True. + .. seealso:: :ref:`tostring` + + :param bool top_level_ns: Display the top-most namespace. + Defaults to True. """ stanza_ns = '' if top_level_ns else self.namespace return tostring(self.xml, xmlns='', @@ -1078,72 +1084,54 @@ class ElementBase(object): top_level=not top_level_ns) def __repr__(self): - """ - Use the stanza's serialized XML as its representation. - """ + """Use the stanza's serialized XML as its representation.""" return self.__str__() class StanzaBase(ElementBase): """ - StanzaBase provides the foundation for all other stanza objects used by - SleekXMPP, and defines a basic set of interfaces common to nearly - all stanzas. These interfaces are the 'id', 'type', 'to', and 'from' - attributes. An additional interface, 'payload', is available to access - the XML contents of the stanza. Most stanza objects will provided more - specific interfaces, however. - - Stanza Interface: - from -- A JID object representing the sender's JID. - id -- An optional id value that can be used to associate stanzas - with their replies. - payload -- The XML contents of the stanza. - to -- A JID object representing the recipient's JID. - type -- The type of stanza, typically will be 'normal', 'error', - 'get', or 'set', etc. - - Attributes: - stream -- The XMLStream instance that will handle sending this stanza. - - Methods: - set_type -- Set the type of the stanza. - get_to -- Return the stanza recipients JID. - set_to -- Set the stanza recipient's JID. - get_from -- Return the stanza sender's JID. - set_from -- Set the stanza sender's JID. - get_payload -- Return the stanza's XML contents. - set_payload -- Append to the stanza's XML contents. - del_payload -- Remove the stanza's XML contents. - reply -- Reset the stanza and modify the 'to' and 'from' - attributes to prepare for sending a reply. - error -- Set the stanza's type to 'error'. - unhandled -- Callback for when the stanza is not handled by a - stream handler. - exception -- Callback for if an exception is raised while - handling the stanza. - send -- Send the stanza using the stanza's stream. + StanzaBase provides the foundation for all other stanza objects used + by SleekXMPP, and defines a basic set of interfaces common to nearly + all stanzas. These interfaces are the ``'id'``, ``'type'``, ``'to'``, + and ``'from'`` attributes. An additional interface, ``'payload'``, is + available to access the XML contents of the stanza. Most stanza objects + will provided more specific interfaces, however. + + **Stanza Interfaces:** + + :id: An optional id value that can be used to associate stanzas + :to: A JID object representing the recipient's JID. + :from: A JID object representing the sender's JID. + with their replies. + :type: The type of stanza, typically will be ``'normal'``, + ``'error'``, ``'get'``, or ``'set'``, etc. + :payload: The XML contents of the stanza. + + :param XMLStream stream: Optional :class:`sleekxmpp.xmlstream.XMLStream` + object responsible for sending this stanza. + :param XML xml: Optional XML contents to initialize stanza values. + :param string stype: Optional stanza type value. + :param sto: Optional string or :class:`sleekxmpp.xmlstream.JID` + object of the recipient's JID. + :param sfrom: Optional string or :class:`sleekxmpp.xmlstream.JID` + object of the sender's JID. + :param string sid: Optional ID value for the stanza. """ - name = 'stanza' + #: The default XMPP client namespace namespace = 'jabber:client' + + #: There is a small set of attributes which apply to all XMPP stanzas: + #: the stanza type, the to and from JIDs, the stanza ID, and, especially + #: in the case of an Iq stanza, a payload. interfaces = set(('type', 'to', 'from', 'id', 'payload')) + + #: A basic set of allowed values for the ``'type'`` interface. types = set(('get', 'set', 'error', None, 'unavailable', 'normal', 'chat')) - sub_interfaces = tuple() def __init__(self, stream=None, xml=None, stype=None, sto=None, sfrom=None, sid=None): - """ - Create a new stanza. - - Arguments: - stream -- Optional XMLStream responsible for sending this stanza. - xml -- Optional XML contents to initialize stanza values. - stype -- Optional stanza type value. - sto -- Optional string or JID object of the recipient's JID. - sfrom -- Optional string or JID object of the sender's JID. - sid -- Optional ID value for the stanza. - """ self.stream = stream if stream is not None: self.namespace = stream.default_ns @@ -1157,38 +1145,34 @@ class StanzaBase(ElementBase): self.tag = "{%s}%s" % (self.namespace, self.name) def set_type(self, value): - """ - Set the stanza's 'type' attribute. + """Set the stanza's ``'type'`` attribute. - Only type values contained in StanzaBase.types are accepted. + Only type values contained in :attr:`types` are accepted. - Arguments: - value -- One of the values contained in StanzaBase.types + :param string value: One of the values contained in :attr:`types` """ if value in self.types: self.xml.attrib['type'] = value return self def get_to(self): - """Return the value of the stanza's 'to' attribute.""" + """Return the value of the stanza's ``'to'`` attribute.""" return JID(self._get_attr('to')) def set_to(self, value): - """ - Set the 'to' attribute of the stanza. + """Set the ``'to'`` attribute of the stanza. - Arguments: - value -- A string or JID object representing the recipient's JID. + :param value: A string or :class:`sleekxmpp.xmlstream.JID` object + representing the recipient's JID. """ return self._set_attr('to', str(value)) def get_from(self): - """Return the value of the stanza's 'from' attribute.""" + """Return the value of the stanza's ``'from'`` attribute.""" return JID(self._get_attr('from')) def set_from(self, value): - """ - Set the 'from' attribute of the stanza. + """Set the 'from' attribute of the stanza. Arguments: from -- A string or JID object representing the sender's JID. @@ -1200,12 +1184,10 @@ class StanzaBase(ElementBase): return self.xml.getchildren() def set_payload(self, value): - """ - Add XML content to the stanza. + """Add XML content to the stanza. - Arguments: - value -- Either an XML or a stanza object, or a list - of XML or stanza objects. + :param value: Either an XML or a stanza object, or a list + of XML or stanza objects. """ if not isinstance(value, list): value = [value] @@ -1219,16 +1201,17 @@ class StanzaBase(ElementBase): return self def reply(self, clear=True): - """ - Swap the 'from' and 'to' attributes to prepare the stanza for - sending a reply. If clear=True, then also remove the stanza's + """Prepare the stanza for sending a reply. + + Swaps the ``'from'`` and ``'to'`` attributes. + + If ``clear=True``, then also remove the stanza's contents to make room for the reply content. - For client streams, the 'from' attribute is removed. + For client streams, the ``'from'`` attribute is removed. - Arguments: - clear -- Indicates if the stanza's contents should be - removed. Defaults to True + :param bool clear: Indicates if the stanza's contents should be + removed. Defaults to ``True``. """ # if it's a component, use from if self.stream and hasattr(self.stream, "is_component") and \ @@ -1242,53 +1225,46 @@ class StanzaBase(ElementBase): return self def error(self): - """Set the stanza's type to 'error'.""" + """Set the stanza's type to ``'error'``.""" self['type'] = 'error' return self def unhandled(self): - """ - Called when no handlers have been registered to process this - stanza. + """Called if no handlers have been registered to process this stanza. Meant to be overridden. """ pass def exception(self, e): - """ - Handle exceptions raised during stanza processing. + """Handle exceptions raised during stanza processing. Meant to be overridden. """ - log.exception('Error handling {%s}%s stanza' % (self.namespace, - self.name)) + log.exception('Error handling {%s}%s stanza', self.namespace, + self.name) def send(self, now=False): - """ - Queue the stanza to be sent on the XML stream. - Arguments: - now -- Indicates if the queue should be skipped and the - stanza sent immediately. Useful for stream - initialization. Defaults to False. + """Queue the stanza to be sent on the XML stream. + + :param bool now: Indicates if the queue should be skipped and the + stanza sent immediately. Useful for stream + initialization. Defaults to ``False``. """ self.stream.send_raw(self.__str__(), now=now) def __copy__(self): - """ - Return a copy of the stanza object that does not share the + """Return a copy of the stanza object that does not share the same underlying XML object, but does share the same XML stream. """ return self.__class__(xml=copy.deepcopy(self.xml), stream=self.stream) def __str__(self, top_level_ns=False): - """ - Serialize the stanza's XML to a string. + """Serialize the stanza's XML to a string. - Arguments: - top_level_ns -- Display the top-most namespace. - Defaults to False. + :param bool top_level_ns: Display the top-most namespace. + Defaults to ``False``. """ stanza_ns = '' if top_level_ns else self.namespace return tostring(self.xml, xmlns='', @@ -1297,6 +1273,27 @@ class StanzaBase(ElementBase): top_level=not top_level_ns) +#: A JSON/dictionary version of the XML content exposed through +#: the stanza interfaces:: +#: +#: >>> msg = Message() +#: >>> msg.values +#: {'body': '', 'from': , 'mucnick': '', 'mucroom': '', +#: 'to': , 'type': 'normal', 'id': '', 'subject': ''} +#: +#: Likewise, assigning to the :attr:`values` will change the XML +#: content:: +#: +#: >>> msg = Message() +#: >>> msg.values = {'body': 'Hi!', 'to': 'user@example.com'} +#: >>> msg +#: '<message to="user@example.com"><body>Hi!</body></message>' +#: +#: Child stanzas are exposed as nested dictionaries. +ElementBase.values = property(ElementBase._get_stanza_values, + ElementBase._set_stanza_values) + + # To comply with PEP8, method names now use underscores. # Deprecated method names are re-mapped for backwards compatibility. ElementBase.initPlugin = ElementBase.init_plugin diff --git a/sleekxmpp/xmlstream/tostring.py b/sleekxmpp/xmlstream/tostring.py index f9674b15..8e729f79 100644 --- a/sleekxmpp/xmlstream/tostring.py +++ b/sleekxmpp/xmlstream/tostring.py @@ -1,9 +1,16 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.tostring + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + This module converts XML objects into Unicode strings and + intelligently includes namespaces only when necessary to + keep the output readable. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ import sys @@ -14,26 +21,28 @@ if sys.version_info < (3, 0): def tostring(xml=None, xmlns='', stanza_ns='', stream=None, outbuffer='', top_level=False): - """ - Serialize an XML object to a Unicode string. - - If namespaces are provided using xmlns or stanza_ns, then elements - that use those namespaces will not include the xmlns attribute in - the output. - - Arguments: - xml -- The XML object to serialize. If the value is None, - then the XML object contained in this stanza - object will be used. - xmlns -- Optional namespace of an element wrapping the XML - object. - stanza_ns -- The namespace of the stanza object that contains - the XML object. - stream -- The XML stream that generated the XML object. - outbuffer -- Optional buffer for storing serializations during - recursive calls. - top_level -- Indicates that the element is the outermost - element. + """Serialize an XML object to a Unicode string. + + If namespaces are provided using ``xmlns`` or ``stanza_ns``, then + elements that use those namespaces will not include the xmlns attribute + in the output. + + :param XML xml: The XML object to serialize. + :param string xmlns: Optional namespace of an element wrapping the XML + object. + :param string stanza_ns: The namespace of the stanza object that contains + the XML object. + :param stream: The XML stream that generated the XML object. + :param string outbuffer: Optional buffer for storing serializations + during recursive calls. + :param bool top_level: Indicates that the element is the outermost + element. + + + :type xml: :py:class:`~xml.etree.ElementTree.Element` + :type stream: :class:`~sleekxmpp.xmlstream.xmlstream.XMLStream` + + :rtype: Unicode string """ # Add previous results to the start of the output. output = [outbuffer] @@ -102,11 +111,10 @@ def tostring(xml=None, xmlns='', stanza_ns='', stream=None, def xml_escape(text): - """ - Convert special characters in XML to escape sequences. + """Convert special characters in XML to escape sequences. - Arguments: - text -- The XML text to convert. + :param string text: The XML text to convert. + :rtype: Unicode string """ if sys.version_info < (3, 0): if type(text) != types.UnicodeType: diff --git a/sleekxmpp/xmlstream/xmlstream.py b/sleekxmpp/xmlstream/xmlstream.py index b6d013b0..fb9f91bc 100644 --- a/sleekxmpp/xmlstream/xmlstream.py +++ b/sleekxmpp/xmlstream/xmlstream.py @@ -1,9 +1,16 @@ +# -*- coding: utf-8 -*- """ - SleekXMPP: The Sleek XMPP Library - Copyright (C) 2010 Nathanael C. Fritz - This file is part of SleekXMPP. + sleekxmpp.xmlstream.xmlstream + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - See the file LICENSE for copying permission. + This module provides the module for creating and + interacting with generic XML streams, along with + the necessary eventing infrastructure. + + Part of SleekXMPP: The Sleek XMPP Library + + :copyright: (c) 2011 Nathanael C. Fritz + :license: MIT, see LICENSE for more details """ from __future__ import with_statement, unicode_literals @@ -45,24 +52,35 @@ else: DNSPYTHON = True -# The time in seconds to wait before timing out waiting for response stanzas. +#: The time in seconds to wait before timing out waiting for response stanzas. RESPONSE_TIMEOUT = 30 -# The time in seconds to wait for events from the event queue, and also the -# time between checks for the process stop signal. +#: The time in seconds to wait for events from the event queue, and also the +#: time between checks for the process stop signal. WAIT_TIMEOUT = 1 -# The number of threads to use to handle XML stream events. This is not the -# same as the number of custom event handling threads. HANDLER_THREADS must -# be at least 1. +#: The number of threads to use to handle XML stream events. This is not the +#: same as the number of custom event handling threads. +#: :data:`HANDLER_THREADS` must be at least 1. For Python implementations +#: with a GIL, this should be left at 1, but for implemetnations without +#: a GIL increasing this value can provide better performance. HANDLER_THREADS = 1 -# Flag indicating if the SSL library is available for use. +#: Flag indicating if the SSL library is available for use. SSL_SUPPORT = True -# Maximum time to delay between connection attempts is one hour. +#: The time in seconds to delay between attempts to resend data +#: after an SSL error. +SSL_RETRY_DELAY = 0.5 + +#: The maximum number of times to attempt resending data due to +#: an SSL error. +SSL_RETRY_MAX = 10 + +#: Maximum time to delay between connection attempts is one hour. RECONNECT_MAX_DELAY = 600 + log = logging.getLogger(__name__) @@ -85,115 +103,83 @@ class XMLStream(object): streams should be complete and valid XML documents. Three types of events are provided to manage the stream: - Stream -- Triggered based on received stanzas, similar in concept - to events in a SAX XML parser. - Custom -- Triggered manually. - Scheduled -- Triggered based on time delays. + :Stream: Triggered based on received stanzas, similar in concept + to events in a SAX XML parser. + :Custom: Triggered manually. + :Scheduled: Triggered based on time delays. Typically, stanzas are first processed by a stream event handler which will then trigger custom events to continue further processing, especially since custom event handlers may run in individual threads. - - Attributes: - address -- The hostname and port of the server. - default_ns -- The default XML namespace that will be applied - to all non-namespaced stanzas. - event_queue -- A queue of stream, custom, and scheduled - events to be processed. - filesocket -- A filesocket created from the main connection socket. - Required for ElementTree.iterparse. - default_port -- Default port to connect to. - namespace_map -- Optional mapping of namespaces to namespace prefixes. - scheduler -- A scheduler object for triggering events - after a given period of time. - send_queue -- A queue of stanzas to be sent on the stream. - socket -- The connection to the server. - ssl_support -- Indicates if a SSL library is available for use. - ssl_version -- The version of the SSL protocol to use. - Defaults to ssl.PROTOCOL_TLSv1. - ca_certs -- File path to a CA certificate to verify the - server's identity. - state -- A state machine for managing the stream's - connection state. - stream_footer -- The start tag and any attributes for the stream's - root element. - stream_header -- The closing tag of the stream's root element. - use_ssl -- Flag indicating if SSL should be used. - use_tls -- Flag indicating if TLS should be used. - use_proxy -- Flag indicating that an HTTP Proxy should be used. - stop -- threading Event used to stop all threads. - proxy_config -- An optional dictionary with the following entries: - host -- The host offering proxy services. - port -- The port for the proxy service. - username -- Optional username for the proxy. - password -- Optional password for the proxy. - - auto_reconnect -- Flag to determine whether we auto reconnect. - reconnect_max_delay -- Maximum time to delay between connection - attempts. Defaults to RECONNECT_MAX_DELAY, - which is one hour. - dns_answers -- List of dns answers not yet used to connect. - - Methods: - add_event_handler -- Add a handler for a custom event. - add_handler -- Shortcut method for registerHandler. - connect -- Connect to the given server. - del_event_handler -- Remove a handler for a custom event. - disconnect -- Disconnect from the server and terminate - processing. - event -- Trigger a custom event. - get_id -- Return the current stream ID. - incoming_filter -- Optionally filter stanzas before processing. - new_id -- Generate a new, unique ID value. - process -- Read XML stanzas from the stream and apply - matching stream handlers. - reconnect -- Reestablish a connection to the server. - register_handler -- Add a handler for a stream event. - register_stanza -- Add a new stanza object type that may appear - as a direct child of the stream's root. - remove_handler -- Remove a stream handler. - remove_stanza -- Remove a stanza object type. - schedule -- Schedule an event handler to execute after a - given delay. - send -- Send a stanza object on the stream. - send_raw -- Send a raw string on the stream. - send_xml -- Send an XML string on the stream. - set_socket -- Set the stream's socket and generate a new - filesocket. - start_stream_handler -- Perform any stream initialization such - as handshakes. - start_tls -- Establish a TLS connection and restart - the stream. + :param socket: Use an existing socket for the stream. Defaults to + ``None`` to generate a new socket. + :param string host: The name of the target server. + :param int port: The port to use for the connection. Defaults to 0. """ def __init__(self, socket=None, host='', port=0): - """ - Establish a new XML stream. - - Arguments: - socket -- Use an existing socket for the stream. - Defaults to None to generate a new socket. - host -- The name of the target server. - Defaults to the empty string. - port -- The port to use for the connection. - Defaults to 0. - """ + #: Flag indicating if the SSL library is available for use. self.ssl_support = SSL_SUPPORT + + #: Most XMPP servers support TLSv1, but OpenFire in particular + #: does not work well with it. For OpenFire, set + #: :attr:`ssl_version` to use ``SSLv23``:: + #: + #: import ssl + #: xmpp.ssl_version = ssl.PROTOCOL_SSLv23 self.ssl_version = ssl.PROTOCOL_TLSv1 + + #: Path to a file containing certificates for verifying the + #: server SSL certificate. A non-``None`` value will trigger + #: certificate checking. + #: + #: .. note:: + #: + #: On Mac OS X, certificates in the system keyring will + #: be consulted, even if they are not in the provided file. self.ca_certs = None + #: The time in seconds to wait for events from the event queue, + #: and also the time between checks for the process stop signal. self.wait_timeout = WAIT_TIMEOUT + + #: The time in seconds to wait before timing out waiting + #: for response stanzas. self.response_timeout = RESPONSE_TIMEOUT + + #: The current amount to time to delay attempting to reconnect. + #: This value doubles (with some jitter) with each failed + #: connection attempt up to :attr:`reconnect_max_delay` seconds. self.reconnect_delay = None + + #: Maximum time to delay between connection attempts is one hour. self.reconnect_max_delay = RECONNECT_MAX_DELAY + #: The time in seconds to delay between attempts to resend data + #: after an SSL error. + self.ssl_retry_max = SSL_RETRY_MAX + + #: The maximum number of times to attempt resending data due to + #: an SSL error. + self.ssl_retry_delay = SSL_RETRY_DELAY + + #: The connection state machine tracks if the stream is + #: ``'connected'`` or ``'disconnected'``. self.state = StateMachine(('disconnected', 'connected')) self.state._set_state('disconnected') + #: The default port to return when querying DNS records. self.default_port = int(port) + + #: The domain to try when querying DNS records. self.default_domain = '' + + #: The desired, or actual, address of the connected server. self.address = (host, int(port)) + + #: A file-like wrapper for the socket for use with the + #: :mod:`~xml.etree.ElementTree` module. self.filesocket = None self.set_socket(socket) @@ -202,31 +188,79 @@ class XMLStream(object): else: self.socket_class = Socket.socket + #: Enable connecting to the server directly over SSL, in + #: particular when the service provides two ports: one for + #: non-SSL traffic and another for SSL traffic. self.use_ssl = False + + #: Enable connecting to the service without using SSL + #: immediately, but allow upgrading the connection later + #: to use SSL. self.use_tls = False + + #: If set to ``True``, attempt to connect through an HTTP + #: proxy based on the settings in :attr:`proxy_config`. self.use_proxy = False + #: An optional dictionary of proxy settings. It may provide: + #: :host: The host offering proxy services. + #: :port: The port for the proxy service. + #: :username: Optional username for accessing the proxy. + #: :password: Optional password for accessing the proxy. self.proxy_config = {} + #: The default namespace of the stream content, not of the + #: stream wrapper itself. self.default_ns = '' + + #: The namespace of the enveloping stream element. self.stream_ns = '' + + #: The default opening tag for the stream element. self.stream_header = "<stream>" + + #: The default closing tag for the stream element. self.stream_footer = "</stream>" + #: If ``True``, periodically send a whitespace character over the + #: wire to keep the connection alive. Mainly useful for connections + #: traversing NAT. self.whitespace_keepalive = True + + #: The default interval between keepalive signals when + #: :attr:`whitespace_keepalive` is enabled. self.whitespace_keepalive_interval = 300 + #: An :class:`~threading.Event` to signal that the application + #: is stopping, and that all threads should shutdown. self.stop = threading.Event() + + #: An :class:`~threading.Event` to signal receiving a closing + #: stream tag from the server. self.stream_end_event = threading.Event() self.stream_end_event.set() + + #: An :class:`~threading.Event` to signal the start of a stream + #: session. Until this event fires, the send queue is not used + #: and data is sent immediately over the wire. self.session_started_event = threading.Event() + + #: The default time in seconds to wait for a session to start + #: after connecting before reconnecting and trying again. self.session_timeout = 45 + #: A queue of stream, custom, and scheduled events to be processed. self.event_queue = queue.Queue() + + #: A queue of string data to be sent over the stream. self.send_queue = queue.Queue() - self.__failed_send_stanza = None + + #: A :class:`~sleekxmpp.xmlstream.scheduler.Scheduler` instance for + #: executing callbacks in the future based on time delays. self.scheduler = Scheduler(self.stop) + self.__failed_send_stanza = None + #: A mapping of XML namespaces to well-known prefixes. self.namespace_map = {StanzaBase.xml_ns: 'xml'} self.__thread = {} @@ -238,7 +272,18 @@ class XMLStream(object): self._id = 0 self._id_lock = threading.Lock() + #: The :attr:`auto_reconnnect` setting controls whether or not + #: the stream will be restarted in the event of an error. self.auto_reconnect = True + + #: The :attr:`disconnect_wait` setting is the default value + #: for controlling if the system waits for the send queue to + #: empty before ending the stream. This may be overridden by + #: passing ``wait=True`` or ``wait=False`` to :meth:`disconnect`. + #: The default :attr:`disconnect_wait` value is ``False``. + self.disconnect_wait = False + + #: A list of DNS results that have not yet been tried. self.dns_answers = [] self.add_event_handler('connected', self._handle_connected) @@ -246,17 +291,16 @@ class XMLStream(object): self.add_event_handler('session_end', self._end_keepalive) def use_signals(self, signals=None): - """ - Register signal handlers for SIGHUP and SIGTERM, if possible, - which will raise a "killed" event when the application is - terminated. + """Register signal handlers for ``SIGHUP`` and ``SIGTERM``. + + By using signals, a ``'killed'`` event will be raised when the + application is terminated. If a signal handler already existed, it will be executed first, - before the "killed" event is raised. + before the ``'killed'`` event is raised. - Arguments: - signals -- A list of signal names to be monitored. - Defaults to ['SIGHUP', 'SIGTERM']. + :param list signals: A list of signal names to be monitored. + Defaults to ``['SIGHUP', 'SIGTERM']``. """ if signals is None: signals = ['SIGHUP', 'SIGTERM'] @@ -272,7 +316,7 @@ class XMLStream(object): def handle_kill(signum, frame): """ Capture kill event and disconnect cleanly after first - spawning the "killed" event. + spawning the ``'killed'`` event. """ if signum in existing_handlers and \ @@ -293,8 +337,7 @@ class XMLStream(object): "SleekXMPP is not running from a main thread.") def new_id(self): - """ - Generate and return a new stream ID in hexadecimal form. + """Generate and return a new stream ID in hexadecimal form. Many stanzas, handlers, or matchers may require unique ID values. Using this method ensures that all new ID values @@ -305,26 +348,25 @@ class XMLStream(object): return self.get_id() def get_id(self): - """ - Return the current unique stream ID in hexadecimal form. - """ + """Return the current unique stream ID in hexadecimal form.""" return "%X" % self._id def connect(self, host='', port=0, use_ssl=False, use_tls=True, reattempt=True): - """ - Create a new socket and connect to the server. - - Setting reattempt to True will cause connection attempts to be made - every second until a successful connection is established. - - Arguments: - host -- The name of the desired server for the connection. - port -- Port to connect to on the server. - use_ssl -- Flag indicating if SSL should be used. - use_tls -- Flag indicating if TLS should be used. - reattempt -- Flag indicating if the socket should reconnect - after disconnections. + """Create a new socket and connect to the server. + + Setting ``reattempt`` to ``True`` will cause connection attempts to + be made every second until a successful connection is established. + + :param host: The name of the desired server for the connection. + :param port: Port to connect to on the server. + :param use_ssl: Flag indicating if SSL should be used by connecting + directly to a port using SSL. + :param use_tls: Flag indicating if TLS should be used, allowing for + connecting to a port without using SSL immediately and + later upgrading the connection. + :param reattempt: Flag indicating if the socket should reconnect + after disconnections. """ if host and port: self.address = (host, int(port)) @@ -343,7 +385,7 @@ class XMLStream(object): # is established. connected = self.state.transition('disconnected', 'connected', func=self._connect) - while reattempt and not connected: + while reattempt and not connected and not self.stop.is_set(): connected = self.state.transition('disconnected', 'connected', func=self._connect) return connected @@ -362,8 +404,18 @@ class XMLStream(object): else: delay = min(self.reconnect_delay * 2, self.reconnect_max_delay) delay = random.normalvariate(delay, delay * 0.1) - log.debug('Waiting %s seconds before connecting.' % delay) - time.sleep(delay) + log.debug('Waiting %s seconds before connecting.', delay) + elapsed = 0 + try: + while elapsed < delay and not self.stop.is_set(): + time.sleep(0.1) + elapsed += 0.1 + except KeyboardInterrupt: + self.stop.set() + return False + except SystemExit: + self.stop.set() + return False if self.use_proxy: connected = self._connect_proxy() @@ -391,7 +443,7 @@ class XMLStream(object): try: if not self.use_proxy: - log.debug("Connecting to %s:%s" % self.address) + log.debug("Connecting to %s:%s", *self.address) self.socket.connect(self.address) self.set_socket(self.socket, ignore=True) @@ -402,8 +454,8 @@ class XMLStream(object): except Socket.error as serr: error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" self.event('socket_error', serr) - log.error(error_msg % (self.address[0], self.address[1], - serr.errno, serr.strerror)) + log.error(error_msg, self.address[0], self.address[1], + serr.errno, serr.strerror) self.reconnect_delay = delay return False @@ -435,18 +487,18 @@ class XMLStream(object): headers = '\r\n'.join(headers) + '\r\n\r\n' try: - log.debug("Connecting to proxy: %s:%s" % address) + log.debug("Connecting to proxy: %s:%s", address) self.socket.connect(address) self.send_raw(headers, now=True) resp = '' - while '\r\n\r\n' not in resp: + while '\r\n\r\n' not in resp and not self.stop.is_set(): resp += self.socket.recv(1024).decode('utf-8') - log.debug('RECV: %s' % resp) + log.debug('RECV: %s', resp) lines = resp.split('\r\n') if '200' not in lines[0]: self.event('proxy_error', resp) - log.error('Proxy Error: %s' % lines[0]) + log.error('Proxy Error: %s', lines[0]) return False # Proxy connection established, continue connecting @@ -455,8 +507,8 @@ class XMLStream(object): except Socket.error as serr: error_msg = "Could not connect to %s:%s. Socket Error #%s: %s" self.event('socket_error', serr) - log.error(error_msg % (self.address[0], self.address[1], - serr.errno, serr.strerror)) + log.error(error_msg, self.address[0], self.address[1], + serr.errno, serr.strerror) return False def _handle_connected(self, event=None): @@ -466,42 +518,48 @@ class XMLStream(object): """ def _handle_session_timeout(): - if not self.session_started_event.isSet(): + if not self.session_started_event.is_set(): log.debug("Session start has taken more " + \ - "than %d seconds" % self.session_timeout) + "than %d seconds", self.session_timeout) self.disconnect(reconnect=self.auto_reconnect) self.schedule("Session timeout check", self.session_timeout, _handle_session_timeout) - - def disconnect(self, reconnect=False, wait=False): - """ - Terminate processing and close the XML streams. + def disconnect(self, reconnect=False, wait=None): + """Terminate processing and close the XML streams. Optionally, the connection may be reconnected and resume processing afterwards. If the disconnect should take place after all items - in the send queue have been sent, use wait=True. However, - take note: If you are constantly adding items to the queue - such that it is never empty, then the disconnect will - not occur and the call will continue to block. - - Arguments: - reconnect -- Flag indicating if the connection - and processing should be restarted. - Defaults to False. - wait -- Flag indicating if the send queue should - be emptied before disconnecting. - """ - self.state.transition('connected', 'disconnected', wait=0.0, + in the send queue have been sent, use ``wait=True``. + + .. warning:: + + If you are constantly adding items to the queue + such that it is never empty, then the disconnect will + not occur and the call will continue to block. + + :param reconnect: Flag indicating if the connection + and processing should be restarted. + Defaults to ``False``. + :param wait: Flag indicating if the send queue should + be emptied before disconnecting, overriding + :attr:`disconnect_wait`. + """ + self.state.transition('connected', 'disconnected', func=self._disconnect, args=(reconnect, wait)) - def _disconnect(self, reconnect=False, wait=False): + def _disconnect(self, reconnect=False, wait=None): + self.event('session_end', direct=True) + # Wait for the send queue to empty. - if wait: + if wait is not None: + if wait: + self.send_queue.join() + elif self.disconnect_wait: self.send_queue.join() # Send the end of stream marker. @@ -510,7 +568,7 @@ class XMLStream(object): # Wait for confirmation that the stream was # closed in the other direction. self.auto_reconnect = reconnect - log.debug('Waiting for %s from server' % self.stream_footer) + log.debug('Waiting for %s from server', self.stream_footer) self.stream_end_event.wait(4) if not self.auto_reconnect: self.stop.set() @@ -522,35 +580,33 @@ class XMLStream(object): self.event('socket_error', serr) finally: #clear your application state - self.event('session_end', direct=True) self.event("disconnected", direct=True) return True - def reconnect(self): - """ - Reset the stream's state and reconnect to the server. - """ + def reconnect(self, reattempt=True): + """Reset the stream's state and reconnect to the server.""" log.debug("reconnecting...") - self.state.transition('connected', 'disconnected', wait=2.0, - func=self._disconnect, args=(True,)) + if self.state.ensure('connected'): + self.state.transition('connected', 'disconnected', wait=2.0, + func=self._disconnect, args=(True,)) log.debug("connecting...") connected = self.state.transition('disconnected', 'connected', wait=2.0, func=self._connect) - while not connected: + while reattempt and not connected and not self.stop.is_set(): connected = self.state.transition('disconnected', 'connected', wait=2.0, func=self._connect) + connected = connected or self.state.ensure('connected') return connected def set_socket(self, socket, ignore=False): - """ - Set the socket to use for the stream. + """Set the socket to use for the stream. The filesocket will be recreated as well. - Arguments: - socket -- The new socket to use. - ignore -- don't set the state + :param socket: The new socket object to use. + :param bool ignore: If ``True``, don't set the connection + state to ``'connected'``. """ self.socket = socket if socket is not None: @@ -568,8 +624,7 @@ class XMLStream(object): self.state._set_state('connected') def configure_socket(self): - """ - Set timeout and other options for self.socket. + """Set timeout and other options for self.socket. Meant to be overridden. """ @@ -577,31 +632,30 @@ class XMLStream(object): def configure_dns(self, resolver, domain=None, port=None): """ - Configure and set options for a dns.resolver.Resolver + Configure and set options for a :class:`~dns.resolver.Resolver` instance, and other DNS related tasks. For example, you - can also check Socket.getaddrinfo to see if you need to - call out to libresolv.so.2 to run res_init(). + can also check :meth:`~socket.socket.getaddrinfo` to see + if you need to call out to ``libresolv.so.2`` to + run ``res_init()``. Meant to be overridden. - Arguments: - resolver -- A dns.resolver.Resolver instance, or None - if dnspython is not installed. - domain -- The initial domain under consideration. - port -- The initial port under consideration. + :param resolver: A :class:`~dns.resolver.Resolver` instance + or ``None`` if ``dnspython`` is not installed. + :param domain: The initial domain under consideration. + :param port: The initial port under consideration. """ pass def start_tls(self): - """ - Perform handshakes for TLS. + """Perform handshakes for TLS. If the handshake is successful, the XML stream will need to be restarted. """ if self.ssl_support: log.info("Negotiating TLS") - log.info("Using SSL version: %s" % str(self.ssl_version)) + log.info("Using SSL version: %s", str(self.ssl_version)) if self.ca_certs is None: cert_policy = ssl.CERT_NONE else: @@ -627,13 +681,14 @@ class XMLStream(object): return False def _start_keepalive(self, event): - """ - Begin sending whitespace periodically to keep the connection alive. + """Begin sending whitespace periodically to keep the connection alive. + + May be disabled by setting:: - May be disabled by setting: self.whitespace_keepalive = False - The keepalive interval can be set using: + The keepalive interval can be set using:: + self.whitespace_keepalive_interval = 300 """ @@ -651,18 +706,18 @@ class XMLStream(object): self.scheduler.remove('Whitespace Keepalive') def start_stream_handler(self, xml): - """ - Perform any initialization actions, such as handshakes, once the - stream header has been sent. + """Perform any initialization actions, such as handshakes, + once the stream header has been sent. Meant to be overridden. """ pass def register_stanza(self, stanza_class): - """ - Add a stanza object class as a known root stanza. A root stanza is - one that appears as a direct child of the stream's root element. + """Add a stanza object class as a known root stanza. + + A root stanza is one that appears as a direct child of the stream's + root element. Stanzas that appear as substanzas of a root stanza do not need to be registered here. That is done using register_stanza_plugin() from @@ -672,15 +727,15 @@ class XMLStream(object): stanza objects, but may still be processed using handlers and matchers. - Arguments: - stanza_class -- The top-level stanza object's class. + :param stanza_class: The top-level stanza object's class. """ self.__root_stanza.append(stanza_class) def remove_stanza(self, stanza_class): - """ - Remove a stanza from being a known root stanza. A root stanza is - one that appears as a direct child of the stream's root element. + """Remove a stanza from being a known root stanza. + + A root stanza is one that appears as a direct child of the stream's + root element. Stanzas that are not registered will not be converted into stanza objects, but may still be processed using handlers and @@ -690,22 +745,24 @@ class XMLStream(object): def add_handler(self, mask, pointer, name=None, disposable=False, threaded=False, filter=False, instream=False): - """ - A shortcut method for registering a handler using XML masks. - - Arguments: - mask -- An XML snippet matching the structure of the - stanzas that will be passed to this handler. - pointer -- The handler function itself. - name -- A unique name for the handler. A name will - be generated if one is not provided. - disposable -- Indicates if the handler should be discarded - after one use. - threaded -- Deprecated. Remains for backwards compatibility. - filter -- Deprecated. Remains for backwards compatibility. - instream -- Indicates if the handler should execute during - stream processing and not during normal event - processing. + """A shortcut method for registering a handler using XML masks. + + The use of :meth:`register_handler()` is preferred. + + :param mask: An XML snippet matching the structure of the + stanzas that will be passed to this handler. + :param pointer: The handler function itself. + :parm name: A unique name for the handler. A name will + be generated if one is not provided. + :param disposable: Indicates if the handler should be discarded + after one use. + :param threaded: **DEPRECATED**. + Remains for backwards compatibility. + :param filter: **DEPRECATED**. + Remains for backwards compatibility. + :param instream: Indicates if the handler should execute during + stream processing and not during normal event + processing. """ # To prevent circular dependencies, we must load the matcher # and handler classes here. @@ -716,23 +773,20 @@ class XMLStream(object): once=disposable, instream=instream)) def register_handler(self, handler, before=None, after=None): - """ - Add a stream event handler that will be executed when a matching + """Add a stream event handler that will be executed when a matching stanza is received. - Arguments: - handler -- The handler object to execute. + :param handler: The :class:`~sleekxmpp.xmlstream.handler.base.BaseHandler` + derived object to execute. """ if handler.stream is None: self.__handlers.append(handler) handler.stream = weakref.ref(self) def remove_handler(self, name): - """ - Remove any stream event handlers with the given name. + """Remove any stream event handlers with the given name. - Arguments: - name -- The name of the handler. + :param name: The name of the handler. """ idx = 0 for handler in self.__handlers: @@ -743,12 +797,10 @@ class XMLStream(object): return False def get_dns_records(self, domain, port=None): - """ - Get the DNS records for a domain. + """Get the DNS records for a domain. - Arguments: - domain -- The domain in question. - port -- If the results don't include a port, use this one. + :param domain: The domain in question. + :param port: If the results don't include a port, use this one. """ if port is None: port = self.default_port @@ -759,11 +811,11 @@ class XMLStream(object): try: answers = resolver.query(domain, dns.rdatatype.A) except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer): - log.warning("No A records for %s" % domain) + log.warning("No A records for %s", domain) return [((domain, port), 0, 0)] except dns.exception.Timeout: log.warning("DNS resolution timed out " + \ - "for A record of %s" % domain) + "for A record of %s", domain) return [((domain, port), 0, 0)] else: return [((ans.address, port), 0, 0) for ans in answers] @@ -774,14 +826,13 @@ class XMLStream(object): return [((domain, port), 0, 0)] def pick_dns_answer(self, domain, port=None): - """ - Pick a server and port from DNS answers. + """Pick a server and port from DNS answers. + Gets DNS answers if none available. Removes used answer from available answers. - Arguments: - domain -- The domain in question. - port -- If the results don't include a port, use this one. + :param domain: The domain in question. + :param port: If the results don't include a port, use this one. """ if not self.dns_answers: self.dns_answers = self.get_dns_records(domain, port) @@ -808,35 +859,31 @@ class XMLStream(object): if self.dns_answers[0] == address: break self.dns_answers.pop(idx) - log.debug("Trying to connect to %s:%s" % address) + log.debug("Trying to connect to %s:%s", *address) return address def add_event_handler(self, name, pointer, threaded=False, disposable=False): - """ - Add a custom event handler that will be executed whenever + """Add a custom event handler that will be executed whenever its event is manually triggered. - Arguments: - name -- The name of the event that will trigger - this handler. - pointer -- The function to execute. - threaded -- If set to True, the handler will execute - in its own thread. Defaults to False. - disposable -- If set to True, the handler will be - discarded after one use. Defaults to False. + :param name: The name of the event that will trigger + this handler. + :param pointer: The function to execute. + :param threaded: If set to ``True``, the handler will execute + in its own thread. Defaults to ``False``. + :param disposable: If set to ``True``, the handler will be + discarded after one use. Defaults to ``False``. """ if not name in self.__event_handlers: self.__event_handlers[name] = [] self.__event_handlers[name].append((pointer, threaded, disposable)) def del_event_handler(self, name, pointer): - """ - Remove a function as a handler for an event. + """Remove a function as a handler for an event. - Arguments: - name -- The name of the event. - pointer -- The function to remove as a handler. + :param name: The name of the event. + :param pointer: The function to remove as a handler. """ if not name in self.__event_handlers: return @@ -851,42 +898,42 @@ class XMLStream(object): self.__event_handlers[name])) def event_handled(self, name): - """ - Indicates if an event has any associated handlers. - - Returns the number of registered handlers. + """Returns the number of registered handlers for an event. - Arguments: - name -- The name of the event to check. + :param name: The name of the event to check. """ return len(self.__event_handlers.get(name, [])) def event(self, name, data={}, direct=False): - """ - Manually trigger a custom event. - - Arguments: - name -- The name of the event to trigger. - data -- Data that will be passed to each event handler. - Defaults to an empty dictionary. - direct -- Runs the event directly if True, skipping the - event queue. All event handlers will run in the - same thread. - """ - for handler in self.__event_handlers.get(name, []): + """Manually trigger a custom event. + + :param name: The name of the event to trigger. + :param data: Data that will be passed to each event handler. + Defaults to an empty dictionary, but is usually + a stanza object. + :param direct: Runs the event directly if True, skipping the + event queue. All event handlers will run in the + same thread. + """ + handlers = self.__event_handlers.get(name, []) + for handler in handlers: + #TODO: Data should not be copied, but should be read only, + # but this might break current code so it's left for future. + + out_data = copy.copy(data) if len(handlers) > 1 else data + old_exception = getattr(data, 'exception', None) if direct: try: - handler[0](copy.copy(data)) + handler[0](out_data) except Exception as e: error_msg = 'Error processing event handler: %s' - log.exception(error_msg % str(handler[0])) - if hasattr(data, 'exception'): - data.exception(e) + log.exception(error_msg, str(handler[0])) + if old_exception: + old_exception(e) else: self.exception(e) else: - self.event_queue.put(('event', handler, copy.copy(data))) - + self.event_queue.put(('event', handler, out_data)) if handler[2]: # If the handler is disposable, we will go ahead and # remove it now instead of waiting for it to be @@ -900,25 +947,22 @@ class XMLStream(object): def schedule(self, name, seconds, callback, args=None, kwargs=None, repeat=False): - """ - Schedule a callback function to execute after a given delay. - - Arguments: - name -- A unique name for the scheduled callback. - seconds -- The time in seconds to wait before executing. - callback -- A pointer to the function to execute. - args -- A tuple of arguments to pass to the function. - kwargs -- A dictionary of keyword arguments to pass to - the function. - repeat -- Flag indicating if the scheduled event should - be reset and repeat after executing. + """Schedule a callback function to execute after a given delay. + + :param name: A unique name for the scheduled callback. + :param seconds: The time in seconds to wait before executing. + :param callback: A pointer to the function to execute. + :param args: A tuple of arguments to pass to the function. + :param kwargs: A dictionary of keyword arguments to pass to + the function. + :param repeat: Flag indicating if the scheduled event should + be reset and repeat after executing. """ self.scheduler.add(name, seconds, callback, args, kwargs, repeat, qpointer=self.event_queue) def incoming_filter(self, xml): - """ - Filter incoming XML objects before they are processed. + """Filter incoming XML objects before they are processed. Possible uses include remapping namespaces, or correcting elements from sources with incorrect behavior. @@ -928,23 +972,23 @@ class XMLStream(object): return xml def send(self, data, mask=None, timeout=None, now=False): - """ - A wrapper for send_raw for sending stanza objects. + """A wrapper for :meth:`send_raw()` for sending stanza objects. May optionally block until an expected response is received. - Arguments: - data -- The stanza object to send on the stream. - mask -- Deprecated. An XML snippet matching the structure - of the expected response. Execution will block - in this thread until the response is received - or a timeout occurs. - timeout -- Time in seconds to wait for a response before - continuing. Defaults to RESPONSE_TIMEOUT. - now -- Indicates if the send queue should be skipped, - sending the stanza immediately. Useful mainly - for stream initialization stanzas. - Defaults to False. + :param data: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + stanza to send on the stream. + :param mask: **DEPRECATED** + An XML string snippet matching the structure + of the expected response. Execution will block + in this thread until the response is received + or a timeout occurs. + :param int timeout: Time in seconds to wait for a response before + continuing. Defaults to :attr:`response_timeout`. + :param bool now: Indicates if the send queue should be skipped, + sending the stanza immediately. Useful mainly + for stream initialization stanzas. + Defaults to ``False``. """ if timeout is None: timeout = self.response_timeout @@ -961,53 +1005,64 @@ class XMLStream(object): return wait_for.wait(timeout) def send_xml(self, data, mask=None, timeout=None, now=False): - """ - Send an XML object on the stream, and optionally wait + """Send an XML object on the stream, and optionally wait for a response. - Arguments: - data -- The XML object to send on the stream. - mask -- Deprecated. An XML snippet matching the structure - of the expected response. Execution will block - in this thread until the response is received - or a timeout occurs. - timeout -- Time in seconds to wait for a response before - continuing. Defaults to RESPONSE_TIMEOUT. - now -- Indicates if the send queue should be skipped, - sending the stanza immediately. Useful mainly - for stream initialization stanzas. - Defaults to False. + :param data: The :class:`~xml.etree.ElementTree.Element` XML object + to send on the stream. + :param mask: **DEPRECATED** + An XML string snippet matching the structure + of the expected response. Execution will block + in this thread until the response is received + or a timeout occurs. + :param int timeout: Time in seconds to wait for a response before + continuing. Defaults to :attr:`response_timeout`. + :param bool now: Indicates if the send queue should be skipped, + sending the stanza immediately. Useful mainly + for stream initialization stanzas. + Defaults to ``False``. """ if timeout is None: timeout = self.response_timeout return self.send(tostring(data), mask, timeout, now) def send_raw(self, data, now=False, reconnect=None): - """ - Send raw data across the stream. - - Arguments: - data -- Any string value. - reconnect -- Indicates if the stream should be - restarted if there is an error sending - the stanza. Used mainly for testing. - Defaults to self.auto_reconnect. + """Send raw data across the stream. + + :param string data: Any string value. + :param bool reconnect: Indicates if the stream should be + restarted if there is an error sending + the stanza. Used mainly for testing. + Defaults to :attr:`auto_reconnect`. """ if now: - log.debug("SEND (IMMED): %s" % data) + log.debug("SEND (IMMED): %s", data) try: data = data.encode('utf-8') total = len(data) sent = 0 count = 0 + tries = 0 while sent < total and not self.stop.is_set(): - sent += self.socket.send(data[sent:]) - count += 1 + try: + sent += self.socket.send(data[sent:]) + count += 1 + except ssl.SSLError as serr: + if tries >= self.ssl_retry_max: + log.debug('SSL error - max retries reached') + self.exception(serr) + log.warning("Failed to send %s", data) + if reconnect is None: + reconnect = self.auto_reconnect + self.disconnect(reconnect) + log.warning('SSL write error - reattempting') + time.sleep(self.ssl_retry_delay) + tries += 1 if count > 1: - log.debug('SENT: %d chunks' % count) + log.debug('SENT: %d chunks', count) except Socket.error as serr: self.event('socket_error', serr) - log.warning("Failed to send %s" % data) + log.warning("Failed to send %s", data) if reconnect is None: reconnect = self.auto_reconnect self.disconnect(reconnect) @@ -1016,27 +1071,29 @@ class XMLStream(object): return True def process(self, **kwargs): - """ - Initialize the XML streams and begin processing events. + """Initialize the XML streams and begin processing events. The number of threads used for processing stream events is determined - by HANDLER_THREADS. - - Arguments: - block -- If block=False then event dispatcher will run - in a separate thread, allowing for the stream to be - used in the background for another application. - Otherwise, process(block=True) blocks the current thread. - Defaults to False. - - **threaded is deprecated and included for API compatibility** - threaded -- If threaded=True then event dispatcher will run - in a separate thread, allowing for the stream to be - used in the background for another application. - Defaults to True. - - Event handlers and the send queue will be threaded - regardless of these parameters. + by :data:`HANDLER_THREADS`. + + :param bool block: If ``False``, then event dispatcher will run + in a separate thread, allowing for the stream to be + used in the background for another application. + Otherwise, ``process(block=True)`` blocks the current + thread. Defaults to ``False``. + :param bool threaded: **DEPRECATED** + If ``True``, then event dispatcher will run + in a separate thread, allowing for the stream to be + used in the background for another application. + Defaults to ``True``. This does **not** mean that no + threads are used at all if ``threaded=False``. + + Regardless of these threading options, these threads will + always exist: + + - The event queue processor + - The send queue processor + - The scheduler """ if 'threaded' in kwargs and 'block' in kwargs: raise ValueError("process() called with both " + \ @@ -1050,7 +1107,6 @@ class XMLStream(object): def start_thread(name, target): self.__thread[name] = threading.Thread(name=name, target=target) - self.__thread[name].daemon = True self.__thread[name].start() for t in range(0, HANDLER_THREADS): @@ -1066,8 +1122,7 @@ class XMLStream(object): self._process() def _process(self): - """ - Start processing the XML streams. + """Start processing the XML streams. Processing will continue after any recoverable errors if reconnections are allowed. @@ -1077,6 +1132,7 @@ class XMLStream(object): # Additional passes will be made only if an error occurs and # reconnecting is permitted. while True: + shutdown = False try: # The call to self.__read_xml will block and prevent # the body of the loop from running until a disconnect @@ -1094,33 +1150,36 @@ class XMLStream(object): if not self.__read_xml(): # If the server terminated the stream, end processing break - except SyntaxError as e: - log.error("Error reading from XML stream.") - self.exception(e) except KeyboardInterrupt: log.debug("Keyboard Escape Detected in _process") - self.stop.set() + self.event('killed', direct=True) + shutdown = True except SystemExit: log.debug("SystemExit in _process") - self.stop.set() - self.scheduler.quit() + shutdown = True + except SyntaxError as e: + log.error("Error reading from XML stream.") + shutdown = True + self.exception(e) except Socket.error as serr: self.event('socket_error', serr) log.exception('Socket Error') - except: + except Exception as e: if not self.stop.is_set(): log.exception('Connection error.') + self.exception(e) - if not self.stop.is_set() and self.auto_reconnect: + if not shutdown and not self.stop.is_set() \ + and self.auto_reconnect: self.reconnect() else: self.disconnect() break def __read_xml(self): - """ - Parse the incoming XML stream, raising stream events for - each received stanza. + """Parse the incoming XML stream + + Stream events are raised for each received stanza. """ depth = 0 root = None @@ -1156,16 +1215,16 @@ class XMLStream(object): log.debug("Ending read XML loop") def _build_stanza(self, xml, default_ns=None): - """ - Create a stanza object from a given XML object. + """Create a stanza object from a given XML object. If a specialized stanza type is not found for the XML, then - a generic StanzaBase stanza will be returned. + a generic :class:`~sleekxmpp.xmlstream.stanzabase.StanzaBase` + stanza will be returned. - Arguments: - xml -- The XML object to convert into a stanza object. - default_ns -- Optional default namespace to use instead of the - stream's current default namespace. + :param xml: The :class:`~xml.etree.ElementTree.Element` XML object + to convert into a stanza object. + :param default_ns: Optional default namespace to use instead of the + stream's current default namespace. """ if default_ns is None: default_ns = self.default_ns @@ -1184,11 +1243,10 @@ class XMLStream(object): objects if applicable and queue stream events to be processed by matching handlers. - Arguments: - xml -- The XML stanza to analyze. + :param xml: The :class:`~sleekxmpp.xmlstream.stanzabase.ElementBase` + stanza to analyze. """ - log.debug("RECV: %s" % tostring(xml, - xmlns=self.default_ns, + log.debug("RECV: %s", tostring(xml, xmlns=self.default_ns, stream=self)) # Apply any preprocessing filters. xml = self.incoming_filter(xml) @@ -1201,17 +1259,20 @@ class XMLStream(object): # to run "in stream" will be executed immediately; the rest will # be queued. unhandled = True - for handler in self.__handlers: - if handler.match(stanza): + matched_handlers = [h for h in self.__handlers if h.match(stanza)] + for handler in matched_handlers: + if len(matched_handlers) > 1: stanza_copy = copy.copy(stanza) - handler.prerun(stanza_copy) - self.event_queue.put(('stanza', handler, stanza_copy)) - try: - if handler.check_delete(): - self.__handlers.remove(handler) - except: - pass # not thread safe - unhandled = False + else: + stanza_copy = stanza + handler.prerun(stanza_copy) + self.event_queue.put(('stanza', handler, stanza_copy)) + try: + if handler.check_delete(): + self.__handlers.remove(handler) + except: + pass # not thread safe + unhandled = False # Some stanzas require responses, such as Iq queries. A default # handler will be executed immediately for this case. @@ -1219,28 +1280,26 @@ class XMLStream(object): stanza.unhandled() def _threaded_event_wrapper(self, func, args): - """ - Capture exceptions for event handlers that run + """Capture exceptions for event handlers that run in individual threads. - Arguments: - func -- The event handler to execute. - args -- Arguments to the event handler. + :param func: The event handler to execute. + :param args: Arguments to the event handler. """ - orig = copy.copy(args[0]) + # this is always already copied before this is invoked + orig = args[0] try: func(*args) except Exception as e: error_msg = 'Error processing event handler: %s' - log.exception(error_msg % str(func)) + log.exception(error_msg, str(func)) if hasattr(orig, 'exception'): orig.exception(e) else: self.exception(e) def _event_runner(self): - """ - Process the event queue and execute handlers. + """Process the event queue and execute handlers. The number of event runner threads is controlled by HANDLER_THREADS. @@ -1249,7 +1308,7 @@ class XMLStream(object): """ log.debug("Loading event runner") try: - while not self.stop.isSet(): + while not self.stop.is_set(): try: wait = self.wait_timeout event = self.event_queue.get(True, timeout=wait) @@ -1267,19 +1326,18 @@ class XMLStream(object): handler.run(args[0]) except Exception as e: error_msg = 'Error processing stream handler: %s' - log.exception(error_msg % handler.name) + log.exception(error_msg, handler.name) orig.exception(e) elif etype == 'schedule': name = args[1] try: - log.debug('Scheduled event: %s: %s' % (name, args[0])) + log.debug('Scheduled event: %s: %s', name, args[0]) handler(*args[0]) except Exception as e: log.exception('Error processing scheduled task') self.exception(e) elif etype == 'event': func, threaded, disposable = handler - orig = copy.copy(args[0]) try: if threaded: x = threading.Thread( @@ -1291,7 +1349,7 @@ class XMLStream(object): func(*args) except Exception as e: error_msg = 'Error processing event handler: %s' - log.exception(error_msg % str(func)) + log.exception(error_msg, str(func)) if hasattr(orig, 'exception'): orig.exception(e) else: @@ -1310,12 +1368,12 @@ class XMLStream(object): return def _send_thread(self): - """ - Extract stanzas from the send queue and send them on the stream. - """ + """Extract stanzas from the send queue and send them on the stream.""" try: while not self.stop.is_set(): - self.session_started_event.wait() + while not self.stop.is_set and \ + not self.session_started_event.is_set(): + self.session_started_event.wait(timeout=1) if self.__failed_send_stanza is not None: data = self.__failed_send_stanza self.__failed_send_stanza = None @@ -1324,37 +1382,48 @@ class XMLStream(object): data = self.send_queue.get(True, 1) except queue.Empty: continue - log.debug("SEND: %s" % data) + log.debug("SEND: %s", data) + enc_data = data.encode('utf-8') + total = len(enc_data) + sent = 0 + count = 0 + tries = 0 try: - enc_data = data.encode('utf-8') - total = len(enc_data) - sent = 0 - count = 0 while sent < total and not self.stop.is_set(): - sent += self.socket.send(enc_data[sent:]) - count += 1 + try: + sent += self.socket.send(enc_data[sent:]) + count += 1 + except ssl.SSLError as serr: + if tries >= self.ssl_retry_max: + log.debug('SSL error - max retries reached') + self.exception(serr) + log.warning("Failed to send %s", data) + if reconnect is None: + reconnect = self.auto_reconnect + self.disconnect(reconnect) + log.warning('SSL write error - reattempting') + time.sleep(self.ssl_retry_delay) + tries += 1 if count > 1: - log.debug('SENT: %d chunks' % count) + log.debug('SENT: %d chunks', count) self.send_queue.task_done() except Socket.error as serr: self.event('socket_error', serr) - log.warning("Failed to send %s" % data) + log.warning("Failed to send %s", data) self.__failed_send_stanza = data self.disconnect(self.auto_reconnect) except Exception as ex: - log.exception('Unexpected error in send thread: %s' % ex) + log.exception('Unexpected error in send thread: %s', ex) self.exception(ex) if not self.stop.is_set(): self.disconnect(self.auto_reconnect) def exception(self, exception): - """ - Process an unknown exception. + """Process an unknown exception. Meant to be overridden. - Arguments: - exception -- An unhandled exception object. + :param exception: An unhandled exception object. """ pass diff --git a/tests/test_stanza_element.py b/tests/test_stanza_element.py index dc67d1c5..f7ec59c0 100644 --- a/tests/test_stanza_element.py +++ b/tests/test_stanza_element.py @@ -68,7 +68,10 @@ class TestElementBase(SleekTest): 'baz': '', 'foo2': {'bar': '', 'baz': 'b'}, - 'substanzas': [{'__childtag__': '{foo}subfoo', + 'substanzas': [{'__childtag__': '{foo}foo2', + 'bar': '', + 'baz': 'b'}, + {'__childtag__': '{foo}subfoo', 'bar': 'c', 'baz': ''}]} self.failUnless(values == expected, diff --git a/tests/test_stanza_xep_0009.py b/tests/test_stanza_xep_0009.py index 6186dd90..36800335 100644 --- a/tests/test_stanza_xep_0009.py +++ b/tests/test_stanza_xep_0009.py @@ -6,23 +6,27 @@ See the file LICENSE for copying permission. """ +import base64 + from sleekxmpp.plugins.xep_0009.stanza.RPC import RPCQuery, MethodCall, \ MethodResponse -from sleekxmpp.plugins.xep_0009.binding import py2xml +from sleekxmpp.plugins.xep_0009.binding import py2xml, xml2py, rpcbase64, \ + rpctime from sleekxmpp.stanza.iq import Iq from sleekxmpp.test.sleektest import SleekTest from sleekxmpp.xmlstream.stanzabase import register_stanza_plugin +from sleekxmpp.xmlstream.tostring import tostring import unittest class TestJabberRPC(SleekTest): - + def setUp(self): register_stanza_plugin(Iq, RPCQuery) - register_stanza_plugin(RPCQuery, MethodCall) + register_stanza_plugin(RPCQuery, MethodCall) register_stanza_plugin(RPCQuery, MethodResponse) - + def testMethodCall(self): iq = self.Iq() iq['rpc_query']['method_call']['method_name'] = 'system.exit' @@ -50,6 +54,235 @@ class TestJabberRPC(SleekTest): </query> </iq> """, use_values=False) - -suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberRPC) - + + def testConvertNil(self): + params = [None] + params_xml = py2xml(*params) + expected_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <nil /> + </value> + </param> + </params> + """) + self.assertTrue(self.compare(expected_xml, params_xml), + "Nil to XML conversion\nExpected: %s\nGot: %s" % ( + tostring(expected_xml), tostring(params_xml))) + self.assertEqual(params, xml2py(expected_xml), + "XML to nil conversion") + + def testConvertBoolean(self): + params = [True, False] + params_xml = py2xml(*params) + expected_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <boolean>1</boolean> + </value> + </param> + <param> + <value> + <boolean>0</boolean> + </value> + </param> + </params> + """) + self.assertTrue(self.compare(expected_xml, params_xml), + "Boolean to XML conversion\nExpected: %s\nGot: %s" % ( + tostring(expected_xml), tostring(params_xml))) + self.assertEqual(params, xml2py(expected_xml), + "XML to boolean conversion") + + def testConvertString(self): + params = ["'This' & \"That\""] + params_xml = py2xml(*params) + expected_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <string>'This' & "That"</string> + </value> + </param> + </params> + """) + self.assertTrue(self.compare(expected_xml, params_xml), + "String to XML conversion\nExpected: %s\nGot: %s" % ( + tostring(expected_xml), tostring(params_xml))) + self.assertEqual(params, xml2py(expected_xml), + "XML to string conversion") + + def testConvertInteger(self): + params = [32767, -32768] + params_xml = py2xml(*params) + expected_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <i4>32767</i4> + </value> + </param> + <param> + <value> + <i4>-32768</i4> + </value> + </param> + </params> + """) + alternate_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <int>32767</int> + </value> + </param> + <param> + <value> + <int>-32768</int> + </value> + </param> + </params> + """) + self.assertTrue(self.compare(expected_xml, params_xml), + "Integer to XML conversion\nExpected: %s\nGot: %s" % ( + tostring(expected_xml), tostring(params_xml))) + self.assertEqual(params, xml2py(expected_xml), + "XML to boolean conversion") + self.assertEqual(params, xml2py(alternate_xml), + "Alternate XML to boolean conversion") + + + def testConvertDouble(self): + params = [3.14159265] + params_xml = py2xml(*params) + expected_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <double>3.14159265</double> + </value> + </param> + </params> + """) + self.assertTrue(self.compare(expected_xml, params_xml), + "Double to XML conversion\nExpected: %s\nGot: %s" % ( + tostring(expected_xml), tostring(params_xml))) + self.assertEqual(params, xml2py(expected_xml), + "XML to double conversion") + + def testConvertBase64(self): + params = [rpcbase64(base64.b64encode(b"Hello, world!"))] + params_xml = py2xml(*params) + expected_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <base64>SGVsbG8sIHdvcmxkIQ==</base64> + </value> + </param> + </params> + """) + alternate_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <Base64>SGVsbG8sIHdvcmxkIQ==</Base64> + </value> + </param> + </params> + """) + self.assertTrue(self.compare(expected_xml, params_xml), + "Base64 to XML conversion\nExpected: %s\nGot: %s" % ( + tostring(expected_xml), tostring(params_xml))) + self.assertEqual(list(map(lambda x: x.decode(), params)), + list(map(lambda x: x.decode(), xml2py(expected_xml))), + "XML to base64 conversion") + self.assertEqual(list(map(lambda x: x.decode(), params)), + list(map(lambda x: x.decode(), xml2py(alternate_xml))), + "Alternate XML to base64 conversion") + + def testConvertDateTime(self): + params = [rpctime("20111220T01:50:00")] + params_xml = py2xml(*params) + expected_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <dateTime.iso8601>20111220T01:50:00</dateTime.iso8601> + </value> + </param> + </params> + """) + self.assertTrue(self.compare(expected_xml, params_xml), + "DateTime to XML conversion\nExpected: %s\nGot: %s" % ( + tostring(expected_xml), tostring(params_xml))) + self.assertEqual(list(map(lambda x: x.iso8601(), params)), + list(map(lambda x: x.iso8601(), xml2py(expected_xml))), + None) + + def testConvertArray(self): + params = [[1,2,3], ('a', 'b', 'c')] + params_xml = py2xml(*params) + expected_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <array> + <data> + <value><i4>1</i4></value> + <value><i4>2</i4></value> + <value><i4>3</i4></value> + </data> + </array> + </value> + </param> + <param> + <value> + <array> + <data> + <value><string>a</string></value> + <value><string>b</string></value> + <value><string>c</string></value> + </data> + </array> + </value> + </param> + </params> + """) + self.assertTrue(self.compare(expected_xml, params_xml), + "Array to XML conversion\nExpected: %s\nGot: %s" % ( + tostring(expected_xml), tostring(params_xml))) + self.assertEqual(list(map(list, params)), xml2py(expected_xml), + "XML to array conversion") + + def testConvertStruct(self): + params = [{"foo": "bar", "baz": False}] + params_xml = py2xml(*params) + expected_xml = self.parse_xml(""" + <params xmlns="jabber:iq:rpc"> + <param> + <value> + <struct> + <member> + <name>foo</name> + <value><string>bar</string></value> + </member> + <member> + <name>baz</name> + <value><boolean>0</boolean></value> + </member> + </struct> + </value> + </param> + </params> + """) + self.assertTrue(self.compare(expected_xml, params_xml), + "Struct to XML conversion\nExpected: %s\nGot: %s" % ( + tostring(expected_xml), tostring(params_xml))) + self.assertEqual(params, xml2py(expected_xml), + "XML to struct conversion") + +suite = unittest.TestLoader().loadTestsFromTestCase(TestJabberRPC) + diff --git a/tests/test_stream_handlers.py b/tests/test_stream_handlers.py index 1b831e21..7fd4e648 100644 --- a/tests/test_stream_handlers.py +++ b/tests/test_stream_handlers.py @@ -1,5 +1,6 @@ import time +from sleekxmpp import Message from sleekxmpp.test import * from sleekxmpp.xmlstream.handler import * from sleekxmpp.xmlstream.matcher import * @@ -152,5 +153,49 @@ class TestHandlers(SleekTest): self.failUnless(events == ['foo'], "Iq callback was not executed: %s" % events) + def testMultipleHandlersForStanza(self): + """ + Test that multiple handlers for a single stanza work + without clobbering each other. + """ + + def handler_1(msg): + msg.reply("Handler 1: %s" % msg['body']).send() + + def handler_2(msg): + msg.reply("Handler 2: %s" % msg['body']).send() + + def handler_3(msg): + msg.reply("Handler 3: %s" % msg['body']).send() + + self.xmpp.add_event_handler('message', handler_1) + self.xmpp.add_event_handler('message', handler_2) + self.xmpp.add_event_handler('message', handler_3) + + self.recv(""" + <message to="tester@localhost" from="user@example.com"> + <body>Testing</body> + </message> + """) + + + # This test is brittle, depending on the fact that handlers + # will be checked in the order they are registered. + self.send(""" + <message to="user@example.com"> + <body>Handler 1: Testing</body> + </message> + """) + self.send(""" + <message to="user@example.com"> + <body>Handler 2: Testing</body> + </message> + """) + self.send(""" + <message to="user@example.com"> + <body>Handler 3: Testing</body> + </message> + """) + suite = unittest.TestLoader().loadTestsFromTestCase(TestHandlers) |