_common.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  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(
  100. page: Path, pages_dirs: Path, check_exists: bool = True
  101. ) -> list[Path]:
  102. """
  103. Get all paths in all languages that match the page.
  104. Parameters:
  105. page (Path): the page to search for.
  106. pages_dirs (Path): directories to search in
  107. check_exists (bool): whether to only return existing paths (default: True)
  108. Returns:
  109. list (list of Path's): A list of Path's.
  110. """
  111. target_paths = []
  112. if not page.lower().endswith(".md"):
  113. page = f"{page}.md"
  114. arg_platform, arg_page = page.split("/")
  115. for pages_dir in pages_dirs:
  116. page_path = pages_dir / arg_platform / arg_page
  117. if check_exists and not page_path.exists():
  118. print(create_colored_line(Colors.RED, f"Page {page_path} does not exist"))
  119. continue
  120. target_paths.append(page_path)
  121. target_paths.sort()
  122. return target_paths
  123. def test_get_target_paths():
  124. root = Path("test_root")
  125. shutil.rmtree(root, True)
  126. root.mkdir(exist_ok=True)
  127. shutil.os.makedirs(root / "pages" / "common")
  128. shutil.os.makedirs(root / "pages.fr" / "common")
  129. file_path = root / "pages" / "common" / "tldr.md"
  130. with open(file_path, "w"):
  131. pass
  132. file_path = root / "pages.fr" / "common" / "tldr.md"
  133. with open(file_path, "w"):
  134. pass
  135. target_paths = get_target_paths("common/tldr", get_pages_dir(root))
  136. for path in target_paths:
  137. rel_path = "/".join(path.parts[-3:])
  138. print(rel_path)
  139. shutil.rmtree(root, True)
  140. def get_locale(path: Path) -> str:
  141. """
  142. Get the locale from the path.
  143. Parameters:
  144. path (Path): the path to extract the locale.
  145. Returns:
  146. str: a POSIX Locale Name in the form of "ll" or "ll_CC" (e.g. "fr" or "pt_BR").
  147. """
  148. # compute locale
  149. pages_dirname = path.parents[1].name
  150. if "." in pages_dirname:
  151. _, locale = pages_dirname.split(".")
  152. else:
  153. locale = "en"
  154. return locale
  155. def test_get_locale():
  156. assert get_locale(Path("path/to/pages.fr/common/tldr.md")) == "fr"
  157. assert get_locale(Path("path/to/pages/common/tldr.md")) == "en"
  158. assert get_locale(Path("path/to/other/common/tldr.md")) == "en"
  159. def get_status(action: str, dry_run: bool, type: str) -> str:
  160. """
  161. Get a colored status line.
  162. Parameters:
  163. action (str): The action to perform.
  164. dry_run (bool): Whether to perform a dry-run.
  165. type (str): The kind of object to modify (alias, link).
  166. Returns:
  167. str: A colored line
  168. """
  169. match action:
  170. case "added":
  171. start_color = Colors.CYAN
  172. case "updated":
  173. start_color = Colors.BLUE
  174. case _:
  175. start_color = Colors.RED
  176. if dry_run:
  177. status = f"{type} would be {action}"
  178. else:
  179. status = f"{type} {action}"
  180. return create_colored_line(start_color, status)
  181. def test_get_status():
  182. # Test dry run status
  183. assert (
  184. get_status("added", True, "alias")
  185. == f"{Colors.CYAN}alias would be added{Colors.RESET}"
  186. )
  187. assert (
  188. get_status("updated", True, "link")
  189. == f"{Colors.BLUE}link would be updated{Colors.RESET}"
  190. )
  191. # Test non-dry run status
  192. assert (
  193. get_status("added", False, "alias") == f"{Colors.CYAN}alias added{Colors.RESET}"
  194. )
  195. assert (
  196. get_status("updated", False, "link")
  197. == f"{Colors.BLUE}link updated{Colors.RESET}"
  198. )
  199. # Test default color for unknown action
  200. assert (
  201. get_status("unknown", True, "alias")
  202. == f"{Colors.RED}alias would be unknown{Colors.RESET}"
  203. )
  204. def create_colored_line(start_color: str, text: str) -> str:
  205. """
  206. Create a colored line.
  207. Parameters:
  208. start_color (str): The color for the line.
  209. text (str): The text to display.
  210. Returns:
  211. str: A colored line
  212. """
  213. return f"{start_color}{text}{Colors.RESET}"
  214. def test_create_colored_line():
  215. assert (
  216. create_colored_line(Colors.CYAN, "TLDR") == f"{Colors.CYAN}TLDR{Colors.RESET}"
  217. )
  218. assert create_colored_line("Hello", "TLDR") == f"HelloTLDR{Colors.RESET}"
  219. def create_argument_parser(description: str) -> argparse.ArgumentParser:
  220. """
  221. Create an argument parser that can be extended.
  222. Parameters:
  223. description (str): The description for the argument parser
  224. Returns:
  225. ArgumentParser: an argument parser.
  226. """
  227. parser = argparse.ArgumentParser(description=description)
  228. parser.add_argument(
  229. "-p",
  230. "--page",
  231. type=str,
  232. default="",
  233. help='page name in the format "platform/alias_command.md"',
  234. )
  235. parser.add_argument(
  236. "-S",
  237. "--sync",
  238. action="store_true",
  239. default=False,
  240. help="synchronize each translation's alias page (if exists) with that of English page",
  241. )
  242. parser.add_argument(
  243. "-l",
  244. "--language",
  245. type=str,
  246. default="",
  247. help='language in the format "ll" or "ll_CC" (e.g. "fr" or "pt_BR")',
  248. )
  249. parser.add_argument(
  250. "-s",
  251. "--stage",
  252. action="store_true",
  253. default=False,
  254. help="stage modified pages (requires `git` to be on $PATH and TLDR_ROOT to be a Git repository)",
  255. )
  256. parser.add_argument(
  257. "-n",
  258. "--dry-run",
  259. action="store_true",
  260. default=False,
  261. help="show what changes would be made without actually modifying the pages",
  262. )
  263. return parser
  264. def test_create_argument_parser():
  265. description = "Test argument parser"
  266. parser = create_argument_parser(description)
  267. assert isinstance(parser, argparse.ArgumentParser)
  268. assert parser.description == description
  269. # Check if each expected argument is added with the correct configurations
  270. arguments = [
  271. ("-p", "--page", str, ""),
  272. ("-l", "--language", str, ""),
  273. ("-s", "--stage", None, False),
  274. ("-S", "--sync", None, False),
  275. ("-n", "--dry-run", None, False),
  276. ]
  277. for short_flag, long_flag, arg_type, default_value in arguments:
  278. action = parser._option_string_actions[short_flag] # Get action for short flag
  279. assert action.dest.replace("_", "-") == long_flag.lstrip(
  280. "-"
  281. ) # Check destination name
  282. assert action.type == arg_type # Check argument type
  283. assert action.default == default_value # Check default value
  284. def stage(paths: list[Path]):
  285. """
  286. Stage the given paths using Git.
  287. Parameters:
  288. paths (list of Paths): the list of Path's to stage using Git.
  289. """
  290. subprocess.call(["git", "add", *(path.resolve() for path in paths)])
  291. @patch("subprocess.call")
  292. def test_stage(mock_subprocess_call):
  293. paths = [Path("/path/to/file1"), Path("/path/to/file2")]
  294. # Call the stage function
  295. stage(paths)
  296. # Verify that subprocess.call was called with the correct arguments
  297. mock_subprocess_call.assert_called_once_with(["git", "add", *paths])