1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187
|
import ast
from brian2.parsing.rendering import NodeRenderer
__all__ = ["parse_synapse_generator"]
def _cname(obj):
return obj.__class__.__name__
def handle_range(*args, **kwds):
"""
Checks the arguments/keywords for the range iterator
Should have 1-3 positional arguments.
Returns a dict with keys low, high, step. Default values are
low=0, step=1.
"""
if len(args) == 0 or len(args) > 3:
raise SyntaxError("Range iterator takes 1-3 positional arguments.")
if len(kwds):
raise SyntaxError("Range iterator doesn't accept any keyword arguments.")
if len(args) == 1:
high = args[0]
low = "0"
step = "1"
elif len(args) == 2:
low, high = args
step = "1"
else:
low, high, step = args
return {"low": low, "high": high, "step": step}
def handle_sample(*args, **kwds):
"""
Checks the arguments/keywords for the sample iterator
Should have 1-3 positional arguments and 1 keyword argument (either p or
size).
Returns a dict with keys ``low, high, step, sample_size, p, size``. Default
values are ``low=0``, ``step=1`. Sample size will be either ``'random'`` or
``'fixed'``. In the first case, ``p`` will have a value and size will be
``None`` (and vice versa for the second case).
"""
if len(args) == 0 or len(args) > 3:
raise SyntaxError("Sample iterator takes 1-3 positional arguments.")
if len(kwds) != 1 or ("p" not in kwds and "size" not in kwds):
raise SyntaxError(
"Sample iterator accepts one keyword argument, either 'p' or 'size'."
)
if len(args) == 1:
high = args[0]
low = "0"
step = "1"
elif len(args) == 2:
low, high = args
step = "1"
else:
low, high, step = args
if "p" in kwds:
sample_size = "random"
p = kwds["p"]
size = None
else:
sample_size = "fixed"
size = kwds["size"]
p = None
return {
"low": low,
"high": high,
"step": step,
"p": p,
"size": size,
"sample_size": sample_size,
}
iterator_function_handlers = {
"range": handle_range,
"sample": handle_sample,
}
def parse_synapse_generator(expr):
"""
Returns a parsed form of a synapse generator expression.
The general form is:
``element for inner_variable in iterator_func(...)``
or
``element for inner_variable in iterator_func(...) if if_expression``
Returns a dictionary with keys:
``original_expression``
The original expression as a string.
``element``
As above, a string expression.
``inner_variable``
A variable name, as above.
``iterator_func``
String. Either ``range`` or ``sample``.
``if_expression``
String expression or ``None``.
``iterator_kwds``
Dictionary of key/value pairs representing the keywords. See
`handle_range` and `handle_sample`.
"""
nr = NodeRenderer()
parse_error = (
"Error parsing expression '%s'. Expression must have "
"generator syntax, for example 'k for k in range(i-10, "
"i+10)'." % expr
)
try:
node = ast.parse(f"[{expr}]", mode="eval").body
except Exception as e:
raise SyntaxError(f"{parse_error} Error encountered was {e}")
if _cname(node) != "ListComp":
raise SyntaxError(f"{parse_error} Expression is not a generator expression.")
element = node.elt
if len(node.generators) != 1:
raise SyntaxError(
f"{parse_error} Generator expression must involve only one iterator."
)
generator = node.generators[0]
target = generator.target
if _cname(target) != "Name":
raise SyntaxError(
f"{parse_error} Generator must iterate over a single variable (not tuple,"
" etc.)."
)
inner_variable = target.id
iterator = generator.iter
if _cname(iterator) != "Call" or _cname(iterator.func) != "Name":
raise SyntaxError(
parse_error
+ " Iterator expression must be one of the supported functions: "
+ str(list(iterator_function_handlers))
)
iterator_funcname = iterator.func.id
if iterator_funcname not in iterator_function_handlers:
raise SyntaxError(
parse_error
+ " Iterator expression must be one of the supported functions: "
+ str(list(iterator_function_handlers))
)
if (
getattr(iterator, "starargs", None) is not None
or getattr(iterator, "kwargs", None) is not None
):
raise SyntaxError(f"{parse_error} Star arguments not supported.")
args = []
for argnode in iterator.args:
args.append(nr.render_node(argnode))
keywords = {}
for kwdnode in iterator.keywords:
keywords[kwdnode.arg] = nr.render_node(kwdnode.value)
try:
iterator_handler = iterator_function_handlers[iterator_funcname]
iterator_kwds = iterator_handler(*args, **keywords)
except SyntaxError as exc:
raise SyntaxError(f"{parse_error} {exc.msg}")
if len(generator.ifs) == 0:
condition = ast.parse("True", mode="eval").body
elif len(generator.ifs) > 1:
raise SyntaxError(
f"{parse_error} Generator must have at most one if statement."
)
else:
condition = generator.ifs[0]
parsed = {
"original_expression": expr,
"element": nr.render_node(element),
"inner_variable": inner_variable,
"iterator_func": iterator_funcname,
"iterator_kwds": iterator_kwds,
"if_expression": nr.render_node(condition),
}
return parsed
|