├── .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 |
--------------------------------------------------------------------------------