Package data_store
[hide private]
[frames] | no frames]

Source Code for Package data_store

  1  ############################################################################### 
  2  #                                                                             # 
  3  # Copyright (C) 2003-2015 Edward d'Auvergne                                   # 
  4  #                                                                             # 
  5  # This file is part of the program relax (http://www.nmr-relax.com).          # 
  6  #                                                                             # 
  7  # This program is free software: you can redistribute it and/or modify        # 
  8  # it under the terms of the GNU General Public License as published by        # 
  9  # the Free Software Foundation, either version 3 of the License, or           # 
 10  # (at your option) any later version.                                         # 
 11  #                                                                             # 
 12  # This program is distributed in the hope that it will be useful,             # 
 13  # but WITHOUT ANY WARRANTY; without even the implied warranty of              # 
 14  # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the               # 
 15  # GNU General Public License for more details.                                # 
 16  #                                                                             # 
 17  # You should have received a copy of the GNU General Public License           # 
 18  # along with this program.  If not, see <http://www.gnu.org/licenses/>.       # 
 19  #                                                                             # 
 20  ############################################################################### 
 21   
 22  # Module docstring. 
 23  """Package containing the relax data storage object.""" 
 24   
 25   
 26  # Python module imports. 
 27  from re import search 
 28  from sys import stderr 
 29  from time import asctime 
 30  import xml.dom.minidom 
 31   
 32  # relax module imports. 
 33  from data_store.gui import Gui 
 34  from data_store.pipe_container import PipeContainer 
 35  from data_store.seq_align import Sequence_alignments 
 36  import pipe_control 
 37  from lib.compat import builtins 
 38  from lib.errors import RelaxError, RelaxPipeError, RelaxNoPipeError 
 39  from lib.xml import fill_object_contents, xml_to_object 
 40  from status import Status; status = Status() 
 41  import version 
 42   
 43   
 44  __all__ = [ 'align_tensor', 
 45              'data_classes', 
 46              'diff_tensor', 
 47              'exp_info', 
 48              'gui', 
 49              'interatomic', 
 50              'mol_res_spin', 
 51              'pipe_container', 
 52              'prototype', 
 53              'seq_align' 
 54  ] 
 55   
 56   
57 -class Relax_data_store(dict):
58 """The relax data storage object.""" 59 60 # The current data pipe. 61 current_pipe = None 62 builtins.cdp = None 63 64 # Class variable for storing the class instance. 65 instance = None 66
67 - def __new__(self, *args, **kargs):
68 """Replacement function for implementing the singleton design pattern.""" 69 70 # First initialisation. 71 if self.instance is None: 72 # Create a new instance. 73 self.instance = dict.__new__(self, *args, **kargs) 74 75 # Add some initial structures. 76 self.instance.pipe_bundles = {} 77 self.instance.relax_gui = Gui() 78 79 # Already initialised, so return the instance. 80 return self.instance
81 82
83 - def __repr__(self):
84 """The string representation of the object. 85 86 Rather than using the standard Python conventions (either the string representation of the 87 value or the "<...desc...>" notation), a rich-formatted description of the object is given. 88 """ 89 90 # Intro text. 91 text = "The relax data storage object.\n" 92 93 # The data pipes. 94 text = text + "\n" 95 text = text + "Data pipes:\n" 96 pipes = sorted(self.instance.keys()) 97 if pipes: 98 for pipe in pipes: 99 text = text + " %s\n" % repr(pipe) 100 else: 101 text = text + " None\n" 102 103 # Data store objects. 104 text = text + "\n" 105 text = text + "Data store objects:\n" 106 names = sorted(self.__class__.__dict__.keys()) 107 for name in names: 108 # The object. 109 obj = getattr(self, name) 110 111 # The text. 112 if obj == None or isinstance(obj, str): 113 text = text + " %s %s: %s\n" % (name, type(obj), obj) 114 else: 115 text = text + " %s %s: %s\n" % (name, type(obj), obj.__doc__.split('\n')[0]) 116 117 # dict methods. 118 text = text + "\n" 119 text = text + "Inherited dictionary methods:\n" 120 for name in dir(dict): 121 # Skip special methods. 122 if search("^_", name): 123 continue 124 125 # Skip overwritten methods. 126 if name in self.__class__.__dict__: 127 continue 128 129 # The object. 130 obj = getattr(self, name) 131 132 # The text. 133 text = text + " %s %s: %s\n" % (name, type(obj), obj.__doc__.split('\n')[0]) 134 135 # All other objects. 136 text = text + "\n" 137 text = text + "All other objects:\n" 138 for name in dir(self): 139 # Skip special methods. 140 if search("^_", name): 141 continue 142 143 # Skip overwritten methods. 144 if name in self.__class__.__dict__: 145 continue 146 147 # Skip dictionary methods. 148 if name in dir(dict): 149 continue 150 151 # The object. 152 obj = getattr(self, name) 153 154 # The text. 155 text = text + " %s %s: %s\n" % (name, type(obj), obj) 156 157 # Return the text. 158 return text
159 160
161 - def __reset__(self):
162 """Delete all the data from the relax data storage object. 163 164 This method is to make the current single instance of the Data object identical to a newly 165 created instance of Data, hence resetting the relax program state. 166 """ 167 168 # Loop over the keys of self.__dict__ and delete the corresponding object. 169 keys = list(self.__dict__.keys()) 170 for key in keys: 171 # Delete the object. 172 del self.__dict__[key] 173 174 # Remove all items from the dictionary. 175 self.instance.clear() 176 177 # Reset the current data pipe. 178 builtins.cdp = None 179 180 # Recreate the pipe bundle object. 181 self.instance.pipe_bundles = {} 182 183 # Re-add the GUI object. 184 self.instance.relax_gui = Gui() 185 186 # Signal the change. 187 status.observers.reset.notify() 188 status.observers.pipe_alteration.notify()
189 190
191 - def _back_compat_hook(self, file_version=None, pipes=None):
192 """Method for converting the old data structures to the new ones. 193 194 @keyword file_version: The relax XML version of the XML file. 195 @type file_version: int 196 @keyword pipes: The list of new pipe names to update. 197 @type pipes: list of str 198 """ 199 200 # Loop over the new data pipes. 201 for pipe_name in pipes: 202 # The data pipe object. 203 dp = self[pipe_name] 204 205 # Convert the molecule-residue-spin data. 206 for mol in dp.mol: 207 # Loop over the residues. 208 for res in mol.res: 209 # Loop over the spins. 210 for spin in res.spin: 211 # The list of objects to remove at the end. 212 eliminate = [] 213 214 # The current spin ID. 215 spin_id = pipe_control.mol_res_spin.generate_spin_id_unique(pipe_cont=dp, mol=mol, res=res, spin=spin) 216 217 # Rename the old peak intensity data structures. 218 if hasattr(spin, 'intensities'): 219 spin.peak_intensity = spin.intensities 220 eliminate.append('intensities') 221 if hasattr(spin, 'intensity_err'): 222 spin.peak_intensity_err = spin.intensity_err 223 eliminate.append('intensity_err') 224 if hasattr(spin, 'intensity_sim'): 225 spin.peak_intensity_sim = spin.intensity_sim 226 eliminate.append('intensity_sim') 227 if hasattr(spin, 'sim_intensity'): 228 spin.peak_intensity_sim = spin.sim_intensity 229 eliminate.append('sim_intensity') 230 if hasattr(spin, 'intensity_bc'): 231 spin.peak_intensity_bc = spin.intensity_bc 232 eliminate.append('intensity_bc') 233 234 # Convert proton spins (the 'heteronuc_type' variable indicates a pre-interatomic container design state). 235 if hasattr(spin, 'heteronuc_type') and hasattr(spin, 'element') and spin.element == 'H': 236 # Rename the nuclear isotope. 237 spin.isotope = spin.proton_type 238 239 # Append the old structures to be eliminated. 240 eliminate.append('proton_type') 241 242 # Convert heteronuclear spins (the 'heteronuc_type' variable indicates a pre-interatomic container design state). 243 elif hasattr(spin, 'heteronuc_type'): 244 # Rename the nuclear isotope. 245 spin.isotope = spin.heteronuc_type 246 247 # Name the spin if needed. 248 if spin.name == None: 249 if search('N', spin.isotope): 250 pipe_control.mol_res_spin.name_spin(spin_id=spin_id, name='N', pipe=pipe_name) 251 elif search('C', spin.isotope): 252 pipe_control.mol_res_spin.name_spin(spin_id=spin_id, name='C', pipe=pipe_name) 253 254 # An attached proton - convert into a spin container. 255 if (hasattr(spin, 'attached_proton') and spin.attached_proton != None) or (hasattr(spin, 'proton_type') and spin.proton_type != None): 256 # The proton name. 257 if hasattr(spin, 'attached_proton') and spin.attached_proton != None: 258 proton_name = spin.attached_proton 259 else: 260 proton_name = 'H' 261 262 # The two spin IDs (newly regenerated due to the above renaming). 263 spin_id1 = pipe_control.mol_res_spin.generate_spin_id_unique(pipe_cont=dp, mol=mol, res=res, spin=spin) 264 spin_id2 = pipe_control.mol_res_spin.generate_spin_id_unique(pipe_cont=dp, mol=mol, res=res, spin_name=proton_name) 265 266 # Fetch the proton spin if it exists. 267 h_spin = pipe_control.mol_res_spin.return_spin(spin_id2, pipe=pipe_name) 268 if h_spin: 269 spin_id2 = pipe_control.mol_res_spin.generate_spin_id_unique(pipe_cont=dp, mol=mol, res=res, spin_name=proton_name, spin_num=h_spin.num) 270 271 # Create a new spin container for the proton if needed. 272 if not h_spin: 273 h_spin = pipe_control.mol_res_spin.create_spin(mol_name=mol.name, res_num=res.num, res_name=res.name, spin_name=proton_name, pipe=pipe_name) 274 h_spin.select = False 275 276 # Set up a dipole interaction between the two spins if needed. 277 if not hasattr(h_spin, 'element'): 278 pipe_control.mol_res_spin.set_spin_element(spin_id=spin_id2, element='H', pipe=pipe_name) 279 if not hasattr(h_spin, 'isotope'): 280 pipe_control.mol_res_spin.set_spin_isotope(spin_id=spin_id2, isotope='1H', pipe=pipe_name) 281 pipe_control.interatomic.define(spin_id1, spin_id2, verbose=False, pipe=pipe_name) 282 283 # Get the interatomic data container. 284 interatom = pipe_control.interatomic.return_interatom(spin_id1=spin_id1, spin_id2=spin_id2, pipe=pipe_name) 285 286 # Set the interatomic distance. 287 if hasattr(spin, 'r'): 288 interatom.r = spin.r 289 290 # Set the interatomic unit vectors. 291 if hasattr(spin, 'xh_vect'): 292 interatom.vector = spin.xh_vect 293 294 # Set the RDC values. 295 if hasattr(spin, 'rdc'): 296 interatom.rdc = spin.rdc 297 if hasattr(spin, 'rdc_err'): 298 interatom.rdc_err = spin.rdc_err 299 if hasattr(spin, 'rdc_sim'): 300 interatom.rdc_sim = spin.rdc_sim 301 if hasattr(spin, 'rdc_bc'): 302 interatom.rdc_bc = spin.rdc_bc 303 304 # Append the old structures to be eliminated. 305 eliminate += ['heteronuc_type', 'proton_type', 'attached_proton', 'r', 'r_err', 'r_sim', 'rdc', 'rdc_err', 'rdc_sim', 'rdc_bc', 'xh_vect'] 306 307 # Delete the old structures. 308 for name in eliminate: 309 if hasattr(spin, name): 310 delattr(spin, name) 311 312 # Conversions for the interatomic data containers. 313 if hasattr(dp, 'interatomic'): 314 for interatom in dp.interatomic: 315 # RDC data. 316 if hasattr(interatom, 'rdc') and not hasattr(interatom, 'rdc_data_types'): 317 # Initialise. 318 interatom.rdc_data_types = {} 319 320 # Add the data type, assumed to be 'D', for each alignment ID. 321 for id in dp.rdc_ids: 322 interatom.rdc_data_types[id] = 'D' 323 324 # Convert the alignment tensors. 325 if hasattr(dp, 'align_tensors'): 326 for i in range(len(dp.align_tensors)): 327 # Fix for the addition of the alignment ID structure as opposed to the tensor name or tag. 328 if not hasattr(dp.align_tensors[i], 'align_id'): 329 dp.align_tensors[i].set('align_id', dp.align_tensors[i].name) 330 331 # Convert spectrometer frequency information. 332 if hasattr(dp, 'frq'): 333 # Convert to the new structure. 334 dp.spectrometer_frq = dp.frq 335 del dp.frq 336 337 # Build the new frequency list structure. 338 dp.spectrometer_frq_list = [] 339 for frq in list(dp.spectrometer_frq.values()): 340 if frq not in dp.spectrometer_frq_list: 341 dp.spectrometer_frq_list.append(frq) 342 343 # And finally count the elements and sort the list. 344 dp.spectrometer_frq_count = len(dp.spectrometer_frq_list) 345 dp.spectrometer_frq_list.sort() 346 347 # PCS Q factor conversions. 348 if hasattr(dp, 'q_factors_pcs'): 349 dp.q_factors_pcs_norm_squared_sum = dp.q_factors_pcs 350 del dp.q_factors_pcs 351 if hasattr(dp, 'q_pcs'): 352 dp.q_pcs_norm_squared_sum = dp.q_pcs 353 del dp.q_pcs 354 355 # RDC Q factor conversions. 356 if hasattr(dp, 'q_factors_rdc'): 357 dp.q_factors_rdc_norm_tensor_size = dp.q_factors_rdc 358 del dp.q_factors_rdc 359 if hasattr(dp, 'q_rdc'): 360 dp.q_rdc_norm_tensor_size = dp.q_rdc 361 del dp.q_rdc 362 if hasattr(dp, 'q_factors_rdc_norm2'): 363 dp.q_factors_rdc_norm_squared_sum = dp.q_factors_rdc_norm2 364 del dp.q_factors_rdc_norm2 365 if hasattr(dp, 'q_rdc_norm2'): 366 dp.q_rdc_norm_squared_sum = dp.q_rdc_norm2 367 del dp.q_rdc_norm2
368 369
370 - def add(self, pipe_name, pipe_type, bundle=None, switch=True):
371 """Method for adding a new data pipe container to the dictionary. 372 373 This method should be used rather than importing the PipeContainer class and using the statement 'D[pipe] = PipeContainer()', where D is the relax data storage object and pipe is the name of the data pipe. 374 375 @param pipe_name: The name of the new data pipe. 376 @type pipe_name: str 377 @param pipe_type: The data pipe type. 378 @type pipe_type: str 379 @keyword bundle: The optional data pipe bundle to associate the data pipe with. 380 @type bundle: str or None 381 @keyword switch: A flag which if True will cause the new data pipe to be set to the current data pipe. 382 @type switch: bool 383 """ 384 385 # Test if the pipe already exists. 386 if pipe_name in self.instance: 387 raise RelaxPipeError(pipe_name) 388 389 # Create a new container. 390 self[pipe_name] = PipeContainer() 391 392 # Add the data pipe type string to the container. 393 self[pipe_name].pipe_type = pipe_type 394 395 # The pipe bundle. 396 if bundle: 397 # A new bundle. 398 if bundle not in self.pipe_bundles: 399 self.pipe_bundles[bundle] = [] 400 401 # Add the pipe to the bundle. 402 self.pipe_bundles[bundle].append(pipe_name) 403 404 # Change the current data pipe. 405 if switch: 406 # Set the current data pipe. 407 self.instance.current_pipe = pipe_name 408 builtins.cdp = self[pipe_name] 409 410 # Signal the switch. 411 status.observers.pipe_alteration.notify()
412 413
414 - def is_empty(self, verbosity=False):
415 """Method for testing if the relax data store is empty. 416 417 @keyword verbosity: A flag which if True will cause messages to be printed to STDERR. 418 @type verbosity: bool 419 @return: True if the data store is empty, False otherwise. 420 @rtype: bool 421 """ 422 423 # No pipes should exist. 424 if len(self): 425 if verbosity: 426 stderr.write("The relax data store contains the data pipes %s.\n" % sorted(self.keys())) 427 return False 428 429 # Objects which should be in here. 430 blacklist = [ 431 'pipe_bundles', 432 'relax_gui' 433 ] 434 435 # An object has been added to the data store. 436 for name in dir(self): 437 # Skip the data store methods. 438 if name in self.__class__.__dict__: 439 continue 440 441 # Skip the dict methods. 442 if name in dict.__dict__: 443 continue 444 445 # Skip special objects. 446 if search("^__", name): 447 continue 448 449 # Blacklisted objects to skip. 450 if name in blacklist: 451 continue 452 453 # An object has been added. 454 if verbosity: 455 stderr.write("The relax data store contains the object %s.\n" % name) 456 return False 457 458 # The data store is empty. 459 return True
460 461
462 - def from_xml(self, file, dir=None, pipe_to=None, verbosity=1):
463 """Parse a XML document representation of a data pipe, and load it into the relax data store. 464 465 @param file: The open file object. 466 @type file: file 467 @keyword dir: The name of the directory containing the results file (needed for loading external files). 468 @type dir: str 469 @keyword pipe_to: The data pipe to load the XML data pipe into (the file must only contain one data pipe). 470 @type pipe_to: str 471 @keyword verbosity: A flag specifying the amount of information to print. The higher the value, the greater the verbosity. 472 @type verbosity: int 473 @raises RelaxError: If pipe_to is given and the file contains multiple pipe elements; or if the data pipes in the XML file already exist in the relax data store; or if the data pipe type is invalid; or if the target data pipe is not empty. 474 @raises RelaxNoPipeError: If pipe_to is given but the data pipe does not exist. 475 @raises RelaxError: If the data pipes in the XML file already exist in the relax data store, or if the data pipe type is invalid. 476 @raises RelaxPipeError: If the data pipes of the XML file are already present in the relax data store. 477 """ 478 479 # Create the XML document from the file. 480 doc = xml.dom.minidom.parse(file) 481 482 # Get the relax node. 483 relax_node = doc.childNodes[0] 484 485 # Get the relax version of the XML file. 486 file_version = relax_node.getAttribute('file_version') 487 if file_version == '': 488 file_version = 1 489 else: 490 file_version = int(file_version) 491 492 # Get the pipe nodes. 493 pipe_nodes = relax_node.getElementsByTagName('pipe') 494 495 # Structure for the names of the new pipes. 496 pipes = [] 497 498 # Target loading to a specific pipe (for pipe results reading). 499 if pipe_to: 500 # Check if there are multiple pipes in the XML file. 501 if len(pipe_nodes) > 1: 502 raise RelaxError("The pipe_to target pipe argument '%s' cannot be given as the file contains multiple pipe elements." % pipe_to) 503 504 # The pipe type. 505 pipe_type = pipe_nodes[0].getAttribute('type') 506 507 # Check that the pipe already exists. 508 if not pipe_to in self: 509 raise RelaxNoPipeError(pipe_to) 510 511 # Check if the pipe type matches. 512 if pipe_type != self[pipe_to].pipe_type: 513 raise RelaxError("The XML file pipe type '%s' does not match the pipe type '%s'" % (pipe_type, self[pipe_to].pipe_type)) 514 515 # Check if the pipe is empty. 516 if not self[pipe_to].is_empty(): 517 raise RelaxError("The data pipe '%s' is not empty." % pipe_to) 518 519 # Load the data. 520 self[pipe_to].from_xml(pipe_nodes[0], dir=dir, file_version=file_version) 521 522 # Store the pipe name. 523 pipes.append(pipe_to) 524 525 # Load the state. 526 else: 527 # Get the GUI nodes. 528 gui_nodes = relax_node.getElementsByTagName('relax_gui') 529 if gui_nodes: 530 self.relax_gui.from_xml(gui_nodes[0], file_version=file_version) 531 532 # Get the sequence alignment nodes. 533 seq_align_nodes = relax_node.getElementsByTagName('sequence_alignments') 534 if seq_align_nodes: 535 # Initialise the object. 536 self.sequence_alignments = Sequence_alignments() 537 538 # Populate it. 539 self.sequence_alignments.from_xml(seq_align_nodes[0], file_version=file_version) 540 541 # Recreate all the data store data structures. 542 xml_to_object(relax_node, self, file_version=file_version, blacklist=['pipe', 'relax_gui', 'sequence_alignments']) 543 544 # Checks. 545 for pipe_node in pipe_nodes: 546 # The pipe name and type. 547 pipe_name = str(pipe_node.getAttribute('name')) 548 pipe_type = pipe_node.getAttribute('type') 549 550 # Existence check. 551 if pipe_name in self: 552 raise RelaxPipeError(pipe_name) 553 554 # Valid type check. 555 if not pipe_type in pipe_control.pipes.VALID_TYPES: 556 raise RelaxError("The data pipe type '%s' is invalid and must be one of the strings in the list %s." % (pipe_type, pipe_control.pipes.VALID_TYPES)) 557 558 # Load the data pipes. 559 for pipe_node in pipe_nodes: 560 # The pipe name and type. 561 pipe_name = str(pipe_node.getAttribute('name')) 562 pipe_type = pipe_node.getAttribute('type') 563 564 # Add the data pipe. 565 switch = False 566 if self.current_pipe == None: 567 switch = True 568 self.add(pipe_name, pipe_type, switch=switch) 569 570 # Fill the pipe. 571 self[pipe_name].from_xml(pipe_node, file_version=file_version, dir=dir) 572 573 # Store the pipe name. 574 pipes.append(pipe_name) 575 576 # Set the current pipe. 577 if self.current_pipe in self: 578 builtins.cdp = self[self.current_pipe] 579 580 # Finally update the molecule, residue, and spin metadata for each data pipe. 581 for pipe in pipes: 582 pipe_control.mol_res_spin.metadata_update(pipe=pipe) 583 584 # Backwards compatibility transformations. 585 self._back_compat_hook(file_version, pipes=pipes)
586 587
588 - def to_xml(self, file, pipes=None):
589 """Create a XML document representation of the current data pipe. 590 591 This method creates the top level XML document including all the information needed 592 about relax, calls the PipeContainer.xml_write() method to fill in the document contents, 593 and writes the XML into the file object. 594 595 @param file: The open file object. 596 @type file: file 597 @param pipes: The name of the pipe, or list of pipes to place in the XML file. 598 @type pipes: str or list of str 599 """ 600 601 # The pipes to include in the XML file. 602 all = False 603 if not pipes: 604 all = True 605 pipes = list(self.keys()) 606 elif isinstance(pipes, str): 607 pipes = [pipes] 608 609 # Sort the pipes. 610 pipes.sort() 611 612 # Create the XML document object. 613 xmldoc = xml.dom.minidom.Document() 614 615 # Create the top level element, including the relax URL. 616 top_element = xmldoc.createElementNS('http://www.nmr-relax.com', 'relax') 617 top_element.setAttribute("xmlns", "http://www.nmr-relax.com") 618 619 # Append the element. 620 xmldoc.appendChild(top_element) 621 622 # Set the relax version number, and add a creation time. 623 top_element.setAttribute('version', version.version) 624 top_element.setAttribute('time', asctime()) 625 top_element.setAttribute('file_version', "2") 626 if version.repo_revision: 627 top_element.setAttribute('revision', version.repo_revision) 628 if version.repo_url: 629 top_element.setAttribute('url', version.repo_url) 630 631 # Add all objects in the data store base object to the XML element. 632 if all: 633 blacklist = list(self.__class__.__dict__.keys()) + list(dict.__dict__.keys()) 634 for name in dir(self): 635 # Skip blacklisted objects. 636 if name in blacklist: 637 continue 638 639 # Skip special objects. 640 if search('^_', name): 641 continue 642 643 # Execute any to_xml() methods, and add that object to the blacklist. 644 obj = getattr(self, name) 645 if hasattr(obj, 'to_xml'): 646 obj.to_xml(xmldoc, top_element) 647 blacklist = blacklist + [name] 648 649 # Remove the current data pipe from the blacklist! 650 blacklist.remove('current_pipe') 651 652 # Add all simple python objects within the store. 653 fill_object_contents(xmldoc, top_element, object=self, blacklist=blacklist) 654 655 # Loop over the pipes. 656 for pipe in pipes: 657 # Create the pipe XML element and add it to the top level XML element. 658 pipe_element = xmldoc.createElement('pipe') 659 top_element.appendChild(pipe_element) 660 661 # Set the data pipe attributes. 662 pipe_element.setAttribute('desc', 'The contents of a relax data pipe') 663 pipe_element.setAttribute('name', pipe) 664 pipe_element.setAttribute('type', self[pipe].pipe_type) 665 666 # Fill the data pipe XML element. 667 self[pipe].to_xml(xmldoc, pipe_element, pipe_type=self[pipe].pipe_type) 668 669 # Write out the XML file. 670 file.write(xmldoc.toprettyxml(indent=' '))
671