python-minifier 3.0.0__tar.gz → 3.1.1__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 (101) hide show
  1. python_minifier-3.1.1/MANIFEST.in +21 -0
  2. {python_minifier-3.0.0/src/python_minifier.egg-info → python_minifier-3.1.1}/PKG-INFO +5 -4
  3. {python_minifier-3.0.0 → python_minifier-3.1.1}/README.md +2 -2
  4. {python_minifier-3.0.0 → python_minifier-3.1.1}/setup.py +3 -3
  5. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/ast_compare.py +4 -1
  6. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/ast_compat.py +3 -1
  7. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/expression_printer.py +9 -2
  8. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/f_string.py +120 -18
  9. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/module_printer.py +4 -4
  10. python_minifier-3.1.1/src/python_minifier/rename/README.md +93 -0
  11. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/rename/rename_literals.py +7 -0
  12. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/rename/renamer.py +1 -1
  13. python_minifier-3.1.1/src/python_minifier/t_string.py +330 -0
  14. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/token_printer.py +11 -1
  15. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_debug.py +28 -8
  16. {python_minifier-3.0.0 → python_minifier-3.1.1/src/python_minifier.egg-info}/PKG-INFO +5 -4
  17. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier.egg-info/SOURCES.txt +13 -1
  18. python_minifier-3.1.1/test/ast_annotation/test_add_parent.py +49 -0
  19. python_minifier-3.1.1/test/conftest.py +6 -0
  20. python_minifier-3.1.1/test/helpers.py +52 -0
  21. python_minifier-3.1.1/test/requirements.txt +1 -0
  22. python_minifier-3.1.1/test/subprocess_compat.py +37 -0
  23. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_best_effort_cli.py +40 -40
  24. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_bind_names_python33.py +25 -25
  25. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_fstring.py +5 -5
  26. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_hoist_literals.py +2 -2
  27. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_is_constant_node.py +42 -2
  28. python_minifier-3.1.1/test/test_raw_fstring_backslash.py +80 -0
  29. python_minifier-3.1.1/test/test_raw_tstring_backslash.py +98 -0
  30. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_remove_debug.py +58 -0
  31. python_minifier-3.1.1/test/test_template_strings.py +254 -0
  32. python_minifier-3.1.1/test/test_tuple_with_bug.py +34 -0
  33. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_unicode_cli.py +12 -4
  34. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_utf8_encoding.py +14 -11
  35. {python_minifier-3.0.0 → python_minifier-3.1.1}/LICENSE +0 -0
  36. {python_minifier-3.0.0 → python_minifier-3.1.1}/setup.cfg +0 -0
  37. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/__init__.py +0 -0
  38. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/__init__.pyi +0 -0
  39. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/__main__.py +0 -0
  40. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/ast_annotation/__init__.py +0 -0
  41. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/ast_printer.py +0 -0
  42. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/ministring.py +0 -0
  43. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/py.typed +0 -0
  44. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/rename/__init__.py +0 -0
  45. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/rename/bind_names.py +0 -0
  46. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/rename/binding.py +0 -0
  47. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/rename/mapper.py +0 -0
  48. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/rename/name_generator.py +0 -0
  49. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/rename/resolve_names.py +0 -0
  50. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/rename/util.py +0 -0
  51. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/__init__.py +0 -0
  52. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/combine_imports.py +0 -0
  53. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/constant_folding.py +0 -0
  54. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_annotations.py +0 -0
  55. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
  56. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
  57. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_asserts.py +0 -0
  58. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
  59. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
  60. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
  61. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_object_base.py +0 -0
  62. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_pass.py +0 -0
  63. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/remove_posargs.py +0 -0
  64. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/transforms/suite_transformer.py +0 -0
  65. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier/util.py +0 -0
  66. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier.egg-info/dependency_links.txt +0 -0
  67. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier.egg-info/entry_points.txt +0 -0
  68. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier.egg-info/top_level.txt +0 -0
  69. {python_minifier-3.0.0 → python_minifier-3.1.1}/src/python_minifier.egg-info/zip-safe +0 -0
  70. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_assignment_expressions.py +0 -0
  71. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_await_fstring.py +0 -0
  72. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_bind_names.py +0 -0
  73. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_bind_names_namedexpr.py +0 -0
  74. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_bind_names_python2.py +0 -0
  75. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_bind_names_python312.py +0 -0
  76. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_bind_names_python313.py +0 -0
  77. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_combine_imports.py +0 -0
  78. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_comprehension_rename.py +0 -0
  79. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_decorator_expressions.py +0 -0
  80. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_dict_expansion.py +0 -0
  81. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_folding.py +0 -0
  82. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_generic_types.py +0 -0
  83. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_import.py +0 -0
  84. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_iterable_unpacking.py +0 -0
  85. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_match.py +0 -0
  86. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_match_rename.py +0 -0
  87. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_name_generator.py +0 -0
  88. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_nonlocal.py +0 -0
  89. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_posargs.py +0 -0
  90. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_preserve_shebang.py +0 -0
  91. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_remove_annotations.py +0 -0
  92. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_remove_assert.py +0 -0
  93. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_remove_exception_brackets.py +0 -0
  94. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_remove_explicit_return_none.py +0 -0
  95. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_remove_literal_statements.py +0 -0
  96. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_remove_object.py +0 -0
  97. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_remove_pass.py +0 -0
  98. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_rename_builtins.py +0 -0
  99. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_rename_locals.py +0 -0
  100. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_slice.py +0 -0
  101. {python_minifier-3.0.0 → python_minifier-3.1.1}/test/test_type_param_defaults.py +0 -0
@@ -0,0 +1,21 @@
1
+ include README.md
2
+ include LICENSE
3
+ include setup.py
4
+ graft src/python_minifier
5
+ graft test
6
+ exclude .gitignore
7
+ exclude tox.ini
8
+ exclude tox-windows.ini
9
+ exclude requirements-dev.txt
10
+ exclude CHANGELOG.md
11
+ prune .github
12
+ prune .config
13
+ prune docker
14
+ prune docs
15
+ prune corpus_test
16
+ prune hypo_test
17
+ prune typing_test
18
+ prune xtest
19
+ prune tox
20
+ global-exclude *.pyc
21
+ global-exclude __pycache__
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_minifier
3
- Version: 3.0.0
3
+ Version: 3.1.1
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
@@ -25,13 +25,14 @@ Classifier: Programming Language :: Python :: 3.10
25
25
  Classifier: Programming Language :: Python :: 3.11
26
26
  Classifier: Programming Language :: Python :: 3.12
27
27
  Classifier: Programming Language :: Python :: 3.13
28
+ Classifier: Programming Language :: Python :: 3.14
28
29
  Classifier: Programming Language :: Python :: 2
29
30
  Classifier: Programming Language :: Python :: 2.7
30
31
  Classifier: Programming Language :: Python :: Implementation :: CPython
31
32
  Classifier: Programming Language :: Python :: Implementation :: PyPy
32
33
  Classifier: Intended Audience :: Developers
33
34
  Classifier: Topic :: Software Development
34
- Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14
35
+ Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15
35
36
  Description-Content-Type: text/markdown
36
37
  License-File: LICENSE
37
38
  Dynamic: author
@@ -53,7 +54,7 @@ Transforms Python source code into its most compact representation.
53
54
 
54
55
  [Try it out!](https://python-minifier.com)
55
56
 
56
- python-minifier currently supports Python 2.7 and Python 3.3 to 3.13. Previous releases supported Python 2.6.
57
+ python-minifier currently supports Python 2.7 and Python 3.3 to 3.14. Previous releases supported Python 2.6.
57
58
 
58
59
  * [PyPI](https://pypi.org/project/python-minifier/)
59
60
  * [Documentation](https://dflook.github.io/python-minifier/)
@@ -159,7 +160,7 @@ and outputs source code compatible with the version of the interpreter it is run
159
160
  This means that if you minify code written for Python 3.11 using python-minifier running with Python 3.12,
160
161
  the minified code may only run with Python 3.12.
161
162
 
162
- 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.
163
164
 
164
165
  ## Usage
165
166
 
@@ -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.13. Previous releases supported Python 2.6.
7
+ python-minifier currently supports Python 2.7 and Python 3.3 to 3.14. 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.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
 
@@ -20,15 +20,14 @@ setup(
20
20
  },
21
21
  keywords='minify minifier',
22
22
 
23
-
24
23
  package_dir={'': 'src'},
25
24
  packages=find_packages('src'),
26
25
  package_data={"python_minifier": ["py.typed", "*.pyi", "rename/*.pyi", "transforms/*.pyi"]},
27
26
  long_description=long_desc,
28
27
  long_description_content_type='text/markdown',
29
28
 
30
- python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14',
31
- version='3.0.0',
29
+ python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15',
30
+ version='3.1.1',
32
31
 
33
32
  classifiers=[
34
33
  'Development Status :: 5 - Production/Stable',
@@ -46,6 +45,7 @@ setup(
46
45
  'Programming Language :: Python :: 3.11',
47
46
  'Programming Language :: Python :: 3.12',
48
47
  'Programming Language :: Python :: 3.13',
48
+ 'Programming Language :: Python :: 3.14',
49
49
  'Programming Language :: Python :: 2',
50
50
  'Programming Language :: Python :: 2.7',
51
51
  'Programming Language :: Python :: Implementation :: CPython',
@@ -59,11 +59,14 @@ def compare_ast(l_ast, r_ast):
59
59
  if type(l_ast) != type(r_ast):
60
60
  raise CompareError(l_ast, r_ast, msg='Nodes do not match! %r != %r' % (l_ast, r_ast))
61
61
 
62
- for field in set(l_ast._fields + r_ast._fields):
62
+ for field in sorted(set(l_ast._fields + r_ast._fields)):
63
63
 
64
64
  if field == 'kind' and isinstance(l_ast, ast.Constant):
65
65
  continue
66
66
 
67
+ if field == 'str' and hasattr(ast, 'Interpolation') and isinstance(l_ast, ast.Interpolation):
68
+ continue
69
+
67
70
  if isinstance(getattr(l_ast, field, None), list):
68
71
 
69
72
  l_list = getattr(l_ast, field, None)
@@ -70,9 +70,11 @@ for _node_type in [
70
70
  'TryStar',
71
71
  'TypeVar',
72
72
  'TypeVarTuple',
73
+ 'TemplateStr',
74
+ 'Interpolation',
73
75
  'YieldFrom',
74
76
  'arg',
75
77
  'withitem',
76
78
  ]:
77
79
  if _node_type not in globals():
78
- globals()[_node_type] = type(_node_type, (AST,), {})
80
+ globals()[_node_type] = type(_node_type, (AST,), {})
@@ -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)
@@ -743,6 +743,13 @@ class ExpressionPrinter(object):
743
743
 
744
744
  self.printer.fstring(str(python_minifier.f_string.OuterFString(node, pep701=pep701)))
745
745
 
746
+ def visit_TemplateStr(self, node):
747
+ assert isinstance(node, ast.TemplateStr)
748
+
749
+ import python_minifier.t_string
750
+
751
+ self.printer.tstring(str(python_minifier.t_string.TString(node)))
752
+
746
753
  def visit_NamedExpr(self, node):
747
754
  self._expression(node.target)
748
755
  self.printer.operator(':=')
@@ -8,6 +8,7 @@ Mostly because FStrings feel like a hack.
8
8
 
9
9
  import copy
10
10
  import re
11
+ import sys
11
12
 
12
13
  import python_minifier.ast_compat as ast
13
14
 
@@ -58,11 +59,12 @@ class FString(object):
58
59
 
59
60
  return [x + '}' for x in conversion_candidates]
60
61
 
61
- def candidates(self):
62
- actual_candidates = []
62
+ def _generate_candidates_with_processor(self, prefix, str_processor):
63
+ """Generate f-string candidates using the given prefix and string processor function."""
64
+ candidates = []
63
65
 
64
66
  for quote in self.allowed_quotes:
65
- candidates = ['']
67
+ quote_candidates = ['']
66
68
  debug_specifier_candidates = []
67
69
  nested_allowed = copy.copy(self.allowed_quotes)
68
70
 
@@ -71,26 +73,24 @@ class FString(object):
71
73
 
72
74
  for v in self.node.values:
73
75
  if is_constant_node(v, ast.Str):
74
-
75
76
  # Could this be used as a debug specifier?
76
- if len(candidates) < 10:
77
+ if len(quote_candidates) < 10:
77
78
  debug_specifier = re.match(r'.*=\s*$', v.s)
78
79
  if debug_specifier:
79
- # Maybe!
80
80
  try:
81
- debug_specifier_candidates = [x + '{' + v.s for x in candidates]
81
+ debug_specifier_candidates = [x + '{' + v.s for x in quote_candidates]
82
82
  except Exception:
83
83
  continue
84
84
 
85
85
  try:
86
- candidates = [x + self.str_for(v.s, quote) for x in candidates]
86
+ quote_candidates = [x + str_processor(v.s, quote) for x in quote_candidates]
87
87
  except Exception:
88
88
  continue
89
89
  elif isinstance(v, ast.FormattedValue):
90
90
  try:
91
91
  completed = self.complete_debug_specifier(debug_specifier_candidates, v)
92
- candidates = [
93
- x + y for x in candidates for y in FormattedValue(v, nested_allowed, self.pep701).get_candidates()
92
+ quote_candidates = [
93
+ x + y for x in quote_candidates for y in FormattedValue(v, nested_allowed, self.pep701).get_candidates()
94
94
  ] + completed
95
95
  debug_specifier_candidates = []
96
96
  except Exception:
@@ -98,13 +98,70 @@ class FString(object):
98
98
  else:
99
99
  raise RuntimeError('Unexpected JoinedStr value')
100
100
 
101
- actual_candidates += ['f' + quote + x + quote for x in candidates]
101
+ candidates += [prefix + quote + x + quote for x in quote_candidates]
102
+
103
+ return candidates
104
+
105
+ def candidates(self):
106
+ actual_candidates = []
107
+
108
+ # Normal f-string candidates
109
+ actual_candidates += self._generate_candidates_with_processor('f', self.str_for)
110
+
111
+ # Raw f-string candidates (if we detect backslashes)
112
+ if self._contains_literal_backslashes():
113
+ actual_candidates += self._generate_candidates_with_processor('rf', lambda s, quote: self.raw_str_for(s))
102
114
 
103
115
  return filter(self.is_correct_ast, actual_candidates)
104
116
 
105
- def str_for(self, s, quote):
117
+ def raw_str_for(self, s):
118
+ """
119
+ Generate string representation for raw f-strings.
120
+ Don't escape backslashes like MiniString does.
121
+ """
106
122
  return s.replace('{', '{{').replace('}', '}}')
107
123
 
124
+ def _contains_literal_backslashes(self):
125
+ """
126
+ Check if this f-string contains literal backslashes in constant values.
127
+ This indicates it may need to be a raw f-string.
128
+ """
129
+ for node in ast.walk(self.node):
130
+ if is_constant_node(node, ast.Str):
131
+ if '\\' in node.s:
132
+ return True
133
+ return False
134
+
135
+
136
+ def str_for(self, s, quote):
137
+ # Escape null bytes and other characters that can't appear in Python source
138
+ escaped = ''
139
+ is_multiline = len(quote) == 3 # Triple-quoted strings
140
+
141
+ for c in s:
142
+ if c == '\0':
143
+ escaped += '\\x00'
144
+ elif c == '\n' and not is_multiline:
145
+ # Only escape newlines in single-quoted strings
146
+ escaped += '\\n'
147
+ elif c == '\r':
148
+ # Always escape carriage returns because Python normalizes them during parsing
149
+ # This prevents semantic changes (\\r -> \\n) in multiline strings
150
+ escaped += '\\r'
151
+ elif c == '\t':
152
+ # Always escape tabs for consistency (though not strictly necessary in multiline)
153
+ escaped += '\\t'
154
+ elif c == '{':
155
+ escaped += '{{'
156
+ elif c == '}':
157
+ escaped += '}}'
158
+ elif ord(c) < 32 and c not in '\n\r\t':
159
+ # Escape other control characters
160
+ escaped += '\\x{:02x}'.format(ord(c))
161
+ else:
162
+ escaped += c
163
+ return escaped
164
+
108
165
 
109
166
  class OuterFString(FString):
110
167
  """
@@ -285,7 +342,9 @@ class Str(object):
285
342
  if literal == '':
286
343
  literal += self.current_quote
287
344
 
288
- if c == '\n':
345
+ if c == '\0':
346
+ literal += '\\x00'
347
+ elif c == '\n':
289
348
  literal += '\\n'
290
349
  elif c == '\r':
291
350
  literal += '\\r'
@@ -302,7 +361,7 @@ class Str(object):
302
361
  if self._s == '':
303
362
  return str(min(self.allowed_quotes, key=len)) * 2
304
363
 
305
- if '\0' in self._s or ('\\' in self._s and not self.pep701):
364
+ if '\\' in self._s and not self.pep701:
306
365
  raise ValueError('Impossible to represent a character in f-string expression part')
307
366
 
308
367
  if not self.pep701 and ('\n' in self._s or '\r' in self._s):
@@ -360,7 +419,35 @@ class FormatSpec(object):
360
419
  return candidates
361
420
 
362
421
  def str_for(self, s):
363
- return s.replace('{', '{{').replace('}', '}}')
422
+ # Special handling for problematic format spec characters that can cause parsing issues
423
+ # If the format spec contains only braces, it's likely an invalid test case
424
+
425
+ # Escape null bytes and other unprintable characters
426
+ escaped = ''
427
+ for c in s:
428
+ if c == '\0':
429
+ escaped += '\\x00'
430
+ elif c == '{':
431
+ escaped += '{{'
432
+ elif c == '}':
433
+ escaped += '}}'
434
+ elif c == '\\':
435
+ # For Python 3.12+ raw f-string regression (fixed in 3.14rc2), we need to escape backslashes
436
+ # in format specs so they round-trip correctly
437
+ if (3, 12) <= sys.version_info < (3, 14):
438
+ escaped += '\\\\'
439
+ else:
440
+ escaped += c
441
+ elif c == '\r':
442
+ # Always escape carriage returns because Python normalizes them to newlines during parsing
443
+ # This prevents AST mismatches (\r -> \n normalization)
444
+ escaped += '\\r'
445
+ elif ord(c) < 32 and c not in '\t\n':
446
+ # Escape other control characters except tab, newline
447
+ escaped += '\\x{:02x}'.format(ord(c))
448
+ else:
449
+ escaped += c
450
+ return escaped
364
451
 
365
452
 
366
453
  class Bytes(object):
@@ -411,7 +498,24 @@ class Bytes(object):
411
498
 
412
499
  if literal == '':
413
500
  literal = 'b' + self.current_quote
414
- literal += chr(b)
501
+
502
+ # Handle special characters that need escaping
503
+ if b == 0: # null byte
504
+ literal += '\\x00'
505
+ elif b == ord('\\'): # backslash
506
+ literal += '\\\\'
507
+ elif b == ord('\n'): # newline
508
+ literal += '\\n'
509
+ elif b == ord('\r'): # carriage return
510
+ literal += '\\r'
511
+ elif b == ord('\t'): # tab
512
+ literal += '\\t'
513
+ elif len(self.current_quote) == 1 and b == ord(self.current_quote): # single quote character
514
+ literal += '\\' + self.current_quote
515
+ elif 32 <= b <= 126: # printable ASCII
516
+ literal += chr(b)
517
+ else: # other non-printable characters
518
+ literal += '\\x{:02x}'.format(b)
415
519
 
416
520
  if literal:
417
521
  literal += self.current_quote
@@ -421,8 +525,6 @@ class Bytes(object):
421
525
  if self._b == b'':
422
526
  return 'b' + str(min(self.allowed_quotes, key=len)) * 2
423
527
 
424
- if b'\0' in self._b or b'\\' in self._b:
425
- raise ValueError('Impossible to represent a %r character in f-string expression part')
426
528
 
427
529
  if b'\n' in self._b or b'\r' in self._b:
428
530
  if '"""' not in self.allowed_quotes and "'''" not in self.allowed_quotes:
@@ -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):
@@ -0,0 +1,93 @@
1
+ # Renaming
2
+
3
+ We can save bytes by shortening the names used in a python program.
4
+
5
+ One simple way to do this is to replace each unique name in a module with a shorter one.
6
+ This will probably exhaust the available single character names, so is not as efficient as it could be.
7
+ Also, not all names can be safely changed this way.
8
+
9
+ By determining the scope of each name, we can assign the same short name to multiple non-overlapping scopes.
10
+ This means sibling namespaces may have the same names, and names will be shadowed in inner namespaces where possible.
11
+
12
+ This file is a guide for how the python_minifier package shortens names.
13
+ There are multiple steps to the renaming process.
14
+
15
+ ## Binding Names
16
+
17
+ Names are bound to the local namespace it is defined in.
18
+
19
+ ### Create namespace nodes
20
+
21
+ Namespaces in python are introduced by Modules, Functions, Comprehensions, Generators and Classes.
22
+ The AST node that introduces a new namespace is called a 'namespace node'.
23
+
24
+ These attributes are added to namespace nodes:
25
+
26
+ - Bindings - A list of Bindings local to this namespace, populated by the Bind names step
27
+ - Globals - A list of global names in this namespace
28
+ - Nonlocals - A list of nonlocal names in this namespace
29
+
30
+ ### Determine parent node
31
+
32
+ Add a parent attribute to each node with the value of the node of which this is a child.
33
+
34
+ ### Determine namespace
35
+
36
+ Add a namespace attribute to each node with the value of the namespace node that will be used for name binding and resolution.
37
+ This is usually the closest parent namespace node. The exceptions are:
38
+
39
+ - Function argument default values are in the same namespace as their function.
40
+ - Function decorators are in the same namespace as their function.
41
+ - Function annotations are in the same namespace as their function.
42
+ - Class decorator are in the same namespace as their class.
43
+ - Class bases, keywords, starargs and kwargs are in the same namespace as their class.
44
+ - The first iteration expression of a comprehension is in the same namespace as it's parent ListComp/SetComp/DictComp or GeneratorExp.
45
+
46
+ ### Bind names
47
+
48
+ Every node that binds a name creates a NameBinding for that name in its namespace.
49
+ The node is added to the NameBinding as a reference.
50
+
51
+ If the name is nonlocal in its namespace it does not create a binding.
52
+
53
+ Nodes that create a binding:
54
+
55
+ - FunctionDef nodes bind their name
56
+ - ClassDef nodes bind their name
57
+ - arg nodes bind their arg
58
+ - Name nodes in Store or Del context bind their id
59
+ - MatchAs nodes bind their name
60
+ - MatchStar nodes bind their name
61
+ - MatchMapping nodes bind their rest
62
+
63
+ ### Resolve names
64
+
65
+ For the remaining unbound name nodes and nodes that normally create a binding but are for a nonlocal name, we find their binding.
66
+
67
+ Bindings for name references are found by searching their namespace, then parent namespaces.
68
+ If a name is global in a searched namespace, skip straight to the module node.
69
+ If a name is nonlocal in a searched namespace, skip to the next parent namespace.
70
+ When traversing parent namespaces, Class namespaces are skipped.
71
+
72
+ If a NameBinding is found, add the node as a reference.
73
+ If no NameBinding is found, check if the name would resolve to a builtin.
74
+ If so, create a BuiltinBinding in the module namespace and add this node as a reference.
75
+
76
+ Otherwise we failed to find a binding for this name - Create a NameBinding in the module namespace and add this node
77
+ as a reference.
78
+
79
+ ## Hoist Literals
80
+
81
+ At this point we do the HoistLiterals transform, which adds new HoistedLiteral bindings to the namespaces where it wants
82
+ to introduce new names.
83
+
84
+ ## Name Assignment
85
+
86
+ Collect all bindings in the module and sort by estimated byte savings
87
+
88
+ For each binding:
89
+
90
+ - Determine it's 'reservation scope', which is the set of namespaces that name is referenced in (and all namespaces between them)
91
+ - Get the next available name that is unassigned and unreserved in all namespaces in the reservation scope.
92
+ - Check if we should proceed with the rename - is it space efficient to do this rename, or has the original name been assigned somewhere else?
93
+ - Rename the binding, rename all referenced nodes to the new name, and record this name as assigned in every namespace of the reservation scope.
@@ -220,6 +220,13 @@ class HoistLiterals(NodeVisitor):
220
220
  continue
221
221
  self.visit(v)
222
222
 
223
+ def visit_TemplateStr(self, node):
224
+ for v in node.values:
225
+ if is_constant_node(v, ast.Str):
226
+ # Can't hoist string literals that are part of the template
227
+ continue
228
+ self.visit(v)
229
+
223
230
  def visit_NameConstant(self, node):
224
231
  self.get_binding(node.value, node).add_reference(node)
225
232
 
@@ -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)