ast-grep rules + CLI to clear WTForms 3.3 deprecations in Python source
Project description
wtforms-3.3-migrator
ast-grep rules that rewrite Python source to clear deprecations introduced in WTForms 3.3.x
The pack is deliberately narrow: two syntactic rewrite rules, plus a handful of things it leaves for you to do by hand. Skim "What it does and doesn't do" below before you run it, so the gaps don't surprise you.
What it does and doesn't do
Rewrites automatically:
wtforms-select-choice- the legacy(value, label)tuple form ofchoices=becomeswtforms.SelectChoice(...).wtforms-datetime-local-field-DateTimeField(which renders the obsolete<input type="datetime">) becomesDateTimeLocalField.
Leaves for you to do by hand:
- Jinja templates (and any code) that unpack
form.X.choices. The rewrite turns choice entries from 2-tuples intoSelectChoiceobjects, so tuple-unpacking breaks. See Templates that iterateform.X.choices. - Enum-backed selects. Nothing here needs migrating, but if you want the 3.3
enum_choices/enum_coercehelpers you convert by hand. See Enum-backed choices. - Edge cases the rules skip by design:
- Already-converted
SelectChoice(...)calls andDateTimeLocalFieldcall sites - Tuples passed via a variable (
choices=my_choices) - the literal isn't visible - Tuples in unrelated keyword arguments (
headers=[("X-Foo", "bar")]) - 3-tuples in
choices=(e.g.(value, label, render_kw)) - adjust if needed - Bare
DateTimeField(...)afterfrom wtforms import DateTimeField fields.DateTimeField(...)without thewtforms.prefix (avoids clobbering unrelatedfieldsmodules)
- Already-converted
Rules
wtforms-select-choice
Rewrites the legacy (value, label) tuple form of choices= to the new
wtforms.SelectChoice helper.
-wtforms.SelectField(choices=[("a", "A"), ("b", "B")])
+wtforms.SelectField(choices=[wtforms.SelectChoice("a", "A"), wtforms.SelectChoice("b", "B")])
Handles list literals, list comprehensions, assignments to .choices
(field.choices = [...] / field.choices += [...]),
and comparison assertions in tests (assert form.field.choices == [...]).
wtforms-datetime-local-field
Renames DateTimeField (renders the obsolete <input type="datetime">)
toDateTimeLocalField.
-wtforms.fields.DateTimeField(render_kw={"readonly": True})
+wtforms.fields.DateTimeLocalField(render_kw={"readonly": True})
Matches wtforms.DateTimeField and wtforms.fields.DateTimeField.
A bare DateTimeField(...) from from wtforms import DateTimeField
is intentionally not rewritten -
that case is rare and the import line needs its own edit anyway,
so do those by hand.
Install
pip install wtforms-3.3-migrator
That pulls in ast-grep (via the ast-grep-cli wheel)
and gives you the wtforms-3.3-migrator command.
Requires Python 3.10+ to run; it migrates source for any Python 3 target.
Usage
Run it from your project root.
It previews by default and rewrites in place with --update-all:
# Preview the changes
wtforms-3.3-migrator .
# Apply the rewrites in place
wtforms-3.3-migrator --update-all .
# Scan just part of the tree (faster, smaller diff)
wtforms-3.3-migrator --update-all src/ tests/
# Run a single rule
wtforms-3.3-migrator --rule wtforms-datetime-local-field --update-all .
The command always scopes the scan to this pack's rules,
so it won't touch unrelated # ast-grep-ignore directives elsewhere in your code.
From a git checkout (without installing)
To hack on the rules or pin to a specific revision, skip the package and point
ast-grep at the pack's sgconfig.yml yourself.
You'll need ast-grep 0.30+ on its own (tested on 0.42):
brew install ast-grep # macOS
cargo install ast-grep # any platform
pipx install ast-grep-cli # via pip
git clone https://github.com/miketheman/wtforms-3.3-migrator.git ~/code/wtforms-3.3-migrator
[!IMPORTANT] Running ast-grep directly, always pass
--filter '^wtforms-'(the installed command does this for you). Skip it and ast-grep treats any# ast-grep-ignore: <other-rule>comment in your code as an unused suppression (because this config doesn't know about your other rules). With--update-all, it then deletes the directive - and any explanatory comment sharing the same line. The filter keeps the scan scoped to this pack's rules.
cd ~/code/my-project
# Preview, then apply
ast-grep --config ~/code/wtforms-3.3-migrator/sgconfig.yml scan \
--filter '^wtforms-' .
ast-grep --config ~/code/wtforms-3.3-migrator/sgconfig.yml scan \
--filter '^wtforms-' --update-all src/ tests/
# A single rule
ast-grep --config ~/code/wtforms-3.3-migrator/sgconfig.yml scan \
--filter '^wtforms-datetime-local-field$' --update-all .
A shell alias
Running this against more than one repo? An alias saves typing:
alias wtforms-migrate='ast-grep --config ~/code/wtforms-3.3-migrator/sgconfig.yml scan --filter "^wtforms-"'
# then, from any project:
wtforms-migrate . # preview
wtforms-migrate --update-all . # apply
Reviewing the diff
These are syntactic rewrites. Before applying, commit or stash anything in flight - you'll want a clean slate to diff against. Then:
- Run your formatter (
black,ruff format). ast-grep sometimes collapses multi-line tuple literals onto a single line. - Run your test suite. Pay extra attention to tests asserting on
rendered HTML, since
<input type="datetime">becomes<input type="datetime-local">. - Spot-check
choices=declarations that pull from a variable. The rule can't see inside a variable, so those aren't touched.
git checkout -- <file> reverts a single file; git reset --hard
throws the whole rewrite away.
Templates that iterate form.X.choices
The wtforms-select-choice rewrite changes each entry of a choices list
from a 2-tuple (value, label) into a 4-field SelectChoice NamedTuple
(value, label, selected, render_kw). Anywhere downstream code
tuple-unpacks a choice into two names, it will break:
{# Before: works on tuples #}
{% for value, label in form.role_name.choices %}
{{ value }} - {{ label }}
{% endfor %}
{# After: use attribute access #}
{% for choice in form.role_name.choices %}
{{ choice.value }} - {{ choice.label }}
{% endfor %}
ast-grep does not (yet) scan Jinja templates - check for this pattern yourself with a quick grep:
grep -rn 'for.*,.*in.*\.choices' templates/
The same applies to any Python code that unpacks form.X.choices (rare,
but worth checking).
Enum-backed choices
There is no rule for Enum-backed selects, on purpose, but here is the lay of the land if you have them.
Nothing here needs migrating from 3.2: a bare coerce=SomeEnumClass does a
plain value lookup (Enum(v)) in 3.3, the same as 3.2, so it keeps working
unchanged.
3.3 does add helpers for declaring enum-backed selects, and they read better than hand-rolling the choice list. For new code, prefer them:
from wtforms.fields import enum_choices, enum_coerce
color = SelectField(
choices=enum_choices(Color),
coerce=enum_coerce(Color),
)
enum_choices(E) builds the option list; by default the HTML value is
member.value and the label is str(member) (so member.value for a
StrEnum, otherwise member.name). enum_coerce(E) resolves the submitted
string back to a member - by default the same value lookup a bare coerce=E
already does, so the two are interchangeable for value-keyed forms. Pass
by="name" to both helpers to key on member.name instead, and
label=<callable> to customize the displayed text.
Converting an existing hand-rolled list is a manual call, not a mechanical rewrite, because the right replacement depends on what the old code put in the label and on the wire:
# Safe: label is the value. enum_choices(E) reproduces it for a StrEnum.
choices=[wtforms.SelectChoice(role.value, role.value) for role in Role]
# -> choices=enum_choices(Role)
# NOT a drop-in: label is member.name, not the value. Bare enum_choices(E)
# would relabel the options to their values. Keep the labels explicitly:
choices=[wtforms.SelectChoice(i.value, i.name) for i in IssuerType]
# -> choices=enum_choices(IssuerType, label=lambda m: m.name)
A syntactic tool can't tell those two apart (it depends on each Enum's value vs
name and its __str__), which is why this is documented here rather than
auto-fixed.
Verifying the pack itself
If you've edited the rules locally, or just upgraded ast-grep and want to
make sure nothing broke, cd into the pack and run its tests:
cd ~/code/wtforms-3.3-migrator
ast-grep test
All rules should pass. Regular users don't need to bother with this.
Authors
- Mike Fiedler (@miketheman)
License
Public domain / CC0. Copy it, fork it, do what you want. Don't blame me if it eats your code.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file wtforms_3_3_migrator-0.1.0.tar.gz.
File metadata
- Download URL: wtforms_3_3_migrator-0.1.0.tar.gz
- Upload date:
- Size: 6.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8aa830ee2e6d93060abef25361d8a863f6bc08cacc45c8f65661e500cdfede2
|
|
| MD5 |
1d18bc5b657ce23532342b613a686e52
|
|
| BLAKE2b-256 |
d444e0eb78e8bd8644caa0a92833dae448d96b89f8e3786ab097338fb1c31aa6
|
Provenance
The following attestation bundles were made for wtforms_3_3_migrator-0.1.0.tar.gz:
Publisher:
release.yml on miketheman/wtforms-3.3-migrator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wtforms_3_3_migrator-0.1.0.tar.gz -
Subject digest:
b8aa830ee2e6d93060abef25361d8a863f6bc08cacc45c8f65661e500cdfede2 - Sigstore transparency entry: 1839337160
- Sigstore integration time:
-
Permalink:
miketheman/wtforms-3.3-migrator@9294e7473b79b31473bdb9de90f7ec4fc1a769c8 -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/miketheman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9294e7473b79b31473bdb9de90f7ec4fc1a769c8 -
Trigger Event:
release
-
Statement type:
File details
Details for the file wtforms_3_3_migrator-0.1.0-py3-none-any.whl.
File metadata
- Download URL: wtforms_3_3_migrator-0.1.0-py3-none-any.whl
- Upload date:
- Size: 9.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.13
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cdd12cdcd60c88784e7c251ea0fb002ef993c91cec27b5f5e9029092e0cba48e
|
|
| MD5 |
d555a8cf1d46308a8a326b853645f761
|
|
| BLAKE2b-256 |
9facba966fab5e2e5402d826ade67bdfa4c0138b94bd554d366fdaa490c48d34
|
Provenance
The following attestation bundles were made for wtforms_3_3_migrator-0.1.0-py3-none-any.whl:
Publisher:
release.yml on miketheman/wtforms-3.3-migrator
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
wtforms_3_3_migrator-0.1.0-py3-none-any.whl -
Subject digest:
cdd12cdcd60c88784e7c251ea0fb002ef993c91cec27b5f5e9029092e0cba48e - Sigstore transparency entry: 1839337420
- Sigstore integration time:
-
Permalink:
miketheman/wtforms-3.3-migrator@9294e7473b79b31473bdb9de90f7ec4fc1a769c8 -
Branch / Tag:
refs/tags/0.1.0 - Owner: https://github.com/miketheman
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@9294e7473b79b31473bdb9de90f7ec4fc1a769c8 -
Trigger Event:
release
-
Statement type: