python-minifier 3.3.0__tar.gz → 3.4.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 (107) hide show
  1. {python_minifier-3.3.0/src/python_minifier.egg-info → python_minifier-3.4.0}/PKG-INFO +5 -4
  2. {python_minifier-3.3.0 → python_minifier-3.4.0}/README.md +2 -2
  3. {python_minifier-3.3.0 → python_minifier-3.4.0}/setup.py +3 -2
  4. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/expression_printer.py +9 -3
  5. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/module_printer.py +5 -0
  6. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/mapper.py +3 -1
  7. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/token_printer.py +2 -2
  8. python_minifier-3.4.0/src/python_minifier/transforms/combine_imports.py +119 -0
  9. {python_minifier-3.3.0 → python_minifier-3.4.0/src/python_minifier.egg-info}/PKG-INFO +5 -4
  10. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier.egg-info/SOURCES.txt +2 -0
  11. python_minifier-3.4.0/test/test_comprehension_unpacking.py +188 -0
  12. python_minifier-3.4.0/test/test_lazy_imports.py +338 -0
  13. python_minifier-3.3.0/src/python_minifier/transforms/combine_imports.py +0 -75
  14. {python_minifier-3.3.0 → python_minifier-3.4.0}/LICENSE +0 -0
  15. {python_minifier-3.3.0 → python_minifier-3.4.0}/MANIFEST.in +0 -0
  16. {python_minifier-3.3.0 → python_minifier-3.4.0}/setup.cfg +0 -0
  17. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/__init__.py +0 -0
  18. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/__init__.pyi +0 -0
  19. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/__main__.py +0 -0
  20. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
  21. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/ast_compare.py +0 -0
  22. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/ast_compat.py +0 -0
  23. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/ast_printer.py +0 -0
  24. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/f_string.py +0 -0
  25. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/ministring.py +0 -0
  26. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/py.typed +0 -0
  27. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/README.md +0 -0
  28. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/__init__.py +0 -0
  29. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/bind_names.py +0 -0
  30. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/binding.py +0 -0
  31. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/name_generator.py +0 -0
  32. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/rename_literals.py +0 -0
  33. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/renamer.py +0 -0
  34. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/resolve_names.py +0 -0
  35. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/rename/util.py +0 -0
  36. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/t_string.py +0 -0
  37. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/__init__.py +0 -0
  38. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/constant_folding.py +0 -0
  39. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
  40. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
  41. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
  42. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
  43. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_dead_branches.py +0 -0
  44. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_debug.py +0 -0
  45. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
  46. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
  47. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
  48. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
  49. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_pass.py +0 -0
  50. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
  51. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/transforms/suite_transformer.py +0 -0
  52. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier/util.py +0 -0
  53. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
  54. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
  55. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier.egg-info/top_level.txt +0 -0
  56. {python_minifier-3.3.0 → python_minifier-3.4.0}/src/python_minifier.egg-info/zip-safe +0 -0
  57. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/ast_annotation/test_add_parent.py +0 -0
  58. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/conftest.py +0 -0
  59. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/helpers.py +0 -0
  60. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/requirements.txt +0 -0
  61. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/subprocess_compat.py +0 -0
  62. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_assignment_expressions.py +0 -0
  63. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_await_fstring.py +0 -0
  64. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_best_effort_cli.py +0 -0
  65. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_bind_names.py +0 -0
  66. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_bind_names_namedexpr.py +0 -0
  67. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_bind_names_python2.py +0 -0
  68. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_bind_names_python312.py +0 -0
  69. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_bind_names_python313.py +0 -0
  70. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_bind_names_python33.py +0 -0
  71. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_combine_imports.py +0 -0
  72. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_comprehension_rename.py +0 -0
  73. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_decorator_expressions.py +0 -0
  74. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_dict_expansion.py +0 -0
  75. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_folding.py +0 -0
  76. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_fstring.py +0 -0
  77. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_generic_types.py +0 -0
  78. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_hoist_literals.py +0 -0
  79. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_import.py +0 -0
  80. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_is_constant_node.py +0 -0
  81. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_iterable_unpacking.py +0 -0
  82. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_match.py +0 -0
  83. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_match_rename.py +0 -0
  84. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_name_generator.py +0 -0
  85. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_nonlocal.py +0 -0
  86. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_posargs.py +0 -0
  87. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_prefer_single_line.py +0 -0
  88. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_preserve_shebang.py +0 -0
  89. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_raw_fstring_backslash.py +0 -0
  90. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_raw_tstring_backslash.py +0 -0
  91. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_remove_annotations.py +0 -0
  92. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_remove_assert.py +0 -0
  93. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_remove_dead_branches.py +0 -0
  94. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_remove_debug.py +0 -0
  95. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_remove_exception_brackets.py +0 -0
  96. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_remove_explicit_return_none.py +0 -0
  97. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_remove_literal_statements.py +0 -0
  98. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_remove_object.py +0 -0
  99. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_remove_pass.py +0 -0
  100. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_rename_builtins.py +0 -0
  101. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_rename_locals.py +0 -0
  102. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_slice.py +0 -0
  103. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_template_strings.py +0 -0
  104. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_tuple_with_bug.py +0 -0
  105. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_type_param_defaults.py +0 -0
  106. {python_minifier-3.3.0 → python_minifier-3.4.0}/test/test_unicode_cli.py +0 -0
  107. {python_minifier-3.3.0 → python_minifier-3.4.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.3.0
3
+ Version: 3.4.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
@@ -26,13 +26,14 @@ Classifier: Programming Language :: Python :: 3.11
26
26
  Classifier: Programming Language :: Python :: 3.12
27
27
  Classifier: Programming Language :: Python :: 3.13
28
28
  Classifier: Programming Language :: Python :: 3.14
29
+ Classifier: Programming Language :: Python :: 3.15
29
30
  Classifier: Programming Language :: Python :: 2
30
31
  Classifier: Programming Language :: Python :: 2.7
31
32
  Classifier: Programming Language :: Python :: Implementation :: CPython
32
33
  Classifier: Programming Language :: Python :: Implementation :: PyPy
33
34
  Classifier: Intended Audience :: Developers
34
35
  Classifier: Topic :: Software Development
35
- Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15
36
+ Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.16
36
37
  Description-Content-Type: text/markdown
37
38
  License-File: LICENSE
38
39
  Dynamic: author
@@ -54,7 +55,7 @@ Transforms Python source code into its most compact representation.
54
55
 
55
56
  [Try it out!](https://python-minifier.com)
56
57
 
57
- python-minifier currently supports Python 2.7 and Python 3.3 to 3.14. Previous releases supported Python 2.6.
58
+ python-minifier currently supports Python 2.7 and Python 3.3 to 3.15. Previous releases supported Python 2.6.
58
59
 
59
60
  * [PyPI](https://pypi.org/project/python-minifier/)
60
61
  * [Documentation](https://dflook.github.io/python-minifier/)
@@ -160,7 +161,7 @@ and outputs source code compatible with the version of the interpreter it is run
160
161
  This means that if you minify code written for Python 3.11 using python-minifier running with Python 3.12,
161
162
  the minified code may only run with Python 3.12.
162
163
 
163
- python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.14.
164
+ python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.15.
164
165
 
165
166
  ## Usage
166
167
 
@@ -4,7 +4,7 @@ Transforms Python source code into its most compact representation.
4
4
 
5
5
  [Try it out!](https://python-minifier.com)
6
6
 
7
- python-minifier currently supports Python 2.7 and Python 3.3 to 3.14. Previous releases supported Python 2.6.
7
+ python-minifier currently supports Python 2.7 and Python 3.3 to 3.15. Previous releases supported Python 2.6.
8
8
 
9
9
  * [PyPI](https://pypi.org/project/python-minifier/)
10
10
  * [Documentation](https://dflook.github.io/python-minifier/)
@@ -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.14.
113
+ python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.15.
114
114
 
115
115
  ## Usage
116
116
 
@@ -26,8 +26,8 @@ setup(
26
26
  long_description=long_desc,
27
27
  long_description_content_type='text/markdown',
28
28
 
29
- python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15',
30
- version='3.3.0',
29
+ python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.16',
30
+ version='3.4.0',
31
31
 
32
32
  classifiers=[
33
33
  'Development Status :: 5 - Production/Stable',
@@ -46,6 +46,7 @@ setup(
46
46
  'Programming Language :: Python :: 3.12',
47
47
  'Programming Language :: Python :: 3.13',
48
48
  'Programming Language :: Python :: 3.14',
49
+ 'Programming Language :: Python :: 3.15',
49
50
  'Programming Language :: Python :: 2',
50
51
  'Programming Language :: Python :: 2.7',
51
52
  'Programming Language :: Python :: Implementation :: CPython',
@@ -524,9 +524,15 @@ class ExpressionPrinter(object):
524
524
 
525
525
  def visit_DictComp(self, node):
526
526
  self.printer.delimiter('{')
527
- self._expression(node.key)
528
- self.printer.delimiter(':')
529
- self._expression(node.value)
527
+
528
+ if node.value is None:
529
+ self.printer.operator('**')
530
+ self._expression(node.key)
531
+ else:
532
+ self._expression(node.key)
533
+ self.printer.delimiter(':')
534
+ self._expression(node.value)
535
+
530
536
  [self.visit_comprehension(x) for x in node.generators]
531
537
  self.printer.delimiter('}')
532
538
 
@@ -234,6 +234,8 @@ class ModulePrinter(ExpressionPrinter):
234
234
  def visit_Import(self, node):
235
235
  assert isinstance(node, ast.Import)
236
236
 
237
+ if getattr(node, 'is_lazy', 0) == 1:
238
+ self.printer.keyword('lazy')
237
239
  self.printer.keyword('import')
238
240
 
239
241
  delimiter = Delimiter(self.printer)
@@ -246,6 +248,9 @@ class ModulePrinter(ExpressionPrinter):
246
248
  def visit_ImportFrom(self, node):
247
249
  assert isinstance(node, ast.ImportFrom)
248
250
 
251
+ if getattr(node, 'is_lazy', 0) == 1:
252
+ self.printer.keyword('lazy')
253
+
249
254
  self.printer.keyword('from')
250
255
  for _i in range(node.level):
251
256
  self.printer.delimiter('.')
@@ -104,7 +104,9 @@ def add_parent_to_comprehension(node, namespace):
104
104
  add_parent(node.elt, namespace=node)
105
105
  elif hasattr(node, 'key'):
106
106
  add_parent(node.key, namespace=node)
107
- add_parent(node.value, namespace=node)
107
+
108
+ if node.value is not None:
109
+ add_parent(node.value, namespace=node)
108
110
 
109
111
  iter_namespace = namespace
110
112
  for generator in node.generators:
@@ -130,7 +130,7 @@ class TokenPrinter(object):
130
130
  'or', 'pass', 'raise', 'return',
131
131
  'try', 'while', 'with', 'yield', '_',
132
132
  'case', 'match', 'print', 'exec',
133
- 'type'
133
+ 'type', 'lazy'
134
134
  ]
135
135
 
136
136
  if self.previous_token in [TokenTypes.Identifier, TokenTypes.Keyword, TokenTypes.SoftKeyword, TokenTypes.NumberLiteral]:
@@ -138,7 +138,7 @@ class TokenPrinter(object):
138
138
 
139
139
  self._code += kw
140
140
 
141
- if kw in ['_', 'case', 'match', 'type']:
141
+ if kw in ['_', 'case', 'match', 'type', 'lazy']:
142
142
  self.previous_token = TokenTypes.SoftKeyword
143
143
  else:
144
144
  self.previous_token = TokenTypes.Keyword
@@ -0,0 +1,119 @@
1
+ import python_minifier.ast_compat as ast
2
+
3
+ from python_minifier.transforms.suite_transformer import SuiteTransformer
4
+
5
+
6
+ def import_kwargs(import_node):
7
+ lazy = getattr(import_node, 'is_lazy', None)
8
+ if lazy is None:
9
+ return {}
10
+ return {'is_lazy': lazy}
11
+
12
+
13
+ class CombineImports(SuiteTransformer):
14
+ """
15
+ Combine multiple import statements where possible
16
+
17
+ This doesn't change the order of imports
18
+
19
+ """
20
+
21
+ def _combine_import(self, node_list, parent):
22
+
23
+ prev_import = None
24
+ alias = []
25
+
26
+ def can_merge_with_previous_group(statement):
27
+ if not isinstance(statement, ast.Import):
28
+ return False
29
+
30
+ if prev_import is None:
31
+ return True
32
+
33
+ if getattr(statement, 'is_lazy', None) != getattr(prev_import, 'is_lazy', None):
34
+ return False
35
+
36
+ return True
37
+
38
+ def can_create_group(statement):
39
+ return isinstance(statement, ast.Import)
40
+
41
+ for statement in node_list:
42
+ if can_merge_with_previous_group(statement):
43
+ prev_import = statement
44
+ alias += statement.names
45
+ else:
46
+ if alias:
47
+ yield self.add_child(ast.Import(names=alias, **import_kwargs(prev_import)), parent=parent, namespace=prev_import.namespace)
48
+ prev_import = None
49
+ alias = []
50
+
51
+ if can_create_group(statement):
52
+ prev_import = statement
53
+ alias += statement.names
54
+ else:
55
+ yield statement
56
+
57
+ if alias:
58
+ yield self.add_child(ast.Import(names=alias, **import_kwargs(prev_import)), parent=parent, namespace=prev_import.namespace)
59
+
60
+ def _combine_import_from(self, node_list, parent):
61
+
62
+ prev_import = None
63
+ alias = []
64
+
65
+ def can_merge_with_previous_group(statement):
66
+ if not isinstance(statement, ast.ImportFrom):
67
+ return False
68
+
69
+ if len(statement.names) == 1 and statement.names[0].name == '*':
70
+ return False
71
+
72
+ if prev_import is None:
73
+ return True
74
+
75
+ if getattr(statement, 'is_lazy', None) != getattr(prev_import, 'is_lazy', None):
76
+ return False
77
+
78
+ if statement.module == prev_import.module and statement.level == prev_import.level:
79
+ return True
80
+
81
+ return False
82
+
83
+ def can_create_group(statement):
84
+ if not isinstance(statement, ast.ImportFrom):
85
+ return False
86
+
87
+ if len(statement.names) == 1 and statement.names[0].name == '*':
88
+ return False
89
+
90
+ return True
91
+
92
+ for statement in node_list:
93
+ if can_merge_with_previous_group(statement):
94
+ prev_import = statement
95
+ alias += statement.names
96
+ else:
97
+ if alias:
98
+ yield self.add_child(
99
+ ast.ImportFrom(module=prev_import.module, names=alias, level=prev_import.level, **import_kwargs(prev_import)), parent=parent, namespace=prev_import.namespace
100
+ )
101
+ prev_import = None
102
+ alias = []
103
+
104
+ if can_create_group(statement):
105
+ prev_import = statement
106
+ alias += statement.names
107
+ else:
108
+ yield statement
109
+
110
+ if alias:
111
+ yield self.add_child(
112
+ ast.ImportFrom(module=prev_import.module, names=alias, level=prev_import.level, **import_kwargs(prev_import)), parent=parent, namespace=prev_import.namespace
113
+ )
114
+
115
+ def suite(self, node_list, parent):
116
+ a = list(self._combine_import(node_list, parent))
117
+ b = list(self._combine_import_from(a, parent))
118
+
119
+ return [self.visit(n) for n in b]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_minifier
3
- Version: 3.3.0
3
+ Version: 3.4.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
@@ -26,13 +26,14 @@ Classifier: Programming Language :: Python :: 3.11
26
26
  Classifier: Programming Language :: Python :: 3.12
27
27
  Classifier: Programming Language :: Python :: 3.13
28
28
  Classifier: Programming Language :: Python :: 3.14
29
+ Classifier: Programming Language :: Python :: 3.15
29
30
  Classifier: Programming Language :: Python :: 2
30
31
  Classifier: Programming Language :: Python :: 2.7
31
32
  Classifier: Programming Language :: Python :: Implementation :: CPython
32
33
  Classifier: Programming Language :: Python :: Implementation :: PyPy
33
34
  Classifier: Intended Audience :: Developers
34
35
  Classifier: Topic :: Software Development
35
- Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15
36
+ Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.16
36
37
  Description-Content-Type: text/markdown
37
38
  License-File: LICENSE
38
39
  Dynamic: author
@@ -54,7 +55,7 @@ Transforms Python source code into its most compact representation.
54
55
 
55
56
  [Try it out!](https://python-minifier.com)
56
57
 
57
- python-minifier currently supports Python 2.7 and Python 3.3 to 3.14. Previous releases supported Python 2.6.
58
+ python-minifier currently supports Python 2.7 and Python 3.3 to 3.15. Previous releases supported Python 2.6.
58
59
 
59
60
  * [PyPI](https://pypi.org/project/python-minifier/)
60
61
  * [Documentation](https://dflook.github.io/python-minifier/)
@@ -160,7 +161,7 @@ and outputs source code compatible with the version of the interpreter it is run
160
161
  This means that if you minify code written for Python 3.11 using python-minifier running with Python 3.12,
161
162
  the minified code may only run with Python 3.12.
162
163
 
163
- python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.14.
164
+ python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.15.
164
165
 
165
166
  ## Usage
166
167
 
@@ -64,6 +64,7 @@ test/test_bind_names_python313.py
64
64
  test/test_bind_names_python33.py
65
65
  test/test_combine_imports.py
66
66
  test/test_comprehension_rename.py
67
+ test/test_comprehension_unpacking.py
67
68
  test/test_decorator_expressions.py
68
69
  test/test_dict_expansion.py
69
70
  test/test_folding.py
@@ -73,6 +74,7 @@ test/test_hoist_literals.py
73
74
  test/test_import.py
74
75
  test/test_is_constant_node.py
75
76
  test/test_iterable_unpacking.py
77
+ test/test_lazy_imports.py
76
78
  test/test_match.py
77
79
  test/test_match_rename.py
78
80
  test/test_name_generator.py
@@ -0,0 +1,188 @@
1
+ """
2
+ PEP 798 - unpacking in comprehensions, new in Python 3.15
3
+
4
+ [*L for L in lists] {*s for s in sets}
5
+ (*L for L in lists) {**d for d in dicts}
6
+
7
+ The dict form is represented as a DictComp whose `value` is None, with the
8
+ unpacked expression in `key`. The iterable forms reuse Starred in the existing
9
+ `elt` slot, so none of this changes any node's `_fields`.
10
+ """
11
+
12
+ import ast
13
+ import sys
14
+
15
+ import pytest
16
+
17
+ from python_minifier import minify, unparse
18
+ from python_minifier.ast_compare import compare_ast
19
+
20
+
21
+ def run(source):
22
+ """
23
+ Execute a module and return the data it defines, for comparing behaviour
24
+
25
+ Functions and classes are dropped: minifying renames them, and two function
26
+ objects never compare equal anyway, so only the values they compute are
27
+ useful for comparison.
28
+ """
29
+
30
+ namespace = {}
31
+ exec(compile(source, '<test>', 'exec'), namespace)
32
+ return {
33
+ k: v for k, v in namespace.items()
34
+ if not k.startswith('__') and not callable(v)
35
+ }
36
+
37
+
38
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
39
+ @pytest.mark.parametrize(
40
+ 'statement', [
41
+ 'x=[*L for L in lists]',
42
+ 'x=[*L for L in lists if L]',
43
+ 'x=[*(a,b)for a in c]',
44
+ 'x={*s for s in sets}',
45
+ 'x=(*L for L in lists)',
46
+ 'x=[*L for L in lists for M in L]'
47
+ ]
48
+ )
49
+ def test_iterable_comprehension_unpacking_unparse(statement):
50
+ """The * form in list/set/generator comprehensions survives a print/parse round trip"""
51
+ assert unparse(ast.parse(statement)) == statement
52
+
53
+
54
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
55
+ @pytest.mark.parametrize(
56
+ 'statement', [
57
+ 'x={**d for d in dicts}',
58
+ 'x={**d for d in dicts if d}',
59
+ "x={**{'a':1}for a in c}"
60
+ ]
61
+ )
62
+ def test_dict_comprehension_unpacking_unparse(statement):
63
+ """The ** form in dict comprehensions survives a print/parse round trip"""
64
+ assert unparse(ast.parse(statement)) == statement
65
+
66
+
67
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
68
+ @pytest.mark.parametrize(
69
+ 'statement', [
70
+ 'x=[*L for L in lists]',
71
+ 'x={*s for s in sets}',
72
+ 'x=(*L for L in lists)',
73
+ 'x={**d for d in dicts}'
74
+ ]
75
+ )
76
+ def test_comprehension_unpacking_ast_preserved(statement):
77
+ """Minifying does not change what the comprehension means"""
78
+ expected_ast = ast.parse(statement)
79
+ actual_ast = ast.parse(unparse(ast.parse(statement)))
80
+ compare_ast(expected_ast, actual_ast)
81
+
82
+
83
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
84
+ def test_iterable_unpacking_behaviour_is_preserved():
85
+ source = '''
86
+ lists = [[1, 2], [3, 4], [5]]
87
+ sets = [{1, 2}, {2, 3}]
88
+ flat_list = [*L for L in lists]
89
+ flat_set = sorted({*s for s in sets})
90
+ flat_gen = list((*L for L in lists))
91
+ '''
92
+ assert run(minify(source)) == run(source)
93
+
94
+
95
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
96
+ def test_dict_unpacking_behaviour_is_preserved():
97
+ source = '''
98
+ dicts = [{'a': 1}, {'b': 2}, {'a': 3}]
99
+ merged = {**d for d in dicts}
100
+ '''
101
+ assert run(minify(source)) == run(source)
102
+
103
+
104
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
105
+ def test_dict_unpacking_in_function_is_preserved():
106
+ """Renaming must reach inside the unpacked expression of a dict comprehension"""
107
+ source = '''
108
+ def merge(dictionaries):
109
+ return {**dictionary for dictionary in dictionaries}
110
+ result = merge([{'a': 1}, {'b': 2}])
111
+ '''
112
+ assert run(minify(source)) == run(source)
113
+
114
+
115
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
116
+ def test_async_comprehension_unpacking():
117
+ source = 'async def f(g):return(*a async for a in g())'
118
+ assert unparse(ast.parse(source)) == source
119
+
120
+
121
+ # PEP 798 only allows the unpacking operator at the *top level of the element
122
+ # expression*, not nested inside a subexpression. How much of an expression the
123
+ # operator swallows differs by comprehension kind, because the grammar rules
124
+ # differ - listcomp/setcomp take `star_named_expression`, genexp takes
125
+ # `starred_expression`, dictcomp takes `double_starred_kvpair`:
126
+ #
127
+ # [*x if c else z for x in y] SyntaxError - needs parentheses
128
+ # {*x if c else z for x in y} SyntaxError - needs parentheses
129
+ # (*x if c else z for x in y) legal - binds the whole conditional
130
+ # {**d if c else e for d in y} legal - binds the whole conditional
131
+ #
132
+ # The restriction has nothing to do with which `for` clause is involved;
133
+ # multiple and nested comprehensions are unrestricted.
134
+
135
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
136
+ @pytest.mark.parametrize(
137
+ 'source', [
138
+ 'q=[*x if c else z for x in y]',
139
+ 'q={*x if c else z for x in y}',
140
+ 'q=[**x for x in y]',
141
+ 'q=(**x for x in y)',
142
+ 'q={*k: v for k,v in y}'
143
+ ]
144
+ )
145
+ def test_unpacking_restrictions_are_syntax_errors(source):
146
+ """Forms PEP 798 does not allow, guarding the assumption the printer relies on"""
147
+ with pytest.raises(SyntaxError):
148
+ ast.parse(source)
149
+
150
+
151
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
152
+ @pytest.mark.parametrize(
153
+ ('source', 'expected'), [
154
+ # the iterable forms need the parentheses kept, or the result will not parse
155
+ ('q=[*(x if c else z) for x in y]', 'q=[*(x if c else z)for x in y]'),
156
+ ('q={*(x if c else z) for x in y}', 'q={*(x if c else z)for x in y}'),
157
+ # a tuple display element also needs its parentheses
158
+ ('q=[(*x, *z) for x in y]', 'q=[(*x,*z)for x in y]'),
159
+ # the dict form does not - the trailing `for` ends the expression
160
+ ('q={**(d if c else e) for d in y}', 'q={**d if c else e for d in y}'),
161
+ # the generator form does not require them either, but keeping them is
162
+ # still correct, and it is the only iterable form where they are optional
163
+ ('q=(*(x if c else z) for x in y)', 'q=(*(x if c else z)for x in y)')
164
+ ]
165
+ )
166
+ def test_unpacking_parenthesisation(source, expected):
167
+ """Parentheses are kept exactly where the grammar requires them, and dropped where it does not"""
168
+ assert unparse(ast.parse(source)) == expected
169
+ compare_ast(ast.parse(source), ast.parse(unparse(ast.parse(source))))
170
+
171
+
172
+ @pytest.mark.skipif(sys.version_info < (3, 15), reason='Comprehension unpacking requires Python 3.15+')
173
+ @pytest.mark.parametrize(
174
+ 'source', [
175
+ 'q=[*x for x in y for z in x]',
176
+ 'q=[*x for x in y if x]',
177
+ 'q=[[*x for x in y] for z in w]',
178
+ 'q=[*[*x for x in y] for z in w]',
179
+ 'q=[*(x := y) for r in s]',
180
+ 'q=f(*x for x in y)',
181
+ 'q=[*x + z for x in y]',
182
+ 'q=[*(lambda: 1)() for x in y]',
183
+ 'q=(*x if c else z for x in y)'
184
+ ]
185
+ )
186
+ def test_unpacking_in_compound_expressions(source):
187
+ """Unpacking composes with multiple for clauses, nesting, walrus and calls"""
188
+ compare_ast(ast.parse(source), ast.parse(unparse(ast.parse(source))))