python-minifier 2.11.3__tar.gz → 3.1.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-3.1.0/MANIFEST.in +21 -0
- {python_minifier-2.11.3/src/python_minifier.egg-info → python_minifier-3.1.0}/PKG-INFO +18 -5
- {python_minifier-2.11.3 → python_minifier-3.1.0}/README.md +2 -2
- {python_minifier-2.11.3 → python_minifier-3.1.0}/setup.py +3 -3
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/__init__.py +2 -1
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/__init__.pyi +3 -3
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/__main__.py +67 -8
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ast_compare.py +15 -12
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ast_compat.py +2 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/expression_printer.py +7 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/f_string.py +151 -49
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/module_printer.py +6 -2
- python_minifier-3.1.0/src/python_minifier/rename/README.md +93 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/rename_literals.py +7 -0
- python_minifier-3.1.0/src/python_minifier/t_string.py +330 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/token_printer.py +19 -1
- {python_minifier-2.11.3 → python_minifier-3.1.0/src/python_minifier.egg-info}/PKG-INFO +18 -5
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/SOURCES.txt +16 -1
- python_minifier-3.1.0/test/ast_annotation/test_add_parent.py +49 -0
- python_minifier-3.1.0/test/conftest.py +6 -0
- python_minifier-3.1.0/test/helpers.py +52 -0
- python_minifier-3.1.0/test/requirements.txt +1 -0
- python_minifier-3.1.0/test/subprocess_compat.py +38 -0
- python_minifier-3.1.0/test/test_best_effort_cli.py +179 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_hoist_literals.py +2 -2
- python_minifier-3.1.0/test/test_is_constant_node.py +93 -0
- python_minifier-3.1.0/test/test_raw_fstring_backslash.py +80 -0
- python_minifier-3.1.0/test/test_raw_tstring_backslash.py +98 -0
- python_minifier-3.1.0/test/test_template_strings.py +254 -0
- python_minifier-3.1.0/test/test_tuple_with_bug.py +34 -0
- python_minifier-3.1.0/test/test_unicode_cli.py +178 -0
- python_minifier-3.1.0/test/test_utf8_encoding.py +117 -0
- python_minifier-2.11.3/test/test_is_constant_node.py +0 -43
- {python_minifier-2.11.3 → python_minifier-3.1.0}/LICENSE +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/setup.cfg +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ast_printer.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ministring.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/py.typed +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/__init__.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/bind_names.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/binding.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/mapper.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/name_generator.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/renamer.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/resolve_names.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/util.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/__init__.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/combine_imports.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/constant_folding.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_debug.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_pass.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/suite_transformer.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/util.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/top_level.txt +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/zip-safe +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_assignment_expressions.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_await_fstring.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_namedexpr.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_python2.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_python312.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_python313.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_python33.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_combine_imports.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_comprehension_rename.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_decorator_expressions.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_dict_expansion.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_folding.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_fstring.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_generic_types.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_import.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_iterable_unpacking.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_match.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_match_rename.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_name_generator.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_nonlocal.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_posargs.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_preserve_shebang.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_annotations.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_assert.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_debug.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_exception_brackets.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_explicit_return_none.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_literal_statements.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_object.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_pass.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_rename_builtins.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_rename_locals.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_slice.py +0 -0
- {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_type_param_defaults.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
include README.md
|
|
2
|
+
include LICENSE
|
|
3
|
+
include setup.py
|
|
4
|
+
graft src/python_minifier
|
|
5
|
+
graft test
|
|
6
|
+
exclude .gitignore
|
|
7
|
+
exclude tox.ini
|
|
8
|
+
exclude tox-windows.ini
|
|
9
|
+
exclude requirements-dev.txt
|
|
10
|
+
exclude CHANGELOG.md
|
|
11
|
+
prune .github
|
|
12
|
+
prune .config
|
|
13
|
+
prune docker
|
|
14
|
+
prune docs
|
|
15
|
+
prune corpus_test
|
|
16
|
+
prune hypo_test
|
|
17
|
+
prune typing_test
|
|
18
|
+
prune xtest
|
|
19
|
+
prune tox
|
|
20
|
+
global-exclude *.pyc
|
|
21
|
+
global-exclude __pycache__
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
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
|
|
@@ -25,15 +25,28 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
25
25
|
Classifier: Programming Language :: Python :: 3.11
|
|
26
26
|
Classifier: Programming Language :: Python :: 3.12
|
|
27
27
|
Classifier: Programming Language :: Python :: 3.13
|
|
28
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
28
29
|
Classifier: Programming Language :: Python :: 2
|
|
29
30
|
Classifier: Programming Language :: Python :: 2.7
|
|
30
31
|
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
31
32
|
Classifier: Programming Language :: Python :: Implementation :: PyPy
|
|
32
33
|
Classifier: Intended Audience :: Developers
|
|
33
34
|
Classifier: Topic :: Software Development
|
|
34
|
-
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.
|
|
35
|
+
Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15
|
|
35
36
|
Description-Content-Type: text/markdown
|
|
36
37
|
License-File: LICENSE
|
|
38
|
+
Dynamic: author
|
|
39
|
+
Dynamic: author-email
|
|
40
|
+
Dynamic: classifier
|
|
41
|
+
Dynamic: description
|
|
42
|
+
Dynamic: description-content-type
|
|
43
|
+
Dynamic: home-page
|
|
44
|
+
Dynamic: keywords
|
|
45
|
+
Dynamic: license
|
|
46
|
+
Dynamic: license-file
|
|
47
|
+
Dynamic: project-url
|
|
48
|
+
Dynamic: requires-python
|
|
49
|
+
Dynamic: summary
|
|
37
50
|
|
|
38
51
|
# Python Minifier
|
|
39
52
|
|
|
@@ -41,7 +54,7 @@ Transforms Python source code into its most compact representation.
|
|
|
41
54
|
|
|
42
55
|
[Try it out!](https://python-minifier.com)
|
|
43
56
|
|
|
44
|
-
python-minifier currently supports Python 2.7 and Python 3.3 to 3.
|
|
57
|
+
python-minifier currently supports Python 2.7 and Python 3.3 to 3.14. Previous releases supported Python 2.6.
|
|
45
58
|
|
|
46
59
|
* [PyPI](https://pypi.org/project/python-minifier/)
|
|
47
60
|
* [Documentation](https://dflook.github.io/python-minifier/)
|
|
@@ -130,7 +143,7 @@ def handler(event,context):
|
|
|
130
143
|
|
|
131
144
|
## Why?
|
|
132
145
|
|
|
133
|
-
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
146
|
+
AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
|
|
134
147
|
than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
|
|
135
148
|
|
|
136
149
|
## Installation
|
|
@@ -4,7 +4,7 @@ Transforms Python source code into its most compact representation.
|
|
|
4
4
|
|
|
5
5
|
[Try it out!](https://python-minifier.com)
|
|
6
6
|
|
|
7
|
-
python-minifier currently supports Python 2.7 and Python 3.3 to 3.
|
|
7
|
+
python-minifier currently supports Python 2.7 and Python 3.3 to 3.14. Previous releases supported Python 2.6.
|
|
8
8
|
|
|
9
9
|
* [PyPI](https://pypi.org/project/python-minifier/)
|
|
10
10
|
* [Documentation](https://dflook.github.io/python-minifier/)
|
|
@@ -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
|
|
@@ -20,15 +20,14 @@ setup(
|
|
|
20
20
|
},
|
|
21
21
|
keywords='minify minifier',
|
|
22
22
|
|
|
23
|
-
|
|
24
23
|
package_dir={'': 'src'},
|
|
25
24
|
packages=find_packages('src'),
|
|
26
25
|
package_data={"python_minifier": ["py.typed", "*.pyi", "rename/*.pyi", "transforms/*.pyi"]},
|
|
27
26
|
long_description=long_desc,
|
|
28
27
|
long_description_content_type='text/markdown',
|
|
29
28
|
|
|
30
|
-
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.
|
|
31
|
-
version='
|
|
29
|
+
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15',
|
|
30
|
+
version='3.1.0',
|
|
32
31
|
|
|
33
32
|
classifiers=[
|
|
34
33
|
'Development Status :: 5 - Production/Stable',
|
|
@@ -46,6 +45,7 @@ setup(
|
|
|
46
45
|
'Programming Language :: Python :: 3.11',
|
|
47
46
|
'Programming Language :: Python :: 3.12',
|
|
48
47
|
'Programming Language :: Python :: 3.13',
|
|
48
|
+
'Programming Language :: Python :: 3.14',
|
|
49
49
|
'Programming Language :: Python :: 2',
|
|
50
50
|
'Programming Language :: Python :: 2.7',
|
|
51
51
|
'Programming Language :: Python :: Implementation :: CPython',
|
|
@@ -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()
|
|
@@ -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
|
)
|
|
@@ -743,6 +743,13 @@ class ExpressionPrinter(object):
|
|
|
743
743
|
|
|
744
744
|
self.printer.fstring(str(python_minifier.f_string.OuterFString(node, pep701=pep701)))
|
|
745
745
|
|
|
746
|
+
def visit_TemplateStr(self, node):
|
|
747
|
+
assert isinstance(node, ast.TemplateStr)
|
|
748
|
+
|
|
749
|
+
import python_minifier.t_string
|
|
750
|
+
|
|
751
|
+
self.printer.tstring(str(python_minifier.t_string.TString(node)))
|
|
752
|
+
|
|
746
753
|
def visit_NamedExpr(self, node):
|
|
747
754
|
self._expression(node.target)
|
|
748
755
|
self.printer.operator(':=')
|
|
@@ -8,6 +8,7 @@ Mostly because FStrings feel like a hack.
|
|
|
8
8
|
|
|
9
9
|
import copy
|
|
10
10
|
import re
|
|
11
|
+
import sys
|
|
11
12
|
|
|
12
13
|
import python_minifier.ast_compat as ast
|
|
13
14
|
|
|
@@ -58,11 +59,12 @@ class FString(object):
|
|
|
58
59
|
|
|
59
60
|
return [x + '}' for x in conversion_candidates]
|
|
60
61
|
|
|
61
|
-
def
|
|
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)
|