Arg Specs¶
For canonical_args
“specs” are dict
-formatted metadata
governing method arguments. They provide the configurable
functionality of the module’s checkspec
method, and associated
method decorators.
The basics of a Spec are as folows:
they are of type
dict
- they contain at least the section
"args"
and may optionally contain the section
"kwargs"
.
- they contain at least the section
- each positional argument entry (
"args"
) contains the keys: "name"
,"type"
, and"values"
.
- each positional argument entry (
each keyword argument entry (
"kwargs"
) contains is of structure:{ "kwarg-name": { "type": ..., "values": ..., "required": ... } }
- types and values may be nested to allow for lists and object-typed
arguments.
Positional Arguments¶
To define positional arguments for a method, let’s first look at an example:
def somemethod(arg1, arg2, arg3):
"""
ensure ``arg1/arg2`` is an entry in ``arg3`` (list).
"""
return (arg1/arg2) in arg3
somemethod
requires three positional arguments. Let’s first decide
the requirements for each of the arguments:
arg1
- an integer or float
arg2
- an integer or float
- greater than 0 (to avoid
ZeroDivisionError
’s)
arg3
- a list
Now we model it in a dict
:
[
{
"name": "arg1",
"type": "one([int, float])"
"values": {
"int": None,
"float": None
}
},
{
"name": "arg2",
"type": "one([int, float])",
"values": {
"int": ">0",
"float": ">0"
}
},
{
"name": "arg3",
"type": list,
"values": None
}
]
arg1
is defined as an int
of float
, but has no value constraint, hence "values": None
.
Note
Where we say "one([int, float])"
, we can also easily do one([int, float])
(note it’s not a string), as long as we import the canonical_args.check.one
method first.
arg2
is defined as an int
or float
, but must be greater than 0, hence "values": {"int": ">0", "float": ">0"}
. For choice of one type refs, the “values” key always contains an entry for each possible type.
arg3
is defined as a list
, but again, has no value constraints.
Note
The "values"
entry for "arg2"
can take the following form:
">{}"
, "<{}"
, ">={}"
, or "<={}"
, where "{}"
is replaced
by an integer or float.
Note
To add value and structure constraints to a list argument, we would do the following:
{
"name": "list_arg",
"type": "list([int, float, str])",
"values": [
"range(0, 15)",
">=50",
["A", "B", "C"]
]
}
list_arg
must be a list of length 3, with positon 0 as an integer between 0 and 14, position 1 as a float greater than or equal to 50, and position 2 a string equal to "A"
, "B"
, or "C"
.
Keyword Arguments¶
Keyword arguments have no guaranteed position, and are not required input to a method. Let’s look at another example:
def anothermethod(complete, total, percent=False):
"""
calculate completion, if percent flag is True,
return answer as a percent.
"""
percentage = float(complete) / float(total)
if percent:
return percentage * 100.0
return percentage
The above function takes two floats as positional arguments (above), and one boolean flag as a keyword argument, defaulting to False. In a spec dict:
{
"args": [
{
"name": "complete",
"type": "float",
"values": None
},
{
"name": "total",
"type": "float",
"values": None
}
],
"kwargs": {
"percent": {
"type": "bool",
"values": None
}
}
}
complete
is a float with no value constraints.
total
is a float with no value constraints.
percent
is a keyword argument of type bool
with no value constraints.
Note
We can call anothermethod
without specifying a percent
argument, and the default value will be checked against the spec.
Required vs. Non-Required Dictionary Keys¶
By default, if a "type"
is a dict
, all keys that appear within that dict are considered to be required. We can turn this off by adding a key to the spec as follows:
{
"args": [],
"kwargs": {
"percent": {
"type": bool,
"values": None,
"required": False
}
}
}
The "required": False
flag indicates to the structure.check_dict
method that the key "percent"
may be missing from the passed in dict
.
Nested Types and Values¶
Specs allow us to nest types and values very easily. Consider a positional argument that must be a list containing:
- an integer greater than or equal to 0
- an integer between -10 and 10
- and a string equal to “A” or “B”
And the accompanying spec dict:
{
"args": [
{
"name": "arg1",
"type": "list([int, int, str])",
"values": [
">=0",
"range(-10, 10)"
["A", "B"]
]
}
]
}
Note that "values"
and "type"
now take the form of lists, with an entry for each required position in the argument.
dict
’s are slightly more complicated. Essentially, we nest the arg spec for a dict
in the parent’s "values"
entry, and let recursion do the work. Once again, let’s use an example:
{
"args": [
{
"name": "arg1",
"type": dict,
"values": {
"dict-keyword": {
"type": int,
"values": None
},
"dict-keywork2": {
"type": float,
"values": ">=0"
}
}
}
]
}
This defines a method that takes a single argument of type dict
. The dict
however, in this case, must contain the keys "dict-keyword"
(of type int
with no value constraints), "dict-keyword2"
(of type float
and greater than or equal to 0).
Note
We can continue to nest as many dict
’s, list
’s and tuple
’s as we choose.
Objects as Parameters¶
It is often necessary to pass instantiated objects as parameters to methods. This can also be handled by canonical_args
. Let’s assume we have a class located at a_package.a_module.SomeClass
. To require a parameter to either instance or subinstance this class, we do the following:
{
"args": [
{
"name": "object_argument",
"type": "a_package.a_module.SomeClass",
"values": None
}
]
}
canonical_args
will now ensure the parameter passed for "object_parameter"
is of type "a_package.a_module.SomeClass"
, and will even import SomeClass
from a_package.a_module
automatically.
Warning
Ensure that any object path is to trusted code, or the import process can open a potential security vulnerability!!