python-minifier 3.0.0__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-3.0.0/src/python_minifier.egg-info → python_minifier-3.1.0}/PKG-INFO +4 -3
- {python_minifier-3.0.0 → python_minifier-3.1.0}/README.md +1 -1
- {python_minifier-3.0.0 → python_minifier-3.1.0}/setup.py +3 -3
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/ast_compare.py +4 -1
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/ast_compat.py +2 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/expression_printer.py +7 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/f_string.py +120 -18
- python_minifier-3.1.0/src/python_minifier/rename/README.md +93 -0
- {python_minifier-3.0.0 → 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-3.0.0 → python_minifier-3.1.0}/src/python_minifier/token_printer.py +10 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0/src/python_minifier.egg-info}/PKG-INFO +4 -3
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier.egg-info/SOURCES.txt +13 -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.0.0 → python_minifier-3.1.0}/test/test_hoist_literals.py +2 -2
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_is_constant_node.py +40 -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.0.0 → python_minifier-3.1.0}/test/test_unicode_cli.py +12 -4
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_utf8_encoding.py +12 -9
- {python_minifier-3.0.0 → python_minifier-3.1.0}/LICENSE +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/setup.cfg +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/__init__.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/__init__.pyi +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/__main__.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/ast_printer.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/ministring.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/module_printer.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/py.typed +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/rename/__init__.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/rename/bind_names.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/rename/binding.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/rename/mapper.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/rename/name_generator.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/rename/renamer.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/rename/resolve_names.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/rename/util.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/__init__.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/combine_imports.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/constant_folding.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_debug.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_pass.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/transforms/suite_transformer.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/util.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier.egg-info/top_level.txt +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier.egg-info/zip-safe +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_assignment_expressions.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_await_fstring.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_best_effort_cli.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_bind_names.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_bind_names_namedexpr.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_bind_names_python2.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_bind_names_python312.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_bind_names_python313.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_bind_names_python33.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_combine_imports.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_comprehension_rename.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_decorator_expressions.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_dict_expansion.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_folding.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_fstring.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_generic_types.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_import.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_iterable_unpacking.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_match.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_match_rename.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_name_generator.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_nonlocal.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_posargs.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_preserve_shebang.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_remove_annotations.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_remove_assert.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_remove_debug.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_remove_exception_brackets.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_remove_explicit_return_none.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_remove_literal_statements.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_remove_object.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_remove_pass.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_rename_builtins.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_rename_locals.py +0 -0
- {python_minifier-3.0.0 → python_minifier-3.1.0}/test/test_slice.py +0 -0
- {python_minifier-3.0.0 → 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
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: python_minifier
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.1.0
|
|
4
4
|
Summary: Transform Python source code into it's most compact representation
|
|
5
5
|
Home-page: https://github.com/dflook/python-minifier
|
|
6
6
|
Author: Daniel Flook
|
|
@@ -25,13 +25,14 @@ 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
|
|
37
38
|
Dynamic: author
|
|
@@ -53,7 +54,7 @@ Transforms Python source code into its most compact representation.
|
|
|
53
54
|
|
|
54
55
|
[Try it out!](https://python-minifier.com)
|
|
55
56
|
|
|
56
|
-
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.
|
|
57
58
|
|
|
58
59
|
* [PyPI](https://pypi.org/project/python-minifier/)
|
|
59
60
|
* [Documentation](https://dflook.github.io/python-minifier/)
|
|
@@ -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/)
|
|
@@ -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='3.
|
|
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',
|
|
@@ -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
|
|
|
@@ -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
|
"""
|
|
@@ -285,7 +342,9 @@ class Str(object):
|
|
|
285
342
|
if literal == '':
|
|
286
343
|
literal += self.current_quote
|
|
287
344
|
|
|
288
|
-
if c == '\
|
|
345
|
+
if c == '\0':
|
|
346
|
+
literal += '\\x00'
|
|
347
|
+
elif c == '\n':
|
|
289
348
|
literal += '\\n'
|
|
290
349
|
elif c == '\r':
|
|
291
350
|
literal += '\\r'
|
|
@@ -302,7 +361,7 @@ class Str(object):
|
|
|
302
361
|
if self._s == '':
|
|
303
362
|
return str(min(self.allowed_quotes, key=len)) * 2
|
|
304
363
|
|
|
305
|
-
if '
|
|
364
|
+
if '\\' in self._s and not self.pep701:
|
|
306
365
|
raise ValueError('Impossible to represent a character in f-string expression part')
|
|
307
366
|
|
|
308
367
|
if not self.pep701 and ('\n' in self._s or '\r' in self._s):
|
|
@@ -360,7 +419,35 @@ class FormatSpec(object):
|
|
|
360
419
|
return candidates
|
|
361
420
|
|
|
362
421
|
def str_for(self, s):
|
|
363
|
-
|
|
422
|
+
# Special handling for problematic format spec characters that can cause parsing issues
|
|
423
|
+
# If the format spec contains only braces, it's likely an invalid test case
|
|
424
|
+
|
|
425
|
+
# Escape null bytes and other unprintable characters
|
|
426
|
+
escaped = ''
|
|
427
|
+
for c in s:
|
|
428
|
+
if c == '\0':
|
|
429
|
+
escaped += '\\x00'
|
|
430
|
+
elif c == '{':
|
|
431
|
+
escaped += '{{'
|
|
432
|
+
elif c == '}':
|
|
433
|
+
escaped += '}}'
|
|
434
|
+
elif c == '\\':
|
|
435
|
+
# For Python 3.12+ raw f-string regression (fixed in 3.14rc2), we need to escape backslashes
|
|
436
|
+
# in format specs so they round-trip correctly
|
|
437
|
+
if (3, 12) <= sys.version_info < (3, 14):
|
|
438
|
+
escaped += '\\\\'
|
|
439
|
+
else:
|
|
440
|
+
escaped += c
|
|
441
|
+
elif c == '\r':
|
|
442
|
+
# Always escape carriage returns because Python normalizes them to newlines during parsing
|
|
443
|
+
# This prevents AST mismatches (\r -> \n normalization)
|
|
444
|
+
escaped += '\\r'
|
|
445
|
+
elif ord(c) < 32 and c not in '\t\n':
|
|
446
|
+
# Escape other control characters except tab, newline
|
|
447
|
+
escaped += '\\x{:02x}'.format(ord(c))
|
|
448
|
+
else:
|
|
449
|
+
escaped += c
|
|
450
|
+
return escaped
|
|
364
451
|
|
|
365
452
|
|
|
366
453
|
class Bytes(object):
|
|
@@ -411,7 +498,24 @@ class Bytes(object):
|
|
|
411
498
|
|
|
412
499
|
if literal == '':
|
|
413
500
|
literal = 'b' + self.current_quote
|
|
414
|
-
|
|
501
|
+
|
|
502
|
+
# Handle special characters that need escaping
|
|
503
|
+
if b == 0: # null byte
|
|
504
|
+
literal += '\\x00'
|
|
505
|
+
elif b == ord('\\'): # backslash
|
|
506
|
+
literal += '\\\\'
|
|
507
|
+
elif b == ord('\n'): # newline
|
|
508
|
+
literal += '\\n'
|
|
509
|
+
elif b == ord('\r'): # carriage return
|
|
510
|
+
literal += '\\r'
|
|
511
|
+
elif b == ord('\t'): # tab
|
|
512
|
+
literal += '\\t'
|
|
513
|
+
elif len(self.current_quote) == 1 and b == ord(self.current_quote): # single quote character
|
|
514
|
+
literal += '\\' + self.current_quote
|
|
515
|
+
elif 32 <= b <= 126: # printable ASCII
|
|
516
|
+
literal += chr(b)
|
|
517
|
+
else: # other non-printable characters
|
|
518
|
+
literal += '\\x{:02x}'.format(b)
|
|
415
519
|
|
|
416
520
|
if literal:
|
|
417
521
|
literal += self.current_quote
|
|
@@ -421,8 +525,6 @@ class Bytes(object):
|
|
|
421
525
|
if self._b == b'':
|
|
422
526
|
return 'b' + str(min(self.allowed_quotes, key=len)) * 2
|
|
423
527
|
|
|
424
|
-
if b'\0' in self._b or b'\\' in self._b:
|
|
425
|
-
raise ValueError('Impossible to represent a %r character in f-string expression part')
|
|
426
528
|
|
|
427
529
|
if b'\n' in self._b or b'\r' in self._b:
|
|
428
530
|
if '"""' not in self.allowed_quotes and "'''" not in self.allowed_quotes:
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# Renaming
|
|
2
|
+
|
|
3
|
+
We can save bytes by shortening the names used in a python program.
|
|
4
|
+
|
|
5
|
+
One simple way to do this is to replace each unique name in a module with a shorter one.
|
|
6
|
+
This will probably exhaust the available single character names, so is not as efficient as it could be.
|
|
7
|
+
Also, not all names can be safely changed this way.
|
|
8
|
+
|
|
9
|
+
By determining the scope of each name, we can assign the same short name to multiple non-overlapping scopes.
|
|
10
|
+
This means sibling namespaces may have the same names, and names will be shadowed in inner namespaces where possible.
|
|
11
|
+
|
|
12
|
+
This file is a guide for how the python_minifier package shortens names.
|
|
13
|
+
There are multiple steps to the renaming process.
|
|
14
|
+
|
|
15
|
+
## Binding Names
|
|
16
|
+
|
|
17
|
+
Names are bound to the local namespace it is defined in.
|
|
18
|
+
|
|
19
|
+
### Create namespace nodes
|
|
20
|
+
|
|
21
|
+
Namespaces in python are introduced by Modules, Functions, Comprehensions, Generators and Classes.
|
|
22
|
+
The AST node that introduces a new namespace is called a 'namespace node'.
|
|
23
|
+
|
|
24
|
+
These attributes are added to namespace nodes:
|
|
25
|
+
|
|
26
|
+
- Bindings - A list of Bindings local to this namespace, populated by the Bind names step
|
|
27
|
+
- Globals - A list of global names in this namespace
|
|
28
|
+
- Nonlocals - A list of nonlocal names in this namespace
|
|
29
|
+
|
|
30
|
+
### Determine parent node
|
|
31
|
+
|
|
32
|
+
Add a parent attribute to each node with the value of the node of which this is a child.
|
|
33
|
+
|
|
34
|
+
### Determine namespace
|
|
35
|
+
|
|
36
|
+
Add a namespace attribute to each node with the value of the namespace node that will be used for name binding and resolution.
|
|
37
|
+
This is usually the closest parent namespace node. The exceptions are:
|
|
38
|
+
|
|
39
|
+
- Function argument default values are in the same namespace as their function.
|
|
40
|
+
- Function decorators are in the same namespace as their function.
|
|
41
|
+
- Function annotations are in the same namespace as their function.
|
|
42
|
+
- Class decorator are in the same namespace as their class.
|
|
43
|
+
- Class bases, keywords, starargs and kwargs are in the same namespace as their class.
|
|
44
|
+
- The first iteration expression of a comprehension is in the same namespace as it's parent ListComp/SetComp/DictComp or GeneratorExp.
|
|
45
|
+
|
|
46
|
+
### Bind names
|
|
47
|
+
|
|
48
|
+
Every node that binds a name creates a NameBinding for that name in its namespace.
|
|
49
|
+
The node is added to the NameBinding as a reference.
|
|
50
|
+
|
|
51
|
+
If the name is nonlocal in its namespace it does not create a binding.
|
|
52
|
+
|
|
53
|
+
Nodes that create a binding:
|
|
54
|
+
|
|
55
|
+
- FunctionDef nodes bind their name
|
|
56
|
+
- ClassDef nodes bind their name
|
|
57
|
+
- arg nodes bind their arg
|
|
58
|
+
- Name nodes in Store or Del context bind their id
|
|
59
|
+
- MatchAs nodes bind their name
|
|
60
|
+
- MatchStar nodes bind their name
|
|
61
|
+
- MatchMapping nodes bind their rest
|
|
62
|
+
|
|
63
|
+
### Resolve names
|
|
64
|
+
|
|
65
|
+
For the remaining unbound name nodes and nodes that normally create a binding but are for a nonlocal name, we find their binding.
|
|
66
|
+
|
|
67
|
+
Bindings for name references are found by searching their namespace, then parent namespaces.
|
|
68
|
+
If a name is global in a searched namespace, skip straight to the module node.
|
|
69
|
+
If a name is nonlocal in a searched namespace, skip to the next parent namespace.
|
|
70
|
+
When traversing parent namespaces, Class namespaces are skipped.
|
|
71
|
+
|
|
72
|
+
If a NameBinding is found, add the node as a reference.
|
|
73
|
+
If no NameBinding is found, check if the name would resolve to a builtin.
|
|
74
|
+
If so, create a BuiltinBinding in the module namespace and add this node as a reference.
|
|
75
|
+
|
|
76
|
+
Otherwise we failed to find a binding for this name - Create a NameBinding in the module namespace and add this node
|
|
77
|
+
as a reference.
|
|
78
|
+
|
|
79
|
+
## Hoist Literals
|
|
80
|
+
|
|
81
|
+
At this point we do the HoistLiterals transform, which adds new HoistedLiteral bindings to the namespaces where it wants
|
|
82
|
+
to introduce new names.
|
|
83
|
+
|
|
84
|
+
## Name Assignment
|
|
85
|
+
|
|
86
|
+
Collect all bindings in the module and sort by estimated byte savings
|
|
87
|
+
|
|
88
|
+
For each binding:
|
|
89
|
+
|
|
90
|
+
- Determine it's 'reservation scope', which is the set of namespaces that name is referenced in (and all namespaces between them)
|
|
91
|
+
- Get the next available name that is unassigned and unreserved in all namespaces in the reservation scope.
|
|
92
|
+
- Check if we should proceed with the rename - is it space efficient to do this rename, or has the original name been assigned somewhere else?
|
|
93
|
+
- Rename the binding, rename all referenced nodes to the new name, and record this name as assigned in every namespace of the reservation scope.
|
{python_minifier-3.0.0 → python_minifier-3.1.0}/src/python_minifier/rename/rename_literals.py
RENAMED
|
@@ -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
|
|