diff --git a/.gitignore b/.gitignore index 526a097..c0ae8ca 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ _birdisle*.so .cache .pytest_cache .coverage +/doc/_build diff --git a/birdisle/__init__.py b/birdisle/__init__.py index f0f1237..572b283 100644 --- a/birdisle/__init__.py +++ b/birdisle/__init__.py @@ -7,6 +7,15 @@ class Server(object): + """Birdisle server instance. + + This is a lower-level interface to Birdisle. Most users will only use the + constructor directly, and then pass instances to the higher-level + wrappers. + + :param config: Configuration file content + :type config: str or bytes + """ def __init__(self, config=None): if config is None: config = "" @@ -18,11 +27,30 @@ def __init__(self, config=None): "Failed to create birdisle server") def add_connection(self, fd): + """Give the server a new client socket. + + Ownership of the given file descriptor is passed to the C code, and + the Python code must not attempt to close it. In particular, do not + directly pass the fileno of a :class:`socket.socket` without + duplicating it. + + This is a low-level function that may be useful to connect Birdisle to + an existing socket (e.g. a TCP socket). Most users will use + :meth:`connect` instead. + + :param fd: File descriptor that the server will own + :type fd: int + """ if self._handle is None: raise RuntimeError("Server is already closed") _birdisle.lib.birdisleAddConnection(self._handle, fd) def connect(self): + """Create a new connection to the server. + + :return: File descriptor for the client end of a socket pair + :rtype: int + """ if self._handle is None: raise RuntimeError("Server is already closed") socks = list(socket.socketpair(socket.AF_UNIX, socket.SOCK_STREAM)) @@ -45,6 +73,13 @@ def connect(self): os.close(fd) def close(self): + """Shut down the server and free the resources. + + This method can be called multiple times, but no other methods + should be called after this one. + + :raises RuntimeError: if the server did not shut down cleanly + """ if self._handle is None: return # Already closed ret = _birdisle.lib.birdisleStopServer(self._handle) diff --git a/birdisle/aioredis.py b/birdisle/aioredis.py index 56dd0fa..72c6c6e 100644 --- a/birdisle/aioredis.py +++ b/birdisle/aioredis.py @@ -8,6 +8,15 @@ async def open_birdisle_connection(server=None, *, limit, loop=None, parser=None, **kwargs): + """Create an asyncio connection to a birdisle server. + + :param server: Server to connect to (if not specified, a new one is created). + :type server: :class:`birdisle.Server` + :param limit: Maximum message length for the reader + :type limit: int + :param loop: Event loop + :type loop: :class:`asyncio.AbstractEventLoop` + """ # XXX: parser is not used (yet). That's inherited from aioredis, # not a birdisle deficiency. if server is None: @@ -30,6 +39,15 @@ async def open_birdisle_connection(server=None, *, async def create_connection(server=None, *, db=None, password=None, ssl=None, encoding=None, parser=None, loop=None, timeout=None, connection_cls=None): + """Create redis connection to a birdisle server. + + For details refer to :func:`aioredis.create_connection`. + + This function is a coroutine. + + :param server: Server to connect to (if not specified, a new one is created). + :type server: :class:`birdisle.Server` + """ # This code is mostly duplicating the work of aioredis.create_connection if timeout is not None and timeout <= 0: raise ValueError("Timeout has to be None or a number greater than 0") @@ -67,7 +85,9 @@ async def create_connection(server=None, *, db=None, password=None, ssl=None, async def create_redis(server=None, *, commands_factory=aioredis.Redis, **kwargs): - """Creates high-level Redis interface. + """Create high-level Redis interface to a birdisle server. + + For details refer to :func:`aioredis.create_redis`. This function is a coroutine. """ @@ -89,6 +109,12 @@ def _create_new_connection(self, address): async def create_pool(server=None, *, pool_cls=None, **kwargs): + """Create pool of low-level connections to a birdisle server. + + For details refer to :func:`aioredis.create_pool`. + + This function is a coroutine. + """ if pool_cls is None: pool_cls = ConnectionsPool if server is None: @@ -98,5 +124,11 @@ async def create_pool(server=None, *, pool_cls=None, **kwargs): async def create_redis_pool(server=None, *, commands_factory=aioredis.Redis, **kwargs): + """Create pool of high-level redis connections to a birdisle server. + + For details refer to :func:`aioredis.create_redis_pool`. + + This function is a coroutine. + """ pool = await create_pool(server, **kwargs) return commands_factory(pool) diff --git a/birdisle/redis.py b/birdisle/redis.py index 52dff0b..c88a732 100644 --- a/birdisle/redis.py +++ b/birdisle/redis.py @@ -10,6 +10,7 @@ class LocalSocketConnection(redis.connection.Connection): + """Socket connection to a Birdisle server""" description_format = "LocalSocketConnection" def __init__(self, server, db=0, password=None, @@ -99,8 +100,18 @@ def __init__(self, host='localhost', port=6379, class StrictRedis(RedisMixin, redis.StrictRedis): + """Replacement for :class:`redis.StrictRedis` that connects to a birdisle server. + + :param server: Server state (keyword only). If unspecified, a new one is created. + :type server: :class:`birdisle.Server` + """ pass class Redis(RedisMixin, redis.Redis): + """Replacement for :class:`redis.Redis` that connects to a birdisle server. + + :param server: Server state (keyword only). If unspecified, a new one is created. + :type server: :class:`birdisle.Server` + """ pass diff --git a/doc/Makefile b/doc/Makefile new file mode 100644 index 0000000..298ea9e --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,19 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/doc/birdisle.rst b/doc/birdisle.rst new file mode 100644 index 0000000..6d555f8 --- /dev/null +++ b/doc/birdisle.rst @@ -0,0 +1,30 @@ +birdisle package +================ + +Submodules +---------- + +birdisle.aioredis module +------------------------ + +.. automodule:: birdisle.aioredis + :members: + :undoc-members: + :show-inheritance: + +birdisle.redis module +--------------------- + +.. automodule:: birdisle.redis + :members: + :undoc-members: + :show-inheritance: + + +Module contents +--------------- + +.. automodule:: birdisle + :members: + :undoc-members: + :show-inheritance: diff --git a/doc/conf.py b/doc/conf.py new file mode 100644 index 0000000..2231797 --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +# +# Configuration file for the Sphinx documentation builder. +# +# This file does only contain a selection of the most common options. For a +# full list see the documentation: +# http://www.sphinx-doc.org/en/master/config + +# -- Path setup -------------------------------------------------------------- + +# 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. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- Project information ----------------------------------------------------- + +project = 'birdisle-py' +copyright = '2018, Bruce Merry' +author = 'Bruce Merry' + +# The short X.Y version +version = '' +# The full version, including alpha/beta/rc tags +release = '' + + +# -- General configuration --------------------------------------------------- + +# 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 = [ + 'sphinx.ext.autodoc', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = None + + +# -- 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 = 'sphinx_rtd_theme' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom 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'] + +# Custom sidebar templates, must be a dictionary that maps document names +# to template names. +# +# The default sidebars (for documents that don't match any pattern) are +# defined by theme itself. Builtin themes are using these templates by +# default: ``['localtoc.html', 'relations.html', 'sourcelink.html', +# 'searchbox.html']``. +# +# html_sidebars = {} + + +# -- Options for HTMLHelp output --------------------------------------------- + +# Output file base name for HTML help builder. +htmlhelp_basename = 'birdisle-pydoc' + + +# -- Options for LaTeX output ------------------------------------------------ + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'birdisle-py.tex', 'birdisle-py Documentation', + 'Bruce Merry', 'manual'), +] + + +# -- Options for manual page output ------------------------------------------ + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'birdisle-py', 'birdisle-py Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ---------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'birdisle-py', 'birdisle-py Documentation', + author, 'birdisle-py', 'One line description of project.', + 'Miscellaneous'), +] + + +# -- Options for Epub output ------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = project + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +# +# epub_identifier = '' + +# A unique identification for the text. +# +# epub_uid = '' + +# A list of files that should not be packed into the epub file. +epub_exclude_files = ['search.html'] + + +# -- Extension configuration ------------------------------------------------- diff --git a/doc/index.rst b/doc/index.rst new file mode 100644 index 0000000..18c4b5b --- /dev/null +++ b/doc/index.rst @@ -0,0 +1,22 @@ +.. birdisle-py documentation master file, created by + sphinx-quickstart on Sun Oct 21 18:02:58 2018. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to birdisle-py's documentation! +======================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + user + Reference + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/doc/make.bat b/doc/make.bat new file mode 100644 index 0000000..27f573b --- /dev/null +++ b/doc/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd diff --git a/doc/modules.rst b/doc/modules.rst new file mode 100644 index 0000000..1f99a98 --- /dev/null +++ b/doc/modules.rst @@ -0,0 +1,7 @@ +birdisle +======== + +.. toctree:: + :maxdepth: 4 + + birdisle diff --git a/doc/user.rst b/doc/user.rst new file mode 100644 index 0000000..bba5d7f --- /dev/null +++ b/doc/user.rst @@ -0,0 +1,65 @@ +User guide +========== + +Birdisle (an anagram of "lib redis") is a modified version of +`redis`_ that runs as a library inside another process. The +primary aim is to simplify unit testing by providing a way to run tests +against what appears to be a redis server, but without the hassle of starting a +separate process and ensuring that it is torn down correctly. + +.. _redis: https://redis.io + +Birdisle-py is a Python wrapper that allows Birdisle to be used from Python. It +contains integrations with `redis-py`_ and `aioredis`_ to simplify usage. It +also has a lower-level API that can be used for integration with other client +libraries. + +.. _redis-py: https://redis-py.readthedocs.io/ +.. _aioredis: https://aioredis.readthedocs.io/ + +Server class +------------ + +The documentation refers a number of times to a "server". A server is an instance of +:class:`birdisle.Server`, and is the equivalent of a separate Redis server: +each instance is backed by separate databases. Note that each server also uses +a non-trivial number of resources (including threads and network sockets), so while it is +reasonable to have a few, you will likely run into problems if you try to +create thousands. The resources can be explicitly freed by calling +:meth:`~birdisle.Server.close`. + +The class optionally takes the contents of a configuration file. It can be +specified as either :class:`bytes` or :class:`str` (:class:`unicode` in Python +2). In the latter case it will be encoded for consumption by Birdisle. Note +that not all redis features work in birdisle (see a list of `limitations`_), +and trying to use an unsupported feature can lead to undefined behaviour +including crashes. + +.. _limitations: https://github.com/bmerry/birdisle#limitations + +redis-py integration +-------------------- + +The redis-py integration is in the module :mod:`birdisle.redis`. +Classes :class:`~birdisle.redis.Redis` or +:class:`~birdisle.redis.StrictRedis` are intended to replace the redis-py +classes of the same names, and are in fact subclasses. + +Instead of host and port arguments, they take a `server` keyword argument to +specify the :class:`~birdisle.Server`. If not specified, a new server is +created. Note that this is different behaviour to `fakeredis`_, where the +default is for all instances to be backed by the same data. + +.. _fakeredis: https://github.com/jamesls/fakeredis + +aioredis integration +-------------------- + +Within the module :mod:`birdisle.aioredis`, the functions +:func:`~birdisle.aioredis.create_connection`, +:func:`~birdisle.aioredis.create_redis`, +:func:`~birdisle.aioredis.create_pool` and +:func:`~birdisle.aioredis.create_redis_pool` are replacements for the aioredis +functions of the same names. Instead of an address, they take a +:class:`~birdisle.Server` as the first argument. If the argument is omitted, a +new :class:`~birdisle.Server` is created. diff --git a/examples/server.py b/examples/server.py index 8427ad2..975187d 100755 --- a/examples/server.py +++ b/examples/server.py @@ -1,4 +1,11 @@ #!/usr/bin/env python + +"""Run a redis server equivalent. + +Note: don't use this in production! Use an actual redis server, which will do +things like making sure the data is persisted before shutdown. +""" + import contextlib import socket import os