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