├── .gitignore ├── BUGS.txt ├── CHANGES ├── MANIFEST.in ├── PKG-INFO ├── README.html ├── README.rst ├── setup.py ├── test.py ├── tox.ini └── unipath ├── __init__.py ├── abstractpath.py ├── errors.py ├── path.py └── tools.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build 3 | dist 4 | MANIFEST 5 | Unipath.egg-info 6 | .tox 7 | -------------------------------------------------------------------------------- /BUGS.txt: -------------------------------------------------------------------------------- 1 | Methods inherited from unicode return strings rather than paths. 2 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | 1.1 (2015-02-10) 2 | ---------------- 3 | * Fix unicode NameError on Python 3. (Appeared only on OSes with unicode 4 | paths.)(Thanks to Hanlle Nicolas, Aaron Lelevier, Dmitry Dygalo.) 5 | * Fix test on MacOSX: temp dir contains symlink. (wrightmx) 6 | * Fix ``walk`` error in arg ``top_down``. (Gabriel Reyla == sesas) 7 | * Fix ``isdir()`` issue on Windows. (Gabriel Reyla) 8 | * Fix ``needs_update`` with string argument. (#15) 9 | * Change license to MIT. It's at least as free as the Python license but 10 | doesn't contain irrelevant Python-specific language. 11 | 12 | 1.0 (2013-04-05) 13 | ------------------ 14 | * Migrate repository to Git and Github. (https://github.com/mikeorr/Unipath) 15 | * Supports Python 2.6, 2.7, 3.2, and 3.3. No longer suppots Python 2.5 or older. 16 | * Python 3 compatibility. (Ricardo Duarte) 17 | * Convert tests to py.test. (Ricardo Duarte) 18 | * Delete old PEP 335 reference documentation. To recover, check out Git rev 19 | 7575cdf, directory "doc/reference". 20 | * Undocument ``Path.copy_tree()``. It was never implemented. 21 | * Fix Path.remove and Path.rmtree for symlinks. (Joel Rosdahl) 22 | 23 | 0.2.1 (released 2008-05-19 by MSO) 24 | ---------------------------------- 25 | * Delete spurious references to deleted ``unipath.platform`` package. 26 | 27 | 0.2.0 (released 2008-05-??) 28 | --------------------------- 29 | * Rename Path to AbstractPath, and FSPath to Path. FSPath remains as an 30 | alias for backward compatibility. 31 | * Allow integers in constructor. 32 | * Path.mkdir() checks whether the directory exists first. 33 | * Test suite now uses nose instead of unittest. 34 | * "+" operator returns concatenated path rather than string. 35 | * Bugfix in Path.rel_path_to(). 36 | * Thanks to Roman for patches and suggestions. 37 | * Delete Path.symlink(); use Path.write_link() instead -- note that the 38 | arg is the desination rather than the source! 39 | * Path.make_relative_link_to() is a shortcut for 40 | ``self.write_link(self.rel_path_to(dst))``. 41 | * Delete the ``platform`` package. See the tests if you need non-native 42 | path syntax.` 43 | 44 | 0.1.0 (released 2007-01-28 by MSO) 45 | ---------------------------------- 46 | * Initial release. 47 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include BUGS.txt 2 | include CHANGES 3 | include README.html 4 | include README.rst 5 | include tox.ini 6 | -------------------------------------------------------------------------------- /PKG-INFO: -------------------------------------------------------------------------------- 1 | Metadata-Version: 1.1 2 | Name: Unipath 3 | Version: 1.0 4 | Summary: Object-oriented alternative to os/os.path/shutil 5 | Home-page: http://sluggo.scrapping.cc/python/unipath/ 6 | Author: Mike Orr 7 | Author-email: sluggoster@gmail.com 8 | License: Python 9 | Download-URL: http://sluggo.scrapping.cc/python/unipath/Unipath-1.0.tar.gz 10 | Description: Unipath is an object-oriented front end to the file/directory functions 11 | scattered throughout several Python library modules. It's based on Jason 12 | Orendorff's *path.py* but does not adhere as strictly to the underlying 13 | functions' syntax, in order to provide more user convenience and higher-level 14 | functionality. Unipath is stable, well-tested, and has been used in production 15 | since 2008. 16 | 17 | Keywords: os.path filename pathspec path files directories filesystem 18 | Platform: UNKNOWN 19 | Classifier: License :: OSI Approved :: Python Software Foundation License 20 | Classifier: Operating System :: OS Independent 21 | Classifier: Topic :: Software Development :: Libraries :: Python Modules 22 | Classifier: Topic :: Utilities 23 | Classifier: Programming Language :: Python 24 | Classifier: Programming Language :: Python :: 2 25 | Classifier: Programming Language :: Python :: 2.6 26 | Classifier: Programming Language :: Python :: 2.7 27 | Classifier: Programming Language :: Python :: 3 28 | Classifier: Programming Language :: Python :: 3.2 29 | Classifier: Programming Language :: Python :: 3.3 30 | -------------------------------------------------------------------------------- /README.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Unipath 8 | 9 | 340 | 341 | 342 |
343 |

Unipath

344 |

An object-oriented approach to file/directory operations

345 | 346 | 347 | 348 | 349 | 350 | 351 | 352 | 353 | 354 | 355 | 356 | 357 | 358 | 359 | 360 |
Version:1.1
Home page:https://github.com/mikeorr/Unipath
Docs:https://github.com/mikeorr/Unipath#readme
Author:Mike Orr <sluggoster@gmail.com>
License:MIT (http://opensource.org/licenses/MIT)
361 | 363 |

Unipath is an object-oriented front end to the file/directory functions 364 | scattered throughout several Python library modules. It's based on Jason 365 | Orendorff's path.py but focuses on user convenience rather than on strict 366 | adherence to the underlying functions' syntax.does not adhere as strictly to the 367 | underlying functions' syntax. Unipath is stable, well-tested, and has been used 368 | in production since 2008. It runs on Python 2.6+ and 3.2+.

369 |

Version 1.1 is a bugfix release. It fixes a Unicode incompatibility on 370 | Python 3 under Windows. (Or more generally, on operating systems with native 371 | Unicode filenames.) The license is changed to MIT. It's as permissive as the 372 | former Python license but is smaller and simpler to read.

373 |

Python 3.4 introduced another object-oriented path library, pathlib. It's 374 | available on PyPI as pathlib2 for older versions of Python. (pathlib on 375 | PyPI is a frozen earlier version.) Unipath is now in maintenance mode. The 376 | author is exploring a subclass of pathlib(2) adding some of Unipath's features.

377 | 411 |
412 |

Introduction

413 |

The Path class encapsulates the file/directory operations in Python's 414 | os, os.path, and shutil modules. (Non-filesystem operations are in 415 | the AbstractPath superclass, but users can ignore this.)

416 |

The API has been streamlined to focus on what the application developer wants 417 | to do rather than on the lowest-level operations; e.g., .mkdir() succeeds 418 | silently if the directory already exists, and .rmtree() doesn't barf if the 419 | target is a file or doesn't exist. This allows the developer to write simple 420 | calls that "just work" rather than entire if-stanzas to handle low-level 421 | details s/he doesn't care about. This makes applications more self-documenting 422 | and less cluttered.

423 |

Convenience methods:

424 |
425 |
    426 |
  • .read_file and .write_file encapsulate the open/read/close pattern.
  • 427 |
  • .needs_update(others) tells whether the path needs updating; i.e., 428 | if it doesn't exist or is older than any of the other paths.
  • 429 |
  • .ancestor(N) returns the Nth parent directory, useful for joining paths.
  • 430 |
  • .child(\*components) is a "safe" version of join.
  • 431 |
  • .split_root() handles slash/drive/UNC absolute paths in a uniform way.
  • 432 |
433 |
434 |

Sample usage for pathname manipulation:

435 |
 436 | >>> from unipath import Path
 437 | >>> p = Path("/usr/lib/python2.5/gopherlib.py")
 438 | >>> p.parent
 439 | Path("/usr/lib/python2.5")
 440 | >>> p.name
 441 | Path("gopherlib.py")
 442 | >>> p.ext
 443 | '.py'
 444 | >>> p.stem
 445 | Path('gopherlib')
 446 | >>> q = Path(p.parent, p.stem + p.ext)
 447 | >>> q
 448 | Path('/usr/lib/python2.5/gopherlib.py')
 449 | >>> q == p
 450 | True
 451 | 
452 |

Sample usage for filesystem access:

453 |
 454 | >>> import tempfile
 455 | >>> from unipath import Path
 456 | >>> d = Path(tempfile.mkdtemp())
 457 | >>> d.isdir()
 458 | True
 459 | >>> p = Path(d, "sample.txt")
 460 | >>> p.exists()
 461 | False
 462 | >>> p.write_file("The king is a fink!")
 463 | >>> p.exists()
 464 | True
 465 | >>> print(p.read_file())
 466 | The king is a fink!
 467 | >>> d.rmtree()
 468 | >>> p.exists()
 469 | False
 470 | 
471 |

Path objects subclass str (Python 2 unicode), so they can be passed 472 | directly to fuctions expecting a string path. They are also immutable and can 473 | be used as dictionary keys.

474 |

The name "Unipath" is short for "universal path". It was originally intended to 475 | unify the competing path APIs as of PEP 334. When the PEP was rejected, Unipath 476 | added some convenience APIs. The code is implemented in layers, with 477 | filesystem-dependent code in the Path class and filesystem-independent code 478 | in its AbstractPath superclass.

479 |
480 |
481 |

Installation and testing

482 |

Run "pip install Unipath". Or to install the development version, check out 483 | the source from the Git repository above and run "python setup.py develop".

484 |

To test the library, install 'pytest' and run "pytest test.py". It also comes 485 | with a Tox INI file.

486 |
487 |
488 |

Path and AbstractPath objects

489 |
490 |

Constructor

491 |

Path (and AbstractPath) objects can be created from a string path, or 492 | from several string arguments which are joined together a la os.path.join. 493 | Each argument can be a string, an (Abstract)Path instance, an int or long, 494 | or a list/tuple of strings to be joined:

495 |
 496 | p = Path("foo/bar.py")       # A relative path
 497 | p = Path("foo", "bar.py")    # Same as previous
 498 | p = Path(["foo", "bar.py"])  # Same as previous
 499 | p = Path("/foo", "bar", "baz.py")       # An absolute path: /foo/bar/baz.py
 500 | p = Path("/foo", Path("bar/baz.py"))    # Same as previous
 501 | p = Path("/foo", ["", "bar", "baz.py"]) # Embedded Path.components() result
 502 | p = Path("record", 123)      # Same as Path("record/123")
 503 | 
 504 | p = Path("")     # An empty path
 505 | p = Path()       # Same as Path(os.curdir)
 506 | 
507 |

To get the actual current directory, use Path.cwd(). (This doesn't work 508 | with AbstractPath, of course.

509 |

Adding two paths results in a concatenated path. The other string methods 510 | return strings, so you'll have to wrap them in Path to make them paths 511 | again. A future version will probably override these methods to return paths. 512 | Multiplying a path returns a string, as if you'd ever want to do that.

513 |
514 |
515 |

Normalization

516 |

The new path is normalized to clean up redundant ".." and "." in the 517 | middle, double slashes, wrong-direction slashes, etc. On 518 | case-insensitive filesystems it also converts uppercase to lowercase. 519 | This is all done via os.path.normpath(). Here are some examples 520 | of normalizations:

521 |
 522 | a//b  => a/b
 523 | a/../b => b
 524 | a/./b => a/b
 525 | 
 526 | a/b => a\\b            # On NT.
 527 | a\\b.JPG => a\\b.jpg   # On NT.
 528 | 
529 |

If the actual filesystem path contains symbolic links, normalizing ".." goes to 530 | the parent of the symbolic link rather than to the parent of the linked-to 531 | file. For this reason, and because there may be other cases where normalizing 532 | produces the wrong path, you can disable automatic normalization by setting the 533 | .auto_norm class attribute to false. I'm not sure whether Unipath should 534 | normalize by default, so if you care one way or the other you should explicitly 535 | set it at the beginning of your application. You can override the auto_norm 536 | setting by passing "norm=True" or "norm=False" as a keyword argument to the 537 | constructor. You can also call .norm() anytime to manually normalize the 538 | path.

539 |
540 |
541 |

Properties

542 |

Path objects have the following properties:

543 |
544 |
.parent
545 |
The path without the final component.
546 |
.name
547 |
The final component only.
548 |
.ext
549 |
The last part of the final component beginning with a dot (e.g., ".gz"), or 550 | "" if there is no dot. This is also known as the extension.
551 |
.stem
552 |
The final component without the extension.
553 |
554 |

Examples are given in the first sample usage above.

555 |
556 |
557 |

Methods

558 |

Path objects have the following methods:

559 |
560 |
.ancestor(N)
561 |
Same as specifying .parent N times.
562 |
.child(*components)
563 |
Join paths in a safe manner. The child components may not contain a path 564 | separator or be curdir or pardir ("." or ".." on Posix). This is to 565 | prevent untrusted arguments from creating a path above the original path's 566 | directory.
567 |
.components()
568 |
Return a list of directory components as strings. The first component will 569 | be the root ("/" on Posix, a Windows drive root, or a UNC share) if the 570 | path is absolute, or "" if it's relative. Calling Path(components), 571 | Path(*components), or os.path.join(*components) will recreate the 572 | original path.
573 |
.expand()
574 |
Same as p.expand_user().expand_vars().norm(). Usually this is all 575 | you need to fix up a path read from a config file.
576 |
.expand_user()
577 |
Interpolate "~" and "~user" if the platform allows, and return a new path.
578 |
.expand_vars()
579 |
Interpolate environment variables like "$BACKUPS" if the platform allows, 580 | and return a new path.
581 |
.isabsolute()
582 |
Is the path absolute?
583 |
.norm()
584 |
See Normalization above. Same as os.path.normpath.
585 |
.norm_case()
586 |
On case-insensitive platforms (Windows) convert the path to lower case. 587 | On case-sensitive platforms (Unix) leave the path as is. This also turns 588 | forward slashes to backslashes on Windows.
589 |
.split_root()
590 |
Split this path at the root and return a tuple of two paths: the root and 591 | the rest of the path. The root is the same as the first subscript of the 592 | .components() result. Calling Path(root, rest) or 593 | os.path.join(root, rest) will produce the original path.
594 |
595 |

Examples:

596 |
 597 | Path("foo/bar.py").components() =>
 598 |     [Path(""), Path("foo"), Path("bar.py")]
 599 | Path("foo/bar.py").split_root() =>
 600 |     (Path(""), Path("foo/bar.py"))
 601 | 
 602 | Path("/foo/bar.py").components() =>
 603 |     [Path("/"), Path("foo"), Path("bar.py")]
 604 | Path("/foo/bar.py").split_root() =>
 605 |     (Path("/"), Path("foo/bar.py"))
 606 | 
 607 | Path("C:\\foo\\bar.py").components() =>
 608 |     ["Path("C:\\"), Path("foo"), Path("bar.py")]
 609 | Path("C:\\foo\\bar.py").split_root() =>
 610 |     ("Path("C:\\"), Path("foo\\bar.py"))
 611 | 
 612 | Path("\\\\UNC_SHARE\\foo\\bar.py").components() =>
 613 |     [Path("\\\\UNC_SHARE"), Path("foo"), Path("bar.py")]
 614 | Path("\\\\UNC_SHARE\\foo\\bar.py").split_root() =>
 615 |     (Path("\\\\UNC_SHARE"), Path("foo\\bar.py"))
 616 | 
 617 | Path("~/bin").expand_user() => Path("/home/guido/bin")
 618 | Path("~timbot/bin").expand_user() => Path("/home/timbot/bin")
 619 | Path("$HOME/bin").expand_vars() => Path("/home/guido/bin")
 620 | Path("~//$BACKUPS").expand() => Path("/home/guido/Backups")
 621 | 
 622 | Path("dir").child("subdir", "file") => Path("dir/subdir/file")
 623 | 
 624 | Path("/foo").isabsolute() => True
 625 | Path("foo").isabsolute() => False
 626 | 
627 |

Note: a Windows drive-relative path like "C:foo" is considered absolute by 628 | .components(), .isabsolute(), and .split_root(), even though 629 | Python's ntpath.isabs() would return false.

630 |
631 |
632 |
633 |

Path objects only

634 |
635 |

Note on arguments

636 |

All arguments that take paths can also take strings.

637 |
638 |
639 |

Current directory

640 |
641 |
Path.cwd()
642 |
Return the actual current directory; e.g., Path("/tmp/my_temp_dir"). 643 | This is a class method.
644 |
.chdir()
645 |
Make self the current directory.
646 |
647 |
648 |
649 |

Calculating paths

650 |
651 |
.resolve()
652 |
Return the equivalent path without any symbolic links. This normalizes 653 | the path as a side effect.
654 |
.absolute()
655 |
Return the absolute equivalent of self. If the path is relative, this 656 | prefixes the current directory; i.e., FSPath(FSPath.cwd(), p).
657 |
.relative()
658 |
Return an equivalent path relative to the current directory if possible. 659 | This may return a path prefixed with many "../..". If the path is on a 660 | different drive, this returns the original path unchanged.
661 |
.rel_path_to(other)
662 |
Return a path from self to other. In other words, return a path for 663 | 'other' relative to self.
664 |
665 |
666 |
667 |

Listing directories

668 |
669 |
.listdir(pattern=None, filter=ALL, names_only=False)
670 |

Return the filenames in this directory.

671 |

'pattern' may be a glob expression like "*.py".

672 |

'filter' may be a function that takes a FSPath and returns true if it 673 | should be included in the results. The following standard filters are 674 | defined in the unipath module:

675 |
676 |
    677 |
  • DIRS: directories only
  • 678 |
  • FILES: files only
  • 679 |
  • LINKS: symbolic links only
  • 680 |
  • FILES_NO_LINKS: files that aren't symbolic links
  • 681 |
  • DIRS_NO_LINKS: directories that aren't symbolic links
  • 682 |
  • DEAD_LINKS: symbolic links that point to nonexistent files
  • 683 |
684 |
685 |

This method normally returns FSPaths prefixed with 'self'. If 686 | 'names_only' is true, it returns the raw filenames as strings without a 687 | directory prefix (same as os.listdir).

688 |

If both 'pattern' and 'filter' are specified, only paths that pass both are 689 | included. 'filter' must not be specified if 'names_only' is true.

690 |

Paths are returned in sorted order.

691 |
692 |
693 |

.walk(pattern=None, filter=None, top_down=True)

694 |
695 |

Yield FSPath objects for all files and directories under self, 696 | recursing subdirectories. Paths are yielded in sorted order.

697 |

'pattern' and 'filter' are the same as for .listdir().

698 |

If 'top_down' is true (default), yield directories before yielding 699 | the items in them. If false, yield the items first.

700 |
701 |
702 |
703 |

File attributes and permissions

704 |
705 |
.atime()
706 |
Return the path's last access time.
707 |
.ctime()
708 |
Return the path's ctime. On Unix this returns the time the path's 709 | permissions and ownership were last modified. On Windows it's the path 710 | creation time.
711 |
.exists()
712 |
Does the path exist? For symbolic links, True if the linked-to file 713 | exists. On some platforms this returns False if Python does not have 714 | permission to stat the file, even if it exists.
715 |
.isdir()
716 |
Is the path a directory? Follows symbolic links.
717 |
.isfile()
718 |
Is the path a file? Follows symbolic links.
719 |
.islink()
720 |
Is the path a symbolic link?
721 |
.ismount()
722 |
Is the path a mount point? Returns true if self's parent is on a 723 | different device than self, or if self and its parent are the same 724 | directory.
725 |
.lexists()
726 |
Same as .exists() but don't follow a final symbolic link.
727 |
.lstat()
728 |
Same as .stat() but do not follow a final symbolic link.
729 |
.size()
730 |
Return the file size in bytes.
731 |
.stat()
732 |
Return a stat object to test file size, type, permissions, etc. 733 | See os.stat() for details.
734 |
.statvfs()
735 |
Return a StatVFS object. This method exists only if the platform 736 | supports it. See os.statvfs() for details.
737 |
738 |
739 |
740 |

Modifying paths

741 |
742 |

Creating/renaming/removing

743 |
744 |
.chmod(mode)
745 |
Change the path's permissions. 'mode' is octal; e.g., 0777.
746 |
.chown(uid, gid)
747 |
Change the path's ownership to the numeric uid and gid specified. 748 | Pass -1 if you don't want one of the IDs changed.
749 |
.mkdir(parents=False)
750 |
Create the directory, or succeed silently if it already exists. If 751 | 'parents' is true, create any necessary ancestor directories.
752 |
.remove()
753 |
Delete the file. Raises OSError if it's a directory.
754 |
.rename(dst, parents=False)
755 |
Rename self to 'dst' atomically. See os.rename() for additional 756 | details. If 'parents' is True, create any intermediate destination 757 | directories necessary, and delete as many empty leaf source directories as 758 | possible.
759 |
.rmdir(parents=False)
760 |
Remove the directory, or succeed silently if it's already gone. If 761 | 'parents' is true, also remove as many empty ancestor directories as 762 | possible.
763 |
.set_times(mtime=None, atime=None)
764 |
Set the path's modification and access times. If 'mtime' is None, use 765 | the current time. If 'atime' is None or not specified, use the same time 766 | as 'mtime'. To set the times based on another file, see .copy_stat().
767 |
768 |
769 | 785 |
786 |
787 |

High-level operations

788 |
789 |
.copy(dst, times=False, perms=False)
790 |
Copy the file to a destination. 'times' and 'perms' are same as for 791 | .copy_stat().
792 |
.copy_stat(dst, times=True, perms=True)
793 |
Copy the access/modification times and/or the permission bits from this 794 | path to another path.
795 |
.move(dst)
796 |
Recursively move a file or directory to another location. This uses 797 | .rename() if possible.
798 |
.needs_update(other_paths)
799 |
Return True if self is missing or is older than any other path. 800 | 'other_paths' can be a (FS)Path, a string path, or a list/tuple 801 | of these. Recurses through subdirectories but compares only files.
802 |
.read_file(mode="r")
803 |
Return the file's content as a str string. This encapsulates the 804 | open/read/close. 'mode' is the same as in Python's open() function.
805 |
.rmtree(parents=False)
806 |
Recursively remove this path, no matter whether it's a file or a 807 | directory. Succeed silently if the path doesn't exist. If 'parents' is 808 | true, also try to remove as many empty ancestor directories as possible.
809 |
.write_file(content, mode="w")
810 |
Replace the file's content, creating the file if 811 | necessary. 'mode' is the same as in Python's open() function. 812 | 'content' is a str string. You'll have to encode Unicode strings 813 | before calling this.
814 |
815 |
816 |
817 |
818 |

Tools

819 |

The following functions are in the unipath.tools module.

820 |
821 |

dict2dir

822 |

dict2dir(dir, dic, mode="w") => None

823 |
824 | Create a directory that matches the dict spec. String values are turned 825 | into files named after the key. Dict values are turned into 826 | subdirectories. 'mode' specifies the mode for files. 'dir' can be an 827 | [FS]Path or a string path.
828 |

dump_path(path, prefix="", tab=" ", file=None) => None

829 |
830 |

Display an ASCII tree of the path. Files are displayed as 831 | "filename (size)". Directories have ":" at the end of the line and 832 | indentation below, like Python syntax blocks. Symbolic links are 833 | shown as "link -> target". 'prefix' is a string prefixed to every 834 | line, normally to controll indentation. 'tab' is the indentation 835 | added for each directory level. 'file' specifies an output file object, 836 | or None for sys.stdout.

837 |

A future version of Unipath will have a command-line program to 838 | dump a path.

839 |
840 |
841 |
842 |
843 |

Acknowledgments

844 |

Jason Orendorff wrote the original path.py. Reinhold Birkenfeld and 845 | Björn Lindkvist modified it for Python PEP 335. Mike Orr changed the API and 846 | released it as Unipath. Ricardo Duarte ported it to Python 3, changed the 847 | tests to py.test, and added Tox support.

848 |
849 |
850 |

Comparision with os/os.path/shutil and path.py

851 |
 852 | p = any path, f =  file, d = directory, l = link
 853 | fsp, fsf, fsd, fsl = filesystem path (i.e., ``Path`` only)
 854 | - = not implemented
 855 | 
856 |

Functions are listed in the same order as the Python Library Reference, version 857 | 2.5. (Does not reflect later changes to Python or path.py.)

858 |
 859 | os/os.path/shutil      path.py        Unipath           Notes
 860 | =================      ============== ==========        =======
 861 | os.path.abspath(p)     p.abspath()    p.absolute()     Return absolute path.
 862 | os.path.basename(p)    p.name         p.name
 863 | os.path.commonprefix(p)  -            -                Common prefix. [1]_
 864 | os.path.dirname(p)     p.parent       p.parent         All except the last component.
 865 | os.path.exists(p)      p.exists()     fsp.exists()     Does the path exist?
 866 | os.path.lexists(p)     p.lexists()    fsp.lexists()    Does the symbolic link exist?
 867 | os.path.expanduser(p)  p.expanduser() p.expand_user()  Expand "~" and "~user" prefix.
 868 | os.path.expandvars(p)  p.expandvars() p.expand_vars()  Expand "$VAR" environment variables.
 869 | os.path.getatime(p)    p.atime        fsp.atime()      Last access time.
 870 | os.path.getmtime(p)    p.mtime        fsp.mtime()      Last modify time.
 871 | os.path.getctime(p)    p.ctime        fsp.ctime()      Platform-specific "ctime".
 872 | os.path.getsize(p)     p.size         fsp.size()       File size.
 873 | os.path.isabs(p)       p.isabs()      p.isabsolute     Is path absolute?
 874 | os.path.isfile(p)      p.isfile()     fsp.isfile()     Is a file?
 875 | os.path.isdir(p)       p.isdir()      fsp.isdir()      Is a directory?
 876 | os.path.islink(p)      p.islink()     fsp.islink()     Is a symbolic link?
 877 | os.path.ismount(p)     p.ismount()    fsp.ismount()    Is a mount point?
 878 | os.path.join(p, "Q/R") p.joinpath("Q/R")  [FS]Path(p, "Q/R")  Join paths.
 879 |                                           -or-
 880 |                                           p.child("Q", "R")
 881 | os.path.normcase(p)    p.normcase()    p.norm_case()   Normalize case.
 882 | os.path.normpath(p)    p.normpath()    p.norm()        Normalize path.
 883 | os.path.realpath(p)    p.realpath()    fsp.real_path() Real path without symbolic links.
 884 | os.path.samefile(p, q) p.samefile(q)   fsp.same_file(q)  True if both paths point to the same filesystem item.
 885 | os.path.sameopenfile(d1, d2)  -          -               [Not a path operation.]
 886 | os.path.samestat(st1, st2)    -          -               [Not a path operation.]
 887 | os.path.split(p)       p.splitpath()   (p.parent, p.name) Split path at basename.
 888 | os.path.splitdrive(p)  p.splitdrive()   -                 [2]_
 889 | os.path.splitext(p)    p.splitext()     -                 [2]_
 890 | os.path.splitunc(p)    p.splitunc()     -                 [2]_
 891 | os.path.walk(p, func, args)  -          -                 [3]_
 892 | 
 893 | os.access(p, const)    p.access(const)  -                 [4]_
 894 | os.chdir(d)            -                fsd.chdir()       Change current directory.
 895 | os.fchdir(fd)          -                -                 [Not a path operation.]
 896 | os.getcwd()           path.getcwd()     FSPath.cwd()      Get current directory.
 897 | os.chroot(d)          d.chroot()        -                 [5]_
 898 | os.chmod(p, 0644)     p.chmod(0644)     fsp.chmod(0644)     Change mode (permission bits).
 899 | os.chown(p, uid, gid) p.chown(uid, gid) fsp.chown(uid, gid) Change ownership.
 900 | os.lchown(p, uid, gid) -                -                 [6]_
 901 | os.link(src, dst)     p.link(dst)       fsp.hardlink(dst)   Make hard link.
 902 | os.listdir(d)         -                 fsd.listdir(names_only=True)  List directory; return base filenames.
 903 | os.lstat(p)           p.lstat()         fsp.lstat()         Like stat but don't follow symbolic link.
 904 | os.mkfifo(p, 0666)    -                 -                 [Not enough of a path operation.]
 905 | os.mknod(p, ...)      -                 -                 [Not enough of a path operation.]
 906 | os.major(device)      -                 -                 [Not a path operation.]
 907 | os.minor(device)      -                 -                 [Not a path operation.]
 908 | os.makedev(...)       -                 -                 [Not a path operation.]
 909 | os.mkdir(d, 0777)     d.mkdir(0777)     fsd.mkdir(mode=0777)     Create directory.
 910 | os.makedirs(d, 0777)  d.makedirs(0777)  fsd.mkdir(True, 0777)    Create a directory and necessary parent directories.
 911 | os.pathconf(p, name)  p.pathconf(name)  -                  Return Posix path attribute.  (What the hell is this?)
 912 | os.readlink(l)        l.readlink()      fsl.read_link()      Return the path a symbolic link points to.
 913 | os.remove(f)          f.remove()        fsf.remove()       Delete file.
 914 | os.removedirs(d)      d.removedirs()    fsd.rmdir(True)    Remove empty directory and all its empty ancestors.
 915 | os.rename(src, dst)   p.rename(dst)     fsp.rename(dst)      Rename a file or directory atomically (must be on same device).
 916 | os.renames(src, dst)  p.renames(dst)    fsp.rename(dst, True) Combines os.rename, os.makedirs, and os.removedirs.
 917 | os.rmdir(d)           d.rmdir()         fsd.rmdir()        Delete empty directory.
 918 | os.stat(p)            p.stat()          fsp.stat()         Return a "stat" object.
 919 | os.statvfs(p)         p.statvfs()       fsp.statvfs()      Return a "statvfs" object.
 920 | os.symlink(src, dst)  p.symlink(dst)    fsp.write_link(link_text)   Create a symbolic link.
 921 |                                         ("write_link" argument order is opposite from Python's!)
 922 | os.tempnam(...)       -                 -                  [7]_
 923 | os.unlink(f)          f.unlink()        -                  Same as .remove().
 924 | os.utime(p, times)    p.utime(times)    fsp.set_times(mtime, atime)  Set access/modification times.
 925 | os.walk(...)          -                 -                  [3]_
 926 | 
 927 | shutil.copyfile(src, dst)  f.copyfile(dst) fsf.copy(dst, ...)  Copy file.  Unipath method is more than copyfile but less than copy2.
 928 | shutil.copyfileobj(...)   -             -                  [Not a path operation.]
 929 | shutil.copymode(src, dst) p.copymode(dst)  fsp.copy_stat(dst, ...)  Copy permission bits only.
 930 | shutil.copystat(src, dst) p.copystat(dst)  fsp.copy_stat(dst, ...)  Copy stat bits.
 931 | shutil.copy(src, dst)  f.copy(dst)      -                  High-level copy a la Unix "cp".
 932 | shutil.copy2(src, dst) f.copy2(dst)     -                  High-level copy a la Unix "cp -p".
 933 | shutil.copytree(...)  d.copytree(...)   fsp.copy_tree(...)   Copy directory tree.  (Not implemented in Unipath 0.1.0.)
 934 | shutil.rmtree(...)    d.rmtree(...)     fsp.rmtree(...)    Recursively delete directory tree.  (Unipath has enhancements.)
 935 | shutil.move(src, dst) p.move(dst)       fsp.move(dst)      Recursively move a file or directory, using os.rename() if possible.
 936 | 
 937 | A + B                 A + B             A+B                Concatenate paths.
 938 | os.path.join(A, B)    A / B             [FS]Path(A, B)     Join paths.
 939 |                                         -or-
 940 |                                         p.child(B)
 941 | -                     p.expand()        p.expand()         Combines expanduser, expandvars, normpath.
 942 | os.path.dirname(p)    p.parent          p.parent           Path without final component.
 943 | os.path.basename(p)   p.name            p.name             Final component only.
 944 | [8]_                  p.namebase        p.stem             Final component without extension.
 945 | [9]_                  p.ext             p.ext              Extension only.
 946 | os.path.splitdrive(p)[0] p.drive        -                  [2]_
 947 | -                     p.stripext()      -                  Strip final extension.
 948 | -                     p.uncshare        -                  [2]_
 949 | -                     p.splitall()      p.components()     List of path components.  (Unipath has special first element.)
 950 | -                     p.relpath()       fsp.relative()       Relative path to current directory.
 951 | -                     p.relpathto(dst)  fsp.rel_path_to(dst) Relative path to 'dst'.
 952 | -                     d.listdir()       fsd.listdir()        List directory, return paths.
 953 | -                     d.files()         fsd.listdir(filter=FILES)  List files in directory, return paths.
 954 | -                     d.dirs()          fsd.listdir(filter=DIRS)   List subdirectories, return paths.
 955 | -                     d.walk(...)       fsd.walk(...)        Recursively yield files and directories.
 956 | -                     d.walkfiles(...)  fsd.walk(filter=FILES)  Recursively yield files.
 957 | -                     d.walkdirs(...)   fsd.walk(filter=DIRS)  Recursively yield directories.
 958 | -                     p.fnmatch(pattern)  -                 True if self.name matches glob pattern.
 959 | -                     p.glob(pattern)   -                   Advanced globbing.
 960 | -                     f.open(mode)      -                   Return open file object.
 961 | -                     f.bytes()         fsf.read_file("rb")   Return file contents in binary mode.
 962 | -                     f.write_bytes()   fsf.write_file(content, "wb")  Replace file contents in binary mode.
 963 | -                     f.text(...)       fsf.read_file()       Return file content.  (Encoding args not implemented yet.)
 964 | -                     f.write_text(...) fsf.write_file(content)  Replace file content.  (Not all Orendorff args supported.)
 965 | -                     f.lines(...)      -                   Return list of lines in file.
 966 | -                     f.write_lines(...)  -                 Write list of lines to file.
 967 | -                     f.read_md5()      -                   Calculate MD5 hash of file.
 968 | -                     p.owner           -                   Advanded "get owner" operation.
 969 | -                     p.readlinkabs()   -                   Return the path this symlink points to, converting to absolute path.
 970 | -                     p.startfile()     -                   What the hell is this?
 971 | 
 972 | -                     -                 p.split_root()      Unified "split root" method.
 973 | -                     -                 p.ancestor(N)       Same as specifying .parent N times.
 974 | -                     -                 p.child(...)        "Safe" way to join paths.
 975 | -                     -                 fsp.needs_update(...) True if self is missing or older than any of the other paths.
 976 | 
977 | 978 | 979 | 980 | 982 | 983 |
[1]The Python method is too dumb; it can end a prefix in the middle of a 981 | [The rest of this footnote has been lost.]
984 | 985 | 986 | 987 | 988 | 989 |
[2]Closest equivalent is p.split_root() for approximate equivalent.
990 | 991 | 992 | 993 | 994 | 995 |
[3]More convenient alternatives exist.
996 | 997 | 998 | 999 | 1000 | 1001 |
[4]Inconvenient constants; not used enough to port.
1002 | 1003 | 1004 | 1005 | 1007 | 1008 |
[5]Chroot is more of an OS operation than a path operation. Plus it's 1006 | dangerous.
1009 | 1010 | 1011 | 1012 | 1014 | 1015 |
[6]Ownership of symbolic link doesn't matter because the OS never 1013 | consults its permission bits.
1016 | 1017 | 1018 | 1019 | 1021 | 1022 |
[7]os.tempnam is insecure; use os.tmpfile or tempfile module 1020 | instead.
1023 | 1024 | 1025 | 1026 | 1027 | 1028 |
[8]os.path.splitext(os.path.split(p))[0]
1029 | 1030 | 1031 | 1032 | 1033 | 1034 |
[9]os.path.splitext(os.path.split(p))[1]
1035 | 1036 | 1037 | 1038 | 1039 | 1040 |
[10]Closest equivalent is p.split_root()[0].
1041 |
1042 |
1043 | 1044 | 1045 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Unipath 2 | %%%%%%% 3 | 4 | An object-oriented approach to file/directory operations 5 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 6 | 7 | :Version: 1.1 8 | :Home page: https://github.com/mikeorr/Unipath 9 | :Docs: https://github.com/mikeorr/Unipath#readme 10 | :Author: Mike Orr 11 | :License: MIT (http://opensource.org/licenses/MIT) 12 | 13 | .. 14 | To format this document as HTML: 15 | rst2html.py README.txt README.html 16 | 17 | **Unipath** is an object-oriented front end to the file/directory functions 18 | scattered throughout several Python library modules. It's based on Jason 19 | Orendorff's *path.py* but focuses more on user convenience rather than on strict 20 | adherence to the underlying functions' syntax. Unipath is stable, well-tested, 21 | and has been used in production since 2008. It runs on Python 2.6+ and 3.2+. 22 | 23 | **Version 1.1** is a bugfix release. It fixes a Unicode incompatibility on 24 | Python 3 under Windows (or operating systems with native unicode filenames). The 25 | license is changed to MIT. It's as permissive as the former Python license but 26 | is smaller and simpler to read. 27 | 28 | Python 3.4 introduced another object-oriented path library, ``pathlib``. It's 29 | available on PyPI as ``pathlib2`` for older versions of Python. (``pathlib`` on 30 | PyPI is a frozen earlier version.) Unipath is now in maintenance mode. The 31 | author is exploring a subclass of pathlib(2) adding some of Unipath's features. 32 | 33 | .. contents:: 34 | 35 | Introduction 36 | ============ 37 | 38 | The ``Path`` class encapsulates the file/directory operations in Python's 39 | ``os``, ``os.path``, and ``shutil`` modules. (Non-filesystem operations are in 40 | the ``AbstractPath`` superclass, but users can ignore this.) 41 | 42 | The API has been streamlined to focus on what the application developer wants 43 | to do rather than on the lowest-level operations; e.g., ``.mkdir()`` succeeds 44 | silently if the directory already exists, and ``.rmtree()`` doesn't barf if the 45 | target is a file or doesn't exist. This allows the developer to write simple 46 | calls that "just work" rather than entire if-stanzas to handle low-level 47 | details s/he doesn't care about. This makes applications more self-documenting 48 | and less cluttered. 49 | 50 | Convenience methods: 51 | 52 | * ``.read_file`` and ``.write_file`` encapsulate the open/read/close pattern. 53 | * ``.needs_update(others)`` tells whether the path needs updating; i.e., 54 | if it doesn't exist or is older than any of the other paths. 55 | * ``.ancestor(N)`` returns the Nth parent directory, useful for joining paths. 56 | * ``.child(\*components)`` is a "safe" version of join. 57 | * ``.split_root()`` handles slash/drive/UNC absolute paths in a uniform way. 58 | 59 | Sample usage for pathname manipulation:: 60 | 61 | >>> from unipath import Path 62 | >>> p = Path("/usr/lib/python2.5/gopherlib.py") 63 | >>> p.parent 64 | Path("/usr/lib/python2.5") 65 | >>> p.name 66 | Path("gopherlib.py") 67 | >>> p.ext 68 | '.py' 69 | >>> p.stem 70 | Path('gopherlib') 71 | >>> q = Path(p.parent, p.stem + p.ext) 72 | >>> q 73 | Path('/usr/lib/python2.5/gopherlib.py') 74 | >>> q == p 75 | True 76 | 77 | Sample usage for filesystem access:: 78 | 79 | >>> import tempfile 80 | >>> from unipath import Path 81 | >>> d = Path(tempfile.mkdtemp()) 82 | >>> d.isdir() 83 | True 84 | >>> p = Path(d, "sample.txt") 85 | >>> p.exists() 86 | False 87 | >>> p.write_file("The king is a fink!") 88 | >>> p.exists() 89 | True 90 | >>> print(p.read_file()) 91 | The king is a fink! 92 | >>> d.rmtree() 93 | >>> p.exists() 94 | False 95 | 96 | Path objects subclass ``str`` (Python 2 ``unicode``), so they can be passed 97 | directly to fuctions expecting a string path. They are also immutable and can 98 | be used as dictionary keys. 99 | 100 | The name "Unipath" is short for "universal path". It was originally intended to 101 | unify the competing path APIs as of PEP 334. When the PEP was rejected, Unipath 102 | added some convenience APIs. The code is implemented in layers, with 103 | filesystem-dependent code in the ``Path`` class and filesystem-independent code 104 | in its ``AbstractPath`` superclass. 105 | 106 | 107 | Installation and testing 108 | ======================== 109 | 110 | Run "pip install Unipath". Or to install the development version, check out 111 | the source from the Git repository above and run "python setup.py develop". 112 | 113 | To test the library, install 'pytest' and run "pytest test.py". It also comes 114 | with a Tox INI file. 115 | 116 | 117 | Path and AbstractPath objects 118 | ============================= 119 | 120 | Constructor 121 | ----------- 122 | ``Path`` (and ``AbstractPath``) objects can be created from a string path, or 123 | from several string arguments which are joined together a la ``os.path.join``. 124 | Each argument can be a string, an ``(Abstract)Path`` instance, an int or long, 125 | or a list/tuple of strings to be joined:: 126 | 127 | p = Path("foo/bar.py") # A relative path 128 | p = Path("foo", "bar.py") # Same as previous 129 | p = Path(["foo", "bar.py"]) # Same as previous 130 | p = Path("/foo", "bar", "baz.py") # An absolute path: /foo/bar/baz.py 131 | p = Path("/foo", Path("bar/baz.py")) # Same as previous 132 | p = Path("/foo", ["", "bar", "baz.py"]) # Embedded Path.components() result 133 | p = Path("record", 123) # Same as Path("record/123") 134 | 135 | p = Path("") # An empty path 136 | p = Path() # Same as Path(os.curdir) 137 | 138 | To get the actual current directory, use ``Path.cwd()``. (This doesn't work 139 | with ``AbstractPath``, of course. 140 | 141 | Adding two paths results in a concatenated path. The other string methods 142 | return strings, so you'll have to wrap them in ``Path`` to make them paths 143 | again. A future version will probably override these methods to return paths. 144 | Multiplying a path returns a string, as if you'd ever want to do that. 145 | 146 | Normalization 147 | ------------- 148 | The new path is normalized to clean up redundant ".." and "." in the 149 | middle, double slashes, wrong-direction slashes, etc. On 150 | case-insensitive filesystems it also converts uppercase to lowercase. 151 | This is all done via ``os.path.normpath()``. Here are some examples 152 | of normalizations:: 153 | 154 | a//b => a/b 155 | a/../b => b 156 | a/./b => a/b 157 | 158 | a/b => a\\b # On NT. 159 | a\\b.JPG => a\\b.jpg # On NT. 160 | 161 | If the actual filesystem path contains symbolic links, normalizing ".." goes to 162 | the parent of the symbolic link rather than to the parent of the linked-to 163 | file. For this reason, and because there may be other cases where normalizing 164 | produces the wrong path, you can disable automatic normalization by setting the 165 | ``.auto_norm`` class attribute to false. I'm not sure whether Unipath should 166 | normalize by default, so if you care one way or the other you should explicitly 167 | set it at the beginning of your application. You can override the auto_norm 168 | setting by passing "norm=True" or "norm=False" as a keyword argument to the 169 | constructor. You can also call ``.norm()`` anytime to manually normalize the 170 | path. 171 | 172 | 173 | Properties 174 | ---------- 175 | Path objects have the following properties: 176 | 177 | .parent 178 | The path without the final component. 179 | .name 180 | The final component only. 181 | .ext 182 | The last part of the final component beginning with a dot (e.g., ".gz"), or 183 | "" if there is no dot. This is also known as the extension. 184 | .stem 185 | The final component without the extension. 186 | 187 | Examples are given in the first sample usage above. 188 | 189 | 190 | Methods 191 | ------- 192 | Path objects have the following methods: 193 | 194 | .ancestor(N) 195 | Same as specifying ``.parent`` N times. 196 | 197 | .child(\*components) 198 | Join paths in a safe manner. The child components may not contain a path 199 | separator or be curdir or pardir ("." or ".." on Posix). This is to 200 | prevent untrusted arguments from creating a path above the original path's 201 | directory. 202 | 203 | .components() 204 | Return a list of directory components as strings. The first component will 205 | be the root ("/" on Posix, a Windows drive root, or a UNC share) if the 206 | path is absolute, or "" if it's relative. Calling ``Path(components)``, 207 | ``Path(*components)``, or ``os.path.join(*components)`` will recreate the 208 | original path. 209 | 210 | .expand() 211 | Same as ``p.expand_user().expand_vars().norm()``. Usually this is all 212 | you need to fix up a path read from a config file. 213 | 214 | .expand_user() 215 | Interpolate "~" and "~user" if the platform allows, and return a new path. 216 | 217 | .expand_vars() 218 | Interpolate environment variables like "$BACKUPS" if the platform allows, 219 | and return a new path. 220 | 221 | .isabsolute() 222 | Is the path absolute? 223 | 224 | .norm() 225 | See Normalization above. Same as ``os.path.normpath``. 226 | 227 | .norm_case() 228 | On case-insensitive platforms (Windows) convert the path to lower case. 229 | On case-sensitive platforms (Unix) leave the path as is. This also turns 230 | forward slashes to backslashes on Windows. 231 | 232 | .split_root() 233 | Split this path at the root and return a tuple of two paths: the root and 234 | the rest of the path. The root is the same as the first subscript of the 235 | ``.components()`` result. Calling ``Path(root, rest)`` or 236 | ``os.path.join(root, rest)`` will produce the original path. 237 | 238 | Examples:: 239 | 240 | Path("foo/bar.py").components() => 241 | [Path(""), Path("foo"), Path("bar.py")] 242 | Path("foo/bar.py").split_root() => 243 | (Path(""), Path("foo/bar.py")) 244 | 245 | Path("/foo/bar.py").components() => 246 | [Path("/"), Path("foo"), Path("bar.py")] 247 | Path("/foo/bar.py").split_root() => 248 | (Path("/"), Path("foo/bar.py")) 249 | 250 | Path("C:\\foo\\bar.py").components() => 251 | ["Path("C:\\"), Path("foo"), Path("bar.py")] 252 | Path("C:\\foo\\bar.py").split_root() => 253 | ("Path("C:\\"), Path("foo\\bar.py")) 254 | 255 | Path("\\\\UNC_SHARE\\foo\\bar.py").components() => 256 | [Path("\\\\UNC_SHARE"), Path("foo"), Path("bar.py")] 257 | Path("\\\\UNC_SHARE\\foo\\bar.py").split_root() => 258 | (Path("\\\\UNC_SHARE"), Path("foo\\bar.py")) 259 | 260 | Path("~/bin").expand_user() => Path("/home/guido/bin") 261 | Path("~timbot/bin").expand_user() => Path("/home/timbot/bin") 262 | Path("$HOME/bin").expand_vars() => Path("/home/guido/bin") 263 | Path("~//$BACKUPS").expand() => Path("/home/guido/Backups") 264 | 265 | Path("dir").child("subdir", "file") => Path("dir/subdir/file") 266 | 267 | Path("/foo").isabsolute() => True 268 | Path("foo").isabsolute() => False 269 | 270 | Note: a Windows drive-relative path like "C:foo" is considered absolute by 271 | ``.components()``, ``.isabsolute()``, and ``.split_root()``, even though 272 | Python's ``ntpath.isabs()`` would return false. 273 | 274 | Path objects only 275 | ================= 276 | 277 | Note on arguments 278 | ----------------- 279 | All arguments that take paths can also take strings. 280 | 281 | Current directory 282 | ----------------- 283 | 284 | Path.cwd() 285 | Return the actual current directory; e.g., Path("/tmp/my_temp_dir"). 286 | This is a class method. 287 | 288 | .chdir() 289 | Make self the current directory. 290 | 291 | Calculating paths 292 | ----------------- 293 | .resolve() 294 | Return the equivalent path without any symbolic links. This normalizes 295 | the path as a side effect. 296 | 297 | .absolute() 298 | Return the absolute equivalent of self. If the path is relative, this 299 | prefixes the current directory; i.e., ``FSPath(FSPath.cwd(), p)``. 300 | 301 | .relative() 302 | Return an equivalent path relative to the current directory if possible. 303 | This may return a path prefixed with many "../..". If the path is on a 304 | different drive, this returns the original path unchanged. 305 | 306 | .rel_path_to(other) 307 | Return a path from self to other. In other words, return a path for 308 | 'other' relative to self. 309 | 310 | Listing directories 311 | ------------------- 312 | 313 | .listdir(pattern=None, filter=ALL, names_only=False) 314 | Return the filenames in this directory. 315 | 316 | 'pattern' may be a glob expression like "\*.py". 317 | 318 | 'filter' may be a function that takes a ``FSPath`` and returns true if it 319 | should be included in the results. The following standard filters are 320 | defined in the ``unipath`` module: 321 | 322 | - ``DIRS``: directories only 323 | - ``FILES``: files only 324 | - ``LINKS``: symbolic links only 325 | - ``FILES_NO_LINKS``: files that aren't symbolic links 326 | - ``DIRS_NO_LINKS``: directories that aren't symbolic links 327 | - ``DEAD_LINKS``: symbolic links that point to nonexistent files 328 | 329 | This method normally returns FSPaths prefixed with 'self'. If 330 | 'names_only' is true, it returns the raw filenames as strings without a 331 | directory prefix (same as ``os.listdir``). 332 | 333 | If both 'pattern' and 'filter' are specified, only paths that pass both are 334 | included. 'filter' must not be specified if 'names_only' is true. 335 | 336 | Paths are returned in sorted order. 337 | 338 | 339 | .walk(pattern=None, filter=None, top_down=True) 340 | 341 | Yield ``FSPath`` objects for all files and directories under self, 342 | recursing subdirectories. Paths are yielded in sorted order. 343 | 344 | 'pattern' and 'filter' are the same as for ``.listdir()``. 345 | 346 | If 'top_down' is true (default), yield directories before yielding 347 | the items in them. If false, yield the items first. 348 | 349 | 350 | File attributes and permissions 351 | ------------------------------- 352 | .atime() 353 | Return the path's last access time. 354 | 355 | .ctime() 356 | Return the path's ctime. On Unix this returns the time the path's 357 | permissions and ownership were last modified. On Windows it's the path 358 | creation time. 359 | 360 | .exists() 361 | Does the path exist? For symbolic links, True if the linked-to file 362 | exists. On some platforms this returns False if Python does not have 363 | permission to stat the file, even if it exists. 364 | 365 | .isdir() 366 | Is the path a directory? Follows symbolic links. 367 | 368 | .isfile() 369 | Is the path a file? Follows symbolic links. 370 | 371 | .islink() 372 | Is the path a symbolic link? 373 | 374 | .ismount() 375 | Is the path a mount point? Returns true if self's parent is on a 376 | different device than self, or if self and its parent are the same 377 | directory. 378 | 379 | .lexists() 380 | Same as ``.exists()`` but don't follow a final symbolic link. 381 | 382 | .lstat() 383 | Same as ``.stat()`` but do not follow a final symbolic link. 384 | 385 | .size() 386 | Return the file size in bytes. 387 | 388 | .stat() 389 | Return a stat object to test file size, type, permissions, etc. 390 | See ``os.stat()`` for details. 391 | 392 | .statvfs() 393 | Return a ``StatVFS`` object. This method exists only if the platform 394 | supports it. See ``os.statvfs()`` for details. 395 | 396 | 397 | Modifying paths 398 | --------------- 399 | 400 | Creating/renaming/removing 401 | ++++++++++++++++++++++++++ 402 | 403 | .chmod(mode) 404 | Change the path's permissions. 'mode' is octal; e.g., 0777. 405 | 406 | .chown(uid, gid) 407 | Change the path's ownership to the numeric uid and gid specified. 408 | Pass -1 if you don't want one of the IDs changed. 409 | 410 | .mkdir(parents=False) 411 | Create the directory, or succeed silently if it already exists. If 412 | 'parents' is true, create any necessary ancestor directories. 413 | 414 | .remove() 415 | Delete the file. Raises OSError if it's a directory. 416 | 417 | .rename(dst, parents=False) 418 | Rename self to 'dst' atomically. See ``os.rename()`` for additional 419 | details. If 'parents' is True, create any intermediate destination 420 | directories necessary, and delete as many empty leaf source directories as 421 | possible. 422 | 423 | .rmdir(parents=False) 424 | Remove the directory, or succeed silently if it's already gone. If 425 | 'parents' is true, also remove as many empty ancestor directories as 426 | possible. 427 | 428 | .set_times(mtime=None, atime=None) 429 | Set the path's modification and access times. If 'mtime' is None, use 430 | the current time. If 'atime' is None or not specified, use the same time 431 | as 'mtime'. To set the times based on another file, see ``.copy_stat()``. 432 | 433 | Symbolic and hard links 434 | +++++++++++++++++++++++ 435 | 436 | .hardlink(src) 437 | Create a hard link at 'src' pointing to self. 438 | 439 | .write_link(target) 440 | Create a symbolic link at self pointing to 'target'. The link will contain 441 | the exact string value of 'target' without checking whether that path exists 442 | or is a even a valid path for the filesystem. 443 | 444 | .make_relative_link_to(dst) 445 | Make a relative symbolic link from self to dst. Same as 446 | ``self.write_link(self.rel_path_to(dst))``. (New in Unipath 0.2.0.) 447 | 448 | .read_link() 449 | Return the path that this symbolic link points to. 450 | 451 | High-level operations 452 | --------------------- 453 | .copy(dst, times=False, perms=False) 454 | Copy the file to a destination. 'times' and 'perms' are same as for 455 | ``.copy_stat()``. 456 | 457 | .copy_stat(dst, times=True, perms=True) 458 | Copy the access/modification times and/or the permission bits from this 459 | path to another path. 460 | 461 | .move(dst) 462 | Recursively move a file or directory to another location. This uses 463 | .rename() if possible. 464 | 465 | .needs_update(other_paths) 466 | Return True if self is missing or is older than any other path. 467 | 'other_paths' can be a ``(FS)Path``, a string path, or a list/tuple 468 | of these. Recurses through subdirectories but compares only files. 469 | 470 | .read_file(mode="r") 471 | Return the file's content as a ``str`` string. This encapsulates the 472 | open/read/close. 'mode' is the same as in Python's ``open()`` function. 473 | 474 | .rmtree(parents=False) 475 | Recursively remove this path, no matter whether it's a file or a 476 | directory. Succeed silently if the path doesn't exist. If 'parents' is 477 | true, also try to remove as many empty ancestor directories as possible. 478 | 479 | .write_file(content, mode="w") 480 | Replace the file's content, creating the file if 481 | necessary. 'mode' is the same as in Python's ``open()`` function. 482 | 'content' is a ``str`` string. You'll have to encode Unicode strings 483 | before calling this. 484 | 485 | Tools 486 | ===== 487 | The following functions are in the ``unipath.tools`` module. 488 | 489 | dict2dir 490 | -------- 491 | dict2dir(dir, dic, mode="w") => None 492 | 493 | Create a directory that matches the dict spec. String values are turned 494 | into files named after the key. Dict values are turned into 495 | subdirectories. 'mode' specifies the mode for files. 'dir' can be an 496 | ``[FS]Path`` or a string path. 497 | 498 | dump_path(path, prefix="", tab=" ", file=None) => None 499 | 500 | Display an ASCII tree of the path. Files are displayed as 501 | "filename (size)". Directories have ":" at the end of the line and 502 | indentation below, like Python syntax blocks. Symbolic links are 503 | shown as "link -> target". 'prefix' is a string prefixed to every 504 | line, normally to controll indentation. 'tab' is the indentation 505 | added for each directory level. 'file' specifies an output file object, 506 | or ``None`` for ``sys.stdout``. 507 | 508 | A future version of Unipath will have a command-line program to 509 | dump a path. 510 | 511 | 512 | Acknowledgments 513 | =============== 514 | 515 | Jason Orendorff wrote the original path.py. Reinhold Birkenfeld and 516 | Björn Lindkvist modified it for Python PEP 335. Mike Orr changed the API and 517 | released it as Unipath. Ricardo Duarte ported it to Python 3, changed the 518 | tests to py.test, and added Tox support. 519 | 520 | Comparision with os/os.path/shutil and path.py 521 | ============================================== 522 | :: 523 | 524 | p = any path, f = file, d = directory, l = link 525 | fsp, fsf, fsd, fsl = filesystem path (i.e., ``Path`` only) 526 | - = not implemented 527 | 528 | Functions are listed in the same order as the Python Library Reference, version 529 | 2.5. (Does not reflect later changes to Python or path.py.) 530 | 531 | :: 532 | 533 | os/os.path/shutil path.py Unipath Notes 534 | ================= ============== ========== ======= 535 | os.path.abspath(p) p.abspath() p.absolute() Return absolute path. 536 | os.path.basename(p) p.name p.name 537 | os.path.commonprefix(p) - - Common prefix. [1]_ 538 | os.path.dirname(p) p.parent p.parent All except the last component. 539 | os.path.exists(p) p.exists() fsp.exists() Does the path exist? 540 | os.path.lexists(p) p.lexists() fsp.lexists() Does the symbolic link exist? 541 | os.path.expanduser(p) p.expanduser() p.expand_user() Expand "~" and "~user" prefix. 542 | os.path.expandvars(p) p.expandvars() p.expand_vars() Expand "$VAR" environment variables. 543 | os.path.getatime(p) p.atime fsp.atime() Last access time. 544 | os.path.getmtime(p) p.mtime fsp.mtime() Last modify time. 545 | os.path.getctime(p) p.ctime fsp.ctime() Platform-specific "ctime". 546 | os.path.getsize(p) p.size fsp.size() File size. 547 | os.path.isabs(p) p.isabs() p.isabsolute Is path absolute? 548 | os.path.isfile(p) p.isfile() fsp.isfile() Is a file? 549 | os.path.isdir(p) p.isdir() fsp.isdir() Is a directory? 550 | os.path.islink(p) p.islink() fsp.islink() Is a symbolic link? 551 | os.path.ismount(p) p.ismount() fsp.ismount() Is a mount point? 552 | os.path.join(p, "Q/R") p.joinpath("Q/R") [FS]Path(p, "Q/R") Join paths. 553 | -or- 554 | p.child("Q", "R") 555 | os.path.normcase(p) p.normcase() p.norm_case() Normalize case. 556 | os.path.normpath(p) p.normpath() p.norm() Normalize path. 557 | os.path.realpath(p) p.realpath() fsp.real_path() Real path without symbolic links. 558 | os.path.samefile(p, q) p.samefile(q) fsp.same_file(q) True if both paths point to the same filesystem item. 559 | os.path.sameopenfile(d1, d2) - - [Not a path operation.] 560 | os.path.samestat(st1, st2) - - [Not a path operation.] 561 | os.path.split(p) p.splitpath() (p.parent, p.name) Split path at basename. 562 | os.path.splitdrive(p) p.splitdrive() - [2]_ 563 | os.path.splitext(p) p.splitext() - [2]_ 564 | os.path.splitunc(p) p.splitunc() - [2]_ 565 | os.path.walk(p, func, args) - - [3]_ 566 | 567 | os.access(p, const) p.access(const) - [4]_ 568 | os.chdir(d) - fsd.chdir() Change current directory. 569 | os.fchdir(fd) - - [Not a path operation.] 570 | os.getcwd() path.getcwd() FSPath.cwd() Get current directory. 571 | os.chroot(d) d.chroot() - [5]_ 572 | os.chmod(p, 0644) p.chmod(0644) fsp.chmod(0644) Change mode (permission bits). 573 | os.chown(p, uid, gid) p.chown(uid, gid) fsp.chown(uid, gid) Change ownership. 574 | os.lchown(p, uid, gid) - - [6]_ 575 | os.link(src, dst) p.link(dst) fsp.hardlink(dst) Make hard link. 576 | os.listdir(d) - fsd.listdir(names_only=True) List directory; return base filenames. 577 | os.lstat(p) p.lstat() fsp.lstat() Like stat but don't follow symbolic link. 578 | os.mkfifo(p, 0666) - - [Not enough of a path operation.] 579 | os.mknod(p, ...) - - [Not enough of a path operation.] 580 | os.major(device) - - [Not a path operation.] 581 | os.minor(device) - - [Not a path operation.] 582 | os.makedev(...) - - [Not a path operation.] 583 | os.mkdir(d, 0777) d.mkdir(0777) fsd.mkdir(mode=0777) Create directory. 584 | os.makedirs(d, 0777) d.makedirs(0777) fsd.mkdir(True, 0777) Create a directory and necessary parent directories. 585 | os.pathconf(p, name) p.pathconf(name) - Return Posix path attribute. (What the hell is this?) 586 | os.readlink(l) l.readlink() fsl.read_link() Return the path a symbolic link points to. 587 | os.remove(f) f.remove() fsf.remove() Delete file. 588 | os.removedirs(d) d.removedirs() fsd.rmdir(True) Remove empty directory and all its empty ancestors. 589 | os.rename(src, dst) p.rename(dst) fsp.rename(dst) Rename a file or directory atomically (must be on same device). 590 | os.renames(src, dst) p.renames(dst) fsp.rename(dst, True) Combines os.rename, os.makedirs, and os.removedirs. 591 | os.rmdir(d) d.rmdir() fsd.rmdir() Delete empty directory. 592 | os.stat(p) p.stat() fsp.stat() Return a "stat" object. 593 | os.statvfs(p) p.statvfs() fsp.statvfs() Return a "statvfs" object. 594 | os.symlink(src, dst) p.symlink(dst) fsp.write_link(link_text) Create a symbolic link. 595 | ("write_link" argument order is opposite from Python's!) 596 | os.tempnam(...) - - [7]_ 597 | os.unlink(f) f.unlink() - Same as .remove(). 598 | os.utime(p, times) p.utime(times) fsp.set_times(mtime, atime) Set access/modification times. 599 | os.walk(...) - - [3]_ 600 | 601 | shutil.copyfile(src, dst) f.copyfile(dst) fsf.copy(dst, ...) Copy file. Unipath method is more than copyfile but less than copy2. 602 | shutil.copyfileobj(...) - - [Not a path operation.] 603 | shutil.copymode(src, dst) p.copymode(dst) fsp.copy_stat(dst, ...) Copy permission bits only. 604 | shutil.copystat(src, dst) p.copystat(dst) fsp.copy_stat(dst, ...) Copy stat bits. 605 | shutil.copy(src, dst) f.copy(dst) - High-level copy a la Unix "cp". 606 | shutil.copy2(src, dst) f.copy2(dst) - High-level copy a la Unix "cp -p". 607 | shutil.copytree(...) d.copytree(...) fsp.copy_tree(...) Copy directory tree. (Not implemented in Unipath 0.1.0.) 608 | shutil.rmtree(...) d.rmtree(...) fsp.rmtree(...) Recursively delete directory tree. (Unipath has enhancements.) 609 | shutil.move(src, dst) p.move(dst) fsp.move(dst) Recursively move a file or directory, using os.rename() if possible. 610 | 611 | A + B A + B A+B Concatenate paths. 612 | os.path.join(A, B) A / B [FS]Path(A, B) Join paths. 613 | -or- 614 | p.child(B) 615 | - p.expand() p.expand() Combines expanduser, expandvars, normpath. 616 | os.path.dirname(p) p.parent p.parent Path without final component. 617 | os.path.basename(p) p.name p.name Final component only. 618 | [8]_ p.namebase p.stem Final component without extension. 619 | [9]_ p.ext p.ext Extension only. 620 | os.path.splitdrive(p)[0] p.drive - [2]_ 621 | - p.stripext() - Strip final extension. 622 | - p.uncshare - [2]_ 623 | - p.splitall() p.components() List of path components. (Unipath has special first element.) 624 | - p.relpath() fsp.relative() Relative path to current directory. 625 | - p.relpathto(dst) fsp.rel_path_to(dst) Relative path to 'dst'. 626 | - d.listdir() fsd.listdir() List directory, return paths. 627 | - d.files() fsd.listdir(filter=FILES) List files in directory, return paths. 628 | - d.dirs() fsd.listdir(filter=DIRS) List subdirectories, return paths. 629 | - d.walk(...) fsd.walk(...) Recursively yield files and directories. 630 | - d.walkfiles(...) fsd.walk(filter=FILES) Recursively yield files. 631 | - d.walkdirs(...) fsd.walk(filter=DIRS) Recursively yield directories. 632 | - p.fnmatch(pattern) - True if self.name matches glob pattern. 633 | - p.glob(pattern) - Advanced globbing. 634 | - f.open(mode) - Return open file object. 635 | - f.bytes() fsf.read_file("rb") Return file contents in binary mode. 636 | - f.write_bytes() fsf.write_file(content, "wb") Replace file contents in binary mode. 637 | - f.text(...) fsf.read_file() Return file content. (Encoding args not implemented yet.) 638 | - f.write_text(...) fsf.write_file(content) Replace file content. (Not all Orendorff args supported.) 639 | - f.lines(...) - Return list of lines in file. 640 | - f.write_lines(...) - Write list of lines to file. 641 | - f.read_md5() - Calculate MD5 hash of file. 642 | - p.owner - Advanded "get owner" operation. 643 | - p.readlinkabs() - Return the path this symlink points to, converting to absolute path. 644 | - p.startfile() - What the hell is this? 645 | 646 | - - p.split_root() Unified "split root" method. 647 | - - p.ancestor(N) Same as specifying .parent N times. 648 | - - p.child(...) "Safe" way to join paths. 649 | - - fsp.needs_update(...) True if self is missing or older than any of the other paths. 650 | 651 | 652 | .. [1] The Python method is too dumb; it can end a prefix in the middle of a 653 | [The rest of this footnote has been lost.] 654 | .. [2] Closest equivalent is ``p.split_root()`` for approximate equivalent. 655 | .. [3] More convenient alternatives exist. 656 | .. [4] Inconvenient constants; not used enough to port. 657 | .. [5] Chroot is more of an OS operation than a path operation. Plus it's 658 | dangerous. 659 | .. [6] Ownership of symbolic link doesn't matter because the OS never 660 | consults its permission bits. 661 | .. [7] ``os.tempnam`` is insecure; use ``os.tmpfile`` or ``tempfile`` module 662 | instead. 663 | .. [8] ``os.path.splitext(os.path.split(p))[0]`` 664 | .. [9] ``os.path.splitext(os.path.split(p))[1]`` 665 | .. [10] Closest equivalent is ``p.split_root()[0]``. 666 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | VERSION = "1.1" 4 | 5 | DESCRIPTION = """\ 6 | Unipath is an object-oriented front end to the file/directory functions 7 | scattered throughout several Python library modules. It's based on Jason 8 | Orendorff's *path.py* but has a friendlier API and higher-level features. 9 | Unipath is stable, well-tested, and has been used in production since 2008. 10 | It runs on Python 2.6+ and 3.3+. 11 | 12 | **Version 1.1** is a bugfix release. Most notably it fixes a Unicode 13 | incompatibility on Python 3 under Windows (or operating systems with native 14 | Unicpde filenames). The license is changed to MIT (from the Python license). 15 | """ 16 | 17 | setup( 18 | name="Unipath", 19 | version=VERSION, 20 | description="Object-oriented alternative to os/os.path/shutil", 21 | long_description=DESCRIPTION, 22 | author="Mike Orr", 23 | author_email="sluggoster@gmail.com", 24 | url="https://github.com/mikeorr/Unipath", 25 | packages=["unipath"], 26 | license="MIT", 27 | #platform="Multiplatform", 28 | keywords="os.path filename pathspec path files directories filesystem", 29 | classifiers=[ 30 | "License :: OSI Approved :: MIT License", 31 | "Operating System :: OS Independent", 32 | "Topic :: Software Development :: Libraries :: Python Modules", 33 | "Topic :: Utilities", 34 | "Programming Language :: Python", 35 | "Programming Language :: Python :: 2", 36 | "Programming Language :: Python :: 2.6", 37 | "Programming Language :: Python :: 2.7", 38 | "Programming Language :: Python :: 3", 39 | "Programming Language :: Python :: 3.2", 40 | "Programming Language :: Python :: 3.3", 41 | "Programming Language :: Python :: 3.4", 42 | ], 43 | ) 44 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Unit tests for unipath.py 3 | 4 | Environment variables: 5 | DUMP : List the contents of test direcories after each test. 6 | NO_CLEANUP : Don't delete test directories. 7 | (These are not command-line args due to the difficulty of merging my args 8 | with unittest's.) 9 | 10 | IMPORTANT: Tests may not assume what the current directory is because the tests 11 | may have been started from anywhere, and some tests chdir to the temprorary 12 | test directory which is then deleted. 13 | """ 14 | from __future__ import print_function 15 | 16 | import ntpath 17 | import os 18 | import posixpath 19 | import re 20 | import tempfile 21 | import time 22 | import sys 23 | 24 | import pytest 25 | 26 | from unipath import * 27 | from unipath.errors import * 28 | from unipath.tools import dict2dir, dump_path 29 | 30 | AbstractPath.auto_norm = False 31 | 32 | class PosixPath(AbstractPath): 33 | pathlib = posixpath 34 | 35 | 36 | class NTPath(AbstractPath): 37 | pathlib = ntpath 38 | 39 | # Global flags 40 | cleanup = not bool(os.environ.get("NO_CLEANUP")) 41 | dump = bool(os.environ.get("DUMP")) 42 | 43 | 44 | class TestPathConstructor(object): 45 | def test_posix(self): 46 | assert str(PosixPath()) == posixpath.curdir 47 | assert str(PosixPath("foo/bar.py")) == "foo/bar.py" 48 | assert str(PosixPath("foo", "bar.py")) == "foo/bar.py" 49 | assert str(PosixPath("foo", "bar", "baz.py")) == "foo/bar/baz.py" 50 | assert str(PosixPath("foo", PosixPath("bar", "baz.py"))) == "foo/bar/baz.py" 51 | assert str(PosixPath("foo", ["", "bar", "baz.py"])) == "foo/bar/baz.py" 52 | assert str(PosixPath("")) == "" 53 | assert str(PosixPath()) == "." 54 | assert str(PosixPath("foo", 1, "bar")) == "foo/1/bar" 55 | 56 | def test_nt(self): 57 | assert str(NTPath()) == ntpath.curdir 58 | assert str(NTPath(r"foo\bar.py")) == r"foo\bar.py" 59 | assert str(NTPath("foo", "bar.py")), r"foo\bar.py" 60 | assert str(NTPath("foo", "bar", "baz.py")) == r"foo\bar\baz.py" 61 | assert str(NTPath("foo", NTPath("bar", "baz.py"))) == r"foo\bar\baz.py" 62 | assert str(NTPath("foo", ["", "bar", "baz.py"])) == r"foo\bar\baz.py" 63 | assert str(PosixPath("")) == "" 64 | assert str(NTPath()) == "." 65 | assert str(NTPath("foo", 1, "bar")) == r"foo\1\bar" 66 | 67 | def test_int_arg(self): 68 | assert str(PosixPath("a", 1)) == "a/1" 69 | 70 | 71 | class TestNorm(object): 72 | def test_posix(self): 73 | assert PosixPath("a//b/../c").norm() == "a/c" 74 | assert PosixPath("a/./b").norm() == "a/b" 75 | assert PosixPath("a/./b", norm=True) == "a/b" 76 | assert PosixPath("a/./b", norm=False) == "a/./b" 77 | 78 | class AutoNormPath(PosixPath): 79 | auto_norm = True 80 | assert AutoNormPath("a/./b") == "a/b" 81 | assert AutoNormPath("a/./b", norm=True) == "a/b" 82 | assert AutoNormPath("a/./b", norm=False) == "a/./b" 83 | 84 | def test_nt(self): 85 | assert NTPath(r"a\\b\..\c").norm() == r"a\c" 86 | assert NTPath(r"a\.\b").norm() == r"a\b" 87 | assert NTPath("a\\.\\b", norm=True) == "a\\b" 88 | assert NTPath("a\\.\\b", norm=False) == "a\\.\\b" 89 | 90 | class AutoNormPath(NTPath): 91 | auto_norm = True 92 | assert AutoNormPath("a\\.\\b") == "a\\b" 93 | assert AutoNormPath("a\\.\\b", norm=True) == "a\\b" 94 | assert AutoNormPath("a\\.\\b", norm=False) == "a\\.\\b" 95 | 96 | 97 | class TestAbstractPath(object): 98 | def test_repr_native(self): 99 | # Don't bother testing whether 'u' prefix appears. 100 | rx = re.compile( r"^Path\(u?'la_la_la'\)$" ) 101 | s = repr(Path("la_la_la")) 102 | assert rx.match(s) 103 | 104 | def test_repr_nt(self): 105 | # Don't bother testing whether 'u' prefix appears. 106 | rx = re.compile( r"^NTPath\(u?'la_la_la'\)$" ) 107 | s = repr(NTPath("la_la_la")) 108 | assert rx.match(s) 109 | 110 | # Not testing expand_user, expand_vars, or expand: too dependent on the 111 | # OS environment. 112 | 113 | def test_properties(self): 114 | p = PosixPath("/first/second/third.jpg") 115 | assert p.parent == "/first/second" 116 | assert p.name == "third.jpg" 117 | assert p.ext == ".jpg" 118 | assert p.stem == "third" 119 | 120 | def test_properties2(self): 121 | # Usage sample in README is based on this. 122 | p = PosixPath("/usr/lib/python2.5/gopherlib.py") 123 | assert p.parent == Path("/usr/lib/python2.5") 124 | assert p.name == Path("gopherlib.py") 125 | assert p.ext == ".py" 126 | assert p.stem == Path("gopherlib") 127 | q = PosixPath(p.parent, p.stem + p.ext) 128 | assert q == p 129 | 130 | def test_split_root(self): 131 | assert PosixPath("foo/bar.py").split_root() == ("", "foo/bar.py") 132 | assert PosixPath("/foo/bar.py").split_root() == ("/", "foo/bar.py") 133 | assert NTPath("foo\\bar.py").split_root() == ("", "foo\\bar.py") 134 | assert NTPath("\\foo\\bar.py").split_root() == ("\\", "foo\\bar.py") 135 | assert NTPath("C:\\foo\\bar.py").split_root() == ("C:\\", "foo\\bar.py") 136 | assert NTPath("C:foo\\bar.py").split_root() == ("C:", "foo\\bar.py") 137 | assert NTPath("\\\\share\\base\\foo\\bar.py").split_root() == ("\\\\share\\base\\", "foo\\bar.py") 138 | 139 | def test_split_root_vs_isabsolute(self): 140 | assert not PosixPath("a/b/c").isabsolute() 141 | assert not PosixPath("a/b/c").split_root()[0] 142 | assert PosixPath("/a/b/c").isabsolute() 143 | assert PosixPath("/a/b/c").split_root()[0] 144 | assert not NTPath("a\\b\\c").isabsolute() 145 | assert not NTPath("a\\b\\c").split_root()[0] 146 | assert NTPath("\\a\\b\\c").isabsolute() 147 | assert NTPath("\\a\\b\\c").split_root()[0] 148 | assert NTPath("C:\\a\\b\\c").isabsolute() 149 | assert NTPath("C:\\a\\b\\c").split_root()[0] 150 | assert NTPath("C:a\\b\\c").isabsolute() 151 | assert NTPath("C:a\\b\\c").split_root()[0] 152 | assert NTPath("\\\\share\\b\\c").isabsolute() 153 | assert NTPath("\\\\share\\b\\c").split_root()[0] 154 | 155 | def test_components(self): 156 | P = PosixPath 157 | assert P("a").components() == [P(""), P("a")] 158 | assert P("a/b/c").components() == [P(""), P("a"), P("b"), P("c")] 159 | assert P("/a/b/c").components() == [P("/"), P("a"), P("b"), P("c")] 160 | P = NTPath 161 | assert P("a\\b\\c").components() == [P(""), P("a"), P("b"), P("c")] 162 | assert P("\\a\\b\\c").components() == [P("\\"), P("a"), P("b"), P("c")] 163 | assert P("C:\\a\\b\\c").components() == [P("C:\\"), P("a"), P("b"), P("c")] 164 | assert P("C:a\\b\\c").components() == [P("C:"), P("a"), P("b"), P("c")] 165 | assert P("\\\\share\\b\\c").components() == [P("\\\\share\\b\\"), P("c")] 166 | 167 | def test_child(self): 168 | PosixPath("foo/bar").child("baz") 169 | with pytest.raises(UnsafePathError): 170 | PosixPath("foo/bar").child("baz/fred") 171 | PosixPath("foo/bar").child("..", "baz") 172 | PosixPath("foo/bar").child(".", "baz") 173 | 174 | 175 | class TestStringMethods(object): 176 | def test_add(self): 177 | P = PosixPath 178 | assert P("a") + P("b") == P("ab") 179 | assert P("a") + "b" == P("ab") 180 | assert "a" + P("b") == P("ab") 181 | 182 | 183 | class FilesystemTest(object): 184 | TEST_HIERARCHY = { 185 | "a_file": "Nothing important.", 186 | "animals": { 187 | "elephant": "large", 188 | "gonzo": "unique", 189 | "mouse": "small"}, 190 | "images": { 191 | "image1.gif": "", 192 | "image2.jpg": "", 193 | "image3.png": ""}, 194 | "swedish": { 195 | "chef": { 196 | "bork": { 197 | "bork": "bork!"}}}, 198 | } 199 | 200 | def setup_method(self, method): 201 | d = tempfile.mkdtemp() 202 | d = os.path.realpath(d) # MacOSX temp dir contains symlink. 203 | d = Path(d) 204 | self.d = d 205 | dict2dir(d, self.TEST_HIERARCHY) 206 | self.a_file = Path(d, "a_file") 207 | self.animals = Path(d, "animals") 208 | self.images = Path(d, "images") 209 | self.chef = Path(d, "swedish", "chef", "bork", "bork") 210 | if hasattr(self.d, "write_link"): 211 | self.link_to_chef_file = Path(d, "link_to_chef_file") 212 | self.link_to_chef_file.write_link(self.chef) 213 | self.link_to_images_dir = Path(d, "link_to_images_dir") 214 | self.link_to_images_dir.write_link(self.images) 215 | self.dead_link = self.d.child("dead_link") 216 | self.dead_link.write_link("nowhere") 217 | self.missing = Path(d, "MISSING") 218 | self.d.chdir() 219 | 220 | def teardown_method(self, method): 221 | d = self.d 222 | d.parent.chdir() # Always need a valid curdir to avoid OSErrors. 223 | if dump: 224 | dump_path(d) 225 | if cleanup: 226 | d.rmtree() 227 | if d.exists(): 228 | raise AssertionError("unable to delete temp dir %s" % d) 229 | else: 230 | print("Not deleting test directory", d) 231 | 232 | 233 | class TestCalculatingPaths(FilesystemTest): 234 | def test_inheritance(self): 235 | assert Path.cwd().name # Can we access the property? 236 | 237 | def test_cwd(self): 238 | assert str(Path.cwd()) == os.getcwd() 239 | 240 | def test_chdir_absolute_relative(self): 241 | save_dir = Path.cwd() 242 | self.d.chdir() 243 | assert Path.cwd() == self.d 244 | assert Path("swedish").absolute() == Path(self.d, "swedish") 245 | save_dir.chdir() 246 | assert Path.cwd() == save_dir 247 | 248 | def test_chef(self): 249 | p = Path(self.d, "swedish", "chef", "bork", "bork") 250 | assert p.read_file() == "bork!" 251 | 252 | def test_absolute(self): 253 | p1 = Path("images").absolute() 254 | p2 = self.d.child("images") 255 | assert p1 == p2 256 | 257 | def test_relative(self): 258 | p = self.d.child("images").relative() 259 | assert p == "images" 260 | 261 | def test_resolve(self): 262 | p1 = Path(self.link_to_images_dir, "image3.png") 263 | p2 = p1.resolve() 264 | assert p1.components()[-2:] == ["link_to_images_dir", "image3.png"] 265 | assert p2.components()[-2:] == ["images", "image3.png"] 266 | assert p1.exists() 267 | assert p2.exists() 268 | assert p1.same_file(p2) 269 | assert p2.same_file(p1) 270 | 271 | 272 | class TestRelPathTo(FilesystemTest): 273 | def test1(self): 274 | p1 = Path("animals", "elephant") 275 | p2 = Path("animals", "mouse") 276 | assert p1.rel_path_to(p2) == Path("mouse") 277 | 278 | def test2(self): 279 | p1 = Path("animals", "elephant") 280 | p2 = Path("images", "image1.gif") 281 | assert p1.rel_path_to(p2) == Path(os.path.pardir, "images", "image1.gif") 282 | 283 | def test3(self): 284 | p1 = Path("animals", "elephant") 285 | assert p1.rel_path_to(self.d) == Path(os.path.pardir) 286 | 287 | def test4(self): 288 | p1 = Path("swedish", "chef") 289 | assert p1.rel_path_to(self.d) == Path(os.path.pardir, os.path.pardir) 290 | 291 | 292 | class TestListingDirectories(FilesystemTest): 293 | def test_listdir_names_only(self): 294 | result = self.images.listdir(names_only=True) 295 | control = ["image1.gif", "image2.jpg", "image3.png"] 296 | assert result == control 297 | 298 | def test_listdir_arg_errors(self): 299 | with pytest.raises(TypeError): 300 | self.d.listdir(filter=FILES, names_only=True) 301 | 302 | def test_listdir(self): 303 | result = Path("images").listdir() 304 | control = [ 305 | Path("images", "image1.gif"), 306 | Path("images", "image2.jpg"), 307 | Path("images", "image3.png")] 308 | assert result == control 309 | 310 | def test_listdir_all(self): 311 | result = Path("").listdir() 312 | control = [ 313 | "a_file", 314 | "animals", 315 | "dead_link", 316 | "images", 317 | "link_to_chef_file", 318 | "link_to_images_dir", 319 | "swedish", 320 | ] 321 | assert result == control 322 | 323 | def test_listdir_files(self): 324 | result = Path("").listdir(filter=FILES) 325 | control = [ 326 | "a_file", 327 | "link_to_chef_file", 328 | ] 329 | assert result == control 330 | 331 | def test_listdir_dirs(self): 332 | result = Path("").listdir(filter=DIRS) 333 | control = [ 334 | "animals", 335 | "images", 336 | "link_to_images_dir", 337 | "swedish", 338 | ] 339 | assert result == control 340 | 341 | @pytest.mark.skipif("not hasattr(os, 'symlink')") 342 | def test_listdir_links(self): 343 | result = Path("").listdir(filter=LINKS) 344 | control = [ 345 | "dead_link", 346 | "link_to_chef_file", 347 | "link_to_images_dir", 348 | ] 349 | assert result == control 350 | 351 | def test_listdir_files_no_links(self): 352 | result = Path("").listdir(filter=FILES_NO_LINKS) 353 | control = [ 354 | "a_file", 355 | ] 356 | assert result == control 357 | 358 | def test_listdir_dirs_no_links(self): 359 | result = Path("").listdir(filter=DIRS_NO_LINKS) 360 | control = [ 361 | "animals", 362 | "images", 363 | "swedish", 364 | ] 365 | assert result == control 366 | 367 | def test_listdir_dead_links(self): 368 | result = Path("").listdir(filter=DEAD_LINKS) 369 | control = [ 370 | "dead_link", 371 | ] 372 | assert result == control 373 | 374 | def test_listdir_pattern_names_only(self): 375 | result = self.images.name.listdir("*.jpg", names_only=True) 376 | control = ["image2.jpg"] 377 | assert result == control 378 | 379 | def test_listdir_pattern(self): 380 | result = self.images.name.listdir("*.jpg") 381 | control = [Path("images", "image2.jpg")] 382 | assert result == control 383 | 384 | def test_walk(self): 385 | result = list(self.d.walk()) 386 | control = [ 387 | Path(self.a_file), 388 | Path(self.animals), 389 | Path(self.animals, "elephant"), 390 | Path(self.animals, "gonzo"), 391 | Path(self.animals, "mouse"), 392 | ] 393 | result = result[:len(control)] 394 | assert result == control 395 | 396 | def test_walk_bottom_up(self): 397 | result = list(self.d.walk(top_down=False)) 398 | control = [ 399 | Path(self.a_file), 400 | Path(self.animals, "elephant"), 401 | Path(self.animals, "gonzo"), 402 | Path(self.animals, "mouse"), 403 | Path(self.animals), 404 | ] 405 | result = result[:len(control)] 406 | assert result == control 407 | 408 | def test_walk_files(self): 409 | result = list(self.d.walk(filter=FILES)) 410 | control = [ 411 | Path(self.a_file), 412 | Path(self.animals, "elephant"), 413 | Path(self.animals, "gonzo"), 414 | Path(self.animals, "mouse"), 415 | Path(self.images, "image1.gif"), 416 | ] 417 | result = result[:len(control)] 418 | assert result == control 419 | 420 | def test_walk_dirs(self): 421 | result = list(self.d.walk(filter=DIRS)) 422 | control = [ 423 | Path(self.animals), 424 | Path(self.images), 425 | Path(self.link_to_images_dir), 426 | Path(self.d, "swedish"), 427 | ] 428 | result = result[:len(control)] 429 | assert result == control 430 | 431 | def test_walk_links(self): 432 | result = list(self.d.walk(filter=LINKS)) 433 | control = [ 434 | Path(self.dead_link), 435 | Path(self.link_to_chef_file), 436 | Path(self.link_to_images_dir), 437 | ] 438 | result = result[:len(control)] 439 | assert result == control 440 | 441 | 442 | class TestStatAttributes(FilesystemTest): 443 | def test_exists(self): 444 | assert self.a_file.exists() 445 | assert self.images.exists() 446 | assert self.link_to_chef_file.exists() 447 | assert self.link_to_images_dir.exists() 448 | assert not self.dead_link.exists() 449 | assert not self.missing.exists() 450 | 451 | def test_lexists(self): 452 | assert self.a_file.lexists() 453 | assert self.images.lexists() 454 | assert self.link_to_chef_file.lexists() 455 | assert self.link_to_images_dir.lexists() 456 | assert self.dead_link.lexists() 457 | assert not self.missing.lexists() 458 | 459 | def test_isfile(self): 460 | assert self.a_file.isfile() 461 | assert not self.images.isfile() 462 | assert self.link_to_chef_file.isfile() 463 | assert not self.link_to_images_dir.isfile() 464 | assert not self.dead_link.isfile() 465 | assert not self.missing.isfile() 466 | 467 | def test_isdir(self): 468 | assert not self.a_file.isdir() 469 | assert self.images.isdir() 470 | assert not self.link_to_chef_file.isdir() 471 | assert self.link_to_images_dir.isdir() 472 | assert not self.dead_link.isdir() 473 | assert not self.missing.isdir() 474 | 475 | def test_islink(self): 476 | assert not self.a_file.islink() 477 | assert not self.images.islink() 478 | assert self.link_to_chef_file.islink() 479 | assert self.link_to_images_dir.islink() 480 | assert self.dead_link.islink() 481 | assert not self.missing.islink() 482 | 483 | def test_ismount(self): 484 | # Can't test on a real mount point because we don't know where it is 485 | assert not self.a_file.ismount() 486 | assert not self.images.ismount() 487 | assert not self.link_to_chef_file.ismount() 488 | assert not self.link_to_images_dir.ismount() 489 | assert not self.dead_link.ismount() 490 | assert not self.missing.ismount() 491 | 492 | def test_times(self): 493 | self.set_times() 494 | assert self.a_file.mtime() == 50000 495 | assert self.a_file.atime() == 60000 496 | # Can't set ctime to constant, so just make sure it returns a positive number. 497 | assert self.a_file.ctime() > 0 498 | 499 | def test_size(self): 500 | assert self.chef.size() == 5 501 | 502 | def test_same_file(self): 503 | if hasattr(self.a_file, "same_file"): 504 | control = Path(self.d, "a_file") 505 | assert self.a_file.same_file(control) 506 | assert not self.a_file.same_file(self.chef) 507 | 508 | def test_stat(self): 509 | st = self.chef.stat() 510 | assert hasattr(st, "st_mode") 511 | 512 | def test_statvfs(self): 513 | if hasattr(self.images, "statvfs"): 514 | stv = self.images.statvfs() 515 | assert hasattr(stv, "f_files") 516 | 517 | def test_chmod(self): 518 | self.a_file.chmod(0o600) 519 | newmode = self.a_file.stat().st_mode 520 | assert newmode & 0o777 == 0o600 521 | 522 | # Can't test chown: requires root privilege and knowledge of local users. 523 | 524 | def set_times(self): 525 | self.a_file.set_times() 526 | self.a_file.set_times(50000) 527 | self.a_file.set_times(50000, 60000) 528 | 529 | 530 | class TestCreateRenameRemove(FilesystemTest): 531 | def test_mkdir_and_rmdir(self): 532 | self.missing.mkdir() 533 | assert self.missing.isdir() 534 | self.missing.rmdir() 535 | assert not self.missing.exists() 536 | 537 | def test_mkdir_and_rmdir_with_parents(self): 538 | abc = Path(self.d, "a", "b", "c") 539 | abc.mkdir(parents=True) 540 | assert abc.isdir() 541 | abc.rmdir(parents=True) 542 | assert not Path(self.d, "a").exists() 543 | 544 | def test_remove(self): 545 | self.a_file.remove() 546 | assert not self.a_file.exists() 547 | self.missing.remove() # Removing a nonexistent file should succeed. 548 | 549 | if hasattr(os, 'symlink'): 550 | @pytest.mark.skipif("not hasattr(os, 'symlink')") 551 | def test_remove_broken_symlink(self): 552 | symlink = Path(self.d, "symlink") 553 | symlink.write_link("broken") 554 | assert symlink.lexists() 555 | symlink.remove() 556 | assert not symlink.lexists() 557 | 558 | @pytest.mark.skipif("not hasattr(os, 'symlink')") 559 | def test_rmtree_broken_symlink(self): 560 | symlink = Path(self.d, "symlink") 561 | symlink.write_link("broken") 562 | assert symlink.lexists() 563 | symlink.rmtree() 564 | assert not symlink.lexists() 565 | 566 | def test_rename(self): 567 | a_file = self.a_file 568 | b_file = Path(a_file.parent, "b_file") 569 | a_file.rename(b_file) 570 | assert not a_file.exists() 571 | assert b_file.exists() 572 | 573 | def test_rename_with_parents(self): 574 | pass # @@MO: Write later. 575 | 576 | 577 | class TestLinks(FilesystemTest): 578 | # @@MO: Write test_hardlink, test_symlink, test_write_link later. 579 | 580 | def test_read_link(self): 581 | assert self.dead_link.read_link() == "nowhere" 582 | 583 | class TestHighLevel(FilesystemTest): 584 | def test_copy(self): 585 | a_file = self.a_file 586 | b_file = Path(a_file.parent, "b_file") 587 | a_file.copy(b_file) 588 | assert b_file.exists() 589 | a_file.copy_stat(b_file) 590 | 591 | def test_copy_tree(self): 592 | return # .copy_tree() not implemented. 593 | images = self.images 594 | images2 = Path(self.images.parent, "images2") 595 | images.copy_tree(images2) 596 | 597 | def test_move(self): 598 | a_file = self.a_file 599 | b_file = Path(a_file.parent, "b_file") 600 | a_file.move(b_file) 601 | assert not a_file.exists() 602 | assert b_file.exists() 603 | 604 | def test_needs_update(self): 605 | control_files = self.images.listdir() 606 | self.a_file.set_times() 607 | assert not self.a_file.needs_update(control_files) 608 | time.sleep(1) 609 | control = Path(self.images, "image2.jpg") 610 | control.set_times() 611 | result = self.a_file.needs_update(self.images.listdir()) 612 | assert self.a_file.needs_update(control_files) 613 | 614 | def test_needs_update_scalar(self): 615 | control_files = self.images.listdir() 616 | self.a_file.set_times() 617 | assert not self.a_file.needs_update(control_files[0]) 618 | 619 | def test_read_file(self): 620 | assert self.chef.read_file() == "bork!" 621 | 622 | # .write_file and .rmtree tested in .setUp. 623 | 624 | 625 | 626 | 627 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34 3 | 4 | [testenv] 5 | deps=pytest 6 | commands = 7 | py.test test.py \ 8 | [] 9 | -------------------------------------------------------------------------------- /unipath/__init__.py: -------------------------------------------------------------------------------- 1 | """unipath.py - A two-class approach to file/directory operations in Python. 2 | """ 3 | 4 | from unipath.abstractpath import AbstractPath 5 | from unipath.path import Path 6 | 7 | FSPath = Path 8 | 9 | #### FILTER FUNCTIONS (PUBLIC) #### 10 | def DIRS(p): return p.isdir() 11 | def FILES(p): return p.isfile() 12 | def LINKS(p): return p.islink() 13 | def DIRS_NO_LINKS(p): return p.isdir() and not p.islink() 14 | def FILES_NO_LINKS(p): return p.isfile() and not p.islink() 15 | def DEAD_LINKS(p): return p.islink() and not p.exists() 16 | 17 | -------------------------------------------------------------------------------- /unipath/abstractpath.py: -------------------------------------------------------------------------------- 1 | """unipath.py - A two-class approach to file/directory operations in Python. 2 | """ 3 | 4 | import os 5 | 6 | from unipath.errors import UnsafePathError 7 | 8 | __all__ = ["AbstractPath"] 9 | 10 | # Use unicode strings if possible. 11 | _base = str # Python 3 str (=unicode), or Python 2 bytes. 12 | if os.path.supports_unicode_filenames: 13 | try: 14 | _base = unicode # Python 2 unicode. 15 | except NameError: 16 | pass 17 | 18 | class AbstractPath(_base): 19 | """An object-oriented approach to os.path functions.""" 20 | pathlib = os.path 21 | auto_norm = False 22 | 23 | #### Special Python methods. 24 | def __new__(class_, *args, **kw): 25 | norm = kw.pop("norm", None) 26 | if norm is None: 27 | norm = class_.auto_norm 28 | if kw: 29 | kw_str = ", ".join(kw.iterkeys()) 30 | raise TypeError("unrecognized keyword args: %s" % kw_str) 31 | newpath = class_._new_helper(args) 32 | if isinstance(newpath, class_): 33 | return newpath 34 | if norm: 35 | newpath = class_.pathlib.normpath(newpath) 36 | # Can't call .norm() because the path isn't instantiated yet. 37 | return _base.__new__(class_, newpath) 38 | 39 | def __add__(self, more): 40 | try: 41 | resultStr = _base.__add__(self, more) 42 | except TypeError: #Python bug 43 | resultStr = NotImplemented 44 | if resultStr is NotImplemented: 45 | return resultStr 46 | return self.__class__(resultStr) 47 | 48 | @classmethod 49 | def _new_helper(class_, args): 50 | pathlib = class_.pathlib 51 | # If no args, return "." or platform equivalent. 52 | if not args: 53 | return pathlib.curdir 54 | # Avoid making duplicate instances of the same immutable path 55 | if len(args) == 1 and isinstance(args[0], class_) and \ 56 | args[0].pathlib == pathlib: 57 | return args[0] 58 | try: 59 | legal_arg_types = (class_, basestring, list, int, long) 60 | except NameError: # Python 3 doesn't have basestring nor long 61 | legal_arg_types = (class_, str, list, int) 62 | args = list(args) 63 | for i, arg in enumerate(args): 64 | if not isinstance(arg, legal_arg_types): 65 | m = "arguments must be str, unicode, list, int, long, or %s" 66 | raise TypeError(m % class_.__name__) 67 | try: 68 | int_types = (int, long) 69 | except NameError: # We are in Python 3 70 | int_types = int 71 | if isinstance(arg, int_types): 72 | args[i] = str(arg) 73 | elif isinstance(arg, class_) and arg.pathlib != pathlib: 74 | arg = getattr(arg, components)() # Now a list. 75 | if arg[0]: 76 | reason = ("must use a relative path when converting " 77 | "from '%s' platform to '%s': %s") 78 | tup = arg.pathlib.__name__, pathlib.__name__, arg 79 | raise ValueError(reason % tup) 80 | # Fall through to convert list of components. 81 | if isinstance(arg, list): 82 | args[i] = pathlib.join(*arg) 83 | return pathlib.join(*args) 84 | 85 | def __repr__(self): 86 | return '%s(%r)' % (self.__class__.__name__, _base(self)) 87 | 88 | def norm(self): 89 | return self.__class__(self.pathlib.normpath(self)) 90 | 91 | def expand_user(self): 92 | return self.__class__(self.pathlib.expanduser(self)) 93 | 94 | def expand_vars(self): 95 | return self.__class__(self.pathlib.expandvars(self)) 96 | 97 | def expand(self): 98 | """ Clean up a filename by calling expandvars(), 99 | expanduser(), and norm() on it. 100 | 101 | This is commonly everything needed to clean up a filename 102 | read from a configuration file, for example. 103 | """ 104 | newpath = self.pathlib.expanduser(self) 105 | newpath = self.pathlib.expandvars(newpath) 106 | newpath = self.pathlib.normpath(newpath) 107 | return self.__class__(newpath) 108 | 109 | #### Properies: parts of the path. 110 | 111 | @property 112 | def parent(self): 113 | """The path without the final component; akin to os.path.dirname(). 114 | Example: Path('/usr/lib/libpython.so').parent => Path('/usr/lib') 115 | """ 116 | return self.__class__(self.pathlib.dirname(self)) 117 | 118 | @property 119 | def name(self): 120 | """The final component of the path. 121 | Example: path('/usr/lib/libpython.so').name => Path('libpython.so') 122 | """ 123 | return self.__class__(self.pathlib.basename(self)) 124 | 125 | @property 126 | def stem(self): 127 | """Same as path.name but with one file extension stripped off. 128 | Example: path('/home/guido/python.tar.gz').stem => Path('python.tar') 129 | """ 130 | return self.__class__(self.pathlib.splitext(self.name)[0]) 131 | 132 | @property 133 | def ext(self): 134 | """The file extension, for example '.py'.""" 135 | return self.__class__(self.pathlib.splitext(self)[1]) 136 | 137 | #### Methods to extract and add parts to the path. 138 | 139 | def split_root(self): 140 | """Split a path into root and remainder. The root is always "/" for 141 | posixpath, or a backslash-root, drive-root, or UNC-root for ntpath. 142 | If the path begins with none of these, the root is returned as "" 143 | and the remainder is the entire path. 144 | """ 145 | P = self.__class__ 146 | if hasattr(self.pathlib, "splitunc"): 147 | root, rest = self.pathlib.splitunc(self) 148 | if root: 149 | if rest.startswith(self.pathlib.sep): 150 | root += self.pathlib.sep 151 | rest = rest[len(self.pathlib.sep):] 152 | return P(root), P(rest) 153 | # @@MO: Should test altsep too. 154 | root, rest = self.pathlib.splitdrive(self) 155 | if root: 156 | if rest.startswith(self.pathlib.sep): 157 | root += self.pathlib.sep 158 | rest = rest[len(self.pathlib.sep):] 159 | return P(root), P(rest) 160 | # @@MO: Should test altsep too. 161 | if self.startswith(self.pathlib.sep): 162 | return P(self.pathlib.sep), P(rest[len(self.pathlib.sep):]) 163 | if self.pathlib.altsep and self.startswith(self.pathlib.altsep): 164 | return P(self.pathlib.altsep), P(rest[len(self.pathlib.altsep):]) 165 | return P(""), self 166 | 167 | def components(self): 168 | # @@MO: Had to prevent "" components from being appended. I don't 169 | # understand why Lindqvist didn't have this problem. 170 | # Also, doesn't this fail to get the beginning components if there's 171 | # a "." or ".." in the middle of the path? 172 | root, loc = self.split_root() 173 | components = [] 174 | while loc != self.pathlib.curdir and loc != self.pathlib.pardir: 175 | prev = loc 176 | loc, child = self.pathlib.split(prev) 177 | #print "prev=%r, loc=%r, child=%r" % (prev, loc, child) 178 | if loc == prev: 179 | break 180 | if child != "": 181 | components.append(child) 182 | if loc == "": 183 | break 184 | if loc != "": 185 | components.append(loc) 186 | components.reverse() 187 | components.insert(0, root) 188 | return [self.__class__(x) for x in components] 189 | 190 | def ancestor(self, n): 191 | p = self 192 | for i in range(n): 193 | p = p.parent 194 | return p 195 | 196 | def child(self, *children): 197 | # @@MO: Compare against Glyph's method. 198 | for child in children: 199 | if self.pathlib.sep in child: 200 | msg = "arg '%s' contains path separator '%s'" 201 | tup = child, self.pathlib.sep 202 | raise UnsafePathError(msg % tup) 203 | if self.pathlib.altsep and self.pathlib.altsep in child: 204 | msg = "arg '%s' contains alternate path separator '%s'" 205 | tup = child, self.pathlib.altsep 206 | raise UnsafePathError(msg % tup) 207 | if child == self.pathlib.pardir: 208 | msg = "arg '%s' is parent directory specifier '%s'" 209 | tup = child, self.pathlib.pardir 210 | raise UnsafePathError(msg % tup) 211 | if child == self.pathlib.curdir: 212 | msg = "arg '%s' is current directory specifier '%s'" 213 | tup = child, self.pathlib.curdir 214 | raise UnsafePathError(msg % tup) 215 | newpath = self.pathlib.join(self, *children) 216 | return self.__class__(newpath) 217 | 218 | def norm_case(self): 219 | return self.__class__(self.pathlib.normcase(self)) 220 | 221 | def isabsolute(self): 222 | """True if the path is absolute. 223 | Note that we consider a Windows drive-relative path ("C:foo") 224 | absolute even though ntpath.isabs() considers it relative. 225 | """ 226 | return bool(self.split_root()[0]) 227 | -------------------------------------------------------------------------------- /unipath/errors.py: -------------------------------------------------------------------------------- 1 | class UnsafePathError(ValueError): 2 | pass 3 | 4 | class RecursionError(OSError): 5 | pass 6 | 7 | class DebugWarning(UserWarning): 8 | pass 9 | -------------------------------------------------------------------------------- /unipath/path.py: -------------------------------------------------------------------------------- 1 | """unipath.py - A two-class approach to file/directory operations in Python. 2 | 3 | Full usage, documentation, changelog, and history are at 4 | http://sluggo.scrapping.cc/python/unipath/ 5 | 6 | (c) 2007 by Mike Orr (and others listed in "History" section of doc page). 7 | Permission is granted to redistribute, modify, and include in commercial and 8 | noncommercial products under the terms of the Python license (i.e., the "Python 9 | Software Foundation License version 2" at 10 | http://www.python.org/download/releases/2.5/license/). 11 | """ 12 | 13 | import errno 14 | import fnmatch 15 | import glob 16 | import os 17 | import shutil 18 | import stat 19 | import sys 20 | import time 21 | import warnings 22 | 23 | from unipath.abstractpath import AbstractPath 24 | from unipath.errors import RecursionError 25 | 26 | __all__ = ["Path"] 27 | 28 | #warnings.simplefilter("ignore", DebugWarning, append=1) 29 | 30 | def flatten(iterable): 31 | """Yield each element of 'iterable', recursively interpolating 32 | lists and tuples. Examples: 33 | [1, [2, 3], 4] => iter([1, 2, 3, 4]) 34 | [1, (2, 3, [4]), 5) => iter([1, 2, 3, 4, 5]) 35 | """ 36 | for elm in iterable: 37 | if isinstance(elm, (list, tuple)): 38 | for relm in flatten(elm): 39 | yield relm 40 | else: 41 | yield elm 42 | 43 | class Path(AbstractPath): 44 | 45 | ##### CURRENT DIRECTORY #### 46 | @classmethod 47 | def cwd(class_): 48 | """ Return the current working directory as a path object. """ 49 | return class_(os.getcwd()) 50 | 51 | def chdir(self): 52 | os.chdir(self) 53 | 54 | #### CALCULATING PATHS #### 55 | def absolute(self): 56 | """Return the absolute Path, prefixing the current directory if 57 | necessary. 58 | """ 59 | return self.__class__(os.path.abspath(self)) 60 | 61 | def relative(self): 62 | """Return a relative path to self from the current working directory. 63 | """ 64 | return self.__class__.cwd().rel_path_to(self) 65 | 66 | def rel_path_to(self, dst): 67 | """ Return a relative path from self to dst. 68 | 69 | This prefixes as many pardirs (``..``) as necessary to reach a common 70 | ancestor. If there's no common ancestor (e.g., they're are on 71 | different Windows drives), the path will be absolute. 72 | """ 73 | origin = self.__class__(self).absolute() 74 | if not origin.isdir(): 75 | origin = origin.parent 76 | dest = self.__class__(dst).absolute() 77 | 78 | orig_list = origin.norm_case().components() 79 | # Don't normcase dest! We want to preserve the case. 80 | dest_list = dest.components() 81 | 82 | if orig_list[0] != os.path.normcase(dest_list[0]): 83 | # Can't get here from there. 84 | return self.__class__(dest) 85 | 86 | # Find the location where the two paths start to differ. 87 | i = 0 88 | for start_seg, dest_seg in zip(orig_list, dest_list): 89 | if start_seg != os.path.normcase(dest_seg): 90 | break 91 | i += 1 92 | 93 | # Now i is the point where the two paths diverge. 94 | # Need a certain number of "os.pardir"s to work up 95 | # from the origin to the point of divergence. 96 | segments = [os.pardir] * (len(orig_list) - i) 97 | # Need to add the diverging part of dest_list. 98 | segments += dest_list[i:] 99 | if len(segments) == 0: 100 | # If they happen to be identical, use os.curdir. 101 | return self.__class__(os.curdir) 102 | else: 103 | newpath = os.path.join(*segments) 104 | return self.__class__(newpath) 105 | 106 | def resolve(self): 107 | """Return an equivalent Path that does not contain symbolic links.""" 108 | return self.__class__(os.path.realpath(self)) 109 | 110 | 111 | #### LISTING DIRECTORIES #### 112 | def listdir(self, pattern=None, filter=None, names_only=False): 113 | if names_only and filter is not None: 114 | raise TypeError("filter not allowed if 'names_only' is true") 115 | empty_path = self == "" 116 | if empty_path: 117 | names = os.listdir(os.path.curdir) 118 | else: 119 | names = os.listdir(self) 120 | if pattern is not None: 121 | names = fnmatch.filter(names, pattern) 122 | names.sort() 123 | if names_only: 124 | return names 125 | ret = [self.child(x) for x in names] 126 | if filter is not None: 127 | ret = [x for x in ret if filter(x)] 128 | return ret 129 | 130 | def walk(self, pattern=None, filter=None, top_down=True): 131 | return self._walk(pattern, filter, top_down=top_down, seen=set()) 132 | 133 | def _walk(self, pattern, filter, top_down, seen): 134 | if not self.isdir(): 135 | raise RecursionError("not a directory: %s" % self) 136 | real_dir = self.resolve() 137 | if real_dir in seen: 138 | return # We've already recursed this directory. 139 | seen.add(real_dir) 140 | for child in self.listdir(pattern): 141 | is_dir = child.isdir() 142 | if is_dir and not top_down: 143 | for grandkid in child._walk(pattern, filter, top_down, seen): 144 | yield grandkid 145 | if filter is None or filter(child): 146 | yield child 147 | if is_dir and top_down: 148 | for grandkid in child._walk(pattern, filter, top_down, seen): 149 | yield grandkid 150 | 151 | 152 | #### STAT ATTRIBUTES #### 153 | exists = os.path.exists 154 | lexists = os.path.lexists 155 | 156 | isfile = os.path.isfile 157 | 158 | def isdir(self): 159 | return os.path.isdir(self) 160 | 161 | islink = os.path.islink 162 | ismount = os.path.ismount 163 | 164 | atime = os.path.getatime 165 | ctime = os.path.getctime 166 | mtime = os.path.getmtime 167 | 168 | size = os.path.getsize 169 | 170 | if hasattr(os.path, 'samefile'): 171 | same_file = os.path.samefile 172 | 173 | # For some reason these functions have to be wrapped in methods. 174 | def stat(self): 175 | return os.stat(self) 176 | 177 | def lstat(self): 178 | return os.lstat(self) 179 | 180 | if hasattr(os, 'statvfs'): 181 | def statvfs(self): 182 | return os.statvfs(self) 183 | 184 | def chmod(self, mode): 185 | os.chmod(self, mode) 186 | 187 | if hasattr(os, 'chown'): 188 | def chown(self, uid, gid): 189 | os.chown(self, uid, gid) 190 | 191 | def set_times(self, mtime=None, atime=None): 192 | """Set a path's modification and access times. 193 | Times must be in ticks as returned by ``time.time()``. 194 | If 'mtime' is None, use the current time. 195 | If 'atime' is None, use 'mtime'. 196 | Creates an empty file if the path does not exists. 197 | On some platforms (Windows), the path must not be a directory. 198 | """ 199 | if not self.exists(): 200 | fd = os.open(self, os.O_WRONLY | os.O_CREAT, 0o666) 201 | os.close(fd) 202 | if mtime is None: 203 | mtime = time.time() 204 | if atime is None: 205 | atime = mtime 206 | times = atime, mtime 207 | os.utime(self, times) 208 | 209 | 210 | #### CREATING, REMOVING, AND RENAMING #### 211 | def mkdir(self, parents=False, mode=0o777): 212 | if self.exists(): 213 | return 214 | if parents: 215 | os.makedirs(self, mode) 216 | else: 217 | os.mkdir(self, mode) 218 | 219 | def rmdir(self, parents=False): 220 | if not self.exists(): 221 | return 222 | if parents: 223 | os.removedirs(self) 224 | else: 225 | os.rmdir(self) 226 | 227 | def remove(self): 228 | if self.lexists(): 229 | os.remove(self) 230 | 231 | def rename(self, new, parents=False): 232 | if parents: 233 | os.renames(self, new) 234 | else: 235 | os.rename(self, new) 236 | 237 | #### SYMBOLIC AND HARD LINKS #### 238 | if hasattr(os, 'link'): 239 | def hardlink(self, newpath): 240 | """Create a hard link at 'newpath' pointing to self. """ 241 | os.link(self, newpath) 242 | 243 | if hasattr(os, 'symlink'): 244 | def write_link(self, link_content): 245 | """Create a symbolic link at self pointing to 'link_content'. 246 | This is the same as .symlink but with the args reversed. 247 | """ 248 | os.symlink(link_content, self) 249 | 250 | def make_relative_link_to(self, dest): 251 | """Make a relative symbolic link from self to dest. 252 | 253 | Same as self.write_link(self.rel_path_to(dest)) 254 | """ 255 | link_content = self.rel_path_to(dest) 256 | self.write_link(link_content) 257 | 258 | 259 | if hasattr(os, 'readlink'): 260 | def read_link(self, absolute=False): 261 | p = self.__class__(os.readlink(self)) 262 | if absolute and not p.isabsolute(): 263 | p = self.__class__(self.parent, p) 264 | return p 265 | 266 | #### HIGH-LEVEL OPERATIONS #### 267 | def copy(self, dst, times=False, perms=False): 268 | """Copy the file, optionally copying the permission bits (mode) and 269 | last access/modify time. If the destination file exists, it will be 270 | replaced. Raises OSError if the destination is a directory. If the 271 | platform does not have the ability to set the permission or times, 272 | ignore it. 273 | This is shutil.copyfile plus bits of shutil.copymode and 274 | shutil.copystat's implementation. 275 | shutil.copy and shutil.copy2 are not supported but are easy to do. 276 | """ 277 | shutil.copyfile(self, dst) 278 | if times or perms: 279 | self.copy_stat(dst, times, perms) 280 | 281 | def copy_stat(self, dst, times=True, perms=True): 282 | st = os.stat(self) 283 | if hasattr(os, 'utime'): 284 | os.utime(dst, (st.st_atime, st.st_mtime)) 285 | if hasattr(os, 'chmod'): 286 | m = stat.S_IMODE(st.st_mode) 287 | os.chmod(dst, m) 288 | 289 | # Undocumented, not implemented method. 290 | def copy_tree(dst, perserve_symlinks=False, times=False, perms=False): 291 | raise NotImplementedError() 292 | 293 | if hasattr(shutil, 'move'): 294 | move = shutil.move 295 | 296 | def needs_update(self, others): 297 | if not isinstance(others, (list, tuple)): 298 | others = [others] 299 | if not self.exists(): 300 | return True 301 | control = self.mtime() 302 | for p in flatten(others): 303 | if p.isdir(): 304 | for child in p.walk(filter=FILES): 305 | if child.mtime() > control: 306 | return True 307 | elif p.mtime() > control: 308 | return True 309 | return False 310 | 311 | def read_file(self, mode="rU"): 312 | f = open(self, mode) 313 | content = f.read() 314 | f.close() 315 | return content 316 | 317 | def rmtree(self, parents=False): 318 | """Delete self recursively, whether it's a file or directory. 319 | directory, remove it recursively (same as shutil.rmtree). If it 320 | doesn't exist, do nothing. 321 | If you're looking for a 'rmtree' method, this is what you want. 322 | """ 323 | if self.isfile() or self.islink(): 324 | os.remove(self) 325 | elif self.isdir(): 326 | shutil.rmtree(self) 327 | if not parents: 328 | return 329 | p = self.parent 330 | while p: 331 | try: 332 | os.rmdir(p) 333 | except os.error: 334 | break 335 | p = p.parent 336 | 337 | def write_file(self, content, mode="w"): 338 | f = open(self, mode) 339 | f.write(content) 340 | f.close() 341 | 342 | -------------------------------------------------------------------------------- /unipath/tools.py: -------------------------------------------------------------------------------- 1 | """Convenience functions. 2 | """ 3 | 4 | from __future__ import print_function, generators 5 | import sys 6 | 7 | from unipath import Path 8 | 9 | def dict2dir(dir, dic, mode="w"): 10 | dir = Path(dir) 11 | if not dir.exists(): 12 | dir.mkdir() 13 | for filename, content in dic.items(): 14 | p = Path(dir, filename) 15 | if isinstance(content, dict): 16 | dict2dir(p, content) 17 | continue 18 | f = open(p, mode) 19 | f.write(content) 20 | f.close() 21 | 22 | def dump_path(path, prefix="", tab=" ", file=None): 23 | if file is None: 24 | file = sys.stdout 25 | p = Path(path) 26 | if p.islink(): 27 | print("%s%s -> %s" % (prefix, p.name, p.read_link()), file=file) 28 | elif p.isdir(): 29 | print("%s%s:" % (prefix, p.name), file=file) 30 | for p2 in p.listdir(): 31 | dump_path(p2, prefix+tab, tab, file) 32 | else: 33 | print("%s%s (%d)" % (prefix, p.name, p.size()), file=file) 34 | --------------------------------------------------------------------------------