Franck Pommereau

added plugin system

...@@ -16,3 +16,6 @@ class ParseError (ZINCError) : ...@@ -16,3 +16,6 @@ class ParseError (ZINCError) :
16 if loc : 16 if loc :
17 msg = "[%s] %s" % (loc, msg) 17 msg = "[%s] %s" % (loc, msg)
18 ZINCError.__init__(self, msg) 18 ZINCError.__init__(self, msg)
19 +
20 +class CompileError (ParseError) :
21 + pass
......
...@@ -40,10 +40,20 @@ _errre = re.compile(r"^.*?error at line ([0-9]+), col ([0-9]+):[ \t]*" ...@@ -40,10 +40,20 @@ _errre = re.compile(r"^.*?error at line ([0-9]+), col ([0-9]+):[ \t]*"
40 "((.|\n)*)$", re.I|re.A) 40 "((.|\n)*)$", re.I|re.A)
41 41
42 class BaseParser (object) : 42 class BaseParser (object) :
43 + @classmethod
44 + def make_parser (cls) :
45 + def parse (source) :
46 + if isinstance(source, str) :
47 + return cls().parse(source, "<string>")
48 + else :
49 + return cls().parse(source.read(), getattr(source, "name", "<string>"))
50 + return parse
43 def init (self, parser) : 51 def init (self, parser) :
44 pass 52 pass
45 def parse (self, source, path) : 53 def parse (self, source, path) :
46 - parser = self.__parser__(indedent(source)) 54 + self.source = source
55 + self.path = path
56 + self.parser = parser = self.__parser__(indedent(source))
47 self.init(parser) 57 self.init(parser)
48 try : 58 try :
49 do_parse = getattr(parser, parser.__default__, "INPUT") 59 do_parse = getattr(parser, parser.__default__, "INPUT")
......
...@@ -10,8 +10,4 @@ class Parser (BaseParser) : ...@@ -10,8 +10,4 @@ class Parser (BaseParser) :
10 ["negate"], 10 ["negate"],
11 ["task"]] 11 ["task"]]
12 12
13 -def parse (source) : 13 +parse = Parser.make_parser()
14 - if isinstance(source, str) :
15 - return Parser().parse(source, "<string>")
16 - else :
17 - return Parser().parse(source.read(), getattr(source, "name", "<string>"))
......
1 +import pathlib, logging, inspect
2 +from .. import CompileError, nets
3 +from . import metaparse, BaseParser
4 +from .meta import Compiler as BaseCompiler, node as decl
5 +
6 +class Parser (BaseParser) :
7 + class __parser__ (metaparse.MetaParser) :
8 + __compound__ = [["if", "*elif", "?else"],
9 + ["for", "?else"],
10 + ["while", "?else"],
11 + ["try", "except"],
12 + ["def"],
13 + ["task"]]
14 +
15 +parse = Parser.make_parser()
16 +
17 +class Context (dict) :
18 + def __call__ (self, **args) :
19 + d = self.copy()
20 + d.update(args)
21 + return d
22 + def __getattr__ (self, key) :
23 + return self[key]
24 + def __setattr__ (self, key, val) :
25 + self[key] = val
26 +
27 +class Compiler (BaseCompiler) :
28 + def __init__ (self, module=nets) :
29 + self.n = module
30 + def __call__ (self, source) :
31 + self.p = Parser()
32 + tree = self.parser.parse(source)
33 + self._tasks = {}
34 + return self.visit(tree, Context(_path=pathlib.Path("/")))
35 + def _get_pos (self, node) :
36 + if "_pos" in node :
37 + return (self.p.parser._p_get_line(node._pos),
38 + self.p.parser._p_get_col(node._pos) - 1)
39 + else :
40 + return None, None
41 + def warn (self, node, message) :
42 + lno, cno = self._get_pos(node)
43 + if lno is not None :
44 + message = "[%s] %s" % (":".join([self.p.path, str(lno)]), message)
45 + logging.warn(message)
46 + def _check (self, node, cond, error) :
47 + lno, cno = self._get_pos(node)
48 + raise CompileError(error, lno=lno, cno=cno, path=self.p.path)
49 + def build_seq (self, seq, ctx) :
50 + net = self.visit(seq[0], ctx)
51 + for other in seq[1:] :
52 + n = self.visit(other, ctx)
53 + if n is not None :
54 + net &= n
55 + return net
56 + def visit_model (self, node, ctx) :
57 + return self.visit_seq(node.body, ctx)
58 + def visit_task (self, node, ctx) :
59 + pass
60 + def visit_def (self, node, ctx) :
61 + self._check(node, node.deco is None, "decorators are not supported")
62 + self._check(node, node.largs, "missing parameters NAME")
63 + self._check(node, node.largs[0].type == "name",
64 + "invalid function name %r (%s)" % (node.largs[0].value,
65 + node.largs[0].type))
66 + for a in node.largs[1:] :
67 + self._check(node, a.kind == "name", "invalid parameter %s (%s)"
68 + % (a.value, a.kind))
69 + for a, v in node.kargs.items() :
70 + self._check(node, v.kind in ("int", "code", "str"),
71 + "invalid defaut value for parameter %s: %r (%s)"
72 + % (a, v.value, v.kind))
73 + name = node.largs[0].value
74 + if name in ctx :
75 + self.warn(node, "previous declaration of %r is masked" % name)
76 + path = ctx._path / name
77 + flag = inspect.Parameter.POSITIONAL_OR_KEYWORD
78 + sig = inspect.Signature([inspect.Parameter(a.value, flag)
79 + for a in node.largs[1:]]
80 + + [inspect.Parameter(a, flag, default=v.value)
81 + for a, v in node.kargs.items()])
82 + ctx[name] = decl(kind="function", args=sig)
83 + body = self.build_seq(node.body, ctx(_path=path,
84 + **{a : decl(kind="variable")
85 + for a in sig.parameters}))
86 + # TODO: add initial transition that gets all the arguments and store them into
87 + # buffer places + terminal transition that flush these places and return
88 + # the value
89 + call = ret = None
90 + net = (call & body & ret) / tuple(sig.parameters)
91 + self._tasks[path] = net.task(name)
92 + def visit_assign (self, node, ctx) :
93 + tgt = node.target
94 + val = node.value
95 + self._check(node, tgt not in ctx or ctx[tgt].kind == "variable",
96 + "cannot assign to previously declared %s %r"
97 + % (ctx[tgt].kind, tgt))
98 + if val.tag == "call" :
99 + net = self.visit(val)
100 + if tgt not in net :
101 + net.add_place(self.n.Place(tgt, status=self.n.buffer(tgt)))
102 + if tgt in ctx :
103 + net.add_input(tgt, "_w", self.n.Variable("_"))
104 + net.add_output(tgt, "_w", self.n.Variable("_r"))
105 + else :
106 + net = self.n.PetriNet("[%s:%s] %s" % (self._get_pos(node) + (node._src,)))
107 + net.add_transition(self.n.Transition("_t"))
108 + net.add_place(self.n.Place(tgt, status=self.n.buffer(tgt)))
109 + net.add_place(self.n.Place("_e", status=self.n.entry))
110 + net.add_place(self.n.Place("_x", status=self.n.exit))
111 + net.add_input("_e", "_t", self.n.Value(self.n.dot))
112 + net.add_output("_x", "_t", self.n.Value(self.n.dot))
113 + if tgt in ctx :
114 + net.add_input(tgt, "_t", self.n.Variable("_"))
115 + if val.tag == "name" :
116 + net.add_place(self.n.Place(tgt.value))
117 + net.add_input(tgt.value, "_t", self.n.Test(self.n.Variable("_v")))
118 + net.add_output(tgt, "_t", self.n.Variable("_v"))
119 + elif val.tag in ("int", "str") :
120 + net.add_output(tgt, "_t", self.n.Value(repr(val.value)))
121 + elif val.tag == "code" :
122 + net.add_output(tgt, "_t", self.n.Expression(val.value))
123 + else :
124 + self._check(node, False, "unsupported assignment %r" % node._src)
125 + ctx[tgt] = decl(kind="variable")
126 + return net
127 + def visit_call (self, node, ctx) :
128 + self._check(node, node.name in ctx, "undeclared function %r" % node.name)
129 + self._check(node, ctx[node.name].kind == "function",
130 + "%s %s is not a function" % (ctx[node.name].kind, node.name))
131 + try :
132 + argmap = ctx[node.name].args.bind(*node.largs, **node.kwargs)
133 + argmap.apply_defaults()
134 + except Exception as err :
135 + self._check(node, False, str(err))
136 + net = self.n.PetriNet("[%s:%s] %s" % (self._get_pos(node) + (node._src,)))
137 + net.add_place(self.n.Place("_e", status=self.n.entry))
138 + net.add_place(self.n.Place("_i", status=self.n.internal))
139 + net.add_place(self.n.Place("_x", status=self.n.exit))
140 + net.add_transition(self.n.Transition("_c"))
141 + net.add_transition(self.n.Transition("_w"))
142 + net.add_input("_e", "_t", self.n.Value(self.n.dot))
143 + net.add_output("_i", "_t", self.n.Value(self.n.dot))
144 + net.add_input("_i", "_w", self.n.Value(self.n.dot))
145 + net.add_output("_x", "_w", self.n.Variable("_s"))
146 + label = []
147 + for arg, val in argmap.arguments :
148 + if val.kind == "name" :
149 + if not net.has_place(val.value) :
150 + net.add_place(self.n.Place(val.value, status=self.n.entry))
151 + net.add_input(val.value, "_c", self.n.Test(self.n.Variable(val.value)))
152 + label.append(self.n.Variable(val.value))
153 + elif val.kind in ("int", "str") :
154 + label.append(self.n.Value(repr(val.value)))
155 + elif val.kind == "code" :
156 + label.append(self.n.Expression(val.value))
157 + net.add_place(self.n.Place("call_" + node.name))
158 + net.add_output("call_" + node.name, "_c", self.n.Tuple(self.n.dot,
159 + self.n.Tuple(*label)))
160 + net.add_place(self.n.Place("ret_" + node.name))
161 + net.add_input("ret_" + node.name, "_w", self.n.Tuple(self.n.Variable("_s"),
162 + self.n.Variable("_r")))
163 + return net
164 + def visit_if (self, node, ctx) :
165 + pass
166 + def visit_for (self, node, ctx) :
167 + pass
168 + def visit_while (self, node, ctx) :
169 + pass
170 + def visit_try (self, node, ctx) :
171 + pass
...@@ -7,10 +7,6 @@ class Parser (BaseParser) : ...@@ -7,10 +7,6 @@ class Parser (BaseParser) :
7 __compound__ = spec 7 __compound__ = spec
8 self.__parser__ = MP 8 self.__parser__ = MP
9 9
10 -def parse (source, spec) : 10 +parse = Parser.make_parser()
11 - if isinstance(source, str) :
12 - return Parser(spec).parse(source, "<string>")
13 - else :
14 - return Parser(spec).parse(source.read(), getattr(source, "name", "<string>"))
15 -
16 Compiler = metaparse.Compiler 11 Compiler = metaparse.Compiler
12 +node = metaparse.node
......
1 # coding: utf-8 1 # coding: utf-8
2 2
3 -import ast, sys 3 +import ast, sys, collections
4 import fastidious 4 import fastidious
5 5
6 class node (object) : 6 class node (object) :
...@@ -114,26 +114,26 @@ class Nest (object) : ...@@ -114,26 +114,26 @@ class Nest (object) :
114 return n 114 return n
115 115
116 class Compiler (object) : 116 class Compiler (object) :
117 - def visit (self, tree) : 117 + def visit (self, tree, *l, **k) :
118 if isinstance(tree, node) : 118 if isinstance(tree, node) :
119 method = getattr(self, "visit_" + tree.tag, self.generic_visit) 119 method = getattr(self, "visit_" + tree.tag, self.generic_visit)
120 - return method(tree) 120 + return method(tree, *l, **k)
121 else : 121 else :
122 return tree 122 return tree
123 - def generic_visit (self, tree) : 123 + def generic_visit (self, tree, *l, **k) :
124 sub = {} 124 sub = {}
125 ret = {tree.tag : sub} 125 ret = {tree.tag : sub}
126 for name, child in tree : 126 for name, child in tree :
127 if isinstance(child, list) : 127 if isinstance(child, list) :
128 sub[name] = [] 128 sub[name] = []
129 for c in child : 129 for c in child :
130 - sub[name].append(self.visit(c)) 130 + sub[name].append(self.visit(c, *l, **k))
131 elif isinstance(child, dict) : 131 elif isinstance(child, dict) :
132 sub[name] = {} 132 sub[name] = {}
133 for k, v in child.items() : 133 for k, v in child.items() :
134 - sub[name][k] = self.visit(v) 134 + sub[name][k] = self.visit(v, *l, **k)
135 else : 135 else :
136 - sub[name] = self.visit(child) 136 + sub[name] = self.visit(child, *l, **k)
137 return ret 137 return ret
138 138
139 class MetaParser (fastidious.Parser) : 139 class MetaParser (fastidious.Parser) :
...@@ -157,7 +157,6 @@ class MetaParser (fastidious.Parser) : ...@@ -157,7 +157,6 @@ class MetaParser (fastidious.Parser) :
157 DEDENT <- _ "↤" NL? _ {_drop} 157 DEDENT <- _ "↤" NL? _ {_drop}
158 NUMBER <- _ ~"[+-]?[0-9]+" {_number} 158 NUMBER <- _ ~"[+-]?[0-9]+" {_number}
159 NAME <- _ ~"[a-z][a-z0-9_]*"i _ {p_flatten} 159 NAME <- _ ~"[a-z][a-z0-9_]*"i _ {p_flatten}
160 - atom <- :name / num:NUMBER / :code / :string {_first}
161 name <- name:NAME {_name} 160 name <- name:NAME {_name}
162 code <- codec / codeb {_code} 161 code <- codec / codeb {_code}
163 codec <- LCB (~"([^{}\\\\]|[{}])+" / codec)* RCB {p_flatten} 162 codec <- LCB (~"([^{}\\\\]|[{}])+" / codec)* RCB {p_flatten}
...@@ -170,14 +169,17 @@ class MetaParser (fastidious.Parser) : ...@@ -170,14 +169,17 @@ class MetaParser (fastidious.Parser) :
170 def _drop (self, match) : 169 def _drop (self, match) :
171 return "" 170 return ""
172 def _number (self, match) : 171 def _number (self, match) :
173 - return node("const", value=int(self.p_flatten(match)), type="int") 172 + return node("const", value=int(self.p_flatten(match)), type="int",
173 + _pos=self.pos, _src=self.p_flatten(match))
174 def _name (self, match, name) : 174 def _name (self, match, name) :
175 - return node("const", value=name, type="name") 175 + return node("const", value=name, type="name",
176 + _pos=self.pos, _src=self.p_flatten(match))
176 def _code (self, match) : 177 def _code (self, match) :
177 - return node("const", value=self.p_flatten(match).strip()[1:-1], type="code") 178 + return node("const", value=self.p_flatten(match).strip()[1:-1], type="code",
179 + _pos=self.pos, _src=self.p_flatten(match))
178 def _string (self, match) : 180 def _string (self, match) :
179 return node("const", value=ast.literal_eval(self.p_flatten(match).strip()), 181 return node("const", value=ast.literal_eval(self.p_flatten(match).strip()),
180 - type="str") 182 + type="str", _pos=self.pos, _src=self.p_flatten(match))
181 def _tuple (self, match) : 183 def _tuple (self, match) :
182 lst = [] 184 lst = []
183 for m in match : 185 for m in match :
...@@ -192,11 +194,14 @@ class MetaParser (fastidious.Parser) : ...@@ -192,11 +194,14 @@ class MetaParser (fastidious.Parser) :
192 if isinstance(v, node) : 194 if isinstance(v, node) :
193 return v 195 return v
194 else : 196 else :
195 - return node(k, value=v) 197 + return node(k, value=v, _pos=self.pos, _src=self.p_flatten(match))
196 return self.NoMatch 198 return self.NoMatch
197 __grammar__ += r""" 199 __grammar__ += r"""
198 model <- stmt:stmt+ 200 model <- stmt:stmt+
199 - stmt <- :call / :block {_first} 201 + stmt <- :assign / :call / :block {_first}
202 + assign <- name:NAME EQ :expr
203 + expr <- :atom / :call {_first}
204 + atom <- :name / num:NUMBER / :code / :string {_first}
200 call <- name:NAME :args NL 205 call <- name:NAME :args NL
201 args <- LP ( :arglist )? RP 206 args <- LP ( :arglist )? RP
202 arglist <- arg (COMMA arg)* {_tuple} 207 arglist <- arg (COMMA arg)* {_tuple}
...@@ -241,13 +246,17 @@ class MetaParser (fastidious.Parser) : ...@@ -241,13 +246,17 @@ class MetaParser (fastidious.Parser) :
241 return s 246 return s
242 def on_model (self, match, stmt) : 247 def on_model (self, match, stmt) :
243 return node("model", body=self._glue(stmt), _pos=self.pos) 248 return node("model", body=self._glue(stmt), _pos=self.pos)
249 + def on_assign (self, match, name, expr) :
250 + return node("assign", target=name, value=expr,
251 + _pos=self.pos, _src=self.p_flatten(match))
244 def on_call (self, match, name, args) : 252 def on_call (self, match, name, args) :
245 - return node("call", name=name, largs=args.l, kargs=args.k, _pos=self.pos) 253 + return node("call", name=name, largs=args.l, kargs=args.k,
254 + _pos=self.pos, _src=self.p_flatten(match))
246 def on_args (self, match, arglist=None) : 255 def on_args (self, match, arglist=None) :
247 if arglist is self.NoMatch or not arglist : 256 if arglist is self.NoMatch or not arglist :
248 arglist = [] 257 arglist = []
249 l = [] 258 l = []
250 - k = {} 259 + k = collections.OrderedDict()
251 pos = True 260 pos = True
252 for arg in arglist : 261 for arg in arglist :
253 if arg.kw : 262 if arg.kw :
...@@ -258,15 +267,17 @@ class MetaParser (fastidious.Parser) : ...@@ -258,15 +267,17 @@ class MetaParser (fastidious.Parser) :
258 else : 267 else :
259 self.p_parse_error("forbidden positional arg after a keyword arg", 268 self.p_parse_error("forbidden positional arg after a keyword arg",
260 arg._pos) 269 arg._pos)
261 - return node("args", l=l, k=k) 270 + return node("args", l=l, k=k, _pos=self.pos, _src=self.p_flatten(match))
262 def on_arg (self, match, left, right=None) : 271 def on_arg (self, match, left, right=None) :
263 if right is None : 272 if right is None :
264 - return node("arg", kw=False, value=left, _pos=self.pos) 273 + return node("arg", kw=False, value=left,
274 + _pos=self.pos, _src=self.p_flatten(match))
265 elif left.type != "name" : 275 elif left.type != "name" :
266 self.p_parse_error("invalid parameter %r (of type %s)" 276 self.p_parse_error("invalid parameter %r (of type %s)"
267 % (left.value, left.type)) 277 % (left.value, left.type))
268 else : 278 else :
269 - return node("arg", kw=True, name=left.value, value=right, _pos=self.pos) 279 + return node("arg", kw=True, name=left.value, value=right,
280 + _pos=self.pos, _src=self.p_flatten(match))
270 def on_block (self, match, name, stmt, args=None, deco=None) : 281 def on_block (self, match, name, stmt, args=None, deco=None) :
271 if args is self.NoMatch or not args : 282 if args is self.NoMatch or not args :
272 args = None 283 args = None
...@@ -275,6 +286,7 @@ class MetaParser (fastidious.Parser) : ...@@ -275,6 +286,7 @@ class MetaParser (fastidious.Parser) :
275 return self.nest(name, body=self._glue(stmt), args=args, deco=deco, 286 return self.nest(name, body=self._glue(stmt), args=args, deco=deco,
276 _pos=self.pos) 287 _pos=self.pos)
277 def on_deco (self, match, name, args=[]) : 288 def on_deco (self, match, name, args=[]) :
278 - return node("deco", name=name, args=args) 289 + return node("deco", name=name, args=args,
290 + _pos=self.pos, _src=self.p_flatten(match))
279 def __INIT__ (self) : 291 def __INIT__ (self) :
280 self.nest = Nest(self.__compound__) 292 self.nest = Nest(self.__compound__)
......
This diff could not be displayed because it is too large.
...@@ -57,6 +57,12 @@ class PetriNet (object) : ...@@ -57,6 +57,12 @@ class PetriNet (object) :
57 return self._place[name] 57 return self._place[name]
58 else : 58 else :
59 raise ConstraintError("place %r not found" % name) 59 raise ConstraintError("place %r not found" % name)
60 + def has_place (self, name) :
61 + return name in self._place
62 + def has_transition (self, name) :
63 + return name in self._trans
64 + def __contains__ (self, name) :
65 + return name in self._node
60 def transition (self, name=None) : 66 def transition (self, name=None) :
61 if name is None : 67 if name is None :
62 return self._trans.values() 68 return self._trans.values()
......
1 +import importlib, types, sys, inspect
2 +
3 +from .. import ZINCError
4 +
5 +class PluginError (ZINCError) :
6 + pass
7 +
8 +class Plugin (object) :
9 + modname = "zn"
10 + plugins = []
11 + modules = []
12 + def __init__ (self, *extends, conflicts=[], depends=[]) :
13 + # check conflicts
14 + for p in conflicts :
15 + if p in self.plugins :
16 + raise PluginError("plugin conflicts with %r" % p)
17 + # load dependencies
18 + for p in depends :
19 + try :
20 + return importlib.import_module(p)
21 + except :
22 + try :
23 + return importlib.import_module("zinc.plugins." + p)
24 + except :
25 + raise PluginError("could not load %r from '.' or 'zinc.plugins'" % p)
26 + # create result module
27 + if self.modname not in sys.modules :
28 + self._mod = sys.modules[self.modname] = types.ModuleType(self.modname)
29 + # load extended modules
30 + for base in extends :
31 + if base not in self.modules :
32 + mod = importlib.import_module(base)
33 + self._mod.__dict__.update(mod.__dict__)
34 + self.modules.append(base)
35 + # record loaded plugin
36 + stack = inspect.stack()
37 + caller = inspect.getmodule(stack[1][0])
38 + self.plugins.append(caller.__name__)
39 + def __getattr__ (self, name) :
40 + return getattr(self._mod, name)
41 + def __call__ (self, obj) :
42 + setattr(self._mod, obj.__name__, obj)
43 + return obj
44 + @classmethod
45 + def reset_plugins (cls, name="zn") :
46 + for p in cls.plugins + cls.modules + [cls.modname] :
47 + del sys.modules[p]
48 + cls.modname = name
49 + cls.plugins = []
50 + cls.modules = []
1 +"""An example plugin that allows instances of `PetriNet` to say hello.
2 +
3 +The source code can be used as a starting example:
4 +1. Import `zinc.plugins.Plugin`
5 +2. Create an instance of it, passing:
6 + * the modules that are extended
7 + * the conflicts (if any)
8 + * the dependencies (if any)
9 +3. This instance will serve as:
10 + * a decorator for each class or function that should be included
11 + in the extended module
12 + * classes or functions to extend are retreived as its attributes
13 +"""
14 +
15 +from . import Plugin
16 +plug = Plugin("zinc.nets")
17 +
18 +@plug
19 +class PetriNet (plug.PetriNet) :
20 + def hello (self, name="world") :
21 + print("hello %s from %s" % (name, self.name))
1 +import enum
1 from zinc.data import hashable, mset 2 from zinc.data import hashable, mset
2 3
3 class Token (object) : 4 class Token (object) :
5 + pass
6 +
7 +class CodeToken (Token) :
4 def __init__ (self, code) : 8 def __init__ (self, code) :
5 self.code = code 9 self.code = code
6 def __ast__ (self, place, ctx) : 10 def __ast__ (self, place, ctx) :
...@@ -37,21 +41,17 @@ class Token (object) : ...@@ -37,21 +41,17 @@ class Token (object) :
37 def __lt__ (self, other) : 41 def __lt__ (self, other) :
38 return not self.__ge__(other) 42 return not self.__ge__(other)
39 43
40 -class BlackToken (Token) : 44 +class BlackToken (Token, enum.IntEnum) :
41 - def __init__ (self) : 45 + DOT = enum.auto()
42 - pass 46 +
43 - def __str__ (self) : 47 +dot = BlackToken.DOT
44 - return "dot" 48 +
45 - def __repr__ (self) : 49 +class BlackWhiteToken (Token, enum.IntEnum) :
46 - return "dot" 50 + BLACK = enum.auto()
47 - def __new__ (cls) : 51 + WHITE = enum.auto()
48 - if not hasattr(cls, "dot") :
49 - cls.dot = object.__new__(cls)
50 - return cls.dot
51 - def __hash__ (self) :
52 - return hash(self.__class__.__name__)
53 52
54 -dot = BlackToken() 53 +black = BlackWhiteToken.BLACK
54 +white = BlackWhiteToken.WHITE
55 55
56 @hashable 56 @hashable
57 class Marking (dict) : 57 class Marking (dict) :
......