|
| 1 | +# CC Brief: unpythonic Modernization — Phase 1 (Audit) |
| 2 | + |
| 3 | +## Context |
| 4 | + |
| 5 | +unpythonic is being updated from Python 3.8–3.12 to 3.10–3.14. This follows the mcpyrate 4.0.0 update — unpythonic is mcpyrate's primary downstream consumer. Version will be 2.0.0 (floor bump + mcpyrate 4.0.0 dependency is breaking). |
| 6 | + |
| 7 | +unpythonic has three tiers: pure Python layer (`unpythonic/`), macro layer (`unpythonic/syntax/`), and dialect layer (`unpythonic/dialects/`). The macro and dialect layers depend on mcpyrate. The pure Python layer has no mcpyrate dependency at runtime. |
| 8 | + |
| 9 | +No code changes in this phase, only a report. |
| 10 | + |
| 11 | +## Reference |
| 12 | + |
| 13 | +- unpythonic CLAUDE.md (in repo root) — architecture, conventions. |
| 14 | +- unpythonic issue #93: consolidated AST change notes (covers mcpyrate, unpythonic, Pyan3). |
| 15 | +- mcpyrate 4.0.0 changelog: removed `getconstant()`, `Num`, `Str`, `Bytes`, `NameConstant`, `Ellipsis`, `Index`, `ExtSlice` from `astcompat` public API. |
| 16 | +- mcpyrate 4.0.0 source tree: `~/Documents/koodit/mcpyrate/` — consult when you need to check what `astcompat` exports, how the unparser handles new nodes, or any other mcpyrate 4.0.0 API details. |
| 17 | + |
| 18 | +## What to audit |
| 19 | + |
| 20 | +### 1. Imports from mcpyrate.astcompat (mcpyrate 4.0.0 breakage) |
| 21 | + |
| 22 | +mcpyrate 4.0.0 removed these from `astcompat`: `getconstant`, `Num`, `Str`, `Bytes`, `NameConstant`, `Ellipsis`, `Index`, `ExtSlice`. |
| 23 | + |
| 24 | +**Find all imports from `mcpyrate.astcompat`** and flag any that reference removed names. |
| 25 | + |
| 26 | +**Known instances** (verify — there may be more): |
| 27 | +- `syntax/lambdatools.py`: imports `getconstant`, `Str`, `NamedExpr` |
| 28 | +- `syntax/letdoutil.py`: imports `getconstant`, `Str`, `NamedExpr` |
| 29 | +- `syntax/tailtools.py`: imports `getconstant`, `NameConstant`, `TryStar` |
| 30 | +- `syntax/autoref.py`: imports `getconstant` |
| 31 | +- `syntax/util.py`: imports `getconstant` |
| 32 | +- `syntax/autocurry.py`: imports `TypeAlias` (still valid) |
| 33 | +- `syntax/lazify.py`: imports `TypeAlias` (still valid) |
| 34 | +- `syntax/scopeanalyzer.py`: imports `TryStar`, `MatchStar`, `MatchMapping`, `MatchClass`, `MatchAs` (still valid) |
| 35 | +- `syntax/tests/test_letdoutil.py`: imports `getconstant`, `Num` |
| 36 | +- `syntax/tests/test_util.py`: imports `getconstant`, `Num`, `Str` |
| 37 | + |
| 38 | +For each removed import, find all usage sites in that file. Most will be type checks like `type(k) in (Constant, Str)` that collapse to `type(k) is Constant`, and `getconstant(node)` calls that become `node.value`. |
| 39 | + |
| 40 | +### 2. `hasattr` checks on AST node fields (3.13) |
| 41 | + |
| 42 | +In Python 3.13, omitted optional fields on AST nodes are set to `None` instead of being absent. Code that uses `hasattr(node, "field")` to detect absence will now always return `True`, breaking guards that relied on absence to detect "not set". |
| 43 | + |
| 44 | +**Known instances** (scan for more): |
| 45 | + |
| 46 | +**Dialect files:** |
| 47 | +- `dialects/listhell.py` (~line 29): `if hasattr(self, "lineno"):` |
| 48 | +- `dialects/lispython.py` (~lines 45, 82): `if hasattr(self, "lineno"):` |
| 49 | +- `dialects/pytkell.py` (~line 45): `if hasattr(self, "lineno"):` |
| 50 | + |
| 51 | +Note: these check `hasattr(self, "lineno")` on dialect classes, not directly on AST nodes. The comment says "mcpyrate 3.6.0+". Check whether `self` here is an AST node or a dialect instance — if it's a dialect instance, the 3.13 AST field change doesn't apply. |
| 52 | + |
| 53 | +**Macro layer:** |
| 54 | +- `syntax/testingtools.py` (~lines 803, 904, 941, 1013): `hasattr(tree, "lineno")` / `hasattr(first_stmt, "lineno")` |
| 55 | +- `syntax/dbg.py` (~lines 229, 240): `hasattr(tree, "lineno")` |
| 56 | +- `syntax/lambdatools.py` (~line 403): `if hasattr(tree, "lineno"):` |
| 57 | +- `syntax/lambdatools.py` (~line 539): `tree.ctx if hasattr(tree, "ctx") else None` |
| 58 | +- `syntax/scopeanalyzer.py` (~lines 389, 411): `hasattr(tree, "ctx") and type(tree.ctx) is Store/Del` |
| 59 | +- `syntax/letdoutil.py` (~line 763): `hasattr(oldb, "lineno") and hasattr(oldb, "col_offset")` |
| 60 | +- `syntax/letdo.py` (~line 478): `hasattr(tree, "ctx")` |
| 61 | + |
| 62 | +**Tests:** |
| 63 | +- `syntax/tests/test_conts_multishot.py` (~line 68): `hasattr(tree, "ctx")` |
| 64 | + |
| 65 | +For `ctx` checks: these are likely checking whether a macro-generated node has had `ctx` set. In 3.13, `ctx` defaults to `Load()` on omission, so `hasattr` will always be `True` — but the node *does* have a meaningful `ctx` now (`Load()`). Determine whether this is a behavior change or harmless. |
| 66 | + |
| 67 | +### 3. `sys.version_info` guards (floor bump cleanup) |
| 68 | + |
| 69 | +With floor at 3.10, all `>= (3, 8)` and `>= (3, 9)` checks are always true. Find all and list. |
| 70 | + |
| 71 | +**Known instances** (~14+ sites, mostly `ast.Index` wrapper removal): |
| 72 | +- `syntax/testingtools.py` (~line 883): `>= (3, 9, 0)` — `ast.Index` wrapper |
| 73 | +- `syntax/letsyntax.py` (~line 372): `>= (3, 9, 0)` — `ast.Index` wrapper |
| 74 | +- `syntax/prefix.py` (~line 197): `>= (3, 9, 0)` — `ast.Index` wrapper |
| 75 | +- `syntax/letdoutil.py` (~lines 25, 30): `>= (3, 9, 0)` — `ast.Index` wrapper |
| 76 | +- `syntax/nameutil.py` (~line 127): `>= (3, 9, 0)` — `ast.Index` wrapper |
| 77 | +- `syntax/letdo.py` (~line 595): `>= (3, 8, 0)` — positional-only args |
| 78 | +- `syntax/tailtools.py` (~line 1118): `>= (3, 8, 0)` — positional-only args |
| 79 | +- `syntax/tests/test_conts_multishot.py` (~line 194): `>= (3, 9, 0)` — `ast.Index` |
| 80 | +- `syntax/tests/test_letdoutil.py` (~lines 611, 624, 631, 650, 663, 670): `>= (3, 9, 0)` — `ast.Index` |
| 81 | +- `typecheck.py` (~line 187): `>= (3, 10, 0)` — `types.UnionType`. Always true with floor at 3.10. |
| 82 | +- `tests/test_fun.py` (~line 259): `< (3, 11, 0)` — check what this guards |
| 83 | + |
| 84 | +### 4. Direct references to deprecated/removed AST node types |
| 85 | + |
| 86 | +Outside of `mcpyrate.astcompat` imports, check for any direct `ast.Num`, `ast.Str`, etc. references. |
| 87 | + |
| 88 | +**Known instance:** |
| 89 | +- `syntax/letdoutil.py` (~line 732): error message mentions `ast.Str` — just a string literal, but should be updated for accuracy. |
| 90 | + |
| 91 | +### 5. `autoreturn` and `match`/`case` (feature gap from issue #93) |
| 92 | + |
| 93 | +`autoreturn` in `syntax/tailtools.py` doesn't handle `match`/`case` statements. This is a known gap — it's a feature addition, not strictly a compat fix, but it's the most significant modernization issue identified in issue #93. |
| 94 | + |
| 95 | +**Scope the work:** check how `autoreturn` handles other compound statements (`if`/`elif`/`else`, `try`/`except`, `with`). The `match`/`case` handler should follow the same pattern — autoreturn the last expression in each `case` body. |
| 96 | + |
| 97 | +**Decision:** Include in 2.0.0. The 3.10 floor means `match`/`case` is always available, and the version bump is happening anyway. This is self-contained relative to the rest of the `autoreturn` machinery — the scary parts of unpythonic (TCO, lazify, autocurry, continuations) are not involved. |
| 98 | + |
| 99 | +### 6. AST constructor calls (3.13 strictness) |
| 100 | + |
| 101 | +In 3.13, omitting required fields or passing unknown kwargs on `ast.*` node constructors emits `DeprecationWarning` (becomes an error in 3.15). Scan for AST node constructor calls that omit required fields or pass unknown kwargs. |
| 102 | + |
| 103 | +Focus on the macro layer (`syntax/*.py`) which constructs AST nodes extensively. The pure Python layer doesn't touch AST. |
| 104 | + |
| 105 | +**Exception — `ctx` fields**: Many AST node constructors intentionally omit `ctx` — mcpyrate's `astfixers.fix_ctx()` auto-injects the correct `ctx` after macro expansion. In 3.13, omitted `ctx` defaults to `Load()`, which is harmless since `astfixers` overwrites it. Don't flag missing `ctx`. |
| 106 | + |
| 107 | +### 7. Version metadata |
| 108 | + |
| 109 | +- `pyproject.toml`: `python_requires`, classifiers, mcpyrate dependency version |
| 110 | +- CI workflow: matrix versions, PyPy versions |
| 111 | +- `CLAUDE.md`: version range mentions |
| 112 | +- `README.md`: any version range mentions |
| 113 | +- `CHANGELOG.md`: will need a 2.0.0 entry (not part of audit, just note) |
| 114 | +- Module docstrings mentioning version ranges |
| 115 | + |
| 116 | +## Deliverable |
| 117 | + |
| 118 | +A report (markdown) listing all sites that need attention, grouped by file. For each site, note: |
| 119 | +- File and line number |
| 120 | +- What the issue is |
| 121 | +- Category: mcpyrate 4.0.0 breakage / floor bump cleanup / 3.13 compat / 3.14 compat / feature gap |
| 122 | +- Severity (will break / will warn / cleanup only) |
| 123 | + |
| 124 | +No code changes. |
0 commit comments