forked from AFPy/potodo
Compare commits
10 Commits
a2e198c49c
...
302204b787
Author | SHA1 | Date | |
---|---|---|---|
302204b787 | |||
4a75c8d819 | |||
7fa84874bc | |||
a06e02d00e | |||
20a32fb658 | |||
9dc5e28764 | |||
15efeaa3de | |||
045aea56c8 | |||
186e35ffba | |||
e180762be6 |
|
@ -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)
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue
Block a user