Metadata-Version: 2.4
Name: wtforms-3.3-migrator
Version: 0.1.0
Summary: ast-grep rules + CLI to clear WTForms 3.3 deprecations in Python source
Keywords: wtforms,ast-grep,migration,codemod,deprecation
Author: Mike Fiedler
License-Expression: CC0-1.0
License-File: LICENSE
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Software Development :: Code Generators
Classifier: Topic :: Software Development :: Quality Assurance
Requires-Dist: ast-grep-cli>=0.30
Requires-Python: >=3.10
Project-URL: Homepage, https://github.com/miketheman/wtforms-3.3-migrator
Project-URL: Source, https://github.com/miketheman/wtforms-3.3-migrator
Description-Content-Type: text/markdown

# 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`](#templates-that-iterate-formxchoices).
- 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](#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.

```diff
-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">`)
to`DateTimeLocalField`.

```diff
-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

```bash
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`:

```bash
# 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):

```bash
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.

```bash
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:

```bash
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:

```jinja
{# 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:

```bash
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:

```python
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:

```python
# 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:

```bash
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](https://github.com/miketheman))

## License

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

[ast-grep]: https://ast-grep.github.io/
