Jens-G opened a new pull request, #3660:
URL: https://github.com/apache/thrift/pull/3660
## Summary
**Depends on #3659 (THRIFT-6114)** -- this branch is based on `THRIFT-6114`,
not `master`, so the diff below includes THRIFT-6114's commit until that one
merges. Please review/merge #3659 first; this will show a clean 1-commit diff
once it's rebased onto `master` after that lands. See "Why the 6114 dependency"
below for why.
Fourth in the THRIFT-5927 follow-up chain (THRIFT-6113: regression test
wasn't running in CI; THRIFT-6114: service module filename and `-remote` script
weren't escaped).
`t_py_generator::type_name()` is the shared helper that renders any
struct/enum/exception/service reference as a Python expression (class
instantiation, type hints, extends declarations, deserialization, `except`
clauses, etc.). None of its three return paths escaped the identifier:
```cpp
string t_py_generator::type_name(t_type* ttype) {
...
if (ttype->is_service()) {
return get_real_py_module(...) + "." + ttype->get_name(); //
service extends / references
}
if (program != nullptr && program != program_) {
return get_real_py_module(...) + ".ttypes." + ttype->get_name(); //
cross-module (include) type references
}
return ttype->get_name(); //
same-module type references
}
```
This meant:
- A service `extends`ing another service whose name is a Python keyword
generated a broken `import module.<keyword>` statement
(`t_py_generator.cc:1298-1301`, a direct call that doesn't go through
`type_name()`) and a broken `class Client(module.<keyword>.Client):` (via
`type_name()`).
- A struct/enum/exception referenced across an `include` boundary whose name
is a keyword generated a broken `module.ttypes.<keyword>` reference wherever
it's used -- including the `except <Type> as <name>:` clauses generated for
service exceptions.
Same class of regression as THRIFT-6114 (5927 escaped identifiers in some
code paths, not others), just reached via `extends`/`include` instead of the
service module and `-remote` script.
**One added wrinkle:** for exception types specifically,
`True`/`False`/`None` are Python keyword *literals*, not statement keywords --
`except True as e:` parses without a `SyntaxError` (`True` is a valid
expression atom), so `py_compile`-based testing alone doesn't catch that
sub-case; it silently binds the wrong object and would raise a runtime
`TypeError` if ever hit. Same fix either way (escape to `True_`), but the test
needed a small addition to see it.
## Fix
- Escape the identifier at `type_name()`'s three return points.
- Escape the parent service name in the direct `import module.<service>`
extends statement (`t_py_generator.cc:1298-1301`).
- Extend `test_keyword_escape.py` with an AST-based check: fail if any
generated file has an `except` clause whose type expression is a bare
`True`/`False`/`None` constant (which `py_compile` alone can't see).
## Why the 6114 dependency
The service-extends-a-keyword-named-service scenario needs both fixes to
work end-to-end: 6114 fixes the *parent* service's own module filename
(`continue.py` -> `continue_.py`); this fix corrects the *child's* reference to
it. Neither alone is sufficient for that one scenario. The
struct/enum/exception cross-module and same-file cases in this PR are
independent of 6114 (those types live in `ttypes.py`, already correctly escaped
by the original THRIFT-5927) -- only the service-extends-service path is
coupled.
## Test fixtures
- Same-file: added `service AlsoDerived extends continue` to the existing
`Thrift5927.thrift`, exercising extends of a keyword-named parent in the same
file.
- Cross-module: added `lib/py/test/test_compiler/thrift5927include.thrift`,
modeled on `tutorial/shared.thrift` + `tutorial/tutorial.thrift`'s
`include`/`extends` pattern, providing a keyword-named struct (`except`) and
service (`class`). `Thrift5927.thrift` now `include`s it, adds `struct
UsesIncluded { 1: thrift5927include.except item }`, and `service Derived
extends thrift5927include.class`.
## Out of scope
Found while investigating, same missed-escaping category but each a
different specific variable bolted onto an otherwise-correct `type_name()` call
site (not part of `type_name()`'s own output, so not covered by this fix) --
flagging for a possible future consolidated pass:
- `t_py_generator.cc:647`, `render_const_value()`: the enum *value* name in
`EnumClass.VALUE` constant rendering is unescaped, inconsistent with that same
value being escaped everywhere else (e.g. as a class attribute).
- `t_py_generator.cc:925`: a trailing field-name use in a `__setattr__`
override's `__members__.get(...)` call is unescaped, inconsistent with two
escaped sibling uses of the same identifier two lines earlier in the same
statement.
- `t_py_generator.cc:2180`, `generate_service_client()`: the exception field
name (`xname`) is unescaped in one `except <Type> as xname:` binding and its
two subsequent uses, inconsistent with the same pattern at `:2246` and `:2322`
which do escape it.
## Test plan
- [x] Before the fix: extending the fixture (extends + include) reproduced
three distinct `SyntaxError`s via `py_compile` -- `import thrift5927.continue`,
`import thrift5927include.class`, and `thrift5927include.ttypes.except()`.
- [x] After the fix: `test_keyword_escape.py` passes (`OK: All 15 generated
Python files compile successfully`), including the new AST check.
- [x] Real import/cross-reference test (not just syntax):
`AlsoDerived.Client`/`Derived.Client` correctly inherit from the escaped parent
`Client` classes; `UsesIncluded`'s field type correctly resolves to the escaped
`except_` class from the included module.
- [x] Regression check: an ordinary (non-keyword) two-file extends+include
fixture generates byte-identical output to before.
- [x] Wider regression: `make -C lib/py check` (full suite, not just this
test) and `make -C test/py check`'s code-generation step both still pass -- 189
generated files across
`ThriftTest.thrift`/`DebugProtoTest.thrift`/`DoubleConstantsTest.thrift`/`Recursive.thrift`
in all 8 generation flavors
(default/slots/oldstyle/no_utf8strings/dynamic/dynamicslots/enum/type_hints)
still compile cleanly, since `type_name()` is used file-wide.
---
This PR includes AI-assisted changes (Claude Code); see commit trailer.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]