_paths.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. # Copyright 2022-present, the HuggingFace Inc. team.
  2. #
  3. # Licensed under the Apache License, Version 2.0 (the "License");
  4. # you may not use this file except in compliance with the License.
  5. # You may obtain a copy of the License at
  6. #
  7. # http://www.apache.org/licenses/LICENSE-2.0
  8. #
  9. # Unless required by applicable law or agreed to in writing, software
  10. # distributed under the License is distributed on an "AS IS" BASIS,
  11. # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. # See the License for the specific language governing permissions and
  13. # limitations under the License.
  14. """Contains utilities to handle paths in Huggingface Hub."""
  15. from collections.abc import Callable, Generator, Iterable
  16. from fnmatch import fnmatch
  17. from pathlib import Path
  18. from typing import TypeVar
  19. T = TypeVar("T")
  20. # Always ignore `.git` and `.cache/huggingface` folders in commits
  21. DEFAULT_IGNORE_PATTERNS = [
  22. ".git",
  23. ".git/*",
  24. "*/.git",
  25. "**/.git/**",
  26. ".cache/huggingface",
  27. ".cache/huggingface/*",
  28. "*/.cache/huggingface",
  29. "**/.cache/huggingface/**",
  30. ]
  31. # Forbidden to commit these folders
  32. FORBIDDEN_FOLDERS = [".git", ".cache"]
  33. def filter_repo_objects(
  34. items: Iterable[T],
  35. *,
  36. allow_patterns: list[str] | str | None = None,
  37. ignore_patterns: list[str] | str | None = None,
  38. key: Callable[[T], str] | None = None,
  39. ) -> Generator[T, None, None]:
  40. """Filter repo objects based on an allowlist and a denylist.
  41. Input must be a list of paths (`str` or `Path`) or a list of arbitrary objects.
  42. In the later case, `key` must be provided and specifies a function of one argument
  43. that is used to extract a path from each element in iterable.
  44. Patterns are Standard Wildcards (globbing patterns), NOT regular expressions.
  45. The pattern matching is based on Python's `fnmatch`. Note that `fnmatch` matches
  46. `*` across path boundaries, unlike traditional Unix shell globbing. For example,
  47. `"data/*.json"` will match both `data/file.json` and `data/subdir/file.json`.
  48. See https://docs.python.org/3/library/fnmatch.html for more details.
  49. Args:
  50. items (`Iterable`):
  51. List of items to filter.
  52. allow_patterns (`str` or `list[str]`, *optional*):
  53. Patterns constituting the allowlist. If provided, item paths must match at
  54. least one pattern from the allowlist.
  55. ignore_patterns (`str` or `list[str]`, *optional*):
  56. Patterns constituting the denylist. If provided, item paths must not match
  57. any patterns from the denylist.
  58. key (`Callable[[T], str]`, *optional*):
  59. Single-argument function to extract a path from each item. If not provided,
  60. the `items` must already be `str` or `Path`.
  61. Returns:
  62. Filtered list of objects, as a generator.
  63. Raises:
  64. :class:`ValueError`:
  65. If `key` is not provided and items are not `str` or `Path`.
  66. Example usage with paths:
  67. ```python
  68. >>> # Filter only PDFs that are not hidden.
  69. >>> list(filter_repo_objects(
  70. ... ["aaa.PDF", "bbb.jpg", ".ccc.pdf", ".ddd.png"],
  71. ... allow_patterns=["*.pdf"],
  72. ... ignore_patterns=[".*"],
  73. ... ))
  74. ["aaa.pdf"]
  75. ```
  76. Example usage with objects:
  77. ```python
  78. >>> list(filter_repo_objects(
  79. ... [
  80. ... CommitOperationAdd(path_or_fileobj="/tmp/aaa.pdf", path_in_repo="aaa.pdf")
  81. ... CommitOperationAdd(path_or_fileobj="/tmp/bbb.jpg", path_in_repo="bbb.jpg")
  82. ... CommitOperationAdd(path_or_fileobj="/tmp/.ccc.pdf", path_in_repo=".ccc.pdf")
  83. ... CommitOperationAdd(path_or_fileobj="/tmp/.ddd.png", path_in_repo=".ddd.png")
  84. ... ],
  85. ... allow_patterns=["*.pdf"],
  86. ... ignore_patterns=[".*"],
  87. ... key=lambda x: x.repo_in_path
  88. ... ))
  89. [CommitOperationAdd(path_or_fileobj="/tmp/aaa.pdf", path_in_repo="aaa.pdf")]
  90. ```
  91. """
  92. if isinstance(allow_patterns, str):
  93. allow_patterns = [allow_patterns]
  94. if isinstance(ignore_patterns, str):
  95. ignore_patterns = [ignore_patterns]
  96. if allow_patterns is not None:
  97. allow_patterns = [_add_wildcard_to_directories(p) for p in allow_patterns]
  98. if ignore_patterns is not None:
  99. ignore_patterns = [_add_wildcard_to_directories(p) for p in ignore_patterns]
  100. if key is None:
  101. def _identity(item: T) -> str:
  102. if isinstance(item, str):
  103. return item
  104. if isinstance(item, Path):
  105. return str(item)
  106. raise ValueError(f"Please provide `key` argument in `filter_repo_objects`: `{item}` is not a string.")
  107. key = _identity # Items must be `str` or `Path`, otherwise raise ValueError
  108. for item in items:
  109. path = key(item)
  110. # Skip if there's an allowlist and path doesn't match any
  111. if allow_patterns is not None and not any(fnmatch(path, r) for r in allow_patterns):
  112. continue
  113. # Skip if there's a denylist and path matches any
  114. if ignore_patterns is not None and any(fnmatch(path, r) for r in ignore_patterns):
  115. continue
  116. yield item
  117. def _add_wildcard_to_directories(pattern: str) -> str:
  118. if pattern[-1] == "/":
  119. return pattern + "*"
  120. return pattern