Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ and will output source code compatible with the version of the interpreter it is
This means that if you minify code written for Python 3.11 using python-minifier running with Python 3.12,
the minified code may only run with Python 3.12.

## [3.1.1] - 2025-12-11

### Fixed
- The remove debug transform could incorrectly remove if branches that did not test `__debug__`

## [3.1.0] - 2025-10-10

### Added
Expand Down Expand Up @@ -306,6 +311,7 @@ the minified code may only run with Python 3.12.
- python-minifier package
- pyminify command

[3.1.1]: https://github.com/dflook/python-minifier/compare/3.1.0...3.1.1
[3.1.0]: https://github.com/dflook/python-minifier/compare/3.0.0...3.1.0
[3.0.0]: https://github.com/dflook/python-minifier/compare/2.11.3...3.0.0
[2.11.3]: https://github.com/dflook/python-minifier/compare/2.11.2...2.11.3
Expand Down
36 changes: 28 additions & 8 deletions src/python_minifier/transforms/remove_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,38 @@ def can_remove(self, node):
if not isinstance(node, ast.If):
return False

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:
return 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

def suite(self, node_list, parent):
Expand Down
58 changes: 58 additions & 0 deletions test/test_remove_debug.py
Original file line number Diff line number Diff line change
Expand Up @@ -167,3 +167,61 @@ def test_no_remove_falsy_debug(condition):
expected_ast = ast.parse(expected)
actual_ast = remove_debug(source)
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)