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 = "&nbsp;"
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">&nbsp;</td></tr>')
377
378    print(template % "\n".join(rows))
379