Skip to content

Commit 573a762

Browse files
committed
Update pyminify to return the input source unchanged if it can't be made smaller
1 parent 515b0fd commit 573a762

File tree

6 files changed

+294
-60
lines changed

6 files changed

+294
-60
lines changed

.github/actions/run-in-container/action.yml

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,9 @@ runs:
2020
- name: Run command
2121
env:
2222
INPUT_VOLUMES: ${{ inputs.volumes }}
23-
INPUT_IMAGE: ${{ inputs.image }}
24-
INPUT_RUN: ${{ inputs.run }}
25-
run: |
23+
run: | # zizmor: ignore[template-injection]
2624
27-
docker pull --quiet ${INPUT_IMAGE}
25+
docker pull --quiet ${{ inputs.image }}
2826
2927
function run() {
3028
docker run --rm \
@@ -83,9 +81,10 @@ runs:
8381
-e GITHUB_TRIGGERING_ACTOR \
8482
-e GITHUB_WORKFLOW_REF \
8583
-e GITHUB_WORKFLOW_SHA \
84+
-e PYTHONUNBUFFERED=1 \
8685
$VOLUMES_ARGS \
8786
--entrypoint /bin/bash \
88-
${INPUT_IMAGE} \
87+
${{ inputs.image }} \
8988
--noprofile --norc -eo pipefail /run.sh
9089
9190
}
@@ -113,7 +112,7 @@ runs:
113112
fi
114113
115114
cat <<"EOF" >"$RUNNER_TEMP/run.sh"
116-
${INPUT_RUN}
115+
${{ inputs.run }}
117116
EOF
118117
119118
set -x

src/python_minifier/__main__.py

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,26 +7,17 @@
77
from python_minifier import minify
88
from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions
99

10-
# Python 2.7 compatibility for UTF-8 file writing
11-
if sys.version_info[0] == 2:
12-
import codecs
13-
def open_utf8(filename, mode):
14-
return codecs.open(filename, mode, encoding='utf-8')
15-
else:
16-
def open_utf8(filename, mode):
17-
return open(filename, mode, encoding='utf-8')
1810

19-
def safe_stdout_write(text):
20-
"""Write text to stdout with proper encoding handling."""
21-
try:
22-
sys.stdout.write(text)
23-
except UnicodeEncodeError:
24-
# Fallback: encode to UTF-8 and write to stdout.buffer (Python 3) or sys.stdout (Python 2)
25-
if sys.version_info[0] >= 3 and hasattr(sys.stdout, 'buffer'):
26-
sys.stdout.buffer.write(text.encode('utf-8'))
27-
else:
28-
# Python 2.7 or no buffer attribute - write UTF-8 encoded bytes
29-
sys.stdout.write(text.encode('utf-8'))
11+
class MinificationNotBeneficialError(Exception):
12+
"""Raised when minification results in larger output than the original."""
13+
pass
14+
15+
def stdout_write_bytes(data):
16+
"""Write bytes to stdout with proper Python 2.7/3.x compatibility."""
17+
if sys.version_info >= (3, 0):
18+
sys.stdout.buffer.write(data)
19+
else:
20+
sys.stdout.write(data)
3021

3122

3223
if sys.version_info >= (3, 8):
@@ -72,12 +63,23 @@ def main():
7263
if len(args.path) == 1 and args.path[0] == '-':
7364
# minify stdin
7465
source = sys.stdin.buffer.read() if sys.version_info >= (3, 0) else sys.stdin.read()
75-
minified = do_minify(source, 'stdin', args)
66+
try:
67+
minified = do_minify(source, 'stdin', args)
68+
except MinificationNotBeneficialError:
69+
# Use original source when minification isn't beneficial
70+
if args.output:
71+
with open(args.output, 'wb') as f:
72+
f.write(source)
73+
else:
74+
# Write original source to stdout
75+
stdout_write_bytes(source)
76+
return
77+
7678
if args.output:
77-
with open_utf8(args.output, 'w') as f:
79+
with open(args.output, 'wb') as f:
7880
f.write(minified)
7981
else:
80-
safe_stdout_write(minified)
82+
stdout_write_bytes(minified)
8183

8284
else:
8385
# minify source paths
@@ -88,16 +90,30 @@ def main():
8890
with open(path, 'rb') as f:
8991
source = f.read()
9092

91-
minified = do_minify(source, path, args)
93+
try:
94+
minified = do_minify(source, path, args)
95+
except MinificationNotBeneficialError:
96+
# Use original source when minification isn't beneficial
97+
if args.in_place:
98+
# File is already the original, no need to write
99+
pass
100+
elif args.output:
101+
# Write original source to output
102+
with open(args.output, 'wb') as f:
103+
f.write(source)
104+
else:
105+
# Write original source to stdout
106+
stdout_write_bytes(source)
107+
continue
92108

93109
if args.in_place:
94-
with open_utf8(path, 'w') as f:
110+
with open(path, 'wb') as f:
95111
f.write(minified)
96112
elif args.output:
97-
with open_utf8(args.output, 'w') as f:
113+
with open(args.output, 'wb') as f:
98114
f.write(minified)
99115
else:
100-
safe_stdout_write(minified)
116+
stdout_write_bytes(minified)
101117

102118

103119
def parse_args():
@@ -301,6 +317,15 @@ def error(os_error):
301317

302318

303319
def do_minify(source, filename, minification_args):
320+
"""Minify Python source code with size-based fallback.
321+
322+
:param bytes source: Source code as bytes (from file 'rb' or stdin.buffer)
323+
:param str filename: Filename for error reporting
324+
:param argparse.Namespace minification_args: CLI arguments for minification options
325+
:returns: Minified source code as UTF-8 bytes
326+
:rtype: bytes
327+
:raises MinificationNotBeneficialError: When minified output is larger than original
328+
"""
304329

305330
preserve_globals = []
306331
if minification_args.preserve_globals:
@@ -329,7 +354,7 @@ def do_minify(source, filename, minification_args):
329354
remove_class_attribute_annotations=minification_args.remove_class_attribute_annotations,
330355
)
331356

332-
return minify(
357+
minified_result = minify(
333358
source,
334359
filename=filename,
335360
combine_imports=minification_args.combine_imports,
@@ -351,6 +376,19 @@ def do_minify(source, filename, minification_args):
351376
constant_folding=minification_args.constant_folding
352377
)
353378

379+
# Encode minified result to bytes for comparison and output
380+
minified_bytes = minified_result.encode('utf-8')
381+
382+
# Check if environment variable forces minified output
383+
if os.environ.get('PYMINIFY_FORCE_BEST_EFFORT'):
384+
return minified_bytes
385+
386+
# Compare byte lengths for accurate size comparison
387+
if len(minified_bytes) > len(source):
388+
raise MinificationNotBeneficialError("Minified output is longer than original")
389+
390+
return minified_bytes
391+
354392

355393
if __name__ == '__main__':
356394
main()

test/conftest.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Pytest configuration and fixtures for python-minifier tests."""
2+
import os
3+
4+
# Set default environment variable to preserve existing test behavior
5+
# Tests can explicitly unset this if they need to test size-based behavior
6+
os.environ.setdefault('PYMINIFY_FORCE_BEST_EFFORT', '1')

test/subprocess_compat.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
"""Subprocess compatibility utilities for Python 2.7/3.x."""
2+
import subprocess
3+
import sys
4+
5+
6+
def run_subprocess(cmd, timeout=None, input_data=None, env=None):
7+
"""Cross-platform subprocess runner for Python 2.7+ compatibility."""
8+
if hasattr(subprocess, 'run'):
9+
# Python 3.5+ - encode string input to bytes for subprocess
10+
input_bytes = input_data.encode('utf-8') if isinstance(input_data, str) else input_data
11+
return subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
12+
input=input_bytes, timeout=timeout, env=env)
13+
else:
14+
# Python 2.7, 3.3, 3.4 - no subprocess.run, no timeout support
15+
popen = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
16+
stdin=subprocess.PIPE if input_data else None, env=env)
17+
# For Python 3.3/3.4, communicate() doesn't support timeout
18+
# Also, Python 3.x needs bytes for stdin, Python 2.x needs str
19+
if input_data and sys.version_info[0] >= 3 and isinstance(input_data, str):
20+
input_data = input_data.encode('utf-8')
21+
stdout, stderr = popen.communicate(input_data)
22+
# Create a simple result object similar to subprocess.CompletedProcess
23+
class Result:
24+
def __init__(self, returncode, stdout, stderr):
25+
self.returncode = returncode
26+
self.stdout = stdout
27+
self.stderr = stderr
28+
return Result(popen.returncode, stdout, stderr)
29+
30+
31+
def safe_decode(data, encoding='utf-8', errors='replace'):
32+
"""Safe decode for Python 2.7/3.x compatibility."""
33+
if isinstance(data, bytes):
34+
try:
35+
return data.decode(encoding, errors)
36+
except UnicodeDecodeError:
37+
return data.decode(encoding, 'replace')
38+
return data

0 commit comments

Comments
 (0)