diff --git a/docs/source/transforms/remove_literal_statements.py b/docs/source/transforms/remove_literal_statements.py index bdd7bc47..0f02e165 100644 --- a/docs/source/transforms/remove_literal_statements.py +++ b/docs/source/transforms/remove_literal_statements.py @@ -5,5 +5,13 @@ 0 1000 -def test(): - 'Function docstring' +... +True +False +None + +class MyClass: + 'my class docstring' + + def test(self): + 'Function docstring' diff --git a/docs/source/transforms/remove_literal_statements.rst b/docs/source/transforms/remove_literal_statements.rst index 9a2dda29..94ac9f92 100644 --- a/docs/source/transforms/remove_literal_statements.rst +++ b/docs/source/transforms/remove_literal_statements.rst @@ -1,14 +1,46 @@ Remove Literal Statements ========================= -This transform removes statements that consist entirely of a literal value. This includes docstrings. -If a statement is required, it is replaced by a literal zero expression statement. +This transform removes statements that consist entirely of a literal value, which can include docstrings. -This transform will strip docstrings from the source. If the module uses the ``__doc__`` name the module docstring will -be retained. +'Literal statements' are statements that consist entirely of a literal value, i.e string, bytes, number, ellipsis, True, False or None. +These statements have no effect on the program and can be removed. +There is one common exception to this, which is docstrings. A string literal that is the first statement in module, function or class is a docstring. +Docstrings are made available to the program at runtime, so could affect it's behaviour if removed. -This transform is disabled by default. Enable by passing the ``remove_literal_statements=True`` argument to the :func:`python_minifier.minify` function, -or passing ``--remove-literal-statements`` to the pyminify command. +This transform has separate options to configure removal of literal statements: + + - Remove module docstring + - Remove function docstrings + - Remove class docstrings + - Remove any other literal statements + +If a literal can be removed but a statement is required, it is replaced by a literal zero expression statement. + +If it looks like docstrings are used by the module they will not be removed regardless of the options. +If the module uses the ``__doc__`` name the module docstring will not be removed. +If a ``__doc__`` attribute is used in the module, docstrings will not be removed from functions or classes. + +By default this transform will remove all literal statements except docstrings. + +Options +------- + +These arguments can be used with the pyminify command: + +``--remove-module-docstring`` removes the module docstring if it is not used. + +``--remove-function-docstrings`` removes function docstrings if they are not used. + +``--remove-class-docstrings`` removes class docstrings if they are not used. + +``--no-remove-literal-expression-statements`` disables removing non-docstring literal statements. + +``--remove-literal-statements`` is an alias for ``--remove-module-docstring --remove-function-docstrings --remove-class-docstrings``. + +When using the :func:`python_minifier.minify` function you can use the ``remove_literal_statements`` argument to control this transform. +You can pass a boolean ``True`` to remove all literal statements (including docstrings) or a boolean ``False`` to not remove any. +You can also pass a :class:`python_minifier.RemoveLiteralStatementsOptions` instance to specify what to remove Example ------- diff --git a/src/python_minifier/__init__.py b/src/python_minifier/__init__.py index a1ceece2..18501912 100644 --- a/src/python_minifier/__init__.py +++ b/src/python_minifier/__init__.py @@ -27,6 +27,7 @@ from python_minifier.transforms.remove_explicit_return_none import RemoveExplicitReturnNone from python_minifier.transforms.remove_exception_brackets import remove_no_arg_exception_call from python_minifier.transforms.remove_literal_statements import RemoveLiteralStatements +from python_minifier.transforms.remove_literal_statements_options import RemoveLiteralStatementsOptions from python_minifier.transforms.remove_object_base import RemoveObject from python_minifier.transforms.remove_pass import RemovePass from python_minifier.transforms.remove_posargs import remove_posargs @@ -50,13 +51,15 @@ 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' +default_remove_annotations_options = RemoveAnnotationsOptions() +default_remove_literal_statements_options = RemoveLiteralStatementsOptions() def minify( source, filename=None, - remove_annotations=RemoveAnnotationsOptions(), + remove_annotations=default_remove_annotations_options, remove_pass=True, - remove_literal_statements=False, + remove_literal_statements=default_remove_literal_statements_options, combine_imports=True, hoist_literals=True, rename_locals=True, @@ -87,6 +90,7 @@ def minify( :type remove_annotations: bool or RemoveAnnotationsOptions :param bool remove_pass: If Pass statements should be removed where possible :param bool remove_literal_statements: If statements consisting of a single literal should be removed, including docstrings + :type remove_literal_statements: bool or RemoveLiteralStatementsOptions :param bool combine_imports: Combine adjacent import statements where possible :param bool hoist_literals: If str and byte literals may be hoisted to the module level where possible. :param bool rename_locals: If local names may be shortened @@ -114,8 +118,20 @@ def minify( add_namespace(module) - if remove_literal_statements: - module = RemoveLiteralStatements()(module) + if isinstance(remove_literal_statements, bool): + remove_literal_statements_options = RemoveLiteralStatementsOptions( + remove_module_docstring=remove_literal_statements, + remove_function_docstrings=remove_literal_statements, + remove_class_docstrings=remove_literal_statements, + remove_literal_expression_statements=remove_literal_statements + ) + elif isinstance(remove_literal_statements, RemoveLiteralStatementsOptions): + remove_literal_statements_options = remove_literal_statements + else: + raise TypeError('remove_literal_statements must be a bool or RemoveLiteralStatements') + + if remove_literal_statements_options: + module = RemoveLiteralStatements(remove_literal_statements_options)(module) if combine_imports: module = CombineImports()(module) diff --git a/src/python_minifier/__init__.pyi b/src/python_minifier/__init__.pyi index 61fc099c..c55a8585 100644 --- a/src/python_minifier/__init__.pyi +++ b/src/python_minifier/__init__.pyi @@ -2,6 +2,7 @@ import ast from typing import List, Text, AnyStr, Optional, Any, Union from .transforms.remove_annotations_options import RemoveAnnotationsOptions as RemoveAnnotationsOptions +from .transforms.remove_literal_statements_options import RemoveLiteralStatementsOptions as RemoveLiteralStatementsOptions class UnstableMinification(RuntimeError): def __init__(self, exception: Any, source: Any, minified: Any): ... @@ -11,7 +12,7 @@ def minify( filename: Optional[str] = ..., remove_annotations: Union[bool, RemoveAnnotationsOptions] = ..., remove_pass: bool = ..., - remove_literal_statements: bool = ..., + remove_literal_statements: Union[bool, RemoveLiteralStatementsOptions] = ..., combine_imports: bool = ..., hoist_literals: bool = ..., rename_locals: bool = ..., @@ -27,8 +28,10 @@ def minify( remove_builtin_exception_brackets: bool = ... ) -> Text: ... + def unparse(module: ast.Module) -> Text: ... + def awslambda( source: AnyStr, filename: Optional[Text] = ..., diff --git a/src/python_minifier/__main__.py b/src/python_minifier/__main__.py index bc34ca3b..a630c4d4 100644 --- a/src/python_minifier/__main__.py +++ b/src/python_minifier/__main__.py @@ -8,6 +8,7 @@ from python_minifier import minify from python_minifier.transforms.remove_annotations_options import RemoveAnnotationsOptions +from python_minifier.transforms.remove_literal_statements_options import RemoveLiteralStatementsOptions try: version = get_distribution('python_minifier').version @@ -108,12 +109,6 @@ def parse_args(): help='Disable removing Pass statements', dest='remove_pass', ) - minification_options.add_argument( - '--remove-literal-statements', - action='store_true', - help='Enable removing statements that are just a literal (including docstrings)', - dest='remove_literal_statements', - ) minification_options.add_argument( '--no-hoist-literals', action='store_false', @@ -223,6 +218,38 @@ def parse_args(): dest='remove_class_attribute_annotations', ) + literal_statement_options = parser.add_argument_group('remove literal statements options', 'Options that affect how literal statements are removed') + literal_statement_options.add_argument( + '--remove-literal-statements', + action='store_true', + help='Enable removing all statements that are just a literal (including docstrings)', + dest='remove_literal_statements', + ) + literal_statement_options.add_argument( + '--remove-module-docstring', + action='store_false', + help='Enable removing non-module docstrings', + dest='remove_docstrings', + ) + literal_statement_options.add_argument( + '--remove-function-docstrings', + action='store_false', + help='Enable removing non-module docstrings', + dest='remove_docstrings', + ) + literal_statement_options.add_argument( + '--remove-class-docstrings', + action='store_false', + help='Enable removing non-module docstrings', + dest='remove_docstrings', + ) + literal_statement_options.add_argument( + '--no-remove-literal-expression-statements', + action='store_false', + help='Disable removing literal expression statements', + dest='remove_literal_expression_statements', + ) + parser.add_argument('--version', '-v', action='version', version=version) args = parser.parse_args() @@ -275,6 +302,21 @@ def do_minify(source, filename, minification_args): names = [name.strip() for name in arg.split(',') if name] preserve_locals.extend(names) + if minification_args.remove_literal_statements is False: + remove_literal_statements = RemoveLiteralStatementsOptions( + remove_module_docstring=True, + remove_function_docstrings=True, + remove_class_docstrings=True, + remove_literal_expression_statements=minification_args.remove_literal_expression_statements, + ) + else: + remove_literal_statements = RemoveLiteralStatementsOptions( + remove_module_docstring=minification_args.remove_module_docstring, + remove_function_docstrings=minification_args.remove_function_docstrings, + remove_class_docstrings=minification_args.remove_class_docstrings, + remove_literal_expression_statements=minification_args.remove_literal_expression_statements, + ) + if minification_args.remove_annotations is False: remove_annotations = RemoveAnnotationsOptions( remove_variable_annotations=False, @@ -296,7 +338,7 @@ def do_minify(source, filename, minification_args): combine_imports=minification_args.combine_imports, remove_pass=minification_args.remove_pass, remove_annotations=remove_annotations, - remove_literal_statements=minification_args.remove_literal_statements, + remove_literal_statements=remove_literal_statements, hoist_literals=minification_args.hoist_literals, rename_locals=minification_args.rename_locals, preserve_locals=preserve_locals, diff --git a/src/python_minifier/transforms/remove_literal_statements.py b/src/python_minifier/transforms/remove_literal_statements.py index 62910e86..1c0bc3f0 100644 --- a/src/python_minifier/transforms/remove_literal_statements.py +++ b/src/python_minifier/transforms/remove_literal_statements.py @@ -1,27 +1,9 @@ import ast +from python_minifier.transforms.remove_literal_statements_options import RemoveLiteralStatementsOptions from python_minifier.transforms.suite_transformer import SuiteTransformer from python_minifier.util import is_ast_node - -def find_doc(node): - - if isinstance(node, ast.Attribute): - if node.attr == '__doc__': - raise ValueError('__doc__ found!') - - for child in ast.iter_child_nodes(node): - find_doc(child) - - -def _doc_in_module(module): - try: - find_doc(module) - return False - except: - return True - - class RemoveLiteralStatements(SuiteTransformer): """ Remove literal expressions from the code @@ -29,33 +11,91 @@ class RemoveLiteralStatements(SuiteTransformer): This includes docstrings """ + def __init__(self, options): + assert isinstance(options, RemoveLiteralStatementsOptions) + self._options = options + super(RemoveLiteralStatements, self).__init__() + def __call__(self, node): - if _doc_in_module(node): - return node + assert isinstance(node, ast.Module) + + def has_doc_attribute(node): + if isinstance(node, ast.Attribute): + if node.attr == '__doc__': + return True + + for child in ast.iter_child_nodes(node): + if has_doc_attribute(child): + return True + + return False + + def has_doc_binding(node): + for binding in node.bindings: + if binding.name == '__doc__': + return True + return False + + self._has_doc_attribute = has_doc_attribute(node) + self._has_doc_binding = has_doc_binding(node) return self.visit(node) - def visit_Module(self, node): - for binding in node.bindings: - if binding.name == '__doc__': - node.body = [self.visit(a) for a in node.body] - return node + def without_literal_statements(self, node_list): + """ + Remove all literal statements except for docstrings + """ + + def is_docstring(node): + assert isinstance(node, ast.Expr) + + if not is_ast_node(node.value, ast.Str): + return False - node.body = self.suite(node.body, parent=node) - return node + if is_ast_node(node.parent, (ast.FunctionDef, 'AsyncFunctionDef', ast.ClassDef, ast.Module)): + return node.parent.body[0] is node - def is_literal_statement(self, node): - if not isinstance(node, ast.Expr): return False - return is_ast_node(node.value, (ast.Num, ast.Str, 'NameConstant', 'Bytes')) + def is_literal_statement(node): + if not isinstance(node, ast.Expr): + return False + + if is_docstring(node): + return False + + return is_ast_node(node.value, (ast.Num, ast.Str, 'NameConstant', 'Bytes', ast.Ellipsis)) + + return [n for n in node_list if not is_literal_statement(n)] + + def without_docstring(self, node_list): + if node_list == []: + return node_list + + if not isinstance(node_list[0], ast.Expr): + return node_list + + if isinstance(node_list[0].value, ast.Str): + return node_list[1:] + + return node_list def suite(self, node_list, parent): - without_literals = [self.visit(n) for n in node_list if not self.is_literal_statement(n)] + suite = [self.visit(node) for node in node_list] + + if self._options.remove_literal_expression_statements is True: + suite = self.without_literal_statements(node_list) + + if isinstance(parent, ast.Module) and self._options.remove_module_docstring is True and self._has_doc_binding is False: + suite = self.without_docstring(node_list) + elif is_ast_node(parent, (ast.FunctionDef, 'AsyncFunctionDef')) and self._options.remove_function_docstrings is True and self._has_doc_attribute is False: + suite = self.without_docstring(node_list) + elif is_ast_node(parent, ast.ClassDef) and self._options.remove_class_docstrings is True and self._has_doc_attribute is False: + suite = self.without_docstring(node_list) - if len(without_literals) == 0: + if len(suite) == 0: if isinstance(parent, ast.Module): return [] else: return [self.add_child(ast.Expr(value=ast.Num(0)), parent=parent)] - return without_literals + return suite diff --git a/src/python_minifier/transforms/remove_literal_statements_options.py b/src/python_minifier/transforms/remove_literal_statements_options.py new file mode 100644 index 00000000..5353e7ff --- /dev/null +++ b/src/python_minifier/transforms/remove_literal_statements_options.py @@ -0,0 +1,37 @@ +class RemoveLiteralStatementsOptions(object): + """ + Options for the RemoveLiteralStatements transform + + This can be passed to the minify function as the remove_literal_statements argument + + :param remove_module_docstring: Remove module docstring + :type remove_module_docstring: bool + :param remove_function_docstrings: Remove function docstrings + :type remove_function_docstrings: bool + :param remove_class_docstrings: Remove class docstrings + :type remove_class_docstrings: bool + :param remove_literal_expression_statements: Remove non-docstring literal statements + :type remove_literal_expression_statements: bool + """ + + remove_module_docstring = False + remove_function_docstrings = False + remove_class_docstrings = False + remove_literal_expression_statements = True + + def __init__(self, remove_module_docstring=False, remove_function_docstrings=False, remove_class_docstrings=False, remove_literal_expression_statements=True): + self.remove_module_docstring = remove_module_docstring + self.remove_function_docstrings = remove_function_docstrings + self.remove_class_docstrings = remove_class_docstrings + self.remove_literal_expression_statements = remove_literal_expression_statements + + def __repr__(self): + return 'RemoveLiteralStatementsOptions(remove_module_docstring=%r, remove_function_docstrings=%r, remove_class_docstrings=%r, remove_literal_expression_statements=%r)' % ( + self.remove_module_docstring, self.remove_function_docstrings, self.remove_class_docstrings, self.remove_literal_expression_statements + ) + + def __nonzero__(self): + return any((self.remove_module_docstring, self.remove_function_docstrings, self.remove_class_docstrings, self.remove_literal_expression_statements)) + + def __bool__(self): + return self.__nonzero__() diff --git a/src/python_minifier/transforms/remove_literal_statements_options.pyi b/src/python_minifier/transforms/remove_literal_statements_options.pyi new file mode 100644 index 00000000..4c06c49b --- /dev/null +++ b/src/python_minifier/transforms/remove_literal_statements_options.pyi @@ -0,0 +1,17 @@ +class RemoveLiteralStatementsOptions: + + remove_module_docstring: bool + remove_function_docstrings: bool + remove_class_docstrings: bool + remove_literal_expression_statements: bool + + def __init__(self, + remove_module_docstring: bool = ..., + remove_function_docstrings: bool = ..., + remove_class_docstrings: bool = ..., + remove_literal_expression_statements: bool = ...): + ... + + def __repr__(self) -> str: ... + def __nonzero__(self) -> bool: ... + def __bool__(self) -> bool: ... diff --git a/test/test_remove_literal_statements.py b/test/test_remove_literal_statements.py index 1268d410..f688d019 100644 --- a/test/test_remove_literal_statements.py +++ b/test/test_remove_literal_statements.py @@ -1,56 +1,508 @@ import ast +import sys -from python_minifier import add_namespace, bind_names, resolve_names +import pytest + +from python_minifier import add_namespace, bind_names, resolve_names, RemoveLiteralStatementsOptions +from python_minifier.ast_printer import print_ast from python_minifier.transforms.remove_literal_statements import RemoveLiteralStatements -from python_minifier.ast_compare import compare_ast +from python_minifier.ast_compare import compare_ast, CompareError + +""" +Test cases: + - Remove expression statements - module docstring, function docstring and class docstring are not removed; Test with all literal types, in module scope, function scope and class scope + - Remove module docstring - function docstring, class docstring and expression statements are not removed; Test class and function docstrings in nested scope + - Remove function docstring - module docstring, class docstring and expression statements are not removed; Test class and function docstrings in nested scope + - Remove class docstring - module docstring, function docstring and expression statements are not removed; Test class and function docstrings in nested scope + - Remove module docstring is suppressed by __doc__ builtin references; Test references in module scope, function scope and class scope + - function and class docstrings are suppressed by __doc__ attribute references; Test references in module scope, function scope and class scope +""" + +if sys.version_info < (3, 3): + CONSTANTS = '' +elif sys.version_info < (3, 4): + CONSTANTS = ['...'] +else: + CONSTANTS = ['...', 'True', 'False', 'None'] + +def insert_constants(indent=0): + indent_str = (' ' * indent) + return ('\n' + indent_str).join(constant for constant in CONSTANTS) + +source = ''' +"""Module docstring""" + +"Module literal" +b"Module literal" +213 +''' + insert_constants() + ''' +0.123 +1.23e-4 + +def test(): + """Function docstring""" + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(4) + ''' + 0.123 + 1.23e-4 + +class Test: + """Class docstring""" + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(4) + ''' + 0.123 + 1.23e-4 + + def test(): + """Function docstring""" + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(8) + ''' + 0.123 + 1.23e-4 +''' -def remove_literals(source): +def remove_literals(source, options): module = ast.parse(source, 'test_remove_literal_statements') add_namespace(module) bind_names(module) resolve_names(module) - return RemoveLiteralStatements()(module) + return RemoveLiteralStatements(options)(module) + +def test_remove_literal_expression_statements(): + + # Don't remove with remove_literal_expression_statements = False + expected = source + + expected_ast = ast.parse(expected) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + # Do remove with defaults + expected = ''' +"""Module docstring""" + +def test(): + """Function docstring""" + +class Test: + """Class docstring""" + + def test(): + """Function docstring""" +''' + + expected_ast = ast.parse(expected) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions()) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + # Do remove with remove_literal_expression_statements = True + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_literal_expression_statements=True)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + +def test_remove_module_docstring(): + + # Don't remove with defaults + expected = source + + expected_ast = ast.parse(expected) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + # Don't remove with remove_module_docstring=False + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_module_docstring=False, remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + # Do remove with remove_module_docstring=True + expected = ''' +"Module literal" +b"Module literal" +213 +''' + insert_constants() + ''' +0.123 +1.23e-4 + +def test(): + """Function docstring""" + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(4) + ''' + 0.123 + 1.23e-4 + +class Test: + """Class docstring""" + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(4) + ''' + 0.123 + 1.23e-4 + + def test(): + """Function docstring""" + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(8) + ''' + 0.123 + 1.23e-4 +''' + + expected_ast = ast.parse(expected) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_module_docstring=True, remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + +def test_remove_function_docstring(): + + # Dont' remove with defaults + expected = source + + expected_ast = ast.parse(expected) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + # Don't remove is remove_function_docstrings=False + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_function_docstrings=False, remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + # Do remove if remove_function_docstrings=True + expected = ''' +"""Module docstring""" + +"Module literal" +b"Module literal" +213 +''' + insert_constants() + ''' +0.123 +1.23e-4 + +def test(): + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(4) + ''' + 0.123 + 1.23e-4 + +class Test: + """Class docstring""" + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(4) + ''' + 0.123 + 1.23e-4 + + def test(): + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(8) + ''' + 0.123 + 1.23e-4 +''' + + expected_ast = ast.parse(expected) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_function_docstrings=True, remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + +def test_remove_class_docstring(): + + # Dont' remove with defaults + expected = source + + expected_ast = ast.parse(expected) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise -def test_remove_literal_num(): - source = '213' - expected = '' + # Don't remove is remove_function_docstrings=False + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_class_docstrings=False, remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + # Do remove if remove_function_docstrings=True + expected = ''' +"""Module docstring""" + +"Module literal" +b"Module literal" +213 +''' + insert_constants() + ''' +0.123 +1.23e-4 + +def test(): + """Function docstring""" + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(4) + ''' + 0.123 + 1.23e-4 + +class Test: + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(4) + ''' + 0.123 + 1.23e-4 + + def test(): + """Function docstring""" + "Module literal" + b"Module literal" + 213 + ''' + insert_constants(8) + ''' + 0.123 + 1.23e-4 +''' expected_ast = ast.parse(expected) - actual_ast = remove_literals(source) - compare_ast(expected_ast, actual_ast) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_class_docstrings=True, remove_literal_expression_statements=False)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + +def test_suppress_remove_module_docstring(): + + source = ''' +"""Module docstring""" + +print(__doc__) + +def test(): + """Function docstring""" + +class Test: + """Class docstring""" + + def test(): + """Function docstring""" +''' + + # function and class docstring still removed + expected = ''' +"""Module docstring""" + +print(__doc__) -def test_remove_literal_str(): - source = '"hello"' - expected = '' +def test():0 + +class Test: + def test():0 +''' expected_ast = ast.parse(expected) - actual_ast = remove_literals(source) - compare_ast(expected_ast, actual_ast) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_module_docstring=True, remove_function_docstrings=True, remove_class_docstrings=True)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + + # With __doc__ in a nested scope -def test_complex(): source = ''' -"module docstring" -a = 'hello' +"""Module docstring""" + +def test(): + """Function docstring""" -def t(): - "function docstring" - a = 2 - 0 - 2 - 'sadfsaf' - def g(): - "just a docstring" +class Test: + """Class docstring""" + def test(): + """Function docstring""" + + def nested(): + print(__doc__) ''' + + # function and class docstring still removed expected = ''' -a = 'hello' -def t(): - a=2 - def g(): - 0 +"""Module docstring""" + +def test():0 + +class Test: + + def test(): + + def nested(): + print(__doc__) ''' expected_ast = ast.parse(expected) - actual_ast = remove_literals(source) - compare_ast(expected_ast, actual_ast) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_module_docstring=True, remove_function_docstrings=True, remove_class_docstrings=True)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + +def test_suppress_remove_class_and_function_docstring(): + + source = ''' +"""Module docstring""" + +def test(): + """Function docstring""" + +class Test: + """Class docstring""" + + def test(): + """Function docstring""" + +print(test.__doc__) +''' + + # function and class docstring still removed + expected = ''' +def test(): + """Function docstring""" + +class Test: + """Class docstring""" + + def test(): + """Function docstring""" + +print(test.__doc__) +''' + + expected_ast = ast.parse(expected) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_module_docstring=True, remove_function_docstrings=True, remove_class_docstrings=True)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise + + # With __doc__ in a nested scope + + source = ''' +"""Module docstring""" + +def test(): + """Function docstring""" + +class Test: + """Class docstring""" + + def test(): + """Function docstring""" + + print(test.__doc__) +''' + + # function and class docstring still removed + expected = ''' +def test(): + """Function docstring""" + +class Test: + """Class docstring""" + + def test(): + """Function docstring""" + + print(test.__doc__) +''' + + expected_ast = ast.parse(expected) + actual_ast = remove_literals(source, RemoveLiteralStatementsOptions(remove_module_docstring=True, remove_function_docstrings=True, remove_class_docstrings=True)) + + try: + compare_ast(expected_ast, actual_ast) + except CompareError: + print(print_ast(expected_ast)) + print(print_ast(actual_ast)) + raise diff --git a/typing_test/test_typing.py b/typing_test/test_typing.py index 3adf3283..24fd4f17 100644 --- a/typing_test/test_typing.py +++ b/typing_test/test_typing.py @@ -4,7 +4,7 @@ import ast -from python_minifier import minify, unparse, awslambda, RemoveAnnotationsOptions +from python_minifier import minify, unparse, awslambda, RemoveAnnotationsOptions, RemoveLiteralStatementsOptions def test_typing() -> None: """ This should have good types """ @@ -42,3 +42,11 @@ def test_typing() -> None: remove_class_attribute_annotations=False ) minify('pass', remove_annotations=annotation_options) + + + annotation_options = RemoveLiteralStatementsOptions( + remove_docstrings=True, + remove_module_docstrings=True, + remove_expression_statements=True + ) + minify('pass', annotation_options=annotation_options)