Simple, hassle-free, dependency-free, AST based source code refactoring toolkit.

Overview

Refactor

PyPI version

Simple, hassle-free, dependency-free, AST based source code refactoring toolkit.

Why? How?

refactor is an end-to-end refactoring framework that is built on top of the 'simple but effective refactorings' assumption. It is much easier to write a simple script with it rather than trying to figure out what sort of a regex you need in order to replace a pattern (if it is even matchable with regexes).

Every refactoring rule offers a single entrypoint, match(), where they accept an AST node (from the ast module in the standard library) and respond with either returning an action to refactor or nothing. If the rule succeeds on the input, then the returned action will build a replacement node and refactor will simply replace the code segment that belong to the input with the new version.

Here is a complete script that will replace every placeholder access with 42 (not the definitions) on the given list of files:

import ast
from refactor import Rule, ReplacementAction, run

class Replace(Rule):
    
    def match(self, node):
        assert isinstance(node, ast.Name)
        assert node.id == 'placeholder'
        
        replacement = ast.Constant(42)
        return ReplacementAction(node, replacement)
        
if __name__ == "__main__":
    run(rules=[Replace])

If we run this on a file, refactor will print the diff by default;

--- test_file.py
+++ test_file.py

@@ -1,11 +1,11 @@

 def main():
-    print(placeholder * 3 + 2)
-    print(2 +               placeholder      + 3)
+    print(42 * 3 + 2)
+    print(2 +               42      + 3)
     # some commments
-    placeholder # maybe other comments
+    42 # maybe other comments
     if something:
         other_thing
-    print(placeholder)
+    print(42)
 
 if __name__ == "__main__":
     main()

As stated above, refactor's scope is usually small stuff, so if you want to do full program transformations we highly advise you to look at CST-based solutions like parso, LibCST and Fixit

Comments
  • How to build composite `Rule`s?

    How to build composite `Rule`s?

    Suppose I want to "lift" some assignments from the body of a function into the arguments e.g.

    def foo(a, b):
      c = 5
      print(3 * c)
    
    ->
    
    def foo(a, b, c=5):
      print(3 * c)
    

    As far as I can tell, each Rule can only do one thing - either LazyReplace a node or Erase some nodes. But since I need both a LazyReplace and an Erase, where either I sequence these rewrites or that there should be shared context between both rules (either the Erase should pass c to the LazyReplace which inserts c in the args, or the LazyReplace inserts c and then passes it to Erase) but that seems like a bad idea (not sure what invariants you're depending on wrt the AST...

    Note that refactor.Erase(a).apply in the context of LazyReplace obviously won't work because i don't have access to the textual source at that point. Currently I'm just doing a del new_node.body[i] in the LazyReplace's build method (where the i is passed in from Rule).

    EDIT:

    seems

    if action := rule.match(node):
    

    could become

    if action := rule.match(node):
       if not isinstance(action, (list, tuple)):
           action = [action]
       for action in in rule.match(node):
    

    here.

    opened by makslevental 5
  • Asterisk symbol not allowed in function args

    Asterisk symbol not allowed in function args

    When running refactor over the following code:

        def __init__(self, *, foo):
            self.foo = foo
    

    I get:

    AttributeError: 'NoneType' object has no attribute '_fields'
    
    bug 
    opened by feluelle 5
  • expand_paths for `__main__` runner

    expand_paths for `__main__` runner

    It looks like the runner in main.py passes options.src to run_files directly https://github.com/isidentical/refactor/blob/master/refactor/main.py#L62 so you must pass in a single python file whereas the unbound runner expands the paths so you can run this on a directory of files: https://github.com/isidentical/refactor/blob/master/refactor/runner.py#L89-L91.

    Would you accept a PR to change this behavior? Or was this separation purposeful?

    enhancement 
    opened by gerrymanoim 4
  • Generate rules from Python AST?

    Generate rules from Python AST?

    Thanks for building such a great lib on Python AST! However, writing asserts in rules is pretty tedious, I wonder if it's possible to parse a target python AST, and generate the rules automatically?

    Also, another question is, now that the contract is based on a single ast.Node, is it possible to do a partial replacement with multiple statements (spans multiple ast.Nodes)?

    For example, how do I write the rule to replace all matches of the following 2 statements:

        boxes[:, 0] = boxes_xywhr[:, 0] - half_w
        boxes[:, 1] = boxes_xywhr[:, 1] - half_h
    

    Do I have to do it in a larger scope node (say ast.FunctionDef)? Or is it possible to get some UD-Chains based on the current ast.Node?

    opened by void-main 2
  • (feat) Adding an InsertBefore feature

    (feat) Adding an InsertBefore feature

    Proposing to add the possibility to insert statement before a node. This was found useful when combining 2 methods and needing to conserve the order of execution

    opened by MementoRC 2
  • Move code by changing `lineno`

    Move code by changing `lineno`

    Hey,

    I am having issues trying to move code by using lineno attribute.

    This is my code:

    import ast
    from dataclasses import dataclass
    
    from refactor import Action, Rule
    
    
    @dataclass
    class SwitchLinesAction(Action):
        node: ast.AST
        lineno: int
    
        def build(self):
            new_node = self.branch()
            new_node.lineno = self.lineno
            return new_node
    
    
    class ReplaceTopLevelImportByFunctionLevelImport(Rule):
        """Replace top-level import by function-level import"""
    
        def match(self, node: ast.AST) -> Action:
            assert isinstance(node, ast.Import)
    
            return SwitchLinesAction(node, lineno=2)
    

    And this is my test code:

    import numpy
    1
    1
    

    When I print before and after lineno, I can see that the value indeed changes, but the final representation is the same - it does not move the code from line 1 to line 2.

    You can already see there what my goal is: "replacing top-level imports by function-level imports"

    question 
    opened by feluelle 2
  • Implement scope / ancestry tracking representatives

    Implement scope / ancestry tracking representatives

    It is a common need when doing refactors to do simple name resolution, as well as gathering data about the parental nodes. It would be really useful to have built-in representatives to do so.

    opened by isidentical 2
  • Adding asyncio.sleep() to Asyncified function

    Adding asyncio.sleep() to Asyncified function

    I seem to hit a conceptualization issue trying to do this. When I reach the FunctionDef, I look for await statements and if there's none, I want to add asyncio.sleep(0) to the last line, I search in the body and try an InsertAfter the last node of the body, but that errors out

    Well, the crude method of adding the Expr at the end of body kinda works, but an empty line is inserted as well and it felt clunky

    opened by MementoRC 1
  • Use `pre-commit` as CI

    Use `pre-commit` as CI

    There are several notes:

    1. I've removed pre-commit/action, because it is deprecated. I recommend switching to https://pre-commit.ci/
    2. I've updated multiple GitHub Actions to newer versions
    3. I've also changed how new version is published. It should be only published from master and using 3.9 (not 3.8)

    Closes https://github.com/isidentical/refactor/issues/28

    opened by sobolevn 1
  • AttributeError: 'Namespace' object has no attribute 'refactor_dir'

    AttributeError: 'Namespace' object has no attribute 'refactor_dir'

    Apologies if I've misunderstood how to use refactor. I was trying to run it with

    refactor -d test_refactor.py test.py
    

    Where test_refactor.py is a file with my Rule and test.py is some source. I get an AttributeError at https://github.com/isidentical/refactor/blob/master/refactor/main.py#L45. Debugging, I think this should be refactor_file = options.refactor_file?

    -> refactor_file = options.refactor_dir
    (Pdb) options
    Namespace(src=[PosixPath('test.py')], refactor_file=PosixPath('test_refactor.py'), dont_apply=True)
    

    Python 3.9 / refactor 0.4.1.

    bug 
    opened by gerrymanoim 1
  • Optimize node dispatching

    Optimize node dispatching

    We could try to reduce the number of .match() calls we are making by giving Rules to register certain types of nodes. This will eliminate the top level assert isinstance(node, <node type>) check from their side and made our refactor loop faster.

    opened by isidentical 1
  • Hint for MaybeOverlap error

    Hint for MaybeOverlap error

    Would it be helpful to add an hint as to the action that generated an overlap error with:

                try:
                    updated_input = path.execute(previous_tree)
                except AccessFailure:
                    raise MaybeOverlappingActions(
                        "When using chained actions, individual actions should not"
                        " overlap with each other."
                        f"   Action attempted: {action}"
                    ) from None
    
    
    opened by MementoRC 0
  • (Experimental) Preserve comments on mostly similar lines

    (Experimental) Preserve comments on mostly similar lines

    Bear with the padawan - This is just for your review and comments. It seems that I always convolute things that can be written much simpler It feels too clunky for just a couples of comments, though

    opened by MementoRC 0
  • Multiline strings get indented

    Multiline strings get indented

    This refactoring, similar to what I actually used:

    import ast
    import refactor
    
    class WrapF(refactor.Rule):
        def match(self, node: ast.AST) -> refactor.BaseAction:
            assert isinstance(node, ast.Constant)
    
            # Prevent wrapping F-strings that are already wrapped in F()
            # Otherwise you get infinite F(F(F(F(...))))
            parent = self.context.ancestry.get_parent(node)
            assert not (isinstance(parent, ast.Call) and isinstance(parent.func, ast.Name) and parent.func.id == 'F')
    
            return refactor.Replace(node, ast.Call(func=ast.Name(id="F"), args=[node], keywords=[]))
    
    
    refactor.run(rules=[WrapF])
    

    produces this:

     def f():
    -    return """
    -a
    -"""
    +    return F("""
    +    a
    +    """)
    

    This changes the value of the string.

    Possibly related is https://github.com/isidentical/refactor/issues/12, but I couldn't reproduce an equivalent problem with just ast.unparse:

    import ast
    
    source = '''
    def f():
        return """
    a
    """
    '''
    
    tree = ast.parse(source)
    node = tree.body[0].body[0].value
    call = ast.Call(func=ast.Name(id="F"), args=[node], keywords=[])
    ast.copy_location(call, node)
    ast.fix_missing_locations(call)
    print(ast.unparse(node))  # '\na\n'
    print(ast.unparse(call))  # F('\na\n')
    
    opened by alexmojaki 1
  • Replacing a `decorator_list`

    Replacing a `decorator_list`

    I encountered a situation where I want to remove some decorators (like replacing aiounittest.async_test with IsolatedAsyncioTestCase). refactor throws the InvalidActionError because it finds that I emptied the list of decorators, as it identified the decorator as critical. On the other hand, i don't want to delete the AsyncFunctionDef just because of the decorator_list The workaround to that in my branch is to butcher the is_critical_node in order to invalidate the is_critical_node. Would you suggest a neater way?

    opened by MementoRC 0
  • Workaround to duplicate decorators

    Workaround to duplicate decorators

    Related to: https://github.com/isidentical/refactor/issues/55 Observation:

    • When Asyncifing a decorated function, the decorators are duplicated Understanding:
    • In apply() method of_ReplaceCodeSegmentAction, the lines exclude the decorators (from position_for())
    • Building the replacement is based on the context, which includes the decorators
    • Replacing the lines slice with the replacement ends up duplicating the decorators Proposal:
    • A possible solution is to include the decorators in the lines since _resyntesize() works with the context that includes the decorators
    opened by MementoRC 0
Releases(v0.6.3)
  • v0.6.3(Oct 29, 2022)

  • 0.6.2(Oct 23, 2022)

    Major

    Nothing new, compared to 0.6.0.

    Other Changes

    • Augmented and annotated assignments now counted towards definitions when analyzing the scope.
    • refactor.actions.InsertAfter now preserves the final line state (e.g. if the anchor doesn't end with a newline, it also will produce code that won't be ending with a newline).
    • getrefactor.com is now available.
    Source code(tar.gz)
    Source code(zip)
  • 0.6.1(Oct 23, 2022)

  • 0.6.0(Oct 22, 2022)

    0.6.0

    Major

    This release adds experimental support for chained actions, a long awaited feature. This would mean that each match() can now return multiple actions (in the form of an iterator), and they will be applied gradually.

    import ast
    from refactor import Rule, actions
    from refactor.context import Representative, Scope
    from typing import Iterator
    
    
    class Usages(Representative):
        context_providers = (Scope,)
    
        def find(self, name: str, needle: ast.AST) -> Iterator[ast.AST]:
            """Iterate all possible usage sites of ``name``."""
            for node in ast.walk(self.context.tree):
                if isinstance(node, ast.Name) and node.id == name:
                    scope = self.context.scope.resolve(node)
                    if needle in scope.get_definitions(name):
                        yield node
    
    
    class PropagateAndDelete(Rule):
        context_providers = (Usages,)
    
        def match(self, node: ast.Import) -> Iterator[actions.BaseAction]:
            # Check if this is a single import with no alias.
            assert isinstance(node, ast.Import)
            assert len(node.names) == 1
    
            [name] = node.names
            assert name.asname is None
    
            # Replace each usage of this module with its own __import__() call.
            import_call = ast.Call(
                func=ast.Name("__import__"),
                args=[ast.Constant(name.name)],
                keywords=[],
            )
            for usage in self.context.usages.find(name.name, node):
                yield actions.Replace(usage, import_call)
    
            # And finally remove the import itself
            yield actions.Erase(node)
    

    Other Changes

    • Encoding is now preserved when using the refactor.Session.run_file API (which means if you use the refactor.Change.apply_diff or using the -a flag in the CLI, the generated source code will be encoded with the original encoding before getting written to the file.)
    • Offset tracking has been changed to be at the encoded byte stream level (mirroring CPython behavior here). This fixes some unicode related problems (with actions like refactor.actions.Replace).
    • refactor.context.ScopeInfo.get_definitions now always returns a list, even if it can't find any definitions.
    Source code(tar.gz)
    Source code(zip)
  • 0.5.0(Aug 7, 2022)

    0.5.0

    Note: With this release, we also started improving our documentation. If you are interested in Refactor, check the new layout and tutorials out and let us know how you feel!

    Major

    This release includes the overhaul of our action system, and with the next releases we will start removing the old ones. A list of changes regarding actions can be seen here:

    • refactor.core no longer contains any actions (the deprecated aliases are still imported and exposed but all the new actions go into refactor.actions)
    • Action is now split into two, a refactor.actions.BaseAction which is the base of all actions (useful for type hinting) and a refactor.actions.LazyReplace (a replace action that builds the node lazily in its build()).
    • ReplacementAction is now refactor.actions.Replace
    • NewStatementAction is now refactor.actions.LazyInsertAfter
    • TargetedNewStatementAction is now refactor.actions.InsertAfter

    For migrating your code base to the new style actions, we wrote a small tool (that we also used internally), examples/deprecated_aliases.py. Feel free to try it, and let us know if the transition was seamless.

    Other Changes

    • Added experimental Windows support, contributed by Hakan Celik
    • common.find_closest now takes end_lineno and end_col_offset into account. It also ensures there is at least one target node.
    • Added debug_mode setting to refactor.context.Configuration.
    • Added a command-line flag (-d/--enable-debug-mode) to the default CLI runner to change session's configuration.
    • When unparsable source code is generated, the contents can be now seen if the debug mode is enabled.
    • [Experimental] Added ability to partially recover floating comments (from preceding or succeeding lines) bound to statements.
    • The context providers now can be accessed with attribute notation, e.g. self.context.scope instead of self.context.metadata["scope].
    • If you access a built-in context provider (scope/ancestry) and it is not already imported, we auto-import it. So most common context providers are now ready to be used.
    • Added common.next_statement_of.
    Source code(tar.gz)
    Source code(zip)
  • 0.5.0b0(Aug 6, 2022)

    What's Changed

    Major

    This release includes the overhaul of our action system, and with the next release we will be starting deprecating the old ones. A list of changes:

    • refactor.core no longer contains any actions (the deprecated aliases are still imported and exposed but all the new actions go into refactor.actions)
    • Action is now split into two, a refactor.actions.BaseAction which is the base of all actions (useful for type hinting) and a refactor.actions.LazyReplace (a replace action that builds the node lazily in its build()).
    • ReplacementAction is now refactor.actions.Replace
    • NewStatementAction is now refactor.actions.LazyInsertAfter
    • TargetedNewStatementAction is now refactor.actions.InsertAfter

    For migrating your code base to the new style actions, we wrote a small tool (that we also used internally), examples/deprecated_aliases.py. Feel free to try it, and let us know if the transition was seamless.

    Other Changes

    • Added experimental Windows support, contributed by Hakan Celik
    • common.find_closest now takes end_lineno and end_col_offset into account. It also ensures there is at least one target node.
    • Added debug_mode setting to refactor.context.Configuration
    • Added a command-line flag (-d/--enable-debug-mode) to the default CLI runner to change session's configuration.
    • When unparsable source code is generated, the contents can be now seen if the debug mode is enabled.
    • [Experimental] Added ability to partially recover floating comments (from preceding or succeeding lines) bound to statements.
    • The context providers now can be accessed with attribute notation, e.g. self.context.scope instead of self.context.metadata["scope].
    • If you access a built-in context provider (scope/ancestry) and it is not already imported, we auto-import it. So most common context providers are now ready to be used.
    Source code(tar.gz)
    Source code(zip)
  • 0.4.4(Jul 5, 2022)

    What's Changed

    • Use pre-commit as CI by @sobolevn in https://github.com/isidentical/refactor/pull/29
    • Fix when using keyword-only argument and default argument is not set by @hakancelikdev in https://github.com/isidentical/refactor/pull/34

    New Contributors

    • @hakancelikdev made their first contribution in https://github.com/isidentical/refactor/pull/34

    Full Changelog: https://github.com/isidentical/refactor/compare/0.4.3...0.4.4

    Source code(tar.gz)
    Source code(zip)
  • 0.4.3(Mar 1, 2022)

    What's Changed

    • Fix NameError in common.py by @sobolevn in https://github.com/isidentical/refactor/pull/27

    Full Changelog: https://github.com/isidentical/refactor/compare/0.4.2...0.4.3

    Source code(tar.gz)
    Source code(zip)
  • 0.4.2(Jan 23, 2022)

    What's Changed

    • Fix passing --refactor-file, allow source directories by @gerrymanoim in https://github.com/isidentical/refactor/pull/22

    New Contributors

    • @gerrymanoim made their first contribution in https://github.com/isidentical/refactor/pull/22

    Full Changelog: https://github.com/isidentical/refactor/compare/0.4.1...0.4.2

    Source code(tar.gz)
    Source code(zip)
  • 0.4.1(Jan 15, 2022)

    What's Changed

    • Preserve indented literal expressions by @isidentical in https://github.com/isidentical/refactor/pull/19

    Full Changelog: https://github.com/isidentical/refactor/compare/0.4.0...0.4.1

    Source code(tar.gz)
    Source code(zip)
  • 0.4.0(Jan 14, 2022)

    • Fixed recursion on dependency resolution.
    • Implemented precise unparsing to leverage from existing structures in the given source.
    • Implemented refactor.core.Configuration to configure the unparser.
    • Renamed refactor.ast.UnparserBase to refactor.ast.BaseUnparser.
    • Removed token_map attribute from refactor.ast.BaseUnparser.
    • Removed refactor.context.CustomUnparser.
    • Changed refactor.core.Action's build method to raise a NotImplementedError. Users now have to override it.
    Source code(tar.gz)
    Source code(zip)
Owner
Batuhan Taskaya
Python developer
Batuhan Taskaya
Codes of CVPR2022 paper: Fixing Malfunctional Objects With Learned Physical Simulation and Functional Prediction

Fixing Malfunctional Objects With Learned Physical Simulation and Functional Prediction Figure 1. Teaser. Introduction This paper studies the problem

Yining Hong 32 Dec 29, 2022
Programmatically edit text files with Python. Useful for source to source transformations.

massedit formerly known as Python Mass Editor Implements a python mass editor to process text files using Python code. The modification(s) is (are) sh

106 Dec 17, 2022
Re-apply type annotations from .pyi stubs to your codebase.

retype Re-apply type annotations from .pyi stubs to your codebase. Usage Usage: retype [OPTIONS] [SRC]... Re-apply type annotations from .pyi stubs

Ɓukasz Langa 131 Nov 17, 2022
IDE allow you to refactor code, Baron allows you to write refactoring code.

Introduction Baron is a Full Syntax Tree (FST) library for Python. By opposition to an AST which drops some syntax information in the process of its c

Python Code Quality Authority 278 Dec 29, 2022
A system for Python that generates static type annotations by collecting runtime types

MonkeyType MonkeyType collects runtime types of function arguments and return values, and can automatically generate stub files or even add draft type

Instagram 4.1k Dec 28, 2022
a python refactoring library

rope, a python refactoring library ... Overview Rope is a python refactoring library. Notes Nick Smith 1.5k Dec 30, 2022

A library that modifies python source code to conform to pep8.

Pep8ify: Clean your code with ease Pep8ify is a library that modifies python source code to conform to pep8. Installation This library currently works

Steve Pulec 117 Jan 03, 2023
AST based refactoring tool for Python.

breakfast AST based refactoring tool. (Very early days, not usable yet.) Why 'breakfast'? I don't know about the most important, but it's a good meal.

eric casteleijn 0 Feb 22, 2022
Find dead Python code

Vulture - Find dead code Vulture finds unused code in Python programs. This is useful for cleaning up and finding errors in large code bases. If you r

Jendrik Seipp 2.4k Dec 27, 2022
Bottom-up approach to refactoring in python

Introduction RedBaron is a python library and tool powerful enough to be used into IPython solely that intent to make the process of writing code that

Python Code Quality Authority 653 Dec 30, 2022
Safe code refactoring for modern Python.

Safe code refactoring for modern Python projects. Overview Bowler is a refactoring tool for manipulating Python at the syntax tree level. It enables s

Facebook Incubator 1.4k Jan 04, 2023
Code generation and code search for Python and Javascript.

Codeon Code generation and code search for Python and Javascript. Similar to GitHub Copilot with one major difference: Code search is leveraged to mak

51 Dec 08, 2022
The python source code sorter

Sorts the contents of python modules so that statements are placed after the things they depend on, but leaves grouping to the programmer. Groups class members by type and enforces topological sortin

Ben Mather 302 Dec 29, 2022
Tool for translation type comments to type annotations in Python

com2ann Tool for translation of type comments to type annotations in Python. The tool requires Python 3.8 to run. But the supported target code versio

Ivan Levkivskyi 123 Nov 12, 2022
A simple Python bytecode framework in pure Python

A simple Python bytecode framework in pure Python

3 Jan 23, 2022
Awesome autocompletion, static analysis and refactoring library for python

Jedi - an awesome autocompletion, static analysis and refactoring library for Python Jedi is a static analysis tool for Python that is typically used

Dave Halter 5.3k Dec 29, 2022
Leap is an experimental package written to enable the utilization of C-like goto statements in Python functions

Leap is an experimental package written to enable the utilization of C-like goto statements in Python functions

6 Dec 26, 2022
Auto-generate PEP-484 annotations

PyAnnotate: Auto-generate PEP-484 annotations Insert annotations into your source code based on call arguments and return types observed at runtime. F

Dropbox 1.4k Dec 26, 2022
Removes unused imports and unused variables as reported by pyflakes

autoflake Introduction autoflake removes unused imports and unused variables from Python code. It makes use of pyflakes to do this. By default, autofl

Steven Myint 678 Jan 04, 2023
A tool (and pre-commit hook) to automatically upgrade syntax for newer versions of the language.

pyupgrade A tool (and pre-commit hook) to automatically upgrade syntax for newer versions of the language. Installation pip install pyupgrade As a pre

Anthony Sottile 2.4k Jan 08, 2023