Skip to content

Commit

Permalink
Deprecate set_queries name (#459)
Browse files Browse the repository at this point in the history
The `set_<fieldname>` pattern is now used for field setters, so `set_queries` needs to be renamed. New name`__post_init__` is borrowed from dataclasses.
  • Loading branch information
jace authored Dec 26, 2023
1 parent c110de5 commit f77df98
Show file tree
Hide file tree
Showing 2 changed files with 87 additions and 41 deletions.
27 changes: 14 additions & 13 deletions src/baseframe/forms/form.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import typing as t
import typing_extensions as te
import uuid
import warnings
from threading import Lock

import wtforms
Expand Down Expand Up @@ -148,6 +149,12 @@ def __init_subclass__(cls, **kwargs: t.Any) -> None:
raise TypeError(
"This form has __expects__ parameters that clash with field names"
)
if 'set_queries' in cls.__dict__ and 'queries' not in cls.__dict__:
warnings.warn(
f"`{cls.__qualname__}.set_queries` is deprecated due to conflict with"
" `set_<fieldname>` methods. Rename it to `__post_init__`",
stacklevel=2,
)

def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
for attr in self.__expects__:
Expand Down Expand Up @@ -178,7 +185,10 @@ def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
super().__init__(*args, **kwargs)

# Finally, populate the ``choices`` attr of selection fields
self.set_queries()
if callable(post_init := getattr(self, '__post_init__', None)):
post_init() # pylint: disable=not-callable
elif callable(post_init := getattr(self, 'set_queries', None)):
post_init() # pylint: disable=not-callable

def __json__(self) -> t.List[t.Any]:
"""Render this form as JSON."""
Expand Down Expand Up @@ -214,7 +224,7 @@ def process(
"""
Take form, object data, and keyword arg input and have the fields process them.
:param formdata: Used to pass data coming from the enduser, usually
:param formdata: Used to pass data coming from the client, usually
`request.POST` or equivalent.
:param obj: If `formdata` is empty or not provided, this object is checked for
attributes matching form field names, which will be used for field values.
Expand Down Expand Up @@ -301,15 +311,6 @@ def errors_with_data(self) -> dict:
if f.errors
}

def set_queries(self) -> None:
"""
Set queries/choices as may be required for fields.
This is an overridable method and is typically required on forms that use
QuerySelectField or QuerySelectMultipleField, or have select fields with choices
that are only available at runtime.
"""


class FormGenerator:
"""
Expand All @@ -335,7 +336,7 @@ def __init__(

self.default_field = default_field

# TODO: Make formstruct a TypedDict
# TODO: Make `formstruct` a TypedDict
def generate(self, formstruct: dict) -> t.Type[Form]:
"""Generate a dynamic form from the given data structure."""

Expand Down Expand Up @@ -383,7 +384,7 @@ class DynamicForm(Form):
itemparams[paramname] = item[paramname]
filters.append(filter_registry[itemname][0](**itemparams))

# TODO: Also validate the parameters in fielddata, like with validators
# TODO: Also validate the parameters in `fielddata`, like with validators
# above
setattr(
DynamicForm,
Expand Down
101 changes: 73 additions & 28 deletions tests/baseframe_tests/forms/form_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def test_no_obj() -> None:
"""Test that the form can be initialized without an object."""
form = GetSetForm(meta={'csrf': False})

# Confirm form is bkank
# Confirm form is blank
assert form.firstname.data is None
assert form.lastname.data is None
assert form.company.data is None
Expand All @@ -104,9 +104,9 @@ def test_get(user) -> None:
form = GetSetForm(obj=user, meta={'csrf': False})

# Confirm form loaded from user object
assert form.firstname.data == "Test"
assert form.lastname.data == "user"
assert form.company.data == "Test company"
assert form.firstname.data == 'Test'
assert form.lastname.data == 'user'
assert form.company.data == 'Test company'
assert form.password.data == ''
assert form.confirm_password.data == ''

Expand All @@ -117,23 +117,23 @@ def test_get_formdata(user) -> None:
form = GetSetForm(
formdata=MultiDict(
{
'firstname': "Ffirst",
'lastname': "Flast",
'company': "Form company",
'password': "Test123",
'confirm_password': "Mismatched",
'firstname': 'Ffirst',
'lastname': 'Flast',
'company': 'Form company',
'password': 'Test123',
'confirm_password': 'Mismatched',
}
),
obj=user,
meta={'csrf': False},
)

# Confirm form loaded from formdata instead of user object
assert form.firstname.data == "Ffirst"
assert form.lastname.data == "Flast"
assert form.company.data == "Form company"
assert form.password.data == "Test123"
assert form.confirm_password.data == "Mismatched"
# Confirm form loaded from `formdata` instead of user object
assert form.firstname.data == 'Ffirst'
assert form.lastname.data == 'Flast'
assert form.company.data == 'Form company'
assert form.password.data == 'Test123'
assert form.confirm_password.data == 'Mismatched'


@pytest.mark.usefixtures('ctx')
Expand All @@ -142,28 +142,28 @@ def test_set(user) -> None:
form = GetSetForm(
formdata=MultiDict(
{
'firstname': "Ffirst",
'lastname': "Flast",
'company': "Form company",
'password': "Test123",
'confirm_password': "Mismatched",
'firstname': 'Ffirst',
'lastname': 'Flast',
'company': 'Form company',
'password': 'Test123',
'confirm_password': 'Mismatched',
}
),
obj=user,
meta={'csrf': False},
)

# Check user object before and after populating
assert user.fullname == "Test user"
assert user.company == "Test company"
assert user.password_is("test")
assert user.fullname == 'Test user'
assert user.company == 'Test company'
assert user.password_is('test')

form.populate_obj(user)

assert user.fullname == "Ffirst Flast"
assert user.company == "Form company"
assert not user.password_is("test")
assert user.password_is("Test123")
assert user.fullname == 'Ffirst Flast'
assert user.company == 'Form company'
assert not user.password_is('test')
assert user.password_is('Test123')
assert not hasattr(user, 'confirm_password')


Expand Down Expand Up @@ -193,9 +193,54 @@ def test_render_field_options() -> None:
'attrfour': '',
}
render = render_field_options(form.string_field, **test_attrs)
# This expicit rendering is based on dictionary key order stability in Python 3.7+
# This explicit rendering is based on dictionary key order stability in Python 3.7+
assert render == (
'<input'
' attrfour="" attrone="test"'
' id="string_field" name="string_field" type="text" value="">'
)


@pytest.mark.usefixtures('ctx')
def test_post_init_gets_called() -> None:
class TestForm(forms.Form):
post_init_called: bool = False

def __post_init__(self) -> None:
self.post_init_called = True

form = TestForm(meta={'csrf': False})
assert form.post_init_called is True


@pytest.mark.usefixtures('ctx')
def test_set_queries_gets_called() -> None:
with pytest.warns(UserWarning):

class TestForm(forms.Form):
set_queries_called: bool = False

def set_queries(self) -> None:
self.set_queries_called = True

form = TestForm(meta={'csrf': False})
assert form.set_queries_called is True


@pytest.mark.usefixtures('ctx')
def test_only_post_init_called() -> None:
with pytest.warns(UserWarning):

class TestForm(forms.Form):
post_init_called: bool = False
set_queries_called: bool = False

def __post_init__(self) -> None:
self.post_init_called = True

def set_queries(self) -> None:
self.set_queries_called = True

form = TestForm(meta={'csrf': False})
assert form.post_init_called is True
assert form.set_queries_called is False

0 comments on commit f77df98

Please sign in to comment.