Remove type hints in Python source programmatically

后端 未结 3 725
孤城傲影
孤城傲影 2020-12-29 07:21

I have some source code written for Python 3.5 that I want to make executable under Python 3.4. The only feature from 3.5 that I use which is not available in 3.4 are type h

相关标签:
3条回答
  • 2020-12-29 07:58

    OK, I got it :D

    Use Python's builtin ast module to parse the source code and then the excellent astunparse library to generate source code from the parsed ast again. Then all that's left is to remove the type annotations:

    import ast
    import astunparse
    
    source="""
    import typing
    from typing import Dict, T, Callable
    from typing import List
    
    def foo(bar: Dict[T, List[T]],
            baz: Callable[[T], int] = lambda x: (x+3)/7,
            **kwargs) -> List[T]:
        pass
    """
    
    class TypeHintRemover(ast.NodeTransformer):
    
        def visit_FunctionDef(self, node):
            # remove the return type defintion
            node.returns = None
            # remove all argument annotations
            if node.args.args:
                for arg in node.args.args:
                    arg.annotation = None
            return node
    
        def visit_Import(self, node):
            node.names = [n for n in node.names if n.name != 'typing']
            return node if node.names else None
    
        def visit_ImportFrom(self, node):
            return node if node.module != 'typing' else None
    
    # parse the source code into an AST
    parsed_source = ast.parse(source)
    # remove all type annotations, function return type definitions
    # and import statements from 'typing'
    transformed = TypeHintRemover().visit(parsed_source)
    # convert the AST back to source code
    print(astunparse.unparse(transformed))
    

    The TypeHintRemover visits all Nodes in the AST and removes all type hints within function arguments, the return type definitions of each function and all import statements that refer to the 'typing' module.

    The result is:

    def foo(bar, baz=(lambda x: ((x + 3) / 7)), **kwargs):
        pass
    
    0 讨论(0)
  • 2020-12-29 08:15

    There are also type hints for local variables (which came from Python 3.6). I've modified @klamann 's code to remove them too. Also, I use astor (https://pypi.org/project/astor/) to generate code.

    import ast
    import astor
    import sys
    
    
    class TypeHintRemover(ast.NodeTransformer):
    
        def visit_FunctionDef(self, node):
            # remove the return type definition
            node.returns = None
            # remove all argument annotations
            if node.args.args:
                for arg in node.args.args:
                    arg.annotation = None
            self.generic_visit(node)
            return node
    
        def visit_AnnAssign(self, node):
            if node.value is None:
                return None
            return ast.Assign([node.target], node.value)
    
        def visit_Import(self, node):
            node.names = [n for n in node.names if n.name != 'typing']
            return node if node.names else None
    
        def visit_ImportFrom(self, node):
            return node if node.module != 'typing' else None
    
    def remove_type_hints(source: str):
        # parse the source code into an AST
        parsed_source = ast.parse(source)
        # remove all type annotations, function return type definitions
        # and import statements from 'typing'
        transformed = TypeHintRemover().visit(parsed_source)
        # convert the AST back to source code
        return astor.to_source(transformed)
    
    
    def main():
        _, source_name, dest_name = sys.argv
        with open(source_name, "r") as sourceFile:
            source = "\n".join(sourceFile.readlines())
            dest = remove_type_hints(source)
            with open(dest_name, "w") as destFile:
                destFile.write(dest)
    
    if __name__ == "__main__":
        main()
    
    0 讨论(0)
  • 2020-12-29 08:19

    You can subclass lib2to3.refactor.RefactoringTool to refactor the code using a fixer that is a subclass of lib2to3.fixer_base.BaseFix with a pattern that looks for either a typed argument, a function declaration with an annotated returning value, or a simple statement that imports or imports from typing, and a transform method that removes the indices of the annotations from the child nodes or replaces the statement node with an empty node:

    from lib2to3 import fixer_base, refactor, fixer_util
    
    class FixParameterAnnotations(fixer_base.BaseFix):
        PATTERN = r'''
            name=tname
            |
            func=funcdef< any+ '->' any+ >
            |
            simple_stmt<
                (
                    import_name< 'import' 'typing' >
                    |
                    import_from< 'from' 'typing' 'import' any+ >
                ) '\n'
            >
        '''
    
        def transform(self, node, results):
            if 'name' in results:
                del node.children[1:] # delete annotation to typed argument
            elif 'func' in results:
                del node.children[-4:-2] # delete annotation to function declaration
            else:
                return fixer_util.BlankLine() # delete statement that imports typing
            return node
    
    class Refactor(refactor.RefactoringTool):
        def __init__(self, fixers):
            self._fixers= [cls(None, None) for cls in fixers]
            super().__init__(None, {'print_function': True})
    
        def get_fixers(self):
            return self._fixers, []
    

    so that:

    source = """
    import typing
    from typing import Dict, T, Callable
    from typing import List
    
    def foo(bar: Dict[T, List[T]],
            baz: Callable[[T], int] = lambda x: (x+3)/7,
            **kwargs) -> List[T]:
        print(line, end="")    # comments and white spaces are preserved
    """
    print(Refactor([FixParameterAnnotations]).refactor_string(source, ''))
    

    outputs:

    def foo(bar,
            baz = lambda x: (x+3)/7,
            **kwargs):
        print(line, end="")    # comments and white spaces are preserved
    

    Demo: https://repl.it/@blhsing/BurlywoodFeistyTrials

    As a bonus, lib2to3 also preserves all comments and white spaces after the transformation. You can find the definition of the Python grammar in Grammar.txt of the lib2to3 module.

    0 讨论(0)
提交回复
热议问题