python-minifier 3.1.1__tar.gz → 3.3.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.
Files changed (105) hide show
  1. {python_minifier-3.1.1/src/python_minifier.egg-info → python_minifier-3.3.0}/PKG-INFO +1 -1
  2. {python_minifier-3.1.1 → python_minifier-3.3.0}/setup.py +1 -1
  3. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/__init__.py +16 -7
  4. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/__init__.pyi +7 -2
  5. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/__main__.py +19 -2
  6. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/expression_printer.py +3 -5
  7. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/module_printer.py +3 -3
  8. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/rename_literals.py +3 -1
  9. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/token_printer.py +1 -1
  10. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/constant_folding.py +91 -23
  11. python_minifier-3.3.0/src/python_minifier/transforms/remove_dead_branches.py +273 -0
  12. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_debug.py +14 -12
  13. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/suite_transformer.py +45 -0
  14. {python_minifier-3.1.1 → python_minifier-3.3.0/src/python_minifier.egg-info}/PKG-INFO +1 -1
  15. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/SOURCES.txt +3 -0
  16. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/subprocess_compat.py +1 -1
  17. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_combine_imports.py +19 -0
  18. python_minifier-3.3.0/test/test_folding.py +690 -0
  19. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_is_constant_node.py +2 -2
  20. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_match.py +28 -0
  21. python_minifier-3.3.0/test/test_prefer_single_line.py +217 -0
  22. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_assert.py +56 -0
  23. python_minifier-3.3.0/test/test_remove_dead_branches.py +695 -0
  24. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_debug.py +157 -5
  25. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_pass.py +79 -0
  26. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_unicode_cli.py +6 -9
  27. python_minifier-3.1.1/test/test_folding.py +0 -151
  28. {python_minifier-3.1.1 → python_minifier-3.3.0}/LICENSE +0 -0
  29. {python_minifier-3.1.1 → python_minifier-3.3.0}/MANIFEST.in +0 -0
  30. {python_minifier-3.1.1 → python_minifier-3.3.0}/README.md +0 -0
  31. {python_minifier-3.1.1 → python_minifier-3.3.0}/setup.cfg +0 -0
  32. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
  33. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ast_compare.py +0 -0
  34. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ast_compat.py +0 -0
  35. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ast_printer.py +0 -0
  36. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/f_string.py +0 -0
  37. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/ministring.py +0 -0
  38. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/py.typed +0 -0
  39. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/README.md +0 -0
  40. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/__init__.py +0 -0
  41. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/bind_names.py +0 -0
  42. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/binding.py +0 -0
  43. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/mapper.py +0 -0
  44. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/name_generator.py +0 -0
  45. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/renamer.py +0 -0
  46. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/resolve_names.py +0 -0
  47. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/rename/util.py +0 -0
  48. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/t_string.py +0 -0
  49. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/__init__.py +0 -0
  50. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/combine_imports.py +0 -0
  51. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
  52. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
  53. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
  54. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
  55. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
  56. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
  57. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
  58. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
  59. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_pass.py +0 -0
  60. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
  61. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier/util.py +0 -0
  62. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
  63. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
  64. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/top_level.txt +0 -0
  65. {python_minifier-3.1.1 → python_minifier-3.3.0}/src/python_minifier.egg-info/zip-safe +0 -0
  66. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/ast_annotation/test_add_parent.py +0 -0
  67. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/conftest.py +0 -0
  68. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/helpers.py +0 -0
  69. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/requirements.txt +0 -0
  70. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_assignment_expressions.py +0 -0
  71. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_await_fstring.py +0 -0
  72. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_best_effort_cli.py +0 -0
  73. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names.py +0 -0
  74. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_namedexpr.py +0 -0
  75. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_python2.py +0 -0
  76. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_python312.py +0 -0
  77. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_python313.py +0 -0
  78. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_bind_names_python33.py +0 -0
  79. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_comprehension_rename.py +0 -0
  80. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_decorator_expressions.py +0 -0
  81. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_dict_expansion.py +0 -0
  82. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_fstring.py +0 -0
  83. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_generic_types.py +0 -0
  84. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_hoist_literals.py +0 -0
  85. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_import.py +0 -0
  86. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_iterable_unpacking.py +0 -0
  87. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_match_rename.py +0 -0
  88. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_name_generator.py +0 -0
  89. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_nonlocal.py +0 -0
  90. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_posargs.py +0 -0
  91. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_preserve_shebang.py +0 -0
  92. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_raw_fstring_backslash.py +0 -0
  93. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_raw_tstring_backslash.py +0 -0
  94. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_annotations.py +0 -0
  95. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_exception_brackets.py +0 -0
  96. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_explicit_return_none.py +0 -0
  97. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_literal_statements.py +0 -0
  98. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_remove_object.py +0 -0
  99. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_rename_builtins.py +0 -0
  100. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_rename_locals.py +0 -0
  101. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_slice.py +0 -0
  102. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_template_strings.py +0 -0
  103. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_tuple_with_bug.py +0 -0
  104. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_type_param_defaults.py +0 -0
  105. {python_minifier-3.1.1 → python_minifier-3.3.0}/test/test_utf8_encoding.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_minifier
3
- Version: 3.1.1
3
+ Version: 3.3.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
@@ -27,7 +27,7 @@ setup(
27
27
  long_description_content_type='text/markdown',
28
28
 
29
29
  python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15',
30
- version='3.1.1',
30
+ version='3.3.0',
31
31
 
32
32
  classifiers=[
33
33
  'Development Status :: 5 - Production/Stable',
@@ -25,6 +25,7 @@ from python_minifier.transforms.constant_folding import FoldConstants
25
25
  from python_minifier.transforms.remove_annotations import RemoveAnnotations
26
26
  from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
27
27
  from python_minifier.transforms.remove_asserts import RemoveAsserts
28
+ from python_minifier.transforms.remove_dead_branches import RemoveDeadBranches
28
29
  from python_minifier.transforms.remove_debug import RemoveDebug
29
30
  from python_minifier.transforms.remove_exception_brackets import remove_no_arg_exception_call
30
31
  from python_minifier.transforms.remove_explicit_return_none import RemoveExplicitReturnNone
@@ -72,7 +73,9 @@ def minify(
72
73
  remove_debug=False,
73
74
  remove_explicit_return_none=True,
74
75
  remove_builtin_exception_brackets=True,
75
- constant_folding=True
76
+ constant_folding=True,
77
+ prefer_single_line=False,
78
+ remove_dead_branches=True
76
79
  ):
77
80
  """
78
81
  Minify a python module
@@ -107,6 +110,8 @@ def minify(
107
110
  :param bool remove_explicit_return_none: If explicit return None statements should be replaced with a bare return
108
111
  :param bool remove_builtin_exception_brackets: If brackets should be removed when raising exceptions with no arguments
109
112
  :param bool constant_folding: If literal expressions should be evaluated
113
+ :param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size
114
+ :param bool remove_dead_branches: If if-statements with a constant False test should be removed
110
115
 
111
116
  :rtype: str
112
117
 
@@ -153,12 +158,15 @@ def minify(
153
158
  if remove_debug:
154
159
  module = RemoveDebug()(module)
155
160
 
156
- if remove_explicit_return_none:
157
- module = RemoveExplicitReturnNone()(module)
158
-
159
161
  if constant_folding:
160
162
  module = FoldConstants()(module)
161
163
 
164
+ if remove_dead_branches:
165
+ module = RemoveDeadBranches()(module)
166
+
167
+ if remove_explicit_return_none:
168
+ module = RemoveExplicitReturnNone()(module)
169
+
162
170
  bind_names(module)
163
171
  resolve_names(module)
164
172
 
@@ -192,7 +200,7 @@ def minify(
192
200
  if convert_posargs_to_args:
193
201
  module = remove_posargs(module)
194
202
 
195
- minified = unparse(module)
203
+ minified = unparse(module, prefer_single_line=prefer_single_line)
196
204
 
197
205
  if preserve_shebang is True:
198
206
  shebang_line = _find_shebang(source)
@@ -219,7 +227,7 @@ def _find_shebang(source):
219
227
  return None
220
228
 
221
229
 
222
- def unparse(module):
230
+ def unparse(module, prefer_single_line=False):
223
231
  """
224
232
  Turn a module AST into python code
225
233
 
@@ -228,13 +236,14 @@ def unparse(module):
228
236
 
229
237
  :param module: The module to turn into python code
230
238
  :type: module: :class:`ast.Module`
239
+ :param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size
231
240
  :rtype: str
232
241
 
233
242
  """
234
243
 
235
244
  assert isinstance(module, ast.Module)
236
245
 
237
- printer = ModulePrinter()
246
+ printer = ModulePrinter(prefer_single_line=prefer_single_line)
238
247
  printer(module)
239
248
 
240
249
  try:
@@ -28,11 +28,16 @@ def minify(
28
28
  remove_debug: bool = ...,
29
29
  remove_explicit_return_none: bool = ...,
30
30
  remove_builtin_exception_brackets: bool = ...,
31
- constant_folding: bool = ...
31
+ constant_folding: bool = ...,
32
+ prefer_single_line: bool = ...,
33
+ remove_dead_branches: bool = ...
32
34
  ) -> Text: ...
33
35
 
34
36
 
35
- def unparse(module: ast.Module) -> Text: ...
37
+ def unparse(
38
+ module: ast.Module,
39
+ prefer_single_line: bool = ...
40
+ ) -> Text: ...
36
41
 
37
42
 
38
43
  def awslambda(
@@ -10,7 +10,6 @@ from python_minifier.transforms.remove_annotations_options import RemoveAnnotati
10
10
 
11
11
  class MinificationNotBeneficialError(Exception):
12
12
  """Raised when minification results in larger output than the original."""
13
- pass
14
13
 
15
14
  def stdout_write_bytes(data):
16
15
  """Write bytes to stdout with proper Python 2.7/3.x compatibility."""
@@ -140,6 +139,13 @@ def parse_args():
140
139
  dest='in_place'
141
140
  )
142
141
 
142
+ parser.add_argument(
143
+ '--prefer-single-line',
144
+ action='store_true',
145
+ help='Prefer multiple statements on a single line separated by semicolons, instead of newlines, where there is no difference in output size',
146
+ dest='prefer_single_line',
147
+ )
148
+
143
149
  # Minification arguments
144
150
  minification_options = parser.add_argument_group('minification options', 'Options that affect how the source is minified')
145
151
  minification_options.add_argument(
@@ -243,6 +249,12 @@ def parse_args():
243
249
  help='Disable evaluating literal expressions',
244
250
  dest='constant_folding',
245
251
  )
252
+ minification_options.add_argument(
253
+ '--no-remove-dead-branches',
254
+ action='store_false',
255
+ help='Disable removing branches that are never executed',
256
+ dest='remove_dead_branches',
257
+ )
246
258
 
247
259
  annotation_options = parser.add_argument_group('remove annotations options', 'Options that affect how annotations are removed')
248
260
  annotation_options.add_argument(
@@ -298,6 +310,10 @@ def parse_args():
298
310
  sys.stderr.write('error: --remove-class-attribute-annotations would do nothing when used with --no-remove-annotations\n')
299
311
  sys.exit(1)
300
312
 
313
+ if args.remove_debug and not args.remove_dead_branches:
314
+ sys.stderr.write('error: --remove-debug would do nothing when used with --no-remove-dead-branches\n')
315
+ sys.exit(1)
316
+
301
317
  return args
302
318
 
303
319
 
@@ -373,7 +389,8 @@ def do_minify(source, filename, minification_args):
373
389
  remove_debug=minification_args.remove_debug,
374
390
  remove_explicit_return_none=minification_args.remove_explicit_return_none,
375
391
  remove_builtin_exception_brackets=minification_args.remove_exception_brackets,
376
- constant_folding=minification_args.constant_folding
392
+ constant_folding=minification_args.constant_folding,
393
+ prefer_single_line=minification_args.prefer_single_line,
377
394
  )
378
395
 
379
396
  # Encode minified result to bytes for comparison and output
@@ -11,7 +11,7 @@ class ExpressionPrinter(object):
11
11
  Builds the smallest possible exact representation of an ast
12
12
  """
13
13
 
14
- def __init__(self):
14
+ def __init__(self, prefer_single_line=False):
15
15
 
16
16
  self.precedences = {
17
17
  'Lambda': 2, # Lambda
@@ -34,7 +34,7 @@ class ExpressionPrinter(object):
34
34
  'Tuple': 18, 'Set': 18, 'List': 18, 'Dict': 18, 'ListComp': 18, 'SetComp': 18, 'DictComp': 18, 'GeneratorExp': 18, # Container
35
35
  }
36
36
 
37
- self.printer = TokenPrinter()
37
+ self.printer = TokenPrinter(prefer_single_line=prefer_single_line)
38
38
 
39
39
  def __call__(self, module):
40
40
  """
@@ -221,9 +221,7 @@ class ExpressionPrinter(object):
221
221
  right_precedence = self.precedence(node.operand)
222
222
  op_precedence = self.precedence(node)
223
223
 
224
- if right_precedence != 0 and (
225
- (op_precedence > right_precedence)
226
- ):
224
+ if right_precedence != 0 and op_precedence > right_precedence:
227
225
  self.printer.delimiter('(')
228
226
  self._expression(node.operand)
229
227
  self.printer.delimiter(')')
@@ -11,8 +11,8 @@ class ModulePrinter(ExpressionPrinter):
11
11
  Builds the smallest possible exact representation of an ast
12
12
  """
13
13
 
14
- def __init__(self, indent_char='\t'):
15
- super(ModulePrinter, self).__init__()
14
+ def __init__(self, indent_char='\t', prefer_single_line=False):
15
+ super(ModulePrinter, self).__init__(prefer_single_line=prefer_single_line)
16
16
  self.indent_char = indent_char
17
17
 
18
18
  def __call__(self, module):
@@ -602,7 +602,7 @@ class ModulePrinter(ExpressionPrinter):
602
602
 
603
603
  if node.guard is not None:
604
604
  self.printer.keyword('if')
605
- self.visit(node.guard)
605
+ self._expression(node.guard)
606
606
 
607
607
  self.printer.delimiter(':')
608
608
  self._suite(node.body)
@@ -1,3 +1,5 @@
1
+ from collections import OrderedDict
2
+
1
3
  import python_minifier.ast_compat as ast
2
4
  from python_minifier.ast_annotation import get_parent, set_parent
3
5
 
@@ -118,7 +120,7 @@ class HoistLiterals(NodeVisitor):
118
120
  def __call__(self, module, ignore_slots=True):
119
121
  self.module = module
120
122
  self._ignore_slots = ignore_slots
121
- self._hoisted = {}
123
+ self._hoisted = OrderedDict()
122
124
  self.visit(module)
123
125
  self.place_bindings()
124
126
 
@@ -306,7 +306,7 @@ class TokenPrinter(object):
306
306
  def end_statement(self):
307
307
  """ End a statement with a newline, or a semi-colon if it saves characters. """
308
308
 
309
- if self.indent == 0:
309
+ if self.indent == 0 and not self._prefer_single_line:
310
310
  self.newline()
311
311
  else:
312
312
  if self._code[-1] != ';':
@@ -10,40 +10,46 @@ from python_minifier.transforms.suite_transformer import SuiteTransformer
10
10
  from python_minifier.util import is_constant_node
11
11
 
12
12
 
13
- class FoldConstants(SuiteTransformer):
13
+ def is_foldable_constant(node):
14
14
  """
15
- Fold Constants if it would reduce the size of the source
15
+ Check if a node is a constant expression that can participate in folding.
16
+
17
+ We can assume that children have already been folded, so foldable constants are either:
18
+ - Simple literals (Num, NameConstant)
19
+ - UnaryOp(USub/Invert) on a Num - these don't fold to shorter forms,
20
+ so they remain after child visiting. UAdd and Not would have been
21
+ folded away since they always produce shorter results.
16
22
  """
23
+ if is_constant_node(node, (ast.Num, ast.NameConstant)):
24
+ return True
17
25
 
18
- def __init__(self):
19
- super(FoldConstants, self).__init__()
26
+ if isinstance(node, ast.UnaryOp):
27
+ if isinstance(node.op, (ast.USub, ast.Invert)):
28
+ return is_constant_node(node.operand, ast.Num)
20
29
 
21
- def visit_BinOp(self, node):
30
+ return False
22
31
 
23
- node.left = self.visit(node.left)
24
- node.right = self.visit(node.right)
25
32
 
26
- # Check this is a constant expression that could be folded
27
- # We don't try to fold strings or bytes, since they have probably been arranged this way to make the source shorter and we are unlikely to beat that
28
- if not is_constant_node(node.left, (ast.Num, ast.NameConstant)):
29
- return node
30
- if not is_constant_node(node.right, (ast.Num, ast.NameConstant)):
31
- return node
32
-
33
- if isinstance(node.op, ast.Div):
34
- # Folding div is subtle, since it can have different results in Python 2 and Python 3
35
- # Do this once target version options have been implemented
36
- return node
33
+ class FoldConstants(SuiteTransformer):
34
+ """
35
+ Fold Constants if it would reduce the size of the source
36
+ """
37
37
 
38
- if isinstance(node.op, ast.Pow):
39
- # This can be folded, but it is unlikely to reduce the size of the source
40
- # It can also be slow to evaluate
41
- return node
38
+ def __init__(self):
39
+ super(FoldConstants, self).__init__()
42
40
 
41
+ def fold(self, node):
43
42
  # Evaluate the expression
44
43
  try:
45
44
  original_expression = unparse_expression(node)
46
- original_value = safe_eval(original_expression)
45
+
46
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Invert) and is_constant_node(node.operand, ast.NameConstant) and isinstance(node.operand.value, bool):
47
+ # ~ on a bool is deprecated from python 3.12, to be removed in 3.16.
48
+ if sys.version_info >= (3, 16):
49
+ return node
50
+ original_value = ~int(node.operand.value)
51
+ else:
52
+ original_value = safe_eval(original_expression)
47
53
  except Exception:
48
54
  return node
49
55
 
@@ -96,6 +102,68 @@ class FoldConstants(SuiteTransformer):
96
102
  # New representation is shorter and has the same value, so use it
97
103
  return self.add_child(new_node, get_parent(node), node.namespace)
98
104
 
105
+ def visit_BinOp(self, node):
106
+
107
+ node.left = self.visit(node.left)
108
+ node.right = self.visit(node.right)
109
+
110
+ # Check this is a constant expression that could be folded
111
+ # We don't try to fold strings or bytes, since they have probably been arranged this way to make the source shorter and we are unlikely to beat that
112
+ if not is_foldable_constant(node.left):
113
+ return node
114
+ if not is_foldable_constant(node.right):
115
+ return node
116
+
117
+ if isinstance(node.op, ast.Div):
118
+ # Folding div is subtle, since it can have different results in Python 2 and Python 3
119
+ # Do this once target version options have been implemented
120
+ return node
121
+
122
+ if isinstance(node.op, ast.Pow):
123
+ # This can be folded, but it is unlikely to reduce the size of the source
124
+ # It can also be slow to evaluate
125
+ return node
126
+
127
+ return self.fold(node)
128
+
129
+ def visit_BoolOp(self, node):
130
+ node.values = [self.visit(v) for v in node.values]
131
+
132
+ # Check this is a constant expression that could be folded
133
+ if not all(is_foldable_constant(v) for v in node.values):
134
+ return node
135
+
136
+ return self.fold(node)
137
+
138
+ def visit_Compare(self, node):
139
+ node.left = self.visit(node.left)
140
+ node.comparators = [self.visit(c) for c in node.comparators]
141
+
142
+ operands = [node.left] + node.comparators
143
+ if not all(is_foldable_constant(n) for n in operands):
144
+ return node
145
+
146
+ if any(isinstance(op, (ast.Is, ast.IsNot)) for op in node.ops):
147
+ # Identity is only language-guaranteed for the singletons
148
+ if not all(is_constant_node(n, ast.NameConstant) for n in operands):
149
+ return node
150
+
151
+ return self.fold(node)
152
+
153
+ def visit_UnaryOp(self, node):
154
+
155
+ node.operand = self.visit(node.operand)
156
+
157
+ # Only fold if the operand is a foldable constant
158
+ if not is_foldable_constant(node.operand):
159
+ return node
160
+
161
+ # Only fold these unary operators
162
+ if not isinstance(node.op, (ast.USub, ast.UAdd, ast.Invert, ast.Not)):
163
+ return node
164
+
165
+ return self.fold(node)
166
+
99
167
 
100
168
  def equal_value_and_type(a, b):
101
169
  if type(a) != type(b):
@@ -0,0 +1,273 @@
1
+ import python_minifier.ast_compat as ast
2
+
3
+ from python_minifier.transforms.suite_transformer import SuiteTransformer
4
+ from python_minifier.util import is_constant_node
5
+
6
+ class NamespaceProperties(object):
7
+ """
8
+ The analysed properties of a namespace, including the names defined in it and whether it is a generator
9
+ """
10
+
11
+ def __init__(self):
12
+ self.local_names = set()
13
+ self.nonlocal_names = set()
14
+ self.global_names = set()
15
+ self.is_generator = False
16
+
17
+ def __eq__(self, other):
18
+ assert isinstance(other, NamespaceProperties)
19
+ return (
20
+ self.local_names == other.local_names and
21
+ self.nonlocal_names == other.nonlocal_names and
22
+ self.global_names == other.global_names and
23
+ self.is_generator is other.is_generator
24
+ )
25
+
26
+ def __ne__(self, other):
27
+ return not self == other
28
+
29
+ def __repr__(self):
30
+ return 'NamespaceProperties(local_names=%r, nonlocal_names=%r, global_names=%r, is_generator=%r)' % (
31
+ self.local_names, self.nonlocal_names, self.global_names, self.is_generator
32
+ )
33
+
34
+
35
+ def in_function_scope(namespace):
36
+ """
37
+ Is this namespace nested in a function scope?
38
+
39
+ A name bound anywhere in a class body - even in unreachable code - makes
40
+ loads of that name in the class body fall through to the global scope,
41
+ instead of an enclosing function scope cell. Removing such a binding is
42
+ only safe when there is no enclosing function scope to fall through to.
43
+ """
44
+ while not isinstance(namespace, ast.Module):
45
+ if isinstance(namespace, (ast.FunctionDef, ast.AsyncFunctionDef)):
46
+ return True
47
+ namespace = namespace.namespace
48
+ return False
49
+
50
+
51
+ class RemoveDeadBranches(SuiteTransformer):
52
+ """
53
+ Remove if statements where the condition tests False
54
+ """
55
+
56
+ def __call__(self, node):
57
+ # Suites that have been condemned during this run, but may not have been
58
+ # detached from the tree yet. Shared across all suites so that decisions
59
+ # in nested or sibling suites don't count already-removed bindings.
60
+ self._removed_suites = []
61
+ return self.visit(node)
62
+
63
+ def get_namespace_properties(self, namespace, removed_suites=None):
64
+ """
65
+ Gather the binding properties of a namespace
66
+
67
+ Walks the namespace and collects the names it binds (as local, nonlocal
68
+ and global name sets) and whether it is a generator.
69
+
70
+ :param namespace: The namespace node to analyse
71
+ :param removed_suites: Suites to skip while walking, for
72
+ branches that are being removed.
73
+ :type removed_suites: list or None
74
+ :rtype: NamespaceProperties
75
+ """
76
+ if removed_suites is None:
77
+ removed_suites = []
78
+
79
+ properties = NamespaceProperties()
80
+
81
+ def explore_namespace(node):
82
+ binds_here = getattr(node, 'namespace', namespace) is namespace
83
+
84
+ if binds_here:
85
+ if isinstance(node, ast.Name):
86
+ if isinstance(node.ctx, (ast.Store, ast.Del, ast.Param)):
87
+ properties.local_names.add(node.id)
88
+ elif isinstance(node, (ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
89
+ properties.local_names.add(node.name)
90
+ elif isinstance(node, ast.alias):
91
+ # What about import *
92
+
93
+ if node.asname is not None:
94
+ properties.local_names.add(node.asname)
95
+ else:
96
+ properties.local_names.add(node.name.split('.')[0])
97
+ elif isinstance(node, ast.arguments):
98
+ if isinstance(node.vararg, str):
99
+ properties.local_names.add(node.vararg)
100
+ if isinstance(node.kwarg, str):
101
+ properties.local_names.add(node.kwarg)
102
+ elif isinstance(node, ast.arg):
103
+ properties.local_names.add(node.arg)
104
+ elif isinstance(node, ast.ExceptHandler):
105
+ if isinstance(node.name, str):
106
+ properties.local_names.add(node.name)
107
+
108
+ elif isinstance(node, ast.Global):
109
+ properties.global_names.update(node.names)
110
+ elif isinstance(node, ast.Nonlocal):
111
+ properties.nonlocal_names.update(node.names)
112
+
113
+ elif isinstance(node, ast.MatchAs):
114
+ if isinstance(node.name, str):
115
+ properties.local_names.add(node.name)
116
+ elif isinstance(node, ast.MatchStar):
117
+ if isinstance(node.name, str):
118
+ properties.local_names.add(node.name)
119
+ elif isinstance(node, ast.MatchMapping):
120
+ if isinstance(node.rest, str):
121
+ properties.local_names.add(node.rest)
122
+
123
+ elif isinstance(node, ast.TypeVar):
124
+ properties.local_names.add(node.name)
125
+ elif isinstance(node, ast.TypeVarTuple):
126
+ properties.local_names.add(node.name)
127
+ elif isinstance(node, ast.ParamSpec):
128
+ properties.local_names.add(node.name)
129
+
130
+ elif isinstance(node, (ast.Yield, ast.YieldFrom)):
131
+ properties.is_generator = True
132
+
133
+ # Skip recursing into the body field of child namespaces, as they can only affect the child namespace
134
+ # Other fields (decorators, annotations, arguments...) can affect this namespace,
135
+ # so we do want to recurse into them
136
+ skip_body = node is not namespace and isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.Lambda, ast.ClassDef))
137
+
138
+ for name, field in ast.iter_fields(node):
139
+ if field in removed_suites:
140
+ continue
141
+
142
+ if skip_body and name == 'body':
143
+ continue
144
+
145
+ if isinstance(field, ast.AST):
146
+ explore_namespace(field)
147
+ elif isinstance(field, list):
148
+ for item in field:
149
+ if isinstance(item, ast.AST):
150
+ explore_namespace(item)
151
+
152
+ explore_namespace(namespace)
153
+
154
+ return properties
155
+
156
+ def changes_semantics(self, namespace, candidate_suite):
157
+ """
158
+ Does removing this branch change the semantics of the program?
159
+
160
+ :param namespace: The namespace of the If statement
161
+ :param candidate_suite: The branch that is being removed
162
+ :return: True if removing the branch changes the semantics of the program, False otherwise
163
+ """
164
+
165
+ # Gather properties in the namespace, excluding suites already condemned
166
+ properties = self.get_namespace_properties(namespace, removed_suites=self._removed_suites)
167
+
168
+ # Gather properties in the namespace, additionally excluding the candidate branch
169
+ candidate_properties = self.get_namespace_properties(namespace, removed_suites=[candidate_suite] + self._removed_suites)
170
+
171
+ declarations_changed = (
172
+ properties.is_generator != candidate_properties.is_generator
173
+ or properties.nonlocal_names != candidate_properties.nonlocal_names
174
+ or properties.global_names != candidate_properties.global_names
175
+ )
176
+
177
+ if isinstance(namespace, ast.Module):
178
+ # The module namespace is dynamic, removing unreachable bindings doesn't
179
+ # change their resolution behaviour, so we can safely remove them.
180
+ return declarations_changed
181
+ elif isinstance(namespace, ast.ClassDef) and not in_function_scope(namespace):
182
+ # Class namespaces are also dynamic, but a binding in the class body stops
183
+ # loads deferring to an enclosing function scope cell, so unreachable local
184
+ # bindings can only be ignored when no function scope encloses the class.
185
+ return declarations_changed
186
+ else:
187
+ return properties != candidate_properties
188
+
189
+ def constant_test(self, node):
190
+ """
191
+ The constant truth value of an if statement's test, or None
192
+
193
+ RemoveDebug marks if statements whose __debug__ test it has resolved
194
+ with a resolved_test attribute. Otherwise only the True and False
195
+ constants are recognised - before python 3.4 they are reassignable
196
+ names, so nothing is recognised.
197
+
198
+ :param node: The statement to examine
199
+ :type node: ast.AST
200
+ :rtype: bool or None
201
+ """
202
+ if not isinstance(node, ast.If):
203
+ return None
204
+
205
+ if hasattr(node, 'resolved_test'):
206
+ if node.resolved_test is True or node.resolved_test is False:
207
+ return node.resolved_test
208
+ return None
209
+
210
+ if is_constant_node(node.test, ast.NameConstant) and isinstance(node.test.value, bool):
211
+ return node.test.value
212
+
213
+ return None
214
+
215
+ def remove_false_branches(self, node_list):
216
+ suite = []
217
+
218
+ for node in node_list:
219
+ condition = self.constant_test(node)
220
+
221
+ # A branch of a constant test can be removed, if that doesn't change
222
+ # the semantics of the program
223
+
224
+ if condition is True and not self.changes_semantics(node.namespace, node.orelse):
225
+ # The else branch is dead, keep only the body
226
+ self._removed_suites.append(node.orelse)
227
+ suite.extend(self.remove_false_branches(node.body))
228
+ continue
229
+
230
+ if condition is False and not self.changes_semantics(node.namespace, node.body):
231
+ # The body is dead, keep only the else branch
232
+ self._removed_suites.append(node.body)
233
+ suite.extend(self.remove_false_branches(node.orelse))
234
+ continue
235
+
236
+ node = self.visit(node)
237
+
238
+ # An else suite that dead branch removal (or another transform) emptied
239
+ # or reduced to the 0 padding can be omitted.
240
+ orelse = getattr(node, 'orelse', None)
241
+ if orelse and self._is_empty_suite(orelse):
242
+ node.orelse = []
243
+
244
+ suite.append(node)
245
+
246
+ return suite
247
+
248
+ def suite(self, node_list, parent):
249
+
250
+ without_dead_branches = self.remove_false_branches(node_list)
251
+
252
+ if len(without_dead_branches) == 0:
253
+ if isinstance(parent, ast.Module):
254
+ return []
255
+ else:
256
+ return [self.add_child(ast.Expr(value=ast.Num(0)), parent=parent)]
257
+
258
+ return without_dead_branches
259
+
260
+ def _is_empty_suite(self, suite):
261
+ """
262
+ Is the suite empty?
263
+
264
+ An empty suite is either a zero length list, or a list containing a single expression statement that is the constant 0.
265
+ The constant 0 is used by various transforms as a placeholder for an empty suite, because not all suites can be empty (e.g. the body of a function or class).
266
+
267
+ :param suite:
268
+ :type suite: list
269
+ :rtype: bool
270
+ """
271
+ if len(suite) == 0:
272
+ return True
273
+ return len(suite) == 1 and isinstance(suite[0], ast.Expr) and is_constant_node(suite[0].value, ast.Num) and suite[0].value.n == 0