_common.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. #!/usr/bin/env python3
  2. # SPDX-License-Identifier: MIT
  3. """
  4. A Python file that makes some commonly used functions available for other scripts to use.
  5. """
  6. from enum import Enum
  7. from pathlib import Path
  8. from unittest.mock import patch
  9. import shutil
  10. import os
  11. import argparse
  12. import subprocess
  13. IGNORE_FILES = (".DS_Store",)
  14. class Colors(str, Enum):
  15. def __str__(self):
  16. return str(
  17. self.value
  18. ) # make str(Colors.COLOR) return the ANSI code instead of an Enum object
  19. RED = "\x1b[31m"
  20. GREEN = "\x1b[32m"
  21. BLUE = "\x1b[34m"
  22. CYAN = "\x1b[36m"
  23. RESET = "\x1b[0m"
  24. def test_ignore_files():
  25. assert IGNORE_FILES == (".DS_Store",)
  26. assert ".DS_Store" in IGNORE_FILES
  27. assert "tldr.md" not in IGNORE_FILES
  28. def get_tldr_root(lookup_path: Path = None) -> Path:
  29. """
  30. Get the path of the local tldr repository, looking for it in each part of the given path. If it is not found, the path in the environment variable TLDR_ROOT is returned.
  31. Parameters:
  32. lookup_path (Path): the path to search for the tldr root. By default, the path of the script.
  33. Returns:
  34. Path: the local tldr repository.
  35. """
  36. if lookup_path is None:
  37. absolute_lookup_path = Path(__file__).resolve()
  38. else:
  39. absolute_lookup_path = Path(lookup_path).resolve()
  40. if (
  41. tldr_root := next(
  42. (path for path in absolute_lookup_path.parents if path.name == "tldr"), None
  43. )
  44. ) is not None:
  45. return tldr_root
  46. elif "TLDR_ROOT" in os.environ:
  47. return Path(os.environ["TLDR_ROOT"])
  48. raise SystemExit(
  49. f"{Colors.RED}Please set the environment variable TLDR_ROOT to the location of a clone of https://github.com/tldr-pages/tldr{Colors.RESET}"
  50. )
  51. def test_get_tldr_root():
  52. tldr_root = get_tldr_root("/path/to/tldr/scripts/test_script.py")
  53. assert tldr_root == Path("/path/to/tldr")
  54. # Set TLDR_ROOT in the environment
  55. os.environ["TLDR_ROOT"] = "/path/to/tldr_clone"
  56. tldr_root = get_tldr_root("/tmp")
  57. assert tldr_root == Path("/path/to/tldr_clone")
  58. del os.environ["TLDR_ROOT"]
  59. # Remove TLDR_ROOT from the environment
  60. original_env = os.environ.pop("TLDR_ROOT", None)
  61. # Check if SystemExit is raised
  62. raised = False
  63. try:
  64. get_tldr_root("/tmp")
  65. except SystemExit:
  66. raised = True
  67. assert raised
  68. # Restore the original values
  69. if original_env is not None:
  70. os.environ["TLDR_ROOT"] = original_env
  71. def get_pages_dir(root: Path) -> list[Path]:
  72. """
  73. Get all pages directories.
  74. Parameters:
  75. root (Path): the path to search for the pages directories.
  76. Returns:
  77. list (list of Path's): Path's of page entry and platform, e.g. "page.fr/common".
  78. """
  79. return [d for d in root.iterdir() if d.name.startswith("pages")]
  80. def test_get_pages_dir():
  81. # Create temporary directories with names starting with "pages"
  82. root = Path("test_root")
  83. shutil.rmtree(root, True)
  84. root.mkdir(exist_ok=True)
  85. # Create temporary directories with names that do not start with "pages"
  86. (root / "other_dir_1").mkdir(exist_ok=True)
  87. (root / "other_dir_2").mkdir(exist_ok=True)
  88. # Call the function and verify that it returns an empty list
  89. result = get_pages_dir(root)
  90. assert result == []
  91. (root / "pages").mkdir(exist_ok=True)
  92. (root / "pages.fr").mkdir(exist_ok=True)
  93. (root / "other_dir").mkdir(exist_ok=True)
  94. # Call the function and verify the result
  95. result = get_pages_dir(root)
  96. expected = [root / "pages", root / "pages.fr"]
  97. assert result.sort() == expected.sort() # the order differs on Unix / macOS
  98. shutil.rmtree(root, True)
  99. def get_target_paths(page: Path, pages_dirs: Path) -> list[Path]:
  100. """
  101. Get all paths in all languages that match the page.
  102. Parameters:
  103. page (Path): the page to search for.
  104. Returns:
  105. list (list of Path's): A list of Path's.
  106. """
  107. target_paths = []
  108. if not page.lower().endswith(".md"):
  109. page = f"{page}.md"
  110. arg_platform, arg_page = page.split("/")
  111. for pages_dir in pages_dirs:
  112. page_path = pages_dir / arg_platform / arg_page
  113. if not page_path.exists():
  114. continue
  115. target_paths.append(page_path)
  116. target_paths.sort()
  117. return target_paths
  118. def test_get_target_paths():
  119. root = Path("test_root")
  120. shutil.rmtree(root, True)
  121. root.mkdir(exist_ok=True)
  122. shutil.os.makedirs(root / "pages" / "common")
  123. shutil.os.makedirs(root / "pages.fr" / "common")
  124. file_path = root / "pages" / "common" / "tldr.md"
  125. with open(file_path, "w"):
  126. pass
  127. file_path = root / "pages.fr" / "common" / "tldr.md"
  128. with open(file_path, "w"):
  129. pass
  130. target_paths = get_target_paths("common/tldr", get_pages_dir(root))
  131. for path in target_paths:
  132. rel_path = "/".join(path.parts[-3:])
  133. print(rel_path)
  134. shutil.rmtree(root, True)
  135. def get_locale(path: Path) -> str:
  136. """
  137. Get the locale from the path.
  138. Parameters:
  139. path (Path): the path to extract the locale.
  140. Returns:
  141. str: a POSIX Locale Name in the form of "ll" or "ll_CC" (e.g. "fr" or "pt_BR").
  142. """
  143. # compute locale
  144. pages_dirname = path.parents[1].name
  145. if "." in pages_dirname:
  146. _, locale = pages_dirname.split(".")
  147. else:
  148. locale = "en"
  149. return locale
  150. def test_get_locale():
  151. assert get_locale(Path("path/to/pages.fr/common/tldr.md")) == "fr"
  152. assert get_locale(Path("path/to/pages/common/tldr.md")) == "en"
  153. assert get_locale(Path("path/to/other/common/tldr.md")) == "en"
  154. def get_status(action: str, dry_run: bool, type: str) -> str:
  155. """
  156. Get a colored status line.
  157. Parameters:
  158. action (str): The action to perform.
  159. dry_run (bool): Whether to perform a dry-run.
  160. type (str): The kind of object to modify (alias, link).
  161. Returns:
  162. str: A colored line
  163. """
  164. match action:
  165. case "added":
  166. start_color = Colors.CYAN
  167. case "updated":
  168. start_color = Colors.BLUE
  169. case _:
  170. start_color = Colors.RED
  171. if dry_run:
  172. status = f"{type} would be {action}"
  173. else:
  174. status = f"{type} {action}"
  175. return create_colored_line(start_color, status)
  176. def test_get_status():
  177. # Test dry run status
  178. assert (
  179. get_status("added", True, "alias")
  180. == f"{Colors.CYAN}alias would be added{Colors.RESET}"
  181. )
  182. assert (
  183. get_status("updated", True, "link")
  184. == f"{Colors.BLUE}link would be updated{Colors.RESET}"
  185. )
  186. # Test non-dry run status
  187. assert (
  188. get_status("added", False, "alias") == f"{Colors.CYAN}alias added{Colors.RESET}"
  189. )
  190. assert (
  191. get_status("updated", False, "link")
  192. == f"{Colors.BLUE}link updated{Colors.RESET}"
  193. )
  194. # Test default color for unknown action
  195. assert (
  196. get_status("unknown", True, "alias")
  197. == f"{Colors.RED}alias would be unknown{Colors.RESET}"
  198. )
  199. def create_colored_line(start_color: str, text: str) -> str:
  200. """
  201. Create a colored line.
  202. Parameters:
  203. start_color (str): The color for the line.
  204. text (str): The text to display.
  205. Returns:
  206. str: A colored line
  207. """
  208. return f"{start_color}{text}{Colors.RESET}"
  209. def test_create_colored_line():
  210. assert (
  211. create_colored_line(Colors.CYAN, "TLDR") == f"{Colors.CYAN}TLDR{Colors.RESET}"
  212. )
  213. assert create_colored_line("Hello", "TLDR") == f"HelloTLDR{Colors.RESET}"
  214. def create_argument_parser(description: str) -> argparse.ArgumentParser:
  215. """
  216. Create an argument parser that can be extended.
  217. Parameters:
  218. description (str): The description for the argument parser
  219. Returns:
  220. ArgumentParser: an argument parser.
  221. """
  222. parser = argparse.ArgumentParser(description=description)
  223. parser.add_argument(
  224. "-p",
  225. "--page",
  226. type=str,
  227. default="",
  228. help='page name in the format "platform/alias_command.md"',
  229. )
  230. parser.add_argument(
  231. "-S",
  232. "--sync",
  233. action="store_true",
  234. default=False,
  235. help="synchronize each translation's alias page (if exists) with that of English page",
  236. )
  237. parser.add_argument(
  238. "-l",
  239. "--language",
  240. type=str,
  241. default="",
  242. help='language in the format "ll" or "ll_CC" (e.g. "fr" or "pt_BR")',
  243. )
  244. parser.add_argument(
  245. "-s",
  246. "--stage",
  247. action="store_true",
  248. default=False,
  249. help="stage modified pages (requires `git` to be on $PATH and TLDR_ROOT to be a Git repository)",
  250. )
  251. parser.add_argument(
  252. "-n",
  253. "--dry-run",
  254. action="store_true",
  255. default=False,
  256. help="show what changes would be made without actually modifying the pages",
  257. )
  258. return parser
  259. def test_create_argument_parser():
  260. description = "Test argument parser"
  261. parser = create_argument_parser(description)
  262. assert isinstance(parser, argparse.ArgumentParser)
  263. assert parser.description == description
  264. # Check if each expected argument is added with the correct configurations
  265. arguments = [
  266. ("-p", "--page", str, ""),
  267. ("-l", "--language", str, ""),
  268. ("-s", "--stage", None, False),
  269. ("-S", "--sync", None, False),
  270. ("-n", "--dry-run", None, False),
  271. ]
  272. for short_flag, long_flag, arg_type, default_value in arguments:
  273. action = parser._option_string_actions[short_flag] # Get action for short flag
  274. assert action.dest.replace("_", "-") == long_flag.lstrip(
  275. "-"
  276. ) # Check destination name
  277. assert action.type == arg_type # Check argument type
  278. assert action.default == default_value # Check default value
  279. def stage(paths: list[Path]):
  280. """
  281. Stage the given paths using Git.
  282. Parameters:
  283. paths (list of Paths): the list of Path's to stage using Git.
  284. """
  285. subprocess.call(["git", "add", *(path.resolve() for path in paths)])
  286. @patch("subprocess.call")
  287. def test_stage(mock_subprocess_call):
  288. paths = [Path("/path/to/file1"), Path("/path/to/file2")]
  289. # Call the stage function
  290. stage(paths)
  291. # Verify that subprocess.call was called with the correct arguments
  292. mock_subprocess_call.assert_called_once_with(["git", "add", *paths])