python-minifier
Advanced tools
| """Tests for the prefer_single_line option.""" | ||
| import ast | ||
| import sys | ||
| import os | ||
| import tempfile | ||
| from python_minifier import minify, unparse | ||
| from python_minifier.ast_annotation import add_parent | ||
| from python_minifier.rename import add_namespace | ||
| from subprocess_compat import run_subprocess, safe_decode | ||
| # minify() tests | ||
| def test_minify_default_uses_newlines(): | ||
| """Default behavior uses newlines between module-level statements.""" | ||
| source = ''' | ||
| a = 1 | ||
| b = 2 | ||
| c = 3 | ||
| ''' | ||
| expected = 'a=1\nb=2\nc=3' | ||
| assert minify(source) == expected | ||
| def test_minify_prefer_single_line_false_uses_newlines(): | ||
| """prefer_single_line=False uses newlines between module-level statements.""" | ||
| source = ''' | ||
| a = 1 | ||
| b = 2 | ||
| c = 3 | ||
| ''' | ||
| expected = 'a=1\nb=2\nc=3' | ||
| assert minify(source, prefer_single_line=False) == expected | ||
| def test_minify_prefer_single_line_true_uses_semicolons(): | ||
| """prefer_single_line=True uses semicolons between module-level statements.""" | ||
| source = ''' | ||
| a = 1 | ||
| b = 2 | ||
| c = 3 | ||
| ''' | ||
| expected = 'a=1;b=2;c=3' | ||
| assert minify(source, prefer_single_line=True) == expected | ||
| def test_minify_single_statement_no_trailing_separator(): | ||
| """Single statement has no trailing separator regardless of option.""" | ||
| source = 'a = 1' | ||
| expected = 'a=1' | ||
| assert minify(source, prefer_single_line=False) == expected | ||
| assert minify(source, prefer_single_line=True) == expected | ||
| def test_minify_empty_module(): | ||
| """Empty module produces empty output.""" | ||
| source = '' | ||
| expected = '' | ||
| assert minify(source, prefer_single_line=False) == expected | ||
| assert minify(source, prefer_single_line=True) == expected | ||
| def test_minify_function_body_uses_semicolons(): | ||
| """Function body statements use semicolons regardless of option.""" | ||
| source = ''' | ||
| def f(): | ||
| a = 1 | ||
| b = 2 | ||
| return a + b | ||
| ''' | ||
| # Both produce identical output since the option only affects module level | ||
| expected = 'def f():A=1;B=2;return A+B' | ||
| assert minify(source, prefer_single_line=False) == expected | ||
| assert minify(source, prefer_single_line=True) == expected | ||
| def test_minify_class_body_uses_semicolons(): | ||
| """Class body statements use semicolons regardless of option.""" | ||
| source = ''' | ||
| class C: | ||
| a = 1 | ||
| b = 2 | ||
| ''' | ||
| # Both produce identical output since the option only affects module level | ||
| expected = 'class C:a=1;b=2' | ||
| assert minify(source, prefer_single_line=False) == expected | ||
| assert minify(source, prefer_single_line=True) == expected | ||
| def test_minify_mixed_module_and_function(): | ||
| """Compound statements like def require newlines regardless of option.""" | ||
| source = ''' | ||
| x = 1 | ||
| def f(): | ||
| a = 1 | ||
| b = 2 | ||
| y = 2 | ||
| ''' | ||
| # Both outputs are identical because compound statements require newlines | ||
| expected = 'x=1\ndef f():A=1;B=2\ny=2' | ||
| assert minify(source, prefer_single_line=False) == expected | ||
| assert minify(source, prefer_single_line=True) == expected | ||
| def test_minify_imports(): | ||
| """Import statements respect prefer_single_line option.""" | ||
| source = ''' | ||
| import os | ||
| import sys | ||
| a = 1 | ||
| ''' | ||
| # Imports are combined, module-level separator differs | ||
| expected_newlines = 'import os,sys\na=1' | ||
| expected_semicolons = 'import os,sys;a=1' | ||
| assert minify(source, prefer_single_line=False) == expected_newlines | ||
| assert minify(source, prefer_single_line=True) == expected_semicolons | ||
| # unparse() tests | ||
| def _prepare_module(source): | ||
| """Parse and annotate a module for unparsing.""" | ||
| module = ast.parse(source) | ||
| add_parent(module) | ||
| add_namespace(module) | ||
| return module | ||
| def test_unparse_default_uses_newlines(): | ||
| """unparse() default uses newlines.""" | ||
| source = 'a=1\nb=2' | ||
| expected = 'a=1\nb=2' | ||
| module = _prepare_module(source) | ||
| assert unparse(module) == expected | ||
| def test_unparse_prefer_single_line_false(): | ||
| """unparse() with prefer_single_line=False uses newlines.""" | ||
| source = 'a=1\nb=2' | ||
| expected = 'a=1\nb=2' | ||
| module = _prepare_module(source) | ||
| assert unparse(module, prefer_single_line=False) == expected | ||
| def test_unparse_prefer_single_line_true(): | ||
| """unparse() with prefer_single_line=True uses semicolons.""" | ||
| source = 'a=1\nb=2' | ||
| expected = 'a=1;b=2' | ||
| module = _prepare_module(source) | ||
| assert unparse(module, prefer_single_line=True) == expected | ||
| # CLI tests | ||
| def _normalize_newlines(text): | ||
| """Normalize line endings for cross-platform comparison.""" | ||
| return text.replace('\r\n', '\n') | ||
| def test_cli_default_uses_newlines(): | ||
| """CLI without --prefer-single-line uses newlines.""" | ||
| code = 'a = 1\nb = 2\nc = 3' | ||
| expected = 'a=1\nb=2\nc=3' | ||
| with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: | ||
| f.write(code) | ||
| temp_file = f.name | ||
| try: | ||
| result = run_subprocess([ | ||
| sys.executable, '-m', 'python_minifier', temp_file | ||
| ], timeout=30) | ||
| assert result.returncode == 0 | ||
| stdout_text = _normalize_newlines(safe_decode(result.stdout)) | ||
| assert stdout_text == expected | ||
| finally: | ||
| os.unlink(temp_file) | ||
| def test_cli_prefer_single_line_flag(): | ||
| """CLI with --prefer-single-line uses semicolons.""" | ||
| code = 'a = 1\nb = 2\nc = 3' | ||
| expected = 'a=1;b=2;c=3' | ||
| with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f: | ||
| f.write(code) | ||
| temp_file = f.name | ||
| try: | ||
| result = run_subprocess([ | ||
| sys.executable, '-m', 'python_minifier', | ||
| '--prefer-single-line', temp_file | ||
| ], timeout=30) | ||
| assert result.returncode == 0 | ||
| stdout_text = safe_decode(result.stdout) | ||
| assert stdout_text == expected | ||
| finally: | ||
| os.unlink(temp_file) | ||
| def test_cli_stdin_prefer_single_line(): | ||
| """CLI --prefer-single-line works with stdin.""" | ||
| code = 'a = 1\nb = 2\nc = 3' | ||
| expected = 'a=1;b=2;c=3' | ||
| result = run_subprocess([ | ||
| sys.executable, '-m', 'python_minifier', | ||
| '--prefer-single-line', '-' | ||
| ], input_data=code, timeout=30) | ||
| assert result.returncode == 0 | ||
| stdout_text = safe_decode(result.stdout) | ||
| assert stdout_text == expected |
+1
-1
| Metadata-Version: 2.4 | ||
| Name: python_minifier | ||
| Version: 3.1.1 | ||
| Version: 3.2.0 | ||
| Summary: Transform Python source code into it's most compact representation | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/dflook/python-minifier |
+1
-1
@@ -30,3 +30,3 @@ import os.path | ||
| python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15', | ||
| version='3.1.1', | ||
| version='3.2.0', | ||
@@ -33,0 +33,0 @@ classifiers=[ |
| Metadata-Version: 2.4 | ||
| Name: python_minifier | ||
| Version: 3.1.1 | ||
| Version: 3.2.0 | ||
| Summary: Transform Python source code into it's most compact representation | ||
@@ -5,0 +5,0 @@ Home-page: https://github.com/dflook/python-minifier |
@@ -80,2 +80,3 @@ LICENSE | ||
| test/test_posargs.py | ||
| test/test_prefer_single_line.py | ||
| test/test_preserve_shebang.py | ||
@@ -82,0 +83,0 @@ test/test_raw_fstring_backslash.py |
@@ -75,3 +75,4 @@ """ | ||
| remove_builtin_exception_brackets=True, | ||
| constant_folding=True | ||
| constant_folding=True, | ||
| prefer_single_line=False, | ||
| ): | ||
@@ -111,2 +112,3 @@ """ | ||
| :param bool constant_folding: If literal expressions should be evaluated | ||
| :param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size | ||
@@ -197,3 +199,3 @@ :rtype: str | ||
| minified = unparse(module) | ||
| minified = unparse(module, prefer_single_line=prefer_single_line) | ||
@@ -225,3 +227,3 @@ if preserve_shebang is True: | ||
| def unparse(module): | ||
| def unparse(module, prefer_single_line=False): | ||
| """ | ||
@@ -235,2 +237,3 @@ Turn a module AST into python code | ||
| :type: module: :class:`ast.Module` | ||
| :param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size | ||
| :rtype: str | ||
@@ -242,3 +245,3 @@ | ||
| printer = ModulePrinter() | ||
| printer = ModulePrinter(prefer_single_line=prefer_single_line) | ||
| printer(module) | ||
@@ -245,0 +248,0 @@ |
@@ -31,7 +31,11 @@ import ast | ||
| remove_builtin_exception_brackets: bool = ..., | ||
| constant_folding: bool = ... | ||
| constant_folding: bool = ..., | ||
| prefer_single_line: bool = ... | ||
| ) -> Text: ... | ||
| def unparse(module: ast.Module) -> Text: ... | ||
| def unparse( | ||
| module: ast.Module, | ||
| prefer_single_line: bool = ... | ||
| ) -> Text: ... | ||
@@ -38,0 +42,0 @@ |
@@ -143,2 +143,9 @@ from __future__ import print_function | ||
| parser.add_argument( | ||
| '--prefer-single-line', | ||
| action='store_true', | ||
| help='Prefer multiple statements on a single line separated by semicolons, instead of newlines, where there is no difference in output size', | ||
| dest='prefer_single_line', | ||
| ) | ||
| # Minification arguments | ||
@@ -377,3 +384,4 @@ minification_options = parser.add_argument_group('minification options', 'Options that affect how the source is minified') | ||
| remove_builtin_exception_brackets=minification_args.remove_exception_brackets, | ||
| constant_folding=minification_args.constant_folding | ||
| constant_folding=minification_args.constant_folding, | ||
| prefer_single_line=minification_args.prefer_single_line, | ||
| ) | ||
@@ -380,0 +388,0 @@ |
@@ -14,3 +14,3 @@ import sys | ||
| def __init__(self): | ||
| def __init__(self, prefer_single_line=False): | ||
@@ -38,3 +38,3 @@ self.precedences = { | ||
| self.printer = TokenPrinter() | ||
| self.printer = TokenPrinter(prefer_single_line=prefer_single_line) | ||
@@ -41,0 +41,0 @@ def __call__(self, module): |
@@ -14,4 +14,4 @@ import sys | ||
| def __init__(self, indent_char='\t'): | ||
| super(ModulePrinter, self).__init__() | ||
| def __init__(self, indent_char='\t', prefer_single_line=False): | ||
| super(ModulePrinter, self).__init__(prefer_single_line=prefer_single_line) | ||
| self.indent_char = indent_char | ||
@@ -18,0 +18,0 @@ |
@@ -309,3 +309,3 @@ """Tools for assembling python code from tokens.""" | ||
| if self.indent == 0: | ||
| if self.indent == 0 and not self._prefer_single_line: | ||
| self.newline() | ||
@@ -312,0 +312,0 @@ else: |
@@ -13,2 +13,22 @@ import math | ||
| def is_foldable_constant(node): | ||
| """ | ||
| Check if a node is a constant expression that can participate in folding. | ||
| We can asume that children have already been folded, so foldable constants are either: | ||
| - Simple literals (Num, NameConstant) | ||
| - UnaryOp(USub/Invert) on a Num - these don't fold to shorter forms, | ||
| so they remain after child visiting. UAdd and Not would have been | ||
| folded away since they always produce shorter results. | ||
| """ | ||
| if is_constant_node(node, (ast.Num, ast.NameConstant)): | ||
| return True | ||
| if isinstance(node, ast.UnaryOp): | ||
| if isinstance(node.op, (ast.USub, ast.Invert)): | ||
| return is_constant_node(node.operand, ast.Num) | ||
| return False | ||
| class FoldConstants(SuiteTransformer): | ||
@@ -22,24 +42,3 @@ """ | ||
| def visit_BinOp(self, node): | ||
| node.left = self.visit(node.left) | ||
| node.right = self.visit(node.right) | ||
| # Check this is a constant expression that could be folded | ||
| # We don't try to fold strings or bytes, since they have probably been arranged this way to make the source shorter and we are unlikely to beat that | ||
| if not is_constant_node(node.left, (ast.Num, ast.NameConstant)): | ||
| return node | ||
| if not is_constant_node(node.right, (ast.Num, ast.NameConstant)): | ||
| return node | ||
| if isinstance(node.op, ast.Div): | ||
| # Folding div is subtle, since it can have different results in Python 2 and Python 3 | ||
| # Do this once target version options have been implemented | ||
| return node | ||
| if isinstance(node.op, ast.Pow): | ||
| # This can be folded, but it is unlikely to reduce the size of the source | ||
| # It can also be slow to evaluate | ||
| return node | ||
| def fold(self, node): | ||
| # Evaluate the expression | ||
@@ -101,3 +100,41 @@ try: | ||
| def visit_BinOp(self, node): | ||
| node.left = self.visit(node.left) | ||
| node.right = self.visit(node.right) | ||
| # Check this is a constant expression that could be folded | ||
| # We don't try to fold strings or bytes, since they have probably been arranged this way to make the source shorter and we are unlikely to beat that | ||
| if not is_foldable_constant(node.left): | ||
| return node | ||
| if not is_foldable_constant(node.right): | ||
| return node | ||
| if isinstance(node.op, ast.Div): | ||
| # Folding div is subtle, since it can have different results in Python 2 and Python 3 | ||
| # Do this once target version options have been implemented | ||
| return node | ||
| if isinstance(node.op, ast.Pow): | ||
| # This can be folded, but it is unlikely to reduce the size of the source | ||
| # It can also be slow to evaluate | ||
| return node | ||
| return self.fold(node) | ||
| def visit_UnaryOp(self, node): | ||
| node.operand = self.visit(node.operand) | ||
| # Only fold if the operand is a foldable constant | ||
| if not is_foldable_constant(node.operand): | ||
| return node | ||
| # Only fold these unary operators | ||
| if not isinstance(node.op, (ast.USub, ast.UAdd, ast.Invert, ast.Not)): | ||
| return node | ||
| return self.fold(node) | ||
| def equal_value_and_type(a, b): | ||
@@ -104,0 +141,0 @@ if type(a) != type(b): |
+412
-3
@@ -6,8 +6,8 @@ import ast | ||
| from python_minifier import minify | ||
| from python_minifier.ast_annotation import add_parent | ||
| from python_minifier.ast_compare import compare_ast | ||
| from python_minifier.rename import add_namespace | ||
| from python_minifier.transforms.constant_folding import FoldConstants | ||
| from python_minifier.transforms.constant_folding import FoldConstants, equal_value_and_type | ||
| def fold_constants(source): | ||
@@ -110,3 +110,5 @@ module = ast.parse(source) | ||
| ('10%3', '1'), | ||
| ('10-100', '-90') | ||
| ('10-100', '-90'), | ||
| ('1+1', '2'), | ||
| ('2+2', '4'), | ||
| ] | ||
@@ -154,1 +156,408 @@ ) | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| # Folding results in infinity, which can be represented as 1e999 | ||
| ('1e308 + 1e308', '1e999'), | ||
| ('1e308 * 2', '1e999'), | ||
| ] | ||
| ) | ||
| def test_fold_infinity(source, expected): | ||
| """ | ||
| Test that expressions resulting in infinity are folded to 1e999. | ||
| Infinity can be represented as 1e999, which is shorter than | ||
| the original expression. | ||
| """ | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| # Folding would result in NaN, which cannot be represented as a literal | ||
| ('1e999 - 1e999', '1e999 - 1e999'), | ||
| ('0.0 * 1e999', '0.0 * 1e999'), | ||
| ] | ||
| ) | ||
| def test_no_fold_nan(source, expected): | ||
| """ | ||
| Test that expressions resulting in NaN are not folded. | ||
| NaN is not a valid Python literal, so we cannot fold expressions | ||
| that would produce it. | ||
| """ | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('100.0+100.0', '200.0'), | ||
| ('1000.0+1000.0', '2000.0'), | ||
| ] | ||
| ) | ||
| def test_fold_float(source, expected): | ||
| """ | ||
| Test that float expressions are folded when the result is shorter. | ||
| """ | ||
| run_test(source, expected) | ||
| def test_equal_value_and_type(): | ||
| """ | ||
| Test the equal_value_and_type helper function. | ||
| """ | ||
| # Same type and value | ||
| assert equal_value_and_type(1, 1) is True | ||
| assert equal_value_and_type(1.0, 1.0) is True | ||
| assert equal_value_and_type(True, True) is True # noqa: FBT003 | ||
| assert equal_value_and_type('hello', 'hello') is True | ||
| # Different types | ||
| assert equal_value_and_type(1, 1.0) is False | ||
| assert equal_value_and_type(1, True) is False # noqa: FBT003 | ||
| assert equal_value_and_type(True, 1) is False # noqa: FBT003 | ||
| # Different values | ||
| assert equal_value_and_type(1, 2) is False | ||
| assert equal_value_and_type(1.0, 2.0) is False | ||
| def test_equal_value_and_type_nan(): | ||
| """ | ||
| Test the equal_value_and_type helper function with NaN values. | ||
| """ | ||
| nan = float('nan') | ||
| # NaN is not equal to itself in Python (nan != nan is True) | ||
| # But if both are NaN, equal_value_and_type returns True via a == b | ||
| # Since nan == nan is False, we need to check the actual behavior | ||
| result = equal_value_and_type(nan, nan) | ||
| # Python's nan == nan is False, so this should be False | ||
| assert result is False | ||
| # NaN compared to non-NaN should be False | ||
| assert equal_value_and_type(nan, 1.0) is False | ||
| assert equal_value_and_type(1.0, nan) is False | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('5 - 10', '-5'), | ||
| ('0 - 100', '-100'), | ||
| ('1.0 - 2.0', '-1.0'), | ||
| ('0.0 - 100.0', '-100.0'), | ||
| ] | ||
| ) | ||
| def test_negative_results(source, expected): | ||
| """ | ||
| Test BinOp expressions that produce negative results. | ||
| """ | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('5 * -2', '-10'), | ||
| ('-5 * 2', '-10'), | ||
| ('-5 + 10', '5'), | ||
| ('-90 + 10', '-80'), | ||
| ('10 - 20 + 5', '-5'), | ||
| ('(5 - 10) * 2', '-10'), | ||
| ('2 * (0 - 5)', '-10'), | ||
| ('(1 - 10) + (2 - 20)', '-27'), | ||
| ] | ||
| ) | ||
| def test_negative_operands_folded(source, expected): | ||
| """ | ||
| Test that expressions with negative operands are folded. | ||
| """ | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('-(-5)', '5'), | ||
| ('--5', '5'), | ||
| ('-(-100)', '100'), | ||
| ('-(-(5 + 5))', '10'), | ||
| ('~(~0)', '0'), | ||
| ('~~5', '5'), | ||
| ('~~100', '100'), | ||
| ('+(+5)', '5'), | ||
| ('+(-5)', '-5'), | ||
| ] | ||
| ) | ||
| def test_unary_folded(source, expected): | ||
| """ | ||
| Test that unary operations on constant expressions are folded. | ||
| """ | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('not not True', 'True'), | ||
| ('not not False', 'False'), | ||
| ('not True', 'False'), | ||
| ('not False', 'True'), | ||
| ] | ||
| ) | ||
| def test_unary_not_folded(source, expected): | ||
| """ | ||
| Test that 'not' operations on constant expressions are folded. | ||
| """ | ||
| if sys.version_info < (3, 4): | ||
| pytest.skip('NameConstant not in python < 3.4') | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('-5', '-5'), | ||
| ('~5', '~5'), | ||
| ] | ||
| ) | ||
| def test_unary_simple_not_folded(source, expected): | ||
| """ | ||
| Test that simple unary operations on literals are not folded | ||
| when the result would not be shorter. | ||
| """ | ||
| run_test(source, expected) | ||
| def test_unary_plus_folded(): | ||
| """ | ||
| Test that unary plus on a literal is folded to remove the plus. | ||
| """ | ||
| run_test('+5', '5') | ||
| def test_not_false_in_conditional(): | ||
| """ | ||
| Test that 'not False' is folded to 'True' in a conditional. | ||
| """ | ||
| if sys.version_info < (3, 4): | ||
| pytest.skip('NameConstant not in python < 3.4') | ||
| run_test('if not False:pass', 'if True:pass') | ||
| def test_not_not_true_in_assignment(): | ||
| """ | ||
| Test that 'not not True' is folded to 'True' in an assignment. | ||
| """ | ||
| if sys.version_info < (3, 4): | ||
| pytest.skip('NameConstant not in python < 3.4') | ||
| run_test('x=not not True', 'x=True') | ||
| def test_bool_not_folded_before_34(): | ||
| """ | ||
| Test that boolean 'not' expressions are not folded in Python < 3.4. | ||
| NameConstant was introduced in Python 3.4, so we cannot fold boolean | ||
| constants in earlier versions. | ||
| """ | ||
| if sys.version_info >= (3, 4): | ||
| pytest.skip('Only applies to python < 3.4') | ||
| run_test('if not False:pass', 'if not False:pass') | ||
| run_test('x=not not True', 'x=not not True') | ||
| def test_constant_folding_enabled_by_default(): | ||
| """Verify constant folding is enabled by default.""" | ||
| source = 'x = 10 + 10' | ||
| result = minify(source) | ||
| assert '20' in result | ||
| assert '10+10' not in result and '10 + 10' not in result # noqa: PT018 | ||
| def test_constant_folding_disabled(): | ||
| """Verify expressions are not folded when constant_folding=False.""" | ||
| source = 'x = 10 + 10' | ||
| result = minify(source, constant_folding=False) | ||
| assert '10+10' in result or '10 + 10' in result | ||
| assert result.strip() != 'x=20' | ||
| def test_constant_folding_disabled_complex_expression(): | ||
| """Verify complex expressions are preserved when disabled.""" | ||
| source = 'SECONDS_IN_A_DAY = 60 * 60 * 24' | ||
| result = minify(source, constant_folding=False) | ||
| assert '60*60*24' in result or '60 * 60 * 24' in result | ||
| def test_constant_folding_enabled_complex_expression(): | ||
| """Verify complex expressions are folded when enabled.""" | ||
| source = 'SECONDS_IN_A_DAY = 60 * 60 * 24' | ||
| result = minify(source, constant_folding=True) | ||
| assert '86400' in result | ||
| @pytest.mark.parametrize( | ||
| ('source', 'should_contain_when_disabled'), [ | ||
| ('x = 5 - 10', '5-10'), | ||
| ('x = True | False', 'True|False'), | ||
| ('x = 0xff ^ 0x0f', '255^15'), | ||
| ] | ||
| ) | ||
| def test_constant_folding_disabled_various_ops(source, should_contain_when_disabled): | ||
| """Verify various operations are not folded when disabled.""" | ||
| if sys.version_info < (3, 4) and 'True' in source: | ||
| pytest.skip('NameConstant not in python < 3.4') | ||
| result = minify(source, constant_folding=False) | ||
| assert should_contain_when_disabled in result.replace(' ', '') | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('1j + 2j', '3j'), | ||
| ('3j * 2', '6j'), | ||
| ('2 * 3j', '6j'), | ||
| ('10j - 5j', '5j'), | ||
| ] | ||
| ) | ||
| def test_complex_folded(source, expected): | ||
| """ | ||
| Test complex number operations that are folded. | ||
| Complex operations are folded when the result is shorter than the original. | ||
| """ | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('1j - 2j', '1j-2j'), | ||
| ('1j * 1j', '1j*1j'), | ||
| ('0j + 5', '0j+5'), | ||
| ('2j / 1j', '2j/1j'), | ||
| ('1j ** 2', '1j**2'), | ||
| ] | ||
| ) | ||
| def test_complex_not_folded(source, expected): | ||
| """ | ||
| Test complex number operations that are not folded. | ||
| """ | ||
| if sys.version_info < (3, 0) and source == '1j - 2j': | ||
| pytest.skip('Complex subtraction representation differs in Python 2') | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| # These fold because the result is 0j or the folded form is shorter | ||
| ('-3j + 3j', '0j'), | ||
| ('1j + -1j', '0j'), | ||
| ] | ||
| ) | ||
| def test_negative_complex_in_binop_folded(source, expected): | ||
| """ | ||
| Test that negative complex numbers (UnaryOp USub on complex) participate in BinOp folding. | ||
| """ | ||
| if sys.version_info < (3, 0): | ||
| pytest.skip('Complex number representation differs in Python 2') | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('-3j + 1j', '-3j+1j'), | ||
| ('-5j * 2', '-5j*2'), | ||
| ('2 * -5j', '2*-5j'), | ||
| ('-10j + 5j', '-10j+5j'), | ||
| ] | ||
| ) | ||
| def test_negative_complex_in_binop_not_folded(source, expected): | ||
| """ | ||
| Test that some negative complex operations don't fold due to representation issues. | ||
| When negating a pure imaginary number like -2j, Python represents -(-2j) as (-0+2j), | ||
| which makes the folded form longer than the original expression. | ||
| """ | ||
| if sys.version_info < (3, 0): | ||
| pytest.skip('Complex number representation differs in Python 2') | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('~0 + 1', '0'), | ||
| ('~5 & 0xff', '250'), | ||
| ('~0 | 5', '-1'), # -1 in binary is all 1s, so -1 | x = -1 | ||
| ('1 + ~0', '0'), | ||
| ('~1 + 2', '0'), | ||
| ('~0xff & 0xff', '0'), | ||
| ] | ||
| ) | ||
| def test_invert_in_binop(source, expected): | ||
| """ | ||
| Test that bitwise invert (UnaryOp Invert) participates in BinOp folding. | ||
| """ | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('~0', '~0'), | ||
| ('~1', '~1'), | ||
| ('~5', '~5'), | ||
| ('~255', '~255'), | ||
| ] | ||
| ) | ||
| def test_invert_not_folded(source, expected): | ||
| """ | ||
| Test that simple bitwise invert on literals is not folded when the result is not shorter. | ||
| ~0 = -1, ~1 = -2, ~5 = -6, etc. These are the same length or longer. | ||
| """ | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| ('~~0', '0'), | ||
| ('~~5', '5'), | ||
| ('~~~0', '~0'), | ||
| ('~~~~5', '5'), | ||
| ] | ||
| ) | ||
| def test_double_invert_folded(source, expected): | ||
| """ | ||
| Test that double bitwise invert is folded. | ||
| ~~x = x, so double invert should fold away. | ||
| """ | ||
| run_test(source, expected) | ||
| @pytest.mark.parametrize( | ||
| ('source', 'expected'), [ | ||
| # In Python, True == 1 and False == 0 for arithmetic | ||
| ('-5 + True', '-4'), | ||
| ('10 * False', '0'), | ||
| ('True + True', '2'), | ||
| ('~True', '-2'), # ~1 = -2, shorter than ~True | ||
| ('~False', '-1'), # ~0 = -1, shorter than ~False | ||
| ] | ||
| ) | ||
| def test_mixed_numeric_bool_folded(source, expected): | ||
| """ | ||
| Test folding of expressions mixing numeric and boolean operands. | ||
| Python treats True as 1 and False as 0 in numeric contexts. | ||
| """ | ||
| if sys.version_info < (3, 4): | ||
| pytest.skip('NameConstant not in python < 3.4') | ||
| run_test(source, expected) |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
507215
4.01%102
0.99%12745
4.45%