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
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
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 4, 2023
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
a python refactoring library

rope, a python refactoring library ... Overview Rope is a python refactoring library. Notes Nick Smith <[email protected]> takes over maintaining rope

null 1.5k Dec 30, 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
Refactoring Python Applications for Simplicity

Python Refactoring Refactoring Python Applications for Simplicity. You can open and read project files or use this summary ?? Concatenate String my_st

Mohammad Dori 3 Jul 15, 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

null 106 Dec 17, 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 3, 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

null 51 Dec 8, 2022
A simple Python bytecode framework in pure Python

A simple Python bytecode framework in pure Python

null 3 Jan 23, 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
Removes commented-out code from Python files

eradicate eradicate removes commented-out code from Python files. Introduction With modern revision control available, there is no reason to save comm

Steven Myint 146 Dec 13, 2022
Turn your C++/Java code into a Python-like format for extra style points and to make everyone hates you

Turn your C++/Java code into a Python-like format for extra style points and to make everyone hates you

Tô Đức (Watson) 4 Feb 7, 2022
Astvuln is a simple AST scanner which recursively scans a directory, parses each file as AST and runs specified method.

Astvuln Astvuln is a simple AST scanner which recursively scans a directory, parses each file as AST and runs specified method. Some search methods ar

Bitstamp Security 7 May 29, 2022
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
MLSpace: Hassle-free machine learning & deep learning development

MLSpace: Hassle-free machine learning & deep learning development

abhishek thakur 293 Jan 3, 2023
Dependency Combobulator is an Open-Source, modular and extensible framework to detect and prevent dependency confusion leakage and potential attacks.

Dependency Combobulator Dependency Combobulator is an Open-Source, modular and extensible framework to detect and prevent dependency confusion leakage

Apiiro 84 Dec 23, 2022
Coltrane - A simple content site framework that harnesses the power of Django without the hassle.

coltrane A simple content site framework that harnesses the power of Django without the hassle. Features Can be a standalone static site or added to I

Adam Hill 58 Jan 2, 2023
Django-static-site - A simple content site framework that harnesses the power of Django without the hassle

coltrane A simple content site framework that harnesses the power of Django with

Adam Hill 57 Dec 6, 2022
DepFine Is a tool to find the unregistered dependency based on dependency confusion valunerablility and lead to RCE

DepFine DepFine Is a tool to find the unregistered dependency based on dependency confusion valunerablility and lead to RCE Installation: You Can inst

Hossam mesbah 14 Nov 11, 2022