Skip to content

A bunch of feature #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
216aeb5
test text
Arthur-Milchior Mar 5, 2020
5f5b32e
test enum
Arthur-Milchior Mar 5, 2020
08080a3
test boolean
Arthur-Milchior Mar 5, 2020
07bea04
spin respect min/max and steps
Arthur-Milchior Mar 5, 2020
5d85aee
use a dict to send type to their default method
Arthur-Milchior Mar 5, 2020
ea9fd82
ignore tmp files
Arthur-Milchior Mar 5, 2020
b7482db
ignore pycache
Arthur-Milchior Mar 5, 2020
9351cc6
pep8 and sort import
Arthur-Milchior Mar 5, 2020
44b3b56
add default value for numeric
Arthur-Milchior Mar 5, 2020
dbb03d0
return default for const
Arthur-Milchior Mar 5, 2020
126b63a
default value when no type, enum nor const
Arthur-Milchior Mar 5, 2020
cbfb0d6
deal with values of multiple possible types
Arthur-Milchior Mar 5, 2020
54c53a0
separate list and tuple defaults
Arthur-Milchior Mar 5, 2020
d596bab
list_default return min_items elements
Arthur-Milchior Mar 5, 2020
c1f7900
deal with "contains" in list
Arthur-Milchior Mar 5, 2020
a7529be
default boolean
Arthur-Milchior Mar 5, 2020
5c38f34
string default
Arthur-Milchior Mar 5, 2020
6d104ed
apply max length to string
Arthur-Milchior Mar 5, 2020
6b5f3f2
comments
Arthur-Milchior Mar 5, 2020
d3c9702
USAGE.md
Arthur-Milchior Mar 5, 2020
c2553e9
allow to give a parent window
Arthur-Milchior Mar 6, 2020
16625af
default empty ui_schema
Arthur-Milchior Mar 6, 2020
ea2a337
save layout as FormWidget parameter
Arthur-Milchior Mar 7, 2020
74a57e7
directory for file
Arthur-Milchior Mar 7, 2020
54c558e
tool tip over labels in objects
Arthur-Milchior Mar 7, 2020
37017b7
error message for some wrong value
Arthur-Milchior Mar 11, 2020
b715080
remove example and redirects to test
Arthur-Milchior Mar 11, 2020
d803b11
create get_widget_variant
Arthur-Milchior Mar 11, 2020
ae1b255
all __init__ have args ans kwargs
Arthur-Milchior Mar 12, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
*~
__pycache__
90 changes: 4 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,90 +11,8 @@ Currently this tool does not support `anyOf` or `oneOf` directives. The reason f

Additionally, the `$ref` keyword is not supported. This will be fixed, but is waiting on some proposed upstream changes in `jsonschema`

## Example
```python3
import sys
from json import dumps

from PyQt5 import QtWidgets

from qt_jsonschema_form import WidgetBuilder

if __name__ == "__main__":
app = QtWidgets.QApplication(sys.argv)

builder = WidgetBuilder()

schema = {
"type": "object",
"title": "Number fields and widgets",
"properties": {
"schema_path": {
"title": "Schema path",
"type": "string"
},
"integerRangeSteps": {
"title": "Integer range (by 10)",
"type": "integer",
"minimum": 55,
"maximum": 100,
"multipleOf": 10
},
"event": {
"type": "string",
"format": "date"
},
"sky_colour": {
"type": "string"
},
"names": {
"type": "array",
"items": [
{
"type": "string",
"pattern": "[a-zA-Z\-'\s]+",
"enum": [
"Jack", "Jill"
]
},
{
"type": "string",
"pattern": "[a-zA-Z\-'\s]+",
"enum": [
"Alice", "Bob"
]
},
],
"additionalItems": {
"type": "number"
},
}
}
}
## Detailed explanation
For more details about each options, see [](USAGE.md)

ui_schema = {
"schema_path": {
"ui:widget": "filepath"
},
"sky_colour": {
"ui:widget": "colour"
}

}
form = builder.create_form(schema, ui_schema)
form.widget.state = {
"schema_path": "some_file.py",
"integerRangeSteps": 60,
"sky_colour": "#8f5902",
"names": [
"Jack",
"Bob"
]
}
form.show()
form.widget.on_changed.connect(lambda d: print(dumps(d, indent=4)))

app.exec_()


```
## Example
See (the test file)[test.py]
99 changes: 99 additions & 0 deletions USAGE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# Main usage
As shown in the example, the main use of this library is as follows:

```python3
from qt_jsonschema_form import WidgetBuilder
builder = WidgetBuilder()
form = builder.create_form(schema, ui_schema)
form.show()
form.widget.state = default_value
```
You can then apply a method `fn(json_value)` to the JSON value each time a change
is done by doing:
```python3
form.widget.on_changed.connect(fn)
```
and you can access to the current json value using
```python3
form.widget.state
```.

# Variants
JSON's type is extremely vague. For example decimal (floating point)
number and integral numbers have the same type. In order to decide
which widget the user see, this library uses "variant". A variant is a
name stating which kind of widget should be used to display a value to
the user and let them change it. A variant can only be assigned to an
element of an object, it is determined by the property name and the
parameter ui_schema. TODO: why?

Each type has the variant "enum". If this variant is used for a
schema, this schema must have an "enum" property. Each element of this
property will be displayed in a `QComboBox`. We now list the other
variants.

## Boolean
The only other variant of "boolean" is "checkbox", which is
implemented using a `QCheckBox`

## Object
The only other variant of "object" is "object", which is implemented
using a `QGroupBox`. That is: its content is displayed in the same
window, with elements grouped together.

## Number

Number has two variants:
* "spin", which uses a `QDoubleSpinBox`
* "text", which uses a `QLineEdit`

## Integer

It admits the same variants as Number, plus:
* "range", which uses a `QSlider`

## String

Strings has the following variant:
* "textarea", which uses a `QTextEdit`
* "text", which uses a `QLineEdit`
* "password", which uses a `TextSchemaWidget`
* "filepath", which adds a button which use the open file name in the computer
* "dirpath", which adds a button which use the open directory name in the computer
* "colour", which uses a `QColorButton`

# Defaults
When the UI is created, default values are inserted. Those values may
be changed by the user for a specific json value. Those values are of
the correct types; it's not guaranteed however that they satisfy all
schema constraints. (Actually, since there can be conjunction,
disjunction, negation of schema, and even if-then-else schema, finding
such value may be actually impossible).

If a schema contains a "default" value, this value is used.

If a schema is an enum, its first value is used as default value.

If the type of the schema is an object, then its default value is an object
containing the values in "properties", and its associated default
values are computed as explained in this section.

If the type of the schema is a list (array whose "items" is a schema)
then the default value is the empty list.

If the type of the schema is a tuple (array whose "items" is an array
of schema) then the default value is a tuple, whose value contains as
many element as the items. Each element's default value is computed as
explained in this section.

The default value of Boolean is True.

The default value of a string is a string containing only spaces, as
short as possible, accordin to the minimal size constraint.

The default value of a number is:
* as close as possible to the average between the maximum and the
minimum if they are set.
* as close as possible to the maximum or to the minimum if only one is
set
* 0 otherwise.
2 changes: 1 addition & 1 deletion qt_jsonschema_form/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
from .form import WidgetBuilder
from .form import WidgetBuilder
76 changes: 68 additions & 8 deletions qt_jsonschema_form/defaults.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
from .numeric_defaults import numeric_defaults


def enum_defaults(schema):
try:
return schema["enum"][0]
Expand All @@ -9,14 +12,63 @@ def object_defaults(schema):
return {k: compute_defaults(s) for k, s in schema["properties"].items()}


def array_defaults(schema):
items_schema = schema['items']
if isinstance(items_schema, dict):
def list_defaults(schema):
# todo: respect unicity constraint.
# todo: deal with intersection of schema, in case there is contains and items
# e.g. add elements at end of tuple
if "contains" in schema:
return list_defaults_contains(schema)
else:
return list_defaults_no_contains(schema)


def list_defaults_contains(schema):
minContains = schema.get("minContains", 1)
if minContains <= 0:
return []
default = compute_defaults(schema["contains"])
return [default] * minContains


def list_defaults_no_contains(schema):
minItems = schema.get("minItems", 0)
if minItems <= 0:
return []
default = compute_defaults(schema["items"])
return [default] * minItems


def tuple_defaults(schema):
return [compute_defaults(s) for s in schema["items"]]


def array_defaults(schema):
if isinstance(schema['items'], dict):
return list_defaults(schema)
else:
return tuple_defaults(schema)


def boolean_defaults(schema):
return True


def string_defaults(schema):
# todo: deal with pattern
minLength = schema.get("minLength", 0)
return " " * minLength


defaults = {
"array": array_defaults,
"object": object_defaults,
"numeric": numeric_defaults,
"integer": numeric_defaults,
"boolean": boolean_defaults,
"string": string_defaults,
}


def compute_defaults(schema):
if "default" in schema:
return schema["default"]
Expand All @@ -25,12 +77,20 @@ def compute_defaults(schema):
if "enum" in schema:
return enum_defaults(schema)

schema_type = schema["type"]
# Const
if "const" in schema:
return schema["const"]

if "type" not in schema:
# any value is valid.
return {}

if schema_type == "object":
return object_defaults(schema)
schema_types = schema["type"]
if not isinstance(schema_types, list):
schema_types = [schema_types]

elif schema_type == "array":
return array_defaults(schema)
for schema_type in schema_types:
if schema_type in defaults:
return defaults[schema_type](schema)

return None
38 changes: 24 additions & 14 deletions qt_jsonschema_form/form.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
from copy import deepcopy

from jsonschema.validators import validator_for
\

from . import widgets
from .defaults import compute_defaults

from typing import Dict, Any

def get_widget_state(schema, state=None):
"""A JSON object. Either the state given in input if any, otherwise
the default value satisfying the current type.

"""
if state is None:
return compute_defaults(schema)
return state
Expand All @@ -22,7 +26,7 @@ class WidgetBuilder:
"object": {"object": widgets.ObjectSchemaWidget, "enum": widgets.EnumSchemaWidget},
"number": {"spin": widgets.SpinDoubleSchemaWidget, "text": widgets.TextSchemaWidget, "enum": widgets.EnumSchemaWidget},
"string": {"textarea": widgets.TextAreaSchemaWidget, "text": widgets.TextSchemaWidget, "password": widgets.PasswordWidget,
"filepath": widgets.FilepathSchemaWidget, "colour": widgets.ColorSchemaWidget, "enum": widgets.EnumSchemaWidget},
"filepath": widgets.FilepathSchemaWidget, "dirpath": widgets.DirectorypathSchemaWidget, "colour": widgets.ColorSchemaWidget, "enum": widgets.EnumSchemaWidget},
"integer": {"spin": widgets.SpinSchemaWidget, "text": widgets.TextSchemaWidget, "range": widgets.IntegerRangeSchemaWidget,
"enum": widgets.EnumSchemaWidget},
"array": {"array": widgets.ArraySchemaWidget, "enum": widgets.EnumSchemaWidget}
Expand All @@ -42,20 +46,24 @@ class WidgetBuilder:
}

def __init__(self, validator_cls=None):
"""validator_cls -- A validator, as in jsonschema library. Schemas are
supposed to be valid for it."""
self.widget_map = deepcopy(self.default_widget_map)
self.validator_cls = validator_cls

def create_form(self, schema: dict, ui_schema: dict, state=None) -> widgets.SchemaWidgetMixin:
def create_form(self, schema: dict, ui_schema: dict = {}, state=None, parent=None) -> widgets.SchemaWidgetMixin:
validator_cls = self.validator_cls
if validator_cls is None:
validator_cls = validator_for(schema)

validator_cls.check_schema(schema)
validator = validator_cls(schema)
schema_widget = self.create_widget(schema, ui_schema, state)
form = widgets.FormWidget(schema_widget)
form = widgets.FormWidget(schema_widget, parent)

def validate(data):
"""Show the error widget iff there are errors, and the error messages
in it."""
form.clear_errors()
errors = [*validator.iter_errors(data)]

Expand All @@ -71,7 +79,17 @@ def validate(data):

def create_widget(self, schema: dict, ui_schema: dict, state=None) -> widgets.SchemaWidgetMixin:
schema_type = get_schema_type(schema)
widget_variant = self.get_widget_variant(schema_type, schema, ui_schema)
widget_cls = self.widget_map[schema_type][widget_variant]

widget = widget_cls(schema, ui_schema, self)

default_state = get_widget_state(schema, state)
if default_state is not None:
widget.state = default_state
return widget

def get_widget_variant(self, schema_type: str, schema: Dict[str, Any], ui_schema: Dict[str, Any]) -> str:
try:
default_variant = self.widget_variant_modifiers[schema_type](schema)
except KeyError:
Expand All @@ -80,12 +98,4 @@ def create_widget(self, schema: dict, ui_schema: dict, state=None) -> widgets.Sc
if "enum" in schema:
default_variant = "enum"

widget_variant = ui_schema.get('ui:widget', default_variant)
widget_cls = self.widget_map[schema_type][widget_variant]

widget = widget_cls(schema, ui_schema, self)

default_state = get_widget_state(schema, state)
if default_state is not None:
widget.state = default_state
return widget
return ui_schema.get('ui:widget', default_variant)
Loading