source: sasview/src/sas/sascalc/dataloader/readers/cansas_reader.py @ 0e0c645

ticket-1243
Last change on this file since 0e0c645 was 0e0c645, checked in by Jeff Krzywon <jkrzywon@…>, 5 years ago

Fix for loading 2D saved projects and small clean up of cansas XML reader.

  • Property mode set to 100644
File size: 61.1 KB
Line 
1import logging
2import os
3import sys
4import datetime
5import inspect
6
7import numpy as np
8
9# The following 2 imports *ARE* used. Do not remove either.
10import xml.dom.minidom
11from xml.dom.minidom import parseString
12
13from lxml import etree
14
15from sas.sascalc.data_util.nxsunit import Converter
16
17# For saving individual sections of data
18from ..data_info import Data1D, Data2D, DataInfo, plottable_1D, plottable_2D, \
19    Collimation, TransmissionSpectrum, Detector, Process, Aperture
20from ..loader_exceptions import FileContentsException, DefaultReaderException, \
21    DataReaderException
22from . import xml_reader
23from .xml_reader import XMLreader
24from .cansas_constants import CansasConstants
25
26logger = logging.getLogger(__name__)
27
28PREPROCESS = "xmlpreprocess"
29ENCODING = "encoding"
30RUN_NAME_DEFAULT = "None"
31INVALID_SCHEMA_PATH_1_1 = "{0}/sas/sascalc/dataloader/readers/schema/cansas1d_invalid_v1_1.xsd"
32INVALID_SCHEMA_PATH_1_0 = "{0}/sas/sascalc/dataloader/readers/schema/cansas1d_invalid_v1_0.xsd"
33INVALID_XML = "\n\nThe loaded xml file, {0} does not fully meet the CanSAS v1.x specification. SasView loaded " + \
34              "as much of the data as possible.\n\n"
35
36CONSTANTS = CansasConstants()
37CANSAS_FORMAT = CONSTANTS.format
38CANSAS_NS = CONSTANTS.names
39ALLOW_ALL = True
40
41class Reader(XMLreader):
42    cansas_version = "1.0"
43    base_ns = "{cansas1d/1.0}"
44    cansas_defaults = None
45    type_name = "canSAS"
46    invalid = True
47    frm = ""
48    # Log messages and errors
49    logging = None
50    errors = set()
51    # Namespace hierarchy for current xml_file object
52    names = None
53    ns_list = None
54    # Temporary storage location for loading multiple data sets in a single file
55    current_data1d = None
56    data = None
57    # Wildcards
58    type = ["XML files (*.xml)|*.xml", "SasView Save Files (*.svs)|*.svs"]
59    # List of allowed extensions
60    ext = ['.xml', '.svs']
61    # Flag to bypass extension check
62    allow_all = True
63
64    def reset_state(self):
65        """
66        Resets the class state to a base case when loading a new data file so previous
67        data files do not appear a second time
68        """
69        super(Reader, self).reset_state()
70        self.data = []
71        self.process = Process()
72        self.transspectrum = TransmissionSpectrum()
73        self.aperture = Aperture()
74        self.collimation = Collimation()
75        self.detector = Detector()
76        self.names = []
77        self.cansas_defaults = {}
78        self.ns_list = None
79        self.logging = []
80        self.encoding = None
81
82    def _read(self, xml_file, schema_path="", invalid=True):
83        if schema_path != "" or not invalid:
84            # read has been called from self.get_file_contents because xml file doens't conform to schema
85            _, self.extension = os.path.splitext(os.path.basename(xml_file))
86            return self.get_file_contents(xml_file=xml_file, schema_path=schema_path, invalid=invalid)
87
88        # Otherwise, read has been called by the data loader - file_reader_base_class handles this
89        return super(XMLreader, self).read(xml_file)
90
91    def get_file_contents(self):
92        return self._get_file_contents(xml_file=None, schema_path="", invalid=True)
93
94    def _get_file_contents(self, xml_file=None, schema_path="", invalid=True):
95        # Reset everything since we're loading a new file
96        self.reset_state()
97        self.invalid = invalid
98        if xml_file is None:
99            xml_file = self.f_open.name
100        # We don't sure f_open since lxml handles opnening/closing files
101        try:
102            # Raises FileContentsException
103            self.load_file_and_schema(xml_file, schema_path)
104            # Parse each SASentry
105            entry_list = self.xmlroot.xpath('/ns:SASroot/ns:SASentry',
106                                            namespaces={
107                                                'ns': self.cansas_defaults.get(
108                                                    "ns")
109                                            })
110            self.is_cansas(self.extension)
111            self.set_processing_instructions()
112            for entry in entry_list:
113                self._parse_entry(entry)
114                self.data_cleanup()
115        except FileContentsException as fc_exc:
116            # File doesn't meet schema - try loading with a less strict schema
117            base_name = xml_reader.__file__
118            base_name = base_name.replace("\\", "/")
119            base = base_name.split("/sas/")[0]
120            if self.cansas_version == "1.1":
121                invalid_schema = INVALID_SCHEMA_PATH_1_1.format(base, self.cansas_defaults.get("schema"))
122            else:
123                invalid_schema = INVALID_SCHEMA_PATH_1_0.format(base, self.cansas_defaults.get("schema"))
124            self.set_schema(invalid_schema)
125            if self.invalid:
126                try:
127                    # Load data with less strict schema
128                    self._get_file_contents(xml_file, invalid_schema, False)
129
130                    # File can still be read but doesn't match schema, so raise exception
131                    self.load_file_and_schema(xml_file) # Reload strict schema so we can find where error are in file
132                    invalid_xml = self.find_invalid_xml()
133                    if invalid_xml != "":
134                        basename, _ = os.path.splitext(
135                            os.path.basename(self.f_open.name))
136                        invalid_xml = INVALID_XML.format(basename + self.extension) + invalid_xml
137                        raise DataReaderException(invalid_xml) # Handled by base class
138                except FileContentsException as fc_exc:
139                    msg = "CanSAS Reader could not load the file {}".format(xml_file)
140                    if fc_exc.message is not None: # Propagate error messages from earlier
141                        msg = fc_exc.message
142                    if not self.extension in self.ext: # If the file has no associated loader
143                        raise DefaultReaderException(msg)
144                    raise FileContentsException(msg)
145                    pass
146            else:
147                raise fc_exc
148        except Exception as e: # Convert all other exceptions to FileContentsExceptions
149            raise FileContentsException(str(e))
150        finally:
151            if not self.f_open.closed:
152                self.f_open.close()
153
154    def load_file_and_schema(self, xml_file, schema_path=""):
155        base_name = xml_reader.__file__
156        base_name = base_name.replace("\\", "/")
157        base = base_name.split("/sas/")[0]
158
159        # Try and parse the XML file
160        try:
161            self.set_xml_file(xml_file)
162        except etree.XMLSyntaxError: # File isn't valid XML so can't be loaded
163            msg = "SasView cannot load {}.\nInvalid XML syntax".format(xml_file)
164            raise FileContentsException(msg)
165
166        self.cansas_version = self.xmlroot.get("version", "1.0")
167        self.cansas_defaults = CANSAS_NS.get(self.cansas_version, "1.0")
168
169        if schema_path == "":
170            schema_path = "{}/sas/sascalc/dataloader/readers/schema/{}".format(
171                base, self.cansas_defaults.get("schema").replace("\\", "/")
172            )
173        self.set_schema(schema_path)
174
175    def is_cansas(self, ext="xml"):
176        """
177        Checks to see if the XML file is a CanSAS file
178
179        :param ext: The file extension of the data file
180        :raises FileContentsException: Raised if XML file isn't valid CanSAS
181        """
182        if self.validate_xml(): # Check file is valid XML
183            name = "{http://www.w3.org/2001/XMLSchema-instance}schemaLocation"
184            value = self.xmlroot.get(name)
185            # Check schema CanSAS version matches file CanSAS version
186            if CANSAS_NS.get(self.cansas_version).get("ns") == value.rsplit(" ")[0]:
187                return True
188        if ext == "svs":
189            return True # Why is this required?
190        # If we get to this point then file isn't valid CanSAS
191        logger.warning("File doesn't meet CanSAS schema. Trying to load anyway.")
192        raise FileContentsException("The file is not valid CanSAS")
193
194    def _parse_entry(self, dom, recurse=False):
195        if not self._is_call_local() and not recurse:
196            self.reset_state()
197        if not recurse:
198            self.current_datainfo = DataInfo()
199            # Raises FileContentsException if file doesn't meet CanSAS schema
200            self.invalid = False
201            # Look for a SASentry
202            self.data = []
203            self.parent_class = "SASentry"
204            self.names.append("SASentry")
205            self.current_datainfo.meta_data["loader"] = "CanSAS XML 1D"
206            self.current_datainfo.meta_data[
207                PREPROCESS] = self.processing_instructions
208        if self._is_call_local() and not recurse:
209            basename, _ = os.path.splitext(os.path.basename(self.f_open.name))
210            self.current_datainfo.filename = basename + self.extension
211        # Create an empty dataset if no data has been passed to the reader
212        if self.current_dataset is None:
213            self._initialize_new_data_set(dom)
214        self.base_ns = "{" + CANSAS_NS.get(self.cansas_version).get("ns") + "}"
215
216        # Loop through each child in the parent element
217        for node in dom:
218            attr = node.attrib
219            name = attr.get("name", "")
220            type = attr.get("type", "")
221            # Get the element name and set the current names level
222            tagname = node.tag.replace(self.base_ns, "")
223            tagname_original = tagname
224            # Skip this iteration when loading in save state information
225            if tagname in ["fitting_plug_in", "pr_inversion", "invariant", "corfunc"]:
226                continue
227            # Get where to store content
228            self.names.append(tagname_original)
229            self.ns_list = CONSTANTS.iterate_namespace(self.names)
230            # If the element is a child element, recurse
231            if len(node.getchildren()) > 0:
232                self.parent_class = tagname_original
233                if tagname == 'SASdata':
234                    self._initialize_new_data_set(node)
235                    if isinstance(self.current_dataset, plottable_2D):
236                        x_bins = attr.get("x_bins", "")
237                        y_bins = attr.get("y_bins", "")
238                        if x_bins is not "" and y_bins is not "":
239                            self.current_dataset.shape = (x_bins, y_bins)
240                        else:
241                            self.current_dataset.shape = ()
242                # Recurse to access data within the group
243                self._parse_entry(node, recurse=True)
244                if tagname == "SASsample":
245                    self.current_datainfo.sample.name = name
246                elif tagname == "beam_size":
247                    self.current_datainfo.source.beam_size_name = name
248                elif tagname == "SAScollimation":
249                    self.collimation.name = name
250                elif tagname == "aperture":
251                    self.aperture.name = name
252                    self.aperture.type = type
253                self._add_intermediate()
254            else:
255                # TODO: Clean this up to make it faster (fewer if/elifs)
256                if isinstance(self.current_dataset, plottable_2D):
257                    data_point = node.text
258                    unit = attr.get('unit', '')
259                else:
260                    data_point, unit = self._get_node_value(node, tagname)
261
262                # If this is a dataset, store the data appropriately
263                if tagname == 'Run':
264                    self.current_datainfo.run_name[data_point] = name
265                    self.current_datainfo.run.append(data_point)
266                elif tagname == 'Title':
267                    self.current_datainfo.title = data_point
268                elif tagname == 'SASnote':
269                    self.current_datainfo.notes.append(data_point)
270
271                # I and Q points
272                elif tagname == 'I' and isinstance(self.current_dataset, plottable_1D):
273                    self.current_dataset.yaxis("Intensity", unit)
274                    self.current_dataset.y = np.append(self.current_dataset.y, data_point)
275                elif tagname == 'Idev' and isinstance(self.current_dataset, plottable_1D):
276                    self.current_dataset.dy = np.append(self.current_dataset.dy, data_point)
277                elif tagname == 'Q':
278                    self.current_dataset.xaxis("Q", unit)
279                    self.current_dataset.x = np.append(self.current_dataset.x, data_point)
280                elif tagname == 'Qdev':
281                    self.current_dataset.dx = np.append(self.current_dataset.dx, data_point)
282                elif tagname == 'dQw':
283                   self.current_dataset.dxw = np.append(self.current_dataset.dxw, data_point)
284                elif tagname == 'dQl':
285                    self.current_dataset.dxl = np.append(self.current_dataset.dxl, data_point)
286                elif tagname == 'Qmean':
287                    pass
288                elif tagname == 'Shadowfactor':
289                    pass
290                elif tagname == 'Sesans':
291                    self.current_datainfo.isSesans = bool(data_point)
292                    self.current_dataset.xaxis(attr.get('x_axis'),
293                                                attr.get('x_unit'))
294                    self.current_dataset.yaxis(attr.get('y_axis'),
295                                                attr.get('y_unit'))
296                elif tagname == 'yacceptance':
297                    self.current_datainfo.sample.yacceptance = (data_point, unit)
298                elif tagname == 'zacceptance':
299                    self.current_datainfo.sample.zacceptance = (data_point, unit)
300
301                # I and Qx, Qy - 2D data
302                elif tagname == 'I' and isinstance(self.current_dataset, plottable_2D):
303                    self.current_dataset.yaxis("Intensity", unit)
304                    self.current_dataset.data = np.fromstring(data_point, dtype=float, sep=",")
305                elif tagname == 'Idev' and isinstance(self.current_dataset, plottable_2D):
306                    self.current_dataset.err_data = np.fromstring(data_point, dtype=float, sep=",")
307                elif tagname == 'Qx':
308                    self.current_dataset.xaxis("Qx", unit)
309                    self.current_dataset.qx_data = np.fromstring(data_point, dtype=float, sep=",")
310                elif tagname == 'Qy':
311                    self.current_dataset.yaxis("Qy", unit)
312                    self.current_dataset.qy_data = np.fromstring(data_point, dtype=float, sep=",")
313                elif tagname == 'Qxdev':
314                    self.current_dataset.xaxis("Qxdev", unit)
315                    self.current_dataset.dqx_data = np.fromstring(data_point, dtype=float, sep=",")
316                elif tagname == 'Qydev':
317                    self.current_dataset.yaxis("Qydev", unit)
318                    self.current_dataset.dqy_data = np.fromstring(data_point, dtype=float, sep=",")
319                elif tagname == 'Mask':
320                    inter = [item == "1" for item in data_point.split(",")]
321                    self.current_dataset.mask = np.asarray(inter, dtype=bool)
322
323                # Sample Information
324                elif tagname == 'ID' and self.parent_class == 'SASsample':
325                    self.current_datainfo.sample.ID = data_point
326                elif tagname == 'Title' and self.parent_class == 'SASsample':
327                    self.current_datainfo.sample.name = data_point
328                elif tagname == 'thickness' and self.parent_class == 'SASsample':
329                    self.current_datainfo.sample.thickness = data_point
330                    self.current_datainfo.sample.thickness_unit = unit
331                elif tagname == 'transmission' and self.parent_class == 'SASsample':
332                    self.current_datainfo.sample.transmission = data_point
333                elif tagname == 'temperature' and self.parent_class == 'SASsample':
334                    self.current_datainfo.sample.temperature = data_point
335                    self.current_datainfo.sample.temperature_unit = unit
336                elif tagname == 'details' and self.parent_class == 'SASsample':
337                    self.current_datainfo.sample.details.append(data_point)
338                elif tagname == 'x' and self.parent_class == 'position':
339                    self.current_datainfo.sample.position.x = data_point
340                    self.current_datainfo.sample.position_unit = unit
341                elif tagname == 'y' and self.parent_class == 'position':
342                    self.current_datainfo.sample.position.y = data_point
343                    self.current_datainfo.sample.position_unit = unit
344                elif tagname == 'z' and self.parent_class == 'position':
345                    self.current_datainfo.sample.position.z = data_point
346                    self.current_datainfo.sample.position_unit = unit
347                elif tagname == 'roll' and self.parent_class == 'orientation' and 'SASsample' in self.names:
348                    self.current_datainfo.sample.orientation.x = data_point
349                    self.current_datainfo.sample.orientation_unit = unit
350                elif tagname == 'pitch' and self.parent_class == 'orientation' and 'SASsample' in self.names:
351                    self.current_datainfo.sample.orientation.y = data_point
352                    self.current_datainfo.sample.orientation_unit = unit
353                elif tagname == 'yaw' and self.parent_class == 'orientation' and 'SASsample' in self.names:
354                    self.current_datainfo.sample.orientation.z = data_point
355                    self.current_datainfo.sample.orientation_unit = unit
356
357                # Instrumental Information
358                elif tagname == 'name' and self.parent_class == 'SASinstrument':
359                    self.current_datainfo.instrument = data_point
360
361                # Detector Information
362                elif tagname == 'name' and self.parent_class == 'SASdetector':
363                    self.detector.name = data_point
364                elif tagname == 'SDD' and self.parent_class == 'SASdetector':
365                    self.detector.distance = data_point
366                    self.detector.distance_unit = unit
367                elif tagname == 'slit_length' and self.parent_class == 'SASdetector':
368                    self.detector.slit_length = data_point
369                    self.detector.slit_length_unit = unit
370                elif tagname == 'x' and self.parent_class == 'offset':
371                    self.detector.offset.x = data_point
372                    self.detector.offset_unit = unit
373                elif tagname == 'y' and self.parent_class == 'offset':
374                    self.detector.offset.y = data_point
375                    self.detector.offset_unit = unit
376                elif tagname == 'z' and self.parent_class == 'offset':
377                    self.detector.offset.z = data_point
378                    self.detector.offset_unit = unit
379                elif tagname == 'x' and self.parent_class == 'beam_center':
380                    self.detector.beam_center.x = data_point
381                    self.detector.beam_center_unit = unit
382                elif tagname == 'y' and self.parent_class == 'beam_center':
383                    self.detector.beam_center.y = data_point
384                    self.detector.beam_center_unit = unit
385                elif tagname == 'z' and self.parent_class == 'beam_center':
386                    self.detector.beam_center.z = data_point
387                    self.detector.beam_center_unit = unit
388                elif tagname == 'x' and self.parent_class == 'pixel_size':
389                    self.detector.pixel_size.x = data_point
390                    self.detector.pixel_size_unit = unit
391                elif tagname == 'y' and self.parent_class == 'pixel_size':
392                    self.detector.pixel_size.y = data_point
393                    self.detector.pixel_size_unit = unit
394                elif tagname == 'z' and self.parent_class == 'pixel_size':
395                    self.detector.pixel_size.z = data_point
396                    self.detector.pixel_size_unit = unit
397                elif tagname == 'roll' and self.parent_class == 'orientation' and 'SASdetector' in self.names:
398                    self.detector.orientation.x = data_point
399                    self.detector.orientation_unit = unit
400                elif tagname == 'pitch' and self.parent_class == 'orientation' and 'SASdetector' in self.names:
401                    self.detector.orientation.y = data_point
402                    self.detector.orientation_unit = unit
403                elif tagname == 'yaw' and self.parent_class == 'orientation' and 'SASdetector' in self.names:
404                    self.detector.orientation.z = data_point
405                    self.detector.orientation_unit = unit
406
407                # Collimation and Aperture
408                elif tagname == 'length' and self.parent_class == 'SAScollimation':
409                    self.collimation.length = data_point
410                    self.collimation.length_unit = unit
411                elif tagname == 'name' and self.parent_class == 'SAScollimation':
412                    self.collimation.name = data_point
413                elif tagname == 'distance' and self.parent_class == 'aperture':
414                    self.aperture.distance = data_point
415                    self.aperture.distance_unit = unit
416                elif tagname == 'x' and self.parent_class == 'size':
417                    self.aperture.size.x = data_point
418                    self.collimation.size_unit = unit
419                elif tagname == 'y' and self.parent_class == 'size':
420                    self.aperture.size.y = data_point
421                    self.collimation.size_unit = unit
422                elif tagname == 'z' and self.parent_class == 'size':
423                    self.aperture.size.z = data_point
424                    self.collimation.size_unit = unit
425
426                # Process Information
427                elif tagname == 'name' and self.parent_class == 'SASprocess':
428                    self.process.name = data_point
429                elif tagname == 'description' and self.parent_class == 'SASprocess':
430                    self.process.description = data_point
431                elif tagname == 'date' and self.parent_class == 'SASprocess':
432                    try:
433                        self.process.date = datetime.datetime.fromtimestamp(data_point)
434                    except:
435                        self.process.date = data_point
436                elif tagname == 'SASprocessnote':
437                    self.process.notes.append(data_point)
438                elif tagname == 'term' and self.parent_class == 'SASprocess':
439                    unit = attr.get("unit", "")
440                    dic = { "name": name, "value": data_point, "unit": unit }
441                    self.process.term.append(dic)
442
443                # Transmission Spectrum
444                elif tagname == 'T' and self.parent_class == 'Tdata':
445                    self.transspectrum.transmission = np.append(self.transspectrum.transmission, data_point)
446                    self.transspectrum.transmission_unit = unit
447                elif tagname == 'Tdev' and self.parent_class == 'Tdata':
448                    self.transspectrum.transmission_deviation = np.append(self.transspectrum.transmission_deviation, data_point)
449                    self.transspectrum.transmission_deviation_unit = unit
450                elif tagname == 'Lambda' and self.parent_class == 'Tdata':
451                    self.transspectrum.wavelength = np.append(self.transspectrum.wavelength, data_point)
452                    self.transspectrum.wavelength_unit = unit
453
454                # Source Information
455                elif tagname == 'wavelength' and (self.parent_class == 'SASsource' or self.parent_class == 'SASData'):
456                    self.current_datainfo.source.wavelength = data_point
457                    self.current_datainfo.source.wavelength_unit = unit
458                elif tagname == 'wavelength_min' and self.parent_class == 'SASsource':
459                    self.current_datainfo.source.wavelength_min = data_point
460                    self.current_datainfo.source.wavelength_min_unit = unit
461                elif tagname == 'wavelength_max' and self.parent_class == 'SASsource':
462                    self.current_datainfo.source.wavelength_max = data_point
463                    self.current_datainfo.source.wavelength_max_unit = unit
464                elif tagname == 'wavelength_spread' and self.parent_class == 'SASsource':
465                    self.current_datainfo.source.wavelength_spread = data_point
466                    self.current_datainfo.source.wavelength_spread_unit = unit
467                elif tagname == 'x' and self.parent_class == 'beam_size':
468                    self.current_datainfo.source.beam_size.x = data_point
469                    self.current_datainfo.source.beam_size_unit = unit
470                elif tagname == 'y' and self.parent_class == 'beam_size':
471                    self.current_datainfo.source.beam_size.y = data_point
472                    self.current_datainfo.source.beam_size_unit = unit
473                elif tagname == 'z' and self.parent_class == 'pixel_size':
474                    self.current_datainfo.source.data_point.z = data_point
475                    self.current_datainfo.source.beam_size_unit = unit
476                elif tagname == 'radiation' and self.parent_class == 'SASsource':
477                    self.current_datainfo.source.radiation = data_point
478                elif tagname == 'beam_shape' and self.parent_class == 'SASsource':
479                    self.current_datainfo.source.beam_shape = data_point
480
481                # Everything else goes in meta_data
482                else:
483                    new_key = self._create_unique_key(self.current_datainfo.meta_data, tagname)
484                    self.current_datainfo.meta_data[new_key] = data_point
485
486            self.names.remove(tagname_original)
487            length = 0
488            if len(self.names) > 1:
489                length = len(self.names) - 1
490            self.parent_class = self.names[length]
491        if not self._is_call_local() and not recurse:
492            self.frm = ""
493            self.current_datainfo.errors = set()
494            for error in self.errors:
495                self.current_datainfo.errors.add(error)
496            self.data_cleanup()
497            self.sort_data()
498            self.reset_data_list()
499            return self.output[0], None
500
501    def _is_call_local(self):
502        if self.frm == "":
503            inter = inspect.stack()
504            self.frm = inter[2]
505        mod_name = self.frm[1].replace("\\", "/").replace(".pyc", "")
506        mod_name = mod_name.replace(".py", "")
507        mod = mod_name.split("sas/")
508        mod_name = mod[1]
509        if mod_name != "sascalc/dataloader/readers/cansas_reader":
510            return False
511        return True
512
513    def _add_intermediate(self):
514        """
515        This method stores any intermediate objects within the final data set after fully reading the set.
516        """
517        if self.parent_class == 'SASprocess':
518            self.current_datainfo.process.append(self.process)
519            self.process = Process()
520        elif self.parent_class == 'SASdetector':
521            self.current_datainfo.detector.append(self.detector)
522            self.detector = Detector()
523        elif self.parent_class == 'SAStransmission_spectrum':
524            self.current_datainfo.trans_spectrum.append(self.transspectrum)
525            self.transspectrum = TransmissionSpectrum()
526        elif self.parent_class == 'SAScollimation':
527            self.current_datainfo.collimation.append(self.collimation)
528            self.collimation = Collimation()
529        elif self.parent_class == 'aperture':
530            self.collimation.aperture.append(self.aperture)
531            self.aperture = Aperture()
532        elif self.parent_class == 'SASdata':
533            self.data.append(self.current_dataset)
534
535    def _get_node_value(self, node, tagname):
536        """
537        Get the value of a node and any applicable units
538
539        :param node: The XML node to get the value of
540        :param tagname: The tagname of the node
541        """
542        #Get the text from the node and convert all whitespace to spaces
543        units = ''
544        node_value = node.text
545        if node_value is not None:
546            node_value = ' '.join(node_value.split())
547        else:
548            node_value = ""
549
550        # If the value is a float, compile with units.
551        if self.ns_list.ns_datatype == "float":
552            # If an empty value is given, set as zero.
553            if node_value is None or node_value.isspace() \
554                                    or node_value.lower() == "nan":
555                node_value = "0.0"
556            #Convert the value to the base units
557            node_value, units = self._unit_conversion(node, tagname, node_value)
558
559        # If the value is a timestamp, convert to a datetime object
560        elif self.ns_list.ns_datatype == "timestamp":
561            if node_value is None or node_value.isspace():
562                pass
563            else:
564                try:
565                    node_value = \
566                        datetime.datetime.fromtimestamp(node_value)
567                except ValueError:
568                    node_value = None
569        return node_value, units
570
571    def _unit_conversion(self, node, tagname, node_value):
572        """
573        A unit converter method used to convert the data included in the file
574        to the default units listed in data_info
575
576        :param node: XML node
577        :param tagname: name of the node
578        :param node_value: The value of the current dom node
579        """
580        attr = node.attrib
581        value_unit = ''
582        err_msg = None
583        default_unit = None
584        if not isinstance(node_value, float):
585            node_value = float(node_value)
586        if 'unit' in attr and attr.get('unit') is not None:
587            try:
588                unit = attr['unit']
589                # Split the units to retain backwards compatibility with
590                # projects, analyses, and saved data from v4.1.0
591                unit_list = unit.split("|")
592                if len(unit_list) > 1:
593                    local_unit = unit_list[1]
594                else:
595                    local_unit = unit
596                unitname = self.ns_list.current_level.get("unit", "")
597                if "SASdetector" in self.names:
598                    save_in = "detector"
599                elif "aperture" in self.names:
600                    save_in = "aperture"
601                elif "SAScollimation" in self.names:
602                    save_in = "collimation"
603                elif "SAStransmission_spectrum" in self.names:
604                    save_in = "transspectrum"
605                elif "SASdata" in self.names:
606                    x = np.zeros(1)
607                    y = np.zeros(1)
608                    self.current_data1d = Data1D(x, y)
609                    save_in = "current_data1d"
610                elif "SASsource" in self.names:
611                    save_in = "current_datainfo.source"
612                elif "SASsample" in self.names:
613                    save_in = "current_datainfo.sample"
614                elif "SASprocess" in self.names:
615                    save_in = "process"
616                else:
617                    save_in = "current_datainfo"
618                default_unit = getattrchain(self, '.'.join((save_in, unitname)))
619                if (local_unit and default_unit
620                        and local_unit.lower() != default_unit.lower()
621                        and local_unit.lower() != "none"):
622                    # Check local units - bad units raise KeyError
623                    #print("loading", tagname, node_value, local_unit, default_unit)
624                    data_conv_q = Converter(local_unit)
625                    value_unit = default_unit
626                    node_value = data_conv_q(node_value, units=default_unit)
627                else:
628                    value_unit = local_unit
629            except KeyError:
630                # Do not throw an error for loading Sesans data in cansas xml
631                # This is a temporary fix.
632                if local_unit != "A" and local_unit != 'pol':
633                    err_msg = "CanSAS reader: unexpected "
634                    err_msg += "\"{0}\" unit [{1}]; "
635                    err_msg = err_msg.format(tagname, local_unit)
636                    err_msg += "expecting [{0}]".format(default_unit)
637                value_unit = local_unit
638            except Exception:
639                err_msg = "CanSAS reader: unknown error converting "
640                err_msg += "\"{0}\" unit [{1}]"
641                err_msg = err_msg.format(tagname, local_unit)
642                value_unit = local_unit
643        elif 'unit' in attr:
644            value_unit = attr['unit']
645        if err_msg:
646            self.errors.add(err_msg)
647        return node_value, value_unit
648
649    def _initialize_new_data_set(self, node=None):
650        if node is not None:
651            for child in node:
652                if child.tag.replace(self.base_ns, "") == "Idata":
653                    for i_child in child:
654                        if i_child.tag.replace(self.base_ns, "") == "Qx":
655                            self.current_dataset = plottable_2D()
656                            return
657        self.current_dataset = plottable_1D(np.array(0), np.array(0))
658
659    ## Writing Methods
660    def write(self, filename, datainfo):
661        """
662        Write the content of a Data1D as a CanSAS XML file
663
664        :param filename: name of the file to write
665        :param datainfo: Data1D object
666        """
667        # Create XML document
668        doc, _ = self._to_xml_doc(datainfo)
669        # Write the file
670        file_ref = open(filename, 'wb')
671        if self.encoding is None:
672            self.encoding = "UTF-8"
673        doc.write(file_ref, encoding=self.encoding,
674                  pretty_print=True, xml_declaration=True)
675        file_ref.close()
676
677    def _to_xml_doc(self, datainfo):
678        """
679        Create an XML document to contain the content of a Data1D
680
681        :param datainfo: Data1D object
682        """
683        is_2d = False
684        if issubclass(datainfo.__class__, Data2D):
685            is_2d = True
686
687        # Get PIs and create root element
688        pi_string = self._get_pi_string()
689        # Define namespaces and create SASroot object
690        main_node = self._create_main_node()
691        # Create ElementTree, append SASroot and apply processing instructions
692        base_string = pi_string + self.to_string(main_node)
693        base_element = self.create_element_from_string(base_string)
694        doc = self.create_tree(base_element)
695        # Create SASentry Element
696        entry_node = self.create_element("SASentry")
697        root = doc.getroot()
698        root.append(entry_node)
699
700        # Add Title to SASentry
701        self.write_node(entry_node, "Title", datainfo.title)
702        # Add Run to SASentry
703        self._write_run_names(datainfo, entry_node)
704        # Add Data info to SASEntry
705        if is_2d:
706            self._write_data_2d(datainfo, entry_node)
707        else:
708            self._write_data(datainfo, entry_node)
709        # Transmission Spectrum Info
710        # TODO: fix the writer to linearize all data, including T_spectrum
711        # self._write_trans_spectrum(datainfo, entry_node)
712        # Sample info
713        self._write_sample_info(datainfo, entry_node)
714        # Instrument info
715        instr = self._write_instrument(datainfo, entry_node)
716        #   Source
717        self._write_source(datainfo, instr)
718        #   Collimation
719        self._write_collimation(datainfo, instr)
720        #   Detectors
721        self._write_detectors(datainfo, instr)
722        # Processes info
723        self._write_process_notes(datainfo, entry_node)
724        # Note info
725        self._write_notes(datainfo, entry_node)
726        # Return the document, and the SASentry node associated with
727        #      the data we just wrote
728        # If the calling function was not the cansas reader, return a minidom
729        #      object rather than an lxml object.
730        self.frm = inspect.stack()[1]
731        doc, entry_node = self._check_origin(entry_node, doc)
732        return doc, entry_node
733
734    def write_node(self, parent, name, value, attr=None):
735        """
736        :param doc: document DOM
737        :param parent: parent node
738        :param name: tag of the element
739        :param value: value of the child text node
740        :param attr: attribute dictionary
741
742        :return: True if something was appended, otherwise False
743        """
744        if value is not None:
745            parent = self.ebuilder(parent, name, value, attr)
746            return True
747        return False
748
749    def _get_pi_string(self):
750        """
751        Creates the processing instructions header for writing to file
752        """
753        pis = self.return_processing_instructions()
754        if len(pis) > 0:
755            pi_tree = self.create_tree(pis[0])
756            i = 1
757            for i in range(1, len(pis) - 1):
758                pi_tree = self.append(pis[i], pi_tree)
759            pi_string = self.to_string(pi_tree)
760        else:
761            pi_string = ""
762        return pi_string
763
764    def _create_main_node(self):
765        """
766        Creates the primary xml header used when writing to file
767        """
768        xsi = "http://www.w3.org/2001/XMLSchema-instance"
769        version = self.cansas_version
770        n_s = CANSAS_NS.get(version).get("ns")
771        if version == "1.1":
772            url = "http://www.cansas.org/formats/1.1/"
773        else:
774            url = "http://svn.smallangles.net/svn/canSAS/1dwg/trunk/"
775        schema_location = "{0} {1}cansas1d.xsd".format(n_s, url)
776        attrib = {"{" + xsi + "}schemaLocation" : schema_location,
777                  "version" : version}
778        nsmap = {'xsi' : xsi, None: n_s}
779
780        main_node = self.create_element("{" + n_s + "}SASroot",
781                                        attrib=attrib, nsmap=nsmap)
782        return main_node
783
784    def _write_run_names(self, datainfo, entry_node):
785        """
786        Writes the run names to the XML file
787
788        :param datainfo: The Data1D object the information is coming from
789        :param entry_node: lxml node ElementTree object to be appended to
790        """
791        if datainfo.run is None or datainfo.run == []:
792            datainfo.run.append(RUN_NAME_DEFAULT)
793            datainfo.run_name[RUN_NAME_DEFAULT] = RUN_NAME_DEFAULT
794        for item in datainfo.run:
795            runname = {}
796            if item in datainfo.run_name and \
797            len(str(datainfo.run_name[item])) > 1:
798                runname = {'name': datainfo.run_name[item]}
799            self.write_node(entry_node, "Run", item, runname)
800
801    def _write_data(self, datainfo, entry_node):
802        """
803        Writes 1D I and Q data to the XML file
804
805        :param datainfo: The Data1D object the information is coming from
806        :param entry_node: lxml node ElementTree object to be appended to
807        """
808        node = self.create_element("SASdata")
809        self.append(node, entry_node)
810
811        for i in range(len(datainfo.x)):
812            point = self.create_element("Idata")
813            node.append(point)
814            self.write_node(point, "Q", datainfo.x[i],
815                            {'unit': datainfo._xunit})
816            if len(datainfo.y) >= i:
817                self.write_node(point, "I", datainfo.y[i],
818                                {'unit': datainfo._yunit})
819            if datainfo.dy is not None and len(datainfo.dy) > i:
820                self.write_node(point, "Idev", datainfo.dy[i],
821                                {'unit': datainfo._yunit})
822            if datainfo.dx is not None and len(datainfo.dx) > i:
823                self.write_node(point, "Qdev", datainfo.dx[i],
824                                {'unit': datainfo._xunit})
825            if datainfo.dxw is not None and len(datainfo.dxw) > i:
826                self.write_node(point, "dQw", datainfo.dxw[i],
827                                {'unit': datainfo._xunit})
828            if datainfo.dxl is not None and len(datainfo.dxl) > i:
829                self.write_node(point, "dQl", datainfo.dxl[i],
830                                {'unit': datainfo._xunit})
831        if datainfo.isSesans:
832            sesans_attrib = {'x_axis': datainfo._xaxis,
833                             'y_axis': datainfo._yaxis,
834                             'x_unit': datainfo.x_unit,
835                             'y_unit': datainfo.y_unit}
836            sesans = self.create_element("Sesans", attrib=sesans_attrib)
837            sesans.text = str(datainfo.isSesans)
838            entry_node.append(sesans)
839            self.write_node(entry_node, "yacceptance", datainfo.sample.yacceptance[0],
840                             {'unit': datainfo.sample.yacceptance[1]})
841            self.write_node(entry_node, "zacceptance", datainfo.sample.zacceptance[0],
842                             {'unit': datainfo.sample.zacceptance[1]})
843
844
845    def _write_data_2d(self, datainfo, entry_node):
846        """
847        Writes 2D data to the XML file
848
849        :param datainfo: The Data2D object the information is coming from
850        :param entry_node: lxml node ElementTree object to be appended to
851        """
852        attr = {}
853        if datainfo.data.shape:
854            attr["x_bins"] = str(len(datainfo.x_bins))
855            attr["y_bins"] = str(len(datainfo.y_bins))
856        node = self.create_element("SASdata", attr)
857        self.append(node, entry_node)
858
859        point = self.create_element("Idata")
860        node.append(point)
861        qx = ','.join(str(v) for v in datainfo.qx_data)
862        qy = ','.join(str(v) for v in datainfo.qy_data)
863        intensity = ','.join(str(v) for v in datainfo.data)
864
865        self.write_node(point, "Qx", qx,
866                        {'unit': datainfo._xunit})
867        self.write_node(point, "Qy", qy,
868                        {'unit': datainfo._yunit})
869        self.write_node(point, "I", intensity,
870                        {'unit': datainfo._zunit})
871        if datainfo.err_data is not None:
872            err = ','.join(str(v) for v in datainfo.err_data)
873            self.write_node(point, "Idev", err,
874                            {'unit': datainfo._zunit})
875        if datainfo.dqy_data is not None:
876            dqy = ','.join(str(v) for v in datainfo.dqy_data)
877            self.write_node(point, "Qydev", dqy,
878                            {'unit': datainfo._yunit})
879        if datainfo.dqx_data is not None:
880            dqx = ','.join(str(v) for v in datainfo.dqx_data)
881            self.write_node(point, "Qxdev", dqx,
882                            {'unit': datainfo._xunit})
883        if datainfo.mask is not None:
884            mask = ','.join("1" if v else "0" for v in datainfo.mask)
885            self.write_node(point, "Mask", mask)
886
887    def _write_trans_spectrum(self, datainfo, entry_node):
888        """
889        Writes the transmission spectrum data to the XML file
890
891        :param datainfo: The Data1D object the information is coming from
892        :param entry_node: lxml node ElementTree object to be appended to
893        """
894        for i in range(len(datainfo.trans_spectrum)):
895            spectrum = datainfo.trans_spectrum[i]
896            node = self.create_element("SAStransmission_spectrum",
897                                       {"name" : spectrum.name})
898            self.append(node, entry_node)
899            if isinstance(spectrum.timestamp, datetime.datetime):
900                node.setAttribute("timestamp", spectrum.timestamp)
901            for i in range(len(spectrum.wavelength)):
902                point = self.create_element("Tdata")
903                node.append(point)
904                self.write_node(point, "Lambda", spectrum.wavelength[i],
905                                {'unit': spectrum.wavelength_unit})
906                self.write_node(point, "T", spectrum.transmission[i],
907                                {'unit': spectrum.transmission_unit})
908                if spectrum.transmission_deviation is not None \
909                and len(spectrum.transmission_deviation) >= i:
910                    self.write_node(point, "Tdev",
911                                    spectrum.transmission_deviation[i],
912                                    {'unit':
913                                     spectrum.transmission_deviation_unit})
914
915    def _write_sample_info(self, datainfo, entry_node):
916        """
917        Writes the sample information to the XML file
918
919        :param datainfo: The Data1D object the information is coming from
920        :param entry_node: lxml node ElementTree object to be appended to
921        """
922        sample = self.create_element("SASsample")
923        if datainfo.sample.name is not None:
924            self.write_attribute(sample, "name",
925                                 str(datainfo.sample.name))
926        self.append(sample, entry_node)
927        self.write_node(sample, "ID", str(datainfo.sample.ID))
928        self.write_node(sample, "thickness", datainfo.sample.thickness,
929                        {"unit": datainfo.sample.thickness_unit})
930        self.write_node(sample, "transmission", datainfo.sample.transmission)
931        self.write_node(sample, "temperature", datainfo.sample.temperature,
932                        {"unit": datainfo.sample.temperature_unit})
933
934        pos = self.create_element("position")
935        written = self.write_node(pos,
936                                  "x",
937                                  datainfo.sample.position.x,
938                                  {"unit": datainfo.sample.position_unit})
939        written = written | self.write_node( \
940            pos, "y", datainfo.sample.position.y,
941            {"unit": datainfo.sample.position_unit})
942        written = written | self.write_node( \
943            pos, "z", datainfo.sample.position.z,
944            {"unit": datainfo.sample.position_unit})
945        if written:
946            self.append(pos, sample)
947
948        ori = self.create_element("orientation")
949        written = self.write_node(ori, "roll",
950                                  datainfo.sample.orientation.x,
951                                  {"unit": datainfo.sample.orientation_unit})
952        written = written | self.write_node( \
953            ori, "pitch", datainfo.sample.orientation.y,
954            {"unit": datainfo.sample.orientation_unit})
955        written = written | self.write_node( \
956            ori, "yaw", datainfo.sample.orientation.z,
957            {"unit": datainfo.sample.orientation_unit})
958        if written:
959            self.append(ori, sample)
960
961        for item in datainfo.sample.details:
962            self.write_node(sample, "details", item)
963
964    def _write_instrument(self, datainfo, entry_node):
965        """
966        Writes the instrumental information to the XML file
967
968        :param datainfo: The Data1D object the information is coming from
969        :param entry_node: lxml node ElementTree object to be appended to
970        """
971        instr = self.create_element("SASinstrument")
972        self.append(instr, entry_node)
973        self.write_node(instr, "name", datainfo.instrument)
974        return instr
975
976    def _write_source(self, datainfo, instr):
977        """
978        Writes the source information to the XML file
979
980        :param datainfo: The Data1D object the information is coming from
981        :param instr: instrument node  to be appended to
982        """
983        source = self.create_element("SASsource")
984        if datainfo.source.name is not None:
985            self.write_attribute(source, "name",
986                                 str(datainfo.source.name))
987        self.append(source, instr)
988        if datainfo.source.radiation is None or datainfo.source.radiation == '':
989            datainfo.source.radiation = "neutron"
990        self.write_node(source, "radiation", datainfo.source.radiation)
991
992        size = self.create_element("beam_size")
993        if datainfo.source.beam_size_name is not None:
994            self.write_attribute(size, "name",
995                                 str(datainfo.source.beam_size_name))
996        written = self.write_node( \
997            size, "x", datainfo.source.beam_size.x,
998            {"unit": datainfo.source.beam_size_unit})
999        written = written | self.write_node( \
1000            size, "y", datainfo.source.beam_size.y,
1001            {"unit": datainfo.source.beam_size_unit})
1002        written = written | self.write_node( \
1003            size, "z", datainfo.source.beam_size.z,
1004            {"unit": datainfo.source.beam_size_unit})
1005        if written:
1006            self.append(size, source)
1007
1008        self.write_node(source, "beam_shape", datainfo.source.beam_shape)
1009        self.write_node(source, "wavelength",
1010                        datainfo.source.wavelength,
1011                        {"unit": datainfo.source.wavelength_unit})
1012        self.write_node(source, "wavelength_min",
1013                        datainfo.source.wavelength_min,
1014                        {"unit": datainfo.source.wavelength_min_unit})
1015        self.write_node(source, "wavelength_max",
1016                        datainfo.source.wavelength_max,
1017                        {"unit": datainfo.source.wavelength_max_unit})
1018        self.write_node(source, "wavelength_spread",
1019                        datainfo.source.wavelength_spread,
1020                        {"unit": datainfo.source.wavelength_spread_unit})
1021
1022    def _write_collimation(self, datainfo, instr):
1023        """
1024        Writes the collimation information to the XML file
1025
1026        :param datainfo: The Data1D object the information is coming from
1027        :param instr: lxml node ElementTree object to be appended to
1028        """
1029        if datainfo.collimation == [] or datainfo.collimation is None:
1030            coll = Collimation()
1031            datainfo.collimation.append(coll)
1032        for item in datainfo.collimation:
1033            coll = self.create_element("SAScollimation")
1034            if item.name is not None:
1035                self.write_attribute(coll, "name", str(item.name))
1036            self.append(coll, instr)
1037
1038            self.write_node(coll, "length", item.length,
1039                            {"unit": item.length_unit})
1040
1041            for aperture in item.aperture:
1042                apert = self.create_element("aperture")
1043                if aperture.name is not None:
1044                    self.write_attribute(apert, "name", str(aperture.name))
1045                if aperture.type is not None:
1046                    self.write_attribute(apert, "type", str(aperture.type))
1047                self.append(apert, coll)
1048
1049                size = self.create_element("size")
1050                if aperture.size_name is not None:
1051                    self.write_attribute(size, "name",
1052                                         str(aperture.size_name))
1053                written = self.write_node(size, "x", aperture.size.x,
1054                                          {"unit": aperture.size_unit})
1055                written = written | self.write_node( \
1056                    size, "y", aperture.size.y,
1057                    {"unit": aperture.size_unit})
1058                written = written | self.write_node( \
1059                    size, "z", aperture.size.z,
1060                    {"unit": aperture.size_unit})
1061                if written:
1062                    self.append(size, apert)
1063
1064                self.write_node(apert, "distance", aperture.distance,
1065                                {"unit": aperture.distance_unit})
1066
1067    def _write_detectors(self, datainfo, instr):
1068        """
1069        Writes the detector information to the XML file
1070
1071        :param datainfo: The Data1D object the information is coming from
1072        :param inst: lxml instrument node to be appended to
1073        """
1074        if datainfo.detector is None or datainfo.detector == []:
1075            det = Detector()
1076            det.name = ""
1077            datainfo.detector.append(det)
1078
1079        for item in datainfo.detector:
1080            det = self.create_element("SASdetector")
1081            written = self.write_node(det, "name", item.name)
1082            written = written | self.write_node(det, "SDD", item.distance,
1083                                                {"unit": item.distance_unit})
1084            if written:
1085                self.append(det, instr)
1086
1087            off = self.create_element("offset")
1088            written = self.write_node(off, "x", item.offset.x,
1089                                      {"unit": item.offset_unit})
1090            written = written | self.write_node(off, "y", item.offset.y,
1091                                                {"unit": item.offset_unit})
1092            written = written | self.write_node(off, "z", item.offset.z,
1093                                                {"unit": item.offset_unit})
1094            if written:
1095                self.append(off, det)
1096
1097            ori = self.create_element("orientation")
1098            written = self.write_node(ori, "roll", item.orientation.x,
1099                                      {"unit": item.orientation_unit})
1100            written = written | self.write_node(ori, "pitch",
1101                                                item.orientation.y,
1102                                                {"unit": item.orientation_unit})
1103            written = written | self.write_node(ori, "yaw",
1104                                                item.orientation.z,
1105                                                {"unit": item.orientation_unit})
1106            if written:
1107                self.append(ori, det)
1108
1109            center = self.create_element("beam_center")
1110            written = self.write_node(center, "x", item.beam_center.x,
1111                                      {"unit": item.beam_center_unit})
1112            written = written | self.write_node(center, "y",
1113                                                item.beam_center.y,
1114                                                {"unit": item.beam_center_unit})
1115            written = written | self.write_node(center, "z",
1116                                                item.beam_center.z,
1117                                                {"unit": item.beam_center_unit})
1118            if written:
1119                self.append(center, det)
1120
1121            pix = self.create_element("pixel_size")
1122            written = self.write_node(pix, "x", item.pixel_size.x,
1123                                      {"unit": item.pixel_size_unit})
1124            written = written | self.write_node(pix, "y", item.pixel_size.y,
1125                                                {"unit": item.pixel_size_unit})
1126            written = written | self.write_node(pix, "z", item.pixel_size.z,
1127                                                {"unit": item.pixel_size_unit})
1128            if written:
1129                self.append(pix, det)
1130            self.write_node(det, "slit_length", item.slit_length,
1131                {"unit": item.slit_length_unit})
1132
1133
1134    def _write_process_notes(self, datainfo, entry_node):
1135        """
1136        Writes the process notes to the XML file
1137
1138        :param datainfo: The Data1D object the information is coming from
1139        :param entry_node: lxml node ElementTree object to be appended to
1140
1141        """
1142        for item in datainfo.process:
1143            node = self.create_element("SASprocess")
1144            self.append(node, entry_node)
1145            self.write_node(node, "name", item.name)
1146            self.write_node(node, "date", item.date)
1147            self.write_node(node, "description", item.description)
1148            for term in item.term:
1149                if isinstance(term, list):
1150                    value = term['value']
1151                    del term['value']
1152                elif isinstance(term, dict):
1153                    value = term.get("value")
1154                    del term['value']
1155                else:
1156                    value = term
1157                self.write_node(node, "term", value, term)
1158            for note in item.notes:
1159                self.write_node(node, "SASprocessnote", note)
1160            if len(item.notes) == 0:
1161                self.write_node(node, "SASprocessnote", "")
1162
1163    def _write_notes(self, datainfo, entry_node):
1164        """
1165        Writes the notes to the XML file and creates an empty note if none
1166        exist
1167
1168        :param datainfo: The Data1D object the information is coming from
1169        :param entry_node: lxml node ElementTree object to be appended to
1170
1171        """
1172        if len(datainfo.notes) == 0:
1173            node = self.create_element("SASnote")
1174            self.append(node, entry_node)
1175        else:
1176            for item in datainfo.notes:
1177                node = self.create_element("SASnote")
1178                self.write_text(node, item)
1179                self.append(node, entry_node)
1180
1181    def _check_origin(self, entry_node, doc):
1182        """
1183        Return the document, and the SASentry node associated with
1184        the data we just wrote.
1185        If the calling function was not the cansas reader, return a minidom
1186        object rather than an lxml object.
1187
1188        :param entry_node: lxml node ElementTree object to be appended to
1189        :param doc: entire xml tree
1190        """
1191        if not self.frm:
1192            self.frm = inspect.stack()[1]
1193        mod_name = self.frm[1].replace("\\", "/").replace(".pyc", "")
1194        mod_name = mod_name.replace(".py", "")
1195        mod = mod_name.split("sas/")
1196        mod_name = mod[1]
1197        if mod_name != "sascalc/dataloader/readers/cansas_reader":
1198            string = self.to_string(doc, pretty_print=False)
1199            doc = parseString(string)
1200            node_name = entry_node.tag
1201            node_list = doc.getElementsByTagName(node_name)
1202            entry_node = node_list.item(0)
1203        return doc, entry_node
1204
1205    # DO NOT REMOVE - used in saving and loading panel states.
1206    def _store_float(self, location, node, variable, storage, optional=True):
1207        """
1208        Get the content of a xpath location and store
1209        the result. Check that the units are compatible
1210        with the destination. The value is expected to
1211        be a float.
1212
1213        The xpath location might or might not exist.
1214        If it does not exist, nothing is done
1215
1216        :param location: xpath location to fetch
1217        :param node: node to read the data from
1218        :param variable: name of the data member to store it in [string]
1219        :param storage: data object that has the 'variable' data member
1220        :param optional: if True, no exception will be raised
1221            if unit conversion can't be done
1222
1223        :raise ValueError: raised when the units are not recognized
1224        """
1225        entry = get_content(location, node)
1226        try:
1227            value = float(entry.text)
1228        except ValueError:
1229            value = None
1230
1231        if value is not None:
1232            # If the entry has units, check to see that they are
1233            # compatible with what we currently have in the data object
1234            units = entry.get('unit')
1235            if units is not None:
1236                toks = variable.split('.')
1237                local_unit = getattr(storage, toks[0]+"_unit")
1238                if local_unit is not None and units.lower() != local_unit.lower():
1239                    try:
1240                        conv = Converter(units)
1241                        setattrchain(storage, variable, conv(value, units=local_unit))
1242                    except Exception:
1243                        _, exc_value, _ = sys.exc_info()
1244                        err_mess = "CanSAS reader: could not convert"
1245                        err_mess += " %s unit [%s]; expecting [%s]\n  %s" \
1246                            % (variable, units, local_unit, exc_value)
1247                        self.errors.add(err_mess)
1248                        if optional:
1249                            logger.info(err_mess)
1250                        else:
1251                            raise ValueError(err_mess)
1252                else:
1253                    setattrchain(storage, variable, value)
1254            else:
1255                setattrchain(storage, variable, value)
1256
1257    # DO NOT REMOVE - used in saving and loading panel states.
1258    def _store_content(self, location, node, variable, storage):
1259        """
1260        Get the content of a xpath location and store
1261        the result. The value is treated as a string.
1262
1263        The xpath location might or might not exist.
1264        If it does not exist, nothing is done
1265
1266        :param location: xpath location to fetch
1267        :param node: node to read the data from
1268        :param variable: name of the data member to store it in [string]
1269        :param storage: data object that has the 'variable' data member
1270
1271        :return: return a list of errors
1272        """
1273        entry = get_content(location, node)
1274        if entry is not None and entry.text is not None:
1275            setattrchain(storage, variable, entry.text.strip())
1276
1277# DO NOT REMOVE Called by outside packages:
1278#    sas.sasgui.perspectives.invariant.invariant_state
1279#    sas.sasgui.perspectives.fitting.pagestate
1280def get_content(location, node):
1281    """
1282    Get the first instance of the content of a xpath location.
1283
1284    :param location: xpath location
1285    :param node: node to start at
1286
1287    :return: Element, or None
1288    """
1289    nodes = node.xpath(location,
1290                       namespaces={'ns': CANSAS_NS.get("1.0").get("ns")})
1291    if len(nodes) > 0:
1292        return nodes[0]
1293    else:
1294        return None
1295
1296# DO NOT REMOVE Called by outside packages:
1297#    sas.sasgui.perspectives.fitting.pagestate
1298def write_node(doc, parent, name, value, attr=None):
1299    """
1300    :param doc: document DOM
1301    :param parent: parent node
1302    :param name: tag of the element
1303    :param value: value of the child text node
1304    :param attr: attribute dictionary
1305
1306    :return: True if something was appended, otherwise False
1307    """
1308    if attr is None:
1309        attr = {}
1310    if value is not None:
1311        node = doc.createElement(name)
1312        node.appendChild(doc.createTextNode(str(value)))
1313        for item in attr:
1314            node.setAttribute(item, attr[item])
1315        parent.appendChild(node)
1316        return True
1317    return False
1318
1319def getattrchain(obj, chain, default=None):
1320    """Like getattr, but the attr may contain multiple parts separated by '.'"""
1321    for part in chain.split('.'):
1322        if hasattr(obj, part):
1323            obj = getattr(obj, part, None)
1324        else:
1325            return default
1326    return obj
1327
1328def setattrchain(obj, chain, value):
1329    """Like setattr, but the attr may contain multiple parts separated by '.'"""
1330    parts = list(chain.split('.'))
1331    for part in parts[-1]:
1332        obj = getattr(obj, part, None)
1333        if obj is None:
1334            raise ValueError("missing parent object "+part)
1335    setattr(obj, value)
Note: See TracBrowser for help on using the repository browser.