Source code for fs.path

"""Useful functions for working with PyFilesystem paths.

This is broadly similar to the standard `os.path` module but works
with paths in the canonical format expected by all FS objects (that is,
separated by forward slashes and with an optional leading slash).

See :ref:`paths` for an explanation of PyFilesystem paths.

"""

from __future__ import print_function
from __future__ import unicode_literals

import re
import typing

from .errors import IllegalBackReference

if False:  # typing.TYPE_CHECKING
    from typing import List, Text, Tuple


__all__ = [
    "abspath",
    "basename",
    "combine",
    "dirname",
    "forcedir",
    "frombase",
    "isabs",
    "isbase",
    "isdotfile",
    "isparent",
    "issamedir",
    "iswildcard",
    "iteratepath",
    "join",
    "normpath",
    "parts",
    "recursepath",
    "relativefrom",
    "relpath",
    "split",
    "splitext",
]

_requires_normalization = re.compile(r"(^|/)\.\.?($|/)|//", re.UNICODE).search


[docs]def normpath(path): # type: (Text) -> Text """Normalize a path. This function simplifies a path by collapsing back-references and removing duplicated separators. Arguments: path (str): Path to normalize. Returns: str: A valid FS path. Example: >>> normpath("/foo//bar/frob/../baz") '/foo/bar/baz' >>> normpath("foo/../../bar") Traceback (most recent call last) ... IllegalBackReference: path 'foo/../../bar' contains back-references outside of filesystem" """ if path in "/": return path # An early out if there is no need to normalize this path if not _requires_normalization(path): return path.rstrip("/") prefix = "/" if path.startswith("/") else "" components = [] # type: List[Text] try: for component in path.split("/"): if component in "..": # True for '..', '.', and '' if component == "..": components.pop() else: components.append(component) except IndexError: raise IllegalBackReference(path) return prefix + "/".join(components)
[docs]def iteratepath(path): # type: (Text) -> List[Text] """Iterate over the individual components of a path. Arguments: path (str): Path to iterate over. Returns: list: A list of path components. Example: >>> iteratepath('/foo/bar/baz') ['foo', 'bar', 'baz'] """ path = relpath(normpath(path)) if not path: return [] return path.split("/")
[docs]def recursepath(path, reverse=False): # type: (Text, bool) -> List[Text] """Get intermediate paths from the root to the given path. Arguments: path (str): A PyFilesystem path reverse (bool): Reverses the order of the paths (default `False`). Returns: list: A list of paths. Example: >>> recursepath('a/b/c') ['/', '/a', '/a/b', '/a/b/c'] """ if path in "/": return ["/"] path = abspath(normpath(path)) + "/" paths = ["/"] find = path.find append = paths.append pos = 1 len_path = len(path) while pos < len_path: pos = find("/", pos) append(path[:pos]) pos += 1 if reverse: return paths[::-1] return paths
[docs]def isabs(path): # type: (Text) -> bool """Check if a path is an absolute path. Arguments: path (str): A PyFilesytem path. Returns: bool: `True` if the path is absolute (starts with a ``'/'``). """ # Somewhat trivial, but helps to make code self-documenting return path.startswith("/")
[docs]def abspath(path): # type: (Text) -> Text """Convert the given path to an absolute path. Since FS objects have no concept of a *current directory*, this simply adds a leading ``/`` character if the path doesn't already have one. Arguments: path (str): A PyFilesytem path. Returns: str: An absolute path. """ if not path.startswith("/"): return "/" + path return path
[docs]def relpath(path): # type: (Text) -> Text """Convert the given path to a relative path. This is the inverse of `abspath`, stripping a leading ``'/'`` from the path if it is present. Arguments: path (str): A path to adjust. Returns: str: A relative path. Example: >>> relpath('/a/b') 'a/b' """ return path.lstrip("/")
[docs]def join(*paths): # type: (*Text) -> Text """Join any number of paths together. Arguments: *paths (str): Paths to join, given as positional arguments. Returns: str: The joined path. Example: >>> join('foo', 'bar', 'baz') 'foo/bar/baz' >>> join('foo/bar', '../baz') 'foo/baz' >>> join('foo/bar', '/baz') '/baz' """ absolute = False relpaths = [] # type: List[Text] for p in paths: if p: if p[0] == "/": del relpaths[:] absolute = True relpaths.append(p) path = normpath("/".join(relpaths)) if absolute: path = abspath(path) return path
[docs]def combine(path1, path2): # type: (Text, Text) -> Text """Join two paths together. This is faster than :func:`~fs.path.join`, but only works when the second path is relative, and there are no back references in either path. Arguments: path1 (str): A PyFilesytem path. path2 (str): A PyFilesytem path. Returns: str: The joint path. Example: >>> combine("foo/bar", "baz") 'foo/bar/baz' """ if not path1: return path2.lstrip() return "{}/{}".format(path1.rstrip("/"), path2.lstrip("/"))
[docs]def parts(path): # type: (Text) -> List[Text] """Split a path in to its component parts. Arguments: path (str): Path to split in to parts. Returns: list: List of components Example: >>> parts('/foo/bar/baz') ['/', 'foo', 'bar', 'baz'] """ _path = normpath(path) components = _path.strip("/") _parts = ["/" if _path.startswith("/") else "./"] if components: _parts += components.split("/") return _parts
[docs]def split(path): # type: (Text) -> Tuple[Text, Text] """Split a path into (head, tail) pair. This function splits a path into a pair (head, tail) where 'tail' is the last pathname component and 'head' is all preceding components. Arguments: path (str): Path to split Returns: (str, str): a tuple containing the head and the tail of the path. Example: >>> split("foo/bar") ('foo', 'bar') >>> split("foo/bar/baz") ('foo/bar', 'baz') >>> split("/foo/bar/baz") ('/foo/bar', 'baz') """ if "/" not in path: return ("", path) split = path.rsplit("/", 1) return (split[0] or "/", split[1])
[docs]def splitext(path): # type: (Text) -> Tuple[Text, Text] """Split the extension from the path. Arguments: path (str): A path to split. Returns: (str, str): A tuple containing the path and the extension. Example: >>> splitext('baz.txt') ('baz', '.txt') >>> splitext('foo/bar/baz.txt') ('foo/bar/baz', '.txt') >>> splitext('foo/bar/.foo') ('foo/bar/.foo', '') """ parent_path, pathname = split(path) if pathname.startswith(".") and pathname.count(".") == 1: return path, "" if "." not in pathname: return path, "" pathname, ext = pathname.rsplit(".", 1) path = join(parent_path, pathname) return path, "." + ext
[docs]def isdotfile(path): # type: (Text) -> bool """Detect if a path references a dot file. Arguments: path (str): Path to check. Returns: bool: `True` if the resource name starts with a ``'.'``. Example: >>> isdotfile('.baz') True >>> isdotfile('foo/bar/.baz') True >>> isdotfile('foo/bar.baz') False """ return basename(path).startswith(".")
[docs]def dirname(path): # type: (Text) -> Text """Return the parent directory of a path. This is always equivalent to the 'head' component of the value returned by ``split(path)``. Arguments: path (str): A PyFilesytem path. Returns: str: the parent directory of the given path. Example: >>> dirname('foo/bar/baz') 'foo/bar' >>> dirname('/foo/bar') '/foo' >>> dirname('/foo') '/' """ return split(path)[0]
[docs]def basename(path): # type: (Text) -> Text """Return the basename of the resource referenced by a path. This is always equivalent to the 'tail' component of the value returned by split(path). Arguments: path (str): A PyFilesytem path. Returns: str: the name of the resource at the given path. Example: >>> basename('foo/bar/baz') 'baz' >>> basename('foo/bar') 'bar' >>> basename('foo/bar/') '' """ return split(path)[1]
[docs]def issamedir(path1, path2): # type: (Text, Text) -> bool """Check if two paths reference a resource in the same directory. Arguments: path1 (str): A PyFilesytem path. path2 (str): A PyFilesytem path. Returns: bool: `True` if the two resources are in the same directory. Example: >>> issamedir("foo/bar/baz.txt", "foo/bar/spam.txt") True >>> issamedir("foo/bar/baz/txt", "spam/eggs/spam.txt") False """ return dirname(normpath(path1)) == dirname(normpath(path2))
[docs]def isbase(path1, path2): # type: (Text, Text) -> bool """Check if ``path1`` is a base of ``path2``. Arguments: path1 (str): A PyFilesytem path. path2 (str): A PyFilesytem path. Returns: bool: `True` if ``path2`` starts with ``path1`` Example: >>> isbase('foo/bar', 'foo/bar/baz/egg.txt') True """ _path1 = forcedir(abspath(path1)) _path2 = forcedir(abspath(path2)) return _path2.startswith(_path1) # longer one is child
[docs]def isparent(path1, path2): # type: (Text, Text) -> bool """Check if ``path1`` is a parent directory of ``path2``. Arguments: path1 (str): A PyFilesytem path. path2 (str): A PyFilesytem path. Returns: bool: `True` if ``path1`` is a parent directory of ``path2`` Example: >>> isparent("foo/bar", "foo/bar/spam.txt") True >>> isparent("foo/bar/", "foo/bar") True >>> isparent("foo/barry", "foo/baz/bar") False >>> isparent("foo/bar/baz/", "foo/baz/bar") False """ bits1 = path1.split("/") bits2 = path2.split("/") while bits1 and bits1[-1] == "": bits1.pop() if len(bits1) > len(bits2): return False for (bit1, bit2) in zip(bits1, bits2): if bit1 != bit2: return False return True
[docs]def forcedir(path): # type: (Text) -> Text """Ensure the path ends with a trailing forward slash. Arguments: path (str): A PyFilesytem path. Returns: str: The path, ending with a slash. Example: >>> forcedir("foo/bar") 'foo/bar/' >>> forcedir("foo/bar/") 'foo/bar/' >>> forcedir("foo/spam.txt") 'foo/spam.txt' """ if not path.endswith("/"): return path + "/" return path
[docs]def frombase(path1, path2): # type: (Text, Text) -> Text """Get the final path of ``path2`` that isn't in ``path1``. Arguments: path1 (str): A PyFilesytem path. path2 (str): A PyFilesytem path. Returns: str: the final part of ``path2``. Example: >>> frombase('foo/bar/', 'foo/bar/baz/egg') 'baz/egg' """ if not isparent(path1, path2): raise ValueError("path1 must be a prefix of path2") return path2[len(path1) :]
[docs]def relativefrom(base, path): # type: (Text, Text) -> Text """Return a path relative from a given base path. Insert backrefs as appropriate to reach the path from the base. Arguments: base (str): Path to a directory. path (str): Path to make relative. Returns: str: the path to ``base`` from ``path``. >>> relativefrom("foo/bar", "baz/index.html") '../../baz/index.html' """ base_parts = list(iteratepath(base)) path_parts = list(iteratepath(path)) common = 0 for component_a, component_b in zip(base_parts, path_parts): if component_a != component_b: break common += 1 return "/".join([".."] * (len(base_parts) - common) + path_parts[common:])
_WILD_CHARS = frozenset("*?[]!{}")
[docs]def iswildcard(path): # type: (Text) -> bool """Check if a path ends with a wildcard. Arguments: path (str): A PyFilesystem path. Returns: bool: `True` if path ends with a wildcard. Example: >>> iswildcard('foo/bar/baz.*') True >>> iswildcard('foo/bar') False """ assert path is not None return not _WILD_CHARS.isdisjoint(path)