1# -*- coding: utf-8 -*- 2from __future__ import absolute_import 3 4import itertools 5import operator 6import os 7import warnings 8from abc import abstractmethod, abstractproperty 9from functools import reduce 10 11from plumbum.lib import six 12 13 14class FSUser(int): 15 """A special object that represents a file-system user. It derives from ``int``, so it behaves 16 just like a number (``uid``/``gid``), but also have a ``.name`` attribute that holds the 17 string-name of the user, if given (otherwise ``None``) 18 """ 19 20 def __new__(cls, val, name=None): 21 self = int.__new__(cls, val) 22 self.name = name 23 return self 24 25 26class Path(str, six.ABC): 27 """An abstraction over file system paths. This class is abstract, and the two implementations 28 are :class:`LocalPath <plumbum.machines.local.LocalPath>` and 29 :class:`RemotePath <plumbum.path.remote.RemotePath>`. 30 """ 31 32 CASE_SENSITIVE = True 33 34 def __repr__(self): 35 return "<{} {}>".format(self.__class__.__name__, str(self)) 36 37 def __div__(self, other): 38 """Joins two paths""" 39 return self.join(other) 40 41 __truediv__ = __div__ 42 43 def __getitem__(self, key): 44 if type(key) == str or isinstance(key, Path): 45 return self / key 46 return str(self)[key] 47 48 def __floordiv__(self, expr): 49 """Returns a (possibly empty) list of paths that matched the glob-pattern under this path""" 50 return self.glob(expr) 51 52 def __iter__(self): 53 """Iterate over the files in this directory""" 54 return iter(self.list()) 55 56 def __eq__(self, other): 57 if isinstance(other, Path): 58 return self._get_info() == other._get_info() 59 elif isinstance(other, str): 60 if self.CASE_SENSITIVE: 61 return str(self) == other 62 else: 63 return str(self).lower() == other.lower() 64 else: 65 return NotImplemented 66 67 def __ne__(self, other): 68 return not (self == other) 69 70 def __gt__(self, other): 71 return str(self) > str(other) 72 73 def __ge__(self, other): 74 return str(self) >= str(other) 75 76 def __lt__(self, other): 77 return str(self) < str(other) 78 79 def __le__(self, other): 80 return str(self) <= str(other) 81 82 def __hash__(self): 83 if self.CASE_SENSITIVE: 84 return hash(str(self)) 85 else: 86 return hash(str(self).lower()) 87 88 def __nonzero__(self): 89 return bool(str(self)) 90 91 __bool__ = __nonzero__ 92 93 def __fspath__(self): 94 """Added for Python 3.6 support""" 95 return str(self) 96 97 def __contains__(self, item): 98 """Paths should support checking to see if an file or folder is in them.""" 99 try: 100 return (self / item.name).exists() 101 except AttributeError: 102 return (self / item).exists() 103 104 @abstractmethod 105 def _form(self, *parts): 106 pass 107 108 def up(self, count=1): 109 """Go up in ``count`` directories (the default is 1)""" 110 return self.join("../" * count) 111 112 def walk( 113 self, filter=lambda p: True, dir_filter=lambda p: True 114 ): # @ReservedAssignment 115 """traverse all (recursive) sub-elements under this directory, that match the given filter. 116 By default, the filter accepts everything; you can provide a custom filter function that 117 takes a path as an argument and returns a boolean 118 119 :param filter: the filter (predicate function) for matching results. Only paths matching 120 this predicate are returned. Defaults to everything. 121 :param dir_filter: the filter (predicate function) for matching directories. Only directories 122 matching this predicate are recursed into. Defaults to everything. 123 """ 124 for p in self.list(): 125 if filter(p): 126 yield p 127 if p.is_dir() and dir_filter(p): 128 for p2 in p.walk(filter, dir_filter): 129 yield p2 130 131 @abstractproperty 132 def name(self): 133 """The basename component of this path""" 134 135 @property 136 def basename(self): 137 """Included for compatibility with older Plumbum code""" 138 warnings.warn("Use .name instead", DeprecationWarning) 139 return self.name 140 141 @abstractproperty 142 def stem(self): 143 """The name without an extension, or the last component of the path""" 144 145 @abstractproperty 146 def dirname(self): 147 """The dirname component of this path""" 148 149 @abstractproperty 150 def root(self): 151 """The root of the file tree (`/` on Unix)""" 152 153 @abstractproperty 154 def drive(self): 155 """The drive letter (on Windows)""" 156 157 @abstractproperty 158 def suffix(self): 159 """The suffix of this file""" 160 161 @abstractproperty 162 def suffixes(self): 163 """This is a list of all suffixes""" 164 165 @abstractproperty 166 def uid(self): 167 """The user that owns this path. The returned value is a :class:`FSUser <plumbum.path.FSUser>` 168 object which behaves like an ``int`` (as expected from ``uid``), but it also has a ``.name`` 169 attribute that holds the string-name of the user""" 170 171 @abstractproperty 172 def gid(self): 173 """The group that owns this path. The returned value is a :class:`FSUser <plumbum.path.FSUser>` 174 object which behaves like an ``int`` (as expected from ``gid``), but it also has a ``.name`` 175 attribute that holds the string-name of the group""" 176 177 @abstractmethod 178 def as_uri(self, scheme=None): 179 """Returns a universal resource identifier. Use ``scheme`` to force a scheme.""" 180 181 @abstractmethod 182 def _get_info(self): 183 pass 184 185 @abstractmethod 186 def join(self, *parts): 187 """Joins this path with any number of paths""" 188 189 @abstractmethod 190 def list(self): 191 """Returns the files in this directory""" 192 193 @abstractmethod 194 def iterdir(self): 195 """Returns an iterator over the directory. Might be slightly faster on Python 3.5 than .list()""" 196 197 @abstractmethod 198 def is_dir(self): 199 """Returns ``True`` if this path is a directory, ``False`` otherwise""" 200 201 def isdir(self): 202 """Included for compatibility with older Plumbum code""" 203 warnings.warn("Use .is_dir() instead", DeprecationWarning) 204 return self.is_dir() 205 206 @abstractmethod 207 def is_file(self): 208 """Returns ``True`` if this path is a regular file, ``False`` otherwise""" 209 210 def isfile(self): 211 """Included for compatibility with older Plumbum code""" 212 warnings.warn("Use .is_file() instead", DeprecationWarning) 213 return self.is_file() 214 215 def islink(self): 216 """Included for compatibility with older Plumbum code""" 217 warnings.warn("Use is_symlink instead", DeprecationWarning) 218 return self.is_symlink() 219 220 @abstractmethod 221 def is_symlink(self): 222 """Returns ``True`` if this path is a symbolic link, ``False`` otherwise""" 223 224 @abstractmethod 225 def exists(self): 226 """Returns ``True`` if this path exists, ``False`` otherwise""" 227 228 @abstractmethod 229 def stat(self): 230 """Returns the os.stats for a file""" 231 pass 232 233 @abstractmethod 234 def with_name(self, name): 235 """Returns a path with the name replaced""" 236 237 @abstractmethod 238 def with_suffix(self, suffix, depth=1): 239 """Returns a path with the suffix replaced. Up to last ``depth`` suffixes will be 240 replaced. None will replace all suffixes. If there are less than ``depth`` suffixes, 241 this will replace all suffixes. ``.tar.gz`` is an example where ``depth=2`` or 242 ``depth=None`` is useful""" 243 244 def preferred_suffix(self, suffix): 245 """Adds a suffix if one does not currently exist (otherwise, no change). Useful 246 for loading files with a default suffix""" 247 if len(self.suffixes) > 0: 248 return self 249 else: 250 return self.with_suffix(suffix) 251 252 @abstractmethod 253 def glob(self, pattern): 254 """Returns a (possibly empty) list of paths that matched the glob-pattern under this path""" 255 256 @abstractmethod 257 def delete(self): 258 """Deletes this path (recursively, if a directory)""" 259 260 @abstractmethod 261 def move(self, dst): 262 """Moves this path to a different location""" 263 264 def rename(self, newname): 265 """Renames this path to the ``new name`` (only the basename is changed)""" 266 return self.move(self.up() / newname) 267 268 @abstractmethod 269 def copy(self, dst, override=None): 270 """Copies this path (recursively, if a directory) to the destination path "dst". 271 Raises TypeError if dst exists and override is False. 272 Will overwrite if override is True. 273 Will silently fail to copy if override is None (the default).""" 274 275 @abstractmethod 276 def mkdir(self, mode=0o777, parents=True, exist_ok=True): 277 """ 278 Creates a directory at this path. 279 280 :param mode: **Currently only implemented for local paths!** Numeric mode to use for directory 281 creation, which may be ignored on some systems. The current implementation 282 reproduces the behavior of ``os.mkdir`` (i.e., the current umask is first masked 283 out), but this may change for remote paths. As with ``os.mkdir``, it is recommended 284 to call :func:`chmod` explicitly if you need to be sure. 285 :param parents: If this is true (the default), the directory's parents will also be created if 286 necessary. 287 :param exist_ok: If this is true (the default), no exception will be raised if the directory 288 already exists (otherwise ``OSError``). 289 290 Note that the defaults for ``parents`` and ``exist_ok`` are the opposite of what they are in 291 Python's own ``pathlib`` - this is to maintain backwards-compatibility with Plumbum's behaviour 292 from before they were implemented. 293 """ 294 295 @abstractmethod 296 def open(self, mode="r"): 297 """opens this path as a file""" 298 299 @abstractmethod 300 def read(self, encoding=None): 301 """returns the contents of this file as a ``str``. By default the data is read 302 as text, but you can specify the encoding, e.g., ``'latin1'`` or ``'utf8'``""" 303 304 @abstractmethod 305 def write(self, data, encoding=None): 306 """writes the given data to this file. By default the data is written as-is 307 (either text or binary), but you can specify the encoding, e.g., ``'latin1'`` 308 or ``'utf8'``""" 309 310 @abstractmethod 311 def touch(self): 312 """Update the access time. Creates an empty file if none exists.""" 313 314 @abstractmethod 315 def chown(self, owner=None, group=None, recursive=None): 316 """Change ownership of this path. 317 318 :param owner: The owner to set (either ``uid`` or ``username``), optional 319 :param group: The group to set (either ``gid`` or ``groupname``), optional 320 :param recursive: whether to change ownership of all contained files and subdirectories. 321 Only meaningful when ``self`` is a directory. If ``None``, the value 322 will default to ``True`` if ``self`` is a directory, ``False`` otherwise. 323 """ 324 325 @abstractmethod 326 def chmod(self, mode): 327 """Change the mode of path to the numeric mode. 328 329 :param mode: file mode as for os.chmod 330 """ 331 332 @staticmethod 333 def _access_mode_to_flags( 334 mode, flags={"f": os.F_OK, "w": os.W_OK, "r": os.R_OK, "x": os.X_OK} 335 ): 336 if isinstance(mode, str): 337 mode = reduce(operator.or_, [flags[m] for m in mode.lower()], 0) 338 return mode 339 340 @abstractmethod 341 def access(self, mode=0): 342 """Test file existence or permission bits 343 344 :param mode: a bitwise-or of access bits, or a string-representation thereof: 345 ``'f'``, ``'x'``, ``'r'``, ``'w'`` for ``os.F_OK``, ``os.X_OK``, 346 ``os.R_OK``, ``os.W_OK`` 347 """ 348 349 @abstractmethod 350 def link(self, dst): 351 """Creates a hard link from ``self`` to ``dst`` 352 353 :param dst: the destination path 354 """ 355 356 @abstractmethod 357 def symlink(self, dst): 358 """Creates a symbolic link from ``self`` to ``dst`` 359 360 :param dst: the destination path 361 """ 362 363 @abstractmethod 364 def unlink(self): 365 """Deletes a symbolic link""" 366 367 def split(self, *dummy_args, **dummy_kargs): 368 """Splits the path on directory separators, yielding a list of directories, e.g, 369 ``"/var/log/messages"`` will yield ``['var', 'log', 'messages']``. 370 """ 371 parts = [] 372 path = self 373 while path != path.dirname: 374 parts.append(path.name) 375 path = path.dirname 376 return parts[::-1] 377 378 @property 379 def parts(self): 380 """Splits the directory into parts, including the base directroy, returns a tuple""" 381 return tuple([self.drive + self.root] + self.split()) 382 383 def relative_to(self, source): 384 """Computes the "relative path" require to get from ``source`` to ``self``. They satisfy the invariant 385 ``source_path + (target_path - source_path) == target_path``. For example:: 386 387 /var/log/messages - /var/log/messages = [] 388 /var/log/messages - /var = [log, messages] 389 /var/log/messages - / = [var, log, messages] 390 /var/log/messages - /var/tmp = [.., log, messages] 391 /var/log/messages - /opt = [.., var, log, messages] 392 /var/log/messages - /opt/lib = [.., .., var, log, messages] 393 """ 394 if isinstance(source, str): 395 source = self._form(source) 396 parts = self.split() 397 baseparts = source.split() 398 ancestors = len( 399 list(itertools.takewhile(lambda p: p[0] == p[1], zip(parts, baseparts))) 400 ) 401 return RelativePath([".."] * (len(baseparts) - ancestors) + parts[ancestors:]) 402 403 def __sub__(self, other): 404 """Same as ``self.relative_to(other)``""" 405 return self.relative_to(other) 406 407 def _glob(self, pattern, fn): 408 """Applies a glob string or list/tuple/iterable to the current path, using ``fn``""" 409 if isinstance(pattern, str): 410 return fn(pattern) 411 else: 412 results = [] 413 for single_pattern in pattern: 414 results.extend(fn(single_pattern)) 415 return sorted(list(set(results))) 416 417 def resolve(self, strict=False): 418 """Added to allow pathlib like syntax. Does nothing since 419 Plumbum paths are always absolute. Does not (currently) resolve 420 symlinks.""" 421 # TODO: Resolve symlinks here 422 return self 423 424 @property 425 def parents(self): 426 """Pathlib like sequence of ancestors""" 427 join = lambda x, y: self._form(x) / y 428 as_list = ( 429 reduce(join, self.parts[:i], self.parts[0]) 430 for i in range(len(self.parts) - 1, 0, -1) 431 ) 432 return tuple(as_list) 433 434 @property 435 def parent(self): 436 """Pathlib like parent of the path.""" 437 return self.parents[0] 438 439 440class RelativePath(object): 441 """ 442 Relative paths are the "delta" required to get from one path to another. 443 Note that relative path do not point at anything, and thus are not paths. 444 Therefore they are system agnostic (but closed under addition) 445 Paths are always absolute and point at "something", whether existent or not. 446 447 Relative paths are created by subtracting paths (``Path.relative_to``) 448 """ 449 450 def __init__(self, parts): 451 self.parts = parts 452 453 def __str__(self): 454 return "/".join(self.parts) 455 456 def __iter__(self): 457 return iter(self.parts) 458 459 def __len__(self): 460 return len(self.parts) 461 462 def __getitem__(self, index): 463 return self.parts[index] 464 465 def __repr__(self): 466 return "RelativePath({!r})".format(self.parts) 467 468 def __eq__(self, other): 469 return str(self) == str(other) 470 471 def __ne__(self, other): 472 return not (self == other) 473 474 def __gt__(self, other): 475 return str(self) > str(other) 476 477 def __ge__(self, other): 478 return str(self) >= str(other) 479 480 def __lt__(self, other): 481 return str(self) < str(other) 482 483 def __le__(self, other): 484 return str(self) <= str(other) 485 486 def __hash__(self): 487 return hash(str(self)) 488 489 def __nonzero__(self): 490 return bool(str(self)) 491 492 __bool__ = __nonzero__ 493 494 def up(self, count=1): 495 return RelativePath(self.parts[:-count]) 496 497 def __radd__(self, path): 498 return path.join(*self.parts) 499