Coverage for src/hods/path.py: 100.00%
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""hods - home directory synchronization.
3Copyright (C) 2016-2020 Mathias Stelzer <knoppo@rolln.de>
5hods is free software: you can redistribute it and/or modify
6it under the terms of the GNU General Public License as published by
7the Free Software Foundation, either version 3 of the License, or
8(at your option) any later version.
10hods is distributed in the hope that it will be useful,
11but WITHOUT ANY WARRANTY; without even the implied warranty of
12MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13GNU General Public License for more details.
15You should have received a copy of the GNU General Public License
16along with this program. If not, see <http://www.gnu.org/licenses/>.
17"""
18import logging
19import os
20import shutil
22logger = logging.getLogger(__name__)
25# TODO: use pathlib.Path # noqa: T101
26class Path:
27 """A path on disk."""
29 @classmethod
30 def _get_path(cls, path):
31 """Get the path string from the given `Path` or string."""
32 if isinstance(path, Path):
33 path = path.path
34 return os.path.normpath(path)
36 def __init__(self, path):
37 """Initialize path.
39 :param path: `str` - Absolute path
40 """
41 logger.debug('Initializing %s %s', self.__class__.__name__, path)
42 if not os.path.isabs(path):
43 raise ValueError('{}: path argument must be an absolute path!'.format(self.__class__.__name__))
44 self.path = path
46 @property
47 def dirname(self):
48 """Shortcut for `os.path.dirname`."""
49 return os.path.dirname(self.path)
51 @property
52 def basename(self):
53 """Shortcut for `os.path.basename`.
55 Set this property to update the `path` attribute.
56 """
57 return os.path.basename(self.path)
59 @basename.setter
60 def basename(self, value):
61 self.path = os.path.join(self.dirname, value)
63 def __str__(self):
64 """String representation of the object."""
65 return str(self.path)
67 def __repr__(self):
68 """Representation of the object."""
69 return '<{} {}>'.format(self.__class__.__name__, self.path)
71 def __hash__(self):
72 """Hash of the object."""
73 return hash((self.__class__, self.path))
75 def __eq__(self, other):
76 """Return a `bool` whether the other object equals this one."""
77 if not isinstance(other, Path):
78 return False
79 return self.path == other.path
81 def __ne__(self, other):
82 """Return a `bool` whether the other object does *not* equal this one."""
83 return not self.__eq__(other)
85 def exists(self):
86 """Shortcut: Call and return `os.path.exists` with ``self.path``."""
87 return os.path.exists(self.path)
89 def islink(self):
90 """Shortcut: Call and return `os.path.islink` with ``self.path``."""
91 return os.path.islink(self.path)
93 def isdir(self):
94 """Shortcut: Call and return `os.path.isdir` with ``self.path``."""
95 return os.path.isdir(self.path)
97 def relpath(self, start):
98 """Shortcut: Call and return `os.path.relpath` with ``self.path``."""
99 return os.path.relpath(self.path, self._get_path(start))
101 def realpath(self):
102 """Shortcut: Call and return `os.path.realpath` with ``self.path``."""
103 return os.path.realpath(self.path)
105 def move(self, destination):
106 """Move this file or directory to the given destination.
108 :param destination: `str` - absolute path to move the file to
109 :return: ``None``
110 """
111 destination_path = self._get_path(destination)
112 if not os.path.isabs(destination_path):
113 raise ValueError('{}.move() requires an absolute path.'.format(self.__class__.__name__))
114 logger.info('mv "%s" "%s"', self.path, destination_path)
115 shutil.move(self.path, destination_path)
117 def _clean_copy_destination(self, destination):
118 destination = self._get_path(destination)
119 if not os.path.isabs(destination):
120 raise ValueError('{}.copy() requires an absolute path.'.format(self.__class__.__name__))
121 if os.path.exists(destination):
122 raise FileExistsError('{}.copy() path must not exist.'.format(self.__class__.__name__))
123 logger.info('cp "%s" "%s"', self.path, destination)
124 return destination
127class Directory(Path):
128 """A path representing a directory on disk."""
130 #: filenames to ignore in `listdir`
131 ignore_basenames = []
133 def rmtree(self):
134 """Shortcut: Call `shutil.rmtree` with ``self.path``."""
135 logger.info('rm -r "%s"', self.path)
136 shutil.rmtree(self.path)
138 def delete(self):
139 """Call `rmtree` if `isdir`, otherwise :func`unlink`."""
140 self.rmtree()
142 def join(self, *parts):
143 """Shortcut: Call and return `os.path.join` with ``self.path`` and the given parts."""
144 parts = [self._get_path(p) for p in parts]
145 return os.path.join(self.path, *parts)
147 def listdir(self, dirs_only=False):
148 """List files in this directory.
150 Call `os.listdir`, filter the basenames by `ignore_basenames` and return them.
151 Return an empty list if the file does not exist or if its not a directory.
153 :param dirs_only: `bool` - Return only directories. Default: ``False``
154 """
155 try:
156 listdir = os.listdir(self.path)
157 except (FileNotFoundError, NotADirectoryError):
158 return []
160 basenames = [n for n in listdir if n not in self.ignore_basenames]
161 if dirs_only:
162 basenames = [n for n in listdir if os.path.isdir(self.join(n))]
163 return basenames
165 def mkdir(self, mode=0o750):
166 """Shortcut: Call `os.mkdir` and `os.chown` with ``self.path``."""
167 logger.info('mkdir "%s" with mode %s', self.path, mode)
168 os.mkdir(self.path)
169 os.chmod(self.path, mode)
171 def makedirs(self, mode=0o750):
172 """Shortcut for `os.makedirs`."""
173 os.makedirs(self.path, mode=mode)
175 def copy(self, destination):
176 """Copy this directory recursively to the given destination."""
177 destination = self._clean_copy_destination(destination)
178 shutil.copytree(self.path, destination, ignore_dangling_symlinks=True)
181class File(Path):
182 """A path representing a file on disk."""
184 def unlink(self):
185 """Shortcut for `os.unlink`."""
186 logger.info('rm "%s"', self.path)
187 os.unlink(self.path)
189 def symlink(self, destination):
190 """Create a symlink at the given destination pointing to this file.
192 :param destination: `str` - Absolute path to point the symlink to
193 """
194 destination_path = self._get_path(destination)
195 os.symlink(self.path, destination_path)
197 def delete(self):
198 """Call `rmtree` if `isdir`, otherwise :func`unlink`."""
199 self.unlink()
201 def _read(self, path):
202 """Read and return the given paths contents.
204 :param path: `str` - The path to read from.
205 :return: `str` - The files contents.
206 """
207 path = self._get_path(path)
208 logger.debug('reading "%s"', path)
209 with open(path) as f:
210 return f.read()
212 def read(self):
213 """Read and return the file contents.
215 :return: `str` - The file contents as string.
216 """
217 return self._read(self)
219 def _write(self, path, content):
220 """Write the given content to the given path.
222 :param path: `str` - The path to write to
223 :param content: `str` - The content to write
224 """
225 path = self._get_path(path)
226 logger.debug('writing "%s"', path)
227 with open(path, 'w') as f:
228 f.write(content)
230 def write(self, content):
231 """Write the given content into the file.
233 :param content: `str` - The string to write to the file.
234 """
235 self._write(self, content)
237 def copy(self, destination):
238 """Copy this file to the given destination."""
239 destination = self._clean_copy_destination(destination)
240 os.makedirs(os.path.dirname(destination), exist_ok=True)
241 shutil.copy2(self.path, destination, follow_symlinks=True)