python-minifier 3.0.0__py2-none-any.whl → 3.1.1__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,11 +59,14 @@ 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
66
 
67
+ if field == 'str' and hasattr(ast, 'Interpolation') and isinstance(l_ast, ast.Interpolation):
68
+ continue
69
+
67
70
  if isinstance(getattr(l_ast, field, None), list):
68
71
 
69
72
  l_list = getattr(l_ast, field, None)
@@ -70,9 +70,11 @@ 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',
76
78
  ]:
77
79
  if _node_type not in globals():
78
- globals()[_node_type] = type(_node_type, (AST,), {})
80
+ globals()[_node_type] = type(_node_type, (AST,), {})
@@ -301,8 +301,8 @@ class ExpressionPrinter(object):
301
301
 
302
302
  if value_precedence != 0 and (
303
303
  (op_precedence > value_precedence)
304
- or op_precedence == value_precedence
305
- and self._is_left_associative(node.op)
304
+ or (op_precedence == value_precedence
305
+ and self._is_left_associative(node.op))
306
306
  ):
307
307
  self.printer.delimiter('(')
308
308
  self._expression(v)
@@ -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:
@@ -596,7 +596,7 @@ class ModulePrinter(ExpressionPrinter):
596
596
  self.printer.keyword('case')
597
597
 
598
598
  if isinstance(node.pattern, ast.MatchSequence):
599
- self.visit_MatchSequence(node.pattern, open=True)
599
+ self.visit_MatchSequence(node.pattern, omit_brackets=True)
600
600
  else:
601
601
  self.pattern(node.pattern)
602
602
 
@@ -626,10 +626,10 @@ class ModulePrinter(ExpressionPrinter):
626
626
  else:
627
627
  self.printer.identifier(node.name)
628
628
 
629
- def visit_MatchSequence(self, node, open=False):
629
+ def visit_MatchSequence(self, node, omit_brackets=False):
630
630
  assert isinstance(node, ast.MatchSequence)
631
631
 
632
- if len(node.patterns) < 2 or not open:
632
+ if len(node.patterns) < 2 or not omit_brackets:
633
633
  self.printer.delimiter('[')
634
634
 
635
635
  delimiter = Delimiter(self.printer)
@@ -637,7 +637,7 @@ class ModulePrinter(ExpressionPrinter):
637
637
  delimiter.new_item()
638
638
  self.pattern(pattern)
639
639
 
640
- if len(node.patterns) < 2 or not open:
640
+ if len(node.patterns) < 2 or not omit_brackets:
641
641
  self.printer.delimiter(']')
642
642
 
643
643
  def visit_MatchMapping(self, node):
@@ -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
 
@@ -35,7 +35,7 @@ def sorted_bindings(module):
35
35
  """
36
36
 
37
37
  def comp(tup):
38
- namespace, binding = tup
38
+ _namespace, binding = tup
39
39
  return binding.new_mention_count()
40
40
 
41
41
  return sorted(all_bindings(module), key=comp, reverse=True)
@@ -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]
@@ -103,7 +103,7 @@ class TokenPrinter(object):
103
103
  def __str__(self):
104
104
  """Return the output code."""
105
105
  return self._code
106
-
106
+
107
107
  def __unicode__(self):
108
108
  """Return the output code as unicode (for Python 2.7 compatibility)."""
109
109
  return self._code
@@ -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 [
@@ -27,18 +27,38 @@ class RemoveDebug(SuiteTransformer):
27
27
  if not isinstance(node, ast.If):
28
28
  return False
29
29
 
30
- if isinstance(node.test, ast.Name) and node.test.id == '__debug__':
31
- return True
30
+ def is_simple_debug_check():
31
+ # Simple case: if __debug__:
32
+ if isinstance(node.test, ast.Name) and node.test.id == '__debug__':
33
+ return True
34
+ return False
32
35
 
33
- if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Is) and self.constant_value(node.test.comparators[0]) is True:
34
- return True
36
+ def is_truthy_debug_comparison():
37
+ # Comparison case: if __debug__ is True / False / etc.
38
+ if not isinstance(node.test, ast.Compare):
39
+ return False
35
40
 
36
- if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.IsNot) and self.constant_value(node.test.comparators[0]) is False:
37
- return True
41
+ if not isinstance(node.test.left, ast.Name):
42
+ return False
38
43
 
39
- if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Eq) and self.constant_value(node.test.comparators[0]) is True:
40
- return True
44
+ if node.test.left.id != '__debug__':
45
+ return False
41
46
 
47
+ if len(node.test.ops) == 1:
48
+ op = node.test.ops[0]
49
+ comparator_value = self.constant_value(node.test.comparators[0])
50
+
51
+ if isinstance(op, ast.Is) and comparator_value is True:
52
+ return True
53
+ if isinstance(op, ast.IsNot) and comparator_value is False:
54
+ return True
55
+ if isinstance(op, ast.Eq) and comparator_value is True:
56
+ return True
57
+
58
+ return False
59
+
60
+ if is_simple_debug_check() or is_truthy_debug_comparison():
61
+ return True
42
62
  return False
43
63
 
44
64
  def suite(self, node_list, parent):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-minifier
3
- Version: 3.0.0
3
+ Version: 3.1.1
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/)
@@ -147,7 +148,7 @@ and outputs source code compatible with the version of the interpreter it is run
147
148
  This means that if you minify code written for Python 3.11 using python-minifier running with Python 3.12,
148
149
  the minified code may only run with Python 3.12.
149
150
 
150
- python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.13.
151
+ python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.14.
151
152
 
152
153
  ## Usage
153
154
 
@@ -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=KQyS9TGA1SoCs8uu4iks1IINFb_pEj7x38Q4oiERWnI,3381
5
+ python_minifier/ast_compat.py,sha256=DFJ7WmrEfYXfaby8EWt1aRlcqVPPoXD4Fh9Hnb9dLCU,2250
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=rBegwnqsjT_buJwSqK_piLwnfCwPyshzUUYgShoZPB4,22676
8
+ python_minifier/f_string.py,sha256=eXYXaxEMSB3iHY_M_RXYpeloJV-7TYnhWIGge0KYCk8,18314
9
9
  python_minifier/ministring.py,sha256=R0xaAZqMlLu1Jm0aBVSI0n8KZiZV7PTs4RE4UbCmTy8,4359
10
- python_minifier/module_printer.py,sha256=CZFZ3WDBgH57_Q2pmC0ySFdRcvQR_7uVQeXKfvT1BN0,25566
10
+ python_minifier/module_printer.py,sha256=zVdsUXm2GkdbIbNUyuavaOWRhn7FP93UqRibhuiokfQ,25602
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=MoVyC_ufvTXtI5bAIq6XdWDrkUTF6oYBmto3ILfq_KU,9983
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,8 +18,8 @@ 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/renamer.py,sha256=B9kNiTkWDw7i7tKQASrCZ4fXsjKf9AspFSUfuxQWZFs,6600
21
+ python_minifier/rename/rename_literals.py,sha256=qKJYCK8X7jEi7IW2PFiOqIjj0pn-1Cz1WaiBEGkUmIw,7488
22
+ python_minifier/rename/renamer.py,sha256=UQWrWrmntT0laNjTxEcH-cK4nZXAzi6LtJCI_STYZbo,6601
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
24
25
  python_minifier/transforms/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -28,7 +29,7 @@ python_minifier/transforms/remove_annotations.py,sha256=qUCdl42yvq1g7B43d88EOz6J
28
29
  python_minifier/transforms/remove_annotations_options.py,sha256=kzg-gEPwhrCZxsuzdpEWjqnhsyF89hnIPv1t-IRty64,1889
29
30
  python_minifier/transforms/remove_annotations_options.pyi,sha256=jZp8hUQ-BfqU1qA_nsPiluMM-1jznSX3pzbmysvKv-c,518
30
31
  python_minifier/transforms/remove_asserts.py,sha256=obccCHRSK6j5z0Slb4vVvK5kPVKobZNQo1HjJeX1N0g,738
31
- python_minifier/transforms/remove_debug.py,sha256=vuWJPFBMPYZEcSU8oRZ8w41tWl16B0mxpVvxEu4PiwY,1869
32
+ python_minifier/transforms/remove_debug.py,sha256=r5owpGW-T9yh7m5OmdIwCMiYF-1LPBQOQ2FYWRuhNWQ,2334
32
33
  python_minifier/transforms/remove_exception_brackets.py,sha256=hOUxrfsl9-u8M1kLbqJ6wR8PFtKWSmmyKdTDkh-XsDA,4214
33
34
  python_minifier/transforms/remove_explicit_return_none.py,sha256=drXry0dejtMWLZJCkGh4d7fc89MqiUVCXgaonn6Como,1301
34
35
  python_minifier/transforms/remove_literal_statements.py,sha256=mrxgSNtI2J1Nv9DaSiXMlrqvEj1lDAAY5_niWh8PmZg,1631
@@ -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.1.dist-info/LICENSE,sha256=FzsyDHb8pAZupSGFjIOuHcHMlFf6N-aIxq9gOYGbNyE,1069
41
+ python_minifier-3.1.1.dist-info/METADATA,sha256=szq8DMXjTaRYTE-tUW8lKyMiojWV2yyu6l2wGSaxu5k,6502
42
+ python_minifier-3.1.1.dist-info/WHEEL,sha256=1VPi6hfNQaRRNuEdK_3dv9o8COtLGnHWJghhj4CQ28k,92
43
+ python_minifier-3.1.1.dist-info/entry_points.txt,sha256=aS7ZUWQeeys8lAbrmmEa2__Bg-anH1tUMyi9cKdTbO4,60
44
+ python_minifier-3.1.1.dist-info/top_level.txt,sha256=4SRDfWKi_KMq7LDrjlzUFoDCs6INYPtxc1Pun4z8LsU,16
45
+ python_minifier-3.1.1.dist-info/zip-safe,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
46
+ python_minifier-3.1.1.dist-info/RECORD,,