Creating and Applying Changes

Once you’ve found some interesting Nodes, you can generate changes to the underlying code and apply them to the project.

Here’s how that works:

  1. Create a ChangeSet object (code_monkey.edit.ChangeSet). This tracks a collection of changes and prevents conflicts.
  2. Create Change objects, which specify a region of code to overwrite or append to, and what new code should go there.
  3. Add Changes to your ChangeSet (if two changes overlap, this will raise an exception).
  1. Preview your changes using ChangeSet.diff().
  2. Apply your changes to the filesystem using ChangeSet.commit().

Creating Change objects by hand would be extremely tedious, so we’ve got a bunch of helpers on every Node to generate them. They live under the Node’s .change property.

Here’s what it looks like in action – we’ll make some edits to the code_monkey test project:

from code_monkey.node_query import project_query
from code_monkey.edit import ChangeSet

q = project_query('./test_project')

# select every class in the project
targets = q.flatten().classes()

changeset = ChangeSet()

for class_node in targets:
    changeset.add(class_node.change.inject_at_body_line(0,
        '    foo = "bar"\n'))

print(changeset.diff())

The output, then is a typical diff:

Changes:

--- ./test_project/lib/edge_cases.py
+++ ./test_project/lib/edge_cases.py
@@ -6,6 +6,7 @@
         Underground,
         CanHire,
         ProducesPonkeys):
+    foo = "bar"
     pass #Lair body starts here

 #multiline function signature
@@ -25,4 +26,5 @@

 #uses dots (a 'getattr node') when specifying parent class
 class WeirdSubclass(datetime.datetime):
+    foo = "bar"
     pass #WeirdSubclass body starts here--- ./test_project/lib/employee.py
+++ ./test_project/lib/employee.py
@@ -2,6 +2,7 @@
 from test_project import settings

 class Employee(object):
+    foo = "bar"

     def __init__(self, first_name, last_name):
         self.first_name = first_name
@@ -13,6 +14,7 @@


 class CodeMonkey(Employee):
+    foo = "bar"
     """He writes code."""
     def __init__(self, *args, **kwargs):
         super(CodeMonkey, self).__init__(*args, **kwargs)

If you’re happy with your changes, you can apply them by changing the last line from print(changeset.diff()) to changeset.commit().

The ChangeGenerator API

Most of the actual power here is in the .change property, which is an instance of the code_monkey.change.ChangeGenerator class. ChangeGenerators contain a variety of methods that return Change objects representing common transformations. Here’s everything that ChangeGenerator can do:

class code_monkey.change.ChangeGenerator(node)[source]

Generates change tuples for a specific node. Every node with a source file should have one, as its .change property.

So, a typical use might be: change = node.change.overwrite(“newtext”)

inject_after(inject_source)[source]

Generate a change that inserts inject_source starting on the line after this node.

inject_at_body_index(index, inject_source)[source]

Generate a change that inserts inject_source into the node, starting at index. index is relative to the beginning of the node body, not the beginning of the file.

inject_at_body_line(line_index, inject_source)[source]

As inject_at_body_index, but takes a line index instead of a character index.

inject_at_index(index, inject_source)[source]

Generate a change that inserts inject_source into the node, starting at index. index is relative to the beginning of the node, not the beginning of the file.

inject_at_line(line_index, inject_source)[source]

As inject_at_index, but takes a line index instead of a character index.

inject_before(inject_source)[source]

Generate a change that inserts inject_source starting on the line before this node.

overwrite(new_source)[source]

Generate a change that overwrites the contents of the Node entirely with new_source

overwrite_body(new_source)[source]

Generate a change that overwrites the body of the node with new_source. In the case of a ModuleNode, this is equivalent to overwrite().

Change Objects

If there’s something particularly finnicky that you can’t do with .change, you can always create Change objects by hand (that’s all that ChangeGenerator does). The API is:

class code_monkey.change.Change(path, start, end, new_text)[source]

A single change to make to a single file. Replaces the file content from indices start through (end-1) with new_text.

Parameters:
  • path (str) – The filesystem path of the file to edit.
  • start (int) – The index of the beginning of the region to overwrite.
  • end (int) – The index of the end of the region to overwrite (non-inclusive).
  • new_text (str) – The new text to write over the old region.

Note that you can insert without overwriting by making the start and end parameters the same.