Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,15 @@
files: "^.+\\.py$"
language: system
stages: [commit]
- id: poetry-sphinx-build-language
# Add sphinx to your poetry environment to use this hook.
name: poetry-sphinx-build-language
description: "Build documentation for a specific language using sphinx from the local poetry environment."
entry: sphinx_build_language
language: python
pass_filenames: false
require_serial: true
stages: [manual]
- id: poetry-types-python
# Add mypy to your poetry environment to use this hook.
name: "types-python"
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ from = 'src'
include = 'cicd_tools_pre_commit'

[tool.poetry.scripts]
sphinx_build_language = 'cicd_tools_pre_commit:sphinx_build_language'
with-cicd-resources = 'cicd_tools_pre_commit:with_cicd_resources'

[tool.ruff]
Expand Down
6 changes: 5 additions & 1 deletion src/cicd_tools_pre_commit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""CICD-Tools pre-commit scripts."""

from .resources import with_cicd_resources
from .sphinx import sphinx_build_language

__all__ = ("with_cicd_resources", )
__all__ = (
"sphinx_build_language",
"with_cicd_resources",
)
1 change: 1 addition & 0 deletions src/cicd_tools_pre_commit/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""CLI related tools and types."""
1 change: 1 addition & 0 deletions src/cicd_tools_pre_commit/cli/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for CLI related tools and types."""
26 changes: 26 additions & 0 deletions src/cicd_tools_pre_commit/cli/tests/test_existing_directory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import argparse
import unittest
from unittest.mock import patch

from cicd_tools_pre_commit.cli.types import existing_directory


class TestExistingDirectory(unittest.TestCase):

@patch("os.path.isdir")
def test_existing_directory__valid_dir__returns_path(
self,
mock_isdir,
):
mock_isdir.return_value = True
path = "/valid/dir"
self.assertEqual(existing_directory(path), path)

@patch("os.path.isdir")
def test_existing_directory__invalid_dir__raises_error(
self,
mock_isdir,
):
mock_isdir.return_value = False
with self.assertRaises(argparse.ArgumentTypeError):
existing_directory("/invalid/dir")
29 changes: 29 additions & 0 deletions src/cicd_tools_pre_commit/cli/tests/test_language_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import argparse
import unittest

from cicd_tools_pre_commit.cli.types import language_code


class TestLanguageCode(unittest.TestCase):

def test_language_code__valid_short__returns_code(self):
self.assertEqual(language_code("en"), "en")

def test_language_code__valid_long__returns_code(self):
self.assertEqual(language_code("zh_CN"), "zh_CN")

def test_language_code__invalid_format__raises_error(self):
with self.assertRaises(argparse.ArgumentTypeError):
language_code("ENG")

def test_language_code__uppercase_short__raises_error(self):
with self.assertRaises(argparse.ArgumentTypeError):
language_code("EN")

def test_language_code__hyphenated__raises_error(self):
with self.assertRaises(argparse.ArgumentTypeError):
language_code("en-US")

def test_language_code__empty__raises_error(self):
with self.assertRaises(argparse.ArgumentTypeError):
language_code("")
38 changes: 38 additions & 0 deletions src/cicd_tools_pre_commit/cli/tests/test_valid_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import argparse
import unittest
from unittest.mock import patch

from cicd_tools_pre_commit.cli.types import valid_path


class TestValidPath(unittest.TestCase):

@patch("os.path.exists")
@patch("os.path.abspath")
@patch("os.path.dirname")
def test_valid_path__parent_exists__returns_path(
self,
mock_dirname,
mock_abspath,
mock_exists,
):
mock_abspath.return_value = "/valid/parent/file"
mock_dirname.return_value = "/valid/parent"
mock_exists.return_value = True
path = "file"
self.assertEqual(valid_path(path), path)

@patch("os.path.exists")
@patch("os.path.abspath")
@patch("os.path.dirname")
def test_valid_path__parent_missing__raises_error(
self,
mock_dirname,
mock_abspath,
mock_exists,
):
mock_abspath.return_value = "/invalid/parent/file"
mock_dirname.return_value = "/invalid/parent"
mock_exists.return_value = False
with self.assertRaises(argparse.ArgumentTypeError):
valid_path("file")
11 changes: 11 additions & 0 deletions src/cicd_tools_pre_commit/cli/types/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""CLI validation types."""

from .existing_directory import existing_directory
from .language_code import language_code
from .valid_path import valid_path

__all__ = (
"existing_directory",
"language_code",
"valid_path",
)
13 changes: 13 additions & 0 deletions src/cicd_tools_pre_commit/cli/types/existing_directory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""existing_directory argparse type."""

import argparse
import os


def existing_directory(path: str) -> str:
"""Check if the provided path is an existing directory."""
if not os.path.isdir(path):
raise argparse.ArgumentTypeError(
f"The directory '{path}' does not exist.",
)
return path
16 changes: 16 additions & 0 deletions src/cicd_tools_pre_commit/cli/types/language_code.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
"""language_code argparse type."""

import argparse
import re

LANGUAGE_CODE_REGEX = re.compile(r"^[a-z]{2}(_[A-Z]{2})?$")


def language_code(code: str) -> str:
"""Check if the language code matches the allowed pattern."""
if not LANGUAGE_CODE_REGEX.match(code):
raise argparse.ArgumentTypeError(
f"Language code '{code}' is invalid. "
"Expected format like 'en' or 'zh_CN'.",
)
return code
14 changes: 14 additions & 0 deletions src/cicd_tools_pre_commit/cli/types/valid_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"""valid_path argparse type."""

import argparse
import os


def valid_path(path: str) -> str:
"""Check if the provided path's parent directory exists."""
parent = os.path.dirname(os.path.abspath(path))
if not os.path.exists(parent):
raise argparse.ArgumentTypeError(
f"The parent directory of '{path}' does not exist.",
)
return path
53 changes: 53 additions & 0 deletions src/cicd_tools_pre_commit/sphinx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Sphinx related pre-commit scripts."""

import argparse
import os

from .cli.types import existing_directory, language_code, valid_path
from .system import call


def sphinx_build_language() -> None:
"""Build sphinx documentation for a specific language."""
parser = argparse.ArgumentParser(
description="Build sphinx documentation for a specific language.",
)
parser.add_argument(
"-l",
"--language",
required=True,
type=language_code,
help="The target language (e.g. en)",
)
parser.add_argument(
"-t",
"--source",
required=True,
type=existing_directory,
help="The source folder",
)
parser.add_argument(
"-b",
"--build",
required=True,
type=valid_path,
help="The build folder",
)

args = parser.parse_args()

target_build_folder = os.path.join(args.build, args.language)
command = [
"poetry",
"run",
"sphinx-build",
"-Ea",
"-b",
"html",
"-D",
f"language={args.language}",
args.source,
target_build_folder,
]

call(command)
102 changes: 102 additions & 0 deletions src/cicd_tools_pre_commit/tests/test_sphinx.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import unittest
from unittest.mock import patch

from cicd_tools_pre_commit.sphinx import sphinx_build_language


class TestSphinx(unittest.TestCase):

@patch(
"sys.argv",
["sphinx_build_language", "-l", "en", "-t", "source", "-b", "build"],
)
@patch("cicd_tools_pre_commit.sphinx.call")
@patch("cicd_tools_pre_commit.sphinx.existing_directory")
@patch("cicd_tools_pre_commit.sphinx.language_code")
@patch("cicd_tools_pre_commit.sphinx.valid_path")
def test_sphinx_build_language__valid_args__calls_sphinx_build(
self,
mock_valid_path,
mock_language_code,
mock_existing_directory,
mock_call,
):
mock_existing_directory.side_effect = lambda x: x
mock_language_code.side_effect = lambda x: x
mock_valid_path.side_effect = lambda x: x

sphinx_build_language()

mock_call.assert_called_once_with(
[
"poetry",
"run",
"sphinx-build",
"-Ea",
"-b",
"html",
"-D",
"language=en",
"source",
"build/en",
],
)

@patch(
"sys.argv",
[
"sphinx_build_language",
"-l",
"invalid",
"-t",
"source",
"-b",
"build",
],
)
@patch("cicd_tools_pre_commit.sphinx.call")
@patch("argparse.ArgumentParser.error")
@patch("cicd_tools_pre_commit.sphinx.existing_directory")
@patch("cicd_tools_pre_commit.sphinx.valid_path")
def test_sphinx_build_language__invalid_language__raises_system_exit(
self,
mock_valid_path,
mock_existing_directory,
mock_error,
mock_call,
):
mock_error.side_effect = SystemExit(2)
mock_existing_directory.side_effect = lambda x: x
mock_valid_path.side_effect = lambda x: x

with self.assertRaises(SystemExit):
sphinx_build_language()

mock_call.assert_not_called()

@patch(
"sys.argv",
["sphinx_build_language", "-l", "en", "-t", "missing", "-b", "build"],
)
@patch("cicd_tools_pre_commit.sphinx.call")
@patch("argparse.ArgumentParser.error")
@patch("cicd_tools_pre_commit.sphinx.existing_directory")
@patch("cicd_tools_pre_commit.sphinx.language_code")
@patch("cicd_tools_pre_commit.sphinx.valid_path")
def test_sphinx_build_language__missing_source__raises_system_exit(
self,
mock_valid_path,
mock_language_code,
mock_existing_directory,
mock_error,
mock_call,
):
mock_error.side_effect = SystemExit(2)
mock_existing_directory.side_effect = SystemExit(2)
mock_language_code.side_effect = lambda x: x
mock_valid_path.side_effect = lambda x: x

with self.assertRaises(SystemExit):
sphinx_build_language()

mock_call.assert_not_called()
Loading