python-minifier 2.11.3__tar.gz → 3.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. {python_minifier-2.11.3/src/python_minifier.egg-info → python_minifier-3.0.0}/PKG-INFO +15 -3
  2. {python_minifier-2.11.3 → python_minifier-3.0.0}/README.md +1 -1
  3. {python_minifier-2.11.3 → python_minifier-3.0.0}/setup.py +1 -1
  4. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/__init__.py +2 -1
  5. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/__init__.pyi +3 -3
  6. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/__main__.py +67 -8
  7. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ast_compare.py +11 -11
  8. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/f_string.py +31 -31
  9. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/module_printer.py +6 -2
  10. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/token_printer.py +9 -1
  11. {python_minifier-2.11.3 → python_minifier-3.0.0/src/python_minifier.egg-info}/PKG-INFO +15 -3
  12. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/SOURCES.txt +4 -1
  13. python_minifier-3.0.0/test/test_best_effort_cli.py +179 -0
  14. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_is_constant_node.py +10 -0
  15. python_minifier-3.0.0/test/test_unicode_cli.py +170 -0
  16. python_minifier-3.0.0/test/test_utf8_encoding.py +114 -0
  17. {python_minifier-2.11.3 → python_minifier-3.0.0}/LICENSE +0 -0
  18. {python_minifier-2.11.3 → python_minifier-3.0.0}/setup.cfg +0 -0
  19. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ast_annotation/__init__.py +0 -0
  20. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ast_compat.py +0 -0
  21. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ast_printer.py +0 -0
  22. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/expression_printer.py +0 -0
  23. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/ministring.py +0 -0
  24. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/py.typed +0 -0
  25. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/__init__.py +0 -0
  26. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/bind_names.py +0 -0
  27. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/binding.py +0 -0
  28. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/mapper.py +0 -0
  29. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/name_generator.py +0 -0
  30. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/rename_literals.py +0 -0
  31. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/renamer.py +0 -0
  32. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/resolve_names.py +0 -0
  33. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/rename/util.py +0 -0
  34. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/__init__.py +0 -0
  35. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/combine_imports.py +0 -0
  36. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/constant_folding.py +0 -0
  37. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_annotations.py +0 -0
  38. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_annotations_options.py +0 -0
  39. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_annotations_options.pyi +0 -0
  40. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_asserts.py +0 -0
  41. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_debug.py +0 -0
  42. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_exception_brackets.py +0 -0
  43. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_explicit_return_none.py +0 -0
  44. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_literal_statements.py +0 -0
  45. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_object_base.py +0 -0
  46. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_pass.py +0 -0
  47. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/remove_posargs.py +0 -0
  48. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/transforms/suite_transformer.py +0 -0
  49. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier/util.py +0 -0
  50. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/dependency_links.txt +0 -0
  51. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/entry_points.txt +0 -0
  52. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/top_level.txt +0 -0
  53. {python_minifier-2.11.3 → python_minifier-3.0.0}/src/python_minifier.egg-info/zip-safe +0 -0
  54. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_assignment_expressions.py +0 -0
  55. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_await_fstring.py +0 -0
  56. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names.py +0 -0
  57. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_namedexpr.py +0 -0
  58. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_python2.py +0 -0
  59. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_python312.py +0 -0
  60. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_python313.py +0 -0
  61. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_bind_names_python33.py +0 -0
  62. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_combine_imports.py +0 -0
  63. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_comprehension_rename.py +0 -0
  64. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_decorator_expressions.py +0 -0
  65. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_dict_expansion.py +0 -0
  66. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_folding.py +0 -0
  67. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_fstring.py +0 -0
  68. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_generic_types.py +0 -0
  69. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_hoist_literals.py +0 -0
  70. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_import.py +0 -0
  71. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_iterable_unpacking.py +0 -0
  72. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_match.py +0 -0
  73. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_match_rename.py +0 -0
  74. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_name_generator.py +0 -0
  75. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_nonlocal.py +0 -0
  76. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_posargs.py +0 -0
  77. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_preserve_shebang.py +0 -0
  78. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_annotations.py +0 -0
  79. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_assert.py +0 -0
  80. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_debug.py +0 -0
  81. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_exception_brackets.py +0 -0
  82. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_explicit_return_none.py +0 -0
  83. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_literal_statements.py +0 -0
  84. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_object.py +0 -0
  85. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_remove_pass.py +0 -0
  86. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_rename_builtins.py +0 -0
  87. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_rename_locals.py +0 -0
  88. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_slice.py +0 -0
  89. {python_minifier-2.11.3 → python_minifier-3.0.0}/test/test_type_param_defaults.py +0 -0
@@ -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.0.0
4
4
  Summary: Transform Python source code into it's most compact representation
5
5
  Home-page: https://github.com/dflook/python-minifier
6
6
  Author: Daniel Flook
@@ -34,6 +34,18 @@ Classifier: Topic :: Software Development
34
34
  Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14
35
35
  Description-Content-Type: text/markdown
36
36
  License-File: LICENSE
37
+ Dynamic: author
38
+ Dynamic: author-email
39
+ Dynamic: classifier
40
+ Dynamic: description
41
+ Dynamic: description-content-type
42
+ Dynamic: home-page
43
+ Dynamic: keywords
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: project-url
47
+ Dynamic: requires-python
48
+ Dynamic: summary
37
49
 
38
50
  # Python Minifier
39
51
 
@@ -130,7 +142,7 @@ def handler(event,context):
130
142
 
131
143
  ## Why?
132
144
 
133
- AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
145
+ AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
134
146
  than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
135
147
 
136
148
  ## Installation
@@ -93,7 +93,7 @@ def handler(event,context):
93
93
 
94
94
  ## Why?
95
95
 
96
- AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
96
+ AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
97
97
  than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
98
98
 
99
99
  ## Installation
@@ -28,7 +28,7 @@ setup(
28
28
  long_description_content_type='text/markdown',
29
29
 
30
30
  python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14',
31
- version='2.11.3',
31
+ version='3.0.0',
32
32
 
33
33
  classifiers=[
34
34
  'Development Status :: 5 - Production/Stable',
@@ -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()
@@ -77,26 +77,26 @@ def compare_ast(l_ast, r_ast):
77
77
  % (type(l_ast), field, len(l_list), type(r_ast), field, len(r_list)),
78
78
  )
79
79
 
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:
80
+ for i, left, right in zip(counter(), l_list, r_list):
81
+ if isinstance(left, ast.AST) or isinstance(right, ast.AST):
82
+ compare_ast(left, right)
83
+ elif left != right:
84
84
  raise CompareError(
85
85
  l_ast,
86
86
  r_ast,
87
87
  '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),
88
+ % (type(l_ast), field, i, left, type(r_ast), field, i, right),
89
89
  )
90
90
 
91
91
  else:
92
- l = getattr(l_ast, field, None)
93
- r = getattr(r_ast, field, None)
92
+ left_field = getattr(l_ast, field, None)
93
+ right_field = getattr(r_ast, field, None)
94
94
 
95
- if isinstance(l, ast.AST) or isinstance(r, ast.AST):
96
- compare_ast(l, r)
97
- elif l != r:
95
+ if isinstance(left_field, ast.AST) or isinstance(right_field, ast.AST):
96
+ compare_ast(left_field, right_field)
97
+ elif left_field != right_field:
98
98
  raise CompareError(
99
99
  l_ast,
100
100
  r_ast,
101
- 'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field, l, type(r_ast), field, r),
101
+ 'Fields do not match! %s.%s=%r, %s.%s=%r' % (type(l_ast), field, left_field, type(r_ast), field, right_field),
102
102
  )
@@ -272,31 +272,31 @@ class Str(object):
272
272
  raise ValueError("Couldn't find a quote")
273
273
 
274
274
  def _literals(self):
275
- l = ''
275
+ literal = ''
276
276
  for c in self._s:
277
277
  if not self._can_quote(c):
278
- if l:
279
- l += self.current_quote
280
- yield l
281
- l = ''
278
+ if literal:
279
+ literal += self.current_quote
280
+ yield literal
281
+ literal = ''
282
282
 
283
283
  self.current_quote = self._get_quote(c)
284
284
 
285
- if l == '':
286
- l += self.current_quote
285
+ if literal == '':
286
+ literal += self.current_quote
287
287
 
288
288
  if c == '\n':
289
- l += '\\n'
289
+ literal += '\\n'
290
290
  elif c == '\r':
291
- l += '\\r'
291
+ literal += '\\r'
292
292
  elif c == '\\':
293
- l += '\\\\'
293
+ literal += '\\\\'
294
294
  else:
295
- l += c
295
+ literal += c
296
296
 
297
- if l:
298
- l += self.current_quote
299
- yield l
297
+ if literal:
298
+ literal += self.current_quote
299
+ yield literal
300
300
 
301
301
  def __str__(self):
302
302
  if self._s == '':
@@ -315,10 +315,10 @@ class Str(object):
315
315
  for start_quote in self.allowed_quotes:
316
316
  self.current_quote = start_quote
317
317
  s = ''
318
- for l in self._literals():
319
- if s and s[-1] == l[0]:
318
+ for literal in self._literals():
319
+ if s and s[-1] == literal[0]:
320
320
  s += ' '
321
- s += l
321
+ s += literal
322
322
 
323
323
  if eval(s) == self._s:
324
324
  candidates.append(s)
@@ -399,23 +399,23 @@ class Bytes(object):
399
399
  raise ValueError("Couldn't find a quote")
400
400
 
401
401
  def _literals(self):
402
- l = ''
402
+ literal = ''
403
403
  for b in self._b:
404
404
  if not self._can_quote(b):
405
- if l:
406
- l += self.current_quote
407
- yield l
408
- l = ''
405
+ if literal:
406
+ literal += self.current_quote
407
+ yield literal
408
+ literal = ''
409
409
 
410
410
  self.current_quote = self._get_quote(b)
411
411
 
412
- if l == '':
413
- l = 'b' + self.current_quote
414
- l += chr(b)
412
+ if literal == '':
413
+ literal = 'b' + self.current_quote
414
+ literal += chr(b)
415
415
 
416
- if l:
417
- l += self.current_quote
418
- yield l
416
+ if literal:
417
+ literal += self.current_quote
418
+ yield literal
419
419
 
420
420
  def __str__(self):
421
421
  if self._b == b'':
@@ -434,10 +434,10 @@ class Bytes(object):
434
434
  for start_quote in self.allowed_quotes:
435
435
  self.current_quote = start_quote
436
436
  s = ''
437
- for l in self._literals():
438
- if s and s[-1] == l[0]:
437
+ for literal in self._literals():
438
+ if s and s[-1] == literal[0]:
439
439
  s += ' '
440
- s += l
440
+ s += literal
441
441
 
442
442
  assert eval(s) == self._b
443
443
  candidates.append(s)
@@ -28,11 +28,15 @@ class ModulePrinter(ExpressionPrinter):
28
28
  assert isinstance(module, ast.Module)
29
29
 
30
30
  self.visit_Module(module)
31
- return str(self.printer).rstrip('\n' + self.indent_char + ';')
31
+ # On Python 2.7, preserve unicode strings to avoid encoding issues
32
+ code = unicode(self.printer) if sys.version_info[0] < 3 else str(self.printer)
33
+ return code.rstrip('\n' + self.indent_char + ';')
32
34
 
33
35
  @property
34
36
  def code(self):
35
- return str(self.printer).rstrip('\n' + self.indent_char + ';')
37
+ # On Python 2.7, preserve unicode strings to avoid encoding issues
38
+ code = unicode(self.printer) if sys.version_info[0] < 3 else str(self.printer)
39
+ return code.rstrip('\n' + self.indent_char + ';')
36
40
 
37
41
  # region Simple Statements
38
42
 
@@ -91,7 +91,11 @@ class TokenPrinter(object):
91
91
  self._prefer_single_line = prefer_single_line
92
92
  self._allow_invalid_num_warnings = allow_invalid_num_warnings
93
93
 
94
- self._code = ''
94
+ # Initialize as unicode string on Python 2.7 to handle Unicode content
95
+ if sys.version_info[0] < 3:
96
+ self._code = u''
97
+ else:
98
+ self._code = ''
95
99
  self.indent = 0
96
100
  self.unicode_literals = False
97
101
  self.previous_token = TokenTypes.NoToken
@@ -99,6 +103,10 @@ class TokenPrinter(object):
99
103
  def __str__(self):
100
104
  """Return the output code."""
101
105
  return self._code
106
+
107
+ def __unicode__(self):
108
+ """Return the output code as unicode (for Python 2.7 compatibility)."""
109
+ return self._code
102
110
 
103
111
  def identifier(self, name):
104
112
  """Add an identifier to the output code."""
@@ -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.0.0
4
4
  Summary: Transform Python source code into it's most compact representation
5
5
  Home-page: https://github.com/dflook/python-minifier
6
6
  Author: Daniel Flook
@@ -34,6 +34,18 @@ Classifier: Topic :: Software Development
34
34
  Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.14
35
35
  Description-Content-Type: text/markdown
36
36
  License-File: LICENSE
37
+ Dynamic: author
38
+ Dynamic: author-email
39
+ Dynamic: classifier
40
+ Dynamic: description
41
+ Dynamic: description-content-type
42
+ Dynamic: home-page
43
+ Dynamic: keywords
44
+ Dynamic: license
45
+ Dynamic: license-file
46
+ Dynamic: project-url
47
+ Dynamic: requires-python
48
+ Dynamic: summary
37
49
 
38
50
  # Python Minifier
39
51
 
@@ -130,7 +142,7 @@ def handler(event,context):
130
142
 
131
143
  ## Why?
132
144
 
133
- AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
145
+ AWS Cloudformation templates may have AWS lambda function source code embedded in them, but only if the function is less
134
146
  than 4KiB. I wrote this package so I could write python normally and still embed the module in a template.
135
147
 
136
148
  ## Installation
@@ -47,6 +47,7 @@ src/python_minifier/transforms/remove_posargs.py
47
47
  src/python_minifier/transforms/suite_transformer.py
48
48
  test/test_assignment_expressions.py
49
49
  test/test_await_fstring.py
50
+ test/test_best_effort_cli.py
50
51
  test/test_bind_names.py
51
52
  test/test_bind_names_namedexpr.py
52
53
  test/test_bind_names_python2.py
@@ -81,4 +82,6 @@ test/test_remove_pass.py
81
82
  test/test_rename_builtins.py
82
83
  test/test_rename_locals.py
83
84
  test/test_slice.py
84
- test/test_type_param_defaults.py
85
+ test/test_type_param_defaults.py
86
+ test/test_unicode_cli.py
87
+ test/test_utf8_encoding.py
@@ -0,0 +1,179 @@
1
+ """Tests for CLI size-based output selection (best effort) functionality."""
2
+ import os
3
+ import sys
4
+ import tempfile
5
+
6
+ from subprocess_compat import run_subprocess, safe_decode
7
+
8
+
9
+ def test_returns_minified_when_smaller():
10
+ """Test CLI returns minified output when it's smaller than original."""
11
+ code = '''
12
+ def hello_world():
13
+ """A simple function."""
14
+ print("Hello, world!")
15
+ return None
16
+
17
+ if __name__ == "__main__":
18
+ hello_world()
19
+ '''
20
+
21
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
22
+ f.write(code)
23
+ temp_file = f.name
24
+
25
+ try:
26
+ env = os.environ.copy()
27
+ env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
28
+
29
+ result = run_subprocess([
30
+ sys.executable, '-m', 'python_minifier', temp_file
31
+ ], timeout=30, env=env)
32
+
33
+ assert result.returncode == 0
34
+
35
+ stdout_text = safe_decode(result.stdout)
36
+ assert len(stdout_text) < len(code)
37
+
38
+ finally:
39
+ os.unlink(temp_file)
40
+
41
+
42
+ def test_returns_original_when_longer():
43
+ """Test CLI returns original code when minified output would be longer."""
44
+ code = 'True if 0in x else False'
45
+
46
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
47
+ f.write(code)
48
+ temp_file = f.name
49
+
50
+ try:
51
+ env = os.environ.copy()
52
+ env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
53
+
54
+ result = run_subprocess([
55
+ sys.executable, '-m', 'python_minifier', temp_file
56
+ ], timeout=30, env=env)
57
+
58
+ assert result.returncode == 0
59
+
60
+ stdout_text = safe_decode(result.stdout)
61
+ assert stdout_text == code
62
+
63
+ finally:
64
+ os.unlink(temp_file)
65
+
66
+
67
+ def test_force_minified_with_env_var():
68
+ """Test environment variable forces minified output regardless of size."""
69
+ code = 'True if 0in x else False'
70
+ expected_output = 'True if 0 in x else False'
71
+
72
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
73
+ f.write(code)
74
+ temp_file = f.name
75
+
76
+ try:
77
+ env = os.environ.copy()
78
+ env['PYMINIFY_FORCE_BEST_EFFORT'] = '1'
79
+
80
+ result = run_subprocess([
81
+ sys.executable, '-m', 'python_minifier', temp_file
82
+ ], timeout=30, env=env)
83
+
84
+ assert result.returncode == 0
85
+
86
+ stdout_text = safe_decode(result.stdout)
87
+ assert stdout_text == expected_output
88
+
89
+ finally:
90
+ os.unlink(temp_file)
91
+
92
+
93
+ def test_stdin_behavior():
94
+ """Test size-based logic works with stdin input."""
95
+ code = 'True if 0in x else False'
96
+ expected_output = 'True if 0 in x else False'
97
+
98
+ # Without env var - should return original
99
+ env = os.environ.copy()
100
+ env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
101
+
102
+ result = run_subprocess([
103
+ sys.executable, '-m', 'python_minifier', '-'
104
+ ], input_data=code, timeout=30, env=env)
105
+
106
+ assert result.returncode == 0
107
+ stdout_text = safe_decode(result.stdout)
108
+ assert stdout_text == code
109
+
110
+ # With env var - should return minified
111
+ env['PYMINIFY_FORCE_BEST_EFFORT'] = '1'
112
+
113
+ result = run_subprocess([
114
+ sys.executable, '-m', 'python_minifier', '-'
115
+ ], input_data=code, timeout=30, env=env)
116
+
117
+ assert result.returncode == 0
118
+ stdout_text = safe_decode(result.stdout)
119
+ assert stdout_text == expected_output
120
+
121
+
122
+ def test_output_file_behavior():
123
+ """Test size-based logic works with --output flag."""
124
+ code = 'True if 0in x else False'
125
+
126
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as input_file:
127
+ input_file.write(code)
128
+ input_filename = input_file.name
129
+
130
+ with tempfile.NamedTemporaryFile(delete=False) as output_file:
131
+ output_filename = output_file.name
132
+
133
+ try:
134
+ env = os.environ.copy()
135
+ env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
136
+
137
+ result = run_subprocess([
138
+ sys.executable, '-m', 'python_minifier',
139
+ input_filename, '--output', output_filename
140
+ ], timeout=30, env=env)
141
+
142
+ assert result.returncode == 0
143
+
144
+ with open(output_filename, 'r') as f:
145
+ output_content = f.read()
146
+
147
+ assert output_content == code
148
+
149
+ finally:
150
+ os.unlink(input_filename)
151
+ os.unlink(output_filename)
152
+
153
+
154
+ def test_in_place_behavior():
155
+ """Test size-based logic works with --in-place flag."""
156
+ code = 'True if 0in x else False'
157
+
158
+ with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
159
+ f.write(code)
160
+ temp_file = f.name
161
+
162
+ try:
163
+ env = os.environ.copy()
164
+ env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
165
+
166
+ result = run_subprocess([
167
+ sys.executable, '-m', 'python_minifier',
168
+ temp_file, '--in-place'
169
+ ], timeout=30, env=env)
170
+
171
+ assert result.returncode == 0
172
+
173
+ with open(temp_file, 'r') as f:
174
+ modified_content = f.read()
175
+
176
+ assert modified_content == code
177
+
178
+ finally:
179
+ os.unlink(temp_file)
@@ -7,6 +7,11 @@ import ast
7
7
  from python_minifier.util import is_constant_node
8
8
 
9
9
 
10
+ @pytest.mark.filterwarnings("ignore:ast.Str is deprecated:DeprecationWarning")
11
+ @pytest.mark.filterwarnings("ignore:ast.Bytes is deprecated:DeprecationWarning")
12
+ @pytest.mark.filterwarnings("ignore:ast.Num is deprecated:DeprecationWarning")
13
+ @pytest.mark.filterwarnings("ignore:ast.NameConstant is deprecated:DeprecationWarning")
14
+ @pytest.mark.filterwarnings("ignore:ast.Ellipsis is deprecated:DeprecationWarning")
10
15
  def test_type_nodes():
11
16
  assert is_constant_node(ast.Str('a'), ast.Str)
12
17
 
@@ -28,6 +33,11 @@ def test_type_nodes():
28
33
  assert is_constant_node(ast.Ellipsis(), ast.Ellipsis)
29
34
 
30
35
 
36
+ @pytest.mark.filterwarnings("ignore:ast.Str is deprecated:DeprecationWarning")
37
+ @pytest.mark.filterwarnings("ignore:ast.Bytes is deprecated:DeprecationWarning")
38
+ @pytest.mark.filterwarnings("ignore:ast.Num is deprecated:DeprecationWarning")
39
+ @pytest.mark.filterwarnings("ignore:ast.NameConstant is deprecated:DeprecationWarning")
40
+ @pytest.mark.filterwarnings("ignore:ast.Ellipsis is deprecated:DeprecationWarning")
31
41
  def test_constant_nodes():
32
42
  # only test on python 3.8+
33
43
  if sys.version_info < (3, 8):
@@ -0,0 +1,170 @@
1
+ # -*- coding: utf-8 -*-
2
+ import tempfile
3
+ import os
4
+ import sys
5
+ import codecs
6
+
7
+ from subprocess_compat import run_subprocess, safe_decode
8
+
9
+
10
+ def test_cli_output_flag_with_unicode():
11
+ """
12
+ Tests that the CLI tool can write Unicode characters to files using --output flag.
13
+ """
14
+ # Minimal source with all problematic Unicode characters from reported issues
15
+ source_code = u'print(u"❌ ✓ 🐍 Привет © ∀")'
16
+
17
+ source_file = tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False)
18
+ source_file.write(source_code.encode('utf-8'))
19
+ source_file.close()
20
+
21
+ output_path = source_file.name + '.min.py'
22
+
23
+ try:
24
+ # Run pyminify CLI with --output flag
25
+ result = run_subprocess([
26
+ sys.executable, '-m', 'python_minifier',
27
+ source_file.name, '--output', output_path
28
+ ], timeout=30)
29
+
30
+ assert result.returncode == 0, "CLI failed with encoding error: {}".format(safe_decode(result.stderr))
31
+
32
+ # Verify the output file was created and contains Unicode characters
33
+ with codecs.open(output_path, 'r', encoding='utf-8') as f:
34
+ minified_content = f.read()
35
+
36
+ # Verify problematic Unicode characters are preserved
37
+ if hasattr(sys, 'pypy_version_info') and sys.version_info[0] >= 3:
38
+ # PyPy3: Unicode characters may be escaped as \\u escapes
39
+ assert "\\u274c" in minified_content
40
+ assert "✓" in minified_content
41
+ assert "\\U0001f40d" in minified_content
42
+ assert "Привет" in minified_content
43
+ assert "©" in minified_content
44
+ assert "∀" in minified_content
45
+ elif hasattr(sys, 'pypy_version_info') and sys.version_info[0] < 3:
46
+ # PyPy2: Unicode characters appear as UTF-8 byte sequences
47
+ assert "\\xe2\\x9d\\x8c" in minified_content # ❌
48
+ assert "\\xe2\\x9c\\x93" in minified_content # ✓
49
+ assert "\\xf0\\x9f\\x90\\x8d" in minified_content # 🐍
50
+ elif sys.version_info[0] >= 3:
51
+ # CPython 3: Unicode characters should appear literally
52
+ assert "❌" in minified_content
53
+ assert "✓" in minified_content
54
+ assert "🐍" in minified_content
55
+ assert "Привет" in minified_content
56
+ assert "©" in minified_content
57
+ assert "∀" in minified_content
58
+ else:
59
+ # Python 2.7: Check for escaped sequences or use Unicode literals
60
+ assert u"\\xe2\\x9d\\x8c" in minified_content
61
+ assert u"\\xe2\\x9c\\x93" in minified_content
62
+ assert u"\\xf0\\x9f\\x90\\x8d" in minified_content
63
+
64
+ finally:
65
+ # Cleanup
66
+ if os.path.exists(source_file.name):
67
+ os.unlink(source_file.name)
68
+ if os.path.exists(output_path):
69
+ os.unlink(output_path)
70
+
71
+
72
+ def test_cli_in_place_with_unicode():
73
+ """
74
+ Tests that the CLI tool can write Unicode characters to files using --in-place flag.
75
+ """
76
+ source_code = u'print(u"❌ ✓ 🐍 Привет © ∀")'
77
+
78
+ temp_file = tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False)
79
+ temp_file.write(source_code.encode('utf-8'))
80
+ temp_file.close()
81
+
82
+ try:
83
+ # Run pyminify with --in-place flag
84
+ result = run_subprocess([
85
+ sys.executable, '-m', 'python_minifier',
86
+ temp_file.name, '--in-place'
87
+ ], timeout=30)
88
+
89
+ assert result.returncode == 0, "CLI failed with encoding error: {}".format(safe_decode(result.stderr))
90
+
91
+ with codecs.open(temp_file.name, 'r', encoding='utf-8') as f:
92
+ content = f.read()
93
+
94
+ if hasattr(sys, 'pypy_version_info') and sys.version_info[0] >= 3:
95
+ # PyPy3: Unicode characters may be escaped as \\u escapes
96
+ assert "✓" in content
97
+ assert "\\u274c" in content # ❌
98
+ assert "\\U0001f40d" in content # 🐍
99
+ assert "Привет" in content
100
+ assert "©" in content
101
+ assert "∀" in content
102
+ elif hasattr(sys, 'pypy_version_info') and sys.version_info[0] < 3:
103
+ # PyPy2: Unicode characters appear as UTF-8 byte sequences
104
+ assert "\\xe2\\x9c\\x93" in content # ✓
105
+ assert "\\xe2\\x9d\\x8c" in content # ❌
106
+ assert "\\xf0\\x9f\\x90\\x8d" in content # 🐍
107
+ elif sys.version_info[0] >= 3:
108
+ # CPython 3: Unicode characters should appear literally
109
+ assert "✓" in content
110
+ assert "❌" in content
111
+ assert "🐍" in content
112
+ assert "Привет" in content
113
+ assert "©" in content
114
+ assert "∀" in content
115
+ else:
116
+ # Python 2.7: Unicode characters appear as escaped sequences
117
+ assert "\\xe2\\x9d\\x8c" in content # ❌
118
+ assert "\\xe2\\x9c\\x93" in content # ✓
119
+ assert "\\xf0\\x9f\\x90\\x8d" in content # 🐍
120
+
121
+ finally:
122
+ if os.path.exists(temp_file.name):
123
+ os.unlink(temp_file.name)
124
+
125
+
126
+ def test_cli_stdout_with_unicode():
127
+ """
128
+ Tests that the CLI tool can write Unicode characters to stdout.
129
+ """
130
+ source_code = u'print(u"❌ ✓ 🐍 Привет © ∀")'
131
+
132
+ temp_file = tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False)
133
+ temp_file.write(source_code.encode('utf-8'))
134
+ temp_file.close()
135
+
136
+ try:
137
+ # Run without --output or --in-place (should output to stdout)
138
+ result = run_subprocess([
139
+ sys.executable, '-m', 'python_minifier', temp_file.name
140
+ ], timeout=30)
141
+
142
+ assert result.returncode == 0, "Stdout output failed: {}".format(safe_decode(result.stderr))
143
+
144
+ stdout_text = safe_decode(result.stdout)
145
+
146
+ if hasattr(sys, 'pypy_version_info') and sys.version_info[0] >= 3:
147
+ # PyPy3: Unicode characters may be escaped as \\u escapes
148
+ assert "\\u274c" in stdout_text # ❌
149
+ assert "✓" in stdout_text # ✓
150
+ assert "\\U0001f40d" in stdout_text # 🐍
151
+ assert "Привет" in stdout_text # Привет
152
+ assert "©" in stdout_text # ©
153
+ assert "∀" in stdout_text # ∀
154
+ elif sys.version_info[0] >= 3:
155
+ # CPython 3: Unicode characters should appear literally
156
+ assert "❌" in stdout_text
157
+ assert "✓" in stdout_text
158
+ assert "🐍" in stdout_text
159
+ assert "Привет" in stdout_text
160
+ assert "©" in stdout_text
161
+ assert "∀" in stdout_text
162
+ else:
163
+ # Python 2.7: Unicode characters appear as escaped sequences
164
+ assert "\\xe2\\x9d\\x8c" in stdout_text # ❌
165
+ assert "\\xe2\\x9c\\x93" in stdout_text # ✓
166
+ assert "\\xf0\\x9f\\x90\\x8d" in stdout_text # 🐍
167
+
168
+ finally:
169
+ if os.path.exists(temp_file.name):
170
+ os.unlink(temp_file.name)
@@ -0,0 +1,114 @@
1
+ # -*- coding: utf-8 -*-
2
+ import pytest
3
+ import python_minifier
4
+ import tempfile
5
+ import os
6
+ import codecs
7
+
8
+
9
+ def test_minify_utf8_file():
10
+ """Test minifying a Python file with UTF-8 characters not in Windows default encoding."""
11
+
12
+ # Create Python source with UTF-8 characters that are not in Windows-1252
13
+ # Using Greek letters, Cyrillic, and mathematical symbols
14
+ source_code = u'''"""
15
+ This module contains UTF-8 characters that are not in Windows-1252 encoding:
16
+ - Greek: α β γ δ ε ζ η θ ι κ λ μ ν ξ ο π ρ σ τ υ φ χ ψ ω
17
+ - Cyrillic: а б в г д е ё ж з и й к л м н о п р с т у ф х ц ч ш щ ъ ы ь э ю я
18
+ - Mathematical: ∀ ∃ ∈ ∉ ∅ ∞ ∑ ∏ √ ∫ ∇ ∂ ≠ ≤ ≥ ≈ ≡ ⊂ ⊃ ⊆ ⊇
19
+ - Arrows: ← → ↑ ↓ ↔ ↕ ↖ ↗ ↘ ↙
20
+ """
21
+
22
+ def greet_in_greek():
23
+ return u"Γεια σας κόσμος" # "Hello world" in Greek
24
+
25
+ def mathematical_formula():
26
+ # Using mathematical symbols in comments
27
+ # ∀x ∈ ℝ: x² ≥ 0
28
+ return u"∑ from i=1 to ∞ of 1/i² = π²/6"
29
+
30
+ def arrow_symbols():
31
+ directions = {
32
+ u"left": u"←",
33
+ u"right": u"→",
34
+ u"up": u"↑",
35
+ u"down": u"↓"
36
+ }
37
+ return directions
38
+
39
+ if __name__ == "__main__":
40
+ print(greet_in_greek())
41
+ print(greet_in_russian())
42
+ print(mathematical_formula())
43
+ print(arrow_symbols())
44
+ '''
45
+
46
+ # Write to temporary file with UTF-8 encoding
47
+ # Python 2.7 doesn't support encoding parameter, so use binary mode
48
+ with tempfile.NamedTemporaryFile(mode='wb', suffix='.py', delete=False) as f:
49
+ f.write(source_code.encode('utf-8'))
50
+ temp_file = f.name
51
+
52
+ try:
53
+ # Read the file and minify it
54
+ # Python 2.7 doesn't support encoding parameter in open()
55
+ with codecs.open(temp_file, 'r', encoding='utf-8') as f:
56
+ original_content = f.read()
57
+
58
+ # This should work - minify the UTF-8 content
59
+ minified = python_minifier.minify(original_content)
60
+
61
+ # Verify the minified code still contains the UTF-8 characters
62
+ # On Python 2.7, Unicode characters in string literals are escaped but preserved
63
+ # Test by executing the minified code and checking the actual values
64
+ minified_globals = {}
65
+ exec(minified, minified_globals)
66
+
67
+ # The minified code should contain the same functions that return Unicode
68
+ assert 'greet_in_greek' in minified_globals
69
+ assert u"Γεια σας κόσμος" == minified_globals['greet_in_greek']()
70
+
71
+ # Test that mathematical symbols are also preserved
72
+ assert 'mathematical_formula' in minified_globals
73
+ assert u"∑ from i=1 to ∞" in minified_globals['mathematical_formula']()
74
+
75
+ finally:
76
+ # Clean up
77
+ os.unlink(temp_file)
78
+
79
+
80
+ def test_minify_utf8_file_direct():
81
+ """Test minifying a file directly with UTF-8 characters."""
82
+
83
+ # Create Python source with UTF-8 characters
84
+ source_code = u'''# UTF-8 test file
85
+ def emoji_function():
86
+ """Function with emoji and special characters: 🐍 ∆ ∑ ∫ ∞"""
87
+ return u"Python is 🐍 awesome! Math symbols: ∆x ≈ 0, ∑∞ = ∞"
88
+
89
+ class UnicodeClass:
90
+ """Class with unicode: ñ ü ö ä ë ï ÿ"""
91
+ def __init__(self):
92
+ self.message = u"Héllö Wörld with àccénts!"
93
+
94
+ def get_symbols(self):
95
+ return u"Symbols: ™ © ® ° ± × ÷ ≠ ≤ ≥"
96
+ '''
97
+
98
+ # Test direct minification
99
+ minified = python_minifier.minify(source_code)
100
+
101
+ # Verify UTF-8 characters are preserved by executing the minified code
102
+ minified_globals = {}
103
+ exec(minified, minified_globals)
104
+
105
+ # Test that the functions return the correct Unicode strings
106
+ assert u"🐍" in minified_globals['emoji_function']()
107
+ assert u"∆" in minified_globals['emoji_function']()
108
+
109
+ # Test the class
110
+ unicode_obj = minified_globals['UnicodeClass']()
111
+ assert u"Héllö" in unicode_obj.message
112
+ assert u"àccénts" in unicode_obj.message
113
+ assert u"™" in unicode_obj.get_symbols()
114
+ assert u"©" in unicode_obj.get_symbols()