├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── pyinline ├── __init__.py └── __main__.py ├── pyproject.toml └── tests ├── test_constant_parameter.py ├── test_indentedblock.py ├── test_name_collission.py ├── test_nesting.py ├── test_recursive.py └── test_simplestatement.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.py[co] 2 | [.][v]env/ 3 | __pycache__ 4 | dist/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "python.formatting.provider": "black", 3 | "markdownlint.config": { 4 | "MD028": false, 5 | "MD025": { 6 | "front_matter_title": "" 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Anthony Shaw 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pyinline 2 | 3 | A function inliner for Python. Import `inline` from `pyinline` and run: 4 | 5 | ```console 6 | $ python -m pyinline source.py 7 | ``` 8 | 9 | This will convert the following: 10 | 11 | ```python 12 | from pyinline import inline 13 | import logging 14 | 15 | log = logging.getLogger(__name__) 16 | 17 | 18 | @inline 19 | def log_error(msg: str, exception: Exception): 20 | log.error(msg, exception, exc_info=True) 21 | 22 | 23 | try: 24 | x = 1 / 0 25 | except Exception as e: 26 | log_error("Could not divide number", e) 27 | 28 | ``` 29 | 30 | will generate: 31 | 32 | ```python 33 | import logging 34 | 35 | log = logging.getLogger(__name__) 36 | 37 | 38 | try: 39 | x = 1 / 0 40 | except Exception as e: 41 | log.error("Could not divide number", e, exc_info=True) 42 | ``` 43 | 44 | Call with `--diff` to generate a patch. -------------------------------------------------------------------------------- /pyinline/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | pyinline, a script for inlining functions that have been decorated with the `@inline` decorator. 3 | 4 | Usage: 5 | 6 | @inline 7 | def my_helpful_function(arg): 8 | # do_things.. 9 | 10 | my_helpful_function(1) 11 | 12 | """ 13 | 14 | import libcst as cst 15 | from typing import Optional, List, Union 16 | from functools import wraps 17 | from libcst.helpers import get_full_name_for_node 18 | import logging 19 | from dataclasses import dataclass 20 | from typing import Optional, Sequence, Union 21 | from libcst._nodes.internal import ( 22 | CodegenState, 23 | visit_sequence, 24 | ) 25 | from libcst._add_slots import add_slots 26 | 27 | __version__ = "0.0.2" 28 | 29 | log = logging.getLogger(__name__) 30 | 31 | 32 | @wraps 33 | def inline(): 34 | pass 35 | 36 | 37 | MODULE_NAME = "pyinline" 38 | DECORATOR_NAME = "inline" 39 | 40 | 41 | @add_slots 42 | @dataclass(frozen=True) 43 | class InlineBlock(cst.BaseSuite): 44 | body: Sequence[cst.BaseStatement] 45 | 46 | #: Any optional trailing comment and the final ``NEWLINE`` at the end of the line. 47 | header: cst.TrailingWhitespace = cst.TrailingWhitespace.field() 48 | 49 | def _visit_and_replace_children(self, visitor: cst.CSTVisitorT) -> "InlineBlock": 50 | return InlineBlock( 51 | body=visit_sequence(self, "body", self.body, visitor), header=self.header 52 | ) 53 | 54 | def _codegen_impl(self, state: CodegenState) -> None: 55 | self.header._codegen(state) 56 | 57 | if self.body: 58 | with state.record_syntactic_position( 59 | self, start_node=self.body[0], end_node=self.body[-1] 60 | ): 61 | for stmt in self.body: 62 | stmt._codegen(state) 63 | 64 | 65 | class AssignedNamesVisitor(cst.CSTVisitor): 66 | def __init__(self) -> None: 67 | self._to_mangle = [] 68 | 69 | def leave_Assign(self, original_node: cst.Assign): 70 | self._to_mangle.extend(target.target.value for target in original_node.targets) 71 | 72 | @property 73 | def names_to_mangle(self): 74 | return self._to_mangle 75 | 76 | 77 | class NameManglerTransformer(cst.CSTTransformer): 78 | def __init__(self, prefix: str, to_mangle=List[str]) -> None: 79 | self.prefix = prefix 80 | self.to_mangle = to_mangle 81 | 82 | def visit_Name(self, node: cst.Name) -> Optional[bool]: 83 | if node.value == self.prefix: 84 | raise ValueError("Inline functions cannot be recursive") 85 | 86 | return super().visit_Name(node) 87 | 88 | def leave_Name( 89 | self, original_node: cst.Name, updated_node: cst.Name 90 | ) -> cst.BaseExpression: 91 | if original_node.value in self.to_mangle: 92 | return original_node.with_changes( 93 | value=f"_{self.prefix}__{original_node.value}" 94 | ) 95 | else: 96 | return original_node 97 | 98 | 99 | class NameToConstantTransformer(cst.CSTTransformer): 100 | def __init__(self, name: cst.Name, constant: cst.CSTNode) -> None: 101 | self.name = name 102 | self.constant = constant 103 | 104 | def leave_Name( 105 | self, original_node: cst.Name, updated_node: cst.Name 106 | ) -> cst.BaseExpression: 107 | if original_node.deep_equals(self.name): 108 | return self.constant 109 | return original_node 110 | 111 | 112 | class NameToNameTransformer(cst.CSTTransformer): 113 | def __init__(self, name: cst.Name, replacement_name: cst.Name) -> None: 114 | self.name = name 115 | self.replacement_name = replacement_name 116 | 117 | def leave_Name( 118 | self, original_node: cst.Name, updated_node: cst.Name 119 | ) -> cst.BaseExpression: 120 | if original_node.deep_equals(self.name): 121 | return self.replacement_name 122 | return original_node 123 | 124 | 125 | class InlineTransformer(cst.CSTTransformer): 126 | def __init__(self) -> None: 127 | self.inline_functions: List[cst.FunctionDef] = [] 128 | super().__init__() 129 | 130 | def visit_FunctionDef(self, node: cst.FunctionDef) -> Optional[bool]: 131 | if node.decorators: 132 | is_inline = any( 133 | get_full_name_for_node(decorator.decorator) == DECORATOR_NAME 134 | for decorator in node.decorators 135 | ) 136 | if is_inline: 137 | self.inline_functions.append(node) 138 | return super().visit_FunctionDef(node) 139 | 140 | def leave_FunctionDef( 141 | self, original_node: cst.FunctionDef, updated_node: cst.FunctionDef 142 | ) -> Union[ 143 | cst.BaseStatement, cst.FlattenSentinel[cst.BaseStatement], cst.RemovalSentinel 144 | ]: 145 | if original_node in self.inline_functions: 146 | return cst.RemovalSentinel.REMOVE 147 | return original_node 148 | 149 | def leave_ImportFrom( 150 | self, original_node: cst.ImportFrom, updated_node: cst.ImportFrom 151 | ) -> Union[ 152 | cst.BaseSmallStatement, 153 | cst.FlattenSentinel[cst.BaseSmallStatement], 154 | cst.RemovalSentinel, 155 | ]: 156 | if ( 157 | original_node.module.value == MODULE_NAME 158 | and original_node.names[0].name.value == DECORATOR_NAME 159 | ): 160 | return cst.RemovalSentinel.REMOVE 161 | return super().leave_ImportFrom(original_node, updated_node) 162 | 163 | def visit_Call(self, node: cst.Call) -> Optional[bool]: 164 | match = [ 165 | f 166 | for f in self.inline_functions 167 | if get_full_name_for_node(f) == get_full_name_for_node(node) 168 | ] 169 | if match: 170 | return False 171 | return super().visit_Call(node) 172 | 173 | def leave_Call( 174 | self, original_node: cst.Call, updated_node: cst.BaseExpression 175 | ) -> Union[cst.Call, cst.BaseSuite]: 176 | match = [ 177 | f 178 | for f in self.inline_functions 179 | if get_full_name_for_node(f) == get_full_name_for_node(original_node) 180 | ] 181 | if not match: 182 | return updated_node 183 | 184 | match = match[0] 185 | log.debug(f"Replacing function call to {get_full_name_for_node(original_node)}") 186 | # IF the inline function has no arguments and is just a single-line, return as a SimpleStatement. 187 | if ( 188 | isinstance(match.body, cst.IndentedBlock) 189 | and len(match.body.body) == 1 190 | and not original_node.args 191 | ): 192 | return match.body.body[0] 193 | 194 | # IF the function has no nesting, use a simple Statement suite 195 | if all(isinstance(line, cst.SimpleStatementLine) for line in match.body.body): 196 | suite = cst.SimpleStatementSuite( 197 | body=[ 198 | fragment 199 | for statement in match.body.body 200 | for fragment in statement.body 201 | ], 202 | leading_whitespace=cst.SimpleWhitespace(""), # TODO: Work out indent 203 | ) 204 | else: 205 | suite = InlineBlock(body=match.body.body) # TODO: Work out indent 206 | 207 | # Mangle names 208 | mangledNamesVisitor = AssignedNamesVisitor() 209 | suite.visit(mangledNamesVisitor) 210 | transformer = NameManglerTransformer( 211 | get_full_name_for_node(original_node), mangledNamesVisitor.names_to_mangle 212 | ) 213 | suite = suite.visit(transformer) 214 | 215 | # resolve arguments 216 | if original_node.args: 217 | for i, arg in enumerate(original_node.args): 218 | # Is this a constant? 219 | if isinstance( 220 | original_node.args[i].value, 221 | ( 222 | cst.SimpleString, 223 | cst.Integer, 224 | cst.Float, 225 | ), # TODO: Other useful constants 226 | ): 227 | # Replace names with constant value in functions 228 | transformer = NameToConstantTransformer( 229 | match.params.params[i].name, arg.value 230 | ) 231 | suite = suite.visit(transformer) 232 | else: 233 | # Replace names with constant value in functions 234 | transformer = NameToNameTransformer( 235 | match.params.params[i].name, arg.value 236 | ) 237 | suite = suite.visit(transformer) 238 | 239 | return suite 240 | -------------------------------------------------------------------------------- /pyinline/__main__.py: -------------------------------------------------------------------------------- 1 | from ast import arg 2 | import sys 3 | import libcst as cst 4 | from pyinline import InlineTransformer 5 | from rich.console import Console 6 | import difflib 7 | import argparse 8 | 9 | if __name__ == "__main__": 10 | parser = argparse.ArgumentParser() 11 | parser.add_argument("--diff", action="store_true", help="Print as diff") 12 | parser.add_argument("file", action="store", help="file to transform") 13 | args = parser.parse_args() 14 | transformer = InlineTransformer() 15 | with open(args.file, "rb") as source: 16 | source_tree = cst.parse_module(source.read()) 17 | 18 | modified_tree = source_tree.visit(transformer) 19 | console = Console() 20 | if args.diff: 21 | console.print( 22 | "\n".join( 23 | difflib.unified_diff( 24 | source_tree.code.splitlines(), modified_tree.code.splitlines() 25 | ) 26 | ) 27 | ) 28 | else: 29 | console.print(modified_tree.code) 30 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | requires = ["flit_core >=3.2,<4"] 3 | build-backend = "flit_core.buildapi" 4 | 5 | [project] 6 | name = "pyinline" 7 | authors = [{name = "Anthony Shaw"}] 8 | classifiers = ["License :: OSI Approved :: MIT License"] 9 | dynamic = ["version", "description"] 10 | dependencies = ["libcst >= 0.3.23,<0.4"] 11 | readme = "README.md" 12 | requires-python = ">=3.8" 13 | 14 | [project.urls] 15 | Home = "https://github.com/tonybaloney/pyinline" 16 | -------------------------------------------------------------------------------- /tests/test_constant_parameter.py: -------------------------------------------------------------------------------- 1 | from pyinline import inline 2 | import logging 3 | 4 | log = logging.getLogger(__name__) 5 | 6 | 7 | @inline 8 | def log_error(msg: str, exception: Exception): 9 | log.error(msg, exception, exc_info=True) 10 | 11 | 12 | try: 13 | x = 1 / 0 14 | except Exception as e: 15 | log_error("Could not divide number", e) 16 | -------------------------------------------------------------------------------- /tests/test_indentedblock.py: -------------------------------------------------------------------------------- 1 | from pyinline import inline 2 | 3 | 4 | @inline 5 | def log_error(msg): 6 | result = msg.upper() 7 | print(result) 8 | 9 | 10 | log_error("Oh no!") 11 | -------------------------------------------------------------------------------- /tests/test_name_collission.py: -------------------------------------------------------------------------------- 1 | from pyinline import inline 2 | 3 | 4 | @inline 5 | def log_error(msg): 6 | x = 2 7 | print(msg, x) 8 | 9 | 10 | x = 1 11 | log_error("Oh no!") 12 | print(x) 13 | -------------------------------------------------------------------------------- /tests/test_nesting.py: -------------------------------------------------------------------------------- 1 | from pyinline import inline 2 | import logging 3 | 4 | log = logging.getLogger(__name__) 5 | 6 | 7 | @inline 8 | def log_error(msg: str, exception: Exception): 9 | if len(msg) > 10: 10 | log.error(msg[:9], exception, exc_info=True) 11 | else: 12 | log.error(msg, exception, exc_info=True) 13 | 14 | 15 | try: 16 | x = 1 / 0 17 | except Exception as e: 18 | log_error("Could not divide number", e) 19 | -------------------------------------------------------------------------------- /tests/test_recursive.py: -------------------------------------------------------------------------------- 1 | from pyinline import inline 2 | import logging 3 | 4 | log = logging.getLogger(__name__) 5 | 6 | 7 | @inline 8 | def log_error(msg: str, exception: Exception): 9 | log.error(msg, exception, exc_info=True) 10 | if len(msg) > 10: 11 | log_error(msg[:9], exception) 12 | 13 | 14 | try: 15 | x = 1 / 0 16 | except Exception as e: 17 | log_error("Could not divide number", e) 18 | -------------------------------------------------------------------------------- /tests/test_simplestatement.py: -------------------------------------------------------------------------------- 1 | from pyinline import inline 2 | import logging 3 | 4 | log = logging.getLogger(__name__) 5 | 6 | 7 | @inline 8 | def log_error(): 9 | log.error("There has been an error!") 10 | 11 | 12 | log_error() 13 | --------------------------------------------------------------------------------