Open Vidminas opened 1 year ago
With more thorough testing, I realised that since sa.Enum inherits from sa.String, the Length validator gets assigned to enum fields too, which leads to a crash when validating a form that contains enums, because they have no length.
Now patched and added a unit test too. tox -e sqlalchemy14
before the change:
================================================= test session starts =================================================
platform win32 -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0
cachedir: .tox\sqlalchemy14\.pytest_cache
rootdir: C:\Users\Vidminas\GitHub\wtforms-alchemy
plugins: cov-4.1.0
collected 250 items
tests\test_class_map.py ................. [ 6%]
tests\test_column_aliases.py ..... [ 8%]
tests\test_configuration.py ...........F...... [ 16%]
tests\test_country_field.py .. [ 16%]
tests\test_custom_fields.py . [ 17%]
tests\test_deep_form_relations.py .. [ 18%]
tests\test_descriptions.py .. [ 18%]
tests\test_field_exclusion.py .... [ 20%]
tests\test_field_order.py . [ 20%]
tests\test_field_parameters.py ............. [ 26%]
tests\test_field_trimming.py .. [ 26%]
tests\test_form_meta.py ........ [ 30%]
tests\test_hybrid_properties.py .. [ 30%]
tests\test_i18n_extension.py ... [ 32%]
tests\test_inheritance.py ..... [ 34%]
tests\test_labels.py .. [ 34%]
tests\test_model_field_list.py ........ [ 38%]
tests\test_model_form_factory.py ............ [ 42%]
tests\test_model_form_field.py ... [ 44%]
tests\test_phone_number.py .... [ 45%]
tests\test_phone_number_field.py ............ [ 50%]
tests\test_query_select_field.py ............. [ 55%]
tests\test_select_field.py .......... [ 59%]
tests\test_synonym.py .. [ 60%]
tests\test_types.py ............................................... [ 79%]
tests\test_unique_validator.py ....................... [ 88%]
tests\test_utils.py . [ 88%]
tests\test_validators.py ...................F [ 96%]
tests\test_weekdays_field.py .. [ 97%]
tests\test_widgets.py ...... [100%]
====================================================== FAILURES =======================================================
___________________________ TestModelFormConfiguration.test_supports_custom_datetime_format ___________________________
self = <tests.test_configuration.TestModelFormConfiguration object at 0x00000296FAA57990>
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'
E AssertionError: assert ['%Y-%m-%dT%H:%M:%S'] == '%Y-%m-%dT%H:%M:%S'
E + where ['%Y-%m-%dT%H:%M:%S'] = <wtforms_components.fields.html5.DateTimeField object at 0x00000296FAEE6250>.format
E + where <wtforms_components.fields.html5.DateTimeField object at 0x00000296FAEE6250> = <tests.test_configuration.TestModelFormConfiguration.test_supports_custom_datetime_format.<locals>.ModelTestForm object at 0x00000296FADF7250>.test_column
tests\test_configuration.py:154: AssertionError
___________________________________ TestAutoAssignedValidators.test_enum_validators ___________________________________
self = <tests.test_validators.TestAutoAssignedValidators object at 0x00000296FAED3790>
def test_enum_validators(self):
class TestEnum(enum.Enum):
A = 'a'
B = 'b'
self.init(type_=sa.Enum(TestEnum), nullable=True)
form = self.form_class()
> assert len(form.test_column.validators) == 1
E assert 2 == 1
E + where 2 = len([<wtforms.validators.Optional object at 0x00000296FBA101D0>, <wtforms.validators.Length object at 0x00000296FBB40C10>])
E + where [<wtforms.validators.Optional object at 0x00000296FBA101D0>, <wtforms.validators.Length object at 0x00000296FBB40C10>] = <wtforms_components.fields.select.SelectField object at 0x00000296FBB40ED0>.validators
E + where <wtforms_components.fields.select.SelectField object at 0x00000296FBB40ED0> = <tests.ModelFormTestCase.init_form.<locals>.ModelTestForm object at 0x00000296FBB39610>.test_column
tests\test_validators.py:278: AssertionError
================================================== warnings summary ===================================================
tests\test_unique_validator.py:12
C:\Users\Vidminas\GitHub\wtforms-alchemy\tests\test_unique_validator.py:12: MovedIn20Warning: Deprecated API features
detected! These feature(s) are not compatible with SQLAlchemy 2.0. To prevent incompatible upgrades prior to updating applications, ensure requirements files are pinned to "sqlalchemy<2.0". Set environment variable SQLALCHEMY_WARN_20=1 to show all deprecation warnings. Set environment variable SQLALCHEMY_SILENCE_UBER_WARNING=1 to silence this message. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
base = declarative_base()
tests/test_model_form_factory.py::TestModelFormFactory::test_class_meta_wtforms2
tests/test_model_form_factory.py::TestModelFormFactory::test_class_meta_wtforms2
C:\Users\Vidminas\GitHub\wtforms-alchemy\tests\test_model_form_factory.py:94: DeprecationWarning: distutils Version classes are deprecated. Use packaging.version instead.
if LooseVersion(wtforms.__version__) < LooseVersion('2'):
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=============================================== short test summary info ===============================================
FAILED tests/test_configuration.py::TestModelFormConfiguration::test_supports_custom_datetime_format - AssertionError...FAILED tests/test_validators.py::TestAutoAssignedValidators::test_enum_validators - assert 2 == 1
====================================== 2 failed, 248 passed, 3 warnings in 2.35s ======================================
sqlalchemy14: exit 1 (4.38 seconds) C:\Users\Vidminas\GitHub\wtforms-alchemy> py.test pid=21920
.pkg: _exit> python C:\Users\Vidminas\.conda\envs\wtforms-alchemy\Lib\site-packages\pyproject_api\_backend.py True setuptools.build_meta __legacy__
sqlalchemy14: FAIL code 1 (102.83=setup[58.31]+cmd[40.14,4.38] seconds)
evaluation failed :( (103.08 seconds)
And after the change:
================================================= test session starts =================================================
platform win32 -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0
cachedir: .tox\sqlalchemy14\.pytest_cache
rootdir: C:\Users\Vidminas\GitHub\wtforms-alchemy
plugins: cov-4.1.0
collected 250 items
tests\test_class_map.py ................. [ 6%]
tests\test_column_aliases.py ..... [ 8%]
tests\test_configuration.py ...........F...... [ 16%]
tests\test_country_field.py .. [ 16%]
tests\test_custom_fields.py . [ 17%]
tests\test_deep_form_relations.py .. [ 18%]
tests\test_descriptions.py .. [ 18%]
tests\test_field_exclusion.py .... [ 20%]
tests\test_field_order.py . [ 20%]
tests\test_field_parameters.py ............. [ 26%]
tests\test_field_trimming.py .. [ 26%]
tests\test_form_meta.py ........ [ 30%]
tests\test_hybrid_properties.py .. [ 30%]
tests\test_i18n_extension.py ... [ 32%]
tests\test_inheritance.py ..... [ 34%]
tests\test_labels.py .. [ 34%]
tests\test_model_field_list.py ........ [ 38%]
tests\test_model_form_factory.py ............ [ 42%]
tests\test_model_form_field.py ... [ 44%]
tests\test_phone_number.py .... [ 45%]
tests\test_phone_number_field.py ............ [ 50%]
tests\test_query_select_field.py ............. [ 55%]
tests\test_select_field.py .......... [ 59%]
tests\test_synonym.py .. [ 60%]
tests\test_types.py ............................................... [ 79%]
tests\test_unique_validator.py ....................... [ 88%]
tests\test_utils.py . [ 88%]
tests\test_validators.py .................... [ 96%]
tests\test_weekdays_field.py .. [ 97%]
tests\test_widgets.py ...... [100%]
====================================================== FAILURES =======================================================
___________________________ TestModelFormConfiguration.test_supports_custom_datetime_format ___________________________
self = <tests.test_configuration.TestModelFormConfiguration object at 0x0000024B3F9B3C50>
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'
E AssertionError: assert ['%Y-%m-%dT%H:%M:%S'] == '%Y-%m-%dT%H:%M:%S'
E + where ['%Y-%m-%dT%H:%M:%S'] = <wtforms_components.fields.html5.DateTimeField object at 0x0000024B3FE21490>.format
E + where <wtforms_components.fields.html5.DateTimeField object at 0x0000024B3FE21490> = <tests.test_configuration.TestModelFormConfiguration.test_supports_custom_datetime_format.<locals>.ModelTestForm object at 0x0000024B3F94F390>.test_column
tests\test_configuration.py:154: AssertionError
================================================== warnings summary ===================================================
tests\test_unique_validator.py:12
C:\Users\Vidminas\GitHub\wtforms-alchemy\tests\test_unique_validator.py:12: MovedIn20Warning: Deprecated API features
detected! These feature(s) are not compatible with SQLAlchemy 2.0. To prevent incompatible upgrades prior to updating applications, ensure requirements files are pinned to "sqlalchemy<2.0". Set environment variable SQLALCHEMY_WARN_20=1 to show all deprecation warnings. Set environment variable SQLALCHEMY_SILENCE_UBER_WARNING=1 to silence this message. (Background on SQLAlchemy 2.0 at: https://sqlalche.me/e/b8d9)
base = declarative_base()
tests/test_model_form_factory.py::TestModelFormFactory::test_class_meta_wtforms2
tests/test_model_form_factory.py::TestModelFormFactory::test_class_meta_wtforms2
C:\Users\Vidminas\GitHub\wtforms-alchemy\tests\test_model_form_factory.py:94: DeprecationWarning: distutils Version classes are deprecated. Use packaging.version instead.
if LooseVersion(wtforms.__version__) < LooseVersion('2'):
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
=============================================== short test summary info ===============================================
FAILED tests/test_configuration.py::TestModelFormConfiguration::test_supports_custom_datetime_format - AssertionError...====================================== 1 failed, 249 passed, 3 warnings in 2.73s ======================================
sqlalchemy14: exit 1 (4.81 seconds) C:\Users\Vidminas\GitHub\wtforms-alchemy> py.test pid=8636
.pkg: _exit> python C:\Users\Vidminas\.conda\envs\wtforms-alchemy\Lib\site-packages\pyproject_api\_backend.py True setuptools.build_meta __legacy__
sqlalchemy14: FAIL code 1 (96.98=setup[49.86]+cmd[42.31,4.81] seconds)
evaluation failed :( (97.23 seconds)
With my above workarounds, rendering and validation no longer crash, but validation still fails for enum fields with "Not a valid choice" because the built-in wtforms_components SelectField does not like the trick of reversing values and labels:
From wtforms_components/fields/select.py
, line 54:
@property
def choice_values(self):
values = []
for value, label in self.concrete_choices:
if isinstance(label, (list, tuple)):
for subvalue, sublabel in label:
values.append(subvalue)
else:
values.append(value)
return values
def pre_validate(self, form):
"""
Don't forget to validate also values from embedded lists.
"""
values = self.choice_values
if (self.data is None and u'' in values) or self.data in values:
return True
raise ValidationError(self.gettext(u'Not a valid choice'))
The exception gets raised, because (with my above example enum column) choices are [('Powder', <MaterialType.POWDER: 'Powder'>), ('Sand', <MaterialType.SAND: 'Sand'>), ('Gravel', <MaterialType.GRAVEL: 'Gravel'>), ('Boulder', <MaterialType.BOULDER: 'Boulder'>)]
but choice_values are ['Powder', 'Sand', 'Gravel', 'Boulder']
, whereas data is an instance of the enum like <MaterialType.POWDER: 'Powder'>
I added a SelectField extension that overrides the choice_values creation to use enum instances instead of string values.
In https://github.com/kvesteri/wtforms-alchemy/commit/bfc25f896725b41a2ca2f12c64501f72fad21a4b, support for Enums backing ChoiceType was added. But this doesn't cover native Enums directly backing the SQLAlchemy Enum column type (supported since SQLAlchemy v1.1).
For example, with an enum and an SQLAlchemy class that contains it like this:
My app crashes with a
ValueError: 'POWDER' is not a valid MaterialType
when attempting to render the form, as the default selected option contains an invalid value.I found several similar (but not the same) issues reported elsewhere:
I've added an additional case to handle this in WTForms-Alchemy > generator.py > select_field_kwargs.
Before the change:
After the change:
(There is also an irrelevant unit test failing in both cases, which I've reported in https://github.com/kvesteri/wtforms-alchemy/issues/163)