├── .gitignore
├── setup.py
├── UNLICENSE
├── README.md
└── src
└── pathobject.py
/.gitignore:
--------------------------------------------------------------------------------
1 | *.egg-info
2 | *.pyc
3 | *.pyo
4 | .DS_Store
5 | build/
6 | dist/
7 | MANIFEST
8 |
--------------------------------------------------------------------------------
/setup.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | import os
4 | import re
5 | from distutils.core import setup
6 |
7 |
8 | rel_file = lambda *args: os.path.join(os.path.dirname(os.path.abspath(__file__)), *args)
9 |
10 | def read_from(filename):
11 | fp = open(filename)
12 | try:
13 | return fp.read()
14 | finally:
15 | fp.close()
16 |
17 | def get_version():
18 | data = read_from(rel_file('src', 'pathobject.py'))
19 | return re.search(r'__version__ = "([^"]+)"', data).group(1)
20 |
21 |
22 | setup(
23 | name = 'pathobject',
24 | version = get_version(),
25 | description = "An update of Jason Orendorff's path.py.",
26 | author = 'Zachary Voase',
27 | author_email = 'z@zacharyvoase.com',
28 | url = 'http://github.com/zacharyvoase/pathobject',
29 | package_dir = {'': 'src'},
30 | py_modules = ['pathobject'],
31 | )
32 |
--------------------------------------------------------------------------------
/UNLICENSE:
--------------------------------------------------------------------------------
1 | This is free and unencumbered software released into the public domain.
2 |
3 | Anyone is free to copy, modify, publish, use, compile, sell, or
4 | distribute this software, either in source code form or as a compiled
5 | binary, for any purpose, commercial or non-commercial, and by any
6 | means.
7 |
8 | In jurisdictions that recognize copyright laws, the author or authors
9 | of this software dedicate any and all copyright interest in the
10 | software to the public domain. We make this dedication for the benefit
11 | of the public at large and to the detriment of our heirs and
12 | successors. We intend this dedication to be an overt act of
13 | relinquishment in perpetuity of all present and future rights to this
14 | software under copyright law.
15 |
16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22 | OTHER DEALINGS IN THE SOFTWARE.
23 |
24 | For more information, please refer to
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # pathobject
2 |
3 | The aim of this library is to provide an `easy_install`-able update of Jason
4 | Orendorff’s [path](http://pypi.python.org/pypi/path.py) library, free from
5 | deprecation warnings and compatible with Python 2.3+.
6 |
7 | Another major goal of this library is to be path module-agnostic. Using a
8 | different path module is quite simple:
9 |
10 | >>> from pathobject import Path
11 | >>> import ntpath
12 | >>> import posixpath
13 |
14 | >>> POSIXPath = Path.for_path_module(posixpath, name='POSIXPath')
15 | >>> POSIXPath('/a/b/c').splitall()
16 | [POSIXPath(u'/'), u'a', u'b', u'c']
17 |
18 | >>> NTPath = Path.for_path_module(ntpath, name='NTPath')
19 | >>> NTPath(u'C:\\Documents and Settings\\Zack').splitdrive()
20 | (NTPath(u'C:'), u'\\Documents and Settings\\Zack')
21 |
22 | The benefit of this is that you can manipulate Windows paths from a UNIX system,
23 | and vice versa—something simple reliance on `os.path` doesn’t allow.
24 |
25 |
26 | ## (Un)license
27 |
28 | This is free and unencumbered software released into the public domain.
29 |
30 | Anyone is free to copy, modify, publish, use, compile, sell, or distribute this
31 | software, either in source code form or as a compiled binary, for any purpose,
32 | commercial or non-commercial, and by any means.
33 |
34 | In jurisdictions that recognize copyright laws, the author or authors of this
35 | software dedicate any and all copyright interest in the software to the public
36 | domain. We make this dedication for the benefit of the public at large and to
37 | the detriment of our heirs and successors. We intend this dedication to be an
38 | overt act of relinquishment in perpetuity of all present and future rights to
39 | this software under copyright law.
40 |
41 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
42 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
43 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE
44 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
45 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
46 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
47 |
48 | For more information, please refer to
49 |
--------------------------------------------------------------------------------
/src/pathobject.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """pathobject.py - A utility class for operating on pathnames."""
4 |
5 | import codecs
6 | import fnmatch
7 | import glob
8 | import hashlib
9 | import os
10 | import shutil
11 | import sys
12 | import warnings
13 |
14 | __all__ = ["Path"]
15 | __version__ = "0.0.1"
16 |
17 |
18 | ## Some functional utilities to save code later on.
19 |
20 | def update_wrapper(wrapper, wrapped):
21 | """Update a wrapper function to look like the wrapped function."""
22 |
23 | for attr in ('__module__', '__name__', '__doc__'):
24 | value = getattr(wrapped, attr, None)
25 | if value:
26 | setattr(wrapper, attr, value)
27 | wrapper.__dict__.update(getattr(wrapped, '__dict__', {}))
28 | return wrapper
29 |
30 |
31 | def wrap(function, doc=None):
32 | """Wrap a basic `os.path` function to return `Path` instances."""
33 |
34 | def method(self, *args, **kwargs):
35 | return type(self)(function(self, *args, **kwargs))
36 | method = update_wrapper(method, function)
37 | if doc:
38 | method.__doc__ = doc
39 | return method
40 |
41 |
42 | def pmethod(name):
43 | """Return a proxy method to a named function on the current path module."""
44 |
45 | return lambda self, *a, **kw: getattr(self._path, name)(self, *a, **kw)
46 |
47 |
48 | def defined_if(predicate):
49 |
50 | """
51 | Declare a method as only defined if `self` meets a given predicate.
52 |
53 | >>> class Test(object):
54 | ... x = defined_if(lambda self: True)(lambda self: 1)
55 | ... y = defined_if(lambda self: False)(lambda self: 1)
56 |
57 | >>> Test().x()
58 | 1
59 |
60 | >>> Test().y # doctest: +ELLIPSIS
61 | Traceback (most recent call last):
62 | ...
63 | AttributeError: not defined for
64 |
65 | """
66 |
67 | def decorator(method):
68 | def wrapper(self):
69 | if not predicate(self):
70 | raise AttributeError("%s not defined for %r" % (method.__name__, self))
71 |
72 | def function(*args, **kwargs):
73 | return method(self, *args, **kwargs)
74 | return update_wrapper(function, method)
75 | return property(wrapper)
76 | return decorator
77 |
78 |
79 | def normalize_line_endings(text, linesep=u'\n'):
80 |
81 | """
82 | Normalize a string's line endings to `linesep` (default ).
83 |
84 | The provided string can be either a `str` or a `unicode`.
85 |
86 | Pass `linesep=''` to remove line endings entirely. This only makes sense
87 | when operating on a single line.
88 | """
89 |
90 | if isinstance(text, str):
91 | return (text.replace('\r\n', '\n')
92 | .replace('\r', '\n')
93 | .replace('\n', linesep))
94 |
95 | return (text.replace(u'\r\n', u'\n')
96 | .replace(u'\r\x85', u'\n')
97 | .replace(u'\r', u'\n')
98 | .replace(u'\x85', u'\n')
99 | .replace(u'\u2028', u'\n')
100 | .replace(u'\n', linesep))
101 |
102 |
103 | class Path(unicode):
104 |
105 | """A utility class for operating on pathnames."""
106 |
107 | _path = os.path
108 |
109 | def __repr__(self):
110 | return "%s(%s)" % (type(self).__name__, unicode.__repr__(self))
111 |
112 | __add__ = wrap(lambda self, other: unicode(self) + other)
113 | __radd__ = wrap(lambda self, other: other + self)
114 | __div__ = wrap(pmethod('join'), "Shortcut for `os.path.join()`.")
115 | __truediv__ = __div__
116 |
117 | # @classmethod
118 | def cwd(cls):
119 | """Return the current working directory as a `Path`."""
120 |
121 | return cls(os.getcwdu())
122 | cwd = classmethod(cwd)
123 |
124 | # @classmethod
125 | def for_path_module(cls, pathmod, name=None):
126 |
127 | """
128 | Return a `Path` class for the given path module.
129 |
130 | This allows you to use `Path` to perform NT path manipulation on UNIX
131 | machines and vice versa.
132 |
133 | Example:
134 |
135 | >>> import ntpath
136 | >>> NTPath = Path.for_path_module(ntpath, name="NTPath")
137 | >>> NTPath(u'C:\\\\A\\\\B\\\\C').splitdrive()
138 | (NTPath(u'C:'), u'\\\\A\\\\B\\\\C')
139 | """
140 |
141 | if name is None:
142 | name = cls.__name__
143 |
144 | return type(name, (cls,), {'_path': pathmod})
145 | for_path_module = classmethod(for_path_module)
146 |
147 | # Simple proxy methods or properties.
148 |
149 | is_absolute = pmethod('isabs')
150 | absolute = wrap(pmethod('abspath'))
151 | normcase = wrap(pmethod('normcase'))
152 | normalize = wrap(pmethod('normpath'))
153 | realpath = wrap(pmethod('realpath'))
154 | joinpath = wrap(pmethod('join'))
155 | expanduser = wrap(pmethod('expanduser'))
156 | expandvars = wrap(pmethod('expandvars'))
157 | dirname = wrap(pmethod('dirname'))
158 | basename = pmethod('basename')
159 |
160 | parent = property(dirname, None, None,
161 | """Property synonym for `os.path.dirname()`.
162 |
163 | Example:
164 |
165 | >>> Path('/usr/local/lib/libpython.so').parent
166 | Path(u'/usr/local/lib')
167 |
168 | """)
169 |
170 | name = property(basename, None, None,
171 | """Property synonym for `os.path.basename()`.
172 |
173 | Example:
174 |
175 | >>> Path('/usr/local/lib/libpython.so').name
176 | u'libpython.so'
177 |
178 | """)
179 |
180 | ext = property(lambda self: self._path.splitext(self)[1], None, None,
181 | """Return the file extension (e.g. '.py').""")
182 |
183 | drive = property(lambda self: self._path.splitdrive(self)[0], None, None,
184 | """Return the drive specifier (e.g. "C:").""")
185 |
186 | def splitpath(self):
187 |
188 | """
189 | Return `(p.parent, p.name)`.
190 |
191 | Example:
192 |
193 | >>> Path('/usr/local/lib/libpython.so').splitpath()
194 | (Path(u'/usr/local/lib'), u'libpython.so')
195 |
196 | """
197 |
198 | parent, child = self._path.split(self)
199 | return type(self)(parent), child
200 |
201 | def splitdrive(self):
202 |
203 | """
204 | Return `(p.drive, )`.
205 |
206 | If there is no drive specifier, `p.drive` is empty (as is always the
207 | case on UNIX), so the result will just be `(Path(u''), u'')`.
208 |
209 | Example:
210 |
211 | >>> import ntpath
212 | >>> import posixpath
213 |
214 | >>> Path.for_path_module(ntpath)('C:\\\\A\\\\B\\\\C').splitdrive()
215 | (Path(u'C:'), u'\\\\A\\\\B\\\\C')
216 |
217 | >>> Path.for_path_module(posixpath)('/a/b/c').splitdrive()
218 | (Path(u''), u'/a/b/c')
219 |
220 | """
221 |
222 | drive, rel = self._path.splitdrive(unicode(self))
223 | return type(self)(drive), rel
224 |
225 | def splitext(self):
226 |
227 | """
228 | Return `(, extension)`.
229 |
230 | Splits the filename on the last `.` character, and returns both pieces.
231 | The extension is prefixed with the `.`, so that the following holds:
232 |
233 | >>> p = Path('/some/path/to/a/file.txt.gz')
234 | >>> a, b = p.splitext()
235 | >>> a + b == p
236 | True
237 |
238 | Example:
239 |
240 | >>> Path('/home/zack/filename.tar.gz').splitext()
241 | (Path(u'/home/zack/filename.tar'), u'.gz')
242 |
243 | """
244 |
245 | filename, extension = self._path.splitext(self)
246 | return type(self)(filename), extension
247 |
248 | def stripext(self):
249 |
250 | """
251 | Remove one file extension from the path.
252 |
253 | Example:
254 |
255 | >>> Path('/home/guido/python.tar.gz').stripext()
256 | Path(u'/home/guido/python.tar')
257 |
258 | """
259 |
260 | return self.splitext()[0]
261 |
262 | # @defined_if(lambda self: hasattr(self._path, 'splitunc'))
263 | def splitunc(self):
264 | unc, rest = self._path.splitunc(self)
265 | return type(self)(unc), rest
266 | splitunc = defined_if(lambda self: hasattr(self._path, 'splitunc'))(splitunc)
267 |
268 | uncshare = property(lambda self: self.splitunc()[0], None, None,
269 | """The UNC mount point for this path. Empty for paths on local drives.""")
270 |
271 | def splitall(self):
272 |
273 | """
274 | Return a list of the path components in this path.
275 |
276 | The first item in the list will be a `Path`. Its value will be either
277 | `path.curdir`, `path.pardir`, empty, or the root directory of this path
278 | (e.g. `'/'` or `'C:\\'`). The other items in the list will be strings.
279 |
280 | By definition, `result[0].joinpath(*result[1:])` will yield the original
281 | path.
282 |
283 | >>> p = Path(u'/home/guido/python.tar.gz')
284 | >>> parts = p.splitall()
285 | >>> parts
286 | [Path(u'/'), u'home', u'guido', u'python.tar.gz']
287 |
288 | >>> parts[0].joinpath(*parts[1:])
289 | Path(u'/home/guido/python.tar.gz')
290 |
291 | """
292 |
293 | parts = []
294 | location = self
295 | while location not in (self._path.curdir, self._path.pardir):
296 | previous = location
297 | location, child = previous.splitpath()
298 | if location == previous:
299 | break
300 | parts.append(child)
301 | parts.append(location)
302 | parts.reverse()
303 | return parts
304 |
305 | def relpath(self):
306 | """Return the relative path from the current directory to this path."""
307 |
308 | return self.relpathfrom(self.cwd())
309 |
310 | def relpathfrom(self, origin):
311 |
312 | """
313 | Return a relative path from a given origin to this one.
314 |
315 | This is a simple wrapper over `relpathto()`.
316 | """
317 |
318 | return type(self)(origin).relpathto(self)
319 |
320 | def relpathto(self, destination):
321 |
322 | """
323 | Return a relative path from this one to a given destination.
324 |
325 | If no relative path exists (e.g. if they reside on different drives on
326 | Windows), this will return `destination.absolute()`.
327 |
328 | >>> Path(u'/a/b/c').relpathto('/a/d/e')
329 | Path(u'../../d/e')
330 |
331 | """
332 |
333 | origin = self.absolute()
334 | destination = type(self)(destination).absolute()
335 |
336 | orig_list = origin.normcase().splitall()
337 | dest_list = destination.splitall()
338 |
339 | if orig_list[0] != self._path.normcase(dest_list[0]):
340 | # No relative path exists.
341 | return destination
342 |
343 | # Find the location where the two paths diverge.
344 | common_index = 0
345 | for orig_part, dest_part in zip(orig_list, dest_list):
346 | if orig_part != self._path.normcase(dest_part):
347 | break
348 | common_index += 1
349 |
350 | # A certain number of pardirs are required to work up from the origin to
351 | # the point of divergence.
352 | segments = [self._path.pardir] * (len(orig_list) - common_index)
353 | segments += dest_list[common_index:]
354 | if not segments:
355 | # The paths are identical; return '.' (or equivalent).
356 | return type(self)(self._path.curdir)
357 | return type(self)(self._path.join(*segments))
358 |
359 | def fnmatch(self, pattern):
360 | """Return `True` if `self.name` matches the given glob pattern."""
361 |
362 | return fnmatch.fnmatch(self.name, pattern)
363 |
364 | ## Reading and Writing.
365 |
366 | def open(self, mode='r', bufsize=None, encoding=None, errors='strict'):
367 |
368 | """
369 | Open this file, returning a file handler.
370 |
371 | You can open a file with `codecs.open()` by passing an `encoding`
372 | keyword argument.
373 | """
374 |
375 | if encoding is not None:
376 | return codecs.open(self, mode=mode, bufsize=bufsize, encoding=encoding, errors=errors)
377 | elif bufsize is not None:
378 | return open(self, mode, bufsize)
379 | return open(self, mode)
380 |
381 | def bytes(self):
382 | """Read the contents of this file as a bytestring."""
383 |
384 | fp = self.open(mode='rb')
385 | try:
386 | return fp.read()
387 | finally:
388 | fp.close()
389 |
390 | def write_bytes(self, bytes, append=False):
391 |
392 | """
393 | Open this file and write the given bytes to it.
394 |
395 | The default behavior is to truncate any existing file. Use `append=True`
396 | to append instead.
397 | """
398 |
399 | fp = self.open(mode=(append and 'ab' or 'wb'))
400 | try:
401 | fp.write(bytes)
402 | finally:
403 | fp.close()
404 |
405 | def text(self, encoding=None, errors='strict'):
406 |
407 | """
408 | Read the contents of this file as text.
409 |
410 | Universal newline mode is used where available, so and
411 | line endings are translated to .
412 |
413 | Pass `encoding` to decode the contents of the file using the given
414 | character set, returning a Unicode string. Without this argument, the
415 | text is returned as a bytestring.
416 |
417 | The `errors` keyword argument will be passed as-is to the decoder (see
418 | `help(str.decode)` for details). The default value is `'strict'`.
419 | """
420 |
421 | if encoding is None:
422 | fp = self.open(mode=(hasattr(file, 'newlines') and 'U' or 'r'))
423 | try:
424 | return fp.read()
425 | finally:
426 | fp.close()
427 |
428 | fp = codecs.open(self, 'r', encoding, errors)
429 | try:
430 | text = fp.read()
431 | finally:
432 | fp.close()
433 |
434 | # Universal newline mode isn't supported by `codecs.open()`, so we have
435 | # to perform the replacement manually.
436 | return normalize_line_endings(text, linesep=u'\n')
437 |
438 | def write_text(self, text, encoding=None, errors='strict', linesep=os.linesep, append=False):
439 |
440 | """
441 | Write the given text to this file.
442 |
443 | There are two differences between `write_text()` and `write_bytes()`:
444 | newline handling and Unicode handling.
445 |
446 | The default behavior is to truncate any existing file. Use `append=True`
447 | to append instead.
448 |
449 | If `text` is a Unicode string, the `encoding` and `errors` parameters
450 | will be used to encode it to a bytestring. In this case, `encoding`
451 | defaults to `sys.getdefaultencoding()`. If `text` is already a
452 | bytestring, no encoding occurs, and passing a value for `encoding` will
453 | raise an `AssertionError`.
454 |
455 | By default, `write_text()` will normalize line endings to `os.linesep`.
456 | You can customize this by passing a `linesep` keyword argument. If
457 | `linesep` is `None`, the line endings will be left as-is.
458 | """
459 |
460 | if isinstance(text, unicode):
461 | if linesep is not None:
462 | # Convert all standard end-of-line sequences to
463 | # ordinary newline characters.
464 | text = normalize_line_endings(text, linesep=linesep)
465 | if encoding is None:
466 | encoding = sys.getdefaultencoding()
467 | bytes = text.encode(encoding, errors)
468 | else:
469 | assert encoding is None, "Passed an encoding for a bytestring."
470 |
471 | if linesep is not None:
472 | bytes = normalize_line_endings(text, linesep=linesep)
473 |
474 | self.write_bytes(bytes, append=append)
475 |
476 | def lines(self, encoding=None, errors='strict', retain=True, linesep='\n'):
477 |
478 | """
479 | Iterate through all the lines in this file.
480 |
481 | Pass `retain=False` to strip the newlines from each line. Otherwise,
482 | all newlines are normalized to `linesep` (default ). The semantics
483 | for the `encoding` and `errors` arguments are similar to those for
484 | `Path.open()`.
485 |
486 | Note that `Path.lines().close()` will only work in Python 2.5+; if you
487 | are using an earlier version of Python you will need to exhaust the
488 | generator to properly close the file handle.
489 | """
490 |
491 | fp = self.open(encoding=encoding, errors=errors)
492 | linesep = retain and linesep or ''
493 |
494 | try:
495 | for line in fp:
496 | yield normalize_line_endings(line, linesep=linesep)
497 | finally:
498 | fp.close()
499 |
--------------------------------------------------------------------------------