Jens Geyer created THRIFT-6115:
----------------------------------
Summary: Python service extends and cross-module (include) type
references also skip keyword escaping
Key: THRIFT-6115
URL: https://issues.apache.org/jira/browse/THRIFT-6115
Project: Thrift
Issue Type: Bug
Components: Python - Compiler
Reporter: Jens Geyer
Fourth follow-up in the THRIFT-5927 chain (see also 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 used to render any
struct/enum/exception/service reference as a Python expression (class
instantiation, type hints, extends declarations, deserialization, etc.). None
of its three return paths call maybe_escape_identifier():
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 means:
- A service that "extends" another service whose name is a Python keyword
generates a broken "import module.<keyword>" statement
(t_py_generator.cc:1298-1301, a direct call not going through type_name()) and
a broken "class Client(module.<keyword>.Client):" (via type_name(), used at
:1387-1388, 1432-1433, 1978-1979).
- A struct/enum/exception referenced across an "include" boundary whose name is
a keyword generates a broken "module.ttypes.<keyword>" reference wherever that
type is used (constant rendering, deserialization, type hints, and the "except
<Type> as <name>:" clauses used for service exceptions).
This is the same class of regression as THRIFT-6114 (5927 escaped identifiers
in some code paths but not others), just reached through extends/include
instead of the service module and -remote script.
One added wrinkle: for exception types specifically (generate_service_client's
"except <Type> as <name>:" clauses), 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 won't catch that particular sub-case; it silently binds the wrong object
and would raise a runtime TypeError instead. The fix (escaping to True_) is the
same either way; the test needs a light AST-based check (no ExceptHandler.type
is a bare keyword-literal Constant) in addition to compileability.
Fix (scoped to what type_name() and the one direct extends-import call control):
- Escape the identifier at type_name()'s three return points (service name,
cross-module type name, same-module type name).
- Escape the service name in the direct "import module.<service>" extends
statement (t_py_generator.cc:1298-1301).
Test coverage:
- Same-file extends of a keyword-named service (Thrift5927.thrift's existing
"continue" service).
- A new included file (thrift5927include.thrift, modeled on
tutorial/shared.thrift + tutorial/tutorial.thrift's include pattern) providing
a keyword-named struct and a keyword-named service, referenced and extended
from Thrift5927.thrift the same way tutorial.thrift does "include
shared.thrift" / "extends shared.SharedService".
- An AST-based check that no exception handler's type expression is a bare
True/False/None constant.
Depends on THRIFT-6114 (PR #3659) for the
service-extends-a-keyword-named-service scenario specifically: that scenario
needs both fixes to work end-to-end (6114 fixes the parent service's own module
filename; this fixes the child's reference to it). The struct/enum/exception
cross-module and same-file cases are independent of 6114 (those types live in
ttypes.py, already correctly escaped by the original THRIFT-5927). This PR is
branched from THRIFT-6114 rather than master to allow real end-to-end
verification of the combined scenario; see PR for details.
Out of scope, found while investigating but not part of type_name()'s own
output (separate unescaped identifiers bolted onto otherwise-correct
type_name() call sites, same missed-escaping category but a different specific
variable each time) -- flagging for a possible future consolidated pass rather
than fixing piecemeal:
- t_py_generator.cc:647, render_const_value(): the enum *value* name
(enum_val->get_name()) in "EnumClass.VALUE" constant rendering is unescaped,
inconsistent with the enum value being escaped everywhere else (e.g. as a class
attribute).
- t_py_generator.cc:925: a trailing (*m_iter)->get_name() in a __setattr__
override's __members__.get(...) call is unescaped, inconsistent with two
sibling escaped uses of the same identifier two lines earlier in the same
statement.
- t_py_generator.cc:2180, generate_service_client(): xname (an exception field
name) is unescaped in the "except <Type> as xname:" binding and its two
subsequent uses, inconsistent with the same pattern at :2246 and :2322 which do
call maybe_escape_identifier(xname).
--
This message was sent by Atlassian Jira
(v8.20.10#820010)