From 198c109d389562d8a5f945adec8930e93396b194 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Thu, 21 Apr 2022 20:26:23 +0100 Subject: [PATCH 1/3] Add backwards default compatibility --- src/python_minifier/__init__.py | 34 ++++- src/python_minifier/__init__.pyi | 5 +- src/python_minifier/__main__.py | 22 ++- src/python_minifier/compat.py | 125 ++++++++++++++++++ src/python_minifier/expression_printer.py | 5 +- src/python_minifier/module_printer.py | 6 +- .../transforms/remove_object_base.py | 6 +- .../transforms/suite_transformer.py | 39 +----- src/python_minifier/util.py | 35 +++++ 9 files changed, 223 insertions(+), 54 deletions(-) create mode 100644 src/python_minifier/compat.py diff --git a/src/python_minifier/__init__.py b/src/python_minifier/__init__.py index 3560b416..386fb32a 100644 --- a/src/python_minifier/__init__.py +++ b/src/python_minifier/__init__.py @@ -6,8 +6,10 @@ import ast import re +import sys from python_minifier.ast_compare import CompareError, compare_ast +from python_minifier.compat import python_source_compat from python_minifier.module_printer import ModulePrinter from python_minifier.rename import ( rename_literals, @@ -64,7 +66,8 @@ def minify( convert_posargs_to_args=True, preserve_shebang=True, remove_asserts=False, - remove_debug=False + remove_debug=False, + minimum_python_version=None ): """ Minify a python module @@ -94,6 +97,8 @@ def minify( :param bool preserve_shebang: Keep any shebang interpreter directive from the source in the minified output :param bool remove_asserts: If assert statements should be removed :param bool remove_debug: If conditional statements that test '__debug__ is True' should be removed + :param minimum_python_version: The minimum version of python the minified module should be compatible with as tuple of major and minor version, e.g. (3, 3). + :type minimum_python_version: tuple[str, str] :rtype: str @@ -104,6 +109,14 @@ def minify( # This will raise if the source file can't be parsed module = ast.parse(source, filename) + if minimum_python_version: + if not isinstance(minimum_python_version[0], int) or not isinstance(minimum_python_version[1], int): + raise ValueError('minimum_python_version should be a tuple of major and minor version numbers, e.g. (3, 6)') + minimum_python_version = max((minimum_python_version[0], minimum_python_version[1]), python_source_compat(module)) + else: + minimum_python_version = python_source_compat(module) + sys.stderr.write('Minifying for python >=' + repr(minimum_python_version)) + add_namespace(module) if remove_literal_statements: @@ -119,7 +132,7 @@ def minify( module = RemovePass()(module) if remove_object_base: - module = RemoveObject()(module) + module = RemoveObject(minimum_python_version)(module) if remove_asserts: module = RemoveAsserts()(module) @@ -145,7 +158,7 @@ def minify( if convert_posargs_to_args: module = remove_posargs(module) - minified = unparse(module) + minified = unparse(module, minimum_python_version) if preserve_shebang is True: shebang_line = _find_shebang(source) @@ -170,22 +183,27 @@ def _find_shebang(source): return None -def unparse(module): +def unparse(module, minimum_python_version=None): """ Turn a module AST into python code This returns an exact representation of the given module, such that it can be parsed back into the same AST. + The minimum python version that should be able to parse the module may be specified. + If not specified, the output can be parsed by any support version of python that has the required syntax features. + :param module: The module to turn into python code :type: module: :class:`ast.Module` + :param minimum_python_version: The minimum python version that should be able to parse the module, as a tuple of minor and major versions. e.g. (2, 7) + :type minimum_python_version: tuple[str, str] :rtype: str """ assert isinstance(module, ast.Module) - printer = ModulePrinter() + printer = ModulePrinter(minimum_python_version) printer(module) try: @@ -221,5 +239,9 @@ def awslambda(source, filename=None, entrypoint=None): rename_globals = False return minify( - source, filename, remove_literal_statements=True, rename_globals=rename_globals, preserve_globals=[entrypoint], + source, + filename, + remove_literal_statements=True, + rename_globals=rename_globals, + preserve_globals=[entrypoint] ) diff --git a/src/python_minifier/__init__.pyi b/src/python_minifier/__init__.pyi index 94987256..7ff1d28f 100644 --- a/src/python_minifier/__init__.pyi +++ b/src/python_minifier/__init__.pyi @@ -1,5 +1,5 @@ import ast -from typing import List, Text, AnyStr, Optional, Any +from typing import List, Text, AnyStr, Optional, Any, Tuple class UnstableMinification(RuntimeError): @@ -21,7 +21,8 @@ def minify( convert_posargs_to_args: bool = ..., preserve_shebang: bool = ..., remove_asserts: bool = ..., - remove_debug: bool = ... + remove_debug: bool = ..., + minimum_python_version: Optional[Tuple[str, str]] = ... ) -> Text: ... def unparse(module: ast.Module) -> Text: ... diff --git a/src/python_minifier/__main__.py b/src/python_minifier/__main__.py index e3272242..02685e5b 100644 --- a/src/python_minifier/__main__.py +++ b/src/python_minifier/__main__.py @@ -1,5 +1,6 @@ from __future__ import print_function +import re import sys import os @@ -168,7 +169,7 @@ def parse_args(): minification_options.add_argument( '--no-preserve-shebang', action='store_false', - help='Preserve any shebang line from the source', + help='Disable preserving any shebang line from the source', dest='preserve_shebang', ) minification_options.add_argument( @@ -183,6 +184,15 @@ def parse_args(): help='Remove conditional statements that test __debug__ is True', dest='remove_debug', ) + minification_options.add_argument( + '--minimum-python', + type=str, + action='store', + default='2.7', + help='The earliest version of python to preserve backwards compatibility with', + dest='minimum_python_version', + metavar='VERSION' + ) parser.add_argument('--version', '-v', action='version', version=version) @@ -201,6 +211,9 @@ def parse_args(): if len(args.path) == 1 and os.path.isdir(args.path[0]) and not args.in_place: sys.stderr.write('error: path ' + args.path[0] + ' is a directory, --in-place required\n') sys.exit(1) + if args.minimum_python_version and not re.match(r'^\d+\.\d+$', args.minimum_python_version): + sys.stderr.write('error: --minimum-python should specify a major and minor version, e.g. --minimum-python 3.8\n') + sys.exit(1) return args @@ -232,6 +245,10 @@ def do_minify(source, filename, minification_args): names = [name.strip() for name in arg.split(',') if name] preserve_locals.extend(names) + minimum_python = None + if minification_args.minimum_python_version: + minimum_python = [int(v) for v in minification_args.minimum_python_version.split('.')] + return minify( source, filename=filename, @@ -248,7 +265,8 @@ def do_minify(source, filename, minification_args): convert_posargs_to_args=minification_args.convert_posargs_to_args, preserve_shebang=minification_args.preserve_shebang, remove_asserts=minification_args.remove_asserts, - remove_debug=minification_args.remove_debug + remove_debug=minification_args.remove_debug, + minimum_python_version=minimum_python ) diff --git a/src/python_minifier/compat.py b/src/python_minifier/compat.py new file mode 100644 index 00000000..0e5c4612 --- /dev/null +++ b/src/python_minifier/compat.py @@ -0,0 +1,125 @@ +import ast +from python_minifier.util import NodeVisitor + + +class PythonSourceCompatability(NodeVisitor): + """ + Determine the minimum python version that can parse this source + + It may or may not be parsable by more recent versions of python. + + The AST will have been parsed by the current version of python. + + This only cares about syntax features. + """ + + class Version(Exception): + def __init__(self, version): + self.version = version + + def __init__(self): + self._min_version = 2, 7 + + def set_minimum(self, major, minor): + if (major, minor) > self._min_version: + self._min_version = major, minor + + def set_version(self, major, minor): + raise self.Version((major, minor)) + + def __call__(self, module): + assert isinstance(module, ast.Module) + + try: + self.visit(module) + return self._min_version + except self.Version as v: + return v.version + + # region Literals + def visit_JoinedStr(self, node): + self.set_minimum(3, 6) + self.generic_visit(node) + # endregion + + # region Expressions + def visit_NamedExpr(self, node): + self.set_minimum(3, 8) + self.generic_visit(node) + + def visit_MatMult(self, node): + self.set_minimum(3, 5) + self.generic_visit(node) + # endregion + + # region Assignments + def visit_AnnAssign(self, node): + self.set_minimum(3, 6) + self.generic_visit(node) + # endregion + + # region Function and Class definitions + def visit_arguments(self, node): + if getattr(node, 'posonlyargs', []): + self.set_minimum(3, 8) + + if getattr(node, 'kwonlyargs', []): + self.set_minimum(3, 0) + + if getattr(node, 'varargannotation', None): + self.set_minimum(3, 0) + + if getattr(node, 'kwargannotation', None): + self.set_minimum(3, 0) + + self.generic_visit(node) + + def visit_arg(self, node): + if getattr(node, 'annotation'): + self.set_minimum(3, 0) + + def visit_YieldFrom(self, node): + self.set_minimum(3, 3) + self.generic_visit(node) + + def visit_nonlocal(self, node): + self.set_minimum(3, 0) + self.generic_visit(node) + # endregion + + # region Async and await + def visit_AsyncFunctionDef(self, node): + self.set_minimum(3, 5) + self.generic_visit(node) + + def visit_Await(self, node): + self.set_minimum(3, 5) + self.generic_visit(node) + + def visit_AsyncFor(self, node): + self.set_minimum(3, 5) + self.generic_visit(node) + + def visit_AsyncWith(self, node): + self.set_minimum(3, 5) + self.generic_visit(node) + # endregion + + # region Pattern Matching + def visit_Match(self, node): + self.set_minimum(3, 10) + self.generic_visit(node) + # endregion + + def visit_Repr(self, node): + self.set_version(2, 7) + self.generic_visit(node) + + def visit_comprehension(self, node): + if getattr(node, 'is_async', False): + self.set_version(3, 6) + self.generic_visit(node) + + +def python_source_compat(module): + return PythonSourceCompatability()(module) diff --git a/src/python_minifier/expression_printer.py b/src/python_minifier/expression_printer.py index 46ebd293..da613e3e 100644 --- a/src/python_minifier/expression_printer.py +++ b/src/python_minifier/expression_printer.py @@ -9,7 +9,8 @@ class ExpressionPrinter(object): Builds the smallest possible exact representation of an ast """ - def __init__(self): + def __init__(self, python_min_compatibility=(2, 7)): + self.python_min_compatibility = python_min_compatibility self.code = '' self.indent = 0 @@ -786,7 +787,7 @@ def token_break(self): if len(self.code) == 0: return - if self.code[-1] not in '[]{}() :"\'=\n\t<>|^&+-*@/%;,': + if self.code[-1] not in '[]{}() :"\'=\n\t<>|^&+-*@/%;,`': self.code += ' ' def visit_JoinedStr(self, node): diff --git a/src/python_minifier/module_printer.py b/src/python_minifier/module_printer.py index 235368ef..89a35498 100644 --- a/src/python_minifier/module_printer.py +++ b/src/python_minifier/module_printer.py @@ -10,8 +10,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, python_min_compatibility, indent_char='\t'): + super(ModulePrinter, self).__init__(python_min_compatibility) self.indent_char = indent_char def __call__(self, module): @@ -157,7 +157,7 @@ def visit_Return(self, node): self.token_break() self.code += 'return' if isinstance(node.value, ast.Tuple): - if sys.version_info < (3, 8) and [n for n in node.value.elts if is_ast_node(n, 'Starred')]: + if self.python_min_compatibility < (3, 8) and [n for n in node.value.elts if is_ast_node(n, 'Starred')]: self.code += '(' self._testlist(node.value) self.code += ')' diff --git a/src/python_minifier/transforms/remove_object_base.py b/src/python_minifier/transforms/remove_object_base.py index d7234128..90a599ac 100644 --- a/src/python_minifier/transforms/remove_object_base.py +++ b/src/python_minifier/transforms/remove_object_base.py @@ -1,12 +1,14 @@ import ast -import sys from python_minifier.transforms.suite_transformer import SuiteTransformer class RemoveObject(SuiteTransformer): + def __init__(self, min_python_version): + self.min_python_version = min_python_version + def __call__(self, node): - if sys.version_info < (3, 0): + if self.min_python_version < (3, 0): return node return self.visit(node) diff --git a/src/python_minifier/transforms/suite_transformer.py b/src/python_minifier/transforms/suite_transformer.py index 2743d902..a8ccae69 100644 --- a/src/python_minifier/transforms/suite_transformer.py +++ b/src/python_minifier/transforms/suite_transformer.py @@ -1,47 +1,12 @@ import ast from python_minifier.rename.mapper import add_parent -from python_minifier.util import is_ast_node - - -class NodeVisitor(object): - def visit(self, node): - """Visit a node.""" - method = 'visit_' + node.__class__.__name__ - visitor = getattr(self, method, self.generic_visit) - return visitor(node) - - def generic_visit(self, node): - """Called if no explicit visitor function exists for a node.""" - for field, value in ast.iter_fields(node): - if isinstance(value, list): - for item in value: - if isinstance(item, ast.AST): - self.visit(item) - elif isinstance(value, ast.AST): - self.visit(value) - - def visit_Constant(self, node): - if node.value in [None, True, False]: - method = 'visit_NameConstant' - elif isinstance(node.value, (int, float, complex)): - method = 'visit_Num' - elif isinstance(node.value, str): - method = 'visit_Str' - elif isinstance(node.value, bytes): - method = 'visit_Bytes' - elif node.value == Ellipsis: - method = 'visit_Ellipsis' - else: - raise RuntimeError('Unknown Constant value %r' % type(node.value)) - - visitor = getattr(self, method, self.generic_visit) - return visitor(node) +from python_minifier.util import is_ast_node, NodeVisitor class SuiteTransformer(NodeVisitor): """ - Transform suites of instructions + Transform suites of statements """ def __call__(self, node): diff --git a/src/python_minifier/util.py b/src/python_minifier/util.py index c7d0cafe..8d5b64ff 100644 --- a/src/python_minifier/util.py +++ b/src/python_minifier/util.py @@ -46,3 +46,38 @@ def is_ast_node(node, types): raise RuntimeError('Unknown Constant value %r' % type(node.value)) return False + +class NodeVisitor(object): + def visit(self, node): + """Visit a node.""" + method = 'visit_' + node.__class__.__name__ + visitor = getattr(self, method, self.generic_visit) + return visitor(node) + + def generic_visit(self, node): + """Called if no explicit visitor function exists for a node.""" + for field, value in ast.iter_fields(node): + if isinstance(value, list): + for item in value: + if isinstance(item, ast.AST): + self.visit(item) + elif isinstance(value, ast.AST): + self.visit(value) + + def visit_Constant(self, node): + if node.value in [None, True, False]: + method = 'visit_NameConstant' + elif isinstance(node.value, (int, float, complex)): + method = 'visit_Num' + elif isinstance(node.value, str): + method = 'visit_Str' + elif isinstance(node.value, bytes): + method = 'visit_Bytes' + elif node.value == Ellipsis: + method = 'visit_Ellipsis' + else: + raise RuntimeError('Unknown Constant value %r' % type(node.value)) + + visitor = getattr(self, method, self.generic_visit) + return visitor(node) + From 955dc502e2411b96cd7a962bbec9aede23b5d776 Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Thu, 21 Apr 2022 20:40:32 +0100 Subject: [PATCH 2/3] Update tests --- src/python_minifier/__init__.py | 2 ++ src/python_minifier/__init__.pyi | 7 +++++-- test/test_iterable_unpacking.py | 32 ++++++++++++++++++++++++-------- test/test_remove_object.py | 16 +++++++--------- 4 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/python_minifier/__init__.py b/src/python_minifier/__init__.py index 386fb32a..d5aa9b3e 100644 --- a/src/python_minifier/__init__.py +++ b/src/python_minifier/__init__.py @@ -202,6 +202,8 @@ def unparse(module, minimum_python_version=None): """ assert isinstance(module, ast.Module) + if minimum_python_version is None: + minimum_python_version = 2, 7 printer = ModulePrinter(minimum_python_version) printer(module) diff --git a/src/python_minifier/__init__.pyi b/src/python_minifier/__init__.pyi index 7ff1d28f..8bd8a32d 100644 --- a/src/python_minifier/__init__.pyi +++ b/src/python_minifier/__init__.pyi @@ -22,10 +22,13 @@ def minify( preserve_shebang: bool = ..., remove_asserts: bool = ..., remove_debug: bool = ..., - minimum_python_version: Optional[Tuple[str, str]] = ... + minimum_python_version: Optional[Tuple[int, int]] = ... ) -> Text: ... -def unparse(module: ast.Module) -> Text: ... +def unparse( + module: ast.Module, + minimum_python_version: Optional[Tuple[int, int]] = ... +) -> Text: ... def awslambda( source: AnyStr, diff --git a/test/test_iterable_unpacking.py b/test/test_iterable_unpacking.py index dd95a2b8..288800bf 100644 --- a/test/test_iterable_unpacking.py +++ b/test/test_iterable_unpacking.py @@ -4,17 +4,33 @@ from python_minifier import unparse from python_minifier.ast_compare import compare_ast -def test_return(): +def test_return_py38(): + if sys.version_info < (3, 8): + pytest.skip('Parenthesis are always required in < 3.8') + + # Parenthesis are required + source = 'def a():return(True,*[False])' + + expected_ast = ast.parse(source) + minified = unparse(expected_ast, minimum_python_version=(3,0)) + compare_ast(expected_ast, ast.parse(minified)) + assert source == minified + + # Parenthesis not required + source = 'def a():return True,*[False]' + + expected_ast = ast.parse(source) + minified = unparse(expected_ast, minimum_python_version=(3,8)) + compare_ast(expected_ast, ast.parse(minified)) + assert source == minified + + +def test_return_py30(): if sys.version_info < (3, 0): pytest.skip('Iterable unpacking in return not allowed in python < 3.0') - elif sys.version_info < (3, 8): - # Parenthesis are required - source = 'def a():return(True,*[False])' - - else: - # Parenthesis not required - source = 'def a():return True,*[False]' + # Parenthesis are required + source = 'def a():return(True,*[False])' expected_ast = ast.parse(source) minified = unparse(expected_ast) diff --git a/test/test_remove_object.py b/test/test_remove_object.py index d4b45418..2b533a27 100644 --- a/test/test_remove_object.py +++ b/test/test_remove_object.py @@ -7,8 +7,7 @@ def test_remove_object_py3(): - if sys.version_info < (3, 0): - pytest.skip('This test is python3 only') + min_version = 3,0 source = ''' class Test(object): @@ -20,7 +19,7 @@ class Test: ''' expected_ast = ast.parse(expected) - actual_ast = RemoveObject()(ast.parse(source)) + actual_ast = RemoveObject(min_version)(ast.parse(source)) compare_ast(expected_ast, actual_ast) source = ''' @@ -33,11 +32,11 @@ class Test(another_base, third_base): ''' expected_ast = ast.parse(expected) - actual_ast = RemoveObject()(ast.parse(source)) + actual_ast = RemoveObject(min_version)(ast.parse(source)) compare_ast(expected_ast, actual_ast) expected_ast = ast.parse(expected) - actual_ast = RemoveObject()(ast.parse(source)) + actual_ast = RemoveObject(min_version)(ast.parse(source)) compare_ast(expected_ast, actual_ast) source = ''' @@ -47,13 +46,12 @@ class Test(other_base): expected = source expected_ast = ast.parse(expected) - actual_ast = RemoveObject()(ast.parse(source)) + actual_ast = RemoveObject(min_version)(ast.parse(source)) compare_ast(expected_ast, actual_ast) def test_no_remove_object_py2(): - if sys.version_info >= (3, 0): - pytest.skip('This test is python2 only') + min_version = 2, 6 source = ''' class Test(object): @@ -65,6 +63,6 @@ class Test(object): ''' expected_ast = ast.parse(expected) - actual_ast = RemoveObject()(ast.parse(source)) + actual_ast = RemoveObject(min_version)(ast.parse(source)) compare_ast(expected_ast, actual_ast) From 596a01e7522ee1862306b9b946c8fcbcea9841cb Mon Sep 17 00:00:00 2001 From: Daniel Flook Date: Sun, 2 Apr 2023 23:52:08 +0100 Subject: [PATCH 3/3] wip --- src/python_minifier/__init__.py | 47 +++++++++++++++++++++++--------- src/python_minifier/__init__.pyi | 3 +- src/python_minifier/__main__.py | 4 +-- src/python_minifier/compat.py | 3 ++ 4 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/python_minifier/__init__.py b/src/python_minifier/__init__.py index d5aa9b3e..3a06e40c 100644 --- a/src/python_minifier/__init__.py +++ b/src/python_minifier/__init__.py @@ -49,6 +49,30 @@ def __init__(self, exception, source, minified): def __str__(self): return 'Minification was unstable! Please create an issue at https://github.com/dflook/python-minifier/issues' +def validate_python_version_arg(python_version): + if not isinstance(python_version, tuple) or (not isinstance(python_version[0], int) or not isinstance(python_version[1], int)): + raise ValueError('minimum_python_version should be a tuple of major and minor version numbers, e.g. (3, 6)') + +def version_constraints(module, minimum_python_version): + """ + Return the minimum and maximum python versions that the module should target. + + The minimum version will be at least what is required by the module's source code. + The maximum version will always be the current python version, as we need to be able to parse it to verify the minification. + + :param module: The module to check + :param minimum_python_version: The minimum python version that the module should target, or None + :return: A tuple of (minimum, maximum) python versions, where is version is a tuple of (major, minor) + """ + + if minimum_python_version is not None: + minimum = max((minimum_python_version[0], minimum_python_version[1]), python_source_compat(module)) + else: + minimum = python_source_compat(module) + + maximum = sys.version_info[:2] + + return minimum, maximum def minify( source, @@ -109,13 +133,8 @@ def minify( # This will raise if the source file can't be parsed module = ast.parse(source, filename) - if minimum_python_version: - if not isinstance(minimum_python_version[0], int) or not isinstance(minimum_python_version[1], int): - raise ValueError('minimum_python_version should be a tuple of major and minor version numbers, e.g. (3, 6)') - minimum_python_version = max((minimum_python_version[0], minimum_python_version[1]), python_source_compat(module)) - else: - minimum_python_version = python_source_compat(module) - sys.stderr.write('Minifying for python >=' + repr(minimum_python_version)) + minimum_python_version, maximum_python_version = version_constraints(module, minimum_python_version) + sys.stderr.write('Targeting python >=' + repr(minimum_python_version) + ', <=' + repr(maximum_python_version)) add_namespace(module) @@ -202,6 +221,7 @@ def unparse(module, minimum_python_version=None): """ assert isinstance(module, ast.Module) + if minimum_python_version is None: minimum_python_version = 2, 7 @@ -213,15 +233,15 @@ def unparse(module, minimum_python_version=None): except SyntaxError as syntax_error: raise UnstableMinification(syntax_error, '', printer.code) - try: - compare_ast(module, minified_module) - except CompareError as compare_error: - raise UnstableMinification(compare_error, '', printer.code) + #try: + # compare_ast(module, minified_module) + #except CompareError as compare_error: + # raise UnstableMinification(compare_error, '', printer.code) return printer.code -def awslambda(source, filename=None, entrypoint=None): +def awslambda(source, filename=None, entrypoint=None, python_version=None): """ Minify a python module for use as an AWS Lambda function @@ -245,5 +265,6 @@ def awslambda(source, filename=None, entrypoint=None): filename, remove_literal_statements=True, rename_globals=rename_globals, - preserve_globals=[entrypoint] + preserve_globals=[entrypoint], + minimum_python_version=python_version, ) diff --git a/src/python_minifier/__init__.pyi b/src/python_minifier/__init__.pyi index 8bd8a32d..e4dfb47a 100644 --- a/src/python_minifier/__init__.pyi +++ b/src/python_minifier/__init__.pyi @@ -33,5 +33,6 @@ def unparse( def awslambda( source: AnyStr, filename: Optional[Text] = ..., - entrypoint: Optional[Text] = ... + entrypoint: Optional[Text] = ..., + python_version: Optional[Tuple[int, int]] = ... ) -> Text: ... diff --git a/src/python_minifier/__main__.py b/src/python_minifier/__main__.py index 02685e5b..562bfe3f 100644 --- a/src/python_minifier/__main__.py +++ b/src/python_minifier/__main__.py @@ -185,7 +185,7 @@ def parse_args(): dest='remove_debug', ) minification_options.add_argument( - '--minimum-python', + '--minimum-python-version', type=str, action='store', default='2.7', @@ -212,7 +212,7 @@ def parse_args(): sys.stderr.write('error: path ' + args.path[0] + ' is a directory, --in-place required\n') sys.exit(1) if args.minimum_python_version and not re.match(r'^\d+\.\d+$', args.minimum_python_version): - sys.stderr.write('error: --minimum-python should specify a major and minor version, e.g. --minimum-python 3.8\n') + sys.stderr.write('error: --minimum-python-version should specify a major and minor version, e.g. --minimum-python 3.8\n') sys.exit(1) return args diff --git a/src/python_minifier/compat.py b/src/python_minifier/compat.py index 0e5c4612..53476972 100644 --- a/src/python_minifier/compat.py +++ b/src/python_minifier/compat.py @@ -120,6 +120,9 @@ def visit_comprehension(self, node): self.set_version(3, 6) self.generic_visit(node) + def visit_TryStar(self, node): + self.set_version(3, 11) + self.generic_visit(node) def python_source_compat(module): return PythonSourceCompatability()(module)