python-minifier 2.11.3__tar.gz → 3.0.0__tar.gz
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-2.11.3/src/python_minifier.egg-info → python_minifier-3.0.0}/PKG-INFO +15 -3
- {python_minifier-2.11.3 → python_minifier-3.0.0}/README.md +1 -1
- {python_minifier-2.11.3 → python_minifier-3.0.0}/setup.py +1 -1
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/__init__.py +2 -1
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/__init__.pyi +3 -3
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/__main__.py +67 -8
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ast_compare.py +11 -11
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/f_string.py +31 -31
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/module_printer.py +6 -2
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/token_printer.py +9 -1
- {python_minifier-2.11.3 → python_minifier-3.0.0/src/python_minifier.egg-info}/PKG-INFO +15 -3
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/SOURCES.txt +4 -1
- python_minifier-3.0.0/test/test_best_effort_cli.py +179 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_is_constant_node.py +10 -0
- python_minifier-3.0.0/test/test_unicode_cli.py +170 -0
- python_minifier-3.0.0/test/test_utf8_encoding.py +114 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/LICENSE +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/setup.cfg +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ast_compat.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ast_printer.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/expression_printer.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ministring.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/py.typed +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/__init__.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/bind_names.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/binding.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/mapper.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/name_generator.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/rename_literals.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/renamer.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/resolve_names.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/util.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/__init__.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/combine_imports.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/constant_folding.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_debug.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_pass.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/suite_transformer.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/util.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/top_level.txt +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/zip-safe +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_assignment_expressions.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_await_fstring.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_namedexpr.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_python2.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_python312.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_python313.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_python33.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_combine_imports.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_comprehension_rename.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_decorator_expressions.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_dict_expansion.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_folding.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_fstring.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_generic_types.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_hoist_literals.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_import.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_iterable_unpacking.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_match.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_match_rename.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_name_generator.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_nonlocal.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_posargs.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_preserve_shebang.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_annotations.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_assert.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_debug.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_exception_brackets.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_explicit_return_none.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_literal_statements.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_object.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_pass.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_rename_builtins.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_rename_locals.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_slice.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_type_param_defaults.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python_minifier
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.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
|
|
@@ -34,6 +34,18 @@ Classifier: Topic :: Software Development
|
|
|
34
34
|
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14
|
|
35
35
|
Description-Content-Type: text/markdown
|
|
36
36
|
License-File: LICENSE
|
|
37
|
+
Dynamic: author
|
|
38
|
+
Dynamic: author-email
|
|
39
|
+
Dynamic: classifier
|
|
40
|
+
Dynamic: description
|
|
41
|
+
Dynamic: description-content-type
|
|
42
|
+
Dynamic: home-page
|
|
43
|
+
Dynamic: keywords
|
|
44
|
+
Dynamic: license
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
Dynamic: project-url
|
|
47
|
+
Dynamic: requires-python
|
|
48
|
+
Dynamic: summary
|
|
37
49
|
|
|
38
50
|
# Python Minifier
|
|
39
51
|
|
|
@@ -130,7 +142,7 @@ def handler(event,context):
|
|
|
130
142
|
|
|
131
143
|
## Why?
|
|
132
144
|
|
|
133
|
-
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
145
|
+
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
134
146
|
than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
|
|
135
147
|
|
|
136
148
|
## Installation
|
|
@@ -93,7 +93,7 @@ def handler(event,context):
|
|
|
93
93
|
|
|
94
94
|
## Why?
|
|
95
95
|
|
|
96
|
-
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
96
|
+
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
97
97
|
than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
|
|
98
98
|
|
|
99
99
|
## Installation
|
|
@@ -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
|
|
@@ -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: ...
|
|
@@ -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()
|
|
@@ -77,26 +77,26 @@ def compare_ast(l_ast, r_ast):
|
|
|
77
77
|
% (type(l_ast), field, len(l_list), type(r_ast), field, len(r_list)),
|
|
78
78
|
)
|
|
79
79
|
|
|
80
|
-
for i,
|
|
81
|
-
if isinstance(
|
|
82
|
-
compare_ast(
|
|
83
|
-
elif
|
|
80
|
+
for i, left, right in zip(counter(), l_list, r_list):
|
|
81
|
+
if isinstance(left, ast.AST) or isinstance(right, ast.AST):
|
|
82
|
+
compare_ast(left, right)
|
|
83
|
+
elif left != right:
|
|
84
84
|
raise CompareError(
|
|
85
85
|
l_ast,
|
|
86
86
|
r_ast,
|
|
87
87
|
'Fields do not match! %s.%s[%i]=%r, %s.%s[%i]=%r'
|
|
88
|
-
% (type(l_ast), field, i,
|
|
88
|
+
% (type(l_ast), field, i, left, type(r_ast), field, i, right),
|
|
89
89
|
)
|
|
90
90
|
|
|
91
91
|
else:
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
left_field = getattr(l_ast, field, None)
|
|
93
|
+
right_field = getattr(r_ast, field, None)
|
|
94
94
|
|
|
95
|
-
if isinstance(
|
|
96
|
-
compare_ast(
|
|
97
|
-
elif
|
|
95
|
+
if isinstance(left_field, ast.AST) or isinstance(right_field, ast.AST):
|
|
96
|
+
compare_ast(left_field, right_field)
|
|
97
|
+
elif left_field != right_field:
|
|
98
98
|
raise CompareError(
|
|
99
99
|
l_ast,
|
|
100
100
|
r_ast,
|
|
101
|
-
'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field,
|
|
101
|
+
'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field, left_field, type(r_ast), field, right_field),
|
|
102
102
|
)
|
|
@@ -272,31 +272,31 @@ class Str(object):
|
|
|
272
272
|
raise ValueError("Couldn't find a quote")
|
|
273
273
|
|
|
274
274
|
def _literals(self):
|
|
275
|
-
|
|
275
|
+
literal = ''
|
|
276
276
|
for c in self._s:
|
|
277
277
|
if not self._can_quote(c):
|
|
278
|
-
if
|
|
279
|
-
|
|
280
|
-
yield
|
|
281
|
-
|
|
278
|
+
if literal:
|
|
279
|
+
literal += self.current_quote
|
|
280
|
+
yield literal
|
|
281
|
+
literal = ''
|
|
282
282
|
|
|
283
283
|
self.current_quote = self._get_quote(c)
|
|
284
284
|
|
|
285
|
-
if
|
|
286
|
-
|
|
285
|
+
if literal == '':
|
|
286
|
+
literal += self.current_quote
|
|
287
287
|
|
|
288
288
|
if c == '\n':
|
|
289
|
-
|
|
289
|
+
literal += '\\n'
|
|
290
290
|
elif c == '\r':
|
|
291
|
-
|
|
291
|
+
literal += '\\r'
|
|
292
292
|
elif c == '\\':
|
|
293
|
-
|
|
293
|
+
literal += '\\\\'
|
|
294
294
|
else:
|
|
295
|
-
|
|
295
|
+
literal += c
|
|
296
296
|
|
|
297
|
-
if
|
|
298
|
-
|
|
299
|
-
yield
|
|
297
|
+
if literal:
|
|
298
|
+
literal += self.current_quote
|
|
299
|
+
yield literal
|
|
300
300
|
|
|
301
301
|
def __str__(self):
|
|
302
302
|
if self._s == '':
|
|
@@ -315,10 +315,10 @@ class Str(object):
|
|
|
315
315
|
for start_quote in self.allowed_quotes:
|
|
316
316
|
self.current_quote = start_quote
|
|
317
317
|
s = ''
|
|
318
|
-
for
|
|
319
|
-
if s and s[-1] ==
|
|
318
|
+
for literal in self._literals():
|
|
319
|
+
if s and s[-1] == literal[0]:
|
|
320
320
|
s += ' '
|
|
321
|
-
s +=
|
|
321
|
+
s += literal
|
|
322
322
|
|
|
323
323
|
if eval(s) == self._s:
|
|
324
324
|
candidates.append(s)
|
|
@@ -399,23 +399,23 @@ class Bytes(object):
|
|
|
399
399
|
raise ValueError("Couldn't find a quote")
|
|
400
400
|
|
|
401
401
|
def _literals(self):
|
|
402
|
-
|
|
402
|
+
literal = ''
|
|
403
403
|
for b in self._b:
|
|
404
404
|
if not self._can_quote(b):
|
|
405
|
-
if
|
|
406
|
-
|
|
407
|
-
yield
|
|
408
|
-
|
|
405
|
+
if literal:
|
|
406
|
+
literal += self.current_quote
|
|
407
|
+
yield literal
|
|
408
|
+
literal = ''
|
|
409
409
|
|
|
410
410
|
self.current_quote = self._get_quote(b)
|
|
411
411
|
|
|
412
|
-
if
|
|
413
|
-
|
|
414
|
-
|
|
412
|
+
if literal == '':
|
|
413
|
+
literal = 'b' + self.current_quote
|
|
414
|
+
literal += chr(b)
|
|
415
415
|
|
|
416
|
-
if
|
|
417
|
-
|
|
418
|
-
yield
|
|
416
|
+
if literal:
|
|
417
|
+
literal += self.current_quote
|
|
418
|
+
yield literal
|
|
419
419
|
|
|
420
420
|
def __str__(self):
|
|
421
421
|
if self._b == b'':
|
|
@@ -434,10 +434,10 @@ class Bytes(object):
|
|
|
434
434
|
for start_quote in self.allowed_quotes:
|
|
435
435
|
self.current_quote = start_quote
|
|
436
436
|
s = ''
|
|
437
|
-
for
|
|
438
|
-
if s and s[-1] ==
|
|
437
|
+
for literal in self._literals():
|
|
438
|
+
if s and s[-1] == literal[0]:
|
|
439
439
|
s += ' '
|
|
440
|
-
s +=
|
|
440
|
+
s += literal
|
|
441
441
|
|
|
442
442
|
assert eval(s) == self._b
|
|
443
443
|
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
|
|
|
@@ -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."""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: python_minifier
|
|
3
|
-
Version:
|
|
3
|
+
Version: 3.0.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
|
|
@@ -34,6 +34,18 @@ Classifier: Topic :: Software Development
|
|
|
34
34
|
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14
|
|
35
35
|
Description-Content-Type: text/markdown
|
|
36
36
|
License-File: LICENSE
|
|
37
|
+
Dynamic: author
|
|
38
|
+
Dynamic: author-email
|
|
39
|
+
Dynamic: classifier
|
|
40
|
+
Dynamic: description
|
|
41
|
+
Dynamic: description-content-type
|
|
42
|
+
Dynamic: home-page
|
|
43
|
+
Dynamic: keywords
|
|
44
|
+
Dynamic: license
|
|
45
|
+
Dynamic: license-file
|
|
46
|
+
Dynamic: project-url
|
|
47
|
+
Dynamic: requires-python
|
|
48
|
+
Dynamic: summary
|
|
37
49
|
|
|
38
50
|
# Python Minifier
|
|
39
51
|
|
|
@@ -130,7 +142,7 @@ def handler(event,context):
|
|
|
130
142
|
|
|
131
143
|
## Why?
|
|
132
144
|
|
|
133
|
-
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
145
|
+
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
134
146
|
than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
|
|
135
147
|
|
|
136
148
|
## Installation
|
|
@@ -47,6 +47,7 @@ src/python_minifier/transforms/remove_posargs.py
|
|
|
47
47
|
src/python_minifier/transforms/suite_transformer.py
|
|
48
48
|
test/test_assignment_expressions.py
|
|
49
49
|
test/test_await_fstring.py
|
|
50
|
+
test/test_best_effort_cli.py
|
|
50
51
|
test/test_bind_names.py
|
|
51
52
|
test/test_bind_names_namedexpr.py
|
|
52
53
|
test/test_bind_names_python2.py
|
|
@@ -81,4 +82,6 @@ test/test_remove_pass.py
|
|
|
81
82
|
test/test_rename_builtins.py
|
|
82
83
|
test/test_rename_locals.py
|
|
83
84
|
test/test_slice.py
|
|
84
|
-
test/test_type_param_defaults.py
|
|
85
|
+
test/test_type_param_defaults.py
|
|
86
|
+
test/test_unicode_cli.py
|
|
87
|
+
test/test_utf8_encoding.py
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
"""Tests for CLI size-based output selection (best effort) functionality."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
from subprocess_compat import run_subprocess, safe_decode
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_returns_minified_when_smaller():
|
|
10
|
+
"""Test CLI returns minified output when it's smaller than original."""
|
|
11
|
+
code = '''
|
|
12
|
+
def hello_world():
|
|
13
|
+
"""A simple function."""
|
|
14
|
+
print("Hello, world!")
|
|
15
|
+
return None
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
hello_world()
|
|
19
|
+
'''
|
|
20
|
+
|
|
21
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
22
|
+
f.write(code)
|
|
23
|
+
temp_file = f.name
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
env = os.environ.copy()
|
|
27
|
+
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
|
|
28
|
+
|
|
29
|
+
result = run_subprocess([
|
|
30
|
+
sys.executable, '-m', 'python_minifier', temp_file
|
|
31
|
+
], timeout=30, env=env)
|
|
32
|
+
|
|
33
|
+
assert result.returncode == 0
|
|
34
|
+
|
|
35
|
+
stdout_text = safe_decode(result.stdout)
|
|
36
|
+
assert len(stdout_text) < len(code)
|
|
37
|
+
|
|
38
|
+
finally:
|
|
39
|
+
os.unlink(temp_file)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_returns_original_when_longer():
|
|
43
|
+
"""Test CLI returns original code when minified output would be longer."""
|
|
44
|
+
code = 'True if 0in x else False'
|
|
45
|
+
|
|
46
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
47
|
+
f.write(code)
|
|
48
|
+
temp_file = f.name
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
env = os.environ.copy()
|
|
52
|
+
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
|
|
53
|
+
|
|
54
|
+
result = run_subprocess([
|
|
55
|
+
sys.executable, '-m', 'python_minifier', temp_file
|
|
56
|
+
], timeout=30, env=env)
|
|
57
|
+
|
|
58
|
+
assert result.returncode == 0
|
|
59
|
+
|
|
60
|
+
stdout_text = safe_decode(result.stdout)
|
|
61
|
+
assert stdout_text == code
|
|
62
|
+
|
|
63
|
+
finally:
|
|
64
|
+
os.unlink(temp_file)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def test_force_minified_with_env_var():
|
|
68
|
+
"""Test environment variable forces minified output regardless of size."""
|
|
69
|
+
code = 'True if 0in x else False'
|
|
70
|
+
expected_output = 'True if 0 in x else False'
|
|
71
|
+
|
|
72
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
73
|
+
f.write(code)
|
|
74
|
+
temp_file = f.name
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
env = os.environ.copy()
|
|
78
|
+
env['PYMINIFY_FORCE_BEST_EFFORT'] = '1'
|
|
79
|
+
|
|
80
|
+
result = run_subprocess([
|
|
81
|
+
sys.executable, '-m', 'python_minifier', temp_file
|
|
82
|
+
], timeout=30, env=env)
|
|
83
|
+
|
|
84
|
+
assert result.returncode == 0
|
|
85
|
+
|
|
86
|
+
stdout_text = safe_decode(result.stdout)
|
|
87
|
+
assert stdout_text == expected_output
|
|
88
|
+
|
|
89
|
+
finally:
|
|
90
|
+
os.unlink(temp_file)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_stdin_behavior():
|
|
94
|
+
"""Test size-based logic works with stdin input."""
|
|
95
|
+
code = 'True if 0in x else False'
|
|
96
|
+
expected_output = 'True if 0 in x else False'
|
|
97
|
+
|
|
98
|
+
# Without env var - should return original
|
|
99
|
+
env = os.environ.copy()
|
|
100
|
+
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
|
|
101
|
+
|
|
102
|
+
result = run_subprocess([
|
|
103
|
+
sys.executable, '-m', 'python_minifier', '-'
|
|
104
|
+
], input_data=code, timeout=30, env=env)
|
|
105
|
+
|
|
106
|
+
assert result.returncode == 0
|
|
107
|
+
stdout_text = safe_decode(result.stdout)
|
|
108
|
+
assert stdout_text == code
|
|
109
|
+
|
|
110
|
+
# With env var - should return minified
|
|
111
|
+
env['PYMINIFY_FORCE_BEST_EFFORT'] = '1'
|
|
112
|
+
|
|
113
|
+
result = run_subprocess([
|
|
114
|
+
sys.executable, '-m', 'python_minifier', '-'
|
|
115
|
+
], input_data=code, timeout=30, env=env)
|
|
116
|
+
|
|
117
|
+
assert result.returncode == 0
|
|
118
|
+
stdout_text = safe_decode(result.stdout)
|
|
119
|
+
assert stdout_text == expected_output
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_output_file_behavior():
|
|
123
|
+
"""Test size-based logic works with --output flag."""
|
|
124
|
+
code = 'True if 0in x else False'
|
|
125
|
+
|
|
126
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as input_file:
|
|
127
|
+
input_file.write(code)
|
|
128
|
+
input_filename = input_file.name
|
|
129
|
+
|
|
130
|
+
with tempfile.NamedTemporaryFile(delete=False) as output_file:
|
|
131
|
+
output_filename = output_file.name
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
env = os.environ.copy()
|
|
135
|
+
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
|
|
136
|
+
|
|
137
|
+
result = run_subprocess([
|
|
138
|
+
sys.executable, '-m', 'python_minifier',
|
|
139
|
+
input_filename, '--output', output_filename
|
|
140
|
+
], timeout=30, env=env)
|
|
141
|
+
|
|
142
|
+
assert result.returncode == 0
|
|
143
|
+
|
|
144
|
+
with open(output_filename, 'r') as f:
|
|
145
|
+
output_content = f.read()
|
|
146
|
+
|
|
147
|
+
assert output_content == code
|
|
148
|
+
|
|
149
|
+
finally:
|
|
150
|
+
os.unlink(input_filename)
|
|
151
|
+
os.unlink(output_filename)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def test_in_place_behavior():
|
|
155
|
+
"""Test size-based logic works with --in-place flag."""
|
|
156
|
+
code = 'True if 0in x else False'
|
|
157
|
+
|
|
158
|
+
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
|
|
159
|
+
f.write(code)
|
|
160
|
+
temp_file = f.name
|
|
161
|
+
|
|
162
|
+
try:
|
|
163
|
+
env = os.environ.copy()
|
|
164
|
+
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
|
|
165
|
+
|
|
166
|
+
result = run_subprocess([
|
|
167
|
+
sys.executable, '-m', 'python_minifier',
|
|
168
|
+
temp_file, '--in-place'
|
|
169
|
+
], timeout=30, env=env)
|
|
170
|
+
|
|
171
|
+
assert result.returncode == 0
|
|
172
|
+
|
|
173
|
+
with open(temp_file, 'r') as f:
|
|
174
|
+
modified_content = f.read()
|
|
175
|
+
|
|
176
|
+
assert modified_content == code
|
|
177
|
+
|
|
178
|
+
finally:
|
|
179
|
+
os.unlink(temp_file)
|
|
@@ -7,6 +7,11 @@ import ast
|
|
|
7
7
|
from python_minifier.util import is_constant_node
|
|
8
8
|
|
|
9
9
|
|
|
10
|
+
@pytest.mark.filterwarnings("ignore:ast.Str is deprecated:DeprecationWarning")
|
|
11
|
+
@pytest.mark.filterwarnings("ignore:ast.Bytes is deprecated:DeprecationWarning")
|
|
12
|
+
@pytest.mark.filterwarnings("ignore:ast.Num is deprecated:DeprecationWarning")
|
|
13
|
+
@pytest.mark.filterwarnings("ignore:ast.NameConstant is deprecated:DeprecationWarning")
|
|
14
|
+
@pytest.mark.filterwarnings("ignore:ast.Ellipsis is deprecated:DeprecationWarning")
|
|
10
15
|
def test_type_nodes():
|
|
11
16
|
assert is_constant_node(ast.Str('a'), ast.Str)
|
|
12
17
|
|
|
@@ -28,6 +33,11 @@ def test_type_nodes():
|
|
|
28
33
|
assert is_constant_node(ast.Ellipsis(), ast.Ellipsis)
|
|
29
34
|
|
|
30
35
|
|
|
36
|
+
@pytest.mark.filterwarnings("ignore:ast.Str is deprecated:DeprecationWarning")
|
|
37
|
+
@pytest.mark.filterwarnings("ignore:ast.Bytes is deprecated:DeprecationWarning")
|
|
38
|
+
@pytest.mark.filterwarnings("ignore:ast.Num is deprecated:DeprecationWarning")
|
|
39
|
+
@pytest.mark.filterwarnings("ignore:ast.NameConstant is deprecated:DeprecationWarning")
|
|
40
|
+
@pytest.mark.filterwarnings("ignore:ast.Ellipsis is deprecated:DeprecationWarning")
|
|
31
41
|
def test_constant_nodes():
|
|
32
42
|
# only test on python 3.8+
|
|
33
43
|
if sys.version_info < (3, 8):
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import tempfile
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
import codecs
|
|
6
|
+
|
|
7
|
+
from subprocess_compat import run_subprocess, safe_decode
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_cli_output_flag_with_unicode():
|
|
11
|
+
"""
|
|
12
|
+
Tests that the CLI tool can write Unicode characters to files using --output flag.
|
|
13
|
+
"""
|
|
14
|
+
# Minimal source with all problematic Unicode characters from reported issues
|
|
15
|
+
source_code = u'print(u"❌ ✓ 🐍 Привет © ∀")'
|
|
16
|
+
|
|
17
|
+
source_file = tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False)
|
|
18
|
+
source_file.write(source_code.encode('utf-8'))
|
|
19
|
+
source_file.close()
|
|
20
|
+
|
|
21
|
+
output_path = source_file.name + '.min.py'
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
# Run pyminify CLI with --output flag
|
|
25
|
+
result = run_subprocess([
|
|
26
|
+
sys.executable, '-m', 'python_minifier',
|
|
27
|
+
source_file.name, '--output', output_path
|
|
28
|
+
], timeout=30)
|
|
29
|
+
|
|
30
|
+
assert result.returncode == 0, "CLI failed with encoding error: {}".format(safe_decode(result.stderr))
|
|
31
|
+
|
|
32
|
+
# Verify the output file was created and contains Unicode characters
|
|
33
|
+
with codecs.open(output_path, 'r', encoding='utf-8') as f:
|
|
34
|
+
minified_content = f.read()
|
|
35
|
+
|
|
36
|
+
# Verify problematic Unicode characters are preserved
|
|
37
|
+
if hasattr(sys, 'pypy_version_info') and sys.version_info[0] >= 3:
|
|
38
|
+
# PyPy3: Unicode characters may be escaped as \\u escapes
|
|
39
|
+
assert "\\u274c" in minified_content
|
|
40
|
+
assert "✓" in minified_content
|
|
41
|
+
assert "\\U0001f40d" in minified_content
|
|
42
|
+
assert "Привет" in minified_content
|
|
43
|
+
assert "©" in minified_content
|
|
44
|
+
assert "∀" in minified_content
|
|
45
|
+
elif hasattr(sys, 'pypy_version_info') and sys.version_info[0] < 3:
|
|
46
|
+
# PyPy2: Unicode characters appear as UTF-8 byte sequences
|
|
47
|
+
assert "\\xe2\\x9d\\x8c" in minified_content # ❌
|
|
48
|
+
assert "\\xe2\\x9c\\x93" in minified_content # ✓
|
|
49
|
+
assert "\\xf0\\x9f\\x90\\x8d" in minified_content # 🐍
|
|
50
|
+
elif sys.version_info[0] >= 3:
|
|
51
|
+
# CPython 3: Unicode characters should appear literally
|
|
52
|
+
assert "❌" in minified_content
|
|
53
|
+
assert "✓" in minified_content
|
|
54
|
+
assert "🐍" in minified_content
|
|
55
|
+
assert "Привет" in minified_content
|
|
56
|
+
assert "©" in minified_content
|
|
57
|
+
assert "∀" in minified_content
|
|
58
|
+
else:
|
|
59
|
+
# Python 2.7: Check for escaped sequences or use Unicode literals
|
|
60
|
+
assert u"\\xe2\\x9d\\x8c" in minified_content
|
|
61
|
+
assert u"\\xe2\\x9c\\x93" in minified_content
|
|
62
|
+
assert u"\\xf0\\x9f\\x90\\x8d" in minified_content
|
|
63
|
+
|
|
64
|
+
finally:
|
|
65
|
+
# Cleanup
|
|
66
|
+
if os.path.exists(source_file.name):
|
|
67
|
+
os.unlink(source_file.name)
|
|
68
|
+
if os.path.exists(output_path):
|
|
69
|
+
os.unlink(output_path)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_cli_in_place_with_unicode():
|
|
73
|
+
"""
|
|
74
|
+
Tests that the CLI tool can write Unicode characters to files using --in-place flag.
|
|
75
|
+
"""
|
|
76
|
+
source_code = u'print(u"❌ ✓ 🐍 Привет © ∀")'
|
|
77
|
+
|
|
78
|
+
temp_file = tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False)
|
|
79
|
+
temp_file.write(source_code.encode('utf-8'))
|
|
80
|
+
temp_file.close()
|
|
81
|
+
|
|
82
|
+
try:
|
|
83
|
+
# Run pyminify with --in-place flag
|
|
84
|
+
result = run_subprocess([
|
|
85
|
+
sys.executable, '-m', 'python_minifier',
|
|
86
|
+
temp_file.name, '--in-place'
|
|
87
|
+
], timeout=30)
|
|
88
|
+
|
|
89
|
+
assert result.returncode == 0, "CLI failed with encoding error: {}".format(safe_decode(result.stderr))
|
|
90
|
+
|
|
91
|
+
with codecs.open(temp_file.name, 'r', encoding='utf-8') as f:
|
|
92
|
+
content = f.read()
|
|
93
|
+
|
|
94
|
+
if hasattr(sys, 'pypy_version_info') and sys.version_info[0] >= 3:
|
|
95
|
+
# PyPy3: Unicode characters may be escaped as \\u escapes
|
|
96
|
+
assert "✓" in content
|
|
97
|
+
assert "\\u274c" in content # ❌
|
|
98
|
+
assert "\\U0001f40d" in content # 🐍
|
|
99
|
+
assert "Привет" in content
|
|
100
|
+
assert "©" in content
|
|
101
|
+
assert "∀" in content
|
|
102
|
+
elif hasattr(sys, 'pypy_version_info') and sys.version_info[0] < 3:
|
|
103
|
+
# PyPy2: Unicode characters appear as UTF-8 byte sequences
|
|
104
|
+
assert "\\xe2\\x9c\\x93" in content # ✓
|
|
105
|
+
assert "\\xe2\\x9d\\x8c" in content # ❌
|
|
106
|
+
assert "\\xf0\\x9f\\x90\\x8d" in content # 🐍
|
|
107
|
+
elif sys.version_info[0] >= 3:
|
|
108
|
+
# CPython 3: Unicode characters should appear literally
|
|
109
|
+
assert "✓" in content
|
|
110
|
+
assert "❌" in content
|
|
111
|
+
assert "🐍" in content
|
|
112
|
+
assert "Привет" in content
|
|
113
|
+
assert "©" in content
|
|
114
|
+
assert "∀" in content
|
|
115
|
+
else:
|
|
116
|
+
# Python 2.7: Unicode characters appear as escaped sequences
|
|
117
|
+
assert "\\xe2\\x9d\\x8c" in content # ❌
|
|
118
|
+
assert "\\xe2\\x9c\\x93" in content # ✓
|
|
119
|
+
assert "\\xf0\\x9f\\x90\\x8d" in content # 🐍
|
|
120
|
+
|
|
121
|
+
finally:
|
|
122
|
+
if os.path.exists(temp_file.name):
|
|
123
|
+
os.unlink(temp_file.name)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_cli_stdout_with_unicode():
|
|
127
|
+
"""
|
|
128
|
+
Tests that the CLI tool can write Unicode characters to stdout.
|
|
129
|
+
"""
|
|
130
|
+
source_code = u'print(u"❌ ✓ 🐍 Привет © ∀")'
|
|
131
|
+
|
|
132
|
+
temp_file = tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False)
|
|
133
|
+
temp_file.write(source_code.encode('utf-8'))
|
|
134
|
+
temp_file.close()
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
# Run without --output or --in-place (should output to stdout)
|
|
138
|
+
result = run_subprocess([
|
|
139
|
+
sys.executable, '-m', 'python_minifier', temp_file.name
|
|
140
|
+
], timeout=30)
|
|
141
|
+
|
|
142
|
+
assert result.returncode == 0, "Stdout output failed: {}".format(safe_decode(result.stderr))
|
|
143
|
+
|
|
144
|
+
stdout_text = safe_decode(result.stdout)
|
|
145
|
+
|
|
146
|
+
if hasattr(sys, 'pypy_version_info') and sys.version_info[0] >= 3:
|
|
147
|
+
# PyPy3: Unicode characters may be escaped as \\u escapes
|
|
148
|
+
assert "\\u274c" in stdout_text # ❌
|
|
149
|
+
assert "✓" in stdout_text # ✓
|
|
150
|
+
assert "\\U0001f40d" in stdout_text # 🐍
|
|
151
|
+
assert "Привет" in stdout_text # Привет
|
|
152
|
+
assert "©" in stdout_text # ©
|
|
153
|
+
assert "∀" in stdout_text # ∀
|
|
154
|
+
elif sys.version_info[0] >= 3:
|
|
155
|
+
# CPython 3: Unicode characters should appear literally
|
|
156
|
+
assert "❌" in stdout_text
|
|
157
|
+
assert "✓" in stdout_text
|
|
158
|
+
assert "🐍" in stdout_text
|
|
159
|
+
assert "Привет" in stdout_text
|
|
160
|
+
assert "©" in stdout_text
|
|
161
|
+
assert "∀" in stdout_text
|
|
162
|
+
else:
|
|
163
|
+
# Python 2.7: Unicode characters appear as escaped sequences
|
|
164
|
+
assert "\\xe2\\x9d\\x8c" in stdout_text # ❌
|
|
165
|
+
assert "\\xe2\\x9c\\x93" in stdout_text # ✓
|
|
166
|
+
assert "\\xf0\\x9f\\x90\\x8d" in stdout_text # 🐍
|
|
167
|
+
|
|
168
|
+
finally:
|
|
169
|
+
if os.path.exists(temp_file.name):
|
|
170
|
+
os.unlink(temp_file.name)
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import pytest
|
|
3
|
+
import python_minifier
|
|
4
|
+
import tempfile
|
|
5
|
+
import os
|
|
6
|
+
import codecs
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_minify_utf8_file():
|
|
10
|
+
"""Test minifying a Python file with UTF-8 characters not in Windows default encoding."""
|
|
11
|
+
|
|
12
|
+
# Create Python source with UTF-8 characters that are not in Windows-1252
|
|
13
|
+
# Using Greek letters, Cyrillic, and mathematical symbols
|
|
14
|
+
source_code = u'''"""
|
|
15
|
+
This module contains UTF-8 characters that are not in Windows-1252 encoding:
|
|
16
|
+
- Greek: α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω
|
|
17
|
+
- Cyrillic: а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я
|
|
18
|
+
- Mathematical: ∀ ∃ ∈ ∉ ∅ ∞ ∑ ∏ √ ∫ ∇ ∂ ≠ ≤ ≥ ≈ ≡ ⊂ ⊃ ⊆ ⊇
|
|
19
|
+
- Arrows: ← → ↑ ↓ ↔ ↕ ↖ ↗ ↘ ↙
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def greet_in_greek():
|
|
23
|
+
return u"Γεια σας κόσμος" # "Hello world" in Greek
|
|
24
|
+
|
|
25
|
+
def mathematical_formula():
|
|
26
|
+
# Using mathematical symbols in comments
|
|
27
|
+
# ∀x ∈ ℝ: x² ≥ 0
|
|
28
|
+
return u"∑ from i=1 to ∞ of 1/i² = π²/6"
|
|
29
|
+
|
|
30
|
+
def arrow_symbols():
|
|
31
|
+
directions = {
|
|
32
|
+
u"left": u"←",
|
|
33
|
+
u"right": u"→",
|
|
34
|
+
u"up": u"↑",
|
|
35
|
+
u"down": u"↓"
|
|
36
|
+
}
|
|
37
|
+
return directions
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
print(greet_in_greek())
|
|
41
|
+
print(greet_in_russian())
|
|
42
|
+
print(mathematical_formula())
|
|
43
|
+
print(arrow_symbols())
|
|
44
|
+
'''
|
|
45
|
+
|
|
46
|
+
# Write to temporary file with UTF-8 encoding
|
|
47
|
+
# Python 2.7 doesn't support encoding parameter, so use binary mode
|
|
48
|
+
with tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False) as f:
|
|
49
|
+
f.write(source_code.encode('utf-8'))
|
|
50
|
+
temp_file = f.name
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
# Read the file and minify it
|
|
54
|
+
# Python 2.7 doesn't support encoding parameter in open()
|
|
55
|
+
with codecs.open(temp_file, 'r', encoding='utf-8') as f:
|
|
56
|
+
original_content = f.read()
|
|
57
|
+
|
|
58
|
+
# This should work - minify the UTF-8 content
|
|
59
|
+
minified = python_minifier.minify(original_content)
|
|
60
|
+
|
|
61
|
+
# Verify the minified code still contains the UTF-8 characters
|
|
62
|
+
# On Python 2.7, Unicode characters in string literals are escaped but preserved
|
|
63
|
+
# Test by executing the minified code and checking the actual values
|
|
64
|
+
minified_globals = {}
|
|
65
|
+
exec(minified, minified_globals)
|
|
66
|
+
|
|
67
|
+
# The minified code should contain the same functions that return Unicode
|
|
68
|
+
assert 'greet_in_greek' in minified_globals
|
|
69
|
+
assert u"Γεια σας κόσμος" == minified_globals['greet_in_greek']()
|
|
70
|
+
|
|
71
|
+
# Test that mathematical symbols are also preserved
|
|
72
|
+
assert 'mathematical_formula' in minified_globals
|
|
73
|
+
assert u"∑ from i=1 to ∞" in minified_globals['mathematical_formula']()
|
|
74
|
+
|
|
75
|
+
finally:
|
|
76
|
+
# Clean up
|
|
77
|
+
os.unlink(temp_file)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_minify_utf8_file_direct():
|
|
81
|
+
"""Test minifying a file directly with UTF-8 characters."""
|
|
82
|
+
|
|
83
|
+
# Create Python source with UTF-8 characters
|
|
84
|
+
source_code = u'''# UTF-8 test file
|
|
85
|
+
def emoji_function():
|
|
86
|
+
"""Function with emoji and special characters: 🐍 ∆ ∑ ∫ ∞"""
|
|
87
|
+
return u"Python is 🐍 awesome! Math symbols: ∆x ≈ 0, ∑∞ = ∞"
|
|
88
|
+
|
|
89
|
+
class UnicodeClass:
|
|
90
|
+
"""Class with unicode: ñ ü ö ä ë ï ÿ"""
|
|
91
|
+
def __init__(self):
|
|
92
|
+
self.message = u"Héllö Wörld with àccénts!"
|
|
93
|
+
|
|
94
|
+
def get_symbols(self):
|
|
95
|
+
return u"Symbols: ™ © ® ° ± × ÷ ≠ ≤ ≥"
|
|
96
|
+
'''
|
|
97
|
+
|
|
98
|
+
# Test direct minification
|
|
99
|
+
minified = python_minifier.minify(source_code)
|
|
100
|
+
|
|
101
|
+
# Verify UTF-8 characters are preserved by executing the minified code
|
|
102
|
+
minified_globals = {}
|
|
103
|
+
exec(minified, minified_globals)
|
|
104
|
+
|
|
105
|
+
# Test that the functions return the correct Unicode strings
|
|
106
|
+
assert u"🐍" in minified_globals['emoji_function']()
|
|
107
|
+
assert u"∆" in minified_globals['emoji_function']()
|
|
108
|
+
|
|
109
|
+
# Test the class
|
|
110
|
+
unicode_obj = minified_globals['UnicodeClass']()
|
|
111
|
+
assert u"Héllö" in unicode_obj.message
|
|
112
|
+
assert u"àccénts" in unicode_obj.message
|
|
113
|
+
assert u"™" in unicode_obj.get_symbols()
|
|
114
|
+
assert u"©" in unicode_obj.get_symbols()
|
|
File without changes
|
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ast_annotation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/name_generator.py
RENAMED
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/rename_literals.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/resolve_names.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/combine_imports.py
RENAMED
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/constant_folding.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_asserts.py
RENAMED
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_debug.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_pass.py
RENAMED
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_posargs.py
RENAMED
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/suite_transformer.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
{python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/entry_points.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|