1
+ """
2
+ Template String (T-String) unparsing
3
+
4
+ T-strings in Python 3.14 follow PEP 750 and are based on PEP 701,
5
+ which means they don't have the quote restrictions of older f-strings.
6
+
7
+ This implementation is much simpler than f_string.py because:
8
+ - No quote tracking needed (PEP 701 benefits)
9
+ - No pep701 parameter needed (always true for t-strings)
10
+ - No Outer vs Inner distinction needed
11
+ - Always use all quote types
12
+ """
13
+
14
+ import python_minifier .ast_compat as ast
15
+
16
+ from python_minifier import UnstableMinification
17
+ from python_minifier .ast_compare import CompareError , compare_ast
18
+ from python_minifier .expression_printer import ExpressionPrinter
19
+ from python_minifier .ministring import MiniString
20
+ from python_minifier .token_printer import TokenTypes
21
+ from python_minifier .util import is_constant_node
22
+
23
+
24
+ class TString (object ):
25
+ """
26
+ A Template String (t-string)
27
+
28
+ Much simpler than f-strings because PEP 701 eliminates quote restrictions
29
+ """
30
+
31
+ def __init__ (self , node ):
32
+ assert isinstance (node , ast .TemplateStr )
33
+ self .node = node
34
+ # Always use all quotes - no restrictions due to PEP 701
35
+ self .allowed_quotes = ['"' , "'" , '"""' , "'''" ]
36
+
37
+ def is_correct_ast (self , code ):
38
+ """Check if the generated code produces the same AST"""
39
+ try :
40
+ c = ast .parse (code , 'TString candidate' , mode = 'eval' )
41
+ compare_ast (self .node , c .body )
42
+ return True
43
+ except Exception :
44
+ return False
45
+
46
+ def complete_debug_specifier (self , partial_specifier_candidates , value_node ):
47
+ """Complete debug specifier candidates for an Interpolation node"""
48
+ assert isinstance (value_node , ast .Interpolation )
49
+
50
+ conversion = ''
51
+ if value_node .conversion == 115 : # 's'
52
+ conversion = '!s'
53
+ elif value_node .conversion == 114 and value_node .format_spec is not None :
54
+ # This is the default for debug specifiers, unless there's a format_spec
55
+ conversion = '!r'
56
+ elif value_node .conversion == 97 : # 'a'
57
+ conversion = '!a'
58
+
59
+ conversion_candidates = [x + conversion for x in partial_specifier_candidates ]
60
+
61
+ if value_node .format_spec is not None :
62
+ # Handle format specifications in debug specifiers
63
+ if isinstance (value_node .format_spec , ast .JoinedStr ):
64
+ import python_minifier .f_string
65
+ format_specs = python_minifier .f_string .FormatSpec (value_node .format_spec , self .allowed_quotes , pep701 = True ).candidates ()
66
+ conversion_candidates = [c + ':' + fs for c in conversion_candidates for fs in format_specs ]
67
+
68
+ return [x + '}' for x in conversion_candidates ]
69
+
70
+ def candidates (self ):
71
+ """Generate all possible representations"""
72
+ actual_candidates = []
73
+
74
+ for quote in self .allowed_quotes :
75
+ candidates = ['' ]
76
+ debug_specifier_candidates = []
77
+
78
+ for v in self .node .values :
79
+ if is_constant_node (v , ast .Constant ) and isinstance (v .value , str ):
80
+ # String literal part - check for debug specifiers
81
+
82
+ # Could this be used as a debug specifier?
83
+ if len (candidates ) < 10 :
84
+ import re
85
+ debug_specifier = re .match (r'.*=\s*$' , v .value )
86
+ if debug_specifier :
87
+ # Maybe! Save for potential debug specifier completion
88
+ try :
89
+ debug_specifier_candidates = [x + '{' + v .value for x in candidates ]
90
+ except Exception :
91
+ continue
92
+
93
+ try :
94
+ candidates = [x + self .str_for (v .value , quote ) for x in candidates ]
95
+ except Exception :
96
+ continue
97
+
98
+ elif isinstance (v , ast .Interpolation ):
99
+ # Interpolated expression part - check for debug completion
100
+ try :
101
+ # Try debug specifier completion
102
+ completed = self .complete_debug_specifier (debug_specifier_candidates , v )
103
+
104
+ # Regular interpolation processing
105
+ interpolation_candidates = InterpolationValue (v ).get_candidates ()
106
+ candidates = [x + y for x in candidates for y in interpolation_candidates ] + completed
107
+
108
+ debug_specifier_candidates = []
109
+ except Exception :
110
+ continue
111
+ else :
112
+ raise RuntimeError ('Unexpected TemplateStr value: %r' % v )
113
+
114
+ actual_candidates .extend (['t' + quote + x + quote for x in candidates ])
115
+
116
+ return filter (self .is_correct_ast , actual_candidates )
117
+
118
+ def str_for (self , s , quote ):
119
+ """Convert string literal to properly escaped form"""
120
+ # Use MiniString for optimal string representation
121
+ # Always allowed due to PEP 701 - no backslash restrictions
122
+ mini_s = str (MiniString (s , quote )).replace ('{' , '{{' ).replace ('}' , '}}' )
123
+
124
+ if mini_s == '' :
125
+ return '\\ \n '
126
+ return mini_s
127
+
128
+ def __str__ (self ):
129
+ """Generate the shortest valid t-string representation"""
130
+ if len (self .node .values ) == 0 :
131
+ return 't' + min (self .allowed_quotes , key = len ) * 2
132
+
133
+ candidates = list (self .candidates ())
134
+
135
+ # Validate all candidates
136
+ for candidate in candidates :
137
+ try :
138
+ minified_t_string = ast .parse (candidate , 'python_minifier.t_string output' , mode = 'eval' ).body
139
+ except SyntaxError as syntax_error :
140
+ raise UnstableMinification (syntax_error , '' , candidate )
141
+
142
+ try :
143
+ compare_ast (self .node , minified_t_string )
144
+ except CompareError as compare_error :
145
+ raise UnstableMinification (compare_error , '' , candidate )
146
+
147
+ if not candidates :
148
+ raise ValueError ('Unable to create representation for t-string' )
149
+
150
+ return min (candidates , key = len )
151
+
152
+
153
+ class InterpolationValue (ExpressionPrinter ):
154
+ """
155
+ A Template String Interpolation Part
156
+
157
+ Handles ast.Interpolation nodes (equivalent to FormattedValue for f-strings)
158
+ """
159
+
160
+ def __init__ (self , node ):
161
+ super (InterpolationValue , self ).__init__ ()
162
+
163
+ assert isinstance (node , ast .Interpolation )
164
+ self .node = node
165
+ # Always use all quotes - no restrictions due to PEP 701
166
+ self .allowed_quotes = ['"' , "'" , '"""' , "'''" ]
167
+ self .candidates = ['' ]
168
+
169
+ def get_candidates (self ):
170
+ """Generate all possible representations of this interpolation"""
171
+
172
+ self .printer .delimiter ('{' )
173
+
174
+ if self .is_curly (self .node .value ):
175
+ self .printer .delimiter (' ' )
176
+
177
+ self ._expression (self .node .value )
178
+
179
+ # Handle conversion specifiers
180
+ if self .node .conversion == 115 : # 's'
181
+ self .printer .append ('!s' , TokenTypes .Delimiter )
182
+ elif self .node .conversion == 114 : # 'r'
183
+ self .printer .append ('!r' , TokenTypes .Delimiter )
184
+ elif self .node .conversion == 97 : # 'a'
185
+ self .printer .append ('!a' , TokenTypes .Delimiter )
186
+
187
+ # Handle format specifications
188
+ if self .node .format_spec is not None :
189
+ self .printer .delimiter (':' )
190
+
191
+ # Format spec is a JoinedStr (f-string) in the AST
192
+ if isinstance (self .node .format_spec , ast .JoinedStr ):
193
+ import python_minifier .f_string
194
+ # Use f-string processing for format specs
195
+ format_candidates = python_minifier .f_string .OuterFString (
196
+ self .node .format_spec , pep701 = True
197
+ ).candidates ()
198
+ # Remove the f prefix and quotes to get just the format part
199
+ format_parts = []
200
+ for fmt in format_candidates :
201
+ if fmt .startswith ('f' ):
202
+ # Remove f prefix and outer quotes
203
+ inner = fmt [1 :]
204
+ if (inner .startswith ('"' ) and inner .endswith ('"' )) or \
205
+ (inner .startswith ("'" ) and inner .endswith ("'" )):
206
+ format_parts .append (inner [1 :- 1 ])
207
+ elif (inner .startswith ('"""' ) and inner .endswith ('"""' )) or \
208
+ (inner .startswith ("'''" ) and inner .endswith ("'''" )):
209
+ format_parts .append (inner [3 :- 3 ])
210
+ else :
211
+ format_parts .append (inner )
212
+
213
+ if format_parts :
214
+ self ._append (format_parts )
215
+ else :
216
+ # Simple constant format spec
217
+ self .printer .append (str (self .node .format_spec ), TokenTypes .Delimiter )
218
+
219
+ self .printer .delimiter ('}' )
220
+
221
+ self ._finalize ()
222
+ return self .candidates
223
+
224
+ def is_curly (self , node ):
225
+ """Check if expression starts with curly braces (needs space)"""
226
+ if isinstance (node , (ast .SetComp , ast .DictComp , ast .Set , ast .Dict )):
227
+ return True
228
+
229
+ if isinstance (node , (ast .Expr , ast .Attribute , ast .Subscript )):
230
+ return self .is_curly (node .value )
231
+
232
+ if isinstance (node , (ast .Compare , ast .BinOp )):
233
+ return self .is_curly (node .left )
234
+
235
+ if isinstance (node , ast .Call ):
236
+ return self .is_curly (node .func )
237
+
238
+ if isinstance (node , ast .BoolOp ):
239
+ return self .is_curly (node .values [0 ])
240
+
241
+ if isinstance (node , ast .IfExp ):
242
+ return self .is_curly (node .body )
243
+
244
+ return False
245
+
246
+ def visit_Constant (self , node ):
247
+ """Handle constant values in interpolations"""
248
+ if isinstance (node .value , str ):
249
+ # Use Str class from f_string module for string handling
250
+ from python_minifier .f_string import Str
251
+ self .printer .append (str (Str (node .value , self .allowed_quotes , pep701 = True )), TokenTypes .NonNumberLiteral )
252
+ elif isinstance (node .value , bytes ):
253
+ # Use Bytes class from f_string module for bytes handling
254
+ from python_minifier .f_string import Bytes
255
+ self .printer .append (str (Bytes (node .value , self .allowed_quotes )), TokenTypes .NonNumberLiteral )
256
+ else :
257
+ # Other constants (numbers, None, etc.)
258
+ super ().visit_Constant (node )
259
+
260
+ def visit_TemplateStr (self , node ):
261
+ """Handle nested t-strings"""
262
+ assert isinstance (node , ast .TemplateStr )
263
+ if self .printer .previous_token in [TokenTypes .Identifier , TokenTypes .Keyword , TokenTypes .SoftKeyword ]:
264
+ self .printer .delimiter (' ' )
265
+ # Nested t-string - no quote restrictions due to PEP 701
266
+ self ._append (TString (node ).candidates ())
267
+
268
+ def visit_JoinedStr (self , node ):
269
+ """Handle nested f-strings in t-strings"""
270
+ assert isinstance (node , ast .JoinedStr )
271
+ if self .printer .previous_token in [TokenTypes .Identifier , TokenTypes .Keyword , TokenTypes .SoftKeyword ]:
272
+ self .printer .delimiter (' ' )
273
+
274
+ import python_minifier .f_string
275
+ # F-strings nested in t-strings also benefit from PEP 701
276
+ self ._append (python_minifier .f_string .OuterFString (node , pep701 = True ).candidates ())
277
+
278
+ def visit_Lambda (self , node ):
279
+ """Handle lambda expressions in interpolations"""
280
+ self .printer .delimiter ('(' )
281
+ super ().visit_Lambda (node )
282
+ self .printer .delimiter (')' )
283
+
284
+ def _finalize (self ):
285
+ """Finalize the current printer state"""
286
+ self .candidates = [x + str (self .printer ) for x in self .candidates ]
287
+ self .printer ._code = ''
288
+
289
+ def _append (self , candidates ):
290
+ """Append multiple candidate strings"""
291
+ self ._finalize ()
292
+ self .candidates = [x + y for x in self .candidates for y in candidates ]
0 commit comments