python-minifier 2.11.3__tar.gz → 3.1.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 (102) hide show
  1. python_minifier-3.1.0/MANIFEST.in +21 -0
  2. {python_minifier-2.11.3/src/python_minifier.egg-info → python_minifier-3.1.0}/PKG-INFO +18 -5
  3. {python_minifier-2.11.3 → python_minifier-3.1.0}/README.md +2 -2
  4. {python_minifier-2.11.3 → python_minifier-3.1.0}/setup.py +3 -3
  5. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/__init__.py +2 -1
  6. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/__init__.pyi +3 -3
  7. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/__main__.py +67 -8
  8. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ast_compare.py +15 -12
  9. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ast_compat.py +2 -0
  10. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/expression_printer.py +7 -0
  11. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/f_string.py +151 -49
  12. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/module_printer.py +6 -2
  13. python_minifier-3.1.0/src/python_minifier/rename/README.md +93 -0
  14. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/rename_literals.py +7 -0
  15. python_minifier-3.1.0/src/python_minifier/t_string.py +330 -0
  16. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/token_printer.py +19 -1
  17. {python_minifier-2.11.3 → python_minifier-3.1.0/src/python_minifier.egg-info}/PKG-INFO +18 -5
  18. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/SOURCES.txt +16 -1
  19. python_minifier-3.1.0/test/ast_annotation/test_add_parent.py +49 -0
  20. python_minifier-3.1.0/test/conftest.py +6 -0
  21. python_minifier-3.1.0/test/helpers.py +52 -0
  22. python_minifier-3.1.0/test/requirements.txt +1 -0
  23. python_minifier-3.1.0/test/subprocess_compat.py +38 -0
  24. python_minifier-3.1.0/test/test_best_effort_cli.py +179 -0
  25. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_hoist_literals.py +2 -2
  26. python_minifier-3.1.0/test/test_is_constant_node.py +93 -0
  27. python_minifier-3.1.0/test/test_raw_fstring_backslash.py +80 -0
  28. python_minifier-3.1.0/test/test_raw_tstring_backslash.py +98 -0
  29. python_minifier-3.1.0/test/test_template_strings.py +254 -0
  30. python_minifier-3.1.0/test/test_tuple_with_bug.py +34 -0
  31. python_minifier-3.1.0/test/test_unicode_cli.py +178 -0
  32. python_minifier-3.1.0/test/test_utf8_encoding.py +117 -0
  33. python_minifier-2.11.3/test/test_is_constant_node.py +0 -43
  34. {python_minifier-2.11.3 → python_minifier-3.1.0}/LICENSE +0 -0
  35. {python_minifier-2.11.3 → python_minifier-3.1.0}/setup.cfg +0 -0
  36. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
  37. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ast_printer.py +0 -0
  38. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/ministring.py +0 -0
  39. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/py.typed +0 -0
  40. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/__init__.py +0 -0
  41. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/bind_names.py +0 -0
  42. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/binding.py +0 -0
  43. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/mapper.py +0 -0
  44. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/name_generator.py +0 -0
  45. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/renamer.py +0 -0
  46. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/resolve_names.py +0 -0
  47. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/rename/util.py +0 -0
  48. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/__init__.py +0 -0
  49. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/combine_imports.py +0 -0
  50. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/constant_folding.py +0 -0
  51. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
  52. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
  53. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
  54. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
  55. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_debug.py +0 -0
  56. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
  57. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
  58. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
  59. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
  60. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_pass.py +0 -0
  61. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
  62. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/transforms/suite_transformer.py +0 -0
  63. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier/util.py +0 -0
  64. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
  65. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
  66. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/top_level.txt +0 -0
  67. {python_minifier-2.11.3 → python_minifier-3.1.0}/src/python_minifier.egg-info/zip-safe +0 -0
  68. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_assignment_expressions.py +0 -0
  69. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_await_fstring.py +0 -0
  70. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names.py +0 -0
  71. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_namedexpr.py +0 -0
  72. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_python2.py +0 -0
  73. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_python312.py +0 -0
  74. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_python313.py +0 -0
  75. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_bind_names_python33.py +0 -0
  76. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_combine_imports.py +0 -0
  77. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_comprehension_rename.py +0 -0
  78. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_decorator_expressions.py +0 -0
  79. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_dict_expansion.py +0 -0
  80. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_folding.py +0 -0
  81. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_fstring.py +0 -0
  82. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_generic_types.py +0 -0
  83. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_import.py +0 -0
  84. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_iterable_unpacking.py +0 -0
  85. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_match.py +0 -0
  86. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_match_rename.py +0 -0
  87. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_name_generator.py +0 -0
  88. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_nonlocal.py +0 -0
  89. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_posargs.py +0 -0
  90. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_preserve_shebang.py +0 -0
  91. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_annotations.py +0 -0
  92. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_assert.py +0 -0
  93. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_debug.py +0 -0
  94. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_exception_brackets.py +0 -0
  95. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_explicit_return_none.py +0 -0
  96. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_literal_statements.py +0 -0
  97. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_object.py +0 -0
  98. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_remove_pass.py +0 -0
  99. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_rename_builtins.py +0 -0
  100. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_rename_locals.py +0 -0
  101. {python_minifier-2.11.3 → python_minifier-3.1.0}/test/test_slice.py +0 -0
  102. {python_minifier-2.11.3 → python_minifier-3.1.0}/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
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.4
2
2
  Name: python_minifier
3
- Version: 2.11.3
3
+ Version: 3.1.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
@@ -25,15 +25,28 @@ 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
38
+ Dynamic: author
39
+ Dynamic: author-email
40
+ Dynamic: classifier
41
+ Dynamic: description
42
+ Dynamic: description-content-type
43
+ Dynamic: home-page
44
+ Dynamic: keywords
45
+ Dynamic: license
46
+ Dynamic: license-file
47
+ Dynamic: project-url
48
+ Dynamic: requires-python
49
+ Dynamic: summary
37
50
 
38
51
  # Python Minifier
39
52
 
@@ -41,7 +54,7 @@ Transforms Python source code into its most compact representation.
41
54
 
42
55
  [Try it out!](https://python-minifier.com)
43
56
 
44
- 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.
45
58
 
46
59
  * [PyPI](https://pypi.org/project/python-minifier/)
47
60
  * [Documentation](https://dflook.github.io/python-minifier/)
@@ -130,7 +143,7 @@ def handler(event,context):
130
143
 
131
144
  ## Why?
132
145
 
133
- AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
146
+ AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
134
147
  than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
135
148
 
136
149
  ## Installation
@@ -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/)
@@ -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
@@ -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='2.11.3',
29
+ python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15',
30
+ version='3.1.0',
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',
@@ -86,7 +86,8 @@ def minify(
86
86
  :param str source: The python module source code
87
87
  :param str filename: The original source filename if known
88
88
 
89
- :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.
90
91
  :type remove_annotations: bool or RemoveAnnotationsOptions
91
92
  :param bool remove_pass: If Pass statements should be removed where possible
92
93
  :param bool remove_literal_statements: If statements consisting of a single literal should be removed, including docstrings
@@ -1,6 +1,6 @@
1
1
  import ast
2
2
 
3
- from typing import Any, AnyStr, List, Optional, Text, Union
3
+ from typing import Any, List, Optional, Text, Union
4
4
 
5
5
  from .transforms.remove_annotations_options import RemoveAnnotationsOptions as RemoveAnnotationsOptions
6
6
 
@@ -10,7 +10,7 @@ class UnstableMinification(RuntimeError):
10
10
 
11
11
 
12
12
  def minify(
13
- source: AnyStr,
13
+ source: Union[str, bytes],
14
14
  filename: Optional[str] = ...,
15
15
  remove_annotations: Union[bool, RemoveAnnotationsOptions] = ...,
16
16
  remove_pass: bool = ...,
@@ -36,7 +36,7 @@ def unparse(module: ast.Module) -> Text: ...
36
36
 
37
37
 
38
38
  def awslambda(
39
- source: AnyStr,
39
+ source: Union[str, bytes],
40
40
  filename: Optional[Text] = ...,
41
41
  entrypoint: Optional[Text] = ...
42
42
  ) -> Text: ...
@@ -8,6 +8,18 @@ from python_minifier import minify
8
8
  from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
9
9
 
10
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
+
11
23
  if sys.version_info >= (3, 8):
12
24
  from importlib import metadata
13
25
 
@@ -51,12 +63,23 @@ examples:
51
63
  if len(args.path) == 1 and args.path[0] == '-':
52
64
  # minify stdin
53
65
  source = sys.stdin.buffer.read() if sys.version_info >= (3, 0) else sys.stdin.read()
54
- 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
+
55
78
  if args.output:
56
- with open(args.output, 'w') as f:
79
+ with open(args.output, 'wb') as f:
57
80
  f.write(minified)
58
81
  else:
59
- sys.stdout.write(minified)
82
+ stdout_write_bytes(minified)
60
83
 
61
84
  else:
62
85
  # minify source paths
@@ -67,16 +90,30 @@ examples:
67
90
  with open(path, 'rb') as f:
68
91
  source = f.read()
69
92
 
70
- 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
71
108
 
72
109
  if args.in_place:
73
- with open(path, 'w') as f:
110
+ with open(path, 'wb') as f:
74
111
  f.write(minified)
75
112
  elif args.output:
76
- with open(args.output, 'w') as f:
113
+ with open(args.output, 'wb') as f:
77
114
  f.write(minified)
78
115
  else:
79
- sys.stdout.write(minified)
116
+ stdout_write_bytes(minified)
80
117
 
81
118
 
82
119
  def parse_args():
@@ -280,6 +317,15 @@ def source_modules(args):
280
317
 
281
318
 
282
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
+ """
283
329
 
284
330
  preserve_globals = []
285
331
  if minification_args.preserve_globals:
@@ -308,7 +354,7 @@ def do_minify(source, filename, minification_args):
308
354
  remove_class_attribute_annotations=minification_args.remove_class_attribute_annotations,
309
355
  )
310
356
 
311
- return minify(
357
+ minified_result = minify(
312
358
  source,
313
359
  filename=filename,
314
360
  combine_imports=minification_args.combine_imports,
@@ -330,6 +376,19 @@ def do_minify(source, filename, minification_args):
330
376
  constant_folding=minification_args.constant_folding
331
377
  )
332
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
+
333
392
 
334
393
  if __name__ == '__main__':
335
394
  main()
@@ -59,10 +59,13 @@ 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
+
67
+ if field == 'str' and hasattr(ast, 'Interpolation') and isinstance(l_ast, ast.Interpolation):
68
+ continue
66
69
 
67
70
  if isinstance(getattr(l_ast, field, None), list):
68
71
 
@@ -77,26 +80,26 @@ def compare_ast(l_ast, r_ast):
77
80
  % (type(l_ast), field, len(l_list), type(r_ast), field, len(r_list)),
78
81
  )
79
82
 
80
- for i, l, r in zip(counter(), l_list, r_list):
81
- if isinstance(l, ast.AST) or isinstance(r, ast.AST):
82
- compare_ast(l, r)
83
- elif l != r:
83
+ for i, left, right in zip(counter(), l_list, r_list):
84
+ if isinstance(left, ast.AST) or isinstance(right, ast.AST):
85
+ compare_ast(left, right)
86
+ elif left != right:
84
87
  raise CompareError(
85
88
  l_ast,
86
89
  r_ast,
87
90
  'Fields do not match! %s.%s[%i]=%r, %s.%s[%i]=%r'
88
- % (type(l_ast), field, i, l, type(r_ast), field, i, r),
91
+ % (type(l_ast), field, i, left, type(r_ast), field, i, right),
89
92
  )
90
93
 
91
94
  else:
92
- l = getattr(l_ast, field, None)
93
- r = getattr(r_ast, field, None)
95
+ left_field = getattr(l_ast, field, None)
96
+ right_field = getattr(r_ast, field, None)
94
97
 
95
- if isinstance(l, ast.AST) or isinstance(r, ast.AST):
96
- compare_ast(l, r)
97
- elif l != r:
98
+ if isinstance(left_field, ast.AST) or isinstance(right_field, ast.AST):
99
+ compare_ast(left_field, right_field)
100
+ elif left_field != right_field:
98
101
  raise CompareError(
99
102
  l_ast,
100
103
  r_ast,
101
- 'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field, l, type(r_ast), field, r),
104
+ 'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field, left_field, type(r_ast), field, right_field),
102
105
  )
@@ -70,6 +70,8 @@ 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',
@@ -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
  """
@@ -272,37 +329,39 @@ class Str(object):
272
329
  raise ValueError("Couldn't find a quote")
273
330
 
274
331
  def _literals(self):
275
- l = ''
332
+ literal = ''
276
333
  for c in self._s:
277
334
  if not self._can_quote(c):
278
- if l:
279
- l += self.current_quote
280
- yield l
281
- l = ''
335
+ if literal:
336
+ literal += self.current_quote
337
+ yield literal
338
+ literal = ''
282
339
 
283
340
  self.current_quote = self._get_quote(c)
284
341
 
285
- if l == '':
286
- l += self.current_quote
342
+ if literal == '':
343
+ literal += self.current_quote
287
344
 
288
- if c == '\n':
289
- l += '\\n'
345
+ if c == '\0':
346
+ literal += '\\x00'
347
+ elif c == '\n':
348
+ literal += '\\n'
290
349
  elif c == '\r':
291
- l += '\\r'
350
+ literal += '\\r'
292
351
  elif c == '\\':
293
- l += '\\\\'
352
+ literal += '\\\\'
294
353
  else:
295
- l += c
354
+ literal += c
296
355
 
297
- if l:
298
- l += self.current_quote
299
- yield l
356
+ if literal:
357
+ literal += self.current_quote
358
+ yield literal
300
359
 
301
360
  def __str__(self):
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):
@@ -315,10 +374,10 @@ class Str(object):
315
374
  for start_quote in self.allowed_quotes:
316
375
  self.current_quote = start_quote
317
376
  s = ''
318
- for l in self._literals():
319
- if s and s[-1] == l[0]:
377
+ for literal in self._literals():
378
+ if s and s[-1] == literal[0]:
320
379
  s += ' '
321
- s += l
380
+ s += literal
322
381
 
323
382
  if eval(s) == self._s:
324
383
  candidates.append(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):
@@ -399,30 +486,45 @@ class Bytes(object):
399
486
  raise ValueError("Couldn't find a quote")
400
487
 
401
488
  def _literals(self):
402
- l = ''
489
+ literal = ''
403
490
  for b in self._b:
404
491
  if not self._can_quote(b):
405
- if l:
406
- l += self.current_quote
407
- yield l
408
- l = ''
492
+ if literal:
493
+ literal += self.current_quote
494
+ yield literal
495
+ literal = ''
409
496
 
410
497
  self.current_quote = self._get_quote(b)
411
498
 
412
- if l == '':
413
- l = 'b' + self.current_quote
414
- l += chr(b)
415
-
416
- if l:
417
- l += self.current_quote
418
- yield l
499
+ if literal == '':
500
+ literal = 'b' + self.current_quote
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)
519
+
520
+ if literal:
521
+ literal += self.current_quote
522
+ yield literal
419
523
 
420
524
  def __str__(self):
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:
@@ -434,10 +536,10 @@ class Bytes(object):
434
536
  for start_quote in self.allowed_quotes:
435
537
  self.current_quote = start_quote
436
538
  s = ''
437
- for l in self._literals():
438
- if s and s[-1] == l[0]:
539
+ for literal in self._literals():
540
+ if s and s[-1] == literal[0]:
439
541
  s += ' '
440
- s += l
542
+ s += literal
441
543
 
442
544
  assert eval(s) == self._b
443
545
  candidates.append(s)