python-minifier 3.1.0__tar.gz → 3.2.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.1.0/src/python_minifier.egg-info → python_minifier-3.2.0}/PKG-INFO +2 -2
  2. {python_minifier-3.1.0 → python_minifier-3.2.0}/README.md +1 -1
  3. {python_minifier-3.1.0 → python_minifier-3.2.0}/setup.py +1 -1
  4. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/__init__.py +7 -4
  5. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/__init__.pyi +6 -2
  6. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/__main__.py +9 -1
  7. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/ast_compare.py +1 -1
  8. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/ast_compat.py +1 -1
  9. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/expression_printer.py +4 -4
  10. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/module_printer.py +6 -6
  11. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/renamer.py +1 -1
  12. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/token_printer.py +2 -2
  13. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/constant_folding.py +59 -22
  14. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_debug.py +28 -8
  15. {python_minifier-3.1.0 → python_minifier-3.2.0/src/python_minifier.egg-info}/PKG-INFO +2 -2
  16. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier.egg-info/SOURCES.txt +1 -0
  17. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/ast_annotation/test_add_parent.py +2 -2
  18. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/conftest.py +1 -1
  19. python_minifier-3.2.0/test/subprocess_compat.py +37 -0
  20. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_best_effort_cli.py +40 -40
  21. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_bind_names_python33.py +25 -25
  22. python_minifier-3.2.0/test/test_folding.py +560 -0
  23. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_fstring.py +5 -5
  24. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_is_constant_node.py +6 -6
  25. python_minifier-3.2.0/test/test_prefer_single_line.py +217 -0
  26. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_raw_fstring_backslash.py +2 -2
  27. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_raw_tstring_backslash.py +3 -3
  28. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_remove_debug.py +58 -0
  29. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_template_strings.py +16 -16
  30. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_tuple_with_bug.py +1 -1
  31. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_utf8_encoding.py +2 -2
  32. python_minifier-3.1.0/test/subprocess_compat.py +0 -38
  33. python_minifier-3.1.0/test/test_folding.py +0 -151
  34. {python_minifier-3.1.0 → python_minifier-3.2.0}/LICENSE +0 -0
  35. {python_minifier-3.1.0 → python_minifier-3.2.0}/MANIFEST.in +0 -0
  36. {python_minifier-3.1.0 → python_minifier-3.2.0}/setup.cfg +0 -0
  37. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
  38. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/ast_printer.py +0 -0
  39. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/f_string.py +0 -0
  40. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/ministring.py +0 -0
  41. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/py.typed +0 -0
  42. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/README.md +0 -0
  43. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/__init__.py +0 -0
  44. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/bind_names.py +0 -0
  45. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/binding.py +0 -0
  46. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/mapper.py +0 -0
  47. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/name_generator.py +0 -0
  48. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/rename_literals.py +0 -0
  49. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/resolve_names.py +0 -0
  50. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/rename/util.py +0 -0
  51. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/t_string.py +0 -0
  52. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/__init__.py +0 -0
  53. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/combine_imports.py +0 -0
  54. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
  55. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
  56. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
  57. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
  58. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
  59. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
  60. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
  61. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
  62. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_pass.py +0 -0
  63. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
  64. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/transforms/suite_transformer.py +0 -0
  65. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier/util.py +0 -0
  66. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
  67. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
  68. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier.egg-info/top_level.txt +0 -0
  69. {python_minifier-3.1.0 → python_minifier-3.2.0}/src/python_minifier.egg-info/zip-safe +0 -0
  70. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/helpers.py +0 -0
  71. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/requirements.txt +0 -0
  72. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_assignment_expressions.py +0 -0
  73. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_await_fstring.py +0 -0
  74. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_bind_names.py +0 -0
  75. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_bind_names_namedexpr.py +0 -0
  76. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_bind_names_python2.py +0 -0
  77. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_bind_names_python312.py +0 -0
  78. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_bind_names_python313.py +0 -0
  79. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_combine_imports.py +0 -0
  80. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_comprehension_rename.py +0 -0
  81. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_decorator_expressions.py +0 -0
  82. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_dict_expansion.py +0 -0
  83. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_generic_types.py +0 -0
  84. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_hoist_literals.py +0 -0
  85. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_import.py +0 -0
  86. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_iterable_unpacking.py +0 -0
  87. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_match.py +0 -0
  88. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_match_rename.py +0 -0
  89. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_name_generator.py +0 -0
  90. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_nonlocal.py +0 -0
  91. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_posargs.py +0 -0
  92. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_preserve_shebang.py +0 -0
  93. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_remove_annotations.py +0 -0
  94. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_remove_assert.py +0 -0
  95. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_remove_exception_brackets.py +0 -0
  96. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_remove_explicit_return_none.py +0 -0
  97. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_remove_literal_statements.py +0 -0
  98. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_remove_object.py +0 -0
  99. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_remove_pass.py +0 -0
  100. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_rename_builtins.py +0 -0
  101. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_rename_locals.py +0 -0
  102. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_slice.py +0 -0
  103. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_type_param_defaults.py +0 -0
  104. {python_minifier-3.1.0 → python_minifier-3.2.0}/test/test_unicode_cli.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_minifier
3
- Version: 3.1.0
3
+ Version: 3.2.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
@@ -160,7 +160,7 @@ and outputs source code compatible with the version of the interpreter it is run
160
160
  This means that if you minify code written for Python 3.11 using python-minifier running with Python 3.12,
161
161
  the minified code may only run with Python 3.12.
162
162
 
163
- python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.13.
163
+ python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.14.
164
164
 
165
165
  ## Usage
166
166
 
@@ -110,7 +110,7 @@ and outputs source code compatible with the version of the interpreter it is run
110
110
  This means that if you minify code written for Python 3.11 using python-minifier running with Python 3.12,
111
111
  the minified code may only run with Python 3.12.
112
112
 
113
- python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.13.
113
+ python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.14.
114
114
 
115
115
  ## Usage
116
116
 
@@ -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.0',
30
+ version='3.2.0',
31
31
 
32
32
  classifiers=[
33
33
  'Development Status :: 5 - Production/Stable',
@@ -72,7 +72,8 @@ def minify(
72
72
  remove_debug=False,
73
73
  remove_explicit_return_none=True,
74
74
  remove_builtin_exception_brackets=True,
75
- constant_folding=True
75
+ constant_folding=True,
76
+ prefer_single_line=False,
76
77
  ):
77
78
  """
78
79
  Minify a python module
@@ -107,6 +108,7 @@ def minify(
107
108
  :param bool remove_explicit_return_none: If explicit return None statements should be replaced with a bare return
108
109
  :param bool remove_builtin_exception_brackets: If brackets should be removed when raising exceptions with no arguments
109
110
  :param bool constant_folding: If literal expressions should be evaluated
111
+ :param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size
110
112
 
111
113
  :rtype: str
112
114
 
@@ -192,7 +194,7 @@ def minify(
192
194
  if convert_posargs_to_args:
193
195
  module = remove_posargs(module)
194
196
 
195
- minified = unparse(module)
197
+ minified = unparse(module, prefer_single_line=prefer_single_line)
196
198
 
197
199
  if preserve_shebang is True:
198
200
  shebang_line = _find_shebang(source)
@@ -219,7 +221,7 @@ def _find_shebang(source):
219
221
  return None
220
222
 
221
223
 
222
- def unparse(module):
224
+ def unparse(module, prefer_single_line=False):
223
225
  """
224
226
  Turn a module AST into python code
225
227
 
@@ -228,13 +230,14 @@ def unparse(module):
228
230
 
229
231
  :param module: The module to turn into python code
230
232
  :type: module: :class:`ast.Module`
233
+ :param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size
231
234
  :rtype: str
232
235
 
233
236
  """
234
237
 
235
238
  assert isinstance(module, ast.Module)
236
239
 
237
- printer = ModulePrinter()
240
+ printer = ModulePrinter(prefer_single_line=prefer_single_line)
238
241
  printer(module)
239
242
 
240
243
  try:
@@ -28,11 +28,15 @@ 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 = ...
32
33
  ) -> Text: ...
33
34
 
34
35
 
35
- def unparse(module: ast.Module) -> Text: ...
36
+ def unparse(
37
+ module: ast.Module,
38
+ prefer_single_line: bool = ...
39
+ ) -> Text: ...
36
40
 
37
41
 
38
42
  def awslambda(
@@ -140,6 +140,13 @@ def parse_args():
140
140
  dest='in_place'
141
141
  )
142
142
 
143
+ parser.add_argument(
144
+ '--prefer-single-line',
145
+ action='store_true',
146
+ help='Prefer multiple statements on a single line separated by semicolons, instead of newlines, where there is no difference in output size',
147
+ dest='prefer_single_line',
148
+ )
149
+
143
150
  # Minification arguments
144
151
  minification_options = parser.add_argument_group('minification options', 'Options that affect how the source is minified')
145
152
  minification_options.add_argument(
@@ -373,7 +380,8 @@ def do_minify(source, filename, minification_args):
373
380
  remove_debug=minification_args.remove_debug,
374
381
  remove_explicit_return_none=minification_args.remove_explicit_return_none,
375
382
  remove_builtin_exception_brackets=minification_args.remove_exception_brackets,
376
- constant_folding=minification_args.constant_folding
383
+ constant_folding=minification_args.constant_folding,
384
+ prefer_single_line=minification_args.prefer_single_line,
377
385
  )
378
386
 
379
387
  # Encode minified result to bytes for comparison and output
@@ -63,7 +63,7 @@ def compare_ast(l_ast, r_ast):
63
63
 
64
64
  if field == 'kind' and isinstance(l_ast, ast.Constant):
65
65
  continue
66
-
66
+
67
67
  if field == 'str' and hasattr(ast, 'Interpolation') and isinstance(l_ast, ast.Interpolation):
68
68
  continue
69
69
 
@@ -77,4 +77,4 @@ for _node_type in [
77
77
  'withitem',
78
78
  ]:
79
79
  if _node_type not in globals():
80
- globals()[_node_type] = type(_node_type, (AST,), {})
80
+ globals()[_node_type] = type(_node_type, (AST,), {})
@@ -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
  """
@@ -301,8 +301,8 @@ class ExpressionPrinter(object):
301
301
 
302
302
  if value_precedence != 0 and (
303
303
  (op_precedence > value_precedence)
304
- or op_precedence == value_precedence
305
- and self._is_left_associative(node.op)
304
+ or (op_precedence == value_precedence
305
+ and self._is_left_associative(node.op))
306
306
  ):
307
307
  self.printer.delimiter('(')
308
308
  self._expression(v)
@@ -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):
@@ -596,7 +596,7 @@ class ModulePrinter(ExpressionPrinter):
596
596
  self.printer.keyword('case')
597
597
 
598
598
  if isinstance(node.pattern, ast.MatchSequence):
599
- self.visit_MatchSequence(node.pattern, open=True)
599
+ self.visit_MatchSequence(node.pattern, omit_brackets=True)
600
600
  else:
601
601
  self.pattern(node.pattern)
602
602
 
@@ -626,10 +626,10 @@ class ModulePrinter(ExpressionPrinter):
626
626
  else:
627
627
  self.printer.identifier(node.name)
628
628
 
629
- def visit_MatchSequence(self, node, open=False):
629
+ def visit_MatchSequence(self, node, omit_brackets=False):
630
630
  assert isinstance(node, ast.MatchSequence)
631
631
 
632
- if len(node.patterns) < 2 or not open:
632
+ if len(node.patterns) < 2 or not omit_brackets:
633
633
  self.printer.delimiter('[')
634
634
 
635
635
  delimiter = Delimiter(self.printer)
@@ -637,7 +637,7 @@ class ModulePrinter(ExpressionPrinter):
637
637
  delimiter.new_item()
638
638
  self.pattern(pattern)
639
639
 
640
- if len(node.patterns) < 2 or not open:
640
+ if len(node.patterns) < 2 or not omit_brackets:
641
641
  self.printer.delimiter(']')
642
642
 
643
643
  def visit_MatchMapping(self, node):
@@ -35,7 +35,7 @@ def sorted_bindings(module):
35
35
  """
36
36
 
37
37
  def comp(tup):
38
- namespace, binding = tup
38
+ _namespace, binding = tup
39
39
  return binding.new_mention_count()
40
40
 
41
41
  return sorted(all_bindings(module), key=comp, reverse=True)
@@ -103,7 +103,7 @@ class TokenPrinter(object):
103
103
  def __str__(self):
104
104
  """Return the output code."""
105
105
  return self._code
106
-
106
+
107
107
  def __unicode__(self):
108
108
  """Return the output code as unicode (for Python 2.7 compatibility)."""
109
109
  return self._code
@@ -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,36 +10,35 @@ 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):
14
- """
15
- Fold Constants if it would reduce the size of the source
13
+ def is_foldable_constant(node):
16
14
  """
15
+ Check if a node is a constant expression that can participate in folding.
17
16
 
18
- def __init__(self):
19
- super(FoldConstants, self).__init__()
17
+ We can asume 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.
22
+ """
23
+ if is_constant_node(node, (ast.Num, ast.NameConstant)):
24
+ return True
20
25
 
21
- def visit_BinOp(self, node):
26
+ if isinstance(node, ast.UnaryOp):
27
+ if isinstance(node.op, (ast.USub, ast.Invert)):
28
+ return is_constant_node(node.operand, ast.Num)
22
29
 
23
- node.left = self.visit(node.left)
24
- node.right = self.visit(node.right)
30
+ return False
25
31
 
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
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)
@@ -96,6 +95,44 @@ class FoldConstants(SuiteTransformer):
96
95
  # New representation is shorter and has the same value, so use it
97
96
  return self.add_child(new_node, get_parent(node), node.namespace)
98
97
 
98
+ def visit_BinOp(self, node):
99
+
100
+ node.left = self.visit(node.left)
101
+ node.right = self.visit(node.right)
102
+
103
+ # Check this is a constant expression that could be folded
104
+ # 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
105
+ if not is_foldable_constant(node.left):
106
+ return node
107
+ if not is_foldable_constant(node.right):
108
+ return node
109
+
110
+ if isinstance(node.op, ast.Div):
111
+ # Folding div is subtle, since it can have different results in Python 2 and Python 3
112
+ # Do this once target version options have been implemented
113
+ return node
114
+
115
+ if isinstance(node.op, ast.Pow):
116
+ # This can be folded, but it is unlikely to reduce the size of the source
117
+ # It can also be slow to evaluate
118
+ return node
119
+
120
+ return self.fold(node)
121
+
122
+ def visit_UnaryOp(self, node):
123
+
124
+ node.operand = self.visit(node.operand)
125
+
126
+ # Only fold if the operand is a foldable constant
127
+ if not is_foldable_constant(node.operand):
128
+ return node
129
+
130
+ # Only fold these unary operators
131
+ if not isinstance(node.op, (ast.USub, ast.UAdd, ast.Invert, ast.Not)):
132
+ return node
133
+
134
+ return self.fold(node)
135
+
99
136
 
100
137
  def equal_value_and_type(a, b):
101
138
  if type(a) != type(b):
@@ -27,18 +27,38 @@ class RemoveDebug(SuiteTransformer):
27
27
  if not isinstance(node, ast.If):
28
28
  return False
29
29
 
30
- if isinstance(node.test, ast.Name) and node.test.id == '__debug__':
31
- return True
30
+ def is_simple_debug_check():
31
+ # Simple case: if __debug__:
32
+ if isinstance(node.test, ast.Name) and node.test.id == '__debug__':
33
+ return True
34
+ return False
32
35
 
33
- if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Is) and self.constant_value(node.test.comparators[0]) is True:
34
- return True
36
+ def is_truthy_debug_comparison():
37
+ # Comparison case: if __debug__ is True / False / etc.
38
+ if not isinstance(node.test, ast.Compare):
39
+ return False
35
40
 
36
- if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.IsNot) and self.constant_value(node.test.comparators[0]) is False:
37
- return True
41
+ if not isinstance(node.test.left, ast.Name):
42
+ return False
38
43
 
39
- if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Eq) and self.constant_value(node.test.comparators[0]) is True:
40
- return True
44
+ if node.test.left.id != '__debug__':
45
+ return False
41
46
 
47
+ if len(node.test.ops) == 1:
48
+ op = node.test.ops[0]
49
+ comparator_value = self.constant_value(node.test.comparators[0])
50
+
51
+ if isinstance(op, ast.Is) and comparator_value is True:
52
+ return True
53
+ if isinstance(op, ast.IsNot) and comparator_value is False:
54
+ return True
55
+ if isinstance(op, ast.Eq) and comparator_value is True:
56
+ return True
57
+
58
+ return False
59
+
60
+ if is_simple_debug_check() or is_truthy_debug_comparison():
61
+ return True
42
62
  return False
43
63
 
44
64
  def suite(self, node_list, parent):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_minifier
3
- Version: 3.1.0
3
+ Version: 3.2.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
@@ -160,7 +160,7 @@ and outputs source code compatible with the version of the interpreter it is run
160
160
  This means that if you minify code written for Python 3.11 using python-minifier running with Python 3.12,
161
161
  the minified code may only run with Python 3.12.
162
162
 
163
- python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.13.
163
+ python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.14.
164
164
 
165
165
  ## Usage
166
166
 
@@ -77,6 +77,7 @@ test/test_match_rename.py
77
77
  test/test_name_generator.py
78
78
  test/test_nonlocal.py
79
79
  test/test_posargs.py
80
+ test/test_prefer_single_line.py
80
81
  test/test_preserve_shebang.py
81
82
  test/test_raw_fstring_backslash.py
82
83
  test/test_raw_tstring_backslash.py
@@ -30,13 +30,13 @@ class A:
30
30
  def test_no_parent_for_root_node():
31
31
  tree = ast.parse('a = 1')
32
32
  add_parent(tree)
33
- with pytest.raises(ValueError):
33
+ with pytest.raises(ValueError, match="Node has no parent"):
34
34
  get_parent(tree)
35
35
 
36
36
 
37
37
  def test_no_parent_for_unannotated_node():
38
38
  tree = ast.parse('a = 1')
39
- with pytest.raises(ValueError):
39
+ with pytest.raises(ValueError, match="Node has no parent"):
40
40
  get_parent(tree.body[0])
41
41
 
42
42
 
@@ -3,4 +3,4 @@ import os
3
3
 
4
4
  # Set default environment variable to preserve existing test behavior
5
5
  # Tests can explicitly unset this if they need to test size-based behavior
6
- os.environ.setdefault('PYMINIFY_FORCE_BEST_EFFORT', '1')
6
+ os.environ.setdefault('PYMINIFY_FORCE_BEST_EFFORT', '1')
@@ -0,0 +1,37 @@
1
+ """Subprocess compatibility utilities for Python 2.7/3.x."""
2
+ import subprocess
3
+ import sys
4
+
5
+
6
+ def run_subprocess(cmd, timeout=None, input_data=None, env=None):
7
+ """Cross-platform subprocess runner for Python 2.7+ compatibility."""
8
+ if hasattr(subprocess, 'run'):
9
+ # Python 3.5+ - encode string input to bytes for subprocess
10
+ input_bytes = input_data.encode('utf-8') if isinstance(input_data, str) else input_data
11
+ return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
12
+ input=input_bytes, timeout=timeout, env=env)
13
+ # Python 2.7, 3.3, 3.4 - no subprocess.run, no timeout support
14
+ popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
15
+ stdin=subprocess.PIPE if input_data else None, env=env)
16
+ # For Python 3.3/3.4, communicate() doesn't support timeout
17
+ # Also, Python 3.x needs bytes for stdin, Python 2.x needs str
18
+ if input_data and sys.version_info[0] >= 3 and isinstance(input_data, str):
19
+ input_data = input_data.encode('utf-8')
20
+ stdout, stderr = popen.communicate(input_data)
21
+ # Create a simple result object similar to subprocess.CompletedProcess
22
+ class Result:
23
+ def __init__(self, returncode, stdout, stderr):
24
+ self.returncode = returncode
25
+ self.stdout = stdout
26
+ self.stderr = stderr
27
+ return Result(popen.returncode, stdout, stderr)
28
+
29
+
30
+ def safe_decode(data, encoding='utf-8', errors='replace'):
31
+ """Safe decode for Python 2.7/3.x compatibility."""
32
+ if isinstance(data, bytes):
33
+ try:
34
+ return data.decode(encoding, errors)
35
+ except UnicodeDecodeError:
36
+ return data.decode(encoding, 'replace')
37
+ return data