diff --git a/.gitignore b/.gitignore index adf7177..3c70b76 100644 --- a/.gitignore +++ b/.gitignore @@ -1,38 +1,15 @@ -*.py[cod] -MANIFEST - -# C extensions -*.so - -# Packages +# general things to ignore +build/ +dist/ +*.egg-info/ *.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 -__pycache__ - -# Installer logs -pip-log.txt +*.py[cod] +__pycache__/ -# Unit test / coverage reports -.coverage +# tox .tox -nosetests.xml - -# Translations -*.mo - -# docs/sphinx -/docs/_build/ -# emacs backup files -*~ +# docutil generated +/README.html +/CHANGELOG.html +/TODO.html diff --git a/CHANGELOG.rst b/CHANGELOG.rst new file mode 100644 index 0000000..e39a251 --- /dev/null +++ b/CHANGELOG.rst @@ -0,0 +1,33 @@ +Changelog +========= + +v0.9.0 (2016-01-04) +------------------- + +No major new features, API cleanup to ensure that connections are +properly closed. Functions that return binary data return ``bytes``. + +* implement dummy context management protocol for ``_Proxy`` + for consitency with _PersistentProxy +* ``OwnetProxy`` class deprecated +* create a diagnostics directory ``./diags`` +* move test suite from ``./test`` to ``./tests`` +* ``pyownet.protocol._OwnetConnection.req()`` returns ``bytes`` and not + ``bytearray`` + + This is due to a simplification in + ``pyownet.protocol._OwnetConnection._read_socket()`` method. +* better connection logic in ``pyownet.protocol.proxy()`` factory: + first connect or raise ``protocol.ConnError``, + then test owserver protocol or raise ``protocol.ProtocolError`` +* use relative imports in ``pyownet.protocol`` +* ``./test`` and ``./examples`` minor code refactor +* ``.gitignore`` cleanup (use only project specific ignores) +* add ``__del__`` in ``_PersistentProxy`` to ensure connection is closed +* use ``with _OwnetConnection`` inside ``_Proxy`` to shutdown sockets +* implement context management protocol for ``_OwnetConnection`` to + guarantee that connection is shutdown on exit +* py26 testing via ``unittest2`` +* transform ``./test`` directory in package, so that common code + (used for reading configuration files) can be shared more easily +* move ``./pyownet`` to ``./src/pyownet`` diff --git a/MANIFEST.in b/MANIFEST.in index abfe3b6..838652b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include *.rst LICENSE.txt -recursive-include test __init__.py test*.py tests.ini +include README.rst CHANGELOG.rst LICENSE.txt +recursive-include tests __init__.py test*.py tests.ini recursive-include examples *.py diff --git a/README.rst b/README.rst index 5304059..12ae0b6 100644 --- a/README.rst +++ b/README.rst @@ -1,41 +1,52 @@ -pyownet, a pythonic interface to ownet -====================================== +pyownet: Python OWFS client library (owserver protocol) +======================================================= -|docs| - -.. |docs| image:: https://readthedocs.org/projects/pyownet/badge/?version=latest&style=flat +.. image:: https://readthedocs.org/projects/pyownet/badge/?version=latest&style=flat :target: http://pyownet.readthedocs.org/en/latest/ :alt: Package Documentation -pyownet is a pure python package that allows to access an `owserver`_ -via the `owserver network protocol`_, in short *ownet*. - -owserver is part of the `OWFS 1-Wire File System`_: - - OWFS is an easy way to use the powerful 1-wire system of - Dallas/Maxim. - - OWFS is a simple and flexible program to monitor and control the - physical environment. You can write scripts to read temperature, - flash lights, write to an LCD, log and graph, ... +.. image:: https://img.shields.io/pypi/v/pyownet.svg + :target: https://pypi.python.org/pypi/pyownet + :alt: Python Package Index version -The ``pyownet.protocol`` module is a low-level implementation of the -ownet protocol. Interaction with an owserver takes place via a proxy -object whose methods correspond to ownet messages: +Pyownet is a pure python package that allows network client access to +the `OWFS 1-Wire File System`_ via an `owserver`_ and the `owserver +network protocol`_, in short *ownet*. -:: +The ``pyownet.protocol`` module is an implementation of the owserver +client protocol that exposes owserver messages as methods of a proxy +object:: >>> owproxy = pyownet.protocol.proxy(host="owserver.example.com", port=4304) - >>> owproxy.ping() >>> owproxy.dir() ['/10.67C6697351FF/', '/05.4AEC29CDBAAB/'] - >>> owproxy.present('/10.67C6697351FF/temperature') - True >>> owproxy.read('/10.67C6697351FF/temperature') ' 91.6195' -Python 3 is supported via ``2to3`` and ``use_2to3 = True`` in -``setup.py``. +Installation +------------ + +To install pyownet:: + + $ pip install pyownet + + +Python version support +---------------------- + +The code base is written in Python 2, but Python 3 is fully supported, +and is the main developing language. Running the ``2to3`` tool will +generate valid and, whenever possible, idiomatic Python 3 code. + +Explicitly supported versions are Python 2.6, 2.7, 3.2 through 3.5. + + +Documentation +------------- + +Full package documentation is available at +http://pyownet.readthedocs.org/en/latest/ + .. _owserver: http://owfs.org/index.php?page=owserver_protocol .. _owserver network protocol: http://owfs.org/index.php?page=owserver-protocol diff --git a/TODO b/TODO deleted file mode 100644 index ded2c09..0000000 --- a/TODO +++ /dev/null @@ -1,14 +0,0 @@ - -document that every pathname has to by an ASCIIZ string on the wire -in older owlib versions - -FLG_ALIAS is apparently not working - -on the dir() reply offset field seems to code some info (?) - ------ - -close sockets on proxy object delete: - *) conn error - *) del of persistent connection -check ResourceWarning in python3 diff --git a/TODO.rst b/TODO.rst new file mode 100644 index 0000000..3117913 --- /dev/null +++ b/TODO.rst @@ -0,0 +1,10 @@ +TODO +==== + +* Document that every pathname had to by an ASCIIZ string on the wire + in older owlib versions. + +* ``FLG_ALIAS`` is apparently not working as expected. + +* In the reply to a ``MSG_DIR`` message it seems that the offset field + has the pourpose of coding some info. diff --git a/diags/README.rst b/diags/README.rst new file mode 100644 index 0000000..d71ac5d --- /dev/null +++ b/diags/README.rst @@ -0,0 +1 @@ +A collection of (yet) undocumented diagnostic tools. diff --git a/test/stress_p.py b/diags/stress_p.py similarity index 75% rename from test/stress_p.py rename to diags/stress_p.py index 58e954e..ce46d34 100644 --- a/test/stress_p.py +++ b/diags/stress_p.py @@ -3,36 +3,31 @@ import atexit import time import threading -import os import sys - if sys.version_info < (3, ): - from ConfigParser import ConfigParser + from urlparse import (urlsplit, ) else: - from configparser import ConfigParser + from urllib.parse import (urlsplit, ) from pyownet import protocol - -config = ConfigParser() - -config.add_section('server') -config.set('server', 'host', 'localhost') -config.set('server', 'port', '4304') - -config.read([os.path.join(os.path.dirname(__file__), 'tests.ini')]) - -HOST = config.get('server', 'host') -PORT = config.get('server', 'port') - MTHR = 10 tst = lambda: time.strftime('%T') + + def log(s): print(tst(), s) + def main(): - proxy = protocol.proxy(HOST, PORT, verbose=False) + assert len(sys.argv) == 2 + urlc = urlsplit(sys.argv[1], scheme='owserver', allow_fragments=False) + host = urlc.hostname or 'localhost' + port = urlc.port or 4304 + assert not urlc.path or urlc.path == '/' + + proxy = protocol.proxy(host, port, verbose=False) pid = ver = '' try: pid = int(proxy.read('/system/process/pid')) @@ -68,6 +63,7 @@ def worker(proxy, id): iter += 1 nap *= 2 + @atexit.register def goodbye(): log('exiting stress_p') diff --git a/diags/stress_ping.py b/diags/stress_ping.py new file mode 100644 index 0000000..9d7fe13 --- /dev/null +++ b/diags/stress_ping.py @@ -0,0 +1,28 @@ +# +# script to test bug #1 +# +import sys + +if sys.version_info < (3, ): + from urlparse import (urlsplit, ) +else: + from urllib.parse import (urlsplit, ) + +from pyownet.protocol import proxy + + +def main(): + assert len(sys.argv) == 2 + urlc = urlsplit(sys.argv[1], scheme='owserver', allow_fragments=False) + host = urlc.hostname or 'localhost' + port = urlc.port or 4304 + path = urlc.path or '/' + + p = proxy(host, port, verbose=True) + + while True: + ret = p.read(path) + assert ret, "'%s'" % ret + +if __name__ == '__main__': + main() diff --git a/diags/stress_s.py b/diags/stress_s.py new file mode 100644 index 0000000..e7f02d1 --- /dev/null +++ b/diags/stress_s.py @@ -0,0 +1,51 @@ +"""floods owserver with non persistent requests + +This program floods the owserver with non persistent dir() requests. +After about 16384 requests should fail with +'[Errno 49] Can't assign requested address' +""" + +from __future__ import print_function + +import itertools +import sys +if sys.version_info < (3, ): + from urlparse import (urlsplit, ) +else: + from urllib.parse import (urlsplit, ) + +import pyownet +from pyownet import protocol + + +def main(): + assert len(sys.argv) == 2 + urlc = urlsplit(sys.argv[1], scheme='owserver', allow_fragments=False) + host = urlc.hostname or 'localhost' + port = urlc.port or 4304 + assert not urlc.path or urlc.path == '/' + + p = protocol.proxy(host, port, persistent=False) + pid = 'unknown' + ver = 'unknown' + try: + pid = int(p.read('/system/process/pid')) + ver = p.read('/system/configuration/version').decode() + except protocol.OwnetError: + pass + print(pyownet.__name__, pyownet.__version__, pyownet.__file__) + print('proxy_obj: {}'.format(p)) + print('server info: pid {}, ver. {}'.format(pid, ver)) + + freq = 1 << 12 + + for i in itertools.count(): + try: + _ = p.dir() + except protocol.Error as exc: + print('Iteration {0} raised exception: {1}'.format(i, exc)) + break + None if i % freq else print('Iteration {}'.format(i)) + +if __name__ == '__main__': + main() diff --git a/diags/stress_t.py b/diags/stress_t.py new file mode 100644 index 0000000..3aa7f97 --- /dev/null +++ b/diags/stress_t.py @@ -0,0 +1,115 @@ +"""stress_t.py -- a stress test for owserver + +This programs parses an owserver URI, constructed in the obvious way: +'owserver://hostname:port/path' and recursively walks down +""" + +from __future__ import print_function + +import sys +import time +import argparse +if sys.version_info < (3, ): + from urlparse import urlsplit +else: + from urllib.parse import urlsplit + +from pyownet import protocol + +__all__ = ['main'] + + +def stress(owproxy, root): + + def walkdir(path): + try: + subdirs = owproxy.dir(path, slash=False, bus=False) + except protocol.OwnetError as error: + if error.errno != 20: + raise ValueError('Wrong error at dir({0}): {1}' + .format(path, error)) + _ = owproxy.read(path) + return 1 + else: + num = 0 + for i in subdirs: + num += walkdir(i) + return num + + def walkread(path): + try: + _ = owproxy.read(path) + except protocol.OwnetError as error: + num = 0 + if error.errno != 21: + raise ValueError('Wrong error at read({0}): {1}' + .format(path, error)) + for i in owproxy.dir(path, slash=False, bus=False): + num += walkread(i) + return num + else: + return 1 + + tic = time.time() + n = walkdir(root) + toc = time.time() + print('walkdir({}) : {:.3f}s for {:d} nodes'.format(root, toc - tic, n)) + + tic = time.time() + n = walkread(root) + toc = time.time() + print('walkread({}): {:.3f}s for {:d} nodes'.format(root, toc - tic, n)) + + +def main(): + """parse commandline arguments and print result""" + + # + # setup command line parsing a la argpase + # + parser = argparse.ArgumentParser() + + # positional args + parser.add_argument('uri', metavar='URI', nargs='?', default='/', + help='[owserver:]//hostname:port/path') + + # + # parse command line args + # + args = parser.parse_args() + + # + # parse args.uri and substitute defaults + # + urlc = urlsplit(args.uri, scheme='owserver', allow_fragments=False) + assert urlc.fragment == '' + if urlc.scheme != 'owserver': + parser.error("Invalid URI scheme '{0}:'".format(urlc.scheme)) + if urlc.query: + parser.error("Invalid URI, query component '?{0}' not allowed" + .format(urlc.query)) + try: + host = urlc.hostname or 'localhost' + port = urlc.port or 4304 + except ValueError as error: + parser.error("Invalid URI: invalid net location '//{0}/'" + .format(urlc.netloc)) + + # + # create owserver proxy object + # + try: + owproxy = protocol.proxy(host, port, persistent=True) + except protocol.ConnError as error: + print("Unable to open connection to '{0}:{1}'\n{2}" + .format(host, port, error), file=sys.stderr) + sys.exit(1) + except protocol.ProtocolError as error: + print("Protocol error, '{0}:{1}' not an owserver?\n{2}" + .format(host, port, error), file=sys.stderr) + sys.exit(1) + + stress(owproxy, urlc.path) + +if __name__ == '__main__': + main() diff --git a/test/timing.py b/diags/timing.py similarity index 84% rename from test/timing.py rename to diags/timing.py index 5d61f58..a60b04a 100644 --- a/test/timing.py +++ b/diags/timing.py @@ -3,22 +3,22 @@ import timeit import argparse if sys.version_info < (3, ): - from urlparse import (urlsplit, urlunsplit) + from urlparse import (urlsplit, ) else: - from urllib.parse import (urlsplit, urlunsplit) + from urllib.parse import (urlsplit, ) import pyownet from pyownet import protocol -def report(name, res): - scale = 1e3 # report times in ms - print('** {:15}'.format(name), end=':') - for t in (sorted(res)): - print(' {:5.2f} ms'.format(t / number * scale), end=',') - print() +def main(): -if __name__ == '__main__': + def report(name, res): + scale = 1e3 # report times in ms + print('** {:15}'.format(name), end=':') + for t in (sorted(res)): + print(' {:6.3f} ms'.format(t / number * scale), end=',') + print() parser = argparse.ArgumentParser() parser.add_argument('uri', metavar='URI', nargs='?', default='/', @@ -42,8 +42,7 @@ def report(name, res): port = urlc.port or 4304 path = urlc.path or '/' - print('pyownet: ver. {} ({})'.format(pyownet.__version__, - pyownet.__file__)) + print(pyownet.__name__, pyownet.__version__, pyownet.__file__) try: base = protocol.proxy(host, port, persistent=False) @@ -74,7 +73,10 @@ def report(name, res): stmt, number, repeat)) print() + global proxy_obj + proxy_obj = base + assert 'proxy_obj' in globals() try: eval(stmt, globals(), ) except protocol.OwnetError as err: @@ -90,3 +92,7 @@ def report(name, res): proxy_obj = protocol.clone(base, persistent=True) res = timer.repeat(number=number, repeat=repeat) report('persistent', res) + + +if __name__ == '__main__': + main() diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..204e166 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +# sphinx build directory +_build/ diff --git a/docs/Makefile b/docs/Makefile index b4fc9a6..b2c91b5 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -45,6 +45,7 @@ help: @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @echo " spellcheck to check spelling" clean: rm -rf $(BUILDDIR)/* @@ -175,3 +176,9 @@ pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." + +spellcheck: + $(SPHINXBUILD) -b spelling $(ALLSPHINXOPTS) $(BUILDDIR)/spelling + @echo + @echo "Spell check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/spelling/output.txt." diff --git a/docs/conf.py b/docs/conf.py index a97a243..319519f 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -1,34 +1,25 @@ # -*- coding: utf-8 -*- # -# pyownet documentation build configuration file, created by -# sphinx-quickstart on Mon Oct 13 21:48:32 2014. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. import sys import os +import re -# 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('.')) +regex = re.compile( + r"__version__ = (?P['\"])(?P[\w.+-]+?)(?P=quot)$", ) -# -- General configuration ------------------------------------------------ +with open('../src/pyownet/__init__.py') as infile: + for line in infile: + version_match = regex.match(line) + if version_match: + __version__ = version_match.group('ver') + break + else: + raise RuntimeError("Unable to find version string.") -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom -# ones. -extensions = [] + +# -- General configuration ------------------------------------------------ # Add any paths that contain templates here, relative to this directory. #templates_path = ['_templates/'] @@ -44,110 +35,32 @@ # General information about the project. project = u'pyownet' -copyright = u'2014, Stefano Miccoli' +copyright = u'2014–2016, Stefano Miccoli' -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# # The short X.Y version. -version = '0.8' +version = '0.9' # The full version, including alpha/beta/rc tags. -release = '0.8.2.doc0' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None +release = __version__ -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# The language for content autogenerated by Sphinx. +language = 'en' -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. exclude_patterns = ['_build'] -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - # -- Options for HTML output ---------------------------------------------- -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. -html_theme = 'classic' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. +# The theme to use for HTML and HTML Help pages. +html_theme = 'sphinx_rtd_theme' #html_theme_options = {} -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". #html_static_path = ['_static/'] -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to +# SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} +html_use_smartypants = True # If false, no module index is generated. html_domain_indices = False @@ -161,20 +74,6 @@ # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - # Output file base name for HTML help builder. htmlhelp_basename = 'pyownetdoc' @@ -256,3 +155,10 @@ # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False + +# -- Extensions: spelling ------------------------------------------------- + +extensions = ['sphinxcontrib.spelling'] +spelling_show_suggestions = False +spelling_ignore_pypi_package_names = False +spelling_word_list_filename = 'spelling_wordlist.txt' diff --git a/docs/index.rst b/docs/index.rst index 9a8a2fa..f88f12b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,6 +15,7 @@ Contents :maxdepth: 2 intro + installation protocol Indices and tables @@ -22,4 +23,3 @@ Indices and tables * :ref:`genindex` * :ref:`search` - diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..50b2998 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,59 @@ +Installation +============ + +Source code +----------- + +Source code for pyownet is hosted on GitHub: +https://github.com/miccoli/pyownet . The project is registered on +PyPI: https://pypi.python.org/pypi/pyownet . + +Python version support +^^^^^^^^^^^^^^^^^^^^^^ + +The code base is written in Python 2, but Python 3 is fully supported, +and is the main developing language. Running the ``2to3`` tool will +generate valid and, whenever possible, idiomatic Python 3 code. The +present documentation refers to the Python 3 version of the package. + +Explicitly supported versions are Python 2.6, 2.7, 3.2 through 3.5. + +Install from PyPI +----------------- + +The preferred installation method is from `PyPI`_ via `pip`_: :: + + pip install pyownet + +This will install the :py:mod:`pyownet` package in the default +location. + +If you are also interested in usage examples and tests you can +download the source package from the PyPI `downloads`_, unpack it, and +install:: + + python setup.py install + +In the source tree there will be ``example`` and ``test`` directories. + +.. _PyPI: https://pypi.python.org/pypi/ +.. _pip: https://pip.pypa.io/en/stable/user_guide/#installing-packages +.. _downloads: https://pypi.python.org/pypi/pyownet#downloads + +Install from GitHub +------------------- + +The most complete source tree is kept on GitHub: :: + + git clone https://github.com/miccoli/pyownet.git + cd pyownet + python setup.py install + +Usually the ``master`` branch should be aligned with the most recent +release, while there could be other feature branches active. + +Reporting bugs +-------------- + +Please open an issue on the pyownet `issues page +`_. diff --git a/docs/intro.rst b/docs/intro.rst index 074bcd2..410eacf 100644 --- a/docs/intro.rst +++ b/docs/intro.rst @@ -20,7 +20,7 @@ but there exist also ethernet or other network adapters. 1-Wire technology brief from Maxim Integrated .. _1-Wire Maxim: - http://www.maximintegrated.com/en/products/comms/one-wire.html + https://www.maximintegrated.com/en/products/digital/one-wire.html `1-Wire Wikipedia `_ description of the 1-Wire bus system on Wikipedia @@ -34,7 +34,7 @@ allows to access a 1-Wire bus via a supported master device. OWFS comprises many different modules which offer different access protocols to 1-Wire data: ``owhttpd`` (http), ``owftpd`` (ftp) and ``owfs`` (filesystem interface via FUSE). Since only a single program -can access the 1-Wire bus at one time, there is also backend +can access the 1-Wire bus at one time, there is also a back end component, ``owserver``, that arbitrates access to the bus from multiple client processes. Client processes can query an ``owserver`` (the program) via network sockets speaking the 'owserver' @@ -137,4 +137,4 @@ A higher-level module :py:mod:`pyownet.sensors` is under development. .. rubric:: Footnotes .. [#pers] For a discussion of this type of keep-alive connection see - :ref:`persistence`. + :ref:`persistence`. diff --git a/docs/protocol.rst b/docs/protocol.rst index 357c3c6..2dae798 100644 --- a/docs/protocol.rst +++ b/docs/protocol.rst @@ -7,23 +7,23 @@ .. warning:: - This software is still in alpha testing. Altough it has been - sucessfully used in production environments for more than 4 years, + This software is still in alpha testing. Although it has been + successfully used in production environments for more than 4 years, its API is not frozen yet, and could be changed. The :mod:`pyownet.protocol` module is a low-level implementation of the client side of the owserver protocol. Interaction with an owserver -takes place via a proxy object whose methods correspond to ownet -messages:: +takes place via a proxy object whose methods correspond to the +owserver protocol messages. + +:: >>> from pyownet import protocol >>> owproxy = protocol.proxy(host="server.example.com", port=4304) >>> owproxy.dir() - [u'/10.A7F1D92A82C8/', u'/05.D8FE434D9855/', u'/26.8CE2B3471711/'] - >>> owproxy.present('/10.A7F1D92A82C8/temperature') - True - >>> owproxy.read('/10.A7F1D92A82C8/temperature') - ' 6.68422' + ['/10.000010EF0000/', '/05.000005FA0100/', '/26.000026D90200/', '/01.000001FE0300/', '/43.000043BC0400/'] + >>> owproxy.read('/10.000010EF0000/temperature') + b' 1.6' .. _persistence: @@ -51,7 +51,7 @@ Correspondingly two different proxy object classes are implemented: * *Persistent* proxy objects are not thread safe, in the sense that the same object cannot be used concurrently by different threads. If - multithread use is desired, it is responsibility of the user to + multithreaded use is desired, it is responsibility of the user to implement a proper locking mechanism. On the first call to a method, a socket is bound to the owserver and kept open for reuse in the subsequent calls. It is responsibility of the user to explicitly @@ -72,12 +72,16 @@ Functions :param str host: host to contact :param int port: tcp port number to connect with :param int flags: protocol flag word to be ORed to each outgoing - message (see :ref:`flags`). + message (see :ref:`flags`). :param bool persistent: whether the requested connection is - persistent or not. + persistent or not. :param bool verbose: if true, print on ``sys.stdout`` debugging messages - related to the owserver protocol. + related to the owserver protocol. :return: proxy object + :raises pyownet.protocol.ConnError: if no connection can be established + with ``host`` at ``port``. + :raises pyownet.protocol.ProtocolError: if a connection can be established + but the server does not support the owserver protocol. Proxy objects are created by this factory function; for ``persistent=False`` will be of class :class:`_Proxy` or @@ -87,7 +91,7 @@ Functions :param proxy: existing proxy object :param bool persistent: whether the new proxy object is persistent - or not + or not :return: new proxy object There are costs involved in creating proxy objects (DNS lookups @@ -96,36 +100,36 @@ Functions functions is to quickly create a new proxy object with the same properties of the old one, with only the persistence parameter changed. Typically this can be useful if one desires to use - persistent connections in a multi-threaded environment, as per - example below. :: + persistent connections in a multithreaded environment, as per + the example below:: from pyownet import protocol def worker(shared_proxy): - with protocol.clone(shared_proxy, persistent=True) as newproxy: - rep1 = newproxy.read(some_path) - rep2 = newproxy.read(some_otherpath) - # do some work + with protocol.clone(shared_proxy, persistent=True) as newproxy: + rep1 = newproxy.read(some_path) + rep2 = newproxy.read(some_otherpath) + # do some work - owproxy = protocol.proxy(persistent=False) - for i in range(NUM_THREADS): - th = threading.Thread(target=worker, args=(owproxy, )) - th.start() + owproxy = protocol.proxy(persistent=False) + for i in range(NUM_THREADS): + th = threading.Thread(target=worker, args=(owproxy, )) + th.start() Of course, is persistence is not needed, the code - could be more simple: :: + could be more simple:: from pyownet import protocol def worker(shared_proxy): rep1 = shared_proxy.read(some_path) - rep2 = shared_proxy.read(some_otherpath) - # do some work + rep2 = shared_proxy.read(some_otherpath) + # do some work - owproxy = protocol.proxy(persistent=False) - for i in range(NUM_THREADS): - th = threading.Thread(target=worker, args=(owproxy, )) - th.start() + owproxy = protocol.proxy(persistent=False) + for i in range(NUM_THREADS): + th = threading.Thread(target=worker, args=(owproxy, )) + th.start() Proxy objects @@ -150,39 +154,66 @@ functions. .. py:method:: ping() - sends a *ping* message to owserver and returns ``None``. This is - actually a no-op, and no response is expected; this method could - be used for verifying that a given server is accepting - connections. + Send a *ping* message to owserver. + + :return: ``None`` + + This is actually a no-op; this method could + be used for verifying that a given server is accepting + connections and alive. .. py:method:: present(path) - returns ``True`` if an entity is present at *path*. + Check if a node is present at path. + + :param str path: OWFS path + :return: ``True`` if an entity is present at path, ``False`` otherwise + :rtype: bool + .. py:method:: dir(path='/', slash=True, bus=False) - returns a list of the pathnames of the entities that are direct + List directory content + + :param str path: OWFS path to list + :param bool slash: ``True`` if directories should be marked with a + trailing slash + :param bool bus: ``True`` if special directories should be listed + :return: directory content + :rtype: list + + Return a list of the pathnames of the entities that are direct descendants of the node at *path*, which has to be a - directory. :: + directory:: - >>> p = protocol.proxy() - >>> p.dir('/') - [u'/10.A7F1D92A82C8/', u'/05.D8FE434D9855/', u'/26.8CE2B3471711/', u'/01.98542F112D05/'] - >>> p.dir('/01.98542F112D05/') - [u'/01.98542F112D05/address', u'/01.98542F112D05/alias', u'/01.98542F112D05/crc8', u'/01.98542F112D05/family', u'/01.98542F112D05/id', u'/01.98542F112D05/locator', u'/01.98542F112D05/r_address', u'/01.98542F112D05/r_id', u'/01.98542F112D05/r_locator', u'/01.98542F112D05/type'] + >>> owproxy = protocol.proxy() + >>> owproxy.dir() + ['/10.000010EF0000/', '/05.000005FA0100/', '/26.000026D90200/', '/01.000001FE0300/', '/43.000043BC0400/'] + >>> owproxy.dir('/10.000010EF0000/') + ['/10.000010EF0000/address', '/10.000010EF0000/alias', '/10.000010EF0000/crc8', '/10.000010EF0000/errata/', '/10.000010EF0000/family', '/10.000010EF0000/id', '/10.000010EF0000/locator', '/10.000010EF0000/power', '/10.000010EF0000/r_address', '/10.000010EF0000/r_id', '/10.000010EF0000/r_locator', '/10.000010EF0000/scratchpad', '/10.000010EF0000/temperature', '/10.000010EF0000/temphigh', '/10.000010EF0000/templow', '/10.000010EF0000/type'] If ``slash=True`` the pathnames of directories are marked by a trailing slash. If ``bus=True`` also special directories (like - ``/settings/``, ``/structure/``, ``/uncached/``) are listed. + ``'/settings'``, ``'/structure'``, ``'/uncached'``) are listed. .. py:method:: read(path, size=MAX_PAYLOAD, offset=0) - returns the data read from node at path, which has not to be a - directory. :: + Read node at path + + :param str path: OWFS path + :param int size: maximum length of data read + :param int offset: offset at which read data + :return: binary buffer + :rtype: bytes + + Return the data read from node at path, which has not to be a + directory. - >>> p = protocol.proxy() - >>> p.read('/01.98542F112D05/type') - 'DS2401' + :: + + >>> owproxy = protocol.proxy() + >>> owproxy.read('/10.000010EF0000/type') + b'DS18S20' The ``size`` parameters can be specified to limit the maximum length of the data buffer returned; when ``offset > 0`` the @@ -192,17 +223,36 @@ functions. .. py:method:: write(path, data, offset=0) - writes binary ``data`` to node at path; when ``offset > 0`` data - is written starting at byte offset ``offset`` in ``path``. :: + Write data at path. + + :param str path: OWFS path + :param bytes data: binary data to write + :param int offset: offset at which write data + :return: ``None`` - >>> p = protocol.proxy() - >>> p.write('01.98542F112D05/alias', b'aaa') + Writes binary ``data`` to node at ``path``; when ``offset > 0`` data + is written starting at byte offset ``offset`` in ``path``. + + :: + + >>> owproxy = protocol.proxy() + >>> owproxy.write('/10.000010EF0000/alias', b'myalias') .. py:method:: sendmess(msgtype, payload, flags=0, size=0, offset=0) - is a low level method meant as direct interface to the *owserver - protocol* useful for generating messages which are not covered - by the other higher level methods of this class. + Send message to owserver. + + :param int msgtype: message type code + :param bytes payload: message payload + :param int flags: message flags + :param size int: message size + :param offset int: message offset + :return: owserver return code and reply data + :rtype: ``(int, bytes)`` tuple + + This is a low level method meant as direct interface to the + *owserver protocol,* useful for generating messages which are not + covered by the other higher level methods of this class. This method sends a message of type ``msgtype`` (see :ref:`msgtypes`) with a given ``payload`` to the server; @@ -213,25 +263,27 @@ functions. The method returns a ``(retcode, data)`` tuple, where ``retcode`` is the server return code (< 0 in case of error) and - ``data`` the binary payload of the reply message. :: + ``data`` the binary payload of the reply message. + + :: - >>> p = protocol.proxy() - >>> p.sendmess(MSG_DIRALL, '/', flags=FLG_BUS_RET) - (0, '/10.A7F1D92A82C8,/05.D8FE434D9855,/26.8CE2B3471711,/01.98542F112D05,/bus.0,/uncached,/settings,/system,/statistics,/structure,/simultaneous,/alarm') - >>> p.sendmess(MSG_DIRALL, '/nonexistent') - (-1, '') + >>> owproxy = protocol.proxy() + >>> owproxy.sendmess(protocol.MSG_DIRALL, b'/', flags=protocol.FLG_BUS_RET) + (0, b'/10.000010EF0000,/05.000005FA0100,/26.000026D90200,/01.000001FE0300,/43.000043BC0400,/bus.0,/uncached,/settings,/system,/statistics,/structure,/simultaneous,/alarm') + >>> owproxy.sendmess(protocol.MSG_DIRALL, b'/nonexistent') + (-1, b'') .. py:class:: _PersistentProxy Objects of this class follow the persistent protocol, reusing the - same socket connection for more than one method - call. :class:`_PersistentProxy` instances are created with a closed - connection to the owserver. When a method is called, it firsts - check for an open connection: if none is found a socket is created - and bound to the owserver. All messages are sent to the server with - the :const:`FLG_PERSISTENCE` flag set; if the server grants - persistence, the socket is kept open, otherwise the socket is shut - down before the method return. + same socket connection for more than one method call. When a + method is called, it firsts check for an open connection: if none + is found a socket is created and bound to the owserver. All + messages are sent to the server with the :const:`FLG_PERSISTENCE` + flag set; if the server grants persistence, the socket is kept + open, otherwise the socket is shut down as for :class:`_Proxy` + instances. In other terms if persistence is not granted there is an + automatic fallback to the non persistent protocol. The use of the persistent protocol is therefore transparent to the user, with an important difference: if persistence is granted by @@ -253,25 +305,29 @@ functions. still be used: in fact a new method call will open a new socket connection. - To facilitate the use of the :meth:`close_connection`, method - :class:`_PersistentProxy` objects support the context management - protocol (i.e. the `with - `_ - statement.) When the ``with`` block is entered a socket connections + To avoid the need of explicitly calling the + :meth:`close_connection` method, :class:`_PersistentProxy` + instances support the context management protocol (i.e. the `with + `_ + statement.) When the ``with`` block is entered a socket connection is opened; the same socket connection is closed at the exit of the - block. A typical usage pattern could be the following. :: + block. A typical usage pattern could be the following:: owproxy = protocol.proxy(persistent=True) with owproxy: - # call methods of owproxy - ... + # here socket is bound to owserver + # do work which requires to call owproxy methods + res = owproxy.dir() + # etc. - # do some work which does not require owproxy + # here socket is closed + # do work that does not require owproxy access with owproxy: - # call methods of owproxy - ... + # again a connection is open + res = owproxy.dir() + # etc. In the above example, outside of the ``with`` blocks all socket connections to the owserver are guaranteed to be closed. Moreover @@ -279,16 +335,90 @@ functions. before the first call to a method, which could be useful for error handling. + For non-persistent connection entering and exiting the ``with`` + block context is a no-op. + + +Exceptions +---------- + +Base classes +^^^^^^^^^^^^ + +.. py:exception:: Error + + The base class for all exceptions raised by this module. + +Concrete exceptions +^^^^^^^^^^^^^^^^^^^ + +.. py:exception:: OwnetError + + This exception is raised to signal an error return code by the + owserver. This exception inherits also from the builtin `OSError`_ + and follows its semantics: it sets arguments ``errno``, + ``strerror``, and, if available, ``filename``. Message errors are + derived from the owserver introspection, by consulting the + ``/settings/return_codes/text.ALL`` node. + +.. _OSError: https://docs.python.org/3/library/exceptions.html#OSError + +.. py:exception:: ConnError + + This exception is raised when a network connection to the owserver + cannot be established. In fact it wraps the causing `OSError`_ + exception along with all its arguments, from which it inherits. + +.. py:exception:: ProtocolError + + This exception is raised when a successful network connection is + established, but the remote server does not speak the owserver + network protocol or some other error occurred during the exchange + of owserver messages. + +.. py:exception:: MalformedHeader + + A subclass of :exc:`ProtocolError`: raised when it is impossible to + decode the reply header received from the remote owserver. + +.. py:exception:: ShortRead + + A subclass of :exc:`ProtocolError`: raised when the payload + received from the remote owserver is too short. + +.. py:exception:: ShortWrite + + A subclass of :exc:`ProtocolError`: raised when it is impossible to + send the complete payload to the remote owserver. + + + +Exception hierarchy +^^^^^^^^^^^^^^^^^^^ + +The exception class hierarchy for this module is: + +.. code-block:: none + + pyownet.Error + +-- pyownet.protocol.Error + +-- pyownet.protocol.OwnetError + +-- pyownet.protocol.ConnError + +-- pyownet.protocol.ProtocolError + +-- pyownet.protocol.MalformedHeader + +-- pyownet.protocol.ShortRead + +-- pyownet.protocol.ShortWrite + Constants --------- .. py:data:: MAX_PAYLOAD -Defines the maximum number of bytes that this module is willing to -read in a single message from the remote owserver. This limit is -enforced to avoid security problems with malformed headers. The limit -is hardcoded to 65536 bytes. [#alpha]_ + Defines the maximum number of bytes that this module is willing to + read in a single message from the remote owserver. This limit is + enforced to avoid security problems with malformed headers. The limit + is hardcoded to 65536 bytes. [#alpha]_ .. _msgtypes: @@ -387,7 +517,7 @@ flag format :py:const:`FLG_FORMAT_FI` 1067C6697351FF ============================ ================== -FICD are format designators defined as below: +FICD are format codes defined as below: ====== ====================================================== format interpretation diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..6d74a3e --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,5 @@ +Sphinx +sphinx-rtd-theme +# extension for spelling check +sphinxcontrib-spelling +pyenchant diff --git a/docs/spelling_wordlist.txt b/docs/spelling_wordlist.txt new file mode 100644 index 0000000..cb140ac --- /dev/null +++ b/docs/spelling_wordlist.txt @@ -0,0 +1,16 @@ +owserver +indices +ethernet +filesystem +perl +php +tcp +udp +multithreaded +ORed +lookups +proxied +pathnames +hardcoded +designator +builtin diff --git a/examples/owget.py b/examples/owget.py index ff001b5..f5b7c7f 100644 --- a/examples/owget.py +++ b/examples/owget.py @@ -1,10 +1,5 @@ """owget.py -- a pyownet implementation of owget -This small example shows how to implement a work-a-like of owget -(which is a C program in module owshell from owfs). - -This implementation is for python 2.X - This programs parses an owserver URI, constructed in the obvious way: 'owserver://hostname:port/path' and prints the corresponding state. If 'path' ends with a slash a DIR operation is executed, otherwise a READ. @@ -50,7 +45,7 @@ def main(): # positional args parser.add_argument('uri', metavar='URI', nargs='?', default='/', - help='[owserver:]//server:port/entity') + help='[owserver:]//hostname:port/path') # optional args for temperature scale parser.set_defaults(t_flags=protocol.FLG_TEMP_C) @@ -81,8 +76,11 @@ def main(): help='format for 1-wire unique serial IDs display') # optional arg for output format - parser.add_argument('--hex', action='store_true', - help='write read data in hex format') + tempg = parser.add_mutually_exclusive_group() + tempg.add_argument('--hex', action='store_true', + help='write data in hex format') + tempg.add_argument('-b', '--binary', action='store_true', + help='output binary data') # # parse command line args @@ -93,14 +91,18 @@ def main(): # parse args.uri and substitute defaults # urlc = urlsplit(args.uri, scheme='owserver', allow_fragments=False) + assert urlc.fragment == '' if urlc.scheme != 'owserver': - parser.error("Invalid URI scheme '{}:'".format(urlc.scheme)) - assert not urlc.fragment + parser.error("Invalid URI scheme '{0}:'".format(urlc.scheme)) if urlc.query: - parser.error( - "Invalid URI '{}', no query component allowed".format(args.uri)) - host = urlc.hostname or 'localhost' - port = urlc.port or 4304 + parser.error("Invalid URI, query component '?{0}' not allowed" + .format(urlc.query)) + try: + host = urlc.hostname or 'localhost' + port = urlc.port or 4304 + except ValueError as error: + parser.error("Invalid URI: invalid net location '//{0}/'" + .format(urlc.netloc)) # # create owserver proxy object @@ -108,20 +110,38 @@ def main(): try: owproxy = protocol.proxy( host, port, flags=args.t_flags | fcodes[args.format], ) - except (protocol.ConnError, protocol.ProtocolError) as error: - parser.exit(status=1, message=str(error)+'\n') + except protocol.ConnError as error: + print("Unable to open connection to '{0}:{1}'\n{2}" + .format(host, port, error), file=sys.stderr) + sys.exit(1) + except protocol.ProtocolError as error: + print("Protocol error, '{0}:{1}' not an owserver?\n{2}" + .format(host, port, error), file=sys.stderr) + sys.exit(1) try: if urlc.path.endswith('/'): - for entity in owproxy.dir(urlc.path, bus=True): - print(entity) + for path in owproxy.dir(urlc.path, bus=True): + print(path) else: data = owproxy.read(urlc.path) - if args.hex: - data = hexlify(data) - print(data, end='') + if args.binary: + if sys.version_info < (3, ): + sys.stdout.write(data) + else: + sys.stdout.buffer.write(data) + else: + if args.hex: + data = hexlify(data) + print(data.decode('ascii', errors='backslashreplace')) except protocol.OwnetError as error: - parser.exit(status=1, message=str(error)+'\n') + print("Ownet error\n{2}" + .format(host, port, error), file=sys.stderr) + sys.exit(1) + except protocol.ProtocolError as error: + print("Protocol error, '{0}:{1}' buggy?\n{2}" + .format(host, port, error), file=sys.stderr) + sys.exit(1) if __name__ == '__main__': main() diff --git a/setup.py b/setup.py index 53f9ecc..aa231de 100644 --- a/setup.py +++ b/setup.py @@ -7,7 +7,7 @@ regex = re.compile( r"__version__ = (?P['\"])(?P[\w.+-]+?)(?P=quot)$", ) -with open('pyownet/__init__.py') as infile: +with open('src/pyownet/__init__.py') as infile: for line in infile: version_match = regex.match(line) if version_match: @@ -20,7 +20,7 @@ setup( name = 'pyownet', version = __version__, - description = 'python ownet client library', + description = 'Python OWFS client library (owserver protocol)', long_description = long_description, author = 'Stefano Miccoli', author_email = 'stefano.miccoli@polimi.it', @@ -31,11 +31,18 @@ 'Environment :: Other Environment', 'Intended Audience :: Developers', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.6', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.2', + 'Programming Language :: Python :: 3.3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', ], + package_dir = {'': 'src'}, packages = ['pyownet', ], - test_suite = "test.test_protocol", + test_suite = "tests.test_protocol", use_2to3 = True, ) diff --git a/pyownet/__init__.py b/src/pyownet/__init__.py similarity index 89% rename from pyownet/__init__.py rename to src/pyownet/__init__.py index b6b5a10..2eefe85 100644 --- a/pyownet/__init__.py +++ b/src/pyownet/__init__.py @@ -1,4 +1,4 @@ -"""python ownet client""" +"""python owserver client""" # # Copyright 2013-2015 Stefano Miccoli @@ -17,8 +17,8 @@ # along with this program. If not, see . # -__all__ = ['__version__', ] -__version__ = '0.8.2' +__all__ = ['__version__', 'Error'] +__version__ = '0.9.0' class Error(Exception): diff --git a/pyownet/protocol.py b/src/pyownet/protocol.py similarity index 89% rename from pyownet/protocol.py rename to src/pyownet/protocol.py index 618eb5f..9ab5960 100644 --- a/pyownet/protocol.py +++ b/src/pyownet/protocol.py @@ -1,18 +1,15 @@ -"""ownet protocol implementation +"""owserver protocol implementation -This module is a pure python, low level implementation of the ownet +This module is a pure python, low level implementation of the owserver protocol. Interaction with an owserver takes place via a proxy object whose methods -correspond to ownet messages. Proxy objects are created by factory function +correspond to owserver messages. Proxy objects are created by factory function 'proxy'. >>> owproxy = proxy(host="owserver.example.com", port=4304) ->>> owproxy.ping() >>> owproxy.dir() [u'/10.67C6697351FF/', u'/05.4AEC29CDBAAB/'] ->>> owproxy.present('/10.67C6697351FF/temperature') -True >>> owproxy.read('/10.67C6697351FF/temperature') ' 91.6195' >>> owproxy.write('/10.67C6697351FF/alias', str2bytez('sensA')) @@ -40,12 +37,11 @@ from __future__ import print_function -import sys import warnings import struct import socket -import pyownet +from . import Error as _Error # # owserver protocol related constants @@ -111,7 +107,7 @@ PTH_PID = '/system/process/pid' # -# pyownet implementation specific constants +# implementation specific constants # # do not attempt to read messages bigger than this (bytes) @@ -152,7 +148,7 @@ def bytes2str(b): # exceptions # -class Error(pyownet.Error): +class Error(_Error): """Base class for all module errors.""" @@ -319,6 +315,12 @@ def __init__(self, sockaddr, family=socket.AF_INET, verbose=False): if self.verbose: print(self.socket.getsockname(), '->', self.socket.getpeername()) + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.shutdown() + def __str__(self): return "_OwnetConnection {0} -> {1}".format(self.socket.getsockname(), self.socket.getpeername()) @@ -364,48 +366,27 @@ def _send_msg(self, header, payload): raise ShortWrite() assert sent == len(header + payload), sent - # - # implementation of _read_socket is version dependent # # NOTE: # '_read_socket(self, nbytes)' was implemented as # 'return self.socket.recv(nbytes, socket.MSG_WAITALL)' # but socket.MSG_WAITALL proved not reliable + # - if sys.version_info < (2, 7, 6, ): - # legacy python support, will be dropped in the future - - def _read_socket(self, nbytes): - """read nbytes bytes from self.socket""" - - buf = '' - while len(buf) < nbytes: - tmp = self.socket.recv(nbytes) - if len(tmp) == 0: - if self.verbose: - print('ee', repr(buf)) - raise ShortRead("short read: read %d bytes instead of %d" - % (len(buf), nbytes, )) - buf += tmp - return buf - - else: - # python >= 2.7.6 and 3.x - - def _read_socket(self, nbytes): - """read nbytes bytes from self.socket""" - - buf = bytearray(nbytes) - view = memoryview(buf) - while nbytes: - nread = self.socket.recv_into(view[-nbytes:]) - if nread == 0: - if self.verbose: - print('ee', repr(buf[:-nbytes])) - raise ShortRead("short read: read %d bytes instead of %d" - % (len(view) - nbytes, len(view), )) - nbytes -= nread - return buf + def _read_socket(self, nbytes): + """read nbytes bytes from self.socket""" + + buf = self.socket.recv(nbytes) + while len(buf) < nbytes: + tmp = self.socket.recv(nbytes - len(buf)) + if len(tmp) == 0: + if self.verbose and buf: + print('ee', repr(buf)) + raise ShortRead("short read: read %d bytes instead of %d" + % (len(buf), nbytes, )) + buf += tmp + assert len(buf) == nbytes, (buf, len(buf), nbytes) + return buf def _read_msg(self): """read message from server""" @@ -453,7 +434,7 @@ def __init__(self, family, address, flags=0, self.errmess = errmess def __str__(self): - return "ownet server at %s" % (self._sockaddr, ) + return "owserver at %s" % (self._sockaddr, ) def _init_errcodes(self): # fetch errcodes array from owserver @@ -464,6 +445,12 @@ def _init_errcodes(self): # failed, leave the default empty errcodes pass + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + pass + def sendmess(self, msgtype, payload, flags=0, size=0, offset=0): """ retcode, data = sendmess(msgtype, payload) send generic message and returns retcode, data @@ -473,11 +460,11 @@ def sendmess(self, msgtype, payload, flags=0, size=0, offset=0): assert not (flags & FLG_PERSISTENCE) try: - conn = _OwnetConnection(self._sockaddr, self._family, self.verbose) - ret, _, data = conn.req(msgtype, payload, flags, size, offset) + with _OwnetConnection( + self._sockaddr, self._family, self.verbose) as conn: + ret, _, data = conn.req(msgtype, payload, flags, size, offset) except IOError as err: raise ConnError(*err.args) - conn.shutdown() return ret, data @@ -569,6 +556,9 @@ def __enter__(self): def __exit__(self, exc_type, exc_val, exc_tb): self.close_connection() + def __del__(self): + self.close_connection() + def _open_connection(self): assert self.conn is None try: @@ -622,7 +612,7 @@ class OwnetProxy(_Proxy): def __init__(self, host='localhost', port=4304, flags=0, verbose=False, ): - """return an ownet proxy object bound at (host, port); default is + """return an owserver proxy object bound at (host, port); default is (localhost, 4304). 'flags' are or-ed in the header of each query sent to owserver. @@ -631,7 +621,8 @@ def __init__(self, host='localhost', port=4304, flags=0, """ # this class will be deprecated in version 0.9.x - warnings.warn(PendingDeprecationWarning("Please use pyownet.proxy()")) + warnings.warn(DeprecationWarning( + "Please use {0}.proxy()".format(__name__))) # save init args self.flags = flags | FLG_OWNET @@ -687,11 +678,6 @@ def proxy(host='localhost', port=4304, flags=0, persistent=False, host, port. """ - if persistent: - pclass = _PersistentProxy - else: - pclass = _Proxy - # resolve host name/port try: gai = socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM) @@ -702,12 +688,11 @@ def proxy(host='localhost', port=4304, flags=0, persistent=False, lasterr = None for (family, _, _, _, sockaddr) in gai: try: - owp = pclass(family, sockaddr, flags, verbose) - owp.ping() + owp = _PersistentProxy(family, sockaddr, flags, verbose) + owp.__enter__() except ConnError as err: # not working, go over to next sockaddr lasterr = err - # fixme: should release owp resources? else: # ok, this is working, stop searching break @@ -716,8 +701,19 @@ def proxy(host='localhost', port=4304, flags=0, persistent=False, assert isinstance(lasterr, ConnError) raise lasterr - # fixme: should this be only optional? - owp._init_errcodes() + with owp: + try: + # fixme: should this be only optional? + owp._init_errcodes() + except ConnError as err: + raise ProtocolError('Error while connecting to owserver: {}' + .format(err)) + except ProtocolError as err: + # pass ProtocolError unchanged + raise err + + if not persistent: + owp = clone(owp, persistent=False) return owp diff --git a/test/__init__.py b/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..eaf6e16 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,27 @@ +"""pyownet unit testing package""" + +# public API +__all__ = ['HOST', 'PORT'] + +# +import sys +import os +if sys.version_info < (3, ): + from ConfigParser import ConfigParser +else: + from configparser import ConfigParser + +# setup config parser +config = ConfigParser() + +# default values +config.add_section('server') +config.set('server', 'host', 'localhost') +config.set('server', 'port', '4304') + +# load config files +config.read(os.path.join(i, 'tests.ini') for i in __path__+['.']) + +# export public API constants +HOST = config.get('server', 'host') +PORT = config.get('server', 'port') diff --git a/test/test_protocol.py b/tests/test_protocol.py similarity index 72% rename from test/test_protocol.py rename to tests/test_protocol.py index 4863bc5..aac9176 100644 --- a/test/test_protocol.py +++ b/tests/test_protocol.py @@ -1,29 +1,16 @@ -import unittest import sys -import os -from pyownet import protocol - -def setUpModule(): - if sys.version_info < (3, ): - from ConfigParser import ConfigParser - else: - from configparser import ConfigParser - import warnings - - warnings.simplefilter('ignore', PendingDeprecationWarning) +if sys.version_info < (2, 7, ): + import unittest2 as unittest +else: + import unittest +import warnings - config = ConfigParser() - - config.add_section('server') - config.set('server', 'host', 'localhost') - config.set('server', 'port', '4304') - - config.read([os.path.join(os.path.dirname(__file__), 'tests.ini')]) +from pyownet import protocol +from . import (HOST, PORT) - global HOST, PORT - HOST = config.get('server', 'host') - PORT = config.get('server', 'port') +def setUpModule(): + "gloabal setup" class _TestProxyMix(object): @@ -63,10 +50,12 @@ class TestOwnetProxy(_TestProxyMix, unittest.TestCase, ): @classmethod def setUpClass(cls): try: - cls.proxy = protocol.OwnetProxy(HOST, PORT) + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + cls.proxy = protocol.OwnetProxy(HOST, PORT) except protocol.ConnError as exc: - raise RuntimeError('no owserver on %s:%s, got:%s' % - (HOST, PORT, exc)) + raise unittest.SkipTest('no owserver on %s:%s, got:%s' % + (HOST, PORT, exc)) class Test_Proxy(_TestProxyMix, unittest.TestCase, ): @@ -76,8 +65,8 @@ def setUpClass(cls): try: cls.proxy = protocol.proxy(HOST, PORT, persistent=False) except protocol.ConnError as exc: - raise RuntimeError('no owserver on %s:%s, got:%s' % - (HOST, PORT, exc)) + raise unittest.SkipTest('no owserver on %s:%s, got:%s' % + (HOST, PORT, exc)) class Test_PersistentProxy(_TestProxyMix, unittest.TestCase, ): @@ -87,8 +76,8 @@ def setUpClass(cls): try: cls.proxy = protocol.proxy(HOST, PORT, persistent=True, ) except protocol.ConnError as exc: - raise RuntimeError('no owserver on %s:%s, got:%s' % - (HOST, PORT, exc)) + raise unittest.SkipTest('no owserver on %s:%s, got:%s' % + (HOST, PORT, exc)) class Test_clone_FT(Test_Proxy): @@ -100,12 +89,14 @@ def setUp(self): def tearDown(self): self.proxy.close_connection() + class Test_clone_FF(Test_Proxy): def setUp(self): assert not isinstance(self.__class__.proxy, protocol._PersistentProxy) self.proxy = protocol.clone(self.__class__.proxy, persistent=False) + class Test_clone_TT(Test_PersistentProxy): def setUp(self): @@ -115,21 +106,30 @@ def setUp(self): def tearDown(self): self.proxy.close_connection() + class Test_clone_TF(Test_PersistentProxy): def setUp(self): assert isinstance(self.__class__.proxy, protocol._PersistentProxy) self.proxy = protocol.clone(self.__class__.proxy, persistent=False) + class Test_misc(unittest.TestCase): def test_exceptions(self): - self.assertRaises(protocol.ConnError, protocol.OwnetProxy, + with warnings.catch_warnings(): + warnings.simplefilter('ignore', DeprecationWarning) + self.assertRaises(protocol.ConnError, protocol.OwnetProxy, + host='nonexistent.fake') + self.assertRaises(protocol.ConnError, protocol.proxy, host='nonexistent.fake') + self.assertRaises(protocol.ConnError, protocol.proxy, + host=HOST, port=-1) + self.assertRaises(protocol.ProtocolError, protocol.proxy, + host='www.google.com', port=80) + self.assertRaises(TypeError, protocol.clone, 1) self.assertRaises(TypeError, protocol._FromServerHeader, bad=0) self.assertRaises(TypeError, protocol._ToServerHeader, bad=0) - self.assertRaises(protocol.ConnError, protocol.proxy, HOST, -1) - self.assertRaises(TypeError, protocol.clone, 1) if __name__ == '__main__': unittest.main() diff --git a/test/tests.ini b/tests/tests.ini similarity index 100% rename from test/tests.ini rename to tests/tests.ini diff --git a/tox.ini b/tox.ini index 49a9260..43cad3b 100644 --- a/tox.ini +++ b/tox.ini @@ -4,21 +4,36 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py34, pypy, pypy3, pep8, docs, +envlist = py26, py27, py32, py33, py34, py35, pypy, pypy3, pep8, docs, [testenv] -commands = {envpython} test/test_protocol.py +commands = {envpython} -m tests.test_protocol + +[testenv:py26] +deps = unittest2 [testenv:pep8] basepython = python2.7 deps = flake8 -commands = flake8 pyownet +commands = + flake8 src/pyownet + flake8 tests + flake8 --max-complexity=16 examples [flake8] -max-complexity = 10 +jobs = auto +#max-complexity = 10 #ignore = E222,E126 [testenv:docs] -deps = sphinx +deps = + sphinx + docutils + sphinxcontrib-spelling + pyenchant commands = sphinx-build -W -b html -d {envtmpdir}/doctrees docs docs/_build/html + sphinx-build -W -b spelling -d {envtmpdir}/doctrees docs docs/_build/spelling + rst2html.py README.rst README.html + rst2html.py CHANGELOG.rst CHANGELOG.html + rst2html.py TODO.rst TODO.html