├── .coveragerc ├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README ├── README.rst ├── docs ├── Makefile ├── build │ └── .gitignore └── source │ ├── api.rst │ ├── conf.py │ ├── getting_started.rst │ ├── getting_started_ohneio_protocol.py │ └── index.rst ├── ohneio.py ├── pytest.ini ├── setup.py ├── test_ohneio.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | branch = True 3 | 4 | [report] 5 | exclude_lines = 6 | pragma: no cover 7 | # This is for debugging only 8 | def __repr__ 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[cod] 2 | *.egg-info 3 | /.cache 4 | /dist 5 | /build 6 | /MANIFEST 7 | /.tox 8 | /htmlcov 9 | .coverage 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: python 3 | python: 4 | - "3.4" 5 | - "3.5" 6 | - "3.5-dev" 7 | - "nightly" 8 | install: pip install tox-travis 9 | script: tox 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2016, Antoine Catton 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 10 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 11 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 12 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 13 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 14 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 15 | PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README 2 | include ohneio.py 3 | include LICENSE 4 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Ohne I/O 2 | ======== 3 | 4 | .. image:: https://travis-ci.org/acatton/ohneio.svg?branch=master 5 | :target: https://travis-ci.org/acatton/ohneio 6 | 7 | Utility library to write network protocol parser, `sans I/O `_. 8 | 9 | `Ohne `_ I/O (without I/O in German) is a library using 10 | `asyncio `_ corouting programming style. 11 | 12 | This library only supports Python 3.4+ (including Python 3.6 nightly) 13 | 14 | ``ohneio`` allows you to write protocol parsers the way you would write an asyncio protocol: 15 | 16 | .. code-block:: python 17 | 18 | >>> import base64 19 | >>> import ohneio 20 | >>> 21 | >>> def wait_for(s): 22 | ... while True: 23 | ... data = yield from ohneio.peek() 24 | ... pos = data.find(s) 25 | ... if pos >= 0: 26 | ... return pos 27 | ... yield from ohneio.wait() 28 | ... 29 | >>> def read_until(s): 30 | ... pos = yield from wait_for(s) 31 | ... data = yield from ohneio.read(pos) 32 | ... return data 33 | ... 34 | >>> @ohneio.protocol 35 | ... def echo_base64(separator): 36 | ... while True: 37 | ... segment = yield from read_until(separator) 38 | ... yield from ohneio.read(len(separator)) 39 | ... yield from ohneio.write(base64.b64encode(segment) + separator) 40 | ... 41 | >>> connection = echo_base64(b'\r\n') 42 | >>> connection.send(b'hello') 43 | >>> connection.read() 44 | b'' 45 | >>> connection.send(b'\r\nworld\r\n') 46 | >>> connection.read() 47 | b'aGVsbG8=\r\nd29ybGQ=\r\n' 48 | 49 | 50 | The example above also shows how ``ohneio`` allows you to combine primitives 51 | into bigger parsing functions (like ``wait_for`` and ``read_until``). 52 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | README -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 16 | 17 | .PHONY: help 18 | help: 19 | @echo "Please use \`make ' where is one of" 20 | @echo " html to make standalone HTML files" 21 | @echo " dirhtml to make HTML files named index.html in directories" 22 | @echo " singlehtml to make a single large HTML file" 23 | @echo " pickle to make pickle files" 24 | @echo " json to make JSON files" 25 | @echo " htmlhelp to make HTML files and a HTML help project" 26 | @echo " qthelp to make HTML files and a qthelp project" 27 | @echo " applehelp to make an Apple Help Book" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " epub3 to make an epub3" 31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 32 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 34 | @echo " text to make text files" 35 | @echo " man to make manual pages" 36 | @echo " texinfo to make Texinfo files" 37 | @echo " info to make Texinfo files and run them through makeinfo" 38 | @echo " gettext to make PO message catalogs" 39 | @echo " changes to make an overview of all changed/added/deprecated items" 40 | @echo " xml to make Docutils-native XML files" 41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 42 | @echo " linkcheck to check all external links for integrity" 43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 44 | @echo " coverage to run coverage check of the documentation (if enabled)" 45 | @echo " dummy to check syntax errors of document sources" 46 | 47 | .PHONY: clean 48 | clean: 49 | rm -rf $(BUILDDIR)/* 50 | 51 | .PHONY: html 52 | html: 53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 54 | @echo 55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 56 | 57 | .PHONY: dirhtml 58 | dirhtml: 59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 60 | @echo 61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 62 | 63 | .PHONY: singlehtml 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | .PHONY: pickle 70 | pickle: 71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 72 | @echo 73 | @echo "Build finished; now you can process the pickle files." 74 | 75 | .PHONY: json 76 | json: 77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 78 | @echo 79 | @echo "Build finished; now you can process the JSON files." 80 | 81 | .PHONY: htmlhelp 82 | htmlhelp: 83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 84 | @echo 85 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 86 | ".hhp project file in $(BUILDDIR)/htmlhelp." 87 | 88 | .PHONY: qthelp 89 | qthelp: 90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 91 | @echo 92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/OhneIO.qhcp" 95 | @echo "To view the help file:" 96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/OhneIO.qhc" 97 | 98 | .PHONY: applehelp 99 | applehelp: 100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 101 | @echo 102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 103 | @echo "N.B. You won't be able to view it unless you put it in" \ 104 | "~/Library/Documentation/Help or install it in your application" \ 105 | "bundle." 106 | 107 | .PHONY: devhelp 108 | devhelp: 109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 110 | @echo 111 | @echo "Build finished." 112 | @echo "To view the help file:" 113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/OhneIO" 114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/OhneIO" 115 | @echo "# devhelp" 116 | 117 | .PHONY: epub 118 | epub: 119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 120 | @echo 121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 122 | 123 | .PHONY: epub3 124 | epub3: 125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 126 | @echo 127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." 128 | 129 | .PHONY: latex 130 | latex: 131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 132 | @echo 133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 135 | "(use \`make latexpdf' here to do that automatically)." 136 | 137 | .PHONY: latexpdf 138 | latexpdf: 139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 140 | @echo "Running LaTeX files through pdflatex..." 141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 143 | 144 | .PHONY: latexpdfja 145 | latexpdfja: 146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 147 | @echo "Running LaTeX files through platex and dvipdfmx..." 148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 150 | 151 | .PHONY: text 152 | text: 153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 154 | @echo 155 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 156 | 157 | .PHONY: man 158 | man: 159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 160 | @echo 161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 162 | 163 | .PHONY: texinfo 164 | texinfo: 165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 166 | @echo 167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 168 | @echo "Run \`make' in that directory to run these through makeinfo" \ 169 | "(use \`make info' here to do that automatically)." 170 | 171 | .PHONY: info 172 | info: 173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 174 | @echo "Running Texinfo files through makeinfo..." 175 | make -C $(BUILDDIR)/texinfo info 176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 177 | 178 | .PHONY: gettext 179 | gettext: 180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 181 | @echo 182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 183 | 184 | .PHONY: changes 185 | changes: 186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 187 | @echo 188 | @echo "The overview file is in $(BUILDDIR)/changes." 189 | 190 | .PHONY: linkcheck 191 | linkcheck: 192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 193 | @echo 194 | @echo "Link check complete; look for any errors in the above output " \ 195 | "or in $(BUILDDIR)/linkcheck/output.txt." 196 | 197 | .PHONY: doctest 198 | doctest: 199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 200 | @echo "Testing of doctests in the sources finished, look at the " \ 201 | "results in $(BUILDDIR)/doctest/output.txt." 202 | 203 | .PHONY: coverage 204 | coverage: 205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 206 | @echo "Testing of coverage in the sources finished, look at the " \ 207 | "results in $(BUILDDIR)/coverage/python.txt." 208 | 209 | .PHONY: xml 210 | xml: 211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 212 | @echo 213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 214 | 215 | .PHONY: pseudoxml 216 | pseudoxml: 217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 218 | @echo 219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 220 | 221 | .PHONY: dummy 222 | dummy: 223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy 224 | @echo 225 | @echo "Build finished. Dummy builder generates no files." 226 | -------------------------------------------------------------------------------- /docs/build/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | .. autofunction:: ohneio.peek 5 | 6 | 7 | .. autofunction:: ohneio.protocol 8 | 9 | 10 | .. autofunction:: ohneio.read 11 | 12 | 13 | .. autofunction:: ohneio.wait 14 | 15 | 16 | .. autofunction:: ohneio.write 17 | 18 | 19 | .. autoclass:: ohneio.Consumer 20 | :members: send, read, has_result, get_result 21 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Ohne I/O documentation build configuration file, created by 5 | # sphinx-quickstart on Sat Aug 20 22:58:54 2016. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | # 28 | # needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc', 35 | 'sphinx.ext.napoleon', 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ['.templates'] 40 | 41 | # The suffix(es) of source filenames. 42 | # You can specify multiple suffix as a list of string: 43 | # 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | # 49 | # source_encoding = 'utf-8-sig' 50 | 51 | # The master toctree document. 52 | master_doc = 'index' 53 | 54 | # General information about the project. 55 | project = 'Ohne I/O' 56 | copyright = '2016, Antoine Catton' 57 | author = 'Antoine Catton' 58 | 59 | # The version info for the project you're documenting, acts as replacement for 60 | # |version| and |release|, also used in various other places throughout the 61 | # built documents. 62 | # 63 | # The short X.Y version. 64 | version = '0.9-dev' 65 | # The full version, including alpha/beta/rc tags. 66 | release = '0.9' 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | # 71 | # This is also used if you do content translation via gettext catalogs. 72 | # Usually you set "language" from the command line for these cases. 73 | language = None 74 | 75 | # There are two options for replacing |today|: either, you set today to some 76 | # non-false value, then it is used: 77 | # 78 | # today = '' 79 | # 80 | # Else, today_fmt is used as the format for a strftime call. 81 | # 82 | # today_fmt = '%B %d, %Y' 83 | 84 | # List of patterns, relative to source directory, that match files and 85 | # directories to ignore when looking for source files. 86 | # This patterns also effect to html_static_path and html_extra_path 87 | exclude_patterns = [] 88 | 89 | # The reST default role (used for this markup: `text`) to use for all 90 | # documents. 91 | # 92 | # default_role = None 93 | 94 | # If true, '()' will be appended to :func: etc. cross-reference text. 95 | # 96 | # add_function_parentheses = True 97 | 98 | # If true, the current module name will be prepended to all description 99 | # unit titles (such as .. function::). 100 | # 101 | # add_module_names = True 102 | 103 | # If true, sectionauthor and moduleauthor directives will be shown in the 104 | # output. They are ignored by default. 105 | # 106 | # show_authors = False 107 | 108 | # The name of the Pygments (syntax highlighting) style to use. 109 | pygments_style = 'sphinx' 110 | 111 | # A list of ignored prefixes for module index sorting. 112 | # modindex_common_prefix = [] 113 | 114 | # If true, keep warnings as "system message" paragraphs in the built documents. 115 | # keep_warnings = False 116 | 117 | # If true, `todo` and `todoList` produce output, else they produce nothing. 118 | todo_include_todos = False 119 | 120 | 121 | # -- Options for HTML output ---------------------------------------------- 122 | 123 | # The theme to use for HTML and HTML Help pages. See the documentation for 124 | # a list of builtin themes. 125 | # 126 | html_theme = 'bizstyle' 127 | 128 | # Theme options are theme-specific and customize the look and feel of a theme 129 | # further. For a list of options available for each theme, see the 130 | # documentation. 131 | # 132 | # html_theme_options = {} 133 | 134 | # Add any paths that contain custom themes here, relative to this directory. 135 | # html_theme_path = [] 136 | 137 | # The name for this set of Sphinx documents. 138 | # " v documentation" by default. 139 | # 140 | # html_title = 'Ohne I/O v0.9' 141 | 142 | # A shorter title for the navigation bar. Default is the same as html_title. 143 | # 144 | # html_short_title = None 145 | 146 | # The name of an image file (relative to this directory) to place at the top 147 | # of the sidebar. 148 | # 149 | # html_logo = None 150 | 151 | # The name of an image file (relative to this directory) to use as a favicon of 152 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 153 | # pixels large. 154 | # 155 | # html_favicon = None 156 | 157 | # Add any paths that contain custom static files (such as style sheets) here, 158 | # relative to this directory. They are copied after the builtin static files, 159 | # so a file named "default.css" will overwrite the builtin "default.css". 160 | html_static_path = ['.static'] 161 | 162 | # Add any extra paths that contain custom files (such as robots.txt or 163 | # .htaccess) here, relative to this directory. These files are copied 164 | # directly to the root of the documentation. 165 | # 166 | # html_extra_path = [] 167 | 168 | # If not None, a 'Last updated on:' timestamp is inserted at every page 169 | # bottom, using the given strftime format. 170 | # The empty string is equivalent to '%b %d, %Y'. 171 | # 172 | # html_last_updated_fmt = None 173 | 174 | # If true, SmartyPants will be used to convert quotes and dashes to 175 | # typographically correct entities. 176 | # 177 | # html_use_smartypants = True 178 | 179 | # Custom sidebar templates, maps document names to template names. 180 | # 181 | # html_sidebars = {} 182 | 183 | # Additional templates that should be rendered to pages, maps page names to 184 | # template names. 185 | # 186 | # html_additional_pages = {} 187 | 188 | # If false, no module index is generated. 189 | # 190 | # html_domain_indices = True 191 | 192 | # If false, no index is generated. 193 | # 194 | # html_use_index = True 195 | 196 | # If true, the index is split into individual pages for each letter. 197 | # 198 | # html_split_index = False 199 | 200 | # If true, links to the reST sources are added to the pages. 201 | # 202 | # html_show_sourcelink = True 203 | 204 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 205 | # 206 | # html_show_sphinx = True 207 | 208 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 209 | # 210 | # html_show_copyright = True 211 | 212 | # If true, an OpenSearch description file will be output, and all pages will 213 | # contain a tag referring to it. The value of this option must be the 214 | # base URL from which the finished HTML is served. 215 | # 216 | # html_use_opensearch = '' 217 | 218 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 219 | # html_file_suffix = None 220 | 221 | # Language to be used for generating the HTML full-text search index. 222 | # Sphinx supports the following languages: 223 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'h', 'it', 'ja' 224 | # 'nl', 'no', 'pt', 'ro', 'r', 'sv', 'tr', 'zh' 225 | # 226 | # html_search_language = 'en' 227 | 228 | # A dictionary with options for the search language support, empty by default. 229 | # 'ja' uses this config value. 230 | # 'zh' user can custom change `jieba` dictionary path. 231 | # 232 | # html_search_options = {'type': 'default'} 233 | 234 | # The name of a javascript file (relative to the configuration directory) that 235 | # implements a search results scorer. If empty, the default will be used. 236 | # 237 | # html_search_scorer = 'scorer.js' 238 | 239 | # Output file base name for HTML help builder. 240 | htmlhelp_basename = 'OhneIOdoc' 241 | 242 | # -- Options for LaTeX output --------------------------------------------- 243 | 244 | latex_elements = { 245 | # The paper size ('letterpaper' or 'a4paper'). 246 | # 247 | # 'papersize': 'letterpaper', 248 | 249 | # The font size ('10pt', '11pt' or '12pt'). 250 | # 251 | # 'pointsize': '10pt', 252 | 253 | # Additional stuff for the LaTeX preamble. 254 | # 255 | # 'preamble': '', 256 | 257 | # Latex figure (float) alignment 258 | # 259 | # 'figure_align': 'htbp', 260 | } 261 | 262 | # Grouping the document tree into LaTeX files. List of tuples 263 | # (source start file, target name, title, 264 | # author, documentclass [howto, manual, or own class]). 265 | latex_documents = [ 266 | (master_doc, 'OhneIO.tex', 'Ohne I/O Documentation', 267 | 'Antoine Catton', 'manual'), 268 | ] 269 | 270 | # The name of an image file (relative to this directory) to place at the top of 271 | # the title page. 272 | # 273 | # latex_logo = None 274 | 275 | # For "manual" documents, if this is true, then toplevel headings are parts, 276 | # not chapters. 277 | # 278 | # latex_use_parts = False 279 | 280 | # If true, show page references after internal links. 281 | # 282 | # latex_show_pagerefs = False 283 | 284 | # If true, show URL addresses after external links. 285 | # 286 | # latex_show_urls = False 287 | 288 | # Documents to append as an appendix to all manuals. 289 | # 290 | # latex_appendices = [] 291 | 292 | # It false, will not define \strong, \code, itleref, \crossref ... but only 293 | # \sphinxstrong, ..., \sphinxtitleref, ... To help avoid clash with user added 294 | # packages. 295 | # 296 | # latex_keep_old_macro_names = True 297 | 298 | # If false, no module index is generated. 299 | # 300 | # latex_domain_indices = True 301 | 302 | 303 | # -- Options for manual page output --------------------------------------- 304 | 305 | # One entry per manual page. List of tuples 306 | # (source start file, name, description, authors, manual section). 307 | man_pages = [ 308 | (master_doc, 'ohneio', 'Ohne I/O Documentation', 309 | [author], 1) 310 | ] 311 | 312 | # If true, show URL addresses after external links. 313 | # 314 | # man_show_urls = False 315 | 316 | 317 | # -- Options for Texinfo output ------------------------------------------- 318 | 319 | # Grouping the document tree into Texinfo files. List of tuples 320 | # (source start file, target name, title, author, 321 | # dir menu entry, description, category) 322 | texinfo_documents = [ 323 | (master_doc, 'OhneIO', 'Ohne I/O Documentation', 324 | author, 'OhneIO', 'One line description of project.', 325 | 'Miscellaneous'), 326 | ] 327 | 328 | # Documents to append as an appendix to all manuals. 329 | # 330 | # texinfo_appendices = [] 331 | 332 | # If false, no module index is generated. 333 | # 334 | # texinfo_domain_indices = True 335 | 336 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 337 | # 338 | # texinfo_show_urls = 'footnote' 339 | 340 | # If true, do not generate a @detailmenu in the "Top" node's menu. 341 | # 342 | # texinfo_no_detailmenu = False 343 | -------------------------------------------------------------------------------- /docs/source/getting_started.rst: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | 5 | FAQ 6 | --- 7 | 8 | 9 | Why not asyncio ? 10 | ~~~~~~~~~~~~~~~~~ 11 | 12 | asyncio_ is a great library, and Ohne I/O can be used in combination with 13 | asyncio_, but it can also be used in combination with `raw sockets`_ and/or 14 | any network library. 15 | 16 | Ohne I/O does not intend to compete in *any way* with asyncio_, it even intends 17 | to be used in combination with asyncio_. (but it is not limited to asyncio_) 18 | 19 | .. _asyncio: https://docs.python.org/3/library/asyncio.html 20 | .. _raw sockets: https://docs.python.org/3/library/socket.html 21 | 22 | The power of ``ohneio`` resides in the fact that **it does not produce any I/O**. 23 | It only buffers byte streams, and allows developers to write simple stream parser 24 | like they would write a coroutine. 25 | 26 | This is how you would use an ``ohneio`` protocol: 27 | 28 | .. code-block:: python 29 | 30 | parser = protocol() 31 | parser.send(bytes_to_send) 32 | received_bytes = parser.read() 33 | 34 | 35 | Because ``ohneio`` can be considered as connection without I/O, this 36 | documentation will referred to them with the variable ``conn``. 37 | 38 | Writing an ``ohneio`` protocol parser is better than writing an asyncio_ 39 | protocol library, in the sense that your protocol could be then used by anybody 40 | with any library. 41 | 42 | Ohne I/O relies on the concept of `sans-io`_, which is the concept of writing 43 | generic protocol parser, easily testable. 44 | 45 | .. _sans-io: https://sans-io.readthedocs.io/ 46 | 47 | 48 | How does it work internally? 49 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 50 | 51 | ``ohneio`` buffers data coming in or out. Thanks to generator functions, it 52 | pauses protocol parsers execution until new data is fed in or read from the 53 | parser. 54 | 55 | 56 | Why not use manual buffers? 57 | ~~~~~~~~~~~~~~~~~~~~~~~~~~~ 58 | 59 | ``ohneio`` kinda borrows from `parser combinators`_, it allows you to write 60 | simple functions without passing objects around. And combine these functions 61 | without managing any state. 62 | 63 | .. _parser combinators: https://en.wikipedia.org/wiki/Parser_combinator 64 | 65 | ``ohneio`` also provides some primitives to wait for a certain amount of data, 66 | or wait for more data. 67 | 68 | Writing simple protocol parser, could very quickly lead to either spaghetti 69 | code, badly abstracted code. ``ohneio`` tries to avoid this. 70 | 71 | 72 | Getting started 73 | --------------- 74 | 75 | To get started we'll write a simple line echo server. This protocol echoes each 76 | line sent to the server. A line is defined by a sequence of bytes terminated by 77 | the bytes ``0x0A``. 78 | 79 | Writing a protocol 80 | ~~~~~~~~~~~~~~~~~~ 81 | 82 | .. literalinclude:: getting_started_ohneio_protocol.py 83 | :language: python 84 | :lines: 1, 3-32 85 | 86 | 87 | Testing the protocol 88 | ~~~~~~~~~~~~~~~~~~~~ 89 | 90 | Because the protocol doesn't produce any I/O, you can directly simulates in 91 | which order, and which segments of data will be read/buffered. And what should 92 | be send back. 93 | 94 | .. literalinclude:: getting_started_ohneio_protocol.py 95 | :language: python 96 | :lines: 2, 33- 97 | 98 | 99 | Using a protocol 100 | ~~~~~~~~~~~~~~~~ 101 | 102 | Now that you wrote your echo protocol, you need to make it use the network, you 103 | can use any network library you want: 104 | 105 | 106 | Raw sockets 107 | ^^^^^^^^^^^ 108 | 109 | This example shows how to use Ohne I/O in combination with raw sockets and 110 | ``select()``. This creates one instance of the ``echo()`` protocol per connection. 111 | 112 | It then feeds data comming in the connection to the Ohne I/O protocol, then it 113 | feeds the output of the Ohne I/O protocol to the socket. 114 | 115 | .. code-block:: python 116 | :emphasize-lines: 38, 45-46 117 | 118 | import contextlib 119 | import select 120 | import socket 121 | 122 | 123 | BUFFER_SIZE = 1024 124 | 125 | 126 | @contextlib.contextmanager 127 | def create_server(host, port): 128 | for res in socket.getaddrinfo(host, port): 129 | af, socktype, proto, canonname, sa = res 130 | try: 131 | s = socket.socket(af, socktype, proto) 132 | except socket.error: 133 | continue 134 | try: 135 | s.bind(sa) 136 | s.listen(1) 137 | yield s 138 | return 139 | except socket.error: 140 | continue 141 | finally: 142 | s.close() 143 | raise RuntimeError("Couldn't create a connection") 144 | 145 | 146 | def echo_server(host, port): 147 | connections = {} # open connections: fileno -> (socket, protocol) 148 | with create_server(host, port) as server: 149 | while True: 150 | rlist, _, _ = select.select([server.fileno()] + list(connections.keys()), [], []) 151 | 152 | for fileno in rlist: 153 | if fileno == server.fileno(): # New connection 154 | conn, _ = server.accept() 155 | connections[conn.fileno()] = (conn, echo()) 156 | else: # Data comming in 157 | sock, proto = connections[fileno] 158 | data = sock.recv(BUFFER_SIZE) 159 | if not data: # Socket closed 160 | del connections[fileno] 161 | continue 162 | proto.send(data) 163 | data = proto.read() 164 | if data: 165 | sock.send(data) 166 | 167 | 168 | if __name__ == '__main__': 169 | import sys 170 | echo_server(host=sys.argv[1], port=sys.argv[2]) 171 | 172 | 173 | ``gevent`` 174 | ^^^^^^^^^^ 175 | 176 | This example shows how to use Ohne I/O in combination with gevent library: 177 | 178 | .. code-block:: python 179 | :emphasize-lines: 17,23-24 180 | 181 | import gevent 182 | from gevent.server import StreamServer 183 | 184 | 185 | BUFFER_SIZE = 1024 186 | 187 | 188 | def echo_server(host, port): 189 | server = StreamServer((host, port), handle) 190 | try: 191 | server.serve_forever() 192 | finally: 193 | server.close() 194 | 195 | 196 | def handle(socket, address): 197 | conn = echo() 198 | try: 199 | while True: 200 | data = socket.recv(BUFFER_SIZE) 201 | if not data: 202 | break 203 | conn.send(data) 204 | data = conn.read() 205 | if data: 206 | socket.send(data) 207 | gevent.sleep(0) # Prevent one green thread from taking over 208 | finally: 209 | socket.close() 210 | 211 | 212 | if __name__ == '__main__': 213 | import sys 214 | echo_server(host=sys.argv[1], port=int(sys.argv[2])) 215 | 216 | 217 | ``asyncio`` 218 | ^^^^^^^^^^^ 219 | 220 | 221 | .. code-block:: python 222 | :emphasize-lines: 7,10-11 223 | 224 | import asyncio 225 | 226 | 227 | class EchoProtocol(asyncio.Protocol): 228 | def connection_made(self, transport): 229 | self.transport = transport 230 | self.ohneio = echo() 231 | 232 | def data_received(self, data): 233 | self.ohneio.send(data) 234 | output = self.ohneio.read() 235 | if output: 236 | self.transport.write(output) 237 | 238 | 239 | 240 | if __name__ == '__main__': 241 | import sys 242 | loop = asyncio.get_event_loop() 243 | coro = loop.create_server(EchoProtocol, host=sys.argv[1], port=int(sys.argv[2])) 244 | server = loop.run_until_complete(coro) 245 | try: 246 | loop.run_forever() 247 | finally: 248 | server.close() 249 | loop.run_until_complete(server.wait_closed()) 250 | loop.close() 251 | -------------------------------------------------------------------------------- /docs/source/getting_started_ohneio_protocol.py: -------------------------------------------------------------------------------- 1 | import ohneio 2 | import pytest 3 | 4 | 5 | NEW_LINE = b'\n' 6 | 7 | 8 | def wait_for(s): 9 | """Wait for a certain string to be available in the buffer.""" 10 | while True: 11 | data = yield from ohneio.peek() 12 | pos = data.find(s) 13 | if pos > 0: 14 | return pos 15 | yield from ohneio.wait() 16 | 17 | 18 | def read_upto(s): 19 | """Read data up to the specified string in the buffer. 20 | 21 | If this string is not available in the buffer, this waits for the string to be available. 22 | """ 23 | pos = yield from wait_for(s) 24 | data = yield from ohneio.read(pos + len(s)) 25 | return data 26 | 27 | 28 | @ohneio.protocol 29 | def echo(): 30 | while True: 31 | line = yield from read_upto(NEW_LINE) 32 | yield from ohneio.write(line) 33 | 34 | 35 | @pytest.fixture 36 | def conn(): 37 | return echo() 38 | 39 | 40 | @pytest.mark.parametrize('input_, expected_output', [ 41 | (b"Hello World", b""), 42 | (b"Hello World\n", b"Hello World\n"), 43 | (b"Hello World\nAnd", b"Hello World\n"), 44 | (b"Hello World\nAnd the universe\n", b"Hello World\nAnd the universe\n"), 45 | ]) 46 | def test_only_echo_complete_lines(conn, input_, expected_output): 47 | conn.send(input_) 48 | assert conn.read() == expected_output 49 | 50 | 51 | def test_buffers_segments(conn): 52 | conn.send(b"Hello ") 53 | assert conn.read() == b'' 54 | conn.send(b"World\n") 55 | assert conn.read() == b"Hello World\n" 56 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | Ohne I/O 2 | ======== 3 | 4 | Ohne I/O (or ``ohneio``) is an utility library to write network protocol 5 | parsers without depending on any I/O system. 6 | 7 | This allows the python community to then use these protocol parsers with 8 | blocking raw sockets, asyncio, twisted, gevent, tornado, and/or 9 | threads/multiprocesses. 10 | 11 | 12 | .. toctree:: 13 | :maxdepth: 2 14 | 15 | getting_started 16 | api 17 | 18 | 19 | 20 | Indices and tables 21 | ================== 22 | 23 | * :ref:`genindex` 24 | * :ref:`modindex` 25 | * :ref:`search` 26 | 27 | -------------------------------------------------------------------------------- /ohneio.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import functools 3 | import inspect 4 | import io 5 | import typing 6 | 7 | 8 | class Buffer(typing.Sized): 9 | def __init__(self) -> None: 10 | self.queue = collections.deque() # type: collections.deque[bytes] 11 | self.position = 0 12 | 13 | def write(self, chunk: bytes) -> None: 14 | self.queue.append(chunk) 15 | 16 | def _get_queue(self) -> typing.Generator[bytes, typing.Any, typing.Any]: 17 | assert len(self.queue) > 0 or self.position == 0, ("We can't have a positive position " 18 | "on an empty queue.") 19 | 20 | q = iter(self.queue) 21 | try: 22 | data = next(q) 23 | yield data[self.position:] 24 | except StopIteration: 25 | pass 26 | else: 27 | yield from q 28 | 29 | def _get_data(self, nbytes: int): 30 | if nbytes == 0: 31 | return len(self.queue), 0, b''.join(self._get_queue()) 32 | else: 33 | acc = io.BytesIO() 34 | q = self._get_queue() 35 | segments_read = 0 36 | position = self.position 37 | to_read = nbytes 38 | 39 | while True: 40 | try: 41 | segment_len = acc.write(next(q)) 42 | to_read -= segment_len 43 | segments_read += 1 44 | except StopIteration: 45 | break 46 | 47 | if acc.tell() >= nbytes: 48 | assert to_read <= 0 49 | position += segment_len + to_read 50 | break 51 | 52 | position = 0 53 | 54 | acc.seek(0) 55 | return segments_read, position, acc.read(nbytes) 56 | 57 | def peek(self, nbytes: int=0) -> bytes: 58 | _, _, data = self._get_data(nbytes) 59 | return data 60 | 61 | def read(self, nbytes: int=0) -> bytes: 62 | segment_read, position, data = self._get_data(nbytes) 63 | if position > 0: 64 | segment_read -= 1 65 | for i in range(segment_read): 66 | self.queue.popleft() 67 | self.position = position 68 | 69 | assert len(self.queue) > 0 or self.position == 0, ("We can't have a positive position " 70 | "on an empty queue.") 71 | 72 | return data 73 | 74 | def __len__(self) -> int: 75 | return sum(len(d) for d in self.queue) - self.position 76 | 77 | def __repr__(self) -> str: 78 | return '<{self.__class__.__name__} {self.queue!r} pos={self.position}>'.format(self=self) 79 | 80 | 81 | class _NoResultType: 82 | pass 83 | 84 | 85 | class _StateEndedType: 86 | pass 87 | 88 | 89 | _no_result = _NoResultType() 90 | _state_ended = _StateEndedType() 91 | 92 | 93 | class NoResult(RuntimeError): 94 | """Raised when no result is available.""" 95 | 96 | 97 | class _Action: 98 | """Action yielded to the consumer. 99 | 100 | Actions yielded to the consumer could be `object()`, but this custom object 101 | with a custom `repr()` ease debugging. 102 | """ 103 | 104 | def __init__(self, name: str) -> None: 105 | self.name = name 106 | 107 | def __repr__(self) -> str: 108 | return ''.format(self.name) 109 | 110 | 111 | _get_input = _Action('get_input') 112 | _get_output = _Action('get_output') 113 | _wait = _Action('wait') 114 | 115 | 116 | T = typing.TypeVar('T') 117 | 118 | 119 | class ProtocolGenerator(typing.Generator[_Action, typing.Union[None, Buffer], T], 120 | typing.Generic[T]): 121 | pass 122 | 123 | 124 | S = typing.TypeVar('S') 125 | 126 | 127 | class Consumer(typing.Generic[S]): 128 | """Protocol consumer 129 | 130 | Allows the developer to send, read and get the result of protocol function. 131 | 132 | This never needs to be instantiated, since this internally done by the :func:`~ohneio.protocol` 133 | decorator. 134 | """ 135 | def __init__(self, gen: ProtocolGenerator[S]) -> None: 136 | self.gen = gen 137 | self.input = Buffer() 138 | self.output = Buffer() 139 | self.state = next(gen) # type: typing.Union[_Action, _StateEndedType] 140 | if not isinstance(self.state, _Action): # pragma: no cover 141 | # This is just a hint for users misusing the library. 142 | raise RuntimeError("Can't yield anything else than an action. Using `yield` instead " 143 | "`yield from`?") 144 | self.res = _no_result # type: typing.Union[S, _NoResultType] 145 | 146 | def _process(self) -> None: 147 | if self.has_result: 148 | return 149 | 150 | while self.state is _wait: 151 | self._next_state() 152 | while True: 153 | if self.state is _get_output: 154 | self._next_state(self.output) 155 | elif self.state is _get_input: 156 | self._next_state(self.input) 157 | else: 158 | break 159 | 160 | def _next_state(self, value: typing.Union[Buffer, None]=None) -> None: 161 | try: 162 | self.state = self.gen.send(value) 163 | if not isinstance(self.state, _Action): # pragma: no cover 164 | # This is just a hint for users misusing the library. 165 | raise RuntimeError("Can't yield anything else than an action. Using `yield` " 166 | "instead `yield from`?") 167 | except StopIteration as e: 168 | self.state = _state_ended 169 | if len(e.args) > 0: 170 | self.res = e.args[0] 171 | 172 | @property 173 | def has_result(self) -> bool: 174 | """bool: Whether a result is available or not""" 175 | return self.res is not _no_result 176 | 177 | def get_result(self) -> S: 178 | """Get the result from the protocol 179 | 180 | Returns: 181 | The object returned by the protocol. 182 | 183 | Raises: 184 | NoResult: When no result is available 185 | """ 186 | self._process() 187 | if not self.has_result: 188 | raise NoResult 189 | assert not isinstance(self.res, _NoResultType) 190 | return self.res 191 | 192 | def read(self, nbytes: int=0) -> bytes: 193 | """Read bytes from the output of the protocol. 194 | 195 | Args: 196 | nbytes: Read *at most* ``nbytes``, less bytes can be returned. If ``nbytes=0`` 197 | then all available bytes are read. 198 | 199 | Returns: 200 | bytes: bytes read 201 | """ 202 | acc = io.BytesIO() 203 | while True: 204 | if nbytes == 0: 205 | to_read = 0 206 | else: 207 | to_read = nbytes - acc.tell() 208 | 209 | acc.write(self.output.read(to_read)) 210 | 211 | if nbytes > 0 and acc.tell() == nbytes: 212 | break 213 | 214 | self._process() 215 | 216 | if len(self.output) == 0: 217 | break 218 | return acc.getvalue() 219 | 220 | def send(self, data: bytes) -> None: 221 | """Send data to the input of the protocol 222 | 223 | Args: 224 | bytes: data to send to the protocol 225 | 226 | Returns: 227 | None 228 | """ 229 | self.input.write(data) 230 | self._process() 231 | 232 | 233 | def peek(nbytes=0) -> typing.Generator[_Action, Buffer, bytes]: 234 | """Read output without consuming it. 235 | 236 | Read but **does not** consume data from the protocol input. 237 | 238 | This is a *non-blocking* primitive, if less data than requested is available, 239 | less data is returned. It is meant to be used in combination with :func:`~ohneio.wait`, but 240 | edge cases and smart people can (and most likely will) prove me wrong. 241 | 242 | Args: 243 | nbytes (:obj:`int`, optional): amount of bytes to read *at most*. ``0`` meaning all bytes. 244 | 245 | Returns: 246 | bytes: data read from the buffer 247 | """ 248 | input_ = yield _get_input 249 | return input_.peek(nbytes) 250 | 251 | 252 | def wait() -> typing.Generator[_Action, None, None]: 253 | """Wait for any action to be triggered on the consumer. 254 | 255 | This waits for any action to be triggered on the consumer, in most cases, this means more 256 | input is available or some output has been consumed. *But, it could also mean that the result 257 | of the protocol has been polled.* Therefore, it is always considered a good practice to wait 258 | at the end of a loop, and execute your non-blocking primitive before. 259 | 260 | Returns: 261 | None 262 | 263 | Example: 264 | >>> def wait_for(b): 265 | ... while True: 266 | ... data = yield from peek() 267 | ... pos = data.find(b) 268 | ... if pos >= 0: 269 | ... return pos 270 | ... yield from wait() 271 | ... 272 | >>> @protocol 273 | ... def linereader(): 274 | ... pos = yield from wait_for(b'\\n') 275 | ... data = yield from read(pos) 276 | ... return data.decode('ascii') 277 | ... 278 | >>> conn = linereader() 279 | >>> conn.send(b"Hello ") 280 | >>> conn.has_result 281 | False 282 | >>> conn.read() 283 | b'' 284 | >>> conn.send(b"World\\nRight?") 285 | >>> conn.get_result() 286 | 'Hello World' 287 | """ 288 | yield _wait 289 | 290 | 291 | def read(nbytes: int=0) -> typing.Generator[_Action, typing.Union[Buffer, None], bytes]: 292 | """Read and consume data. 293 | 294 | Read and consume data from the protocol input. And wait for it if an amount 295 | of bytes is specified. 296 | 297 | Args: 298 | nbytes (:obj:`int`, optional): amount of bytes to read. If ``nbytes=0``, it reads all 299 | data available in the buffer, and does not block. Therefore, it means if no data 300 | is available, it will return 0 bytes. 301 | 302 | Returns: 303 | bytes: data read from the buffer. 304 | 305 | Example: 306 | 307 | >>> @protocol 308 | ... def reader(): 309 | ... data = yield from read() 310 | ... print("Read:", repr(data)) 311 | ... data = yield from read(3) 312 | ... print("Read:", repr(data)) 313 | ... 314 | >>> conn = reader() 315 | >>> conn.send(b'Hello') 316 | Read: b'Hello' 317 | >>> conn.send(b'fo') 318 | >>> conn.send(b'obar') 319 | Read: b'foo' 320 | """ 321 | while True: 322 | input_ = yield _get_input 323 | if len(input_) >= nbytes: 324 | return input_.read(nbytes) 325 | yield from wait() 326 | 327 | 328 | def write(data: bytes) -> typing.Generator[_Action, typing.Union[Buffer, None], None]: 329 | """Write and flush data. 330 | 331 | Write data to the protocol output, and wait for it to be entirely consumed. 332 | 333 | *This is a generator function that has to be used with ``yield from``.* 334 | 335 | Args: 336 | data (bytes): data to write to the output. 337 | 338 | Returns: 339 | None: Only when the data has been entirely consumed. 340 | 341 | Example: 342 | 343 | >>> @protocol 344 | ... def foo(): 345 | ... yield from write(b'foo') 346 | ... return "Done!" 347 | ... 348 | >>> conn = foo() 349 | >>> conn.read(1) 350 | b'f' 351 | >>> conn.has_result 352 | False 353 | >>> conn.read(2) 354 | b'oo' 355 | >>> conn.get_result() 356 | 'Done!' 357 | """ 358 | output = yield _get_output 359 | output.write(data) 360 | while len(output) != 0: 361 | yield from wait() 362 | output = yield _get_output 363 | 364 | 365 | R = typing.TypeVar('R') 366 | 367 | 368 | def protocol(func: typing.Callable[..., ProtocolGenerator[R]]) -> typing.Callable[..., Consumer[R]]: 369 | """Wraps a Ohne I/O protocol function. 370 | 371 | Under the hood this wraps the generator inside a :class:`~ohneio.Consumer`. 372 | 373 | Args: 374 | func (callable): Protocol function to wrap. (Protocol functions have to be generators) 375 | 376 | Returns: 377 | callable: wrapped function. 378 | """ 379 | if not callable(func): # pragma: no cover 380 | # This is for users misusing the library, type hinting already checks this 381 | raise ValueError("A protocol needs to a be a callable") 382 | if not inspect.isgeneratorfunction(func): # pragma: no cover 383 | # This is for users misusing the library, type hinting already checks this 384 | raise ValueError("A protocol needs to be a generator function") 385 | 386 | @functools.wraps(func) 387 | def wrapper(*args, **kwargs): 388 | return Consumer(func(*args, **kwargs)) 389 | 390 | return wrapper 391 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --doctest-modules --doctest-glob=*.rst --ignore="setup.py" 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import os 4 | import sys 5 | 6 | from distutils.core import setup 7 | 8 | 9 | def read(fname): 10 | with open(os.path.abspath(os.path.join(os.path.dirname(__file__), fname)), 'r') as fp: 11 | return fp.read() 12 | 13 | 14 | install_requires = [] 15 | if sys.version_info < (3, 5): 16 | install_requires.append('typing') 17 | 18 | 19 | setup(name="ohneio", 20 | version="0.9.0-dev", 21 | description="Utility to write network protocol parser without any I/O", 22 | long_description=read('README'), 23 | license='ISC', 24 | author="Antoine Catton", 25 | author_email="devel@antoine.catton.fr", 26 | url="https://github.com/acatton/ohneio", 27 | py_modules=['ohneio'], 28 | install_requires=install_requires, 29 | classifiers=[ 30 | "Intended Audience :: Developers", 31 | "Intended Audience :: Telecommunications Industry", 32 | "License :: OSI Approved :: ISC License (ISCL)", 33 | "Operating System :: OS Independent", 34 | "Programming Language :: Python :: 3.4", 35 | "Programming Language :: Python :: 3.5", 36 | ], 37 | ) 38 | -------------------------------------------------------------------------------- /test_ohneio.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import ohneio 4 | 5 | BUFFER_SIZES = [1, 2, 3, 5, 7, 16, 100, 210, 256] 6 | 7 | 8 | @ohneio.protocol 9 | def echo_n_bytes(nbytes): 10 | while True: 11 | data = yield from ohneio.read(nbytes) 12 | yield from ohneio.write(data) 13 | 14 | 15 | @pytest.mark.parametrize('read_len', BUFFER_SIZES) 16 | @pytest.mark.parametrize('write_len', BUFFER_SIZES) 17 | def test_buffer(read_len, write_len): 18 | buf = ohneio.Buffer() 19 | data = bytes(write_len) 20 | buf.write(data) 21 | assert len(buf.read(read_len)) == min(write_len, read_len) 22 | assert len(buf.read(read_len)) == min(max(write_len - read_len, 0), read_len) 23 | 24 | 25 | def test_buffer_read_same_segment_multiple_times(): 26 | buf = ohneio.Buffer() 27 | data = [b'Hello', b'world'] 28 | for segment in data: 29 | buf.write(segment) 30 | for b in b''.join(data): 31 | assert buf.read(1) == bytes([b]) 32 | 33 | 34 | def test_buffer_read_chunks_over_different_segments(): 35 | buf = ohneio.Buffer() 36 | for segment in [b'Hello', b'World', b'No?']: 37 | buf.write(segment) 38 | assert buf.read(3) == b'Hel' 39 | assert buf.read(3) == b'loW' 40 | assert buf.read(3) == b'orl' 41 | assert buf.read(4) == b'dNo?' 42 | 43 | 44 | @pytest.mark.parametrize('nbytes', BUFFER_SIZES) 45 | @pytest.mark.parametrize('data_len', BUFFER_SIZES) 46 | def test_echo_n_bytes(nbytes, data_len): 47 | conn = echo_n_bytes(nbytes) 48 | data = b'\x00' * data_len 49 | 50 | sent = 0 51 | while sent < nbytes: 52 | assert len(conn.read()) == 0 53 | conn.send(data) 54 | sent += data_len 55 | 56 | assert len(conn.read(nbytes)) == nbytes 57 | 58 | 59 | def wait_for(s): 60 | while True: 61 | data = yield from ohneio.peek() 62 | pos = data.find(s) 63 | if pos >= 0: 64 | return pos 65 | yield from ohneio.wait() 66 | 67 | 68 | def read_until(s): 69 | pos = yield from wait_for(LINE_SEPARATOR) 70 | if pos > 0: 71 | data = yield from ohneio.read(pos) 72 | else: 73 | data = b'' 74 | return data 75 | 76 | 77 | LINE_SEPARATOR = b'\n' 78 | 79 | 80 | @ohneio.protocol 81 | def line_reader(): 82 | line = yield from read_until(LINE_SEPARATOR) 83 | return line 84 | 85 | 86 | @pytest.mark.parametrize('segment_len', BUFFER_SIZES) 87 | @pytest.mark.parametrize('input_,expected', [ 88 | (b'\nhello', b''), 89 | (b'hello\n', b'hello'), 90 | (b'hello\nhello', b'hello'), 91 | ]) 92 | def test_line_reader(segment_len, input_, expected): 93 | conn = line_reader() 94 | 95 | for start in range(0, len(input_) + 1, segment_len): 96 | end = start + segment_len 97 | segment = input_[start:end] 98 | conn.send(segment) 99 | 100 | assert conn.has_result 101 | assert conn.get_result() == expected 102 | 103 | 104 | def test_line_reader_no_result(): 105 | conn = line_reader() 106 | conn.send(b'hello') 107 | assert not conn.has_result 108 | 109 | 110 | @ohneio.protocol 111 | def echo(): 112 | while True: 113 | line = yield from read_until(LINE_SEPARATOR) 114 | yield from ohneio.read(len(LINE_SEPARATOR)) 115 | yield from ohneio.write(line) 116 | yield from ohneio.write(LINE_SEPARATOR) 117 | 118 | 119 | def test_echo(): 120 | conn = echo() 121 | conn.send(b'hello') 122 | assert conn.read() == b'' 123 | conn.send(b'\nworld') 124 | assert conn.read() == b'hello\n' 125 | conn.send(b'\nand the rest\n') 126 | assert conn.read() == b'world\nand the rest\n' 127 | 128 | 129 | @ohneio.protocol 130 | def hello(): 131 | yield from ohneio.write(b"Hello") 132 | return "Hello" 133 | 134 | 135 | def test_get_result(): 136 | conn = hello() 137 | with pytest.raises(ohneio.NoResult): 138 | conn.get_result() 139 | assert conn.read(5) == b"Hello" 140 | assert conn.get_result() == "Hello" 141 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py34, py35, flake8 3 | 4 | [testenv] 5 | deps = 6 | pytest 7 | pytest-cov 8 | commands = pytest --cov=ohneio.py --cov-report html --cov-report term {posargs} 9 | 10 | [testenv:py34] 11 | python = python3.4 12 | 13 | [testenv:py35] 14 | python = python3.5 15 | 16 | [testenv:py36] 17 | python = python3.6 18 | 19 | [testenv:mypy] 20 | # Waiting for to be merged before enabling 21 | # this. 22 | python = python3.5 23 | deps = mypy-lang 24 | commands = mypy ohneio.py 25 | 26 | [testenv:docs] 27 | python = python3.5 28 | deps = sphinx 29 | changedir = docs 30 | whitelist_externals = make 31 | commands = make html 32 | 33 | [testenv:flake8] 34 | skipsdist = True 35 | skip_install = True 36 | deps = hacking 37 | commands = flake8 ohneio.py test_ohneio.py 38 | 39 | [flake8] 40 | ignore = H238 41 | max-line-length = 100 42 | 43 | [tox:travis] 44 | 3.4 = py34 45 | 3.5 = py35, flake8 46 | 3.5-dev = py35 47 | nightly = py36 48 | --------------------------------------------------------------------------------