1import os
2
3
4class Path:
5    # cache for better performance
6    __cached_existing_dir = None
7    _path = None  # type: str
8
9    def __init__(self, path):
10        self._path = os.path.expandvars(os.path.expanduser(path.lstrip()))
11
12    def get_abs_path(self) -> str:
13        return self._path
14
15    def exists(self) -> bool:
16        return os.path.exists(self._path)
17
18    def get_basename(self) -> str:
19        return os.path.basename(self._path.rstrip('/'))
20
21    def get_dirname(self) -> str:
22        return os.path.dirname(self.get_user_path())
23
24    def is_dir(self) -> bool:
25        return os.path.isdir(self._path)
26
27    def is_exe(self) -> bool:
28        return os.access(self._path, os.X_OK)
29
30    def get_ext(self) -> str:
31        return os.path.splitext(self._path)[1].lower()[1:]
32
33    def get_user_path(self) -> str:
34        result = self._path.rstrip('/')
35
36        user_dir = os.path.expanduser('~')
37        if result.startswith(user_dir):
38            return result.replace(user_dir, '~', 1)
39
40        return result
41
42    def get_existing_dir(self) -> str:
43        """
44        Example (assuming foo & bar do not exist):
45
46        >>> p = Path('/home/aleksandr/projects/foo/bar')
47        >>> p.get_existing_dir() == '/home/aleksandr/projects'
48
49        :returns: path to the last dir that exists in the user's query
50        """
51        if self.__cached_existing_dir:
52            return self.__cached_existing_dir
53
54        result = self._path
55        while result and (not os.path.exists(result) or not os.path.isdir(result)):
56            result = os.path.dirname(result)
57
58        # special case when dir ends with "/."
59        # we want to return path without .
60        # example: /bin/env/. -> /bin/env
61        if result.endswith('/.'):
62            result = result[:-2]
63
64        if not result:
65            raise InvalidPathError('Invalid path "%s"' % self._path)
66
67        self.__cached_existing_dir = result
68        return result
69
70    def get_search_part(self) -> str:
71        """
72        :returns: remaining part of the query that goes after :meth:`get_existing_dir`
73        """
74        return self._path[len(self.get_existing_dir()):].strip('/')
75
76
77class InvalidPathError(RuntimeError):
78    pass
79