├── .codeclimate.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── publish.yml │ └── pythonpackage.yml ├── .gitignore ├── .hound.yml ├── .pre-commit-config.yaml ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.md ├── conftest.py ├── development.txt ├── docs ├── Introduction.md ├── Makefile ├── apidoc.rst ├── conf.py └── index.rst ├── python_jsonschema_objects ├── __init__.py ├── _version.py ├── classbuilder.py ├── descriptors.py ├── examples │ ├── README.md │ └── __init__.py ├── literals.py ├── markdown_support.py ├── pattern_properties.py ├── util.py ├── validators.py └── wrapper_types.py ├── register.py ├── setup.cfg ├── setup.py ├── test ├── resources │ ├── adaptive-card.json │ ├── query.json │ └── schema.json ├── test_229.py ├── test_232.py ├── test_292.py ├── test_297.py ├── test_array_validation.py ├── test_circular_references.py ├── test_default_values.py ├── test_feature_151.py ├── test_feature_177.py ├── test_feature_51.py ├── test_nested_arrays.py ├── test_nondefault_resolver_validator.py ├── test_pattern_properties.py ├── test_pytest.py ├── test_regression_114.py ├── test_regression_126.py ├── test_regression_133.py ├── test_regression_143.py ├── test_regression_156.py ├── test_regression_165.py ├── test_regression_17.py ├── test_regression_185.py ├── test_regression_208.py ├── test_regression_213.py ├── test_regression_214.py ├── test_regression_218.py ├── test_regression_232.py ├── test_regression_49.py ├── test_regression_8.py ├── test_regression_87.py ├── test_regression_88.py ├── test_regression_89.py ├── test_regression_90.py ├── test_util_pytest.py ├── test_wrong_exception_protocolbase_getitem.py ├── thing-one.json └── thing-two.json ├── tox.ini └── versioneer.py /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | pep8: 3 | enabled: true 4 | checks: 5 | E128: 6 | enabled: false 7 | E203: 8 | enabled: false 9 | exclude_paths: 10 | - versioneer.py 11 | 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | python_jsonschema_objects/_version.py export-subst 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Example Schema and code** 14 | ``` 15 | A schema and code that shows the incorrect behavior 16 | ``` 17 | 18 | **Expected behavior** 19 | A clear and concise description of what you expected to happen. 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: Feature Request 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What are you trying to do?** 11 | A clear and concise description of what you're trying to do. e.g. "We use this library to automatically generate classes to validate incoming web requests, and ..." 12 | 13 | **Is this a currently unsupported jsonschema directive? If so, please link to the documentation** 14 | 15 | I'd like to be able to use the format validator from v7, as described here: https://json-schema.org/understanding-json-schema/reference/string.html#format 16 | 17 | **Do you have an idea about how this should work?** 18 | 19 | A suggested way this should work. 20 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: github-actions 4 | directory: '/' 5 | schedule: 6 | interval: weekly 7 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: Publish Packages 4 | 5 | # Controls when the action will run. Triggers the workflow on push or pull request 6 | # events but only for the master branch 7 | on: 8 | release: 9 | types: 10 | - created 11 | 12 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 13 | jobs: 14 | deploy: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Set up Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: 3.8 22 | - name: build-archives 23 | run: | 24 | python -m pip install -U pip wheel 25 | python setup.py bdist_wheel sdist 26 | - name: pypi-publish 27 | uses: pypa/gh-action-pypi-publish@v1.12.4 28 | with: 29 | user: __token__ 30 | password: ${{ secrets.PYPI_PJO_TOKEN }} 31 | -------------------------------------------------------------------------------- /.github/workflows/pythonpackage.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Build 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | workflow_dispatch: 12 | 13 | 14 | jobs: 15 | lint: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: "3.8" 23 | - name: Install dependencies 24 | run: | 25 | python -m pip install --upgrade pip 26 | pip install black==23.7.0 27 | - name: Lint with black 28 | run: | 29 | black --check --diff --extend-exclude=versioneer.py . 30 | 31 | test: 32 | 33 | needs: lint 34 | name: Test (${{ matrix.os }}, ${{ matrix.python-version }}, ${{ matrix.experimental }}) 35 | runs-on: ${{ matrix.os }}-latest 36 | continue-on-error: ${{ matrix.experimental }} 37 | strategy: 38 | fail-fast: true 39 | matrix: 40 | os: [ubuntu, macos, windows] 41 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 42 | include: 43 | - experimental: false 44 | - python-version: "3.12" 45 | experimental: true 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | - name: Set up Python ${{ matrix.python-version }} 50 | uses: actions/setup-python@v5 51 | with: 52 | python-version: ${{ matrix.python-version }} 53 | allow-prereleases: ${{ matrix.experimental }} 54 | - name: Install dependencies 55 | run: | 56 | python -m pip install --upgrade pip 57 | pip install tox tox-gh-actions 58 | - name: Test with pytest 59 | run: | 60 | tox 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Virtual environment 10 | env/ 11 | .env/ 12 | venv/ 13 | .venv/ 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib/ 24 | lib64/ 25 | parts/ 26 | sdist/ 27 | var/ 28 | *.egg-info/ 29 | .installed.cfg 30 | *.egg 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .coverage 46 | .coverage.* 47 | .cache 48 | coverage.xml 49 | *,cover 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | 58 | # Sphinx documentation 59 | docs/_build/ 60 | 61 | # PyBuilder 62 | itarget/ 63 | .idea 64 | -------------------------------------------------------------------------------- /.hound.yml: -------------------------------------------------------------------------------- 1 | 2 | flake8: 3 | enabled: true 4 | config_file: setup.cfg 5 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/psf/black-pre-commit-mirror 3 | rev: 23.7.0 4 | hooks: 5 | - id: black 6 | language_version: python3.11 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwacek/python-jsonschema-objects/d19534058154dc5a480465a03d666197668a6859/.travis.yml -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Chris Wacek 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include requirements.txt 2 | 3 | include versioneer.py 4 | include python_jsonschema_objects/_version.py 5 | 6 | include *.py 7 | include *.txt 8 | include LICENSE 9 | include tox.ini 10 | recursive-include docs *.md 11 | recursive-include docs *.py 12 | recursive-include docs *.rst 13 | recursive-include docs Makefile 14 | recursive-include test *.json 15 | recursive-include test *.py 16 | recursive-include python_jsonschema_objects/examples *.md 17 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python_jsonschema_objects/examples/README.md -------------------------------------------------------------------------------- /conftest.py: -------------------------------------------------------------------------------- 1 | import importlib.resources 2 | import importlib.resources 3 | import json 4 | 5 | import pytest 6 | 7 | import python_jsonschema_objects as pjs 8 | 9 | 10 | @pytest.fixture 11 | def markdown_examples(): 12 | if hasattr(importlib.resources, "as_file"): 13 | filehandle = importlib.resources.as_file( 14 | importlib.resources.files("python_jsonschema_objects.examples") 15 | / "README.md" 16 | ) 17 | else: 18 | filehandle = importlib.resources.path( 19 | "python_jsonschema_objects.examples", "README.md" 20 | ) 21 | with filehandle as md: 22 | examples = pjs.markdown_support.extract_code_blocks(md) 23 | 24 | return {json.loads(v)["title"]: json.loads(v) for v in examples["schema"]} 25 | 26 | 27 | @pytest.fixture(autouse=True) 28 | def inject_examples(doctest_namespace, markdown_examples): 29 | doctest_namespace["examples"] = markdown_examples 30 | 31 | 32 | @pytest.fixture 33 | def Person(markdown_examples): 34 | builder = pjs.ObjectBuilder( 35 | markdown_examples["Example Schema"], resolved=markdown_examples 36 | ) 37 | assert builder 38 | return builder.classes["ExampleSchema"] 39 | -------------------------------------------------------------------------------- /development.txt: -------------------------------------------------------------------------------- 1 | black 2 | coverage 3 | flake8 4 | isort 5 | pandoc 6 | pyandoc 7 | pytest 8 | pytest-mock 9 | recommonmark 10 | sphinx 11 | sphinx-autobuild 12 | -------------------------------------------------------------------------------- /docs/Introduction.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /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 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 21 | 22 | .PHONY: help 23 | help: 24 | @echo "Please use \`make ' where is one of" 25 | @echo " html to make standalone HTML files" 26 | @echo " dirhtml to make HTML files named index.html in directories" 27 | @echo " singlehtml to make a single large HTML file" 28 | @echo " pickle to make pickle files" 29 | @echo " json to make JSON files" 30 | @echo " htmlhelp to make HTML files and a HTML help project" 31 | @echo " qthelp to make HTML files and a qthelp project" 32 | @echo " applehelp to make an Apple Help Book" 33 | @echo " devhelp to make HTML files and a Devhelp project" 34 | @echo " epub to make an epub" 35 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 36 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 37 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 38 | @echo " text to make text files" 39 | @echo " man to make manual pages" 40 | @echo " texinfo to make Texinfo files" 41 | @echo " info to make Texinfo files and run them through makeinfo" 42 | @echo " gettext to make PO message catalogs" 43 | @echo " changes to make an overview of all changed/added/deprecated items" 44 | @echo " xml to make Docutils-native XML files" 45 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 46 | @echo " linkcheck to check all external links for integrity" 47 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 48 | @echo " coverage to run coverage check of the documentation (if enabled)" 49 | 50 | .PHONY: clean 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | .PHONY: html 55 | html: 56 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 57 | @echo 58 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 59 | 60 | .PHONY: dirhtml 61 | dirhtml: 62 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 63 | @echo 64 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 65 | 66 | .PHONY: singlehtml 67 | singlehtml: 68 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 69 | @echo 70 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 71 | 72 | .PHONY: pickle 73 | pickle: 74 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 75 | @echo 76 | @echo "Build finished; now you can process the pickle files." 77 | 78 | .PHONY: json 79 | json: 80 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 81 | @echo 82 | @echo "Build finished; now you can process the JSON files." 83 | 84 | .PHONY: htmlhelp 85 | htmlhelp: 86 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 87 | @echo 88 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 89 | ".hhp project file in $(BUILDDIR)/htmlhelp." 90 | 91 | .PHONY: qthelp 92 | qthelp: 93 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 94 | @echo 95 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 96 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 97 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/PythonJSONSchemaObjects.qhcp" 98 | @echo "To view the help file:" 99 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/PythonJSONSchemaObjects.qhc" 100 | 101 | .PHONY: applehelp 102 | applehelp: 103 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 104 | @echo 105 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 106 | @echo "N.B. You won't be able to view it unless you put it in" \ 107 | "~/Library/Documentation/Help or install it in your application" \ 108 | "bundle." 109 | 110 | .PHONY: devhelp 111 | devhelp: 112 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 113 | @echo 114 | @echo "Build finished." 115 | @echo "To view the help file:" 116 | @echo "# mkdir -p $$HOME/.local/share/devhelp/PythonJSONSchemaObjects" 117 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/PythonJSONSchemaObjects" 118 | @echo "# devhelp" 119 | 120 | .PHONY: epub 121 | epub: 122 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 123 | @echo 124 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 125 | 126 | .PHONY: latex 127 | latex: 128 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 129 | @echo 130 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 131 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 132 | "(use \`make latexpdf' here to do that automatically)." 133 | 134 | .PHONY: latexpdf 135 | latexpdf: 136 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 137 | @echo "Running LaTeX files through pdflatex..." 138 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 139 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 140 | 141 | .PHONY: latexpdfja 142 | latexpdfja: 143 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 144 | @echo "Running LaTeX files through platex and dvipdfmx..." 145 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 146 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 147 | 148 | .PHONY: text 149 | text: 150 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 151 | @echo 152 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 153 | 154 | .PHONY: man 155 | man: 156 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 157 | @echo 158 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 159 | 160 | .PHONY: texinfo 161 | texinfo: 162 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 163 | @echo 164 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 165 | @echo "Run \`make' in that directory to run these through makeinfo" \ 166 | "(use \`make info' here to do that automatically)." 167 | 168 | .PHONY: info 169 | info: 170 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 171 | @echo "Running Texinfo files through makeinfo..." 172 | make -C $(BUILDDIR)/texinfo info 173 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 174 | 175 | .PHONY: gettext 176 | gettext: 177 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 178 | @echo 179 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 180 | 181 | .PHONY: changes 182 | changes: 183 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 184 | @echo 185 | @echo "The overview file is in $(BUILDDIR)/changes." 186 | 187 | .PHONY: linkcheck 188 | linkcheck: 189 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 190 | @echo 191 | @echo "Link check complete; look for any errors in the above output " \ 192 | "or in $(BUILDDIR)/linkcheck/output.txt." 193 | 194 | .PHONY: doctest 195 | doctest: 196 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 197 | @echo "Testing of doctests in the sources finished, look at the " \ 198 | "results in $(BUILDDIR)/doctest/output.txt." 199 | 200 | .PHONY: coverage 201 | coverage: 202 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 203 | @echo "Testing of coverage in the sources finished, look at the " \ 204 | "results in $(BUILDDIR)/coverage/python.txt." 205 | 206 | .PHONY: xml 207 | xml: 208 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 209 | @echo 210 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 211 | 212 | .PHONY: pseudoxml 213 | pseudoxml: 214 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 215 | @echo 216 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 217 | -------------------------------------------------------------------------------- /docs/apidoc.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | 5 | Generated Classes 6 | ----------------- 7 | 8 | Classes generated using ``python_jsonschema_objects`` expose all defined 9 | properties as both attributes and through dictionary access. 10 | 11 | In addition, classes contain a number of utility methods for serialization, 12 | deserialization, and validation. 13 | 14 | .. autoclass:: python_jsonschema_objects.classbuilder.ProtocolBase 15 | :members: 16 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Python JSONSchema Objects documentation build configuration file, created by 4 | # sphinx-quickstart on Tue Mar 15 12:48:39 2016. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import os 16 | 17 | # If extensions (or modules to document with autodoc) are in another directory, 18 | # add these directories to sys.path here. If the directory is relative to the 19 | # documentation root, use os.path.abspath to make it absolute, like shown here. 20 | # sys.path.insert(0, os.path.abspath('.')) 21 | 22 | # -- General configuration ------------------------------------------------ 23 | 24 | # If your documentation needs a minimal Sphinx version, state it here. 25 | # needs_sphinx = '1.0' 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | "sphinx.ext.autodoc", 32 | "sphinx.ext.napoleon", 33 | "sphinx.ext.coverage", 34 | "sphinx.ext.ifconfig", 35 | "sphinx.ext.viewcode", 36 | ] 37 | 38 | # Add any paths that contain templates here, relative to this directory. 39 | templates_path = ["_templates"] 40 | 41 | from recommonmark.parser import CommonMarkParser 42 | 43 | source_parsers = {".md": CommonMarkParser} 44 | 45 | # The suffix(es) of source filenames. 46 | # You can specify multiple suffix as a list of string: 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = [".rst", ".md"] 49 | 50 | # The encoding of source files. 51 | # source_encoding = 'utf-8-sig' 52 | 53 | # The master toctree document. 54 | master_doc = "index" 55 | 56 | # General information about the project. 57 | project = "Python JSONSchema Objects" 58 | copyright = "2016, Chris Wacek" 59 | author = "Chris Wacek" 60 | 61 | # The version info for the project you're documenting, acts as replacement for 62 | # |version| and |release|, also used in various other places throughout the 63 | # built documents. 64 | # 65 | # The short X.Y version. 66 | version = "0.0.18" 67 | # The full version, including alpha/beta/rc tags. 68 | release = "0.0.18" 69 | 70 | # The language for content autogenerated by Sphinx. Refer to documentation 71 | # for a list of supported languages. 72 | # 73 | # This is also used if you do content translation via gettext catalogs. 74 | # Usually you set "language" from the command line for these cases. 75 | language = None 76 | 77 | # There are two options for replacing |today|: either, you set today to some 78 | # non-false value, then it is used: 79 | # today = '' 80 | # Else, today_fmt is used as the format for a strftime call. 81 | # today_fmt = '%B %d, %Y' 82 | 83 | # List of patterns, relative to source directory, that match files and 84 | # directories to ignore when looking for source files. 85 | exclude_patterns = ["_build"] 86 | 87 | # The reST default role (used for this markup: `text`) to use for all 88 | # documents. 89 | # default_role = None 90 | 91 | # If true, '()' will be appended to :func: etc. cross-reference text. 92 | # add_function_parentheses = True 93 | 94 | # If true, the current module name will be prepended to all description 95 | # unit titles (such as .. function::). 96 | # add_module_names = True 97 | 98 | # If true, sectionauthor and moduleauthor directives will be shown in the 99 | # output. They are ignored by default. 100 | # show_authors = False 101 | 102 | # The name of the Pygments (syntax highlighting) style to use. 103 | pygments_style = "sphinx" 104 | 105 | # A list of ignored prefixes for module index sorting. 106 | # modindex_common_prefix = [] 107 | 108 | # If true, keep warnings as "system message" paragraphs in the built documents. 109 | # keep_warnings = False 110 | 111 | # If true, `todo` and `todoList` produce output, else they produce nothing. 112 | todo_include_todos = False 113 | 114 | 115 | # -- Options for HTML output ---------------------------------------------- 116 | 117 | # The theme to use for HTML and HTML Help pages. See the documentation for 118 | # a list of builtin themes. 119 | # html_theme = 'alabaster' 120 | 121 | # Theme options are theme-specific and customize the look and feel of a theme 122 | # further. For a list of options available for each theme, see the 123 | # documentation. 124 | # html_theme_options = {} 125 | 126 | # Add any paths that contain custom themes here, relative to this directory. 127 | # html_theme_path = [] 128 | 129 | # The name for this set of Sphinx documents. If None, it defaults to 130 | # " v documentation". 131 | # html_title = None 132 | 133 | # A shorter title for the navigation bar. Default is the same as html_title. 134 | # html_short_title = None 135 | 136 | # The name of an image file (relative to this directory) to place at the top 137 | # of the sidebar. 138 | # html_logo = None 139 | 140 | # The name of an image file (relative to this directory) to use as a favicon of 141 | # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 142 | # pixels large. 143 | # html_favicon = None 144 | 145 | # Add any paths that contain custom static files (such as style sheets) here, 146 | # relative to this directory. They are copied after the builtin static files, 147 | # so a file named "default.css" will overwrite the builtin "default.css". 148 | html_static_path = ["_static"] 149 | 150 | # Add any extra paths that contain custom files (such as robots.txt or 151 | # .htaccess) here, relative to this directory. These files are copied 152 | # directly to the root of the documentation. 153 | # html_extra_path = [] 154 | 155 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 156 | # using the given strftime format. 157 | # html_last_updated_fmt = '%b %d, %Y' 158 | 159 | # If true, SmartyPants will be used to convert quotes and dashes to 160 | # typographically correct entities. 161 | # html_use_smartypants = True 162 | 163 | # Custom sidebar templates, maps document names to template names. 164 | # html_sidebars = {} 165 | 166 | # Additional templates that should be rendered to pages, maps page names to 167 | # template names. 168 | # html_additional_pages = {} 169 | 170 | # If false, no module index is generated. 171 | # html_domain_indices = True 172 | 173 | # If false, no index is generated. 174 | # html_use_index = True 175 | 176 | # If true, the index is split into individual pages for each letter. 177 | # html_split_index = False 178 | 179 | on_rtd = os.environ.get("READTHEDOCS", None) == "True" 180 | 181 | if not on_rtd: # only import and set the theme if we're building docs locally 182 | import sphinx_rtd_theme 183 | 184 | html_theme = "sphinx_rtd_theme" 185 | html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] 186 | 187 | # If true, links to the reST sources are added to the pages. 188 | # html_show_sourcelink = True 189 | 190 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 191 | # html_show_sphinx = True 192 | 193 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 194 | # html_show_copyright = True 195 | 196 | # If true, an OpenSearch description file will be output, and all pages will 197 | # contain a tag referring to it. The value of this option must be the 198 | # base URL from which the finished HTML is served. 199 | # html_use_opensearch = '' 200 | 201 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 202 | # html_file_suffix = None 203 | 204 | # Language to be used for generating the HTML full-text search index. 205 | # Sphinx supports the following languages: 206 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 207 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 208 | # html_search_language = 'en' 209 | 210 | # A dictionary with options for the search language support, empty by default. 211 | # Now only 'ja' uses this config value 212 | # html_search_options = {'type': 'default'} 213 | 214 | # The name of a javascript file (relative to the configuration directory) that 215 | # implements a search results scorer. If empty, the default will be used. 216 | # html_search_scorer = 'scorer.js' 217 | 218 | # Output file base name for HTML help builder. 219 | htmlhelp_basename = "PythonJSONSchemaObjectsdoc" 220 | 221 | # -- Options for LaTeX output --------------------------------------------- 222 | 223 | latex_elements = { 224 | # The paper size ('letterpaper' or 'a4paper'). 225 | # 'papersize': 'letterpaper', 226 | # The font size ('10pt', '11pt' or '12pt'). 227 | # 'pointsize': '10pt', 228 | # Additional stuff for the LaTeX preamble. 229 | # 'preamble': '', 230 | # Latex figure (float) alignment 231 | # 'figure_align': 'htbp', 232 | } 233 | 234 | # Grouping the document tree into LaTeX files. List of tuples 235 | # (source start file, target name, title, 236 | # author, documentclass [howto, manual, or own class]). 237 | latex_documents = [ 238 | ( 239 | master_doc, 240 | "PythonJSONSchemaObjects.tex", 241 | "Python JSONSchema Objects Documentation", 242 | "Chris Wacek", 243 | "manual", 244 | ) 245 | ] 246 | 247 | # The name of an image file (relative to this directory) to place at the top of 248 | # the title page. 249 | # latex_logo = None 250 | 251 | # For "manual" documents, if this is true, then toplevel headings are parts, 252 | # not chapters. 253 | # latex_use_parts = False 254 | 255 | # If true, show page references after internal links. 256 | # latex_show_pagerefs = False 257 | 258 | # If true, show URL addresses after external links. 259 | # latex_show_urls = False 260 | 261 | # Documents to append as an appendix to all manuals. 262 | # latex_appendices = [] 263 | 264 | # If false, no module index is generated. 265 | # latex_domain_indices = True 266 | 267 | 268 | # -- Options for manual page output --------------------------------------- 269 | 270 | # One entry per manual page. List of tuples 271 | # (source start file, name, description, authors, manual section). 272 | man_pages = [ 273 | ( 274 | master_doc, 275 | "pythonjsonschemaobjects", 276 | "Python JSONSchema Objects Documentation", 277 | [author], 278 | 1, 279 | ) 280 | ] 281 | 282 | # If true, show URL addresses after external links. 283 | # man_show_urls = False 284 | 285 | 286 | # -- Options for Texinfo output ------------------------------------------- 287 | 288 | # Grouping the document tree into Texinfo files. List of tuples 289 | # (source start file, target name, title, author, 290 | # dir menu entry, description, category) 291 | texinfo_documents = [ 292 | ( 293 | master_doc, 294 | "PythonJSONSchemaObjects", 295 | "Python JSONSchema Objects Documentation", 296 | author, 297 | "PythonJSONSchemaObjects", 298 | "One line description of project.", 299 | "Miscellaneous", 300 | ) 301 | ] 302 | 303 | # Documents to append as an appendix to all manuals. 304 | # texinfo_appendices = [] 305 | 306 | # If false, no module index is generated. 307 | # texinfo_domain_indices = True 308 | 309 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 310 | # texinfo_show_urls = 'footnote' 311 | 312 | # If true, do not generate a @detailmenu in the "Top" node's menu. 313 | # texinfo_no_detailmenu = False 314 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. Python JSONSchema Objects documentation master file, created by 2 | sphinx-quickstart on Tue Mar 15 12:48:39 2016. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Python JSONSchema Objects's documentation! 7 | ===================================================== 8 | 9 | python-jsonschema-objects provides an *automatic* class-based 10 | binding to JSON schemas for use in python. 11 | 12 | .. toctree:: 13 | :includehidden: 14 | 15 | Introduction 16 | 17 | apidoc 18 | 19 | -------------------------------------------------------------------------------- /python_jsonschema_objects/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ["ObjectBuilder", "markdown_support", "ValidationError"] 2 | 3 | import codecs 4 | import copy 5 | import json 6 | import logging 7 | import os.path 8 | import warnings 9 | from typing import Optional 10 | import typing 11 | 12 | import inflection 13 | import jsonschema 14 | import referencing.jsonschema 15 | import referencing.retrieval 16 | import referencing._core 17 | from referencing import Registry, Resource 18 | 19 | import python_jsonschema_objects.classbuilder as classbuilder 20 | import python_jsonschema_objects.markdown_support 21 | import python_jsonschema_objects.util 22 | from python_jsonschema_objects.validators import ValidationError 23 | 24 | logger = logging.getLogger(__name__) 25 | 26 | __all__ = ["ObjectBuilder", "markdown_support", "ValidationError"] 27 | 28 | FILE = __file__ 29 | 30 | SUPPORTED_VERSIONS = ( 31 | "http://json-schema.org/draft-03/schema", 32 | "http://json-schema.org/draft-04/schema", 33 | ) 34 | 35 | 36 | class ObjectBuilder(object): 37 | def __init__( 38 | self, 39 | schema_uri: typing.Union[typing.AnyStr, typing.Mapping], 40 | resolved: typing.Dict[typing.AnyStr, typing.Mapping] = {}, 41 | registry: Optional[referencing.Registry] = None, 42 | resolver: Optional[referencing.typing.Retrieve] = None, 43 | specification_uri: Optional[str] = None, 44 | ): 45 | if isinstance(schema_uri, str): 46 | uri = os.path.normpath(schema_uri) 47 | self.basedir = os.path.dirname(uri) 48 | with codecs.open(uri, "r", "utf-8") as fin: 49 | self.schema = json.loads(fin.read()) 50 | else: 51 | self.schema = schema_uri 52 | uri = os.path.normpath(FILE) 53 | self.basedir = os.path.dirname(uri) 54 | 55 | if ( 56 | "$schema" in self.schema 57 | and self.schema["$schema"].rstrip("#") not in SUPPORTED_VERSIONS 58 | ): 59 | warnings.warn( 60 | "Schema version {} not recognized. Some " 61 | "keywords and features may not be supported.".format( 62 | self.schema["$schema"] 63 | ) 64 | ) 65 | 66 | if registry is not None: 67 | if not isinstance(registry, referencing.Registry): 68 | raise TypeError("registry must be a Registry instance") 69 | 70 | if resolver is not None: 71 | raise AttributeError( 72 | "Cannot specify both registry and resolver. If you provide your own registry, pass the resolver " 73 | "directly to that" 74 | ) 75 | self.registry = registry 76 | else: 77 | if resolver is not None: 78 | 79 | def file_and_memory_handler(uri): 80 | if uri.startswith("file:"): 81 | return Resource.from_contents(self.relative_file_resolver(uri)) 82 | return resolver(uri) 83 | 84 | self.registry = Registry(retrieve=file_and_memory_handler) 85 | else: 86 | 87 | def file_and_memory_handler(uri): 88 | if uri.startswith("file:"): 89 | return Resource.from_contents(self.relative_file_resolver(uri)) 90 | raise RuntimeError( 91 | "No remote resource resolver provided. Cannot resolve {}".format( 92 | uri 93 | ) 94 | ) 95 | 96 | self.registry = Registry(retrieve=file_and_memory_handler) 97 | 98 | if "$schema" not in self.schema: 99 | warnings.warn( 100 | "Schema version not specified. Defaulting to {}".format( 101 | specification_uri or "http://json-schema.org/draft-04/schema" 102 | ) 103 | ) 104 | updated = { 105 | "$schema": specification_uri or "http://json-schema.org/draft-04/schema" 106 | } 107 | updated.update(self.schema) 108 | self.schema = updated 109 | 110 | schema = Resource.from_contents(self.schema) 111 | if schema.id() is None: 112 | warnings.warn("Schema id not specified. Defaulting to 'self'") 113 | updated = {"$id": "self", "id": "self"} 114 | updated.update(self.schema) 115 | self.schema = updated 116 | schema = Resource.from_contents(self.schema) 117 | 118 | self.registry = self.registry.with_resource("", schema) 119 | 120 | if len(resolved) > 0: 121 | warnings.warn( 122 | "Use of 'memory:' URIs is deprecated. Provide a registry with properly resolved references " 123 | "if you want to resolve items externally.", 124 | DeprecationWarning, 125 | ) 126 | for uri, contents in resolved.items(): 127 | from referencing.jsonschema import specification_with 128 | 129 | specification = specification_with( 130 | specification_uri or self.schema["$schema"] 131 | ) 132 | self.registry = self.registry.with_resource( 133 | "memory:" + uri, 134 | referencing.Resource.from_contents(contents, specification), 135 | ) 136 | 137 | validatorClass = jsonschema.validators.validator_for( 138 | {"$schema": specification_uri or self.schema["$schema"]} 139 | ) 140 | 141 | meta_validator = validatorClass( 142 | validatorClass.META_SCHEMA, registry=self.registry 143 | ) 144 | meta_validator.validate(self.schema) 145 | self.validator = validatorClass(self.schema, registry=self.registry) 146 | 147 | self._classes = None 148 | self._resolved = None 149 | 150 | @property 151 | def resolver(self) -> referencing._core.Resolver: 152 | return self.registry.resolver() 153 | 154 | @property 155 | def schema(self): 156 | try: 157 | return copy.deepcopy(self._schema) 158 | except AttributeError: 159 | raise ValidationError("No schema provided") 160 | 161 | @schema.setter 162 | def schema(self, val): 163 | setattr(self, "_schema", val) 164 | 165 | @property 166 | def classes(self): 167 | if self._classes is None: 168 | self._classes = self.build_classes() 169 | return self._classes 170 | 171 | def get_class(self, uri): 172 | if self._resolved is None: 173 | self._classes = self.build_classes() 174 | return self._resolved.get(uri, None) 175 | 176 | def relative_file_resolver(self, uri): 177 | path = os.path.join(self.basedir, uri[8:]) 178 | with codecs.open(path, "r", "utf-8") as fin: 179 | result = json.loads(fin.read()) 180 | return result 181 | 182 | def validate(self, obj): 183 | try: 184 | return self.validator.validate(obj) 185 | except jsonschema.ValidationError as e: 186 | raise ValidationError(e) 187 | 188 | def build_classes( 189 | self, 190 | strict=False, 191 | named_only=False, 192 | standardize_names=True, 193 | any_of: typing.Optional[typing.Literal["use-first"]] = None, 194 | ): 195 | """ 196 | Build all of the classes named in the JSONSchema. 197 | 198 | Class names will be transformed using inflection by default, so names 199 | with spaces in the schema will be camelcased, while names without 200 | spaces will have internal capitalization dropped. Thus "Home Address" 201 | becomes "HomeAddress", while "HomeAddress" becomes "Homeaddress". To 202 | disable this behavior, pass standardize_names=False, but be aware that 203 | accessing names with spaces from the namespace can be problematic. 204 | 205 | Args: 206 | strict: (bool) use this to validate required fields while creating the class 207 | named_only: (bool) If true, only properties with an actual title attribute 208 | will be included in the resulting namespace (although all will be 209 | generated). 210 | standardize_names: (bool) If true (the default), class names will be 211 | transformed by camel casing 212 | any_of: (literal) If not set to None, defines the way anyOf clauses are resolved: 213 | - 'use-first': Generate to the first matching schema in the list under the anyOf 214 | - None: default behavior, anyOf is not supported in the schema 215 | 216 | Returns: 217 | A namespace containing all the generated classes 218 | 219 | """ 220 | opts = {"strict": strict, "any_of": any_of} 221 | builder = classbuilder.ClassBuilder(self.resolver, opts) 222 | for nm, defn in self.schema.get("definitions", {}).items(): 223 | resolved = self.resolver.lookup("#/definitions/" + nm) 224 | uri = python_jsonschema_objects.util.resolve_ref_uri( 225 | self.resolver._base_uri, "#/definitions/" + nm 226 | ) 227 | builder.construct(uri, resolved.contents) 228 | 229 | if standardize_names: 230 | name_transform = lambda t: inflection.camelize( 231 | inflection.parameterize(str(t), "_") 232 | ) 233 | else: 234 | name_transform = lambda t: t 235 | 236 | nm = self.schema["title"] if "title" in self.schema else self.schema["$id"] 237 | nm = inflection.parameterize(str(nm), "_") 238 | 239 | builder.construct(nm, self.schema) 240 | self._resolved = builder.resolved 241 | 242 | classes = {} 243 | for uri, klass in builder.resolved.items(): 244 | title = getattr(klass, "__title__", None) 245 | if title is not None: 246 | classes[name_transform(title)] = klass 247 | elif not named_only: 248 | classes[name_transform(uri.split("/")[-1])] = klass 249 | 250 | return python_jsonschema_objects.util.Namespace.from_mapping(classes) 251 | 252 | 253 | if __name__ == "__main__": 254 | validator = ObjectBuilder("../../protocol/json/schema.json") 255 | 256 | from . import _version 257 | 258 | __version__ = _version.get_versions()["version"] 259 | -------------------------------------------------------------------------------- /python_jsonschema_objects/_version.py: -------------------------------------------------------------------------------- 1 | # This file helps to compute a version number in source trees obtained from 2 | # git-archive tarball (such as those provided by githubs download-from-tag 3 | # feature). Distribution tarballs (built by setup.py sdist) and build 4 | # directories (produced by setup.py build) will contain a much shorter file 5 | # that just contains the computed version number. 6 | 7 | # This file is released into the public domain. 8 | # Generated by versioneer-0.29 9 | # https://github.com/python-versioneer/python-versioneer 10 | 11 | """Git implementation of _version.py.""" 12 | 13 | import errno 14 | import functools 15 | import os 16 | import re 17 | import subprocess 18 | import sys 19 | from typing import Any, Callable, Dict, List, Optional, Tuple 20 | 21 | 22 | def get_keywords() -> Dict[str, str]: 23 | """Get the keywords needed to look up the version information.""" 24 | # these strings will be replaced by git during git-archive. 25 | # setup.py/versioneer.py will grep for the variable names, so they must 26 | # each be defined on a line of their own. _version.py will just call 27 | # get_keywords(). 28 | git_refnames = " (HEAD -> master)" 29 | git_full = "d19534058154dc5a480465a03d666197668a6859" 30 | git_date = "2025-02-17 22:33:46 -0500" 31 | keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} 32 | return keywords 33 | 34 | 35 | class VersioneerConfig: 36 | """Container for Versioneer configuration parameters.""" 37 | 38 | VCS: str 39 | style: str 40 | tag_prefix: str 41 | parentdir_prefix: str 42 | versionfile_source: str 43 | verbose: bool 44 | 45 | 46 | def get_config() -> VersioneerConfig: 47 | """Create, populate and return the VersioneerConfig() object.""" 48 | # these strings are filled in when 'setup.py versioneer' creates 49 | # _version.py 50 | cfg = VersioneerConfig() 51 | cfg.VCS = "git" 52 | cfg.style = "pep440" 53 | cfg.tag_prefix = "" 54 | cfg.parentdir_prefix = "None" 55 | cfg.versionfile_source = "python_jsonschema_objects/_version.py" 56 | cfg.verbose = False 57 | return cfg 58 | 59 | 60 | class NotThisMethod(Exception): 61 | """Exception raised if a method is not valid for the current scenario.""" 62 | 63 | 64 | LONG_VERSION_PY: Dict[str, str] = {} 65 | HANDLERS: Dict[str, Dict[str, Callable]] = {} 66 | 67 | 68 | def register_vcs_handler(vcs: str, method: str) -> Callable: # decorator 69 | """Create decorator to mark a method as the handler of a VCS.""" 70 | 71 | def decorate(f: Callable) -> Callable: 72 | """Store f in HANDLERS[vcs][method].""" 73 | if vcs not in HANDLERS: 74 | HANDLERS[vcs] = {} 75 | HANDLERS[vcs][method] = f 76 | return f 77 | 78 | return decorate 79 | 80 | 81 | def run_command( 82 | commands: List[str], 83 | args: List[str], 84 | cwd: Optional[str] = None, 85 | verbose: bool = False, 86 | hide_stderr: bool = False, 87 | env: Optional[Dict[str, str]] = None, 88 | ) -> Tuple[Optional[str], Optional[int]]: 89 | """Call the given command(s).""" 90 | assert isinstance(commands, list) 91 | process = None 92 | 93 | popen_kwargs: Dict[str, Any] = {} 94 | if sys.platform == "win32": 95 | # This hides the console window if pythonw.exe is used 96 | startupinfo = subprocess.STARTUPINFO() 97 | startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW 98 | popen_kwargs["startupinfo"] = startupinfo 99 | 100 | for command in commands: 101 | try: 102 | dispcmd = str([command] + args) 103 | # remember shell=False, so use git.cmd on windows, not just git 104 | process = subprocess.Popen( 105 | [command] + args, 106 | cwd=cwd, 107 | env=env, 108 | stdout=subprocess.PIPE, 109 | stderr=(subprocess.PIPE if hide_stderr else None), 110 | **popen_kwargs, 111 | ) 112 | break 113 | except OSError as e: 114 | if e.errno == errno.ENOENT: 115 | continue 116 | if verbose: 117 | print("unable to run %s" % dispcmd) 118 | print(e) 119 | return None, None 120 | else: 121 | if verbose: 122 | print("unable to find command, tried %s" % (commands,)) 123 | return None, None 124 | stdout = process.communicate()[0].strip().decode() 125 | if process.returncode != 0: 126 | if verbose: 127 | print("unable to run %s (error)" % dispcmd) 128 | print("stdout was %s" % stdout) 129 | return None, process.returncode 130 | return stdout, process.returncode 131 | 132 | 133 | def versions_from_parentdir( 134 | parentdir_prefix: str, 135 | root: str, 136 | verbose: bool, 137 | ) -> Dict[str, Any]: 138 | """Try to determine the version from the parent directory name. 139 | 140 | Source tarballs conventionally unpack into a directory that includes both 141 | the project name and a version string. We will also support searching up 142 | two directory levels for an appropriately named parent directory 143 | """ 144 | rootdirs = [] 145 | 146 | for _ in range(3): 147 | dirname = os.path.basename(root) 148 | if dirname.startswith(parentdir_prefix): 149 | return { 150 | "version": dirname[len(parentdir_prefix) :], 151 | "full-revisionid": None, 152 | "dirty": False, 153 | "error": None, 154 | "date": None, 155 | } 156 | rootdirs.append(root) 157 | root = os.path.dirname(root) # up a level 158 | 159 | if verbose: 160 | print( 161 | "Tried directories %s but none started with prefix %s" 162 | % (str(rootdirs), parentdir_prefix) 163 | ) 164 | raise NotThisMethod("rootdir doesn't start with parentdir_prefix") 165 | 166 | 167 | @register_vcs_handler("git", "get_keywords") 168 | def git_get_keywords(versionfile_abs: str) -> Dict[str, str]: 169 | """Extract version information from the given file.""" 170 | # the code embedded in _version.py can just fetch the value of these 171 | # keywords. When used from setup.py, we don't want to import _version.py, 172 | # so we do it with a regexp instead. This function is not used from 173 | # _version.py. 174 | keywords: Dict[str, str] = {} 175 | try: 176 | with open(versionfile_abs, "r") as fobj: 177 | for line in fobj: 178 | if line.strip().startswith("git_refnames ="): 179 | mo = re.search(r'=\s*"(.*)"', line) 180 | if mo: 181 | keywords["refnames"] = mo.group(1) 182 | if line.strip().startswith("git_full ="): 183 | mo = re.search(r'=\s*"(.*)"', line) 184 | if mo: 185 | keywords["full"] = mo.group(1) 186 | if line.strip().startswith("git_date ="): 187 | mo = re.search(r'=\s*"(.*)"', line) 188 | if mo: 189 | keywords["date"] = mo.group(1) 190 | except OSError: 191 | pass 192 | return keywords 193 | 194 | 195 | @register_vcs_handler("git", "keywords") 196 | def git_versions_from_keywords( 197 | keywords: Dict[str, str], 198 | tag_prefix: str, 199 | verbose: bool, 200 | ) -> Dict[str, Any]: 201 | """Get version information from git keywords.""" 202 | if "refnames" not in keywords: 203 | raise NotThisMethod("Short version file found") 204 | date = keywords.get("date") 205 | if date is not None: 206 | # Use only the last line. Previous lines may contain GPG signature 207 | # information. 208 | date = date.splitlines()[-1] 209 | 210 | # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant 211 | # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 212 | # -like" string, which we must then edit to make compliant), because 213 | # it's been around since git-1.5.3, and it's too difficult to 214 | # discover which version we're using, or to work around using an 215 | # older one. 216 | date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 217 | refnames = keywords["refnames"].strip() 218 | if refnames.startswith("$Format"): 219 | if verbose: 220 | print("keywords are unexpanded, not using") 221 | raise NotThisMethod("unexpanded keywords, not a git-archive tarball") 222 | refs = {r.strip() for r in refnames.strip("()").split(",")} 223 | # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of 224 | # just "foo-1.0". If we see a "tag: " prefix, prefer those. 225 | TAG = "tag: " 226 | tags = {r[len(TAG) :] for r in refs if r.startswith(TAG)} 227 | if not tags: 228 | # Either we're using git < 1.8.3, or there really are no tags. We use 229 | # a heuristic: assume all version tags have a digit. The old git %d 230 | # expansion behaves like git log --decorate=short and strips out the 231 | # refs/heads/ and refs/tags/ prefixes that would let us distinguish 232 | # between branches and tags. By ignoring refnames without digits, we 233 | # filter out many common branch names like "release" and 234 | # "stabilization", as well as "HEAD" and "master". 235 | tags = {r for r in refs if re.search(r"\d", r)} 236 | if verbose: 237 | print("discarding '%s', no digits" % ",".join(refs - tags)) 238 | if verbose: 239 | print("likely tags: %s" % ",".join(sorted(tags))) 240 | for ref in sorted(tags): 241 | # sorting will prefer e.g. "2.0" over "2.0rc1" 242 | if ref.startswith(tag_prefix): 243 | r = ref[len(tag_prefix) :] 244 | # Filter out refs that exactly match prefix or that don't start 245 | # with a number once the prefix is stripped (mostly a concern 246 | # when prefix is '') 247 | if not re.match(r"\d", r): 248 | continue 249 | if verbose: 250 | print("picking %s" % r) 251 | return { 252 | "version": r, 253 | "full-revisionid": keywords["full"].strip(), 254 | "dirty": False, 255 | "error": None, 256 | "date": date, 257 | } 258 | # no suitable tags, so version is "0+unknown", but full hex is still there 259 | if verbose: 260 | print("no suitable tags, using unknown + full revision id") 261 | return { 262 | "version": "0+unknown", 263 | "full-revisionid": keywords["full"].strip(), 264 | "dirty": False, 265 | "error": "no suitable tags", 266 | "date": None, 267 | } 268 | 269 | 270 | @register_vcs_handler("git", "pieces_from_vcs") 271 | def git_pieces_from_vcs( 272 | tag_prefix: str, root: str, verbose: bool, runner: Callable = run_command 273 | ) -> Dict[str, Any]: 274 | """Get version from 'git describe' in the root of the source tree. 275 | 276 | This only gets called if the git-archive 'subst' keywords were *not* 277 | expanded, and _version.py hasn't already been rewritten with a short 278 | version string, meaning we're inside a checked out source tree. 279 | """ 280 | GITS = ["git"] 281 | if sys.platform == "win32": 282 | GITS = ["git.cmd", "git.exe"] 283 | 284 | # GIT_DIR can interfere with correct operation of Versioneer. 285 | # It may be intended to be passed to the Versioneer-versioned project, 286 | # but that should not change where we get our version from. 287 | env = os.environ.copy() 288 | env.pop("GIT_DIR", None) 289 | runner = functools.partial(runner, env=env) 290 | 291 | _, rc = runner(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=not verbose) 292 | if rc != 0: 293 | if verbose: 294 | print("Directory %s not under git control" % root) 295 | raise NotThisMethod("'git rev-parse --git-dir' returned error") 296 | 297 | # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] 298 | # if there isn't one, this yields HEX[-dirty] (no NUM) 299 | describe_out, rc = runner( 300 | GITS, 301 | [ 302 | "describe", 303 | "--tags", 304 | "--dirty", 305 | "--always", 306 | "--long", 307 | "--match", 308 | f"{tag_prefix}[[:digit:]]*", 309 | ], 310 | cwd=root, 311 | ) 312 | # --long was added in git-1.5.5 313 | if describe_out is None: 314 | raise NotThisMethod("'git describe' failed") 315 | describe_out = describe_out.strip() 316 | full_out, rc = runner(GITS, ["rev-parse", "HEAD"], cwd=root) 317 | if full_out is None: 318 | raise NotThisMethod("'git rev-parse' failed") 319 | full_out = full_out.strip() 320 | 321 | pieces: Dict[str, Any] = {} 322 | pieces["long"] = full_out 323 | pieces["short"] = full_out[:7] # maybe improved later 324 | pieces["error"] = None 325 | 326 | branch_name, rc = runner(GITS, ["rev-parse", "--abbrev-ref", "HEAD"], cwd=root) 327 | # --abbrev-ref was added in git-1.6.3 328 | if rc != 0 or branch_name is None: 329 | raise NotThisMethod("'git rev-parse --abbrev-ref' returned error") 330 | branch_name = branch_name.strip() 331 | 332 | if branch_name == "HEAD": 333 | # If we aren't exactly on a branch, pick a branch which represents 334 | # the current commit. If all else fails, we are on a branchless 335 | # commit. 336 | branches, rc = runner(GITS, ["branch", "--contains"], cwd=root) 337 | # --contains was added in git-1.5.4 338 | if rc != 0 or branches is None: 339 | raise NotThisMethod("'git branch --contains' returned error") 340 | branches = branches.split("\n") 341 | 342 | # Remove the first line if we're running detached 343 | if "(" in branches[0]: 344 | branches.pop(0) 345 | 346 | # Strip off the leading "* " from the list of branches. 347 | branches = [branch[2:] for branch in branches] 348 | if "master" in branches: 349 | branch_name = "master" 350 | elif not branches: 351 | branch_name = None 352 | else: 353 | # Pick the first branch that is returned. Good or bad. 354 | branch_name = branches[0] 355 | 356 | pieces["branch"] = branch_name 357 | 358 | # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] 359 | # TAG might have hyphens. 360 | git_describe = describe_out 361 | 362 | # look for -dirty suffix 363 | dirty = git_describe.endswith("-dirty") 364 | pieces["dirty"] = dirty 365 | if dirty: 366 | git_describe = git_describe[: git_describe.rindex("-dirty")] 367 | 368 | # now we have TAG-NUM-gHEX or HEX 369 | 370 | if "-" in git_describe: 371 | # TAG-NUM-gHEX 372 | mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) 373 | if not mo: 374 | # unparsable. Maybe git-describe is misbehaving? 375 | pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out 376 | return pieces 377 | 378 | # tag 379 | full_tag = mo.group(1) 380 | if not full_tag.startswith(tag_prefix): 381 | if verbose: 382 | fmt = "tag '%s' doesn't start with prefix '%s'" 383 | print(fmt % (full_tag, tag_prefix)) 384 | pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( 385 | full_tag, 386 | tag_prefix, 387 | ) 388 | return pieces 389 | pieces["closest-tag"] = full_tag[len(tag_prefix) :] 390 | 391 | # distance: number of commits since tag 392 | pieces["distance"] = int(mo.group(2)) 393 | 394 | # commit: short hex revision ID 395 | pieces["short"] = mo.group(3) 396 | 397 | else: 398 | # HEX: no tags 399 | pieces["closest-tag"] = None 400 | out, rc = runner(GITS, ["rev-list", "HEAD", "--left-right"], cwd=root) 401 | pieces["distance"] = len(out.split()) # total number of commits 402 | 403 | # commit date: see ISO-8601 comment in git_versions_from_keywords() 404 | date = runner(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[0].strip() 405 | # Use only the last line. Previous lines may contain GPG signature 406 | # information. 407 | date = date.splitlines()[-1] 408 | pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) 409 | 410 | return pieces 411 | 412 | 413 | def plus_or_dot(pieces: Dict[str, Any]) -> str: 414 | """Return a + if we don't already have one, else return a .""" 415 | if "+" in pieces.get("closest-tag", ""): 416 | return "." 417 | return "+" 418 | 419 | 420 | def render_pep440(pieces: Dict[str, Any]) -> str: 421 | """Build up version string, with post-release "local version identifier". 422 | 423 | Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you 424 | get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty 425 | 426 | Exceptions: 427 | 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] 428 | """ 429 | if pieces["closest-tag"]: 430 | rendered = pieces["closest-tag"] 431 | if pieces["distance"] or pieces["dirty"]: 432 | rendered += plus_or_dot(pieces) 433 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 434 | if pieces["dirty"]: 435 | rendered += ".dirty" 436 | else: 437 | # exception #1 438 | rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 439 | if pieces["dirty"]: 440 | rendered += ".dirty" 441 | return rendered 442 | 443 | 444 | def render_pep440_branch(pieces: Dict[str, Any]) -> str: 445 | """TAG[[.dev0]+DISTANCE.gHEX[.dirty]] . 446 | 447 | The ".dev0" means not master branch. Note that .dev0 sorts backwards 448 | (a feature branch will appear "older" than the master branch). 449 | 450 | Exceptions: 451 | 1: no tags. 0[.dev0]+untagged.DISTANCE.gHEX[.dirty] 452 | """ 453 | if pieces["closest-tag"]: 454 | rendered = pieces["closest-tag"] 455 | if pieces["distance"] or pieces["dirty"]: 456 | if pieces["branch"] != "master": 457 | rendered += ".dev0" 458 | rendered += plus_or_dot(pieces) 459 | rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) 460 | if pieces["dirty"]: 461 | rendered += ".dirty" 462 | else: 463 | # exception #1 464 | rendered = "0" 465 | if pieces["branch"] != "master": 466 | rendered += ".dev0" 467 | rendered += "+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) 468 | if pieces["dirty"]: 469 | rendered += ".dirty" 470 | return rendered 471 | 472 | 473 | def pep440_split_post(ver: str) -> Tuple[str, Optional[int]]: 474 | """Split pep440 version string at the post-release segment. 475 | 476 | Returns the release segments before the post-release and the 477 | post-release version number (or -1 if no post-release segment is present). 478 | """ 479 | vc = str.split(ver, ".post") 480 | return vc[0], int(vc[1] or 0) if len(vc) == 2 else None 481 | 482 | 483 | def render_pep440_pre(pieces: Dict[str, Any]) -> str: 484 | """TAG[.postN.devDISTANCE] -- No -dirty. 485 | 486 | Exceptions: 487 | 1: no tags. 0.post0.devDISTANCE 488 | """ 489 | if pieces["closest-tag"]: 490 | if pieces["distance"]: 491 | # update the post release segment 492 | tag_version, post_version = pep440_split_post(pieces["closest-tag"]) 493 | rendered = tag_version 494 | if post_version is not None: 495 | rendered += ".post%d.dev%d" % (post_version + 1, pieces["distance"]) 496 | else: 497 | rendered += ".post0.dev%d" % (pieces["distance"]) 498 | else: 499 | # no commits, use the tag as the version 500 | rendered = pieces["closest-tag"] 501 | else: 502 | # exception #1 503 | rendered = "0.post0.dev%d" % pieces["distance"] 504 | return rendered 505 | 506 | 507 | def render_pep440_post(pieces: Dict[str, Any]) -> str: 508 | """TAG[.postDISTANCE[.dev0]+gHEX] . 509 | 510 | The ".dev0" means dirty. Note that .dev0 sorts backwards 511 | (a dirty tree will appear "older" than the corresponding clean one), 512 | but you shouldn't be releasing software with -dirty anyways. 513 | 514 | Exceptions: 515 | 1: no tags. 0.postDISTANCE[.dev0] 516 | """ 517 | if pieces["closest-tag"]: 518 | rendered = pieces["closest-tag"] 519 | if pieces["distance"] or pieces["dirty"]: 520 | rendered += ".post%d" % pieces["distance"] 521 | if pieces["dirty"]: 522 | rendered += ".dev0" 523 | rendered += plus_or_dot(pieces) 524 | rendered += "g%s" % pieces["short"] 525 | else: 526 | # exception #1 527 | rendered = "0.post%d" % pieces["distance"] 528 | if pieces["dirty"]: 529 | rendered += ".dev0" 530 | rendered += "+g%s" % pieces["short"] 531 | return rendered 532 | 533 | 534 | def render_pep440_post_branch(pieces: Dict[str, Any]) -> str: 535 | """TAG[.postDISTANCE[.dev0]+gHEX[.dirty]] . 536 | 537 | The ".dev0" means not master branch. 538 | 539 | Exceptions: 540 | 1: no tags. 0.postDISTANCE[.dev0]+gHEX[.dirty] 541 | """ 542 | if pieces["closest-tag"]: 543 | rendered = pieces["closest-tag"] 544 | if pieces["distance"] or pieces["dirty"]: 545 | rendered += ".post%d" % pieces["distance"] 546 | if pieces["branch"] != "master": 547 | rendered += ".dev0" 548 | rendered += plus_or_dot(pieces) 549 | rendered += "g%s" % pieces["short"] 550 | if pieces["dirty"]: 551 | rendered += ".dirty" 552 | else: 553 | # exception #1 554 | rendered = "0.post%d" % pieces["distance"] 555 | if pieces["branch"] != "master": 556 | rendered += ".dev0" 557 | rendered += "+g%s" % pieces["short"] 558 | if pieces["dirty"]: 559 | rendered += ".dirty" 560 | return rendered 561 | 562 | 563 | def render_pep440_old(pieces: Dict[str, Any]) -> str: 564 | """TAG[.postDISTANCE[.dev0]] . 565 | 566 | The ".dev0" means dirty. 567 | 568 | Exceptions: 569 | 1: no tags. 0.postDISTANCE[.dev0] 570 | """ 571 | if pieces["closest-tag"]: 572 | rendered = pieces["closest-tag"] 573 | if pieces["distance"] or pieces["dirty"]: 574 | rendered += ".post%d" % pieces["distance"] 575 | if pieces["dirty"]: 576 | rendered += ".dev0" 577 | else: 578 | # exception #1 579 | rendered = "0.post%d" % pieces["distance"] 580 | if pieces["dirty"]: 581 | rendered += ".dev0" 582 | return rendered 583 | 584 | 585 | def render_git_describe(pieces: Dict[str, Any]) -> str: 586 | """TAG[-DISTANCE-gHEX][-dirty]. 587 | 588 | Like 'git describe --tags --dirty --always'. 589 | 590 | Exceptions: 591 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 592 | """ 593 | if pieces["closest-tag"]: 594 | rendered = pieces["closest-tag"] 595 | if pieces["distance"]: 596 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 597 | else: 598 | # exception #1 599 | rendered = pieces["short"] 600 | if pieces["dirty"]: 601 | rendered += "-dirty" 602 | return rendered 603 | 604 | 605 | def render_git_describe_long(pieces: Dict[str, Any]) -> str: 606 | """TAG-DISTANCE-gHEX[-dirty]. 607 | 608 | Like 'git describe --tags --dirty --always -long'. 609 | The distance/hash is unconditional. 610 | 611 | Exceptions: 612 | 1: no tags. HEX[-dirty] (note: no 'g' prefix) 613 | """ 614 | if pieces["closest-tag"]: 615 | rendered = pieces["closest-tag"] 616 | rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) 617 | else: 618 | # exception #1 619 | rendered = pieces["short"] 620 | if pieces["dirty"]: 621 | rendered += "-dirty" 622 | return rendered 623 | 624 | 625 | def render(pieces: Dict[str, Any], style: str) -> Dict[str, Any]: 626 | """Render the given version pieces into the requested style.""" 627 | if pieces["error"]: 628 | return { 629 | "version": "unknown", 630 | "full-revisionid": pieces.get("long"), 631 | "dirty": None, 632 | "error": pieces["error"], 633 | "date": None, 634 | } 635 | 636 | if not style or style == "default": 637 | style = "pep440" # the default 638 | 639 | if style == "pep440": 640 | rendered = render_pep440(pieces) 641 | elif style == "pep440-branch": 642 | rendered = render_pep440_branch(pieces) 643 | elif style == "pep440-pre": 644 | rendered = render_pep440_pre(pieces) 645 | elif style == "pep440-post": 646 | rendered = render_pep440_post(pieces) 647 | elif style == "pep440-post-branch": 648 | rendered = render_pep440_post_branch(pieces) 649 | elif style == "pep440-old": 650 | rendered = render_pep440_old(pieces) 651 | elif style == "git-describe": 652 | rendered = render_git_describe(pieces) 653 | elif style == "git-describe-long": 654 | rendered = render_git_describe_long(pieces) 655 | else: 656 | raise ValueError("unknown style '%s'" % style) 657 | 658 | return { 659 | "version": rendered, 660 | "full-revisionid": pieces["long"], 661 | "dirty": pieces["dirty"], 662 | "error": None, 663 | "date": pieces.get("date"), 664 | } 665 | 666 | 667 | def get_versions() -> Dict[str, Any]: 668 | """Get version information or return default if unable to do so.""" 669 | # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have 670 | # __file__, we can work backwards from there to the root. Some 671 | # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which 672 | # case we can only use expanded keywords. 673 | 674 | cfg = get_config() 675 | verbose = cfg.verbose 676 | 677 | try: 678 | return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) 679 | except NotThisMethod: 680 | pass 681 | 682 | try: 683 | root = os.path.realpath(__file__) 684 | # versionfile_source is the relative path from the top of the source 685 | # tree (where the .git directory might live) to this file. Invert 686 | # this to find the root from __file__. 687 | for _ in cfg.versionfile_source.split("/"): 688 | root = os.path.dirname(root) 689 | except NameError: 690 | return { 691 | "version": "0+unknown", 692 | "full-revisionid": None, 693 | "dirty": None, 694 | "error": "unable to find root of source tree", 695 | "date": None, 696 | } 697 | 698 | try: 699 | pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) 700 | return render(pieces, cfg.style) 701 | except NotThisMethod: 702 | pass 703 | 704 | try: 705 | if cfg.parentdir_prefix: 706 | return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) 707 | except NotThisMethod: 708 | pass 709 | 710 | return { 711 | "version": "0+unknown", 712 | "full-revisionid": None, 713 | "dirty": None, 714 | "error": "unable to compute version", 715 | "date": None, 716 | } 717 | -------------------------------------------------------------------------------- /python_jsonschema_objects/descriptors.py: -------------------------------------------------------------------------------- 1 | from python_jsonschema_objects import util, validators, wrapper_types 2 | from python_jsonschema_objects.classbuilder import ProtocolBase, TypeProxy, TypeRef 3 | 4 | 5 | class AttributeDescriptor(object): 6 | """Provides property access for constructed class properties""" 7 | 8 | def __init__(self, prop, info, desc=""): 9 | self.prop = prop 10 | self.info = info 11 | self.desc = desc 12 | 13 | def __doc__(self): 14 | return self.desc 15 | 16 | def __get__(self, obj, owner=None): 17 | if obj is None and owner is not None: 18 | return self 19 | 20 | try: 21 | return obj._properties[self.prop] 22 | except KeyError: 23 | raise AttributeError("No such attribute") 24 | 25 | def __set__(self, obj, val): 26 | info = self.info 27 | if isinstance(info["type"], (list, tuple)): 28 | ok = False 29 | errors = [] 30 | type_checks = [] 31 | 32 | for typ in info["type"]: 33 | if not isinstance(typ, dict): 34 | type_checks.append(typ) 35 | continue 36 | typ = next( 37 | t for n, t in validators.SCHEMA_TYPE_MAPPING if typ["type"] == n 38 | ) 39 | if typ is None: 40 | typ = type(None) 41 | if isinstance(typ, (list, tuple)): 42 | type_checks.extend(typ) 43 | else: 44 | type_checks.append(typ) 45 | 46 | for typ in type_checks: 47 | if not isinstance(typ, TypeProxy) and isinstance(val, typ): 48 | ok = True 49 | break 50 | elif hasattr(typ, "isLiteralClass"): 51 | try: 52 | validator = typ(val) 53 | validator.validate() 54 | except Exception as e: 55 | errors.append("Failed to coerce to '{0}': {1}".format(typ, e)) 56 | pass 57 | else: 58 | ok = True 59 | break 60 | elif util.safe_issubclass(typ, ProtocolBase): 61 | # Force conversion- thus the val rather than validator assignment. 62 | try: 63 | val = typ(**val) 64 | val.validate() 65 | except Exception as e: 66 | errors.append("Failed to coerce to '{0}': {1}".format(typ, e)) 67 | pass 68 | else: 69 | ok = True 70 | break 71 | elif util.safe_issubclass(typ, wrapper_types.ArrayWrapper): 72 | try: 73 | val = typ(val) 74 | val.validate() 75 | except Exception as e: 76 | errors.append("Failed to coerce to '{0}': {1}".format(typ, e)) 77 | pass 78 | else: 79 | ok = True 80 | break 81 | elif isinstance(typ, TypeProxy): 82 | try: 83 | # Handle keyword expansion according to expected types. Using 84 | # keywords like oneOf, value can be an object, array or literal. 85 | if isinstance(val, dict): 86 | val = typ(**val) 87 | else: 88 | val = typ(val) 89 | val.validate() 90 | except Exception as e: 91 | errors.append("Failed to coerce to '{0}': {1}".format(typ, e)) 92 | pass 93 | else: 94 | ok = True 95 | break 96 | 97 | if not ok: 98 | errstr = "\n".join(errors) 99 | raise validators.ValidationError( 100 | "Object must be one of {0}: \n{1}".format(info["type"], errstr) 101 | ) 102 | 103 | elif info["type"] == "array": 104 | val = info["validator"](val) 105 | val.validate() 106 | 107 | elif util.safe_issubclass(info["type"], wrapper_types.ArrayWrapper): 108 | # An array type may have already been converted into an ArrayValidator. 109 | val = info["type"](val) 110 | val.validate() 111 | 112 | elif getattr(info["type"], "isLiteralClass", False) is True: 113 | if not isinstance(val, info["type"]): 114 | validator = info["type"](val) 115 | validator.validate() 116 | if validator._value is not None: 117 | # This allows setting of default Literal values. 118 | val = validator 119 | 120 | elif util.safe_issubclass(info["type"], ProtocolBase): 121 | if not isinstance(val, info["type"]): 122 | val = info["type"](**val) 123 | 124 | val.validate() 125 | 126 | elif isinstance(info["type"], TypeProxy): 127 | if isinstance(val, dict): 128 | val = info["type"](**val) 129 | else: 130 | val = info["type"](val) 131 | 132 | elif isinstance(info["type"], TypeRef): 133 | if not isinstance(val, info["type"].ref_class): 134 | val = info["type"](**val) 135 | 136 | val.validate() 137 | 138 | elif info["type"] is None: 139 | # This is the null value 140 | if val is not None: 141 | raise validators.ValidationError("None is only valid value for null") 142 | 143 | else: 144 | raise TypeError("Unknown object type: '{0}'".format(info["type"])) 145 | 146 | obj._properties[self.prop] = val 147 | 148 | def __delete__(self, obj): 149 | prop = self.prop 150 | if prop in obj.__required__: 151 | raise AttributeError("'%s' is required" % prop) 152 | else: 153 | obj._properties[prop] = None 154 | -------------------------------------------------------------------------------- /python_jsonschema_objects/examples/README.md: -------------------------------------------------------------------------------- 1 | [![Build](https://github.com/cwacek/python-jsonschema-objects/actions/workflows/pythonpackage.yml/badge.svg?branch=master)](https://github.com/cwacek/python-jsonschema-objects/actions/workflows/pythonpackage.yml) 2 | 3 | ## What 4 | 5 | python-jsonschema-objects provides an *automatic* class-based 6 | binding to JSON Schemas for use in python. See [Draft Schema 7 | Support](#draft-schema-support) to see supported keywords 8 | 9 | For example, given the following schema: 10 | 11 | ``` schema 12 | { 13 | "title": "Example Schema", 14 | "type": "object", 15 | "properties": { 16 | "firstName": { 17 | "type": "string" 18 | }, 19 | "lastName": { 20 | "type": "string" 21 | }, 22 | "age": { 23 | "description": "Age in years", 24 | "type": "integer", 25 | "minimum": 0 26 | }, 27 | "dogs": { 28 | "type": "array", 29 | "items": {"type": "string"}, 30 | "maxItems": 4 31 | }, 32 | "address": { 33 | "type": "object", 34 | "properties": { 35 | "street": {"type": "string"}, 36 | "city": {"type": "string"}, 37 | "state": {"type": "string"} 38 | }, 39 | "required":["street", "city"] 40 | }, 41 | "gender": { 42 | "type": "string", 43 | "enum": ["male", "female"] 44 | }, 45 | "deceased": { 46 | "enum": ["yes", "no", 1, 0, "true", "false"] 47 | } 48 | }, 49 | "required": ["firstName", "lastName"] 50 | } 51 | ``` 52 | 53 | jsonschema-objects can generate a class based binding. Assume 54 | here that the schema above has been loaded in a variable called 55 | `examples`: 56 | 57 | ``` python 58 | >>> import python_jsonschema_objects as pjs 59 | >>> builder = pjs.ObjectBuilder(examples['Example Schema']) 60 | >>> ns = builder.build_classes() 61 | >>> Person = ns.ExampleSchema 62 | >>> james = Person(firstName="James", lastName="Bond") 63 | >>> james.lastName 64 | Bond> 65 | >>> james.lastName == "Bond" 66 | True 67 | >>> james 68 | James> gender=None lastName= Bond>> 69 | 70 | ``` 71 | 72 | Validations will also be applied as the object is manipulated. 73 | 74 | ``` python 75 | >>> james.age = -2 # doctest: +IGNORE_EXCEPTION_DETAIL 76 | Traceback (most recent call last): 77 | ... 78 | ValidationError: -2 is less than 0 79 | 80 | >>> james.dogs= ["Jasper", "Spot", "Noodles", "Fido", "Dumbo"] # doctest: +IGNORE_EXCEPTION_DETAIL 81 | Traceback (most recent call last): 82 | ... 83 | ValidationError: ["Jasper", "Spot", "Noodles", "Fido", "Dumbo"] has too many elements. Wanted 4. 84 | 85 | ``` 86 | 87 | The object can be serialized out to JSON. Options are passed 88 | through to the standard library JSONEncoder object. 89 | 90 | ``` python 91 | >>> james.serialize(sort_keys=True) 92 | '{"firstName": "James", "lastName": "Bond"}' 93 | 94 | ``` 95 | 96 | ## Why 97 | 98 | Ever struggled with how to define message formats? Been 99 | frustrated by the difficulty of keeping documentation and message 100 | definition in lockstep? Me too. 101 | 102 | There are lots of tools designed to help define JSON object 103 | formats, foremost among them [JSON Schema](http://json-schema.org). 104 | JSON Schema allows you to define JSON object formats, complete 105 | with validations. 106 | 107 | However, JSON Schema is language agnostic. It validates encoded 108 | JSON directly - using it still requires an object binding in 109 | whatever language we use. Often writing the binding is just as 110 | tedious as writing the schema itself. 111 | 112 | This avoids that problem by auto-generating classes, complete 113 | with validation, directly from an input JSON schema. These 114 | classes can seamlessly encode back and forth to JSON valid 115 | according to the schema. 116 | 117 | ## Fully Functional Literals 118 | 119 | Literal values are wrapped when constructed to support validation 120 | and other schema-related operations. However, you can still use 121 | them just as you would other literals. 122 | 123 | ``` python 124 | >>> import python_jsonschema_objects as pjs 125 | >>> builder = pjs.ObjectBuilder(examples['Example Schema']) 126 | >>> ns = builder.build_classes() 127 | >>> Person = ns.ExampleSchema 128 | >>> james = Person(firstName="James", lastName="Bond") 129 | >>> str(james.lastName) 130 | 'Bond' 131 | >>> james.lastName += "ing" 132 | >>> str(james.lastName) 133 | 'Bonding' 134 | >>> james.age = 4 135 | >>> james.age - 1 136 | 3 137 | >>> 3 + james.age 138 | 7 139 | >>> james.lastName / 4 140 | Traceback (most recent call last): 141 | ... 142 | TypeError: unsupported operand type(s) for /: 'str' and 'int' 143 | 144 | ``` 145 | 146 | ## Accessing Generated Objects 147 | 148 | Sometimes what you really want to do is define a couple 149 | of different objects in a schema, and then be able to use 150 | them flexibly. 151 | 152 | Any object built as a reference can be obtained from the top 153 | level namespace. Thus, to obtain multiple top level classes, 154 | define them separately in a definitions structure, then simply 155 | make the top level schema refer to each of them as a `oneOf`. 156 | 157 | Other classes identified during the build process will also be 158 | available from the top level object. However, if you pass `named_only` 159 | to the build_classes call, then only objects with a `title` will be 160 | included in the output namespace. 161 | 162 | Finally, by default, the names in the returned namespace are transformed 163 | by passing them through a camel case function. If you want to have names unchanged, 164 | pass `standardize_names=False` to the build call. 165 | 166 | The schema and code example below show how this works. 167 | 168 | ``` schema 169 | { 170 | "title": "MultipleObjects", 171 | "id": "foo", 172 | "type": "object", 173 | "oneOf":[ 174 | {"$ref": "#/definitions/ErrorResponse"}, 175 | {"$ref": "#/definitions/VersionGetResponse"} 176 | ], 177 | "definitions": { 178 | "ErrorResponse": { 179 | "title": "Error Response", 180 | "id": "Error Response", 181 | "type": "object", 182 | "properties": { 183 | "message": {"type": "string"}, 184 | "status": {"type": "integer"} 185 | }, 186 | "required": ["message", "status"] 187 | }, 188 | "VersionGetResponse": { 189 | "title": "Version Get Response", 190 | "type": "object", 191 | "properties": { 192 | "local": {"type": "boolean"}, 193 | "version": {"type": "string"} 194 | }, 195 | "required": ["version"] 196 | } 197 | } 198 | } 199 | ``` 200 | 201 | ``` python 202 | >>> builder = pjs.ObjectBuilder(examples["MultipleObjects"]) 203 | >>> classes = builder.build_classes() 204 | >>> [str(x) for x in dir(classes)] 205 | ['ErrorResponse', 'Local', 'Message', 'Multipleobjects', 'Status', 'Version', 'VersionGetResponse'] 206 | >>> classes = builder.build_classes(named_only=True, standardize_names=False) 207 | >>> [str(x) for x in dir(classes)] 208 | ['Error Response', 'MultipleObjects', 'Version Get Response'] 209 | >>> classes = builder.build_classes(named_only=True) 210 | >>> [str(x) for x in dir(classes)] 211 | ['ErrorResponse', 'Multipleobjects', 'VersionGetResponse'] 212 | 213 | ``` 214 | 215 | 216 | ## Supported Operators 217 | 218 | ### $ref 219 | 220 | The `$ref` operator is supported in nearly all locations, and 221 | dispatches the actual reference resolution to the 222 | `referencing.Registry` resolver. 223 | 224 | This example shows using the memory URI (described in more detail 225 | below) to create a wrapper object that is just a string literal. 226 | 227 | ``` schema 228 | { 229 | "title": "Just a Reference", 230 | "$ref": "memory:Address" 231 | } 232 | ``` 233 | 234 | ```python 235 | >>> builder = pjs.ObjectBuilder(examples['Just a Reference'], resolved=examples) 236 | >>> ns = builder.build_classes() 237 | >>> ns.JustAReference('Hello') 238 | Hello> 239 | 240 | ``` 241 | 242 | #### Circular References 243 | 244 | Circular references are not a good idea, but they're supported 245 | anyway via lazy loading (as much as humanly possible). 246 | 247 | Given the crazy schema below, we can actually generate these 248 | classes. 249 | 250 | ```schema 251 | { 252 | "title": "Circular References", 253 | "id": "foo", 254 | "type": "object", 255 | "oneOf":[ 256 | {"$ref": "#/definitions/A"}, 257 | {"$ref": "#/definitions/B"} 258 | ], 259 | "definitions": { 260 | "A": { 261 | "type": "object", 262 | "properties": { 263 | "message": {"type": "string"}, 264 | "reference": {"$ref": "#/definitions/B"} 265 | }, 266 | "required": ["message"] 267 | }, 268 | "B": { 269 | "type": "object", 270 | "properties": { 271 | "author": {"type": "string"}, 272 | "oreference": {"$ref": "#/definitions/A"} 273 | }, 274 | "required": ["author"] 275 | } 276 | } 277 | } 278 | ``` 279 | 280 | We can instantiate objects that refer to each other. 281 | 282 | ``` 283 | >>> builder = pjs.ObjectBuilder(examples['Circular References']) 284 | >>> klasses = builder.build_classes() 285 | >>> a = klasses.A() 286 | >>> b = klasses.B() 287 | >>> a.message= 'foo' 288 | >>> a.reference = b # doctest: +IGNORE_EXCEPTION_DETAIL 289 | Traceback (most recent call last): 290 | ... 291 | ValidationError: '[u'author']' are required attributes for B 292 | >>> b.author = "James Dean" 293 | >>> a.reference = b 294 | >>> a 295 | foo> reference= James Dean> oreference=None>> 296 | 297 | ``` 298 | 299 | #### The "memory:" URI 300 | 301 | **"memory:" URIs are deprecated (although they still work). Load resources into a 302 | `referencing.Registry` instead and pass those in** 303 | 304 | The ObjectBuilder can be passed a dictionary specifying 305 | 'memory' schemas when instantiated. This will allow it to 306 | resolve references where the referenced schemas are retrieved 307 | out of band and provided at instantiation. 308 | 309 | For instance, given the following schemas: 310 | 311 | ``` schema 312 | { 313 | "title": "Address", 314 | "type": "string" 315 | } 316 | ``` 317 | 318 | ``` schema 319 | { 320 | "title": "AddlPropsAllowed", 321 | "type": "object", 322 | "additionalProperties": true 323 | } 324 | ``` 325 | 326 | ``` schema 327 | { 328 | "title": "Other", 329 | "type": "object", 330 | "properties": { 331 | "MyAddress": {"$ref": "memory:Address"} 332 | }, 333 | "additionalProperties": false 334 | } 335 | ``` 336 | 337 | The ObjectBuilder can be used to build the "Other" object by 338 | passing in a definition for "Address". 339 | 340 | ``` python 341 | >>> builder = pjs.ObjectBuilder(examples['Other'], resolved={"Address": {"type":"string"}}) 342 | >>> builder.validate({"MyAddress": '1234'}) 343 | >>> ns = builder.build_classes() 344 | >>> thing = ns.Other() 345 | >>> thing 346 | 347 | >>> thing.MyAddress = "Franklin Square" 348 | >>> thing 349 | Franklin Square>> 350 | >>> thing.MyAddress = 423 # doctest: +IGNORE_EXCEPTION_DETAIL 351 | Traceback (most recent call last): 352 | ... 353 | ValidationError: 432 is not a string 354 | 355 | ``` 356 | 357 | ### oneOf 358 | 359 | Generated wrappers can properly deserialize data 360 | representing 'oneOf' relationships, so long as the candidate 361 | schemas are unique. 362 | 363 | ``` schema 364 | { 365 | "title": "Age", 366 | "type": "integer" 367 | } 368 | 369 | ``` 370 | 371 | ``` schema 372 | { 373 | "title": "OneOf", 374 | "type": "object", 375 | "properties": { 376 | "MyData": { "oneOf":[ 377 | {"$ref": "memory:Address"}, 378 | {"$ref": "memory:Age"} 379 | ] 380 | } 381 | }, 382 | "additionalProperties": false 383 | } 384 | ``` 385 | ``` schema 386 | { 387 | "title": "OneOfBare", 388 | "type": "object", 389 | "oneOf":[ 390 | {"$ref": "memory:Other"}, 391 | {"$ref": "memory:Example Schema"} 392 | ], 393 | "additionalProperties": false 394 | } 395 | ``` 396 | 397 | ## Installation 398 | 399 | pip install python_jsonschema_objects 400 | 401 | ## Tests 402 | 403 | Tests are managed using the excellent Tox. Simply `pip install 404 | tox`, then `tox`. 405 | 406 | ## Draft Keyword Support 407 | 408 | Most of draft-4 is supported, so only exceptions are noted 409 | in the table. Where a keyword functionality changed between 410 | drafts, the version that is supported is noted. 411 | 412 | The library will warn (but not throw an exception) if you give 413 | it an unsupported `$schema` 414 | 415 | | Keyword | supported | version | 416 | | --------| -----------| --------- | 417 | | $id | true | draft-6 | 418 | | propertyNames | false | | 419 | | contains | false | | 420 | | const | false | | 421 | | required | true | draft-4 | 422 | | examples | false | | 423 | | format | false | | 424 | 425 | 426 | ## Changelog 427 | 428 | *Please refer to Github releases for up to date changelogs.* 429 | 430 | **0.0.18** 431 | 432 | + Fix assignment to schemas defined using 'oneOf' 433 | + Add sphinx documentation and support for readthedocs 434 | 435 | 0.0.16 - Fix behavior of exclusiveMinimum and exclusiveMaximum 436 | validators so that they work properly. 437 | 438 | 0.0.14 - Roll in a number of fixes from Github contributors, 439 | including fixes for oneOf handling, array validation, and Python 440 | 3 support. 441 | 442 | 0.0.13 - Lazily build object classes. Allows low-overhead use 443 | of jsonschema validators. 444 | 445 | 0.0.12 - Support "true" as a value for 'additionalProperties' 446 | 447 | 0.0.11 - Generated wrappers can now properly deserialize data 448 | representing 'oneOf' relationships, so long as the candidate 449 | schemas are unique. 450 | 451 | 0.0.10 - Fixed incorrect checking of enumerations which 452 | previously enforced that all enumeration values be of the same 453 | type. 454 | 455 | 0.0.9 - Added support for 'memory:' schema URIs, which can be 456 | used to reference externally resolved schemas. 457 | 458 | 0.0.8 - Fixed bugs that occurred when the same class was read 459 | from different locations in the schema, and thus had a different 460 | URI 461 | 462 | 0.0.7 - Required properties containing the '@' symbol no longer 463 | cause `build_classes()` to fail. 464 | 465 | 0.0.6 - All literals now use a standardized LiteralValue type. 466 | Array validation actually coerces element types. `as_dict` can 467 | translate objects to dictionaries seamlessly. 468 | 469 | 0.0.5 - Improved validation for additionalItems (and tests to 470 | match). Provided dictionary-syntax access to object properties 471 | and iteration over properties. 472 | 473 | 0.0.4 - Fixed some bugs that only showed up under specific schema 474 | layouts, including one which forced remote lookups for 475 | schema-local references. 476 | 477 | 0.0.3b - Fixed ReStructuredText generation 478 | 479 | 0.0.3 - Added support for other array validations (minItems, 480 | maxItems, uniqueItems). 481 | 482 | 0.0.2 - Array item type validation now works. Specifying 'items', 483 | will now enforce types, both in the tuple and list syntaxes. 484 | 485 | 0.0.1 - Class generation works, including 'oneOf' and 'allOf' 486 | relationships. All basic validations work. 487 | -------------------------------------------------------------------------------- /python_jsonschema_objects/examples/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cwacek/python-jsonschema-objects/d19534058154dc5a480465a03d666197668a6859/python_jsonschema_objects/examples/__init__.py -------------------------------------------------------------------------------- /python_jsonschema_objects/literals.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import operator 3 | 4 | 5 | from python_jsonschema_objects import util, validators 6 | 7 | 8 | def MakeLiteral(name, typ, value, **properties): 9 | properties.update({"type": typ}) 10 | klass = type( 11 | str(name), 12 | tuple((LiteralValue,)), 13 | { 14 | "__propinfo__": { 15 | "__literal__": properties, 16 | "__default__": properties.get("default"), 17 | } 18 | }, 19 | ) 20 | 21 | return klass(value) 22 | 23 | 24 | @functools.total_ordering 25 | class LiteralValue(object): 26 | """Docstring for LiteralValue""" 27 | 28 | isLiteralClass = True 29 | 30 | def __init__(self, value, typ=None): 31 | """@todo: to be defined 32 | 33 | :value: @todo 34 | 35 | """ 36 | if isinstance(value, LiteralValue): 37 | self._value = value._value 38 | else: 39 | self._value = value 40 | 41 | self.validate() 42 | 43 | constval = self.const() 44 | if constval is not None: 45 | self._value = constval 46 | 47 | def as_dict(self): 48 | return self.for_json() 49 | 50 | def for_json(self): 51 | return self._value 52 | 53 | @classmethod 54 | def default(cls): 55 | return cls.__propinfo__.get("__default__") 56 | 57 | @classmethod 58 | def const(cls): 59 | return cls.__propinfo__.get("__literal__", {}).get("const", None) 60 | 61 | @classmethod 62 | def propinfo(cls, propname): 63 | if propname not in cls.__propinfo__: 64 | return {} 65 | return cls.__propinfo__[propname] 66 | 67 | def serialize(self): 68 | self.validate() 69 | enc = util.ProtocolJSONEncoder() 70 | return enc.encode(self) 71 | 72 | def __repr__(self): 73 | return " %s>" % (self._value.__class__.__name__, str(self._value)) 74 | 75 | def __str__(self): 76 | if isinstance(self._value, str): 77 | return self._value 78 | return str(self._value) 79 | 80 | def validate(self): 81 | info = self.propinfo("__literal__") 82 | 83 | # TODO: this duplicates logic in validators.ArrayValidator.check_items; 84 | # unify it. 85 | for param, paramval in sorted( 86 | info.items(), key=lambda x: x[0].lower() != "type" 87 | ): 88 | validator = validators.registry(param) 89 | if validator is not None: 90 | validator(paramval, self._value, info) 91 | 92 | def __eq__(self, other): 93 | if isinstance(other, LiteralValue): 94 | return self._value == other._value 95 | return self._value == other 96 | 97 | def __hash__(self): 98 | return hash(self._value) 99 | 100 | def __lt__(self, other): 101 | if isinstance(other, LiteralValue): 102 | return self._value < other._value 103 | return self._value < other 104 | 105 | def __int__(self): 106 | return int(self._value) 107 | 108 | def __float__(self): 109 | return float(self._value) 110 | 111 | def __bool__(self): 112 | return bool(self._value) 113 | 114 | __nonzero__ = __bool__ 115 | 116 | 117 | EXCLUDED_OPERATORS = set( 118 | util.CLASS_ATTRS 119 | + util.NEWCLASS_ATTRS 120 | + [ 121 | "__name__", 122 | "__setattr__", 123 | "__getattr__", 124 | "__dict__", 125 | "__matmul__", 126 | "__imatmul__", 127 | ] 128 | ) 129 | 130 | 131 | def dispatch_to_value(fn): 132 | def wrapper(self, other): 133 | return fn(self._value, other) 134 | pass 135 | 136 | return wrapper 137 | 138 | 139 | """ This attaches all the literal operators to LiteralValue 140 | except for the reverse ones.""" 141 | for op in dir(operator): 142 | if op.startswith("__") and op not in EXCLUDED_OPERATORS: 143 | opfn = getattr(operator, op) 144 | setattr(LiteralValue, op, dispatch_to_value(opfn)) 145 | 146 | 147 | """ We also have to patch the reverse operators, 148 | which aren't conveniently defined anywhere """ 149 | LiteralValue.__radd__ = lambda self, other: other + self._value 150 | LiteralValue.__rsub__ = lambda self, other: other - self._value 151 | LiteralValue.__rmul__ = lambda self, other: other * self._value 152 | LiteralValue.__rtruediv__ = lambda self, other: other / self._value 153 | LiteralValue.__rfloordiv__ = lambda self, other: other // self._value 154 | LiteralValue.__rmod__ = lambda self, other: other % self._value 155 | LiteralValue.__rdivmod__ = lambda self, other: divmod(other, self._value) 156 | LiteralValue.__rpow__ = lambda self, other, modulo=None: pow(other, self._value, modulo) 157 | LiteralValue.__rlshift__ = lambda self, other: other << self._value 158 | LiteralValue.__rrshift__ = lambda self, other: other >> self._value 159 | LiteralValue.__rand__ = lambda self, other: other & self._value 160 | LiteralValue.__rxor__ = lambda self, other: other ^ self._value 161 | LiteralValue.__ror__ = lambda self, other: other | self._value 162 | -------------------------------------------------------------------------------- /python_jsonschema_objects/markdown_support.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | import markdown 4 | from markdown.extensions import Extension 5 | from markdown.preprocessors import Preprocessor 6 | 7 | try: 8 | from markdown import __version_info__ as markdown_version_info 9 | except ImportError: 10 | from markdown import version_info as markdown_version_info 11 | 12 | 13 | def extract_code_blocks(filename): 14 | with open(filename) as fin: 15 | doc = fin.read().split("\n") 16 | 17 | M = markdown.Markdown(extensions=[SpecialFencedCodeExtension()]) 18 | 19 | preprocessors = M.preprocessors 20 | tree_processors = M.treeprocessors 21 | 22 | # Markdown 3.* stores the processors in a class that can be iterated directly. 23 | # Markdown 2.* stores them in a dict, so we have to pull out the values. 24 | if markdown_version_info[0] == 2: 25 | # Note: `markdown.version_info` will be deprecated in favor of 26 | # `markdown.__version_info__` in later versions of Markdown. 27 | preprocessors = preprocessors.values() 28 | tree_processors = tree_processors.values() 29 | 30 | for prep in preprocessors: 31 | doc = prep.run(doc) 32 | 33 | root = M.parser.parseDocument(doc).getroot() 34 | 35 | for treeproc in tree_processors: 36 | newRoot = treeproc.run(root) 37 | if newRoot is not None: 38 | root = newRoot 39 | 40 | return SpecialFencePreprocessor.EXAMPLES 41 | 42 | 43 | class SpecialFencedCodeExtension(Extension): 44 | def extendMarkdown(self, md, md_globals=None): 45 | """Add FencedBlockPreprocessor to the Markdown instance.""" 46 | md.registerExtension(self) 47 | 48 | if markdown_version_info[0] >= 3: 49 | md.preprocessors.register( 50 | SpecialFencePreprocessor(md), "fenced_code_block", 10 51 | ) 52 | else: 53 | md.preprocessors.add( 54 | "fenced_code_block", 55 | SpecialFencePreprocessor(md), 56 | ">normalize_whitespace", 57 | ) 58 | 59 | 60 | class SpecialFencePreprocessor(Preprocessor): 61 | EXAMPLES = {} 62 | FENCED_BLOCK_RE = re.compile( 63 | r""" 64 | (?P^(?:~{3,}|`{3,}))[ ]* # Opening ``` or ~~~ 65 | (\{?\.?(?P[a-zA-Z0-9_+-]*))?[ ]* # Optional {, and lang 66 | # Optional highlight lines, single- or double-quote-delimited 67 | (hl_lines=(?P"|')(?P.*?)(?P=quot))?[ ]* 68 | }?[ ]*\n # Optional closing } 69 | (?P.*?)(?<=\n) 70 | (?P=fence)[ ]*$""", 71 | re.MULTILINE | re.DOTALL | re.VERBOSE, 72 | ) 73 | 74 | def __init__(self, md): 75 | super(SpecialFencePreprocessor, self).__init__(md) 76 | 77 | self.checked_for_codehilite = False 78 | self.codehilite_conf = {} 79 | 80 | def run(self, lines): 81 | text = "\n".join(lines) 82 | 83 | while True: 84 | m = self.FENCED_BLOCK_RE.search(text) 85 | if m: 86 | if m.group("lang"): 87 | lang = m.group("lang") 88 | example = m.group("code") 89 | try: 90 | self.EXAMPLES[lang].append(example) 91 | except KeyError: 92 | self.EXAMPLES[lang] = [example] 93 | 94 | text = "%s\n%s" % (text[: m.start()], text[m.end() :]) 95 | else: 96 | break 97 | return text.split("\n") 98 | 99 | 100 | if __name__ == "__main__": 101 | print(extract_code_blocks()) 102 | -------------------------------------------------------------------------------- /python_jsonschema_objects/pattern_properties.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import logging 3 | import re 4 | 5 | 6 | from python_jsonschema_objects import util, validators, wrapper_types 7 | from python_jsonschema_objects.literals import MakeLiteral 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | PatternDef = collections.namedtuple("PatternDef", "pattern schema_type") 12 | 13 | 14 | class ExtensibleValidator(object): 15 | def __init__(self, name, schemadef, builder): 16 | import python_jsonschema_objects.classbuilder as cb 17 | 18 | self._pattern_types = [] 19 | self._additional_type = True 20 | 21 | addlProp = schemadef.get("additionalProperties", True) 22 | 23 | if addlProp is False: 24 | self._additional_type = False 25 | elif addlProp is True: 26 | self._additional_type = True 27 | else: 28 | if "$ref" in addlProp: 29 | typ = builder.resolve_type(addlProp["$ref"], name) 30 | else: 31 | uri = "{0}/{1}_{2}".format( 32 | name, "", "" 33 | ) 34 | builder.resolved[uri] = builder.construct( 35 | uri, addlProp, (cb.ProtocolBase,) 36 | ) 37 | typ = builder.resolved[uri] 38 | 39 | self._additional_type = typ 40 | 41 | for pattern, typedef in schemadef.get("patternProperties", {}).items(): 42 | if "$ref" in typedef: 43 | typ = builder.resolve_type(typedef["$ref"], name) 44 | else: 45 | uri = "{0}/{1}_{2}".format(name, "", pattern) 46 | 47 | builder.resolved[uri] = builder.construct( 48 | uri, typedef, (cb.ProtocolBase,) 49 | ) 50 | typ = builder.resolved[uri] 51 | 52 | self._pattern_types.append( 53 | PatternDef(pattern=re.compile(pattern), schema_type=typ) 54 | ) 55 | 56 | def _make_type(self, typ, val): 57 | import python_jsonschema_objects.classbuilder as cb 58 | 59 | if getattr(typ, "isLiteralClass", None) is True: 60 | return typ(val) 61 | 62 | if util.safe_issubclass(typ, cb.ProtocolBase): 63 | return typ(**val) 64 | 65 | if util.safe_issubclass(typ, wrapper_types.ArrayWrapper): 66 | return typ(val) 67 | 68 | if isinstance(typ, cb.TypeProxy): 69 | if isinstance(val, dict): 70 | val = typ(**val) 71 | else: 72 | val = typ(val) 73 | return val 74 | 75 | raise validators.ValidationError( 76 | "additionalProperty type {0} was neither a literal " 77 | "nor a schema wrapper: {1}".format(typ, val) 78 | ) 79 | 80 | def instantiate(self, name, val): 81 | import python_jsonschema_objects.classbuilder as cb 82 | 83 | for p in self._pattern_types: 84 | if p.pattern.search(name): 85 | logger.debug( 86 | "Found patternProperties match: %s %s" % (p.pattern.pattern, name) 87 | ) 88 | return self._make_type(p.schema_type, val) 89 | 90 | if self._additional_type is True: 91 | valtype = [ 92 | k 93 | for k, t in validators.SCHEMA_TYPE_MAPPING 94 | if t is not None and isinstance(val, t) 95 | ] 96 | valtype = valtype[0] 97 | return MakeLiteral(name, valtype, val) 98 | 99 | elif isinstance(self._additional_type, (type, cb.TypeProxy)): 100 | return self._make_type(self._additional_type, val) 101 | 102 | raise validators.ValidationError( 103 | "additionalProperties not permitted " "and no patternProperties specified" 104 | ) 105 | -------------------------------------------------------------------------------- /python_jsonschema_objects/util.py: -------------------------------------------------------------------------------- 1 | """Utility and namespace module.""" 2 | 3 | __all__ = ["Namespace", "as_namespace"] 4 | 5 | import copy 6 | import json 7 | from collections.abc import Mapping, Sequence 8 | 9 | 10 | class lazy_format(object): 11 | __slots__ = ("fmt", "args", "kwargs") 12 | 13 | def __init__(self, fmt, *args, **kwargs): 14 | self.fmt = fmt 15 | self.args = args 16 | self.kwargs = kwargs 17 | 18 | def __str__(self): 19 | return self.fmt.format(*self.args, **self.kwargs) 20 | 21 | 22 | def safe_issubclass(x, y): 23 | """Safe version of issubclass() that will not throw TypeErrors. 24 | 25 | Invoking issubclass('object', some-abc.meta instances) will result 26 | in the underlying implementation throwing TypeError's from trying to 27 | memoize the result- 'object' isn't a usable weakref target at that level. 28 | Unfortunately this gets exposed all the way up to our code; thus a 29 | 'safe' version of the function. 30 | """ 31 | try: 32 | return issubclass(x, y) 33 | except TypeError: 34 | return False 35 | 36 | 37 | class ProtocolJSONEncoder(json.JSONEncoder): 38 | def default(self, obj): 39 | from python_jsonschema_objects import classbuilder, wrapper_types 40 | 41 | if isinstance( 42 | obj, 43 | ( 44 | wrapper_types.ArrayWrapper, 45 | classbuilder.ProtocolBase, 46 | classbuilder.LiteralValue, 47 | ), 48 | ): 49 | return obj.for_json() 50 | else: 51 | return json.JSONEncoder.default(self, obj) 52 | 53 | 54 | def propmerge(into, data_from): 55 | """Merge JSON schema requirements into a dictionary""" 56 | newprops = copy.deepcopy(into) 57 | 58 | for prop, propval in data_from.items(): 59 | if prop not in newprops: 60 | newprops[prop] = propval 61 | continue 62 | 63 | new_sp = newprops[prop] 64 | for subprop, spval in propval.items(): 65 | if subprop not in new_sp: 66 | new_sp[subprop] = spval 67 | 68 | elif subprop == "enum": 69 | new_sp[subprop] = set(spval) & set(new_sp[subprop]) 70 | 71 | elif subprop == "type": 72 | if spval != new_sp[subprop]: 73 | raise TypeError("Type cannot conflict in allOf'") 74 | 75 | elif subprop in ("minLength", "minimum"): 76 | new_sp[subprop] = new_sp[subprop] if new_sp[subprop] > spval else spval 77 | elif subprop in ("maxLength", "maximum"): 78 | new_sp[subprop] = new_sp[subprop] if new_sp[subprop] < spval else spval 79 | elif subprop == "multipleOf": 80 | if new_sp[subprop] % spval == 0: 81 | new_sp[subprop] = spval 82 | else: 83 | raise AttributeError("Cannot set conflicting multipleOf values") 84 | else: 85 | new_sp[subprop] = spval 86 | 87 | newprops[prop] = new_sp 88 | 89 | return newprops 90 | 91 | 92 | def resolve_ref_uri(base, ref): 93 | if ref[0] == "#": 94 | # Local ref 95 | uri = base.rsplit("#", 1)[0] + ref 96 | else: 97 | uri = ref 98 | 99 | return uri 100 | 101 | 102 | class _Dummy: 103 | pass 104 | 105 | 106 | CLASS_ATTRS = dir(_Dummy) 107 | NEWCLASS_ATTRS = dir(object) 108 | del _Dummy 109 | 110 | 111 | class Namespace(dict): 112 | """A dict subclass that exposes its items as attributes. 113 | 114 | Warning: Namespace instances do not have direct access to the 115 | dict methods. 116 | """ 117 | 118 | def __init__(self, obj={}): 119 | dict.__init__(self, obj) 120 | 121 | def __dir__(self): 122 | return list(self) 123 | 124 | def __repr__(self): 125 | return "%s(%s)" % (type(self).__name__, super(dict, self).__repr__()) 126 | 127 | def __getattribute__(self, name): 128 | try: 129 | return self[name] 130 | except KeyError: 131 | msg = "'%s' object has no attribute '%s'" 132 | raise AttributeError(msg % (type(self).__name__, name)) 133 | 134 | def __setattr__(self, name, value): 135 | self[name] = value 136 | 137 | def __delattr__(self, name): 138 | del self[name] 139 | 140 | # ------------------------ 141 | # "copy constructors" 142 | 143 | @classmethod 144 | def from_object(cls, obj, names=None): 145 | if names is None: 146 | names = dir(obj) 147 | ns = {name: getattr(obj, name) for name in names} 148 | return cls(ns) 149 | 150 | @classmethod 151 | def from_mapping(cls, ns, names=None): 152 | if names: 153 | ns = {name: ns[name] for name in names} 154 | return cls(ns) 155 | 156 | @classmethod 157 | def from_sequence(cls, seq, names=None): 158 | if names: 159 | seq = {name: val for name, val in seq if name in names} 160 | return cls(seq) 161 | 162 | # ------------------------ 163 | # static methods 164 | 165 | @staticmethod 166 | def hasattr(ns, name): 167 | try: 168 | object.__getattribute__(ns, name) 169 | except AttributeError: 170 | return False 171 | return True 172 | 173 | @staticmethod 174 | def getattr(ns, name): 175 | return object.__getattribute__(ns, name) 176 | 177 | @staticmethod 178 | def setattr(ns, name, value): 179 | return object.__setattr__(ns, name, value) 180 | 181 | @staticmethod 182 | def delattr(ns, name): 183 | return object.__delattr__(ns, name) 184 | 185 | 186 | def as_namespace(obj, names=None): 187 | # functions 188 | if isinstance(obj, type(as_namespace)): 189 | obj = obj() 190 | 191 | # special cases 192 | if isinstance(obj, type): 193 | names = (name for name in dir(obj) if name not in CLASS_ATTRS) 194 | return Namespace.from_object(obj, names) 195 | if isinstance(obj, Mapping): 196 | return Namespace.from_mapping(obj, names) 197 | if isinstance(obj, Sequence): 198 | return Namespace.from_sequence(obj, names) 199 | 200 | # default 201 | return Namespace.from_object(obj, names) 202 | -------------------------------------------------------------------------------- /python_jsonschema_objects/validators.py: -------------------------------------------------------------------------------- 1 | import decimal 2 | import logging 3 | import numbers 4 | 5 | 6 | logger = logging.getLogger(__name__) 7 | 8 | SCHEMA_TYPE_MAPPING = ( 9 | ("array", list), 10 | ("boolean", bool), 11 | ("integer", int), 12 | ("number", numbers.Real), 13 | ("null", type(None)), 14 | ("string", str), 15 | ("object", dict), 16 | ) 17 | """Sequence of schema type mappings to be checked in precedence order.""" 18 | 19 | 20 | class ValidationError(Exception): 21 | pass 22 | 23 | 24 | class ValidatorRegistry(object): 25 | def __init__(self): 26 | self.registry = {} 27 | 28 | def register(self, name=None): 29 | def f(functor): 30 | self.registry[name if name is not None else functor.__name__] = functor 31 | return functor 32 | 33 | return f 34 | 35 | def __call__(self, name): 36 | return self.registry.get(name) 37 | 38 | 39 | registry = ValidatorRegistry() 40 | 41 | 42 | @registry.register() 43 | def multipleOf(param, value, _): 44 | # This conversion to string is intentional because floats are imprecise. 45 | # >>> decimal.Decimal(33.069) 46 | # Decimal('33.0690000000000026147972675971686840057373046875') 47 | # >>> decimal.Decimal('33.069') 48 | # Decimal('33.069') 49 | value = decimal.Decimal(str(value)) 50 | divisor = decimal.Decimal(str(param)) 51 | if value % divisor != 0: 52 | raise ValidationError("{0} is not a multiple of {1}".format(value, param)) 53 | 54 | 55 | @registry.register() 56 | def enum(param, value, _): 57 | if value not in param: 58 | raise ValidationError("{0} is not one of {1}".format(value, param)) 59 | 60 | 61 | @registry.register() 62 | def const(param, value, _): 63 | if value != param: 64 | raise ValidationError("{0} is not constant {1}".format(value, param)) 65 | 66 | 67 | @registry.register() 68 | def minimum(param, value, type_data): 69 | exclusive = type_data.get("exclusiveMinimum") 70 | if exclusive: 71 | if value <= param: 72 | raise ValidationError( 73 | "{0} is less than or equal to {1}".format(value, param) 74 | ) 75 | elif value < param: 76 | raise ValidationError("{0} is less than {1}".format(value, param)) 77 | 78 | 79 | @registry.register() 80 | def maximum(param, value, type_data): 81 | exclusive = type_data.get("exclusiveMaximum") 82 | if exclusive: 83 | if value >= param: 84 | raise ValidationError( 85 | "{0} is greater than or equal to {1}".format(value, param) 86 | ) 87 | elif value > param: 88 | raise ValidationError("{0} is greater than {1}".format(value, param)) 89 | 90 | 91 | @registry.register() 92 | def maxLength(param, value, _): 93 | if len(value) > param: 94 | raise ValidationError("{0} is longer than {1} characters".format(value, param)) 95 | 96 | 97 | @registry.register() 98 | def minLength(param, value, _): 99 | if len(value) < param: 100 | raise ValidationError("{0} is fewer than {1} characters".format(value, param)) 101 | 102 | 103 | @registry.register() 104 | def pattern(param, value, _): 105 | import re 106 | 107 | match = re.search(param, value) 108 | if not match: 109 | raise ValidationError("{0} does not match {1}".format(value, param)) 110 | 111 | 112 | try: 113 | from jsonschema import FormatChecker 114 | except ImportError: 115 | pass 116 | else: 117 | 118 | @registry.register() 119 | def format(param, value, _): 120 | if not FormatChecker().conforms(value, param): 121 | raise ValidationError( 122 | "'{0}' is not formatted as a {1}".format(value, param) 123 | ) 124 | 125 | 126 | type_registry = ValidatorRegistry() 127 | 128 | 129 | @type_registry.register(name="boolean") 130 | def check_boolean_type(param, value, _): 131 | if not isinstance(value, bool): 132 | raise ValidationError("{0} is not a boolean".format(value)) 133 | 134 | 135 | @type_registry.register(name="integer") 136 | def check_integer_type(param, value, _): 137 | if not isinstance(value, int) or isinstance(value, bool): 138 | raise ValidationError("{0} is not an integer".format(value)) 139 | 140 | 141 | @type_registry.register(name="number") 142 | def check_number_type(param, value, _): 143 | if not isinstance(value, numbers.Real): 144 | raise ValidationError("{0} is neither an integer nor a float".format(value)) 145 | 146 | 147 | @type_registry.register(name="null") 148 | def check_null_type(param, value, _): 149 | if value is not None: 150 | raise ValidationError("{0} is not None".format(value)) 151 | 152 | 153 | @type_registry.register(name="string") 154 | def check_string_type(param, value, _): 155 | if not isinstance(value, str): 156 | raise ValidationError("{0} is not a string".format(value)) 157 | 158 | 159 | @type_registry.register(name="array") 160 | def check_array_type(param, value, _): 161 | if not isinstance(value, list): 162 | raise ValidationError("{0} is not an array".format(value)) 163 | 164 | 165 | @type_registry.register(name="object") 166 | def check_object_type(param, value, _): 167 | from python_jsonschema_objects.classbuilder import ProtocolBase 168 | 169 | if not isinstance(value, (dict, ProtocolBase)): 170 | raise ValidationError( 171 | "{0} is not an object (neither dict nor ProtocolBase)".format(value) 172 | ) 173 | 174 | 175 | @registry.register(name="type") 176 | def check_type(param, value, type_data): 177 | type_check = type_registry(param) 178 | if type_check is None: 179 | raise ValidationError("{0} is an invalid type".format(value)) 180 | type_check(param, value, type_data) 181 | -------------------------------------------------------------------------------- /python_jsonschema_objects/wrapper_types.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import logging 3 | 4 | 5 | from python_jsonschema_objects import util 6 | from python_jsonschema_objects.util import lazy_format as fmt 7 | from python_jsonschema_objects.validators import ValidationError, registry 8 | 9 | logger = logging.getLogger(__name__) 10 | 11 | 12 | class ArrayWrapper(collections.abc.MutableSequence): 13 | """A wrapper for array-like structures. 14 | 15 | This implements all of the array like behavior that one would want, 16 | with a dirty-tracking mechanism to avoid constant validation costs. 17 | """ 18 | 19 | @property 20 | def strict(self): 21 | return getattr(self, "_strict_", False) 22 | 23 | def __len__(self): 24 | return len(self.data) 25 | 26 | def mark_or_revalidate(self): 27 | if self.strict: 28 | self.validate() 29 | else: 30 | self._dirty = True 31 | 32 | def __delitem__(self, index): 33 | self.data.pop(index) 34 | self.mark_or_revalidate() 35 | 36 | def insert(self, index, value): 37 | self.data.insert(index, value) 38 | self.mark_or_revalidate() 39 | 40 | def __setitem__(self, index, value): 41 | self.data[index] = value 42 | self.mark_or_revalidate() 43 | 44 | def __getitem__(self, idx): 45 | return self.typed_elems[idx] 46 | 47 | def __eq__(self, other): 48 | if isinstance(other, ArrayWrapper): 49 | return self.for_json() == other.for_json() 50 | else: 51 | return self.for_json() == other 52 | 53 | def __init__(self, ary): 54 | """Initialize a wrapper for the array 55 | 56 | Args: 57 | ary: (list-like, or ArrayWrapper) 58 | """ 59 | 60 | """ Marks whether or not the underlying array has been modified """ 61 | self._dirty = True 62 | 63 | """ Holds a typed copy of the array """ 64 | self._typed = None 65 | 66 | if isinstance(ary, (list, tuple, collections.abc.Sequence)): 67 | self.data = ary 68 | else: 69 | raise TypeError("Invalid value given to array validator: {0}".format(ary)) 70 | 71 | logger.debug(fmt("Initializing ArrayWrapper {} with {}", self, ary)) 72 | 73 | @property 74 | def typed_elems(self): 75 | logger.debug(fmt("Accessing typed_elems of ArrayWrapper {} ", self)) 76 | if self._typed is None or self._dirty is True: 77 | self.validate() 78 | 79 | return self._typed 80 | 81 | def __repr__(self): 82 | return "<%s=%s>" % (self.__class__.__name__, str(self.data)) 83 | 84 | @classmethod 85 | def from_json(cls, jsonmsg): 86 | import json 87 | 88 | msg = json.loads(jsonmsg) 89 | obj = cls(msg) 90 | obj.validate() 91 | return obj 92 | 93 | def serialize(self): 94 | enc = util.ProtocolJSONEncoder() 95 | return enc.encode(self.typed_elems) 96 | 97 | def for_json(self): 98 | from python_jsonschema_objects import classbuilder 99 | 100 | out = [] 101 | for item in self.typed_elems: 102 | if isinstance( 103 | item, 104 | (classbuilder.ProtocolBase, classbuilder.LiteralValue, ArrayWrapper), 105 | ): 106 | out.append(item.for_json()) 107 | else: 108 | out.append(item) 109 | 110 | return out 111 | 112 | def validate(self): 113 | if self.strict or self._dirty: 114 | self.validate_items() 115 | self.validate_length() 116 | self.validate_uniqueness() 117 | return True 118 | 119 | def validate_uniqueness(self): 120 | if getattr(self, "uniqueItems", False) is True: 121 | testset = set(repr(item) for item in self.data) 122 | if len(testset) != len(self.data): 123 | raise ValidationError( 124 | "{0} has duplicate elements, but uniqueness required".format( 125 | self.data 126 | ) 127 | ) 128 | 129 | def validate_length(self): 130 | if getattr(self, "minItems", None) is not None: 131 | if len(self.data) < self.minItems: 132 | raise ValidationError( 133 | "{1} has too few elements. Wanted {0}.".format( 134 | self.minItems, self.data 135 | ) 136 | ) 137 | 138 | if getattr(self, "maxItems", None) is not None: 139 | if len(self.data) > self.maxItems: 140 | raise ValidationError( 141 | "{1} has too many elements. Wanted {0}.".format( 142 | self.maxItems, self.data 143 | ) 144 | ) 145 | 146 | def validate_items(self): 147 | """Validates the items in the backing array, including 148 | performing type validation. 149 | 150 | Sets the _typed property and clears the dirty flag as a side effect 151 | 152 | Returns: 153 | The typed array 154 | """ 155 | logger.debug(fmt("Validating {}", self)) 156 | from python_jsonschema_objects import classbuilder 157 | 158 | if self.__itemtype__ is None: 159 | return 160 | 161 | type_checks = self.__itemtype__ 162 | if not isinstance(type_checks, (tuple, list)): 163 | # We were given items = {'type': 'blah'}. 164 | # Thus ensure the type for all data. 165 | type_checks = [type_checks] * len(self.data) 166 | elif len(type_checks) > len(self.data): 167 | raise ValidationError( 168 | "{1} does not have sufficient elements to validate against {0}".format( 169 | self.__itemtype__, self.data 170 | ) 171 | ) 172 | 173 | typed_elems = [] 174 | for elem, typ in zip(self.data, type_checks): 175 | if isinstance(typ, dict): 176 | for param, paramval in typ.items(): 177 | validator = registry(param) 178 | if validator is not None: 179 | validator(paramval, elem, typ) 180 | typed_elems.append(elem) 181 | 182 | elif util.safe_issubclass(typ, classbuilder.LiteralValue): 183 | val = typ(elem) 184 | val.validate() 185 | typed_elems.append(val) 186 | elif util.safe_issubclass(typ, classbuilder.ProtocolBase): 187 | if not isinstance(elem, typ): 188 | try: 189 | if isinstance(elem, (str, int, float)): 190 | val = typ(elem) 191 | else: 192 | val = typ(**elem) 193 | except TypeError as e: 194 | raise ValidationError( 195 | "'{0}' is not a valid value for '{1}': {2}".format( 196 | elem, typ, e 197 | ) 198 | ) 199 | else: 200 | val = elem 201 | val.validate() 202 | typed_elems.append(val) 203 | 204 | elif util.safe_issubclass(typ, ArrayWrapper): 205 | val = typ(elem) 206 | val.validate() 207 | typed_elems.append(val) 208 | 209 | elif isinstance(typ, (classbuilder.TypeProxy, classbuilder.TypeRef)): 210 | try: 211 | if isinstance(elem, (str, int, float)): 212 | val = typ(elem) 213 | elif isinstance(elem, classbuilder.LiteralValue): 214 | val = typ(elem._value) 215 | else: 216 | val = typ(**elem) 217 | except TypeError as e: 218 | raise ValidationError( 219 | "'{0}' is not a valid value for '{1}': {2}".format(elem, typ, e) 220 | ) 221 | else: 222 | val.validate() 223 | typed_elems.append(val) 224 | 225 | self._dirty = False 226 | self._typed = typed_elems 227 | return typed_elems 228 | 229 | @staticmethod 230 | def create(name, item_constraint=None, **addl_constraints): 231 | """Create an array validator based on the passed in constraints. 232 | 233 | If item_constraint is a tuple, it is assumed that tuple validation 234 | is being performed. If it is a class or dictionary, list validation 235 | will be performed. Classes are assumed to be subclasses of ProtocolBase, 236 | while dictionaries are expected to be basic types ('string', 'number', ...). 237 | 238 | addl_constraints is expected to be key-value pairs of any of the other 239 | constraints permitted by JSON Schema v4. 240 | """ 241 | logger.debug( 242 | fmt( 243 | "Constructing ArrayValidator with {} and {}", 244 | item_constraint, 245 | addl_constraints, 246 | ) 247 | ) 248 | from python_jsonschema_objects import classbuilder 249 | 250 | klassbuilder = addl_constraints.pop( 251 | "classbuilder", None 252 | ) # type: python_jsonschema_objects.classbuilder.ClassBuilder 253 | props = {} 254 | 255 | if item_constraint is not None: 256 | if isinstance(item_constraint, (tuple, list)): 257 | for i, elem in enumerate(item_constraint): 258 | isdict = isinstance(elem, (dict,)) 259 | isklass = isinstance(elem, type) and util.safe_issubclass( 260 | elem, (classbuilder.ProtocolBase, classbuilder.LiteralValue) 261 | ) 262 | 263 | if not any([isdict, isklass]): 264 | raise TypeError( 265 | "Item constraint (position {0}) is not a schema".format(i) 266 | ) 267 | elif isinstance( 268 | item_constraint, (classbuilder.TypeProxy, classbuilder.TypeRef) 269 | ): 270 | pass 271 | elif util.safe_issubclass(item_constraint, ArrayWrapper): 272 | pass 273 | else: 274 | isdict = isinstance(item_constraint, (dict,)) 275 | isklass = isinstance(item_constraint, type) and util.safe_issubclass( 276 | item_constraint, 277 | (classbuilder.ProtocolBase, classbuilder.LiteralValue), 278 | ) 279 | 280 | if not any([isdict, isklass]): 281 | raise TypeError("Item constraint is not a schema") 282 | 283 | if isdict and "$ref" in item_constraint: 284 | if klassbuilder is None: 285 | raise TypeError( 286 | "Cannot resolve {0} without classbuilder".format( 287 | item_constraint["$ref"] 288 | ) 289 | ) 290 | 291 | item_constraint = klassbuilder.resolve_type( 292 | item_constraint["$ref"], name 293 | ) 294 | 295 | elif isdict and item_constraint.get("type") == "array": 296 | # We need to create a sub-array validator. 297 | item_constraint = ArrayWrapper.create( 298 | name + "#sub", 299 | item_constraint=item_constraint["items"], 300 | addl_constraints=item_constraint, 301 | ) 302 | elif isdict and "oneOf" in item_constraint: 303 | # We need to create a TypeProxy validator. 304 | uri = "{0}_{1}".format(name, "") 305 | type_array = klassbuilder.construct_objects( 306 | item_constraint["oneOf"], uri 307 | ) 308 | 309 | item_constraint = classbuilder.TypeProxy(type_array) 310 | 311 | elif isdict and item_constraint.get("type") == "object": 312 | # We need to create a ProtocolBase object for this 313 | # anonymous definition. 314 | uri = "{0}_{1}".format(name, "") 315 | item_constraint = klassbuilder.construct(uri, item_constraint) 316 | 317 | props["__itemtype__"] = item_constraint 318 | 319 | strict = addl_constraints.pop("strict", False) 320 | props["_strict_"] = strict 321 | props.update(addl_constraints) 322 | 323 | validator = type(str(name), (ArrayWrapper,), props) 324 | 325 | return validator 326 | -------------------------------------------------------------------------------- /register.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import pandoc 4 | 5 | 6 | def markdown_to_rst(src): 7 | pandoc.core.PANDOC_PATH = "/usr/local/bin/pandoc" 8 | if not os.path.exists(pandoc.core.PANDOC_PATH): 9 | raise Exception("Pandoc not available") 10 | 11 | doc = pandoc.Document() 12 | doc.markdown = open("README.md").read() 13 | return doc.rst 14 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal=1 3 | 4 | [versioneer] 5 | VCS = git 6 | style = pep440 7 | versionfile_source = python_jsonschema_objects/_version.py 8 | versionfile_build = python_jsonschema_objects/_version.py 9 | tag_prefix = 10 | 11 | [flake8] 12 | extend-exclude = build, env, .env, venv, .venv, .tox, versioneer.py 13 | extend-ignore = 14 | # Ignore "whitespace before ':'" because black enforces a different rule. 15 | E203 16 | # Ignore "do not assign a lambda expression, use a def". 17 | E731 18 | max-line-length = 88 19 | per-file-ignores = 20 | # Ignore "module level import not at top of file" for files generated that way. 21 | docs/conf.py:E402 22 | python_jsonschema_objects/__init__.py:E402 23 | 24 | [isort] 25 | atomic = true 26 | extend_skip = versioneer.py 27 | profile = black 28 | skip_gitignore = true 29 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | # 4 | # Copyright (C) <2014-2016> Chris Wacek =18.0.0"], 41 | install_requires=[ 42 | "inflection>=0.2", 43 | "Markdown>=2.4", 44 | "jsonschema>=4.18", 45 | ], 46 | python_requires=">=3.8", 47 | cmdclass=versioneer.get_cmdclass(), 48 | classifiers=[ 49 | "Programming Language :: Python :: 3", 50 | "Programming Language :: Python :: 3.8", 51 | "Programming Language :: Python :: 3.9", 52 | "Programming Language :: Python :: 3.10", 53 | "Programming Language :: Python :: 3.11", 54 | "Intended Audience :: Developers", 55 | "Development Status :: 4 - Beta", 56 | "License :: OSI Approved :: MIT License", 57 | "Operating System :: OS Independent", 58 | ], 59 | ) 60 | -------------------------------------------------------------------------------- /test/resources/query.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema", 3 | 4 | "type": "object", 5 | 6 | "definitions": { 7 | "_query_ctype": { 8 | "properties": { 9 | "@ctype": { 10 | "enum": [ 11 | "query" 12 | ] 13 | } 14 | } 15 | }, 16 | "query_request": { 17 | "allOf": [ 18 | {"$ref": "file:schema.json#/definitions/_base"}, 19 | {"$ref": "file:schema.json#/definitions/_request"}, 20 | {"$ref": "#/definitions/_query_ctype"} 21 | ] 22 | }, 23 | "query_reply": { 24 | "allOf": [ 25 | {"$ref": "file:schema.json#/definitions/_base"}, 26 | {"$ref": "file:schema.json#/definitions/_reply"}, 27 | {"$ref": "#/definitions/_query_ctype"} 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /test/resources/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/schema#", 3 | "id": "file:///schema.json#", 4 | "definitions": { 5 | "_base": { 6 | "type": "object", 7 | "properties": { 8 | "@version": { 9 | "type": "number" 10 | }, 11 | "@mtype": { 12 | "type": "string", 13 | "enum": [ 14 | "request", 15 | "reply" 16 | ] 17 | }, 18 | "@ctype": { 19 | "type": "string", 20 | "enum": [ 21 | "query", 22 | "ping", 23 | "register" 24 | ] 25 | }, 26 | "header": { 27 | "$ref": "#/definitions/header" 28 | }, 29 | "data": { "type": "object"} 30 | }, 31 | "required": [ 32 | "@version", 33 | "@mtype", 34 | "@ctype", 35 | "header" 36 | ] 37 | }, 38 | "header": { 39 | "type": "object", 40 | "properties": { 41 | "id": { 42 | "type": "string" 43 | }, 44 | "origin": { 45 | "type": "string" 46 | }, 47 | "expires": { 48 | "type": "integer" 49 | } 50 | }, 51 | "required": [ 52 | "id", 53 | "origin", 54 | "expires" 55 | ] 56 | }, 57 | "_reply": { 58 | "properties": { 59 | "@status": { 60 | "type": "string", 61 | "enum": [ 62 | "success", 63 | "failed", 64 | "unauthorized", 65 | "duplicate" 66 | ] 67 | }, 68 | "@mtype": { 69 | "enum": [ 70 | "reply" 71 | ] 72 | } 73 | }, 74 | "required": ["@status"], 75 | "allOf": [ 76 | { 77 | "$ref": "#/definitions/_base" 78 | } 79 | ] 80 | }, 81 | "_request": { 82 | 83 | "allOf": [ 84 | { 85 | "$ref": "#/definitions/_base" 86 | }, 87 | { 88 | "properties": { 89 | "@mtype": { 90 | "enum": [ 91 | "request" 92 | ] 93 | } 94 | } 95 | } 96 | ] 97 | } 98 | }, 99 | 100 | "title": "Message", 101 | "oneOf": [ 102 | { "$ref": "file:query.json#/definitions/query_request" }, 103 | { "$ref": "file:query.json#/definitions/query_reply" } 104 | ] 105 | } 106 | -------------------------------------------------------------------------------- /test/test_229.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | def test_const_properties(): 7 | schema = { 8 | "title": "Example", 9 | "type": "object", 10 | "properties": { 11 | "url": { 12 | "type": "string", 13 | "default": "https://example.com/your-username/my-project", 14 | }, 15 | "type": {"type": "string", "const": "git"}, 16 | }, 17 | } 18 | 19 | ob = pjo.ObjectBuilder(schema) 20 | ns1 = ob.build_classes() 21 | ex = ns1.Example() 22 | ex.url = "can be anything" 23 | 24 | # we expect the value to be set already for const values 25 | assert ex.type == "git" 26 | with pytest.raises(pjo.ValidationError): 27 | # Trying to set the value to something else should throw validation errors 28 | ex.type = "mercurial" 29 | 30 | # setting the value to the const value is a no-op, but permitted 31 | ex.type = "git" 32 | 33 | 34 | def test_const_bare_type(): 35 | schema = { 36 | "title": "Example", 37 | "type": "string", 38 | "const": "I stand alone", 39 | } 40 | 41 | ob = pjo.ObjectBuilder(schema) 42 | ns1 = ob.build_classes() 43 | ex = ns1.Example("I stand alone") 44 | # we expect the value to be set already for const values 45 | assert ex == "I stand alone" 46 | with pytest.raises(pjo.ValidationError): 47 | # Trying to set the value to something else should throw validation errors 48 | ex = ns1.Example("mercurial") 49 | 50 | # setting the value to the const value is a no-op, but permitted 51 | ex = ns1.Example("I stand alone") 52 | -------------------------------------------------------------------------------- /test/test_232.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | def test_default_objects(): 7 | schema = { 8 | "title": "Example", 9 | "type": "object", 10 | "properties": { 11 | "url": { 12 | "type": "string", 13 | "default": "https://example.com/your-username/my-project", 14 | }, 15 | "type": {"type": "string", "default": "git"}, 16 | }, 17 | } 18 | 19 | ob = pjo.ObjectBuilder(schema) 20 | ns1 = ob.build_classes() 21 | ex = ns1.Example() 22 | assert ex.url == "https://example.com/your-username/my-project" 23 | assert ex.type == "git" 24 | 25 | data = {"url": "https://example.com/your-username/my-project2", "type": "hg"} 26 | ex2 = ns1.Example.from_json(json.dumps(data)) 27 | assert ex2.url == "https://example.com/your-username/my-project2" 28 | assert ex2.type == "hg" 29 | -------------------------------------------------------------------------------- /test/test_292.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import pytest 4 | 5 | import python_jsonschema_objects as pjs 6 | import referencing 7 | 8 | SCHEMA_A = { 9 | "$id": "schema-a", 10 | "$schema": "http://json-schema.org/draft-04/schema#", 11 | "title": "Schema A", 12 | "definitions": { 13 | "myint": {"type": "integer", "minimum": 42}, 14 | "myintarray": { 15 | "type": "array", 16 | "items": { 17 | "$ref": "#/definitions/myint", # using 'schema-a#/definitions/myint' would work 18 | }, 19 | }, 20 | "myintref": { 21 | "$ref": "#/definitions/myint", # using 'schema-a#/definitions/myint' would work 22 | }, 23 | }, 24 | "type": "object", 25 | "properties": { 26 | "theint": { 27 | "$ref": "#/definitions/myint", # using 'schema-a#/definitions/myint' would work 28 | }, 29 | }, 30 | } 31 | 32 | 33 | def test_referenced_schema_works_indirectly(): 34 | registry = referencing.Registry().with_resources( 35 | [ 36 | ("schema-a", referencing.Resource.from_contents(SCHEMA_A)), 37 | ] 38 | ) 39 | 40 | # works fine 41 | builder_a = pjs.ObjectBuilder(SCHEMA_A, registry=registry) 42 | namespace_a = builder_a.build_classes(named_only=False) 43 | 44 | b = namespace_a.SchemaA() 45 | b.obja = {"theint": 42} 46 | b.theintarray = [42, 43, 44] 47 | b.theintref = 42 48 | print(b.for_json()) 49 | 50 | 51 | @pytest.mark.parametrize( 52 | "ref", 53 | [ 54 | "schema-a#/definitions/myint", 55 | "schema-a#/definitions/myintref", # This is an interesting variation, because this ref is itself a ref. 56 | ], 57 | ) 58 | def test_referenced_schema_works_directly(ref): 59 | registry = referencing.Registry().with_resources( 60 | [ 61 | ("schema-a", referencing.Resource.from_contents(SCHEMA_A)), 62 | ] 63 | ) 64 | 65 | # WE make a dumb schema that references this 66 | schema = { 67 | "$schema": "http://json-schema.org/draft-04/schema#", 68 | "$id": "test", 69 | "title": "Test", 70 | "type": "object", 71 | "properties": {"name": {"$ref": ref}}, 72 | } 73 | 74 | # works fine 75 | builder_a = pjs.ObjectBuilder(schema, registry=registry) 76 | namespace_a = builder_a.build_classes(named_only=False) 77 | 78 | b = namespace_a.Test() 79 | b.name = 42 80 | print(b.for_json()) 81 | 82 | 83 | def test_you_do_actually_need_a_reference(): 84 | logging.basicConfig(level=logging.DEBUG) 85 | 86 | registry = referencing.Registry().with_resources( 87 | [ 88 | ("schema-a", referencing.Resource.from_contents(SCHEMA_A)), 89 | ] 90 | ) 91 | 92 | # WE make a dumb schema that references 93 | schema = { 94 | "$schema": "http://json-schema.org/draft-04/schema#", 95 | "$id": "test", 96 | "title": "Test", 97 | "type": "object", 98 | "properties": { 99 | "name": { 100 | "$ref": "#/definitions/myint", 101 | } 102 | }, 103 | } 104 | 105 | # works fine 106 | builder_a = pjs.ObjectBuilder(schema, registry=registry) 107 | with pytest.raises(Exception): 108 | # WE would expect this to fail because this isn't actually 109 | # a good reference. Our schema doesn't have a definitions block, 110 | # so schema-a should be a required URI 111 | builder_a.build_classes(named_only=False) 112 | 113 | 114 | def test_regression_292(): 115 | SCHEMA_B = { 116 | "$id": "schema-b", 117 | "$schema": "http://json-schema.org/draft-04/schema#", 118 | "title": "Schema B", 119 | "type": "object", 120 | "definitions": { 121 | "myintref": { 122 | "$ref": "schema-a#/definitions/myint", 123 | }, 124 | }, 125 | "properties": { 126 | # all three properties cause the failure 127 | "obja": { 128 | "$ref": "schema-a", 129 | }, 130 | "theintarray": { 131 | "$ref": "schema-a#/definitions/myintarray", 132 | }, 133 | "thedirectintref": { 134 | "$ref": "schema-a#/definitions/myint", 135 | }, 136 | "theintref": { 137 | "$ref": "#/definitions/myintref", 138 | }, 139 | }, 140 | } 141 | 142 | registry = referencing.Registry().with_resources( 143 | [ 144 | ("schema-a", referencing.Resource.from_contents(SCHEMA_A)), 145 | ] 146 | ) 147 | 148 | # fails 149 | builder_b = pjs.ObjectBuilder(SCHEMA_B, registry=registry) 150 | namespace_b = builder_b.build_classes( 151 | named_only=False 152 | ) # referencing.exceptions.PointerToNowhere: '/definitions/myint' does not exist within SCHEMA_B 153 | 154 | b = namespace_b.SchemaB() 155 | b.obja = {"theint": 42} 156 | b.theintarray = [42, 43, 44] 157 | b.theintref = 42 158 | print(b.for_json()) 159 | -------------------------------------------------------------------------------- /test/test_297.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path 3 | 4 | import python_jsonschema_objects as pjs 5 | 6 | import pytest 7 | 8 | 9 | @pytest.fixture() 10 | def adaptive_card_schema(request) -> dict: 11 | with open( 12 | os.path.join(os.path.dirname(request.path), "resources", "adaptive-card.json") 13 | ) as f: 14 | return json.load(f) 15 | 16 | 17 | """ 18 | This test is expected to fail because the any_of flag is not set. 19 | """ 20 | 21 | 22 | def test_297_expect_fail_without_anyof_flag(adaptive_card_schema): 23 | builder = pjs.ObjectBuilder(adaptive_card_schema) 24 | with pytest.raises(NotImplementedError): 25 | builder.build_classes() 26 | 27 | 28 | @pytest.mark.xfail( 29 | reason="Microsoft apparently doesn't care about valid schemas: https://github.com/microsoft/AdaptiveCards/discussions/8943" 30 | ) 31 | def test_should_work_with_anyof_flag_set(adaptive_card_schema): 32 | builder = pjs.ObjectBuilder(adaptive_card_schema) 33 | ns = builder.build_classes(any_of="use-first") 34 | -------------------------------------------------------------------------------- /test/test_array_validation.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | @pytest.fixture 7 | def arrayClass(): 8 | schema = { 9 | "title": "ArrayVal", 10 | "type": "object", 11 | "properties": { 12 | "min": { 13 | "type": "array", 14 | "items": {"type": "string"}, 15 | "default": [], 16 | "minItems": 1, 17 | }, 18 | "max": { 19 | "type": "array", 20 | "items": {"type": "string"}, 21 | "default": [], 22 | "maxItems": 1, 23 | }, 24 | "both": { 25 | "type": "array", 26 | "items": {"type": "string"}, 27 | "default": [], 28 | "maxItems": 2, 29 | "minItems": 1, 30 | }, 31 | "unique": { 32 | "type": "array", 33 | "items": {"type": "string"}, 34 | "default": [], 35 | "uniqueItems": True, 36 | }, 37 | "reffed": { 38 | "type": "array", 39 | "items": {"$ref": "#/definitions/myref"}, 40 | "minItems": 1, 41 | }, 42 | }, 43 | "definitions": {"myref": {"type": "string"}}, 44 | } 45 | 46 | ns = pjo.ObjectBuilder(schema).build_classes() 47 | return ns["Arrayval"](min=["1"], both=["1"]) 48 | 49 | 50 | def test_validators_work_with_reference(arrayClass): 51 | arrayClass.reffed = ["foo"] 52 | 53 | with pytest.raises(pjo.ValidationError): 54 | arrayClass.reffed = [] 55 | 56 | 57 | def test_array_length_validates(markdown_examples): 58 | builder = pjo.ObjectBuilder( 59 | markdown_examples["Example Schema"], resolved=markdown_examples 60 | ) 61 | ns = builder.build_classes() 62 | 63 | with pytest.raises(pjo.ValidationError): 64 | ns.ExampleSchema( 65 | firstName="Fred", 66 | lastName="Huckstable", 67 | dogs=["Fido", "Spot", "Jasper", "Lady", "Tramp"], 68 | ) 69 | 70 | 71 | def test_minitems(arrayClass): 72 | arrayClass.min = ["1"] 73 | arrayClass.min.append("2") 74 | 75 | with pytest.raises(pjo.ValidationError): 76 | arrayClass.min = [] 77 | 78 | 79 | def test_maxitems(arrayClass): 80 | arrayClass.max = [] 81 | arrayClass.max.append("2") 82 | 83 | assert arrayClass.max == ["2"] 84 | 85 | with pytest.raises(pjo.ValidationError): 86 | arrayClass.max.append("3") 87 | # You have to explicitly validate with append 88 | arrayClass.validate() 89 | 90 | with pytest.raises(pjo.ValidationError): 91 | arrayClass.max = ["45", "42"] 92 | 93 | 94 | def test_unique(arrayClass): 95 | arrayClass.unique = ["hi", "there"] 96 | with pytest.raises(pjo.ValidationError): 97 | arrayClass.unique.append("hi") 98 | # You have to explicitly validate with append 99 | arrayClass.validate() 100 | 101 | with pytest.raises(pjo.ValidationError): 102 | arrayClass.unique = ["Fred", "Fred"] 103 | -------------------------------------------------------------------------------- /test/test_circular_references.py: -------------------------------------------------------------------------------- 1 | import python_jsonschema_objects as pjo 2 | 3 | 4 | def test_circular_references(markdown_examples): 5 | builder = pjo.ObjectBuilder(markdown_examples["Circular References"]) 6 | klasses = builder.build_classes() 7 | a = klasses.A() 8 | b = klasses.B() 9 | a.message = "foo" 10 | b.author = "James Dean" 11 | a.reference = b 12 | a.reference = b 13 | 14 | assert a.reference == b 15 | assert b.oreference is None 16 | assert a.message == "foo" 17 | 18 | serialized = a.serialize(sort_keys=True) 19 | assert serialized == '{"message": "foo", "reference": {"author": "James Dean"}}' 20 | -------------------------------------------------------------------------------- /test/test_default_values.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | import python_jsonschema_objects as pjo 6 | 7 | schema = """ 8 | { 9 | "$schema": "http://json-schema.org/draft-04/schema#", 10 | "title": "Default Test", 11 | "type": "object", 12 | "properties": { 13 | "p1": { 14 | "type": ["integer", "null"], 15 | "default": 0 16 | }, 17 | "p2": { 18 | "type": ["integer", "null"], 19 | "default": null 20 | }, 21 | "p3": { 22 | "type": ["integer", "null"] 23 | } 24 | } 25 | } 26 | """ 27 | 28 | 29 | @pytest.fixture 30 | def schema_json(): 31 | return json.loads(schema) 32 | 33 | 34 | @pytest.fixture 35 | def ns(schema_json): 36 | builder = pjo.ObjectBuilder(schema_json) 37 | ns = builder.build_classes() 38 | return ns 39 | 40 | 41 | def test_defaults_serialize_for_nullable_types(ns): 42 | thing1 = ns.DefaultTest() 43 | 44 | assert thing1.as_dict() == {"p1": 0, "p2": None} 45 | 46 | 47 | def test_nullable_types_are_still_nullable(ns): 48 | thing1 = ns.DefaultTest() 49 | 50 | thing1.p1 = 10 51 | thing1.validate() 52 | assert thing1.as_dict() == {"p1": 10, "p2": None} 53 | 54 | thing1.p1 = None 55 | thing1.validate() 56 | assert thing1.as_dict() == {"p1": None, "p2": None} 57 | 58 | 59 | def test_null_types_without_defaults_do_not_serialize(ns): 60 | thing1 = ns.DefaultTest() 61 | 62 | assert thing1.as_dict() == {"p1": 0, "p2": None} 63 | 64 | thing1.p3 = 10 65 | thing1.validate() 66 | thing1.p1 = None 67 | thing1.validate() 68 | 69 | assert thing1.as_dict() == {"p1": None, "p2": None, "p3": 10} 70 | -------------------------------------------------------------------------------- /test/test_feature_151.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | def test_simple_array_oneOf(): 7 | basicSchemaDefn = { 8 | "$schema": "http://json-schema.org/draft-04/schema#", 9 | "title": "Test", 10 | "properties": { 11 | "SimpleArrayOfNumberOrString": {"$ref": "#/definitions/simparray"} 12 | }, 13 | "required": ["SimpleArrayOfNumberOrString"], 14 | "type": "object", 15 | "definitions": { 16 | "simparray": { 17 | "oneOf": [ 18 | {"type": "array", "items": {"type": "number"}}, 19 | {"type": "array", "items": {"type": "string"}}, 20 | ] 21 | } 22 | }, 23 | } 24 | 25 | builder = pjo.ObjectBuilder(basicSchemaDefn) 26 | 27 | ns = builder.build_classes() 28 | ns.Test().from_json('{"SimpleArrayOfNumberOrString" : [0, 1]}') 29 | ns.Test().from_json('{"SimpleArrayOfNumberOrString" : ["Hi", "There"]}') 30 | 31 | with pytest.raises(pjo.ValidationError): 32 | ns.Test().from_json('{"SimpleArrayOfNumberOrString" : ["Hi", 0]}') 33 | -------------------------------------------------------------------------------- /test/test_feature_177.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | @pytest.fixture 7 | def classes(): 8 | schema = { 9 | "title": "Config", 10 | "type": "object", 11 | "additionalProperties": {"$ref": "#/definitions/Parameter"}, 12 | "definitions": { 13 | "Parameter": { 14 | "$id": "Parameter", 15 | "type": "object", 16 | "properties": { 17 | "key": {"type": "string"}, 18 | "value": {"oneOf": [{"type": "string"}, {"type": "number"}]}, 19 | }, 20 | } 21 | }, 22 | } 23 | 24 | return pjo.ObjectBuilder(schema).build_classes() 25 | 26 | 27 | def test_pjo_objects_can_be_used_as_property_keys(classes): 28 | container = classes.Config() 29 | obj1 = classes.Parameter(key="volume", value=11) 30 | obj2 = classes.Parameter(key="compression", value="on") 31 | 32 | container[obj1.key] = obj1 33 | container[obj2.key] = obj2 34 | 35 | assert container.volume.value == 11 36 | 37 | assert sorted(container.keys()) == sorted(str(k) for k in [obj1.key, obj2.key]) 38 | -------------------------------------------------------------------------------- /test/test_feature_51.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | def test_simple_array_anyOf(): 7 | basicSchemaDefn = { 8 | "$schema": "http://json-schema.org/draft-04/schema#", 9 | "title": "Test", 10 | "properties": {"ExampleAnyOf": {"$ref": "#/definitions/exampleAnyOf"}}, 11 | "required": ["ExampleAnyOf"], 12 | "type": "object", 13 | "definitions": { 14 | "exampleAnyOf": { 15 | # "type": "string", "format": "email" 16 | "anyOf": [ 17 | {"type": "string", "format": "email"}, 18 | {"type": "string", "maxlength": 0}, 19 | ] 20 | } 21 | }, 22 | } 23 | 24 | builder = pjo.ObjectBuilder(basicSchemaDefn) 25 | 26 | ns = builder.build_classes(any_of="use-first") 27 | ns.Test().from_json('{"ExampleAnyOf" : "test@example.com"}') 28 | 29 | with pytest.raises(pjo.ValidationError): 30 | # Because string maxlength 0 is not selected, as we are using the first validation in anyOf: 31 | ns.Test().from_json('{"ExampleAnyOf" : ""}') 32 | # Because this does not match the email format: 33 | ns.Test().from_json('{"ExampleAnyOf" : "not-an-email"}') 34 | 35 | # Does it also work when not deserializing? 36 | x = ns.Test() 37 | with pytest.raises(pjo.ValidationError): 38 | x.ExampleAnyOf = "" 39 | 40 | with pytest.raises(pjo.ValidationError): 41 | x.ExampleAnyOf = "not-an-email" 42 | 43 | x.ExampleAnyOf = "test@example.com" 44 | out = x.serialize() 45 | y = ns.Test.from_json(out) 46 | assert y.ExampleAnyOf == "test@example.com" 47 | 48 | 49 | def test_nested_anyOf(): 50 | basicSchemaDefn = { 51 | "$schema": "http://json-schema.org/draft-04/schema#", 52 | "title": "Test", 53 | "properties": {"ExampleAnyOf": {"$ref": "#/definitions/externalItem"}}, 54 | "required": ["ExampleAnyOf"], 55 | "type": "object", 56 | "definitions": { 57 | "externalItem": { 58 | "type": "object", 59 | "properties": { 60 | "something": {"type": "string"}, 61 | "exampleAnyOf": { 62 | "anyOf": [ 63 | {"type": "string", "format": "email"}, 64 | {"type": "string", "maxlength": 0}, 65 | ] 66 | }, 67 | }, 68 | } 69 | }, 70 | } 71 | 72 | builder = pjo.ObjectBuilder(basicSchemaDefn) 73 | 74 | ns = builder.build_classes(any_of="use-first") 75 | ns.Test().from_json( 76 | '{"ExampleAnyOf" : {"something": "someone", "exampleAnyOf": "test@example.com"} }' 77 | ) 78 | 79 | with pytest.raises(pjo.ValidationError): 80 | # Because this does not match the email format: 81 | ns.Test().from_json( 82 | '{"ExampleAnyOf" : {"something": "someone", "exampleAnyOf": "not-a-email-com"} }' 83 | ) 84 | 85 | # Does it also work when not deserializing? 86 | x = ns.Test(ExampleAnyOf={"something": "somestring"}) 87 | with pytest.raises(pjo.ValidationError): 88 | x.ExampleAnyOf.exampleAnyOf = "" 89 | 90 | with pytest.raises(pjo.ValidationError): 91 | x.ExampleAnyOf.exampleAnyOf = "not-an-email" 92 | 93 | x.ExampleAnyOf.exampleAnyOf = "test@example.com" 94 | out = x.serialize() 95 | y = ns.Test.from_json(out) 96 | assert y.ExampleAnyOf.exampleAnyOf == "test@example.com" 97 | 98 | 99 | def test_simple_array_anyOf_withoutConfig(): 100 | basicSchemaDefn = { 101 | "$schema": "http://json-schema.org/draft-04/schema#", 102 | "title": "Test", 103 | "properties": {"ExampleAnyOf": {"$ref": "#/definitions/exampleAnyOf"}}, 104 | "required": ["ExampleAnyOf"], 105 | "type": "object", 106 | "definitions": { 107 | "exampleAnyOf": { 108 | # "type": "string", "format": "email" 109 | "anyOf": [ 110 | {"type": "string", "format": "email"}, 111 | {"type": "string", "maxlength": 0}, 112 | ] 113 | } 114 | }, 115 | } 116 | 117 | builder = pjo.ObjectBuilder(basicSchemaDefn) 118 | 119 | with pytest.raises(NotImplementedError): 120 | builder.build_classes() 121 | -------------------------------------------------------------------------------- /test/test_nested_arrays.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | 4 | import pytest 5 | from jsonschema import validate 6 | 7 | import python_jsonschema_objects as pjo 8 | 9 | 10 | @pytest.fixture 11 | def nested_arrays(): 12 | return { 13 | "$schema": "http://json-schema.org/draft-04/schema#", 14 | "title": "example", 15 | "properties": { 16 | "foo": { 17 | "type": "array", 18 | "items": { 19 | "type": "array", 20 | # FIXME: not supported anymore in 21 | # https://json-schema.org/draft/2020-12 22 | "items": [{"type": "number"}, {"type": "number"}], 23 | }, 24 | } 25 | }, 26 | } 27 | 28 | 29 | @pytest.fixture 30 | def instance(): 31 | return {"foo": [[42, 44]]} 32 | 33 | 34 | def test_validates(nested_arrays, instance): 35 | validate(instance, nested_arrays) 36 | 37 | 38 | def test_nested_array_regression(nested_arrays, instance): 39 | logging.basicConfig(level=logging.DEBUG) 40 | logging.getLogger().setLevel(logging.DEBUG) 41 | builder = pjo.ObjectBuilder(nested_arrays) 42 | ns = builder.build_classes() 43 | 44 | q = ns.Example.from_json(json.dumps(instance)) 45 | 46 | assert q.serialize() == '{"foo": [[42, 44]]}' 47 | 48 | assert q.as_dict() == {"foo": [[42, 44]]} 49 | 50 | 51 | @pytest.fixture 52 | def complex_schema(): 53 | return json.loads( 54 | r'{"definitions": {"pnu_info": {"required": ["unit_name", "unit_type", "version", "system_time"], "type": "object", "properties": {"unit_type": {"enum": ["Other", "Backpack"], "type": "string"}, "unit_name": {"type": "string"}, "system_time": {"type": "string"}, "version": {"type": "string"}, "recording_state": {"type": "string"}}}, "error": {"additionalProperties": true, "required": ["message"], "type": "object", "properties": {"message": {"type": "string"}}}, "ptu_location": {"required": ["ptu_id", "latitude", "longitude"], "type": "object", "properties": {"latitude": {"type": "number"}, "ptu_id": {"type": "string"}, "longitude": {"type": "number"}, "orientation": {"minimum": 0, "type": "number", "description": "The orientation of this PTU (in degrees). 360 means *unknown*", "maximum": 360}}}, "geopath": {"items": {"required": ["lat", "lng"], "type": "object", "properties": {"lat": {"type": "number"}, "lng": {"type": "number"}}}, "type": "array", "description": "A path described by an ordered\\nlist of lat/long coordinates\\n"}}, "required": ["status", "boundary", "members", "name"], "type": "object", "properties": {"status": {"enum": ["pending", "active", "completed"]}, "boundary": {"$ref": "#/definitions/geopath"}, "name": {"type": "string"}, "members": {"minItems": 1, "items": {"type": "string"}, "type": "array"}}, "title": "mission"}' # noqa: E501 55 | ) 56 | 57 | 58 | def test_array_wrapper(complex_schema): 59 | instance = { 60 | "scenario_config": {"location_master": "MOCK"}, 61 | "status": "pending", 62 | "boundary": [ 63 | {"lat": 38.8821, "lng": -77.11461}, 64 | {"lat": 38.882403, "lng": -77.107867}, 65 | {"lat": 38.876293, "lng": -77.1083}, 66 | {"lat": 38.880834, "lng": -77.115043}, 67 | ], 68 | "name": "Test1", 69 | "members": ["Frobnaz", "MOCK"], 70 | } 71 | 72 | logging.basicConfig(level=logging.DEBUG) 73 | logging.getLogger().setLevel(logging.DEBUG) 74 | builder = pjo.ObjectBuilder(complex_schema) 75 | ns = builder.build_classes() 76 | m = ns.Mission(**instance) 77 | m.validate() 78 | -------------------------------------------------------------------------------- /test/test_nondefault_resolver_validator.py: -------------------------------------------------------------------------------- 1 | import jsonschema.exceptions 2 | import pytest # noqa 3 | import referencing 4 | import referencing.exceptions 5 | import referencing.jsonschema 6 | 7 | import python_jsonschema_objects 8 | import python_jsonschema_objects as pjo 9 | 10 | 11 | def test_custom_spec_validator(markdown_examples): 12 | # This schema shouldn't be valid under DRAFT-03 13 | schema = { 14 | "$schema": "http://json-schema.org/draft-03/schema", 15 | "title": "other", 16 | "type": "any", # this wasn't valid starting in 04 17 | } 18 | pjo.ObjectBuilder( 19 | schema, 20 | resolved=markdown_examples, 21 | ) 22 | 23 | with pytest.raises(jsonschema.exceptions.ValidationError): 24 | pjo.ObjectBuilder( 25 | schema, 26 | specification_uri="http://json-schema.org/draft-04/schema", 27 | resolved=markdown_examples, 28 | ) 29 | 30 | 31 | def test_non_default_resolver_finds_refs(): 32 | registry = referencing.Registry() 33 | 34 | remote_schema = { 35 | "$schema": "http://json-schema.org/draft-04/schema", 36 | "type": "number", 37 | } 38 | registry = registry.with_resource( 39 | "https://example.org/schema/example", 40 | referencing.Resource.from_contents(remote_schema), 41 | ) 42 | 43 | schema = { 44 | "$schema": "http://json-schema.org/draft-04/schema", 45 | "title": "other", 46 | "type": "object", 47 | "properties": { 48 | "local": {"type": "string"}, 49 | "remote": {"$ref": "https://example.org/schema/example"}, 50 | }, 51 | } 52 | 53 | builder = pjo.ObjectBuilder( 54 | schema, 55 | registry=registry, 56 | ) 57 | ns = builder.build_classes() 58 | 59 | thing = ns.Other(local="foo", remote=1) 60 | with pytest.raises(python_jsonschema_objects.ValidationError): 61 | thing = ns.Other(local="foo", remote="NaN") 62 | -------------------------------------------------------------------------------- /test/test_pattern_properties.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | @pytest.fixture 7 | def base_schema(): 8 | return { 9 | "title": "example", 10 | "type": "object", 11 | "properties": {"foobar": {"type": "boolean"}}, 12 | "patternProperties": { 13 | "^foo.*": {"type": "string"}, 14 | "^bar.*": {"type": "integer"}, 15 | }, 16 | } 17 | 18 | 19 | def test_standard_properties_take_precedence(base_schema): 20 | """foobar is a boolean, and it's a standard property, 21 | so we expect it will validate properly as a boolean, 22 | not using the patternProperty that matches it. 23 | """ 24 | builder = pjo.ObjectBuilder(base_schema) 25 | ns = builder.build_classes() 26 | 27 | t = ns.Example(foobar=True) 28 | t.validate() 29 | 30 | with pytest.raises(pjo.ValidationError): 31 | # Try against the foo pattern 32 | x = ns.Example(foobar="hi") 33 | x.validate() 34 | 35 | 36 | @pytest.mark.parametrize( 37 | "permit_addl,property,value,is_error", 38 | [ 39 | (False, "foo", "hello", False), 40 | (False, "foobarro", "hello", False), 41 | (False, "foo", 24, True), 42 | (False, "barkeep", 24, False), 43 | (False, "barkeep", "John", True), 44 | (False, "extraprop", "John", True), 45 | (True, "extraprop", "John", False), 46 | # Test that the pattern props take precedence. 47 | # because these should validate against them, not the 48 | # additionalProperties that match 49 | (True, "foobar", True, False), 50 | (True, "foobar", "John", True), 51 | (True, "foobar", 24, True), 52 | ], 53 | ) 54 | def test_pattern_properties_work(base_schema, permit_addl, property, value, is_error): 55 | base_schema["additionalProperties"] = permit_addl 56 | 57 | builder = pjo.ObjectBuilder(base_schema) 58 | ns = builder.build_classes() 59 | 60 | props = dict([(property, value)]) 61 | 62 | if is_error: 63 | with pytest.raises(pjo.ValidationError): 64 | t = ns.Example(**props) 65 | t.validate() 66 | else: 67 | t = ns.Example(**props) 68 | t.validate() 69 | -------------------------------------------------------------------------------- /test/test_pytest.py: -------------------------------------------------------------------------------- 1 | import json 2 | import logging 3 | import warnings 4 | 5 | import jsonschema 6 | import pytest 7 | 8 | import python_jsonschema_objects as pjs 9 | 10 | logging.basicConfig(level=logging.DEBUG) 11 | 12 | 13 | @pytest.mark.parametrize( 14 | "version, warn, error", 15 | [ 16 | ("http://json-schema.org/schema#", True, True), 17 | ("http://json-schema.org/draft-03/schema#", False, False), 18 | ("http://json-schema.org/draft-04/schema#", False, False), 19 | ("http://json-schema.org/draft-06/schema#", True, False), 20 | ("http://json-schema.org/draft-07/schema#", True, False), 21 | ], 22 | ) 23 | def test_warnings_on_schema_version(version, warn, error): 24 | schema = {"$schema": version, "$id": "test", "type": "object", "properties": {}} 25 | 26 | with warnings.catch_warnings(record=True) as w: 27 | try: 28 | pjs.ObjectBuilder(schema) 29 | except Exception: 30 | assert error == True # noqa 31 | else: 32 | warn_msgs = [str(m.message) for m in w] 33 | present = [ 34 | "Schema version %s not recognized" % version in msg for msg in warn_msgs 35 | ] 36 | if warn: 37 | assert any(present) 38 | else: 39 | assert not any(present) 40 | 41 | 42 | def test_schema_validation(): 43 | """Test that the ObjectBuilder validates the schema itself.""" 44 | schema = { 45 | "$schema": "http://json-schema.org/draft-04/schema#", 46 | "$id": "test", 47 | "type": "object", 48 | "properties": { 49 | "name": "string", # <-- this is invalid 50 | "email": {"oneOf": [{"type": "string"}, {"type": "integer"}]}, 51 | }, 52 | "required": ["email"], 53 | } 54 | with pytest.raises(jsonschema.ValidationError): 55 | pjs.ObjectBuilder(schema) 56 | 57 | 58 | def test_regression_9(): 59 | schema = { 60 | "$schema": "http://json-schema.org/draft-04/schema#", 61 | "$id": "test", 62 | "type": "object", 63 | "properties": { 64 | "name": {"type": "string"}, 65 | "email": {"oneOf": [{"type": "string"}, {"type": "integer"}]}, 66 | }, 67 | "required": ["email"], 68 | } 69 | builder = pjs.ObjectBuilder(schema) 70 | builder.build_classes() 71 | 72 | 73 | def test_build_classes_is_idempotent(): 74 | schema = { 75 | "$schema": "http://json-schema.org/draft-04/schema#", 76 | "title": "test", 77 | "type": "object", 78 | "properties": { 79 | "name": {"$ref": "#/definitions/foo"}, 80 | "email": {"oneOf": [{"type": "string"}, {"type": "integer"}]}, 81 | }, 82 | "required": ["email"], 83 | "definitions": { 84 | "reffed": {"type": "string"}, 85 | "foo": {"type": "array", "items": {"$ref": "#/definitions/reffed"}}, 86 | }, 87 | } 88 | builder = pjs.ObjectBuilder(schema) 89 | _ = builder.build_classes() 90 | builder.build_classes() 91 | 92 | 93 | def test_underscore_properties(): 94 | schema = { 95 | "$schema": "http://json-schema.org/draft-04/schema#", 96 | "title": "AggregateQuery", 97 | "type": "object", 98 | "properties": {"group": {"type": "object", "properties": {}}}, 99 | } 100 | 101 | builder = pjs.ObjectBuilder(schema) 102 | ns = builder.build_classes() 103 | my_obj_type = ns.Aggregatequery 104 | request_object = my_obj_type( 105 | group={ 106 | "_id": {"foo_id": "$foo_id", "foo_type": "$foo_type"}, 107 | "foo": {"$sum": 1}, 108 | } 109 | ) 110 | 111 | assert request_object.group._id == {"foo_id": "$foo_id", "foo_type": "$foo_type"} 112 | 113 | 114 | def test_array_regressions(): 115 | schema = { 116 | "$schema": "http://json-schema.org/draft-04/schema#", 117 | "$id": "test", 118 | "type": "object", 119 | "properties": { 120 | "name": {"type": "string"}, 121 | "email_aliases": { 122 | "type": "object", 123 | "additionalProperties": { 124 | "type": "array", 125 | "items": {"$ref": "#/definitions/foo"}, 126 | }, 127 | }, 128 | }, 129 | "definitions": {"foo": {"type": "string"}}, 130 | } 131 | builder = pjs.ObjectBuilder(schema) 132 | 133 | ns = builder.build_classes() 134 | 135 | x = ns.Test.from_json( 136 | """{"email_aliases": { 137 | "Freddie": ["james", "bond"] 138 | }}""" 139 | ) 140 | x.validate() 141 | 142 | y = ns.Test(email_aliases={"Freddie": ["james", "bond"]}) 143 | y.validate() 144 | 145 | 146 | def test_arrays_can_have_reffed_items_of_mixed_type(): 147 | schema = { 148 | "$schema": "http://json-schema.org/draft-04/schema#", 149 | "$id": "test", 150 | "type": "object", 151 | "properties": { 152 | "list": { 153 | "type": "array", 154 | "items": { 155 | "oneOf": [ 156 | {"$ref": "#/definitions/foo"}, 157 | { 158 | "type": "object", 159 | "properties": {"bar": {"type": "string"}}, 160 | "required": ["bar"], 161 | }, 162 | ] 163 | }, 164 | } 165 | }, 166 | "definitions": {"foo": {"type": "string"}}, 167 | } 168 | builder = pjs.ObjectBuilder(schema) 169 | ns = builder.build_classes() 170 | 171 | x = ns.Test(list=["foo", "bar"]) 172 | ns.Test(list=[{"bar": "nice"}, "bar"]) 173 | with pytest.raises(pjs.ValidationError): 174 | ns.Test(list=[100]) 175 | 176 | assert x.list == ["foo", "bar"] 177 | x.list.append(ns.Foo("bleh")) 178 | assert x.list == ["foo", "bar", "bleh"] 179 | 180 | 181 | def test_regression_39(): 182 | builder = pjs.ObjectBuilder("test/thing-two.json") 183 | ns = builder.build_classes() 184 | 185 | for thing in ("BarMessage", "BarGroup", "Bar", "Header"): 186 | assert thing in ns 187 | 188 | x = ns.BarMessage( 189 | id="message_id", title="my bar group", bars=[{"name": "Freddies Half Shell"}] 190 | ) 191 | 192 | x.validate() 193 | 194 | # Now an invalid one 195 | with pytest.raises(pjs.ValidationError): 196 | ns.BarMessage( 197 | id="message_id", 198 | title="my bar group", 199 | bars=[{"Address": "I should have a name"}], 200 | ) 201 | 202 | 203 | def test_loads_markdown_schema_extraction(markdown_examples): 204 | assert "Other" in markdown_examples 205 | 206 | 207 | def test_object_builder_loads_memory_references(markdown_examples): 208 | builder = pjs.ObjectBuilder(markdown_examples["Other"], resolved=markdown_examples) 209 | assert builder 210 | 211 | with pytest.raises(pjs.ValidationError): 212 | builder.validate({"MyAddress": 1234}) 213 | 214 | builder.validate({"MyAddress": "1234"}) 215 | 216 | 217 | def test_object_builder_reads_all_definitions(markdown_examples): 218 | for nm, ex in markdown_examples.items(): 219 | builder = pjs.ObjectBuilder(ex, resolved=markdown_examples) 220 | assert builder 221 | 222 | 223 | @pytest.fixture 224 | def oneOf(markdown_examples): 225 | builder = pjs.ObjectBuilder(markdown_examples["OneOf"], resolved=markdown_examples) 226 | return builder.classes["Oneof"] 227 | 228 | 229 | @pytest.mark.parametrize( 230 | "json_object", ['{"MyData": "an address"}', '{"MyData": "1234"}'] 231 | ) 232 | def test_oneOf_validates_against_any_valid(oneOf, json_object): 233 | oneOf.from_json(json_object) 234 | 235 | 236 | def test_oneOf_fails_against_non_matching(oneOf): 237 | with pytest.raises(pjs.ValidationError): 238 | oneOf.from_json('{"MyData": 1234.234}') 239 | 240 | 241 | @pytest.fixture 242 | def oneOfBare(markdown_examples): 243 | builder = pjs.ObjectBuilder( 244 | markdown_examples["OneOfBare"], resolved=markdown_examples 245 | ) 246 | return builder.classes["Oneofbare"] 247 | 248 | 249 | @pytest.mark.parametrize( 250 | "json_object", 251 | ['{"MyAddress": "an address"}', '{"firstName": "John", "lastName": "Winnebago"}'], 252 | ) 253 | def test_oneOfBare_validates_against_any_valid(oneOfBare, json_object): 254 | oneOfBare.from_json(json_object) 255 | 256 | 257 | def test_oneOfBare_fails_against_non_matching(oneOfBare): 258 | with pytest.raises(pjs.ValidationError): 259 | oneOfBare.from_json('{"MyData": 1234.234}') 260 | 261 | 262 | @pytest.fixture 263 | def Other(markdown_examples): 264 | builder = pjs.ObjectBuilder(markdown_examples["Other"], resolved=markdown_examples) 265 | assert builder 266 | return builder.classes["Other"] 267 | 268 | 269 | def test_additional_props_allowed_by_default(Person): 270 | person = Person() 271 | person.randomAttribute = 4 272 | assert int(person.randomAttribute) == 4 273 | 274 | 275 | def test_additional_props_permitted_explicitly(markdown_examples): 276 | builder = pjs.ObjectBuilder( 277 | markdown_examples["AddlPropsAllowed"], resolved=markdown_examples 278 | ) 279 | assert builder 280 | 281 | test = builder.classes["Addlpropsallowed"]() 282 | test.randomAttribute = 40 283 | assert int(test.randomAttribute) == 40 284 | 285 | 286 | def test_still_raises_when_accessing_undefined_attrs(Person): 287 | person = Person() 288 | person.firstName = "James" 289 | 290 | # If the attribute doesn't exist, we expect an AttributeError. 291 | with pytest.raises(AttributeError): 292 | print(person.randomFoo) 293 | 294 | # If the attribute is literal-esq, isn't set but isn't required, accessing 295 | # it should be fine. 296 | assert person.gender is None 297 | 298 | # If the attribute is an object, isn't set, but isn't required accessing it 299 | # should throw an exception. 300 | with pytest.raises(AttributeError) as e: 301 | print(person.address.street) 302 | assert "'NoneType' object has no attribute 'street'" in e 303 | 304 | 305 | def test_permits_deletion_of_additional_properties(Person): 306 | person = Person() 307 | person.randomthing = 44 308 | assert int(person.randomthing) == 44 309 | 310 | del person["randomthing"] 311 | 312 | with pytest.raises(AttributeError): 313 | assert person.randomthing is None 314 | 315 | 316 | def test_additional_props_disallowed_explicitly(Other): 317 | other = Other() 318 | with pytest.raises(pjs.ValidationError): 319 | other.randomAttribute = 4 320 | 321 | 322 | def test_objects_can_be_empty(Person): 323 | assert Person() 324 | 325 | 326 | def test_object_equality_should_compare_data(Person): 327 | person = Person(firstName="john") 328 | person2 = Person(firstName="john") 329 | 330 | assert person == person2 331 | 332 | person2.lastName = "Wayne" 333 | assert person != person2 334 | 335 | 336 | def test_object_allows_attributes_in_oncstructor(Person): 337 | person = Person(firstName="James", lastName="Bond", age=35) 338 | 339 | assert person 340 | assert str(person.firstName) == "James" 341 | assert str(person.lastName) == "Bond" 342 | assert int(person.age) == 35 343 | 344 | 345 | def test_object_validates_on_json_decode(Person): 346 | with pytest.raises(pjs.ValidationError): 347 | Person.from_json('{"firstName": "James"}') 348 | 349 | 350 | def test_object_validates_enumerations(Person): 351 | person = Person() 352 | 353 | with pytest.raises(pjs.ValidationError): 354 | person.gender = "robot" 355 | 356 | person.gender = "male" 357 | person.gender = "female" 358 | 359 | 360 | def test_validation_of_mixed_type_enums(Person): 361 | person = Person() 362 | 363 | person.deceased = "yes" 364 | person.deceased = "no" 365 | person.deceased = 1 366 | 367 | with pytest.raises(pjs.ValidationError): 368 | person.deceased = "robot" 369 | 370 | with pytest.raises(pjs.ValidationError): 371 | person.deceased = 2 372 | 373 | with pytest.raises(pjs.ValidationError): 374 | person.deceased = 2.3 375 | 376 | 377 | def test_objects_allow_non_required_attrs_to_be_missing(Person): 378 | person = Person(firstName="James", lastName="Bond") 379 | 380 | assert person 381 | assert str(person.firstName) == "James" 382 | assert str(person.lastName) == "Bond" 383 | 384 | 385 | def test_objects_require_required_attrs_on_validate(Person): 386 | person = Person(firstName="James") 387 | 388 | assert person 389 | 390 | with pytest.raises(pjs.ValidationError): 391 | person.validate() 392 | 393 | 394 | @pytest.fixture(scope="function") 395 | def person_object(Person): 396 | return Person(firstName="James") 397 | 398 | 399 | def test_attribute_access_via_dict(person_object): 400 | name = person_object["firstName"] 401 | assert str(name) == "James" 402 | 403 | 404 | def test_attribute_set_via_dict(person_object): 405 | person_object["firstName"] = "John" 406 | 407 | name = person_object["firstName"] 408 | assert str(name) == "John" 409 | 410 | 411 | def test_keys_can_be_other_pjo_objects(Person, person_object): 412 | person_object.lastName = "Smith" 413 | 414 | jane = Person(firstName="Jane", lastName=person_object.lastName) 415 | 416 | assert jane.lastName == person_object.lastName 417 | 418 | # We don't want the names to be the same literal object though. 419 | # Changing one after assignment should not change the other. 420 | jane.lastName = "Borges" 421 | assert jane.lastName != person_object.lastName 422 | 423 | 424 | def test_numeric_attribute_validation(Person): 425 | with pytest.raises(pjs.ValidationError): 426 | Person(firstName="James", lastName="Bond", age=-10) 427 | 428 | p = Person(firstName="James", lastName="Bond") 429 | with pytest.raises(pjs.ValidationError): 430 | p.age = -1 431 | 432 | 433 | def test_objects_validate_prior_to_serializing(Person): 434 | p = Person(firstName="James", lastName="Bond") 435 | 436 | p._properties["age"] = -1 437 | 438 | with pytest.raises(pjs.ValidationError): 439 | p.serialize() 440 | 441 | 442 | def test_serializing_removes_null_objects(Person): 443 | person = Person(firstName="James", lastName="Bond") 444 | 445 | json_str = person.serialize() 446 | assert "age" not in json.loads(json_str) 447 | 448 | 449 | def test_lists_get_serialized_correctly(Person): 450 | person = Person(firstName="James", lastName="Bond", dogs=["Lassie", "Bobo"]) 451 | 452 | json_str = person.serialize() 453 | assert json.loads(json_str) == { 454 | "firstName": "James", 455 | "lastName": "Bond", 456 | "dogs": ["Lassie", "Bobo"], 457 | } 458 | 459 | 460 | @pytest.mark.parametrize( 461 | "pdict", 462 | [ 463 | dict(firstName="James", lastName="Bond", dogs=["Lassie", "Bobo"]), 464 | dict( 465 | firstName="James", 466 | lastName="Bond", 467 | address={ 468 | "street": "10 Example Street", 469 | "city": "Springfield", 470 | "state": "USA", 471 | }, 472 | dogs=["Lassie", "Bobo"], 473 | ), 474 | ], 475 | ) 476 | def test_dictionary_transformation(Person, pdict): 477 | person = Person(**pdict) 478 | 479 | assert person.as_dict() == pdict 480 | 481 | 482 | def test_strict_mode(): 483 | schema = { 484 | "$schema": "http://json-schema.org/draft-04/schema#", 485 | "type": "object", 486 | "properties": {"firstName": {"type": "string"}, "lastName": {"type": "string"}}, 487 | "$id": "test", 488 | "title": "Name Data", 489 | "required": ["firstName"], 490 | } 491 | builder = pjs.ObjectBuilder(schema) 492 | ns = builder.build_classes() # by defualt strict = False 493 | NameData = ns.NameData 494 | # no strict flag - so should pass even no firstName 495 | NameData(lastName="hello") 496 | with pytest.raises(pjs.ValidationError): 497 | ns = builder.build_classes(strict=True) 498 | NameData = ns.NameData 499 | NameData(lastName="hello") 500 | 501 | 502 | def test_boolean_in_child_object(): 503 | schema = { 504 | "$schema": "http://json-schema.org/draft-04/schema#", 505 | "$id": "test", 506 | "type": "object", 507 | "properties": {"data": {"type": "object", "additionalProperties": True}}, 508 | } 509 | builder = pjs.ObjectBuilder(schema) 510 | ns = builder.build_classes() 511 | 512 | ns.Test(data={"my_bool": True}) 513 | 514 | 515 | @pytest.mark.parametrize( 516 | "default", 517 | [ 518 | '{"type": "boolean", "default": false}', 519 | '{"type": "string", "default": "Hello"}', 520 | '{"type": "integer", "default": 500}', 521 | ], 522 | ) 523 | def test_default_values(default): 524 | default = json.loads(default) 525 | schema = { 526 | "$schema": "http://json-schema.org/draft-04/schema#", 527 | "$id": "test", 528 | "type": "object", 529 | "properties": {"sample": default}, 530 | } 531 | 532 | builder = pjs.ObjectBuilder(schema) 533 | ns = builder.build_classes() 534 | 535 | x = ns.Test() 536 | assert x.sample == default["default"] 537 | 538 | 539 | def test_justareference_example(markdown_examples): 540 | builder = pjs.ObjectBuilder( 541 | markdown_examples["Just a Reference"], resolved=markdown_examples 542 | ) 543 | ns = builder.build_classes() 544 | ns.JustAReference("Hello") 545 | 546 | 547 | def test_number_multiple_of_validation(): 548 | schema = { 549 | "$schema": "http://json-schema.org/draft-04/schema#", 550 | "$id": "test", 551 | "type": "object", 552 | "title": "Base", 553 | "properties": { 554 | "sample": { 555 | "type": "number", 556 | "minimum": 0, 557 | "maximum": 1000000000, 558 | "multipleOf": 0.001, 559 | }, 560 | }, 561 | } 562 | 563 | builder = pjs.ObjectBuilder(schema) 564 | ns = builder.build_classes() 565 | ns.Base(sample=33.069) 566 | -------------------------------------------------------------------------------- /test/test_regression_114.py: -------------------------------------------------------------------------------- 1 | import python_jsonschema_objects as pjo 2 | 3 | 4 | def test_114(): 5 | schema = { 6 | "title": "Example", 7 | "type": "object", 8 | "properties": { 9 | "test_regression_114_anon_array": { 10 | "type": "array", 11 | "items": [ 12 | { 13 | "oneOf": [ 14 | { 15 | "type": "string", 16 | "pattern": r"^(?:(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(?:\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$", # noqa: E501 17 | }, 18 | { 19 | "type": "string", 20 | "pattern": r"^((([0-9A-Fa-f]{1,4}:){1,6}:)|(([0-9A-Fa-f]{1,4}:){7}))([0-9A-Fa-f]{1,4})$", # noqa: E501 21 | }, 22 | ] 23 | } 24 | ], 25 | } 26 | }, 27 | } 28 | 29 | builder = pjo.ObjectBuilder(schema) 30 | test = builder.build_classes() 31 | assert test 32 | -------------------------------------------------------------------------------- /test/test_regression_126.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjs 4 | 5 | 6 | @pytest.fixture 7 | def schema(): 8 | return { 9 | "$schema": "http://json-schema.org/draft-04/schema#", 10 | "title": "Test", 11 | "definitions": { 12 | "MyEnum1": {"type": "string", "enum": ["E_A", "E_B"]}, 13 | "MyEnum2": {"type": "string", "enum": ["F_A", "F_B", "F_C", "F_D"]}, 14 | "MyInt": { 15 | "default": "0", 16 | "type": "integer", 17 | "minimum": 0, 18 | "maximum": 4294967295, 19 | }, 20 | "MyObj1": { 21 | "type": "object", 22 | "properties": { 23 | "e1": {"$ref": "#/definitions/MyEnum1"}, 24 | "e2": {"$ref": "#/definitions/MyEnum2"}, 25 | "i1": {"$ref": "#/definitions/MyInt"}, 26 | }, 27 | "required": ["e1", "e2", "i1"], 28 | }, 29 | "MyArray": { 30 | "type": "array", 31 | "items": {"$ref": "#/definitions/MyObj1"}, 32 | "minItems": 0, 33 | "uniqueItems": True, 34 | }, 35 | "MyMsg1": { 36 | "type": "object", 37 | "properties": {"a1": {"$ref": "#/definitions/MyArray"}}, 38 | }, 39 | "MyMsg2": {"type": "object", "properties": {"s1": {"type": "string"}}}, 40 | }, 41 | "type": "object", 42 | "oneOf": [{"$ref": "#/definitions/MyMsg1"}, {"$ref": "#/definitions/MyMsg2"}], 43 | } 44 | 45 | 46 | def test_regression_126(schema): 47 | builder = pjs.ObjectBuilder(schema) 48 | ns = builder.build_classes(standardize_names=False) 49 | 50 | Obj1 = ns.MyObj1 51 | Array1 = ns.MyArray 52 | Msg1 = ns.MyMsg1 53 | o1 = Obj1(e1="E_A", e2="F_C", i1=2600) 54 | o2 = Obj1(e1="E_B", e2="F_D", i1=2500) 55 | objs = Array1([o1, o2]) 56 | msg = Msg1(a1=objs) 57 | 58 | print(msg.serialize()) 59 | -------------------------------------------------------------------------------- /test/test_regression_133.py: -------------------------------------------------------------------------------- 1 | import python_jsonschema_objects as pjs 2 | 3 | 4 | def test_nested_arrays_work_fine(): 5 | schema = { 6 | "title": "Example Schema", 7 | "type": "object", 8 | "properties": { 9 | "a": {"type": "array", "items": {"type": "string"}, "default": []} 10 | }, 11 | } 12 | 13 | ns1 = pjs.ObjectBuilder(schema).build_classes() 14 | 15 | foo = ns1.ExampleSchema() 16 | foo.a.append("foo") 17 | print(foo.for_json()) 18 | 19 | assert foo.a == ["foo"] 20 | 21 | bar = ns1.ExampleSchema() 22 | bar.a.append("bar") 23 | print(bar.for_json()) 24 | 25 | assert bar.a == ["bar"] 26 | -------------------------------------------------------------------------------- /test/test_regression_143.py: -------------------------------------------------------------------------------- 1 | import python_jsonschema_objects as pjs 2 | 3 | 4 | def test_limited_validation(mocker): 5 | schema = { 6 | "title": "Example Schema", 7 | "type": "object", 8 | "properties": { 9 | "a": {"type": "array", "items": {"type": "string"}, "default": []} 10 | }, 11 | } 12 | 13 | ob = pjs.ObjectBuilder(schema) 14 | ns1 = ob.build_classes() 15 | validator = ns1.ExampleSchema.a.info["validator"]([]) 16 | validate_items = mocker.patch.object( 17 | validator, "validate_items", side_effect=validator.validate_items 18 | ) 19 | mocker.patch.dict( 20 | ns1.ExampleSchema.a.info, 21 | {"validator": mocker.MagicMock(return_value=validator)}, 22 | ) 23 | 24 | foo = ns1.ExampleSchema() 25 | # We expect validation to be called on creation 26 | assert validate_items.call_count == 1 27 | 28 | # We expect manipulation to not revalidate immediately without strict 29 | foo.a.append("foo") 30 | foo.a.append("bar") 31 | assert validate_items.call_count == 1 32 | 33 | # We expect accessing data elements to cause a revalidate, but only the first time 34 | print(foo.a[0]) 35 | assert validate_items.call_count == 2 36 | 37 | print(foo.a) 38 | assert foo.a == ["foo", "bar"] 39 | assert validate_items.call_count == 2 40 | 41 | 42 | def test_strict_validation(mocker): 43 | """Validate that when specified as strict, validation still occurs on every 44 | change. 45 | """ 46 | schema = { 47 | "title": "Example Schema", 48 | "type": "object", 49 | "properties": { 50 | "a": {"type": "array", "items": {"type": "string"}, "default": []} 51 | }, 52 | } 53 | 54 | ob = pjs.ObjectBuilder(schema) 55 | ns1 = ob.build_classes(strict=True) 56 | validator = ns1.ExampleSchema.a.info["validator"]([]) 57 | validate_items = mocker.patch.object( 58 | validator, "validate_items", side_effect=validator.validate_items 59 | ) 60 | mocker.patch.dict( 61 | ns1.ExampleSchema.a.info, 62 | {"validator": mocker.MagicMock(return_value=validator)}, 63 | ) 64 | 65 | foo = ns1.ExampleSchema() 66 | # We expect validation to be called on creation. 67 | assert validate_items.call_count == 1 68 | 69 | # We expect manipulation to revalidate immediately with strict. 70 | foo.a.append("foo") 71 | foo.a.append("bar") 72 | assert validate_items.call_count == 3 73 | 74 | # We expect accessing data elements to not revalidate because strict would 75 | # have revalidated on load. 76 | print(foo.a[0]) 77 | print(foo.a) 78 | assert foo.a == ["foo", "bar"] 79 | -------------------------------------------------------------------------------- /test/test_regression_156.py: -------------------------------------------------------------------------------- 1 | import python_jsonschema_objects as pjo 2 | 3 | 4 | def test_regression_156(markdown_examples): 5 | builder = pjo.ObjectBuilder( 6 | markdown_examples["MultipleObjects"], resolved=markdown_examples 7 | ) 8 | classes = builder.build_classes(named_only=True) 9 | 10 | er = classes.ErrorResponse(message="Danger!", status=99) 11 | vgr = classes.VersionGetResponse(local=False, version="1.2.3") 12 | 13 | # round-trip serialize-deserialize into named classes 14 | classes.ErrorResponse.from_json(er.serialize()) 15 | classes.VersionGetResponse.from_json(vgr.serialize()) 16 | 17 | # round-trip serialize-deserialize into class defined with `oneOf` 18 | classes.Multipleobjects.from_json(er.serialize()) 19 | classes.Multipleobjects.from_json(vgr.serialize()) 20 | 21 | 22 | def test_toplevel_oneof_gets_a_name(markdown_examples): 23 | builder = pjo.ObjectBuilder( 24 | markdown_examples["MultipleObjects"], resolved=markdown_examples 25 | ) 26 | classes = builder.build_classes(named_only=True) 27 | assert classes.Multipleobjects.__title__ is not None 28 | -------------------------------------------------------------------------------- /test/test_regression_165.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjs 4 | 5 | 6 | @pytest.fixture 7 | def testclass(): 8 | builder = pjs.ObjectBuilder({"title": "Test", "type": "object"}) 9 | ns = builder.build_classes() 10 | return ns.Test() 11 | 12 | 13 | def test_extra_properties_can_be_deleted_with_item_syntax(testclass): 14 | # Deletion before setting should throw attribute errors 15 | with pytest.raises(AttributeError): 16 | del testclass["foo"] 17 | 18 | testclass.foo = 42 19 | assert testclass.foo == 42 20 | del testclass["foo"] 21 | 22 | # Etestclasstra properties not set should raise AttributeErrors when accessed 23 | with pytest.raises(AttributeError): 24 | testclass.foo 25 | 26 | 27 | def test_extra_properties_can_be_deleted_with_attribute_syntax(testclass): 28 | # Deletion before setting should throw attribute errors 29 | with pytest.raises(AttributeError): 30 | del testclass.foo 31 | 32 | testclass.foo = 42 33 | assert testclass.foo == 42 34 | del testclass.foo 35 | 36 | # Extra properties not set should raise AttributeErrors when accessed 37 | with pytest.raises(AttributeError): 38 | testclass.foo 39 | 40 | 41 | def test_extra_properties_can_be_deleted_directly(testclass): 42 | # Deletion before setting should throw attribute errors 43 | with pytest.raises(AttributeError): 44 | delattr(testclass, "foo") 45 | 46 | testclass.foo = 42 47 | assert testclass.foo == 42 48 | delattr(testclass, "foo") 49 | 50 | # Extra properties not set should raise AttributeErrors when accessed 51 | with pytest.raises(AttributeError): 52 | testclass.foo 53 | 54 | 55 | def test_unrequired_real_properties_arent_really_deleted(Person): 56 | p = Person(age=20) 57 | 58 | assert p.age == 20 59 | del p.age 60 | assert p.age is None 61 | 62 | p.age = 20 63 | assert p.age == 20 64 | delattr(p, "age") 65 | assert p.age is None 66 | 67 | p.age = 20 68 | assert p.age == 20 69 | del p["age"] 70 | assert p.age is None 71 | 72 | 73 | def test_required_real_properties_throw_attributeerror_on_delete(Person): 74 | p = Person(firstName="Fred") 75 | 76 | assert p.firstName == "Fred" 77 | with pytest.raises(AttributeError): 78 | del p.firstName 79 | 80 | p.firstName = "Fred" 81 | assert p.firstName == "Fred" 82 | with pytest.raises(AttributeError): 83 | delattr(p, "firstName") 84 | 85 | p.firstName = "Fred" 86 | assert p.firstName == "Fred" 87 | with pytest.raises(AttributeError): 88 | del p["firstName"] 89 | -------------------------------------------------------------------------------- /test/test_regression_17.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | @pytest.fixture 7 | def test_class(): 8 | schema = { 9 | "title": "Example", 10 | "properties": { 11 | "claimed_by": { 12 | "$id": "claimed", 13 | "type": ["string", "integer", "null"], 14 | "description": "Robots Only. The human agent that has claimed this robot.", # noqa: E501 15 | } 16 | }, 17 | } 18 | 19 | builder = pjo.ObjectBuilder(schema) 20 | ns = builder.build_classes() 21 | return ns 22 | 23 | 24 | @pytest.mark.parametrize("value", ["Hi", 4, None]) 25 | def test_properties_can_have_multiple_types(test_class, value): 26 | test_class.Example(claimed_by=value) 27 | 28 | 29 | @pytest.mark.parametrize("value", [2.4]) 30 | def test_multiply_typed_properties_still_validate(test_class, value): 31 | with pytest.raises(pjo.ValidationError): 32 | test_class.Example(claimed_by=value) 33 | -------------------------------------------------------------------------------- /test/test_regression_185.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import pprint 4 | 5 | import jsonschema 6 | 7 | import python_jsonschema_objects as pjs 8 | 9 | pp = pprint.PrettyPrinter(indent=2) 10 | 11 | lpid_class_schema = { 12 | "$id": "https://example.com/schema-repository/lpid", 13 | "$schema": "http://json-schema.org/draft-04/schema#", 14 | "type": "object", 15 | "title": "Logical Process ID", 16 | "properties": {"lpid_str": {"type": "string"}}, 17 | "required": ["lpid_str"], 18 | "additionalProperties": True, 19 | } 20 | 21 | 22 | class A(object): 23 | a_str = None 24 | 25 | def __init__(self, a_str): 26 | self.a_str = a_str 27 | 28 | 29 | def test_regression_185_deepcopy(): 30 | # Class building and instantiation work great and round-trip. 31 | lpid_class_builder = pjs.ObjectBuilder(lpid_class_schema) 32 | LPID = lpid_class_builder.classes.LogicalProcessId 33 | lpid = LPID(lpid_str="foobaz") 34 | instance = json.loads(lpid.serialize()) 35 | jsonschema.validate(lpid_class_schema, instance) 36 | 37 | # Deep-copying seems to work with instances of normal popos. 38 | a = A(a_str="barqux") 39 | assert a 40 | a_2 = copy.deepcopy(a) 41 | assert a_2 42 | 43 | # But deep-copying with objects of generated classes doesn't terminate. 44 | lpid_2 = copy.deepcopy(lpid) 45 | assert repr(lpid_2) == repr(lpid) 46 | -------------------------------------------------------------------------------- /test/test_regression_208.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | import python_jsonschema_objects as pjo 6 | 7 | schema = """ 8 | { 9 | "$schema": "http://json-schema.org/draft-04/schema#", 10 | "$id": "roundtrip.json", 11 | "type": "object", 12 | "properties": 13 | { 14 | "container": {"$ref": "#/definitions/config"} 15 | }, 16 | "definitions": { 17 | "config": { 18 | "properties":{ 19 | "something": { 20 | "allOf": [ 21 | { 22 | "$ref": "#/definitions/params" 23 | }, 24 | { 25 | "properties":{ 26 | "parameters": { 27 | "oneOf": [ 28 | { 29 | "$ref": "#/definitions/parametersA" 30 | }, 31 | { 32 | "$ref": "#/definitions/parametersB" 33 | } 34 | ] 35 | } 36 | }, 37 | "required": ["parameters"] 38 | } 39 | ] 40 | } 41 | } 42 | }, 43 | "params": { 44 | "type": "object", 45 | "properties": { 46 | "param1": { 47 | "type": "string" 48 | }, 49 | "param2": { 50 | "type": "string" 51 | } 52 | }, 53 | "required": [ 54 | "param1", 55 | "param2" 56 | ] 57 | }, 58 | "parametersA": { 59 | "properties": { 60 | "name": { 61 | "type": "string" 62 | }, 63 | "value": { 64 | "type": "string" 65 | } 66 | }, 67 | "required": [ 68 | "name", 69 | "value" 70 | ] 71 | }, 72 | "parametersB": { 73 | "properties": { 74 | "something": { 75 | "type": "string" 76 | }, 77 | "another": { 78 | "type": "string" 79 | } 80 | }, 81 | "required": [ 82 | "something", 83 | "another" 84 | ] 85 | } 86 | } 87 | } 88 | """ 89 | 90 | 91 | @pytest.fixture 92 | def schema_json(): 93 | return json.loads(schema) 94 | 95 | 96 | def test_roundtrip_oneof_serializer(schema_json): 97 | builder = pjo.ObjectBuilder(schema_json) 98 | namespace = builder.build_classes() 99 | 100 | data_config = """ 101 | { 102 | "something": "a name", 103 | "another": "a value" 104 | } 105 | """ 106 | paramsTypeB = namespace.Parametersb().from_json(data_config) 107 | somethingInstance = namespace.Something(param1="toto", param2="tata") 108 | somethingInstance.parameters = paramsTypeB 109 | 110 | json_object = somethingInstance.serialize() 111 | print(json_object) 112 | 113 | aNewsomething = namespace.Something.from_json(json_object) 114 | 115 | json_object2 = aNewsomething.serialize() 116 | assert json_object == json_object2 117 | -------------------------------------------------------------------------------- /test/test_regression_213.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | import python_jsonschema_objects as pjo 6 | 7 | schema = """ 8 | { 9 | "title":"whatever", 10 | "properties": { 11 | "test": {"type": "number"} 12 | } 13 | } 14 | """ 15 | 16 | 17 | @pytest.fixture 18 | def schema_json(): 19 | return json.loads(schema) 20 | 21 | 22 | def test_literals_support_comparisons(schema_json): 23 | builder = pjo.ObjectBuilder(schema_json) 24 | ns = builder.build_classes() 25 | 26 | thing1 = ns.Whatever() 27 | thing2 = ns.Whatever() 28 | 29 | thing1.test = 10 30 | thing2.test = 12 31 | 32 | assert thing1.test < thing2.test 33 | assert thing1.test == thing1.test 34 | 35 | thing2.test = 10 36 | 37 | assert thing1.test <= thing2.test 38 | assert thing1.test == thing2.test 39 | -------------------------------------------------------------------------------- /test/test_regression_214.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | schema = { 6 | "$schema": "http://json-schema.org/draft-07/schema", 7 | "title": "myschema", 8 | "type": "object", 9 | "definitions": { 10 | "MainObject": { 11 | "title": "Main Object", 12 | "additionalProperties": False, 13 | "type": "object", 14 | "properties": { 15 | "location": { 16 | "title": "location", 17 | "type": ["object", "string"], 18 | "oneOf": [ 19 | {"$ref": "#/definitions/UNIQUE_STRING"}, 20 | {"$ref": "#/definitions/Location"}, 21 | ], 22 | } 23 | }, 24 | }, 25 | "Location": { 26 | "title": "Location", 27 | "description": "A Location represents a span on a specific sequence.", 28 | "type": "object", 29 | "oneOf": [ 30 | {"$ref": "#/definitions/Location1"}, 31 | {"$ref": "#/definitions/Location2"}, 32 | ], 33 | "discriminator": {"propertyName": "type"}, 34 | }, 35 | "Location1": { 36 | "additionalProperties": False, 37 | "type": "object", 38 | "properties": { 39 | "type": { 40 | "type": "string", 41 | "enum": ["Location1"], 42 | "default": "Location1", 43 | } 44 | }, 45 | }, 46 | "Location2": { 47 | "additionalProperties": False, 48 | "type": "object", 49 | "properties": { 50 | "type": { 51 | "type": "string", 52 | "enum": ["Location2"], 53 | "default": "Location2", 54 | } 55 | }, 56 | }, 57 | "UNIQUE_STRING": { 58 | "additionalProperties": False, 59 | "type": "string", 60 | "pattern": r"^\w[^:]*:.+$", 61 | }, 62 | }, 63 | } 64 | 65 | 66 | @pytest.fixture 67 | def schema_json(): 68 | return schema 69 | 70 | 71 | def test_nested_oneofs_still_work(schema_json): 72 | builder = pjo.ObjectBuilder(schema_json) 73 | ns = builder.build_classes() 74 | 75 | obj1 = ns.MainObject(**{"location": {"type": "Location1"}}) 76 | obj2 = ns.MainObject(**{"location": {"type": "Location2"}}) 77 | obj3 = ns.MainObject(**{"location": "unique:12"}) 78 | 79 | assert obj1.location.type == "Location1" 80 | assert obj2.location.type == "Location2" 81 | assert obj3.location == "unique:12" 82 | -------------------------------------------------------------------------------- /test/test_regression_218.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import json 3 | import python_jsonschema_objects as pjo 4 | 5 | import logging 6 | 7 | logging.basicConfig(level=logging.DEBUG) 8 | logging.getLogger().setLevel(logging.DEBUG) 9 | 10 | schema = """ 11 | { 12 | "$schema": "http://json-schema.org/draft-04/schema#", 13 | "$id": "schema.json", 14 | "title":"Null Test", 15 | "type": "object", 16 | "$ref": "#/definitions/test", 17 | "definitions": { 18 | "test": { 19 | "type": "object", 20 | "properties": { 21 | "name": { 22 | "type": "string", 23 | "default": "String" 24 | }, 25 | "id": { 26 | "type": "null", 27 | "default": null 28 | } 29 | } 30 | } 31 | } 32 | } 33 | """ 34 | 35 | 36 | @pytest.fixture 37 | def schema_json(): 38 | return json.loads(schema) 39 | 40 | 41 | @pytest.fixture 42 | def ns(schema_json): 43 | builder = pjo.ObjectBuilder(schema_json) 44 | ns = builder.build_classes() 45 | return ns 46 | 47 | 48 | def test_defaults_serialize_for_nullable_types(ns): 49 | logging.basicConfig(level=logging.DEBUG) 50 | logging.getLogger().setLevel(logging.DEBUG) 51 | thing1 = ns.NullTest() 52 | 53 | serialized = thing1.as_dict() 54 | print(serialized) 55 | assert json.dumps(serialized) == """{"name": "String", "id": null}""" 56 | serialized = thing1.serialize() 57 | assert serialized == """{"name": "String", "id": null}""" 58 | -------------------------------------------------------------------------------- /test/test_regression_232.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | schema = { 6 | "$schema": "http://json-schema.org/draft-07/schema", 7 | "title": "myschema", 8 | "type": "object", 9 | "definitions": { 10 | "RefObject": { 11 | "title": "Ref Object", 12 | "properties": {"location": {"$ref": "#/definitions/Location"}}, 13 | }, 14 | "MapObject": { 15 | "title": "Map Object", 16 | "additionalProperties": {"$ref": "#/definitions/Location"}, 17 | }, 18 | "MainObject": { 19 | "title": "Main Object", 20 | "additionalProperties": False, 21 | "type": "object", 22 | "properties": { 23 | "location": { 24 | "title": "location", 25 | "oneOf": [ 26 | {"$ref": "#/definitions/UNIQUE_STRING"}, 27 | {"$ref": "#/definitions/Location"}, 28 | ], 29 | } 30 | }, 31 | }, 32 | "Location": { 33 | "title": "Location", 34 | "description": "A Location represents a span on a specific sequence.", 35 | "oneOf": [ 36 | {"$ref": "#/definitions/LocationIdentifier"}, 37 | {"$ref": "#/definitions/LocationTyped"}, 38 | ], 39 | }, 40 | "LocationIdentifier": { 41 | "type": "integer", 42 | "minimum": 1, 43 | }, 44 | "LocationTyped": { 45 | "additionalProperties": False, 46 | "type": "object", 47 | "properties": { 48 | "type": { 49 | "type": "string", 50 | "enum": ["Location"], 51 | "default": "Location", 52 | } 53 | }, 54 | }, 55 | "UNIQUE_STRING": { 56 | "additionalProperties": False, 57 | "type": "string", 58 | "pattern": r"^\w[^:]*:.+$", 59 | }, 60 | }, 61 | } 62 | 63 | 64 | @pytest.fixture 65 | def schema_json(): 66 | return schema 67 | 68 | 69 | def test_nested_oneof_with_different_types(schema_json): 70 | builder = pjo.ObjectBuilder(schema_json) 71 | ns = builder.build_classes() 72 | 73 | test1 = {"location": 12345} 74 | test2 = {"location": {"type": "Location"}} 75 | test3 = {"location": "unique:12"} 76 | 77 | obj1 = ns.MainObject(**test1) 78 | obj2 = ns.MainObject(**test2) 79 | obj3 = ns.MainObject(**test3) 80 | 81 | assert obj1.location == 12345 82 | assert obj2.location.type == "Location" 83 | assert obj3.location == "unique:12" 84 | 85 | 86 | def test_nested_oneof_with_different_types_by_reference(schema_json): 87 | builder = pjo.ObjectBuilder(schema_json) 88 | ns = builder.build_classes() 89 | 90 | test1 = {"location": 12345} 91 | test2 = {"location": {"type": "Location"}} 92 | 93 | obj1 = ns.RefObject(**test1) 94 | obj2 = ns.RefObject(**test2) 95 | 96 | assert obj1.location == 12345 97 | assert obj2.location.type == "Location" 98 | 99 | 100 | def test_nested_oneof_with_different_types_in_additional_properties(schema_json): 101 | builder = pjo.ObjectBuilder(schema_json) 102 | ns = builder.build_classes() 103 | 104 | x_prop_name = "location-id" 105 | 106 | test1 = {x_prop_name: 12345} 107 | test2 = {x_prop_name: {"type": "Location"}} 108 | 109 | obj1 = ns.MapObject(**test1) 110 | obj2 = ns.MapObject(**test2) 111 | 112 | assert obj1[x_prop_name] == 12345 113 | assert obj2[x_prop_name].type == "Location" 114 | -------------------------------------------------------------------------------- /test/test_regression_49.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | from jsonschema import validate 5 | 6 | import python_jsonschema_objects as pjo 7 | 8 | 9 | @pytest.fixture 10 | def bad_schema_49(): 11 | return { 12 | "title": "example", 13 | "type": "array", 14 | "items": { 15 | "oneOf": [ 16 | { 17 | "type": "object", 18 | "properties": {"a": {"type": "string"}}, 19 | "required": ["a"], 20 | }, 21 | { 22 | "type": "object", 23 | "properties": {"b": {"type": "string"}}, 24 | "required": ["b"], 25 | }, 26 | ] 27 | }, 28 | } 29 | 30 | 31 | @pytest.fixture 32 | def instance(): 33 | return [{"a": ""}, {"b": ""}] 34 | 35 | 36 | def test_is_valid_jsonschema(bad_schema_49, instance): 37 | validate(instance, bad_schema_49) 38 | 39 | 40 | def test_regression_49(bad_schema_49, instance): 41 | builder = pjo.ObjectBuilder(bad_schema_49) 42 | ns = builder.build_classes() 43 | 44 | ns.Example.from_json(json.dumps(instance)) 45 | -------------------------------------------------------------------------------- /test/test_regression_8.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | @pytest.fixture 7 | def test_instance(): 8 | schema = { 9 | "title": "Example", 10 | "properties": { 11 | "stringProp": {"type": "string"}, 12 | "arrayProp": {"type": "array", "items": {"type": "string"}}, 13 | }, 14 | } 15 | 16 | builder = pjo.ObjectBuilder(schema) 17 | ns = builder.build_classes() 18 | instance = ns.Example( 19 | stringProp="This seems fine", arrayProp=["these", "are", "problematic"] 20 | ) 21 | return instance 22 | 23 | 24 | def test_string_properties_compare_to_strings(test_instance): 25 | test = test_instance.stringProp == "This seems fine" 26 | assert test 27 | 28 | 29 | def test_arrays_of_strings_compare_to_strings(test_instance): 30 | test = test_instance.arrayProp == ["these", "are", "problematic"] 31 | assert test 32 | 33 | 34 | def test_arrays_can_be_extended(test_instance): 35 | test_instance.arrayProp.extend(["...", "maybe", "not"]) 36 | test = test_instance.arrayProp == [ 37 | "these", 38 | "are", 39 | "problematic", 40 | "...", 41 | "maybe", 42 | "not", 43 | ] 44 | assert test 45 | 46 | 47 | def test_array_elements_compare_to_types(test_instance): 48 | elem = test_instance.arrayProp[0] 49 | test = elem == "these" 50 | assert test 51 | 52 | 53 | def test_repr_shows_property_values(test_instance): 54 | expected = " these>" 55 | assert repr(test_instance.arrayProp[0]) == expected 56 | 57 | 58 | def test_str_shows_just_strings(test_instance): 59 | test = str(test_instance.arrayProp[0]) 60 | assert test == "these" 61 | -------------------------------------------------------------------------------- /test/test_regression_87.py: -------------------------------------------------------------------------------- 1 | import python_jsonschema_objects as pjs 2 | 3 | 4 | def test_multiple_objects_are_defined(markdown_examples): 5 | builder = pjs.ObjectBuilder( 6 | markdown_examples["MultipleObjects"], resolved=markdown_examples 7 | ) 8 | 9 | assert builder 10 | classes = builder.build_classes() 11 | assert "ErrorResponse" in classes 12 | assert "VersionGetResponse" in classes 13 | print(dir(classes)) 14 | -------------------------------------------------------------------------------- /test/test_regression_88.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import python_jsonschema_objects as pjs 4 | 5 | 6 | def test_nested_arrays_work_fine(): 7 | schema = { 8 | "$schema": "http://json-schema.org/draft-04/schema", 9 | "title": "Example1", 10 | "type": "object", 11 | "properties": { 12 | "name": { 13 | "type": "array", 14 | "items": {"type": "object", "properties": {"name": {"type": "string"}}}, 15 | } 16 | }, 17 | } 18 | 19 | ns1 = pjs.ObjectBuilder(schema).build_classes() 20 | j1 = ns1.Example1.from_json( 21 | json.dumps({"name": [{"value": "foo"}, {"value": "bar"}]}) 22 | ) 23 | assert j1.name[0].value == "foo" 24 | assert j1.name[1].value == "bar" 25 | 26 | 27 | def test_top_level_arrays_are_converted_to_objects_properly(): 28 | schema = { 29 | "$schema": "http://json-schema.org/draft-04/schema", 30 | "title": "Example2", 31 | "type": "array", 32 | "items": {"type": "object", "properties": {"name": {"type": "string"}}}, 33 | } 34 | 35 | ns2 = pjs.ObjectBuilder(schema).build_classes() 36 | j2 = ns2.Example2.from_json(json.dumps([{"name": "foo"}, {"name": "bar"}])) 37 | assert not isinstance(j2[0], dict) # Out[173]: {'name': 'foo'} 38 | assert j2[0].name == "foo" 39 | assert j2[1].name == "bar" 40 | -------------------------------------------------------------------------------- /test/test_regression_89.py: -------------------------------------------------------------------------------- 1 | import python_jsonschema_objects as pjs 2 | 3 | 4 | def test_one_of_types(): 5 | schema = { 6 | "$schema": "http://json-schema.org/draft-04/schema", 7 | "title": "Example1", 8 | "type": "object", 9 | "properties": {"foo": {"oneOf": [{"type": "string"}, {"type": "null"}]}}, 10 | } 11 | 12 | ns1 = pjs.ObjectBuilder(schema).build_classes() 13 | ns1.Example1(foo="bar") 14 | ns1.Example1(foo=None) 15 | -------------------------------------------------------------------------------- /test/test_regression_90.py: -------------------------------------------------------------------------------- 1 | import python_jsonschema_objects as pjs 2 | 3 | 4 | def test_null_type(): 5 | schema = { 6 | "$schema": "http://json-schema.org/draft-04/schema", 7 | "title": "Example1", 8 | "type": "object", 9 | "properties": {"foo": {"type": "null"}}, 10 | "required": ["foo"], 11 | } 12 | 13 | ns1 = pjs.ObjectBuilder(schema).build_classes(strict=True) 14 | ns1.Example1(foo=None) 15 | 16 | 17 | def test_null_type_one_of(): 18 | schema = { 19 | "$schema": "http://json-schema.org/draft-04/schema", 20 | "title": "Example1", 21 | "type": "object", 22 | "properties": {"foo": {"oneOf": [{"type": "string"}, {"type": "null"}]}}, 23 | "required": ["foo"], 24 | } 25 | 26 | ns1 = pjs.ObjectBuilder(schema).build_classes(strict=True) 27 | ns1.Example1(foo="bar") 28 | ns1.Example1(foo=None) 29 | -------------------------------------------------------------------------------- /test/test_util_pytest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from python_jsonschema_objects.validators import ValidationError 4 | from python_jsonschema_objects.wrapper_types import ArrayWrapper 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "kwargs", 9 | [ 10 | {}, 11 | {"item_constraint": {"type": "string"}}, 12 | {"item_constraint": [{"type": "string"}, {"type": "string"}]}, 13 | ], 14 | ) 15 | def test_ArrayValidator_initializer(kwargs): 16 | assert ArrayWrapper.create("hello", **kwargs) 17 | 18 | 19 | def test_ArrayValidator_throws_error_if_not_classes_or_dicts(): 20 | with pytest.raises(TypeError): 21 | ArrayWrapper.create("hello", item_constraint=["winner"]) 22 | 23 | 24 | def test_validate_basic_array_types(): 25 | validator = ArrayWrapper.create("test", item_constraint={"type": "number"}) 26 | 27 | instance = validator([1, 2, 3, 4]) 28 | 29 | instance.validate() 30 | 31 | instance = validator([1, 2, "Hello"]) 32 | with pytest.raises(ValidationError): 33 | instance.validate() 34 | 35 | 36 | def test_validate_basic_tuple__types(): 37 | validator = ArrayWrapper.create( 38 | "test", item_constraint=[{"type": "number"}, {"type": "number"}] 39 | ) 40 | 41 | instance = validator([1, 2, 3, 4]) 42 | 43 | instance.validate() 44 | 45 | instance = validator([1, "Hello"]) 46 | with pytest.raises(ValidationError): 47 | instance.validate() 48 | 49 | 50 | def test_validate_arrays_with_object_types(Person): 51 | validator = ArrayWrapper.create("test", item_constraint=Person) 52 | 53 | instance = validator([{"firstName": "winner", "lastName": "Smith"}]) 54 | instance.validate() 55 | 56 | instance = validator( 57 | [{"firstName": "winner", "lastName": "Dinosaur"}, {"firstName": "BadMan"}] 58 | ) 59 | with pytest.raises(ValidationError): 60 | instance.validate() 61 | 62 | 63 | def test_validate_arrays_with_mixed_types(Person): 64 | validator = ArrayWrapper.create( 65 | "test", item_constraint=[Person, {"type": "number"}] 66 | ) 67 | 68 | instance = validator([{"firstName": "winner", "lastName": "Dinosaur"}, "fried"]) 69 | with pytest.raises(ValidationError): 70 | instance.validate() 71 | 72 | instance = validator([{"firstName": "winner", "lastName": "Dinosaur"}, 12324]) 73 | instance.validate() 74 | 75 | 76 | def test_validate_arrays_nested(): 77 | validator = ArrayWrapper.create( 78 | "test", item_constraint={"type": "array", "items": {"type": "integer"}} 79 | ) 80 | 81 | instance = validator([[1, 2, 4, 5], [1, 2, 4]]) 82 | instance.validate() 83 | 84 | instance = validator([[1, 2, "h", 5], [1, 2, 4]]) 85 | with pytest.raises(ValidationError): 86 | instance.validate() 87 | 88 | instance = validator([[1, 2, "h", 5], [1, 2, "4"]]) 89 | with pytest.raises(ValidationError): 90 | instance.validate() 91 | 92 | 93 | def test_validate_arrays_length(): 94 | validator = ArrayWrapper.create("test", minItems=1, maxItems=3) 95 | 96 | instance = validator(range(1)) 97 | instance.validate() 98 | 99 | instance = validator(range(2)) 100 | instance.validate() 101 | 102 | instance = validator(range(3)) 103 | instance.validate() 104 | 105 | instance = validator(range(4)) 106 | with pytest.raises(ValidationError): 107 | instance.validate() 108 | 109 | instance = validator([]) 110 | with pytest.raises(ValidationError): 111 | instance.validate() 112 | 113 | 114 | def test_validate_arrays_uniqueness(): 115 | validator = ArrayWrapper.create("test", uniqueItems=True) 116 | 117 | instance = validator([]) 118 | instance.validate() 119 | 120 | instance = validator([1, 2, 3, 4]) 121 | instance.validate() 122 | 123 | instance = validator([1, 2, 2, 4]) 124 | with pytest.raises(ValidationError): 125 | instance.validate() 126 | -------------------------------------------------------------------------------- /test/test_wrong_exception_protocolbase_getitem.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import python_jsonschema_objects as pjo 4 | 5 | 6 | @pytest.fixture 7 | def base_schema(): 8 | return { 9 | "title": "example", 10 | "type": "object", 11 | "additionalProperties": False, 12 | "properties": { 13 | "dictLike": {"additionalProperties": {"type": "integer"}, "type": "object"} 14 | }, 15 | } 16 | 17 | 18 | def test_wrong_exception_protocolbase_getitem(base_schema): 19 | """ 20 | to declare a dict like object in json-schema, we are supposed 21 | to declare it as an object of additional properties. 22 | When trying to use it as dict, for instance testing if a key is inside 23 | the dictionary, methods like __contains__ in the ProtocolBase expect 24 | __getitem__ to raise a KeyError. getitem calls __getattr__ without any 25 | exception handling, which raises an AttributeError (necessary for proper 26 | behaviour of getattr, for instance). 27 | Solution found is to handle AttributeError in getitem and to raise KeyError 28 | """ 29 | builder = pjo.ObjectBuilder(base_schema) 30 | ns = builder.build_classes() 31 | 32 | t = ns.Example(dictLike={"a": 0, "b": 1}) 33 | t.validate() 34 | assert "a" in t.dictLike 35 | assert "c" not in t.dictLike 36 | assert getattr(t, "not_present", None) is None 37 | 38 | with pytest.raises(AttributeError): 39 | assert "a" not in t.notAnAttribute 40 | 41 | 42 | if __name__ == "__main__": 43 | test_wrong_exception_protocolbase_getitem(base_schema()) 44 | -------------------------------------------------------------------------------- /test/thing-one.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "thing_one", 4 | "title": "thing_one", 5 | "description": "The first thing.", 6 | "type": "object", 7 | "definitions": { 8 | "bar": { 9 | "description": "a bar", 10 | "properties": { 11 | "name": { 12 | "description": "unique name for bar", 13 | "type": "string", 14 | "example": "awesome-bar" 15 | } 16 | }, 17 | "required": ["name"] 18 | }, 19 | "bar_group": { 20 | "description": "a group of bars", 21 | "properties": { 22 | "title": { 23 | "description": "group title", 24 | "type": "string" 25 | }, 26 | "bars": { 27 | "description": "list of bars", 28 | "type": "array", 29 | "items": { 30 | "$ref": "file:///thing-one.json#/definitions/bar" 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/thing-two.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "id": "thing_two", 4 | "title": "thing_two", 5 | "description": "The second thing.", 6 | "type": "object", 7 | "properties": { 8 | "message": {"$ref": "#/definitions/bar_message"} 9 | }, 10 | "definitions": { 11 | "header": { 12 | "description": "a message header", 13 | "properties": { 14 | "id": { 15 | "description": "message id", 16 | "type": "string" 17 | } 18 | } 19 | }, 20 | "bar_message": { 21 | "description": "a bar message", 22 | "allOf": [ 23 | {"$ref": "file:///thing-two.json#/definitions/header"}, 24 | {"$ref": "file:///thing-one.json#/definitions/bar_group"} 25 | ] 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py{38,39,310,311,312}-jsonschema{40}-markdown{2,3} 3 | skip_missing_interpreters = true 4 | 5 | [gh-actions] 6 | python = 7 | 3.8: py38 8 | 3.9: py39 9 | 3.10: py310 10 | 3.11: py311 11 | 3.12: py312 12 | 13 | [testenv] 14 | commands = 15 | coverage run -m pytest --doctest-glob='python_jsonschema_objects/*.md' {posargs} 16 | coverage xml --omit='*test*' 17 | deps = 18 | coverage 19 | pytest 20 | pytest-mock 21 | jsonschema40: jsonschema>=4.18 22 | markdown2: Markdown~=2.4 23 | markdown3: Markdown~=3.0 24 | --------------------------------------------------------------------------------