├── .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 | --------------------------------------------------------------------------------