#!/usr/bin/env python3 # SPDX-License-Identifier: MIT """ A Python file that makes some commonly used functions available for other scripts to use. """ from enum import Enum from pathlib import Path from unittest.mock import patch import shutil import os import argparse import subprocess IGNORE_FILES = (".DS_Store",) class Colors(str, Enum): def __str__(self): return str( self.value ) # make str(Colors.COLOR) return the ANSI code instead of an Enum object RED = "\x1b[31m" GREEN = "\x1b[32m" BLUE = "\x1b[34m" CYAN = "\x1b[36m" RESET = "\x1b[0m" def test_ignore_files(): assert IGNORE_FILES == (".DS_Store",) assert ".DS_Store" in IGNORE_FILES assert "tldr.md" not in IGNORE_FILES def get_tldr_root(lookup_path: Path = None) -> Path: """ 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. Parameters: lookup_path (Path): the path to search for the tldr root. By default, the path of the script. Returns: Path: the local tldr repository. """ if lookup_path is None: absolute_lookup_path = Path(__file__).resolve() else: absolute_lookup_path = Path(lookup_path).resolve() if ( tldr_root := next( (path for path in absolute_lookup_path.parents if path.name == "tldr"), None ) ) is not None: return tldr_root elif "TLDR_ROOT" in os.environ: return Path(os.environ["TLDR_ROOT"]) raise SystemExit( 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}" ) def test_get_tldr_root(): tldr_root = get_tldr_root("/path/to/tldr/scripts/test_script.py") assert tldr_root == Path("/path/to/tldr") # Set TLDR_ROOT in the environment os.environ["TLDR_ROOT"] = "/path/to/tldr_clone" tldr_root = get_tldr_root("/tmp") assert tldr_root == Path("/path/to/tldr_clone") del os.environ["TLDR_ROOT"] # Remove TLDR_ROOT from the environment original_env = os.environ.pop("TLDR_ROOT", None) # Check if SystemExit is raised raised = False try: get_tldr_root("/tmp") except SystemExit: raised = True assert raised # Restore the original values if original_env is not None: os.environ["TLDR_ROOT"] = original_env def get_pages_dir(root: Path) -> list[Path]: """ Get all pages directories. Parameters: root (Path): the path to search for the pages directories. Returns: list (list of Path's): Path's of page entry and platform, e.g. "page.fr/common". """ return [d for d in root.iterdir() if d.name.startswith("pages")] def test_get_pages_dir(): # Create temporary directories with names starting with "pages" root = Path("test_root") shutil.rmtree(root, True) root.mkdir(exist_ok=True) # Create temporary directories with names that do not start with "pages" (root / "other_dir_1").mkdir(exist_ok=True) (root / "other_dir_2").mkdir(exist_ok=True) # Call the function and verify that it returns an empty list result = get_pages_dir(root) assert result == [] (root / "pages").mkdir(exist_ok=True) (root / "pages.fr").mkdir(exist_ok=True) (root / "other_dir").mkdir(exist_ok=True) # Call the function and verify the result result = get_pages_dir(root) expected = [root / "pages", root / "pages.fr"] assert result.sort() == expected.sort() # the order differs on Unix / macOS shutil.rmtree(root, True) def get_target_paths(page: Path, pages_dirs: Path) -> list[Path]: """ Get all paths in all languages that match the page. Parameters: page (Path): the page to search for. Returns: list (list of Path's): A list of Path's. """ target_paths = [] if not page.lower().endswith(".md"): page = f"{page}.md" arg_platform, arg_page = page.split("/") for pages_dir in pages_dirs: page_path = pages_dir / arg_platform / arg_page if not page_path.exists(): continue target_paths.append(page_path) target_paths.sort() return target_paths def test_get_target_paths(): root = Path("test_root") shutil.rmtree(root, True) root.mkdir(exist_ok=True) shutil.os.makedirs(root / "pages" / "common") shutil.os.makedirs(root / "pages.fr" / "common") file_path = root / "pages" / "common" / "tldr.md" with open(file_path, "w"): pass file_path = root / "pages.fr" / "common" / "tldr.md" with open(file_path, "w"): pass target_paths = get_target_paths("common/tldr", get_pages_dir(root)) for path in target_paths: rel_path = "/".join(path.parts[-3:]) print(rel_path) shutil.rmtree(root, True) def get_locale(path: Path) -> str: """ Get the locale from the path. Parameters: path (Path): the path to extract the locale. Returns: str: a POSIX Locale Name in the form of "ll" or "ll_CC" (e.g. "fr" or "pt_BR"). """ # compute locale pages_dirname = path.parents[1].name if "." in pages_dirname: _, locale = pages_dirname.split(".") else: locale = "en" return locale def test_get_locale(): assert get_locale(Path("path/to/pages.fr/common/tldr.md")) == "fr" assert get_locale(Path("path/to/pages/common/tldr.md")) == "en" assert get_locale(Path("path/to/other/common/tldr.md")) == "en" def get_status(action: str, dry_run: bool, type: str) -> str: """ Get a colored status line. Parameters: action (str): The action to perform. dry_run (bool): Whether to perform a dry-run. type (str): The kind of object to modify (alias, link). Returns: str: A colored line """ match action: case "added": start_color = Colors.CYAN case "updated": start_color = Colors.BLUE case _: start_color = Colors.RED if dry_run: status = f"{type} would be {action}" else: status = f"{type} {action}" return create_colored_line(start_color, status) def test_get_status(): # Test dry run status assert ( get_status("added", True, "alias") == f"{Colors.CYAN}alias would be added{Colors.RESET}" ) assert ( get_status("updated", True, "link") == f"{Colors.BLUE}link would be updated{Colors.RESET}" ) # Test non-dry run status assert ( get_status("added", False, "alias") == f"{Colors.CYAN}alias added{Colors.RESET}" ) assert ( get_status("updated", False, "link") == f"{Colors.BLUE}link updated{Colors.RESET}" ) # Test default color for unknown action assert ( get_status("unknown", True, "alias") == f"{Colors.RED}alias would be unknown{Colors.RESET}" ) def create_colored_line(start_color: str, text: str) -> str: """ Create a colored line. Parameters: start_color (str): The color for the line. text (str): The text to display. Returns: str: A colored line """ return f"{start_color}{text}{Colors.RESET}" def test_create_colored_line(): assert ( create_colored_line(Colors.CYAN, "TLDR") == f"{Colors.CYAN}TLDR{Colors.RESET}" ) assert create_colored_line("Hello", "TLDR") == f"HelloTLDR{Colors.RESET}" def create_argument_parser(description: str) -> argparse.ArgumentParser: """ Create an argument parser that can be extended. Parameters: description (str): The description for the argument parser Returns: ArgumentParser: an argument parser. """ parser = argparse.ArgumentParser(description=description) parser.add_argument( "-p", "--page", type=str, default="", help='page name in the format "platform/alias_command.md"', ) parser.add_argument( "-S", "--sync", action="store_true", default=False, help="synchronize each translation's alias page (if exists) with that of English page", ) parser.add_argument( "-l", "--language", type=str, default="", help='language in the format "ll" or "ll_CC" (e.g. "fr" or "pt_BR")', ) parser.add_argument( "-s", "--stage", action="store_true", default=False, help="stage modified pages (requires `git` to be on $PATH and TLDR_ROOT to be a Git repository)", ) parser.add_argument( "-n", "--dry-run", action="store_true", default=False, help="show what changes would be made without actually modifying the pages", ) return parser def test_create_argument_parser(): description = "Test argument parser" parser = create_argument_parser(description) assert isinstance(parser, argparse.ArgumentParser) assert parser.description == description # Check if each expected argument is added with the correct configurations arguments = [ ("-p", "--page", str, ""), ("-l", "--language", str, ""), ("-s", "--stage", None, False), ("-S", "--sync", None, False), ("-n", "--dry-run", None, False), ] for short_flag, long_flag, arg_type, default_value in arguments: action = parser._option_string_actions[short_flag] # Get action for short flag assert action.dest.replace("_", "-") == long_flag.lstrip( "-" ) # Check destination name assert action.type == arg_type # Check argument type assert action.default == default_value # Check default value def stage(paths: list[Path]): """ Stage the given paths using Git. Parameters: paths (list of Paths): the list of Path's to stage using Git. """ subprocess.call(["git", "add", *(path.resolve() for path in paths)]) @patch("subprocess.call") def test_stage(mock_subprocess_call): paths = [Path("/path/to/file1"), Path("/path/to/file2")] # Call the stage function stage(paths) # Verify that subprocess.call was called with the correct arguments mock_subprocess_call.assert_called_once_with(["git", "add", *paths])