Compare commits

...

10 Commits

4 changed files with 113 additions and 93 deletions

View File

@ -201,17 +201,9 @@ def check_args(args: Namespace) -> None:
args.path = Path(args.path).resolve()
args.logging_level = None
if args.verbose:
if args.verbose == 1:
# Will only show ERROR and CRITICAL
args.logging_level = logging.WARNING
if args.verbose == 2:
# Will only show ERROR, CRITICAL and WARNING
args.logging_level = logging.INFO
if args.verbose >= 3:
# Will show INFO WARNING ERROR DEBUG CRITICAL
args.logging_level = logging.DEBUG
else:
# Disable all logging
logging.disable(logging.CRITICAL)
try:
levels = [logging.CRITICAL, logging.WARNING, logging.INFO, logging.DEBUG]
args.logging_level = levels[args.verbose]
except IndexError:
print("Too many `-v`, what do you think you'll get?", file=sys.stderr)
sys.exit(1)

View File

@ -4,7 +4,7 @@ import os
import pickle
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Any, Callable, Dict, List, Optional, Sequence, cast
from typing import Any, Callable, Dict, List, Optional, Sequence, Set, cast
import polib
@ -15,6 +15,9 @@ class PoFileStats:
"""Statistics about a po file.
Contains all the necessary information about the progress of a given po file.
Beware this file is pickled (for the cache), don't store actual
entries in its __dict__, just stats.
"""
def __init__(self, path: Path):
@ -22,63 +25,65 @@ class PoFileStats:
self.path: Path = path
self.filename: str = path.name
self.mtime = os.path.getmtime(path)
self.pofile: Optional[polib.POFile] = None
self.directory: str = self.path.parent.name
self.reserved_by: Optional[str] = None
self.reservation_date: Optional[str] = None
self.filename_dir: str = self.directory + "/" + self.filename
self.stats: Dict[str, int] = {}
def __eq__(self, other: object) -> bool:
return isinstance(other, type(self)) and self.path == other.path
def __hash__(self) -> int:
return hash(("PoFileStats", self.path))
@property
def fuzzy(self) -> int:
self.parse()
assert self.pofile
return len(
[entry for entry in self.pofile if entry.fuzzy and not entry.obsolete]
)
return self.stats["fuzzy"]
@property
def translated(self) -> int:
self.parse()
assert self.pofile
return len(self.pofile.translated_entries())
return self.stats["translated"]
@property
def untranslated(self) -> int:
self.parse()
assert self.pofile
return len(self.pofile.untranslated_entries())
return self.stats["untranslated"]
@property
def entries(self) -> int:
self.parse()
assert self.pofile
return len([e for e in self.pofile if not e.obsolete])
return self.stats["entries"]
@property
def percent_translated(self) -> int:
self.parse()
assert self.pofile
return self.pofile.percent_translated()
return self.stats["percent_translated"]
def parse(self) -> None:
if self.pofile is None:
self.pofile = polib.pofile(str(self.path))
if self.stats:
return # Stats already computed.
pofile = polib.pofile(str(self.path))
self.stats = {
"fuzzy": len(
[entry for entry in pofile if entry.fuzzy and not entry.obsolete]
),
"percent_translated": pofile.percent_translated(),
"entries": len([e for e in pofile if not e.obsolete]),
"untranslated": len(pofile.untranslated_entries()),
"translated": len(pofile.translated_entries()),
}
def __str__(self) -> str:
return (
f"Filename: {self.filename}\n"
f"Fuzzy Entries: {self.fuzzy}\n"
f"Percent Translated: {self.percent_translated}\n"
f"Translated Entries: {self.translated}\n"
f"Untranslated Entries: {self.untranslated}"
)
def __repr__(self) -> str:
if self.stats:
return f"<PoFileStats {self.path!r} {self.entries} entries>"
return f"<PoFileStats {self.path!r} (unparsed)>"
def __lt__(self, other: "PoFileStats") -> bool:
"""When two PoFiles are compared, their filenames are compared."""
return self.filename < other.filename
return self.path < other.path
def reservation_str(self, with_reservation_dates: bool = False) -> str:
if self.reserved_by is None:
@ -108,19 +113,22 @@ class PoFileStats:
class PoDirectoryStats:
"""Represent a directory containing multiple `.po` files."""
def __init__(self, path: Path, files: Sequence[PoFileStats]):
def __init__(self, path: Path, files_stats: Sequence[PoFileStats]):
self.path = path
self.files = files
self.files_stats = files_stats
def __repr__(self) -> str:
return f"<PoDirectoryStats {self.path!r} with {len(self.files_stats)} files>"
@property
def translated(self) -> int:
"""Qty of translated entries in the po files of this directory."""
return sum(po_file.translated for po_file in self.files)
return sum(po_file.translated for po_file in self.files_stats)
@property
def entries(self) -> int:
"""Qty of entries in the po files of this directory."""
return sum(po_file.entries for po_file in self.files)
return sum(po_file.entries for po_file in self.files_stats)
@property
def completion(self) -> float:
@ -132,22 +140,22 @@ class PoDirectoryStats:
def __lt__(self, other: object) -> bool:
if not isinstance(other, type(self)):
return NotImplemented
return False
return self.path < other.path
def __le__(self, other: object) -> bool:
if not isinstance(other, type(self)):
return NotImplemented
return False
return self.path <= other.path
def __gt__(self, other: object) -> bool:
if not isinstance(other, type(self)):
return NotImplemented
return False
return self.path > other.path
def __ge__(self, other: object) -> bool:
if not isinstance(other, type(self)):
return NotImplemented
return False
return self.path >= other.path
@ -158,20 +166,37 @@ class PoProjectStats:
self.path = path
# self.files can be persisted on disk
# using `.write_cache()` and `.read_cache()
self.files: List[PoFileStats] = []
self.files: Set[PoFileStats] = set()
self.excluded_files: Set[PoFileStats] = set()
def filter(self, filter_func: Callable[[PoFileStats], bool]) -> None:
self.files = [po_file for po_file in self.files if filter_func(po_file)]
"""Filter files according to a filter function.
If filter is applied multiple times, it behave like only last
filter has been applied.
"""
all_files = self.files | self.excluded_files
self.files = set()
self.excluded_files = set()
for file in all_files:
if filter_func(file):
self.files.add(file)
else:
self.excluded_files.add(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())
return sum(
directory_stats.translated for directory_stats 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())
return sum(
directory_stats.entries for directory_stats in self.stats_by_directory()
)
@property
def completion(self) -> float:
@ -185,24 +210,24 @@ class PoProjectStats:
"""
for path in list(self.path.rglob("*.po")):
if PoFileStats(path) not in self.files:
self.files.append(PoFileStats(path))
self.files.add(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
sorted(self.files, key=lambda po_file: po_file.path.parent),
key=lambda po_file: po_file.path.parent,
)
]
def read_cache(
self,
cache_path: Path = Path(".potodo/cache.pickle"),
) -> None:
def read_cache(self) -> None:
"""Restore all PoFileStats from disk.
While reading the cache, outdated entires are **not** loaded.
"""
cache_path = self.path / ".potodo" / "cache.pickle"
logging.debug("Trying to load cache from %s", cache_path)
try:
with open(cache_path, "rb") as handle:
@ -216,12 +241,13 @@ class PoProjectStats:
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)
self.files.add(po_file)
def write_cache(self, cache_path: Path = Path(".potodo/cache.pickle")) -> None:
def write_cache(self) -> None:
"""Persists all PoFileStats to disk."""
cache_path = self.path / ".potodo" / "cache.pickle"
os.makedirs(cache_path.parent, exist_ok=True)
data = {"version": VERSION, "data": self.files}
data = {"version": VERSION, "data": self.files | self.excluded_files}
with NamedTemporaryFile(
mode="wb", delete=False, dir=str(cache_path.parent), prefix=cache_path.name
) as tmp:

View File

@ -1,7 +1,7 @@
import json
import logging
from pathlib import Path
from typing import Any, Callable, Dict, List
from typing import Callable, List
from gitignore_parser import rule_from_pattern
@ -20,18 +20,14 @@ def scan_path(
) -> PoProjectStats:
logging.debug("Finding po files in %s", path)
po_project = PoProjectStats(path)
cache_path = path.resolve() / ".potodo" / "cache.pickle"
if no_cache:
logging.debug("Creating PoFileStats objects for each file without cache")
else:
po_project.read_cache(cache_path)
po_project.read_cache()
po_project.rescan()
if not no_cache:
po_project.write_cache(cache_path)
if api_url and not hide_reserved:
issue_reservations = get_issue_reservations(api_url)
for po_file_stats in po_project.files:
@ -41,33 +37,38 @@ def scan_path(
if reserved_by and reservation_date:
po_file_stats.reserved_by = reserved_by
po_file_stats.reservation_date = reservation_date
else: # Just in case we remember it's reserved from the cache:
po_file_stats.reserved_by = None
po_file_stats.reservation_date = None
return po_project
def print_matching_files(po_project: PoProjectStats) -> None:
for directory in sorted(po_project.stats_by_directory()):
for po_file in sorted(directory.files):
print(po_file.path)
for directory_stats in sorted(po_project.stats_by_directory()):
for file_stat in sorted(directory_stats.files_stats):
print(file_stat.path)
def print_po_project(
po_project: PoProjectStats, counts: bool, show_reservation_dates: bool
) -> None:
for directory in sorted(po_project.stats_by_directory()):
print(f"\n\n# {directory.path.name} ({directory.completion:.2f}% done)\n")
for directory_stats in sorted(po_project.stats_by_directory()):
print(
f"\n\n# {directory_stats.path.name} ({directory_stats.completion:.2f}% done)\n"
)
for po_file in sorted(directory.files):
line = f"- {po_file.filename:<30} "
for file_stat in sorted(directory_stats.files_stats):
line = f"- {file_stat.filename:<30} "
if counts:
line += f"{po_file.missing:3d} to do"
line += f"{file_stat.missing:3d} to do"
else:
line += f"{po_file.translated:3d} / {po_file.entries:3d}"
line += f" ({po_file.percent_translated:5.1f}% translated)"
if po_file.fuzzy:
line += f", {po_file.fuzzy} fuzzy"
if po_file.reserved_by is not None:
line += ", " + po_file.reservation_str(show_reservation_dates)
line += f"{file_stat.translated:3d} / {file_stat.entries:3d}"
line += f" ({file_stat.percent_translated:5.1f}% translated)"
if file_stat.fuzzy:
line += f", {file_stat.fuzzy} fuzzy"
if file_stat.reserved_by is not None:
line += ", " + file_stat.reservation_str(show_reservation_dates)
print(line + ".")
if po_project.entries != 0:
@ -75,18 +76,19 @@ def print_po_project(
def print_po_project_as_json(po_project: PoProjectStats) -> None:
dir_stats: List[Dict[str, Any]] = []
for directory in sorted(po_project.stats_by_directory()):
dir_stats.append(
{
"name": f"{directory.path.name}/",
"percent_translated": directory.completion,
"files": [po_file.as_dict() for po_file in sorted(directory.files)],
}
)
print(
json.dumps(
dir_stats,
[
{
"name": f"{directory_stats.path.name}/",
"percent_translated": directory_stats.completion,
"files": [
po_file.as_dict()
for po_file in sorted(directory_stats.files_stats)
],
}
for directory_stats in sorted(po_project.stats_by_directory())
],
indent=4,
separators=(",", ": "),
sort_keys=False,
@ -158,3 +160,4 @@ def main() -> None:
print_po_project_as_json(po_project)
else:
print_po_project(po_project, args.counts, args.show_reservation_dates)
po_project.write_cache()

View File

@ -1,3 +1,4 @@
from contextlib import suppress
from pathlib import Path
import pytest
@ -16,10 +17,8 @@ def run_potodo(repo_dir, capsys, monkeypatch):
monkeypatch.setattr(
"sys.argv", ["potodo", "--no-cache", "-p", str(repo_dir)] + argv
)
try:
with suppress(SystemExit):
main()
except SystemExit:
pass
return capsys.readouterr()
return run_it