1# 2# Licensed to the Apache Software Foundation (ASF) under one 3# or more contributor license agreements. See the NOTICE file 4# distributed with this work for additional information 5# regarding copyright ownership. The ASF licenses this file 6# to you under the Apache License, Version 2.0 (the 7# "License"); you may not use this file except in compliance 8# with the License. You may obtain a copy of the License at 9# 10# http://www.apache.org/licenses/LICENSE-2.0 11# 12# Unless required by applicable law or agreed to in writing, 13# software distributed under the License is distributed on an 14# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15# KIND, either express or implied. See the License for the 16# specific language governing permissions and limitations 17# under the License. 18# 19 20""" 21This module loads protocol metadata into python objects. It provides 22access to spec metadata via a python object model, and can also 23dynamically creating python methods, classes, and modules based on the 24spec metadata. All the generated methods have proper signatures and 25doc strings based on the spec metadata so the python help system can 26be used to browse the spec documentation. The generated methods all 27dispatch to the self.invoke(meth, args) callback of the containing 28class so that the generated code can be reused in a variety of 29situations. 30""" 31 32import re 33import textwrap 34import types 35import os 36 37from txamqp import xmlutil 38 39 40DEFAULT_SPEC = os.path.join(os.path.dirname(__file__), "../specs/standard/amqp0-9.stripped.xml") 41 42 43class SpecContainer(object): 44 def __init__(self): 45 self.items = [] 46 self.byname = {} 47 self.byid = {} 48 self.indexes = {} 49 self.bypyname = {} 50 51 def add(self, item): 52 if item.name in self.byname: 53 raise ValueError("duplicate name: %s" % item) 54 if item.id in self.byid: 55 raise ValueError("duplicate id: %s" % item) 56 pyname = pythonize(item.name) 57 if pyname in self.bypyname: 58 raise ValueError("duplicate pyname: %s" % item) 59 self.indexes[item] = len(self.items) 60 self.items.append(item) 61 self.byname[item.name] = item 62 self.byid[item.id] = item 63 self.bypyname[pyname] = item 64 65 def index(self, item): 66 try: 67 return self.indexes[item] 68 except KeyError: 69 raise ValueError(item) 70 71 def __iter__(self): 72 return iter(self.items) 73 74 def __len__(self): 75 return len(self.items) 76 77 78class Metadata(object): 79 PRINT = [] 80 81 def __init__(self): 82 pass 83 84 def __str__(self): 85 args = map(lambda f: "%s=%s" % (f, getattr(self, f)), self.PRINT) 86 return "%s(%s)" % (self.__class__.__name__, ", ".join(args)) 87 88 def __repr__(self): 89 return str(self) 90 91 92class Spec(Metadata): 93 PRINT = ["major", "minor", "file"] 94 95 def __init__(self, major, minor, file): 96 Metadata.__init__(self) 97 98 self.major = major 99 self.minor = minor 100 self.file = file 101 self.constants = SpecContainer() 102 self.classes = SpecContainer() 103 104 def post_load(self): 105 self.module = self.define_module("amqp%s%s" % (self.major, self.minor)) 106 self.klass = self.define_class("Amqp%s%s" % (self.major, self.minor)) 107 108 def parse_method(self, name): 109 parts = re.split(r"\s*\.\s*", name) 110 if len(parts) != 2: 111 raise ValueError(name) 112 klass, meth = parts 113 return self.classes.byname[klass].methods.byname[meth] 114 115 def define_module(self, name, doc=None): 116 module = types.ModuleType(name, doc) 117 module.__file__ = self.file 118 for c in self.classes: 119 classname = pythonize(c.name) 120 cls = c.define_class(classname) 121 cls.__module__ = module.__name__ 122 setattr(module, classname, cls) 123 return module 124 125 def define_class(self, name): 126 methods = {} 127 for c in self.classes: 128 for m in c.methods: 129 meth = pythonize(m.klass.name + "_" + m.name) 130 methods[meth] = m.define_method(meth) 131 return type(name, (), methods) 132 133 134class Constant(Metadata): 135 PRINT = ["name", "id"] 136 137 def __init__(self, spec, name, id, klass, docs): 138 Metadata.__init__(self) 139 self.spec = spec 140 self.name = name 141 self.id = id 142 self.klass = klass 143 self.docs = docs 144 145 146class Class(Metadata): 147 PRINT = ["name", "id"] 148 149 def __init__(self, spec, name, id, handler, docs): 150 Metadata.__init__(self) 151 self.spec = spec 152 self.name = name 153 self.id = id 154 self.handler = handler 155 self.fields = SpecContainer() 156 self.methods = SpecContainer() 157 self.docs = docs 158 159 def define_class(self, name): 160 methods = {} 161 for m in self.methods: 162 meth = pythonize(m.name) 163 methods[meth] = m.define_method(meth) 164 return type(name, (), methods) 165 166 167class Method(Metadata): 168 PRINT = ["name", "id"] 169 170 def __init__(self, klass, name, id, content, responses, synchronous, 171 description, docs): 172 Metadata.__init__(self) 173 self.klass = klass 174 self.name = name 175 self.id = id 176 self.content = content 177 self.responses = responses 178 self.synchronous = synchronous 179 self.fields = SpecContainer() 180 self.description = description 181 self.docs = docs 182 self.response = False 183 184 def docstring(self): 185 s = "\n\n".join([fill(d, 2) for d in [self.description] + self.docs]) 186 for f in self.fields: 187 if f.docs: 188 s += "\n\n" + "\n\n".join([fill(f.docs[0], 4, pythonize(f.name))] + 189 [fill(d, 4) for d in f.docs[1:]]) 190 return s 191 192 METHOD = "__method__" 193 DEFAULTS = {"bit": False, 194 "shortstr": "", 195 "longstr": "", 196 "table": {}, 197 "octet": 0, 198 "short": 0, 199 "long": 0, 200 "longlong": 0, 201 "timestamp": 0, 202 "content": None} 203 204 def define_method(self, name): 205 g = {Method.METHOD: self} 206 l = {} 207 args = [(pythonize(f.name), Method.DEFAULTS[f.type]) for f in self.fields] 208 if self.content: 209 args += [("content", None)] 210 code = "def %s(self, %s):\n" % \ 211 (name, ", ".join(["%s = %r" % a for a in args])) 212 code += " %r\n" % self.docstring() 213 if self.content: 214 methargs = args[:-1] 215 else: 216 methargs = args 217 argnames = ", ".join([a[0] for a in methargs]) 218 code += " return self.invoke(%s" % Method.METHOD 219 if argnames: 220 code += ", (%s,)" % argnames 221 else: 222 code += ", ()" 223 if self.content: 224 code += ", content" 225 code += ")" 226 exec(code, g, l) 227 return l[name] 228 229 230class Field(Metadata): 231 PRINT = ["name", "id", "type"] 232 233 def __init__(self, name, id, type, docs): 234 Metadata.__init__(self) 235 self.name = name 236 self.id = id 237 self.type = type 238 self.docs = docs 239 240 241def get_docs(nd): 242 return [n.text for n in nd["doc"]] 243 244 245def load_fields(nd, l, domains): 246 for f_nd in nd["field"]: 247 try: 248 field_type = f_nd["@domain"] 249 except KeyError: 250 field_type = f_nd["@type"] 251 while field_type in domains and domains[field_type] != field_type: 252 field_type = domains[field_type] 253 l.add(Field(f_nd["@name"], f_nd.index(), field_type, get_docs(f_nd))) 254 255 256def load(specfile): 257 doc = xmlutil.parse(specfile) 258 return load_from_doc(doc, specfilename=specfile) 259 260 261def load_string(specfilestr, specfilename=None): 262 doc = xmlutil.parse_string(specfilestr) 263 return load_from_doc(doc, specfilename=specfilename) 264 265 266def load_from_doc(doc, specfilename=None): 267 root = doc["amqp"][0] 268 spec = Spec(int(root["@major"]), int(root["@minor"]), specfilename) 269 270 # constants 271 for nd in root["constant"]: 272 const = Constant(spec, nd["@name"], int(nd["@value"]), nd.get("@class"), 273 get_docs(nd)) 274 spec.constants.add(const) 275 276 # domains are typedefs 277 domains = {} 278 for nd in root["domain"]: 279 domains[nd["@name"]] = nd["@type"] 280 281 # classes 282 for c_nd in root["class"]: 283 klass = Class(spec, c_nd["@name"], int(c_nd["@index"]), c_nd["@handler"], 284 get_docs(c_nd)) 285 load_fields(c_nd, klass.fields, domains) 286 for m_nd in c_nd["method"]: 287 meth = Method(klass, m_nd["@name"], 288 int(m_nd["@index"]), 289 m_nd.get_bool("@content", False), 290 [nd["@name"] for nd in m_nd["response"]], 291 m_nd.get_bool("@synchronous", False), 292 m_nd.text, 293 get_docs(m_nd)) 294 load_fields(m_nd, meth.fields, domains) 295 klass.methods.add(meth) 296 # resolve the responses 297 for m in klass.methods: 298 m.responses = [klass.methods.byname[r] for r in m.responses] 299 for resp in m.responses: 300 resp.response = True 301 spec.classes.add(klass) 302 spec.post_load() 303 return spec 304 305REPLACE = {" ": "_", "-": "_"} 306KEYWORDS = {"global": "global_", 307 "return": "return_"} 308 309 310def pythonize(name): 311 name = str(name) 312 for key, val in REPLACE.items(): 313 name = name.replace(key, val) 314 try: 315 name = KEYWORDS[name] 316 except KeyError: 317 pass 318 return name 319 320 321def fill(text, indent, heading=None): 322 sub = indent * " " 323 if heading: 324 init = (indent - 2) * " " + heading + " -- " 325 else: 326 init = sub 327 w = textwrap.TextWrapper(initial_indent=init, subsequent_indent=sub) 328 return w.fill(" ".join(text.split())) 329 330 331class Rule(Metadata): 332 PRINT = ["text", "implement", "tests"] 333 334 def __init__(self, text, implement, tests, path): 335 Metadata.__init__(self) 336 337 self.text = text 338 self.implement = implement 339 self.tests = tests 340 self.path = path 341 342 343def find_rules(node, rules): 344 if node.name == "rule": 345 rules.append(Rule(node.text, node.get("@implement"), 346 [ch.text for ch in node if ch.name == "test"], 347 node.path())) 348 if node.name == "doc" and node.get("@name") == "rule": 349 tests = [] 350 if node.has("@test"): 351 tests.append(node["@test"]) 352 rules.append(Rule(node.text, None, tests, node.path())) 353 for child in node: 354 find_rules(child, rules) 355 356 357def load_rules(specfile): 358 rules = [] 359 find_rules(xmlutil.parse(specfile), rules) 360 return rules 361 362 363def test_summary(): 364 template = '<html><head><title>AMQP Tests</title></head><body><table width="80%%" align="center">%s</table></body></html>' 365 rows = [] 366 for rule in load_rules("amqp.org/specs/amqp7.xml"): 367 if rule.tests: 368 tests = ", ".join(rule.tests) 369 else: 370 tests = " " 371 rows.append('<tr bgcolor="#EEEEEE"><td><b>Path:</b> %s</td>' 372 '<td><b>Implement:</b> %s</td>' 373 '<td><b>Tests:</b> %s</td></tr>' % 374 (rule.path[len("/root/amqp"):], rule.implement, tests)) 375 rows.append('<tr><td colspan="3">%s</td></tr>' % rule.text) 376 rows.append('<tr><td colspan="3"> </td></tr>') 377 378 print(template % "\n".join(rows)) 379