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
16 changes: 16 additions & 0 deletions .config/.coveragerc
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
[run]
branch = true
parallel = true
relative_files = true
context = ${COVERAGE_CONTEXT}

[report]
exclude_lines =
pragma: no cover
raise NotImplementedError
if TYPE_CHECKING:

[paths]
source =
src/python_minifier
*/site-packages/python_minifier
14 changes: 14 additions & 0 deletions .config/.coveragerc-legacy
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[run]
branch = true
parallel = true

[report]
exclude_lines =
pragma: no cover
raise NotImplementedError
if TYPE_CHECKING:

[paths]
source =
src/python_minifier
*/site-packages/python_minifier
59 changes: 59 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,14 @@ jobs:
run: |
tox -r -e $(echo "${{ matrix.python }}" | tr -d .)

- name: Upload coverage
if: ${{ matrix.python != 'python3.3' && matrix.python != 'python3.4' }}
uses: actions/upload-artifact@v4
with:
name: coverage-${{ matrix.python }}
path: .coverage.*
include-hidden-files: true

test-windows:
name: Test Windows
runs-on: windows-2025
Expand Down Expand Up @@ -155,3 +163,54 @@ jobs:
with:
dockerfile: ./docker/${{ matrix.dockerfile }}
config: .config/hadolint.yaml

coverage-report:
name: Coverage Report
runs-on: ubuntu-24.04
needs: test
steps:
- name: Checkout
uses: actions/checkout@v4.2.2
with:
fetch-depth: 1
show-progress: false
persist-credentials: false

- name: Download coverage artifacts
uses: actions/download-artifact@v4
with:
pattern: coverage-*
merge-multiple: true

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.14'

- name: Install coverage
run: pip install coverage

- name: Combine coverage
run: |
ls -la .coverage*
coverage combine --rcfile=.config/.coveragerc
ls -la .coverage*

- name: Upload combined coverage
uses: actions/upload-artifact@v4
with:
name: coverage-combined
path: .coverage
include-hidden-files: true

- name: Generate report
run: |
coverage report --rcfile=.config/.coveragerc --format=markdown 2>&1 | tee coverage-report.md
cat coverage-report.md >> "$GITHUB_STEP_SUMMARY"
coverage html --rcfile=.config/.coveragerc

- name: Upload HTML report
uses: actions/upload-artifact@v4
with:
name: coverage-html-report
path: htmlcov/
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,4 @@ docs/source/transforms/*.min.py
.circleci-config.yml
.coverage
.mypy_cache/
.tox/
20 changes: 19 additions & 1 deletion hypo_test/folding.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,29 @@ def BinOp(draw, expression) -> ast.BinOp:
return ast.BinOp(le[0], op, le[1])


@composite
def UnaryOp(draw, expression) -> ast.UnaryOp:
op = draw(
sampled_from(
[
ast.USub(), # Unary minus: -x
ast.UAdd(), # Unary plus: +x
ast.Invert(), # Bitwise not: ~x
ast.Not(), # Logical not: not x
]
)
)

operand = draw(expression)

return ast.UnaryOp(op, operand)


def expression() -> SearchStrategy:
return recursive(
leaves,
lambda expression:
BinOp(expression),
one_of(BinOp(expression), UnaryOp(expression)),
max_leaves=150
)

Expand Down
81 changes: 59 additions & 22 deletions src/python_minifier/transforms/constant_folding.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,36 +10,35 @@
from python_minifier.util import is_constant_node


class FoldConstants(SuiteTransformer):
"""
Fold Constants if it would reduce the size of the source
def is_foldable_constant(node):
"""
Check if a node is a constant expression that can participate in folding.

def __init__(self):
super(FoldConstants, self).__init__()
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

def visit_BinOp(self, node):
if isinstance(node, ast.UnaryOp):
if isinstance(node.op, (ast.USub, ast.Invert)):
return is_constant_node(node.operand, ast.Num)

node.left = self.visit(node.left)
node.right = self.visit(node.right)
return False

# 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
class FoldConstants(SuiteTransformer):
"""
Fold Constants if it would reduce the size of the source
"""

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 __init__(self):
super(FoldConstants, self).__init__()

def fold(self, node):
# Evaluate the expression
try:
original_expression = unparse_expression(node)
Expand Down Expand Up @@ -96,6 +95,44 @@ def visit_BinOp(self, node):
# New representation is shorter and has the same value, so use it
return self.add_child(new_node, get_parent(node), node.namespace)

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):
if type(a) != type(b):
Expand Down
Loading