python-minifier 3.2.0__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 (104) hide show
  1. {python_minifier-3.2.0/src/python_minifier.egg-info → python_minifier-3.3.0}/PKG-INFO +1 -1
  2. {python_minifier-3.2.0 → python_minifier-3.3.0}/setup.py +1 -1
  3. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/__init__.py +9 -3
  4. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/__init__.pyi +2 -1
  5. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/__main__.py +10 -1
  6. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/expression_printer.py +1 -3
  7. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/module_printer.py +1 -1
  8. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/rename_literals.py +3 -1
  9. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/constant_folding.py +33 -2
  10. python_minifier-3.3.0/src/python_minifier/transforms/remove_dead_branches.py +273 -0
  11. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_debug.py +14 -12
  12. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/suite_transformer.py +45 -0
  13. {python_minifier-3.2.0 → python_minifier-3.3.0/src/python_minifier.egg-info}/PKG-INFO +1 -1
  14. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/SOURCES.txt +2 -0
  15. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/subprocess_compat.py +1 -1
  16. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_combine_imports.py +19 -0
  17. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_folding.py +132 -2
  18. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_is_constant_node.py +2 -2
  19. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_match.py +28 -0
  20. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_assert.py +56 -0
  21. python_minifier-3.3.0/test/test_remove_dead_branches.py +695 -0
  22. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_debug.py +157 -5
  23. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_pass.py +79 -0
  24. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_unicode_cli.py +6 -9
  25. {python_minifier-3.2.0 → python_minifier-3.3.0}/LICENSE +0 -0
  26. {python_minifier-3.2.0 → python_minifier-3.3.0}/MANIFEST.in +0 -0
  27. {python_minifier-3.2.0 → python_minifier-3.3.0}/README.md +0 -0
  28. {python_minifier-3.2.0 → python_minifier-3.3.0}/setup.cfg +0 -0
  29. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
  30. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ast_compare.py +0 -0
  31. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ast_compat.py +0 -0
  32. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ast_printer.py +0 -0
  33. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/f_string.py +0 -0
  34. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/ministring.py +0 -0
  35. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/py.typed +0 -0
  36. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/README.md +0 -0
  37. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/__init__.py +0 -0
  38. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/bind_names.py +0 -0
  39. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/binding.py +0 -0
  40. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/mapper.py +0 -0
  41. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/name_generator.py +0 -0
  42. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/renamer.py +0 -0
  43. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/resolve_names.py +0 -0
  44. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/rename/util.py +0 -0
  45. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/t_string.py +0 -0
  46. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/token_printer.py +0 -0
  47. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/__init__.py +0 -0
  48. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/combine_imports.py +0 -0
  49. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
  50. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
  51. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
  52. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
  53. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
  54. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
  55. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
  56. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
  57. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_pass.py +0 -0
  58. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
  59. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier/util.py +0 -0
  60. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
  61. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
  62. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/top_level.txt +0 -0
  63. {python_minifier-3.2.0 → python_minifier-3.3.0}/src/python_minifier.egg-info/zip-safe +0 -0
  64. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/ast_annotation/test_add_parent.py +0 -0
  65. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/conftest.py +0 -0
  66. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/helpers.py +0 -0
  67. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/requirements.txt +0 -0
  68. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_assignment_expressions.py +0 -0
  69. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_await_fstring.py +0 -0
  70. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_best_effort_cli.py +0 -0
  71. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names.py +0 -0
  72. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_namedexpr.py +0 -0
  73. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_python2.py +0 -0
  74. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_python312.py +0 -0
  75. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_python313.py +0 -0
  76. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_bind_names_python33.py +0 -0
  77. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_comprehension_rename.py +0 -0
  78. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_decorator_expressions.py +0 -0
  79. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_dict_expansion.py +0 -0
  80. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_fstring.py +0 -0
  81. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_generic_types.py +0 -0
  82. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_hoist_literals.py +0 -0
  83. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_import.py +0 -0
  84. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_iterable_unpacking.py +0 -0
  85. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_match_rename.py +0 -0
  86. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_name_generator.py +0 -0
  87. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_nonlocal.py +0 -0
  88. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_posargs.py +0 -0
  89. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_prefer_single_line.py +0 -0
  90. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_preserve_shebang.py +0 -0
  91. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_raw_fstring_backslash.py +0 -0
  92. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_raw_tstring_backslash.py +0 -0
  93. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_annotations.py +0 -0
  94. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_exception_brackets.py +0 -0
  95. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_explicit_return_none.py +0 -0
  96. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_literal_statements.py +0 -0
  97. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_remove_object.py +0 -0
  98. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_rename_builtins.py +0 -0
  99. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_rename_locals.py +0 -0
  100. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_slice.py +0 -0
  101. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_template_strings.py +0 -0
  102. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_tuple_with_bug.py +0 -0
  103. {python_minifier-3.2.0 → python_minifier-3.3.0}/test/test_type_param_defaults.py +0 -0
  104. {python_minifier-3.2.0 → 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.2.0
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.2.0',
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
@@ -74,6 +75,7 @@ def minify(
74
75
  remove_builtin_exception_brackets=True,
75
76
  constant_folding=True,
76
77
  prefer_single_line=False,
78
+ remove_dead_branches=True
77
79
  ):
78
80
  """
79
81
  Minify a python module
@@ -109,6 +111,7 @@ def minify(
109
111
  :param bool remove_builtin_exception_brackets: If brackets should be removed when raising exceptions with no arguments
110
112
  :param bool constant_folding: If literal expressions should be evaluated
111
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
112
115
 
113
116
  :rtype: str
114
117
 
@@ -155,12 +158,15 @@ def minify(
155
158
  if remove_debug:
156
159
  module = RemoveDebug()(module)
157
160
 
158
- if remove_explicit_return_none:
159
- module = RemoveExplicitReturnNone()(module)
160
-
161
161
  if constant_folding:
162
162
  module = FoldConstants()(module)
163
163
 
164
+ if remove_dead_branches:
165
+ module = RemoveDeadBranches()(module)
166
+
167
+ if remove_explicit_return_none:
168
+ module = RemoveExplicitReturnNone()(module)
169
+
164
170
  bind_names(module)
165
171
  resolve_names(module)
166
172
 
@@ -29,7 +29,8 @@ def minify(
29
29
  remove_explicit_return_none: bool = ...,
30
30
  remove_builtin_exception_brackets: bool = ...,
31
31
  constant_folding: bool = ...,
32
- prefer_single_line: bool = ...
32
+ prefer_single_line: bool = ...,
33
+ remove_dead_branches: bool = ...
33
34
  ) -> Text: ...
34
35
 
35
36
 
@@ -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."""
@@ -250,6 +249,12 @@ def parse_args():
250
249
  help='Disable evaluating literal expressions',
251
250
  dest='constant_folding',
252
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
+ )
253
258
 
254
259
  annotation_options = parser.add_argument_group('remove annotations options', 'Options that affect how annotations are removed')
255
260
  annotation_options.add_argument(
@@ -305,6 +310,10 @@ def parse_args():
305
310
  sys.stderr.write('error: --remove-class-attribute-annotations would do nothing when used with --no-remove-annotations\n')
306
311
  sys.exit(1)
307
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
+
308
317
  return args
309
318
 
310
319
 
@@ -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(')')
@@ -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
 
@@ -14,7 +14,7 @@ def is_foldable_constant(node):
14
14
  """
15
15
  Check if a node is a constant expression that can participate in folding.
16
16
 
17
- We can asume that children have already been folded, so foldable constants are either:
17
+ We can assume that children have already been folded, so foldable constants are either:
18
18
  - Simple literals (Num, NameConstant)
19
19
  - UnaryOp(USub/Invert) on a Num - these don't fold to shorter forms,
20
20
  so they remain after child visiting. UAdd and Not would have been
@@ -42,7 +42,14 @@ class FoldConstants(SuiteTransformer):
42
42
  # Evaluate the expression
43
43
  try:
44
44
  original_expression = unparse_expression(node)
45
- 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)
46
53
  except Exception:
47
54
  return node
48
55
 
@@ -119,6 +126,30 @@ class FoldConstants(SuiteTransformer):
119
126
 
120
127
  return self.fold(node)
121
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
+
122
153
  def visit_UnaryOp(self, node):
123
154
 
124
155
  node.operand = self.visit(node.operand)
@@ -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
@@ -8,9 +8,11 @@ from python_minifier.util import is_constant_node
8
8
 
9
9
  class RemoveDebug(SuiteTransformer):
10
10
  """
11
- Remove if statements where the condition tests __debug__ is True
11
+ Mark if statements whose condition tests __debug__ is True as dead
12
12
 
13
- If a statement is syntactically necessary, use an empty expression instead
13
+ The marked statements are removed by the RemoveDeadBranches transform, which
14
+ keeps any else branch and any branch that can't be removed without changing
15
+ the meaning of the program.
14
16
  """
15
17
 
16
18
  def __call__(self, node):
@@ -18,7 +20,11 @@ class RemoveDebug(SuiteTransformer):
18
20
 
19
21
  def constant_value(self, node):
20
22
  if sys.version_info < (3, 4):
21
- return node.id == 'True'
23
+ # True and False are Name nodes before python 3.4.
24
+ # The comparator may be any expression, so check it is a Name.
25
+ if isinstance(node, ast.Name) and node.id in ('True', 'False'):
26
+ return node.id == 'True'
27
+ return None
22
28
  elif is_constant_node(node, ast.NameConstant):
23
29
  return node.value
24
30
  return None
@@ -61,14 +67,10 @@ class RemoveDebug(SuiteTransformer):
61
67
  return True
62
68
  return False
63
69
 
64
- def suite(self, node_list, parent):
70
+ def visit_If(self, node):
71
+ assert isinstance(node, ast.If)
65
72
 
66
- without_debug = [self.visit(a) for a in filter(lambda n: not self.can_remove(n), node_list)]
73
+ if self.can_remove(node):
74
+ node.resolved_test = False
67
75
 
68
- if len(without_debug) == 0:
69
- if isinstance(parent, ast.Module):
70
- return []
71
- else:
72
- return [self.add_child(ast.Expr(value=ast.Num(0)), parent=parent)]
73
-
74
- return without_debug
76
+ return self.generic_visit(node)
@@ -117,6 +117,51 @@ class SuiteTransformer(NodeVisitor):
117
117
 
118
118
  return node
119
119
 
120
+ def visit_TryStar(self, node):
121
+ return self.visit_Try(node)
122
+
123
+ def visit_TryFinally(self, node):
124
+ # Python 2
125
+ node.body = self.suite(node.body, parent=node)
126
+ node.finalbody = self.suite(node.finalbody, parent=node)
127
+ return node
128
+
129
+ def visit_TryExcept(self, node):
130
+ # Python 2
131
+ node.body = self.suite(node.body, parent=node)
132
+
133
+ node.handlers = [self.visit(h) for h in node.handlers]
134
+
135
+ if node.orelse:
136
+ node.orelse = self.suite(node.orelse, parent=node)
137
+
138
+ return node
139
+
140
+ def visit_ExceptHandler(self, node):
141
+ if node.type is not None:
142
+ node.type = self.visit(node.type)
143
+
144
+ if node.name is not None and isinstance(node.name, ast.AST):
145
+ # Python 2 uses a Name node
146
+ node.name = self.visit(node.name)
147
+
148
+ node.body = self.suite(node.body, parent=node)
149
+ return node
150
+
151
+ def visit_Match(self, node):
152
+ node.subject = self.visit(node.subject)
153
+ node.cases = [self.visit(c) for c in node.cases]
154
+ return node
155
+
156
+ def visit_match_case(self, node):
157
+ node.pattern = self.visit(node.pattern)
158
+
159
+ if node.guard is not None:
160
+ node.guard = self.visit(node.guard)
161
+
162
+ node.body = self.suite(node.body, parent=node)
163
+ return node
164
+
120
165
  def visit_While(self, node):
121
166
  node.test = self.visit(node.test)
122
167
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_minifier
3
- Version: 3.2.0
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
@@ -40,6 +40,7 @@ src/python_minifier/transforms/remove_annotations.py
40
40
  src/python_minifier/transforms/remove_annotations_options.py
41
41
  src/python_minifier/transforms/remove_annotations_options.pyi
42
42
  src/python_minifier/transforms/remove_asserts.py
43
+ src/python_minifier/transforms/remove_dead_branches.py
43
44
  src/python_minifier/transforms/remove_debug.py
44
45
  src/python_minifier/transforms/remove_exception_brackets.py
45
46
  src/python_minifier/transforms/remove_explicit_return_none.py
@@ -83,6 +84,7 @@ test/test_raw_fstring_backslash.py
83
84
  test/test_raw_tstring_backslash.py
84
85
  test/test_remove_annotations.py
85
86
  test/test_remove_assert.py
87
+ test/test_remove_dead_branches.py
86
88
  test/test_remove_debug.py
87
89
  test/test_remove_exception_brackets.py
88
90
  test/test_remove_explicit_return_none.py
@@ -9,7 +9,7 @@ def run_subprocess(cmd, timeout=None, input_data=None, env=None):
9
9
  # Python 3.5+ - encode string input to bytes for subprocess
10
10
  input_bytes = input_data.encode('utf-8') if isinstance(input_data, str) else input_data
11
11
  return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
12
- input=input_bytes, timeout=timeout, env=env)
12
+ input=input_bytes, timeout=timeout, env=env, check=False)
13
13
  # Python 2.7, 3.3, 3.4 - no subprocess.run, no timeout support
14
14
  popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
15
15
  stdin=subprocess.PIPE if input_data else None, env=env)
@@ -86,6 +86,25 @@ def test_import_in_function():
86
86
  compare_ast(expected_ast, actual_ast)
87
87
 
88
88
 
89
+ def test_import_in_except_handler():
90
+ # SuiteTransformer now visits except handler bodies
91
+ source = '''try:
92
+ pass
93
+ except Exception:
94
+ import collection as c
95
+ import builtins
96
+ '''
97
+ expected = '''try:
98
+ pass
99
+ except Exception:
100
+ import collection as c, builtins
101
+ '''
102
+
103
+ expected_ast = ast.parse(expected)
104
+ actual_ast = combine_imports(ast.parse(source))
105
+ compare_ast(expected_ast, actual_ast)
106
+
107
+
89
108
  def test_import_star():
90
109
  source = '''
91
110
  from breakfast import hashbrown