diff --git a/sphinx/application.py b/sphinx/application.py index 98d9854acde..417dd3f7bc3 100644 --- a/sphinx/application.py +++ b/sphinx/application.py @@ -316,9 +316,9 @@ def _init_i18n(self) -> None: catalog.write_mo(self.config.language, self.config.gettext_allow_fuzzy_translations) - locale_dirs: list[str | None] = list(repo.locale_dirs) + locale_dirs: list[_StrPath | None] = list(repo.locale_dirs) locale_dirs += [None] - locale_dirs += [path.join(package_dir, 'locale')] + locale_dirs += [_StrPath(package_dir, 'locale')] self.translator, has_translation = locale.init(locale_dirs, self.config.language) if has_translation or self.config.language == 'en': diff --git a/sphinx/environment/__init__.py b/sphinx/environment/__init__.py index 85e2dc9a930..34ef4cc8066 100644 --- a/sphinx/environment/__init__.py +++ b/sphinx/environment/__init__.py @@ -463,7 +463,7 @@ def find_files(self, config: Config, builder: Builder) -> None: for docname in self.found_docs: domain = docname_to_domain(docname, self.config.gettext_compact) if domain in mo_paths: - self.dependencies[docname].add(mo_paths[domain]) + self.dependencies[docname].add(str(mo_paths[domain])) except OSError as exc: raise DocumentError( __('Failed to scan documents in %s: %r') % (self.srcdir, exc) diff --git a/sphinx/util/i18n.py b/sphinx/util/i18n.py index 2a30c1b0f7c..cd5619a729e 100644 --- a/sphinx/util/i18n.py +++ b/sphinx/util/i18n.py @@ -6,7 +6,7 @@ import re from datetime import datetime from os import path -from typing import TYPE_CHECKING, NamedTuple +from typing import TYPE_CHECKING import babel.dates from babel.messages.mofile import write_mo @@ -15,11 +15,11 @@ from sphinx.errors import SphinxError from sphinx.locale import __ from sphinx.util import logging +from sphinx.util._pathlib import _StrPath from sphinx.util.osutil import ( SEP, _last_modified_time, canon_path, - relpath, ) if TYPE_CHECKING: @@ -64,34 +64,36 @@ def __call__( # NoQA: E704 logger = logging.getLogger(__name__) -class LocaleFileInfoBase(NamedTuple): - base_dir: str - domain: str - charset: str +class CatalogInfo: + __slots__ = ('base_dir', 'domain', 'charset') + def __init__( + self, base_dir: str | os.PathLike[str], domain: str, charset: str + ) -> None: + self.base_dir = _StrPath(base_dir) + self.domain = domain + self.charset = charset -class CatalogInfo(LocaleFileInfoBase): @property def po_file(self) -> str: - return self.domain + '.po' + return f'{self.domain}.po' @property def mo_file(self) -> str: - return self.domain + '.mo' + return f'{self.domain}.mo' @property - def po_path(self) -> str: - return path.join(self.base_dir, self.po_file) + def po_path(self) -> _StrPath: + return self.base_dir / self.po_file @property - def mo_path(self) -> str: - return path.join(self.base_dir, self.mo_file) + def mo_path(self) -> _StrPath: + return self.base_dir / self.mo_file def is_outdated(self) -> bool: - return ( - not path.exists(self.mo_path) - or _last_modified_time(self.mo_path) < _last_modified_time(self.po_path) - ) # fmt: skip + return not self.mo_path.exists() or ( + _last_modified_time(self.mo_path) < _last_modified_time(self.po_path) + ) def write_mo(self, locale: str, use_fuzzy: bool = False) -> None: with open(self.po_path, encoding=self.charset) as file_po: @@ -118,38 +120,33 @@ def __init__( language: str, encoding: str, ) -> None: - self.basedir = basedir + self.basedir = _StrPath(basedir) self._locale_dirs = locale_dirs self.language = language self.encoding = encoding @property - def locale_dirs(self) -> Iterator[str]: + def locale_dirs(self) -> Iterator[_StrPath]: if not self.language: return for locale_dir in self._locale_dirs: - locale_dir = path.join(self.basedir, locale_dir) - locale_path = path.join(locale_dir, self.language, 'LC_MESSAGES') - if path.exists(locale_path): - yield locale_dir + locale_path = self.basedir / locale_dir / self.language / 'LC_MESSAGES' + if locale_path.exists(): + yield self.basedir / locale_dir else: logger.verbose(__('locale_dir %s does not exist'), locale_path) @property - def pofiles(self) -> Iterator[tuple[str, str]]: + def pofiles(self) -> Iterator[tuple[_StrPath, _StrPath]]: for locale_dir in self.locale_dirs: - basedir = path.join(locale_dir, self.language, 'LC_MESSAGES') - for root, dirnames, filenames in os.walk(basedir): + locale_path = locale_dir / self.language / 'LC_MESSAGES' + for abs_path in locale_path.rglob('*.po'): + rel_path = abs_path.relative_to(locale_path) # skip dot-directories - dot_directories = [d for d in dirnames if d.startswith('.')] - for dirname in dot_directories: - dirnames.remove(dirname) - - for filename in filenames: - if filename.endswith('.po'): - fullpath = path.join(root, filename) - yield basedir, relpath(fullpath, basedir) + if any(part.startswith('.') for part in rel_path.parts[:-1]): + continue + yield locale_path, rel_path @property def catalogs(self) -> Iterator[CatalogInfo]: diff --git a/tests/test_util/test_util_i18n.py b/tests/test_util/test_util_i18n.py index 46d2f88af6d..973b054a1d8 100644 --- a/tests/test_util/test_util_i18n.py +++ b/tests/test_util/test_util_i18n.py @@ -3,6 +3,7 @@ import datetime import os import time +from pathlib import Path import babel import pytest @@ -18,16 +19,16 @@ def test_catalog_info_for_file_and_path(): cat = i18n.CatalogInfo('path', 'domain', 'utf-8') assert cat.po_file == 'domain.po' assert cat.mo_file == 'domain.mo' - assert cat.po_path == os.path.join('path', 'domain.po') - assert cat.mo_path == os.path.join('path', 'domain.mo') + assert cat.po_path == str(Path('path', 'domain.po')) + assert cat.mo_path == str(Path('path', 'domain.mo')) def test_catalog_info_for_sub_domain_file_and_path(): cat = i18n.CatalogInfo('path', 'sub/domain', 'utf-8') assert cat.po_file == 'sub/domain.po' assert cat.mo_file == 'sub/domain.mo' - assert cat.po_path == os.path.join('path', 'sub/domain.po') - assert cat.mo_path == os.path.join('path', 'sub/domain.mo') + assert cat.po_path == str(Path('path', 'sub', 'domain.po')) + assert cat.mo_path == str(Path('path', 'sub', 'domain.mo')) def test_catalog_outdated(tmp_path): @@ -48,8 +49,9 @@ def test_catalog_write_mo(tmp_path): (tmp_path / 'test.po').write_text('#', encoding='utf8') cat = i18n.CatalogInfo(tmp_path, 'test', 'utf-8') cat.write_mo('en') - assert os.path.exists(cat.mo_path) - with open(cat.mo_path, 'rb') as f: + mo_path = Path(cat.mo_path) + assert mo_path.exists() + with open(mo_path, 'rb') as f: assert read_mo(f) is not None