File: file.py

package info (click to toggle)
turing 0.11-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,340 kB
  • sloc: python: 106,582; xml: 101; makefile: 53; sh: 29
file content (443 lines) | stat: -rw-r--r-- 15,369 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
"""
This module contains the file helper implementation

"""
import locale
import logging
import mimetypes
import os
from pyqode.core.api.manager import Manager
from pyqode.core.api.utils import TextHelper
from pyqode.qt import QtCore, QtWidgets
from pyqode.core.cache import Cache


# needed on windows
mimetypes.add_type('text/x-python', '.py')
mimetypes.add_type('text/xml', '.ui')


def _logger():
    return logging.getLogger(__name__)


class FileManager(Manager):
    """
    Helps manage file operations:
        - opening and saving files
        - providing file icon
        - detecting mimetype

    Example of usage::

        editor = CodeEdit()
        assert editor.file.path == ''
        # open a file with default locale encoding or using the cached one.
        editor.open(__file__)
        assert editor.file.path == __file__
        print(editor.file.encoding)

        # reload with another encoding
        editor.open(__file__, encoding='cp1252', use_cached_encoding=False)
        assert editor.file.path == __file__
        editor.file.encoding == 'cp1252'

    """
    class EOL:
        """
        This class enumerates the possible EOL conventions:
            - System: apply the system EOL
            - Linux: force the use of Linux EOL (\n)
            - Mac: force the use of Macintosh EOL (\r)
            - Windows: force the use of Windows EOL (\r\n)
        """
        #:
        System = 0
        #: Linux EOL: \n
        Linux = 1
        #: Macintosh EOL: \r
        Mac = 2
        #: Windows EOL: \r\n
        Windows = 3

        _map = {
            System: os.linesep,
            Linux: '\n',
            Mac: '\r',
            Windows: '\r\n'
        }

        @classmethod
        def string(cls, value):
            return cls._map[value]

    @property
    def path(self):
        """ Gets the file path """
        if self._path:
            return os.path.normpath(self._path)
        return ''

    @property
    def name(self):
        """ Gets the file base name """
        return os.path.split(self.path)[1]

    @property
    def extension(self):
        """ Gets the file path """
        return os.path.splitext(self.path)[1]

    @property
    def dirname(self):
        """ Gets the file directory name """
        return os.path.dirname(self._path)

    @property
    def encoding(self):
        """ Gets the file encoding """
        return self._encoding

    @property
    def icon(self):
        """ Gets the file icon, provided by _get_icon """
        return self._get_icon()

    @property
    def autodetect_eol(self):
        return self._autodetect_eol

    @autodetect_eol.setter
    def autodetect_eol(self, value):
        self._autodetect_eol = value
        if not self._autodetect_eol:
            self._eol = self.EOL.string(self._preferred_eol)

    @property
    def preferred_eol(self):
        return self._preferred_eol

    @preferred_eol.setter
    def preferred_eol(self, eol):
        self._preferred_eol = eol
        if not self._autodetect_eol:
            self._eol = self.EOL.string(self._preferred_eol)

    @property
    def file_size_limit(self):
        """
        Returns the file size limit. If the size of the file to open
        is superior to the limit, then we disabled syntax highlighting, code
        folding,... to improve the load time and the runtime performances.

        Default is 10MB.
        """
        return self._limit

    @file_size_limit.setter
    def file_size_limit(self, value):
        self._limit = value

    def _get_icon(self):
        return QtWidgets.QFileIconProvider().icon(QtCore.QFileInfo(self.path))

    def __init__(self, editor, replace_tabs_by_spaces=True):
        """
        :param editor: Code edit instance to work on.
        :param replace_tabs_by_spaces: True to replace tabs by spaces on
            load/save.
        """
        super(FileManager, self).__init__(editor)
        self._limit = 10000000
        self._path = ''
        #: File mimetype
        self.mimetype = ''
        #: store the last file encoding used to open or save the file.
        self._encoding = locale.getpreferredencoding()
        #: True to replace tabs by spaces
        self.replace_tabs_by_spaces = replace_tabs_by_spaces
        #: Opening flag. Set to true during the opening of a file.
        self.opening = False
        #: Saving flag. Set to while saving the editor content to a file.
        self.saving = True
        #: If True, the file is saved to a temporary file first. If the save
        #: went fine, the temporary file is renamed to the final filename.
        self.safe_save = True
        #: True to clean trailing whitespaces of changed lines. Default is
        #: True
        self.clean_trailing_whitespaces = True
        #: True to restore cursor position (if the document has already been
        # opened once).
        self.restore_cursor = True
        #: Preferred EOL convention. This setting will be used for saving the
        #: document unles autodetect_eol is True.
        self._preferred_eol = self.EOL.System
        self._eol = self.EOL.string(self._preferred_eol)
        #: If true, automatically detects file EOL and use it instead of the
        #: preferred EOL when saving files.
        self._autodetect_eol = True

    @staticmethod
    def get_mimetype(path):
        """
        Guesses the mime type of a file. If mime type cannot be detected, plain
        text is assumed.

        :param path: path of the file
        :return: the corresponding mime type.
        """
        filename = os.path.split(path)[1]
        mimetype = mimetypes.guess_type(filename)[0]
        if mimetype is None:
            mimetype = 'text/x-plain'
        _logger().debug('mimetype detected: %s', mimetype)
        return mimetype

    def open(self, path, encoding=None, use_cached_encoding=True):
        """
        Open a file and set its content on the editor widget.

        pyqode does not try to guess encoding. It's up to the client code to
        handle encodings. You can either use a charset detector to detect
        encoding or rely on a settings in your application. It is also up to
        you to handle UnicodeDecodeError, unless you've added
        class:`pyqode.core.panels.EncodingPanel` on the editor.

        pyqode automatically caches file encoding that you can later reuse it
        automatically.

        :param path: Path of the file to open.
        :param encoding: Default file encoding. Default is to use the locale
                         encoding.
        :param use_cached_encoding: True to use the cached encoding instead
            of ``encoding``. Set it to True if you want to force reload with a
            new encoding.

        :raises: UnicodeDecodeError in case of error if no EncodingPanel
            were set on the editor.
        """
        ret_val = False
        if encoding is None:
            encoding = locale.getpreferredencoding()
        self.opening = True
        settings = Cache()
        self._path = path
        # get encoding from cache
        if use_cached_encoding:
            try:
                cached_encoding = settings.get_file_encoding(
                    path, preferred_encoding=encoding)
            except KeyError:
                pass
            else:
                encoding = cached_encoding
        enable_modes = os.path.getsize(path) < self._limit
        for m in self.editor.modes:
            if m.enabled:
                m.enabled = enable_modes
        # open file and get its content
        try:
            with open(path, 'Ur', encoding=encoding) as file:
                content = file.read()
                if self.autodetect_eol:
                    self._eol = file.newlines
                    if isinstance(self._eol, tuple):
                        self._eol = self._eol[0]
                    if self._eol is None:
                        # empty file has no newlines
                        self._eol = self.EOL.string(self.preferred_eol)
                else:
                    self._eol = self.EOL.string(self.preferred_eol)
        except (UnicodeDecodeError, UnicodeError) as e:
            try:
                from pyqode.core.panels import EncodingPanel
                panel = self.editor.panels.get(EncodingPanel)
            except KeyError:
                raise e  # panel not found, not automatic error management
            else:
                panel.on_open_failed(path, encoding)
        else:
            # success! Cache the encoding
            settings.set_file_encoding(path, encoding)
            self._encoding = encoding
            # replace tabs by spaces
            if self.replace_tabs_by_spaces:
                content = content.replace("\t", " " * self.editor.tab_length)
            # set plain text
            self.editor.setPlainText(
                content, self.get_mimetype(path), self.encoding)
            self.editor.setDocumentTitle(self.editor.file.name)
            ret_val = True
            _logger().debug('file open: %s', path)
        self.opening = False
        if self.restore_cursor:
            self._restore_cached_pos()
        self._check_for_readonly()
        return ret_val

    def _check_for_readonly(self):
        self.read_only = not os.access(self.path, os.W_OK)
        self.editor.setReadOnly(self.read_only)

    def _restore_cached_pos(self):
        pos = Cache().get_cursor_position(self.path)
        max_pos = self.editor.document().characterCount()
        if pos > max_pos:
            pos = max_pos - 1
        tc = self.editor.textCursor()
        tc.setPosition(pos)
        self.editor.setTextCursor(tc)
        QtCore.QTimer.singleShot(1, self.editor.centerCursor)

    def reload(self, encoding):
        """
        Reload the file with another encoding.

        :param encoding: the new encoding to use to reload the file.
        """
        assert os.path.exists(self.path)
        self.open(self.path, encoding=encoding,
                  use_cached_encoding=False)

    @staticmethod
    def _rm(tmp_path):
        if os.path.exists(tmp_path):
            os.remove(tmp_path)

    def _reset_selection(self, sel_end, sel_start):
        text_cursor = self.editor.textCursor()
        text_cursor.setPosition(sel_start)
        text_cursor.setPosition(sel_end, text_cursor.KeepAnchor)
        self.editor.setTextCursor(text_cursor)

    def _get_selection(self):
        sel_start = self.editor.textCursor().selectionStart()
        sel_end = self.editor.textCursor().selectionEnd()
        return sel_end, sel_start

    def _get_text(self, encoding):
        lines = self.editor.toPlainText().splitlines()
        if self.clean_trailing_whitespaces:
            lines = [l.rstrip() for l in lines]
        # remove emtpy ending lines
        try:
            last_line = lines[-1]
        except IndexError:
            pass  # empty file
        else:
            while last_line == '':
                try:
                    lines.pop()
                    last_line = lines[-1]
                except IndexError:
                    last_line = None
        text = self._eol.join(lines) + self._eol
        return text.encode(encoding)

    def save(self, path=None, encoding=None, fallback_encoding=None):
        """
        Save the editor content to a file.

        :param path: optional file path. Set it to None to save using the
                     current path (save), set a new path to save as.
        :param encoding: optional encoding, will use the current
                         file encoding if None.
        :param fallback_encoding: Fallback encoding to use in case of encoding
            error. None to use the locale preferred encoding

        """
        if not self.editor.dirty and \
                (encoding is None and encoding == self.encoding) and \
                (path is None and path == self.path):
            # avoid saving if editor not dirty or if encoding or path did not
            # change
            return
        if fallback_encoding is None:
            fallback_encoding = locale.getpreferredencoding()
        _logger().log(
            5, "saving %r with %r encoding", path, encoding)
        if path is None:
            if self.path:
                path = self.path
            else:
                _logger().debug(
                    'failed to save file, path argument cannot be None if '
                    'FileManager.path is also None')
                return False
        # use cached encoding if None were specified
        if encoding is None:
            encoding = self._encoding
        self.saving = True
        self.editor.text_saving.emit(str(path))

        # get file persmission on linux
        try:
            st_mode = os.stat(path).st_mode
        except (ImportError, TypeError, AttributeError, OSError):
            st_mode = None

        # perform a safe save: we first save to a temporary file, if the save
        # succeeded we just rename the temporary file to the final file name
        # and remove it.
        if self.safe_save:
            tmp_path = path + '~'
        else:
            tmp_path = path
        try:
            with open(tmp_path, 'wb') as file:
                file.write(self._get_text(encoding))
        except UnicodeEncodeError:
            # fallback to utf-8 in case of error.
            with open(tmp_path, 'wb') as file:
                file.write(self._get_text(fallback_encoding))
        except (IOError, OSError) as e:
            self._rm(tmp_path)
            self.saving = False
            self.editor.text_saved.emit(str(path))
            raise e
        # cache update encoding
        Cache().set_file_encoding(path, encoding)
        self._encoding = encoding
        # remove path and rename temp file, if safe save is on
        if self.safe_save:
            self._rm(path)
            os.rename(tmp_path, path)
            self._rm(tmp_path)
        # reset dirty flags
        self.editor.document().setModified(False)
        # remember path for next save
        self._path = os.path.normpath(path)
        self.editor.text_saved.emit(str(path))
        self.saving = False
        _logger().debug('file saved: %s', path)
        self._check_for_readonly()

        # restore file permission
        if st_mode:
            try:
                os.chmod(path, st_mode)
            except (ImportError, TypeError, AttributeError):
                pass

    def close(self, clear=True):
        """
        Close the file open in the editor:
            - clear editor content
            - reset file attributes to their default values

        :param clear: True to clear the editor content. Default is True.
        """
        Cache().set_cursor_position(
            self.path, self.editor.textCursor().position())
        self.editor._original_text = ''
        if clear:
            self.editor.clear()
        self._path = ''
        self.mimetype = ''
        self._encoding = locale.getpreferredencoding()

    def clone_settings(self, original):
        self.replace_tabs_by_spaces = original.replace_tabs_by_spaces
        self.safe_save = original.replace_tabs_by_spaces
        self.clean_trailing_whitespaces = original.clean_trailing_whitespaces
        self.restore_cursor = original.restore_cursor