theenglishway (time) 6 лет назад
Сommit
e63724abc7
8 измененных файлов с 234 добавлено и 0 удалено
  1. 10 0
      .gitignore
  2. 5 0
      pydantic_form/__init__.py
  3. 97 0
      pydantic_form/pydantic_form.py
  4. 58 0
      setup.cfg
  5. 3 0
      setup.py
  6. 0 0
      tests/__init__.py
  7. 57 0
      tests/conftest.py
  8. 4 0
      tests/test_process.py

+ 10 - 0
.gitignore

@@ -0,0 +1,10 @@
+venv/
+*.pyc
+*.egg-info/
+dist/
+*.log*
+doc/_build
+.idea/
+htmlcov/
+.coverage
+*.mo

+ 5 - 0
pydantic_form/__init__.py

@@ -0,0 +1,5 @@
+__author__ = """theenglishway"""
+__email__ = 'me@theenglishway.eu'
+__version__ = '0.1.0'
+
+from .pydantic_form import PydanticForm

+ 97 - 0
pydantic_form/pydantic_form.py

@@ -0,0 +1,97 @@
+from collections import defaultdict
+from wtforms import Form, FieldList, FormField
+from pydantic import ValidationError, BaseModel
+
+
+class PydanticFieldList(FieldList):
+    _baked_instance = None
+
+    def populate_obj(self, obj, name):
+        attr = getattr(obj, name)
+        for n, v in enumerate(self._baked_instance):
+            self.entries[n].form._baked_instance = v
+            model = self.entries[n].form.get_model()
+            new = model(**v.dict())
+            attr.append(new)
+
+
+class PydanticForm(Form):
+    _errors = {}
+    _schema = None
+    _baked_instance = None
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self._errors = {}
+
+    @staticmethod
+    def _schema_to_form_key(schema_key):
+        return schema_key
+
+    @staticmethod
+    def _add_field_error(form, field, value):
+        form.errors.update({field: value})
+        field = getattr(form, field)
+        if isinstance(field, FormField):
+            field.form._errors = value
+        elif isinstance(field, FieldList):
+            raise NotImplementedError()
+        else:
+            field.errors = value
+
+    def _update_field_errors(self, field_key, value):
+        if len(field_key) > 1:
+            path, latest = field_key[:-1], field_key[-1]
+            if len(path) == 1:
+                self._add_field_error(self, path[0], {latest: value})
+                sub_form = getattr(self, path[0]).form
+                self._add_field_error(sub_form, latest, value)
+        else:
+            self._add_field_error(self, field_key[0], value)
+
+    def _process_data_for_schema(self, data):
+        """Process data form for pydantic schema"""
+        return data
+
+    def _form_field_from_baked_field(self, baked_field_key):
+        """Get the form field to fill and the value from a field in the baked
+        instance"""
+        value = getattr(self._baked_instance, baked_field_key)
+        return getattr(self, baked_field_key), value
+
+    def validate(self):
+        """Validate the form, relying on Pydantic"""
+        try:
+            data = self._process_data_for_schema(self.data)
+            self._baked_instance = self._schema(**data)
+
+            # Fill the values of every sub-forms (if any)
+            for k, v in self._baked_instance.fields.items():
+                if issubclass(v.type_, BaseModel):
+                    field_attr, value = self._form_field_from_baked_field(k)
+                    if isinstance(field_attr, FieldList):
+                        field_attr._baked_instance = value
+                    else:
+                        field_attr.form._baked_instance = value
+
+            return True
+        except ValidationError as e:
+            errors_dict = defaultdict(list)
+            for e in e.errors():
+                errors_dict[self._schema_to_form_key(e['loc'])].append(e)
+
+            for k, v in errors_dict.items():
+                self._update_field_errors(k, v)
+            return False
+
+    def populate_obj(self, obj):
+        """Populate `obj` with data from the form
+
+        :param obj: the object to populate """
+        for k, v in self._baked_instance.dict().items():
+            setattr(obj, k, v)
+
+    @property
+    def errors(self):
+        """Errors in the form, in the format of a dict"""
+        return self._errors

+ 58 - 0
setup.cfg

@@ -0,0 +1,58 @@
+[bumpversion]
+current_version = 0.1.1
+commit = True
+tag = True
+allow_dirty = True
+
+[metadata]
+name = pydantic_form
+author = theenglishway
+author_email = me@theenglishway.eu
+version = attr: pydantic_form.__version__
+description = Humanizen
+long_description = file: README.rst, CHANGELOG.rst
+license = BSD 3-Clause License
+classifiers = 
+	Programming Language :: Python :: 3
+	Programming Language :: Python :: 3.7
+url = https://code.theenglishway.eu/theenglishway-corp/pydantic_form/
+
+[options]
+zip_safe = False
+include_package_data = True
+packages = find:
+install_requires = 
+	pydantic
+	wtforms
+
+[options.packages.find]
+exclude = 
+	tests
+	tests.*
+
+[options.entry_points]
+flask.commands = 
+	seed = pydantic_form.commands:seed
+	clear = pydantic_form.commands:clear
+
+[bumpversion:file:pydantic_form/__init__.py]
+search = __version__ = '{current_version}'
+replace = __version__ = '{new_version}'
+
+[tool:pytest]
+filterwarnings = 
+	error
+	ignore::DeprecationWarning
+
+[coverage:run]
+branch = True
+source = pydantic_form, tests
+
+[coverage:report]
+precision = 2
+
+[coverage:paths]
+paths = 
+	.
+	/tmp/build/*/source/
+

+ 3 - 0
setup.py

@@ -0,0 +1,3 @@
+from setuptools import setup
+
+setup()

+ 0 - 0
tests/__init__.py


+ 57 - 0
tests/conftest.py

@@ -0,0 +1,57 @@
+import pytest
+import factory
+from collections import namedtuple
+from wtforms import Form, fields
+from werkzeug.datastructures import ImmutableMultiDict
+from pydantic_form import PydanticForm
+from pydantic import BaseModel
+
+
+ScenarioClasses = namedtuple("ScenarioClasses", ['wtf_form', 'pydantic_form', 'schema', 'data_factory'])
+ScenarioInstances = namedtuple("ScenarioInstances", ['wtf_form', 'pydantic_form', 'formdata', 'data'])
+
+
+class SimpleSchema(BaseModel):
+    integer: int
+    string: str
+
+
+class SimpleWTForm(Form):
+    _schema = SimpleSchema
+
+    integer = fields.IntegerField()
+    string = fields.StringField()
+
+
+class SimpleForm(SimpleWTForm, PydanticForm):
+    _schema = SimpleSchema
+
+
+class SimpleFormDataFactory(factory.Factory):
+    class Meta:
+        model = dict
+
+    integer = factory.Faker('pyint')
+    string = factory.Faker('pystr')
+
+
+@pytest.fixture(scope="session")
+def scenario_classes_simple():
+    return ScenarioClasses(SimpleWTForm, SimpleForm, SimpleSchema, SimpleFormDataFactory)
+
+
+@pytest.fixture(scope="session")
+def instance_factory():
+    def _factory(scenario_classes):
+        data = scenario_classes.data_factory()
+        return ScenarioInstances(
+            scenario_classes.wtf_form(data=data),
+            scenario_classes.pydantic_form(data=data),
+            ImmutableMultiDict(data),
+            data
+        )
+    return _factory
+
+@pytest.fixture(scope="session")
+def scenario_instance_simple(instance_factory, scenario_classes_simple):
+    return instance_factory(scenario_classes_simple)

+ 4 - 0
tests/test_process.py

@@ -0,0 +1,4 @@
+def test_process(scenario_instance_simple):
+    print(scenario_instance_simple)
+
+