Skip to content

Using the Curriculum Helpers

The test runner has access to a few helpers that can assist with testing campers’ code.

To instantiate the helper within a test block, use this:

const helper = new __helpers.CSSHelp(document);

In that example, the document variable refers to the document object of the test runner’s iframe.

There are a few methods available for parsing and testing CSS.

The .getStyle() method takes a CSS selector and returns a CSS style declaration object.

For example, if the camper has written the following CSS:

body {
background: linear-gradient(45deg, rgb(118, 201, 255), rgb(247, 255, 222));
margin: 0;
padding: 0;
width: 100%;
height: 100vh;
overflow: hidden;
}

If you call the getStyle method with body as an argument, you would get an object that looks like this:

{
0: "background-image",
1: "background-position-x",
2: "background-position-y",
3: "background-size",
4: "background-repeat-x",
5: "background-repeat-y",
6: "background-attachment",
7: "background-origin",
8: "background-clip",
9: "background-color",
10: "margin-top",
11: "margin-right",
12: "margin-bottom",
13: "margin-left",
14: "padding-top",
15: "padding-right",
16: "padding-bottom",
17: "padding-left",
18: "width",
19: "height",
20: "overflow-x",
21: "overflow-y",
"accentColor": "",
"additiveSymbols": "",
"alignContent": "",
"alignItems": "",
...
}

This method allows you to test that specific properties have been set:

assert.strictEqual(helper.getStyle('body')?.width, '100%');

The helper attaches a .getPropVal() method to the style declaration object that allows you to get the value of a specific property:

assert.strictEqual(helper.getStyle('body').getPropVal('width'), '100%');

On the style object returned, there is a native method called getPropertyValue that can retrieve CSS properties. More info at MDN Docs.

The .getStyleAny() method takes multiple CSS possible selectors and returns the first matching CSS style declaration object.

For example, if the camper has written the following CSS:

.sky {
background: radial-gradient(
closest-corner circle at 15% 15%,
#ffcf33,
#ffcf33 20%,
#ffff66 21%,
#bbeeff 100%
);
}

If you call the getStyleAny() method with .earth and .sky as arguments, you would get an object that looks like this:

{
0: "background-image",
1: "background-position-x",
2: "background-position-y",
3: "background-size",
4: "background-repeat-x",
5: "background-repeat-y",
6: "background-attachment",
7: "background-origin",
8: "background-clip",
9: "background-color",
10: "margin-top",
11: "margin-right",
12: "margin-bottom",
13: "margin-left",
14: "padding-top",
15: "padding-right",
16: "padding-bottom",
17: "padding-left",
18: "width",
19: "height",
20: "overflow-x",
21: "overflow-y",
"accentColor": "",
"additiveSymbols": "",
"alignContent": "",
"alignItems": "",
...
}

This method allows you to test that specific properties have been set:

assert.strictEqual(helper.getStyleAny(['.earth', '.sky'])?.width, '100%');

The helper attaches a .getPropVal() method to the style declaration object that allows you to get the value of a specific property:

assert.strictEqual(
helper.getStyleAny(['.earth', '.sky']).getPropVal('width'),
'100%'
);

The .getCSSRules() takes an at-rule type from the union media | fontface | import | keyframes, and returns an array of CSS rules matching that at-rule.

For example, if the camper has written the following code:

@media (max-width: 100px) {
body {
background-color: green;
}
}

Then the returned value of helper.getCSSRules('media') would be this array:

[
{
conditionText: "(max-width: 100px)",
cssRules: [
selectorText: 'body',
style: CSSStyleDeclaration,
styleMap: StylePropertyMap,
cssRules: CSSRuleList,
type: 1,
...
],
cssText: "@media (max-width: 100px) {\n body { background-color: green; }\n}",
...
}
]

You can then test that the camper has written the correct media query:

const hasCorrectHeight = helper
.getCSSRules('media')
.some(x => x.style.height === '3px');
assert.isTrue(hasCorrectHeight);

The .getRuleListsWithinMedia() method takes a media text (e.g. ("max-width: 200")) and returns the CSS rules within that media query.

The return result is the equivalent of that media rule’s cssRules property from the return value of .getCSSRules("media").

These methods are not as commonly used, but are available if needed.

The .getStyleDeclarations() method takes a CSS selector and returns an array of CSS style declaration objects (from the .getStyle() method).

The .isPropertyUsed() method takes a CSS property and checks if it has been set/used anywhere in the camper’s CSS.

The .getStyleRule() method takes a CSS selector and returns the CSS Style Declaration, much like .getStyle(). However, the declaration returned from this method includes an additional .isDeclaredAfter() method which takes a selector and returns whether this rule is declared after the selector passed in.

The .getStyleSheet() method returns the camper’s CSS, parsed into a CSS Style Sheet object.

The .selectorsFromSelector method returns all equivalent selectors from a given selector based on what’s nearby the given element.

For example if a is passed in a selector, it will return an array filled with several alternate selectors based on what’s nearby. The selectors could include a, label > a , label a , form label a and body form label a.

These mock methods that can prove to be unpredictable for testing purposes.

Mocks Math.random for testing purposes. Each time mock() is called the pseudo-random number generator is reset to its initial state, so that the same sequence of random numbers is generated each time. restore() restores the native Math.random function.

const randomMocker = new RandomMocker();
randomMocker.mock();
Math.random(); // first call is always 0.2523451747838408
Math.random(); // second call is always 0.08812504541128874
randomMocker.mock();
Math.random(); // generator is reset, so we get 0.2523451747838408 again
randomMocker.restore();
Math.random(); // back to native Math.random

Wraps an object’s property in a spy object that records calls and returns.

The spy’s calls property is an array of arrays, each containing the arguments passed to the method in the order it was called.

The spy’s returns property is an array of the return values of the method.

To restore the original method, call restore() on the spy object.

const obj = { method: (arg1, arg2) => `called by: ${arg1} + ${arg2}` };
const spy = helper.spyOn(obj, 'method');
obj.method('arg', 'another arg');
obj.method('2nd call');
spy.calls; // [["arg", "another arg"], ["2nd call"]]
spy.returns; // ["called by: arg + another arg", "called by: 2nd call + undefined"]
// to turn it off
spy.restore();

Wraps a method in a spy object that records calls to callbacks passed to that method.

The spy’s callbackSpies property is an array of arrays, where the inner arrays contain spy objects. One for each callback passed to the method. Each time the method is called, a new array of spies is created. i.e. the length of the callbackSpies array is equal to the number of times the spied on method was called.

The spies are either the original argument (if it was not a function) or a spy object with calls and returns properties. The calls is populated with the arguments passed to that callback and the returns with the return values of that callback.

const obj = {
method: (callback, value) => {
return arg1 => {
return callback(arg1, value);
};
}
};
const spy = helper.spyOnCallbacks(obj, 'method');
const cb = (arg1, arg2) => {
return `callback called with: ${arg1} + ${arg2}`;
};
const func = obj.method(cb, 'test');
func('one');
func('two');
obj.method(cb, 'test2')('another one');
spy.callbackSpies[0][0].calls; // [["one", "test"], ["two", "test"]]
spy.callbackSpies[0][0].returns; // ["callback called with: one + test", "callback called with: two + test"]
spy.callbackSpies[1][0].calls; // [["another one", "test2"]]
spy.callbackSpies[1][0].returns; // ["callback called with: another one + test2"]

Finally, the spy’s restore method restores the original method.

There are a few methods on the __helpers object to retrieve data from JavaScript methods.

These do NOT need to be instantiated; they are static methods.

This method checks the current code string to see if a function is called with any arguments.

If so, true is returned. Otherwise it returns false. Additionally, if the function call is commented out, false is returned.

This method combines and returns a compiled regex when given two regexes or strings.

For example, if .concatRegex is called with .* and b\s the resulting regex will be equivalent to .*b\s.

const regex1 = /a\s/;
const regex2 = /b/;
concatRegex(regex1, regex2).source === 'a\\sb';

Given a function name and, optionally, a list of parameters, returns a regex that can be used to match that function declaration in a code block.

const regex = functionRegex('foo', ['bar', 'baz']);
regex.test('function foo(bar, baz){}'); // true
regex.test('function foo(bar, baz, qux){}'); // false
regex.test('foo = (bar, baz) => {}'); // true

Options

  • capture: boolean - If true, the regex will capture the function definition, including its body, otherwise not. Defaults to false.
  • includeBody: boolean - If true, the regex will include the function body in the match. Otherwise it will stop at the first bracket. Defaults to true.
const regEx = functionRegex('foo', ['bar', 'baz'], { capture: true });
const combinedRegEx = concatRegex(/var x = "y"; /, regEx);
const match = `var x = "y";
function foo(bar, baz){}`.match(regex);
match[1]; // "function foo(bar, baz){}"
// i.e. only the function definition is captured
const regEx = functionRegex('foo', ['bar', 'baz'], { includeBody: false });
const match = `function foo(bar, baz){console.log('ignored')}`.match(regex);
match[1]; // "function foo(bar, baz){"

NOTE: capture does not work properly with arrow functions. It will capture text after the function body, too.

const regEx = functionRegex('myFunc', ['arg1'], { capture: true });
const match =
'myFunc = arg1 => arg1; console.log();\n // captured, unfortunately'.match(
regEx
);
match[1]; // "myFunc = arg1 => arg1; console.log();\n // captured, unfortunately"

There are a few methods on the __helpers object to remove content from the camper’s code.

These do NOT need to be instantiated they are static methods.

Using __helpers.removeCssComments(), __helpers.removeHtmlComments(), or __helpers.removeJSComments() allows you to pass the camper’s code (through the code variable) to remove comments matching the language’s comment syntax.

Using __helpers.removeWhiteSpace() allows you to pass the camper’s code (through the code variable) to remove all whitespace.

Permutates regular expressions or source strings, to create regex matching elements in any order.

const source1 = 'a';
const regex1 = /b/;
const source2 = 'c';
permutateRegex([source1, regex1, source2]).source ===
new RegExp(
/(?:a\s*\|\|\s*b\s*\|\|\s*c|b\s*\|\|\s*a\s*\|\|\s*c|c\s*\|\|\s*a\s*\|\|\s*b|a\s*\|\|\s*c\s*\|\|\s*b|b\s*\|\|\s*c\s*\|\|\s*a|c\s*\|\|\s*b\s*\|\|\s*a)/
).source;

Inputs can have capturing groups, but both groups and backreferrences need to be named. In the resulting regex they will be renamed to avoid duplicated names, and to allow backreferrences to refer to correct group.

const regex = permutateRegex(['a', /(?<ref>'|"|`)b\k<ref>/], {
elementsSeparator: String.raw`\s*===\s*`
});
regex.source ===
new RegExp(
/(?:a\s*===\s*(?<ref_0>'|"|`)b\k<ref_0>|(?<ref_1>'|"|`)b\k<ref_1>\s*===\s*a)/
).source;
regex.test('a === "b"'); // true
regex.test("'b' === a"); // true
regex.test('a === `b`'); // true
regex.test(`a === 'b"`); // false
  • capture: boolean - Whole regex is wrapped in regex group. If capture is true the group will be capturing, otherwise it will be non-capturing. Defaults to false.
  • elementsSeparator: string - Separates permutated elements within single permutation. Defaults to \s*\|\|\s*.
  • permutationsSeparator: string - Separates permutations. Defaults to |.
permutateRegex(['a', /b/, 'c'], { capture: true }).source ===
new RegExp(
/(a\s*\|\|\s*b\s*\|\|\s*c|b\s*\|\|\s*a\s*\|\|\s*c|c\s*\|\|\s*a\s*\|\|\s*b|a\s*\|\|\s*c\s*\|\|\s*b|b\s*\|\|\s*c\s*\|\|\s*a|c\s*\|\|\s*b\s*\|\|\s*a)/
).source;
permutateRegex(['a', /b/, 'c'], { elementsSeparator: ',' }).source ===
new RegExp(/(?:a,b,c|b,a,c|c,a,b|a,c,b|b,c,a|c,b,a)/).source;
permutateRegex(['a', /b/, 'c'], { permutationsSeparator: '&' }).source ===
new RegExp(
/(?:a\s*\|\|\s*b\s*\|\|\s*c&b\s*\|\|\s*a\s*\|\|\s*c&c\s*\|\|\s*a\s*\|\|\s*b&a\s*\|\|\s*c\s*\|\|\s*b&b\s*\|\|\s*c\s*\|\|\s*a&c\s*\|\|\s*b\s*\|\|\s*a)/
).source;

These helpers need to be run in Python and tests that use them need to be in the format:

({ test: () => assert(runPython(`<python code>`)) });

_Node is a chainable class that allows you to call methods on the result of parsing a string. To create an instance of _Node that parses the camper’s code use _Node(_code).

To access a specific statement within the code you need to chain method calls until you reach the desired scope.

For example, if the camper has written the following code:

class Spam:
def __str__(self):
return 'spam!'

To check the return value of __str__ you would write:

({
test: () =>
assert(
runPython(
`_Node(_code).find_class("Spam").find_function("__str__").has_return("'spam!'")`
)
)
});

AST-based helpers are also exposed in the Python challenges in ast_helpers module. _Node class corresponds to the ast_helpers.Node:

import ast_helpers
code = '''
class Spam:
def __str__(self):
return 'spam!'
'''
print(
ast_helpers.Node(code)
.find_class("Spam")
.find_function("__str__")
.has_return("'spam!'")
)

str returns a string that would parse to the same AST as the node. For example:

function_str = """
def foo():
# will not be in the output
x = 1
"""
output_str = """
def foo():
x = 1"""
str(Node(function_str)) == output_str # True

The output and source string compile to the same AST, but the output is indented with 4 spaces. Comments and trailing whitespace are removed.

find_ functions search the current scope and return one of the following:

  • A single Node object if there can be only one match. E.g. find_function
  • A list of Node objects if there can be multiple matches. E.g.: find_ifs
Node('def foo():\n x = "1"').find_function("foo").has_variable("x") # True
code_str = """
class Spam(ABC):
@property
@abstractmethod
def foo(self):
return self.x
@foo.setter
@abstractmethod
def foo(self, new_x):
self.x = new_x
"""
explorer = Node(code_str)
len(explorer.find_class("Spam").find_functions("foo")) # 2
explorer.find_class("Spam").find_functions("foo")[0].is_equivalent("@property\n@abstractmethod\ndef foo(self):\n return self.x") # True
explorer.find_class("Spam").find_functions("foo")[1].is_equivalent("@foo.setter\n@abstractmethod\ndef foo(self, new_x):\n self.x = new_x") # True
Node('async def foo():\n await bar()').find_async_function("foo").is_equivalent("async def foo():\n await bar()") # True
code_str = """
async def foo(spam):
if spam:
await spam()
await bar()
await func()
"""
explorer = Node(code_str)
explorer.find_async_function("foo").find_awaits()[0].is_equivalent("await bar()") # True
explorer.find_async_function("foo").find_awaits()[1].is_equivalent("await func()") # True
explorer.find_async_function("foo").find_ifs()[0].find_awaits()[0].is_equivalent("await spam()") # True
Node("y = 2\nx = 1").find_variable("x").is_equivalent("x = 1")
Node("a: int = 1").find_variable("a").is_equivalent("a: int = 1")
Node("self.spam = spam").find_variable("self.spam").is_equivalent("self.spam = spam")

Returns a list of all variable assignments with the given name (unlike find_variable which returns only the first match).

code_str = """
x: int = 0
a.b = 0
x = 5
a.b = 2
x = 10
"""
node = Node(code_str)
len(node.find_variables("x")) # 3
node.find_variables("x")[0].is_equivalent("x: int = 0") # True
node.find_variables("x")[1].is_equivalent("x = 5") # True
node.find_variables("x")[2].is_equivalent("x = 10") # True
len(node.find_variables("a.b")) # 2
node.find_variables("a.b")[0].is_equivalent("a.b = 0") # True
node.find_variables("a.b")[1].is_equivalent("a.b = 2") # True
Node("x += 1").find_aug_variable("x").is_equivalent("x += 1")
Node("x -= 1").find_aug_variable("x").is_equivalent("x -= 1")

When the variable is out of scope, find_variable returns an None node (i.e. Node() or Node(None)):

Node('def foo():\n x = "1"').find_variable("x") == Node() # True
func_str = """
def foo():
x = 1"""
Node(func_str).find_function("foo").find_body().is_equivalent("x = 1") # True
code_str = """
def foo():
if x == 1:
return False
return True
"""
Node(code_str).find_function("foo").find_return().is_equivalent("return True") # True
Node(code_str).find_function("foo").find_ifs()[0].find_return().is_equivalent("return False") # True
code_str = """
print(1)
print(2)
foo("spam")
obj.foo("spam")
obj.bar.foo("spam")
"""
explorer = Node(code_str)
len(explorer.find_calls("print")) # 2
explorer.find_calls("print")[0].is_equivalent("print(1)")
explorer.find_calls("print")[1].is_equivalent("print(2)")
len(explorer.find_calls("foo")) # 3
explorer.find_calls("foo")[0].is_equivalent("foo('spam')")
explorer.find_calls("foo")[1].is_equivalent("obj.foo('spam')")
explorer.find_calls("foo")[2].is_equivalent("obj.bar.foo('spam')")
explorer = Node("print(1, 2)")
len(explorer.find_calls("print")[0].find_call_args()) # 2
explorer.find_calls("print")[0].find_call_args()[0].is_equivalent("1")
explorer.find_calls("print")[0].find_call_args()[1].is_equivalent("2")
class_str = """
class Foo:
def __init__(self):
pass
"""
Node(class_str).find_class("Foo").has_function("__init__") # True
if_str = """
if x == 1:
x += 1
elif x == 2:
pass
else:
return
if True:
pass
"""
Node(if_str).find_ifs()[0].is_equivalent("if x == 1:\n x += 1\nelif x == 2:\n pass\nelse:\n return")
Node(if_str).find_ifs()[1].is_equivalent("if True:\n pass")
if_str = """
if x == 1:
x += 1
elif x == 2:
pass
else:
return
if True:
pass
"""
Node(if_str).find_if("x == 1").is_equivalent("if x == 1:\n x += 1\nelif x == 2:\n pass\nelse:\n return")
Node(if_str).find_if("True").is_equivalent("if True:\n pass")
while_str = """
while True:
x += 1
else:
return
while False:
pass
"""
explorer = Node(while_str)
explorer.find_whiles()[0].is_equivalent("while True:\n x += 1\nelse:\n return") # True
explorer.find_whiles()[1].is_equivalent("while False:\n pass") # True
while_str = """
while True:
x += 1
else:
return
while False:
pass
"""
explorer = Node(while_str)
explorer.find_while("True").is_equivalent("while True:\n x += 1\nelse:\n return") # True
explorer.find_while("False").is_equivalent("while False:\n pass") # True
if_str = """
if x > 0:
x = 1
elif x < 0:
x = -1
else:
return x
"""
explorer1 = Node(if_str)
len(explorer1.find_ifs()[0].find_conditions()) # 3
explorer1.find_ifs()[0].find_conditions()[0].is_equivalent("x > 0") # True
explorer1.find_ifs()[0].find_conditions()[1].is_equivalent("x < 0") # True
explorer1.find_ifs()[0].find_conditions()[2] == Node() # True
Node("x = 1").find_conditions() # []
while_str = """
while True:
x += 1
else:
return
while False:
pass
"""
explorer2 = Node(while_str)
explorer2.find_whiles()[0].find_conditions()[0].is_equivalent("True") # True
explorer2.find_whiles()[0].find_conditions()[1] == Node() # True
explorer2.find_whiles()[1].find_conditions()[0].is_equivalent("False") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for_loops()[0].is_equivalent("for x, y in enumerate(dict):\n print(x, y)\nelse:\n pass") # True
explorer.find_for_loops()[1].is_equivalent("for i in range(4):\n pass") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for("(x, y)", "enumerate(dict)").is_equivalent("for x, y in enumerate(dict):\n print(x, y)\nelse:\n pass") # True
explorer.find_for("i", "range(4)").is_equivalent("for i in range(4):\n pass") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for_loops()[0].find_for_vars().is_equivalent("(x, y)") # True
explorer.find_for_loops()[1].find_for_vars().is_equivalent("i") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
pass
for i in range(4):
pass
"""
explorer = Node(for_str)
explorer.find_for_loops()[0].find_for_iter().is_equivalent("enumerate(dict)") # True
explorer.find_for_loops()[1].find_for_iter().is_equivalent("range(4)") # True
if_str = """
if True:
x = 1
elif False:
x = 2
"""
explorer1 = Node(if_str)
explorer1.find_ifs()[0].find_bodies()[0].is_equivalent("x = 1") # True
explorer1.find_ifs()[0].find_bodies()[1].is_equivalent("x = 2") # True
while_str = """
while True:
x += 1
else:
x = 0
while False:
pass
"""
explorer2 = Node(while_str)
explorer2.find_whiles()[0].find_bodies()[0].is_equivalent("x += 1") # True
explorer2.find_whiles()[0].find_bodies()[1].is_equivalent("x = 0") # True
explorer2.find_whiles()[1].find_bodies()[0].is_equivalent("pass") # True
for_str = """
dict = {'a': 1, 'b': 2, 'c': 3}
for x, y in enumerate(dict):
print(x, y)
else:
print(x)
for i in range(4):
pass
"""
explorer3 = Node(for_str)
explorer3.find_for_loops()[0].find_bodies()[0].is_equivalent("print(x, y)") # True
explorer3.find_for_loops()[0].find_bodies()[1].is_equivalent("print(x)") # True
explorer3.find_for_loops()[1].find_bodies()[0].is_equivalent("pass") # True
code_str = """
import ast, sys
from math import factorial as f
"""
explorer = Node(code_str)
len(explorer.find_imports()) # 2
explorer.find_imports()[0].is_equivalent("import ast, sys")
explorer.find_imports()[1].is_equivalent("from math import factorial as f")

Returns a list of list comprehensions, set comprehensions, dictionary comprehensions and generator expressions nodes not assigned to a variable or part of other statements.

code_str = """
[i**2 for i in lst]
(i for i in lst)
{i * j for i in spam for j in lst}
{k: v for k,v in dict}
comp = [i for i in lst]
"""
explorer = Node(code_str)
len(explorer.find_comps()) # 4
explorer.find_comps()[0].is_equivalent("[i**2 for i in lst]")
explorer.find_comps()[1].is_equivalent("(i for i in lst)")
explorer.find_comps()[2].is_equivalent("{i * j for i in spam for j in lst}")
explorer.find_comps()[3].is_equivalent("{k: v for k,v in dict}")

Returns a list of comprehension/generator expression iterables. It can be chained to find_variable, find_return, find_call_args()[n].

code_str = """
x = [i**2 for i in lst]
def foo(spam):
return [i * j for i in spam for j in lst]
"""
explorer = Node(code_str)
len(explorer.find_variable("x").find_comp_iters()) # 1
explorer.find_variable("x").find_comp_iters()[0].is_equivalent("lst")
len(explorer.find_function("foo").find_return().find_comp_iters()) # 2
explorer.find_function("foo").find_return().find_comp_iters()[0].is_equivalent("spam")
explorer.find_function("foo").find_return().find_comp_iters()[1].is_equivalent("lst")

Returns a list of comprhension/generator expression targets (i.e. the iteration variables).

code_str = """
x = [i**2 for i in lst]
def foo(spam):
return [i * j for i in spam for j in lst]
"""
explorer = Node(code_str)
len(explorer.find_variable("x").find_comp_targets()) # 1
explorer.find_variable("x").find_comp_targets()[0].is_equivalent("i")
len(explorer.find_function("foo").find_return().find_comp_targets()) # 2
explorer.find_function("foo").find_return().find_comp_targets()[0].is_equivalent("i")
explorer.find_function("foo").find_return().find_comp_targets()[1].is_equivalent("j")

Returns the dictionary comprehension key.

code_str = """
x = {k: v for k,v in dict}
def foo(spam):
return {k: v for k in spam for v in lst}
"""
explorer = Node(code_str)
explorer.find_variable("x").find_comp_key().is_equivalent("k")
explorer.find_function("foo").find_return().find_comp_key().is_equivalent("k")

Returns the expression evaluated at each iteration of comprehensions/generator expressions. It includes the if/else portion. In case only the if is present, use find_comp_ifs.

code_str = """
x = [i**2 if i else -1 for i in lst]
def foo(spam):
return [i * j for i in spam for j in lst]
"""
explorer = Node(code_str)
explorer.find_variable("x").find_comp_expr().is_equivalent("i**2 if i else -1")
explorer.find_function("foo").find_return().find_comp_expr().is_equivalent("i * j")

Returns a list of comprehension/generator expression if conditions. The if/else construct instead is part of the expression and is found with find_comp_expr.

code_str = """
x = [i**2 if i else -1 for i in lst]
def foo(spam):
return [i * j for i in spam if i > 0 for j in lst if j != 6]
"""
explorer = Node(code_str)
len(explorer.find_variable("x").find_comp_ifs()) # 0
len(explorer.find_function("foo").find_return().find_comp_ifs()) # 2
explorer.find_function("foo").find_return().find_comp_ifs()[0].is_equivalent("i > 0")
explorer.find_function("foo").find_return().find_comp_ifs()[1].is_equivalent("j != 6")

Returns a list of all try statements.

code_str = """
try:
x = 1 / 0
except ZeroDivisionError:
print("division by zero")
else:
print("no error")
finally:
print("cleanup")
try:
y = int("abc")
except:
pass
"""
node = Node(code_str)
len(node.find_trys()) # 2
node.find_trys()[0].is_equivalent("try:\n x = 1 / 0\nexcept ZeroDivisionError:\n print('division by zero')\nelse:\n print('no error')\nfinally:\n print('cleanup')") # True
node.find_trys()[1].is_equivalent("try:\n y = int('abc')\nexcept:\n pass") # True

Returns a list of all except handlers in a try statement.

code_str = """
try:
x = 1 / 0
except ZeroDivisionError:
print("division by zero")
except ValueError as e:
print(f"value error: {e}")
except:
print("other error")
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
len(try_stmt.find_excepts()) # 3

is_equivalent() cannot be used on the items found by find_excepts() because a standalone except raises a SyntaxError.

Returns a specific except handler matching the exception type and optional variable name.

code_str = """
try:
x = 1 / 0
except ZeroDivisionError:
print("division by zero")
except ValueError as e:
print(f"value error: {e}")
except:
print("other error")
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
try_stmt.find_except("ZeroDivisionError").find_body().is_equivalent("print('division by zero')") # True
try_stmt.find_except("ValueError", "e").find_body().is_equivalent("print(f'value error: {e}')") # True
try_stmt.find_except().find_body().is_equivalent("print('other error')") # True (bare except)

Returns True if a try statement has an except handler for the given exception type and variable name.

code_str = """
try:
x = 1 / 0
except ZeroDivisionError:
print("division by zero")
except ValueError as e:
print(f"value error: {e}")
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
try_stmt.has_except("ZeroDivisionError") # True
try_stmt.has_except("ValueError", "e") # True
try_stmt.has_except("ValueError") # False

Returns the else block of a try statement.

code_str = """
try:
x = 1
except ValueError:
print("error")
else:
print("success")
x = 2
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
try_stmt.find_try_else().is_equivalent("print('success')\nx = 2") # True

Returns the finally block of a try statement.

code_str = """
try:
x = 1
except ValueError:
print("error")
finally:
print("cleanup")
x = None
"""
node = Node(code_str)
try_stmt = node.find_trys()[0]
try_stmt.find_finally().is_equivalent("print('cleanup')\nx = None") # True

Returns a list of match statements.

code_str = """
match x:
case 0:
pass
case _:
pass
match y:
case 1:
pass
"""
node = Node(code_str)
len(node.find_matches()) # 2
node.find_matches()[0].is_equivalent("match x:\n case 0:\n pass\n case _:\n pass") # True

Returns the subject of a match statement.

code_str = """
match x:
case 0:
pass
"""
node = Node(code_str)
node.find_matches()[0].find_match_subject().is_equivalent("x") # True

Returns a list of case blocks in a match statement.

code_str = """
match x:
case 0:
print(0)
print('spam')
case _:
pass
"""
node = Node(code_str)
len(node.find_matches()[0].find_match_cases()) # 2
node.find_matches()[0].find_match_cases()[0].find_body().is_equivalent("print(0)\nprint('spam')") # True
node.find_matches()[0].find_match_cases()[1].find_body().is_equivalent("pass") # True

is_equivalent() cannot be used on the items found by find_match_cases() because a standalone case raises a SyntaxError.

Returns the pattern of a case block.

code_str = """
match x:
case 0:
pass
case [a, b]:
pass
case _:
pass
"""
node = Node(code_str)
node.find_matches()[0].find_match_cases()[0].find_case_pattern().is_equivalent("0") # True
node.find_matches()[0].find_match_cases()[1].find_case_pattern().is_equivalent("[a, b]") # True
node.find_matches()[0].find_match_cases()[2].find_case_pattern().is_equivalent("_") # True

Returns the guard condition of a case block.

code_str = """
match x:
case 0 if y > 0:
pass
case [a, b] if y == -1:
pass
case _:
pass
"""
node = Node(code_str)
node.find_matches()[0].find_match_cases()[0].find_case_guard().is_equivalent("y > 0") # True
node.find_matches()[0].find_match_cases()[1].find_case_guard().is_equivalent("y == -1") # True
node.find_matches()[0].find_match_cases()[2].find_case_guard().is_empty() # True (no guard)

get_ functions return the value of the node, not the node itself.

Node("x = 1").get_variable("x") # 1

has_ functions return a boolean indicating whether the node exists.

Node("x = 1").has_variable("x") # True
Node("def foo():\n pass").has_function("foo") # True

Returns a boolean indicating if the specified statement is found.

Node("name = input('hi')\nself.matrix[1][5] = 3").has_stmt("self.matrix[1][5] = 3") # True
Node("def foo(*, a, b, c=0):\n pass").find_function("foo").has_args("*, a, b, c=0") # True
Node("def foo():\n pass").find_function("foo").has_pass() # True
Node("if x==1:\n x+=1\nelse: pass").find_ifs()[0].find_bodies()[1].has_pass() # True

Returns a boolean indicating if the function returns the specified expression/object.

code_str = """
def foo():
if x == 1:
return False
return True
"""
explorer = Node(code_str)
explorer.find_function("foo").has_return("True") # True
explorer.find_function("foo").find_ifs()[0].has_return("False") # True

Returns a boolean indicating if the function has the specified return annotation.

Node("def foo() -> int:\n return 0").find_function("foo").has_returns("int") # True
Node("def foo() -> 'spam':\n pass").find_function("foo").has_returns("spam") # True
code_str = """
class A:
@property
@staticmethod
def foo():
pass
"""
Node(code_str).find_class("A").find_function("foo").has_decorators("property") # True
Node(code_str).find_class("A").find_function("foo").has_decorators("property", "staticmethod") # True
Node(code_str).find_class("A").find_function("foo").has_decorators("staticmethod", "property") # False, order does matter
code_str = """
print(math.sqrt(25))
if True:
spam()
"""
explorer = Node(code_str)
explorer.has_call("print(math.sqrt(25))")
explorer.find_ifs()[0].find_bodies()[0].has_call("spam()")

Checks if a function/method call exists within a specific function or the entire code.

code_str = """
srt = sorted([5, 1, 9])
def foo(lst):
return sorted(lst)
def spam(lst):
return lst.sort()
def eggs(dictionary):
if True:
k = dictionary.get(key)
"""
node = Node(code_str)
node.block_has_call("sorted", "foo") # True
node.block_has_call("sorted") # True
node.block_has_call("sort", "spam") # True
node.block_has_call("get", "eggs") # True
node.block_has_call("get") # True
node.block_has_call("split") # False
code_str = """
import ast, sys
from math import factorial as f
"""
explorer = Node(code_str)
explorer.has_import("import ast, sys")
explorer.has_import("from math import factorial as f")
code_str = """
class spam:
pass
"""
Node(code_str).has_class("spam")

This is a somewhat loose check. The AST of the target string and the AST of the node do not have to be identical, but the code must be equivalent.

Node("x = 1").is_equivalent("x = 1") # True
Node("\nx = 1").is_equivalent("x = 1") # True
Node("x = 1").is_equivalent("x = 2") # False

This is syntactic sugar for == Node().

Node().is_empty() # True
Node("x = 1").find_variable("x").is_empty() # False

This allows you to check if the return value of a function call is assigned to a variable.

explorer = Node("def foo():\n x = bar()")
explorer.find_function("foo").find_variable("x").value_is_call("bar") # True
Node("x = 1").find_variable("x").is_integer() # True
Node("x = '1'").find_variable("x").is_integer() # False
Node("class C(A, B):\n pass").find_class("C").inherits_from("A") # True
Node("class C(A, B):\n pass").find_class("C").inherits_from("A", "B") # True

Returs a boolean indicating if the statements passed as arguments are found in the same order in the tree (statements can be non-consecutive)

code_str = """
x = 1
if x:
print("x is:")
y = 0
print(x)
return y
x = 0
"""
if_str = """
if x:
print("x is:")
y = 0
print(x)
return y
"""
explorer = Node(code_str)
explorer.is_ordered("x=1", "x=0") # True
explorer.is_ordered("x=1", if_str, "x=0") # True
explorer.find_ifs()[0].is_ordered("print('x is:')", "print(x)", "return y") # True
explorer.is_ordered("x=0", "x=1") # False
explorer.find_ifs()[0].is_ordered("print(x)", "print('x is:')") # False
stmts = """
if True:
pass
x = 1
"""
Node(stmts)[1].is_equivalent("x = 1") # True
  • Python does not allow newline characters between keywords and their arguments. E.g:
def
add(x, y):
return x + y
  • Python does allow newline characters between function arguments. E.g:
def add(
x,
y
):
return x + y