├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── doc_requires.txt ├── docs ├── Makefile ├── conf.py ├── decode.rst ├── encode.rst ├── index.rst └── schema.rst ├── jsonweb ├── __init__.py ├── _local.py ├── decode.py ├── encode.py ├── exceptions.py ├── py3k.py ├── schema.py ├── tests │ ├── __init__.py │ ├── test_decode.py │ ├── test_decode_schema.py │ ├── test_encode.py │ ├── test_local.py │ ├── test_py3k.py │ ├── test_schema.py │ └── test_validators.py └── validators.py ├── setup.cfg ├── setup.py └── test_requires.txt /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | .coverage 4 | *.egg-info 5 | *.egg 6 | dist/ 7 | build/ 8 | docs/_build 9 | docs/_static 10 | docs/_templates 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.2" 5 | - "3.3" 6 | install: "pip install -r test_requires.txt --use-mirrors" 7 | script: python setup.py test -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | JsonWeb Changelog 2 | ================= 3 | 4 | Version 0.8.1 5 | ------------- 6 | -- Added :meth:`~jsonweb.schema.ObjectSchema.create` which be used to create 7 | object schemas in a non-declarative style. 8 | 9 | Version 0.8 10 | ----------- 11 | 12 | -- Added :class:`~jsonweb.validators.Dict` 13 | -- Added ``default`` kw arg to validators 14 | -- Added ``reason_code`` to :class:`~jsonweb.validators.ValidationError` 15 | -- Refactored the ``jsonweb.schema`` package into `~jsonweb.validators` and 16 | `~jsonweb.schema` modules. (Breaks backwards compatibility) 17 | 18 | 19 | Version 0.7.1 20 | ------------- 21 | 22 | -- Fixed exception when calling ValidationError.to_json in python 3 23 | 24 | 25 | Version 0.7.0 26 | ------------- 27 | 28 | -- Python 3 support! 29 | 30 | 31 | Version 0.6.6 32 | ------------- 33 | - Added `min_len` kw arg to :class:`~jsonweb.schema.validators.String` 34 | 35 | 36 | Version 0.6.5 37 | ------------- 38 | 39 | - Renamed README to README.rst 40 | - Renamed CHANGES to CHANGES.rst 41 | 42 | 43 | Version 0.6.4 44 | -------------- 45 | 46 | - Added :class:`~jsonweb.schema.validators.OneOf` 47 | - Added :class:`~jsonweb.schema.validators.SubSetOf` 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 by Shawn Adams 2 | 3 | Some rights reserved. 4 | 5 | Redistribution and use in source and binary forms of the software as well 6 | as documentation, with or without modification, are permitted provided 7 | that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright 10 | notice, this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above 13 | copyright notice, this list of conditions and the following 14 | disclaimer in the documentation and/or other materials provided 15 | with the distribution. 16 | 17 | * The names of the contributors may not be used to endorse or 18 | promote products derived from this software without specific 19 | prior written permission. 20 | 21 | THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND 22 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT 23 | NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 24 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER 25 | OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 26 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 27 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 28 | PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 29 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING 30 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH 32 | DAMAGE. -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include test_requires.txt 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | JsonWeb 2 | ======== 3 | 4 | .. image:: https://travis-ci.org/boris317/JsonWeb.png?branch=master 5 | :target: https://travis-ci.org/boris317/JsonWeb 6 | 7 | Add JSON (de)serialization to your python objects :: 8 | 9 | >>> from jsonweb import decode, encode 10 | 11 | >>> @encode.to_object() 12 | ... @decode.from_object() 13 | ... class User(object): 14 | ... def __init__(self, nick, email): 15 | ... self.nick = nick 16 | ... self.email = email 17 | 18 | >>> json_str = encode.dumper(User("cool_user123", "cool_user123@example.com")) 19 | >>> print json_str 20 | {"nick": "cool_user123", "__type__": "User", "email": "cool_user123@example.com"} 21 | 22 | >>> user = decode.loader(json_str) 23 | >>> print user.nick 24 | cool_user123 25 | >>> print user 26 | 27 | 28 | .. note :: 29 | 30 | JsonWeb is still very much under development. Things will change. 31 | 32 | See `documentation `_ 33 | 34 | -------------------------------------------------------------------------------- /doc_requires.txt: -------------------------------------------------------------------------------- 1 | alabaster 2 | -------------------------------------------------------------------------------- /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) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/JsonWeb.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/JsonWeb.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/JsonWeb" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/JsonWeb" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # JsonWeb documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Jan 23 12:09:37 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | sys.path.append("../") 17 | 18 | # If extensions (or modules to document with autodoc) are in another directory, 19 | # add these directories to sys.path here. If the directory is relative to the 20 | # documentation root, use os.path.abspath to make it absolute, like shown here. 21 | #sys.path.insert(0, os.path.abspath('.')) 22 | 23 | # -- General configuration ----------------------------------------------------- 24 | 25 | # If your documentation needs a minimal Sphinx version, state it here. 26 | #needs_sphinx = '1.0' 27 | 28 | # Add any Sphinx extension module names here, as strings. They can be extensions 29 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 30 | extensions = [ 31 | 'sphinx.ext.autodoc', 32 | 'sphinx.ext.todo', 33 | 'sphinx.ext.coverage', 34 | 'sphinx.ext.intersphinx', 35 | 'alabaster' 36 | ] 37 | 38 | intersphinx_mapping = {'python': ('http://docs.python.org/2.7', None)} 39 | 40 | # Add any paths that contain templates here, relative to this directory. 41 | templates_path = ['_templates'] 42 | 43 | # The suffix of source filenames. 44 | source_suffix = '.rst' 45 | 46 | # The encoding of source files. 47 | #source_encoding = 'utf-8-sig' 48 | 49 | # The master toctree document. 50 | master_doc = 'index' 51 | 52 | # General information about the project. 53 | project = u'JsonWeb' 54 | copyright = u'2014, shawn adams' 55 | 56 | # The version info for the project you're documenting, acts as replacement for 57 | # |version| and |release|, also used in various other places throughout the 58 | # built documents. 59 | # 60 | # The short X.Y version. 61 | version = '0.8.2' 62 | # The full version, including alpha/beta/rc tags. 63 | release = '0.8.2' 64 | 65 | # The language for content autogenerated by Sphinx. Refer to documentation 66 | # for a list of supported languages. 67 | #language = None 68 | 69 | # There are two options for replacing |today|: either, you set today to some 70 | # non-false value, then it is used: 71 | #today = '' 72 | # Else, today_fmt is used as the format for a strftime call. 73 | #today_fmt = '%B %d, %Y' 74 | 75 | # List of patterns, relative to source directory, that match files and 76 | # directories to ignore when looking for source files. 77 | exclude_patterns = ['_build'] 78 | 79 | # The reST default role (used for this markup: `text`) to use for all documents. 80 | #default_role = None 81 | 82 | # If true, '()' will be appended to :func: etc. cross-reference text. 83 | #add_function_parentheses = True 84 | 85 | # If true, the current module name will be prepended to all description 86 | # unit titles (such as .. function::). 87 | #add_module_names = True 88 | 89 | # If true, sectionauthor and moduleauthor directives will be shown in the 90 | # output. They are ignored by default. 91 | #show_authors = False 92 | 93 | # The name of the Pygments (syntax highlighting) style to use. 94 | pygments_style = 'sphinx' 95 | 96 | # A list of ignored prefixes for module index sorting. 97 | #modindex_common_prefix = [] 98 | 99 | 100 | # -- Options for HTML output --------------------------------------------------- 101 | 102 | # The theme to use for HTML and HTML Help pages. See the documentation for 103 | # a list of builtin themes. 104 | import alabaster 105 | 106 | html_theme = 'alabaster' 107 | html_theme_path = [alabaster.get_path()] 108 | html_sidebars = { 109 | '**': ['about.html', 'navigation.html', 'searchbox.html'] 110 | } 111 | 112 | # Theme options are theme-specific and customize the look and feel of a theme 113 | # further. For a list of options available for each theme, see the 114 | # documentation. 115 | html_theme_options = { 116 | 'github_user': 'boris317', 117 | 'github_repo': 'JsonWeb', 118 | 'travis_button': True, 119 | 'github_banner': True 120 | } 121 | 122 | # Add any paths that contain custom themes here, relative to this directory. 123 | #html_theme_path = [] 124 | 125 | # The name for this set of Sphinx documents. If None, it defaults to 126 | # " v documentation". 127 | #html_title = None 128 | 129 | # A shorter title for the navigation bar. Default is the same as html_title. 130 | #html_short_title = None 131 | 132 | # The name of an image file (relative to this directory) to place at the top 133 | # of the sidebar. 134 | #html_logo = None 135 | 136 | # The name of an image file (within the static path) to use as favicon of the 137 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 138 | # pixels large. 139 | #html_favicon = None 140 | 141 | # Add any paths that contain custom static files (such as style sheets) here, 142 | # relative to this directory. They are copied after the builtin static files, 143 | # so a file named "default.css" will overwrite the builtin "default.css". 144 | html_static_path = ['_static'] 145 | 146 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 147 | # using the given strftime format. 148 | #html_last_updated_fmt = '%b %d, %Y' 149 | 150 | # If true, SmartyPants will be used to convert quotes and dashes to 151 | # typographically correct entities. 152 | #html_use_smartypants = True 153 | 154 | # Custom sidebar templates, maps document names to template names. 155 | #html_sidebars = {} 156 | 157 | # Additional templates that should be rendered to pages, maps page names to 158 | # template names. 159 | #html_additional_pages = {} 160 | 161 | # If false, no module index is generated. 162 | #html_domain_indices = True 163 | 164 | # If false, no index is generated. 165 | #html_use_index = True 166 | 167 | # If true, the index is split into individual pages for each letter. 168 | #html_split_index = False 169 | 170 | # If true, links to the reST sources are added to the pages. 171 | #html_show_sourcelink = True 172 | 173 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 174 | #html_show_sphinx = True 175 | 176 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 177 | #html_show_copyright = True 178 | 179 | # If true, an OpenSearch description file will be output, and all pages will 180 | # contain a tag referring to it. The value of this option must be the 181 | # base URL from which the finished HTML is served. 182 | #html_use_opensearch = '' 183 | 184 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 185 | #html_file_suffix = None 186 | 187 | # Output file base name for HTML help builder. 188 | htmlhelp_basename = 'JsonWebdoc' 189 | 190 | 191 | # -- Options for LaTeX output -------------------------------------------------- 192 | 193 | latex_elements = { 194 | # The paper size ('letterpaper' or 'a4paper'). 195 | #'papersize': 'letterpaper', 196 | 197 | # The font size ('10pt', '11pt' or '12pt'). 198 | #'pointsize': '10pt', 199 | 200 | # Additional stuff for the LaTeX preamble. 201 | #'preamble': '', 202 | } 203 | 204 | # Grouping the document tree into LaTeX files. List of tuples 205 | # (source start file, target name, title, author, documentclass [howto/manual]). 206 | latex_documents = [ 207 | ('index', 'JsonWeb.tex', u'JsonWeb Documentation', 208 | u'shawn adams', 'manual'), 209 | ] 210 | 211 | # The name of an image file (relative to this directory) to place at the top of 212 | # the title page. 213 | #latex_logo = None 214 | 215 | # For "manual" documents, if this is true, then toplevel headings are parts, 216 | # not chapters. 217 | #latex_use_parts = False 218 | 219 | # If true, show page references after internal links. 220 | #latex_show_pagerefs = False 221 | 222 | # If true, show URL addresses after external links. 223 | #latex_show_urls = False 224 | 225 | # Documents to append as an appendix to all manuals. 226 | #latex_appendices = [] 227 | 228 | # If false, no module index is generated. 229 | #latex_domain_indices = True 230 | 231 | 232 | # -- Options for manual page output -------------------------------------------- 233 | 234 | # One entry per manual page. List of tuples 235 | # (source start file, name, description, authors, manual section). 236 | man_pages = [ 237 | ('index', 'jsonweb', u'JsonWeb Documentation', 238 | [u'shawn adams'], 1) 239 | ] 240 | 241 | # If true, show URL addresses after external links. 242 | #man_show_urls = False 243 | 244 | 245 | # -- Options for Texinfo output ------------------------------------------------ 246 | 247 | # Grouping the document tree into Texinfo files. List of tuples 248 | # (source start file, target name, title, author, 249 | # dir menu entry, description, category) 250 | texinfo_documents = [ 251 | ('index', 'JsonWeb', u'JsonWeb Documentation', 252 | u'shawn adams', 'JsonWeb', 'One line description of project.', 253 | 'Miscellaneous'), 254 | ] 255 | 256 | # Documents to append as an appendix to all manuals. 257 | #texinfo_appendices = [] 258 | 259 | # If false, no module index is generated. 260 | texinfo_domain_indices = True 261 | 262 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 263 | #texinfo_show_urls = 'footnote' 264 | -------------------------------------------------------------------------------- /docs/decode.rst: -------------------------------------------------------------------------------- 1 | :mod:`jsonweb.decode` -- decode your python classes 2 | =================================================== 3 | 4 | .. automodule:: jsonweb.decode 5 | 6 | loader 7 | ------ 8 | .. autofunction:: loader 9 | 10 | Decorators 11 | ---------- 12 | 13 | .. autofunction:: from_object 14 | 15 | .. _object_hook: 16 | 17 | Object hook 18 | ----------- 19 | .. autofunction:: object_hook 20 | .. autoclass:: ObjectHook 21 | :members: decode_obj 22 | 23 | ``as_type`` context mananger 24 | ----------------------------- 25 | .. autofunction:: ensure_type 26 | -------------------------------------------------------------------------------- /docs/encode.rst: -------------------------------------------------------------------------------- 1 | :mod:`jsonweb.encode` -- encode your python classes 2 | =================================================== 3 | 4 | .. automodule:: jsonweb.encode 5 | 6 | dumper 7 | ------ 8 | .. autofunction:: dumper 9 | 10 | Decorators 11 | ---------- 12 | 13 | .. autofunction:: to_object 14 | .. autofunction:: to_list 15 | .. autofunction:: handler 16 | 17 | JsonWebEncoder 18 | -------------- 19 | 20 | .. autoclass:: JsonWebEncoder 21 | :members: -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. JsonWeb documentation master file, created by 2 | sphinx-quickstart on Mon Jan 23 12:09:37 2012. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | JsonWeb 7 | ======= 8 | Quickly add JSON encoding/decoding to your python objects. 9 | 10 | 11 | Main documentation 12 | ------------------- 13 | 14 | To get the best understanding of JsonWeb you should read the 15 | documentation in order. As each section builds a little bit on the last. 16 | 17 | .. toctree:: 18 | :maxdepth: 2 19 | 20 | encode 21 | decode 22 | schema 23 | 24 | Indices and tables 25 | ------------------ 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | 31 | -------------------------------------------------------------------------------- /docs/schema.rst: -------------------------------------------------------------------------------- 1 | 2 | .. contents:: Table of Contents 3 | :depth: 3 4 | 5 | :mod:`jsonweb.schema` 6 | ============================= 7 | 8 | .. automodule:: jsonweb.schema 9 | 10 | :mod:`jsonweb.validators` 11 | ============================= 12 | 13 | .. automodule:: jsonweb.validators 14 | 15 | .. autoclass:: jsonweb.validators.BaseValidator 16 | :members: 17 | 18 | .. automethod:: __init__ 19 | 20 | .. autoclass:: jsonweb.validators.String 21 | .. autoclass:: jsonweb.validators.Regex 22 | .. autoclass:: jsonweb.validators.Number 23 | .. autoclass:: jsonweb.validators.Integer 24 | .. autoclass:: jsonweb.validators.Float 25 | .. autoclass:: jsonweb.validators.Boolean 26 | .. autoclass:: jsonweb.validators.DateTime 27 | .. autoclass:: jsonweb.validators.EnsureType 28 | .. autoclass:: jsonweb.validators.List 29 | .. autoclass:: jsonweb.validators.Dict 30 | .. autoclass:: jsonweb.validators.OneOf 31 | .. autoclass:: jsonweb.validators.SubSetOf 32 | 33 | ValidationErrors 34 | ---------------- 35 | 36 | .. autoclass:: jsonweb.validators.ValidationError 37 | :members: 38 | 39 | .. automethod:: __init__ 40 | -------------------------------------------------------------------------------- /jsonweb/__init__.py: -------------------------------------------------------------------------------- 1 | from jsonweb.decode import loader, from_object 2 | from jsonweb.encode import dumper, to_object 3 | 4 | -------------------------------------------------------------------------------- /jsonweb/_local.py: -------------------------------------------------------------------------------- 1 | try: 2 | from threading import local 3 | except ImportError: 4 | from dummy_threading import local 5 | 6 | 7 | class LocalStack(local): 8 | def __init__(self): 9 | self.stack = [] 10 | 11 | def push(self, obj): 12 | self.stack.append(obj) 13 | 14 | def pop(self): 15 | try: 16 | return self.stack.pop() 17 | except IndexError: 18 | return None 19 | 20 | def clear(self): 21 | self.stack = [] 22 | 23 | @property 24 | def top(self): 25 | try: 26 | return self.stack[-1] 27 | except IndexError: 28 | return None 29 | -------------------------------------------------------------------------------- /jsonweb/decode.py: -------------------------------------------------------------------------------- 1 | """ 2 | Sometimes it would be nice to have :func:`json.loads` return class instances. 3 | For example if you do something like this :: 4 | 5 | person = json.loads(''' 6 | { 7 | "__type__": "Person", 8 | "first_name": "Shawn", 9 | "last_name": "Adams" 10 | } 11 | ''') 12 | 13 | it would be pretty cool if instead of ``person`` being a :class:`dict` it was 14 | an instance of a class we defined called :class:`Person`. Luckily the python 15 | standard :mod:`json` module provides support for *class hinting* in the form 16 | of the ``object_hook`` keyword argument accepted by :func:`json.loads`. 17 | 18 | The code in :mod:`jsonweb.decode` uses this ``object_hook`` interface to 19 | accomplish the awesomeness you are about to witness. Lets turn that 20 | ``person`` :class:`dict` into a proper :class:`Person` instance. :: 21 | 22 | >>> from jsonweb.decode import from_object, loader 23 | 24 | >>> @from_object() 25 | ... class Person(object): 26 | ... def __init__(self, first_name, last_name): 27 | ... self.first_name = first_name 28 | ... self.last_name = last_name 29 | ... 30 | 31 | >>> person = loader(''' 32 | ... { 33 | ... "__type__": "Person", 34 | ... "first_name": "Shawn", 35 | ... "last_name": "Adams" 36 | ... } 37 | ... ''') 38 | 39 | >>> print type(person) 40 | 41 | >>> print person.first_name 42 | "Shawn" 43 | 44 | But how was :mod:`jsonweb` able to determine how to instantiate the 45 | :class:`Person` class? Take a look at the :func:`from_object` decorator for a 46 | detailed explanation. 47 | """ 48 | 49 | import inspect 50 | import json 51 | from contextlib import contextmanager 52 | from jsonweb.py3k import items 53 | 54 | from jsonweb.validators import EnsureType 55 | from jsonweb.exceptions import JsonWebError 56 | from jsonweb._local import LocalStack 57 | 58 | _DATETIME_FORMAT = "%Y-%m-%dT%H:%M:%S" 59 | 60 | # Thread local object stack used by :func:`ensure_type` 61 | _as_type_context = LocalStack() 62 | 63 | 64 | class JsonDecodeError(JsonWebError): 65 | """ 66 | Raised for malformed json. 67 | """ 68 | def __init__(self, message, **extras): 69 | JsonWebError.__init__(self, message, **extras) 70 | 71 | 72 | class ObjectDecodeError(JsonWebError): 73 | """ 74 | Raised when python containers (dicts and lists) cannot be decoded into 75 | complex types. These exceptions are raised from within an ObjectHook 76 | instance. 77 | """ 78 | def __init__(self, message, **extras): 79 | JsonWebError.__init__(self, message, **extras) 80 | 81 | 82 | class ObjectAttributeError(ObjectDecodeError): 83 | def __init__(self, obj_type, attr): 84 | ObjectDecodeError.__init__( 85 | self, 86 | "Missing {0} attribute for {1}.".format(attr, obj_type), 87 | obj_type=obj_type, 88 | attribute=attr 89 | ) 90 | 91 | 92 | class ObjectNotFoundError(ObjectDecodeError): 93 | def __init__(self, obj_type): 94 | ObjectDecodeError.__init__( 95 | self, 96 | "Cannot decode object {0}. No such object.".format(obj_type), 97 | obj_type=obj_type, 98 | ) 99 | 100 | 101 | class JsonWebObjectHandler(object): 102 | def __init__(self, args, kw_args=None): 103 | self.args = args 104 | self.kw_args = kw_args 105 | 106 | def __call__(self, cls, obj): 107 | cls_args = [] 108 | cls_kw_args = {} 109 | 110 | for arg in self.args: 111 | cls_args.append(obj[arg]) 112 | 113 | if self.kw_args: 114 | for key, default in self.kw_args: 115 | cls_kw_args[key] = obj.get(key, default) 116 | 117 | return cls(*cls_args, **cls_kw_args) 118 | 119 | 120 | class _ObjectHandlers(object): 121 | def __init__(self): 122 | self.__handlers = {} 123 | self.__deferred_updates = {} 124 | 125 | def add_handler(self, cls, handler, type_name=None, schema=None): 126 | name = type_name or cls.__name__ 127 | self.__handlers[name] = self.__merge_tuples( 128 | self.__deferred_updates.get(name, (None,)*3), 129 | (handler, cls, schema) 130 | ) 131 | 132 | def get(self, name): 133 | """ 134 | Get a handler tuple. Return None if no such handler. 135 | """ 136 | return self.__handlers.get(name) 137 | 138 | def set(self, name, handler_tuple): 139 | """ 140 | Add a handler tuple (handler, cls, schema) 141 | """ 142 | self.__handlers[name] = handler_tuple 143 | 144 | def clear(self): 145 | self.__handlers = {} 146 | self.__deferred_updates = {} 147 | 148 | def update_handler(self, name, cls=None, handler=None, schema=None): 149 | """ 150 | Modify cls, handler and schema for a decorated class. 151 | """ 152 | handler_tuple = self.__handlers[name] 153 | self.set(name, self.__merge_tuples((handler, cls, schema), 154 | handler_tuple)) 155 | 156 | def update_handler_deferred(self, name, cls=None, 157 | handler=None, schema=None): 158 | """ 159 | If an entry does not exist in __handlers an entry will be added to 160 | __deferred_updates instead. Then when add_handler is finally called 161 | values will be updated accordingly. Items in __deferred_updates will 162 | take precedence over those passed into add_handler. 163 | """ 164 | if name in self.__handlers: 165 | self.update_handler(name, cls, handler, schema) 166 | return 167 | d = self.__deferred_updates.get(name, (None,)*3) 168 | self.__deferred_updates[name] = self.__merge_tuples( 169 | (handler, cls, schema), d) 170 | 171 | def copy(self): 172 | handler_copy = _ObjectHandlers() 173 | [handler_copy.set(n, t) for n, t in self] 174 | return handler_copy 175 | 176 | def __merge_tuples(self, a_tuple, b_tuple): 177 | """ 178 | "Merge" two tuples of the same length. a takes precedence over b. 179 | """ 180 | if len(a_tuple) != len(b_tuple): 181 | raise ValueError("Iterators differ in length.") 182 | return tuple([(a or b) for a, b in zip(a_tuple, b_tuple)]) 183 | 184 | def __contains__(self, handler_name): 185 | return handler_name in self.__handlers 186 | 187 | def __getitem__(self, handler): 188 | return self.__handlers[handler] 189 | 190 | def __iter__(self): 191 | for name, handler_tuple in items(self.__handlers): 192 | yield name, handler_tuple 193 | 194 | 195 | class ObjectHook(object): 196 | """ 197 | This class does most of the work in managing the handlers that decode the 198 | json into python class instances. You should not need to use this class 199 | directly. :func:`object_hook` is responsible for instantiating and using it. 200 | """ 201 | 202 | def __init__(self, handlers, validate=True): 203 | self.handlers = handlers 204 | self.validate = validate 205 | 206 | def decode_obj(self, obj): 207 | """ 208 | This method is called for every dict decoded in a json string. The 209 | presence of the key ``__type__`` in ``obj`` will trigger a lookup in 210 | ``self.handlers``. If a handler is not found for ``__type__`` then an 211 | :exc:`ObjectNotFoundError` is raised. If a handler is found it will 212 | be called with ``obj`` as it only argument. If an :class:`ObjectSchema` 213 | was supplied for the class, ``obj`` will first be validated then passed 214 | to handler. The handler should return a new python instant of type ``__type__``. 215 | """ 216 | if "__type__" not in obj: 217 | return obj 218 | 219 | obj_type = obj["__type__"] 220 | try: 221 | factory, cls, schema = self.handlers[obj_type] 222 | except KeyError: 223 | raise ObjectNotFoundError(obj_type) 224 | 225 | if schema and self.validate: 226 | obj = schema().validate(obj) 227 | try: 228 | return factory(cls, obj) 229 | except KeyError as e: 230 | raise ObjectAttributeError(obj_type, e.args[0]) 231 | 232 | 233 | def get_arg_spec(func): 234 | arg_spec = inspect.getargspec(func) 235 | args = arg_spec.args 236 | 237 | try: 238 | if args[0] == "self": 239 | del args[0] 240 | except IndexError: 241 | pass 242 | 243 | if not args: 244 | return None 245 | 246 | kw_args = [] 247 | if arg_spec.defaults: 248 | for default in reversed(arg_spec.defaults): 249 | kw_args.append((args.pop(), default)) 250 | 251 | return args, kw_args 252 | 253 | 254 | def get_jsonweb_handler(cls): 255 | arg_spec = get_arg_spec(cls.__init__) 256 | if arg_spec is None: 257 | raise JsonWebError("Unable to generate an object_hook handler from " 258 | "{0}'s `__init__` method.".format(cls.__name__)) 259 | args, kw = get_arg_spec(cls.__init__) 260 | 261 | return JsonWebObjectHandler(args, kw or None) 262 | 263 | _default_object_handlers = _ObjectHandlers() 264 | 265 | 266 | def from_object(handler=None, type_name=None, schema=None): 267 | """ 268 | Decorating a class with :func:`from_object` will allow :func:`json.loads` 269 | to return instances of that class. 270 | 271 | ``handler`` is a callable that should return your class instance. It 272 | receives two arguments, your class and a python dict. Here is an 273 | example:: 274 | 275 | >>> from jsonweb.decode import from_object, loader 276 | >>> def person_decoder(cls, obj): 277 | ... return cls( 278 | ... obj["first_name"], 279 | ... obj["last_name"] 280 | ... ) 281 | ... 282 | >>> @from_object(person_decoder) 283 | ... class Person(object): 284 | ... def __init__(self, first_name, last_name): 285 | ... self.first_name 286 | ... self.last_name = last_name 287 | ... 288 | >>> person_json = '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams"}' 289 | >>> person = loader(person_json) 290 | >>> person 291 | 292 | >>> person.first_name 293 | 'Shawn' 294 | 295 | The ``__type__`` key is very important. Without it :mod:`jsonweb` would 296 | not know which handler to delegate the python dict to. By default 297 | :func:`from_object` assumes ``__type__`` will be the class's ``__name__`` 298 | attribute. You can specify your own value by setting the ``type_name`` 299 | keyword argument :: 300 | 301 | @from_object(person_decoder, type_name="PersonObject") 302 | 303 | Which means the json string would need to be modified to look like this:: 304 | 305 | '{"__type__": "PersonObject", "first_name": "Shawn", "last_name": "Adams"}' 306 | 307 | If a handler cannot be found for ``__type__`` an exception is raised :: 308 | 309 | >>> luke = loader('{"__type__": "Jedi", "name": "Luke"}') 310 | Traceback (most recent call last): 311 | ... 312 | ObjectNotFoundError: Cannot decode object Jedi. No such object. 313 | 314 | You may have noticed that ``handler`` is optional. If you do not specify 315 | a ``handler`` :mod:`jsonweb` will attempt to generate one. It will 316 | inspect your class's ``__init__`` method. Any positional arguments will 317 | be considered required while keyword arguments will be optional. 318 | 319 | .. warning:: 320 | 321 | A handler cannot be generated from a method signature containing only 322 | ``*args`` and ``**kwargs``. The handler would not know which keys to 323 | pull out of the python dict. 324 | 325 | Lets look at a few examples:: 326 | 327 | >>> from jsonweb import from_object 328 | >>> @from_object() 329 | ... class Person(object): 330 | ... def __init__(self, first_name, last_name, gender): 331 | ... self.first_name = first_name 332 | ... self.last_name = last_name 333 | ... self.gender = gender 334 | 335 | >>> person_json = '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams", "gender": "male"}' 336 | >>> person = loader(person_json) 337 | 338 | What happens if we dont want to specify ``gender``:: 339 | 340 | >>> person_json = '''{ 341 | ... "__type__": "Person", 342 | ... "first_name": "Shawn", 343 | ... "last_name": "Adams" 344 | ... }''' 345 | >>> person = loader(person_json) 346 | Traceback (most recent call last): 347 | ... 348 | ObjectAttributeError: Missing gender attribute for Person. 349 | 350 | To make ``gender`` optional it must be a keyword argument:: 351 | 352 | >>> from jsonweb import from_object 353 | >>> @from_object() 354 | ... class Person(object): 355 | ... def __init__(self, first_name, last_name, gender=None): 356 | ... self.first_name = first_name 357 | ... self.last_name = last_name 358 | ... self.gender = gender 359 | 360 | >>> person_json = '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams"}' 361 | >>> person = loader(person_json) 362 | >>> print person.gender 363 | None 364 | 365 | You can specify a json validator for a class with the ``schema`` keyword agrument. 366 | Here is a quick example:: 367 | 368 | >>> from jsonweb import from_object 369 | >>> from jsonweb.schema import ObjectSchema 370 | >>> from jsonweb.validators import ValidationError, String 371 | >>> class PersonSchema(ObjectSchema): 372 | ... first_name = String() 373 | ... last_name = String() 374 | ... gender = String(optional=True) 375 | ... 376 | >>> @from_object(schema=PersonSchema) 377 | ... class Person(object): 378 | ... def __init__(self, first_name, last_name, gender=None): 379 | ... self.first_name = first_name 380 | ... self.last_name = last_name 381 | ... self.gender = gender 382 | ... 383 | >>> person_json = '{"__type__": "Person", "first_name": 12345, "last_name": "Adams"}' 384 | >>> try: 385 | ... person = loader(person_json) 386 | ... except ValidationError, e: 387 | ... print e.errors["first_name"].message 388 | Expected str got int instead. 389 | 390 | Schemas are useful for validating user supplied json in web services or 391 | other web applications. For a detailed explanation on using schemas see 392 | the :mod:`jsonweb.schema`. 393 | """ 394 | def wrapper(cls): 395 | _default_object_handlers.add_handler( 396 | cls, handler or get_jsonweb_handler(cls), type_name, schema 397 | ) 398 | return cls 399 | return wrapper 400 | 401 | 402 | def object_hook(handlers=None, as_type=None, validate=True): 403 | """ 404 | Wrapper around :class:`ObjectHook`. Calling this function will configure 405 | an instance of :class:`ObjectHook` and return a callable suitable for 406 | passing to :func:`json.loads` as ``object_hook``. 407 | 408 | If you need to decode a JSON string that does not contain a ``__type__`` 409 | key and you know that the JSON represents a certain object or list of 410 | objects you can use ``as_type`` to specify it :: 411 | 412 | >>> json_str = '{"first_name": "bob", "last_name": "smith"}' 413 | >>> loader(json_str, as_type="Person") 414 | 415 | >>> # lists work too 416 | >>> json_str = '''[ 417 | ... {"first_name": "bob", "last_name": "smith"}, 418 | ... {"first_name": "jane", "last_name": "smith"} 419 | ... ]''' 420 | >>> loader(json_str, as_type="Person") 421 | [, ] 422 | 423 | .. note:: 424 | 425 | Assumes every object WITHOUT a ``__type__`` kw is of 426 | the type specified by ``as_type`` . 427 | 428 | ``handlers`` is a dict with this format:: 429 | 430 | {"Person": {"cls": Person, "handler": person_decoder, "schema": PersonSchema)} 431 | 432 | If you do not wish to decorate your classes with :func:`from_object` you 433 | can specify the same parameters via the ``handlers`` keyword argument. 434 | Here is an example:: 435 | 436 | >>> class Person(object): 437 | ... def __init__(self, first_name, last_name): 438 | ... self.first_name = first_name 439 | ... self.last_name = last_name 440 | ... 441 | >>> def person_decoder(cls, obj): 442 | ... return cls(obj["first_name"], obj["last_name"]) 443 | 444 | >>> handlers = {"Person": {"cls": Person, "handler": person_decoder}} 445 | >>> person = loader(json_str, handlers=handlers) 446 | >>> # Or invoking the object_hook interface ourselves 447 | >>> person = json.loads(json_str, object_hook=object_hook(handlers)) 448 | 449 | .. note:: 450 | 451 | If you decorate a class with :func:`from_object` you can override the 452 | ``handler`` and ``schema`` values later. Here is an example of 453 | overriding a schema you defined with :func:`from_object` (some code 454 | is left out for brevity):: 455 | 456 | >>> from jsonweb import from_object 457 | >>> @from_object(schema=PersonSchema) 458 | >>> class Person(object): 459 | ... 460 | 461 | >>> # and later on in the code... 462 | >>> handlers = {"Person": {"schema": NewPersonSchema}} 463 | >>> person = loader(json_str, handlers=handlers) 464 | 465 | If you need to use ``as_type`` or ``handlers`` many times in your code 466 | you can forgo using :func:`loader` in favor of configuring a "custom" 467 | object hook callable. Here is an example :: 468 | 469 | >>> my_obj_hook = object_hook(handlers) 470 | >>> # this call uses custom handlers 471 | >>> person = json.loads(json_str, object_hook=my_obj_hook) 472 | >>> # and so does this one ... 473 | >>> another_person = json.loads(json_str, object_hook=my_obj_hook) 474 | """ 475 | if handlers: 476 | _object_handlers = _default_object_handlers.copy() 477 | for name, handler_dict in items(handlers): 478 | if name in _object_handlers: 479 | _object_handlers.update_handler(name, **handler_dict) 480 | else: 481 | _object_handlers.add_handler( 482 | handler_dict.pop('cls'), 483 | **handler_dict 484 | ) 485 | else: 486 | _object_handlers = _default_object_handlers 487 | 488 | decode = ObjectHook(_object_handlers, validate) 489 | 490 | def handler(obj): 491 | if as_type and "__type__" not in obj: 492 | obj["__type__"] = as_type 493 | return decode.decode_obj(obj) 494 | 495 | return handler 496 | 497 | 498 | def loader(json_str, **kw): 499 | """ 500 | Call this function as you would call :func:`json.loads`. It wraps the 501 | :ref:`object_hook` interface and returns python class instances from JSON 502 | strings. 503 | 504 | :param ensure_type: Check that the resulting object is of type 505 | ``ensure_type``. Raise a ValidationError otherwise. 506 | :param handlers: is a dict of handlers. see :func:`object_hook`. 507 | :param as_type: explicitly specify the type of object the JSON 508 | represents. see :func:`object_hook` 509 | :param validate: Set to False to turn off validation (ie dont run the 510 | schemas) during this load operation. Defaults to True. 511 | :param kw: the rest of the kw args will be passed to the underlying 512 | :func:`json.loads` calls. 513 | 514 | 515 | """ 516 | kw["object_hook"] = object_hook( 517 | kw.pop("handlers", None), 518 | kw.pop("as_type", None), 519 | kw.pop("validate", True) 520 | ) 521 | 522 | ensure_type = kw.pop("ensure_type", _as_type_context.top) 523 | 524 | try: 525 | obj = json.loads(json_str, **kw) 526 | except ValueError as e: 527 | raise JsonDecodeError(e.args[0]) 528 | 529 | if ensure_type: 530 | return EnsureType(ensure_type).validate(obj) 531 | return obj 532 | 533 | 534 | @contextmanager 535 | def ensure_type(cls): 536 | """ 537 | This context manager lets you "inject" a value for ``ensure_type`` into 538 | :func:`loader` calls made in the active context. This will allow a 539 | :class:`~jsonweb.schema.ValidationError` to bubble up from the underlying 540 | :func:`loader` call if the resultant type is not of type ``ensure_type``. 541 | 542 | Here is an example :: 543 | 544 | # example_app.model.py 545 | from jsonweb.decode import from_object 546 | # import db model stuff 547 | from example_app import db 548 | 549 | @from_object() 550 | class Person(db.Base): 551 | first_name = db.Column(String) 552 | last_name = db.Column(String) 553 | 554 | def __init__(self, first_name, last_name): 555 | self.first_name = first_name 556 | self.last_name = last_name 557 | 558 | 559 | # example_app.__init__.py 560 | from example_app.model import session, Person 561 | from jsonweb.decode import from_object, ensure_type 562 | from jsonweb.schema import ValidationError 563 | from flask import Flask, request, abort 564 | 565 | app.errorhandler(ValidationError) 566 | def json_validation_error(e): 567 | return json_response({"error": e}) 568 | 569 | 570 | def load_request_json(): 571 | if request.headers.get('content-type') == 'application/json': 572 | return loader(request.data) 573 | abort(400) 574 | 575 | 576 | @app.route("/person", methods=["POST", "PUT"]) 577 | def add_person(): 578 | with ensure_type(Person): 579 | person = load_request_json() 580 | session.add(person) 581 | session.commit() 582 | return "ok" 583 | 584 | 585 | The above example is pretty contrived. We could have just made ``load_json_request`` 586 | accept an ``ensure_type`` kw, but imagine if the call to :func:`loader` was burried 587 | deeper in our api and such a thing was not possible. 588 | """ 589 | _as_type_context.push(cls) 590 | try: 591 | yield None 592 | finally: 593 | _as_type_context.pop() -------------------------------------------------------------------------------- /jsonweb/encode.py: -------------------------------------------------------------------------------- 1 | """ 2 | Often times in a web application the data you wish to return to users is 3 | described by some sort of data model or resource in the form of a class 4 | object. This module provides an easy way to encode your python class 5 | instances to JSON. Here is a quick example:: 6 | 7 | >>> from jsonweb.encode import to_object, dumper 8 | >>> @to_object() 9 | ... class DataModel(object): 10 | ... def __init__(self, id, value): 11 | ... self.id = id 12 | ... self.value = value 13 | 14 | >>> data = DataModel(5, "foo") 15 | >>> dumper(data) 16 | '{"__type__": "DataModel", "id": 5, "value": "foo"}' 17 | 18 | If you have a class you wish to serialize to a JSON object decorate it with 19 | :func:`to_object`. If your class should serialize into a JSON list decorate 20 | it with :func:`to_list`. 21 | """ 22 | 23 | import json 24 | import datetime 25 | import types 26 | 27 | 28 | class EncodeArgs: 29 | __type__ = None 30 | serialize_as = None 31 | handler = None 32 | suppress = None 33 | 34 | 35 | def handler(func): 36 | """ 37 | Use this decorator to mark a method on a class as being its jsonweb 38 | encode handler. It will be called any time your class is serialized to a 39 | JSON string. :: 40 | 41 | >>> from jsonweb import encode 42 | >>> @encode.to_object() 43 | ... class Person(object): 44 | ... def __init__(self, first_name, last_name): 45 | ... self.first_name = first_name 46 | ... self.last_name = last_name 47 | ... @encode.handler 48 | ... def to_obj(self): 49 | ... return {"FirstName": person.first_name, 50 | ... "LastName": person.last_name} 51 | ... 52 | >>> @encode.to_list() 53 | ... class People(object): 54 | ... def __init__(self, *persons): 55 | ... self.persons = persons 56 | ... @encode.handler 57 | ... def to_list(self): 58 | ... return self.persons 59 | ... 60 | >>> people = People( 61 | ... Person("Luke", "Skywalker"), 62 | ... Person("Darth", "Vader"), 63 | ... Person("Obi-Wan" "Kenobi") 64 | ... ) 65 | ... 66 | >>> print dumper(people, indent=2) 67 | [ 68 | { 69 | "FirstName": "Luke", 70 | "LastName": "Skywalker" 71 | }, 72 | { 73 | "FirstName": "Darth", 74 | "LastName": "Vader" 75 | }, 76 | { 77 | "FirstName": "Obi-Wan", 78 | "LastName": "Kenobi" 79 | } 80 | ] 81 | 82 | """ 83 | func._jsonweb_encode_handler = True 84 | return func 85 | 86 | 87 | def __inspect_for_handler(cls): 88 | cls._encode.handler_is_instance_method = False 89 | if cls._encode.handler: 90 | return cls 91 | for attr in dir(cls): 92 | if attr.startswith("_"): 93 | continue 94 | obj = getattr(cls, attr) 95 | if hasattr(obj, "_jsonweb_encode_handler"): 96 | cls._encode.handler_is_instance_method = True 97 | # we store the handler as a string name here. This is 98 | # because obj is an unbound method. When its time to 99 | # encode the class instance we want to call the bound 100 | # instance method. 101 | cls._encode.handler = attr 102 | break 103 | return cls 104 | 105 | 106 | def to_object(cls_type=None, suppress=None, handler=None, exclude_nulls=False): 107 | """ 108 | To make your class instances JSON encodable decorate them with 109 | :func:`to_object`. The python built-in :py:func:`dir` is called on the 110 | class instance to retrieve key/value pairs that will make up the JSON 111 | object (*Minus any attributes that start with an underscore or any 112 | attributes that were specified via the* ``suppress`` *keyword argument*). 113 | 114 | Here is an example:: 115 | 116 | >>> from jsonweb import to_object 117 | >>> @to_object() 118 | ... class Person(object): 119 | ... def __init__(self, first_name, last_name): 120 | ... self.first_name = first_name 121 | ... self.last_name = last_name 122 | 123 | >>> person = Person("Shawn", "Adams") 124 | >>> dumper(person) 125 | '{"__type__": "Person", "first_name": "Shawn", "last_name": "Adams"}' 126 | 127 | A ``__type__`` key is automatically added to the JSON object. Its value 128 | should represent the object type being encoded. By default it is set to 129 | the value of the decorated class's ``__name__`` attribute. You can 130 | specify your own value with ``cls_type``:: 131 | 132 | >>> from jsonweb import to_object 133 | >>> @to_object(cls_type="PersonObject") 134 | ... class Person(object): 135 | ... def __init__(self, first_name, last_name): 136 | ... self.first_name = first_name 137 | ... self.last_name = last_name 138 | 139 | >>> person = Person("Shawn", "Adams") 140 | >>> dumper(person) 141 | '{"__type__": "PersonObject", "first_name": "Shawn", "last_name": "Adams"}' 142 | 143 | If you would like to leave some attributes out of the resulting JSON 144 | simply use the ``suppress`` kw argument to pass a list of attribute 145 | names:: 146 | 147 | >>> from jsonweb import to_object 148 | >>> @to_object(suppress=["last_name"]) 149 | ... class Person(object): 150 | ... def __init__(self, first_name, last_name): 151 | ... self.first_name = first_name 152 | ... self.last_name = last_name 153 | 154 | >>> person = Person("Shawn", "Adams") 155 | >>> dumper(person) 156 | '{"__type__": "Person", "first_name": "Shawn"}' 157 | 158 | You can even suppress the ``__type__`` attribute :: 159 | 160 | @to_object(suppress=["last_name", "__type__"]) 161 | ... 162 | 163 | Sometimes it's useful to suppress ``None`` values from your JSON output. 164 | Setting ``exclude_nulls`` to ``True`` will accomplish this :: 165 | 166 | >>> from jsonweb import to_object 167 | >>> @to_object(exclude_nulls=True) 168 | ... class Person(object): 169 | ... def __init__(self, first_name, last_name): 170 | ... self.first_name = first_name 171 | ... self.last_name = last_name 172 | 173 | >>> person = Person("Shawn", None) 174 | >>> dumper(person) 175 | '{"__type__": "Person", "first_name": "Shawn"}' 176 | 177 | .. note:: 178 | 179 | You can also pass most of these arguments to :func:`dumper`. They 180 | will take precedence over what you passed to :func:`to_object` and 181 | only effects that one call. 182 | 183 | If you need greater control over how your object is encoded you can 184 | specify a ``handler`` callable. It should accept one argument, which is 185 | the object to encode, and it should return a dict. This would override the 186 | default object handler :func:`JsonWebEncoder.object_handler`. 187 | 188 | Here is an example:: 189 | 190 | >>> from jsonweb import to_object 191 | >>> def person_encoder(person): 192 | ... return {"FirstName": person.first_name, 193 | ... "LastName": person.last_name} 194 | ... 195 | >>> @to_object(handler=person_encoder) 196 | ... class Person(object): 197 | ... def __init__(self, first_name, last_name): 198 | ... self.guid = 12334 199 | ... self.first_name = first_name 200 | ... self.last_name = last_name 201 | 202 | >>> person = Person("Shawn", "Adams") 203 | >>> dumper(person) 204 | '{"FirstName": "Shawn", "LastName": "Adams"}' 205 | 206 | 207 | You can also use the alternate decorator syntax to accomplish this. See 208 | :func:`jsonweb.encode.handler`. 209 | 210 | """ 211 | def wrapper(cls): 212 | cls._encode = EncodeArgs() 213 | cls._encode.serialize_as = "json_object" 214 | cls._encode.handler = handler 215 | cls._encode.suppress = suppress or [] 216 | cls._encode.exclude_nulls = exclude_nulls 217 | cls._encode.__type__ = cls_type or cls.__name__ 218 | return __inspect_for_handler(cls) 219 | return wrapper 220 | 221 | 222 | def to_list(handler=None): 223 | """ 224 | If your class instances should serialize into a JSON list decorate it 225 | with :func:`to_list`. By default The python built in :class:`list` will 226 | be called with your class instance as its argument. ie **list(obj)**. 227 | This means your class needs to define the ``__iter__`` method. 228 | 229 | Here is an example:: 230 | 231 | @to_object(suppress=["__type__"]) 232 | class Person(object): 233 | def __init__(self, first_name, last_name): 234 | self.first_name = first_name 235 | self.last_name = last_name 236 | 237 | @to_list() 238 | class People(object): 239 | def __init__(self, *persons): 240 | self.persons = persons 241 | 242 | def __iter__(self): 243 | for p in self.persons: 244 | yield p 245 | 246 | people = People( 247 | Person("Luke", "Skywalker"), 248 | Person("Darth", "Vader"), 249 | Person("Obi-Wan" "Kenobi") 250 | ) 251 | 252 | Encoding ``people`` produces this JSON:: 253 | 254 | [ 255 | {"first_name": "Luke", "last_name": "Skywalker"}, 256 | {"first_name": "Darth", "last_name": "Vader"}, 257 | {"first_name": "Obi-Wan", "last_name": "Kenobi"} 258 | ] 259 | 260 | .. versionadded:: 0.6.0 You can now specify a custom handler callable 261 | with the ``handler`` kw argument. It should accept one argument, your 262 | class instance. You can also use the :func:`jsonweb.encode.handler` 263 | decorator to mark one of the class's methods as the list handler. 264 | 265 | """ 266 | def wrapper(cls): 267 | cls._encode = EncodeArgs() 268 | cls._encode.serialize_as = "json_list" 269 | cls._encode.handler = handler 270 | cls._encode.__type__ = cls.__name__ 271 | return __inspect_for_handler(cls) 272 | return wrapper 273 | 274 | 275 | class JsonWebEncoder(json.JSONEncoder): 276 | """ 277 | This :class:`json.JSONEncoder` subclass is responsible for encoding 278 | instances of classes that have been decorated with :func:`to_object` or 279 | :func:`to_list`. Pass :class:`JsonWebEncoder` as the value for the 280 | ``cls`` keyword argument to :func:`json.dump` or :func:`json.dumps`. 281 | 282 | Example:: 283 | 284 | json.dumps(obj_instance, cls=JsonWebEncoder) 285 | 286 | Using :func:`dumper` is a shortcut for the above call to 287 | :func:`json.dumps` :: 288 | 289 | dumper(obj_instance) #much nicer! 290 | 291 | """ 292 | 293 | _DT_FORMAT = "%Y-%m-%dT%H:%M:%S" 294 | _D_FORMAT = "%Y-%m-%d" 295 | 296 | def __init__(self, **kw): 297 | self.__hard_suppress = kw.pop("suppress", []) 298 | self.__exclude_nulls = kw.pop("exclude_nulls", None) 299 | self.__handlers = kw.pop("handlers", {}) 300 | if not isinstance(self.__hard_suppress, list): 301 | self.__hard_suppress = [self.__hard_suppress] 302 | json.JSONEncoder.__init__(self, **kw) 303 | 304 | def default(self, o): 305 | try: 306 | e_args = getattr(o, "_encode") 307 | except AttributeError: 308 | pass 309 | else: 310 | # Passed in handlers take precedence. 311 | if e_args.__type__ in self.__handlers: 312 | return self.__handlers[e_args.__type__](o) 313 | elif e_args.handler: 314 | if e_args.handler_is_instance_method: 315 | return getattr(o, e_args.handler)() 316 | return e_args.handler(o) 317 | elif e_args.serialize_as == "json_object": 318 | return self.object_handler(o) 319 | elif e_args.serialize_as == "json_list": 320 | return self.list_handler(o) 321 | 322 | if isinstance(o, datetime.datetime): 323 | return o.strftime(self._DT_FORMAT) 324 | if isinstance(o, datetime.date): 325 | return o.strftime(self._D_FORMAT) 326 | return json.JSONEncoder.default(self, o) 327 | 328 | def object_handler(self, obj): 329 | """ 330 | Handles encoding instance objects of classes decorated by 331 | :func:`to_object`. Returns a dict containing all the key/value pairs 332 | in ``obj.__dict__``. Excluding attributes that 333 | 334 | * start with an underscore. 335 | * were specified with the ``suppress`` keyword argument. 336 | 337 | The returned dict will be encoded into JSON. 338 | 339 | .. note:: 340 | 341 | Override this method if you wish to change how ALL objects are 342 | encoded into JSON objects. 343 | 344 | """ 345 | suppress = obj._encode.suppress 346 | if self.__exclude_nulls is not None: 347 | exclude_nulls = self.__exclude_nulls 348 | else: 349 | exclude_nulls = obj._encode.exclude_nulls 350 | json_obj = {} 351 | 352 | def suppressed(key): 353 | return key in suppress or key in self.__hard_suppress 354 | 355 | for attr in dir(obj): 356 | if not attr.startswith("_") and not suppressed(attr): 357 | value = getattr(obj, attr) 358 | if value is None and exclude_nulls: 359 | continue 360 | if not isinstance(value, types.MethodType): 361 | json_obj[attr] = value 362 | if not suppressed("__type__"): 363 | json_obj["__type__"] = obj._encode.__type__ 364 | return json_obj 365 | 366 | def list_handler(self, obj): 367 | """ 368 | Handles encoding instance objects of classes decorated by 369 | :func:`to_list`. Simply calls :class:`list` on ``obj``. 370 | 371 | .. note:: 372 | 373 | Override this method if you wish to change how ALL objects are 374 | encoded into JSON lists. 375 | 376 | """ 377 | return list(obj) 378 | 379 | 380 | def dumper(obj, **kw): 381 | """ 382 | JSON encode your class instances by calling this function as you would 383 | call :func:`json.dumps`. ``kw`` args will be passed to the underlying 384 | json.dumps call. 385 | 386 | :param handlers: A dict of type name/handler callable to use. 387 | ie {"Person:" person_handler} 388 | 389 | :param cls: To override the given encoder. Should be a subclass 390 | of :class:`JsonWebEncoder`. 391 | 392 | :param suppress: A list of extra fields to suppress (as well as those 393 | suppressed by the class). 394 | 395 | :param exclude_nulls: Set True to suppress keys with null (None) values 396 | from the JSON output. Defaults to False. 397 | """ 398 | return json.dumps(obj, cls=kw.pop("cls", JsonWebEncoder), **kw) 399 | 400 | -------------------------------------------------------------------------------- /jsonweb/exceptions.py: -------------------------------------------------------------------------------- 1 | class JsonWebError(Exception): 2 | def __init__(self, message, **extras): 3 | Exception.__init__(self, message) 4 | self.extras = extras -------------------------------------------------------------------------------- /jsonweb/py3k.py: -------------------------------------------------------------------------------- 1 | """ 2 | Code needed to support python 3. :func:`items` is a slightly modified version 3 | of ``six.iteritems`` `six `_ 4 | """ 5 | 6 | import sys 7 | 8 | PY3k = sys.version_info[0] == 3 9 | 10 | if PY3k: 11 | basestring = (str, bytes) 12 | _iteritems = "items" 13 | else: 14 | basestring = basestring 15 | _iteritems = "iteritems" 16 | 17 | 18 | def items(d): 19 | return getattr(d, _iteritems)() -------------------------------------------------------------------------------- /jsonweb/schema.py: -------------------------------------------------------------------------------- 1 | # noinspection PyUnresolvedReferences 2 | """ 3 | Declarative 4 | ----------- 5 | 6 | :mod:`jsonweb.schema` provides a layer of validation before :mod:`json.decode` 7 | returns your object instances. It can also simply be used to validate the 8 | resulting python data structures returned from :func:`json.loads`. It's main 9 | use is through a declarative style api. Here is an example of validating the 10 | structure of a python dict:: 11 | 12 | >>> 13 | >>> from jsonweb.schema import ObjectSchema, ValidationError 14 | >>> from jsonweb.validators import String 15 | 16 | >>> class PersonSchema(ObjectSchema): 17 | ... first_name = String() 18 | ... last_name = String() 19 | 20 | >>> try: 21 | ... PersonSchema().validate({"first_name": "shawn"}) 22 | ... except ValidationError, e: 23 | ... print e.errors 24 | {"last_name": "Missing required parameter."} 25 | 26 | Validating plain old python data structures is fine, but the more interesting 27 | exercise is tying a schema to a class definition:: 28 | 29 | >>> from jsonweb.decode import from_object, loader 30 | >>> from jsonweb.schema import ObjectSchema, ValidationError 31 | >>> from jsonweb.validators import String, Integer, EnsureType 32 | 33 | >>> class PersonSchema(ObjectSchema): 34 | ... id = Integer() 35 | ... first_name = String() 36 | ... last_name = String() 37 | ... gender = String(optional=True) 38 | ... job = EnsureType("Job") 39 | 40 | You can make any field optional by setting ``optional`` to :class:`True`. 41 | 42 | .. warning:: 43 | 44 | The field is only optional at the schema level. If you've bound a schema 45 | to a class via :func:`~jsonweb.decode.from_object` and the underlying 46 | class requires that field a :class:`~jsonweb.decode 47 | .ObjectAttributeError` will be raised if missing. 48 | 49 | As you can see its fine to pass a class name as a string, which we have done 50 | for the :class:`Job` class above. We must later define :class:`Job` and 51 | decorate it with :func:`~jsonweb.decode.from_object` :: 52 | 53 | >>> class JobSchema(ObjectSchema): 54 | ... id = Integer() 55 | ... title = String() 56 | 57 | >>> @from_object(schema=JobSchema) 58 | ... class Job(object): 59 | ... def __init__(self, id, title): 60 | ... self.id = id 61 | ... self.title = title 62 | 63 | >>> @from_object(schema=PersonSchema) 64 | ... class Person(object): 65 | ... def __init__(self, first_name, last_name, job, gender=None): 66 | ... self.first_name = first_name 67 | ... self.last_name = last_name 68 | ... self.gender = gender 69 | ... self.job = job 70 | ... def __str__(self): 71 | ... return ''.format( 72 | ... " ".join((self.first_name, self.last_name)), 73 | ... self.job.title 74 | ... ) 75 | 76 | >>> person_json = ''' 77 | ... { 78 | ... "__type__": "Person", 79 | ... "id": 1, 80 | ... "first_name": "Bob", 81 | ... "last_name": "Smith", 82 | ... "job": {"__type__": "Job", "id": 5, "title": "Police Officer"}, 83 | ... }''' 84 | ... 85 | 86 | >>> person = loader(person_json) 87 | >>> print person 88 | 89 | 90 | Non-Declarative 91 | --------------- 92 | 93 | .. versionadded:: 0.8.1 94 | 95 | 96 | Use the staticmethod :meth:`ObjectSchema.create` to build object schemas in 97 | a non declarative style. Handy for validating dicts with string keys that 98 | are not valid python identifiers (e.g "first-name"):: 99 | 100 | MySchema = ObjectSchema.create("MySchema", { 101 | "first-name": String(), 102 | "last-name": String(optional=True) 103 | }) 104 | 105 | """ 106 | 107 | from jsonweb.py3k import PY3k, items 108 | from jsonweb.validators import BaseValidator, _Errors, ValidationError, \ 109 | isinstance_or_raise 110 | 111 | 112 | class SchemaMeta(type): 113 | def __new__(mcs, cls_name, bases, cls_dict): 114 | cls_dict["_fields"] = [k for k, v in items(cls_dict) 115 | if hasattr(v, "_validate")] 116 | 117 | for base in bases: 118 | if hasattr(base, "_fields"): 119 | cls_dict["_fields"].extend(base._fields) 120 | 121 | return type.__new__(mcs, cls_name, bases, cls_dict) 122 | 123 | 124 | class ObjectSchema(BaseValidator): 125 | __metaclass__ = SchemaMeta 126 | 127 | @staticmethod 128 | def create(name, schema_dict): 129 | """ 130 | Dynamically create an ObjectSchema class. 131 | 132 | :param name: The name of your generated schema class 133 | :param schema_dict: dict of validators that will make up this schema 134 | :return: A subclass of :class:`ObjectSchema` 135 | """ 136 | schema_dict.update(dict(vars(ObjectSchema))) 137 | return SchemaMeta(name, (ObjectSchema,), schema_dict) 138 | 139 | def to_json(self): 140 | return super(ObjectSchema, self).to_json( 141 | fields=dict([(f, getattr(self, f)) for f in self._fields]) 142 | ) 143 | 144 | def _validate(self, obj): 145 | isinstance_or_raise(obj, dict) 146 | val_obj = {} 147 | errors = _Errors({}) 148 | 149 | for field in self._fields: 150 | v = getattr(self, field) 151 | try: 152 | if field not in obj: 153 | if v.default is not None: 154 | val_obj[field] = v.default 155 | elif v.required: 156 | errors.add_error( 157 | "Missing required parameter.", 158 | key=field, 159 | error_type="required_but_missing" 160 | ) 161 | else: 162 | val_obj[field] = v.validate(obj[field]) 163 | except ValidationError as e: 164 | errors.add_error(e, key=field) 165 | 166 | errors.raise_if_errors("Error validating object.", 167 | error_type="invalid_object") 168 | return val_obj 169 | 170 | 171 | def bind_schema(type_name, schema_obj): 172 | """ 173 | Use this function to add an :class:`ObjectSchema` to a class already 174 | decorated by :func:`from_object`. 175 | """ 176 | from jsonweb.decode import _default_object_handlers 177 | _default_object_handlers.update_handler_deferred(type_name, 178 | schema=schema_obj) 179 | 180 | if PY3k: 181 | ObjectSchema = SchemaMeta( 182 | ObjectSchema.__name__, 183 | (BaseValidator, ), 184 | dict(vars(ObjectSchema)) 185 | ) -------------------------------------------------------------------------------- /jsonweb/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boris317/JsonWeb/020dee05408414cc8b9e5e2e7a7fba0eaed48335/jsonweb/tests/__init__.py -------------------------------------------------------------------------------- /jsonweb/tests/test_decode.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from jsonweb import from_object, loader, decode 4 | from jsonweb.decode import ObjectAttributeError, ObjectDecodeError, object_hook, JsonDecodeError 5 | from jsonweb.exceptions import JsonWebError 6 | from jsonweb.validators import ValidationError 7 | 8 | 9 | class TestJsonWebObjectDecoder(unittest.TestCase): 10 | def setUp(self): 11 | from jsonweb.decode import _default_object_handlers 12 | _default_object_handlers.clear() 13 | 14 | def test_decodes_to_class_instance(self): 15 | from jsonweb.decode import from_object, loader 16 | 17 | @from_object() 18 | class Person(object): 19 | def __init__(self, first_name, last_name): 20 | self.first_name = first_name 21 | self.last_name = last_name 22 | 23 | json_str = ('{"__type__": "Person", ' 24 | '"first_name": "shawn", ' 25 | '"last_name": "adams"}') 26 | person = loader(json_str) 27 | 28 | self.assertTrue(isinstance(person, Person)) 29 | self.assertEqual(person.first_name, "shawn") 30 | self.assertEqual(person.last_name, "adams") 31 | 32 | def test_supplied_handler_decodes_to_class_instance(self): 33 | from jsonweb.decode import from_object, loader 34 | 35 | def person_handler(cls, obj): 36 | return cls( 37 | obj['first_name'], 38 | obj['last_name'] 39 | ) 40 | 41 | @from_object(person_handler) 42 | class Person(object): 43 | def __init__(self, first_name, last_name): 44 | self.first_name = first_name 45 | self.last_name = last_name 46 | 47 | json_str = '{"__type__": "Person", "first_name": "shawn", "last_name": "adams"}' 48 | person = loader(json_str) 49 | 50 | self.assertTrue(isinstance(person, Person)) 51 | 52 | def test_class_kw_args_are_optional(self): 53 | """ 54 | Test that class keyword agruments are optional 55 | """ 56 | 57 | @from_object() 58 | class Person(object): 59 | def __init__(self, first_name, last_name, job=None): 60 | self.first_name = first_name 61 | self.last_name = last_name 62 | self.job = job 63 | 64 | json_str = ('{"__type__": "Person", ' 65 | '"first_name": "shawn", ' 66 | '"last_name": "adams"}') 67 | 68 | person = loader(json_str) 69 | 70 | self.assertTrue(isinstance(person, Person)) 71 | self.assertEqual(person.first_name, "shawn") 72 | self.assertEqual(person.last_name, "adams") 73 | self.assertEqual(person.job, None) 74 | 75 | json_str = ('{"__type__": "Person", ' 76 | '"first_name": "shawn", ' 77 | '"last_name": "adams", ' 78 | '"job": "Jedi Knight"}') 79 | 80 | person = loader(json_str) 81 | self.assertEqual(person.job, "Jedi Knight") 82 | 83 | def test_ignores_extra_keys_in_json(self): 84 | 85 | @from_object() 86 | class Person(object): 87 | def __init__(self, first_name, last_name): 88 | self.first_name = first_name 89 | self.last_name = last_name 90 | 91 | json_str = ('{"__type__": "Person", ' 92 | '"first_name": "shawn", ' 93 | '"last_name": "adams", ' 94 | '"no_such_key": "hello"}') 95 | 96 | person = loader(json_str) 97 | 98 | self.assertTrue(isinstance(person, Person)) 99 | self.assertEqual(person.first_name, "shawn") 100 | self.assertEqual(person.last_name, "adams") 101 | 102 | def test_bad__init__raises_error(self): 103 | """ 104 | Test that if a class has a no argument __init__ method or a *args/**kw only __init__ 105 | method a JsonWebError is raised. 106 | """ 107 | with self.assertRaises(JsonWebError) as context: 108 | @from_object() 109 | class Person(object): 110 | def __init__(self): 111 | self.first_name = None 112 | self.last_name = None 113 | 114 | self.assertEqual( 115 | str(context.exception), 116 | "Unable to generate an object_hook handler from Person's `__init__` method." 117 | ) 118 | 119 | def test_supplied_handler_missing_required_attrs_raise_error(self): 120 | """ 121 | KeyErrors raised from within supplied object handlers should be caught and 122 | turned into ObjectAttributeErrors. 123 | """ 124 | 125 | def person_handler(cls, obj): 126 | return cls( 127 | obj['first_name'], 128 | obj['last_name'] 129 | ) 130 | 131 | @from_object(person_handler) 132 | class Person(object): 133 | def __init__(self, first_name, last_name): 134 | self.first_name = first_name 135 | self.last_name = last_name 136 | 137 | json_str = '{"__type__": "Person", "first_name": "shawn"}' 138 | with self.assertRaises(ObjectAttributeError) as context: 139 | loader(json_str) 140 | 141 | exc = context.exception 142 | self.assertEqual(exc.extras["attribute"], "last_name") 143 | self.assertEqual(str(exc), "Missing last_name attribute for Person.") 144 | 145 | def test_generated_handler_missing_required_attrs_raise_error(self): 146 | """ 147 | KeyErrors raised from within JsonWebObjectHandler should be caught and 148 | turned into ObjectAttributeErrors. 149 | """ 150 | 151 | @from_object() 152 | class Person(object): 153 | def __init__(self, first_name, last_name): 154 | self.first_name = first_name 155 | self.last_name = last_name 156 | 157 | json_str = '{"__type__": "Person", "first_name": "shawn"}' 158 | with self.assertRaises(ObjectAttributeError) as context: 159 | loader(json_str) 160 | 161 | exc = context.exception 162 | self.assertEqual(exc.extras["attribute"], "last_name") 163 | self.assertEqual(str(exc), "Missing last_name attribute for Person.") 164 | 165 | def test_supplied_handler_dict(self): 166 | """ 167 | Test that supplied ``handlers`` dict decodes objects. 168 | """ 169 | 170 | class Person(object): 171 | def __init__(self, first_name, last_name): 172 | self.first_name = first_name 173 | self.last_name = last_name 174 | 175 | def person_decoder(cls, obj): 176 | return cls(obj["first_name"], obj["last_name"]) 177 | 178 | handlers = {"Person": {"cls": Person, "handler": person_decoder}} 179 | 180 | json_str = ('{"__type__": "Person", ' 181 | '"first_name": "shawn", ' 182 | '"last_name": "adams"}') 183 | 184 | person = loader(json_str, handlers=handlers) 185 | self.assertTrue(isinstance(person, Person)) 186 | self.assertEqual(person.first_name, "shawn") 187 | self.assertEqual(person.last_name, "adams") 188 | 189 | def test_supplied_handler_dict_overrides_from_object(self): 190 | """ 191 | Test that a class decorated with from_object can have its 192 | handler and schema overridden with a supplied dict to 193 | decode.object_hook 194 | """ 195 | @from_object() 196 | class Person(object): 197 | def __init__(self, first_name, last_name): 198 | self.first_name = first_name 199 | self.last_name = last_name 200 | 201 | did_run = [] 202 | def person_decoder(cls, obj): 203 | did_run.append(True) 204 | return cls(obj["first_name"], obj["last_name"]) 205 | 206 | json_str = ('{"__type__": "Person", ' 207 | '"first_name": "shawn", ' 208 | '"last_name": "adams"}') 209 | 210 | person = loader(json_str) 211 | self.assertTrue(isinstance(person, Person)) 212 | 213 | handlers = {"Person": {"handler": person_decoder}} 214 | person = loader(json_str, handlers=handlers) 215 | self.assertTrue(isinstance(person, Person)) 216 | self.assertTrue(did_run) 217 | 218 | def test_supplied_type_with_as_type(self): 219 | """ 220 | Test that specifying an explicit type with ``as_type`` 221 | decodes a json string this is missing the ``__type__`` key. 222 | """ 223 | 224 | @from_object() 225 | class Person(object): 226 | def __init__(self, first_name, last_name): 227 | self.first_name = first_name 228 | self.last_name = last_name 229 | 230 | json_str = '{"first_name": "shawn", "last_name": "adams"}' 231 | self.assertTrue(isinstance(loader(json_str, as_type="Person"), Person)) 232 | json_str = '''[ 233 | {"first_name": "shawn", "last_name": "adams"}, 234 | {"first_name": "luke", "last_name": "skywalker"} 235 | ]''' 236 | persons = loader(json_str, as_type="Person") 237 | self.assertTrue(isinstance(persons, list)) 238 | self.assertEqual(len(persons), 2) 239 | self.assertTrue(isinstance(persons[0], Person)) 240 | self.assertTrue(isinstance(persons[1], Person)) 241 | 242 | def test_supplied__type__trumps_as_type(self): 243 | """ 244 | Test that the __type__ key trumps as_type 245 | """ 246 | 247 | @from_object() 248 | class Person(object): 249 | def __init__(self, first_name, last_name): 250 | self.first_name = first_name 251 | self.last_name = last_name 252 | 253 | @from_object() 254 | class Alien(object): 255 | def __init__(self, planet, number_of_legs): 256 | self.planet = planet 257 | self.number_of_legs = number_of_legs 258 | 259 | json_str = ('{"__type__": "Person", ' 260 | '"first_name": "shawn", ' 261 | '"last_name": "adams"}') 262 | 263 | self.assertTrue(isinstance(loader(json_str, as_type="Alien"), Person)) 264 | 265 | # This will fail because json_str is not valid for decoding 266 | # into an Alient object. 267 | json_str = '{"first_name": "shawn", "last_name": "adams"}' 268 | with self.assertRaises(ObjectDecodeError): 269 | loader(json_str, as_type="Alien") 270 | 271 | def test_configured_object_hook_closure(self): 272 | """ 273 | Test that we can configure a "custom" object_hook callable. 274 | """ 275 | 276 | class Person(object): 277 | def __init__(self, first_name, last_name): 278 | self.first_name = first_name 279 | self.last_name = last_name 280 | 281 | did_run = [] 282 | def person_decoder(cls, obj): 283 | did_run.append(True) 284 | return cls(obj["first_name"], obj["last_name"]) 285 | 286 | custom_object_hook = object_hook( 287 | handlers={"Person": {"handler": person_decoder, "cls": Person}}, 288 | as_type="Person" 289 | ) 290 | 291 | json_str = '{"first_name": "shawn", "last_name": "adams"}' 292 | person = json.loads(json_str, object_hook=custom_object_hook) 293 | self.assertTrue(isinstance(person, Person)) 294 | self.assertEqual(did_run, [True]) 295 | 296 | json.loads(json_str, object_hook=custom_object_hook) 297 | self.assertEqual(did_run, [True, True]) 298 | 299 | def test_invalid_json_raises_JsonDecodeError(self): 300 | with self.assertRaises(JsonDecodeError): 301 | loader("{'foo':'bar'}") 302 | 303 | def test_ensure_type_kw_argument(self): 304 | 305 | @from_object() 306 | class Person(object): 307 | def __init__(self, first_name, last_name): 308 | self.first_name = first_name 309 | self.last_name = last_name 310 | 311 | json_str = ('{"__type__": "Person", ' 312 | '"first_name": "shawn", ' 313 | '"last_name": "adams"}') 314 | 315 | self.assertTrue(isinstance(loader(json_str), Person)) 316 | 317 | @from_object() 318 | class Alien(object): 319 | def __init__(self, planet, number_of_legs): 320 | self.planet = planet 321 | self.number_of_legs = number_of_legs 322 | 323 | json_str = '{"__type__": "Person", "first_name": "shawn", "last_name": "adams"}' 324 | with self.assertRaises(ValidationError): 325 | loader(json_str, ensure_type=Alien) 326 | 327 | def test_decode_as_type(self): 328 | 329 | @decode.from_object() 330 | class Person(object): 331 | def __init__(self, first_name, last_name): 332 | self.first_name = first_name 333 | self.last_name = last_name 334 | 335 | @decode.from_object() 336 | class Job(object): 337 | def __init__(self, title): 338 | self.title = title 339 | 340 | json_str = ('{"__type__": "Person", ' 341 | '"first_name": "shawn", ' 342 | '"last_name": "adams"}') 343 | 344 | self.assertEqual(decode._as_type_context.top, None) 345 | 346 | with decode.ensure_type(Person): 347 | self.assertEqual(decode._as_type_context.top, Person) 348 | decode.loader(json_str) 349 | 350 | # Test nested context 351 | with decode.ensure_type(Job): 352 | self.assertEqual(decode._as_type_context.top, Job) 353 | with self.assertRaises(ValidationError): 354 | decode.loader(json_str) 355 | 356 | self.assertEqual(decode._as_type_context.top, Person) 357 | 358 | self.assertEqual(decode._as_type_context.top, None) 359 | -------------------------------------------------------------------------------- /jsonweb/tests/test_decode_schema.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from jsonweb import from_object 4 | from jsonweb.decode import object_hook, loader, ObjectDecodeError 5 | from jsonweb.exceptions import JsonWebError 6 | from jsonweb.schema import ObjectSchema, bind_schema 7 | from jsonweb.validators import String, ValidationError, Integer, EnsureType, List 8 | 9 | 10 | class TestDecodeSchema(unittest.TestCase): 11 | def setUp(self): 12 | from jsonweb.decode import _default_object_handlers 13 | _default_object_handlers.clear() 14 | print("clearing handlers") 15 | 16 | def test_decode_with_schema(self): 17 | 18 | class PersonSchema(ObjectSchema): 19 | first_name = String() 20 | last_name = String() 21 | 22 | @from_object(schema=PersonSchema) 23 | class Person(object): 24 | def __init__(self, first_name, last_name): 25 | self.first_name = first_name 26 | self.last_name = last_name 27 | 28 | json_str = '{"__type__": "Person", "first_name": "shawn", ' \ 29 | '"last_name": "adams"}' 30 | person = json.loads(json_str, object_hook=object_hook()) 31 | self.assertTrue(isinstance(person, Person)) 32 | 33 | def test_schemas_do_not_run_when_validate_kw_is_false(self): 34 | 35 | class PersonSchema(ObjectSchema): 36 | first_name = String() 37 | last_name = String() 38 | 39 | @from_object(schema=PersonSchema) 40 | class Person(object): 41 | def __init__(self, first_name, last_name): 42 | self.first_name = first_name 43 | self.last_name = last_name 44 | 45 | json_str = '{"__type__": "Person", "last_name": "adams"}' 46 | with self.assertRaises(ValidationError): 47 | loader(json_str) 48 | with self.assertRaises(ObjectDecodeError): 49 | loader(json_str, validate=False) 50 | 51 | def test_decode_with_schema_raises_error(self): 52 | """ 53 | when using a supplied schema test that a validation error 54 | is raised for invalid json. 55 | """ 56 | 57 | class PersonSchema(ObjectSchema): 58 | first_name = String() 59 | last_name = String() 60 | 61 | @from_object(schema=PersonSchema) 62 | class Person(object): 63 | def __init__(self, first_name, last_name): 64 | self.first_name = first_name 65 | self.last_name = last_name 66 | 67 | json_str = '{"__type__": "Person", "first_name": 123, "last_name": "adams"}' 68 | with self.assertRaises(ValidationError) as context: 69 | json.loads(json_str, object_hook=object_hook()) 70 | 71 | exc = context.exception 72 | self.assertEqual(str(exc.errors["first_name"]), "Expected str got int instead.") 73 | 74 | def test_class_name_as_string_to_ensuretype(self): 75 | """ 76 | Test that we can pass a string for a class name to EnsureType. The class 77 | must of course be defined later and decorated with @from_object 78 | """ 79 | 80 | class JobSchema(ObjectSchema): 81 | id = Integer() 82 | title = String() 83 | 84 | class PersonSchema(ObjectSchema): 85 | first_name = String() 86 | last_name = String() 87 | job = EnsureType("Job") 88 | 89 | @from_object(schema=JobSchema) 90 | class Job(object): 91 | def __init__(self, id, title): 92 | self.id = id 93 | self.title = title 94 | 95 | @from_object(schema=PersonSchema) 96 | class Person(object): 97 | def __init__(self, first_name, last_name, job): 98 | self.first_name = first_name 99 | self.last_name = last_name 100 | self.job = job 101 | 102 | obj = { 103 | "__type__": "Person", 104 | "first_name": "Shawn", 105 | "last_name": "Adams", 106 | "id": 1, 107 | "test": 12.0, 108 | "job": { 109 | "__type__": "Job", 110 | "title": "zoo keeper", 111 | "id": 1 112 | }} 113 | 114 | person = json.loads(json.dumps(obj), object_hook=object_hook()) 115 | self.assertTrue(isinstance(person, Person)) 116 | self.assertTrue(isinstance(person.job, Job)) 117 | 118 | def test_class_name_as_string_to_ensuretype_no_such_class(self): 119 | """ 120 | Test that an error is raised if you pass a string name of a non 121 | existent class to EnsureType. Meaning it either was not defined or 122 | was not decorated with @from_object 123 | """ 124 | 125 | class JobSchema(ObjectSchema): 126 | id = Integer() 127 | title = String() 128 | 129 | class PersonSchema(ObjectSchema): 130 | first_name = String() 131 | last_name = String() 132 | job = EnsureType("Job") # No such class Job 133 | 134 | @from_object(schema=PersonSchema) 135 | class Person(object): 136 | def __init__(self, first_name, last_name, job): 137 | self.first_name = first_name 138 | self.last_name = last_name 139 | self.job = job 140 | 141 | obj = { 142 | "__type__": "Person", 143 | "first_name": "Shawn", 144 | "last_name": "Adams", 145 | "id": 1, 146 | "test": 12.0, 147 | "job": { 148 | "title": "zoo keeper", 149 | "id": 1 150 | }} 151 | 152 | with self.assertRaises(JsonWebError) as context: 153 | json.loads(json.dumps(obj), object_hook=object_hook()) 154 | 155 | exc = context.exception 156 | self.assertEqual(str(exc), "Cannot find class Job.") 157 | 158 | def test_mixed_type_schema(self): 159 | """ 160 | Test ObjectSchema validates a mix of regular dicts and object hook classes. 161 | """ 162 | 163 | class TestRequestSchema(ObjectSchema): 164 | request_guid = String() 165 | players = List(EnsureType("Person")) 166 | 167 | class PersonSchema(ObjectSchema): 168 | first_name = String() 169 | last_name = String() 170 | 171 | @from_object(schema=PersonSchema) 172 | class Person(object): 173 | def __init__(self, first_name, last_name): 174 | self.first_name = first_name 175 | self.last_name = last_name 176 | 177 | obj = { 178 | "request_guid": "abcd", 179 | "persons": [{"__type__": "Person", "first_name": "shawn", "last_name": "adams"}]*2 180 | } 181 | 182 | request_obj = json.loads(json.dumps(obj), object_hook=object_hook()) 183 | self.assertEqual(len(request_obj["persons"]), 2) 184 | self.assertTrue(isinstance(request_obj["persons"][0], Person)) 185 | 186 | def test_map_schema_func(self): 187 | 188 | class PersonSchema(ObjectSchema): 189 | first_name = String() 190 | last_name = String() 191 | 192 | @from_object() 193 | class Person(object): 194 | def __init__(self, first_name, last_name): 195 | self.first_name = first_name 196 | self.last_name = last_name 197 | 198 | bind_schema("Person", PersonSchema) 199 | with self.assertRaises(ValidationError) as context: 200 | loader('{"__type__": "Person"}') 201 | 202 | def test_map_schema_called_before_class_is_decorated(self): 203 | """ 204 | Test binding a schema to a class before it is defined works. 205 | """ 206 | 207 | class PersonSchema(ObjectSchema): 208 | first_name = String() 209 | last_name = String() 210 | 211 | bind_schema("Person", PersonSchema) 212 | 213 | @from_object() 214 | class Person(object): 215 | def __init__(self, first_name, last_name): 216 | self.first_name = first_name 217 | self.last_name = last_name 218 | 219 | with self.assertRaises(ValidationError): 220 | loader('{"__type__": "Person"}') 221 | 222 | def test_EnsureType_invoked_via_List_validator_honors_string_class_names(self): 223 | 224 | class JobSchema(ObjectSchema): 225 | id = Integer() 226 | title = String() 227 | 228 | class PersonSchema(ObjectSchema): 229 | first_name = String() 230 | last_name = String() 231 | jobs = List(EnsureType("Job")) 232 | 233 | @from_object(schema=JobSchema) 234 | class Job(object): 235 | def __init__(self, id, title): 236 | self.id = id 237 | self.title = title 238 | 239 | @from_object(schema=PersonSchema) 240 | class Person(object): 241 | def __init__(self, first_name, last_name, jobs): 242 | self.first_name = first_name 243 | self.last_name = last_name 244 | self.jobs = jobs 245 | 246 | obj = { 247 | "__type__": "Person", 248 | "first_name": "Shawn", 249 | "last_name": "Adams", 250 | "id": 1, 251 | "test": 12.0, 252 | "jobs": [{ 253 | "__type__": "Job", 254 | "title": "zoo keeper", 255 | "id": 1 256 | }] 257 | } 258 | 259 | person = loader(json.dumps(obj)) 260 | self.assertTrue(isinstance(person, Person)) 261 | self.assertTrue(isinstance(person.jobs, list)) 262 | self.assertTrue(isinstance(person.jobs[0], Job)) 263 | -------------------------------------------------------------------------------- /jsonweb/tests/test_encode.py: -------------------------------------------------------------------------------- 1 | import json 2 | import unittest 3 | from jsonweb import dumper, to_object, from_object, loader, encode 4 | from jsonweb.encode import to_list, JsonWebEncoder 5 | 6 | 7 | class TestJsonEncode(unittest.TestCase): 8 | def test_json_object_decorator(self): 9 | 10 | @to_object(suppress=["foo", "__type__"]) 11 | class Person(object): 12 | def __init__(self, first_name, last_name): 13 | self.foo = "bar" 14 | self.first_name = first_name 15 | self.last_name = last_name 16 | 17 | person = Person("shawn", "adams") 18 | json_obj = json.loads(dumper(person)) 19 | 20 | self.assertEqual(json_obj, {"first_name": "shawn", "last_name": "adams"}) 21 | 22 | def test_json_list_decorator(self): 23 | 24 | @to_list() 25 | class NickNames(object): 26 | def __init__(self, nicknames): 27 | self.nicknames = nicknames 28 | 29 | def __iter__(self): 30 | return iter(self.nicknames) 31 | 32 | @to_object(suppress=["foo", "__type__"]) 33 | class Person(object): 34 | def __init__(self, first_name, last_name, nicknames=[]): 35 | self.foo = "bar" 36 | self.first_name = first_name 37 | self.last_name = last_name 38 | self.nicknames = NickNames(nicknames) 39 | 40 | person = Person("shawn", "adams", ["Boss", "Champ"]) 41 | json_obj = json.loads(dumper(person)) 42 | 43 | self.assertEqual(json_obj, { 44 | "first_name": "shawn", 45 | "last_name": "adams", 46 | "nicknames": ["Boss", "Champ"] 47 | }) 48 | 49 | def test_subclass_json_web_encoder(self): 50 | message = [] 51 | 52 | class MyJsonWebEncoder(JsonWebEncoder): 53 | def object_handler(self, obj): 54 | message.append("my_object_handler") 55 | suppress = obj._encode.suppress 56 | json_obj = dict([(k, v) for k, v in obj.__dict__.items() 57 | if not k.startswith("_") and k not in suppress]) 58 | if "__type__" not in suppress: 59 | json_obj["__type__"] = obj._encode.__type__ 60 | return json_obj 61 | 62 | @to_object() 63 | class Person(object): 64 | def __init__(self, first_name, last_name): 65 | self.first_name = first_name 66 | self.last_name = last_name 67 | 68 | person = Person("shawn", "adams") 69 | json_obj = json.loads(dumper(person, cls=MyJsonWebEncoder)) 70 | 71 | self.assertEqual(json_obj, { 72 | "__type__": "Person", 73 | "first_name": "shawn", 74 | "last_name": "adams" 75 | }) 76 | self.assertEqual(message[0], "my_object_handler") 77 | 78 | def test_methods_dont_get_serialized(self): 79 | 80 | @to_object() 81 | class Person(object): 82 | def __init__(self, first_name, last_name): 83 | self.foo = "bar" 84 | self.first_name = first_name 85 | self.last_name = last_name 86 | 87 | def foo_method(self): 88 | return self.foo 89 | 90 | person = Person("shawn", "adams") 91 | # the dumper call with actually fail if it tries to 92 | # serialize a method type. 93 | self.assertIsInstance(dumper(person), str) 94 | 95 | def test_supplied_obj_handler(self): 96 | 97 | def person_handler(obj): 98 | return {"FirstName": obj.first_name, "LastName": obj.last_name} 99 | 100 | @to_object(handler=person_handler) 101 | class Person(object): 102 | def __init__(self, first_name, last_name): 103 | self.foo = "bar" 104 | self.first_name = first_name 105 | self.last_name = last_name 106 | 107 | person = Person("shawn", "adams") 108 | json_obj = json.loads(dumper(person)) 109 | 110 | self.assertEqual(json_obj, {"FirstName": "shawn", "LastName": "adams"}) 111 | 112 | def test_stacked_decorators(self): 113 | 114 | def person_handler(cls, obj): 115 | return cls( 116 | obj['first_name'], 117 | obj['last_name'] 118 | ) 119 | 120 | @to_object(suppress=["foo"]) 121 | @from_object(person_handler) 122 | class Person(object): 123 | def __init__(self, first_name, last_name): 124 | self.foo = "bar" 125 | self.first_name = first_name 126 | self.last_name = last_name 127 | 128 | person = Person("shawn", "adams") 129 | json_str = dumper(person) 130 | del person 131 | person = loader(json_str) 132 | self.assertTrue(isinstance(person, Person)) 133 | 134 | def test_attributes_are_suppressed(self): 135 | 136 | @to_object(suppress=["foo"]) 137 | class Person(object): 138 | def __init__(self, first_name, last_name): 139 | self.foo = "bar" 140 | self.first_name = first_name 141 | self.last_name = last_name 142 | 143 | person = Person("shawn", "adams") 144 | json_obj = json.loads(dumper(person)) 145 | self.assertTrue("foo" not in json_obj) 146 | 147 | def test_suppress__type__attribute(self): 148 | 149 | @to_object(suppress=["__type__"]) 150 | class Person(object): 151 | def __init__(self, first_name, last_name): 152 | self.foo = "bar" 153 | self.first_name = first_name 154 | self.last_name = last_name 155 | 156 | person = Person("shawn", "adams") 157 | json_obj = json.loads(dumper(person)) 158 | self.assertTrue("__type__" not in json_obj) 159 | 160 | def test_suppress_kw_arg_to_dumper(self): 161 | 162 | @to_object(suppress=["foo"]) 163 | class Person(object): 164 | def __init__(self, first_name, last_name): 165 | self.foo = "bar" 166 | self.first_name = first_name 167 | self.last_name = last_name 168 | 169 | person = Person("shawn", "adams") 170 | json_obj = json.loads(dumper(person)) 171 | 172 | self.assertTrue("foo" not in json_obj) 173 | self.assertTrue("first_name" in json_obj) 174 | self.assertTrue("last_name" in json_obj) 175 | 176 | json_obj = json.loads(dumper(person, suppress="first_name")) 177 | self.assertTrue("foo" not in json_obj) 178 | self.assertTrue("first_name" not in json_obj) 179 | self.assertTrue("last_name" in json_obj) 180 | 181 | def test_exclude_nulls_kw_arg_to_dumper(self): 182 | 183 | @to_object() 184 | class Person(object): 185 | def __init__(self, first_name, last_name): 186 | self.foo = "bar" 187 | self.first_name = first_name 188 | self.last_name = last_name 189 | 190 | person = Person("shawn", None) 191 | json_obj = json.loads(dumper(person, exclude_nulls=True)) 192 | self.assertTrue("last_name" not in json_obj) 193 | 194 | def test_exclude_nulls_kw_args_to_object(self): 195 | 196 | @to_object(exclude_nulls=True) 197 | class Person(object): 198 | def __init__(self, first_name, last_name): 199 | self.foo = "bar" 200 | self.first_name = first_name 201 | self.last_name = last_name 202 | 203 | person = Person("shawn", None) 204 | json_obj = json.loads(dumper(person)) 205 | self.assertTrue("last_name" not in json_obj) 206 | 207 | def test_exclude_nulls_on_dumper_trumps_to_object(self): 208 | 209 | @to_object(exclude_nulls=True) 210 | class Person(object): 211 | def __init__(self, first_name, last_name): 212 | self.foo = "bar" 213 | self.first_name = first_name 214 | self.last_name = last_name 215 | 216 | person = Person("shawn", None) 217 | json_obj = json.loads(dumper(person, exclude_nulls=False)) 218 | self.assertTrue("last_name" in json_obj) 219 | 220 | def test_supplied_handlers_kw_to_dumper(self): 221 | 222 | def person_handler(obj): 223 | return {"FirstName": obj.first_name, 224 | "LastName": obj.last_name, 225 | "Titles": obj.titles} 226 | 227 | def titles_handler(obj): 228 | return obj.titles 229 | 230 | @to_object() 231 | class Person(object): 232 | def __init__(self, first_name, last_name, titles=None): 233 | self.first_name = first_name 234 | self.last_name = last_name 235 | self.titles = titles 236 | 237 | @to_list() 238 | class Titles(object): 239 | def __init__(self, *titles): 240 | self.titles = titles 241 | 242 | person = Person("Joe", "Smith", Titles("Mr", "Dr")) 243 | json_obj = json.loads(dumper(person, handlers={ 244 | "Person": person_handler, 245 | "Titles": titles_handler 246 | })) 247 | 248 | self.assertEqual(json_obj, { 249 | "FirstName": "Joe", 250 | "LastName": "Smith", 251 | "Titles": ["Mr", "Dr"] 252 | }) 253 | 254 | def test_handler_decorator_for_object(self): 255 | 256 | @to_object() 257 | class Person(object): 258 | def __init__(self, first_name, last_name): 259 | self.first_name = first_name 260 | self.last_name = last_name 261 | 262 | @encode.handler 263 | def to_json(self): 264 | return {"FirstName": self.first_name, "LastName": self.last_name} 265 | 266 | person = Person("shawn", "adams") 267 | json_obj = json.loads(encode.dumper(person)) 268 | 269 | self.assertEqual(json_obj, {"FirstName": "shawn", "LastName": "adams"}) 270 | 271 | def test_handler_decorator_for_list(self): 272 | 273 | @encode.to_list() 274 | class ValueList(object): 275 | def __init__(self, *values): 276 | self.values = values 277 | @encode.handler 278 | def to_list(self): 279 | return self.values 280 | 281 | self.assertEqual(encode.dumper(ValueList(1 ,2, 3)), "[1, 2, 3]") 282 | -------------------------------------------------------------------------------- /jsonweb/tests/test_local.py: -------------------------------------------------------------------------------- 1 | from threading import Thread 2 | import unittest 3 | 4 | from jsonweb._local import LocalStack 5 | 6 | stack = LocalStack() 7 | 8 | class TestLocalStack(unittest.TestCase): 9 | def test_each_thread_has_own_stack(self): 10 | stack.push(42) 11 | 12 | self.assertEqual(stack.top, 42) 13 | 14 | class TestThread(Thread): 15 | def run(inst): 16 | self.assertEqual(stack.top, None) 17 | stack.push(10) 18 | self.assertEqual(stack.top, 10) 19 | 20 | t = TestThread() 21 | t.start() 22 | t.join() 23 | 24 | self.assertEqual(stack.top, 42) 25 | stack.clear() 26 | 27 | def test_pop_on_empty_stack_returns_None(self): 28 | self.assertEqual(stack.pop(), None) 29 | 30 | def test_push_and_pop(self): 31 | stack.push(66) 32 | stack.push(166) 33 | 34 | self.assertEqual(stack.top, 166) 35 | self.assertEqual(stack.pop(), 166) 36 | self.assertEqual(stack.pop(), 66) 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /jsonweb/tests/test_py3k.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import unittest 3 | 4 | 5 | def major_version(): 6 | return sys.version_info[0] 7 | 8 | 9 | class TestPy3k(unittest.TestCase): 10 | 11 | def test_PY3K_is_set_correctly(self): 12 | from jsonweb.py3k import PY3k 13 | 14 | if major_version() == 3: 15 | self.assertTrue(PY3k) 16 | if major_version() == 2: 17 | self.assertFalse(PY3k) 18 | 19 | def test_basestring_is_set_correctly(self): 20 | from jsonweb.py3k import basestring as _basestring 21 | 22 | if major_version() == 3: 23 | self.assertEqual(_basestring, (str, bytes)) 24 | if major_version() == 2: 25 | self.assertEqual(_basestring, basestring) 26 | 27 | def test_items(self): 28 | from jsonweb.py3k import items 29 | 30 | if major_version() == 3: 31 | self.assertEqual(type(items({})), type({}.items())) 32 | if major_version() == 2: 33 | self.assertEqual(type(items({})), type({}.iteritems())) 34 | -------------------------------------------------------------------------------- /jsonweb/tests/test_schema.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from jsonweb import from_object 3 | from jsonweb.schema import ObjectSchema, SchemaMeta 4 | from jsonweb.validators import String, Float, Integer, ValidationError, List, \ 5 | EnsureType 6 | 7 | 8 | class TestSchema(unittest.TestCase): 9 | def test_non_nested_obj_schema(self): 10 | 11 | class PersonSchema(ObjectSchema): 12 | first_name = String() 13 | last_name = String() 14 | id = Integer() 15 | test = Float() 16 | 17 | obj = {"first_name": "Shawn", "last_name": "Adams", "id": 1, "test": 12.0} 18 | self.assertEqual(obj, PersonSchema().validate(obj)) 19 | 20 | def test_nested_schema(self): 21 | 22 | class JobSchema(ObjectSchema): 23 | title = String() 24 | id = Integer() 25 | 26 | class PersonSchema(ObjectSchema): 27 | first_name = String() 28 | last_name = String() 29 | id = Integer() 30 | test = Float() 31 | job = JobSchema() 32 | 33 | obj = { 34 | "first_name": "Shawn", 35 | "last_name": "Adams", 36 | "id": 1, 37 | "test": 12.0, 38 | "job": { 39 | "title": "zoo keeper", 40 | "id": 1 41 | } 42 | } 43 | 44 | self.assertEqual(obj, PersonSchema().validate(obj)) 45 | 46 | def test_raises_validation_error(self): 47 | 48 | class PersonSchema(ObjectSchema): 49 | first_name = String() 50 | last_name = String() 51 | id = Integer() 52 | test = Float() 53 | 54 | schema = PersonSchema() 55 | obj = {"first_name": "shawn"} 56 | 57 | with self.assertRaises(ValidationError) as c: 58 | schema.validate(obj) 59 | 60 | exc = c.exception 61 | self.assertEqual(3, len(exc.errors)) 62 | self.assertTrue("last_name" in exc.errors) 63 | self.assertTrue("id" in exc.errors) 64 | self.assertTrue("test" in exc.errors) 65 | 66 | self.assertEqual("Missing required parameter.", 67 | str(exc.errors["last_name"])) 68 | 69 | self.assertEqual("Missing required parameter.", 70 | str(exc.errors["id"])) 71 | 72 | self.assertEqual("Missing required parameter.", 73 | str(exc.errors["test"])) 74 | 75 | obj = { 76 | "id": 1, 77 | "first_name": 10, 78 | "last_name": "Adams", 79 | "test": "bad type" 80 | } 81 | 82 | with self.assertRaises(ValidationError) as c: 83 | schema.validate(obj) 84 | 85 | exc = c.exception 86 | self.assertEqual(2, len(exc.errors)) 87 | self.assertTrue("first_name" in exc.errors) 88 | self.assertTrue("test" in exc.errors) 89 | 90 | self.assertEqual("Expected str got int instead.", 91 | str(exc.errors["first_name"])) 92 | 93 | self.assertEqual("Expected float got str instead.", 94 | str(exc.errors["test"])) 95 | 96 | def test_compound_error(self): 97 | """ 98 | Test a nested schema raises a compound (nested) ValidationError. 99 | """ 100 | 101 | class JobSchema(ObjectSchema): 102 | title = String() 103 | id = Integer() 104 | 105 | class PersonSchema(ObjectSchema): 106 | first_name = String() 107 | last_name = String() 108 | id = Integer() 109 | test = Float() 110 | job = JobSchema() 111 | 112 | schema = PersonSchema() 113 | 114 | obj = { 115 | "first_name": "Shawn", 116 | "last_name": "Adams", 117 | "id": 1, 118 | "test": 12.0, 119 | "job": {} 120 | } 121 | 122 | with self.assertRaises(ValidationError) as c: 123 | schema.validate(obj) 124 | 125 | exc = c.exception 126 | self.assertTrue("job" in exc.errors) 127 | self.assertEqual(len(exc.errors["job"].errors), 2) 128 | self.assertEqual(str(exc.errors["job"].errors["id"]), 129 | "Missing required parameter.") 130 | 131 | self.assertEqual(str(exc.errors["job"].errors["title"]), 132 | "Missing required parameter.") 133 | 134 | def test_list_schema_error(self): 135 | 136 | class PersonSchema(ObjectSchema): 137 | first_name = String() 138 | last_name = String() 139 | 140 | persons = [ 141 | { 142 | "first_name": "shawn", 143 | "last_name": "adams" 144 | }, 145 | { 146 | "first_name": "luke" 147 | } 148 | ] 149 | 150 | with self.assertRaises(ValidationError) as c: 151 | List(PersonSchema()).validate(persons) 152 | 153 | exc = c.exception 154 | self.assertEqual(1, exc.errors[0].extras["index"]) 155 | 156 | def test_EnsureType_raises_validation_error(self): 157 | 158 | class Foo(object): 159 | pass 160 | 161 | class JobSchema(ObjectSchema): 162 | title = String() 163 | id = EnsureType(Foo) 164 | 165 | with self.assertRaises(ValidationError) as c: 166 | JobSchema().validate({"title": "jedi", "id": 1}) 167 | 168 | exc = c.exception 169 | self.assertEqual(str(exc.errors["id"]), "Expected Foo got int instead.") 170 | 171 | def test_EnsureType_kw_arguments_stick_around(self): 172 | """ 173 | Tests bug fix for: http://github.com/boris317/JsonWeb/issues/7 174 | """ 175 | 176 | class FooSchema(ObjectSchema): 177 | bar = EnsureType("Bar", optional=True, nullable=True) 178 | 179 | @from_object() 180 | class Bar(object): 181 | def __init__(self, honk): 182 | self.honk = honk 183 | 184 | ensure_type = FooSchema().bar 185 | 186 | self.assertTrue(ensure_type.nullable) 187 | self.assertFalse(ensure_type.required) 188 | 189 | def test_attributes_can_be_optional(self): 190 | 191 | class PersonSchema(ObjectSchema): 192 | first_name = String() 193 | last_name = String(optional=True) 194 | 195 | person = {"first_name": "shawn"} 196 | self.assertEqual(person, PersonSchema().validate(person)) 197 | 198 | def test_attributes_can_have_default_values(self): 199 | 200 | class PersonSchema(ObjectSchema): 201 | species = String(default="Human") 202 | first_name = String() 203 | last_name = String() 204 | 205 | person = PersonSchema().validate( 206 | {"first_name": "shawn", "last_name": "adams"} 207 | ) 208 | self.assertEqual(person.get("species"), "Human") 209 | 210 | def test_create(self): 211 | schema_cls = ObjectSchema.create("MySchema", { 212 | "first-name": String(), 213 | "last-name": String(optional=True) 214 | }) 215 | 216 | self.assertEqual(type(schema_cls), SchemaMeta) 217 | self.assertIsInstance(schema_cls(), ObjectSchema) 218 | 219 | with self.assertRaises(ValidationError) as c: 220 | schema_cls().validate({'first-name': 1}) 221 | 222 | self.assertIn('first-name', c.exception.errors) 223 | 224 | def test_kw_args_are_passed_correctly(self): 225 | 226 | class PersonSchema(ObjectSchema): 227 | first_name = String() 228 | last_name = String(optional=True) 229 | 230 | schema = PersonSchema(default="foo", optional=True, 231 | nullable=True, reason_code="BECAUSE") 232 | 233 | self.assertEqual("foo", schema.default) 234 | self.assertEqual("BECAUSE", schema.reason_code) 235 | self.assertTrue(schema.nullable) 236 | self.assertFalse(schema.required) 237 | 238 | def test_subclasses_inherit_validators_from_base_schema(self): 239 | 240 | class BaseSchema(ObjectSchema): 241 | foo = String() 242 | 243 | class BarSchema(BaseSchema): 244 | bar = String() 245 | 246 | schema = BarSchema() 247 | 248 | # assert _fields are merged 249 | self.assertIn("foo", schema._fields) 250 | self.assertIn("bar", schema._fields) 251 | 252 | with self.assertRaises(ValidationError) as c: 253 | schema.validate({}) 254 | 255 | # assert validators run correctly 256 | exc = c.exception 257 | self.assertIn("foo", exc.errors) 258 | self.assertIn("bar", exc.errors) 259 | -------------------------------------------------------------------------------- /jsonweb/tests/test_validators.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from datetime import datetime 3 | 4 | from jsonweb.validators import Dict, List, String, Regex, Integer, Float, \ 5 | Number, OneOf, SubSetOf, DateTime, Boolean, EnsureType, ValidationError 6 | 7 | 8 | class TestEachValidator(unittest.TestCase): 9 | def test_string_validator(self): 10 | v = String() 11 | self.assertEqual("foo", v.validate("foo")) 12 | with self.assertRaises(ValidationError) as c: 13 | v.validate(1) 14 | 15 | self.assertEqual("Expected str got int instead.", str(c.exception)) 16 | 17 | def test_string_validator_max_len_kw(self): 18 | v = String(max_len=3) 19 | self.assertEqual("foo", v.validate("foo")) 20 | with self.assertRaises(ValidationError) as c: 21 | v.validate("foobar") 22 | 23 | self.assertEqual("String exceeds max length of 3.", str(c.exception)) 24 | 25 | def test_string_validator_min_len_kw(self): 26 | v = String(min_len=3) 27 | 28 | with self.assertRaises(ValidationError) as c: 29 | v.validate("fo") 30 | 31 | self.assertEqual("String must be at least length 3.", str(c.exception)) 32 | 33 | def test_regex_validator(self): 34 | v = Regex(r"^foo[0-9]", max_len=10) 35 | self.assertEqual("foo12", v.validate("foo12")) 36 | 37 | with self.assertRaises(ValidationError) as c: 38 | v.validate("foo") 39 | 40 | exc = c.exception 41 | self.assertEqual("String does not match pattern '^foo[0-9]'.", str(exc)) 42 | self.assertEqual("invalid_str", exc.reason_code) 43 | 44 | with self.assertRaises(ValidationError) as c: 45 | v.validate("a"*11) 46 | self.assertEqual("String exceeds max length of 10.", str(c.exception)) 47 | 48 | def test_integer_validator(self): 49 | v = Integer() 50 | self.assertEqual(v.validate(42), 42) 51 | with self.assertRaises(ValidationError) as c: 52 | v.validate("foo") 53 | 54 | self.assertEqual("Expected int got str instead.", str(c.exception)) 55 | 56 | def test_float_validator(self): 57 | v = Float() 58 | self.assertEqual(42.0, v.validate(42.0)) 59 | with self.assertRaises(ValidationError) as c: 60 | v.validate(42) 61 | 62 | self.assertEqual("Expected float got int instead.", str(c.exception)) 63 | 64 | def test_boolean_validator(self): 65 | v = Boolean() 66 | self.assertEqual(True, v.validate(True)) 67 | with self.assertRaises(ValidationError) as c: 68 | v.validate("5") 69 | 70 | self.assertEqual("Expected bool got str instead.", str(c.exception)) 71 | 72 | def test_number_validator(self): 73 | v = Number() 74 | self.assertEqual(42.0, v.validate(42.0)) 75 | self.assertEqual(42, v.validate(42)) 76 | with self.assertRaises(ValidationError) as c: 77 | v.validate("foo") 78 | 79 | self.assertEqual("Expected number got str instead.", str(c.exception)) 80 | 81 | def test_dict_validator(self): 82 | v = Dict(Number) 83 | dict_to_test = {"foo": 1, "bar": 1.2} 84 | self.assertDictEqual(v.validate(dict_to_test), dict_to_test) 85 | 86 | with self.assertRaises(ValidationError) as c: 87 | v.validate({"foo": []}) 88 | 89 | exc = c.exception 90 | self.assertEqual("Error validating dict.", str(exc)) 91 | self.assertEqual(1, len(exc.errors)) 92 | self.assertEqual("Expected number got list instead.", str(exc.errors["foo"])) 93 | self.assertEqual("invalid_dict", exc.reason_code) 94 | 95 | def test_dict_key_validator(self): 96 | v = Dict(Number, key_validator=Regex("[a-z]{2}_[A-Z]{2}")) 97 | dict_to_test = {"en_US": 1} 98 | self.assertDictEqual(v.validate(dict_to_test), dict_to_test) 99 | 100 | with self.assertRaises(ValidationError) as c: 101 | v.validate({"en-US": "1"}) 102 | 103 | exc = c.exception 104 | self.assertEqual("String does not match pattern " 105 | "'[a-z]{2}_[A-Z]{2}'.", str(exc.errors["en-US"])) 106 | self.assertEqual("invalid_dict_key", exc.errors["en-US"].reason_code) 107 | 108 | def test_list_validator(self): 109 | v = List(Number) 110 | self.assertEqual([1, 2, 3], v.validate([1, 2, 3])) 111 | 112 | with self.assertRaises(ValidationError) as c: 113 | v.validate("foo") 114 | 115 | self.assertEqual("Expected list got str instead.", str(c.exception)) 116 | 117 | with self.assertRaises(ValidationError) as c: 118 | v.validate(["foo"]) 119 | 120 | exc = c.exception 121 | self.assertEqual("Error validating list.", str(exc)) 122 | self.assertEqual(1, len(exc.errors)) 123 | self.assertEqual(0, exc.errors[0].extras["index"]) 124 | self.assertEqual("invalid_list_item", exc.errors[0].reason_code) 125 | self.assertEqual("Expected number got str instead.", str(exc.errors[0])) 126 | 127 | def test_ensuretype_validator(self): 128 | v = EnsureType((int, float)) 129 | self.assertEqual(42.0, v.validate(42.0)) 130 | self.assertEqual(42, v.validate(42)) 131 | 132 | with self.assertRaises(ValidationError) as c: 133 | v.validate("foo") 134 | 135 | exc = c.exception 136 | self.assertEqual("Expected one of (int, float) got str instead.", str(exc)) 137 | self.assertEqual("invalid_type", exc.reason_code) 138 | 139 | def test_datetime_validator(self): 140 | v = DateTime() 141 | self.assertIsInstance(v.validate("2012-01-01 12:30:00"), datetime) 142 | 143 | with self.assertRaises(ValidationError) as c: 144 | v.validate("01-01-2012") 145 | 146 | exc = c.exception 147 | self.assertEqual("time data '01-01-2012' does not " 148 | "match format '%Y-%m-%d %H:%M:%S'", str(exc)) 149 | self.assertEqual("invalid_datetime", exc.reason_code) 150 | 151 | def test_nullable_is_true(self): 152 | v = Integer(nullable=True) 153 | self.assertEqual(None, v.validate(None)) 154 | 155 | def test_one_of_validator(self): 156 | self.assertEqual(OneOf(1, 2, 3).validate(1), 1) 157 | 158 | with self.assertRaises(ValidationError) as c: 159 | OneOf(1, "2", 3).validate("1") 160 | 161 | exc = c.exception 162 | self.assertEqual("Expected one of (1, '2', 3) " 163 | "but got '1' instead.", str(exc)) 164 | self.assertEqual("not_one_of", exc.reason_code) 165 | 166 | def test_sub_set_of_validator(self): 167 | self.assertEqual(SubSetOf([1, 2, 3]).validate([1, 3]), [1, 3]) 168 | 169 | with self.assertRaises(ValidationError) as c: 170 | SubSetOf([1, 2, 3]).validate([2, 5]) 171 | 172 | exc = c.exception 173 | self.assertEqual("[2, 5] is not a subset of [1, 2, 3]", str(exc)) 174 | self.assertEqual("not_a_sub_set_of", exc.reason_code) 175 | 176 | 177 | class TestValidationError(unittest.TestCase): 178 | 179 | def test_to_json_with_errors(self): 180 | e = ValidationError("Boom", errors={"key": "value"}) 181 | 182 | expected_dict = {"reason": "Boom", "errors": {"key": "value"}} 183 | self.assertDictEqual(e.to_json(), expected_dict) 184 | 185 | def test_to_json_with_no_errors(self): 186 | e = ValidationError("Boom") 187 | 188 | self.assertEqual(e.to_json(), {"reason": "Boom"}) 189 | 190 | def test_to_json_with_extras(self): 191 | e = ValidationError("Boom", errors={"key": "value"}, foo="bar") 192 | 193 | expected_dict = { 194 | "reason": "Boom", 195 | "foo": "bar", 196 | "errors": {"key": "value"} 197 | } 198 | 199 | self.assertDictEqual(e.to_json(), expected_dict) 200 | 201 | def test_to_json_with_reason_code(self): 202 | e = ValidationError("Boom", reason_code="because", 203 | errors={"key": "value"}, foo="bar") 204 | 205 | expected_dict = { 206 | "reason": "Boom", 207 | "reason_code": "because", 208 | "foo": "bar", 209 | "errors": {"key": "value"} 210 | } 211 | 212 | self.assertDictEqual(e.to_json(), expected_dict) -------------------------------------------------------------------------------- /jsonweb/validators.py: -------------------------------------------------------------------------------- 1 | 2 | import inspect 3 | import re 4 | from datetime import datetime 5 | from jsonweb import encode 6 | 7 | from jsonweb.py3k import basestring, items 8 | from jsonweb.exceptions import JsonWebError 9 | 10 | 11 | class _Errors(object): 12 | def __init__(self, list_or_dict): 13 | self.errors = list_or_dict 14 | 15 | def add_error(self, exc, key=None, reason_code=None, **extras): 16 | exc = isinstance(exc, ValidationError) and exc or ValidationError(exc) 17 | if reason_code is not None: 18 | exc.reason_code = reason_code 19 | exc.extras.update(extras) 20 | 21 | if key is not None: 22 | self.errors[key] = exc 23 | else: 24 | self.errors.append(exc) 25 | 26 | def raise_if_errors(self, reason, **kw): 27 | if self.errors: 28 | raise ValidationError(reason, errors=self.errors, **kw) 29 | 30 | 31 | @encode.to_object() 32 | class ValidationError(JsonWebError): 33 | """ 34 | Raised from ``JsonWeb`` validators when validation of an object fails. 35 | """ 36 | def __init__(self, reason, reason_code=None, errors=None, **extras): 37 | """ 38 | :param reason: A nice message describing what was not valid 39 | :param reason_code: programmatic friendly reason code 40 | :param errors: A ``list`` or ``dict`` of nested ``ValidationError`` 41 | :param extras: Any extra info about the error you want to convey 42 | """ 43 | JsonWebError.__init__(self, reason, **extras) 44 | self.errors = errors 45 | self.reason_code = reason_code 46 | 47 | @encode.handler 48 | def to_json(self): 49 | obj = {"reason": str(self)} 50 | obj.update(self.extras) 51 | 52 | if self.errors: 53 | obj["errors"] = self.errors 54 | if self.reason_code: 55 | obj["reason_code"] = self.reason_code 56 | 57 | return obj 58 | 59 | 60 | @encode.to_object() 61 | class BaseValidator(object): 62 | """ 63 | Abstract base validator which all ``JsonWeb`` validators should inherit 64 | from. Out of the box ``JsonWeb`` comes with a dozen or so validators. 65 | 66 | All validators will override :meth:`BaseValidator._validate` method which 67 | should accept an object and return the passed in object or raise a 68 | :class:`ValidationError` if validation failed. 69 | 70 | .. note:: 71 | 72 | You are not `required` to return the exact passed in object. You may 73 | for instance want to transform the object in some way. This is exactly 74 | what :class:`DateTime` does. 75 | 76 | """ 77 | def __init__(self, optional=False, nullable=False, 78 | default=None, reason_code=None): 79 | """ 80 | All validators that inherit from ``BaseValidator`` should pass 81 | ``optional``, ``nullable`` and ``default`` as explicit kw arguments 82 | or ``**kw``. 83 | 84 | :param optional: Is the item optional? 85 | :param nullable: Can the item's value can be None? 86 | :param default: A default value for this item. 87 | :param reason_code: Failure reason_code that is passed to any 88 | :class:`ValidationError` raised from this instance. 89 | """ 90 | self.required = (not optional) 91 | self.nullable = nullable 92 | self.default = default 93 | self.reason_code = reason_code 94 | 95 | def validate(self, item): 96 | if item is None: 97 | if self.nullable: 98 | return item 99 | self.raise_error("Cannot be null.") 100 | return self._validate(item) 101 | 102 | @encode.handler 103 | def to_json(self, **kw): 104 | obj = { 105 | "required": self.required, 106 | "nullable": self.nullable 107 | } 108 | obj.update(kw) 109 | return obj 110 | 111 | def _validate(self, item): 112 | raise NotImplemented 113 | 114 | def raise_error(self, message, *args): 115 | if len(args): 116 | message = message.format(*args) 117 | 118 | exc = ValidationError(message) 119 | 120 | if self.reason_code is not None: 121 | exc.reason_code = self.reason_code 122 | raise exc 123 | 124 | 125 | class Dict(BaseValidator): 126 | """ 127 | .. versionadded:: 0.8 128 | 129 | Validates a dict of things. The Dict constructor accepts 130 | a validator and each value in the dict will be validated 131 | against it :: 132 | 133 | >>> Dict(Number).validate({'foo': 1}) 134 | ... {'foo': 1} 135 | 136 | >>> Dict(Number).validate({'foo': "abc"}) 137 | Traceback (most recent call last): 138 | ... 139 | ValidationError: Error validating dict. 140 | 141 | In order see what part of the dict failed validation we must dig 142 | deeper into the exception:: 143 | 144 | >>> str(e.errors["foo"]) 145 | ... 'Expected number got str instead.' 146 | 147 | ``Dict`` also accepts an optional ``key_validator``, which must be a subclass 148 | of :class:`String`: :: 149 | 150 | validator = Dict(Number, key_validator=Regex(r'^[a-z]{2}_[A-Z]{2}$')) 151 | try: 152 | validator.validate({"en-US": 1}) 153 | except ValidationError as e: 154 | print(e.errors["en-US"]) 155 | print(e.errors["en-US"].reason_code) 156 | 157 | # output 158 | # String does not match pattern '^[a-z]{2}_[A-Z]{2}$'. 159 | # invalid_dict_key 160 | 161 | """ 162 | 163 | def __init__(self, validator, key_validator=None, **kw): 164 | """ 165 | :param validator: A :class:`BaseValidator` subclass which all values 166 | of a dict will be validated against. 167 | :param key_validator: A :class:`String` subclass which all keys 168 | of a dict will be validated against. 169 | (Default: :class:`String`) 170 | """ 171 | super(Dict, self).__init__(reason_code="invalid_dict", **kw) 172 | self.validator = to_instance(validator) 173 | self.key_validator = to_instance(key_validator) or String() 174 | assert isinstance(self.key_validator, String) 175 | 176 | def _validate(self, obj): 177 | isinstance_or_raise(obj, dict) 178 | errors = _Errors({}) 179 | validated_obj = {} 180 | 181 | for k, v in items(obj): 182 | if not self._key_is_valid(k, errors): 183 | continue 184 | try: 185 | validated_obj[k] = get_validator(self.validator).validate(v) 186 | except ValidationError as e: 187 | errors.add_error(e, key=k, reason_code="invalid_dict_value") 188 | 189 | errors.raise_if_errors("Error validating dict.", 190 | reason_code=self.reason_code) 191 | return validated_obj 192 | 193 | def _key_is_valid(self, key, errors): 194 | if self.key_validator is None: 195 | return True 196 | try: 197 | self.key_validator.validate(key) 198 | except ValidationError as e: 199 | errors.add_error(e, key=key, reason_code="invalid_dict_key") 200 | return False 201 | return True 202 | 203 | 204 | class List(BaseValidator): 205 | """ 206 | Validates a list of things. The List constructor accepts 207 | a validator and each item in the list will be validated 208 | against it :: 209 | 210 | >>> List(Integer).validate([1,2,3,4]) 211 | ... [1,2,3,4] 212 | 213 | >>> List(Integer).validate(10) 214 | Traceback (most recent call last): 215 | ... 216 | ValidationError: Expected list got int instead. 217 | 218 | Since :class:`ObjectSchema` is also a validator we can do this :: 219 | 220 | >>> class PersonSchema(ObjectSchema): 221 | ... first_name = String() 222 | ... last_name = String() 223 | ... 224 | >>> List(PersonSchema).validate([ 225 | ... {"first_name": "bob", "last_name": "smith"}, 226 | ... {"first_name": "jane", "last_name": "smith"} 227 | ... ]) 228 | 229 | """ 230 | def __init__(self, validator, **kw): 231 | super(List, self).__init__(reason_code="invalid_list", **kw) 232 | self.validator = to_instance(validator) 233 | 234 | def _validate(self, item): 235 | isinstance_or_raise(item, list) 236 | validated_objs = [] 237 | errors = _Errors([]) 238 | 239 | for i, obj in enumerate(item): 240 | try: 241 | validated_objs.append( 242 | get_validator(self.validator).validate(obj) 243 | ) 244 | except ValidationError as e: 245 | errors.add_error(e, reason_code="invalid_list_item", index=i) 246 | 247 | errors.raise_if_errors("Error validating list.", 248 | reason_code=self.reason_code) 249 | return validated_objs 250 | 251 | def to_json(self): 252 | return super(List, self).to_json() 253 | 254 | 255 | class EnsureType(BaseValidator): 256 | """ 257 | Validates something is a certain type :: 258 | 259 | >>> class Person(object): 260 | ... pass 261 | >>> EnsureType(Person).validate(Person()) 262 | ... 263 | >>> EnsureType(Person).validate(10) 264 | Traceback (most recent call last): 265 | ... 266 | ValidationError: Expected Person got int instead. 267 | 268 | """ 269 | 270 | def __init__(self, _type, type_name=None, **kw): 271 | super(EnsureType, self).__init__( 272 | reason_code=kw.pop("reason_code", "invalid_type"), **kw 273 | ) 274 | self.__type = _type 275 | #``_type`` can be a string. This way you can reference a class 276 | #that may not be defined yet. In this case we must explicitly 277 | #set type_name or an instance error is raised inside ``__type_name`` 278 | if isinstance(_type, str): 279 | type_name = _type 280 | self.__type_name = type_name or self.__type_name(_type) 281 | 282 | def _validate(self, item): 283 | if not isinstance(item, self.__type): 284 | self.raise_error("Expected {0} got {1} instead.", 285 | self.__type_name, cls_name(item)) 286 | return item 287 | 288 | def __type_name(self, _type): 289 | if isinstance(_type, tuple): 290 | return "one of ({0})".format(", ".join((t.__name__ for t in _type))) 291 | return _type.__name__ 292 | 293 | def __get__(self, obj, type=None): 294 | 295 | if type is None: 296 | return self 297 | if not isinstance(self.__type, str): 298 | return self 299 | 300 | from jsonweb.decode import _default_object_handlers 301 | #``_type`` was a string and now we must get the actual class 302 | handler = _default_object_handlers.get(self.__type) 303 | 304 | if not handler: 305 | raise JsonWebError("Cannot find class {0}.".format(self.__type)) 306 | 307 | return EnsureType( 308 | handler[1], 309 | type_name=self.__type_name, 310 | optional=(not self.required), 311 | nullable=self.nullable 312 | ) 313 | 314 | def to_json(self, **kw): 315 | return super(EnsureType, self).to_json( 316 | type=self.__type_name, **kw 317 | ) 318 | 319 | 320 | class String(EnsureType): 321 | """ 322 | Validates something is a string :: 323 | 324 | >>> String().validate("foo") 325 | ... 'foo' 326 | >>> String().validate(1) 327 | Traceback (most recent call last): 328 | ... 329 | ValidationError: Expected str got int instead. 330 | 331 | Specify a maximum length :: 332 | 333 | >>> String(max_len=3).validate("foobar") 334 | Traceback (most recent call last): 335 | ... 336 | ValidationError: String exceeds max length of 3. 337 | 338 | Specify a minimum length :: 339 | 340 | >>> String(min_len=3).validate("fo") 341 | Traceback (most recent call last): 342 | ... 343 | ValidationError: String must be at least length 3. 344 | 345 | """ 346 | def __init__(self, min_len=None, max_len=None, **kw): 347 | super(String, self).__init__(basestring, type_name="str", 348 | reason_code="invalid_str", **kw) 349 | self.max_len = max_len 350 | self.min_len = min_len 351 | 352 | def _validate(self, item): 353 | value = super(String, self)._validate(item) 354 | if self.min_len and len(value) < self.min_len: 355 | self.raise_error("String must be at least length {0}.", self.min_len) 356 | if self.max_len and len(value) > self.max_len: 357 | self.raise_error("String exceeds max length of {0}.", self.max_len) 358 | return value 359 | 360 | 361 | class Regex(String): 362 | """ 363 | .. versionadded:: 0.6.3 Validates a string against a regular expression :: 364 | 365 | >>> Regex(r"^foo").validate("barfoo") 366 | Traceback (most recent call last): 367 | ... 368 | ValidationError: String does not match pattern '^foo'. 369 | """ 370 | 371 | def __init__(self, regex, **kw): 372 | super(Regex, self).__init__(**kw) 373 | self.regex = re.compile(regex) 374 | 375 | def _validate(self, item): 376 | value = super(Regex, self)._validate(item) 377 | if self.regex.match(value) is None: 378 | self.raise_error("String does not match pattern '{0}'.", 379 | self.regex.pattern) 380 | return value 381 | 382 | 383 | class Integer(EnsureType): 384 | """ Validates something in an integer """ 385 | def __init__(self, **kw): 386 | super(Integer, self).__init__(int, **kw) 387 | 388 | 389 | class Float(EnsureType): 390 | """ Validates something is a float """ 391 | def __init__(self, **kw): 392 | super(Float, self).__init__(float, **kw) 393 | 394 | 395 | class Boolean(EnsureType): 396 | """ Validates something is a Boolean (True/False)""" 397 | def __init__(self, **kw): 398 | super(Boolean, self).__init__(bool, **kw) 399 | 400 | 401 | class Number(EnsureType): 402 | """ 403 | Validates something is a number :: 404 | 405 | >>> Number().validate(1) 406 | ... 1 407 | >>> Number().validate(1.1) 408 | >>> 1.1 409 | >>> Number().validate("foo") 410 | Traceback (most recent call last): 411 | ... 412 | ValidationError: Expected number got int instead. 413 | 414 | """ 415 | def __init__(self, **kw): 416 | super(Number, self).__init__((float, int), type_name="number", 417 | reason_code="invalid_number", **kw) 418 | 419 | 420 | class DateTime(BaseValidator): 421 | """ 422 | Validates that something is a valid date/datetime string and turns it 423 | into a :py:class:`datetime.datetime` instance :: 424 | 425 | >>> DateTime().validate("2010-01-02 12:30:00") 426 | ... datetime.datetime(2010, 1, 2, 12, 30) 427 | 428 | >>> DateTime().validate("2010-01-02 12:300") 429 | Traceback (most recent call last): 430 | ... 431 | ValidationError: time data '2010-01-02 12:300' does not match format '%Y-%m-%d %H:%M:%S' 432 | 433 | The default datetime format is ``%Y-%m-%d %H:%M:%S``. You can specify your 434 | own :: 435 | 436 | >>> DateTime("%m/%d/%Y").validate("01/02/2010") 437 | ... datetime.datetime(2010, 1, 2, 0, 0) 438 | 439 | 440 | """ 441 | def __init__(self, format="%Y-%m-%d %H:%M:%S", **kw): 442 | super(DateTime, self).__init__(reason_code="invalid_datetime", **kw) 443 | self.format = format 444 | 445 | def _validate(self, item): 446 | try: 447 | return datetime.strptime(item, self.format) 448 | except ValueError as e: 449 | self.raise_error(str(e)) 450 | 451 | def to_json(self): 452 | return super(DateTime, self).to_json( 453 | type="DateTime", 454 | format=self.format 455 | ) 456 | 457 | 458 | class OneOf(BaseValidator): 459 | """ 460 | .. versionadded:: 0.6.4 461 | Validates something is a one of a list of allowed values:: 462 | 463 | >>> OneOf("a", "b", "c").validate(1) 464 | Traceback (most recent call last): 465 | ... 466 | ValidationError: Expected one of (a, b, c) got 1 instead. 467 | """ 468 | def __init__(self, *values, **kw): 469 | super(OneOf, self).__init__(reason_code="not_one_of", **kw) 470 | self.allowed_values = values 471 | 472 | def _validate(self, item): 473 | 474 | def stringify(item): 475 | if isinstance(item, str): 476 | return "'{0}'".format(item) 477 | return str(item) 478 | 479 | if item in self.allowed_values: 480 | return item 481 | 482 | self.raise_error("Expected one of {0} but got {1} instead.", 483 | self.allowed_values, stringify(item)) 484 | 485 | 486 | class SubSetOf(BaseValidator): 487 | """ 488 | .. versionadded:: 0.6.4 Validates a list is subset of another list:: 489 | 490 | >>> SubSetOf([1, 2, 3, 4]).validate([1, 4]) 491 | ... [1, 4] 492 | >>> SubSetOf([1, 2, 3, 4]).validate([1,5]) 493 | Traceback (most recent call last): 494 | ... 495 | ValidationError: [1, 5] is not a subset of [1, 2, 3, 4]. 496 | """ 497 | 498 | def __init__(self, super_set, **kw): 499 | super(SubSetOf, self).__init__(reason_code="not_a_sub_set_of", **kw) 500 | self.super_set = super_set 501 | 502 | def _validate(self, sub_set): 503 | if set(sub_set).issubset(self.super_set): 504 | return sub_set 505 | self.raise_error("{0} is not a subset of {1}", sub_set, self.super_set) 506 | 507 | 508 | def to_instance(obj): 509 | return inspect.isclass(obj) and obj() or obj 510 | 511 | 512 | def get_validator(validator): 513 | # We must manually invoke the descriptor protocol so that 514 | # any string names passed to EnsureType get translated to 515 | # an actual class. 516 | if isinstance(validator, EnsureType): 517 | return validator.__get__(None, List) 518 | return validator 519 | 520 | 521 | def isinstance_or_raise(obj, cls): 522 | if not isinstance(obj, cls): 523 | raise ValidationError("Expected {0} got {1} instead.".format( 524 | cls_name(cls), 525 | cls_name(obj) 526 | ), reason_code="invalid_type") 527 | 528 | 529 | def cls_name(obj): 530 | if inspect.isclass(obj): 531 | return obj.__name__ 532 | return obj.__class__.__name__ -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [aliases] 2 | test = nosetests -v --with-coverage --cover-package=jsonweb 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import os 2 | from setuptools import setup, find_packages 3 | 4 | version = '0.8.2' 5 | 6 | if os.path.exists("README.rst"): 7 | long_description = open("README.rst").read() 8 | else: 9 | long_description = "See http://www.jsonweb.net/en/latest" 10 | 11 | setup(name='JsonWeb', 12 | version=version, 13 | description="Quickly add json serialization and deserialization to your python classes.", 14 | long_description=long_description, 15 | keywords='', 16 | author='Shawn Adams', 17 | author_email='', 18 | url='', 19 | license='BSD', 20 | packages=find_packages(exclude=['examples', 'tests']), 21 | include_package_data=True, 22 | zip_safe=False, 23 | install_requires=[ 24 | # -*- Extra requirements: -*- 25 | ], 26 | entry_points=""" 27 | # -*- Entry points: -*- 28 | """, 29 | classifiers=[ 30 | 'Development Status :: 4 - Beta', 31 | 'Environment :: Web Environment', 32 | 'Intended Audience :: Developers', 33 | 'License :: OSI Approved :: BSD License', 34 | 'Operating System :: OS Independent', 35 | 'Programming Language :: Python :: 2.7', 36 | 'Programming Language :: Python :: 3', 37 | 'Programming Language :: Python :: Implementation :: CPython', 38 | 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 39 | 'Topic :: Software Development :: Libraries :: Python Modules' 40 | ] 41 | ) 42 | -------------------------------------------------------------------------------- /test_requires.txt: -------------------------------------------------------------------------------- 1 | #The following packages are required for testing 2 | nose 3 | coverage 4 | --------------------------------------------------------------------------------