util.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792
  1. """
  2. This module provides utility methods for dealing with path-specs.
  3. """
  4. import os
  5. import os.path
  6. import pathlib
  7. import posixpath
  8. import stat
  9. import sys
  10. import warnings
  11. from collections.abc import (
  12. Collection as CollectionType,
  13. Iterable as IterableType)
  14. from dataclasses import (
  15. dataclass)
  16. from os import (
  17. PathLike)
  18. from typing import (
  19. Any,
  20. AnyStr,
  21. Callable, # Replaced by `collections.abc.Callable` in 3.9.
  22. Collection, # Replaced by `collections.abc.Collection` in 3.9.
  23. Dict, # Replaced by `dict` in 3.9.
  24. Generic,
  25. Iterable, # Replaced by `collections.abc.Iterable` in 3.9.
  26. Iterator, # Replaced by `collections.abc.Iterator` in 3.9.
  27. List, # Replaced by `list` in 3.9.
  28. Optional, # Replaced by `X | None` in 3.10.
  29. Sequence, # Replaced by `collections.abc.Sequence` in 3.9.
  30. Set, # Replaced by `set` in 3.9.
  31. Tuple, # Replaced by `tuple` in 3.9.
  32. TypeVar,
  33. Union) # Replaced by `X | Y` in 3.10.
  34. from .pattern import (
  35. Pattern)
  36. if sys.version_info >= (3, 9):
  37. StrPath = Union[str, PathLike[str]]
  38. else:
  39. StrPath = Union[str, PathLike]
  40. TStrPath = TypeVar("TStrPath", bound=StrPath)
  41. """
  42. Type variable for :class:`str` or :class:`os.PathLike`.
  43. """
  44. NORMALIZE_PATH_SEPS = [
  45. __sep
  46. for __sep in [os.sep, os.altsep]
  47. if __sep and __sep != posixpath.sep
  48. ]
  49. """
  50. *NORMALIZE_PATH_SEPS* (:class:`list` of :class:`str`) contains the path
  51. separators that need to be normalized to the POSIX separator for the
  52. current operating system. The separators are determined by examining
  53. :data:`os.sep` and :data:`os.altsep`.
  54. """
  55. _registered_patterns = {}
  56. """
  57. *_registered_patterns* (:class:`dict`) maps a name (:class:`str`) to the
  58. registered pattern factory (:class:`~collections.abc.Callable`).
  59. """
  60. def append_dir_sep(path: pathlib.Path) -> str:
  61. """
  62. Appends the path separator to the path if the path is a directory.
  63. This can be used to aid in distinguishing between directories and
  64. files on the file-system by relying on the presence of a trailing path
  65. separator.
  66. *path* (:class:`pathlib.Path`) is the path to use.
  67. Returns the path (:class:`str`).
  68. """
  69. str_path = str(path)
  70. if path.is_dir():
  71. str_path += os.sep
  72. return str_path
  73. def check_match_file(
  74. patterns: Iterable[Tuple[int, Pattern]],
  75. file: str,
  76. ) -> Tuple[Optional[bool], Optional[int]]:
  77. """
  78. Check the file against the patterns.
  79. *patterns* (:class:`~collections.abc.Iterable`) yields each indexed pattern
  80. (:class:`tuple`) which contains the pattern index (:class:`int`) and actual
  81. pattern (:class:`~pathspec.pattern.Pattern`).
  82. *file* (:class:`str`) is the normalized file path to be matched
  83. against *patterns*.
  84. Returns a :class:`tuple` containing whether to include *file* (:class:`bool`
  85. or :data:`None`), and the index of the last matched pattern (:class:`int` or
  86. :data:`None`).
  87. """
  88. out_include: Optional[bool] = None
  89. out_index: Optional[int] = None
  90. for index, pattern in patterns:
  91. if pattern.include is not None and pattern.match_file(file) is not None:
  92. out_include = pattern.include
  93. out_index = index
  94. return out_include, out_index
  95. def detailed_match_files(
  96. patterns: Iterable[Pattern],
  97. files: Iterable[str],
  98. all_matches: Optional[bool] = None,
  99. ) -> Dict[str, 'MatchDetail']:
  100. """
  101. Matches the files to the patterns, and returns which patterns matched
  102. the files.
  103. *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
  104. contains the patterns to use.
  105. *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains
  106. the normalized file paths to be matched against *patterns*.
  107. *all_matches* (:class:`bool` or :data:`None`) is whether to return all
  108. matches patterns (:data:`True`), or only the last matched pattern
  109. (:data:`False`). Default is :data:`None` for :data:`False`.
  110. Returns the matched files (:class:`dict`) which maps each matched file
  111. (:class:`str`) to the patterns that matched in order (:class:`.MatchDetail`).
  112. """
  113. all_files = files if isinstance(files, CollectionType) else list(files)
  114. return_files = {}
  115. for pattern in patterns:
  116. if pattern.include is not None:
  117. result_files = pattern.match(all_files) # TODO: Replace with `.match_file()`.
  118. if pattern.include:
  119. # Add files and record pattern.
  120. for result_file in result_files:
  121. if result_file in return_files:
  122. if all_matches:
  123. return_files[result_file].patterns.append(pattern)
  124. else:
  125. return_files[result_file].patterns[0] = pattern
  126. else:
  127. return_files[result_file] = MatchDetail([pattern])
  128. else:
  129. # Remove files.
  130. for file in result_files:
  131. del return_files[file]
  132. return return_files
  133. def _filter_check_patterns(
  134. patterns: Iterable[Pattern],
  135. ) -> List[Tuple[int, Pattern]]:
  136. """
  137. Filters out null-patterns.
  138. *patterns* (:class:`Iterable` of :class:`.Pattern`) contains the
  139. patterns.
  140. Returns a :class:`list` containing each indexed pattern (:class:`tuple`) which
  141. contains the pattern index (:class:`int`) and the actual pattern
  142. (:class:`~pathspec.pattern.Pattern`).
  143. """
  144. return [
  145. (__index, __pat)
  146. for __index, __pat in enumerate(patterns)
  147. if __pat.include is not None
  148. ]
  149. def _is_iterable(value: Any) -> bool:
  150. """
  151. Check whether the value is an iterable (excludes strings).
  152. *value* is the value to check,
  153. Returns whether *value* is a iterable (:class:`bool`).
  154. """
  155. return isinstance(value, IterableType) and not isinstance(value, (str, bytes))
  156. def iter_tree_entries(
  157. root: StrPath,
  158. on_error: Optional[Callable[[OSError], None]] = None,
  159. follow_links: Optional[bool] = None,
  160. ) -> Iterator['TreeEntry']:
  161. """
  162. Walks the specified directory for all files and directories.
  163. *root* (:class:`str` or :class:`os.PathLike`) is the root directory to
  164. search.
  165. *on_error* (:class:`~collections.abc.Callable` or :data:`None`)
  166. optionally is the error handler for file-system exceptions. It will be
  167. called with the exception (:exc:`OSError`). Reraise the exception to
  168. abort the walk. Default is :data:`None` to ignore file-system
  169. exceptions.
  170. *follow_links* (:class:`bool` or :data:`None`) optionally is whether
  171. to walk symbolic links that resolve to directories. Default is
  172. :data:`None` for :data:`True`.
  173. Raises :exc:`RecursionError` if recursion is detected.
  174. Returns an :class:`~collections.abc.Iterator` yielding each file or
  175. directory entry (:class:`.TreeEntry`) relative to *root*.
  176. """
  177. if on_error is not None and not callable(on_error):
  178. raise TypeError(f"on_error:{on_error!r} is not callable.")
  179. if follow_links is None:
  180. follow_links = True
  181. yield from _iter_tree_entries_next(os.path.abspath(root), '', {}, on_error, follow_links)
  182. def _iter_tree_entries_next(
  183. root_full: str,
  184. dir_rel: str,
  185. memo: Dict[str, str],
  186. on_error: Callable[[OSError], None],
  187. follow_links: bool,
  188. ) -> Iterator['TreeEntry']:
  189. """
  190. Scan the directory for all descendant files.
  191. *root_full* (:class:`str`) the absolute path to the root directory.
  192. *dir_rel* (:class:`str`) the path to the directory to scan relative to
  193. *root_full*.
  194. *memo* (:class:`dict`) keeps track of ancestor directories
  195. encountered. Maps each ancestor real path (:class:`str`) to relative
  196. path (:class:`str`).
  197. *on_error* (:class:`~collections.abc.Callable` or :data:`None`)
  198. optionally is the error handler for file-system exceptions.
  199. *follow_links* (:class:`bool`) is whether to walk symbolic links that
  200. resolve to directories.
  201. Yields each entry (:class:`.TreeEntry`).
  202. """
  203. dir_full = os.path.join(root_full, dir_rel)
  204. dir_real = os.path.realpath(dir_full)
  205. # Remember each encountered ancestor directory and its canonical
  206. # (real) path. If a canonical path is encountered more than once,
  207. # recursion has occurred.
  208. if dir_real not in memo:
  209. memo[dir_real] = dir_rel
  210. else:
  211. raise RecursionError(real_path=dir_real, first_path=memo[dir_real], second_path=dir_rel)
  212. with os.scandir(dir_full) as scan_iter:
  213. node_ent: os.DirEntry
  214. for node_ent in scan_iter:
  215. node_rel = os.path.join(dir_rel, node_ent.name)
  216. # Inspect child node.
  217. try:
  218. node_lstat = node_ent.stat(follow_symlinks=False)
  219. except OSError as e:
  220. if on_error is not None:
  221. on_error(e)
  222. continue
  223. if node_ent.is_symlink():
  224. # Child node is a link, inspect the target node.
  225. try:
  226. node_stat = node_ent.stat()
  227. except OSError as e:
  228. if on_error is not None:
  229. on_error(e)
  230. continue
  231. else:
  232. node_stat = node_lstat
  233. if node_ent.is_dir(follow_symlinks=follow_links):
  234. # Child node is a directory, recurse into it and yield its
  235. # descendant files.
  236. yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat)
  237. yield from _iter_tree_entries_next(root_full, node_rel, memo, on_error, follow_links)
  238. elif node_ent.is_file() or node_ent.is_symlink():
  239. # Child node is either a file or an unfollowed link, yield it.
  240. yield TreeEntry(node_ent.name, node_rel, node_lstat, node_stat)
  241. # NOTE: Make sure to remove the canonical (real) path of the directory
  242. # from the ancestors memo once we are done with it. This allows the
  243. # same directory to appear multiple times. If this is not done, the
  244. # second occurrence of the directory will be incorrectly interpreted
  245. # as a recursion. See <https://github.com/cpburnz/python-path-specification/pull/7>.
  246. del memo[dir_real]
  247. def iter_tree_files(
  248. root: StrPath,
  249. on_error: Optional[Callable[[OSError], None]] = None,
  250. follow_links: Optional[bool] = None,
  251. ) -> Iterator[str]:
  252. """
  253. Walks the specified directory for all files.
  254. *root* (:class:`str` or :class:`os.PathLike`) is the root directory to
  255. search for files.
  256. *on_error* (:class:`~collections.abc.Callable` or :data:`None`)
  257. optionally is the error handler for file-system exceptions. It will be
  258. called with the exception (:exc:`OSError`). Reraise the exception to
  259. abort the walk. Default is :data:`None` to ignore file-system
  260. exceptions.
  261. *follow_links* (:class:`bool` or :data:`None`) optionally is whether
  262. to walk symbolic links that resolve to directories. Default is
  263. :data:`None` for :data:`True`.
  264. Raises :exc:`RecursionError` if recursion is detected.
  265. Returns an :class:`~collections.abc.Iterator` yielding the path to
  266. each file (:class:`str`) relative to *root*.
  267. """
  268. for entry in iter_tree_entries(root, on_error=on_error, follow_links=follow_links):
  269. if not entry.is_dir(follow_links):
  270. yield entry.path
  271. def iter_tree(root, on_error=None, follow_links=None):
  272. """
  273. DEPRECATED: The :func:`.iter_tree` function is an alias for the
  274. :func:`.iter_tree_files` function.
  275. """
  276. warnings.warn((
  277. "util.iter_tree() is deprecated. Use util.iter_tree_files() instead."
  278. ), DeprecationWarning, stacklevel=2)
  279. return iter_tree_files(root, on_error=on_error, follow_links=follow_links)
  280. def lookup_pattern(name: str) -> Callable[[AnyStr], Pattern]:
  281. """
  282. Lookups a registered pattern factory by name.
  283. *name* (:class:`str`) is the name of the pattern factory.
  284. Returns the registered pattern factory (:class:`~collections.abc.Callable`).
  285. If no pattern factory is registered, raises :exc:`KeyError`.
  286. """
  287. return _registered_patterns[name]
  288. def match_file(patterns: Iterable[Pattern], file: str) -> bool:
  289. """
  290. Matches the file to the patterns.
  291. *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
  292. contains the patterns to use.
  293. *file* (:class:`str`) is the normalized file path to be matched
  294. against *patterns*.
  295. Returns :data:`True` if *file* matched; otherwise, :data:`False`.
  296. """
  297. matched = False
  298. for pattern in patterns:
  299. if pattern.include is not None and pattern.match_file(file) is not None:
  300. matched = pattern.include
  301. return matched
  302. def match_files(
  303. patterns: Iterable[Pattern],
  304. files: Iterable[str],
  305. ) -> Set[str]:
  306. """
  307. DEPRECATED: This is an old function no longer used. Use the
  308. :func:`~pathspec.util.match_file` function with a loop for better results.
  309. Matches the files to the patterns.
  310. *patterns* (:class:`~collections.abc.Iterable` of :class:`~pathspec.pattern.Pattern`)
  311. contains the patterns to use.
  312. *files* (:class:`~collections.abc.Iterable` of :class:`str`) contains
  313. the normalized file paths to be matched against *patterns*.
  314. Returns the matched files (:class:`set` of :class:`str`).
  315. """
  316. warnings.warn((
  317. f"{__name__}.match_files() is deprecated. Use {__name__}.match_file() with "
  318. f"a loop for better results."
  319. ), DeprecationWarning, stacklevel=2)
  320. use_patterns = [__pat for __pat in patterns if __pat.include is not None]
  321. return_files = set()
  322. for file in files:
  323. if match_file(use_patterns, file):
  324. return_files.add(file)
  325. return return_files
  326. def normalize_file(
  327. file: StrPath,
  328. separators: Optional[Collection[str]] = None,
  329. ) -> str:
  330. """
  331. Normalizes the file path to use the POSIX path separator (i.e.,
  332. ``"/"``), and make the paths relative (remove leading ``"/"``).
  333. *file* (:class:`str` or :class:`os.PathLike`) is the file path.
  334. *separators* (:class:`~collections.abc.Collection` of :class:`str`; or
  335. ``None``) optionally contains the path separators to normalize.
  336. This does not need to include the POSIX path separator (``"/"``),
  337. but including it will not affect the results. Default is ``None``
  338. for ``NORMALIZE_PATH_SEPS``. To prevent normalization, pass an
  339. empty container (e.g., an empty tuple ``()``).
  340. Returns the normalized file path (:class:`str`).
  341. """
  342. # Normalize path separators.
  343. if separators is None:
  344. separators = NORMALIZE_PATH_SEPS
  345. # Convert path object to string.
  346. norm_file: str = os.fspath(file)
  347. for sep in separators:
  348. norm_file = norm_file.replace(sep, posixpath.sep)
  349. if norm_file.startswith('/'):
  350. # Make path relative.
  351. norm_file = norm_file[1:]
  352. elif norm_file.startswith('./'):
  353. # Remove current directory prefix.
  354. norm_file = norm_file[2:]
  355. return norm_file
  356. def normalize_files(
  357. files: Iterable[StrPath],
  358. separators: Optional[Collection[str]] = None,
  359. ) -> Dict[str, List[StrPath]]:
  360. """
  361. DEPRECATED: This function is no longer used. Use the :func:`.normalize_file`
  362. function with a loop for better results.
  363. Normalizes the file paths to use the POSIX path separator.
  364. *files* (:class:`~collections.abc.Iterable` of :class:`str` or
  365. :class:`os.PathLike`) contains the file paths to be normalized.
  366. *separators* (:class:`~collections.abc.Collection` of :class:`str`; or
  367. :data:`None`) optionally contains the path separators to normalize.
  368. See :func:`normalize_file` for more information.
  369. Returns a :class:`dict` mapping each normalized file path (:class:`str`)
  370. to the original file paths (:class:`list` of :class:`str` or
  371. :class:`os.PathLike`).
  372. """
  373. warnings.warn((
  374. "util.normalize_files() is deprecated. Use util.normalize_file() "
  375. "with a loop for better results."
  376. ), DeprecationWarning, stacklevel=2)
  377. norm_files = {}
  378. for path in files:
  379. norm_file = normalize_file(path, separators=separators)
  380. if norm_file in norm_files:
  381. norm_files[norm_file].append(path)
  382. else:
  383. norm_files[norm_file] = [path]
  384. return norm_files
  385. def register_pattern(
  386. name: str,
  387. pattern_factory: Callable[[AnyStr], Pattern],
  388. override: Optional[bool] = None,
  389. ) -> None:
  390. """
  391. Registers the specified pattern factory.
  392. *name* (:class:`str`) is the name to register the pattern factory
  393. under.
  394. *pattern_factory* (:class:`~collections.abc.Callable`) is used to
  395. compile patterns. It must accept an uncompiled pattern (:class:`str`)
  396. and return the compiled pattern (:class:`.Pattern`).
  397. *override* (:class:`bool` or :data:`None`) optionally is whether to
  398. allow overriding an already registered pattern under the same name
  399. (:data:`True`), instead of raising an :exc:`AlreadyRegisteredError`
  400. (:data:`False`). Default is :data:`None` for :data:`False`.
  401. """
  402. if not isinstance(name, str):
  403. raise TypeError(f"name:{name!r} is not a string.")
  404. if not callable(pattern_factory):
  405. raise TypeError(f"pattern_factory:{pattern_factory!r} is not callable.")
  406. if name in _registered_patterns and not override:
  407. raise AlreadyRegisteredError(name, _registered_patterns[name])
  408. _registered_patterns[name] = pattern_factory
  409. class AlreadyRegisteredError(Exception):
  410. """
  411. The :exc:`AlreadyRegisteredError` exception is raised when a pattern
  412. factory is registered under a name already in use.
  413. """
  414. def __init__(
  415. self,
  416. name: str,
  417. pattern_factory: Callable[[AnyStr], Pattern],
  418. ) -> None:
  419. """
  420. Initializes the :exc:`AlreadyRegisteredError` instance.
  421. *name* (:class:`str`) is the name of the registered pattern.
  422. *pattern_factory* (:class:`~collections.abc.Callable`) is the
  423. registered pattern factory.
  424. """
  425. super(AlreadyRegisteredError, self).__init__(name, pattern_factory)
  426. @property
  427. def message(self) -> str:
  428. """
  429. *message* (:class:`str`) is the error message.
  430. """
  431. return "{name!r} is already registered for pattern factory:{pattern_factory!r}.".format(
  432. name=self.name,
  433. pattern_factory=self.pattern_factory,
  434. )
  435. @property
  436. def name(self) -> str:
  437. """
  438. *name* (:class:`str`) is the name of the registered pattern.
  439. """
  440. return self.args[0]
  441. @property
  442. def pattern_factory(self) -> Callable[[AnyStr], Pattern]:
  443. """
  444. *pattern_factory* (:class:`~collections.abc.Callable`) is the
  445. registered pattern factory.
  446. """
  447. return self.args[1]
  448. class RecursionError(Exception):
  449. """
  450. The :exc:`RecursionError` exception is raised when recursion is
  451. detected.
  452. """
  453. def __init__(
  454. self,
  455. real_path: str,
  456. first_path: str,
  457. second_path: str,
  458. ) -> None:
  459. """
  460. Initializes the :exc:`RecursionError` instance.
  461. *real_path* (:class:`str`) is the real path that recursion was
  462. encountered on.
  463. *first_path* (:class:`str`) is the first path encountered for
  464. *real_path*.
  465. *second_path* (:class:`str`) is the second path encountered for
  466. *real_path*.
  467. """
  468. super(RecursionError, self).__init__(real_path, first_path, second_path)
  469. @property
  470. def first_path(self) -> str:
  471. """
  472. *first_path* (:class:`str`) is the first path encountered for
  473. :attr:`self.real_path <RecursionError.real_path>`.
  474. """
  475. return self.args[1]
  476. @property
  477. def message(self) -> str:
  478. """
  479. *message* (:class:`str`) is the error message.
  480. """
  481. return "Real path {real!r} was encountered at {first!r} and then {second!r}.".format(
  482. real=self.real_path,
  483. first=self.first_path,
  484. second=self.second_path,
  485. )
  486. @property
  487. def real_path(self) -> str:
  488. """
  489. *real_path* (:class:`str`) is the real path that recursion was
  490. encountered on.
  491. """
  492. return self.args[0]
  493. @property
  494. def second_path(self) -> str:
  495. """
  496. *second_path* (:class:`str`) is the second path encountered for
  497. :attr:`self.real_path <RecursionError.real_path>`.
  498. """
  499. return self.args[2]
  500. @dataclass(frozen=True)
  501. class CheckResult(Generic[TStrPath]):
  502. """
  503. The :class:`CheckResult` class contains information about the file and which
  504. pattern matched it.
  505. """
  506. # Make the class dict-less.
  507. __slots__ = (
  508. 'file',
  509. 'include',
  510. 'index',
  511. )
  512. file: TStrPath
  513. """
  514. *file* (:class:`str` or :class:`os.PathLike`) is the file path.
  515. """
  516. include: Optional[bool]
  517. """
  518. *include* (:class:`bool` or :data:`None`) is whether to include or exclude the
  519. file. If :data:`None`, no pattern matched.
  520. """
  521. index: Optional[int]
  522. """
  523. *index* (:class:`int` or :data:`None`) is the index of the last pattern that
  524. matched. If :data:`None`, no pattern matched.
  525. """
  526. class MatchDetail(object):
  527. """
  528. The :class:`.MatchDetail` class contains information about
  529. """
  530. # Make the class dict-less.
  531. __slots__ = ('patterns',)
  532. def __init__(self, patterns: Sequence[Pattern]) -> None:
  533. """
  534. Initialize the :class:`.MatchDetail` instance.
  535. *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.Pattern`)
  536. contains the patterns that matched the file in the order they were
  537. encountered.
  538. """
  539. self.patterns = patterns
  540. """
  541. *patterns* (:class:`~collections.abc.Sequence` of :class:`~pathspec.pattern.Pattern`)
  542. contains the patterns that matched the file in the order they were
  543. encountered.
  544. """
  545. class TreeEntry(object):
  546. """
  547. The :class:`.TreeEntry` class contains information about a file-system
  548. entry.
  549. """
  550. # Make the class dict-less.
  551. __slots__ = ('_lstat', 'name', 'path', '_stat')
  552. def __init__(
  553. self,
  554. name: str,
  555. path: str,
  556. lstat: os.stat_result,
  557. stat: os.stat_result,
  558. ) -> None:
  559. """
  560. Initialize the :class:`.TreeEntry` instance.
  561. *name* (:class:`str`) is the base name of the entry.
  562. *path* (:class:`str`) is the relative path of the entry.
  563. *lstat* (:class:`os.stat_result`) is the stat result of the direct
  564. entry.
  565. *stat* (:class:`os.stat_result`) is the stat result of the entry,
  566. potentially linked.
  567. """
  568. self._lstat: os.stat_result = lstat
  569. """
  570. *_lstat* (:class:`os.stat_result`) is the stat result of the direct
  571. entry.
  572. """
  573. self.name: str = name
  574. """
  575. *name* (:class:`str`) is the base name of the entry.
  576. """
  577. self.path: str = path
  578. """
  579. *path* (:class:`str`) is the path of the entry.
  580. """
  581. self._stat: os.stat_result = stat
  582. """
  583. *_stat* (:class:`os.stat_result`) is the stat result of the linked
  584. entry.
  585. """
  586. def is_dir(self, follow_links: Optional[bool] = None) -> bool:
  587. """
  588. Get whether the entry is a directory.
  589. *follow_links* (:class:`bool` or :data:`None`) is whether to follow
  590. symbolic links. If this is :data:`True`, a symlink to a directory
  591. will result in :data:`True`. Default is :data:`None` for :data:`True`.
  592. Returns whether the entry is a directory (:class:`bool`).
  593. """
  594. if follow_links is None:
  595. follow_links = True
  596. node_stat = self._stat if follow_links else self._lstat
  597. return stat.S_ISDIR(node_stat.st_mode)
  598. def is_file(self, follow_links: Optional[bool] = None) -> bool:
  599. """
  600. Get whether the entry is a regular file.
  601. *follow_links* (:class:`bool` or :data:`None`) is whether to follow
  602. symbolic links. If this is :data:`True`, a symlink to a regular file
  603. will result in :data:`True`. Default is :data:`None` for :data:`True`.
  604. Returns whether the entry is a regular file (:class:`bool`).
  605. """
  606. if follow_links is None:
  607. follow_links = True
  608. node_stat = self._stat if follow_links else self._lstat
  609. return stat.S_ISREG(node_stat.st_mode)
  610. def is_symlink(self) -> bool:
  611. """
  612. Returns whether the entry is a symbolic link (:class:`bool`).
  613. """
  614. return stat.S_ISLNK(self._lstat.st_mode)
  615. def stat(self, follow_links: Optional[bool] = None) -> os.stat_result:
  616. """
  617. Get the cached stat result for the entry.
  618. *follow_links* (:class:`bool` or :data:`None`) is whether to follow
  619. symbolic links. If this is :data:`True`, the stat result of the
  620. linked file will be returned. Default is :data:`None` for :data:`True`.
  621. Returns that stat result (:class:`os.stat_result`).
  622. """
  623. if follow_links is None:
  624. follow_links = True
  625. return self._stat if follow_links else self._lstat