test_function_v2¶
-
test_function_v2
(name, index=1, params=[], signature=None, eq_condition='equal', do_eval=True, not_called_msg=None, params_not_matched_msg=None, params_not_specified_msg=None, incorrect_msg=None, add_more=False, highlight=True, state=None)¶ Test if function calls match (v2).
This function compares a function call in the student’s code with the corresponding one in the solution code. It will cause the reporter to fail if the corresponding calls do not match. The fail message that is returned will depend on the sort of fail.
Parameters: - name (str) – the name of the function to be tested.
- index (int) – index of the function call to be checked. Defaults to 1.
- params (list(str)) – the parameter names of the function call that you want to check.
- signature (Signature) – Normally, test_function() can figure out what the function signature is, but it might be necessary to use build_sig to manually build a signature and pass this along.
- eq_condition (str) – how parameters are compared. Currently, only “equal” is supported, meaning that the arguments in student and solution process should have exactly the same value.
- do_eval (list(bool)) – Boolean or list of booleans (parameter-specific) that specify whether or not arguments should be evaluated. True: arguments are evaluated and compared. False: arguments are not evaluated but ‘string-matched’. None: arguments are not evaluated; it is only checked if they are specified.
- not_called_msg (str) – custom feedback message if the function is not called.
- params_not_matched_message (str) – custom feedback message if the function parameters were not successfully matched.
- params_not_specified_msg (str) – string or list of strings (parameter-specific). Custom feedback message if not all parameters listed in params are specified by the student.
- incorrect_msg (list(str)) – string or list of strings (parameter-specific). Custom feedback messages if the arguments don’t correspond between student and solution code.
test_function_v2(name,
index=1,
params=None,
signature=None,
eq_condition="equal",
do_eval=True,
not_called_msg=None,
params_not_matched_msg=None,
params_not_specified_msg=None,
incorrect_msg=None)
test_function_v2()
enables you to test whether the student called a function correctly. The function first tests if the specified function is actually called by the student, and then compares the call with calls of the function in the solution code. Next, it can compare the arguments passed to these functions. Because test_function_v2()
also uses the student and solution process, this can be done in a very concise way. test_function_v2()
is an improved version of test_function()
, where:
- there is resilience against different ways of calling a function (arguments vs keywords),
- you have to be specific about which parameters you want to check,
- you can specify parameter-specific evaluation forms (
do_eval
can be a list), - you can specify parameter-specific custom messages (
params_not_matched_msg
andparams_not_specified_msg
can be lists), - you have more control over messaging in general.
Example 1¶
Suppose you want the student to call the round()
function on pi, as follows:
*** =solution
```{python}
# This is pi
pi = 3.14159
# Round pi to 3 digits
r_pi = round(pi, 3)
```
The following SCT tests whether the round()
function is used correctly:
*** =sct
```{python}
test_function_v2("round", params=["number", "ndigits"])
success_msg("Great job!")
```
test_function_v2()
tests whether the student has called the function round()
and checks whether the values of the arguments are the same as in the solution. So in this case, it tests whether round()
is used and the number
and ndigits
parameters, that round()
expects, are specified correctly, i.e. equal to 3.14159
and 3
respectively. test_function_v2()
figures out the values of these arguments from the solution code and the solution process that corresponds with it. The above SCT would accept all of the following student submissions:
round(3.14159, 3)
round(number=3.14159, 3)
round(number=3.14159, ndigits=3)
round(ndigits=3, number=3.14159)
pi=3.14159; dig=3; round(pi, dig)
pi=3.14159; dig=3; round(number=pi, dig)
int_part = 3; dec_part = 0.14159; round(int_part + dec_part, 3)
In params
, you have to explicitly list all the parameters that you want to test. If you only want to check the number
parameter, for example, you can use:
*** =sct
```{python}
test_function_v2("round", params=["number"])
success_msg("Great job!")
```
This SCT will only test whether the number
parameter was specified to be 3.14159
. If a student submits round(pi, 5)
, this would also pass this SCT.
If you specify params
to be an empty list, which is the default, you are simply checking whether the round()
function was called in the first place:
*** =sct
```{python}
test_function_v2("round") # same as test_function_v2("round", params=[])
success_msg("Great job!")
```
test_function_v2()
will automatically generate meaningful feedback, but you can also override these messages through the different *_msg
parameters that test_function_v2()
features:
not_called_msg
: message if the student didn’t call the specified function or didn’t call the specified function often enough (if you’re testing multiple calls of the same function in the same submission).params_not_matched_msg
: message if the function call of the student was invalid, i.e. if the way of specifying the different parameters was invalid.params_not_specified_msg
: message if the student did not specify all parameters that are specified insideparams
. This argument can either be a string, to give the same message for each parameter that is missing, or a list of strings with the same length asparams
. In case of a missing parameter,test_function_v2()
will present the corresponding message.incorrect_msg
: message if the student did not specify all parameters correctly, so when his or her specifications don’t correspond with the solution. This argument can again be a single string, or a list of parameter-specific feedback messages.
Below is an example of an SCT that specified all feedback messages. This is not required, though; you can depend on the automatic feedback messages for the not_called_msg
, params_not_specified_msg
and incorrect_msg
and only manually specify the params_not_matched_msg
, for example.
*** =sct
```{python}
test_function_v2("round", params=["number", "ndigits"]
not_called_msg="You did not call `round()` to round the irrational number, `pi`.",
params_not_matched_msg="Are you sure you correctly called the `round()` function?",
params_not_specified_msg="Make sure to specify both the `number` and `ndigits` parameter!",
incorrect_msg=["Make sure to correctly specify `number`; it should be `pi`, or `3.14159`.",
"Have you specified `ndigits` so that `pi` is rounded to 3 digits?"])
success_msg("Great job!")
```
Example 2: Multiple function calls¶
index
, which is 1 by default, becomes important when there are several calls of the same function. Suppose that your exercise requires the student to call the round()
function twice: once on pi
and once on e
, Euler’s number. A possible solution could be the following:
*** =solution
```{python}
# Call round on pi
round(3.14159, 3)
# Call round on e
round(2.71828, 3)
```
To test both these function calls, you’ll need the following SCT:
*** =sct
```{python}
test_function_v2("round", params=["number","ndigits"], index=1)
test_function_v2("round", params=["number","ndigits"], index=2)
success_msg("Two in a row, great!")
```
The first test_function_v2()
call, where index=1
, checks the solution code for the first function call of round()
, finds it - round(3.14159, 3)
- and then goes to look through the student code to find a function call of round()
that matches the arguments. It is perfectly possible that there are 5 function calls of round()
in the student’s submission, and that only the fourth call matches the requirements for test_function_v2()
. As soon as a function call is found in the student code that passes all tests, pythonwhat
heads over to the second test_function_v2()
call, where index=2
. The same thing happens: the second call of round()
is found from the solution code, and a match is sought for in the student code. This time, however, the function call that was matched before is now ‘blacklisted’; it is not possible that the same function call in the student code causes both test_function_v2()
calls to pass.
This means that all of the following student submissions would be accepted:
round(3.14159, 3); round(2.71828, 3)
round(2.71828, 3); round(3.14159, 3)
round(number=3.14159, ndigts=3); round(number=2.71828, 3)
round(number=2.71828, 3); round(number=3.14159, 3)
round(3.14159, 3); round(123.456); round(2.71828, 3)
round(2.71828, 3); round(123.456); round(3.14159, 3)
Of course, you can also specify all other arguments to customize your test to perfection, such as custom messages and do_eval
(example 3).
Example 3: do_eval
¶
With do_eval
, you can control how parameter specifications are compared between student and solution code. There are two ways to specify do_eval
: you can specify a single value, that will be used for comparing all params
that you specified. However, you can also specify a list of values, with the same length as params
; the way in which parameter specifications are compared becomes parameter specific. In both cases, there are three valid values:
True
, where the evaluated version of the student and solution arguments is compared.False
, where the ‘string version’ of the arguments is compared;None
, in which case the arguments are not compared;test_function_v2
simply checks if the parameter(s) in question has/have been specified.
Say, for example, you want to check if a student called the round()
function and specified the parameters number
and ndigits
. You want to test the actual equality of number
, but you don’t care about the value of ndigits
, you just want to make sure the student specified it, nothing more.
The following solution and SCT implement this train of thought (custom feedback messages have not been specified, although this is perfectly possible):
*** =solution
```{python}
# This is pi
pi = 3.14159
# Round pi to 3 digits
r_pi = round(pi, 3)
```
*** =sct
```{python}
test_function_v2("round",
params=["number", "ndigits"],
do_eval=[True, None])
success_msg("Great job!")
```
All of the following submissions would be accepted by this SCT:
round(pi, 3)
round(number=pi, ndigits=3)
round(number=pi, ndigits=4)
round(pi, 4)
round(pi, 0)
Example 4: Function calls in packages¶
If you’re testing whether function calls of particular packages are used correctly, you should always refer to these functions with their ‘full name’. Suppose you want to test whether the function show
of matplotlib.pyplot
was used correctly, you should use
*** =sct
```{python}
test_function_v2("matplotlib.pyplot.show")
```
The test_function_v2()
call can handle it when a student used aliases for the python packages (all import
and import * from *
calls are supported). In case there is an error, test_function_v2()
will automatically generated a feedback message that uses the alias that the student used.
NOTE: No matter how you import the function, you always have to refer to the function with its full name, e.g. package.subpackage1.subpackage2.function
.
Example 5: Manual signatures¶
To implement resilience against different ways of specify function parameters, the inspect
module is used, that is part of Python’s basic distribution. Through inspect.signature()
a function’s parameters can be inferred, and then ‘bound’ to the arguments that the student specified. However, this signature is not available for all of Python’s functions. More specifically, Python’s built-in functions that are implemented in C don’t allow a signature to be extracted from them. pythonwhat
already includes manually specified signatures for functions such as print()
, str()
, hasattr()
, etc, but it’s still possible that some signatures are missing.
That’s why test_function_v2()
features a signature
parameter, that is None
by default. If pythonwhat
can’t retrieve a signature for the function you want to test, you can pass an object of the class inspect.Signature
to the signature
parameter.
Suppose, for the sake of example, that test_function_v2()
can’t find a signature for the round()
function (you will be informed by this through automated testing; running the solution against an SCT that depends on a signature that is not found will throw a backend error). To be able to implement this function test, you can use the sig_from_params()
function:
*** =sct
```{python}
sig = sig_from_params(param("number", param.POSITIONAL_OR_KEYWORD),
param("ndigits", param.POSITIONAL_OR_KEYWORD, default=0))
test_function_v2("round", params=["number", "ndigits"], signature=sig)
```
param
is an alias of the Parameter
class that’s inside the inspect
module. You can pass sig_from_params()
as many parameters as you want. The first argument of param()
should be the name of the parameter, the second argument should be the ‘kind’ of parameter. param.POSITIONAL_OR_KEYWORD
tells test_function_v2
that the parameter can be specified either through a positional argument or through a keyword argument. Other common possibilities are param.POSITIONAL_ONLY
and param.KEYWORD_ONLY
(for a full list, refer to the Python docs on inspect
). The third, optional argument, allows you to specify a default value for the parameter.
NOTE: If you find vital Python functions that are used very often and that are not included in pythonwhat
by default, you can let us know and we’ll add the function to our list of manual signatures.
Example 6: Methods¶
Python also features methods, i.e. functions that are called on objects. For testing such a thing, you can also use test_function_v2()
. Consider the following solution code, that creates a connection to an SQLite Database with sqlalchemy
.
*** =solution
```{python}
# Prepare everything
from urllib.request import urlretrieve from sqlalchemy import create_engine, MetaData, Table engine = create_engine(‘sqlite:///census.sqlite’) metadata = MetaData() connection = engine.connect() from sqlalchemy import select census = Table(‘census’, metadata, autoload=True, autoload_with=engine) stmt = select([census])
# execute the query and fetch the results.
connection.execute(stmt).fetchall()
```
To test the last chained method calls, you can use the following SCT. Notice from the second test_function_v2()
call here that you have to describe the entire chain (leaving out the arguments that are passed to execute()
). This way, you explicitly list the order in which the methods should be called.
*** =sct
```{python}
test_function_v2("connection.execute", params = ["object"], do_eval = False)
test_function_v2("connection.execute.fetchall")
```
NOTE: currently, it is not possible to easily test the arguments inside chained method calls, methods inside arguments, etc. We are working on a massive update of pythonwhat
to easily support this very customized testing, with virtually no limit to ‘how deep you want the tests to go’. More on this later!
Example 7: Signatures for methods¶
In the previous example, you might have noticed that test_funtion_v2()
was capable to infer that connection
is a Connection
object, and that execute()
is a method of the Connection
class. For checking method calls that aren’t chained, this is possible, but for chained method calls, such as connection.execute.fetchall
, this is not possible. In those cases you’ll have to manually specify a signature. With sig_from_obj()
you can specify the function from which to extract a signature.
The following full example shows how it’s done:
*** =pre_exercise_code
```{python}
class Test():
def __init__(self, a):
self.a = a
def set_a(self, value):
self.a = value
return(self)
x = Test(123)
```
*** =solution
```{python}
x.set_a(843).set_a(102)
```
*** =sct
```{python}
sig = sig_from_obj('x.set_a')
test_function_v2('x.set_a.set_a', params=['value'], signature=sig)
```
NOTE: You can also use the sig_from_params()
function to manually build the signature from scratch, but this this more work than simply specifying the function object as a string from which to extract the signature.
Extra: Argument equality¶
Just like with test_object()
, evaluated arguments are compared using the ==
operator (check out the section about Object equality). For a lot of complex objects, the implementation of ==
causes the object instances to be compared... not their underlying meaning. For example when the solution is:
*** =solution
```
from urllib.request import urlretrieve
fn1 = 'https://s3.amazonaws.com/assets.datacamp.com/production/course_998/datasets/Chinook.sqlite'
urlretrieve(fn1, 'Chinook.sqlite')
from sqlalchemy import create_engine
import pandas as pd
engine = create_engine('sqlite:///Chinook.sqlite')
# Execute query and store records in dataframe: df
df = pd.read_sql_query("SELECT * FROM Album", engine)
```
And the SCT is:
*** =sct
```
test_function_v2("pandas.read_sql_query", params = ['sql', 'con'], do_eval = [True, False])
```
The SCT will fail even if the student uses this exact solution code. The reason being that the engine
object is compared in the solution and student process. The engine object is evaluated by create_engine('sqlite:///Chinook.sqlite')
. As you can try out yourself, create_engine('sqlite:///Chinook.sqlite') == create_engine('sqlite:///Chinook.sqlite')
will always be False
, even though they are semantically exactly the same. A better way of testing this code would be:
*** =sct
test_correct(
lambda: test_object("df"),
lambda: test_function_v2("pandas.read_sql_query", do_eval=False)
)
This SCT will not do exactly the same, but it will test enough in practice 99% of the time. Check out the section about Object equality for complex objects that DO have a good equality implementation.
NOTE: Behind the scenes, pythonwhat
has to fetch the value of objects from sub-processes. The required ‘dilling’ and ‘undilling’ can cause issues for exotic objects. For more information on this and possible errors that can occur, read the Processes article.