From c5ece8d232b9f720ac8cb8be3f954a3793c1a331 Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Mon, 20 Apr 2015 19:45:24 -0400 Subject: [PATCH 1/8] gitignore --- .gitignore | 66 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 65 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 5ff10bf..8f1c027 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,66 @@ +# vim +[._]*.s[a-w][a-z] +[._]s[a-w][a-z] +*.un~ +Session.vim +.netrwhist *~ -*.py[cdo] + + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover + +# Translations +*.mo +*.pot + +# Django stuff: +*.log + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ From bafb193f3e7d93c1b19f06f1e075d99e98982038 Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Mon, 20 Apr 2015 19:46:22 -0400 Subject: [PATCH 2/8] jsonform compatible output See: https://github.com/joshfire/jsonform --- wtforms_jsonschema/jsonschema.py | 73 +++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 15 deletions(-) diff --git a/wtforms_jsonschema/jsonschema.py b/wtforms_jsonschema/jsonschema.py index 1f4f518..3d2756f 100644 --- a/wtforms_jsonschema/jsonschema.py +++ b/wtforms_jsonschema/jsonschema.py @@ -13,59 +13,95 @@ class WTFormToJSONSchema(object): 'URLField': { 'type': 'string', 'format': 'uri', - }, - 'URIField': { - 'type': 'string', - 'format': 'uri', - }, - 'URIFileField': { - 'type': 'string', - 'format': 'uri', - 'ux-widget': 'file-select', #not part of spec but flags behavior + 'form': { + 'type': 'url', + }, }, 'FileField': { 'type': 'string', 'format': 'uri', - 'ux-widget': 'file-select', #not part of spec but flags behavior + 'form': { + 'type': 'file', + }, }, 'DateField': { 'type': 'string', 'format': 'date', + 'form': { + 'type': 'date', + }, }, 'DateTimeField': { 'type': 'string', 'format': 'datetime', + 'form': { + 'type': 'datetime', + }, }, 'DecimalField': { 'type': 'number', + 'form': { + 'type': 'number', + 'step': 'any', + }, }, 'IntegerField': { 'type': 'integer', + 'form': { + 'type': 'number', + 'min': '1', + 'step': '1', + }, }, 'BooleanField': { 'type': 'boolean', + 'form': {}, }, 'StringField': { 'type': 'string', + 'form': { + 'type': 'text', + }, + }, + 'PasswordField': { + 'type': 'string', + 'form': { + 'type': 'password', + }, }, 'SearchField': { 'type': 'string', + 'form': { + 'type': 'search', + }, }, 'TelField': { 'type': 'string', 'format': 'phone', + 'form': { + 'type': 'tel', + }, }, 'EmailField': { 'type': 'string', 'format': 'email', + 'form': { + 'type': 'email', + }, }, 'DateTimeLocalField': { 'type': 'string', 'format': 'datetime', + 'form': { + 'type': 'datetime-local', + }, }, 'ColorField': { 'type': 'string', 'format': 'color', + 'form': { + 'type': 'color', + }, }, #TODO min/max 'DecimalRangeField': { @@ -96,10 +132,11 @@ def convert_form(self, form, json_schema=None, forms_seen=None, path=None): path = [] if json_schema is None: json_schema = { - #'title':dockit_schema._meta - #'description' 'type': 'object', - 'properties': OrderedDict(), + 'schema': { + 'properties': OrderedDict(), + }, + 'form': [], } key = id(form) if key in forms_seen: @@ -118,8 +155,12 @@ def convert_form(self, form, json_schema=None, forms_seen=None, path=None): if name not in form._fields: continue field = form._fields[name] - json_schema['properties'][name] = \ + json_schema['schema']['properties'][name], form_obj = \ self.convert_formfield(name, field, json_schema, forms_seen, path) + if form_obj is not None: + form_obj['key'] = name + json_schema['form'].append(form_obj) + return json_schema @@ -138,7 +179,9 @@ def convert_formfield(self, name, field, json_schema, forms_seen, path): if hasattr(self, 'convert_%s' % ftype): return getattr(self, 'convert_%s' % ftype)(name, field, json_schema) params = self.conversions.get(ftype) + form = None if params is not None: + form = params.pop('form', None) target_def.update(params) elif ftype == 'FormField': key = id(field.form_class) @@ -163,7 +206,7 @@ def convert_formfield(self, name, field, json_schema, forms_seen, path): target_def.update(self.conversions[it]) else: target_def['type'] = 'string' - return target_def + return target_def, form def convert_SelectField(self, name, field, json_schema): values = list() From d41adafd3e5de7cecfbec1f21aac7df2dd737e43 Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Tue, 21 Apr 2015 08:18:28 -0400 Subject: [PATCH 3/8] don't alter class attributes! --- wtforms_jsonschema/jsonschema.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/wtforms_jsonschema/jsonschema.py b/wtforms_jsonschema/jsonschema.py index 3d2756f..4e42faa 100644 --- a/wtforms_jsonschema/jsonschema.py +++ b/wtforms_jsonschema/jsonschema.py @@ -157,9 +157,11 @@ def convert_form(self, form, json_schema=None, forms_seen=None, path=None): field = form._fields[name] json_schema['schema']['properties'][name], form_obj = \ self.convert_formfield(name, field, json_schema, forms_seen, path) - if form_obj is not None: + if form_obj is None: + form_obj = {'key': name} + else: form_obj['key'] = name - json_schema['form'].append(form_obj) + json_schema['form'].append(form_obj) return json_schema @@ -178,7 +180,7 @@ def convert_formfield(self, name, field, json_schema, forms_seen, path): ftype = type(field).__name__ if hasattr(self, 'convert_%s' % ftype): return getattr(self, 'convert_%s' % ftype)(name, field, json_schema) - params = self.conversions.get(ftype) + params = dict(self.conversions.get(ftype)) form = None if params is not None: form = params.pop('form', None) From cd5d0ec5d861ce0bf689ede4052c01aa9a172c99 Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Wed, 12 Apr 2017 12:24:52 -0400 Subject: [PATCH 4/8] use isinstance instead of __name__, and use deepcopy --- setup.py | 62 ++++++------ wtforms_jsonschema/jsonschema.py | 156 +++++++++++++++++++------------ 2 files changed, 132 insertions(+), 86 deletions(-) diff --git a/setup.py b/setup.py index 71e40fe..e723bee 100755 --- a/setup.py +++ b/setup.py @@ -8,32 +8,40 @@ VERSION = '0.0.7' PATH = os.path.dirname(os.path.abspath(__file__)) try: - LONG_DESC = '\n===='+open(os.path.join(PATH, 'README.rst'), 'r').read().split('====', 1)[-1] -except IOError: #happens when using tox + LONG_DESC = '\n====' + open( + os.path.join(PATH, 'README.rst'), 'r' + ).read().split('====', 1)[-1] +except IOError: # happens when using tox LONG_DESC = '' -setup(name='wtforms-jsonschema', - version=VERSION, - description="wtforms-jsonschema converts WTForms into JSON Schema compatibile representations", - long_description=LONG_DESC, - classifiers=[ - 'Programming Language :: Python', - 'Environment :: Web Environment', - 'Operating System :: OS Independent', - 'Natural Language :: English', - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', - ], - keywords='wtforms json schema', - author = 'Jason Kraus', - author_email = 'zbyte64@gmail.com', - maintainer = 'Jason Kraus', - maintainer_email = 'zbyte64@gmail.com', - url='http://github.com/zbyte64/wtforms-jsonschema', - license='New BSD License', - packages=find_packages(exclude=['tests']), - include_package_data = True, - zip_safe = False, - ) +setup( + name='wtforms-jsonschema', + version=VERSION, + description=("wtforms-jsonschema converts WTForms into JSON Schema " + "compatibile representations"), + long_description=LONG_DESC, + classifiers=[ + 'Programming Language :: Python', + 'Environment :: Web Environment', + 'Operating System :: OS Independent', + 'Natural Language :: English', + 'Development Status :: 4 - Beta', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: BSD License', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + ], + keywords='wtforms json schema', + author='Jason Kraus', + author_email='zbyte64@gmail.com', + maintainer='Jason Kraus', + maintainer_email='zbyte64@gmail.com', + url='http://github.com/zbyte64/wtforms-jsonschema', + license='New BSD License', + packages=find_packages(exclude=['tests']), + include_package_data=True, + zip_safe=False, + install_requires=[ + 'wtforms>=2.1', + 'six>=1.9.0', + ] +) diff --git a/wtforms_jsonschema/jsonschema.py b/wtforms_jsonschema/jsonschema.py index 4e42faa..35ed7ab 100644 --- a/wtforms_jsonschema/jsonschema.py +++ b/wtforms_jsonschema/jsonschema.py @@ -1,5 +1,10 @@ +import copy from collections import OrderedDict +import six +import wtforms +from wtforms.fields import html5 + def pretty_name(name): """Converts 'first_name' to 'First name'""" @@ -8,44 +13,71 @@ def pretty_name(name): return name.replace('_', ' ').capitalize() +_DEFAULT_CONVERSIONS = {} +_INPUT_TYPE_MAP = {} +try: + import wtforms_components + + _DEFAULT_CONVERSIONS[wtforms_components.ColorField] = { + 'type': 'string', + 'format': 'color', + 'form': { + 'type': 'color', + }, + } + + # TODO min/max + _DEFAULT_CONVERSIONS[wtforms_components.DecimalRangeField] = { + 'type': 'number', + } + _DEFAULT_CONVERSIONS[wtforms_components.IntegerRangeField] = { + 'type': 'integer', + } + _INPUT_TYPE_MAP['color'] = wtforms_components.ColorField + +except ImportError: + pass + + class WTFormToJSONSchema(object): + DEFAULT_CONVERSIONS = { - 'URLField': { + html5.URLField: { 'type': 'string', 'format': 'uri', 'form': { 'type': 'url', }, }, - 'FileField': { + wtforms.fields.FileField: { 'type': 'string', 'format': 'uri', 'form': { 'type': 'file', }, }, - 'DateField': { + wtforms.fields.DateField: { 'type': 'string', 'format': 'date', 'form': { 'type': 'date', }, }, - 'DateTimeField': { + wtforms.fields.DateTimeField: { 'type': 'string', 'format': 'datetime', 'form': { 'type': 'datetime', }, }, - 'DecimalField': { + wtforms.fields.DecimalField: { 'type': 'number', 'form': { 'type': 'number', 'step': 'any', }, }, - 'IntegerField': { + wtforms.fields.IntegerField: { 'type': 'integer', 'form': { 'type': 'number', @@ -53,98 +85,80 @@ class WTFormToJSONSchema(object): 'step': '1', }, }, - 'BooleanField': { + wtforms.fields.BooleanField: { 'type': 'boolean', 'form': {}, }, - 'StringField': { + wtforms.fields.StringField: { 'type': 'string', 'form': { 'type': 'text', }, }, - 'PasswordField': { + wtforms.fields.PasswordField: { 'type': 'string', 'form': { 'type': 'password', }, }, - 'SearchField': { + html5.SearchField: { 'type': 'string', 'form': { 'type': 'search', }, }, - 'TelField': { + html5.TelField: { 'type': 'string', 'format': 'phone', 'form': { 'type': 'tel', }, }, - 'EmailField': { + html5.EmailField: { 'type': 'string', 'format': 'email', 'form': { 'type': 'email', }, }, - 'DateTimeLocalField': { + html5.DateTimeLocalField: { 'type': 'string', 'format': 'datetime', 'form': { 'type': 'datetime-local', }, }, - 'ColorField': { - 'type': 'string', - 'format': 'color', - 'form': { - 'type': 'color', - }, - }, - #TODO min/max - 'DecimalRangeField': { - 'type': 'number', - }, - 'IntegerRangeField': { - 'type': 'integer', - }, } INPUT_TYPE_MAP = { - 'text': 'StringField', - 'checkbox': 'BooleanField', - 'color': 'ColorField', - 'tel': 'TelField', + 'text': wtforms.fields.StringField, + 'checkbox': wtforms.fields.BooleanField, + 'tel': html5.TelField, } def __init__(self, conversions=None, include_array_item_titles=True, - include_array_title=True): + include_array_title=True): self.conversions = conversions or self.DEFAULT_CONVERSIONS self.include_array_item_titles = include_array_item_titles self.include_array_title = include_array_title def convert_form(self, form, json_schema=None, forms_seen=None, path=None): - if forms_seen is None: - forms_seen = dict() - if path is None: - path = [] - if json_schema is None: - json_schema = { - 'type': 'object', - 'schema': { - 'properties': OrderedDict(), - }, - 'form': [], - } + forms_seen = forms_seen or dict() + path = path or [] + json_schema = json_schema or { + 'type': 'object', + 'schema': { + 'properties': OrderedDict(), + }, + 'form': [], + } key = id(form) if key in forms_seen: json_schema['$ref'] = '#'+'/'.join(forms_seen[key]) json_schema.pop('properties', None) return json_schema forms_seen[key] = path - #_unbound_fields preserves order, _fields does not + # _unbound_fields preserves order, _fields does not if hasattr(form, '_unbound_fields'): if form._unbound_fields is None: form = form() @@ -155,8 +169,9 @@ def convert_form(self, form, json_schema=None, forms_seen=None, path=None): if name not in form._fields: continue field = form._fields[name] - json_schema['schema']['properties'][name], form_obj = \ - self.convert_formfield(name, field, json_schema, forms_seen, path) + json_schema['schema']['properties'][name], form_obj = ( + self.convert_formfield(name, field, json_schema, + forms_seen, path)) if form_obj is None: form_obj = {'key': name} else: @@ -165,6 +180,25 @@ def convert_form(self, form, json_schema=None, forms_seen=None, path=None): return json_schema + def _find_conversion_class(self, cls): + if self.conversions.get(cls): + return cls + else: + for klass in six.iterkeys(self.conversions): + if issubclass(cls, klass): + return klass + raise KeyError(cls) + + def _find_conversion(self, field, name): + cls = field.__class__ + try: + klass = self._find_conversion_class(cls) + return copy.deepcopy(self.conversions.get(klass)) + except (KeyError, TypeError) as exc: + niexc = NotImplementedError( + 'Unsupported field {name}: {field!r}'.format( + name=name, field=field)) + six.raise_from(niexc, exc) def convert_formfield(self, name, field, json_schema, forms_seen, path): widget = field.widget @@ -175,23 +209,24 @@ def convert_formfield(self, name, field, json_schema, forms_seen, path): } if field.flags.required: target_def['required'] = True - json_schema.setdefault('required', list()) + json_schema.setdefault('required', []) json_schema['required'].append(name) - ftype = type(field).__name__ - if hasattr(self, 'convert_%s' % ftype): - return getattr(self, 'convert_%s' % ftype)(name, field, json_schema) - params = dict(self.conversions.get(ftype)) - form = None - if params is not None: - form = params.pop('form', None) - target_def.update(params) - elif ftype == 'FormField': + if hasattr(self, 'convert_%s' % field.__class__.__name__): + func = getattr(self, 'convert_%s' % field.__class__.__name__) + return func(name, field, json_schema) + + params = self._find_conversion(field, name) + + form = params.pop('form', None) + target_def.update(params) + + if isinstance(field, wtforms.fields.FormField): key = id(field.form_class) if key in forms_seen: return {"$ref": "#"+"/".join(forms_seen[key])} forms_seen[key] = path target_def.update(self.convert_form(field.form_class(obj=getattr(field, '_obj', None)), None, forms_seen, path)) - elif ftype == 'FieldList': + elif isinstance(field, wtforms.fields.FieldList): if not self.include_array_title: target_def.pop('title') target_def.pop('description') @@ -202,7 +237,7 @@ def convert_formfield(self, name, field, json_schema, forms_seen, path): target_def['items'].pop('title', None) target_def['items'].pop('description', None) elif hasattr(widget, 'input_type'): - it = self.INPUT_TYPE_MAP.get(widget.input_type, 'StringField') + it = self.INPUT_TYPE_MAP.get(widget.input_type, wtforms.fields.StringField) if hasattr(self, 'convert_%s' % it): return getattr(self, 'convert_%s' % it)(name, field, json_schema) target_def.update(self.conversions[it]) @@ -240,3 +275,6 @@ def convert_RadioField(self, name, field, json_schema): target_def['required'] = True return target_def + +WTFormToJSONSchema.DEFAULT_CONVERSIONS.update(_DEFAULT_CONVERSIONS) +WTFormToJSONSchema.INPUT_TYPE_MAP.update(_INPUT_TYPE_MAP) From 6695eb4274a021570bba142c8b631cea02351d03 Mon Sep 17 00:00:00 2001 From: Stephen Fuhry Date: Mon, 28 Aug 2017 15:57:18 +0000 Subject: [PATCH 5/8] Password support --- wtforms_jsonschema/jsonschema.py | 65 ++++++++++++++++---------------- 1 file changed, 33 insertions(+), 32 deletions(-) diff --git a/wtforms_jsonschema/jsonschema.py b/wtforms_jsonschema/jsonschema.py index 35ed7ab..c740e25 100644 --- a/wtforms_jsonschema/jsonschema.py +++ b/wtforms_jsonschema/jsonschema.py @@ -41,97 +41,98 @@ def pretty_name(name): class WTFormToJSONSchema(object): - DEFAULT_CONVERSIONS = { - html5.URLField: { + DEFAULT_CONVERSIONS = OrderedDict([ + (html5.URLField, { 'type': 'string', 'format': 'uri', 'form': { 'type': 'url', }, - }, - wtforms.fields.FileField: { + }), + (wtforms.fields.FileField, { 'type': 'string', 'format': 'uri', 'form': { 'type': 'file', }, - }, - wtforms.fields.DateField: { + }), + (wtforms.fields.DateField, { 'type': 'string', 'format': 'date', 'form': { 'type': 'date', }, - }, - wtforms.fields.DateTimeField: { + }), + (wtforms.fields.DateTimeField, { 'type': 'string', 'format': 'datetime', 'form': { 'type': 'datetime', }, - }, - wtforms.fields.DecimalField: { + }), + (wtforms.fields.DecimalField, { 'type': 'number', 'form': { 'type': 'number', 'step': 'any', }, - }, - wtforms.fields.IntegerField: { + }), + (wtforms.fields.IntegerField, { 'type': 'integer', 'form': { 'type': 'number', 'min': '1', 'step': '1', }, - }, - wtforms.fields.BooleanField: { + }), + (wtforms.fields.BooleanField, { 'type': 'boolean', 'form': {}, - }, - wtforms.fields.StringField: { - 'type': 'string', - 'form': { - 'type': 'text', - }, - }, - wtforms.fields.PasswordField: { + }), + (wtforms.fields.PasswordField, { 'type': 'string', 'form': { 'type': 'password', }, - }, - html5.SearchField: { + }), + (html5.SearchField, { 'type': 'string', 'form': { 'type': 'search', }, - }, - html5.TelField: { + }), + (html5.TelField, { 'type': 'string', 'format': 'phone', 'form': { 'type': 'tel', }, - }, - html5.EmailField: { + }), + (html5.EmailField, { 'type': 'string', 'format': 'email', 'form': { 'type': 'email', }, - }, - html5.DateTimeLocalField: { + }), + (html5.DateTimeLocalField, { 'type': 'string', 'format': 'datetime', 'form': { 'type': 'datetime-local', }, - }, - } + }), + (wtforms.fields.StringField, { + 'type': 'string', + 'form': { + 'type': 'text', + }, + }), + ]) INPUT_TYPE_MAP = { 'text': wtforms.fields.StringField, + 'password': wtforms.fields.PasswordField, 'checkbox': wtforms.fields.BooleanField, 'tel': html5.TelField, } From 5dcddc562171745101753352b5a5622f23d942b2 Mon Sep 17 00:00:00 2001 From: Stephen Fuhry Date: Thu, 5 Oct 2017 13:40:02 +0000 Subject: [PATCH 6/8] fix TypeError & support json-form --- wtforms_jsonschema/jsonschema.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/wtforms_jsonschema/jsonschema.py b/wtforms_jsonschema/jsonschema.py index c740e25..54dea30 100644 --- a/wtforms_jsonschema/jsonschema.py +++ b/wtforms_jsonschema/jsonschema.py @@ -262,7 +262,10 @@ def convert_SelectField(self, name, field, json_schema): } if field.flags.required: target_def['required'] = True - return target_def + return target_def, { + 'type': 'select', + 'enum': values, + } def convert_RadioField(self, name, field, json_schema): target_def = { From 396c3d3d2906fe7c59577cd7e13e4bcc6523fc2a Mon Sep 17 00:00:00 2001 From: Jason Rojas Date: Wed, 16 Oct 2019 12:28:18 -0500 Subject: [PATCH 7/8] Add length to front end --- wtforms_jsonschema/jsonschema.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/wtforms_jsonschema/jsonschema.py b/wtforms_jsonschema/jsonschema.py index 54dea30..cf0fecc 100644 --- a/wtforms_jsonschema/jsonschema.py +++ b/wtforms_jsonschema/jsonschema.py @@ -4,6 +4,7 @@ import six import wtforms from wtforms.fields import html5 +from wtforms.validators import Length def pretty_name(name): @@ -212,6 +213,12 @@ def convert_formfield(self, name, field, json_schema, forms_seen, path): target_def['required'] = True json_schema.setdefault('required', []) json_schema['required'].append(name) + for validator in field.validators: + if isinstance(validator, Length): + if validator.min != -1: + target_def['minLength'] = validator.min + if validator.max != -1: + target_def['maxLength'] = validator.max if hasattr(self, 'convert_%s' % field.__class__.__name__): func = getattr(self, 'convert_%s' % field.__class__.__name__) return func(name, field, json_schema) @@ -281,4 +288,4 @@ def convert_RadioField(self, name, field, json_schema): WTFormToJSONSchema.DEFAULT_CONVERSIONS.update(_DEFAULT_CONVERSIONS) -WTFormToJSONSchema.INPUT_TYPE_MAP.update(_INPUT_TYPE_MAP) +WTFormToJSONSchema.INPUT_TYPE_MAP.update(_INPUT_TYPE_MAP) \ No newline at end of file From f7585dc18512f72b387cb50bc1c8d44e85bce3a3 Mon Sep 17 00:00:00 2001 From: "Stephen J. Fuhry" Date: Sat, 21 Mar 2026 11:56:39 +0000 Subject: [PATCH 8/8] Update wtforms-jsonschema for WTForms 3.x compatibility - Remove html5 field imports (moved to wtforms.fields in WTForms 3.x) - Remove six dependency (Python 2 compat no longer needed) - Bump wtforms requirement to >=3 in setup.py --- setup.py | 57 ++--- wtforms_jsonschema/jsonschema.py | 370 +++++++++++++++++-------------- 2 files changed, 239 insertions(+), 188 deletions(-) diff --git a/setup.py b/setup.py index e723bee..ce3918c 100755 --- a/setup.py +++ b/setup.py @@ -1,47 +1,50 @@ #!/usr/bin/env python import os + try: from setuptools import setup, find_packages except ImportError: from distutils.core import setup, find_packages -VERSION = '0.0.7' +VERSION = "0.0.7" PATH = os.path.dirname(os.path.abspath(__file__)) try: - LONG_DESC = '\n====' + open( - os.path.join(PATH, 'README.rst'), 'r' - ).read().split('====', 1)[-1] + LONG_DESC = ( + "\n====" + + open(os.path.join(PATH, "README.rst"), "r").read().split("====", 1)[-1] + ) except IOError: # happens when using tox - LONG_DESC = '' + LONG_DESC = "" setup( - name='wtforms-jsonschema', + name="wtforms-jsonschema", version=VERSION, - description=("wtforms-jsonschema converts WTForms into JSON Schema " - "compatibile representations"), + description=( + "wtforms-jsonschema converts WTForms into JSON Schema " + "compatibile representations" + ), long_description=LONG_DESC, classifiers=[ - 'Programming Language :: Python', - 'Environment :: Web Environment', - 'Operating System :: OS Independent', - 'Natural Language :: English', - 'Development Status :: 4 - Beta', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: BSD License', - 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + "Programming Language :: Python", + "Environment :: Web Environment", + "Operating System :: OS Independent", + "Natural Language :: English", + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "License :: OSI Approved :: BSD License", + "Topic :: Internet :: WWW/HTTP :: Dynamic Content", ], - keywords='wtforms json schema', - author='Jason Kraus', - author_email='zbyte64@gmail.com', - maintainer='Jason Kraus', - maintainer_email='zbyte64@gmail.com', - url='http://github.com/zbyte64/wtforms-jsonschema', - license='New BSD License', - packages=find_packages(exclude=['tests']), + keywords="wtforms json schema", + author="Jason Kraus", + author_email="zbyte64@gmail.com", + maintainer="Jason Kraus", + maintainer_email="zbyte64@gmail.com", + url="http://github.com/zbyte64/wtforms-jsonschema", + license="New BSD License", + packages=find_packages(exclude=["tests"]), include_package_data=True, zip_safe=False, install_requires=[ - 'wtforms>=2.1', - 'six>=1.9.0', - ] + "wtforms>=3", + ], ) diff --git a/wtforms_jsonschema/jsonschema.py b/wtforms_jsonschema/jsonschema.py index cf0fecc..6c01aad 100644 --- a/wtforms_jsonschema/jsonschema.py +++ b/wtforms_jsonschema/jsonschema.py @@ -1,17 +1,15 @@ import copy from collections import OrderedDict -import six import wtforms -from wtforms.fields import html5 from wtforms.validators import Length def pretty_name(name): """Converts 'first_name' to 'First name'""" if not name: - return u'' - return name.replace('_', ' ').capitalize() + return "" + return name.replace("_", " ").capitalize() _DEFAULT_CONVERSIONS = {} @@ -20,126 +18,168 @@ def pretty_name(name): import wtforms_components _DEFAULT_CONVERSIONS[wtforms_components.ColorField] = { - 'type': 'string', - 'format': 'color', - 'form': { - 'type': 'color', + "type": "string", + "format": "color", + "form": { + "type": "color", }, } # TODO min/max _DEFAULT_CONVERSIONS[wtforms_components.DecimalRangeField] = { - 'type': 'number', + "type": "number", } _DEFAULT_CONVERSIONS[wtforms_components.IntegerRangeField] = { - 'type': 'integer', + "type": "integer", } - _INPUT_TYPE_MAP['color'] = wtforms_components.ColorField + _INPUT_TYPE_MAP["color"] = wtforms_components.ColorField except ImportError: pass -class WTFormToJSONSchema(object): +class WTFormToJSONSchema: - DEFAULT_CONVERSIONS = OrderedDict([ - (html5.URLField, { - 'type': 'string', - 'format': 'uri', - 'form': { - 'type': 'url', - }, - }), - (wtforms.fields.FileField, { - 'type': 'string', - 'format': 'uri', - 'form': { - 'type': 'file', - }, - }), - (wtforms.fields.DateField, { - 'type': 'string', - 'format': 'date', - 'form': { - 'type': 'date', - }, - }), - (wtforms.fields.DateTimeField, { - 'type': 'string', - 'format': 'datetime', - 'form': { - 'type': 'datetime', - }, - }), - (wtforms.fields.DecimalField, { - 'type': 'number', - 'form': { - 'type': 'number', - 'step': 'any', - }, - }), - (wtforms.fields.IntegerField, { - 'type': 'integer', - 'form': { - 'type': 'number', - 'min': '1', - 'step': '1', - }, - }), - (wtforms.fields.BooleanField, { - 'type': 'boolean', - 'form': {}, - }), - (wtforms.fields.PasswordField, { - 'type': 'string', - 'form': { - 'type': 'password', - }, - }), - (html5.SearchField, { - 'type': 'string', - 'form': { - 'type': 'search', - }, - }), - (html5.TelField, { - 'type': 'string', - 'format': 'phone', - 'form': { - 'type': 'tel', - }, - }), - (html5.EmailField, { - 'type': 'string', - 'format': 'email', - 'form': { - 'type': 'email', - }, - }), - (html5.DateTimeLocalField, { - 'type': 'string', - 'format': 'datetime', - 'form': { - 'type': 'datetime-local', - }, - }), - (wtforms.fields.StringField, { - 'type': 'string', - 'form': { - 'type': 'text', - }, - }), - ]) + DEFAULT_CONVERSIONS = OrderedDict( + [ + ( + wtforms.fields.URLField, + { + "type": "string", + "format": "uri", + "form": { + "type": "url", + }, + }, + ), + ( + wtforms.fields.FileField, + { + "type": "string", + "format": "uri", + "form": { + "type": "file", + }, + }, + ), + ( + wtforms.fields.DateField, + { + "type": "string", + "format": "date", + "form": { + "type": "date", + }, + }, + ), + ( + wtforms.fields.DateTimeField, + { + "type": "string", + "format": "datetime", + "form": { + "type": "datetime", + }, + }, + ), + ( + wtforms.fields.DecimalField, + { + "type": "number", + "form": { + "type": "number", + "step": "any", + }, + }, + ), + ( + wtforms.fields.IntegerField, + { + "type": "integer", + "form": { + "type": "number", + "min": "1", + "step": "1", + }, + }, + ), + ( + wtforms.fields.BooleanField, + { + "type": "boolean", + "form": {}, + }, + ), + ( + wtforms.fields.PasswordField, + { + "type": "string", + "form": { + "type": "password", + }, + }, + ), + ( + wtforms.fields.SearchField, + { + "type": "string", + "form": { + "type": "search", + }, + }, + ), + ( + wtforms.fields.TelField, + { + "type": "string", + "format": "phone", + "form": { + "type": "tel", + }, + }, + ), + ( + wtforms.fields.EmailField, + { + "type": "string", + "format": "email", + "form": { + "type": "email", + }, + }, + ), + ( + wtforms.fields.DateTimeLocalField, + { + "type": "string", + "format": "datetime", + "form": { + "type": "datetime-local", + }, + }, + ), + ( + wtforms.fields.StringField, + { + "type": "string", + "form": { + "type": "text", + }, + }, + ), + ] + ) INPUT_TYPE_MAP = { - 'text': wtforms.fields.StringField, - 'password': wtforms.fields.PasswordField, - 'checkbox': wtforms.fields.BooleanField, - 'tel': html5.TelField, + "text": wtforms.fields.StringField, + "password": wtforms.fields.PasswordField, + "checkbox": wtforms.fields.BooleanField, + "tel": wtforms.fields.TelField, } - def __init__(self, conversions=None, include_array_item_titles=True, - include_array_title=True): + def __init__( + self, conversions=None, include_array_item_titles=True, include_array_title=True + ): self.conversions = conversions or self.DEFAULT_CONVERSIONS self.include_array_item_titles = include_array_item_titles self.include_array_title = include_array_title @@ -148,20 +188,20 @@ def convert_form(self, form, json_schema=None, forms_seen=None, path=None): forms_seen = forms_seen or dict() path = path or [] json_schema = json_schema or { - 'type': 'object', - 'schema': { - 'properties': OrderedDict(), + "type": "object", + "schema": { + "properties": OrderedDict(), }, - 'form': [], + "form": [], } key = id(form) if key in forms_seen: - json_schema['$ref'] = '#'+'/'.join(forms_seen[key]) - json_schema.pop('properties', None) + json_schema["$ref"] = "#" + "/".join(forms_seen[key]) + json_schema.pop("properties", None) return json_schema forms_seen[key] = path # _unbound_fields preserves order, _fields does not - if hasattr(form, '_unbound_fields'): + if hasattr(form, "_unbound_fields"): if form._unbound_fields is None: form = form() fields = [name for name, ufield in form._unbound_fields] @@ -171,14 +211,14 @@ def convert_form(self, form, json_schema=None, forms_seen=None, path=None): if name not in form._fields: continue field = form._fields[name] - json_schema['schema']['properties'][name], form_obj = ( - self.convert_formfield(name, field, json_schema, - forms_seen, path)) + json_schema["schema"]["properties"][name], form_obj = ( + self.convert_formfield(name, field, json_schema, forms_seen, path) + ) if form_obj is None: - form_obj = {'key': name} + form_obj = {"key": name} else: - form_obj['key'] = name - json_schema['form'].append(form_obj) + form_obj["key"] = name + json_schema["form"].append(form_obj) return json_schema @@ -186,7 +226,7 @@ def _find_conversion_class(self, cls): if self.conversions.get(cls): return cls else: - for klass in six.iterkeys(self.conversions): + for klass in self.conversions.keys(): if issubclass(cls, klass): return klass raise KeyError(cls) @@ -197,60 +237,68 @@ def _find_conversion(self, field, name): klass = self._find_conversion_class(cls) return copy.deepcopy(self.conversions.get(klass)) except (KeyError, TypeError) as exc: - niexc = NotImplementedError( - 'Unsupported field {name}: {field!r}'.format( - name=name, field=field)) - six.raise_from(niexc, exc) + raise NotImplementedError( + "Unsupported field {name}: {field!r}".format(name=name, field=field) + ) from exc def convert_formfield(self, name, field, json_schema, forms_seen, path): widget = field.widget path = path + [name] target_def = { - 'title': field.label.text, - 'description': field.description, + "title": field.label.text, + "description": field.description, } if field.flags.required: - target_def['required'] = True - json_schema.setdefault('required', []) - json_schema['required'].append(name) + target_def["required"] = True + json_schema.setdefault("required", []) + json_schema["required"].append(name) for validator in field.validators: if isinstance(validator, Length): if validator.min != -1: - target_def['minLength'] = validator.min + target_def["minLength"] = validator.min if validator.max != -1: - target_def['maxLength'] = validator.max - if hasattr(self, 'convert_%s' % field.__class__.__name__): - func = getattr(self, 'convert_%s' % field.__class__.__name__) + target_def["maxLength"] = validator.max + if hasattr(self, "convert_%s" % field.__class__.__name__): + func = getattr(self, "convert_%s" % field.__class__.__name__) return func(name, field, json_schema) params = self._find_conversion(field, name) - form = params.pop('form', None) + form = params.pop("form", None) target_def.update(params) if isinstance(field, wtforms.fields.FormField): key = id(field.form_class) if key in forms_seen: - return {"$ref": "#"+"/".join(forms_seen[key])} + return {"$ref": "#" + "/".join(forms_seen[key])} forms_seen[key] = path - target_def.update(self.convert_form(field.form_class(obj=getattr(field, '_obj', None)), None, forms_seen, path)) + target_def.update( + self.convert_form( + field.form_class(obj=getattr(field, "_obj", None)), + None, + forms_seen, + path, + ) + ) elif isinstance(field, wtforms.fields.FieldList): if not self.include_array_title: - target_def.pop('title') - target_def.pop('description') - target_def['type'] = 'array' - subfield = field.unbound_field.bind(getattr(field, '_obj', None), name) - target_def['items'] = self.convert_formfield(name, subfield, json_schema, forms_seen, path) + target_def.pop("title") + target_def.pop("description") + target_def["type"] = "array" + subfield = field.unbound_field.bind(getattr(field, "_obj", None), name) + target_def["items"] = self.convert_formfield( + name, subfield, json_schema, forms_seen, path + ) if not self.include_array_item_titles: - target_def['items'].pop('title', None) - target_def['items'].pop('description', None) - elif hasattr(widget, 'input_type'): + target_def["items"].pop("title", None) + target_def["items"].pop("description", None) + elif hasattr(widget, "input_type"): it = self.INPUT_TYPE_MAP.get(widget.input_type, wtforms.fields.StringField) - if hasattr(self, 'convert_%s' % it): - return getattr(self, 'convert_%s' % it)(name, field, json_schema) + if hasattr(self, "convert_%s" % it): + return getattr(self, "convert_%s" % it)(name, field, json_schema) target_def.update(self.conversions[it]) else: - target_def['type'] = 'string' + target_def["type"] = "string" return target_def, form def convert_SelectField(self, name, field, json_schema): @@ -262,30 +310,30 @@ def convert_SelectField(self, name, field, json_schema): values.append(val) target_def = { - 'title': field.label.text, - 'description': field.description, - 'enum': values, - 'ux-widget-choices': list(field.choices), + "title": field.label.text, + "description": field.description, + "enum": values, + "ux-widget-choices": list(field.choices), } if field.flags.required: - target_def['required'] = True + target_def["required"] = True return target_def, { - 'type': 'select', - 'enum': values, + "type": "select", + "enum": values, } def convert_RadioField(self, name, field, json_schema): target_def = { - 'title': field.label.text, - 'description': field.description, - 'enum': [x for x, y in field.choices], - 'ux-widget': 'radio', - 'ux-widget-choices': list(field.choices), + "title": field.label.text, + "description": field.description, + "enum": [x for x, y in field.choices], + "ux-widget": "radio", + "ux-widget-choices": list(field.choices), } if field.flags.required: - target_def['required'] = True + target_def["required"] = True return target_def WTFormToJSONSchema.DEFAULT_CONVERSIONS.update(_DEFAULT_CONVERSIONS) -WTFormToJSONSchema.INPUT_TYPE_MAP.update(_INPUT_TYPE_MAP) \ No newline at end of file +WTFormToJSONSchema.INPUT_TYPE_MAP.update(_INPUT_TYPE_MAP)