python-minifier 2.11.2__tar.gz → 3.0.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 (95) hide show
  1. {python_minifier-2.11.2/src/python_minifier.egg-info → python_minifier-3.0.0}/PKG-INFO +15 -3
  2. {python_minifier-2.11.2 → python_minifier-3.0.0}/README.md +1 -1
  3. {python_minifier-2.11.2 → python_minifier-3.0.0}/setup.py +1 -1
  4. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/__init__.py +14 -9
  5. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/__init__.pyi +8 -3
  6. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/__main__.py +77 -15
  7. python_minifier-3.0.0/src/python_minifier/ast_annotation/__init__.py +82 -0
  8. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/ast_compare.py +12 -14
  9. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/ast_compat.py +38 -0
  10. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/ast_printer.py +8 -5
  11. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/expression_printer.py +14 -13
  12. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/f_string.py +44 -45
  13. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/ministring.py +3 -3
  14. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/module_printer.py +19 -16
  15. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/rename/__init__.py +1 -1
  16. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/rename/bind_names.py +2 -1
  17. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/rename/binding.py +39 -40
  18. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/rename/mapper.py +66 -50
  19. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/rename/rename_literals.py +17 -15
  20. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/rename/renamer.py +26 -28
  21. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/rename/resolve_names.py +8 -7
  22. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/rename/util.py +7 -5
  23. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/token_printer.py +12 -1
  24. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/combine_imports.py +0 -1
  25. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/constant_folding.py +16 -10
  26. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_annotations.py +18 -16
  27. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_annotations_options.py +3 -1
  28. python_minifier-3.0.0/src/python_minifier/transforms/remove_annotations_options.pyi +18 -0
  29. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_asserts.py +1 -2
  30. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_debug.py +4 -3
  31. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_exception_brackets.py +12 -7
  32. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_explicit_return_none.py +5 -4
  33. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_literal_statements.py +5 -6
  34. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_object_base.py +2 -1
  35. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_pass.py +1 -2
  36. python_minifier-3.0.0/src/python_minifier/transforms/remove_posargs.py +12 -0
  37. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/suite_transformer.py +6 -5
  38. python_minifier-3.0.0/src/python_minifier/util.py +42 -0
  39. {python_minifier-2.11.2 → python_minifier-3.0.0/src/python_minifier.egg-info}/PKG-INFO +15 -3
  40. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier.egg-info/SOURCES.txt +7 -2
  41. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_assignment_expressions.py +5 -2
  42. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_await_fstring.py +3 -0
  43. python_minifier-3.0.0/test/test_best_effort_cli.py +179 -0
  44. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_bind_names.py +61 -7
  45. python_minifier-3.0.0/test/test_bind_names_namedexpr.py +259 -0
  46. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_bind_names_python2.py +1 -0
  47. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_bind_names_python312.py +10 -1
  48. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_bind_names_python313.py +10 -1
  49. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_bind_names_python33.py +1 -0
  50. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_combine_imports.py +17 -8
  51. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_comprehension_rename.py +6 -1
  52. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_decorator_expressions.py +5 -2
  53. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_dict_expansion.py +3 -0
  54. python_minifier-3.0.0/test/test_folding.py +151 -0
  55. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_fstring.py +19 -16
  56. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_generic_types.py +5 -2
  57. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_hoist_literals.py +41 -11
  58. python_minifier-3.0.0/test/test_import.py +32 -0
  59. python_minifier-3.0.0/test/test_is_constant_node.py +53 -0
  60. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_iterable_unpacking.py +3 -0
  61. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_match.py +15 -10
  62. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_match_rename.py +55 -46
  63. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_name_generator.py +1 -0
  64. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_nonlocal.py +9 -6
  65. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_posargs.py +5 -1
  66. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_remove_annotations.py +96 -62
  67. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_remove_assert.py +18 -6
  68. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_remove_debug.py +29 -23
  69. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_remove_exception_brackets.py +13 -2
  70. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_remove_literal_statements.py +8 -2
  71. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_remove_pass.py +18 -6
  72. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_rename_builtins.py +9 -7
  73. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_rename_locals.py +69 -55
  74. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_slice.py +1 -0
  75. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_type_param_defaults.py +7 -1
  76. python_minifier-3.0.0/test/test_unicode_cli.py +170 -0
  77. python_minifier-3.0.0/test/test_utf8_encoding.py +114 -0
  78. python_minifier-2.11.2/src/python_minifier/transforms/remove_annotations_options.pyi +0 -17
  79. python_minifier-2.11.2/src/python_minifier/transforms/remove_posargs.py +0 -13
  80. python_minifier-2.11.2/src/python_minifier/util.py +0 -48
  81. python_minifier-2.11.2/test/test_folding.py +0 -136
  82. python_minifier-2.11.2/test/test_import.py +0 -30
  83. python_minifier-2.11.2/test/test_is_ast_node.py +0 -40
  84. {python_minifier-2.11.2 → python_minifier-3.0.0}/LICENSE +0 -0
  85. {python_minifier-2.11.2 → python_minifier-3.0.0}/setup.cfg +0 -0
  86. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/py.typed +0 -0
  87. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/rename/name_generator.py +0 -0
  88. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier/transforms/__init__.py +0 -0
  89. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
  90. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
  91. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier.egg-info/top_level.txt +0 -0
  92. {python_minifier-2.11.2 → python_minifier-3.0.0}/src/python_minifier.egg-info/zip-safe +0 -0
  93. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_preserve_shebang.py +0 -0
  94. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_remove_explicit_return_none.py +0 -0
  95. {python_minifier-2.11.2 → python_minifier-3.0.0}/test/test_remove_object.py +3 -3
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python_minifier
3
- Version: 2.11.2
3
+ Version: 3.0.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
@@ -34,6 +34,18 @@ Classifier: Topic :: Software Development
34
34
  Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14
35
35
  Description-Content-Type: text/markdown
36
36
  License-File: LICENSE
37
+ Dynamic: author
38
+ Dynamic: author-email
39
+ Dynamic: classifier
40
+ Dynamic: description
41
+ Dynamic: description-content-type
42
+ Dynamic: home-page
43
+ Dynamic: keywords
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: project-url
47
+ Dynamic: requires-python
48
+ Dynamic: summary
37
49
 
38
50
  # Python Minifier
39
51
 
@@ -130,7 +142,7 @@ def handler(event,context):
130
142
 
131
143
  ## Why?
132
144
 
133
- AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
145
+ AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
134
146
  than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
135
147
 
136
148
  ## Installation
@@ -93,7 +93,7 @@ def handler(event,context):
93
93
 
94
94
  ## Why?
95
95
 
96
- AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
96
+ AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
97
97
  than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
98
98
 
99
99
  ## Installation
@@ -28,7 +28,7 @@ setup(
28
28
  long_description_content_type='text/markdown',
29
29
 
30
30
  python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14',
31
- version='2.11.2',
31
+ version='3.0.0',
32
32
 
33
33
  classifiers=[
34
34
  'Development Status :: 5 - Production/Stable',
@@ -4,29 +4,30 @@ a 'minified' representation of the same source code.
4
4
 
5
5
  """
6
6
 
7
- import python_minifier.ast_compat as ast
8
7
  import re
9
8
 
9
+ import python_minifier.ast_compat as ast
10
+ from python_minifier.ast_annotation import add_parent
11
+
10
12
  from python_minifier.ast_compare import CompareError, compare_ast
11
13
  from python_minifier.module_printer import ModulePrinter
12
14
  from python_minifier.rename import (
13
- rename_literals,
14
- bind_names,
15
- resolve_names,
16
- rename,
15
+ add_namespace,
17
16
  allow_rename_globals,
18
17
  allow_rename_locals,
19
- add_namespace,
18
+ bind_names,
19
+ rename,
20
+ rename_literals,
21
+ resolve_names
20
22
  )
21
-
22
23
  from python_minifier.transforms.combine_imports import CombineImports
23
24
  from python_minifier.transforms.constant_folding import FoldConstants
24
25
  from python_minifier.transforms.remove_annotations import RemoveAnnotations
25
26
  from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
26
27
  from python_minifier.transforms.remove_asserts import RemoveAsserts
27
28
  from python_minifier.transforms.remove_debug import RemoveDebug
28
- from python_minifier.transforms.remove_explicit_return_none import RemoveExplicitReturnNone
29
29
  from python_minifier.transforms.remove_exception_brackets import remove_no_arg_exception_call
30
+ from python_minifier.transforms.remove_explicit_return_none import RemoveExplicitReturnNone
30
31
  from python_minifier.transforms.remove_literal_statements import RemoveLiteralStatements
31
32
  from python_minifier.transforms.remove_object_base import RemoveObject
32
33
  from python_minifier.transforms.remove_pass import RemovePass
@@ -85,7 +86,8 @@ def minify(
85
86
  :param str source: The python module source code
86
87
  :param str filename: The original source filename if known
87
88
 
88
- :param remove_annotations: Configures the removal of type annotations. True removes all annotations, False removes none. RemoveAnnotationsOptions can be used to configure the removal of specific annotations.
89
+ :param remove_annotations: Configures the removal of type annotations. True removes all annotations, False removes none.
90
+ RemoveAnnotationsOptions can be used to configure the removal of specific annotations.
89
91
  :type remove_annotations: bool or RemoveAnnotationsOptions
90
92
  :param bool remove_pass: If Pass statements should be removed where possible
91
93
  :param bool remove_literal_statements: If statements consisting of a single literal should be removed, including docstrings
@@ -115,6 +117,7 @@ def minify(
115
117
  # This will raise if the source file can't be parsed
116
118
  module = ast.parse(source, filename)
117
119
 
120
+ add_parent(module)
118
121
  add_namespace(module)
119
122
 
120
123
  if remove_literal_statements:
@@ -198,6 +201,7 @@ def minify(
198
201
 
199
202
  return minified
200
203
 
204
+
201
205
  def _find_shebang(source):
202
206
  """
203
207
  Find a shebang line in source
@@ -214,6 +218,7 @@ def _find_shebang(source):
214
218
 
215
219
  return None
216
220
 
221
+
217
222
  def unparse(module):
218
223
  """
219
224
  Turn a module AST into python code
@@ -1,13 +1,16 @@
1
1
  import ast
2
- from typing import List, Text, AnyStr, Optional, Any, Union
2
+
3
+ from typing import Any, List, Optional, Text, Union
3
4
 
4
5
  from .transforms.remove_annotations_options import RemoveAnnotationsOptions as RemoveAnnotationsOptions
5
6
 
7
+
6
8
  class UnstableMinification(RuntimeError):
7
9
  def __init__(self, exception: Any, source: Any, minified: Any): ...
8
10
 
11
+
9
12
  def minify(
10
- source: AnyStr,
13
+ source: Union[str, bytes],
11
14
  filename: Optional[str] = ...,
12
15
  remove_annotations: Union[bool, RemoveAnnotationsOptions] = ...,
13
16
  remove_pass: bool = ...,
@@ -28,10 +31,12 @@ def minify(
28
31
  constant_folding: bool = ...
29
32
  ) -> Text: ...
30
33
 
34
+
31
35
  def unparse(module: ast.Module) -> Text: ...
32
36
 
37
+
33
38
  def awslambda(
34
- source: AnyStr,
39
+ source: Union[str, bytes],
35
40
  filename: Optional[Text] = ...,
36
41
  entrypoint: Optional[Text] = ...
37
42
  ) -> Text: ...
@@ -7,14 +7,29 @@ import sys
7
7
  from python_minifier import minify
8
8
  from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
9
9
 
10
+
11
+ class MinificationNotBeneficialError(Exception):
12
+ """Raised when minification results in larger output than the original."""
13
+ pass
14
+
15
+ def stdout_write_bytes(data):
16
+ """Write bytes to stdout with proper Python 2.7/3.x compatibility."""
17
+ if sys.version_info >= (3, 0):
18
+ sys.stdout.buffer.write(data)
19
+ else:
20
+ sys.stdout.write(data)
21
+
22
+
10
23
  if sys.version_info >= (3, 8):
11
24
  from importlib import metadata
25
+
12
26
  try:
13
27
  version = metadata.version('python-minifier')
14
28
  except metadata.PackageNotFoundError:
15
29
  version = '0.0.0'
16
30
  else:
17
- from pkg_resources import get_distribution, DistributionNotFound
31
+ from pkg_resources import DistributionNotFound, get_distribution
32
+
18
33
  try:
19
34
  version = get_distribution('python_minifier').version
20
35
  except DistributionNotFound:
@@ -48,12 +63,23 @@ examples:
48
63
  if len(args.path) == 1 and args.path[0] == '-':
49
64
  # minify stdin
50
65
  source = sys.stdin.buffer.read() if sys.version_info >= (3, 0) else sys.stdin.read()
51
- minified = do_minify(source, 'stdin', args)
66
+ try:
67
+ minified = do_minify(source, 'stdin', args)
68
+ except MinificationNotBeneficialError:
69
+ # Use original source when minification isn't beneficial
70
+ if args.output:
71
+ with open(args.output, 'wb') as f:
72
+ f.write(source)
73
+ else:
74
+ # Write original source to stdout
75
+ stdout_write_bytes(source)
76
+ return
77
+
52
78
  if args.output:
53
- with open(args.output, 'w') as f:
79
+ with open(args.output, 'wb') as f:
54
80
  f.write(minified)
55
81
  else:
56
- sys.stdout.write(minified)
82
+ stdout_write_bytes(minified)
57
83
 
58
84
  else:
59
85
  # minify source paths
@@ -64,16 +90,30 @@ examples:
64
90
  with open(path, 'rb') as f:
65
91
  source = f.read()
66
92
 
67
- minified = do_minify(source, path, args)
93
+ try:
94
+ minified = do_minify(source, path, args)
95
+ except MinificationNotBeneficialError:
96
+ # Use original source when minification isn't beneficial
97
+ if args.in_place:
98
+ # File is already the original, no need to write
99
+ pass
100
+ elif args.output:
101
+ # Write original source to output
102
+ with open(args.output, 'wb') as f:
103
+ f.write(source)
104
+ else:
105
+ # Write original source to stdout
106
+ stdout_write_bytes(source)
107
+ continue
68
108
 
69
109
  if args.in_place:
70
- with open(path, 'w') as f:
110
+ with open(path, 'wb') as f:
71
111
  f.write(minified)
72
112
  elif args.output:
73
- with open(args.output, 'w') as f:
113
+ with open(args.output, 'wb') as f:
74
114
  f.write(minified)
75
115
  else:
76
- sys.stdout.write(minified)
116
+ stdout_write_bytes(minified)
77
117
 
78
118
 
79
119
  def parse_args():
@@ -170,25 +210,25 @@ def parse_args():
170
210
  minification_options.add_argument(
171
211
  '--no-preserve-shebang',
172
212
  action='store_false',
173
- help='Preserve any shebang line from the source',
213
+ help='Disable preserving any shebang line from the source',
174
214
  dest='preserve_shebang',
175
215
  )
176
216
  minification_options.add_argument(
177
217
  '--remove-asserts',
178
218
  action='store_true',
179
- help='Remove assert statements',
219
+ help='Enable removing assert statements',
180
220
  dest='remove_asserts',
181
221
  )
182
222
  minification_options.add_argument(
183
223
  '--remove-debug',
184
224
  action='store_true',
185
- help='Remove conditional statements that test __debug__ is True',
225
+ help='Enable removing conditional statements that test __debug__ is True',
186
226
  dest='remove_debug',
187
227
  )
188
228
  minification_options.add_argument(
189
229
  '--no-remove-explicit-return-none',
190
230
  action='store_false',
191
- help='Replace explicit return None with a bare return',
231
+ help='Disable replacing explicit return None with a bare return',
192
232
  dest='remove_explicit_return_none',
193
233
  )
194
234
  minification_options.add_argument(
@@ -268,15 +308,24 @@ def source_modules(args):
268
308
 
269
309
  for path_arg in args.path:
270
310
  if os.path.isdir(path_arg):
271
- for root, dirs, files in os.walk(path_arg, onerror=error, followlinks=True):
311
+ for root, _dirs, files in os.walk(path_arg, onerror=error, followlinks=True):
272
312
  for file in files:
273
- if file.endswith('.py') or file.endswith('.pyw'):
313
+ if file.endswith(('.py', '.pyw')):
274
314
  yield os.path.join(root, file)
275
315
  else:
276
316
  yield path_arg
277
317
 
278
318
 
279
319
  def do_minify(source, filename, minification_args):
320
+ """Minify Python source code with size-based fallback.
321
+
322
+ :param bytes source: Source code as bytes (from file 'rb' or stdin.buffer)
323
+ :param str filename: Filename for error reporting
324
+ :param argparse.Namespace minification_args: CLI arguments for minification options
325
+ :returns: Minified source code as UTF-8 bytes
326
+ :rtype: bytes
327
+ :raises MinificationNotBeneficialError: When minified output is larger than original
328
+ """
280
329
 
281
330
  preserve_globals = []
282
331
  if minification_args.preserve_globals:
@@ -305,7 +354,7 @@ def do_minify(source, filename, minification_args):
305
354
  remove_class_attribute_annotations=minification_args.remove_class_attribute_annotations,
306
355
  )
307
356
 
308
- return minify(
357
+ minified_result = minify(
309
358
  source,
310
359
  filename=filename,
311
360
  combine_imports=minification_args.combine_imports,
@@ -327,6 +376,19 @@ def do_minify(source, filename, minification_args):
327
376
  constant_folding=minification_args.constant_folding
328
377
  )
329
378
 
379
+ # Encode minified result to bytes for comparison and output
380
+ minified_bytes = minified_result.encode('utf-8')
381
+
382
+ # Check if environment variable forces minified output
383
+ if os.environ.get('PYMINIFY_FORCE_BEST_EFFORT'):
384
+ return minified_bytes
385
+
386
+ # Compare byte lengths for accurate size comparison
387
+ if len(minified_bytes) > len(source):
388
+ raise MinificationNotBeneficialError("Minified output is longer than original")
389
+
390
+ return minified_bytes
391
+
330
392
 
331
393
  if __name__ == '__main__':
332
394
  main()
@@ -0,0 +1,82 @@
1
+ """
2
+ This module provides utilities for annotating Abstract Syntax Tree (AST) nodes with parent references.
3
+ """
4
+
5
+ import ast
6
+
7
+ class _NoParent(ast.AST):
8
+ """A placeholder class used to indicate that a node has no parent."""
9
+
10
+ def __repr__(self):
11
+ # type: () -> str
12
+ return 'NoParent()'
13
+
14
+ def add_parent(node, parent=_NoParent()):
15
+ # type: (ast.AST, ast.AST) -> None
16
+ """
17
+ Recursively adds a parent reference to each node in the AST.
18
+
19
+ >>> tree = ast.parse('a = 1')
20
+ >>> add_parent(tree)
21
+ >>> get_parent(tree.body[0]) == tree
22
+ True
23
+
24
+ :param node: The current AST node.
25
+ :param parent: The parent :class:`ast.AST` node.
26
+ """
27
+
28
+ node._parent = parent # type: ignore[attr-defined]
29
+ for child in ast.iter_child_nodes(node):
30
+ add_parent(child, node)
31
+
32
+ def get_parent(node):
33
+ # type: (ast.AST) -> ast.AST
34
+ """
35
+ Retrieves the parent of the given AST node.
36
+
37
+ >>> tree = ast.parse('a = 1')
38
+ >>> add_parent(tree)
39
+ >>> get_parent(tree.body[0]) == tree
40
+ True
41
+
42
+ :param node: The AST node whose parent is to be retrieved.
43
+ :return: The parent AST node.
44
+ :raises ValueError: If the node has no parent.
45
+ """
46
+
47
+ if not hasattr(node, '_parent') or isinstance(node._parent, _NoParent): # type: ignore[attr-defined]
48
+ raise ValueError('Node has no parent')
49
+
50
+ return node._parent # type: ignore[attr-defined]
51
+
52
+ def set_parent(node, parent):
53
+ # type: (ast.AST, ast.AST) -> None
54
+ """
55
+ Replace the parent of the given AST node.
56
+
57
+ Create a simple AST:
58
+ >>> tree = ast.parse('a = func()')
59
+ >>> add_parent(tree)
60
+ >>> isinstance(tree.body[0], ast.Assign) and isinstance(tree.body[0].value, ast.Call)
61
+ True
62
+ >>> assign = tree.body[0]
63
+ >>> call = tree.body[0].value
64
+ >>> get_parent(call) == assign
65
+ True
66
+
67
+ Replace the parent of the call node:
68
+ >>> tree.body[0] = call
69
+ >>> set_parent(call, tree)
70
+ >>> get_parent(call) == tree
71
+ True
72
+ >>> from python_minifier.ast_printer import print_ast
73
+ >>> print(print_ast(tree))
74
+ Module(body=[
75
+ Call(Name('func'))
76
+ ])
77
+
78
+ :param node: The AST node whose parent is to be set.
79
+ :param parent: The parent AST node.
80
+ """
81
+
82
+ node._parent = parent # type: ignore[attr-defined]
@@ -1,7 +1,5 @@
1
1
  import python_minifier.ast_compat as ast
2
2
 
3
- from python_minifier.util import is_ast_node
4
-
5
3
 
6
4
  class CompareError(RuntimeError):
7
5
  """
@@ -18,7 +16,7 @@ class CompareError(RuntimeError):
18
16
 
19
17
  def namespace(self, node):
20
18
  if hasattr(node, 'namespace'):
21
- if is_ast_node(node.namespace, (ast.FunctionDef, ast.ClassDef, 'AsyncFunctionDef')):
19
+ if isinstance(node.namespace, (ast.FunctionDef, ast.ClassDef, ast.AsyncFunctionDef)):
22
20
  return self.namespace(node.namespace) + '.' + node.namespace.name
23
21
  elif isinstance(node.namespace, ast.Module):
24
22
  return ''
@@ -79,26 +77,26 @@ def compare_ast(l_ast, r_ast):
79
77
  % (type(l_ast), field, len(l_list), type(r_ast), field, len(r_list)),
80
78
  )
81
79
 
82
- for i, l, r in zip(counter(), l_list, r_list):
83
- if isinstance(l, ast.AST) or isinstance(r, ast.AST):
84
- compare_ast(l, r)
85
- elif l != r:
80
+ for i, left, right in zip(counter(), l_list, r_list):
81
+ if isinstance(left, ast.AST) or isinstance(right, ast.AST):
82
+ compare_ast(left, right)
83
+ elif left != right:
86
84
  raise CompareError(
87
85
  l_ast,
88
86
  r_ast,
89
87
  'Fields do not match! %s.%s[%i]=%r, %s.%s[%i]=%r'
90
- % (type(l_ast), field, i, l, type(r_ast), field, i, r),
88
+ % (type(l_ast), field, i, left, type(r_ast), field, i, right),
91
89
  )
92
90
 
93
91
  else:
94
- l = getattr(l_ast, field, None)
95
- r = getattr(r_ast, field, None)
92
+ left_field = getattr(l_ast, field, None)
93
+ right_field = getattr(r_ast, field, None)
96
94
 
97
- if isinstance(l, ast.AST) or isinstance(r, ast.AST):
98
- compare_ast(l, r)
99
- elif l != r:
95
+ if isinstance(left_field, ast.AST) or isinstance(right_field, ast.AST):
96
+ compare_ast(left_field, right_field)
97
+ elif left_field != right_field:
100
98
  raise CompareError(
101
99
  l_ast,
102
100
  r_ast,
103
- 'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field, l, type(r_ast), field, r),
101
+ 'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field, left_field, type(r_ast), field, right_field),
104
102
  )
@@ -8,6 +8,7 @@ deprecation warnings.
8
8
 
9
9
  from ast import *
10
10
 
11
+
11
12
  # Ideally we don't import anything else
12
13
 
13
14
  if 'TypeAlias' in globals():
@@ -16,6 +17,7 @@ if 'TypeAlias' in globals():
16
17
  Constant.n = property(lambda self: self.value, lambda self, value: setattr(self, 'value', value)) # type: ignore[assignment]
17
18
  Constant.s = property(lambda self: self.value, lambda self, value: setattr(self, 'value', value)) # type: ignore[assignment]
18
19
 
20
+
19
21
  # These classes are redefined from the ones in ast that complain about deprecation
20
22
  # They will continue to work once they are removed from ast
21
23
 
@@ -23,18 +25,54 @@ if 'TypeAlias' in globals():
23
25
  def __new__(cls, s, *args, **kwargs):
24
26
  return Constant(value=s, *args, **kwargs)
25
27
 
28
+
26
29
  class Bytes(Constant): # type: ignore[no-redef]
27
30
  def __new__(cls, s, *args, **kwargs):
28
31
  return Constant(value=s, *args, **kwargs)
29
32
 
33
+
30
34
  class Num(Constant): # type: ignore[no-redef]
31
35
  def __new__(cls, n, *args, **kwargs):
32
36
  return Constant(value=n, *args, **kwargs)
33
37
 
38
+
34
39
  class NameConstant(Constant): # type: ignore[no-redef]
35
40
  def __new__(cls, *args, **kwargs):
36
41
  return Constant(*args, **kwargs)
37
42
 
43
+
38
44
  class Ellipsis(Constant): # type: ignore[no-redef]
39
45
  def __new__(cls, *args, **kwargs):
40
46
  return Constant(value=literal_eval('...'), *args, **kwargs)
47
+
48
+
49
+ # Create a dummy class for missing AST nodes
50
+ for _node_type in [
51
+ 'AnnAssign',
52
+ 'AsyncFor',
53
+ 'AsyncFunctionDef',
54
+ 'AsyncFunctionDef',
55
+ 'AsyncWith',
56
+ 'Bytes',
57
+ 'Constant',
58
+ 'DictComp',
59
+ 'Exec',
60
+ 'ListComp',
61
+ 'MatchAs',
62
+ 'MatchMapping',
63
+ 'MatchStar',
64
+ 'NameConstant',
65
+ 'NamedExpr',
66
+ 'Nonlocal',
67
+ 'ParamSpec',
68
+ 'SetComp',
69
+ 'Starred',
70
+ 'TryStar',
71
+ 'TypeVar',
72
+ 'TypeVarTuple',
73
+ 'YieldFrom',
74
+ 'arg',
75
+ 'withitem',
76
+ ]:
77
+ if _node_type not in globals():
78
+ globals()[_node_type] = type(_node_type, (AST,), {})
@@ -12,7 +12,8 @@ fields or field names may be omitted for clarity. It should still be precise and
12
12
 
13
13
  import python_minifier.ast_compat as ast
14
14
 
15
- from python_minifier.util import is_ast_node
15
+ from python_minifier.util import is_constant_node
16
+
16
17
 
17
18
  INDENT = ' '
18
19
 
@@ -63,24 +64,26 @@ default_fields = {
63
64
  'AugAssign': 'op',
64
65
  }
65
66
 
67
+
66
68
  def is_literal(node, field):
67
69
  if hasattr(ast, 'Constant') and isinstance(node, ast.Constant) and field == 'value':
68
70
  return True
69
71
 
70
- if is_ast_node(node, ast.Num) and field == 'n':
72
+ if is_constant_node(node, ast.Num) and field == 'n':
71
73
  return True
72
74
 
73
- if is_ast_node(node, ast.Str) and field == 's':
75
+ if is_constant_node(node, ast.Str) and field == 's':
74
76
  return True
75
77
 
76
- if is_ast_node(node, 'Bytes') and field == 's':
78
+ if is_constant_node(node, ast.Bytes) and field == 's':
77
79
  return True
78
80
 
79
- if is_ast_node(node, 'NameConstant') and field == 'value':
81
+ if is_constant_node(node, ast.NameConstant) and field == 'value':
80
82
  return True
81
83
 
82
84
  return False
83
85
 
86
+
84
87
  def print_ast(node):
85
88
  if not isinstance(node, ast.AST):
86
89
  return repr(node)