Skip to main content

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 of choices= becomes wtforms.SelectChoice(...).
  • wtforms-datetime-local-field - DateTimeField (which renders the obsolete <input type="datetime">) becomes DateTimeLocalField.

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 into SelectChoice objects, so tuple-unpacking breaks. See Templates that iterate form.X.choices.
  • Enum-backed selects. Nothing here needs migrating, but if you want the 3.3 enum_choices / enum_coerce helpers you convert by hand. See Enum-backed choices.
  • Edge cases the rules skip by design:
    • Already-converted SelectChoice(...) calls and DateTimeLocalField call 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(...) after from wtforms import DateTimeField
    • fields.DateTimeField(...) without the wtforms. prefix (avoids clobbering unrelated fields modules)

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:

  1. Run your formatter (black, ruff format). ast-grep sometimes collapses multi-line tuple literals onto a single line.
  2. Run your test suite. Pay extra attention to tests asserting on rendered HTML, since <input type="datetime"> becomes <input type="datetime-local">.
  3. 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

License

Public domain / CC0. Copy it, fork it, do what you want. Don't blame me if it eats your code.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

wtforms_3_3_migrator-0.1.0.tar.gz (6.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

wtforms_3_3_migrator-0.1.0-py3-none-any.whl (9.9 kB view details)

Uploaded Python 3

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

Hashes for wtforms_3_3_migrator-0.1.0.tar.gz
Algorithm Hash digest
SHA256 b8aa830ee2e6d93060abef25361d8a863f6bc08cacc45c8f65661e500cdfede2
MD5 1d18bc5b657ce23532342b613a686e52
BLAKE2b-256 d444e0eb78e8bd8644caa0a92833dae448d96b89f8e3786ab097338fb1c31aa6

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file wtforms_3_3_migrator-0.1.0-py3-none-any.whl.

File metadata

File hashes

Hashes for wtforms_3_3_migrator-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cdd12cdcd60c88784e7c251ea0fb002ef993c91cec27b5f5e9029092e0cba48e
MD5 d555a8cf1d46308a8a326b853645f761
BLAKE2b-256 9facba966fab5e2e5402d826ade67bdfa4c0138b94bd554d366fdaa490c48d34

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page