1# Copyright (C) 2011 Jeremy S. Sanders 2# Email: Jeremy Sanders <jeremy@jeremysanders.net> 3# 4# This program is free software; you can redistribute it and/or modify 5# it under the terms of the GNU General Public License as published by 6# the Free Software Foundation; either version 2 of the License, or 7# (at your option) any later version. 8# 9# This program is distributed in the hope that it will be useful, 10# but WITHOUT ANY WARRANTY; without even the implied warranty of 11# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12# GNU General Public License for more details. 13# 14# You should have received a copy of the GNU General Public License along 15# with this program; if not, write to the Free Software Foundation, Inc., 16# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 17############################################################################## 18 19"""Parameters for import routines.""" 20 21from __future__ import division, print_function 22import sys 23import copy 24 25from ..compat import citems, cstr 26from .. import utils 27 28class ImportingError(RuntimeError): 29 """Common error when import fails.""" 30 31class ImportParamsBase(object): 32 """Import parameters for the various imports. 33 34 Parameters: 35 filename: filename to import from 36 linked: whether to link to file 37 encoding: encoding for file 38 prefix: prefix for output dataset names 39 suffix: suffix for output dataset names 40 tags: list of tags to apply to output datasets 41 renames: dict map of names to renamed datasets 42 """ 43 44 defaults = { 45 'filename': None, 46 'linked': False, 47 'encoding': 'utf_8', 48 'prefix': '', 49 'suffix': '', 50 'tags': None, 51 'renames': None, 52 } 53 54 def __init__(self, **argsv): 55 """Initialise the reader to import data from filename. 56 """ 57 58 # set defaults 59 for k, v in citems(self.defaults): 60 setattr(self, k, v) 61 62 # set parameters 63 for k, v in citems(argsv): 64 if k not in self.defaults: 65 raise ValueError("Invalid parameter %s" % k) 66 setattr(self, k, v) 67 68 # extra parameters to copy besides defaults 69 self._extras = [] 70 71 def copy(self): 72 """Make a copy of the parameters object.""" 73 74 newp = {} 75 for k in list(self.defaults.keys()) + self._extras: 76 newp[k] = getattr(self, k) 77 return self.__class__(**newp) 78 79class LinkedFileBase(object): 80 """A base class for linked files containing common routines.""" 81 82 def __init__(self, params): 83 """Save parameters.""" 84 self.params = params 85 86 def createOperation(self): 87 """Return operation to recreate self.""" 88 return None 89 90 @property 91 def filename(self): 92 """Get filename.""" 93 return self.params.filename 94 95 def _saveHelper(self, fileobj, cmd, fixedparams, 96 renameparams={}, relpath=None, extraargs={}): 97 """Helper to write command to reload data. 98 99 fileobj: file object to write to 100 cmd: name of command to write 101 fixedparams: list of parameters to list at start of command lines 102 renameparams: optional map of params to command line params 103 relpath: relative path for writing filename 104 extraargs: other options to add to command line 105 """ 106 107 p = self.params 108 args = [] 109 110 # arguments without names at command start 111 for par in fixedparams: 112 if par == 'filename': 113 v = self._getSaveFilename(relpath) 114 else: 115 v = getattr(p, par) 116 args.append(utils.rrepr(v)) 117 118 # parameters key, values to put in command line 119 plist = sorted( [(par, getattr(p, par)) for par in p.defaults] + 120 list(citems(extraargs)) ) 121 122 for par, val in plist: 123 if ( val and 124 (par not in p.defaults or p.defaults[par] != val) and 125 par not in fixedparams and 126 par != 'tags' ): 127 128 if par in renameparams: 129 par = renameparams[par] 130 args.append('%s=%s' % (par, utils.rrepr(val))) 131 132 # write command using comma-separated list 133 fileobj.write('%s(%s)\n' % (cmd, ', '.join(args))) 134 135 def saveToFile(self, fileobj, relpath=None): 136 """Save the link to the document file.""" 137 pass 138 139 def _getSaveFilename(self, relpath): 140 """Get filename to write to save file. 141 If relpath is a string, write relative to path given 142 """ 143 if relpath: 144 f = utils.relpath(self.params.filename, relpath) 145 else: 146 f = self.filename 147 # Here we convert backslashes in Windows to forward slashes 148 # This is compatible, but also works on Unix/Mac 149 if sys.platform == 'win32': 150 f = f.replace('\\', '/') 151 return f 152 153 def _deleteLinkedDatasets(self, document): 154 """Delete linked datasets from document linking to self. 155 Returns tags for deleted datasets. 156 """ 157 158 tags = {} 159 for name, ds in list(document.data.items()): 160 if ds.linked == self: 161 tags[name] = document.data[name].tags 162 document.deleteData(name) 163 return tags 164 165 def _moveReadDatasets(self, tempdoc, document, tags): 166 """Move datasets from tempdoc to document if they do not exist 167 in the destination. 168 169 tags is a dict of tags for each dataset 170 """ 171 172 read = [] 173 for name, ds in list(tempdoc.data.items()): 174 if name not in document.data: 175 ds.linked = self 176 if name in tags: 177 ds.tags = tags[name] 178 document.setData(name, ds) 179 read.append(name) 180 return read 181 182 def reloadLinks(self, document): 183 """Reload links using an operation""" 184 185 # get the operation for reloading 186 op = self.createOperation()(self.params) 187 188 # load data into a temporary document 189 tempdoc = document.__class__() 190 191 try: 192 tempdoc.applyOperation(op) 193 except Exception as ex: 194 # if something breaks, record an error and return nothing 195 document.log(cstr(ex)) 196 197 # find datasets which are linked using this link object 198 # return errors for them 199 errors = dict([(name, 1) for name, ds in citems(document.data) 200 if ds.linked is self]) 201 return ([], errors) 202 203 # delete datasets which are linked and imported here 204 tags = self._deleteLinkedDatasets(document) 205 # move datasets into document 206 read = self._moveReadDatasets(tempdoc, document, tags) 207 208 # return errors (if any) 209 errors = op.outinvalids 210 211 return (read, errors) 212 213class OperationDataImportBase(object): 214 """Default useful import class.""" 215 216 def __init__(self, params): 217 self.params = params 218 219 def doImport(self, document): 220 """Do import, override this. 221 Set outdatasets 222 """ 223 224 def addCustoms(self, document, customs): 225 """Optionally, add the customs return by plugins to document.""" 226 227 type_attrs = { 228 'import': 'def_imports', 229 'color': 'def_colors', 230 'colormap': 'def_colormaps', 231 'constant': 'def_definitions', 232 'function': 'def_definitions', 233 'definition': 'def_definitions', 234 } 235 236 if len(customs) > 0: 237 doceval = document.evaluate 238 self.oldcustoms = [ 239 copy.deepcopy(doceval.def_imports), 240 copy.deepcopy(doceval.def_definitions), 241 copy.deepcopy(doceval.def_colors), 242 copy.deepcopy(doceval.def_colormaps)] 243 244 # FIXME: inefficient for large number of definitions 245 for item in customs: 246 ctype, name, val = item 247 clist = getattr(doceval, type_attrs[ctype]) 248 for idx, (cname, cval) in enumerate(clist): 249 if cname == name: 250 clist[idx][1] = val 251 break 252 else: 253 clist.append([name, val]) 254 255 doceval.update() 256 257 def do(self, document): 258 """Do import.""" 259 260 # list of returned dataset names 261 self.outnames = [] 262 # map of names to datasets 263 self.outdatasets = {} 264 # list of returned custom variables 265 self.outcustoms = [] 266 # invalid conversions 267 self.outinvalids = {} 268 269 # remember datasets in document for undo 270 self.oldcustoms = None 271 272 # do actual import 273 retn = self.doImport() 274 275 # these are custom values returned from the plugin 276 if self.outcustoms: 277 self.addCustoms(document, self.outcustoms) 278 279 # handle tagging/renaming 280 for name, ds in list(citems(self.outdatasets)): 281 if self.params.tags: 282 ds.tags.update(self.params.tags) 283 if self.params.renames and name in self.params.renames: 284 del self.outdatasets[name] 285 self.outdatasets[self.params.renames[name]] = ds 286 287 # only remember the parts we need 288 self.olddatasets = [ (n, document.data.get(n)) 289 for n in self.outdatasets ] 290 291 self.olddatasets = [] 292 for name, ds in citems(self.outdatasets): 293 self.olddatasets.append( (name, document.data.get(name)) ) 294 document.setData(name, ds) 295 296 self.outnames = sorted(self.outdatasets) 297 298 return retn 299 300 def undo(self, document): 301 """Undo import.""" 302 303 # put back old datasets 304 for name, ds in self.olddatasets: 305 if ds is None: 306 document.deleteData(name) 307 else: 308 document.setData(name, ds) 309 310 # for custom definitions 311 if self.oldcustoms is not None: 312 doceval = document.evaluate 313 doceval.def_imports = self.oldcustoms[0] 314 doceval.def_definitions = self.oldcustoms[1] 315 doceval.def_colors = self.oldcustoms[2] 316 doceval.def_colormaps = self.oldcustoms[3] 317 doceval.update() 318