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.0
to
3.1.1
+2
-2
PKG-INFO
Metadata-Version: 2.4
Name: python_minifier
Version: 3.1.0
Version: 3.1.1
Summary: Transform Python source code into it's most compact representation

@@ -163,3 +163,3 @@ Home-page: https://github.com/dflook/python-minifier

python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.13.
python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.14.

@@ -166,0 +166,0 @@ ## Usage

@@ -113,3 +113,3 @@ # Python Minifier

python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.13.
python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.14.

@@ -116,0 +116,0 @@ ## Usage

@@ -30,3 +30,3 @@ import os.path

python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, <3.15',
version='3.1.0',
version='3.1.1',

@@ -33,0 +33,0 @@ classifiers=[

Metadata-Version: 2.4
Name: python_minifier
Version: 3.1.0
Version: 3.1.1
Summary: Transform Python source code into it's most compact representation

@@ -163,3 +163,3 @@ Home-page: https://github.com/dflook/python-minifier

python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.13.
python-minifier runs with and can minify code written for Python 2.7 and Python 3.3 to 3.14.

@@ -166,0 +166,0 @@ ## Usage

@@ -66,3 +66,3 @@ import python_minifier.ast_compat as ast

continue
if field == 'str' and hasattr(ast, 'Interpolation') and isinstance(l_ast, ast.Interpolation):

@@ -69,0 +69,0 @@ continue

@@ -80,2 +80,2 @@ """

if _node_type not in globals():
globals()[_node_type] = type(_node_type, (AST,), {})
globals()[_node_type] = type(_node_type, (AST,), {})

@@ -304,4 +304,4 @@ import sys

(op_precedence > value_precedence)
or op_precedence == value_precedence
and self._is_left_associative(node.op)
or (op_precedence == value_precedence
and self._is_left_associative(node.op))
):

@@ -308,0 +308,0 @@ self.printer.delimiter('(')

@@ -599,3 +599,3 @@ import sys

if isinstance(node.pattern, ast.MatchSequence):
self.visit_MatchSequence(node.pattern, open=True)
self.visit_MatchSequence(node.pattern, omit_brackets=True)
else:

@@ -630,6 +630,6 @@ self.pattern(node.pattern)

def visit_MatchSequence(self, node, open=False):
def visit_MatchSequence(self, node, omit_brackets=False):
assert isinstance(node, ast.MatchSequence)
if len(node.patterns) < 2 or not open:
if len(node.patterns) < 2 or not omit_brackets:
self.printer.delimiter('[')

@@ -642,3 +642,3 @@

if len(node.patterns) < 2 or not open:
if len(node.patterns) < 2 or not omit_brackets:
self.printer.delimiter(']')

@@ -645,0 +645,0 @@

@@ -38,3 +38,3 @@ import python_minifier.ast_compat as ast

def comp(tup):
namespace, binding = tup
_namespace, binding = tup
return binding.new_mention_count()

@@ -41,0 +41,0 @@

@@ -106,3 +106,3 @@ """Tools for assembling python code from tokens."""

return self._code
def __unicode__(self):

@@ -109,0 +109,0 @@ """Return the output code as unicode (for Python 2.7 compatibility)."""

@@ -30,14 +30,34 @@ import sys

if isinstance(node.test, ast.Name) and node.test.id == '__debug__':
return True
def is_simple_debug_check():
# Simple case: if __debug__:
if isinstance(node.test, ast.Name) and node.test.id == '__debug__':
return True
return False
if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Is) and self.constant_value(node.test.comparators[0]) is True:
return True
def is_truthy_debug_comparison():
# Comparison case: if __debug__ is True / False / etc.
if not isinstance(node.test, ast.Compare):
return False
if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.IsNot) and self.constant_value(node.test.comparators[0]) is False:
return True
if not isinstance(node.test.left, ast.Name):
return False
if isinstance(node.test, ast.Compare) and len(node.test.ops) == 1 and isinstance(node.test.ops[0], ast.Eq) and self.constant_value(node.test.comparators[0]) is True:
if node.test.left.id != '__debug__':
return False
if len(node.test.ops) == 1:
op = node.test.ops[0]
comparator_value = self.constant_value(node.test.comparators[0])
if isinstance(op, ast.Is) and comparator_value is True:
return True
if isinstance(op, ast.IsNot) and comparator_value is False:
return True
if isinstance(op, ast.Eq) and comparator_value is True:
return True
return False
if is_simple_debug_check() or is_truthy_debug_comparison():
return True
return False

@@ -44,0 +64,0 @@

@@ -33,3 +33,3 @@ import pytest

add_parent(tree)
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Node has no parent"):
get_parent(tree)

@@ -40,3 +40,3 @@

tree = ast.parse('a = 1')
with pytest.raises(ValueError):
with pytest.raises(ValueError, match="Node has no parent"):
get_parent(tree.body[0])

@@ -43,0 +43,0 @@

@@ -6,2 +6,2 @@ """Pytest configuration and fixtures for python-minifier tests."""

# Tests can explicitly unset this if they need to test size-based behavior
os.environ.setdefault('PYMINIFY_FORCE_BEST_EFFORT', '1')
os.environ.setdefault('PYMINIFY_FORCE_BEST_EFFORT', '1')

@@ -11,20 +11,19 @@ """Subprocess compatibility utilities for Python 2.7/3.x."""

input_bytes = input_data.encode('utf-8') if isinstance(input_data, str) else input_data
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
input=input_bytes, timeout=timeout, env=env)
else:
# Python 2.7, 3.3, 3.4 - no subprocess.run, no timeout support
popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE if input_data else None, env=env)
# For Python 3.3/3.4, communicate() doesn't support timeout
# Also, Python 3.x needs bytes for stdin, Python 2.x needs str
if input_data and sys.version_info[0] >= 3 and isinstance(input_data, str):
input_data = input_data.encode('utf-8')
stdout, stderr = popen.communicate(input_data)
# Create a simple result object similar to subprocess.CompletedProcess
class Result:
def __init__(self, returncode, stdout, stderr):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
return Result(popen.returncode, stdout, stderr)
# Python 2.7, 3.3, 3.4 - no subprocess.run, no timeout support
popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
stdin=subprocess.PIPE if input_data else None, env=env)
# For Python 3.3/3.4, communicate() doesn't support timeout
# Also, Python 3.x needs bytes for stdin, Python 2.x needs str
if input_data and sys.version_info[0] >= 3 and isinstance(input_data, str):
input_data = input_data.encode('utf-8')
stdout, stderr = popen.communicate(input_data)
# Create a simple result object similar to subprocess.CompletedProcess
class Result:
def __init__(self, returncode, stdout, stderr):
self.returncode = returncode
self.stdout = stdout
self.stderr = stderr
return Result(popen.returncode, stdout, stderr)

@@ -39,2 +38,2 @@

return data.decode(encoding, 'replace')
return data
return data

@@ -20,20 +20,20 @@ """Tests for CLI size-based output selection (best effort) functionality."""

'''
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
temp_file = f.name
try:
env = os.environ.copy()
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
result = run_subprocess([
sys.executable, '-m', 'python_minifier', temp_file
], timeout=30, env=env)
assert result.returncode == 0
stdout_text = safe_decode(result.stdout)
assert len(stdout_text) < len(code)
finally:

@@ -46,20 +46,20 @@ os.unlink(temp_file)

code = 'True if 0in x else False'
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
temp_file = f.name
try:
env = os.environ.copy()
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
result = run_subprocess([
sys.executable, '-m', 'python_minifier', temp_file
], timeout=30, env=env)
assert result.returncode == 0
stdout_text = safe_decode(result.stdout)
assert stdout_text == code
finally:

@@ -73,20 +73,20 @@ os.unlink(temp_file)

expected_output = 'True if 0 in x else False'
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
temp_file = f.name
try:
env = os.environ.copy()
env['PYMINIFY_FORCE_BEST_EFFORT'] = '1'
result = run_subprocess([
sys.executable, '-m', 'python_minifier', temp_file
], timeout=30, env=env)
assert result.returncode == 0
stdout_text = safe_decode(result.stdout)
assert stdout_text == expected_output
finally:

@@ -100,22 +100,22 @@ os.unlink(temp_file)

expected_output = 'True if 0 in x else False'
# Without env var - should return original
env = os.environ.copy()
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
result = run_subprocess([
sys.executable, '-m', 'python_minifier', '-'
], input_data=code, timeout=30, env=env)
assert result.returncode == 0
stdout_text = safe_decode(result.stdout)
assert stdout_text == code
# With env var - should return minified
env['PYMINIFY_FORCE_BEST_EFFORT'] = '1'
result = run_subprocess([
sys.executable, '-m', 'python_minifier', '-'
], input_data=code, timeout=30, env=env)
assert result.returncode == 0

@@ -129,14 +129,14 @@ stdout_text = safe_decode(result.stdout)

code = 'True if 0in x else False'
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as input_file:
input_file.write(code)
input_filename = input_file.name
with tempfile.NamedTemporaryFile(delete=False) as output_file:
output_filename = output_file.name
try:
env = os.environ.copy()
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
result = run_subprocess([

@@ -146,10 +146,10 @@ sys.executable, '-m', 'python_minifier',

], timeout=30, env=env)
assert result.returncode == 0
with open(output_filename, 'r') as f:
output_content = f.read()
assert output_content == code
finally:

@@ -163,11 +163,11 @@ os.unlink(input_filename)

code = 'True if 0in x else False'
with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
f.write(code)
temp_file = f.name
try:
env = os.environ.copy()
env.pop('PYMINIFY_FORCE_BEST_EFFORT', None)
result = run_subprocess([

@@ -177,11 +177,11 @@ sys.executable, '-m', 'python_minifier',

], timeout=30, env=env)
assert result.returncode == 0
with open(temp_file, 'r') as f:
modified_content = f.read()
assert modified_content == code
finally:
os.unlink(temp_file)
os.unlink(temp_file)

@@ -9,3 +9,3 @@ import sys

def test_module_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -28,3 +28,3 @@

def test_lambda_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -52,3 +52,3 @@

def test_function_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -90,3 +90,3 @@

def test_generator_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -110,3 +110,3 @@

def test_multi_generator_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -133,3 +133,3 @@

def test_multi_generator_namespace_2():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -159,3 +159,3 @@

def test_nested_generator():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -186,3 +186,3 @@

def test_nested_generator_2():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -214,3 +214,3 @@

def test_setcomp_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -234,3 +234,3 @@

def test_multi_setcomp_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -257,3 +257,3 @@

def test_multi_setcomp_namespace_2():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -283,3 +283,3 @@

def test_nested_setcomp():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -310,3 +310,3 @@

def test_nested_setcomp_2():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -337,3 +337,3 @@

def test_listcomp_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -357,3 +357,3 @@

def test_multi_listcomp_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -380,3 +380,3 @@

def test_multi_listcomp_namespace_2():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -406,3 +406,3 @@

def test_nested_listcomp():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -433,3 +433,3 @@

def test_nested_listcomp_2():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -460,3 +460,3 @@

def test_dictcomp_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -480,3 +480,3 @@

def test_multi_dictcomp_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -503,3 +503,3 @@

def test_multi_dictcomp_namespace_2():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -529,3 +529,3 @@

def test_nested_dictcomp():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -556,3 +556,3 @@

def test_nested_dictcomp_2():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -581,3 +581,3 @@

def test_class_namespace():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -616,3 +616,3 @@

def test_class_name_rebinding():
if sys.version_info < (3, 3) or sys.version_info > (3, 4):
if sys.version_info < (3, 3) or sys.version_info >= (3, 4):
pytest.skip('Test is for python3.3 only')

@@ -619,0 +619,0 @@

@@ -50,4 +50,4 @@ import ast

# statement = '''print(f"This is the playlist: {"\N{BLACK HEART SUIT}".join(songs)}")'''
# assert unparse(ast.parse(statement)) == statement
statement = '''print(f"This is the playlist: {"\N{BLACK HEART SUIT}".join(songs)}")'''
assert unparse(ast.parse(statement)) == statement

@@ -82,5 +82,5 @@ statement = '''f"Magic wand: {bag["wand"]}"'''

# statement = '''f"{"":*^{1:{1:{1}}}}"'''
# assert unparse(ast.parse(statement)) == statement
# SyntaxError: f-string: expressions nested too deeply
# Verify that Python raises SyntaxError for f-strings nested too deeply
with pytest.raises(SyntaxError, match="expressions nested too deeply"):
ast.parse('''f"{"":*^{1:{1:{1}}}}"''')

@@ -87,0 +87,0 @@ statement = '''f"___{

@@ -11,3 +11,3 @@ import sys

@pytest.mark.filterwarnings("ignore:ast.Str is deprecated:DeprecationWarning")
@pytest.mark.filterwarnings("ignore:ast.Bytes is deprecated:DeprecationWarning")
@pytest.mark.filterwarnings("ignore:ast.Bytes is deprecated:DeprecationWarning")
@pytest.mark.filterwarnings("ignore:ast.Num is deprecated:DeprecationWarning")

@@ -40,3 +40,3 @@ @pytest.mark.filterwarnings("ignore:ast.NameConstant is deprecated:DeprecationWarning")

@pytest.mark.filterwarnings("ignore:ast.Str is deprecated:DeprecationWarning")
@pytest.mark.filterwarnings("ignore:ast.Bytes is deprecated:DeprecationWarning")
@pytest.mark.filterwarnings("ignore:ast.Bytes is deprecated:DeprecationWarning")
@pytest.mark.filterwarnings("ignore:ast.Num is deprecated:DeprecationWarning")

@@ -66,5 +66,5 @@ @pytest.mark.filterwarnings("ignore:ast.NameConstant is deprecated:DeprecationWarning")

pytest.skip('ast_compat types test only for Python 3.14+')
import python_minifier.ast_compat as ast_compat
# Test that ast_compat provides the removed types

@@ -85,5 +85,5 @@ assert is_constant_node(ast_compat.Str('a'), ast_compat.Str)

pytest.skip('ast_compat constant test only for Python 3.14+')
import python_minifier.ast_compat as ast_compat
# Test that Constant nodes work with ast_compat types

@@ -90,0 +90,0 @@ assert is_constant_node(ast.Constant('a'), ast_compat.Str)

@@ -14,3 +14,3 @@ """

@pytest.mark.parametrize('source,description', [
@pytest.mark.parametrize(('source', 'description'), [
# Raw f-string backslash tests - core regression fix

@@ -56,3 +56,3 @@ pytest.param(r'rf"{x:\\xFF}"', 'Single backslash in format spec (minimal failing case)', id='raw-fstring-backslash-format-spec'),

@pytest.mark.parametrize('source,description', [
@pytest.mark.parametrize(('source', 'description'), [
pytest.param(r'f"{f"\\n{x}\\t"}"', 'Nested f-strings with backslashes in inner string parts', id='nested-fstring-backslashes'),

@@ -59,0 +59,0 @@ pytest.param(r'f"{rf"\\xFF{y}\\n"}"', 'Nested raw f-strings with backslashes', id='nested-raw-fstring-backslashes'),

@@ -19,3 +19,3 @@ """

@pytest.mark.parametrize('source,description', [
@pytest.mark.parametrize(('source', 'description'), [
# Raw t-string backslash tests - core regression testing

@@ -75,3 +75,3 @@ pytest.param(r'rt"{x:\\xFF}"', 'Single backslash in format spec (minimal case)', id='raw-tstring-backslash-format-spec'),

@pytest.mark.parametrize('source,description', [
@pytest.mark.parametrize(('source', 'description'), [
pytest.param(r't"{t"\\n{x}\\t"}"', 'Nested t-strings with backslashes in inner string parts', id='nested-tstring-backslashes'),

@@ -100,2 +100,2 @@ pytest.param(r't"{rt"\\xFF{y}\\n"}"', 'Nested raw t-strings with backslashes', id='nested-raw-tstring-backslashes'),

actual_code = unparse(expected_ast)
compare_ast(expected_ast, ast.parse(actual_code))
compare_ast(expected_ast, ast.parse(actual_code))

@@ -170,1 +170,59 @@ import ast

compare_ast(expected_ast, actual_ast)
@pytest.mark.parametrize(
'condition', [
'__sandwich__',
'__sandwich__ is True',
'__sandwich__ is False',
'__sandwich__ is not False',
'__sandwich__ == True',
'__sandwich__ == __debug__',
'__sandwich() == True',
'func() is True',
'some_call(a, b) is True',
'obj.method() is True',
'obj.attr is True',
'True is something',
'True == something',
]
)
def test_no_remove_not_debug(condition):
source = '''
value = 10
# Not a __debug__ test
if ''' + condition + ''':
value += 1
print(value)
'''
expected = source
expected_ast = ast.parse(expected)
actual_ast = remove_debug(source)
compare_ast(expected_ast, actual_ast)
def test_no_remove_is_true_in_elif_chain():
"""Regression test for issue #142 - if/elif/else with 'is True' comparisons"""
source = '''
def check_is_internet_working(c):
url, url_hostname = get_url_and_url_hostname(c)
if is_internet_working_socket_test(c, url_hostname) is True:
c.is_internet_connected = True
elif is_internet_working_urllib_open(c, url) is True:
c.is_internet_connected = True
else:
c.is_internet_connected = False
return c.is_internet_connected
'''
expected = source
expected_ast = ast.parse(expected)
actual_ast = remove_debug(source)
compare_ast(expected_ast, actual_ast)

@@ -70,3 +70,3 @@ import ast

]
for statement in statements:

@@ -105,3 +105,3 @@ # Just test that it parses and round-trips correctly, don't care about exact quote style

]
for input_statement, expected_output in test_cases:

@@ -124,3 +124,3 @@ assert unparse(ast.parse(input_statement)) == expected_output

]
for input_statement, expected_output in test_cases:

@@ -155,3 +155,3 @@ assert unparse(ast.parse(input_statement)) == expected_output

]
for statement in statements:

@@ -180,10 +180,10 @@ # Test that it parses and round-trips correctly

fstring = 'f"Hello {name}"'
t_ast = ast.parse(tstring)
f_ast = ast.parse(fstring)
# Should be different node types in the expression
assert type(t_ast.body[0].value).__name__ == 'TemplateStr'
assert type(f_ast.body[0].value).__name__ == 'JoinedStr'
# But should unparse correctly

@@ -203,18 +203,18 @@ assert unparse(t_ast) == tstring

]
for statement in raw_statements:
# Raw t-strings should parse successfully
ast.parse(statement)
# Test that raw behavior is preserved in the AST even if prefix is lost
raw_backslash = 'rt"backslash \\\\n and {name}"'
regular_backslash = 't"backslash \\n and {name}"' # Only two backslashes for regular
raw_ast = ast.parse(raw_backslash)
regular_ast = ast.parse(regular_backslash)
# The AST should show different string content
raw_content = raw_ast.body[0].value.values[0].value
regular_content = regular_ast.body[0].value.values[0].value
# Raw should have literal backslash-n, regular should have actual newline

@@ -232,3 +232,3 @@ assert '\\\\n' in raw_content # literal backslash-n (two chars: \ and n)

assert unparse(ast.parse('t"{value=:.2f}"')) == 't"{value=:.2f}"'
# But are lost when there's a preceding literal (same limitation as f-strings)

@@ -238,3 +238,3 @@ assert unparse(ast.parse('t"Hello {name=}"')) == 't"Hello name={name!r}"'

assert unparse(ast.parse('t"Hello {name=:.2f}"')) == 't"Hello name={name:.2f}"'
# This matches f-string behavior exactly

@@ -253,3 +253,3 @@ assert unparse(ast.parse('f"Hello {name=}"')) == 'f"Hello name={name!r}"'

]
for case in complex_cases:

@@ -263,2 +263,2 @@ try:

except Exception as e:
pytest.fail("Failed to handle complex case {}: {}".format(case, e))
pytest.fail("Failed to handle complex case {}: {}".format(case, e))

@@ -34,2 +34,2 @@ import ast

minified = unparse(expected_ast)
compare_ast(expected_ast, ast.parse(minified))
compare_ast(expected_ast, ast.parse(minified))

@@ -32,3 +32,3 @@ # -*- coding: utf-8 -*-

u"left": u"←",
u"right": u"→",
u"right": u"→",
u"up": u"↑",

@@ -97,3 +97,3 @@ u"down": u"↓"

self.message = u"Héllö Wörld with àccénts!"
def get_symbols(self):

@@ -100,0 +100,0 @@ return u"Symbols: ™ © ® ° ± × ÷ ≠ ≤ ≥"