Using the Curriculum Helpers
The test runner has access to a few helpers that can assist with testing campers’ code.
CSS Helper
Section titled “CSS Helper”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.
Methods
Section titled “Methods”There are a few methods available for parsing and testing CSS.
.getStyle()
Section titled “.getStyle()”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.
.getStyleAny()
Section titled “.getStyleAny()”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%');
.getCSSRules()
Section titled “.getCSSRules()”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);
.getRuleListsWithinMedia()
Section titled “.getRuleListsWithinMedia()”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")
.
Less Frequent Methods
Section titled “Less Frequent Methods”These methods are not as commonly used, but are available if needed.
.getStyleDeclarations()
Section titled “.getStyleDeclarations()”The .getStyleDeclarations()
method takes a CSS selector and returns an array of CSS style declaration objects (from the .getStyle()
method).
.isPropertyUsed()
Section titled “.isPropertyUsed()”The .isPropertyUsed()
method takes a CSS property and checks if it has been set/used anywhere in the camper’s CSS.
.getStyleRule()
Section titled “.getStyleRule()”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.
.getStyleSheet()
Section titled “.getStyleSheet()”The .getStyleSheet()
method returns the camper’s CSS, parsed into a CSS Style Sheet object.
.selectorsFromSelector()
Section titled “.selectorsFromSelector()”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
.
JS Helper
Section titled “JS Helper”Mockers
Section titled “Mockers”These mock methods that can prove to be unpredictable for testing purposes.
RandomMocker
Section titled “RandomMocker”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.2523451747838408Math.random(); // second call is always 0.08812504541128874randomMocker.mock();Math.random(); // generator is reset, so we get 0.2523451747838408 againrandomMocker.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 offspy.restore();
spyOnCallbacks
Section titled “spyOnCallbacks”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.
Static Methods
Section titled “Static Methods”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.
.isCalledWithNoArgs()
Section titled “.isCalledWithNoArgs()”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.
.concatRegex()
Section titled “.concatRegex()”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';
.functionRegex()
Section titled “.functionRegex()”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){}'); // trueregex.test('function foo(bar, baz, qux){}'); // falseregex.test('foo = (bar, baz) => {}'); // true
Options
capture: boolean
- Iftrue
, the regex will capture the function definition, including its body, otherwise not. Defaults tofalse
.includeBody: boolean
- Iftrue
, the regex will include the function body in the match. Otherwise it will stop at the first bracket. Defaults totrue
.
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"
Strip Content
Section titled “Strip Content”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.
Removing Comments
Section titled “Removing Comments”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.
Removing Whitespace
Section titled “Removing Whitespace”Using __helpers.removeWhiteSpace()
allows you to pass the camper’s code (through the code
variable) to remove all whitespace.
permutateRegex
Section titled “permutateRegex”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"'); // trueregex.test("'b' === a"); // trueregex.test('a === `b`'); // trueregex.test(`a === 'b"`); // false
Options
Section titled “Options”- capture: boolean - Whole regex is wrapped in regex group. If
capture
istrue
the group will be capturing, otherwise it will be non-capturing. Defaults tofalse
. - 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;
AST-based Helpers
Section titled “AST-based Helpers”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!'"))
Methods
Section titled “Methods”Formatting output
Section titled “Formatting output”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.
Finding nodes
Section titled “Finding nodes”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
find_function()
Section titled “find_function()”Node('def foo():\n x = "1"').find_function("foo").has_variable("x") # True
find_functions()
Section titled “find_functions()”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")) # 2explorer.find_class("Spam").find_functions("foo")[0].is_equivalent("@property\n@abstractmethod\ndef foo(self):\n return self.x") # Trueexplorer.find_class("Spam").find_functions("foo")[1].is_equivalent("@foo.setter\n@abstractmethod\ndef foo(self, new_x):\n self.x = new_x") # True
find_async_function()
Section titled “find_async_function()”Node('async def foo():\n await bar()').find_async_function("foo").is_equivalent("async def foo():\n await bar()") # True
find_awaits()
Section titled “find_awaits()”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()") # Trueexplorer.find_async_function("foo").find_awaits()[1].is_equivalent("await func()") # Trueexplorer.find_async_function("foo").find_ifs()[0].find_awaits()[0].is_equivalent("await spam()") # True
find_variable()
Section titled “find_variable()”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")
find_variables()
Section titled “find_variables()”Returns a list of all variable assignments with the given name (unlike find_variable
which returns only the first match).
code_str = """x: int = 0a.b = 0x = 5a.b = 2x = 10"""node = Node(code_str)len(node.find_variables("x")) # 3node.find_variables("x")[0].is_equivalent("x: int = 0") # Truenode.find_variables("x")[1].is_equivalent("x = 5") # Truenode.find_variables("x")[2].is_equivalent("x = 10") # Truelen(node.find_variables("a.b")) # 2node.find_variables("a.b")[0].is_equivalent("a.b = 0") # Truenode.find_variables("a.b")[1].is_equivalent("a.b = 2") # True
find_aug_variable()
Section titled “find_aug_variable()”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
find_body()
Section titled “find_body()”func_str = """def foo(): x = 1"""Node(func_str).find_function("foo").find_body().is_equivalent("x = 1") # True
find_return()
Section titled “find_return()”code_str = """def foo(): if x == 1: return False return True"""Node(code_str).find_function("foo").find_return().is_equivalent("return True") # TrueNode(code_str).find_function("foo").find_ifs()[0].find_return().is_equivalent("return False") # True
find_calls()
Section titled “find_calls()” code_str = """print(1)print(2)foo("spam")obj.foo("spam")obj.bar.foo("spam")"""explorer = Node(code_str)len(explorer.find_calls("print")) # 2explorer.find_calls("print")[0].is_equivalent("print(1)")explorer.find_calls("print")[1].is_equivalent("print(2)")len(explorer.find_calls("foo")) # 3explorer.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')")
find_call_args()
Section titled “find_call_args()”explorer = Node("print(1, 2)")len(explorer.find_calls("print")[0].find_call_args()) # 2explorer.find_calls("print")[0].find_call_args()[0].is_equivalent("1")explorer.find_calls("print")[0].find_call_args()[1].is_equivalent("2")
find_class()
Section titled “find_class()”class_str = """class Foo: def __init__(self): pass"""Node(class_str).find_class("Foo").has_function("__init__") # True
find_ifs()
Section titled “find_ifs()”if_str = """if x == 1: x += 1elif x == 2: passelse: 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")
find_if()
Section titled “find_if()”if_str = """if x == 1: x += 1elif x == 2: passelse: 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")
find_whiles()
Section titled “find_whiles()”while_str = """while True: x += 1else: return
while False: pass"""explorer = Node(while_str)explorer.find_whiles()[0].is_equivalent("while True:\n x += 1\nelse:\n return") # Trueexplorer.find_whiles()[1].is_equivalent("while False:\n pass") # True
find_while()
Section titled “find_while()”while_str = """while True: x += 1else: return
while False: pass"""explorer = Node(while_str)explorer.find_while("True").is_equivalent("while True:\n x += 1\nelse:\n return") # Trueexplorer.find_while("False").is_equivalent("while False:\n pass") # True
find_conditions()
Section titled “find_conditions()”if_str = """if x > 0: x = 1elif x < 0: x = -1else: return x"""explorer1 = Node(if_str)len(explorer1.find_ifs()[0].find_conditions()) # 3explorer1.find_ifs()[0].find_conditions()[0].is_equivalent("x > 0") # Trueexplorer1.find_ifs()[0].find_conditions()[1].is_equivalent("x < 0") # Trueexplorer1.find_ifs()[0].find_conditions()[2] == Node() # TrueNode("x = 1").find_conditions() # []
while_str = """while True: x += 1else: return
while False: pass"""explorer2 = Node(while_str)explorer2.find_whiles()[0].find_conditions()[0].is_equivalent("True") # Trueexplorer2.find_whiles()[0].find_conditions()[1] == Node() # Trueexplorer2.find_whiles()[1].find_conditions()[0].is_equivalent("False") # True
find_for_loops()
Section titled “find_for_loops()”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") # Trueexplorer.find_for_loops()[1].is_equivalent("for i in range(4):\n pass") # True
find_for()
Section titled “find_for()”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") # Trueexplorer.find_for("i", "range(4)").is_equivalent("for i in range(4):\n pass") # True
find_for_vars()
Section titled “find_for_vars()”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)") # Trueexplorer.find_for_loops()[1].find_for_vars().is_equivalent("i") # True
find_for_iter()
Section titled “find_for_iter()”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)") # Trueexplorer.find_for_loops()[1].find_for_iter().is_equivalent("range(4)") # True
find_bodies()
Section titled “find_bodies()”if_str = """if True: x = 1elif False: x = 2"""explorer1 = Node(if_str)explorer1.find_ifs()[0].find_bodies()[0].is_equivalent("x = 1") # Trueexplorer1.find_ifs()[0].find_bodies()[1].is_equivalent("x = 2") # True
while_str = """while True: x += 1else: x = 0
while False: pass"""explorer2 = Node(while_str)explorer2.find_whiles()[0].find_bodies()[0].is_equivalent("x += 1") # Trueexplorer2.find_whiles()[0].find_bodies()[1].is_equivalent("x = 0") # Trueexplorer2.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)") # Trueexplorer3.find_for_loops()[0].find_bodies()[1].is_equivalent("print(x)") # Trueexplorer3.find_for_loops()[1].find_bodies()[0].is_equivalent("pass") # True
find_imports()
Section titled “find_imports()”code_str = """import ast, sysfrom math import factorial as f"""
explorer = Node(code_str)len(explorer.find_imports()) # 2explorer.find_imports()[0].is_equivalent("import ast, sys")explorer.find_imports()[1].is_equivalent("from math import factorial as f")
find_comps()
Section titled “find_comps()”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()) # 4explorer.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}")
find_comp_iters()
Section titled “find_comp_iters()”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()) # 1explorer.find_variable("x").find_comp_iters()[0].is_equivalent("lst")
len(explorer.find_function("foo").find_return().find_comp_iters()) # 2explorer.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")
find_comp_targets()
Section titled “find_comp_targets()”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()) # 1explorer.find_variable("x").find_comp_targets()[0].is_equivalent("i")
len(explorer.find_function("foo").find_return().find_comp_targets()) # 2explorer.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")
find_comp_key()
Section titled “find_comp_key()”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")
find_comp_expr()
Section titled “find_comp_expr()”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")
find_comp_ifs()
Section titled “find_comp_ifs()”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()) # 2explorer.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")
find_trys()
Section titled “find_trys()”Returns a list of all try statements.
code_str = """try: x = 1 / 0except 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()) # 2node.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')") # Truenode.find_trys()[1].is_equivalent("try:\n y = int('abc')\nexcept:\n pass") # True
find_excepts()
Section titled “find_excepts()”Returns a list of all except handlers in a try statement.
code_str = """try: x = 1 / 0except 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
.
find_except()
Section titled “find_except()”Returns a specific except handler matching the exception type and optional variable name.
code_str = """try: x = 1 / 0except 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')") # Truetry_stmt.find_except("ValueError", "e").find_body().is_equivalent("print(f'value error: {e}')") # Truetry_stmt.find_except().find_body().is_equivalent("print('other error')") # True (bare except)
has_except()
Section titled “has_except()”Returns True if a try statement has an except handler for the given exception type and variable name.
code_str = """try: x = 1 / 0except 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") # Truetry_stmt.has_except("ValueError", "e") # Truetry_stmt.has_except("ValueError") # False
find_try_else()
Section titled “find_try_else()”Returns the else block of a try statement.
code_str = """try: x = 1except 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
find_finally()
Section titled “find_finally()”Returns the finally block of a try statement.
code_str = """try: x = 1except 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
find_matches()
Section titled “find_matches()”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()) # 2node.find_matches()[0].is_equivalent("match x:\n case 0:\n pass\n case _:\n pass") # True
find_match_subject()
Section titled “find_match_subject()”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
find_match_cases()
Section titled “find_match_cases()”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()) # 2node.find_matches()[0].find_match_cases()[0].find_body().is_equivalent("print(0)\nprint('spam')") # Truenode.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
.
find_case_pattern()
Section titled “find_case_pattern()”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") # Truenode.find_matches()[0].find_match_cases()[1].find_case_pattern().is_equivalent("[a, b]") # Truenode.find_matches()[0].find_match_cases()[2].find_case_pattern().is_equivalent("_") # True
find_case_guard()
Section titled “find_case_guard()”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") # Truenode.find_matches()[0].find_match_cases()[1].find_case_guard().is_equivalent("y == -1") # Truenode.find_matches()[0].find_match_cases()[2].find_case_guard().is_empty() # True (no guard)
Getting values
Section titled “Getting values”get_
functions return the value of the node, not the node itself.
get_variable()
Section titled “get_variable()”Node("x = 1").get_variable("x") # 1
Checking for existence
Section titled “Checking for existence”has_
functions return a boolean indicating whether the node exists.
has_variable()
Section titled “has_variable()”Node("x = 1").has_variable("x") # True
has_function()
Section titled “has_function()”Node("def foo():\n pass").has_function("foo") # True
has_stmt()
Section titled “has_stmt()”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
has_args()
Section titled “has_args()”Node("def foo(*, a, b, c=0):\n pass").find_function("foo").has_args("*, a, b, c=0") # True
has_pass()
Section titled “has_pass()”Node("def foo():\n pass").find_function("foo").has_pass() # TrueNode("if x==1:\n x+=1\nelse: pass").find_ifs()[0].find_bodies()[1].has_pass() # True
has_return()
Section titled “has_return()”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") # Trueexplorer.find_function("foo").find_ifs()[0].has_return("False") # True
has_returns()
Section titled “has_returns()”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") # TrueNode("def foo() -> 'spam':\n pass").find_function("foo").has_returns("spam") # True
has_decorators()
Section titled “has_decorators()”code_str = """class A: @property @staticmethod def foo(): pass"""Node(code_str).find_class("A").find_function("foo").has_decorators("property") # TrueNode(code_str).find_class("A").find_function("foo").has_decorators("property", "staticmethod") # TrueNode(code_str).find_class("A").find_function("foo").has_decorators("staticmethod", "property") # False, order does matter
has_call()
Section titled “has_call()”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()")
block_has_call()
Section titled “block_has_call()”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") # Truenode.block_has_call("sorted") # Truenode.block_has_call("sort", "spam") # Truenode.block_has_call("get", "eggs") # Truenode.block_has_call("get") # Truenode.block_has_call("split") # False
has_import()
Section titled “has_import()”code_str = """import ast, sysfrom math import factorial as f"""
explorer = Node(code_str)explorer.has_import("import ast, sys")explorer.has_import("from math import factorial as f")
has_class()
Section titled “has_class()”code_str = """class spam: pass"""
Node(code_str).has_class("spam")
is_equivalent()
Section titled “is_equivalent()”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") # TrueNode("\nx = 1").is_equivalent("x = 1") # TrueNode("x = 1").is_equivalent("x = 2") # False
is_empty()
Section titled “is_empty()”This is syntactic sugar for == Node()
.
Node().is_empty() # TrueNode("x = 1").find_variable("x").is_empty() # False
value_is_call()
Section titled “value_is_call()”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
is_integer()
Section titled “is_integer()”Node("x = 1").find_variable("x").is_integer() # TrueNode("x = '1'").find_variable("x").is_integer() # False
inherits_from()
Section titled “inherits_from()”Node("class C(A, B):\n pass").find_class("C").inherits_from("A") # TrueNode("class C(A, B):\n pass").find_class("C").inherits_from("A", "B") # True
is_ordered()
Section titled “is_ordered()”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 = 1if x: print("x is:") y = 0 print(x) return yx = 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") # Trueexplorer.is_ordered("x=1", if_str, "x=0") # Trueexplorer.find_ifs()[0].is_ordered("print('x is:')", "print(x)", "return y") # Trueexplorer.is_ordered("x=0", "x=1") # Falseexplorer.find_ifs()[0].is_ordered("print(x)", "print('x is:')") # False
How to get the nth statement
Section titled “How to get the nth statement”stmts = """if True: pass
x = 1"""
Node(stmts)[1].is_equivalent("x = 1") # True
Notes on Python
Section titled “Notes on Python”- 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