Skip to content

Commit a0eae6e

Browse files
authored
Merge pull request #144 from dflook/coverage-2
Fold UnaryOp nodes
2 parents 2d066ae + a14a779 commit a0eae6e

34 files changed

+914
-338
lines changed

.config/.coveragerc

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
[run]
2+
branch = true
3+
parallel = true
4+
relative_files = true
5+
context = ${COVERAGE_CONTEXT}
6+
7+
[report]
8+
exclude_lines =
9+
pragma: no cover
10+
raise NotImplementedError
11+
if TYPE_CHECKING:
12+
13+
[paths]
14+
source =
15+
src/python_minifier
16+
*/site-packages/python_minifier

.config/.coveragerc-legacy

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
[run]
2+
branch = true
3+
parallel = true
4+
5+
[report]
6+
exclude_lines =
7+
pragma: no cover
8+
raise NotImplementedError
9+
if TYPE_CHECKING:
10+
11+
[paths]
12+
source =
13+
src/python_minifier
14+
*/site-packages/python_minifier

.github/workflows/test.yaml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,14 @@ jobs:
3434
run: |
3535
tox -r -e $(echo "${{ matrix.python }}" | tr -d .)
3636
37+
- name: Upload coverage
38+
if: ${{ matrix.python != 'python3.3' && matrix.python != 'python3.4' }}
39+
uses: actions/upload-artifact@v4
40+
with:
41+
name: coverage-${{ matrix.python }}
42+
path: .coverage.*
43+
include-hidden-files: true
44+
3745
test-windows:
3846
name: Test Windows
3947
runs-on: windows-2025
@@ -155,3 +163,54 @@ jobs:
155163
with:
156164
dockerfile: ./docker/${{ matrix.dockerfile }}
157165
config: .config/hadolint.yaml
166+
167+
coverage-report:
168+
name: Coverage Report
169+
runs-on: ubuntu-24.04
170+
needs: test
171+
steps:
172+
- name: Checkout
173+
uses: actions/[email protected]
174+
with:
175+
fetch-depth: 1
176+
show-progress: false
177+
persist-credentials: false
178+
179+
- name: Download coverage artifacts
180+
uses: actions/download-artifact@v4
181+
with:
182+
pattern: coverage-*
183+
merge-multiple: true
184+
185+
- name: Set up Python
186+
uses: actions/setup-python@v5
187+
with:
188+
python-version: '3.14'
189+
190+
- name: Install coverage
191+
run: pip install coverage
192+
193+
- name: Combine coverage
194+
run: |
195+
ls -la .coverage*
196+
coverage combine --rcfile=.config/.coveragerc
197+
ls -la .coverage*
198+
199+
- name: Upload combined coverage
200+
uses: actions/upload-artifact@v4
201+
with:
202+
name: coverage-combined
203+
path: .coverage
204+
include-hidden-files: true
205+
206+
- name: Generate report
207+
run: |
208+
coverage report --rcfile=.config/.coveragerc --format=markdown 2>&1 | tee coverage-report.md
209+
cat coverage-report.md >> "$GITHUB_STEP_SUMMARY"
210+
coverage html --rcfile=.config/.coveragerc
211+
212+
- name: Upload HTML report
213+
uses: actions/upload-artifact@v4
214+
with:
215+
name: coverage-html-report
216+
path: htmlcov/

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@ docs/source/transforms/*.min.py
1818
.circleci-config.yml
1919
.coverage
2020
.mypy_cache/
21+
.tox/

hypo_test/folding.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,29 @@ def BinOp(draw, expression) -> ast.BinOp:
3737
return ast.BinOp(le[0], op, le[1])
3838

3939

40+
@composite
41+
def UnaryOp(draw, expression) -> ast.UnaryOp:
42+
op = draw(
43+
sampled_from(
44+
[
45+
ast.USub(), # Unary minus: -x
46+
ast.UAdd(), # Unary plus: +x
47+
ast.Invert(), # Bitwise not: ~x
48+
ast.Not(), # Logical not: not x
49+
]
50+
)
51+
)
52+
53+
operand = draw(expression)
54+
55+
return ast.UnaryOp(op, operand)
56+
57+
4058
def expression() -> SearchStrategy:
4159
return recursive(
4260
leaves,
4361
lambda expression:
44-
BinOp(expression),
62+
one_of(BinOp(expression), UnaryOp(expression)),
4563
max_leaves=150
4664
)
4765

src/python_minifier/transforms/constant_folding.py

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,36 +10,35 @@
1010
from python_minifier.util import is_constant_node
1111

1212

13-
class FoldConstants(SuiteTransformer):
14-
"""
15-
Fold Constants if it would reduce the size of the source
13+
def is_foldable_constant(node):
1614
"""
15+
Check if a node is a constant expression that can participate in folding.
1716
18-
def __init__(self):
19-
super(FoldConstants, self).__init__()
17+
We can asume that children have already been folded, so foldable constants are either:
18+
- Simple literals (Num, NameConstant)
19+
- UnaryOp(USub/Invert) on a Num - these don't fold to shorter forms,
20+
so they remain after child visiting. UAdd and Not would have been
21+
folded away since they always produce shorter results.
22+
"""
23+
if is_constant_node(node, (ast.Num, ast.NameConstant)):
24+
return True
2025

21-
def visit_BinOp(self, node):
26+
if isinstance(node, ast.UnaryOp):
27+
if isinstance(node.op, (ast.USub, ast.Invert)):
28+
return is_constant_node(node.operand, ast.Num)
2229

23-
node.left = self.visit(node.left)
24-
node.right = self.visit(node.right)
30+
return False
2531

26-
# Check this is a constant expression that could be folded
27-
# 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
28-
if not is_constant_node(node.left, (ast.Num, ast.NameConstant)):
29-
return node
30-
if not is_constant_node(node.right, (ast.Num, ast.NameConstant)):
31-
return node
3232

33-
if isinstance(node.op, ast.Div):
34-
# Folding div is subtle, since it can have different results in Python 2 and Python 3
35-
# Do this once target version options have been implemented
36-
return node
33+
class FoldConstants(SuiteTransformer):
34+
"""
35+
Fold Constants if it would reduce the size of the source
36+
"""
3737

38-
if isinstance(node.op, ast.Pow):
39-
# This can be folded, but it is unlikely to reduce the size of the source
40-
# It can also be slow to evaluate
41-
return node
38+
def __init__(self):
39+
super(FoldConstants, self).__init__()
4240

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

98+
def visit_BinOp(self, node):
99+
100+
node.left = self.visit(node.left)
101+
node.right = self.visit(node.right)
102+
103+
# Check this is a constant expression that could be folded
104+
# 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
105+
if not is_foldable_constant(node.left):
106+
return node
107+
if not is_foldable_constant(node.right):
108+
return node
109+
110+
if isinstance(node.op, ast.Div):
111+
# Folding div is subtle, since it can have different results in Python 2 and Python 3
112+
# Do this once target version options have been implemented
113+
return node
114+
115+
if isinstance(node.op, ast.Pow):
116+
# This can be folded, but it is unlikely to reduce the size of the source
117+
# It can also be slow to evaluate
118+
return node
119+
120+
return self.fold(node)
121+
122+
def visit_UnaryOp(self, node):
123+
124+
node.operand = self.visit(node.operand)
125+
126+
# Only fold if the operand is a foldable constant
127+
if not is_foldable_constant(node.operand):
128+
return node
129+
130+
# Only fold these unary operators
131+
if not isinstance(node.op, (ast.USub, ast.UAdd, ast.Invert, ast.Not)):
132+
return node
133+
134+
return self.fold(node)
135+
99136

100137
def equal_value_and_type(a, b):
101138
if type(a) != type(b):

0 commit comments

Comments
 (0)