Full Code of kvesteri/wtforms-alchemy for AI

master 44bd4cbc9965 cached
62 files
241.9 KB
55.6k tokens
478 symbols
1 requests
Download .txt
Showing preview only (258K chars total). Download the full file or copy to clipboard to get everything.
Repository: kvesteri/wtforms-alchemy
Branch: master
Commit: 44bd4cbc9965
Files: 62
Total size: 241.9 KB

Directory structure:
gitextract_nsm2hc9e/

├── .github/
│   └── workflows/
│       ├── lint.yml
│       └── test.yaml
├── .gitignore
├── CHANGES.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs/
│   ├── Makefile
│   ├── advanced.rst
│   ├── api.rst
│   ├── column_conversion.rst
│   ├── conf.py
│   ├── configuration.rst
│   ├── customization.rst
│   ├── index.rst
│   ├── introduction.rst
│   ├── license.rst
│   ├── make.bat
│   ├── relationships.rst
│   ├── types.rst
│   └── validators.rst
├── pyproject.toml
├── setup.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_class_map.py
│   ├── test_column_aliases.py
│   ├── test_configuration.py
│   ├── test_country_field.py
│   ├── test_custom_fields.py
│   ├── test_deep_form_relations.py
│   ├── test_descriptions.py
│   ├── test_field_exclusion.py
│   ├── test_field_order.py
│   ├── test_field_parameters.py
│   ├── test_field_trimming.py
│   ├── test_form_meta.py
│   ├── test_hybrid_properties.py
│   ├── test_i18n_extension.py
│   ├── test_inheritance.py
│   ├── test_labels.py
│   ├── test_model_field_list.py
│   ├── test_model_form_factory.py
│   ├── test_model_form_field.py
│   ├── test_phone_number.py
│   ├── test_phone_number_field.py
│   ├── test_query_select_field.py
│   ├── test_select_field.py
│   ├── test_synonym.py
│   ├── test_types.py
│   ├── test_unique_validator.py
│   ├── test_utils.py
│   ├── test_validators.py
│   ├── test_weekdays_field.py
│   └── test_widgets.py
├── tox.ini
└── wtforms_alchemy/
    ├── __init__.py
    ├── exc.py
    ├── fields.py
    ├── generator.py
    ├── utils.py
    └── validators.py

================================================
FILE CONTENTS
================================================

================================================
FILE: .github/workflows/lint.yml
================================================
name: Lint

on:
  - push
  - pull_request

jobs:
  test:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: 3.12

      - name: Install dependencies
        run: |
          python -m pip install --upgrade pip
          pip install .[test]

      - name: Run linting
        run: |
          ruff check .
          ruff format --check


================================================
FILE: .github/workflows/test.yaml
================================================
name: Tests
on: [push, pull_request]
jobs:
  test:
    name: Python ${{ matrix.python-version }} + SQLAlchemy ${{ matrix.sqlalchemy-version }} + WTForms ${{ matrix.wtforms-version }}
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
        sqlalchemy-version: ["1.4", "2.0"]
        wtforms-version: ["3.1", "3.2"]
    steps:
      - uses: actions/checkout@v2
      - name: Set up Python
        uses: actions/setup-python@v2
        with:
          python-version: ${{ matrix.python-version }}
      - name: Install tox
        run: pip install tox
      - name: Run tests
        run: tox -e sqlalchemy${{ matrix.sqlalchemy-version }}-wtforms${{ matrix.wtforms-version }}


================================================
FILE: .gitignore
================================================
*.py[co]

# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg

# Installer logs
pip-log.txt

# Unit test / coverage reports
.coverage
.tox

#Translations
*.mo

#Mr Developer
.mr.developer.cfg

# Built docs
docs/_build/


================================================
FILE: CHANGES.rst
================================================
Changelog
=========

Here you can see the full list of changes between each WTForms-Alchemy release.

0.19.1 (2025-08-11)
^^^^^^^^^^^^^^^^^^^

- Fixed issue where empty list was ignored for the ``only`` meta parameter of ``ModelForm``.

0.19.0 (2024-11-18)
^^^^^^^^^^^^^^^^^^^

- Dropped support for Python 3.8 and earlier. The minimum supported Python version is now 3.9.
- Dropped support for SQLAlchemy 1.3 and earlier. The minimum supported SQLAlchemy version is now 1.4.
- Added support for Python 3.10–3.13.
- Added support for SQLAlchemy 2.0.
- Added support for WTForms 3.2. The minimum supported WTForms version is now 3.1.

0.18.0 (2021-12-21)
^^^^^^^^^^^^^^^^^^^

- Dropped WTForms 1.0 support
- Added WTForms 3.0 and SA 1.4 support
- Dropped py35 support


0.17.0 (2020-06-02)
^^^^^^^^^^^^^^^^^^^

- Dropped py27, py33 and py34 support


0.16.9 (2019-03-06)
^^^^^^^^^^^^^^^^^^^

- Added support for JSON type in TypeMap (#142, pull request courtesy of fedExpress)


0.16.8 (2018-12-04)
^^^^^^^^^^^^^^^^^^^

- Fixed QuerySelectField.query allowing no results (#136, pull request courtesy of TrilceAC)


0.16.7 (2018-05-07)
^^^^^^^^^^^^^^^^^^^

- Fixed UnknownTypeException being thrown correctly for unsupported types (#131, pull request courtesy of tvuotila)


0.16.6 (2018-01-21)
^^^^^^^^^^^^^^^^^^^

- Added SQLAlchemy 1.2 support


0.16.5 (2017-07-29)
^^^^^^^^^^^^^^^^^^^

- Fixed GroupedQuerySelectMultipleField validator to support empty data (#123, pull request courtesy of superosku)


0.16.4 (2017-07-29)
^^^^^^^^^^^^^^^^^^^

- Fixed GroupedQuerySelectMultipleField validator (#121, pull request courtesy of superosku)


0.16.3 (2017-06-25)
^^^^^^^^^^^^^^^^^^^

- Fixed ChoiceType conversion for Enums (#112, pull request courtesy of fayazkhan)


0.16.2 (2017-02-28)
^^^^^^^^^^^^^^^^^^^

- Added GroupedQueryMultipleSelectField (#113, pull request courtesy of adarshk7)


0.16.1 (2016-05-11)
^^^^^^^^^^^^^^^^^^^

- Updated SQLAlchemy-Utils requirement to 0.32.6
- Fixed PhoneNumberType conversion (#102)


0.16.0 (2016-04-20)
^^^^^^^^^^^^^^^^^^^

- Dropped python 2.6 support
- Made PhoneNumberField work correctly together with DataRequired (#101, pull request courtesy of jmagnusson)


0.15.0 (2016-01-27)
^^^^^^^^^^^^^^^^^^^

- Moved GroupedQuerySelectField from WTForms-Components package to WTForms-Alchemy
- Moved WeekdaysField from WTForms-Components package to WTForms-Alchemy
- Moved PhoneNumberField from WTForms-Components package to WTForms-Alchemy
- Moved Unique validator from WTForms-Components package to WTForms-Alchemy


0.14.0 (2016-01-23)
^^^^^^^^^^^^^^^^^^^

- Added QuerySelectField and QuerySelectMultipleField which were deprecated from
WTForms as of version 2.1


0.13.3 (2015-06-17)
^^^^^^^^^^^^^^^^^^^

- Removed ClassMap's inheritance sorting. This never really worked properly and resulted in weird undeterministic bugs on Python 3.


0.13.2 (2015-05-21)
^^^^^^^^^^^^^^^^^^^

- Added support for callables in type map argument


0.13.1 (2015-04-19)
^^^^^^^^^^^^^^^^^^^

- Added flake8 checks
- Added isort checks
- Fixed country import caused by SQLAlchemy-Utils 0.30.0
- Update SQLAlchemy-Utils dependency to 0.30.0


0.13.0 (2014-10-14)
^^^^^^^^^^^^^^^^^^^

- Made all default validators configurable in model_form_factory
- Added support for disabling default validators


0.12.9 (2014-08-30)
^^^^^^^^^^^^^^^^^^^

- Added support for composite primary keys in ModelFieldList


0.12.8 (2014-07-28)
^^^^^^^^^^^^^^^^^^^

- Added support for URLType of SQLAlchemy-Utils


0.12.7 (2014-07-21)
^^^^^^^^^^^^^^^^^^^

- Fix ModelFieldList handling of simultaneous deletes and updates


0.12.6 (2014-06-12)
^^^^^^^^^^^^^^^^^^^

- Fix various issues with new-style classes


0.12.5 (2014-05-29)
^^^^^^^^^^^^^^^^^^^

- Added CountryField
- Added CountryType to CountryField conversion
- Fixed various issues with column aliases


0.12.4 (2014-03-26)
^^^^^^^^^^^^^^^^^^^

- Added WeekDaysType to WeekDaysField conversion


0.12.3 (2014-03-24)
^^^^^^^^^^^^^^^^^^^

- Fixed ChoiceType coercion for SelectFields


0.12.2 (2014-02-20)
^^^^^^^^^^^^^^^^^^^

- New configuration option: attr_errors
- Min and max info attributes generate NumberRange validator for Numeric, Float, IntRangeType and NumericRangeType columns


0.12.1 (2014-02-13)
^^^^^^^^^^^^^^^^^^^

- Updated SQLAlchemy-i18n optional dependency to 0.8.2


0.12.0 (2013-12-19)
^^^^^^^^^^^^^^^^^^^

- Added support for SQLAlchemy-Utils range types IntRange, NumericRange, DateRange and DateTimeRange
- Deprecated support for NumberRangeField
- Updated SQLAlchemy-Utils dependency to 0.23.1
- Updated WTForms-Components dependency to 0.9.0


0.11.0 (2013-12-19)
^^^^^^^^^^^^^^^^^^^

- Added configurable default validators
- Fixed ModelFieldList processing


0.10.0 (2013-12-16)
^^^^^^^^^^^^^^^^^^^

- Replaced assign_required configuration option with not_null_validator for more fine grained control of not null validation
- Replaced not_null_str_validator with not_null_validator_type_map


0.9.3 (2013-12-12)
^^^^^^^^^^^^^^^^^^

- Support for hybrid properties that return column properties
- Better exception messages for properties that are not of type ColumnProperty
- Support for class level type map customization


0.9.2 (2013-12-11)
^^^^^^^^^^^^^^^^^^

- Smarter object value inspection for ModelFieldList
- Changed ModelFieldList default population strategy to 'update' instead of 'replace'


0.9.1 (2013-12-03)
^^^^^^^^^^^^^^^^^^

- Fixed property alias handling (issue #46)


0.9.0 (2013-11-30)
^^^^^^^^^^^^^^^^^^

- Initial WTForms 2.0 support
- New configuration options: not_null_validator, not_null_str_validator


0.8.6 (2013-11-18)
^^^^^^^^^^^^^^^^^^

- Form fields now generated in class initialization time rather than on form object initialization


0.8.5 (2013-11-13)
^^^^^^^^^^^^^^^^^^

- Added Numeric type scale to DecimalField places conversion


0.8.4 (2013-11-11)
^^^^^^^^^^^^^^^^^^

- Declaration order of model fields now preserved in generated forms


0.8.3 (2013-10-28)
^^^^^^^^^^^^^^^^^^

- Added Python 2.6 support (supported versions now 2.6, 2.7 and 3.3)
- Enhanced coerce func generator


0.8.2 (2013-10-25)
^^^^^^^^^^^^^^^^^^

- TypeDecorator derived type support SelectField coerce callable generator


0.8.1 (2013-10-24)
^^^^^^^^^^^^^^^^^^

- Added support for SQLAlchemy-Utils ChoiceType
- Updated SQLAlchemy-Utils dependency to 0.18.0


0.8.0 (2013-10-11)
^^^^^^^^^^^^^^^^^^

- Fixed None value handling in string stripping when strip_string_fields option is enabled
- Python 3 support
- ModelFormMeta now configurable


0.7.15 (2013-09-06)
^^^^^^^^^^^^^^^^^^^

- Form generation now understands column aliases


0.7.14 (2013-08-27)
^^^^^^^^^^^^^^^^^^^

- Length validators only assigned to string typed columns


0.7.13 (2013-08-22)
^^^^^^^^^^^^^^^^^^^

- Model column_property methods now skipped in model generation process


0.7.12 (2013-08-18)
^^^^^^^^^^^^^^^^^^^

- Updated SQLAlchemy-Utils dependency to 0.16.7
- Updated SQLAlchemy-i18n dependency to 0.6.3


0.7.11 (2013-08-05)
^^^^^^^^^^^^^^^^^^^

- Added configuration skip_unknown_types to silently skip columns with types WTForms-Alchemy does not understand


0.7.10 (2013-08-01)
^^^^^^^^^^^^^^^^^^^

- DecimalField with scales and choices now generate SelectField as expected


0.7.9 (2013-08-01)
^^^^^^^^^^^^^^^^^^

- TSVectorType columns excluded by default


0.7.8 (2013-07-31)
^^^^^^^^^^^^^^^^^^

- String typed columns now convert to WTForms-Components StringFields instead of WTForms TextFields


0.7.7 (2013-07-31)
^^^^^^^^^^^^^^^^^^

- HTML5 step widget param support added
- Updated WTForms-Components dependency to 0.6.6


0.7.6 (2013-07-24)
^^^^^^^^^^^^^^^^^^

- TypeDecorator support added


0.7.5 (2013-05-30)
^^^^^^^^^^^^^^^^^^

- Fixed _obj setting to better cope with wtforms_components unique validator


0.7.4 (2013-05-30)
^^^^^^^^^^^^^^^^^^

- Fixed min and max arg handling when using zero values


0.7.3 (2013-05-24)
^^^^^^^^^^^^^^^^^^

- Fixed ModelFieldList object population when using 'update' population strategy


0.7.2 (2013-05-24)
^^^^^^^^^^^^^^^^^^

- Updated WTForms-Components dependency to 0.6.3
- Made type conversion use WTForms-Components HTML5 fields


0.7.1 (2013-05-23)
^^^^^^^^^^^^^^^^^^

- DataRequired validator now added to not nullable booleans by default


0.7.0 (2013-05-14)
^^^^^^^^^^^^^^^^^^

- SQLAlchemy-i18n support added


0.6.0 (2013-05-07)
^^^^^^^^^^^^^^^^^^

- Updated WTForms dependency to 1.0.4
- Updated WTForms-Components dependency to 0.5.5
- EmailType now converts to HTML5 EmailField
- Integer now converts to HTML5 IntegerField
- Numeric now converts to HTML5 DecimalField
- Date now converts to HTML5 DateField
- DateTime now converts to HTML5 DateTimeField


0.5.7 (2013-05-03)
^^^^^^^^^^^^^^^^^^

- Fixed trim function for None values


0.5.6 (2013-05-02)
^^^^^^^^^^^^^^^^^^

- Column trim option added for fine-grained control of string field trimming


0.5.5 (2013-05-02)
^^^^^^^^^^^^^^^^^^

- Bug fix: strip_string_fields applied only for string fields


0.5.4 (2013-05-02)
^^^^^^^^^^^^^^^^^^

- Possibility to give default configuration for model_form_factory function
- strip_string_fields configuration option


0.5.3 (2013-04-30)
^^^^^^^^^^^^^^^^^^

- Updated SQLAlchemy-Utils dependency to 0.10.0
- Updated WTForms-Components dependency to 0.5.4
- Added support for ColorType


0.5.2 (2013-04-25)
^^^^^^^^^^^^^^^^^^

- Added custom widget support
- Added custom filters support


0.5.1 (2013-04-16)
^^^^^^^^^^^^^^^^^^

- Updated SQLAlchemy-Utils dependency to 0.9.1
- Updated WTForms-Components dependency to 0.5.2
- Fixed Email validator auto-assigning for EmailType
- Smarter type conversion for subclassed types
- Fixed ModelFormField update handling


0.5.0 (2013-04-12)
^^^^^^^^^^^^^^^^^^

- Updated SQLAlchemy dependency to 0.8
- Completely rewritten ModelFieldList implementation


0.4.5 (2013-03-27)
^^^^^^^^^^^^^^^^^^

- Updated WTForms-Components dependencies
- Updated docs


0.4.4 (2013-03-27)
^^^^^^^^^^^^^^^^^^

- Updated WTForms-Components and SQLAlchemy-Utils dependencies


0.4.3 (2013-03-26)
^^^^^^^^^^^^^^^^^^

- Disalbed length validation for PhoneNumberType


0.4.2 (2013-03-26)
^^^^^^^^^^^^^^^^^^

- Added conversion from NumberRangeType to NumberRangeField


0.4.1 (2013-03-21)
^^^^^^^^^^^^^^^^^^

- Added conversion from PhoneNumberType to PhoneNumberField


0.4 (2013-03-15)
^^^^^^^^^^^^^^^^

- Moved custome fields, validators and widgets to WTForms-Components package


0.3.3 (2013-03-14)
^^^^^^^^^^^^^^^^^^

- Added handling of form_field_class = None


0.3.2 (2013-03-14)
^^^^^^^^^^^^^^^^^^

- Added custom field class attribute


0.3.1 (2013-03-01)
^^^^^^^^^^^^^^^^^^

- Better exception messages


0.3.0 (2013-03-01)
^^^^^^^^^^^^^^^^^^

- New unique validator syntax


0.2.5 (2013-02-16)
^^^^^^^^^^^^^^^^^^

- API documentation


0.2.4 (2013-02-08)
^^^^^^^^^^^^^^^^^^

- Enhanced unique validator
- Documented new unique validator


0.2.3 (2012-11-26)
^^^^^^^^^^^^^^^^^^

- Another fix for empty choices handling


0.2.2 (2012-11-26)
^^^^^^^^^^^^^^^^^^

- Fixed empty choices handling for string fields


0.2.1 (2012-11-22)
^^^^^^^^^^^^^^^^^^

- If validator
- Chain validator


0.2 (2012-11-05)
^^^^^^^^^^^^^^^^^^

- DateRange validator
- SelectField with optgroup support


0.1.1
^^^^^

- Added smart one-to-one and one-to-many relationship population

0.1.0
^^^^^

- Initial public release


================================================
FILE: LICENSE
================================================
Copyright (c) 2012, Konsta Vesterinen

All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
  list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
  this list of conditions and the following disclaimer in the documentation
  and/or other materials provided with the distribution.

* The names of the contributors may not be used to endorse or promote products
  derived from this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY DIRECT,
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.


================================================
FILE: MANIFEST.in
================================================
include CHANGES.rst LICENSE README.rst
recursive-include tests *
recursive-exclude tests *.pyc
recursive-include docs *
recursive-exclude docs *.pyc
prune docs/_build
exclude docs/_themes/.git


================================================
FILE: README.rst
================================================
WTForms-Alchemy
===============

|Version Status| |Downloads|

Tools for creating WTForms forms from SQLAlchemy models


Resources
---------

- `Documentation <https://wtforms-alchemy.readthedocs.io/>`_
- `Issue Tracker <http://github.com/kvesteri/wtforms-alchemy/issues>`_
- `Code <http://github.com/kvesteri/wtforms-alchemy/>`_

.. |Version Status| image:: https://img.shields.io/pypi/v/WTForms-Alchemy.svg
   :target: https://pypi.python.org/pypi/WTForms-Alchemy/
.. |Downloads| image:: https://img.shields.io/pypi/dm/WTForms-Alchemy.svg
   :target: https://pypi.python.org/pypi/WTForms-Alchemy/


================================================
FILE: docs/Makefile
================================================
# Makefile for Sphinx documentation
#

# You can set these variables from the command line.
SPHINXOPTS    =
SPHINXBUILD   = sphinx-build
PAPER         =
BUILDDIR      = _build

# Internal variables.
PAPEROPT_a4     = -D latex_paper_size=a4
PAPEROPT_letter = -D latex_paper_size=letter
ALLSPHINXOPTS   = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .

.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest

help:
	@echo "Please use \`make <target>' where <target> is one of"
	@echo "  html       to make standalone HTML files"
	@echo "  dirhtml    to make HTML files named index.html in directories"
	@echo "  singlehtml to make a single large HTML file"
	@echo "  pickle     to make pickle files"
	@echo "  json       to make JSON files"
	@echo "  htmlhelp   to make HTML files and a HTML help project"
	@echo "  qthelp     to make HTML files and a qthelp project"
	@echo "  devhelp    to make HTML files and a Devhelp project"
	@echo "  epub       to make an epub"
	@echo "  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
	@echo "  latexpdf   to make LaTeX files and run them through pdflatex"
	@echo "  text       to make text files"
	@echo "  man        to make manual pages"
	@echo "  changes    to make an overview of all changed/added/deprecated items"
	@echo "  linkcheck  to check all external links for integrity"
	@echo "  doctest    to run all doctests embedded in the documentation (if enabled)"

clean:
	-rm -rf $(BUILDDIR)/*

html:
	$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

dirhtml:
	$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
	@echo
	@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."

singlehtml:
	$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
	@echo
	@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."

pickle:
	$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
	@echo
	@echo "Build finished; now you can process the pickle files."

json:
	$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
	@echo
	@echo "Build finished; now you can process the JSON files."

htmlhelp:
	$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
	@echo
	@echo "Build finished; now you can run HTML Help Workshop with the" \
	      ".hhp project file in $(BUILDDIR)/htmlhelp."

qthelp:
	$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
	@echo
	@echo "Build finished; now you can run "qcollectiongenerator" with the" \
	      ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
	@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/WTForms-Alchemy.qhcp"
	@echo "To view the help file:"
	@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/WTForms-Alchemy.qhc"

devhelp:
	$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
	@echo
	@echo "Build finished."
	@echo "To view the help file:"
	@echo "# mkdir -p $$HOME/.local/share/devhelp/WTForms-Alchemy"
	@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/WTForms-Alchemy"
	@echo "# devhelp"

epub:
	$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
	@echo
	@echo "Build finished. The epub file is in $(BUILDDIR)/epub."

latex:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo
	@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
	@echo "Run \`make' in that directory to run these through (pdf)latex" \
	      "(use \`make latexpdf' here to do that automatically)."

latexpdf:
	$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
	@echo "Running LaTeX files through pdflatex..."
	make -C $(BUILDDIR)/latex all-pdf
	@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."

text:
	$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
	@echo
	@echo "Build finished. The text files are in $(BUILDDIR)/text."

man:
	$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
	@echo
	@echo "Build finished. The manual pages are in $(BUILDDIR)/man."

changes:
	$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
	@echo
	@echo "The overview file is in $(BUILDDIR)/changes."

linkcheck:
	$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
	@echo
	@echo "Link check complete; look for any errors in the above output " \
	      "or in $(BUILDDIR)/linkcheck/output.txt."

doctest:
	$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
	@echo "Testing of doctests in the sources finished, look at the " \
	      "results in $(BUILDDIR)/doctest/output.txt."


================================================
FILE: docs/advanced.rst
================================================
Advanced concepts
=================

Using WTForms-Alchemy with SQLAlchemy-Defaults
----------------------------------------------

WTForms-Alchemy works wonderfully with `SQLAlchemy-Defaults`_. When using `SQLAlchemy-Defaults`_ with WTForms-Alchemy you
can define your models and model forms with much more robust syntax. For more information see `SQLAlchemy-Defaults`_ documentation.


.. _SQLAlchemy-Defaults: https://github.com/kvesteri/sqlalchemy-defaults


Example ::

    from sqlalchemy_defaults import LazyConfigured


    class User(Base, LazyConfigured):
        __tablename__ = 'user'
        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(
            sa.Unicode(255),
            nullable=False,
            label=u'Name'
        )
        age = sa.Column(
            sa.Integer,
            nullable=False,
            min=18,
            max=100,
            label=u'Age'
        )


    class UserForm(ModelForm):
        class Meta:
            model = User


Using WTForms-Alchemy with Flask-WTF
------------------------------------

In order to make WTForms-Alchemy work with `Flask-WTF`_ you need the following snippet:

.. _Flask-WTF: https://github.com/lepture/flask-wtf/

::


    from flask_wtf import FlaskForm
    from wtforms_alchemy import model_form_factory
    # The variable db here is a SQLAlchemy object instance from
    # Flask-SQLAlchemy package
    from myproject.extensions import db

    BaseModelForm = model_form_factory(FlaskForm)

    class ModelForm(BaseModelForm):
        @classmethod
        def get_session(self):
            return db.session

Then you can use the ModelForm just like before:


::


    class UserForm(ModelForm):
        class Meta:
            model = User


================================================
FILE: docs/api.rst
================================================
API Documentation
=================

This part of the documentation covers all the public classes and functions
in WTForms-Alchemy.

:mod:`wtforms_alchemy`
----------------------

.. module:: wtforms_alchemy
.. autoclass:: ModelForm
    :members:
.. autofunction:: model_form_factory
.. autoclass:: ModelFormMeta
    :members:
.. autofunction:: model_form_meta_factory

:mod:`wtforms_alchemy.generator`
--------------------------------

.. module:: wtforms_alchemy.generator

.. autoclass:: FormGenerator
    :members:

:mod:`wtforms_alchemy.fields`
--------------------------------

.. module:: wtforms_alchemy.fields

.. autoclass:: QuerySelectField
    :members:

.. autoclass:: QuerySelectMultipleField
    :members:


:mod:`wtforms_alchemy.utils`
----------------------------

.. module:: wtforms_alchemy.utils

.. autofunction:: translated_attributes

.. autoclass:: ClassMap
    :members:
    :special-members:


================================================
FILE: docs/column_conversion.rst
================================================
Column to form field conversion
===============================

Basic type conversion
---------------------

By default WTForms-Alchemy converts SQLAlchemy model columns using the following
type table. So for example if an Unicode column would be converted to TextField.

The reason why so many types here convert to wtforms_components based fields is that
wtforms_components provides better HTML5 compatible type handling than WTForms at the moment.


====================================    =================
 **SQAlchemy column type**              **Form field**
------------------------------------    -----------------
    BigInteger                          wtforms_components.fields.IntegerField
    Boolean                             BooleanField
    Date                                wtforms_components.fields.DateField
    DateTime                            wtforms_components.fields.DateTimeField
    Enum                                wtforms_components.fields.SelectField
    Float                               FloatField
    Integer                             wtforms_components.fields.IntegerField
    Numeric                             wtforms_components.fields.DecimalField
    SmallInteger                        wtforms_components.fields.IntegerField
    String                              TextField
    Text                                TextAreaField
    Time                                wtforms_components.fields.TimeField
    Unicode                             TextField
    UnicodeText                         TextAreaField
====================================    =================


WTForms-Alchemy also supports many types provided by SQLAlchemy-Utils.


====================================    =================
 **SQAlchemy-Utils type**               **Form field**
------------------------------------    -----------------
    ArrowType                           wtforms_components.fields.DateTimeField
    ChoiceType                          wtforms_components.fields.SelectField
    ColorType                           wtforms_components.fields.ColorField
    CountryType                         wtforms_alchemy.fields.CountryType
    EmailType                           wtforms_components.fields.EmailField
    IPAddressType                       wtforms_components.fields.IPAddressField
    PasswordType                        wtforms.fields.PasswordField
    PhoneNumberType                     wtforms_components.fields.PhoneNumberField
    URLType                             wtforms_components.fields.StringField + URL validator
    UUIDType                            wtforms.fields.TextField + UUID validator
    WeekDaysType                        wtforms_components.fields.WeekDaysField
====================================    =================


====================================    =================
 **SQAlchemy-Utils range type**         **Form field**
------------------------------------    -----------------
    DateRangeType                       wtforms_components.fields.DateIntervalField
    DateTimeRangeType                   wtforms_components.fields.DateTimeIntervalField
    IntRangeType                        wtforms_components.fields.IntIntervalField
    NumericRangeType                    wtforms_components.fields.DecimalIntervalField
====================================    =================




Excluded fields
---------------
By default WTForms-Alchemy excludes a column from the ModelForm if one of the following conditions is True:
    * Column is primary key
    * Column is foreign key
    * Column is DateTime field which has default value (usually this is a generated value)
    * Column is of TSVectorType type
    * Column is set as model inheritance discriminator field


Using include, exclude and only
-------------------------------

If you wish the include some of the excluded fields described in the earlier chapter you can use the 'include' configuration parameter.


In the following example we include the field 'author_id' in the ArticleForm (by default it is excluded since it is a foreign key column).

::


    class Article(Base):
        __tablename__ = 'article'

        id = sa.Column(sa.Integer, primary_key=True, nullable=False)
        name = sa.Column(
            sa.Unicode(255),
            nullable=False
        )
        author_id = sa.Column(sa.Integer, sa.ForeignKey(User.id))
        author = sa.orm.relationship(User)


    class ArticleForm(Form):
        class Meta:
            include = ['author_id']


If you wish the exclude fields you can either use 'exclude' or 'only' configuration parameters. The recommended way is using only, since in most cases it is desirable to explicitly tell which fields the form should contain.

Consider the following model:

::


    class Article(Base):
        __tablename__ = 'article'

        id = sa.Column(sa.Integer, primary_key=True, nullable=False)
        name = sa.Column(
            sa.Unicode(255),
            nullable=False
        )
        content = sa.Column(
            sa.UnicodeText
        )
        description = sa.Column(
            sa.UnicodeText
        )


Now let's say we want to exclude 'description' from the form. This can be achieved as follows:

::


    class ArticleForm(Form):
        class Meta:
            exclude = ['description']


Or as follows (the recommended way):


::


    class ArticleForm(Form):
        class Meta:
            only = ['name', 'content']




Adding/overriding fields
------------------------

Example::

    from wtforms.fields import TextField, IntegerField
    from wtforms.validators import Email

    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        email = sa.Column(
            sa.Unicode(255),
            nullable=False
        )

    class UserForm(ModelForm):
        class Meta:
            model = User

        email = TextField(validators=[Optional()])
        age = IntegerField()

Now the UserForm would have three fields:
    * name, a required TextField
    * email, an optional TextField
    * age, IntegerField


Type decorators
---------------

WTForms-Alchemy supports SQLAlchemy TypeDecorator based types. When WTForms-Alchemy encounters a TypeDecorator typed column it tries to convert it to underlying type field.

Example::


    import sqlalchemy as sa
    from wtforms.fields import TextField, IntegerField
    from wtforms.validators import Email


    class CustomUnicodeType(sa.types.TypeDecorator):
        impl = sa.types.Unicode

    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        name = sa.Column(CustomUnicodeType(100), primary_key=True)


    class UserForm(ModelForm):
        class Meta:
            model = User


Now the name field of UserForm would be a simple TextField since the underlying type implementation is Unicode.


================================================
FILE: docs/conf.py
================================================
#
# WTForms-Alchemy documentation build configuration file, created by
# sphinx-quickstart on Wed Aug 29 16:20:21 2012.
#
# This file is execfile()d with the current directory set to its containing dir.
#
# Note that not all possible configuration values are present in this
# autogenerated file.
#
# All configuration values have a default; values that are commented out
# serve to show the default.

import os
import sys

# If extensions (or modules to document with autodoc) are in another directory,
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
sys.path.insert(0, os.path.abspath(".."))
from wtforms_alchemy import __version__

# -- General configuration -----------------------------------------------------

# If your documentation needs a minimal Sphinx version, state it here.
# needs_sphinx = '1.0'

# Add any Sphinx extension module names here, as strings. They can be extensions
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = [
    "sphinx.ext.autodoc",
    "sphinx.ext.intersphinx",
    "sphinx.ext.todo",
    "sphinx.ext.viewcode",
]

# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]

# The suffix of source filenames.
source_suffix = ".rst"

# The encoding of source files.
# source_encoding = 'utf-8-sig'

# The master toctree document.
master_doc = "index"

# General information about the project.
project = "WTForms-Alchemy"
copyright = "2012, Konsta Vesterinen"

# The version info for the project you're documenting, acts as replacement for
# |version| and |release|, also used in various other places throughout the
# built documents.
#
# The short X.Y version.
version = __version__
# The full version, including alpha/beta/rc tags.
release = version

# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
# language = None

# There are two options for replacing |today|: either, you set today to some
# non-false value, then it is used:
# today = ''
# Else, today_fmt is used as the format for a strftime call.
# today_fmt = '%B %d, %Y'

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
exclude_patterns = ["_build"]

# The reST default role (used for this markup: `text`) to use for all documents.
# default_role = None

# If true, '()' will be appended to :func: etc. cross-reference text.
# add_function_parentheses = True

# If true, the current module name will be prepended to all description
# unit titles (such as .. function::).
# add_module_names = True

# If true, sectionauthor and moduleauthor directives will be shown in the
# output. They are ignored by default.
# show_authors = False

# The name of the Pygments (syntax highlighting) style to use.
pygments_style = "sphinx"

# A list of ignored prefixes for module index sorting.
# modindex_common_prefix = []


# -- Options for HTML output ---------------------------------------------------

# The theme to use for HTML and HTML Help pages.  See the documentation for
# a list of builtin themes.
html_theme = "default"

# Theme options are theme-specific and customize the look and feel of a theme
# further.  For a list of options available for each theme, see the
# documentation.
# html_theme_options = {}

# Add any paths that contain custom themes here, relative to this directory.
# html_theme_path = []

# The name for this set of Sphinx documents.  If None, it defaults to
# "<project> v<release> documentation".
# html_title = None

# A shorter title for the navigation bar.  Default is the same as html_title.
# html_short_title = None

# The name of an image file (relative to this directory) to place at the top
# of the sidebar.
# html_logo = None

# The name of an image file (within the static path) to use as favicon of the
# docs.  This file should be a Windows icon file (.ico) being 16x16 or 32x32
# pixels large.
# html_favicon = None

# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ["_static"]

# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
# using the given strftime format.
# html_last_updated_fmt = '%b %d, %Y'

# If true, SmartyPants will be used to convert quotes and dashes to
# typographically correct entities.
# html_use_smartypants = True

# Custom sidebar templates, maps document names to template names.
# html_sidebars = {}

# Additional templates that should be rendered to pages, maps page names to
# template names.
# html_additional_pages = {}

# If false, no module index is generated.
# html_domain_indices = True

# If false, no index is generated.
# html_use_index = True

# If true, the index is split into individual pages for each letter.
# html_split_index = False

# If true, links to the reST sources are added to the pages.
# html_show_sourcelink = True

# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
# html_show_sphinx = True

# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
# html_show_copyright = True

# If true, an OpenSearch description file will be output, and all pages will
# contain a <link> tag referring to it.  The value of this option must be the
# base URL from which the finished HTML is served.
# html_use_opensearch = ''

# This is the file name suffix for HTML files (e.g. ".xhtml").
# html_file_suffix = None

# Output file base name for HTML help builder.
htmlhelp_basename = "WTForms-Alchemydoc"


# -- Options for LaTeX output --------------------------------------------------

# The paper size ('letter' or 'a4').
# latex_paper_size = 'letter'

# The font size ('10pt', '11pt' or '12pt').
# latex_font_size = '10pt'

# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
    (
        "index",
        "WTForms-Alchemy.tex",
        "WTForms-Alchemy Documentation",
        "Konsta Vesterinen",
        "manual",
    ),
]

# The name of an image file (relative to this directory) to place at the top of
# the title page.
# latex_logo = None

# For "manual" documents, if this is true, then toplevel headings are parts,
# not chapters.
# latex_use_parts = False

# If true, show page references after internal links.
# latex_show_pagerefs = False

# If true, show URL addresses after external links.
# latex_show_urls = False

# Additional stuff for the LaTeX preamble.
# latex_preamble = ''

# Documents to append as an appendix to all manuals.
# latex_appendices = []

# If false, no module index is generated.
# latex_domain_indices = True


# -- Options for manual page output --------------------------------------------

# One entry per manual page. List of tuples
# (source start file, name, description, authors, manual section).
man_pages = [
    (
        "index",
        "wtforms-alchemy",
        "WTForms-Alchemy Documentation",
        ["Konsta Vesterinen"],
        1,
    )
]


# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {
    "http://docs.python.org/": None,
    "https://wtforms.readthedocs.io/en/latest/": None,
    "https://sqlalchemy-utils.readthedocs.io/en/latest/": None,
    "https://wtforms-components.readthedocs.io/en/latest/": None,
}


================================================
FILE: docs/configuration.rst
================================================
Configuration
=============

ModelForm meta parameters
-------------------------

The following configuration options are available for ModelForm's Meta subclass.

**include_primary_keys** (default: False)

If you wish to include primary keys in the generated form please set this to True.
This is useful when dealing with natural primary keys. In the following example each
user has a natural primary key on its column name.

The UserForm would contain two fields name and email. ::

    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        email = sa.Column(sa.Unicode(255), nullable=False)


    class UserForm(ModelForm):
        class Meta:
            model = User
            include_primary_keys = True


**exclude**

.. warning::

    Using ``exclude`` might lead to problems in situations where you add columns to your model
    and forget to exclude those from the form by using ``exclude``, hence it is recommended to
    use ``only`` rather than ``exclude``.


You can exclude certain fields by adding them to the exclude list. ::

    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        email = sa.Column(sa.Unicode(255), nullable=False)


    class UserForm(ModelForm):
        class Meta:
            model = User
            include_primary_keys = True
            exclude = ['email']
            # this form contains only 'name' field


**only**

Generates a form using only the field names provided in ``only``.

::

    class UserForm(ModelForm):
        class Meta:
            model = User
            only = ['email']


**field_args** (default: {})

This parameter can be used for overriding field arguments. In the following example we force the email field optional.

::


     class UserForm(ModelForm):
        class Meta:
            model = User
            field_args = {'email': {'validators': [Optional()]}}


**include_foreign_keys** (default: False)

Foreign keys can be included in the form by setting include_foreign_keys to True.

**only_indexed_fields** (default: False)

When setting this option to True, only fields that have an index will be included in
the form. This is very useful when creating forms for searching a specific model.


**include_datetimes_with_default** (default: False)

When setting this option to True, datetime with default values will be included in the
form. By default this is False since usually datetime fields that have default values
are generated columns such as "created_at" or "updated_at", which should not be included
in the form.


**validators**

A dict containing additional validators for the generated form field objects.

Example::

    from wtforms.validators import Email


    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        email = sa.Column(sa.Unicode(255), nullable=False)


    class UserForm(ModelForm):
        class Meta:
            model = User
            include_primary_keys = True
            validators = {'email': [Email()]}

**datetime_format** (default: '%Y-%m-%d %H:%M:%S')

Defines the default datetime format, which will be assigned to generated datetime
fields.

**date_format** (default: '%Y-%m-%d')

Defines the default date format, which will be assigned to generated datetime
fields.


**all_fields_optional** (default: False)

Defines all generated fields as optional (useful for update forms).

**assign_required** (default: True)

Whether or not to assign non-nullable fields as required.

**strip_string_fields** (default: False)

Whether or not to add stripping filter to all string fields.

Example ::


    from werkzeug.datastructures import MultiDict


    class UserForm(ModelForm):
        class Meta:
            model = User
            strip_string_fields = True


    form = UserForm(MultiDict([('name', 'someone     ')]))

    assert form.name.data == 'someone'


You can also fine-grain field stripping by using trim argument for columns. In the example
below the field 'name' would have its values stripped whereas field 'password' would not. ::


    from wtforms.validators import Email


    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.Unicode(100))
        password = sa.Column(sa.Unicode(100), info={'trim': False})


    class UserForm(ModelForm):
        class Meta:
            model = User
            strip_string_fields = True


**form_generator** (default: FormGenerator class)

Change this if you want to use custom form generator class.


Form inheritance
----------------

ModelForm's configuration support inheritance. This means that child classes inherit
parents Meta properties.

Example::

    from wtforms.validators import Email


    class UserForm(ModelForm):
        class Meta:
            model = User
            validators = {'email': [Email()]}


    class UserUpdateForm(UserForm):
        class Meta:
            all_fields_optional = True


Here UserUpdateForm inherits the configuration properties of UserForm, hence it would
use model User and have additional Email validator on column 'email'. Also it assigns
all fields as optional.


Not nullable column validation
------------------------------

WTForms-Alchemy offers two options for configuring how not nullable columns are validated:

* not_null_validator

    The default validator to be used for not nullable columns. Set this to `None`
    if you wish to disable it. By default this is `[InputRequired()]`.


* not_null_validator_type_map

    Type map which overrides the **not_null_validator** on specific column type. By default this is `ClassMap({sa.String: [InputRequired(), DataRequired()]})`.


In the following example we set `DataRequired` validator for all not nullable Enum typed columns:


::

    import sqlalchemy as sa
    from wtforms.validators import DataRequired
    from wtforms_alchemy import ClassMap


    class MyForm(ModelForm):
        class Meta:
            not_null_validator_type_map = ClassMap({sa.Enum: [DataRequired()]})



Customizing type conversion
---------------------------

You can customize the SQLAlchemy type conversion on class level with type_map Meta property.

Type map accepts dictionary of SQLAlchemy types as keys and WTForms field classes
as values. The key value pairs of this dictionary override the key value pairs of FormGenerator.TYPE_MAP.

Let's say we want to convert all unicode typed properties to TextAreaFields instead of StringFields. We can do this by assigning Unicode, TextAreaField key value pair into type map.

::


    from wtforms.fields import TextAreaField
    from wtforms_alchemy import ClassMap


    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.Unicode(100))


    class UserForm(ModelForm):
        class Meta:
            type_map = ClassMap({sa.Unicode: TextAreaField})


In case the type_map dictionary values are not inherited from WTForm field class, they are considered callable functions. These functions will be called with the corresponding column as their only parameter.


.. _custom_base:

Custom form base class
----------------------

You can use custom base class for your model forms by using model_form_factory
function. In the following example we have a UserForm which uses Flask-WTF
form as a parent form for ModelForm. ::


    from flask.ext.wtf import Form
    from wtforms_alchemy import model_form_factory


    ModelForm = model_form_factory(Form)


    class UserForm(ModelForm):
        class Meta:
            model = User


You can also pass any form generator option to model_form_factory. ::


    ModelForm = model_form_factory(Form, strip_string_fields=True)


    class UserForm(ModelForm):
        class Meta:
            model = User


================================================
FILE: docs/customization.rst
================================================
Form customization
==================


Custom fields
-------------

If you want to use a custom field class, you can pass it by using
form_field_class parameter for the column info dictionary.

Example ::


    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        color = sa.Column(
            sa.String(7),
            info={'form_field_class': ColorField},
            nullable=False
        )

    class UserForm(ModelForm):
        class Meta:
            model = User

Now the 'color' field of UserForm would be a custom ColorField.


Forcing the use of SelectField
------------------------------

Sometimes you may want to have integer and unicode fields convert to SelectFields.
Probably the easiest way to achieve this is by using choices parameter for the column
info dictionary.

Example ::


    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        age = sa.Column(
            sa.Integer,
            info={'choices': [(i, i) for i in xrange(13, 99)]},
            nullable=False
        )

    class UserForm(ModelForm):
        class Meta:
            model = User


Here the UserForm would have two fields. One TextField for the name column and one
SelectField for the age column containing range of choices from 13 to 99.

Notice that WTForms-Alchemy is smart enough to use the right coerce function based on
the underlying column type, hence in the previous example the age column would convert
to the following SelectField. ::


    SelectField('Age', coerce=int, choices=[(i, i) for i in xrange(13, 99)])


For nullable unicode and string columns WTForms-Alchemy uses special null_or_unicode
coerce function, which converts empty strings to None values.


Field descriptions
------------------

Example::

    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        email = sa.Column(
            sa.Unicode(255),
            nullable=False,
            info={'description': 'This is the description of email.'}
        )

    class UserForm(ModelForm):
        class Meta:
            model = User

Now the 'email' field of UserForm would have description 'This is the description of email.'


Field labels
------------

Example::

    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(
            sa.Unicode(100), primary_key=True, nullable=False,
            info={'label': 'Name'}
        )

    class UserForm(ModelForm):
        class Meta:
            model = User

Now the 'name' field of UserForm would have label 'Name'.


Custom widgets
--------------

Example::

    from wtforms import widgets


    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(
            sa.Unicode(100), primary_key=True, nullable=False,
            info={'widget': widgets.HiddenInput()}
        )

    class UserForm(ModelForm):
        class Meta:
            model = User

Now the 'name' field of UserForm would use HiddenInput widget instead of TextInput.


Default values
--------------

By default WTForms-Alchemy ModelForm assigns the default values from column definitions.
Example ::

    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        level = sa.Column(sa.Integer, default=1)

    class UserForm(ModelForm):
        class Meta:
            model = User

Now the UseForm 'level' field default value would be 1.


================================================
FILE: docs/index.rst
================================================
WTForms-Alchemy
===============

WTForms-Alchemy is a WTForms extension toolkit for easier creation of model
based forms. Strongly influenced by Django ModelForm.




.. toctree::
   :maxdepth: 2

   introduction
   column_conversion
   types
   customization
   validators
   configuration
   relationships
   advanced
   api
   license





.. _`SQLAlchemy-Utils ChoiceType`: https://sqlalchemy-utils.readthedocs.io/en/latest/#choicetype
.. _`SQLAlchemy-Defaults`: https://sqlalchemy-defaults.readthedocs.io/en/latest/
.. _`Flask-WTF`: https://flask-wtf.readthedocs.io/en/latest/



================================================
FILE: docs/introduction.rst
================================================
Introduction
============

What for?
---------
Many times when building modern web apps with SQLAlchemy you’ll have forms that
map closely to models. For example, you might have a Article model,
and you want to create a form that lets people post new article. In this case,
it would be time-consuming to define the field types and basic validators in
your form, because you’ve already defined the fields in your model.

WTForms-Alchemy provides a helper class that let you create a Form class from a
SQLAlchemy model.

Differences with wtforms.ext.sqlalchemy model_form
--------------------------------------------------

WTForms-Alchemy does not try to replace all the functionality of wtforms.ext.sqlalchemy.
It only tries to replace the model_form function of wtforms.ext.sqlalchemy by a much better solution.
Other functionality of .ext.sqlalchemy such as QuerySelectField and QuerySelectMultipleField can be used
along with WTForms-Alchemy.

Now how is WTForms-Alchemy ModelForm better than wtforms.ext.sqlachemy's model_form?

* Provides explicit declaration of ModelForms (much easier to override certain columns)
* Form generation supports Unique and NumberRange validators
* Form inheritance support (along with form configuration inheritance)
* Automatic SelectField type coercing based on underlying column type
* By default uses wtforms_components SelectField for fields with choices. This field understands None values and renders nested datastructures as optgroups.
* Provides better Unique validator
* Supports custom user defined types as well as type decorators
* Supports SQLAlchemy-Utils datatypes
* Supports ModelForm model relations population
* Smarter field exclusion
* Smarter field conversion
* Understands join table inheritance
* Better configuration


Installation
------------

::


    pip install WTForms-Alchemy



The supported Python versions are 3.9–3.13.



QuickStart
----------

Lets say we have a model called User with couple of fields::

    import sqlalchemy as sa
    from sqlalchemy import create_engine
    from sqlalchemy.orm import declarative_base, sessionmaker
    from wtforms_alchemy import ModelForm

    engine = create_engine('sqlite:///:memory:')
    Base = declarative_base(engine)
    Session = sessionmaker(bind=engine)
    session = Session()

    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.BigInteger, autoincrement=True, primary_key=True)
        name = sa.Column(sa.Unicode(100), nullable=False)
        email = sa.Column(sa.Unicode(255), nullable=False)


Now we can create our first ModelForm for the User model. ModelForm behaves almost
like your ordinary WTForms Form except it accepts special Meta arguments. Every ModelForm
must define model parameter in the Meta arguments.::

    class UserForm(ModelForm):
        class Meta:
            model = User


Now this ModelForm is essentially the same as ::

    class UserForm(Form):
        name = TextField(validators=[DataRequired(), Length(max=100)])
        email = TextField(validators=[DataRequired(), Length(max=255)])

In the following chapters you'll learn how WTForms-Alchemy converts SQLAlchemy model
columns to form fields.


================================================
FILE: docs/license.rst
================================================
License
=======

.. include:: ../LICENSE


================================================
FILE: docs/make.bat
================================================
@ECHO OFF

REM Command file for Sphinx documentation

if "%SPHINXBUILD%" == "" (
	set SPHINXBUILD=sphinx-build
)
set BUILDDIR=_build
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
if NOT "%PAPER%" == "" (
	set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
)

if "%1" == "" goto help

if "%1" == "help" (
	:help
	echo.Please use `make ^<target^>` where ^<target^> is one of
	echo.  html       to make standalone HTML files
	echo.  dirhtml    to make HTML files named index.html in directories
	echo.  singlehtml to make a single large HTML file
	echo.  pickle     to make pickle files
	echo.  json       to make JSON files
	echo.  htmlhelp   to make HTML files and a HTML help project
	echo.  qthelp     to make HTML files and a qthelp project
	echo.  devhelp    to make HTML files and a Devhelp project
	echo.  epub       to make an epub
	echo.  latex      to make LaTeX files, you can set PAPER=a4 or PAPER=letter
	echo.  text       to make text files
	echo.  man        to make manual pages
	echo.  changes    to make an overview over all changed/added/deprecated items
	echo.  linkcheck  to check all external links for integrity
	echo.  doctest    to run all doctests embedded in the documentation if enabled
	goto end
)

if "%1" == "clean" (
	for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
	del /q /s %BUILDDIR%\*
	goto end
)

if "%1" == "html" (
	%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The HTML pages are in %BUILDDIR%/html.
	goto end
)

if "%1" == "dirhtml" (
	%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
	goto end
)

if "%1" == "singlehtml" (
	%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
	goto end
)

if "%1" == "pickle" (
	%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; now you can process the pickle files.
	goto end
)

if "%1" == "json" (
	%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; now you can process the JSON files.
	goto end
)

if "%1" == "htmlhelp" (
	%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; now you can run HTML Help Workshop with the ^
.hhp project file in %BUILDDIR%/htmlhelp.
	goto end
)

if "%1" == "qthelp" (
	%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; now you can run "qcollectiongenerator" with the ^
.qhcp project file in %BUILDDIR%/qthelp, like this:
	echo.^> qcollectiongenerator %BUILDDIR%\qthelp\WTForms-Alchemy.qhcp
	echo.To view the help file:
	echo.^> assistant -collectionFile %BUILDDIR%\qthelp\WTForms-Alchemy.ghc
	goto end
)

if "%1" == "devhelp" (
	%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished.
	goto end
)

if "%1" == "epub" (
	%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The epub file is in %BUILDDIR%/epub.
	goto end
)

if "%1" == "latex" (
	%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
	goto end
)

if "%1" == "text" (
	%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The text files are in %BUILDDIR%/text.
	goto end
)

if "%1" == "man" (
	%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
	if errorlevel 1 exit /b 1
	echo.
	echo.Build finished. The manual pages are in %BUILDDIR%/man.
	goto end
)

if "%1" == "changes" (
	%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
	if errorlevel 1 exit /b 1
	echo.
	echo.The overview file is in %BUILDDIR%/changes.
	goto end
)

if "%1" == "linkcheck" (
	%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
	if errorlevel 1 exit /b 1
	echo.
	echo.Link check complete; look for any errors in the above output ^
or in %BUILDDIR%/linkcheck/output.txt.
	goto end
)

if "%1" == "doctest" (
	%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
	if errorlevel 1 exit /b 1
	echo.
	echo.Testing of doctests in the sources finished, look at the ^
results in %BUILDDIR%/doctest/output.txt.
	goto end
)

:end


================================================
FILE: docs/relationships.rst
================================================
Forms with relations
====================

WTForms-Alchemy provides special Field subtypes ModelFormField and ModelFieldList.
When using these types WTForms-Alchemy understands model relations and is smart enough to populate related
objects accordingly.

One-to-one relations
--------------------

Consider the following example. We have Event and Location
classes with each event having one location. ::

    from sqlalchemy.orm import declarative_base
    from wtforms_alchemy import ModelForm, ModelFormField

    Base = declarative_base()


    class Location(Base):
        __tablename__ = 'location'
        id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
        name = sa.Column(sa.Unicode(255), nullable=True)

    class Event(Base):
        __tablename__ = 'event'
        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.Unicode(255), nullable=False)
        location_id = sa.Column(sa.Integer, sa.ForeignKey(Location.id))
        location = sa.orm.relationship(Location)

    class LocationForm(ModelForm):
        class Meta:
            model = Location

    class EventForm(ModelForm):
        class Meta:
            model = Event

        location = ModelFormField(LocationForm)

Now if we populate the EventForm, WTForms-Alchemy is smart enough to populate related
location too. ::

    event = Event()
    form = EventForm(request.POST)
    form.populate_obj(event)



One-to-many relations
---------------------

Consider the following example. We have Event and Location
classes with each event having many location. Notice we are using FormField along
with ModelFieldList. ::

    from sqlalchemy.orm import declarative_base
    from wtforms_alchemy import ModelForm, ModelFieldList
    from wtforms.fields import FormField

    Base = declarative_base()


    class Event(Base):
        __tablename__ = 'event'
        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.Unicode(255), nullable=False)


    class Location(Base):
        __tablename__ = 'location'
        id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
        name = sa.Column(sa.Unicode(255), nullable=True)

        event_id = sa.Column(sa.Integer, sa.ForeignKey(Event.id))
        event = sa.orm.relationship(
            Location,
            backref='locations'  # the event needs to have this
        )


    class LocationForm(ModelForm):
        class Meta:
            model = Location


    class EventForm(ModelForm):
        class Meta:
            model = Event

        locations = ModelFieldList(FormField(LocationForm))

Now if we populate the EventForm, WTForms-Alchemy is smart enough to populate related
locations too. ::

    event = Event()
    form = EventForm(request.POST)
    form.populate_obj(event)


================================================
FILE: docs/types.rst
================================================
Type specific conversion
========================


Numeric type
------------

WTForms-Alchemy automatically converts Numeric columns to DecimalFields. The
converter is also smart enough to convert different decimal scales to
appropriate HTML5 input step args.


::


    class Account(Base):
        __tablename__ = 'event'

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        balance = sa.Column(
            sa.Numeric(scale=2),
            nullable=False
        )

    class AccountForm(ModelForm):
        class Meta:
            model = Account


Now rendering AccountForm.balance would return the following HTML:

<input type='decimal' required step="0.01">


Arrow type
----------

WTForms-Alchemy supports the ArrowType of SQLAlchemy-Utils and converts it to
HTML5 compatible DateTimeField of WTForms-Components.

::


    from sqlalchemy_utils import ArrowType


    class Event(Base):
        __tablename__ = 'event'

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        start_time = sa.Column(
            ArrowType(),
            nullable=False
        )

    class EventForm(ModelForm):
        class Meta:
            model = Event


Now the EventForm is essentially the same as:

::


    class EventForm(Form):
        start_time = DateTimeField(validators=[DataRequired()])


Choice type
-----------

WTForms-Alchemy automatically converts
:class:`sqlalchemy_utils.types.choice.ChoiceType` to WTForms-Components
SelectField.


::


    from sqlalchemy_utils import ChoiceType


    class Event(Base):
        __tablename__ = 'event'
        TYPES = [
            (u'party', u'Party'),
            (u'training, u'Training')
        ]

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        type = sa.Column(ChoiceType(TYPES))


    class EventForm(ModelForm):
        class Meta:
            model = Event


Now the EventForm is essentially the same as:

::

    from wtforms_alchemy.utils import choice_type_coerce_factory


    class EventForm(Form):
        type = SelectField(
            choices=Event.TYPES,
            coerce=choice_type_coerce_factory(Event.type.type),
            validators=[DataRequired()]
        )



Color type
----------

::


    from sqlalchemy_utils import ColorType


    class CustomView(Base):
        __tablename__ = 'view'

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        background_color = sa.Column(
            ColorType(),
            nullable=False
        )

    class CustomViewForm(ModelForm):
        class Meta:
            model = CustomView


Now the CustomViewForm is essentially the same as:

::


    from wtforms_components import ColorField


    class CustomViewForm(Form):
        color = ColorField(validators=[DataRequired()])



Country type
------------

::


    from sqlalchemy_utils import CountryType


    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        country = sa.Column(CountryType, nullable=False)


    class UserForm(ModelForm):
        class Meta:
            model = User


The UserForm is essentially the same as:

::


    from wtforms_components import CountryField


    class UserForm(Form):
        country = CountryField(validators=[DataRequired()])



Email type
----------

::


    from sqlalchemy_utils import EmailType


    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        email = sa.Column(EmailType, nullable=False)


    class UserForm(ModelForm):
        class Meta:
            model = User


The good old wtforms equivalent of this form would be:

::


    from wtforms_components import EmailField


    class UserForm(Form):
        email = EmailField(validators=[DataRequired()])



Password type
-------------

Consider the following model definition:

::


    from sqlalchemy_utils import PasswordType


    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        name = sa.Column(sa.Unicode(100), nullable=False)
        password = sa.Column(
            PasswordType(
                schemes=['pbkdf2_sha512']
            ),
            nullable=False
        )

    class UserForm(ModelForm):
        class Meta:
            model = User


Now the UserForm is essentially the same as:

::

    class UserForm(Form):
        name = TextField(validators=[DataRequired(), Length(max=100)])
        password = PasswordField(validators=[DataRequired()])




Phonenumber type
----------------

WTForms-Alchemy supports the PhoneNumberType of SQLAlchemy-Utils and converts it automatically
to WTForms-Components PhoneNumberField. This field renders itself as HTML5 compatible phonenumber input.


Consider the following model definition:

::


    from sqlalchemy_utils import PhoneNumberType


    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        name = sa.Column(sa.Unicode(100), nullable=False)
        phone_number = sa.Column(PhoneNumberType())


    class UserForm(ModelForm):
        class Meta:
            model = User


Now the UserForm is essentially the same as:

::

    from wtforms_components import PhoneNumberField


    class UserForm(Form):
        name = TextField(validators=[DataRequired(), Length(max=100)])
        phone_number = PhoneNumberField(validators=[DataRequired()])


URL type
--------

WTForms-Alchemy automatically converts SQLAlchemy-Utils URLType to StringField and adds URL validator for it.

Consider the following model definition:

::


    from sqlalchemy_utils import URLType


    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.Integer, primary_key=True, autoincrement=True)
        website = sa.Column(URLType())


    class UserForm(ModelForm):
        class Meta:
            model = User


Now the UserForm is essentially the same as:

::

    from wtforms_components import StringField
    from wtforms.validators import URL


    class UserForm(Form):
        website = StringField(validators=[URL()])


================================================
FILE: docs/validators.rst
================================================
Validators
==========


Auto-assigned validators
------------------------

By default WTForms-Alchemy ModelForm assigns the following validators:
    * InputRequired validator if column is not nullable and has no default value
    * DataRequired validator if column is not nullable, has no default value and is of type `sqlalchemy.types.String`
    * NumberRange validator if column if of type Integer, Float or Decimal and column info parameter has min or max arguments defined
    * DateRange validator if column is of type Date or DateTime and column info parameter has min or max arguments defined
    * TimeRange validator if column is of type Time and info parameter has min or max arguments defined
    * Unique validator if column has a unique index
    * Length validator for String/Unicode columns with max length
    * Optional validator for all nullable columns


Unique validator
----------------

WTForms-Alchemy automatically assigns unique validators for columns which have unique indexes defined. Unique validator raises ValidationError exception whenever a non-unique value for given column is assigned. Consider the following model/form definition. Notice how you need to define get_session() classmethod for your form. Unique validator uses this method for getting the appropriate SQLAlchemy session.
::


    engine = create_engine('sqlite:///:memory:')

    Base = declarative_base()

    Session = sessionmaker(bind=engine)
    session = Session()


    class User(Base):
        __tablename__ = 'user'

        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.Unicode(100), nullable=False)
        email = sa.Column(
            sa.Unicode(255),
            nullable=False,
            unique=True
        )

    class UserForm(ModelForm):
        class Meta:
            model = User

        @classmethod
        def get_session():
            # this method should return sqlalchemy session
            return session


Here UserForm would behave the same as the following form:
::


    class UserForm(Form):
        name = TextField('Name', validators=[DataRequired(), Length(max=100)])
        email = TextField(
            'Email',
            validators=[
                DataRequired(),
                Length(max=255),
                Unique(User.email, get_session=lambda: session)
            ]
        )


If you are using Flask-SQLAlchemy or similar tool, which assigns session-bound query property to your declarative models, you don't need to define the get_session() method. Simply use:

::

    Unique(User.email)


Using unique validator with existing objects
--------------------------------------------

When editing an existing object, WTForms-Alchemy must know the object currently edited to avoid raising a ValidationError. Here how to proceed to inform WTForms-Alchemy of this case.
Example::

    obj = MyModel.query.get(1)
    form = MyForm(obj=obj)
    form.populate_obj(obj)
    form.validate()

WTForms-Alchemy will then understand to avoid the unique validation of the object with this same object.


Range validators
----------------

WTForms-Alchemy automatically assigns range validators based on column type and assigned column info min and max attributes.

In the following example we create a form for Event model where start_time can't be set in the past.

::

    class Event(Base):
        __tablename__ = 'event'

        id = sa.Column(sa.Integer, primary_key=True)
        name = sa.Column(sa.Unicode(255))
        start_time = sa.Column(sa.DateTime, info={'min': datetime.now()})


    class EventForm(ModelForm):
        class Meta:
            model = Event



Additional field validators
---------------------------

Example::

    from wtforms.validators import Email

    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        email = sa.Column(
            sa.Unicode(255),
            nullable=False,
            info={'validators': Email()}
        )

    class UserForm(ModelForm):
        class Meta:
            model = User

Now the 'email' field of UserForm would have Email validator.


Overriding default validators
-----------------------------

Sometimes you may want to override what class WTForms-Alchemy uses for email, number_range, length etc. validations.
For all automatically assigned validators WTForms-Alchemy provides configuration options to override the default validator.

In the following example we set a custom Email validator for User class.

::


    from sqlalchemy_utils import EmailType
    from wtforms_components import Email


    class User(Base):
        __tablename__ = 'user'

        name = sa.Column(sa.Unicode(100), primary_key=True, nullable=False)
        email = sa.Column(
            EmailType,
            nullable=False,
        )

    class MyEmailValidator(Email):
        def __init__(self, message='My custom email error message'):
            Email.__init__(self, message=message)


    class UserForm(ModelForm):
        class Meta:
            model = User
            email_validator = MyEmailValidator


If you don't wish to subclass you can simply use functions / lambdas:

::


    def email():
        return Email(message='My custom email error message')


    class UserForm(ModelForm):
        class Meta:
            model = User
            email_validator = email


You can also override validators that take multiple arguments this way:

::


    def length(min=None, max=None):
        return Length(min=min, max=max, message='Wrong length')


    class UserForm(ModelForm):
        class Meta:
            model = User
            length_validator = length


Here is the full list of configuration options you can use to override default validators:

* email_validator

* length_validator

* unique_validator

* number_range_validator

* date_range_validator

* time_range_validator

* optional_validator


Disabling validators
--------------------

You can disable certain validators by assigning them as `None`. Let's say you want to disable nullable columns having `Optional` validator. This can be achieved as follows::


    class UserForm(ModelForm):
        class Meta:
            model = User
            optional_validator = None


================================================
FILE: pyproject.toml
================================================
[tool.pytest.ini_options]
filterwarnings = [
    'error:.*:sqlalchemy.exc.SADeprecationWarning',
    'error:.*:sqlalchemy.exc.SAWarning',
    'ignore:.*:sqlalchemy.exc.SADeprecationWarning:sqlalchemy_i18n',
]

[tool.ruff]
target-version = "py39"

[tool.ruff.lint]
select = [
    "C90", # mccabe
    "E",   # pycodestyle errors
    "F",   # Pyflakes
    "I",   # isort
    "UP",  # pyupgrade
    "W",   # pycodestyle warnings
]

[tool.ruff.lint.isort]
order-by-type = false


================================================
FILE: setup.py
================================================
"""
WTForms-Alchemy
---------------

Generates WTForms forms from SQLAlchemy models.
"""

import os
import re

from setuptools import setup

HERE = os.path.dirname(os.path.abspath(__file__))


def get_version():
    filename = os.path.join(HERE, "wtforms_alchemy", "__init__.py")
    with open(filename) as f:
        contents = f.read()
    pattern = r'^__version__ = "(.*?)"$'
    return re.search(pattern, contents, re.MULTILINE).group(1)


extras_require = {
    "test": [
        "enum34",
        "pytest>=2.3",
        "Pygments>=1.2",
        "Jinja2>=2.3",
        "docutils>=0.10",
        "flexmock>=0.9.7",
        "ruff==0.7.4",
        "WTForms-Test>=0.1.1",
    ],
    "babel": ["Babel>=1.3"],
    "arrow": ["arrow>=0.3.4"],
    "phone": ["phonenumbers>=5.9.2"],
    "intervals": ["intervals>=0.2.0"],
    "password": ["passlib >= 1.6, < 2.0"],
    "color": ["colour>=0.0.4"],
    "i18n": ["SQLAlchemy-i18n >= 0.8.2"],
    "timezone": ["python-dateutil"],
}


# Add all optional dependencies to testing requirements.
for name, requirements in extras_require.items():
    if name != "test":
        extras_require["test"] += requirements


setup(
    name="WTForms-Alchemy",
    version=get_version(),
    url="https://github.com/kvesteri/wtforms-alchemy",
    license="BSD",
    author="Konsta Vesterinen",
    author_email="konsta@fastmonkeys.com",
    description="Generates WTForms forms from SQLAlchemy models.",
    long_description=__doc__,
    packages=["wtforms_alchemy"],
    zip_safe=False,
    include_package_data=True,
    platforms="any",
    install_requires=[
        "SQLAlchemy>=1.4",
        "WTForms>=3.1.0",
        "WTForms-Components>=0.11.0",
        "SQLAlchemy-Utils>=0.40.0",
    ],
    extras_require=extras_require,
    classifiers=[
        "Environment :: Web Environment",
        "Intended Audience :: Developers",
        "License :: OSI Approved :: BSD License",
        "Operating System :: OS Independent",
        "Programming Language :: Python",
        "Programming Language :: Python :: 3",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3.11",
        "Programming Language :: Python :: 3.12",
        "Programming Language :: Python :: 3.13",
        "Topic :: Internet :: WWW/HTTP :: Dynamic Content",
        "Topic :: Software Development :: Libraries :: Python Modules",
    ],
)


================================================
FILE: tests/__init__.py
================================================
import sqlalchemy as sa
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy.orm.session import close_all_sessions
from sqlalchemy_utils import force_auto_coercion
from wtforms_test import FormTestCase

from wtforms_alchemy import ModelForm

force_auto_coercion()


class MultiDict(dict):
    def getlist(self, key):
        return [self[key]]


class ModelFormTestCase(FormTestCase):
    dns = "sqlite:///:memory:"

    def setup_method(self, method):
        self.engine = create_engine(self.dns)
        self.base = declarative_base()

    def teardown_method(self, method):
        self.engine.dispose()
        self.ModelTest = None
        self.form_class = None

    def init_model(self, type_=sa.Unicode(255), **kwargs):
        kwargs.setdefault("nullable", False)

        class ModelTest(self.base):
            __tablename__ = "model_test"
            query = None
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(type_, **kwargs)
            some_property = "something"

        self.ModelTest = ModelTest

    def init(self, type_=sa.Unicode(255), **kwargs):
        self.init_model(type_=type_, **kwargs)
        self.init_form()

    def init_form(self):
        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest

        self.form_class = ModelTestForm


class FormRelationsTestCase:
    dns = "sqlite:///:memory:"

    def setup_method(self, method):
        self.engine = create_engine(self.dns)

        self.base = declarative_base()
        self.create_models()
        self.create_forms()

        self.base.metadata.create_all(self.engine)

        Session = sessionmaker(bind=self.engine)
        self.session = Session()

    def teardown_method(self, method):
        close_all_sessions()
        self.base.metadata.drop_all(self.engine)
        self.engine.dispose()


================================================
FILE: tests/conftest.py
================================================
import pytest
from wtforms.form import FormMeta

from wtforms_alchemy import (
    model_form_factory,
    model_form_meta_factory,
    ModelFormMeta,
)


class _MetaWithInit(FormMeta):
    def __init__(cls, *args, **kwargs):
        cls.test_attr = "SomeVal"
        FormMeta.__init__(cls, *args, **kwargs)


MetaWithInit = model_form_meta_factory(_MetaWithInit)


class _MetaWithoutInit(FormMeta):
    test_attr = "SomeVal"


MetaWithoutInit = model_form_meta_factory(_MetaWithoutInit)


@pytest.fixture(params=[MetaWithInit, MetaWithoutInit, ModelFormMeta])
def model_form_all(request):
    """Returns one of each possible model form classes with custom and the
    original metaclass."""
    ModelForm = model_form_factory(meta=request.param)
    return ModelForm


@pytest.fixture(params=[MetaWithInit, MetaWithoutInit])
def model_form_custom(request):
    """Returns one of each possible model form classes with custom
    metaclasses."""
    return model_form_factory(meta=request.param)


================================================
FILE: tests/test_class_map.py
================================================
from pytest import mark, raises

from wtforms_alchemy.utils import ClassMap


class A:
    pass


class B:
    pass


class A2(A):
    pass


class A3(A2):
    pass


class A4(A3):
    pass


class A5(A4):
    pass


class B2(B):
    pass


class C:
    pass


@mark.parametrize("key", [B2, B, A, A2])
def test_contains_with_subclass_check(key):
    class_map = ClassMap({A: 3, B: 6})
    assert key in class_map


@mark.parametrize("key", [B2(), B(), A(), A2()])
def test_contains_with_isinstance_check(key):
    class_map = ClassMap({A: 3, B: 6})
    assert key in class_map


@mark.parametrize(("key", "value"), [(B2, 6), (B, 6), (A, 3), (A2, 3)])
def test_getitem_with_classes(key, value):
    class_map = ClassMap({A: 3, B: 6})
    assert class_map[key] == value


@mark.parametrize(("key", "value"), [(B2(), 6), (B(), 6), (A(), 3), (A2(), 3)])
def test_getitem_with_objects(key, value):
    class_map = ClassMap({A: 3, B: 6})
    assert class_map[key] == value


def test_getitem_throws_keyerror_for_unknown_key():
    class_map = ClassMap({A: 3, B: 6})
    with raises(KeyError):
        class_map["unknown"]


================================================
FILE: tests/test_column_aliases.py
================================================
import sqlalchemy as sa
from wtforms.validators import NumberRange

from tests import ModelFormTestCase
from wtforms_alchemy import ModelForm


class TestColumnAliases(ModelFormTestCase):
    def test_supports_column_aliases(self):
        class TestModel(self.base):
            __tablename__ = "TestTable"
            id = sa.Column(sa.Integer, primary_key=True)
            some_alias = sa.Column("some_name", sa.Integer)

        class TestForm(ModelForm):
            class Meta:
                model = TestModel

        form = TestForm()
        assert hasattr(form, "some_alias")
        assert not hasattr(form, "some_name")

    def test_labels(self):
        class TestModel(self.base):
            __tablename__ = "TestTable"
            id = sa.Column(sa.Integer, primary_key=True)
            some_alias = sa.Column(
                "some_name",
                sa.Integer,
            )

        class TestForm(ModelForm):
            class Meta:
                model = TestModel

        form = TestForm()
        assert form.some_alias.label.text == "some_alias"

    def test_unique_indexes(self):
        class TestModel(self.base):
            __tablename__ = "TestTable"
            id = sa.Column(sa.Integer, primary_key=True)
            some_alias = sa.Column("some_name", sa.Integer, unique=True)

        class TestForm(ModelForm):
            class Meta:
                model = TestModel

            @staticmethod
            def get_session():
                return None

        form = TestForm()
        assert hasattr(form, "some_alias")
        assert not hasattr(form, "some_name")

    def test_meta_field_args(self):
        class TestModel(self.base):
            __tablename__ = "TestTable"
            id = sa.Column(sa.Integer, primary_key=True)
            some_alias = sa.Column("some_name", sa.Integer)

        validators = [NumberRange(max=4)]

        class TestForm(ModelForm):
            class Meta:
                model = TestModel
                field_args = {"some_alias": {"validators": validators}}

        form = TestForm()
        assert hasattr(form, "some_alias")
        assert not hasattr(form, "some_name")
        assert form.some_alias.validators == validators

    def test_additional_validators(self):
        class TestModel(self.base):
            __tablename__ = "TestTable"
            id = sa.Column(sa.Integer, primary_key=True)
            some_alias = sa.Column("some_name", sa.Integer)

        number_range = NumberRange(max=4)
        validator_list = [number_range]

        class TestForm(ModelForm):
            class Meta:
                model = TestModel
                validators = {"some_alias": validator_list}

        form = TestForm()
        assert number_range in form.some_alias.validators


================================================
FILE: tests/test_configuration.py
================================================
import sqlalchemy as sa
from pytest import raises
from wtforms.fields import IntegerField
from wtforms.validators import Email

from tests import ModelFormTestCase, MultiDict
from wtforms_alchemy import (
    AttributeTypeException,
    InvalidAttributeException,
    ModelForm,
)


class UnknownType(sa.types.UserDefinedType):
    def get_col_spec(self):
        return "UNKNOWN()"


class TestModelFormConfiguration(ModelFormTestCase):
    def test_skip_unknown_types(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            query = None
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(UnknownType)
            some_property = "something"

        self.ModelTest = ModelTest

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                skip_unknown_types = True

        self.form_class = ModelTestForm
        assert not self.has_field("test_column")

    def test_supports_field_exclusion(self):
        self.init_model()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                exclude = ["test_column"]

        self.form_class = ModelTestForm
        assert not self.has_field("test_column")

    def test_throws_exception_for_unknown_excluded_column(self):
        self.init_model()

        with raises(InvalidAttributeException):

            class ModelTestForm(ModelForm):
                class Meta:
                    model = self.ModelTest
                    exclude = ["some_unknown_column"]

    def test_invalid_exclude_with_attr_errors_as_false(self):
        self.init_model()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                attr_errors = False
                exclude = ["some_unknown_column"]

    def test_throws_exception_for_unknown_included_column(self):
        self.init_model()

        with raises(InvalidAttributeException):

            class ModelTestForm(ModelForm):
                class Meta:
                    model = self.ModelTest
                    include = ["some_unknown_column"]

    def test_invalid_include_with_attr_errors_as_false(self):
        self.init_model()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                attr_errors = False
                include = ["some_unknown_column"]

    def test_throws_exception_for_non_column_fields(self):
        self.init_model()

        with raises(AttributeTypeException):

            class ModelTestForm(ModelForm):
                class Meta:
                    model = self.ModelTest
                    include = ["some_property"]

    def test_supports_field_inclusion(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                include = ["id"]

        self.form_class = ModelTestForm
        assert self.has_field("id")

    def test_supports_only_attribute(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            query = None
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.UnicodeText)
            test_column2 = sa.Column(sa.UnicodeText)

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                only = ["test_column"]

        form = ModelTestForm()
        assert len(form._fields) == 1

    def test_empty_only_attribute(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                only = []

        form = ModelTestForm()
        assert len(form._fields) == 0

    def test_supports_field_overriding(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest

            test_column = IntegerField()

        self.form_class = ModelTestForm
        self.assert_type("test_column", IntegerField)

    def test_supports_assigning_all_fields_as_optional(self):
        self.init(nullable=False)

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                all_fields_optional = True

        self.form_class = ModelTestForm
        self.assert_not_required("test_column")
        self.assert_optional("test_column")

    def test_supports_custom_datetime_format(self):
        self.init(sa.DateTime, nullable=False)

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                datetime_format = "%Y-%m-%dT%H:%M:%S"

        form = ModelTestForm()
        assert form.test_column.format == ["%Y-%m-%dT%H:%M:%S"]

    def test_supports_additional_validators(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                validators = {"test_column": Email()}

        self.form_class = ModelTestForm
        self.assert_has_validator("test_column", Email)

    def test_inherits_config_params_from_parent_meta(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                only = ["test_column"]

        class AnotherModelTestForm(ModelTestForm):
            class Meta:
                pass

        assert AnotherModelTestForm.Meta.only == ["test_column"]

    def test_child_classes_override_parents_config_params(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                only = ["test_column"]

        class AnotherModelTestForm(ModelTestForm):
            class Meta:
                only = []

        assert AnotherModelTestForm.Meta.only == []

    def test_strip_strings_fields(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                only = ["test_column"]
                strip_string_fields = True

        form = ModelTestForm(MultiDict(test_column=" something "))
        assert form.test_column.data == "something"

    def test_strip_strings_fields_with_empty_values(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                only = ["test_column"]
                strip_string_fields = True

        ModelTestForm()

    def test_class_meta_regression(self):
        self.init()

        class SomeForm(ModelForm):
            class Meta:
                model = self.ModelTest
                foo = 9

        class OtherForm(SomeForm):
            class Meta:
                pass

        assert issubclass(OtherForm.Meta, SomeForm.Meta)
        form = OtherForm()

        # Create a side effect on the base meta.
        assert form.Meta.foo == 9
        SomeForm.Meta.foo = 12
        assert form.Meta.foo == 12


================================================
FILE: tests/test_country_field.py
================================================
import sqlalchemy_utils
from babel import Locale
from wtforms import Form

from tests import MultiDict
from wtforms_alchemy import CountryField

sqlalchemy_utils.i18n.get_locale = lambda: Locale("en")


class TestCountryField:
    field_class = CountryField

    def init_form(self, **kwargs):
        class TestForm(Form):
            test_field = self.field_class(**kwargs)

        self.form_class = TestForm
        return self.form_class

    def setup_method(self, method):
        self.valid_countries = ["US", "SA", "FI"]
        self.invalid_countries = [
            "unknown",
        ]

    def test_valid_countries(self):
        form_class = self.init_form()
        for country in self.valid_countries:
            form = form_class(MultiDict(test_field=country))
            form.validate()
            assert len(form.errors) == 0

    def test_invalid_countries(self):
        form_class = self.init_form()
        for country in self.invalid_countries:
            form = form_class(MultiDict(test_field=country))
            form.validate()
            assert len(form.errors["test_field"]) == 2


================================================
FILE: tests/test_custom_fields.py
================================================
from wtforms import Form
from wtforms_components import SelectField

from tests import MultiDict
from wtforms_alchemy import null_or_unicode


class TestSelectField:
    def test_understands_none_values(self):
        class MyForm(Form):
            choice_field = SelectField(
                choices=[("", "-- Choose --"), ("choice 1", "Something")],
                coerce=null_or_unicode,
            )

        form = MyForm(MultiDict({"choice_field": ""}))
        form.validate()
        assert form.errors == {}


================================================
FILE: tests/test_deep_form_relations.py
================================================
import sqlalchemy as sa
from wtforms.fields import FormField

from tests import FormRelationsTestCase, MultiDict
from wtforms_alchemy import ModelFieldList, ModelForm, ModelFormField


class TestDeepFormRelationsOneToManyToOne(FormRelationsTestCase):
    def create_models(self):
        class Event(self.base):
            __tablename__ = "event"
            id = sa.Column(sa.Integer, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=False)

        class Address(self.base):
            __tablename__ = "address"
            id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
            street = sa.Column(sa.Unicode(255), nullable=True)

        class Location(self.base):
            __tablename__ = "location"
            id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=True)

            address_id = sa.Column(sa.Integer, sa.ForeignKey(Address.id))
            address = sa.orm.relationship(Address)

            event_id = sa.Column(sa.Integer, sa.ForeignKey(Event.id))
            event = sa.orm.relationship(Event, backref="locations")

        self.Event = Event
        self.Location = Location
        self.Address = Address

    def create_forms(self):
        class AddressForm(ModelForm):
            class Meta:
                model = self.Address

        class LocationForm(ModelForm):
            class Meta:
                model = self.Location

            address = ModelFormField(AddressForm)

        class EventForm(ModelForm):
            class Meta:
                model = self.Event

            locations = ModelFieldList(FormField(LocationForm))

        self.LocationForm = LocationForm
        self.EventForm = EventForm
        self.AddressForm = AddressForm

    def save(self):
        data = {
            "name": "Some event",
            "locations-0-name": "Some location",
            "locations-0-address-street": "Some address",
        }
        event = self.Event()
        self.session.add(event)
        form = self.EventForm(MultiDict(data))
        form.validate()
        form.populate_obj(event)
        self.session.commit()

    def test_assigment_and_deletion(self):
        self.save()
        event = self.session.query(self.Event).first()
        assert event.locations[0].name == "Some location"
        assert event.locations[0].address.street == "Some address"
        data = {"name": "Some event"}
        form = self.EventForm(MultiDict(data))
        form.validate()
        form.populate_obj(event)
        self.session.commit()
        event = self.session.query(self.Event).first()
        assert event.locations == []


class TestDeepFormRelationsOneToOneToMany(FormRelationsTestCase):
    def create_models(self):
        class Location(self.base):
            __tablename__ = "location"
            id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=True)

        class Address(self.base):
            __tablename__ = "address"
            id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
            street = sa.Column(sa.Unicode(255), nullable=True)

            location_id = sa.Column(sa.Integer, sa.ForeignKey(Location.id))
            location = sa.orm.relationship(Location, backref="addresses")

        class Event(self.base):
            __tablename__ = "event"
            id = sa.Column(sa.Integer, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=False)
            location_id = sa.Column(sa.Integer, sa.ForeignKey(Location.id))
            location = sa.orm.relationship(Location)

        self.Event = Event
        self.Location = Location
        self.Address = Address

    def create_forms(self):
        class AddressForm(ModelForm):
            class Meta:
                model = self.Address

        class LocationForm(ModelForm):
            class Meta:
                model = self.Location

            addresses = ModelFieldList(FormField(AddressForm))

        class EventForm(ModelForm):
            class Meta:
                model = self.Event

            location = ModelFormField(LocationForm)

        self.LocationForm = LocationForm
        self.EventForm = EventForm
        self.AddressForm = AddressForm

    def save(self):
        data = {
            "name": "Some event",
            "location-name": "Some location",
            "location-addresses-0-street": "Some address",
        }
        event = self.Event()
        self.session.add(event)
        form = self.EventForm(MultiDict(data))
        form.validate()
        form.populate_obj(event)
        self.session.commit()

    def test_assigment_and_deletion(self):
        self.save()
        event = self.session.query(self.Event).first()
        assert event.location.name == "Some location"
        assert event.location.addresses[0].street == "Some address"
        data = {"name": "Some event"}
        form = self.EventForm(MultiDict(data))
        form.validate()
        form.populate_obj(event)
        self.session.commit()
        event = self.session.query(self.Event).first()
        assert event.location.addresses == []


================================================
FILE: tests/test_descriptions.py
================================================
from tests import ModelFormTestCase
from wtforms_alchemy import ModelForm


class TestFieldParameters(ModelFormTestCase):
    def test_assigns_description_from_column_info(self):
        self.init(info={"description": "Description"})
        self.assert_description("test_column", "Description")

    def test_assigns_descriptions_from_form_configuration(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest

                field_args = {"test_column": {"description": "TESTING"}}

        self.form_class = ModelTestForm
        self.assert_description("test_column", "TESTING")


================================================
FILE: tests/test_field_exclusion.py
================================================
from datetime import datetime

import sqlalchemy as sa
from sqlalchemy_utils import TSVectorType

from tests import ModelFormTestCase


class TestFieldExclusion(ModelFormTestCase):
    def test_does_not_include_datetime_columns_with_default(self):
        self.init(sa.DateTime, default=datetime.now())
        assert not self.has_field("test_column")

    def test_excludes_surrogate_primary_keys_by_default(self):
        self.init()
        assert not self.has_field("id")

    def test_excludes_column_properties(self):
        self.init()
        self.ModelTest.calculated_value = sa.orm.column_property(
            sa.func.lower(self.ModelTest.test_column)
        )
        self.init_form()
        self.form_class()


class TestTSVectorType(ModelFormTestCase):
    def test_does_not_include_tsvector_typed_columns_with_default(self):
        self.init(TSVectorType)
        assert not self.has_field("test_column")


================================================
FILE: tests/test_field_order.py
================================================
import sqlalchemy as sa

from tests import ModelFormTestCase


class TestFieldOrder(ModelFormTestCase):
    def setup_method(self, method):
        ModelFormTestCase.setup_method(self, method)

        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=True)
            full_description = sa.Column(sa.UnicodeText)
            description = sa.Column(sa.UnicodeText)
            start_time = sa.Column(sa.DateTime)
            end_time = sa.Column(sa.DateTime)
            category = sa.Column(sa.Unicode(255))
            entry_fee = sa.Column(sa.Numeric)
            type = sa.Column(sa.Unicode(255))

        self.ModelTest = ModelTest
        self.init_form()

    def test_field_definition_order(self):
        field_names = [field.name for field in self.form_class()]
        assert field_names == sa.inspect(self.ModelTest).attrs.keys()[1:]


================================================
FILE: tests/test_field_parameters.py
================================================
from datetime import date, time

import sqlalchemy as sa
from sqlalchemy_utils import IntRangeType
from wtforms import widgets
from wtforms.fields import StringField
from wtforms.validators import NumberRange
from wtforms_components import DateRange, TimeRange

from tests import ModelFormTestCase
from wtforms_alchemy import ModelForm


class TestFieldParameters(ModelFormTestCase):
    def test_accepts_custom_widgets(self):
        self.init(info={"widget": widgets.HiddenInput()})
        form = self.form_class()
        assert isinstance(form.test_column.widget, widgets.HiddenInput)

    def test_accepts_custom_filters(self):
        def test_filter(a):
            return a

        self.init(info={"filters": [test_filter]})
        form = self.form_class()
        assert test_filter in form.test_column.filters

    def test_assigns_description_from_column_info(self):
        self.init(info={"description": "Description"})
        self.assert_description("test_column", "Description")

    def test_does_not_add_default_value_if_default_is_callable(self):
        self.init(default=lambda: "test")
        self.assert_default("test_column", None)

    def test_assigns_scalar_defaults(self):
        self.init(default="test")
        self.assert_default("test_column", "test")

    def test_min_and_max_info_attributes_with_integer_field(self):
        self.init(type_=sa.Integer, info={"min": 1, "max": 100})
        validator = self.get_validator("test_column", NumberRange)
        assert validator.min == 1
        assert validator.max == 100

    def test_min_and_max_info_attributes_with_numeric_field(self):
        self.init(type_=sa.Numeric, info={"min": 1, "max": 100})
        validator = self.get_validator("test_column", NumberRange)
        assert validator.min == 1
        assert validator.max == 100

    def test_min_and_max_info_attributes_with_float_field(self):
        self.init(type_=sa.Float, info={"min": 1, "max": 100})
        validator = self.get_validator("test_column", NumberRange)
        assert validator.min == 1
        assert validator.max == 100

    def test_min_and_max_info_attributes_with_int_range_field(self):
        self.init(type_=IntRangeType, info={"min": 1, "max": 100})
        validator = self.get_validator("test_column", NumberRange)
        assert validator.min == 1
        assert validator.max == 100

    def test_min_and_max_info_attributes_generate_time_range_validator(self):
        self.init(type_=sa.types.Time, info={"min": time(12, 30), "max": time(14, 30)})
        validator = self.get_validator("test_column", TimeRange)
        assert validator.min == time(12, 30)
        assert validator.max == time(14, 30)

    def test_min_and_max_info_attributes_generate_date_range_validator(self):
        self.init(
            type_=sa.Date, info={"min": date(1990, 1, 1), "max": date(2000, 1, 1)}
        )
        validator = self.get_validator("test_column", DateRange)
        assert validator.min == date(1990, 1, 1)
        assert validator.max == date(2000, 1, 1)

    def test_uses_custom_field_class(self):
        class InputTest(widgets.Input):
            input_type = "color"
            validation_attrs = []

        class FieldTest(StringField):
            widget = InputTest()

        class ModelTest(self.base):
            __tablename__ = "model_test"
            query = None
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(
                sa.UnicodeText, info={"form_field_class": FieldTest}
            )

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest

        form = ModelTestForm()
        assert 'type="color"' in str(form.test_column)

    def test_accepts_none_as_custom_field_class(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            query = None
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.UnicodeText, info={"form_field_class": None})

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest

        assert ModelTestForm()


================================================
FILE: tests/test_field_trimming.py
================================================
from tests import ModelFormTestCase, MultiDict
from wtforms_alchemy import ModelForm


class TestStringFieldTrimming(ModelFormTestCase):
    def test_strip_string_fields_set_for_string_field(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                strip_string_fields = True

        f = ModelTestForm(MultiDict([("test_column", "strip this   ")]))
        assert f.test_column.data == "strip this"

    def test_does_not_trim_fields_when_trim_param_is_false(self):
        self.init(info={"trim": False})

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                strip_string_fields = True

        f = ModelTestForm(MultiDict([("test_column", "strip this   ")]))
        assert f.test_column.data == "strip this   "


================================================
FILE: tests/test_form_meta.py
================================================
import sqlalchemy as sa

from tests import ModelFormTestCase


class TestModelFormMetaWithInheritance(ModelFormTestCase):
    def test_skip_unknown_types(self, model_form_all):
        self.init(type_=sa.Integer)

        class ModelTestForm(model_form_all):
            class Meta:
                skip_unknown_types = True

        class ModelTestForm2(ModelTestForm):
            class Meta:
                model = self.ModelTest

        self.form_class = ModelTestForm2
        assert self.form_class.Meta.skip_unknown_types is True

    def test_inheritance_attributes(self, model_form_custom):
        self.init(type_=sa.Integer)

        class ModelTestForm(model_form_custom):
            class Meta:
                model = self.ModelTest

        assert ModelTestForm.test_attr == "SomeVal"


class TestUnboundFieldsInitialization(ModelFormTestCase):
    def test_skip_unknown_types(self, model_form_all):
        self.init(type_=sa.Integer)

        class ModelTestForm(model_form_all):
            class Meta:
                model = self.ModelTest
                skip_unknown_types = True

        assert ModelTestForm.test_column


================================================
FILE: tests/test_hybrid_properties.py
================================================
import sqlalchemy as sa
from pytest import raises
from sqlalchemy.ext.hybrid import hybrid_property

from tests import ModelFormTestCase
from wtforms_alchemy import ModelForm
from wtforms_alchemy.exc import AttributeTypeException


class TestHybridProperties(ModelFormTestCase):
    def test_hybrid_property_returning_column_property(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            _test_column = sa.Column("test_column", sa.Boolean, nullable=False)

            @hybrid_property
            def test_column_hybrid(self):
                return self._test_column

            @test_column_hybrid.setter
            def test_column_hybrid(self, value):
                self._test_column = value

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                not_null_str_validator = None
                not_null_validator = None
                include = ("test_column_hybrid",)
                exclude = ("_test_column",)

        form = ModelTestForm()
        assert form.test_column_hybrid

    def test_hybrid_property_returning_expression(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            _test_column = sa.Column("test_column", sa.Boolean, nullable=False)

            @hybrid_property
            def test_column_hybrid(self):
                return self._test_column + self._test_column

            @test_column_hybrid.setter
            def test_column_hybrid(self, value):
                self._test_column = value

        with raises(AttributeTypeException):

            class ModelTestForm(ModelForm):
                class Meta:
                    model = ModelTest
                    not_null_str_validator = None
                    not_null_validator = None
                    include = ("test_column_hybrid",)
                    exclude = ("_test_column",)


================================================
FILE: tests/test_i18n_extension.py
================================================
import sqlalchemy as sa
from packaging.version import Version
from pytest import raises, skip
from sqlalchemy_i18n import make_translatable, Translatable, translation_base

from tests import ModelFormTestCase, MultiDict
from wtforms_alchemy import ModelForm

sqlalchemy_version = sa.__version__
if Version(sqlalchemy_version) >= Version("2.0"):
    skip("sqlalchemy_i18n does not support SQLAlchemy 2.0", allow_module_level=True)

make_translatable()


class TestInternationalizationExtension(ModelFormTestCase):
    def init(self):
        class ModelTest(self.base, Translatable):
            __tablename__ = "model_test"
            __translatable__ = {"locales": ["fi", "en"]}

            id = sa.Column(sa.Integer, primary_key=True)
            some_property = "something"

            locale = "en"

        class ModelTranslation(translation_base(ModelTest)):
            __tablename__ = "model_translation"

            name = sa.Column(sa.Unicode(255))
            content = sa.Column(sa.Unicode(255))

        self.ModelTest = ModelTest
        sa.orm.configure_mappers()
        Session = sa.orm.sessionmaker(bind=self.engine)
        self.session = Session()

        self.init_form()

    def test_supports_translated_columns(self):
        self.init()
        form = self.form_class()
        assert form.name
        assert form.content

    def test_supports_field_exclusion(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest
                exclude = ["name"]

        with raises(AttributeError):
            ModelTestForm().name

    def test_model_population(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest

        form = ModelTestForm(
            MultiDict([("name", "something"), ("content", "something")])
        )
        obj = self.ModelTest()
        form.populate_obj(obj)
        assert obj.name == "something"
        assert obj.content == "something"


================================================
FILE: tests/test_inheritance.py
================================================
from wtforms import Form
from wtforms_test import FormTestCase

from wtforms_alchemy import model_form_factory, ModelForm


class TestInheritance(FormTestCase):
    class Base(Form):
        @classmethod
        def get_session(self):
            return "TestSession"

    def test_default_base(self):
        assert ModelForm.get_session is None

    def test_custom_base_without_session(self):
        cls = model_form_factory(Form)
        assert cls.get_session is None

    def test_custom_base_with_session(self):
        cls = model_form_factory(self.Base)
        assert cls.get_session() == "TestSession"

    def test_inherit_with_new_session(self):
        cls = model_form_factory(self.Base)

        class Sub(cls):
            @classmethod
            def get_session(self):
                return "SubTestSession"

        assert Sub.get_session() == "SubTestSession"

    def test_inherit_without_new_session(self):
        cls = model_form_factory(self.Base)

        class Sub(cls):
            pass

        assert Sub.get_session() == "TestSession"


================================================
FILE: tests/test_labels.py
================================================
from tests import ModelFormTestCase
from wtforms_alchemy import ModelForm


class TestFieldLabels(ModelFormTestCase):
    def test_assigns_labels_from_column_info(self):
        self.init(info={"label": "Test Column"})
        self.assert_label("test_column", "Test Column")

    def test_assigns_labels_from_form_configuration(self):
        self.init()

        class ModelTestForm(ModelForm):
            class Meta:
                model = self.ModelTest

                field_args = {"test_column": {"label": "TESTING"}}

        self.form_class = ModelTestForm
        self.assert_label("test_column", "TESTING")


================================================
FILE: tests/test_model_field_list.py
================================================
import sqlalchemy as sa
from wtforms.fields import FormField
from wtforms_components import PassiveHiddenField

from tests import FormRelationsTestCase, MultiDict
from wtforms_alchemy import ModelFieldList, ModelForm


class ModelFieldListTestCase(FormRelationsTestCase):
    def create_models(self):
        class Event(self.base):
            __tablename__ = "event"
            id = sa.Column(sa.Integer, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=False)

        class Location(self.base):
            __tablename__ = "location"
            id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=True)

            event_id = sa.Column(sa.Integer, sa.ForeignKey(Event.id))
            event = sa.orm.relationship(Event, backref="locations")

        self.Event = Event
        self.Location = Location

    def save(self, event=None, data=None):
        if data is None:
            data = {
                "name": "Some event",
                "locations-0-name": "Some location",
                "locations-0-description": "Some description",
            }
        if not event:
            event = self.Event()
            self.session.add(event)
            form = self.EventForm(MultiDict(data))
        else:
            form = self.EventForm(MultiDict(data), obj=event)

        form.validate()
        form.populate_obj(event)
        self.session.commit()
        return event


class TestReplaceStrategy(ModelFieldListTestCase):
    def create_forms(self):
        class LocationForm(ModelForm):
            class Meta:
                model = self.Location

        class EventForm(ModelForm):
            class Meta:
                model = self.Event

            locations = ModelFieldList(FormField(LocationForm))

        self.LocationForm = LocationForm
        self.EventForm = EventForm

    def test_assigment_and_deletion(self):
        self.save()
        event = self.session.query(self.Event).first()
        assert event.locations[0].name == "Some location"
        data = {"name": "Some event"}
        form = self.EventForm(MultiDict(data))
        form.validate()
        form.populate_obj(event)
        self.session.commit()
        event = self.session.query(self.Event).first()
        assert event.locations == []


class TestUpdateStrategy(ModelFieldListTestCase):
    def create_models(self):
        class Event(self.base):
            __tablename__ = "event"
            id = sa.Column(sa.Integer, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=False)

        class Location(self.base):
            __tablename__ = "location"
            TYPES = ("", "football field", "restaurant")

            id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=True)
            description = sa.Column(sa.Unicode(255), default="")
            type = sa.Column(
                sa.Unicode(255), info={"choices": zip(TYPES, TYPES)}, default=""
            )

            event_id = sa.Column(sa.Integer, sa.ForeignKey(Event.id))
            event = sa.orm.relationship(Event, backref="locations")

            def __repr__(self):
                return f"Location(id={self.id!r}, name={self.name!r})"

        self.Event = Event
        self.Location = Location

    def create_forms(self):
        class LocationForm(ModelForm):
            class Meta:
                model = self.Location
                only = ["name", "description", "type"]

            id = PassiveHiddenField()

        class EventForm(ModelForm):
            class Meta:
                model = self.Event

            locations = ModelFieldList(
                FormField(LocationForm), population_strategy="update"
            )

        self.LocationForm = LocationForm
        self.EventForm = EventForm

    def test_single_entry_update(self):
        event = self.save()
        location_id = event.locations[0].id
        data = {
            "name": "Some event",
            "locations-0-id": location_id,
            "locations-0-name": "Some other location",
        }
        self.save(event, data)

        assert len(event.locations) == 1
        assert event.locations[0].id == location_id
        assert event.locations[0].name == "Some other location"

    def test_creates_new_objects_for_entries_with_unknown_identifiers(self):
        event = self.save()
        location_id = event.locations[0].id
        data = {
            "name": "Some event",
            "locations-0-id": 12,
            "locations-0-name": "Some other location",
        }
        self.save(event, data)
        assert event.locations
        assert event.locations[0].id != location_id

    def test_replace_entry(self):
        data = {
            "name": "Some event",
            "locations-0-name": "Some location",
            "locations-0-description": "Some description",
            "locations-0-type": "restaurant",
        }
        event = self.save(data=data)
        location_id = event.locations[0].id
        self.session.commit()
        data = {
            "name": "Some event",
            "locations-0-name": "Some other location",
        }
        self.save(event, data)
        location = event.locations[0]
        assert location.id != location_id
        assert location.name == "Some other location"
        assert location.description == ""
        assert location.type == ""
        assert len(event.locations) == 1

    def test_replace_and_update(self):
        data = {
            "name": "Some event",
            "locations-0-name": "Location 1",
            "locations-0-description": "Location 1 description",
            "locations-1-name": "Location 2",
            "locations-1-description": "Location 2 description",
        }
        event = self.save(data=data)
        self.session.commit()
        data = {
            "name": "Some event",
            "locations-0-id": event.locations[1].id,
            "locations-0-name": "Location 2 updated",
            "locations-0-description": "Location 2 description updated",
            "locations-1-name": "Location 3",
        }
        self.save(event, data)
        self.session.commit()
        location = event.locations[0]
        location2 = event.locations[1]
        assert location.name == "Location 2 updated"
        assert location.description == "Location 2 description updated"
        assert len(event.locations) == 2
        assert location2.name == "Location 3"
        assert location2.description == ""

    def test_multiple_entries(self):
        event = self.save()
        location_id = event.locations[0].id
        data = {
            "name": "Some event",
            "locations-0-name": "Some location",
            "locations-1-id": str(location_id),  # test coercing works
            "locations-1-name": "Some other location",
            "locations-2-name": "Third location",
            "locations-3-id": 123,
            "locations-3-name": "Fourth location",
        }
        self.save(event, data)
        assert len(event.locations) == 4
        assert event.locations[0].id == location_id
        assert event.locations[0].name == "Some other location"
        assert event.locations[1].name == "Some location"
        assert event.locations[2].name == "Third location"
        assert event.locations[3].name == "Fourth location"

    def test_delete_all_field_list_entries(self):
        event = self.save()
        data = {"name": "Some event"}
        self.save(event, data)
        assert not event.locations

    def test_update_and_remove(self):
        location = self.Location(name="Location #2")
        event = self.Event(
            name="Some event", locations=[self.Location(name="Location #1"), location]
        )
        self.session.add(event)
        self.session.commit()
        data = {
            "locations-0-id": location.id,
            "locations-0-name": "Location",
        }
        self.save(event, data)
        self.session.refresh(event)
        assert len(event.locations) == 1
        assert event.locations[0] == location


================================================
FILE: tests/test_model_form_factory.py
================================================
import wtforms
from pytest import raises
from wtforms import Form

from tests import ModelFormTestCase
from wtforms_alchemy import (
    FormGenerator,
    model_form_factory,
    UnknownConfigurationOption,
)


class TestModelFormFactory(ModelFormTestCase):
    def test_supports_parameter_overriding(self):
        self.init()

        class MyFormGenerator(FormGenerator):
            pass

        defaults = {
            "all_fields_optional": True,
            "only_indexed_fields": True,
            "include_primary_keys": True,
            "include_foreign_keys": True,
            "strip_string_fields": True,
            "include_datetimes_with_default": True,
            "form_generator": True,
            "date_format": "%d-%m-%Y",
            "datetime_format": "%Y-%m-%dT%H:%M:%S",
        }
        ModelForm = model_form_factory(Form, **defaults)
        for key, value in defaults.items():
            assert getattr(ModelForm.Meta, key) == value

    def test_throws_exception_for_unknown_configuration_option(self):
        self.init()

        class SomeForm(Form):
            pass

        defaults = {"unknown": "something"}
        with raises(UnknownConfigurationOption):
            model_form_factory(SomeForm, **defaults)

    def test_supports_custom_base_class_with_model_form_factory(self):
        self.init()

        class SomeForm(Form):
            pass

        class TestCustomBase(model_form_factory(SomeForm)):
            class Meta:
                model = self.ModelTest

        assert isinstance(TestCustomBase(), SomeForm)

    def test_url_validator(self):
        form = model_form_factory(url_validator=None)
        assert form.Meta.url_validator is None

    def test_email_validator(self):
        form = model_form_factory(email_validator=None)
        assert form.Meta.email_validator is None

    def test_length_validator(self):
        form = model_form_factory(length_validator=None)
        assert form.Meta.length_validator is None

    def test_number_range_validator(self):
        form = model_form_factory(number_range_validator=None)
        assert form.Meta.number_range_validator is None

    def test_date_range_validator(self):
        form = model_form_factory(date_range_validator=None)
        assert form.Meta.date_range_validator is None

    def test_time_range_validator(self):
        form = model_form_factory(time_range_validator=None)
        assert form.Meta.time_range_validator is None

    def test_optional_validator(self):
        form = model_form_factory(optional_validator=None)
        assert form.Meta.optional_validator is None

    def test_unique_validator(self):
        form = model_form_factory(unique_validator=None)
        assert form.Meta.unique_validator is None

    def test_class_meta_wtforms2(self):
        self.init()

        class SomeForm(Form):
            class Meta:
                locales = ["fr"]
                foo = 9

        class OtherForm(SomeForm):
            class Meta:
                pass

        class TestCustomBase(model_form_factory(SomeForm)):
            class Meta:
                model = self.ModelTest

        form = TestCustomBase()
        other_form = OtherForm()
        assert isinstance(form.meta, wtforms.meta.DefaultMeta)
        assert form.meta.locales == ["fr"]
        assert hasattr(form.meta, "model")
        assert hasattr(form.meta, "csrf")

        assert form.meta.foo == 9
        # Create a side effect on the base meta.
        SomeForm.Meta.foo = 12
        assert other_form.meta.foo == 12
        assert form.meta.foo == 12


================================================
FILE: tests/test_model_form_field.py
================================================
import sqlalchemy as sa
from pytest import raises

from tests import FormRelationsTestCase, MultiDict
from wtforms_alchemy import ModelForm, ModelFormField


class TestOneToOneModelFormRelations(FormRelationsTestCase):
    def create_models(self):
        class Location(self.base):
            __tablename__ = "location"
            id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=True)

        class Event(self.base):
            __tablename__ = "event"
            id = sa.Column(sa.Integer, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=False)
            location_id = sa.Column(sa.Integer, sa.ForeignKey(Location.id))
            location = sa.orm.relationship(Location)

        self.Event = Event
        self.Location = Location

    def create_forms(self):
        class LocationForm(ModelForm):
            class Meta:
                model = self.Location

        class EventForm(ModelForm):
            class Meta:
                model = self.Event

            location = ModelFormField(LocationForm)

        self.LocationForm = LocationForm
        self.EventForm = EventForm

    def save(self, event=None, data={}):
        if not data:
            data = {
                "name": "Some event",
                "location-name": "Some location",
            }
        if not event:
            event = self.Event()
        self.session.add(event)
        form = self.EventForm(MultiDict(data))
        form.validate()
        form.populate_obj(event)
        self.session.commit()
        return event

    def test_assigment_and_deletion(self):
        self.save()
        event = self.session.query(self.Event).first()
        assert event.location.name == "Some location"
        data = {"name": "Some event"}
        form = self.EventForm(MultiDict(data))
        form.validate()
        form.populate_obj(event)
        self.session.commit()
        event = self.session.query(self.Event).first()
        assert not event.location.name

    def test_only_populates_related_if_they_are_obj_attributes(self):
        class EventForm(ModelForm):
            class Meta:
                model = self.Event

            unknown_field = ModelFormField(self.LocationForm)

        self.EventForm = EventForm

        with raises(TypeError):
            self.save(
                data={
                    "name": "Some event",
                    "unknown_field-name": "Some location",
                }
            )

    def test_updating_related_object(self):
        event = self.save()
        location_id = event.location.id
        self.save(event, {"name": "some name", "location-name": "Some other location"})
        assert event.name == "some name"
        assert event.location.id == location_id


================================================
FILE: tests/test_phone_number.py
================================================
import sqlalchemy as sa
from pytest import mark
from sqlalchemy import create_engine
from sqlalchemy.orm import declarative_base, sessionmaker
from sqlalchemy.orm.session import close_all_sessions
from sqlalchemy_utils.types import phone_number

from tests import MultiDict
from wtforms_alchemy import ModelForm


class TestCase:
    def setup_method(self, method):
        self.engine = create_engine("sqlite:///:memory:")
        self.Base = declarative_base()

        self.create_models()
        self.Base.metadata.create_all(self.engine)

        Session = sessionmaker(bind=self.engine)
        self.session = Session()

    def teardown_method(self, method):
        close_all_sessions()
        self.Base.metadata.drop_all(self.engine)
        self.engine.dispose()

    def create_models(self):
        class User(self.Base):
            __tablename__ = "user"
            id = sa.Column(sa.Integer, autoincrement=True, primary_key=True)
            name = sa.Column(sa.Unicode(255))
            phone_number = sa.Column(phone_number.PhoneNumberType(country_code="FI"))

        self.User = User


@mark.xfail("phone_number.phonenumbers is None")
class TestPhoneNumbers(TestCase):
    """
    Simple tests to ensure that sqlalchemy_utils.PhoneNumber,
    wtforms_alchemy.PhoneNumberType and sqlalchemy_utils.PhoneNumberField work
    nicely together.
    """

    def setup_method(self, method):
        super().setup_method(method)

        class UserForm(ModelForm):
            class Meta:
                model = self.User

        self.UserForm = UserForm

        super().setup_method(method)
        self.phone_number = phone_number.PhoneNumber("040 1234567", "FI")
        self.user = self.User()
        self.user.name = "Someone"
        self.user.phone_number = self.phone_number
        self.session.add(self.user)
        self.session.commit()

    def test_query_returns_phone_number_object(self):
        queried_user = self.session.query(self.User).first()
        assert queried_user.phone_number == self.phone_number

    def test_phone_number_is_stored_as_string(self):
        result = self.session.execute(
            sa.text("SELECT phone_number FROM user WHERE id=:param"),
            {"param": self.user.id},
        )
        assert result.first()[0] == "+358401234567"

    def test_phone_number_in_form(self):
        form = self.UserForm(
            MultiDict(name="Matti Meikalainen", phone_number="+358401231233")
        )
        form.validate()
        assert len(form.errors) == 0
        assert form.data["phone_number"] == (phone_number.PhoneNumber("+358401231233"))

    def test_empty_phone_number_in_form(self):
        form = self.UserForm(MultiDict(name="Matti Meikalainen", phone_number=""))
        form.validate()
        assert len(form.errors) == 0
        assert form.data["phone_number"] is None


================================================
FILE: tests/test_phone_number_field.py
================================================
import pytest
from wtforms import Form

from tests import MultiDict
from wtforms_alchemy import DataRequired, PhoneNumberField


class TestPhoneNumberField:
    def setup_method(self, method):
        self.valid_phone_numbers = [
            "040 1234567",
            "+358 401234567",
            "09 2501234",
            "+358 92501234",
            "0800 939393",
            "09 4243 0456",
            "0600 900 500",
        ]
        self.invalid_phone_numbers = ["abc", "+040 1234567", "0111234567", "358"]

    def init_form(self, **kwargs):
        class TestForm(Form):
            phone_number = PhoneNumberField(**kwargs)

        return TestForm

    def test_valid_phone_numbers(self):
        form_class = self.init_form(region="FI")
        for phone_number in self.valid_phone_numbers:
            form = form_class(MultiDict(phone_number=phone_number))
            form.validate()
            assert len(form.errors) == 0

    def test_invalid_phone_numbers(self):
        form_class = self.init_form(region="FI")
        for phone_number in self.invalid_phone_numbers:
            form = form_class(MultiDict(phone_number=phone_number))
            form.validate()
            assert len(form.errors["phone_number"]) == 1

    def test_render_empty_phone_number_value(self):
        form_class = self.init_form(region="FI")
        form = form_class(MultiDict(phone_number=""))
        assert 'value=""' in form.phone_number()

    def test_empty_phone_number_value_passed_as_none(self):
        form_class = self.init_form(region="FI")
        form = form_class(MultiDict(phone_number=""))
        form.validate()
        assert len(form.errors) == 0
        assert form.data["phone_number"] is None

    def test_default_display_format(self):
        form_class = self.init_form(region="FI")
        form = form_class(MultiDict(phone_number="+358401234567"))
        assert 'value="040 1234567"' in form.phone_number()

    def test_international_display_format(self):
        form_class = self.init_form(region="FI", display_format="international")
        form = form_class(MultiDict(phone_number="0401234567"))
        assert 'value="+358 40 1234567"' in form.phone_number()

    def test_e164_display_format(self):
        form_class = self.init_form(region="FI", display_format="e164")
        form = form_class(MultiDict(phone_number="0401234567"))
        assert 'value="+358401234567"' in form.phone_number()

    def test_field_rendering_when_invalid_phone_number(self):
        form_class = self.init_form()
        form = form_class(MultiDict(phone_number="invalid"))
        form.validate()
        assert 'value="invalid"' in form.phone_number()

    @pytest.mark.parametrize(
        "number,error_msg,check_value",
        (
            ("", "This field is required.", lambda v, orig: v is None),
            ("1", "Not a valid phone number value", lambda v, orig: v is not None),
            ("123", "Not a valid phone number value", lambda v, orig: v is not None),
            ("+46123456789", None, lambda v, orig: v.e164 == orig),
        ),
    )
    def test_required_phone_number_form(self, number, error_msg, check_value):
        class PhoneNumberForm(Form):
            phone = PhoneNumberField("Phone number", validators=[DataRequired()])

        form = PhoneNumberForm(MultiDict(phone=number))
        form.validate()
        if error_msg:
            assert len(form.errors) == 1
            assert form.errors["phone"][0] == error_msg
        else:
            assert len(form.errors) == 0
        assert check_value(form.phone.data, number) is True


================================================
FILE: tests/test_query_select_field.py
================================================
import sqlalchemy as sa
from sqlalchemy.orm import declarative_base
from sqlalchemy.orm.session import close_all_sessions
from wtforms import Form

from wtforms_alchemy import (
    GroupedQuerySelectField,
    GroupedQuerySelectMultipleField,
    ModelForm,
    QuerySelectField,
    QuerySelectMultipleField,
)


class DummyPostData(dict):
    def getlist(self, key):
        v = self[key]
        if not isinstance(v, (list, tuple)):
            v = [v]
        return v


class LazySelect:
    def __call__(self, field, **kwargs):
        return list(
            (val, str(label), selected)
            for val, label, selected, _ in field.iter_choices()
        )


class Base:
    def __init__(self, **kwargs):
        for k, v in kwargs.items():
            setattr(self, k, v)


class TestBase:
    def create_models(self):
        class Test(self.base):
            __tablename__ = "test"
            id = sa.Column(sa.Integer, primary_key=True, nullable=False)
            name = sa.Column(sa.String, nullable=False)

        self.Test = Test

        class PKTest(self.base):
            __tablename__ = "pk_test"
            foobar = sa.Column(sa.String, primary_key=True, nullable=False)
            baz = sa.Column(sa.String, nullable=False)

            def __str__(self):
                return self.baz

        self.PKTest = PKTest

    def _fill(self, sess):
        for i, n in [(1, "apple"), (2, "banana")]:
            s = self.Test(id=i, name=n)
            p = self.PKTest(foobar=f"hello{i}", baz=n)
            sess.add(s)
            sess.add(p)
        sess.flush()
        sess.commit()


class TestQuerySelectField(TestBase):
    def setup_method(self):
        self.engine = sa.create_engine("sqlite:///:memory:", echo=False)

        self.base = declarative_base()
        self.create_models()

        self.base.metadata.create_all(self.engine)

        Session = sa.orm.session.sessionmaker(bind=self.engine)
        self.session = Session()

    def teardown_method(self):
        close_all_sessions()
        self.base.metadata.drop_all(self.engine)
        self.engine.dispose()

    def test_without_factory(self):
        self._fill(self.session)

        class F(Form):
            a = QuerySelectField(
                get_label="name", widget=LazySelect(), get_pk=lambda x: x.id
            )

        form = F(DummyPostData(a=["1"]))
        form.a.query = self.session.query(self.Test)
        assert form.a.data is not None
        assert form.a.data.id, 1
        assert form.a(), [("1", "apple", True), ("2", "banana", False)]
        assert form.validate()

        form = F(a=self.session.query(self.Test).filter_by(name="banana").first())
        form.a.query = self.session.query(self.Test).filter(self.Test.name != "banana")
        assert not form.validate()
        assert form.a.errors, ["Not a valid choice"]

        # Test query with no results
        form = F()
        form.a.query = (
            self.session.query(self.Test)
            .filter(self.Test.id == 1, self.Test.id != 1)
            .all()
        )
        assert form.a() == []

    def test_with_query_factory(self):
        self._fill(self.session)

        class F(Form):
            a = QuerySelectField(
                get_label=(lambda model: model.name),
                query_factory=lambda: self.session.query(self.Test),
                widget=LazySelect(),
            )
            b = QuerySelectField(
                allow_blank=True,
                query_factory=lambda: self.session.query(self.PKTest),
                widget=LazySelect(),
            )

        form = F()
        assert form.a.data is None
        assert form.a() == [("1", "apple", False), ("2", "banana", False)]
        assert form.b.data is None
        assert form.b() == [
            ("__None", "", True),
            ("hello1", "apple", False),
            ("hello2", "banana", False),
        ]
        assert not form.validate()

        # Test query with no results
        form = F()
        form.a.query = (
            self.session.query(self.Test)
            .filter(self.Test.id == 1, self.Test.id != 1)
            .all()
        )
        assert form.a() == []

        form = F(DummyPostData(a=["1"], b=["hello2"]))
        assert form.a.data.id == 1
        assert form.a() == [("1", "apple", True), ("2", "banana", False)]
        assert form.b.data.baz == "banana"
        assert form.b() == [
            ("__None", "", False),
            ("hello1", "apple", False),
            ("hello2", "banana", True),
        ]
        assert form.validate()

        # Make sure the query is cached
        self.session.add(self.Test(id=3, name="meh"))
        self.session.flush()
        self.session.commit()
        assert form.a() == [("1", "apple", True), ("2", "banana", False)]
        form.a._object_list = None
        assert form.a() == [
            ("1", "apple", True),
            ("2", "banana", False),
            ("3", "meh", False),
        ]

        # Test bad data
        form = F(DummyPostData(b=["__None"], a=["fail"]))
        assert not form.validate()
        assert form.a.errors == ["Not a valid choice"]
        assert form.b.errors == []
        assert form.b.data is None


class TestQuerySelectMultipleField(TestBase):
    def setup_method(self):
        self.engine = sa.create_engine("sqlite:///:memory:", echo=False)

        self.base = declarative_base()
        self.create_models()

        self.base.metadata.create_all(self.engine)

        Session = sa.orm.session.sessionmaker(bind=self.engine)
        self.session = Session()

        self._fill(self.session)

    def teardown_method(self):
        close_all_sessions()
        self.base.metadata.drop_all(self.engine)
        self.engine.dispose()

    class F(Form):
        a = QuerySelectMultipleField(get_label="name", widget=LazySelect())

    def test_unpopulated_default(self):
        form = self.F()
        assert [] == form.a.data

    def test_single_value_without_factory(self):
        form = self.F(DummyPostData(a=["1"]))
        form.a.query = self.session.query(self.Test)
        assert [1] == [v.id for v in form.a.data]
        assert form.a() == [("1", "apple", True), ("2", "banana", False)]
        assert form.validate()

    def test_multiple_values_without_query_factory(self):
        form = self.F(DummyPostData(a=["1", "2"]))
        form.a.query = self.session.query(self.Test)
        assert [1, 2] == [v.id for v in form.a.data]
        assert form.a() == [("1", "apple", True), ("2", "banana", True)]
        assert form.validate()

        form = self.F(DummyPostData(a=["1", "3"]))
        form.a.query = self.session.query(self.Test)
        assert [x.id for x in form.a.data], [1]
        assert not form.validate()

    def test_single_default_value(self):
        first_test = self.session.get(self.Test, 2)

        class F(Form):
            a = QuerySelectMultipleField(
                get_label="name",
                default=[first_test],
                widget=LazySelect(),
                query_factory=lambda: self.session.query(self.Test),
            )

        form = F()
        assert [v.id for v in form.a.data], [2]
        assert form.a(), [("1", "apple", False), ("2", "banana", True)]
        assert form.validate()

    def test_empty_query(self):
        # Test query with no results
        form = self.F()
        form.a.query = (
            self.session.query(self.Test)
            .filter(self.Test.id == 1, self.Test.id != 1)
            .all()
        )
        assert form.a() == []


class DatabaseTestCase:
    def setup_method(self, method):
        self.engine = sa.create_engine("sqlite:///:memory:")

        self.base = declarative_base()
        self.create_models()

        self.base.metadata.create_all(self.engine)

        Session = sa.orm.session.sessionmaker(bind=self.engine)
        self.session = Session()

    def teardown_method(self, method):
        close_all_sessions()
        self.base.metadata.drop_all(self.engine)
        self.engine.dispose()

    def create_models(self):
        class City(self.base):
            __tablename__ = "city"
            id = sa.Column(sa.Integer, primary_key=True)
            name = sa.Column(sa.String)
            country = sa.Column(sa.String)
            state_id = sa.Column(sa.Integer, sa.ForeignKey("state.id"))

        self.City = City

        class State(self.base):
            __tablename__ = "state"
            id = sa.Column(sa.Integer, primary_key=True)
            cities = sa.orm.relationship("City")

        self.State = State

    def create_cities(self):
        self.session.add_all(
            [
                self.City(name="Helsinki", country="Finland"),
                self.City(name="Vantaa", country="Finland"),
                self.City(name="New York", country="USA"),
                self.City(name="Washington", country="USA"),
                self.City(name="Stockholm", country="Sweden"),
            ]
        )


class TestGroupedQuerySelectField(DatabaseTestCase):
    def create_form(self, **kwargs):
        query = self.session.query(self.City).order_by("name", "country")

        class MyForm(Form):
            city = GroupedQuerySelectField(
                label=kwargs.get("label", "City"),
                query_factory=kwargs.get("query_factory", lambda: query),
                get_label=kwargs.get("get_label", lambda c: c.name),
                get_group=kwargs.get("get_group", lambda c: c.country),
                allow_blank=kwargs.get("allow_blank", False),
                blank_text=kwargs.get("blank_text", ""),
                blank_value=kwargs.get("blank_value", "__None"),
            )

        return MyForm

    def test_custom_none_value(self):
        self.create_cities()
        MyForm = self.create_form(
            allow_blank=True, blank_text="Choose city...", blank_value=""
        )
        form = MyForm(DummyPostData({"city": ""}))
        assert form.validate(), form.errors
        assert '<option selected value="">Choose city...</option>' in (str(form.city))

    def test_rendering(self):
        MyForm = self.create_form()
        self.create_cities()
        assert str(MyForm().city).replace("\n", "") == (
            '<select id="city" name="city">'
            '<optgroup label="Finland">'
            '<option value="1">Helsinki</option>'
            '<option value="2">Vantaa</option>'
            '</optgroup><optgroup label="Sweden">'
            '<option value="5">Stockholm</option>'
            "</optgroup>"
            '<optgroup label="USA">'
            '<option value="3">New York</option>'
            '<option value="4">Washington</option>'
            "</optgroup>"
            "</select>"
        )


class TestGroupedQuerySelectMultipleField(DatabaseTestCase):
    def create_form(self, **kwargs):
        query = self.session.query(self.City).order_by("name", "country")

        class MyForm(ModelForm):
            class Meta:
                model = self.State

            cities = GroupedQuerySelectMultipleField(
                label=kwargs.get("label", "City"),
                query_factory=kwargs.get("query_factory", lambda: query),
                get_label=kwargs.get("get_label", lambda c: c.name),
                get_group=kwargs.get("get_group", lambda c: c.country),
                blank_text=kwargs.get("blank_text", ""),
            )

        return MyForm

    def test_unpopulated_default(self):
        MyForm = self.create_form()
        self.create_cities()
        assert MyForm().cities.data == []

    def test_single_value_without_factory(self):
        obj = self.State()
        MyForm = self.create_form()
        self.create_cities()
        form = MyForm(DummyPostData(cities=["1"]), obj=obj)
        assert [1] == [v.id for v in form.cities.data]
        assert form.validate()
        form.populate_obj(obj)
        assert [city.id for city in obj.cities] == [1]

    def test_multiple_values_without_query_factory(self):
        obj = self.State()
        MyForm = self.create_form()
        self.create_cities()
        form = MyForm(DummyPostData(cities=["1", "2"]), obj=obj)
        form.cities.query = self.session.query(self.City)

        assert [1, 2] == [v.id for v in form.cities.data]
        assert form.validate()
        form.populate_obj(obj)
        assert [city.id for city in obj.cities] == [1, 2]

        form = MyForm(DummyPostData(cities=["1", "666"]))
        form.cities.query = self.session.query(self.City)
        assert not form.validate()
        assert [x.id for x in form.cities.data] == [1]
        assert not form.validate()
        form.populate_obj(obj)
        assert [city.id for city in obj.cities] == [1]

        form = MyForm(DummyPostData(cities=["666"]))
        form.cities.query = self.session.query(self.City)
        assert not form.validate()
        assert [x.id for x in form.cities.data] == []
        assert not form.validate()
        form.populate_obj(obj)
        assert [city.id for city in obj.cities] == []

    def test_rendering(self):
        MyForm = self.create_form()
        self.create_cities()
        assert str(MyForm().cities).replace("\n", "") == (
            '<select id="cities" multiple name="cities">'
            '<optgroup label="Finland">'
            '<option value="1">Helsinki</option>'
            '<option value="2">Vantaa</option>'
            '</optgroup><optgroup label="Sweden">'
            '<option value="5">Stockholm</option>'
            "</optgroup>"
            '<optgroup label="USA">'
            '<option value="3">New York</option>'
            '<option value="4">Washington</option>'
            "</optgroup>"
            "</select>"
        )


================================================
FILE: tests/test_select_field.py
================================================
from decimal import Decimal

import sqlalchemy as sa
from wtforms_components import SelectField

from tests import ModelFormTestCase


class MultiDict(dict):
    def getlist(self, key):
        return [self[key]]


class TestSelectFieldDefaultValue(ModelFormTestCase):
    def test_option_selected_by_field_default_value(self):
        choices = [("1", "1"), ("2", "2")]
        self.init(type_=sa.Integer, default="1", info={"choices": choices})
        form = self.form_class(MultiDict({"test_column": "2"}))
        assert '<option selected value="2">2</option>' in str(form.test_column)


class TestSelectFieldCoerce(ModelFormTestCase):
    def test_integer_coerces_values_to_integers(self):
        choices = [("1", "1"), ("2", "2")]
        self.init(type_=sa.Integer, info={"choices": choices})
        form = self.form_class(MultiDict({"test_column": "2"}))
        assert form.test_column.data == 2

    def test_nullable_integer_coerces_values_to_integers(self):
        choices = [("1", "1"), ("2", "2")]
        self.init(type_=sa.Integer, nullable=True, info={"choices": choices})
        form = self.form_class(MultiDict({"test_column": "2"}))
        assert form.test_column.data == 2

    def test_integer_coerces_empty_strings_to_nulls(self):
        choices = [("1", "1"), ("2", "2")]
        self.init(type_=sa.Integer, info={"choices": choices})
        form = self.form_class(MultiDict({"test_column": ""}))
        assert form.test_column.data is None

    def test_big_integer_coerces_values_to_integers(self):
        choices = [("1", "1"), ("2", "2")]
        self.init(type_=sa.BigInteger, info={"choices": choices})
        self.assert_type("test_column", SelectField)
        form = self.form_class(MultiDict({"test_column": "2"}))
        assert form.test_column.data == 2

    def test_small_integer_coerces_values_to_integers(self):
        choices = [("1", "1"), ("2", "2")]
        self.init(type_=sa.SmallInteger, info={"choices": choices})
        form = self.form_class(MultiDict({"test_column": "2"}))
        assert form.test_column.data == 2

    def test_numeric_coerces_values_to_decimals(self):
        choices = [("1.0", "1.0"), ("2.0", "2.0")]
        self.init(type_=sa.Numeric, info={"choices": choices})
        form = self.form_class(MultiDict({"test_column": "2.0"}))
        assert form.test_column.data == Decimal("2.0")

    def test_float_coerces_values_to_floats(self):
        choices = [("1.0", "1.0"), ("2.0", "2.0")]
        self.init(type_=sa.Float, info={"choices": choices})
        form = self.form_class(MultiDict({"test_column": "2.0"}))
        assert form.test_column.data == 2.0

    def test_unicode_coerces_values_to_unicode_strings(self):
        choices = [("1.0", "1.0"), ("2.0", "2.0")]
        self.init(type_=sa.Unicode(255), info={"choices": choices})
        form = self.form_class(MultiDict({"test_column": "2.0"}))
        assert form.test_column.data == "2.0"
        assert isinstance(form.test_column.data, str)

    def test_unicode_text_coerces_values_to_unicode_strings(self):
        choices = [("1.0", "1.0"), ("2.0", "2.0")]
        self.init(type_=sa.UnicodeText, info={"choices": choices})
        form = self.form_class(MultiDict({"test_column": "2.0"}))
        assert form.test_column.data == "2.0"
        assert isinstance(form.test_column.data, str)


================================================
FILE: tests/test_synonym.py
================================================
import sqlalchemy as sa
from sqlalchemy.ext.hybrid import hybrid_property

from tests import ModelFormTestCase
from wtforms_alchemy import ModelForm


class TestSynonym(ModelFormTestCase):
    def test_synonym_returning_column_property_with_include(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            _test_column = sa.Column("test_column", sa.Integer, nullable=False)

            @hybrid_property
            def test_column_hybrid(self):
                return self.test_column * 2

            @test_column_hybrid.setter
            def test_column_hybrid(self, value):
                self._test_column = value

            test_column = sa.orm.synonym("_test_column")

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                not_null_str_validator = None
                not_null_validator = None
                include = ("test_column",)
                exclude = ("_test_column",)

        form = ModelTestForm()
        assert form.test_column

    def test_synonym_returning_column_property_with_only(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            _test_column = sa.Column("test_column", sa.Integer, nullable=False)

            @hybrid_property
            def test_column_hybrid(self):
                return self.test_column * 2

            @test_column_hybrid.setter
            def test_column_hybrid(self, value):
                self._test_column = value

            test_column = sa.orm.synonym("_test_column")

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                not_null_str_validator = None
                not_null_validator = None
                only = ("test_column",)

        form = ModelTestForm()
        assert form.test_column


================================================
FILE: tests/test_types.py
================================================
from enum import Enum

import sqlalchemy as sa
from pytest import mark, raises
from sqlalchemy_utils import (
    ChoiceType,
    ColorType,
    CountryType,
    EmailType,
    IntRangeType,
    PasswordType,
    PhoneNumberType,
    URLType,
    UUIDType,
)
from sqlalchemy_utils.types import arrow, phone_number, WeekDaysType  # noqa
from wtforms.fields import (
    BooleanField,
    FloatField,
    PasswordField,
    TextAreaField,
)
from wtforms.validators import Length, URL
from wtforms_components import Email
from wtforms_components.fields import (
    ColorField,
    DateField,
    DateTimeField,
    DecimalField,
    EmailField,
    IntegerField,
    IntIntervalField,
    SelectField,
    StringField,
    TimeField,
)

from tests import ModelFormTestCase
from wtforms_alchemy import (
    CountryField,
    ModelForm,
    null_or_unicode,
    PhoneNumberField,
    UnknownTypeException,
    WeekDaysField,
)
from wtforms_alchemy.utils import ClassMap

try:
    import passlib  # noqa
except ImportError:
    passlib = None


class UnknownType(sa.types.UserDefinedType):
    def get_col_spec(self):
        return "UNKNOWN()"


class CustomUnicodeTextType(sa.types.TypeDecorator):
    impl = sa.types.UnicodeText


class CustomUnicodeType(sa.types.TypeDecorator):
    impl = sa.types.Unicode


class CustomNumericType(sa.types.TypeDecorator):
    impl = sa.types.Numeric


class TestModelColumnToFormFieldTypeConversion(ModelFormTestCase):
    def test_raises_exception_for_unknown_type(self):
        with raises(UnknownTypeException):
            self.init(type_=UnknownType)
            self.form_class()

    def test_raises_exception_for_array_type(self):
        with raises(UnknownTypeException):
            self.init(type_=sa.ARRAY(sa.Integer))
            self.form_class()

    def test_unicode_converts_to_text_field(self):
        self.init()
        self.assert_type("test_column", StringField)

    def test_custom_unicode_converts_to_text_field(self):
        self.init(type_=CustomUnicodeType)
        self.assert_type("test_column", StringField)

    def test_string_converts_to_text_field(self):
        self.init(type_=sa.String)
        self.assert_type("test_column", StringField)

    def test_integer_converts_to_integer_field(self):
        self.init(type_=sa.Integer)
        self.assert_type("test_column", IntegerField)

    def test_unicode_text_converts_to_text_area_field(self):
        self.init(type_=sa.UnicodeText)
        self.assert_type("test_column", TextAreaField)

    def test_custom_unicode_text_converts_to_text_area_field(self):
        self.init(type_=CustomUnicodeTextType)
        self.assert_type("test_column", TextAreaField)

    def test_boolean_converts_to_boolean_field(self):
        self.init(type_=sa.Boolean)
        self.assert_type("test_column", BooleanField)

    def test_datetime_converts_to_datetime_field(self):
        self.init(type_=sa.DateTime)
        self.assert_type("test_column", DateTimeField)

    def test_date_converts_to_date_field(self):
        self.init(type_=sa.Date)
        self.assert_type("test_column", DateField)

    def test_float_converts_to_float_field(self):
        self.init(type_=sa.Float)
        self.assert_type("test_column", FloatField)

    def test_numeric_converts_to_decimal_field(self):
        self.init(type_=sa.Numeric)
        self.assert_type("test_column", DecimalField)

    def test_numeric_scale_converts_to_decimal_field_scale(self):
        self.init(type_=sa.Numeric(scale=4))
        form = self.form_class()
        assert form.test_column.places == 4

    def test_custom_numeric_converts_to_decimal_field(self):
        self.init(type_=CustomNumericType)
        self.assert_type("test_column", DecimalField)

    def test_enum_field_converts_to_select_field(self):
        choices = ["1", "2"]
        self.init(type_=sa.Enum(*choices))
        self.assert_type("test_column", SelectField)
        form = self.form_class()
        assert form.test_column.choices == [(s, s) for s in choices]

    def test_nullable_enum_uses_null_or_unicode_coerce_func_by_default(self):
        choices = ["1", "2"]
        self.init(type_=sa.Enum(*choices), nullable=True)
        field = self._get_field("test_column")
        assert field.coerce == null_or_unicode

    def test_custom_choices_override_enum_choices(self):
        choices = ["1", "2"]
        custom_choices = [("2", "2"), ("3", "3")]
        self.init(type_=sa.Enum(*choices), info={"choices": custom_choices})
        form = self.form_class()
        assert form.test_column.choices == custom_choices

    def test_column_with_choices_converts_to_select_field(self):
        choices = [("1", "1"), ("2", "2")]
        self.init(type_=sa.Integer, info={"choices": choices})
        self.assert_type("test_column", SelectField)
        form = self.form_class()
        assert form.test_column.choices == choices

    def test_assigns_email_validator_for_email_type(self):
        self.init(type_=EmailType)
        self.assert_has_validator("test_column", Email)

    def test_assigns_url_validator_for_url_type(self):
        self.init(type_=URLType)
        self.assert_has_validator("test_column", URL)

    def test_time_converts_to_time_field(self):
        self.init(type_=sa.types.Time)
        self.assert_type("test_column", TimeField)

    def test_varchar_converts_to_text_field(self):
        self.init(type_=sa.types.VARCHAR)
        self.assert_type("test_column", StringField)

    def test_text_converts_to_textarea_field(self):
        self.init(type_=sa.types.TEXT)
        self.assert_type("test_column", TextAreaField)

    def test_char_converts_to_text_field(self):
        self.init(type_=sa.types.CHAR)
        self.assert_type("test_column", StringField)

    def test_real_converts_to_float_field(self):
        self.init(type_=sa.types.REAL)
        self.assert_type("test_column", FloatField)

    def test_json_converts_to_textarea_field(self):
        self.init(type_=sa.types.JSON)
        self.assert_type("test_column", TextAreaField)

    @mark.xfail("phone_number.phonenumbers is None")
    def test_phone_number_converts_to_phone_number_field(self):
        self.init(type_=PhoneNumberType)
        self.assert_type("test_column", PhoneNumberField)

    @mark.xfail("phone_number.phonenumbers is None")
    def test_phone_number_country_code_passed_to_field(self):
        self.init(type_=PhoneNumberType(region="SE"))
        form = self.form_class()
        assert form.test_column.region == "SE"

    @mark.xfail("phone_number.phonenumbers is None")
    def test_phone_number_type_has_no_length_validation(self):
        self.init(type_=PhoneNumberType(country_code="FI"))
        field = self._get_field("test_column")
        for validator in field.validators:
            assert validator.__class__ != Length

    @mark.parametrize(("type", "field"), ((IntRangeType, IntIntervalField),))
    def test_range_type_conversion(self, type, field):
        self.init(type_=type)
        self.assert_type("test_column", field)

    @mark.xfail("passlib is None")
    def test_password_type_converts_to_password_field(self):
        self.init(type_=PasswordType)
        self.assert_type("test_column", PasswordField)

    @mark.xfail("arrow.arrow is None")
    def test_arrow_type_converts_to_datetime_field(self):
        self.init(type_=arrow.ArrowType)
        self.assert_type("test_column", DateTimeField)

    def test_url_type_converts_to_string_field(self):
        self.init(type_=URLType)
        self.assert_type("test_column", StringField)

    def test_uuid_type_converst_to_uuid_type(self):
        self.init(type_=UUIDType)
        self.assert_type("test_column", StringField)

    def test_color_type_converts_to_color_field(self):
        self.init(type_=ColorType)
        self.assert_type("test_column", ColorField)

    def test_email_type_converts_to_email_field(self):
        self.init(type_=EmailType)
        self.assert_type("test_column", EmailField)

    def test_country_type_converts_to_country_field(self):
        self.init(type_=CountryType)
        self.assert_type("test_column", CountryField)

    def test_choice_type_converts_to_select_field(self):
        choices = [("1", "choice 1"), ("2", "choice 2")]
        self.init(type_=ChoiceType(choices))
        self.assert_type("test_column", SelectField)
        assert list(self.form_class().test_column.choices) == choices

    def test_choice_type_uses_custom_coerce_func(self):
        choices = [("1", "choice 1"), ("2", "choice 2")]
        self.init(type_=ChoiceType(choices))
        self.assert_type("test_column", SelectField)
        model = self.ModelTest(test_column="2")
        form = self.form_class(obj=model)
        assert '<option selected value="2">' in str(form.test_column)

    def test_choice_type_with_enum(self):
        class Choice(Enum):
            choice1 = 1
            choice2 = 2

            def __str__(self):
                return self.name

        self.init(type_=ChoiceType(Choice))
        self.assert_type("test_column", SelectField)
        assert self.form_class().test_column.choices == [(1, "choice1"), (2, "choice2")]

    @mark.parametrize(["type_", "impl"], [(int, sa.Integer()), (str, sa.String())])
    def test_choice_type_with_enum_uses_custom_coerce_func(self, type_, impl):
        class Choice(Enum):
            choice1 = type_(1)
            choice2 = type_(2)

            def __str__(self):
                return self.name

        self.init(type_=ChoiceType(Choice, impl=impl))
        self.assert_type("test_column", SelectField)
        model = self.ModelTest(test_column=type_(2))
        form = self.form_class(obj=model)
        assert '<option selected value="2">' in str(form.test_column)


class TestWeekDaysTypeConversion(ModelFormTestCase):
    def test_weekdays_type_converts_to_weekdays_field(self):
        self.init(type_=WeekDaysType)
        self.assert_type("test_column", WeekDaysField)


class TestCustomTypeMap(ModelFormTestCase):
    def test_override_type_map_on_class_level(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.Unicode(255), nullable=False)

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                not_null_validator = None
                type_map = ClassMap({sa.Unicode: TextAreaField})

        form = ModelTestForm()
        assert isinstance(form.test_column, TextAreaField)

    def test_override_type_map_with_callable(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column_short = sa.Column(sa.Unicode(255), nullable=False)
            test_column_long = sa.Column(sa.Unicode(), nullable=False)

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                not_null_validator = None
                type_map = ClassMap(
                    {
                        sa.Unicode: lambda column: (
                            StringField if column.type.length else TextAreaField
                        )
                    }
                )

        form = ModelTestForm()
        assert isinstance(form.test_column_short, StringField)
        assert isinstance(form.test_column_long, TextAreaField)


================================================
FILE: tests/test_unique_validator.py
================================================
import sqlalchemy as sa
from pytest import mark, raises
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm.session import close_all_sessions
from wtforms import Form
from wtforms.fields import StringField

from tests import MultiDict
from wtforms_alchemy import ModelForm, QuerySelectField, Unique

base = declarative_base()


class Color(base):
    __tablename__ = "color"
    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.Unicode(255), unique=True)


class User(base):
    __tablename__ = "user"
    id = sa.Column(sa.Integer, primary_key=True)
    name = sa.Column(sa.Unicode(255), unique=True)
    email = sa.Column(sa.Unicode(255))
    favorite_color_id = sa.Column(sa.Integer, sa.ForeignKey(Color.id))
    favorite_color = relationship(Color)


class TestUniqueValidator:
    def create_models(self):
        # This is a hack so we can use our classes
        # without initializing self first
        self.base = base

    def setup_method(self, method):
        self.engine = sa.create_engine("sqlite:///:memory:")

        self.base = declarative_base()
        self.create_models()

        self.base.metadata.create_all(self.engine)

        Session = sa.orm.session.sessionmaker(bind=self.engine)
        self.session = Session()

    def teardown_method(self, method):
        close_all_sessions()
        self.base.metadata.drop_all(self.engine)
        self.engine.dispose()

    def _test_syntax(self, column, expected_dict):
        class MyForm(ModelForm):
            name = StringField()
            email = StringField()

        validator = Unique(column, get_session=lambda: self.session)
        form = MyForm()
        if not hasattr(form, "Meta"):
            form.Meta = lambda: None
        form.Meta.model = User
        result = validator._syntaxes_as_tuples(form, form.name, column)
        assert result == expected_dict

    def test_with_form_obj_unavailable(self):
        class MyForm(Form):
            name = StringField(
                validators=[Unique(User.name, get_session=lambda: self.session)]
            )

        form = MyForm()
        with raises(Exception) as e:
            form.validate()
        assert "Couldn't access Form._obj attribute" in str(e)

    @mark.parametrize(
        ["column", "expected_dict"],
        (
            (User.name, (("name", User.name),)),
            ("name", (("name", User.name),)),
            (("name", "email"), (("name", User.name), ("email", User.email))),
            ({"exampleName": User.name}, (("exampleName", User.name),)),
            ((User.name, User.email), (("name", User.name), ("email", User.email))),
            (
                (User.name, User.favorite_color),
                (("name", User.name), ("favorite_color", User.favorite_color)),
            ),
        ),
    )
    def test_columns_as_tuples(self, column, expected_dict):
        self._test_syntax(column, expected_dict)

    def test_columns_as_tuples_classical_mapping(self):
        users = sa.Table("users", sa.MetaData(None), sa.Column("name", sa.Unicode(255)))
        self._test_syntax(users.c.name, (("name", users.c.name),))

    @mark.parametrize(
        "column", (User.name, {"name": User.name}, (("name", User.name),))
    )
    def test_raises_exception_if_improperly_configured(self, column):
        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique(
                        column,
                    )
                ]
            )

        with raises(Exception):
            MyForm().validate()

    def test_raises_exception_string_if_improperly_configured(self):
        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique(
                        ("name", "email"),
                    )
                ]
            )

        with raises(Exception):
            MyForm().validate()

    def test_existing_name_collision(self):
        class MyForm(ModelForm):
            name = StringField(
                validators=[Unique(User.name, get_session=lambda: self.session)]
            )

        self.session.add(User(name="someone"))
        self.session.commit()

        form = MyForm(MultiDict({"name": "someone"}))
        form.validate()
        assert form.errors == {"name": ["Already exists."]}

    def test_existing_name_collision_multiple(self):
        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique([User.name, User.email], get_session=lambda: self.session)
                ]
            )
            email = StringField()

        self.session.add(User(name="someone", email="someone@example.com"))
        self.session.commit()

        form = MyForm(MultiDict({"name": "someone", "email": "someone@example.com"}))
        form.validate()
        assert form.errors == {"name": ["Already exists."]}

    def test_works_with_flask_sqlalchemy_syntax(self, monkeypatch):
        monkeypatch.setattr(User, "query", self.session.query(User), False)

        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique([User.name, User.email], get_session=lambda: self.session)
                ]
            )
            email = StringField()

        self.session.add(User(name="someone", email="someone@example.com"))
        self.session.commit()

        form = MyForm(MultiDict({"name": "someone", "email": "someone@example.com"}))
        form.validate()
        assert form.errors == {"name": ["Already exists."]}

    def test_existing_name_collision_classical_mapping(self):
        sa.Table(
            "user",
            sa.MetaData(None),
            sa.Column("name", sa.String(255)),
            sa.Column("email", sa.String(255)),
        )

        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique([User.name, User.email], get_session=lambda: self.session)
                ]
            )
            email = StringField()

        self.session.add(User(name="someone", email="someone@example.com"))
        self.session.commit()

        form = MyForm(MultiDict({"name": "someone", "email": "someone@example.com"}))
        form.validate()
        assert form.errors == {"name": ["Already exists."]}

    def test_relationship_multiple_collision(self):
        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique(
                        [User.name, User.favorite_color],
                        get_session=lambda: self.session,
                    )
                ]
            )
            email = StringField()
            favorite_color = QuerySelectField(
                query_factory=lambda: self.session.query(Color).all(), allow_blank=True
            )

        red_color = Color(name="red")
        blue_color = Color(name="blue")
        self.session.add(red_color)
        self.session.add(blue_color)
        self.session.add(
            User(
                name="someone",
                email="first.email@example.com",
                favorite_color=red_color,
            )
        )
        self.session.commit()

        form = MyForm(
            MultiDict(
                {
                    "name": "someone",
                    "email": "second.email@example.com",
                    "favorite_color": str(red_color.id),
                }
            )
        )
        form.validate()
        assert form.errors == {"name": ["Already exists."]}

    def test_relationship_multiple_no_collision(self):
        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique(
                        [User.name, User.favorite_color],
                        get_session=lambda: self.session,
                    )
                ]
            )
            email = StringField()
            favorite_color = QuerySelectField(
                query_factory=lambda: self.session.query(Color).all(), allow_blank=True
            )

        red_color = Color(name="red")
        blue_color = Color(name="blue")
        self.session.add(red_color)
        self.session.add(blue_color)
        self.session.add(
            User(
                name="someone",
                email="first.email@example.com",
                favorite_color=red_color,
            )
        )
        self.session.commit()

        form = MyForm(
            MultiDict(
                {
                    "name": "someone",
                    "email": "second.email@example.com",
                    "favorite_color": str(blue_color.id),
                }
            )
        )
        form.validate()
        assert form.errors == {}

    def test_without_obj_without_collision(self):
        class MyForm(ModelForm):
            name = StringField(
                validators=[Unique(User.name, get_session=lambda: self.session)]
            )

        self.session.add(User(name="someone else"))
        self.session.commit()

        form = MyForm(MultiDict({"name": "someone"}))
        assert form.validate()

    def test_without_obj_without_collision_multiple(self):
        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique([User.name, User.email], get_session=lambda: self.session)
                ]
            )
            email = StringField()

        self.session.add(User(name="someone", email="someone@example.com"))
        self.session.commit()

        form = MyForm(MultiDict({"name": "someone", "email": "else@example.com"}))
        assert form.validate()

    def test_existing_name_no_collision(self):
        class MyForm(ModelForm):
            name = StringField(
                validators=[Unique(User.name, get_session=lambda: self.session)]
            )

        obj = User(name="someone")
        self.session.add(obj)

        form = MyForm(MultiDict({"name": "someone"}), obj=obj)
        assert form.validate()

    def test_existing_name_no_collision_multiple(self):
        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique((User.name, User.email), get_session=lambda: self.session)
                ]
            )
            email = StringField()

        obj = User(name="someone", email="hello@world.com")
        self.session.add(obj)

        form = MyForm(
            MultiDict({"name": "someone", "email": "hello@world.com"}), obj=obj
        )
        assert form.validate()

    def test_supports_model_query_parameter(self):
        User.query = self.session.query(User)

        class MyForm(ModelForm):
            name = StringField(
                validators=[
                    Unique(
                        User.name,
                    )
                ]
            )

        form = MyForm(MultiDict({"name": "someone"}))
        assert form.validate()


================================================
FILE: tests/test_utils.py
================================================
import sqlalchemy as sa

from tests import FormRelationsTestCase
from wtforms_alchemy import utils


class TestUtils(FormRelationsTestCase):
    def create_models(self):
        class Band(self.base):
            __tablename__ = "band"
            id = sa.Column(sa.Integer, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=False)

        class Person(self.base):
            __tablename__ = "person"
            id = sa.Column(sa.Integer, primary_key=True)
            name = sa.Column(sa.Unicode(255), nullable=False)

        class BandMember(self.base):
            __tablename__ = "band_members"
            band_id = sa.Column(sa.Integer, sa.ForeignKey(Band.id), primary_key=True)
            band = sa.orm.relationship(Band, backref="members")
            person_id = sa.Column(
                sa.Integer, sa.ForeignKey(Person.id), primary_key=True
            )
            person = sa.orm.relationship(
                Person, backref=sa.orm.backref("band_role", uselist=False)
            )
            role = sa.Column(sa.Unicode(255), nullable=False)

            def __repr__(self):
                fmt = "<BandMember band_id={} person_id={} at {:x}>"
                return fmt.format(repr(self.band_id), repr(self.person_id), id(self))

        self.Band = Band
        self.Person = Person
        self.BandMember = BandMember

    def create_forms(self):
        pass

    def test_find_entity(self):
        band = self.Band(name="The Furious")
        self.session.add(band)
        singer = self.Person(name="Paul Insane")
        self.session.add(self.BandMember(band=band, person=singer, role="singer"))
        guitarist = self.Person(name="John Crazy")
        self.session.add(self.BandMember(band=band, person=guitarist, role="guitar"))
        self.session.commit()

        sing_data = dict(band_id=band.id, person_id=singer.id, role="singer")
        guitar_data = dict(band_id=band.id, person_id=guitarist.id, role="guitar")

        assert (
            utils.find_entity(band.members, self.BandMember, sing_data)
            is singer.band_role
        )
        assert (
            utils.find_entity(band.members, self.BandMember, guitar_data)
            is guitarist.band_role
        )


================================================
FILE: tests/test_validators.py
================================================
from datetime import datetime, time

import sqlalchemy as sa
from sqlalchemy_utils import EmailType
from wtforms.validators import (
    DataRequired,
    Email,
    InputRequired,
    Length,
    NumberRange,
    Optional,
)
from wtforms_components import DateRange, TimeRange

from tests import ModelFormTestCase
from wtforms_alchemy import ClassMap, ModelForm, Unique


class TestAutoAssignedValidators(ModelFormTestCase):
    def test_auto_assigns_length_validators(self):
        self.init()
        self.assert_max_length("test_column", 255)

    def test_assigns_validators_from_info_field(self):
        self.init(info={"validators": Email()})
        self.assert_has_validator("test_column", Email)

    def test_assigns_unique_validator_for_unique_fields(self):
        self.init(unique=True)
        self.assert_has_validator("test_column", Unique)

    def test_assigns_non_nullable_fields_as_required(self):
        self.init(nullable=False)
        self.assert_has_validator("test_column", DataRequired)
        self.assert_has_validator("test_column", InputRequired)

    def test_type_level_not_nullable_validators(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.Unicode(255), nullable=False)

        validator = DataRequired()

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                not_null_validator_type_map = ClassMap()
                not_null_validator = validator

        form = ModelTestForm()
        assert validator in form.test_column.validators

    def test_not_nullable_validator_with_type_decorator(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(EmailType, nullable=False)

        validator = DataRequired()

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                not_null_validator_type_map = ClassMap([(sa.String, validator)])
                not_null_validator = []

        form = ModelTestForm()
        assert validator in form.test_column.validators

    def test_not_null_validator_as_empty_list(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.Boolean, nullable=False)

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                not_null_validator_type_map = ClassMap()
                not_null_validator = []

        form = ModelTestForm()
        assert list(form.test_column.validators) == []

    def test_not_null_validator_as_none(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.Boolean, nullable=False)

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                not_null_validator_type_map = ClassMap()
                not_null_validator = None

        form = ModelTestForm()
        assert len(form.test_column.validators) == 1
        assert isinstance(form.test_column.validators[0], Optional)

    def test_not_nullable_booleans_are_required(self):
        self.init(sa.Boolean, nullable=False)
        self.assert_has_validator("test_column", InputRequired)

    def test_not_nullable_fields_with_defaults_are_not_required(self):
        self.init(nullable=False, default="default")
        self.assert_not_required("test_column")

    def test_assigns_nullable_integers_as_optional(self):
        self.init(sa.Integer, nullable=True)
        self.assert_optional("test_column")

    def test_override_email_validator(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(EmailType, nullable=True)

        def validator():
            return Email("Wrong email")

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                email_validator = validator

        form = ModelTestForm()
        assert form.test_column.validators[1].message == "Wrong email"

    def test_override_optional_validator(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(EmailType, nullable=True)

        class MyOptionalValidator:
            def __init__(self, *args, **kwargs):
                pass

            def __call__(self, form, field):
                pass

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                optional_validator = MyOptionalValidator

        form = ModelTestForm()
        assert isinstance(form.test_column.validators[0], MyOptionalValidator)

    def test_override_number_range_validator(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.Integer, info={"min": 3}, nullable=True)

        def number_range(min=-1, max=-1):
            return NumberRange(min=min, max=max, message="Wrong number range")

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                number_range_validator = number_range

        form = ModelTestForm()
        assert form.test_column.validators[1].message == "Wrong number range"

    def test_override_date_range_validator(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(
                sa.DateTime, info={"min": datetime(2000, 1, 1)}, nullable=True
            )

        def date_range(min=None, max=None):
            return DateRange(min=min, max=max, message="Wrong date range")

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                date_range_validator = date_range

        form = ModelTestForm()
        assert form.test_column.validators[1].message == "Wrong date range"

    def test_override_time_range_validator(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.Time, info={"min": time(14, 30)}, nullable=True)

        def time_range(min=None, max=None):
            return TimeRange(min=min, max=max, message="Wrong time")

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                time_range_validator = time_range

        form = ModelTestForm()
        assert form.test_column.validators[1].message == "Wrong time"

    def test_override_length_validator(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.Unicode(255), nullable=True)

        def length(min=-1, max=-1):
            return Length(min=min, max=max, message="Wrong length")

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                length_validator = length

        form = ModelTestForm()
        assert form.test_column.validators[1].message == "Wrong length"

    def test_override_optional_validator_as_none(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.Boolean, nullable=True)

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                optional_validator = None

        form = ModelTestForm()
        assert list(form.test_column.validators) == []

    def test_override_unique_validator(self):
        class ModelTest(self.base):
            __tablename__ = "model_test"
            id = sa.Column(sa.Integer, primary_key=True)
            test_column = sa.Column(sa.Unicode(255), unique=True, nullable=True)

        def unique(column, get_session):
            return Unique(column, get_session=get_session, message="Not unique")

        class ModelTestForm(ModelForm):
            class Meta:
                model = ModelTest
                unique_validator = unique

            @staticmethod
            def get_session():
                return None

        form = ModelTestForm()
        assert form.test_column.validators[2].message == "Not unique"


================================================
FILE: tests/test_weekdays_field.py
================================================
from wtforms import Form

from tests import MultiDict
from wtforms_alchemy import WeekDaysField


class TestWeekDaysField:
    def init_form(self, **kwargs):
        class TestForm(Form):
            test_field = WeekDaysField(**kwargs)

        return TestForm

    def test_valid_weekdays(self):
        form_class = self.init_form()
        form = form_class(MultiDict(test_field=0))
        form.validate()
        assert len(form.errors) == 0

    def test_invalid_weekdays(self):
        form_class = self.init_form()
        form = form_class(
            MultiDict(
                [
                    ("test_field", "8"),
                ]
            )
        )
        form.validate()
        assert len(form.errors["test_field"]) == 1


================================================
FILE: tests/test_widgets.py
================================================
from decimal import Decimal

import sqlalchemy as sa

from tests import ModelFormTestCase


class TestNumericFieldWidgets(ModelFormTestCase):
    def test_converts_numeric_scale_to_steps(self):
        self.init(
            type_=sa.Numeric(scale=2),
        )
        form = self.form_class()
        assert 'step="0.01"' in str(form.test_column)

    def test_supports_numeric_column_without_scale(self):
        self.init(
            type_=sa.Numeric(),
        )
        form = self.form_class()
        assert 'step="any"' in str(form.test_column)

    def test_supports_step_as_info_arg(self):
        self.init(
            type_=sa.Numeric(),
            info={"step": "0.1"},
        )
        form = self.form_class()
        assert 'step="0.1"' in str(form.test_column)

    def test_numeric_field_with_scale_and_choices(self):
        self.init(
            type_=sa.Numeric(scale=2),
            info={
                "choices": [(Decimal("1.1"), "choice1"), (Decimal("1.2"), "choice2")]
            },
        )
        form = self.form_class()
        assert 'step="0.1"' not in str(form.test_column)


class TestIntegerFieldWidgets(ModelFormTestCase):
    def test_supports_step_as_info_arg(self):
        self.init(
            type_=sa.Integer,
            info={"step": "3"},
        )
        form = self.form_class()
        assert 'step="3"' in str(form.test_column)


class TestFloatFieldWidgets(ModelFormTestCase):
    def test_supports_step_as_info_arg(self):
        self.init(
            type_=sa.Float,
            info={"step": "0.2"},
        )
        form = self.form_class()
        assert 'step="0.2"' in str(form.test_column)


================================================
FILE: tox.ini
================================================
[tox]
envlist = py{39,310,311,312,313}-sqlalchemy{1.4,2.0}-wtforms{3.1,3.2}

[testenv]
deps =
    .[test_all]
    pytest-cov
    sqlalchemy1.4: SQLAlchemy~=1.4
    sqlalchemy2.0: SQLAlchemy~=2.0
    wtforms3.1: WTForms~=3.1
    wtforms3.2: WTForms~=3.2
commands = pip install -e ".[test]"
           py.test
install_command = pip install {packages}
recreate = True


================================================
FILE: wtforms_alchemy/__init__.py
================================================
import sqlalchemy as sa
from wtforms import Form
from wtforms.form import FormMeta
from wtforms.validators import (
    DataRequired,
    InputRequired,
    Length,
    NumberRange,
    Optional,
    URL,
)
from wtforms_components import DateRange, Email, TimeRange

from .exc import (
    AttributeTypeException,
    InvalidAttributeException,
    UnknownConfigurationOption,
    UnknownTypeException,
)
from .fields import (  # noqa
    CountryField,
    GroupedQuerySelectField,
    GroupedQuerySelectMultipleField,
    ModelFieldList,
    ModelFormField,
    PhoneNumberField,
    QuerySelectField,
    QuerySelectMultipleField,
    WeekDaysField,
)
from .generator import FormGenerator
from .utils import (
    ClassMap,
    is_date_column,
    is_scalar,
    null_or_int,
    null_or_unicode,
)
from .validators import Unique  # noqa

__all__ = (
    AttributeTypeException,
    CountryField,
    DateRange,
    InvalidAttributeException,
    ModelFieldList,
    ModelFormField,
    PhoneNumberField,
    Unique,
    UnknownTypeException,
    is_date_column,
    is_scalar,
    null_or_int,
    null_or_unicode,
)


__version__ = "0.19.1"


def model_form_meta_factory(base=FormMeta):
    """
    Create a new class usable as a metaclass for the
    :func:`model_form_factory`. You only need to concern yourself with this if
    you desire to have a custom metclass. Otherwise, a default class is
    created and is used as a metaclass on :func:`model_form_factory`.

    :param base: The base class to use for the meta class. This is an optional
                 parameter that defaults to :class:`.FormMeta`. If you want to
                 provide your own, your class must derive from this class and
                 not directly from ``type``.

    :return: A new class suitable as a metaclass for the actual model form.
             Therefore, it should be passed as the ``meta`` argument to
             :func:`model_form_factory`.

    Example usage:

    .. code-block:: python

        from wtforms.form import FormMeta


        class MyModelFormMeta(FormMeta):
            # do some metaclass magic here
            pass

        ModelFormMeta = model_form_meta_factory(MyModelFormMeta)
        ModelForm = model_form_factory(meta=ModelFormMeta)
    """

    class ModelFormMeta(base):
        """
        Meta class that overrides WTForms base meta class. The primary purpose
        of this class is allowing ModelForms use special configuration params
        under the 'Meta' class namespace.

        ModelForm classes inherit parent's Meta class properties.
        """

        def __init__(cls, *args, **kwargs):
            bases = []
            for class_ in cls.__mro__:
                if "Meta" in class_.__dict__:
                    bases.append(getattr(class_, "Meta"))

            if object not in bases:
                bases.append(object)

            cls.Meta = type("Meta", tuple(bases), {})

            base.__init__(cls, *args, **kwargs)

            if hasattr(cls.Meta, "model") and cls.Meta.model:
                generator = cls.Meta.form_generator(cls)
                generator.create_form(cls)

    return ModelFormMeta


ModelFormMeta = model_form_meta_factory()


def model_form_factory(base=Form, meta=ModelFormMeta, **defaults):
    """
    Create a base class for all model forms to derive from.

    :param base: Class that should be used as a base for the returned class.
                 By default, this is WTForms's base class
                 :class:`wtforms.Form`.

    :param meta: A metaclass to use on this class. Normally, you do not need to
                 provide this value, but if you want, you should check out
                 :func:`model_form_meta_factory`.

    :return: A class to be used as the base class for all forms that should be
             connected to a SQLAlchemy model class.

    Additional arguments provided to the form override the default
    configuration as described in :ref:`custom_base`.
    """

    class ModelForm(base, metaclass=meta):
        """
        Standard base-class for all forms to be combined with a model. Use
        :func:`model_form_factory` in case you wish to change its behavior.

        ``get_session``: If you want to use the Unique validator, you should
        define this method. If you are using Flask-SQLAlchemy along with
        WTForms-Alchemy you don't need to set this. If you define this in the
        superclass, it will not be overriden.
        """

        if not hasattr(base, "get_session"):
            get_session = None

        class Meta:
            model = None

            default = None

            #: Whether or not to skip unknown types. If this is set to True,
            #: fields with types that are not present in FormGenerator type map
            #: will be silently excluded from the generated form.
            #:
            #: By default this is set to False, meaning unknown types throw
            #: exceptions when encountered.
            skip_unknown_types = defaults.pop("skip_unknown_types", False)

            #: Whether or not to assign all fields as optional, useful when
            #: creating update forms for patch requests
            all_fields_optional = defaults.pop("all_fields_optional", False)

            validators = defaults.pop("validators", {})

            #: A dict with keys as field names and values as field arguments.
            field_args 
Download .txt
gitextract_nsm2hc9e/

├── .github/
│   └── workflows/
│       ├── lint.yml
│       └── test.yaml
├── .gitignore
├── CHANGES.rst
├── LICENSE
├── MANIFEST.in
├── README.rst
├── docs/
│   ├── Makefile
│   ├── advanced.rst
│   ├── api.rst
│   ├── column_conversion.rst
│   ├── conf.py
│   ├── configuration.rst
│   ├── customization.rst
│   ├── index.rst
│   ├── introduction.rst
│   ├── license.rst
│   ├── make.bat
│   ├── relationships.rst
│   ├── types.rst
│   └── validators.rst
├── pyproject.toml
├── setup.py
├── tests/
│   ├── __init__.py
│   ├── conftest.py
│   ├── test_class_map.py
│   ├── test_column_aliases.py
│   ├── test_configuration.py
│   ├── test_country_field.py
│   ├── test_custom_fields.py
│   ├── test_deep_form_relations.py
│   ├── test_descriptions.py
│   ├── test_field_exclusion.py
│   ├── test_field_order.py
│   ├── test_field_parameters.py
│   ├── test_field_trimming.py
│   ├── test_form_meta.py
│   ├── test_hybrid_properties.py
│   ├── test_i18n_extension.py
│   ├── test_inheritance.py
│   ├── test_labels.py
│   ├── test_model_field_list.py
│   ├── test_model_form_factory.py
│   ├── test_model_form_field.py
│   ├── test_phone_number.py
│   ├── test_phone_number_field.py
│   ├── test_query_select_field.py
│   ├── test_select_field.py
│   ├── test_synonym.py
│   ├── test_types.py
│   ├── test_unique_validator.py
│   ├── test_utils.py
│   ├── test_validators.py
│   ├── test_weekdays_field.py
│   └── test_widgets.py
├── tox.ini
└── wtforms_alchemy/
    ├── __init__.py
    ├── exc.py
    ├── fields.py
    ├── generator.py
    ├── utils.py
    └── validators.py
Download .txt
SYMBOL INDEX (478 symbols across 39 files)

FILE: setup.py
  function get_version (line 16) | def get_version():

FILE: tests/__init__.py
  class MultiDict (line 13) | class MultiDict(dict):
    method getlist (line 14) | def getlist(self, key):
  class ModelFormTestCase (line 18) | class ModelFormTestCase(FormTestCase):
    method setup_method (line 21) | def setup_method(self, method):
    method teardown_method (line 25) | def teardown_method(self, method):
    method init_model (line 30) | def init_model(self, type_=sa.Unicode(255), **kwargs):
    method init (line 42) | def init(self, type_=sa.Unicode(255), **kwargs):
    method init_form (line 46) | def init_form(self):
  class FormRelationsTestCase (line 54) | class FormRelationsTestCase:
    method setup_method (line 57) | def setup_method(self, method):
    method teardown_method (line 69) | def teardown_method(self, method):

FILE: tests/conftest.py
  class _MetaWithInit (line 11) | class _MetaWithInit(FormMeta):
    method __init__ (line 12) | def __init__(cls, *args, **kwargs):
  class _MetaWithoutInit (line 20) | class _MetaWithoutInit(FormMeta):
  function model_form_all (line 28) | def model_form_all(request):
  function model_form_custom (line 36) | def model_form_custom(request):

FILE: tests/test_class_map.py
  class A (line 6) | class A:
  class B (line 10) | class B:
  class A2 (line 14) | class A2(A):
  class A3 (line 18) | class A3(A2):
  class A4 (line 22) | class A4(A3):
  class A5 (line 26) | class A5(A4):
  class B2 (line 30) | class B2(B):
  class C (line 34) | class C:
  function test_contains_with_subclass_check (line 39) | def test_contains_with_subclass_check(key):
  function test_contains_with_isinstance_check (line 45) | def test_contains_with_isinstance_check(key):
  function test_getitem_with_classes (line 51) | def test_getitem_with_classes(key, value):
  function test_getitem_with_objects (line 57) | def test_getitem_with_objects(key, value):
  function test_getitem_throws_keyerror_for_unknown_key (line 62) | def test_getitem_throws_keyerror_for_unknown_key():

FILE: tests/test_column_aliases.py
  class TestColumnAliases (line 8) | class TestColumnAliases(ModelFormTestCase):
    method test_supports_column_aliases (line 9) | def test_supports_column_aliases(self):
    method test_labels (line 23) | def test_labels(self):
    method test_unique_indexes (line 39) | def test_unique_indexes(self):
    method test_meta_field_args (line 57) | def test_meta_field_args(self):
    method test_additional_validators (line 75) | def test_additional_validators(self):

FILE: tests/test_configuration.py
  class UnknownType (line 14) | class UnknownType(sa.types.UserDefinedType):
    method get_col_spec (line 15) | def get_col_spec(self):
  class TestModelFormConfiguration (line 19) | class TestModelFormConfiguration(ModelFormTestCase):
    method test_skip_unknown_types (line 20) | def test_skip_unknown_types(self):
    method test_supports_field_exclusion (line 38) | def test_supports_field_exclusion(self):
    method test_throws_exception_for_unknown_excluded_column (line 49) | def test_throws_exception_for_unknown_excluded_column(self):
    method test_invalid_exclude_with_attr_errors_as_false (line 59) | def test_invalid_exclude_with_attr_errors_as_false(self):
    method test_throws_exception_for_unknown_included_column (line 68) | def test_throws_exception_for_unknown_included_column(self):
    method test_invalid_include_with_attr_errors_as_false (line 78) | def test_invalid_include_with_attr_errors_as_false(self):
    method test_throws_exception_for_non_column_fields (line 87) | def test_throws_exception_for_non_column_fields(self):
    method test_supports_field_inclusion (line 97) | def test_supports_field_inclusion(self):
    method test_supports_only_attribute (line 108) | def test_supports_only_attribute(self):
    method test_empty_only_attribute (line 124) | def test_empty_only_attribute(self):
    method test_supports_field_overriding (line 135) | def test_supports_field_overriding(self):
    method test_supports_assigning_all_fields_as_optional (line 147) | def test_supports_assigning_all_fields_as_optional(self):
    method test_supports_custom_datetime_format (line 159) | def test_supports_custom_datetime_format(self):
    method test_supports_additional_validators (line 170) | def test_supports_additional_validators(self):
    method test_inherits_config_params_from_parent_meta (line 181) | def test_inherits_config_params_from_parent_meta(self):
    method test_child_classes_override_parents_config_params (line 195) | def test_child_classes_override_parents_config_params(self):
    method test_strip_strings_fields (line 209) | def test_strip_strings_fields(self):
    method test_strip_strings_fields_with_empty_values (line 221) | def test_strip_strings_fields_with_empty_values(self):
    method test_class_meta_regression (line 232) | def test_class_meta_regression(self):

FILE: tests/test_country_field.py
  class TestCountryField (line 11) | class TestCountryField:
    method init_form (line 14) | def init_form(self, **kwargs):
    method setup_method (line 21) | def setup_method(self, method):
    method test_valid_countries (line 27) | def test_valid_countries(self):
    method test_invalid_countries (line 34) | def test_invalid_countries(self):

FILE: tests/test_custom_fields.py
  class TestSelectField (line 8) | class TestSelectField:
    method test_understands_none_values (line 9) | def test_understands_none_values(self):

FILE: tests/test_deep_form_relations.py
  class TestDeepFormRelationsOneToManyToOne (line 8) | class TestDeepFormRelationsOneToManyToOne(FormRelationsTestCase):
    method create_models (line 9) | def create_models(self):
    method create_forms (line 35) | def create_forms(self):
    method save (line 56) | def save(self):
    method test_assigment_and_deletion (line 69) | def test_assigment_and_deletion(self):
  class TestDeepFormRelationsOneToOneToMany (line 83) | class TestDeepFormRelationsOneToOneToMany(FormRelationsTestCase):
    method create_models (line 84) | def create_models(self):
    method create_forms (line 109) | def create_forms(self):
    method save (line 130) | def save(self):
    method test_assigment_and_deletion (line 143) | def test_assigment_and_deletion(self):

FILE: tests/test_descriptions.py
  class TestFieldParameters (line 5) | class TestFieldParameters(ModelFormTestCase):
    method test_assigns_description_from_column_info (line 6) | def test_assigns_description_from_column_info(self):
    method test_assigns_descriptions_from_form_configuration (line 10) | def test_assigns_descriptions_from_form_configuration(self):

FILE: tests/test_field_exclusion.py
  class TestFieldExclusion (line 9) | class TestFieldExclusion(ModelFormTestCase):
    method test_does_not_include_datetime_columns_with_default (line 10) | def test_does_not_include_datetime_columns_with_default(self):
    method test_excludes_surrogate_primary_keys_by_default (line 14) | def test_excludes_surrogate_primary_keys_by_default(self):
    method test_excludes_column_properties (line 18) | def test_excludes_column_properties(self):
  class TestTSVectorType (line 27) | class TestTSVectorType(ModelFormTestCase):
    method test_does_not_include_tsvector_typed_columns_with_default (line 28) | def test_does_not_include_tsvector_typed_columns_with_default(self):

FILE: tests/test_field_order.py
  class TestFieldOrder (line 6) | class TestFieldOrder(ModelFormTestCase):
    method setup_method (line 7) | def setup_method(self, method):
    method test_field_definition_order (line 25) | def test_field_definition_order(self):

FILE: tests/test_field_parameters.py
  class TestFieldParameters (line 14) | class TestFieldParameters(ModelFormTestCase):
    method test_accepts_custom_widgets (line 15) | def test_accepts_custom_widgets(self):
    method test_accepts_custom_filters (line 20) | def test_accepts_custom_filters(self):
    method test_assigns_description_from_column_info (line 28) | def test_assigns_description_from_column_info(self):
    method test_does_not_add_default_value_if_default_is_callable (line 32) | def test_does_not_add_default_value_if_default_is_callable(self):
    method test_assigns_scalar_defaults (line 36) | def test_assigns_scalar_defaults(self):
    method test_min_and_max_info_attributes_with_integer_field (line 40) | def test_min_and_max_info_attributes_with_integer_field(self):
    method test_min_and_max_info_attributes_with_numeric_field (line 46) | def test_min_and_max_info_attributes_with_numeric_field(self):
    method test_min_and_max_info_attributes_with_float_field (line 52) | def test_min_and_max_info_attributes_with_float_field(self):
    method test_min_and_max_info_attributes_with_int_range_field (line 58) | def test_min_and_max_info_attributes_with_int_range_field(self):
    method test_min_and_max_info_attributes_generate_time_range_validator (line 64) | def test_min_and_max_info_attributes_generate_time_range_validator(self):
    method test_min_and_max_info_attributes_generate_date_range_validator (line 70) | def test_min_and_max_info_attributes_generate_date_range_validator(self):
    method test_uses_custom_field_class (line 78) | def test_uses_custom_field_class(self):
    method test_accepts_none_as_custom_field_class (line 101) | def test_accepts_none_as_custom_field_class(self):

FILE: tests/test_field_trimming.py
  class TestStringFieldTrimming (line 5) | class TestStringFieldTrimming(ModelFormTestCase):
    method test_strip_string_fields_set_for_string_field (line 6) | def test_strip_string_fields_set_for_string_field(self):
    method test_does_not_trim_fields_when_trim_param_is_false (line 17) | def test_does_not_trim_fields_when_trim_param_is_false(self):

FILE: tests/test_form_meta.py
  class TestModelFormMetaWithInheritance (line 6) | class TestModelFormMetaWithInheritance(ModelFormTestCase):
    method test_skip_unknown_types (line 7) | def test_skip_unknown_types(self, model_form_all):
    method test_inheritance_attributes (line 21) | def test_inheritance_attributes(self, model_form_custom):
  class TestUnboundFieldsInitialization (line 31) | class TestUnboundFieldsInitialization(ModelFormTestCase):
    method test_skip_unknown_types (line 32) | def test_skip_unknown_types(self, model_form_all):

FILE: tests/test_hybrid_properties.py
  class TestHybridProperties (line 10) | class TestHybridProperties(ModelFormTestCase):
    method test_hybrid_property_returning_column_property (line 11) | def test_hybrid_property_returning_column_property(self):
    method test_hybrid_property_returning_expression (line 36) | def test_hybrid_property_returning_expression(self):

FILE: tests/test_i18n_extension.py
  class TestInternationalizationExtension (line 16) | class TestInternationalizationExtension(ModelFormTestCase):
    method init (line 17) | def init(self):
    method test_supports_translated_columns (line 40) | def test_supports_translated_columns(self):
    method test_supports_field_exclusion (line 46) | def test_supports_field_exclusion(self):
    method test_model_population (line 57) | def test_model_population(self):

FILE: tests/test_inheritance.py
  class TestInheritance (line 7) | class TestInheritance(FormTestCase):
    class Base (line 8) | class Base(Form):
      method get_session (line 10) | def get_session(self):
    method test_default_base (line 13) | def test_default_base(self):
    method test_custom_base_without_session (line 16) | def test_custom_base_without_session(self):
    method test_custom_base_with_session (line 20) | def test_custom_base_with_session(self):
    method test_inherit_with_new_session (line 24) | def test_inherit_with_new_session(self):
    method test_inherit_without_new_session (line 34) | def test_inherit_without_new_session(self):

FILE: tests/test_labels.py
  class TestFieldLabels (line 5) | class TestFieldLabels(ModelFormTestCase):
    method test_assigns_labels_from_column_info (line 6) | def test_assigns_labels_from_column_info(self):
    method test_assigns_labels_from_form_configuration (line 10) | def test_assigns_labels_from_form_configuration(self):

FILE: tests/test_model_field_list.py
  class ModelFieldListTestCase (line 9) | class ModelFieldListTestCase(FormRelationsTestCase):
    method create_models (line 10) | def create_models(self):
    method save (line 27) | def save(self, event=None, data=None):
  class TestReplaceStrategy (line 47) | class TestReplaceStrategy(ModelFieldListTestCase):
    method create_forms (line 48) | def create_forms(self):
    method test_assigment_and_deletion (line 62) | def test_assigment_and_deletion(self):
  class TestUpdateStrategy (line 75) | class TestUpdateStrategy(ModelFieldListTestCase):
    method create_models (line 76) | def create_models(self):
    method create_forms (line 102) | def create_forms(self):
    method test_single_entry_update (line 121) | def test_single_entry_update(self):
    method test_creates_new_objects_for_entries_with_unknown_identifiers (line 135) | def test_creates_new_objects_for_entries_with_unknown_identifiers(self):
    method test_replace_entry (line 147) | def test_replace_entry(self):
    method test_replace_and_update (line 169) | def test_replace_and_update(self):
    method test_multiple_entries (line 196) | def test_multiple_entries(self):
    method test_delete_all_field_list_entries (line 216) | def test_delete_all_field_list_entries(self):
    method test_update_and_remove (line 222) | def test_update_and_remove(self):

FILE: tests/test_model_form_factory.py
  class TestModelFormFactory (line 13) | class TestModelFormFactory(ModelFormTestCase):
    method test_supports_parameter_overriding (line 14) | def test_supports_parameter_overriding(self):
    method test_throws_exception_for_unknown_configuration_option (line 35) | def test_throws_exception_for_unknown_configuration_option(self):
    method test_supports_custom_base_class_with_model_form_factory (line 45) | def test_supports_custom_base_class_with_model_form_factory(self):
    method test_url_validator (line 57) | def test_url_validator(self):
    method test_email_validator (line 61) | def test_email_validator(self):
    method test_length_validator (line 65) | def test_length_validator(self):
    method test_number_range_validator (line 69) | def test_number_range_validator(self):
    method test_date_range_validator (line 73) | def test_date_range_validator(self):
    method test_time_range_validator (line 77) | def test_time_range_validator(self):
    method test_optional_validator (line 81) | def test_optional_validator(self):
    method test_unique_validator (line 85) | def test_unique_validator(self):
    method test_class_meta_wtforms2 (line 89) | def test_class_meta_wtforms2(self):

FILE: tests/test_model_form_field.py
  class TestOneToOneModelFormRelations (line 8) | class TestOneToOneModelFormRelations(FormRelationsTestCase):
    method create_models (line 9) | def create_models(self):
    method create_forms (line 25) | def create_forms(self):
    method save (line 39) | def save(self, event=None, data={}):
    method test_assigment_and_deletion (line 54) | def test_assigment_and_deletion(self):
    method test_only_populates_related_if_they_are_obj_attributes (line 66) | def test_only_populates_related_if_they_are_obj_attributes(self):
    method test_updating_related_object (line 83) | def test_updating_related_object(self):

FILE: tests/test_phone_number.py
  class TestCase (line 12) | class TestCase:
    method setup_method (line 13) | def setup_method(self, method):
    method teardown_method (line 23) | def teardown_method(self, method):
    method create_models (line 28) | def create_models(self):
  class TestPhoneNumbers (line 39) | class TestPhoneNumbers(TestCase):
    method setup_method (line 46) | def setup_method(self, method):
    method test_query_returns_phone_number_object (line 63) | def test_query_returns_phone_number_object(self):
    method test_phone_number_is_stored_as_string (line 67) | def test_phone_number_is_stored_as_string(self):
    method test_phone_number_in_form (line 74) | def test_phone_number_in_form(self):
    method test_empty_phone_number_in_form (line 82) | def test_empty_phone_number_in_form(self):

FILE: tests/test_phone_number_field.py
  class TestPhoneNumberField (line 8) | class TestPhoneNumberField:
    method setup_method (line 9) | def setup_method(self, method):
    method init_form (line 21) | def init_form(self, **kwargs):
    method test_valid_phone_numbers (line 27) | def test_valid_phone_numbers(self):
    method test_invalid_phone_numbers (line 34) | def test_invalid_phone_numbers(self):
    method test_render_empty_phone_number_value (line 41) | def test_render_empty_phone_number_value(self):
    method test_empty_phone_number_value_passed_as_none (line 46) | def test_empty_phone_number_value_passed_as_none(self):
    method test_default_display_format (line 53) | def test_default_display_format(self):
    method test_international_display_format (line 58) | def test_international_display_format(self):
    method test_e164_display_format (line 63) | def test_e164_display_format(self):
    method test_field_rendering_when_invalid_phone_number (line 68) | def test_field_rendering_when_invalid_phone_number(self):
    method test_required_phone_number_form (line 83) | def test_required_phone_number_form(self, number, error_msg, check_val...

FILE: tests/test_query_select_field.py
  class DummyPostData (line 15) | class DummyPostData(dict):
    method getlist (line 16) | def getlist(self, key):
  class LazySelect (line 23) | class LazySelect:
    method __call__ (line 24) | def __call__(self, field, **kwargs):
  class Base (line 31) | class Base:
    method __init__ (line 32) | def __init__(self, **kwargs):
  class TestBase (line 37) | class TestBase:
    method create_models (line 38) | def create_models(self):
    method _fill (line 56) | def _fill(self, sess):
  class TestQuerySelectField (line 66) | class TestQuerySelectField(TestBase):
    method setup_method (line 67) | def setup_method(self):
    method teardown_method (line 78) | def teardown_method(self):
    method test_without_factory (line 83) | def test_without_factory(self):
    method test_with_query_factory (line 112) | def test_with_query_factory(self):
  class TestQuerySelectMultipleField (line 178) | class TestQuerySelectMultipleField(TestBase):
    method setup_method (line 179) | def setup_method(self):
    method teardown_method (line 192) | def teardown_method(self):
    class F (line 197) | class F(Form):
    method test_unpopulated_default (line 200) | def test_unpopulated_default(self):
    method test_single_value_without_factory (line 204) | def test_single_value_without_factory(self):
    method test_multiple_values_without_query_factory (line 211) | def test_multiple_values_without_query_factory(self):
    method test_single_default_value (line 223) | def test_single_default_value(self):
    method test_empty_query (line 239) | def test_empty_query(self):
  class DatabaseTestCase (line 250) | class DatabaseTestCase:
    method setup_method (line 251) | def setup_method(self, method):
    method teardown_method (line 262) | def teardown_method(self, method):
    method create_models (line 267) | def create_models(self):
    method create_cities (line 284) | def create_cities(self):
  class TestGroupedQuerySelectField (line 296) | class TestGroupedQuerySelectField(DatabaseTestCase):
    method create_form (line 297) | def create_form(self, **kwargs):
    method test_custom_none_value (line 313) | def test_custom_none_value(self):
    method test_rendering (line 322) | def test_rendering(self):
  class TestGroupedQuerySelectMultipleField (line 341) | class TestGroupedQuerySelectMultipleField(DatabaseTestCase):
    method create_form (line 342) | def create_form(self, **kwargs):
    method test_unpopulated_default (line 359) | def test_unpopulated_default(self):
    method test_single_value_without_factory (line 364) | def test_single_value_without_factory(self):
    method test_multiple_values_without_query_factory (line 374) | def test_multiple_values_without_query_factory(self):
    method test_rendering (line 402) | def test_rendering(self):

FILE: tests/test_select_field.py
  class MultiDict (line 9) | class MultiDict(dict):
    method getlist (line 10) | def getlist(self, key):
  class TestSelectFieldDefaultValue (line 14) | class TestSelectFieldDefaultValue(ModelFormTestCase):
    method test_option_selected_by_field_default_value (line 15) | def test_option_selected_by_field_default_value(self):
  class TestSelectFieldCoerce (line 22) | class TestSelectFieldCoerce(ModelFormTestCase):
    method test_integer_coerces_values_to_integers (line 23) | def test_integer_coerces_values_to_integers(self):
    method test_nullable_integer_coerces_values_to_integers (line 29) | def test_nullable_integer_coerces_values_to_integers(self):
    method test_integer_coerces_empty_strings_to_nulls (line 35) | def test_integer_coerces_empty_strings_to_nulls(self):
    method test_big_integer_coerces_values_to_integers (line 41) | def test_big_integer_coerces_values_to_integers(self):
    method test_small_integer_coerces_values_to_integers (line 48) | def test_small_integer_coerces_values_to_integers(self):
    method test_numeric_coerces_values_to_decimals (line 54) | def test_numeric_coerces_values_to_decimals(self):
    method test_float_coerces_values_to_floats (line 60) | def test_float_coerces_values_to_floats(self):
    method test_unicode_coerces_values_to_unicode_strings (line 66) | def test_unicode_coerces_values_to_unicode_strings(self):
    method test_unicode_text_coerces_values_to_unicode_strings (line 73) | def test_unicode_text_coerces_values_to_unicode_strings(self):

FILE: tests/test_synonym.py
  class TestSynonym (line 8) | class TestSynonym(ModelFormTestCase):
    method test_synonym_returning_column_property_with_include (line 9) | def test_synonym_returning_column_property_with_include(self):
    method test_synonym_returning_column_property_with_only (line 36) | def test_synonym_returning_column_property_with_only(self):

FILE: tests/test_types.py
  class UnknownType (line 55) | class UnknownType(sa.types.UserDefinedType):
    method get_col_spec (line 56) | def get_col_spec(self):
  class CustomUnicodeTextType (line 60) | class CustomUnicodeTextType(sa.types.TypeDecorator):
  class CustomUnicodeType (line 64) | class CustomUnicodeType(sa.types.TypeDecorator):
  class CustomNumericType (line 68) | class CustomNumericType(sa.types.TypeDecorator):
  class TestModelColumnToFormFieldTypeConversion (line 72) | class TestModelColumnToFormFieldTypeConversion(ModelFormTestCase):
    method test_raises_exception_for_unknown_type (line 73) | def test_raises_exception_for_unknown_type(self):
    method test_raises_exception_for_array_type (line 78) | def test_raises_exception_for_array_type(self):
    method test_unicode_converts_to_text_field (line 83) | def test_unicode_converts_to_text_field(self):
    method test_custom_unicode_converts_to_text_field (line 87) | def test_custom_unicode_converts_to_text_field(self):
    method test_string_converts_to_text_field (line 91) | def test_string_converts_to_text_field(self):
    method test_integer_converts_to_integer_field (line 95) | def test_integer_converts_to_integer_field(self):
    method test_unicode_text_converts_to_text_area_field (line 99) | def test_unicode_text_converts_to_text_area_field(self):
    method test_custom_unicode_text_converts_to_text_area_field (line 103) | def test_custom_unicode_text_converts_to_text_area_field(self):
    method test_boolean_converts_to_boolean_field (line 107) | def test_boolean_converts_to_boolean_field(self):
    method test_datetime_converts_to_datetime_field (line 111) | def test_datetime_converts_to_datetime_field(self):
    method test_date_converts_to_date_field (line 115) | def test_date_converts_to_date_field(self):
    method test_float_converts_to_float_field (line 119) | def test_float_converts_to_float_field(self):
    method test_numeric_converts_to_decimal_field (line 123) | def test_numeric_converts_to_decimal_field(self):
    method test_numeric_scale_converts_to_decimal_field_scale (line 127) | def test_numeric_scale_converts_to_decimal_field_scale(self):
    method test_custom_numeric_converts_to_decimal_field (line 132) | def test_custom_numeric_converts_to_decimal_field(self):
    method test_enum_field_converts_to_select_field (line 136) | def test_enum_field_converts_to_select_field(self):
    method test_nullable_enum_uses_null_or_unicode_coerce_func_by_default (line 143) | def test_nullable_enum_uses_null_or_unicode_coerce_func_by_default(self):
    method test_custom_choices_override_enum_choices (line 149) | def test_custom_choices_override_enum_choices(self):
    method test_column_with_choices_converts_to_select_field (line 156) | def test_column_with_choices_converts_to_select_field(self):
    method test_assigns_email_validator_for_email_type (line 163) | def test_assigns_email_validator_for_email_type(self):
    method test_assigns_url_validator_for_url_type (line 167) | def test_assigns_url_validator_for_url_type(self):
    method test_time_converts_to_time_field (line 171) | def test_time_converts_to_time_field(self):
    method test_varchar_converts_to_text_field (line 175) | def test_varchar_converts_to_text_field(self):
    method test_text_converts_to_textarea_field (line 179) | def test_text_converts_to_textarea_field(self):
    method test_char_converts_to_text_field (line 183) | def test_char_converts_to_text_field(self):
    method test_real_converts_to_float_field (line 187) | def test_real_converts_to_float_field(self):
    method test_json_converts_to_textarea_field (line 191) | def test_json_converts_to_textarea_field(self):
    method test_phone_number_converts_to_phone_number_field (line 196) | def test_phone_number_converts_to_phone_number_field(self):
    method test_phone_number_country_code_passed_to_field (line 201) | def test_phone_number_country_code_passed_to_field(self):
    method test_phone_number_type_has_no_length_validation (line 207) | def test_phone_number_type_has_no_length_validation(self):
    method test_range_type_conversion (line 214) | def test_range_type_conversion(self, type, field):
    method test_password_type_converts_to_password_field (line 219) | def test_password_type_converts_to_password_field(self):
    method test_arrow_type_converts_to_datetime_field (line 224) | def test_arrow_type_converts_to_datetime_field(self):
    method test_url_type_converts_to_string_field (line 228) | def test_url_type_converts_to_string_field(self):
    method test_uuid_type_converst_to_uuid_type (line 232) | def test_uuid_type_converst_to_uuid_type(self):
    method test_color_type_converts_to_color_field (line 236) | def test_color_type_converts_to_color_field(self):
    method test_email_type_converts_to_email_field (line 240) | def test_email_type_converts_to_email_field(self):
    method test_country_type_converts_to_country_field (line 244) | def test_country_type_converts_to_country_field(self):
    method test_choice_type_converts_to_select_field (line 248) | def test_choice_type_converts_to_select_field(self):
    method test_choice_type_uses_custom_coerce_func (line 254) | def test_choice_type_uses_custom_coerce_func(self):
    method test_choice_type_with_enum (line 262) | def test_choice_type_with_enum(self):
    method test_choice_type_with_enum_uses_custom_coerce_func (line 275) | def test_choice_type_with_enum_uses_custom_coerce_func(self, type_, im...
  class TestWeekDaysTypeConversion (line 290) | class TestWeekDaysTypeConversion(ModelFormTestCase):
    method test_weekdays_type_converts_to_weekdays_field (line 291) | def test_weekdays_type_converts_to_weekdays_field(self):
  class TestCustomTypeMap (line 296) | class TestCustomTypeMap(ModelFormTestCase):
    method test_override_type_map_on_class_level (line 297) | def test_override_type_map_on_class_level(self):
    method test_override_type_map_with_callable (line 312) | def test_override_type_map_with_callable(self):

FILE: tests/test_unique_validator.py
  class Color (line 14) | class Color(base):
  class User (line 20) | class User(base):
  class TestUniqueValidator (line 29) | class TestUniqueValidator:
    method create_models (line 30) | def create_models(self):
    method setup_method (line 35) | def setup_method(self, method):
    method teardown_method (line 46) | def teardown_method(self, method):
    method _test_syntax (line 51) | def _test_syntax(self, column, expected_dict):
    method test_with_form_obj_unavailable (line 64) | def test_with_form_obj_unavailable(self):
    method test_columns_as_tuples (line 89) | def test_columns_as_tuples(self, column, expected_dict):
    method test_columns_as_tuples_classical_mapping (line 92) | def test_columns_as_tuples_classical_mapping(self):
    method test_raises_exception_if_improperly_configured (line 99) | def test_raises_exception_if_improperly_configured(self, column):
    method test_raises_exception_string_if_improperly_configured (line 112) | def test_raises_exception_string_if_improperly_configured(self):
    method test_existing_name_collision (line 125) | def test_existing_name_collision(self):
    method test_existing_name_collision_multiple (line 138) | def test_existing_name_collision_multiple(self):
    method test_works_with_flask_sqlalchemy_syntax (line 154) | def test_works_with_flask_sqlalchemy_syntax(self, monkeypatch):
    method test_existing_name_collision_classical_mapping (line 172) | def test_existing_name_collision_classical_mapping(self):
    method test_relationship_multiple_collision (line 195) | def test_relationship_multiple_collision(self):
    method test_relationship_multiple_no_collision (line 235) | def test_relationship_multiple_no_collision(self):
    method test_without_obj_without_collision (line 275) | def test_without_obj_without_collision(self):
    method test_without_obj_without_collision_multiple (line 287) | def test_without_obj_without_collision_multiple(self):
    method test_existing_name_no_collision (line 302) | def test_existing_name_no_collision(self):
    method test_existing_name_no_collision_multiple (line 314) | def test_existing_name_no_collision_multiple(self):
    method test_supports_model_query_parameter (line 331) | def test_supports_model_query_parameter(self):

FILE: tests/test_utils.py
  class TestUtils (line 7) | class TestUtils(FormRelationsTestCase):
    method create_models (line 8) | def create_models(self):
    method create_forms (line 39) | def create_forms(self):
    method test_find_entity (line 42) | def test_find_entity(self):

FILE: tests/test_validators.py
  class TestAutoAssignedValidators (line 19) | class TestAutoAssignedValidators(ModelFormTestCase):
    method test_auto_assigns_length_validators (line 20) | def test_auto_assigns_length_validators(self):
    method test_assigns_validators_from_info_field (line 24) | def test_assigns_validators_from_info_field(self):
    method test_assigns_unique_validator_for_unique_fields (line 28) | def test_assigns_unique_validator_for_unique_fields(self):
    method test_assigns_non_nullable_fields_as_required (line 32) | def test_assigns_non_nullable_fields_as_required(self):
    method test_type_level_not_nullable_validators (line 37) | def test_type_level_not_nullable_validators(self):
    method test_not_nullable_validator_with_type_decorator (line 54) | def test_not_nullable_validator_with_type_decorator(self):
    method test_not_null_validator_as_empty_list (line 71) | def test_not_null_validator_as_empty_list(self):
    method test_not_null_validator_as_none (line 86) | def test_not_null_validator_as_none(self):
    method test_not_nullable_booleans_are_required (line 102) | def test_not_nullable_booleans_are_required(self):
    method test_not_nullable_fields_with_defaults_are_not_required (line 106) | def test_not_nullable_fields_with_defaults_are_not_required(self):
    method test_assigns_nullable_integers_as_optional (line 110) | def test_assigns_nullable_integers_as_optional(self):
    method test_override_email_validator (line 114) | def test_override_email_validator(self):
    method test_override_optional_validator (line 131) | def test_override_optional_validator(self):
    method test_override_number_range_validator (line 152) | def test_override_number_range_validator(self):
    method test_override_date_range_validator (line 169) | def test_override_date_range_validator(self):
    method test_override_time_range_validator (line 188) | def test_override_time_range_validator(self):
    method test_override_length_validator (line 205) | def test_override_length_validator(self):
    method test_override_optional_validator_as_none (line 222) | def test_override_optional_validator_as_none(self):
    method test_override_unique_validator (line 236) | def test_override_unique_validator(self):

FILE: tests/test_weekdays_field.py
  class TestWeekDaysField (line 7) | class TestWeekDaysField:
    method init_form (line 8) | def init_form(self, **kwargs):
    method test_valid_weekdays (line 14) | def test_valid_weekdays(self):
    method test_invalid_weekdays (line 20) | def test_invalid_weekdays(self):

FILE: tests/test_widgets.py
  class TestNumericFieldWidgets (line 8) | class TestNumericFieldWidgets(ModelFormTestCase):
    method test_converts_numeric_scale_to_steps (line 9) | def test_converts_numeric_scale_to_steps(self):
    method test_supports_numeric_column_without_scale (line 16) | def test_supports_numeric_column_without_scale(self):
    method test_supports_step_as_info_arg (line 23) | def test_supports_step_as_info_arg(self):
    method test_numeric_field_with_scale_and_choices (line 31) | def test_numeric_field_with_scale_and_choices(self):
  class TestIntegerFieldWidgets (line 42) | class TestIntegerFieldWidgets(ModelFormTestCase):
    method test_supports_step_as_info_arg (line 43) | def test_supports_step_as_info_arg(self):
  class TestFloatFieldWidgets (line 52) | class TestFloatFieldWidgets(ModelFormTestCase):
    method test_supports_step_as_info_arg (line 53) | def test_supports_step_as_info_arg(self):

FILE: wtforms_alchemy/__init__.py
  function model_form_meta_factory (line 61) | def model_form_meta_factory(base=FormMeta):
  function model_form_factory (line 124) | def model_form_factory(base=Form, meta=ModelFormMeta, **defaults):
  class ModelCreateForm (line 286) | class ModelCreateForm(ModelForm):
  class ModelUpdateForm (line 290) | class ModelUpdateForm(ModelForm):
    class Meta (line 291) | class Meta:
  class ModelSearchForm (line 296) | class ModelSearchForm(ModelForm):
    class Meta (line 297) | class Meta:

FILE: wtforms_alchemy/exc.py
  class UnknownTypeException (line 1) | class UnknownTypeException(Exception):
    method __init__ (line 2) | def __init__(self, column):
  class InvalidAttributeException (line 8) | class InvalidAttributeException(Exception):
    method __init__ (line 9) | def __init__(self, attr_name):
  class AttributeTypeException (line 15) | class AttributeTypeException(Exception):
    method __init__ (line 16) | def __init__(self, attr_name):
  class UnknownConfigurationOption (line 22) | class UnknownConfigurationOption(Exception):
    method __init__ (line 23) | def __init__(self, option):

FILE: wtforms_alchemy/fields.py
  class SkipOperation (line 20) | class SkipOperation(Exception):
  class ModelFormField (line 24) | class ModelFormField(FormField):
    method populate_obj (line 25) | def populate_obj(self, obj, name):
  class ModelFieldList (line 35) | class ModelFieldList(FieldList):
    method __init__ (line 36) | def __init__(self, unbound_field, population_strategy="update", **kwar...
    method model (line 41) | def model(self):
    method _get_bound_field_for_entry (line 44) | def _get_bound_field_for_entry(self, formdata, data, index):
    method _add_entry (line 55) | def _add_entry(self, formdata=None, data=unset_value, index=None):
    method populate_obj (line 74) | def populate_obj(self, obj, name):
  class CountryField (line 98) | class CountryField(SelectField):
    method __init__ (line 99) | def __init__(self, *args, **kwargs):
    method _get_choices (line 104) | def _get_choices(self):
  class QuerySelectField (line 116) | class QuerySelectField(SelectFieldBase):
    method __init__ (line 145) | def __init__(
    method _get_data (line 176) | def _get_data(self):
    method _set_data (line 184) | def _set_data(self, data):
    method _get_object_list (line 190) | def _get_object_list(self):
    method iter_choices (line 197) | def iter_choices(self):
    method process_formdata (line 204) | def process_formdata(self, valuelist):
    method pre_validate (line 212) | def pre_validate(self, form):
  class QuerySelectMultipleField (line 224) | class QuerySelectMultipleField(QuerySelectField):
    method __init__ (line 235) | def __init__(self, label=None, validators=None, default=None, **kwargs):
    method _get_data (line 247) | def _get_data(self):
    method _set_data (line 262) | def _set_data(self, data):
    method iter_choices (line 268) | def iter_choices(self):
    method process_formdata (line 272) | def process_formdata(self, valuelist):
    method pre_validate (line 275) | def pre_validate(self, form):
  function get_pk_from_identity (line 285) | def get_pk_from_identity(obj):
  class GroupedQuerySelectField (line 290) | class GroupedQuerySelectField(SelectField):
    method __init__ (line 293) | def __init__(
    method _get_object_list (line 325) | def _get_object_list(self):
    method _pre_process_object_list (line 329) | def _pre_process_object_list(self, object_list):
    method choices (line 335) | def choices(self):
    method choices (line 356) | def choices(self, value):
    method data (line 360) | def data(self):
    method data (line 369) | def data(self, data):
    method iter_choices (line 373) | def iter_choices(self):
    method process_formdata (line 389) | def process_formdata(self, valuelist):
    method pre_validate (line 397) | def pre_validate(self, form):
  class GroupedQuerySelectMultipleField (line 409) | class GroupedQuerySelectMultipleField(SelectField):
    method __init__ (line 412) | def __init__(
    method _get_object_list (line 453) | def _get_object_list(self):
    method _pre_process_object_list (line 457) | def _pre_process_object_list(self, object_list):
    method choices (line 463) | def choices(self):
    method choices (line 484) | def choices(self, value):
    method data (line 488) | def data(self):
    method data (line 504) | def data(self, valuelist):
    method iter_choices (line 508) | def iter_choices(self):
    method process_formdata (line 521) | def process_formdata(self, valuelist):
    method pre_validate (line 524) | def pre_validate(self, form):
  class WeekDaysField (line 535) | class WeekDaysField(SelectMultipleField):
    method __init__ (line 539) | def __init__(self, *args, **kwargs):
    method _get_choices (line 544) | def _get_choices(self):
    method process_data (line 549) | def process_data(self, value):
    method process_formdata (line 552) | def process_formdata(self, valuelist):
    method pre_validate (line 555) | def pre_validate(self, form):
  class PhoneNumberField (line 559) | class PhoneNumberField(StringField):
    method __init__ (line 576) | def __init__(
    method _value (line 588) | def _value(self):
    method process_formdata (line 599) | def process_formdata(self, valuelist):

FILE: wtforms_alchemy/generator.py
  class FormGenerator (line 64) | class FormGenerator:
    method __init__ (line 125) | def __init__(self, form_class):
    method create_form (line 137) | def create_form(self, form):
    method filter_attributes (line 156) | def filter_attributes(self, attrs):
    method validate_attribute (line 190) | def validate_attribute(self, attr_name):
    method create_fields (line 218) | def create_fields(self, form, properties):
    method skip_column_property (line 238) | def skip_column_property(self, column_property):
    method skip_column (line 249) | def skip_column(self, column):
    method has_index (line 281) | def has_index(self, column):
    method create_field (line 295) | def create_field(self, prop, column):
    method default (line 318) | def default(self, column):
    method filters (line 330) | def filters(self, column):
    method date_format (line 346) | def date_format(self, column):
    method type_specific_parameters (line 360) | def type_specific_parameters(self, column):
    method widget (line 384) | def widget(self, column):
    method scale_to_step (line 408) | def scale_to_step(self, scale):
    method type_agnostic_parameters (line 416) | def type_agnostic_parameters(self, key, column):
    method select_field_kwargs (line 427) | def select_field_kwargs(self, column):
    method coerce (line 452) | def coerce(self, column):
    method create_validators (line 471) | def create_validators(self, prop, column):
    method required_validator (line 492) | def required_validator(self, column):
    method get_validator (line 519) | def get_validator(self, name, **kwargs):
    method additional_validators (line 527) | def additional_validators(self, key, column):
    method unique_validator (line 548) | def unique_validator(self, key, column):
    method range_validator (line 562) | def range_validator(self, column):
    method length_validator (line 580) | def length_validator(self, column):
    method get_field_class (line 593) | def get_field_class(self, column):

FILE: wtforms_alchemy/utils.py
  function choice_type_coerce_factory (line 11) | def choice_type_coerce_factory(type_):
  function strip_string (line 34) | def strip_string(value):
  function is_scalar (line 40) | def is_scalar(value):
  function null_or_unicode (line 44) | def null_or_unicode(value):
  function null_or_int (line 48) | def null_or_int(value):
  function flatten (line 55) | def flatten(list_):
  function is_number (line 65) | def is_number(type):
  function is_number_range (line 69) | def is_number_range(type):
  function is_date_column (line 73) | def is_date_column(column):
  function table (line 79) | def table(model):
  function find_entity (line 86) | def find_entity(coll, model, data):
  function translated_attributes (line 111) | def translated_attributes(model):
  class ClassMap (line 133) | class ClassMap(OrderedDict):
    method __init__ (line 145) | def __init__(self, items=None):
    method __contains__ (line 150) | def __contains__(self, key):
    method __getitem__ (line 191) | def __getitem__(self, key):

FILE: wtforms_alchemy/validators.py
  class Unique (line 8) | class Unique:
    method __init__ (line 31) | def __init__(self, column, get_session=None, message=None):
    method query (line 37) | def query(self):
    method _check_for_session (line 49) | def _check_for_session(self, model):
    method _syntaxes_as_tuples (line 53) | def _syntaxes_as_tuples(self, form, field, column):
    method __call__ (line 69) | def __call__(self, form, field):
Condensed preview — 62 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (262K chars).
[
  {
    "path": ".github/workflows/lint.yml",
    "chars": 450,
    "preview": "name: Lint\n\non:\n  - push\n  - pull_request\n\njobs:\n  test:\n    name: Lint\n    runs-on: ubuntu-latest\n    steps:\n      - us"
  },
  {
    "path": ".github/workflows/test.yaml",
    "chars": 767,
    "preview": "name: Tests\non: [push, pull_request]\njobs:\n  test:\n    name: Python ${{ matrix.python-version }} + SQLAlchemy ${{ matrix"
  },
  {
    "path": ".gitignore",
    "chars": 259,
    "preview": "*.py[co]\n\n# Packages\n*.egg\n*.egg-info\ndist\nbuild\neggs\nparts\nbin\nvar\nsdist\ndevelop-eggs\n.installed.cfg\n\n# Installer logs\n"
  },
  {
    "path": "CHANGES.rst",
    "chars": 11400,
    "preview": "Changelog\n=========\n\nHere you can see the full list of changes between each WTForms-Alchemy release.\n\n0.19.1 (2025-08-11"
  },
  {
    "path": "LICENSE",
    "chars": 1437,
    "preview": "Copyright (c) 2012, Konsta Vesterinen\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or "
  },
  {
    "path": "MANIFEST.in",
    "chars": 193,
    "preview": "include CHANGES.rst LICENSE README.rst\nrecursive-include tests *\nrecursive-exclude tests *.pyc\nrecursive-include docs *\n"
  },
  {
    "path": "README.rst",
    "chars": 599,
    "preview": "WTForms-Alchemy\n===============\n\n|Version Status| |Downloads|\n\nTools for creating WTForms forms from SQLAlchemy models\n\n"
  },
  {
    "path": "docs/Makefile",
    "chars": 4626,
    "preview": "# Makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line.\nSPHINXOPTS    =\nSPHINXBUILD "
  },
  {
    "path": "docs/advanced.rst",
    "chars": 1750,
    "preview": "Advanced concepts\n=================\n\nUsing WTForms-Alchemy with SQLAlchemy-Defaults\n------------------------------------"
  },
  {
    "path": "docs/api.rst",
    "chars": 918,
    "preview": "API Documentation\n=================\n\nThis part of the documentation covers all the public classes and functions\nin WTFor"
  },
  {
    "path": "docs/column_conversion.rst",
    "chars": 6972,
    "preview": "Column to form field conversion\n===============================\n\nBasic type conversion\n---------------------\n\nBy default"
  },
  {
    "path": "docs/conf.py",
    "chars": 7629,
    "preview": "#\n# WTForms-Alchemy documentation build configuration file, created by\n# sphinx-quickstart on Wed Aug 29 16:20:21 2012.\n"
  },
  {
    "path": "docs/configuration.rst",
    "chars": 7994,
    "preview": "Configuration\n=============\n\nModelForm meta parameters\n-------------------------\n\nThe following configuration options ar"
  },
  {
    "path": "docs/customization.rst",
    "chars": 3609,
    "preview": "Form customization\n==================\n\n\nCustom fields\n-------------\n\nIf you want to use a custom field class, you can pa"
  },
  {
    "path": "docs/index.rst",
    "chars": 583,
    "preview": "WTForms-Alchemy\n===============\n\nWTForms-Alchemy is a WTForms extension toolkit for easier creation of model\nbased forms"
  },
  {
    "path": "docs/introduction.rst",
    "chars": 3186,
    "preview": "Introduction\n============\n\nWhat for?\n---------\nMany times when building modern web apps with SQLAlchemy you’ll have form"
  },
  {
    "path": "docs/license.rst",
    "chars": 41,
    "preview": "License\n=======\n\n.. include:: ../LICENSE\n"
  },
  {
    "path": "docs/make.bat",
    "chars": 4529,
    "preview": "@ECHO OFF\n\nREM Command file for Sphinx documentation\n\nif \"%SPHINXBUILD%\" == \"\" (\n\tset SPHINXBUILD=sphinx-build\n)\nset BUI"
  },
  {
    "path": "docs/relationships.rst",
    "chars": 2796,
    "preview": "Forms with relations\n====================\n\nWTForms-Alchemy provides special Field subtypes ModelFormField and ModelField"
  },
  {
    "path": "docs/types.rst",
    "chars": 6236,
    "preview": "Type specific conversion\n========================\n\n\nNumeric type\n------------\n\nWTForms-Alchemy automatically converts Nu"
  },
  {
    "path": "docs/validators.rst",
    "chars": 6288,
    "preview": "Validators\n==========\n\n\nAuto-assigned validators\n------------------------\n\nBy default WTForms-Alchemy ModelForm assigns "
  },
  {
    "path": "pyproject.toml",
    "chars": 473,
    "preview": "[tool.pytest.ini_options]\nfilterwarnings = [\n    'error:.*:sqlalchemy.exc.SADeprecationWarning',\n    'error:.*:sqlalchem"
  },
  {
    "path": "setup.py",
    "chars": 2438,
    "preview": "\"\"\"\nWTForms-Alchemy\n---------------\n\nGenerates WTForms forms from SQLAlchemy models.\n\"\"\"\n\nimport os\nimport re\n\nfrom setu"
  },
  {
    "path": "tests/__init__.py",
    "chars": 1942,
    "preview": "import sqlalchemy as sa\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import declarative_base, sessionmaker\nf"
  },
  {
    "path": "tests/conftest.py",
    "chars": 995,
    "preview": "import pytest\nfrom wtforms.form import FormMeta\n\nfrom wtforms_alchemy import (\n    model_form_factory,\n    model_form_me"
  },
  {
    "path": "tests/test_class_map.py",
    "chars": 1116,
    "preview": "from pytest import mark, raises\n\nfrom wtforms_alchemy.utils import ClassMap\n\n\nclass A:\n    pass\n\n\nclass B:\n    pass\n\n\ncl"
  },
  {
    "path": "tests/test_column_aliases.py",
    "chars": 2789,
    "preview": "import sqlalchemy as sa\nfrom wtforms.validators import NumberRange\n\nfrom tests import ModelFormTestCase\nfrom wtforms_alc"
  },
  {
    "path": "tests/test_configuration.py",
    "chars": 7138,
    "preview": "import sqlalchemy as sa\nfrom pytest import raises\nfrom wtforms.fields import IntegerField\nfrom wtforms.validators import"
  },
  {
    "path": "tests/test_country_field.py",
    "chars": 1116,
    "preview": "import sqlalchemy_utils\nfrom babel import Locale\nfrom wtforms import Form\n\nfrom tests import MultiDict\nfrom wtforms_alch"
  },
  {
    "path": "tests/test_custom_fields.py",
    "chars": 520,
    "preview": "from wtforms import Form\nfrom wtforms_components import SelectField\n\nfrom tests import MultiDict\nfrom wtforms_alchemy im"
  },
  {
    "path": "tests/test_deep_form_relations.py",
    "chars": 5237,
    "preview": "import sqlalchemy as sa\nfrom wtforms.fields import FormField\n\nfrom tests import FormRelationsTestCase, MultiDict\nfrom wt"
  },
  {
    "path": "tests/test_descriptions.py",
    "chars": 659,
    "preview": "from tests import ModelFormTestCase\nfrom wtforms_alchemy import ModelForm\n\n\nclass TestFieldParameters(ModelFormTestCase)"
  },
  {
    "path": "tests/test_field_exclusion.py",
    "chars": 924,
    "preview": "from datetime import datetime\n\nimport sqlalchemy as sa\nfrom sqlalchemy_utils import TSVectorType\n\nfrom tests import Mode"
  },
  {
    "path": "tests/test_field_order.py",
    "chars": 999,
    "preview": "import sqlalchemy as sa\n\nfrom tests import ModelFormTestCase\n\n\nclass TestFieldOrder(ModelFormTestCase):\n    def setup_me"
  },
  {
    "path": "tests/test_field_parameters.py",
    "chars": 4167,
    "preview": "from datetime import date, time\n\nimport sqlalchemy as sa\nfrom sqlalchemy_utils import IntRangeType\nfrom wtforms import w"
  },
  {
    "path": "tests/test_field_trimming.py",
    "chars": 870,
    "preview": "from tests import ModelFormTestCase, MultiDict\nfrom wtforms_alchemy import ModelForm\n\n\nclass TestStringFieldTrimming(Mod"
  },
  {
    "path": "tests/test_form_meta.py",
    "chars": 1147,
    "preview": "import sqlalchemy as sa\n\nfrom tests import ModelFormTestCase\n\n\nclass TestModelFormMetaWithInheritance(ModelFormTestCase)"
  },
  {
    "path": "tests/test_hybrid_properties.py",
    "chars": 2042,
    "preview": "import sqlalchemy as sa\nfrom pytest import raises\nfrom sqlalchemy.ext.hybrid import hybrid_property\n\nfrom tests import M"
  },
  {
    "path": "tests/test_i18n_extension.py",
    "chars": 2048,
    "preview": "import sqlalchemy as sa\nfrom packaging.version import Version\nfrom pytest import raises, skip\nfrom sqlalchemy_i18n impor"
  },
  {
    "path": "tests/test_inheritance.py",
    "chars": 1069,
    "preview": "from wtforms import Form\nfrom wtforms_test import FormTestCase\n\nfrom wtforms_alchemy import model_form_factory, ModelFor"
  },
  {
    "path": "tests/test_labels.py",
    "chars": 620,
    "preview": "from tests import ModelFormTestCase\nfrom wtforms_alchemy import ModelForm\n\n\nclass TestFieldLabels(ModelFormTestCase):\n  "
  },
  {
    "path": "tests/test_model_field_list.py",
    "chars": 8184,
    "preview": "import sqlalchemy as sa\nfrom wtforms.fields import FormField\nfrom wtforms_components import PassiveHiddenField\n\nfrom tes"
  },
  {
    "path": "tests/test_model_form_factory.py",
    "chars": 3596,
    "preview": "import wtforms\nfrom pytest import raises\nfrom wtforms import Form\n\nfrom tests import ModelFormTestCase\nfrom wtforms_alch"
  },
  {
    "path": "tests/test_model_form_field.py",
    "chars": 2828,
    "preview": "import sqlalchemy as sa\nfrom pytest import raises\n\nfrom tests import FormRelationsTestCase, MultiDict\nfrom wtforms_alche"
  },
  {
    "path": "tests/test_phone_number.py",
    "chars": 2857,
    "preview": "import sqlalchemy as sa\nfrom pytest import mark\nfrom sqlalchemy import create_engine\nfrom sqlalchemy.orm import declarat"
  },
  {
    "path": "tests/test_phone_number_field.py",
    "chars": 3604,
    "preview": "import pytest\nfrom wtforms import Form\n\nfrom tests import MultiDict\nfrom wtforms_alchemy import DataRequired, PhoneNumbe"
  },
  {
    "path": "tests/test_query_select_field.py",
    "chars": 13762,
    "preview": "import sqlalchemy as sa\nfrom sqlalchemy.orm import declarative_base\nfrom sqlalchemy.orm.session import close_all_session"
  },
  {
    "path": "tests/test_select_field.py",
    "chars": 3346,
    "preview": "from decimal import Decimal\n\nimport sqlalchemy as sa\nfrom wtforms_components import SelectField\n\nfrom tests import Model"
  },
  {
    "path": "tests/test_synonym.py",
    "chars": 1988,
    "preview": "import sqlalchemy as sa\nfrom sqlalchemy.ext.hybrid import hybrid_property\n\nfrom tests import ModelFormTestCase\nfrom wtfo"
  },
  {
    "path": "tests/test_types.py",
    "chars": 11477,
    "preview": "from enum import Enum\n\nimport sqlalchemy as sa\nfrom pytest import mark, raises\nfrom sqlalchemy_utils import (\n    Choice"
  },
  {
    "path": "tests/test_unique_validator.py",
    "chars": 11073,
    "preview": "import sqlalchemy as sa\nfrom pytest import mark, raises\nfrom sqlalchemy.orm import declarative_base, relationship\nfrom s"
  },
  {
    "path": "tests/test_utils.py",
    "chars": 2249,
    "preview": "import sqlalchemy as sa\n\nfrom tests import FormRelationsTestCase\nfrom wtforms_alchemy import utils\n\n\nclass TestUtils(For"
  },
  {
    "path": "tests/test_validators.py",
    "chars": 8908,
    "preview": "from datetime import datetime, time\n\nimport sqlalchemy as sa\nfrom sqlalchemy_utils import EmailType\nfrom wtforms.validat"
  },
  {
    "path": "tests/test_weekdays_field.py",
    "chars": 750,
    "preview": "from wtforms import Form\n\nfrom tests import MultiDict\nfrom wtforms_alchemy import WeekDaysField\n\n\nclass TestWeekDaysFiel"
  },
  {
    "path": "tests/test_widgets.py",
    "chars": 1665,
    "preview": "from decimal import Decimal\n\nimport sqlalchemy as sa\n\nfrom tests import ModelFormTestCase\n\n\nclass TestNumericFieldWidget"
  },
  {
    "path": "tox.ini",
    "chars": 365,
    "preview": "[tox]\nenvlist = py{39,310,311,312,313}-sqlalchemy{1.4,2.0}-wtforms{3.1,3.2}\n\n[testenv]\ndeps =\n    .[test_all]\n    pytest"
  },
  {
    "path": "wtforms_alchemy/__init__.py",
    "chars": 10282,
    "preview": "import sqlalchemy as sa\nfrom wtforms import Form\nfrom wtforms.form import FormMeta\nfrom wtforms.validators import (\n    "
  },
  {
    "path": "wtforms_alchemy/exc.py",
    "chars": 743,
    "preview": "class UnknownTypeException(Exception):\n    def __init__(self, column):\n        Exception.__init__(\n            self, f\"U"
  },
  {
    "path": "wtforms_alchemy/fields.py",
    "chars": 20298,
    "preview": "import operator\nfrom itertools import groupby\n\nimport sqlalchemy as sa\nfrom sqlalchemy.orm.util import identity_key\nfrom"
  },
  {
    "path": "wtforms_alchemy/generator.py",
    "chars": 20149,
    "preview": "import inspect\nfrom collections import OrderedDict\nfrom decimal import Decimal\nfrom enum import Enum\n\nimport sqlalchemy "
  },
  {
    "path": "wtforms_alchemy/utils.py",
    "chars": 5722,
    "preview": "from collections import OrderedDict\nfrom enum import Enum\nfrom inspect import isclass\n\nimport sqlalchemy as sa\nfrom sqla"
  },
  {
    "path": "wtforms_alchemy/validators.py",
    "chars": 3244,
    "preview": "from collections.abc import Iterable, Mapping\n\nfrom sqlalchemy import Column\nfrom sqlalchemy.orm.attributes import Instr"
  }
]

About this extraction

This page contains the full source code of the kvesteri/wtforms-alchemy GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 62 files (241.9 KB), approximately 55.6k tokens, and a symbol index with 478 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!