Programmable API#

Refactor provides a top-level API for you to freely use your rules on any sort of input. You might want to use this API if you are interested in:

  • Writing your own command line interface

  • Testing the rules and the actions you wrote

  • Going to next-level and building GitHub bots on top of Refactor.

Sessions#

A “session” is a collection of rules that will run together on the same context.

>>> from refactor import Session
>>> from previous_tutorial import FoldMyConstants, PropagateMyConstants

You can initialize session with the rules that you want to include (so if you are building your own CLI, you can read user’s configuration to see what rules they disabled and exclude them at this step).

>>> session = Session(rules=[FoldMyConstants, PropagateMyConstants])

Tip

You can also optionally pass a config object to customize some internal behaviors (e.g. enabling the debug mode or changing the default unparser).

With session in our hands, we can start exploring the two entry points it offers: run() and run_file().

run()#

run() takes the raw source code as text, transforms it, and returns to you as a text. With this property, it is very useful for tests (especially the parametrized ones).

>>> session.run("a = 2 + 2")
'a = 4'

Warning

If you pass a problematic source code (e.g. code that contains invalid syntax), it won’t raise any errors and will return the given source code unchanged.

>>> session.run("$$$ invalid $$$")
>>> '$$$ invalid $$$'
>>>

run_file()#

run_file() is however a bit different, in the sense that it takes a pathlib.Path object instead of the source code and returns a Change object instead of returning the transformed result directly. As you might have guessed, this is an API suitable for tools that process files and something that you might consider using in your CLI interface.

>>> from pathlib import Path
>>> target_file = Path("test.py")
>>>
>>> with open(target_file, "w") as stream:
...     stream.write("a = 2 + 2\n")
...     stream.write("print('hello')\n")
...     stream.write("b = 5 + a\n")
...     stream.write("print(b)\n")
...     stream.write("print('done')")
...
>>> change = session.run_file(target_file)

If any of the rules changed the source code in any way, run_file() returns a Change object (otherwise, if there are no changes, it will just return None). The change object contains both the original source code, as well as the potential transformation so we can compare it and apply it depending on our case. Let’s view the diff:

>>> print(change.compute_diff())
--- test.py
+++ test.py

@@ -1,5 +1,5 @@

-a = 2 + 2
+a = 4
 print('hello')
-b = 5 + a
+b = 9
-print(b)
+print(9)
 print('done')

As you can see, it shows what sort of changes would have been made without actually doing them. In some cases, the ability of a dry run like this would be very meaningful to observe how a rule interacts with source code before blindly trusting it. Now that we have verified that all the transformations make sense, we’ll continue and apply it directly to the source file.

run_file() won’t actually cause any changes until the user applies the diff manually so the file is still untouched.

>>> print(target_file.read_text())
a = 2 + 2
print('hello')
b = 5 + a
print(b)
print('done')

But once we move forward and call apply_diff(), it will be transformed in place.

>>> change.apply_diff()

And we can read the file again to verify the results:

>>> print(target_file.read_text())
a = 4
print('hello')
b = 9
print(9)
print('done')

Now if you try calling run_file() again on this file, you’ll get a None since there were no changes made.

>>> assert session.run_file(target_file) is None
>>>