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.
- python_minifier/ast_compare.py +4 -1
- python_minifier/ast_compat.py +2 -0
- python_minifier/expression_printer.py +7 -0
- python_minifier/f_string.py +120 -18
- python_minifier/rename/rename_literals.py +7 -0
- python_minifier/t_string.py +330 -0
- python_minifier/token_printer.py +10 -0
- {python_minifier-3.0.0.dist-info → python_minifier-3.1.0.dist-info}/METADATA +4 -3
- {python_minifier-3.0.0.dist-info → python_minifier-3.1.0.dist-info}/RECORD +14 -13
- {python_minifier-3.0.0.dist-info → python_minifier-3.1.0.dist-info}/LICENSE +0 -0
- {python_minifier-3.0.0.dist-info → python_minifier-3.1.0.dist-info}/WHEEL +0 -0
- {python_minifier-3.0.0.dist-info → python_minifier-3.1.0.dist-info}/entry_points.txt +0 -0
- {python_minifier-3.0.0.dist-info → python_minifier-3.1.0.dist-info}/top_level.txt +0 -0
- {python_minifier-3.0.0.dist-info → python_minifier-3.1.0.dist-info}/zip-safe +0 -0
python_minifier/ast_compare.py
CHANGED
|
@@ -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
|
|
python_minifier/ast_compat.py
CHANGED
|
@@ -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(':=')
|
python_minifier/f_string.py
CHANGED
|
@@ -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
|
|
62
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
x + y for x in
|
|
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
|
-
|
|
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
|
|
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 == '\
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
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]
|
python_minifier/token_printer.py
CHANGED
|
@@ -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.
|
|
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.
|
|
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.
|
|
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=
|
|
5
|
-
python_minifier/ast_compat.py,sha256=
|
|
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=
|
|
8
|
-
python_minifier/f_string.py,sha256=
|
|
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/
|
|
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=
|
|
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.
|
|
40
|
-
python_minifier-3.
|
|
41
|
-
python_minifier-3.
|
|
42
|
-
python_minifier-3.
|
|
43
|
-
python_minifier-3.
|
|
44
|
-
python_minifier-3.
|
|
45
|
-
python_minifier-3.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|