summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.rst2
-rw-r--r--conn_tests/test_pubsubjobs.py6
-rw-r--r--conn_tests/test_pubsubserver.py6
-rwxr-xr-xconn_tests/testpubsub.py4
-rw-r--r--docs/_static/pygments.css70
-rw-r--r--docs/_templates/defindex.html35
-rw-r--r--docs/_templates/indexcontent.html61
-rw-r--r--docs/_templates/layout.html69
-rw-r--r--docs/api/basexmpp.rst2
-rw-r--r--docs/api/clientxmpp.rst13
-rw-r--r--docs/api/componentxmpp.rst8
-rw-r--r--docs/api/xmlstream.rst8
-rw-r--r--docs/api/xmlstream/filesocket.rst12
-rw-r--r--docs/api/xmlstream/handler.rst24
-rw-r--r--docs/api/xmlstream/jid.rst7
-rw-r--r--docs/api/xmlstream/matcher.rst41
-rw-r--r--docs/api/xmlstream/scheduler.rst11
-rw-r--r--docs/api/xmlstream/stanzabase.rst123
-rw-r--r--docs/api/xmlstream/tostring.rst46
-rw-r--r--docs/api/xmlstream/xmlstream.rst10
-rw-r--r--docs/architecture.rst176
-rw-r--r--docs/conf.py12
-rw-r--r--docs/howto/stanzas.rst28
-rw-r--r--docs/index.rst40
-rw-r--r--docs/python-objects.invbin0 -> 105830 bytes
-rw-r--r--docs/xeps.rst48
-rwxr-xr-xexamples/ping.py2
-rw-r--r--sleekxmpp/basexmpp.py482
-rw-r--r--sleekxmpp/clientxmpp.py197
-rw-r--r--sleekxmpp/componentxmpp.py103
-rw-r--r--sleekxmpp/exceptions.py60
-rw-r--r--sleekxmpp/features/feature_bind/bind.py4
-rw-r--r--sleekxmpp/features/feature_mechanisms/mechanisms.py2
-rw-r--r--sleekxmpp/features/feature_starttls/starttls.py4
-rw-r--r--sleekxmpp/plugins/gmail_notify.py4
-rw-r--r--sleekxmpp/plugins/jobs.py2
-rw-r--r--sleekxmpp/plugins/xep_0004/stanza/form.py12
-rw-r--r--sleekxmpp/plugins/xep_0009/binding.py46
-rw-r--r--sleekxmpp/plugins/xep_0009/remote.py19
-rw-r--r--sleekxmpp/plugins/xep_0009/rpc.py8
-rw-r--r--sleekxmpp/plugins/xep_0012.py4
-rw-r--r--sleekxmpp/plugins/xep_0030/disco.py37
-rw-r--r--sleekxmpp/plugins/xep_0045.py4
-rw-r--r--sleekxmpp/plugins/xep_0050/adhoc.py7
-rw-r--r--sleekxmpp/plugins/xep_0060/stanza/pubsub_errors.py2
-rw-r--r--sleekxmpp/plugins/xep_0078/legacyauth.py4
-rw-r--r--sleekxmpp/plugins/xep_0082.py31
-rw-r--r--sleekxmpp/plugins/xep_0085/chat_states.py2
-rw-r--r--sleekxmpp/plugins/xep_0199/ping.py6
-rw-r--r--sleekxmpp/plugins/xep_0224/attention.py2
-rw-r--r--sleekxmpp/roster/item.py1
-rw-r--r--sleekxmpp/stanza/rootstanza.py3
-rw-r--r--sleekxmpp/version.py4
-rw-r--r--sleekxmpp/xmlstream/filesocket.py24
-rw-r--r--sleekxmpp/xmlstream/handler/base.py85
-rw-r--r--sleekxmpp/xmlstream/handler/callback.py82
-rw-r--r--sleekxmpp/xmlstream/handler/waiter.py93
-rw-r--r--sleekxmpp/xmlstream/jid.py58
-rw-r--r--sleekxmpp/xmlstream/matcher/base.py23
-rw-r--r--sleekxmpp/xmlstream/matcher/id.py25
-rw-r--r--sleekxmpp/xmlstream/matcher/stanzapath.py28
-rw-r--r--sleekxmpp/xmlstream/matcher/xmlmask.py71
-rw-r--r--sleekxmpp/xmlstream/matcher/xpath.py43
-rw-r--r--sleekxmpp/xmlstream/scheduler.py149
-rw-r--r--sleekxmpp/xmlstream/stanzabase.py925
-rw-r--r--sleekxmpp/xmlstream/tostring.py64
-rw-r--r--sleekxmpp/xmlstream/xmlstream.py907
-rw-r--r--tests/test_stanza_element.py5
-rw-r--r--tests/test_stanza_xep_0009.py247
-rw-r--r--tests/test_stream_handlers.py45
70 files changed, 2560 insertions, 2228 deletions
diff --git a/README.rst b/README.rst
index 5e806ad5..d175822c 100644
--- a/README.rst
+++ b/README.rst
@@ -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 &amp; 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 %}
- «&#160;&#160;<a href="{{ prev.link|e }}">{{ prev.title }}</a>
- &#160;&#160;::&#160;&#160;
- {%- endif %}
- <a class="uplink" href="{{ pathto(master_doc) }}">{{ _('Contents') }}</a>
- {%- if next %}
- &#160;&#160;::&#160;&#160;
- <a href="{{ next.link|e }}">{{ next.title }}</a>&#160;&#160;»
- {%- 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: ``&amp;``, ``&lt;``, ``&gt;``, ``&apos;``, and ``&quot;``.
+
+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
new file mode 100644
index 00000000..b7afc07e
--- /dev/null
+++ b/docs/python-objects.inv
Binary files differ
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>&apos;This&apos; &amp; &quot;That&quot;</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)