1 """
2 A macro mechanism primarily for string replacement in resource descriptors.
3 """
4
5
6
7
8
9
10
11 import datetime
12 import re
13 import urllib
14
15 from pyparsing import (
16 ZeroOrMore, Forward,
17 Regex, Suppress,
18 Literal)
19
20
21 from gavo import utils
22 from gavo.base import attrdef
23 from gavo.base import common
24 from gavo.base import complexattrs
25 from gavo.base import config
26 from gavo.base import meta
27 from gavo.base import osinter
28 from gavo.base import structure
29
30
32 """is raised when something bad happens during macro expansion.
33
34 It is constructed with an error message, a macro name, and optionally
35 a hint and a position.
36 """
37 - def __init__(self, message, macroName, hint=None, pos=None):
42
44 return "Error during macro expansion: %s"%(
45 self.message)
46
47
49 """is a generic "macro" expander for scripts of all kinds.
50
51 It is loosely inspired by TeX, but of course much simpler. See the
52 syntax below.
53
54 The macros themselves come from a MacroPackage object. There are
55 a few of these around, implementing different functionality depending
56 on the script context (i.e., whether it belongs to an RD, a DD, or
57 a Table.
58
59 All macros are just functions receiving and returning strings. The
60 arguments are written as {arg1}{arg2}, where you can escape curly
61 braces with a backslash. There must be no whitespace between
62 a macro and its first argument.
63
64 If you need to glue together a macro expansion and text following,
65 use the glue sequence \\+
66
67 The main entry point to the class is the expand function below,
68 taking a string possibly containing macro calls and returning
69 a string.
70
71 The construction of such a macro expander is relatively expensive,
72 so it pays to cache them. MacroPackage below has a getExpander
73 method that does the caching for you.
74 """
76 self.package = package
77 self._macroGrammar = self._getMacroGrammar()
78
80 toks = toks.asList()
81 macName, args = toks[0], toks[1:]
82 return self.package.execMacro(macName, args)
83
86
88 with utils.pyparsingWhitechars(" \t"):
89 macro = Forward()
90 quoteEscape = (Literal("\\{").addParseAction(lambda *args: "{") |
91 Literal("\\}").addParseAction(lambda *args: "}"))
92 charRun = Regex(r"[^}\\]+").leaveWhitespace()
93 argElement = macro | quoteEscape | charRun
94 argument = Suppress("{") + ZeroOrMore(argElement) + Suppress("}")
95 argument.addParseAction(lambda s, pos, toks: "".join(toks))
96 arguments = ZeroOrMore(argument)
97 arguments.setWhitespaceChars("")
98 macroName = Regex("[A-Za-z_][A-Za-z_0-9]+")
99 macroName.setWhitespaceChars("")
100 macro << Suppress( "\\" ) + macroName + arguments
101 macro.addParseAction(self._execMacro)
102 literalBackslash = Literal("\\\\")
103 literalBackslash.addParseAction(lambda *args: "\\")
104 suppressedLF = Literal("\\\n")
105 suppressedLF.addParseAction(lambda *args: " ")
106 glue = Literal("\\+")
107 glue.addParseAction(lambda *args: "")
108 return literalBackslash | suppressedLF | glue | macro
109
110
112 """A mixin to make a class expand macros by delegating everything to
113 its parent.
114
115 This is intended for base.Structures that have a parent attribute;
116 by mixing this in, they use their parents to expand macros for them.
117 """
120
121
123 r"""is a function dispatcher for MacroExpander.
124
125 Basically, you inherit from this class and define macro_xxx functions.
126 MacroExpander can then call \xxx, possibly with arguments.
127 """
129 fun = getattr(self, "macro_"+macName, None)
130 if fun is not None:
131 return fun
132 if hasattr(self, "rd"):
133 fun = getattr(self.rd, "macro_"+macName, None)
134 if fun is not None:
135 return fun
136 raise MacroError(
137 "No macro \\%s available in a %s context"%(
138 macName, self.__class__.__name__),
139 macName, hint="%s objects have the following macros: %s."%(
140 self.__class__.__name__, ", ".join(self.listMacros())))
141
143 return [n[6:] for n in dir(self) if n.startswith("macro_")]
144
146 fun = self.__findMacro(macName)
147 try:
148 return fun(*args)
149 except TypeError:
150 raise utils.logOldExc(MacroError(
151 "Invalid macro arguments to \\%s: %s"%(macName, args), macName,
152 hint="You supplied too few or too many arguments"))
153 except utils.Error:
154 raise
155 except Exception as msg:
156 argRepr = "}{".join(utils.safe_str(a) for a in args)
157 if argRepr:
158 argRepr = "{%s}"%argRepr
159
160 raise utils.logOldExc(MacroError(
161 "While expanding macro \\%s%s: %s"%(macName, argRepr, msg),
162 macName,
163 hint="This means that the code dealing with your arguments"
164 " was throroughly confused by what you passed. If you really"
165 " cannot see why it was, file a bug."))
166
168 try:
169 return self.__macroExpander
170 except AttributeError:
171 self.__macroExpander = MacroExpander(self)
172 return self.getExpander()
173
176
178 """returns the argument in quotes (with internal quotes backslash-escaped
179 if necessary).
180 """
181 return '"%s"'%(arg.replace('"', '\\"'))
182
184 """returns the argument as a quoted string, unless it is 'NULL' or
185 None, in which case just NULL is returned.
186 """
187 if arg is None or arg=='NULL':
188 return "NULL"
189 return "'%s'"%arg.replace("'", "''")
190
192 """returns a SQL expression returning a DaCHS standard pubDID generated
193 from the accref (or something overridden) column.
194
195 This is convenient in obscore or ssa views when the underlying table
196 just has accrefs. If your code actually uses the pubDID to search
197 in the table (and it probably shouldn't), better use an actual column
198 and index it.
199 """
200 auth = config.get("ivoa", "authority")
201 return "'ivo://%s/~?' || gavo_urlescape(%s)"%(
202 auth.replace("'", "''"),
203 fromCol)
204
206 """returns the string with the python RE pattern replaced with
207 replacement.
208
209 This is directly handed through to python re.sub, so you can (but
210 probably shouldn't) play all the RE tricks you can in python (e.g.,
211 back references).
212
213 If you find yourself having to use reSub, you should regard that as
214 an alarm sign that you're probably doing it wrong.
215
216 Oh: closing curly braces can be included in the argument by
217 backslash-escaping them.
218 """
219 return re.sub(pattern, replacement, string)
220
221
223 """is a mixin providing some macros for scripting's MacroExpander.
224
225 The class mixing in needs to provide its resource descriptor in the
226 rd attribute.
227 """
229 """returns __EMPTY__ if val is empty.
230
231 This is necessary when feeding possibly empty params from mixin
232 parameters (don't worry if you don't understand this).
233 """
234 if val:
235 return val
236 else:
237 return "__EMPTY__"
238
240 """the identifier of the current resource descriptor.
241 """
242 return self.rd.sourceId
243
245 """the identifier for the current resource descriptor with slashes replaced
246 with dots (so they work as the "host part" in URIs.
247 """
248 return self.rd.sourceId.replace("/", ".")
249
251 """the schema of the current resource descriptor.
252 """
253 return self.rd.schema
254
256 """a link to an internal service; id is <rdId>/<serviceId>/<renderer>,
257 title, if given, is the anchor text.
258
259 The result is a link in the short form for restructured test.
260 """
261 if title is None:
262 title = serviceId
263 return "`%s <%s>`_"%(title, osinter.makeSitePath(serviceId))
264
271
273 """an absolute URL from a path relative to the DC root.
274 """
275 return osinter.makeAbsoluteURL(relPath)
276
278 """wraps urllib.quote.
279 """
280 return urllib.quote(string)
281
283 """today's date in ISO representation.
284 """
285 return str(datetime.date.today())
286
288 """the current value of configuration item {section}{name}.
289
290 You can also only give one argument to access settings from the
291 general section.
292 """
293 if name is None:
294 section, name = "general", section
295 val = config.get(section, name)
296 if isinstance(val, basestring):
297 return val
298 else:
299 return str(val)
300
326
339
340
342 """returns aString uppercased.
343
344 There's no guarantees for characters outside ASCII.
345 """
346 return aString.upper()
347
349 """returns aString with the first character lowercased.
350 """
351 if aString:
352 return aString[0].lower()+aString[1:]
353
355 """always "test macro expansion".
356 """
357 return "test macro expansion"
358
359
360 -class MacDef(structure.Structure):
385
386
390