diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c31121..3a89bec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 @@ -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 diff --git a/src/python_minifier/transforms/remove_debug.py b/src/python_minifier/transforms/remove_debug.py index 0d37b02..6d90e29 100644 --- a/src/python_minifier/transforms/remove_debug.py +++ b/src/python_minifier/transforms/remove_debug.py @@ -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): diff --git a/test/test_remove_debug.py b/test/test_remove_debug.py index c02d8e9..9496c75 100644 --- a/test/test_remove_debug.py +++ b/test/test_remove_debug.py @@ -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)