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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ 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.2.0] - 2025-12-31

### Added
- New `--prefer-single-line` option to use semicolons instead of newlines between top-level statements when there is no difference in output size. This doesn't make the output any smaller, but may be preferred.
- Constant folding can now fold unary operators (e.g. `-1`, `not True`, `~0`) in addition to binary operators.

## [3.1.1] - 2025-12-11

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

[3.2.0]: https://github.com/dflook/python-minifier/compare/3.1.1...3.2.0
[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
Expand Down
9 changes: 5 additions & 4 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,17 @@ def create_example(option):
'rename_globals': False,
'rename_locals': False,
'remove_object_base': False,
'convert_posargs_to_args': False
'convert_posargs_to_args': False,
'prefer_single_line': False
}

options[option] = True

with open(f'transforms/{option}.py') as source:
with open(f'transforms/{option}.min.py', 'w') as minified:
with open(f'minification_options/{option}.py') as source:
with open(f'minification_options/{option}.min.py', 'w') as minified:
minified.write(minify(source.read(), filename=f'{option}.py', **options))

for file in os.listdir('transforms'):
for file in os.listdir('minification_options'):
if file.endswith('.py') and not file.endswith('.min.py'):
create_example(file[:-len('.py')])

Expand Down
2 changes: 1 addition & 1 deletion docs/source/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ This package transforms python source code into a 'minified' representation of t
installation
command_usage
api_usage
transforms/index
minification_options/index



Expand Down
3 changes: 3 additions & 0 deletions docs/source/minification_options/combine_imports.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import requests,collections
from typing import Dict,List,Optional
import sys,os
2 changes: 2 additions & 0 deletions docs/source/minification_options/constant_folding.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
SECONDS_IN_A_DAY=86400
SECONDS_IN_A_WEEK=SECONDS_IN_A_DAY*7
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def name(p1,p2,p_or_kw,*,kw):pass
def name(p1,p2=None,p_or_kw=None,*,kw):pass
def name(p1,p2=None,*,kw):pass
def name(p1,p2=None):pass
def name(p1,p2,p_or_kw):pass
def name(p1,p2):pass
12 changes: 12 additions & 0 deletions docs/source/minification_options/hoist_literals.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
def validate(arn,props):
H='Value';G='Type';F='Name';E='ValidationStatus';D='PENDING_VALIDATION';C=False;B='ValidationMethod';A='ResourceRecord'
if B in props and props[B]=='DNS':
all_records_created=C
while not all_records_created:
all_records_created=True;certificate=acm.describe_certificate(CertificateArn=arn)['Certificate']
if certificate['Status']!=D:return
for v in certificate['DomainValidationOptions']:
if E not in v or A not in v:all_records_created=C;continue
records=[]
if v[E]==D:records.append({'Action':'UPSERT','ResourceRecordSet':{F:v[A][F],G:v[A][G],'TTL':60,'ResourceRecords':[{H:v[A][H]}]}})
if records:response=boto3.client('route53').change_resource_record_sets(HostedZoneId=get_zone_for(v['DomainName'],props),ChangeBatch={'Comment':'Domain validation for %s'%arn,'Changes':records})
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,4 @@ They can be enabled or disabled through the minify function, or passing options
rename_globals
remove_asserts
remove_debug
prefer_single_line
1 change: 1 addition & 0 deletions docs/source/minification_options/prefer_single_line.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import os;import sys;name='world';print('Hello, '+name)
5 changes: 5 additions & 0 deletions docs/source/minification_options/prefer_single_line.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os
import sys

name = "world"
print("Hello, " + name)
31 changes: 31 additions & 0 deletions docs/source/minification_options/prefer_single_line.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Prefer Single Line
==================

When minifying, statements at module level are separated by either newlines or semicolons. Both take the same number of bytes, so this option controls which is preferred.

When enabled, semicolons are used to keep multiple statements on a single line. When disabled (the default), newlines are used for slightly better readability of the minified output.

This option has no effect on the size of the output.

This option is disabled by default. Enable by passing the ``prefer_single_line=True`` argument to the :func:`python_minifier.minify` function,
or passing ``--prefer-single-line`` to the pyminify command.

Example
-------

Input
~~~~~

.. literalinclude:: prefer_single_line.py

Output with ``--prefer-single-line``
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. literalinclude:: prefer_single_line.min.py
:language: python

Output without ``--prefer-single-line`` (default)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. literalinclude:: prefer_single_line_false.min.py
:language: python
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import os,sys
name="world"
print("Hello, "+name)
3 changes: 3 additions & 0 deletions docs/source/minification_options/preserve_shebang.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#!/usr/bin/python
import sys
print(sys.executable)
3 changes: 3 additions & 0 deletions docs/source/minification_options/remove_annotations.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class A:
b:0;c=2
def a(self,val):b:0;c=2
2 changes: 2 additions & 0 deletions docs/source/minification_options/remove_asserts.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
word='hello'
print(word)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
class MyBaseClass:
def override_me(self):raise NotImplementedError
6 changes: 6 additions & 0 deletions docs/source/minification_options/remove_debug.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
value=10
if not __debug__:value+=1
if __debug__ is False:value+=1
if __debug__ is not True:value+=1
if __debug__==False:value+=1
print(value)
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
def important(a):
if a>3:return a
if a<2:return
a.adjust(1)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test():0
1 change: 1 addition & 0 deletions docs/source/minification_options/remove_object_base.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
class MyClass:pass
1 change: 1 addition & 0 deletions docs/source/minification_options/remove_pass.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
def test():0
5 changes: 5 additions & 0 deletions docs/source/minification_options/rename_globals.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
A=print
import collections as B
C=B.Counter([True,True,True,False,False])
A('Contents:')
A(list(C))
6 changes: 6 additions & 0 deletions docs/source/minification_options/rename_locals.min.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
def rename_locals_example(module,another_argument=False,third_argument=None):
B=module;A=third_argument
if A is None:A=[]
A.extend(B)
for C in B.things:
if another_argument is False or C.name in A:C.my_method()
11 changes: 7 additions & 4 deletions src/python_minifier/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,8 @@ def minify(
remove_debug=False,
remove_explicit_return_none=True,
remove_builtin_exception_brackets=True,
constant_folding=True
constant_folding=True,
prefer_single_line=False,
):
"""
Minify a python module
Expand Down Expand Up @@ -107,6 +108,7 @@ def minify(
:param bool remove_explicit_return_none: If explicit return None statements should be replaced with a bare return
:param bool remove_builtin_exception_brackets: If brackets should be removed when raising exceptions with no arguments
:param bool constant_folding: If literal expressions should be evaluated
:param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size

:rtype: str

Expand Down Expand Up @@ -192,7 +194,7 @@ def minify(
if convert_posargs_to_args:
module = remove_posargs(module)

minified = unparse(module)
minified = unparse(module, prefer_single_line=prefer_single_line)

if preserve_shebang is True:
shebang_line = _find_shebang(source)
Expand All @@ -219,7 +221,7 @@ def _find_shebang(source):
return None


def unparse(module):
def unparse(module, prefer_single_line=False):
"""
Turn a module AST into python code

Expand All @@ -228,13 +230,14 @@ def unparse(module):

:param module: The module to turn into python code
:type: module: :class:`ast.Module`
:param bool prefer_single_line: If semi-colons should be preferred over newlines where there is no difference in output size
:rtype: str

"""

assert isinstance(module, ast.Module)

printer = ModulePrinter()
printer = ModulePrinter(prefer_single_line=prefer_single_line)
printer(module)

try:
Expand Down
8 changes: 6 additions & 2 deletions src/python_minifier/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,15 @@ def minify(
remove_debug: bool = ...,
remove_explicit_return_none: bool = ...,
remove_builtin_exception_brackets: bool = ...,
constant_folding: bool = ...
constant_folding: bool = ...,
prefer_single_line: bool = ...
) -> Text: ...


def unparse(module: ast.Module) -> Text: ...
def unparse(
module: ast.Module,
prefer_single_line: bool = ...
) -> Text: ...


def awslambda(
Expand Down
10 changes: 9 additions & 1 deletion src/python_minifier/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ def parse_args():
dest='in_place'
)

parser.add_argument(
'--prefer-single-line',
action='store_true',
help='Prefer multiple statements on a single line separated by semicolons, instead of newlines, where there is no difference in output size',
dest='prefer_single_line',
)

# Minification arguments
minification_options = parser.add_argument_group('minification options', 'Options that affect how the source is minified')
minification_options.add_argument(
Expand Down Expand Up @@ -373,7 +380,8 @@ def do_minify(source, filename, minification_args):
remove_debug=minification_args.remove_debug,
remove_explicit_return_none=minification_args.remove_explicit_return_none,
remove_builtin_exception_brackets=minification_args.remove_exception_brackets,
constant_folding=minification_args.constant_folding
constant_folding=minification_args.constant_folding,
prefer_single_line=minification_args.prefer_single_line,
)

# Encode minified result to bytes for comparison and output
Expand Down
4 changes: 2 additions & 2 deletions src/python_minifier/expression_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class ExpressionPrinter(object):
Builds the smallest possible exact representation of an ast
"""

def __init__(self):
def __init__(self, prefer_single_line=False):

self.precedences = {
'Lambda': 2, # Lambda
Expand All @@ -34,7 +34,7 @@ def __init__(self):
'Tuple': 18, 'Set': 18, 'List': 18, 'Dict': 18, 'ListComp': 18, 'SetComp': 18, 'DictComp': 18, 'GeneratorExp': 18, # Container
}

self.printer = TokenPrinter()
self.printer = TokenPrinter(prefer_single_line=prefer_single_line)

def __call__(self, module):
"""
Expand Down
4 changes: 2 additions & 2 deletions src/python_minifier/module_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class ModulePrinter(ExpressionPrinter):
Builds the smallest possible exact representation of an ast
"""

def __init__(self, indent_char='\t'):
super(ModulePrinter, self).__init__()
def __init__(self, indent_char='\t', prefer_single_line=False):
super(ModulePrinter, self).__init__(prefer_single_line=prefer_single_line)
self.indent_char = indent_char

def __call__(self, module):
Expand Down
2 changes: 1 addition & 1 deletion src/python_minifier/token_printer.py
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ def leave_block(self):
def end_statement(self):
""" End a statement with a newline, or a semi-colon if it saves characters. """

if self.indent == 0:
if self.indent == 0 and not self._prefer_single_line:
self.newline()
else:
if self._code[-1] != ';':
Expand Down
Loading