python-minifier 2.11.3__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/__init__.py +2 -1
- python_minifier/__init__.pyi +3 -3
- python_minifier/__main__.py +67 -8
- python_minifier/ast_compare.py +15 -12
- python_minifier/ast_compat.py +2 -0
- python_minifier/expression_printer.py +7 -0
- python_minifier/f_string.py +151 -49
- python_minifier/module_printer.py +6 -2
- python_minifier/rename/rename_literals.py +7 -0
- python_minifier/t_string.py +330 -0
- python_minifier/token_printer.py +19 -1
- {python_minifier-2.11.3.dist-info → python_minifier-3.1.0.dist-info}/METADATA +5 -4
- {python_minifier-2.11.3.dist-info → python_minifier-3.1.0.dist-info}/RECORD +18 -17
- {python_minifier-2.11.3.dist-info → python_minifier-3.1.0.dist-info}/LICENSE +0 -0
- {python_minifier-2.11.3.dist-info → python_minifier-3.1.0.dist-info}/WHEEL +0 -0
- {python_minifier-2.11.3.dist-info → python_minifier-3.1.0.dist-info}/entry_points.txt +0 -0
- {python_minifier-2.11.3.dist-info → python_minifier-3.1.0.dist-info}/top_level.txt +0 -0
- {python_minifier-2.11.3.dist-info → python_minifier-3.1.0.dist-info}/zip-safe +0 -0
python_minifier/__init__.py
CHANGED
|
@@ -86,7 +86,8 @@ def minify(
|
|
|
86
86
|
:param str source: The python module source code
|
|
87
87
|
:param str filename: The original source filename if known
|
|
88
88
|
|
|
89
|
-
:param remove_annotations: Configures the removal of type annotations. True removes all annotations, False removes none.
|
|
89
|
+
:param remove_annotations: Configures the removal of type annotations. True removes all annotations, False removes none.
|
|
90
|
+
RemoveAnnotationsOptions can be used to configure the removal of specific annotations.
|
|
90
91
|
:type remove_annotations: bool or RemoveAnnotationsOptions
|
|
91
92
|
:param bool remove_pass: If Pass statements should be removed where possible
|
|
92
93
|
:param bool remove_literal_statements: If statements consisting of a single literal should be removed, including docstrings
|
python_minifier/__init__.pyi
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import ast
|
|
2
2
|
|
|
3
|
-
from typing import Any,
|
|
3
|
+
from typing import Any, List, Optional, Text, Union
|
|
4
4
|
|
|
5
5
|
from .transforms.remove_annotations_options import RemoveAnnotationsOptions as RemoveAnnotationsOptions
|
|
6
6
|
|
|
@@ -10,7 +10,7 @@ class UnstableMinification(RuntimeError):
|
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def minify(
|
|
13
|
-
source:
|
|
13
|
+
source: Union[str, bytes],
|
|
14
14
|
filename: Optional[str] = ...,
|
|
15
15
|
remove_annotations: Union[bool, RemoveAnnotationsOptions] = ...,
|
|
16
16
|
remove_pass: bool = ...,
|
|
@@ -36,7 +36,7 @@ def unparse(module: ast.Module) -> Text: ...
|
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def awslambda(
|
|
39
|
-
source:
|
|
39
|
+
source: Union[str, bytes],
|
|
40
40
|
filename: Optional[Text] = ...,
|
|
41
41
|
entrypoint: Optional[Text] = ...
|
|
42
42
|
) -> Text: ...
|
python_minifier/__main__.py
CHANGED
|
@@ -8,6 +8,18 @@ from python_minifier import minify
|
|
|
8
8
|
from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
|
|
9
9
|
|
|
10
10
|
|
|
11
|
+
class MinificationNotBeneficialError(Exception):
|
|
12
|
+
"""Raised when minification results in larger output than the original."""
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
def stdout_write_bytes(data):
|
|
16
|
+
"""Write bytes to stdout with proper Python 2.7/3.x compatibility."""
|
|
17
|
+
if sys.version_info >= (3, 0):
|
|
18
|
+
sys.stdout.buffer.write(data)
|
|
19
|
+
else:
|
|
20
|
+
sys.stdout.write(data)
|
|
21
|
+
|
|
22
|
+
|
|
11
23
|
if sys.version_info >= (3, 8):
|
|
12
24
|
from importlib import metadata
|
|
13
25
|
|
|
@@ -51,12 +63,23 @@ examples:
|
|
|
51
63
|
if len(args.path) == 1 and args.path[0] == '-':
|
|
52
64
|
# minify stdin
|
|
53
65
|
source = sys.stdin.buffer.read() if sys.version_info >= (3, 0) else sys.stdin.read()
|
|
54
|
-
|
|
66
|
+
try:
|
|
67
|
+
minified = do_minify(source, 'stdin', args)
|
|
68
|
+
except MinificationNotBeneficialError:
|
|
69
|
+
# Use original source when minification isn't beneficial
|
|
70
|
+
if args.output:
|
|
71
|
+
with open(args.output, 'wb') as f:
|
|
72
|
+
f.write(source)
|
|
73
|
+
else:
|
|
74
|
+
# Write original source to stdout
|
|
75
|
+
stdout_write_bytes(source)
|
|
76
|
+
return
|
|
77
|
+
|
|
55
78
|
if args.output:
|
|
56
|
-
with open(args.output, '
|
|
79
|
+
with open(args.output, 'wb') as f:
|
|
57
80
|
f.write(minified)
|
|
58
81
|
else:
|
|
59
|
-
|
|
82
|
+
stdout_write_bytes(minified)
|
|
60
83
|
|
|
61
84
|
else:
|
|
62
85
|
# minify source paths
|
|
@@ -67,16 +90,30 @@ examples:
|
|
|
67
90
|
with open(path, 'rb') as f:
|
|
68
91
|
source = f.read()
|
|
69
92
|
|
|
70
|
-
|
|
93
|
+
try:
|
|
94
|
+
minified = do_minify(source, path, args)
|
|
95
|
+
except MinificationNotBeneficialError:
|
|
96
|
+
# Use original source when minification isn't beneficial
|
|
97
|
+
if args.in_place:
|
|
98
|
+
# File is already the original, no need to write
|
|
99
|
+
pass
|
|
100
|
+
elif args.output:
|
|
101
|
+
# Write original source to output
|
|
102
|
+
with open(args.output, 'wb') as f:
|
|
103
|
+
f.write(source)
|
|
104
|
+
else:
|
|
105
|
+
# Write original source to stdout
|
|
106
|
+
stdout_write_bytes(source)
|
|
107
|
+
continue
|
|
71
108
|
|
|
72
109
|
if args.in_place:
|
|
73
|
-
with open(path, '
|
|
110
|
+
with open(path, 'wb') as f:
|
|
74
111
|
f.write(minified)
|
|
75
112
|
elif args.output:
|
|
76
|
-
with open(args.output, '
|
|
113
|
+
with open(args.output, 'wb') as f:
|
|
77
114
|
f.write(minified)
|
|
78
115
|
else:
|
|
79
|
-
|
|
116
|
+
stdout_write_bytes(minified)
|
|
80
117
|
|
|
81
118
|
|
|
82
119
|
def parse_args():
|
|
@@ -280,6 +317,15 @@ def source_modules(args):
|
|
|
280
317
|
|
|
281
318
|
|
|
282
319
|
def do_minify(source, filename, minification_args):
|
|
320
|
+
"""Minify Python source code with size-based fallback.
|
|
321
|
+
|
|
322
|
+
:param bytes source: Source code as bytes (from file 'rb' or stdin.buffer)
|
|
323
|
+
:param str filename: Filename for error reporting
|
|
324
|
+
:param argparse.Namespace minification_args: CLI arguments for minification options
|
|
325
|
+
:returns: Minified source code as UTF-8 bytes
|
|
326
|
+
:rtype: bytes
|
|
327
|
+
:raises MinificationNotBeneficialError: When minified output is larger than original
|
|
328
|
+
"""
|
|
283
329
|
|
|
284
330
|
preserve_globals = []
|
|
285
331
|
if minification_args.preserve_globals:
|
|
@@ -308,7 +354,7 @@ def do_minify(source, filename, minification_args):
|
|
|
308
354
|
remove_class_attribute_annotations=minification_args.remove_class_attribute_annotations,
|
|
309
355
|
)
|
|
310
356
|
|
|
311
|
-
|
|
357
|
+
minified_result = minify(
|
|
312
358
|
source,
|
|
313
359
|
filename=filename,
|
|
314
360
|
combine_imports=minification_args.combine_imports,
|
|
@@ -330,6 +376,19 @@ def do_minify(source, filename, minification_args):
|
|
|
330
376
|
constant_folding=minification_args.constant_folding
|
|
331
377
|
)
|
|
332
378
|
|
|
379
|
+
# Encode minified result to bytes for comparison and output
|
|
380
|
+
minified_bytes = minified_result.encode('utf-8')
|
|
381
|
+
|
|
382
|
+
# Check if environment variable forces minified output
|
|
383
|
+
if os.environ.get('PYMINIFY_FORCE_BEST_EFFORT'):
|
|
384
|
+
return minified_bytes
|
|
385
|
+
|
|
386
|
+
# Compare byte lengths for accurate size comparison
|
|
387
|
+
if len(minified_bytes) > len(source):
|
|
388
|
+
raise MinificationNotBeneficialError("Minified output is longer than original")
|
|
389
|
+
|
|
390
|
+
return minified_bytes
|
|
391
|
+
|
|
333
392
|
|
|
334
393
|
if __name__ == '__main__':
|
|
335
394
|
main()
|
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
|
|
|
@@ -77,26 +80,26 @@ def compare_ast(l_ast, r_ast):
|
|
|
77
80
|
% (type(l_ast), field, len(l_list), type(r_ast), field, len(r_list)),
|
|
78
81
|
)
|
|
79
82
|
|
|
80
|
-
for i,
|
|
81
|
-
if isinstance(
|
|
82
|
-
compare_ast(
|
|
83
|
-
elif
|
|
83
|
+
for i, left, right in zip(counter(), l_list, r_list):
|
|
84
|
+
if isinstance(left, ast.AST) or isinstance(right, ast.AST):
|
|
85
|
+
compare_ast(left, right)
|
|
86
|
+
elif left != right:
|
|
84
87
|
raise CompareError(
|
|
85
88
|
l_ast,
|
|
86
89
|
r_ast,
|
|
87
90
|
'Fields do not match! %s.%s[%i]=%r, %s.%s[%i]=%r'
|
|
88
|
-
% (type(l_ast), field, i,
|
|
91
|
+
% (type(l_ast), field, i, left, type(r_ast), field, i, right),
|
|
89
92
|
)
|
|
90
93
|
|
|
91
94
|
else:
|
|
92
|
-
|
|
93
|
-
|
|
95
|
+
left_field = getattr(l_ast, field, None)
|
|
96
|
+
right_field = getattr(r_ast, field, None)
|
|
94
97
|
|
|
95
|
-
if isinstance(
|
|
96
|
-
compare_ast(
|
|
97
|
-
elif
|
|
98
|
+
if isinstance(left_field, ast.AST) or isinstance(right_field, ast.AST):
|
|
99
|
+
compare_ast(left_field, right_field)
|
|
100
|
+
elif left_field != right_field:
|
|
98
101
|
raise CompareError(
|
|
99
102
|
l_ast,
|
|
100
103
|
r_ast,
|
|
101
|
-
'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field,
|
|
104
|
+
'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field, left_field, type(r_ast), field, right_field),
|
|
102
105
|
)
|
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
|
"""
|
|
@@ -272,37 +329,39 @@ class Str(object):
|
|
|
272
329
|
raise ValueError("Couldn't find a quote")
|
|
273
330
|
|
|
274
331
|
def _literals(self):
|
|
275
|
-
|
|
332
|
+
literal = ''
|
|
276
333
|
for c in self._s:
|
|
277
334
|
if not self._can_quote(c):
|
|
278
|
-
if
|
|
279
|
-
|
|
280
|
-
yield
|
|
281
|
-
|
|
335
|
+
if literal:
|
|
336
|
+
literal += self.current_quote
|
|
337
|
+
yield literal
|
|
338
|
+
literal = ''
|
|
282
339
|
|
|
283
340
|
self.current_quote = self._get_quote(c)
|
|
284
341
|
|
|
285
|
-
if
|
|
286
|
-
|
|
342
|
+
if literal == '':
|
|
343
|
+
literal += self.current_quote
|
|
287
344
|
|
|
288
|
-
if c == '\
|
|
289
|
-
|
|
345
|
+
if c == '\0':
|
|
346
|
+
literal += '\\x00'
|
|
347
|
+
elif c == '\n':
|
|
348
|
+
literal += '\\n'
|
|
290
349
|
elif c == '\r':
|
|
291
|
-
|
|
350
|
+
literal += '\\r'
|
|
292
351
|
elif c == '\\':
|
|
293
|
-
|
|
352
|
+
literal += '\\\\'
|
|
294
353
|
else:
|
|
295
|
-
|
|
354
|
+
literal += c
|
|
296
355
|
|
|
297
|
-
if
|
|
298
|
-
|
|
299
|
-
yield
|
|
356
|
+
if literal:
|
|
357
|
+
literal += self.current_quote
|
|
358
|
+
yield literal
|
|
300
359
|
|
|
301
360
|
def __str__(self):
|
|
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):
|
|
@@ -315,10 +374,10 @@ class Str(object):
|
|
|
315
374
|
for start_quote in self.allowed_quotes:
|
|
316
375
|
self.current_quote = start_quote
|
|
317
376
|
s = ''
|
|
318
|
-
for
|
|
319
|
-
if s and s[-1] ==
|
|
377
|
+
for literal in self._literals():
|
|
378
|
+
if s and s[-1] == literal[0]:
|
|
320
379
|
s += ' '
|
|
321
|
-
s +=
|
|
380
|
+
s += literal
|
|
322
381
|
|
|
323
382
|
if eval(s) == self._s:
|
|
324
383
|
candidates.append(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):
|
|
@@ -399,30 +486,45 @@ class Bytes(object):
|
|
|
399
486
|
raise ValueError("Couldn't find a quote")
|
|
400
487
|
|
|
401
488
|
def _literals(self):
|
|
402
|
-
|
|
489
|
+
literal = ''
|
|
403
490
|
for b in self._b:
|
|
404
491
|
if not self._can_quote(b):
|
|
405
|
-
if
|
|
406
|
-
|
|
407
|
-
yield
|
|
408
|
-
|
|
492
|
+
if literal:
|
|
493
|
+
literal += self.current_quote
|
|
494
|
+
yield literal
|
|
495
|
+
literal = ''
|
|
409
496
|
|
|
410
497
|
self.current_quote = self._get_quote(b)
|
|
411
498
|
|
|
412
|
-
if
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
499
|
+
if literal == '':
|
|
500
|
+
literal = 'b' + self.current_quote
|
|
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)
|
|
519
|
+
|
|
520
|
+
if literal:
|
|
521
|
+
literal += self.current_quote
|
|
522
|
+
yield literal
|
|
419
523
|
|
|
420
524
|
def __str__(self):
|
|
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:
|
|
@@ -434,10 +536,10 @@ class Bytes(object):
|
|
|
434
536
|
for start_quote in self.allowed_quotes:
|
|
435
537
|
self.current_quote = start_quote
|
|
436
538
|
s = ''
|
|
437
|
-
for
|
|
438
|
-
if s and s[-1] ==
|
|
539
|
+
for literal in self._literals():
|
|
540
|
+
if s and s[-1] == literal[0]:
|
|
439
541
|
s += ' '
|
|
440
|
-
s +=
|
|
542
|
+
s += literal
|
|
441
543
|
|
|
442
544
|
assert eval(s) == self._b
|
|
443
545
|
candidates.append(s)
|
|
@@ -28,11 +28,15 @@ class ModulePrinter(ExpressionPrinter):
|
|
|
28
28
|
assert isinstance(module, ast.Module)
|
|
29
29
|
|
|
30
30
|
self.visit_Module(module)
|
|
31
|
-
|
|
31
|
+
# On Python 2.7, preserve unicode strings to avoid encoding issues
|
|
32
|
+
code = unicode(self.printer) if sys.version_info[0] < 3 else str(self.printer)
|
|
33
|
+
return code.rstrip('\n' + self.indent_char + ';')
|
|
32
34
|
|
|
33
35
|
@property
|
|
34
36
|
def code(self):
|
|
35
|
-
|
|
37
|
+
# On Python 2.7, preserve unicode strings to avoid encoding issues
|
|
38
|
+
code = unicode(self.printer) if sys.version_info[0] < 3 else str(self.printer)
|
|
39
|
+
return code.rstrip('\n' + self.indent_char + ';')
|
|
36
40
|
|
|
37
41
|
# region Simple Statements
|
|
38
42
|
|
|
@@ -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
|
@@ -91,7 +91,11 @@ class TokenPrinter(object):
|
|
|
91
91
|
self._prefer_single_line = prefer_single_line
|
|
92
92
|
self._allow_invalid_num_warnings = allow_invalid_num_warnings
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
# Initialize as unicode string on Python 2.7 to handle Unicode content
|
|
95
|
+
if sys.version_info[0] < 3:
|
|
96
|
+
self._code = u''
|
|
97
|
+
else:
|
|
98
|
+
self._code = ''
|
|
95
99
|
self.indent = 0
|
|
96
100
|
self.unicode_literals = False
|
|
97
101
|
self.previous_token = TokenTypes.NoToken
|
|
@@ -99,6 +103,10 @@ class TokenPrinter(object):
|
|
|
99
103
|
def __str__(self):
|
|
100
104
|
"""Return the output code."""
|
|
101
105
|
return self._code
|
|
106
|
+
|
|
107
|
+
def __unicode__(self):
|
|
108
|
+
"""Return the output code as unicode (for Python 2.7 compatibility)."""
|
|
109
|
+
return self._code
|
|
102
110
|
|
|
103
111
|
def identifier(self, name):
|
|
104
112
|
"""Add an identifier to the output code."""
|
|
@@ -173,6 +181,16 @@ class TokenPrinter(object):
|
|
|
173
181
|
self._code += s
|
|
174
182
|
self.previous_token = TokenTypes.NonNumberLiteral
|
|
175
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
|
+
|
|
176
194
|
def delimiter(self, d):
|
|
177
195
|
"""Add a delimiter to the output code."""
|
|
178
196
|
assert d in [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: python-minifier
|
|
3
|
-
Version:
|
|
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/)
|
|
@@ -130,7 +131,7 @@ def handler(event,context):
|
|
|
130
131
|
|
|
131
132
|
## Why?
|
|
132
133
|
|
|
133
|
-
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
134
|
+
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
134
135
|
than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
|
|
135
136
|
|
|
136
137
|
## Installation
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
python_minifier/__init__.py,sha256=
|
|
2
|
-
python_minifier/__init__.pyi,sha256
|
|
3
|
-
python_minifier/__main__.py,sha256=
|
|
4
|
-
python_minifier/ast_compare.py,sha256=
|
|
5
|
-
python_minifier/ast_compat.py,sha256=
|
|
1
|
+
python_minifier/__init__.py,sha256=Q95ufDbd7hadjVtJZYclsJqX9S4NsKZSpqCui0Fw7oI,9542
|
|
2
|
+
python_minifier/__init__.pyi,sha256=-xLoKXbQsV7ko_YzyXL9WkI2vmGc6yn4AW5biK_GJXE,1233
|
|
3
|
+
python_minifier/__main__.py,sha256=r7_Vh9n-aoc4jd5CERKO3KqJeqXHeWoycDRlvV7xTFM,14053
|
|
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
|
-
python_minifier/module_printer.py,sha256=
|
|
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-
|
|
40
|
-
python_minifier-
|
|
41
|
-
python_minifier-
|
|
42
|
-
python_minifier-
|
|
43
|
-
python_minifier-
|
|
44
|
-
python_minifier-
|
|
45
|
-
python_minifier-
|
|
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
|