python-minifier 3.0.0__py2-none-any.whl → 3.1.0__py2-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -59,10 +59,13 @@ def compare_ast(l_ast, r_ast):
59
59
  if type(l_ast) != type(r_ast):
60
60
  raise CompareError(l_ast, r_ast, msg='Nodes do not match! %r != %r' % (l_ast, r_ast))
61
61
 
62
- for field in set(l_ast._fields + r_ast._fields):
62
+ for field in sorted(set(l_ast._fields + r_ast._fields)):
63
63
 
64
64
  if field == 'kind' and isinstance(l_ast, ast.Constant):
65
65
  continue
66
+
67
+ if field == 'str' and hasattr(ast, 'Interpolation') and isinstance(l_ast, ast.Interpolation):
68
+ continue
66
69
 
67
70
  if isinstance(getattr(l_ast, field, None), list):
68
71
 
@@ -70,6 +70,8 @@ for _node_type in [
70
70
  'TryStar',
71
71
  'TypeVar',
72
72
  'TypeVarTuple',
73
+ 'TemplateStr',
74
+ 'Interpolation',
73
75
  'YieldFrom',
74
76
  'arg',
75
77
  'withitem',
@@ -743,6 +743,13 @@ class ExpressionPrinter(object):
743
743
 
744
744
  self.printer.fstring(str(python_minifier.f_string.OuterFString(node, pep701=pep701)))
745
745
 
746
+ def visit_TemplateStr(self, node):
747
+ assert isinstance(node, ast.TemplateStr)
748
+
749
+ import python_minifier.t_string
750
+
751
+ self.printer.tstring(str(python_minifier.t_string.TString(node)))
752
+
746
753
  def visit_NamedExpr(self, node):
747
754
  self._expression(node.target)
748
755
  self.printer.operator(':=')
@@ -8,6 +8,7 @@ Mostly because FStrings feel like a hack.
8
8
 
9
9
  import copy
10
10
  import re
11
+ import sys
11
12
 
12
13
  import python_minifier.ast_compat as ast
13
14
 
@@ -58,11 +59,12 @@ class FString(object):
58
59
 
59
60
  return [x + '}' for x in conversion_candidates]
60
61
 
61
- def candidates(self):
62
- actual_candidates = []
62
+ def _generate_candidates_with_processor(self, prefix, str_processor):
63
+ """Generate f-string candidates using the given prefix and string processor function."""
64
+ candidates = []
63
65
 
64
66
  for quote in self.allowed_quotes:
65
- candidates = ['']
67
+ quote_candidates = ['']
66
68
  debug_specifier_candidates = []
67
69
  nested_allowed = copy.copy(self.allowed_quotes)
68
70
 
@@ -71,26 +73,24 @@ class FString(object):
71
73
 
72
74
  for v in self.node.values:
73
75
  if is_constant_node(v, ast.Str):
74
-
75
76
  # Could this be used as a debug specifier?
76
- if len(candidates) < 10:
77
+ if len(quote_candidates) < 10:
77
78
  debug_specifier = re.match(r'.*=\s*$', v.s)
78
79
  if debug_specifier:
79
- # Maybe!
80
80
  try:
81
- debug_specifier_candidates = [x + '{' + v.s for x in candidates]
81
+ debug_specifier_candidates = [x + '{' + v.s for x in quote_candidates]
82
82
  except Exception:
83
83
  continue
84
84
 
85
85
  try:
86
- candidates = [x + self.str_for(v.s, quote) for x in candidates]
86
+ quote_candidates = [x + str_processor(v.s, quote) for x in quote_candidates]
87
87
  except Exception:
88
88
  continue
89
89
  elif isinstance(v, ast.FormattedValue):
90
90
  try:
91
91
  completed = self.complete_debug_specifier(debug_specifier_candidates, v)
92
- candidates = [
93
- x + y for x in candidates for y in FormattedValue(v, nested_allowed, self.pep701).get_candidates()
92
+ quote_candidates = [
93
+ x + y for x in quote_candidates for y in FormattedValue(v, nested_allowed, self.pep701).get_candidates()
94
94
  ] + completed
95
95
  debug_specifier_candidates = []
96
96
  except Exception:
@@ -98,13 +98,70 @@ class FString(object):
98
98
  else:
99
99
  raise RuntimeError('Unexpected JoinedStr value')
100
100
 
101
- actual_candidates += ['f' + quote + x + quote for x in candidates]
101
+ candidates += [prefix + quote + x + quote for x in quote_candidates]
102
+
103
+ return candidates
104
+
105
+ def candidates(self):
106
+ actual_candidates = []
107
+
108
+ # Normal f-string candidates
109
+ actual_candidates += self._generate_candidates_with_processor('f', self.str_for)
110
+
111
+ # Raw f-string candidates (if we detect backslashes)
112
+ if self._contains_literal_backslashes():
113
+ actual_candidates += self._generate_candidates_with_processor('rf', lambda s, quote: self.raw_str_for(s))
102
114
 
103
115
  return filter(self.is_correct_ast, actual_candidates)
104
116
 
105
- def str_for(self, s, quote):
117
+ def raw_str_for(self, s):
118
+ """
119
+ Generate string representation for raw f-strings.
120
+ Don't escape backslashes like MiniString does.
121
+ """
106
122
  return s.replace('{', '{{').replace('}', '}}')
107
123
 
124
+ def _contains_literal_backslashes(self):
125
+ """
126
+ Check if this f-string contains literal backslashes in constant values.
127
+ This indicates it may need to be a raw f-string.
128
+ """
129
+ for node in ast.walk(self.node):
130
+ if is_constant_node(node, ast.Str):
131
+ if '\\' in node.s:
132
+ return True
133
+ return False
134
+
135
+
136
+ def str_for(self, s, quote):
137
+ # Escape null bytes and other characters that can't appear in Python source
138
+ escaped = ''
139
+ is_multiline = len(quote) == 3 # Triple-quoted strings
140
+
141
+ for c in s:
142
+ if c == '\0':
143
+ escaped += '\\x00'
144
+ elif c == '\n' and not is_multiline:
145
+ # Only escape newlines in single-quoted strings
146
+ escaped += '\\n'
147
+ elif c == '\r':
148
+ # Always escape carriage returns because Python normalizes them during parsing
149
+ # This prevents semantic changes (\\r -> \\n) in multiline strings
150
+ escaped += '\\r'
151
+ elif c == '\t':
152
+ # Always escape tabs for consistency (though not strictly necessary in multiline)
153
+ escaped += '\\t'
154
+ elif c == '{':
155
+ escaped += '{{'
156
+ elif c == '}':
157
+ escaped += '}}'
158
+ elif ord(c) < 32 and c not in '\n\r\t':
159
+ # Escape other control characters
160
+ escaped += '\\x{:02x}'.format(ord(c))
161
+ else:
162
+ escaped += c
163
+ return escaped
164
+
108
165
 
109
166
  class OuterFString(FString):
110
167
  """
@@ -285,7 +342,9 @@ class Str(object):
285
342
  if literal == '':
286
343
  literal += self.current_quote
287
344
 
288
- if c == '\n':
345
+ if c == '\0':
346
+ literal += '\\x00'
347
+ elif c == '\n':
289
348
  literal += '\\n'
290
349
  elif c == '\r':
291
350
  literal += '\\r'
@@ -302,7 +361,7 @@ class Str(object):
302
361
  if self._s == '':
303
362
  return str(min(self.allowed_quotes, key=len)) * 2
304
363
 
305
- if '\0' in self._s or ('\\' in self._s and not self.pep701):
364
+ if '\\' in self._s and not self.pep701:
306
365
  raise ValueError('Impossible to represent a character in f-string expression part')
307
366
 
308
367
  if not self.pep701 and ('\n' in self._s or '\r' in self._s):
@@ -360,7 +419,35 @@ class FormatSpec(object):
360
419
  return candidates
361
420
 
362
421
  def str_for(self, s):
363
- return s.replace('{', '{{').replace('}', '}}')
422
+ # Special handling for problematic format spec characters that can cause parsing issues
423
+ # If the format spec contains only braces, it's likely an invalid test case
424
+
425
+ # Escape null bytes and other unprintable characters
426
+ escaped = ''
427
+ for c in s:
428
+ if c == '\0':
429
+ escaped += '\\x00'
430
+ elif c == '{':
431
+ escaped += '{{'
432
+ elif c == '}':
433
+ escaped += '}}'
434
+ elif c == '\\':
435
+ # For Python 3.12+ raw f-string regression (fixed in 3.14rc2), we need to escape backslashes
436
+ # in format specs so they round-trip correctly
437
+ if (3, 12) <= sys.version_info < (3, 14):
438
+ escaped += '\\\\'
439
+ else:
440
+ escaped += c
441
+ elif c == '\r':
442
+ # Always escape carriage returns because Python normalizes them to newlines during parsing
443
+ # This prevents AST mismatches (\r -> \n normalization)
444
+ escaped += '\\r'
445
+ elif ord(c) < 32 and c not in '\t\n':
446
+ # Escape other control characters except tab, newline
447
+ escaped += '\\x{:02x}'.format(ord(c))
448
+ else:
449
+ escaped += c
450
+ return escaped
364
451
 
365
452
 
366
453
  class Bytes(object):
@@ -411,7 +498,24 @@ class Bytes(object):
411
498
 
412
499
  if literal == '':
413
500
  literal = 'b' + self.current_quote
414
- literal += chr(b)
501
+
502
+ # Handle special characters that need escaping
503
+ if b == 0: # null byte
504
+ literal += '\\x00'
505
+ elif b == ord('\\'): # backslash
506
+ literal += '\\\\'
507
+ elif b == ord('\n'): # newline
508
+ literal += '\\n'
509
+ elif b == ord('\r'): # carriage return
510
+ literal += '\\r'
511
+ elif b == ord('\t'): # tab
512
+ literal += '\\t'
513
+ elif len(self.current_quote) == 1 and b == ord(self.current_quote): # single quote character
514
+ literal += '\\' + self.current_quote
515
+ elif 32 <= b <= 126: # printable ASCII
516
+ literal += chr(b)
517
+ else: # other non-printable characters
518
+ literal += '\\x{:02x}'.format(b)
415
519
 
416
520
  if literal:
417
521
  literal += self.current_quote
@@ -421,8 +525,6 @@ class Bytes(object):
421
525
  if self._b == b'':
422
526
  return 'b' + str(min(self.allowed_quotes, key=len)) * 2
423
527
 
424
- if b'\0' in self._b or b'\\' in self._b:
425
- raise ValueError('Impossible to represent a %r character in f-string expression part')
426
528
 
427
529
  if b'\n' in self._b or b'\r' in self._b:
428
530
  if '"""' not in self.allowed_quotes and "'''" not in self.allowed_quotes:
@@ -220,6 +220,13 @@ class HoistLiterals(NodeVisitor):
220
220
  continue
221
221
  self.visit(v)
222
222
 
223
+ def visit_TemplateStr(self, node):
224
+ for v in node.values:
225
+ if is_constant_node(v, ast.Str):
226
+ # Can't hoist string literals that are part of the template
227
+ continue
228
+ self.visit(v)
229
+
223
230
  def visit_NameConstant(self, node):
224
231
  self.get_binding(node.value, node).add_reference(node)
225
232
 
@@ -0,0 +1,330 @@
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
+ # Normal t-string candidates
75
+ actual_candidates.extend(self._generate_candidates_with_processor('t', self.str_for))
76
+
77
+ # Raw t-string candidates (if we detect backslashes)
78
+ if self._contains_literal_backslashes():
79
+ actual_candidates.extend(self._generate_candidates_with_processor('rt', self.raw_str_for))
80
+
81
+ return filter(self.is_correct_ast, actual_candidates)
82
+
83
+ def _generate_candidates_with_processor(self, prefix, str_processor):
84
+ """Generate t-string candidates using the given prefix and string processor function."""
85
+ candidates = []
86
+
87
+ for quote in self.allowed_quotes:
88
+ quote_candidates = ['']
89
+ debug_specifier_candidates = []
90
+
91
+ for v in self.node.values:
92
+ if is_constant_node(v, ast.Constant) and isinstance(v.value, str):
93
+ # String literal part - check for debug specifiers
94
+
95
+ # Could this be used as a debug specifier?
96
+ if len(quote_candidates) < 10:
97
+ import re
98
+ debug_specifier = re.match(r'.*=\s*$', v.value)
99
+ if debug_specifier:
100
+ # Maybe! Save for potential debug specifier completion
101
+ try:
102
+ debug_specifier_candidates = [x + '{' + v.value for x in quote_candidates]
103
+ except Exception:
104
+ continue
105
+
106
+ try:
107
+ quote_candidates = [x + str_processor(v.value, quote) for x in quote_candidates]
108
+ except Exception:
109
+ continue
110
+
111
+ elif isinstance(v, ast.Interpolation):
112
+ # Interpolated expression part - check for debug completion
113
+ try:
114
+ # Try debug specifier completion
115
+ completed = self.complete_debug_specifier(debug_specifier_candidates, v)
116
+
117
+ # Regular interpolation processing
118
+ interpolation_candidates = InterpolationValue(v).get_candidates()
119
+ quote_candidates = [x + y for x in quote_candidates for y in interpolation_candidates] + completed
120
+
121
+ debug_specifier_candidates = []
122
+ except Exception:
123
+ continue
124
+ else:
125
+ raise RuntimeError('Unexpected TemplateStr value: %r' % v)
126
+
127
+ candidates.extend([prefix + quote + x + quote for x in quote_candidates])
128
+
129
+ return candidates
130
+
131
+ def str_for(self, s, quote):
132
+ """Convert string literal to properly escaped form"""
133
+ # Use MiniString for optimal string representation
134
+ # Always allowed due to PEP 701 - no backslash restrictions
135
+ mini_s = str(MiniString(s, quote)).replace('{', '{{').replace('}', '}}')
136
+
137
+ if mini_s == '':
138
+ return '\\\n'
139
+ return mini_s
140
+
141
+ def raw_str_for(self, s):
142
+ """
143
+ Generate string representation for raw t-strings.
144
+ Don't escape backslashes like MiniString does.
145
+ """
146
+ return s.replace('{', '{{').replace('}', '}}')
147
+
148
+ def _contains_literal_backslashes(self):
149
+ """
150
+ Check if this t-string contains literal backslashes in constant values.
151
+ This indicates it may need to be a raw t-string.
152
+ """
153
+ for node in ast.walk(self.node):
154
+ if is_constant_node(node, ast.Str):
155
+ if '\\' in node.s:
156
+ return True
157
+ return False
158
+
159
+ def __str__(self):
160
+ """Generate the shortest valid t-string representation"""
161
+ if len(self.node.values) == 0:
162
+ return 't' + min(self.allowed_quotes, key=len) * 2
163
+
164
+ candidates = list(self.candidates())
165
+
166
+ # Validate all candidates
167
+ for candidate in candidates:
168
+ try:
169
+ minified_t_string = ast.parse(candidate, 'python_minifier.t_string output', mode='eval').body
170
+ except SyntaxError as syntax_error:
171
+ raise UnstableMinification(syntax_error, '', candidate)
172
+
173
+ try:
174
+ compare_ast(self.node, minified_t_string)
175
+ except CompareError as compare_error:
176
+ raise UnstableMinification(compare_error, '', candidate)
177
+
178
+ if not candidates:
179
+ raise ValueError('Unable to create representation for t-string')
180
+
181
+ return min(candidates, key=len)
182
+
183
+
184
+ class InterpolationValue(ExpressionPrinter):
185
+ """
186
+ A Template String Interpolation Part
187
+
188
+ Handles ast.Interpolation nodes (equivalent to FormattedValue for f-strings)
189
+ """
190
+
191
+ def __init__(self, node):
192
+ super(InterpolationValue, self).__init__()
193
+
194
+ assert isinstance(node, ast.Interpolation)
195
+ self.node = node
196
+ # Always use all quotes - no restrictions due to PEP 701
197
+ self.allowed_quotes = ['"', "'", '"""', "'''"]
198
+ self.candidates = ['']
199
+
200
+ def get_candidates(self):
201
+ """Generate all possible representations of this interpolation"""
202
+
203
+ self.printer.delimiter('{')
204
+
205
+ if self.is_curly(self.node.value):
206
+ self.printer.delimiter(' ')
207
+
208
+ self._expression(self.node.value)
209
+
210
+ # Handle conversion specifiers
211
+ if self.node.conversion == 115: # 's'
212
+ self.printer.append('!s', TokenTypes.Delimiter)
213
+ elif self.node.conversion == 114: # 'r'
214
+ self.printer.append('!r', TokenTypes.Delimiter)
215
+ elif self.node.conversion == 97: # 'a'
216
+ self.printer.append('!a', TokenTypes.Delimiter)
217
+
218
+ # Handle format specifications
219
+ if self.node.format_spec is not None:
220
+ self.printer.delimiter(':')
221
+
222
+ # Format spec is a JoinedStr (f-string) in the AST
223
+ if isinstance(self.node.format_spec, ast.JoinedStr):
224
+ import python_minifier.f_string
225
+ # Use f-string processing for format specs
226
+ format_candidates = python_minifier.f_string.OuterFString(
227
+ self.node.format_spec, pep701=True
228
+ ).candidates()
229
+ # Remove the f/rf prefix and quotes to get just the format part
230
+ format_parts = []
231
+ for fmt in format_candidates:
232
+ # Handle both f"..." and rf"..." patterns
233
+ if fmt.startswith('rf'):
234
+ # Remove rf prefix and outer quotes
235
+ inner = fmt[2:]
236
+ elif fmt.startswith('f'):
237
+ # Remove f prefix and outer quotes
238
+ inner = fmt[1:]
239
+ else:
240
+ continue
241
+
242
+ if (inner.startswith('"') and inner.endswith('"')) or \
243
+ (inner.startswith("'") and inner.endswith("'")):
244
+ format_parts.append(inner[1:-1])
245
+ elif (inner.startswith('"""') and inner.endswith('"""')) or \
246
+ (inner.startswith("'''") and inner.endswith("'''")):
247
+ format_parts.append(inner[3:-3])
248
+ else:
249
+ format_parts.append(inner)
250
+
251
+ if format_parts:
252
+ self._append(format_parts)
253
+ else:
254
+ # Simple constant format spec
255
+ self.printer.append(str(self.node.format_spec), TokenTypes.Delimiter)
256
+
257
+ self.printer.delimiter('}')
258
+
259
+ self._finalize()
260
+ return self.candidates
261
+
262
+ def is_curly(self, node):
263
+ """Check if expression starts with curly braces (needs space)"""
264
+ if isinstance(node, (ast.SetComp, ast.DictComp, ast.Set, ast.Dict)):
265
+ return True
266
+
267
+ if isinstance(node, (ast.Expr, ast.Attribute, ast.Subscript)):
268
+ return self.is_curly(node.value)
269
+
270
+ if isinstance(node, (ast.Compare, ast.BinOp)):
271
+ return self.is_curly(node.left)
272
+
273
+ if isinstance(node, ast.Call):
274
+ return self.is_curly(node.func)
275
+
276
+ if isinstance(node, ast.BoolOp):
277
+ return self.is_curly(node.values[0])
278
+
279
+ if isinstance(node, ast.IfExp):
280
+ return self.is_curly(node.body)
281
+
282
+ return False
283
+
284
+ def visit_Constant(self, node):
285
+ """Handle constant values in interpolations"""
286
+ if isinstance(node.value, str):
287
+ # Use Str class from f_string module for string handling
288
+ from python_minifier.f_string import Str
289
+ self.printer.append(str(Str(node.value, self.allowed_quotes, pep701=True)), TokenTypes.NonNumberLiteral)
290
+ elif isinstance(node.value, bytes):
291
+ # Use Bytes class from f_string module for bytes handling
292
+ from python_minifier.f_string import Bytes
293
+ self.printer.append(str(Bytes(node.value, self.allowed_quotes)), TokenTypes.NonNumberLiteral)
294
+ else:
295
+ # Other constants (numbers, None, etc.)
296
+ super().visit_Constant(node)
297
+
298
+ def visit_TemplateStr(self, node):
299
+ """Handle nested t-strings"""
300
+ assert isinstance(node, ast.TemplateStr)
301
+ if self.printer.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword, TokenTypes.SoftKeyword]:
302
+ self.printer.delimiter(' ')
303
+ # Nested t-string - no quote restrictions due to PEP 701
304
+ self._append(TString(node).candidates())
305
+
306
+ def visit_JoinedStr(self, node):
307
+ """Handle nested f-strings in t-strings"""
308
+ assert isinstance(node, ast.JoinedStr)
309
+ if self.printer.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword, TokenTypes.SoftKeyword]:
310
+ self.printer.delimiter(' ')
311
+
312
+ import python_minifier.f_string
313
+ # F-strings nested in t-strings also benefit from PEP 701
314
+ self._append(python_minifier.f_string.OuterFString(node, pep701=True).candidates())
315
+
316
+ def visit_Lambda(self, node):
317
+ """Handle lambda expressions in interpolations"""
318
+ self.printer.delimiter('(')
319
+ super().visit_Lambda(node)
320
+ self.printer.delimiter(')')
321
+
322
+ def _finalize(self):
323
+ """Finalize the current printer state"""
324
+ self.candidates = [x + str(self.printer) for x in self.candidates]
325
+ self.printer._code = ''
326
+
327
+ def _append(self, candidates):
328
+ """Append multiple candidate strings"""
329
+ self._finalize()
330
+ self.candidates = [x + y for x in self.candidates for y in candidates]
@@ -181,6 +181,16 @@ class TokenPrinter(object):
181
181
  self._code += s
182
182
  self.previous_token = TokenTypes.NonNumberLiteral
183
183
 
184
+ def tstring(self, s):
185
+ """Add a template string (t-string) to the output code."""
186
+ assert isinstance(s, str)
187
+
188
+ if self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword, TokenTypes.SoftKeyword]:
189
+ self.delimiter(' ')
190
+
191
+ self._code += s
192
+ self.previous_token = TokenTypes.NonNumberLiteral
193
+
184
194
  def delimiter(self, d):
185
195
  """Add a delimiter to the output code."""
186
196
  assert d in [
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-minifier
3
- Version: 3.0.0
3
+ Version: 3.1.0
4
4
  Summary: Transform Python source code into it's most compact representation
5
5
  Home-page: https://github.com/dflook/python-minifier
6
6
  Author: Daniel Flook
@@ -26,13 +26,14 @@ Classifier: Programming Language :: Python :: 3.10
26
26
  Classifier: Programming Language :: Python :: 3.11
27
27
  Classifier: Programming Language :: Python :: 3.12
28
28
  Classifier: Programming Language :: Python :: 3.13
29
+ Classifier: Programming Language :: Python :: 3.14
29
30
  Classifier: Programming Language :: Python :: 2
30
31
  Classifier: Programming Language :: Python :: 2.7
31
32
  Classifier: Programming Language :: Python :: Implementation :: CPython
32
33
  Classifier: Programming Language :: Python :: Implementation :: PyPy
33
34
  Classifier: Intended Audience :: Developers
34
35
  Classifier: Topic :: Software Development
35
- Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14
36
+ Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15
36
37
  Description-Content-Type: text/markdown
37
38
 
38
39
  # Python Minifier
@@ -41,7 +42,7 @@ Transforms Python source code into its most compact representation.
41
42
 
42
43
  [Try it out!](https://python-minifier.com)
43
44
 
44
- python-minifier currently supports Python 2.7 and Python 3.3 to 3.13. Previous releases supported Python 2.6.
45
+ python-minifier currently supports Python 2.7 and Python 3.3 to 3.14. Previous releases supported Python 2.6.
45
46
 
46
47
  * [PyPI](https://pypi.org/project/python-minifier/)
47
48
  * [Documentation](https://dflook.github.io/python-minifier/)
@@ -1,15 +1,16 @@
1
1
  python_minifier/__init__.py,sha256=Q95ufDbd7hadjVtJZYclsJqX9S4NsKZSpqCui0Fw7oI,9542
2
2
  python_minifier/__init__.pyi,sha256=-xLoKXbQsV7ko_YzyXL9WkI2vmGc6yn4AW5biK_GJXE,1233
3
3
  python_minifier/__main__.py,sha256=r7_Vh9n-aoc4jd5CERKO3KqJeqXHeWoycDRlvV7xTFM,14053
4
- python_minifier/ast_compare.py,sha256=TlkEdQyiEXZZxbu3NrOgmYE7d8nV8dYhHDMysni44-Q,3249
5
- python_minifier/ast_compat.py,sha256=nubno_yFHtuoxbT7a7CtlcAfdNj9yhniKSfNoOsQ3Fw,2209
4
+ python_minifier/ast_compare.py,sha256=fYtIIo_d16I4GgjPIjXSWcQkauNYQvF7qiQV-cilSg8,3389
5
+ python_minifier/ast_compat.py,sha256=8QcPcujdHtDT7691d48_1L3_CzHgtvpWDAtWMr2Z9Eg,2249
6
6
  python_minifier/ast_printer.py,sha256=TlkyKl9_9ScWeop3W1TwXZ46MJWINfkJ7NvmULpwt4A,3254
7
- python_minifier/expression_printer.py,sha256=AwkDTs-NJhLyDppE_zlgQdtQQTVQlxdOdgTtSt_DxT8,22465
8
- python_minifier/f_string.py,sha256=9Zkb2SksoTdx0-dfQKy_Q76KrOR4cxPokR5tnF7jldY,14131
7
+ python_minifier/expression_printer.py,sha256=EEtnpdipgf0zjkbacloAAjyWBw0tvFz1tloLL6ZnNig,22670
8
+ python_minifier/f_string.py,sha256=eXYXaxEMSB3iHY_M_RXYpeloJV-7TYnhWIGge0KYCk8,18314
9
9
  python_minifier/ministring.py,sha256=R0xaAZqMlLu1Jm0aBVSI0n8KZiZV7PTs4RE4UbCmTy8,4359
10
10
  python_minifier/module_printer.py,sha256=CZFZ3WDBgH57_Q2pmC0ySFdRcvQR_7uVQeXKfvT1BN0,25566
11
11
  python_minifier/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- python_minifier/token_printer.py,sha256=TbN7VDGM_icmruLnBLgtWpHz1iY4pagGKsWCBMWSisA,9640
12
+ python_minifier/t_string.py,sha256=z3KzWaSDqvy701SY3dm8VSkiJuiUfopSWGNIm-nDpAo,13261
13
+ python_minifier/token_printer.py,sha256=t4MmgulfQROWGd4n8HNyIDqo8cIxJg9LBxnFIbJwIr4,9987
13
14
  python_minifier/util.py,sha256=60iT3XkKlPlZhXQ1NuLB27Wv3ihl-gBC_WjOjEiFn-Q,1184
14
15
  python_minifier/ast_annotation/__init__.py,sha256=BJ4gyS-_bytIZf0SP8JJG6FECWf8MUaUi3vOAfD60qQ,2259
15
16
  python_minifier/rename/__init__.py,sha256=8DLEFgliakf_5T_m-l1Nd35QxKpM4h7RJyFVpjsxjWA,375
@@ -17,7 +18,7 @@ python_minifier/rename/bind_names.py,sha256=CCL2ZT2px___1yE5euO_Wv423DLQFIp5-D7m
17
18
  python_minifier/rename/binding.py,sha256=WPospKLbZN7nnzDayJ55HMxuS-E5zvOE78RJkKiyPrE,15335
18
19
  python_minifier/rename/mapper.py,sha256=h3dgg3JgUGQhW7oGwBX1syUgmt5AuaF_OP8dW5ImS5o,6482
19
20
  python_minifier/rename/name_generator.py,sha256=70Klx2DteYRpBT8dr6jYDB31ARfinS42e08kKlWAzLg,1269
20
- python_minifier/rename/rename_literals.py,sha256=dBe-6MsPDjzgQ_B5iz184PkEylxdOeBiTuZgeSME4bU,7246
21
+ python_minifier/rename/rename_literals.py,sha256=qKJYCK8X7jEi7IW2PFiOqIjj0pn-1Cz1WaiBEGkUmIw,7488
21
22
  python_minifier/rename/renamer.py,sha256=B9kNiTkWDw7i7tKQASrCZ4fXsjKf9AspFSUfuxQWZFs,6600
22
23
  python_minifier/rename/resolve_names.py,sha256=VbePyUUqggvvcF3fAC6XMv6b3Kmb_cmlO9G7vf5Ti-c,4320
23
24
  python_minifier/rename/util.py,sha256=ItJykxM_QRjIePMcKYo-LngmP5hLQZK-bOZIefg1cKA,5469
@@ -36,10 +37,10 @@ python_minifier/transforms/remove_object_base.py,sha256=j4se6OXyZK1lr_ABmGT0Oivu
36
37
  python_minifier/transforms/remove_pass.py,sha256=S9e59nBEZ8rBBifvAhbPRDTfUA7s8_a7pSIsZbFZD2A,735
37
38
  python_minifier/transforms/remove_posargs.py,sha256=aV0wFnGiDXu4uQBQP7-cB1uLSF3mamIq5-txvDg8BVo,314
38
39
  python_minifier/transforms/suite_transformer.py,sha256=WZCVkkBSniWeEtBybVweeMmy-A1wC3lBIYSgxCHelBU,6324
39
- python_minifier-3.0.0.dist-info/LICENSE,sha256=FzsyDHb8pAZupSGFjIOuHcHMlFf6N-aIxq9gOYGbNyE,1069
40
- python_minifier-3.0.0.dist-info/METADATA,sha256=qyng9Qp0kOp9hiup4wcfJW2DQJT3_hCjbFemSrIpTpU,6451
41
- python_minifier-3.0.0.dist-info/WHEEL,sha256=1VPi6hfNQaRRNuEdK_3dv9o8COtLGnHWJghhj4CQ28k,92
42
- python_minifier-3.0.0.dist-info/entry_points.txt,sha256=aS7ZUWQeeys8lAbrmmEa2__Bg-anH1tUMyi9cKdTbO4,60
43
- python_minifier-3.0.0.dist-info/top_level.txt,sha256=4SRDfWKi_KMq7LDrjlzUFoDCs6INYPtxc1Pun4z8LsU,16
44
- python_minifier-3.0.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
45
- python_minifier-3.0.0.dist-info/RECORD,,
40
+ python_minifier-3.1.0.dist-info/LICENSE,sha256=FzsyDHb8pAZupSGFjIOuHcHMlFf6N-aIxq9gOYGbNyE,1069
41
+ python_minifier-3.1.0.dist-info/METADATA,sha256=I_DsUDjeg9LVWVlOzWeilaeQEbCINF6I-5BTvnGDSRE,6502
42
+ python_minifier-3.1.0.dist-info/WHEEL,sha256=1VPi6hfNQaRRNuEdK_3dv9o8COtLGnHWJghhj4CQ28k,92
43
+ python_minifier-3.1.0.dist-info/entry_points.txt,sha256=aS7ZUWQeeys8lAbrmmEa2__Bg-anH1tUMyi9cKdTbO4,60
44
+ python_minifier-3.1.0.dist-info/top_level.txt,sha256=4SRDfWKi_KMq7LDrjlzUFoDCs6INYPtxc1Pun4z8LsU,16
45
+ python_minifier-3.1.0.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
46
+ python_minifier-3.1.0.dist-info/RECORD,,