forked from AFPy/potodo
213 lines
7.5 KiB
Python
213 lines
7.5 KiB
Python
import itertools
|
|
import logging
|
|
import os
|
|
import pickle
|
|
from pathlib import Path
|
|
from tempfile import NamedTemporaryFile
|
|
from typing import Any, Callable, Dict, List, Optional, Sequence, cast
|
|
|
|
import polib
|
|
|
|
from potodo import __version__ as VERSION
|
|
|
|
|
|
class PoFileStats:
|
|
"""Statistics about a po file.
|
|
|
|
Contains all the necessary information about the progress of a given po file.
|
|
"""
|
|
|
|
def __init__(self, path: Path):
|
|
"""Initializes the class with all the correct information"""
|
|
self.path: Path = path
|
|
self.filename: str = path.name
|
|
self.mtime = os.path.getmtime(path)
|
|
self.pofile: polib.POFile = polib.pofile(str(self.path))
|
|
self.directory: str = self.path.parent.name
|
|
self.reserved_by: Optional[str] = None
|
|
self.reservation_date: Optional[str] = None
|
|
self.obsolete_entries: Sequence[polib.POEntry] = self.pofile.obsolete_entries()
|
|
self.obsolete_nb: int = len(self.pofile.obsolete_entries())
|
|
|
|
self.fuzzy_entries: List[polib.POEntry] = [
|
|
entry for entry in self.pofile if entry.fuzzy and not entry.obsolete
|
|
]
|
|
self.fuzzy_nb: int = len(self.fuzzy_entries)
|
|
|
|
self.translated_entries: Sequence[
|
|
polib.POEntry
|
|
] = self.pofile.translated_entries()
|
|
self.translated_nb: int = len(self.translated_entries)
|
|
|
|
self.untranslated_entries: Sequence[
|
|
polib.POEntry
|
|
] = self.pofile.untranslated_entries()
|
|
self.untranslated_nb: int = len(self.untranslated_entries)
|
|
|
|
self.entries_count: int = len([e for e in self.pofile if not e.obsolete])
|
|
self.percent_translated: int = self.pofile.percent_translated()
|
|
self.entries = len(self.pofile) - self.obsolete_nb
|
|
self.filename_dir: str = self.directory + "/" + self.filename
|
|
|
|
def __str__(self) -> str:
|
|
return (
|
|
f"Filename: {self.filename}\n"
|
|
f"Fuzzy Entries: {self.fuzzy_entries}\n"
|
|
f"Percent Translated: {self.percent_translated}\n"
|
|
f"Translated Entries: {self.translated_entries}\n"
|
|
f"Untranslated Entries: {self.untranslated_entries}"
|
|
)
|
|
|
|
def __lt__(self, other: "PoFileStats") -> bool:
|
|
"""When two PoFiles are compared, their filenames are compared."""
|
|
return self.filename < other.filename
|
|
|
|
def reservation_str(self, with_reservation_dates: bool = False) -> str:
|
|
if self.reserved_by is None:
|
|
return ""
|
|
as_string = f"reserved by {self.reserved_by}"
|
|
if with_reservation_dates:
|
|
as_string += f" ({self.reservation_date})"
|
|
return as_string
|
|
|
|
@property
|
|
def missing(self) -> int:
|
|
return len(self.fuzzy_entries) + len(self.untranslated_entries)
|
|
|
|
def as_dict(self) -> Dict[str, Any]:
|
|
return {
|
|
"name": f"{self.directory}/{self.filename.replace('.po', '')}",
|
|
"path": str(self.path),
|
|
"entries": self.entries,
|
|
"fuzzies": self.fuzzy_nb,
|
|
"translated": self.translated_nb,
|
|
"percent_translated": self.percent_translated,
|
|
"reserved_by": self.reserved_by,
|
|
"reservation_date": self.reservation_date,
|
|
}
|
|
|
|
|
|
class PoDirectoryStats:
|
|
"""Represent a directory containing multiple `.po` files."""
|
|
|
|
def __init__(self, path: Path, files: Sequence[PoFileStats]):
|
|
self.path = path
|
|
self.files = files
|
|
|
|
@property
|
|
def translated(self) -> int:
|
|
"""Qty of translated entries in the po files of this directory."""
|
|
return sum(po_file.translated_nb for po_file in self.files)
|
|
|
|
@property
|
|
def entries(self) -> int:
|
|
"""Qty of entries in the po files of this directory."""
|
|
return sum(po_file.entries_count for po_file in self.files)
|
|
|
|
@property
|
|
def completion(self) -> float:
|
|
"""Return % of completion of this directory."""
|
|
return 100 * self.translated / self.entries
|
|
|
|
def __eq__(self, other: object) -> bool:
|
|
return isinstance(other, type(self)) and self.path == other.path
|
|
|
|
def __lt__(self, other: object) -> bool:
|
|
if not isinstance(other, type(self)):
|
|
return NotImplemented
|
|
return self.path < other.path
|
|
|
|
def __le__(self, other: object) -> bool:
|
|
if not isinstance(other, type(self)):
|
|
return NotImplemented
|
|
return self.path <= other.path
|
|
|
|
def __gt__(self, other: object) -> bool:
|
|
if not isinstance(other, type(self)):
|
|
return NotImplemented
|
|
return self.path > other.path
|
|
|
|
def __ge__(self, other: object) -> bool:
|
|
if not isinstance(other, type(self)):
|
|
return NotImplemented
|
|
return self.path >= other.path
|
|
|
|
|
|
class PoProjectStats:
|
|
"""Represents the root of the hierarchy of `.po` files."""
|
|
|
|
def __init__(self, path: Path):
|
|
self.path = path
|
|
# self.files can be persisted on disk
|
|
# using `.write_cache()` and `.read_cache()
|
|
self.files: List[PoFileStats] = []
|
|
|
|
def filter(self, filter_func: Callable[[PoFileStats], bool]) -> None:
|
|
self.files = [po_file for po_file in self.files if filter_func(po_file)]
|
|
|
|
@property
|
|
def translated(self) -> int:
|
|
"""Qty of translated entries in the po files of this directory."""
|
|
return sum(directory.translated for directory in self.stats_by_directory())
|
|
|
|
@property
|
|
def entries(self) -> int:
|
|
"""Qty of entries in the po files of this directory."""
|
|
return sum(directory.entries for directory in self.stats_by_directory())
|
|
|
|
@property
|
|
def completion(self) -> float:
|
|
"""Return % of completion of this project."""
|
|
return 100 * self.translated / self.entries
|
|
|
|
def rescan(self) -> None:
|
|
"""Scan disk to search for po files.
|
|
|
|
This is the only function that hit the disk.
|
|
"""
|
|
for path in list(self.path.rglob("*.po")):
|
|
if path not in self.files:
|
|
self.files.append(PoFileStats(path))
|
|
|
|
def stats_by_directory(self) -> List[PoDirectoryStats]:
|
|
return [
|
|
PoDirectoryStats(directory, list(po_files))
|
|
for directory, po_files in itertools.groupby(
|
|
self.files, key=lambda po_file: po_file.path.parent
|
|
)
|
|
]
|
|
|
|
def read_cache(
|
|
self,
|
|
cache_path: Path = Path(".potodo/cache.pickle"),
|
|
) -> None:
|
|
"""Restore all PoFileStats from disk.
|
|
|
|
While reading the cache, outdated entires are **not** loaded.
|
|
"""
|
|
logging.debug("Trying to load cache from %s", cache_path)
|
|
try:
|
|
with open(cache_path, "rb") as handle:
|
|
data = pickle.load(handle)
|
|
except FileNotFoundError:
|
|
logging.warning("No cache found")
|
|
return
|
|
logging.debug("Found cache")
|
|
if data.get("version") != VERSION:
|
|
logging.info("Found old cache, ignored it.")
|
|
return
|
|
for po_file in cast(List[PoFileStats], data["data"]):
|
|
if os.path.getmtime(po_file.path.resolve()) == po_file.mtime:
|
|
self.files.append(po_file)
|
|
|
|
def write_cache(self, cache_path: Path = Path(".potodo/cache.pickle")) -> None:
|
|
"""Persists all PoFileStats to disk."""
|
|
os.makedirs(cache_path.parent, exist_ok=True)
|
|
data = {"version": VERSION, "data": self.files}
|
|
with NamedTemporaryFile(
|
|
mode="wb", delete=False, dir=str(cache_path.parent), prefix=cache_path.name
|
|
) as tmp:
|
|
pickle.dump(data, tmp)
|
|
os.rename(tmp.name, cache_path)
|
|
logging.debug("Wrote PoProjectStats cache to %s", cache_path)
|