Completed
Push — master ( 2ffa8b...574cdc )
by Bjorn
01:01
created

Path.append()   A

Complexity

Conditions 2

Size

Total Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
c 1
b 0
f 0
dl 0
loc 3
rs 10
1
# -*- coding: utf-8 -*-
2
"""Poor man's pathlib.
3
4
   (Path instances are subclasses of str, so interoperability with existing
5
   os.path code is greater than with Python 3's pathlib.)
6
"""
7
# pylint:disable=C0111,R0904
8
# R0904: too many public methods in Path
9
import os
10
import re
11
from contextlib import contextmanager
12
import shutil
13
14
15
def doc(srcfn):
16
    def decorator(fn):
17
        fn.__doc__ = srcfn.__doc__.replace(srcfn.__name__, fn.__name__)
18
        return fn
19
    return decorator
20
21
22
class Path(str):
23
    """Poor man's pathlib.
24
    """
25
26
    def __div__(self, other):
27
        return Path(
28
            os.path.normcase(
29
                os.path.normpath(
30
                    os.path.join(self, other)
31
                )
32
            )
33
        )
34
35
    @doc(os.unlink)
36
    def unlink(self):
37
        os.unlink(self)
38
39
    def open(self, mode='r'):
40
        return open(self, mode)
41
42
    def read(self, mode='r'):
43
        with self.open(mode) as fp:
44
            return fp.read()
45
46
    def write(self, txt, mode='w'):
47
        with self.open(mode) as fp:
48
            fp.write(txt)
49
50
    def append(self, txt, mode='a'):
51
        with self.open(mode) as fp:
52
            fp.write(txt)
53
54
    def __iter__(self):
55
        for root, dirs, files in os.walk(self):
56
            dotdirs = [d for d in dirs if d.startswith('.')]
57
            for d in dotdirs:
58
                dirs.remove(d)
59
            dotfiles = [d for d in files if d.startswith('.')]
60
            for d in dotfiles:
61
                files.remove(d)
62
            for fname in files:
63
                yield Path(os.path.join(root, fname))
64
65
    def __contains__(self, item):
66
        if self.isdir():
67
            return item in self.listdir()
68
        super(Path, self).__contains__(item)
69
70
    @doc(shutil.rmtree)
71
    def rmtree(self, subdir=None):
72
        if subdir is not None:
73
            shutil.rmtree(self / subdir, ignore_errors=True)
74
        else:
75
            shutil.rmtree(self, ignore_errors=True)
76
77
    def contents(self):
78
        res = [d.relpath(self) for d in self.glob('**/*')]
79
        res.sort()
80
        return res
81
82
    @classmethod
83
    def curdir(cls):
84
        """Initialize a Path object on the current directory.
85
        """
86
        return cls(os.getcwd())
87
88
    def touch(self, mode=0o666, exist_ok=True):
89
        """Create this file with the given access mode, if it doesn't exist.
90
           (based on https://github.com/python/cpython/blob/master/Lib/pathlib.py)
91
        """
92
        if exist_ok:
93
            # First try to bump modification time
94
            # Implementation note: GNU touch uses the UTIME_NOW option of
95
            # the utimensat() / futimens() functions.
96
            try:
97
                os.utime(self, None)
98
            except OSError:
99
                # Avoid exception chaining
100
                pass
101
            else:
102
                return
103
        flags = os.O_CREAT | os.O_WRONLY
104
        if not exist_ok:
105
            flags |= os.O_EXCL
106
        fd = os.open(self, flags, mode)
107
        os.close(fd)
108
109
    def glob(self, pat):
110
        """`pat` can be an extended glob pattern, e.g. `'**/*.less'`
111
           This code handles negations similarly to node.js' minimatch, i.e.
112
           a leading `!` will negate the entire pattern.
113
        """
114
        r = ""
115
        negate = int(pat.startswith('!'))
116
        i = negate
117
118
        while i < len(pat):
119
            if pat[i:i + 3] == '**/':
120
                r += "(?:.*/)?"
121
                i += 3
122
            elif pat[i] == "*":
123
                r += "[^/]*"
124
                i += 1
125
            elif pat[i] == ".":
126
                r += "[.]"
127
                i += 1
128
            elif pat[i] == "?":
129
                r += "."
130
                i += 1
131
            else:
132
                r += pat[i]
133
                i += 1
134
        r += r'\Z(?ms)'
135
        # print '\n\npat', pat
136
        # print 'regex:', r
137
        # print [s.relpath(self).replace('\\', '/') for s in self]
138
        rx = re.compile(r)
139
140
        def match(d):
141
            m = rx.match(d)
142
            return not m if negate else m
143
144
        return [s for s in self if match(s.relpath(self).replace('\\', '/'))]
145
146
    @doc(os.path.abspath)
147
    def abspath(self):
148
        return Path(os.path.abspath(self))
149
    absolute = abspath  # pathlib
150
151
    def drive(self):
152
        """Return the drive of `self`.
153
        """
154
        return self.splitdrive()[0]
155
156
    def drivepath(self):
157
        """The path local to this drive (i.e. remove drive letter).
158
        """
159
        return self.splitdrive()[1]
160
161
    @doc(os.path.basename)
162
    def basename(self):
163
        return Path(os.path.basename(self))
164
165
    @doc(os.path.commonprefix)
166
    def commonprefix(self, *args):
167
        return os.path.commonprefix([str(self)] + [str(a) for a in args])
168
169
    @doc(os.path.dirname)
170
    def dirname(self):
171
        return Path(os.path.dirname(self))
172
173
    @doc(os.path.exists)
174
    def exists(self):
175
        return os.path.exists(self)
176
177
    @doc(os.path.expanduser)
178
    def expanduser(self):
179
        return Path(os.path.expanduser(self))
180
181
    @doc(os.path.expandvars)
182
    def expandvars(self):
183
        return Path(os.path.expandvars(self))
184
185
    @doc(os.path.getatime)
186
    def getatime(self):
187
        return os.path.getatime(self)
188
189
    @doc(os.path.getctime)
190
    def getctime(self):
191
        return os.path.getctime(self)
192
193
    @doc(os.path.getmtime)
194
    def getmtime(self):
195
        return os.path.getmtime(self)
196
197
    @doc(os.path.getsize)
198
    def getsize(self):
199
        return os.path.getsize(self)
200
201
    @doc(os.path.isabs)
202
    def isabs(self):
203
        return os.path.isabs(self)
204
205
    @doc(os.path.isdir)
206
    def isdir(self, *args, **kw):
207
        return os.path.isdir(self, *args, **kw)
208
209
    @doc(os.path.isfile)
210
    def isfile(self):
211
        return os.path.isfile(self)
212
213
    @doc(os.path.islink)
214
    def islink(self):
215
        return os.path.islink(self)
216
217
    @doc(os.path.ismount)
218
    def ismount(self):
219
        return os.path.ismount(self)
220
221
    @doc(os.path.join)
222
    def join(self, *args):
223
        return Path(os.path.join(self, *args))
224
225
    @doc(os.path.lexists)
226
    def lexists(self):
227
        return os.path.lexists(self)
228
229
    @doc(os.path.normcase)
230
    def normcase(self):
231
        return Path(os.path.normcase(self))
232
233
    @doc(os.path.normpath)
234
    def normpath(self):
235
        return Path(os.path.normpath(str(self)))
236
237
    @doc(os.path.realpath)
238
    def realpath(self):
239
        return Path(os.path.realpath(self))
240
241
    @doc(os.path.relpath)
242
    def relpath(self, other=""):
243
        return Path(os.path.relpath(str(self), str(other)))
244
245
    @doc(os.path.split)
246
    def split(self, sep=None, maxsplit=-1):
247
        # some heuristics to determine if this is a str.split call or
248
        # a os.split call...
249
        sval = str(self)
250
        if sep is not None or ' ' in sval:
251
            return sval.split(sep or ' ', maxsplit)
252
        return os.path.split(self)
253
254
    def parts(self):
255
        res = re.split(r"\\|/", self)
256
        if res and os.path.splitdrive(res[0]) == (res[0], ''):
257
            res[0] += os.path.sep
258
        return res
259
260
    def parent_iter(self):
261
        parts = self.abspath().normpath().normcase().parts()
262
        for i in range(1, len(parts)):
263
            yield Path(os.path.join(*parts[:-i]))
264
265
    @property
266
    def parents(self):
267
        return list(self.parent_iter())
268
269
    @property
270
    def parent(self):
271
        return self.parents[0]
272
273
    @doc(os.path.splitdrive)
274
    def splitdrive(self):
275
        drive, pth = os.path.splitdrive(self)
276
        return drive, Path(pth)
277
278
    @doc(os.path.splitext)
279
    def splitext(self):
280
        return os.path.splitext(self)
281
282
    @property
283
    def ext(self):
284
        return self.splitext()[1]
285
286
    if hasattr(os.path, 'splitunc'):  # pragma: nocover
287
        @doc(os.path.splitunc)
288
        def splitunc(self):
289
            return os.path.splitunc(self)
290
291
    @doc(os.access)
292
    def access(self, *args, **kw):
293
        return os.access(self, *args, **kw)
294
295
    @doc(os.chdir)
296
    def chdir(self):
297
        return os.chdir(self)
298
299
    @contextmanager
300
    def cd(self):
301
        cwd = os.getcwd()
302
        try:
303
            self.chdir()
304
            yield self
305
        finally:
306
            os.chdir(cwd)
307
308
    @doc(os.chmod)
309
    def chmod(self, *args, **kw):
310
        return os.chmod(self, *args, **kw)
311
312
    @doc(os.listdir)
313
    def listdir(self):
314
        return [Path(p) for p in os.listdir(self)]
315
316
    def list(self, filterfn=lambda x: True):
317
        """Return all direct descendands of directory `self` for which
318
           `filterfn` returns True.
319
        """
320
        return [self / p for p in self.listdir() if filterfn(self / p)]
321
322
    def subdirs(self):
323
        """Return all direct sub-directories.
324
        """
325
        return self.list(lambda p: p.isdir())
326
327
    def files(self):
328
        """Return all files in directory.
329
        """
330
        return self.list(lambda p: p.isfile())
331
332
    @doc(os.lstat)
333
    def lstat(self):
334
        return os.lstat(self)
335
336
    @doc(os.makedirs)
337
    def makedirs(self, path=None, mode=0777):
338
        pth = os.path.join(self, path) if path else self
339
        try:
340
            os.makedirs(pth, mode)
341
        except OSError:
342
            pass
343
        return Path(pth)
344
345
    @doc(os.mkdir)
346
    def mkdir(self, path, mode=0777):
347
        pth = os.path.join(self, path)
348
        os.mkdir(pth, mode)
349
        return Path(pth)
350
351
    @doc(os.remove)
352
    def remove(self):
353
        return os.remove(self)
354
355
    def rm(self, fname=None):
356
        "Remove a file, don't raise exception if file does not exist."
357
        if fname is not None:
358
            return (self / fname).rm()
359
        try:
360
            self.remove()
361
        except OSError:
362
            pass
363
364
    @doc(os.removedirs)
365
    def removedirs(self):
366
        return os.removedirs(self)
367
368
    @doc(os.rename)
369
    def rename(self, *args, **kw):
370
        return os.rename(self, *args, **kw)
371
372
    @doc(os.renames)
373
    def renames(self, *args, **kw):
374
        return os.renames(self, *args, **kw)
375
376
    @doc(os.rmdir)
377
    def rmdir(self):
378
        return os.rmdir(self)
379
380
    if hasattr(os, 'startfile'):  # pragma: nocover
381
        @doc(os.startfile)
382
        def startfile(self, *args, **kw):
383
            return os.startfile(self, *args, **kw)
384
385
    @doc(os.stat)
386
    def stat(self, *args, **kw):
387
        return os.stat(self, *args, **kw)
388
389
    @doc(os.utime)
390
    def utime(self, time=None):
391
        os.utime(self, time)
392
        return self.stat()
393
394
    def __add__(self, other):
395
        return Path(str(self) + str(other))
396
397
398
@contextmanager
399
def cd(pth):
400
    cwd = os.getcwd()
401
    try:
402
        os.chdir(pth)
403
        yield
404
    finally:
405
        os.chdir(cwd)
406