New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

python-minifier

Package Overview
Dependencies
Maintainers
1
Versions
28
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

python-minifier - pypi Package Compare versions

Comparing version
3.1.1
to
3.2.0
+217
test/test_prefer_single_line.py
"""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

@@ -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):

@@ -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)