param.py 7.71 KB
Newer Older
Kipp Cannon's avatar
Kipp Cannon committed
1
# Copyright (C) 2006--2009,2012--2019  Kipp Cannon
2 3 4
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
duncan's avatar
duncan committed
5
# Free Software Foundation; either version 3 of the License, or (at your
6 7 8 9 10 11 12 13 14 15 16
# option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General
# Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

kipp's avatar
kipp committed
17

18 19 20 21 22 23 24 25
#
# =============================================================================
#
#                                   Preamble
#
# =============================================================================
#

kipp's avatar
kipp committed
26

27 28 29 30 31
"""
High-level support for Param elements.
"""


32 33
import re
import sys
34
from xml.sax.saxutils import escape as xmlescape
Kipp Cannon's avatar
Kipp Cannon committed
35
import yaml
36

37

38
from . import __author__, __date__, __version__
39 40
from . import ligolw
from . import types as ligolwtypes
41 42 43 44 45


#
# =============================================================================
#
46
#                                  Utilities
47 48 49 50
#
# =============================================================================
#

kipp's avatar
kipp committed
51

52 53 54 55 56
def get_param(xmldoc, name):
	"""
	Scan xmldoc for a param named name.  Raises ValueError if not
	exactly 1 such param is found.
	"""
57
	params = Param.getParamsByName(xmldoc, name)
58
	if len(params) != 1:
59
		raise ValueError("document must contain exactly one %s param" % Param.ParamName(name))
60 61 62
	return params[0]


kipp's avatar
kipp committed
63 64 65 66 67
def get_pyvalue(xml, name):
	"""
	Convenience wrapper for get_param() that recovers an instance of a
	Python builtin type from a Param element.
	"""
68 69 70
	# Note:  the Param is automatically parsed into the correct Python
	# type, so this function is mostly a no-op.
	return get_param(xml, name).pcdata
kipp's avatar
kipp committed
71 72


73 74 75 76 77 78 79 80
#
# =============================================================================
#
#                               Element Classes
#
# =============================================================================
#

kipp's avatar
kipp committed
81 82

#
83 84 85 86 87 88 89 90 91 92
# FIXME: params of type string should be quoted in order to correctly
# delimit their extent.  If that were done, then the pcdata in a Param
# element could be parsed using the Stream tokenizer (i.e., as though it
# were a single-token stream), which would guarantee that Stream data and
# Param data is parsed using the exact same rules.  Unfortunately, common
# practice is to not quote Param string values, so we parse things
# differently here.  In particular, we strip whitespace from the start and
# stop of all Param pcdata.  If this causes your string Param values to be
# corrupted (because you need leading and trailing white space preserved),
# then you need to make everyone switch to quoting their string Param
kipp's avatar
kipp committed
93
# values, and once that is done then this code will be changed.  Perhaps a
94 95
# warning should be emitted for non-quoted strings to encourage a
# transition?
kipp's avatar
kipp committed
96 97
#

98

99 100
class Param(ligolw.Param):
	"""
101 102
	High-level Param element.  The value is stored in the pcdata
	attribute as the native Python type rather than as a string.
103
	"""
104 105 106
	class ParamName(ligolw.LLWNameAttr):
		dec_pattern = re.compile(r"(?P<Name>[a-z0-9_:]+):param\Z")
		enc_pattern = u"%s:param"
Kipp Cannon's avatar
Kipp Cannon committed
107

108
	Name = ligolw.attributeproxy(u"Name", enc = ParamName.enc, dec = ParamName)
Kipp Cannon's avatar
Kipp Cannon committed
109 110 111
	Scale = ligolw.attributeproxy(u"Scale", enc = ligolwtypes.FormatFunc[u"real_8"], dec = ligolwtypes.ToPyType[u"real_8"])
	Type = ligolw.attributeproxy(u"Type", default = u"lstring")

112
	def endElement(self):
113 114
		if self.pcdata is not None:
			# convert pcdata from string to native Python type
115
			if self.Type == u"yaml":
116 117 118
				self.pcdata = yaml.load(self.pcdata)
			else:
				self.pcdata = ligolwtypes.ToPyType[self.Type](self.pcdata.strip())
119

120
	def write(self, fileobj = sys.stdout, indent = u""):
121
		fileobj.write(self.start_tag(indent))
122 123
		for c in self.childNodes:
			if c.tagName not in self.validchildren:
124
				raise ligolw.ElementError("invalid child %s for %s" % (c.tagName, self.tagName))
125
			c.write(fileobj, indent + ligolw.Indent)
126
		if self.pcdata is not None:
127 128
			if self.Type == u"yaml":
				fileobj.write(xmlescape(yaml.dump(self.pcdata).strip()))
129 130
			else:
				# we have to strip quote characters from
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147
				# string formats (see comment above).  if
				# the result is a zero-length string it
				# will get parsed as None when the document
				# is loaded, but on this code path we know
				# that .pcdata is not None, so as a hack
				# until something better comes along we
				# replace zero-length strings here with a
				# bit of whitespace.  whitespace is
				# stripped from strings during parsing so
				# this will turn .pcdata back into a
				# zero-length string.  NOTE:  if .pcdata is
				# None, then it will become a zero-length
				# string, which will be turned back into
				# None on parsing, so this mechanism is how
				# None is encoded (a zero-length Param is
				# None)
				fileobj.write(xmlescape(ligolwtypes.FormatFunc[self.Type](self.pcdata).strip(u"\"") or u" "))
148 149
		fileobj.write(self.end_tag(u"") + u"\n")

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
	@classmethod
	def build(cls, name, Type, value, start = None, scale = None, unit = None, dataunit = None, comment = None):
		"""
		Construct a LIGO Light Weight XML Param document subtree.
		FIXME: document keyword arguments.
		"""
		elem = cls()
		elem.Name = name
		elem.Type = Type
		elem.pcdata = value
		# FIXME:  I have no idea how most of the attributes should be
		# encoded, I don't even know what they're supposed to be.
		if dataunit is not None:
			elem.DataUnit = dataunit
		if scale is not None:
			elem.Scale = scale
		if start is not None:
			elem.Start = start
		if unit is not None:
			elem.Unit = unit
		if comment is not None:
			elem.appendChild(ligolw.Comment()).pcdata = comment
		return elem

	@classmethod
	def from_pyvalue(cls, name, value, **kwargs):
		"""
		Convenience wrapper for .build() that constructs a Param
		element from an instance of a Python builtin type.  See
		.build() for a description of the valid keyword arguments.
180 181 182

		Examples:

Kipp Cannon's avatar
Kipp Cannon committed
183 184
		>>> import sys
		>>> Param.from_pyvalue(u"float", 3.0).write(sys.stdout)
Kipp Cannon's avatar
Kipp Cannon committed
185
		<Param Name="float:param" Type="real_8">3</Param>
Kipp Cannon's avatar
Kipp Cannon committed
186
		>>> Param.from_pyvalue(u"string", u"test").write(sys.stdout)
Kipp Cannon's avatar
Kipp Cannon committed
187
		<Param Name="string:param" Type="lstring">test</Param>
Kipp Cannon's avatar
Kipp Cannon committed
188
		>>> Param.from_pyvalue(u"shortstring", u"").write(sys.stdout)
Kipp Cannon's avatar
Kipp Cannon committed
189
		<Param Name="shortstring:param" Type="lstring"> </Param>
Kipp Cannon's avatar
Kipp Cannon committed
190
		>>> Param.from_pyvalue(u"none", None).write(sys.stdout)
Kipp Cannon's avatar
Kipp Cannon committed
191
		<Param Name="none:param" Type="None"></Param>
192
		"""
193 194 195
		if value is not None:
			return cls.build(name, ligolwtypes.FromPyType[type(value)], value, **kwargs)
		return cls.build(name, None, None, **kwargs)
196

197 198 199 200 201 202 203 204
	@classmethod
	def getParamsByName(cls, elem, name):
		"""
		Return a list of params with name name under elem.
		"""
		name = cls.ParamName(name)
		return elem.getElements(lambda e: (e.tagName == cls.tagName) and (e.Name == name))

205

206 207 208 209 210 211 212 213
#
# =============================================================================
#
#                               Content Handler
#
# =============================================================================
#

kipp's avatar
kipp committed
214

215
#
216
# Override portions of a ligolw.LIGOLWContentHandler class
217 218
#

kipp's avatar
kipp committed
219

220 221 222
def use_in(ContentHandler):
	"""
	Modify ContentHandler, a sub-class of
223
	ligo.lw.ligolw.LIGOLWContentHandler, to cause it to use the Param
224 225 226 227
	class defined in this module when parsing XML documents.

	Example:

228
	>>> from ligo.lw import ligolw
229
	>>> class MyContentHandler(ligolw.LIGOLWContentHandler):
230 231
	...	pass
	...
232
	>>> use_in(MyContentHandler)
233
	<class 'ligo.lw.param.MyContentHandler'>
234
	"""
235
	def startParam(self, parent, attrs):
236
		return Param(attrs)
237

238
	ContentHandler.startParam = startParam
239

240
	return ContentHandler