forked from AFPy/potodo
491 lines
14 KiB
Python
491 lines
14 KiB
Python
import argparse
|
|
import json
|
|
import logging
|
|
from pathlib import Path
|
|
from typing import Any
|
|
from typing import Callable
|
|
from typing import Dict
|
|
from typing import List
|
|
from typing import Sequence
|
|
from typing import Tuple
|
|
|
|
from gitignore_parser import rule_from_pattern
|
|
|
|
from potodo import __version__
|
|
from potodo.arguments_handling import check_args
|
|
from potodo.forge_api import get_issue_reservations
|
|
from potodo.json import json_dateconv
|
|
from potodo.logging import setup_logging
|
|
from potodo.po_file import get_po_stats_from_repo_or_cache
|
|
from potodo.po_file import PoFileStats
|
|
|
|
|
|
def print_dir_stats(
|
|
directory_name: str,
|
|
buffer: Sequence[str],
|
|
folder_stats: Dict[str, int],
|
|
printed_list: Sequence[bool],
|
|
) -> None:
|
|
"""This function prints the directory name, its stats and the buffer"""
|
|
if True in printed_list:
|
|
logging.debug("Printing directory %s", directory_name)
|
|
# If at least one of the files isn't done then print the
|
|
# folder stats and file(s) Each time a file is went over True
|
|
# or False is placed in the printed_list list. If False is
|
|
# placed it means it doesnt need to be printed
|
|
|
|
folder_completion = 100 * folder_stats["translated"] / folder_stats["total"]
|
|
|
|
print(f"\n\n# {directory_name} ({folder_completion:.2f}% done)\n")
|
|
print("\n".join(buffer))
|
|
logging.debug("Not printing directory %s", directory_name)
|
|
|
|
|
|
def add_dir_stats(
|
|
directory_name: str,
|
|
buffer: List[Dict[str, str]],
|
|
folder_stats: Dict[str, int],
|
|
printed_list: Sequence[bool],
|
|
all_stats: List[Dict[str, Any]],
|
|
) -> None:
|
|
"""Appends directory name, its stats and the buffer to stats"""
|
|
if any(printed_list):
|
|
folder_completion = 100 * folder_stats["translated"] / folder_stats["total"]
|
|
all_stats.append(
|
|
dict(
|
|
name=f"{directory_name}/",
|
|
percent_translated=float(f"{folder_completion:.2f}"),
|
|
files=buffer,
|
|
)
|
|
)
|
|
|
|
|
|
def non_interactive_output(
|
|
path: Path,
|
|
exclude: List[str],
|
|
above: int,
|
|
below: int,
|
|
only_fuzzy: bool,
|
|
hide_reserved: bool,
|
|
counts: bool,
|
|
json_format: bool,
|
|
exclude_fuzzy: bool,
|
|
exclude_reserved: bool,
|
|
only_reserved: bool,
|
|
show_reservation_dates: bool,
|
|
no_cache: bool,
|
|
is_interactive: bool,
|
|
matching_files: bool,
|
|
ignore_matches: Callable[[str], bool],
|
|
api_url: str,
|
|
) -> None:
|
|
dir_stats: List[Any] = []
|
|
# Initialize the arguments
|
|
if api_url:
|
|
issue_reservations = get_issue_reservations(hide_reserved, api_url)
|
|
else:
|
|
issue_reservations = {}
|
|
|
|
total_translated: int = 0
|
|
total_entries: int = 0
|
|
po_files_and_dirs = get_po_stats_from_repo_or_cache(path, ignore_matches, no_cache)
|
|
for directory_name, po_files in sorted(po_files_and_dirs.items()):
|
|
# For each directory and files in this directory
|
|
buffer: List[Any] = []
|
|
folder_stats: Dict[str, int] = {"translated": 0, "total": 0}
|
|
printed_list: List[bool] = []
|
|
|
|
for po_file in sorted(po_files):
|
|
# For each file in those files from that directory
|
|
if not only_fuzzy or po_file.fuzzy_entries:
|
|
if exclude_fuzzy and po_file.fuzzy_entries:
|
|
continue
|
|
buffer_add(
|
|
buffer,
|
|
folder_stats,
|
|
printed_list,
|
|
po_file,
|
|
issue_reservations,
|
|
above,
|
|
below,
|
|
counts,
|
|
json_format,
|
|
exclude_reserved,
|
|
only_reserved,
|
|
show_reservation_dates,
|
|
matching_files,
|
|
)
|
|
|
|
# Once all files have been processed, print the dir and the files
|
|
# or store them into a dict to print them once all directories have
|
|
# been processed.
|
|
if json_format:
|
|
add_dir_stats(directory_name, buffer, folder_stats, printed_list, dir_stats)
|
|
else:
|
|
print_dir_stats(directory_name, buffer, folder_stats, printed_list)
|
|
|
|
total_translated += folder_stats["translated"]
|
|
total_entries += folder_stats["total"]
|
|
|
|
if json_format:
|
|
print(
|
|
json.dumps(
|
|
dir_stats,
|
|
indent=4,
|
|
separators=(",", ": "),
|
|
sort_keys=False,
|
|
default=json_dateconv,
|
|
)
|
|
)
|
|
else:
|
|
if total_entries != 0:
|
|
total_completion = 100 * total_translated / total_entries
|
|
print(f"\n\n# TOTAL ({total_completion:.2f}% done)\n")
|
|
|
|
|
|
def build_ignore_matcher(path: Path, exclude: List[str]) -> Callable[[str], bool]:
|
|
path = path.resolve()
|
|
potodo_ignore = path / ".potodoignore"
|
|
rules = []
|
|
if potodo_ignore.exists():
|
|
for line in potodo_ignore.read_text().splitlines():
|
|
rule = rule_from_pattern(line, path)
|
|
if rule:
|
|
rules.append(rule)
|
|
rules.append(rule_from_pattern(".git/", path))
|
|
for rule in exclude:
|
|
rules.append(rule_from_pattern(rule, path))
|
|
return lambda file_path: any(r.match(file_path) for r in rules)
|
|
|
|
|
|
def exec_potodo(
|
|
path: Path,
|
|
exclude: List[str],
|
|
above: int,
|
|
below: int,
|
|
only_fuzzy: bool,
|
|
hide_reserved: bool,
|
|
counts: bool,
|
|
json_format: bool,
|
|
exclude_fuzzy: bool,
|
|
exclude_reserved: bool,
|
|
only_reserved: bool,
|
|
show_reservation_dates: bool,
|
|
no_cache: bool,
|
|
is_interactive: bool,
|
|
matching_files: bool,
|
|
api_url: str,
|
|
) -> None:
|
|
"""
|
|
Will run everything based on the given parameters
|
|
|
|
:param path: The path to search into
|
|
:param exclude: folders or files to be ignored
|
|
:param above: The above threshold
|
|
:param below: The below threshold
|
|
:param only_fuzzy: Should only fuzzies be printed
|
|
:param hide_reserved: Will not show the reserved files
|
|
:param counts: Render list with counts not percentage
|
|
:param json_format: Format output as JSON.
|
|
:param exclude_fuzzy: Will exclude files with fuzzies in output.
|
|
:param exclude_reserved: Will print out only files that aren't reserved
|
|
:param only_reserved: Will print only reserved files
|
|
:param show_reservation_dates: Will show the reservation dates
|
|
:param no_cache: Disables cache (Cache is disabled when files are modified)
|
|
:param is_interactive: Switches output to an interactive CLI menu
|
|
:param matching_files: Should the file paths be printed instead of normal output
|
|
:param api_url: API URL for reservation tickets on Gitea or GitHub
|
|
"""
|
|
|
|
ignore_matches = build_ignore_matcher(path, exclude)
|
|
if is_interactive:
|
|
from potodo.interactive import interactive_output
|
|
|
|
interactive_output(path, ignore_matches)
|
|
else:
|
|
non_interactive_output(
|
|
path,
|
|
exclude,
|
|
above,
|
|
below,
|
|
only_fuzzy,
|
|
hide_reserved,
|
|
counts,
|
|
json_format,
|
|
exclude_fuzzy,
|
|
exclude_reserved,
|
|
only_reserved,
|
|
show_reservation_dates,
|
|
no_cache,
|
|
is_interactive,
|
|
matching_files,
|
|
ignore_matches,
|
|
api_url,
|
|
)
|
|
|
|
|
|
def buffer_add(
|
|
buffer: List[Any],
|
|
folder_stats: Dict[str, int],
|
|
printed_list: List[bool],
|
|
po_file_stats: PoFileStats,
|
|
issue_reservations: Dict[str, Tuple[Any, Any]],
|
|
above: int,
|
|
below: int,
|
|
counts: bool,
|
|
json_format: bool,
|
|
exclude_reserved: bool,
|
|
only_reserved: bool,
|
|
show_reservation_dates: bool,
|
|
matching_files: bool,
|
|
) -> None:
|
|
"""Will add to the buffer the information to print about the file is
|
|
the file isn't translated entirely or above or below requested
|
|
values.
|
|
"""
|
|
# If the file is completely translated,
|
|
# or is translated below what's requested
|
|
# or is translated above what's requested
|
|
if (
|
|
po_file_stats.percent_translated == 100
|
|
or po_file_stats.percent_translated < above
|
|
or po_file_stats.percent_translated > below
|
|
):
|
|
# add the percentage of the file to the stats of the folder
|
|
folder_stats["translated"] += po_file_stats.translated_nb
|
|
folder_stats["total"] += po_file_stats.entries_count
|
|
|
|
if not json_format:
|
|
# don't print that file
|
|
printed_list.append(False)
|
|
|
|
# return without adding anything to the buffer
|
|
return
|
|
|
|
fuzzy_entries = po_file_stats.fuzzy_entries
|
|
untranslated_entries = po_file_stats.untranslated_entries
|
|
# nb of fuzzies in the file IF there are some fuzzies in the file
|
|
fuzzy_nb = po_file_stats.fuzzy_nb if fuzzy_entries else 0
|
|
# number of entries translated
|
|
translated_nb = po_file_stats.translated_nb
|
|
# file size
|
|
po_file_size = po_file_stats.po_file_size
|
|
# percentage of the file already translated
|
|
percent_translated = po_file_stats.percent_translated
|
|
|
|
# `reserved by` if the file is reserved
|
|
reserved_by, reservation_date = issue_reservations.get(
|
|
po_file_stats.filename_dir.lower(), (None, None)
|
|
)
|
|
# unless the offline/hide_reservation are enabled
|
|
if exclude_reserved and reserved_by:
|
|
return
|
|
if only_reserved and not reserved_by:
|
|
return
|
|
|
|
directory = po_file_stats.directory
|
|
filename = po_file_stats.filename
|
|
path = po_file_stats.path
|
|
|
|
if matching_files:
|
|
print(path)
|
|
return
|
|
elif json_format:
|
|
# the order of the keys is the display order
|
|
d = dict(
|
|
name=f"{directory}/{filename.replace('.po', '')}",
|
|
path=str(path),
|
|
entries=po_file_size,
|
|
fuzzies=fuzzy_nb,
|
|
translated=translated_nb,
|
|
percent_translated=percent_translated,
|
|
reserved_by=reserved_by,
|
|
reservation_date=reservation_date,
|
|
)
|
|
|
|
buffer.append(d)
|
|
|
|
else:
|
|
s = f"- {filename:<30} " # The filename
|
|
|
|
if counts:
|
|
missing = len(fuzzy_entries) + len(untranslated_entries)
|
|
s += f"{missing:3d} to do"
|
|
s += f", including {fuzzy_nb} fuzzies." if fuzzy_nb else ""
|
|
|
|
else:
|
|
s += f"{translated_nb:3d} / {po_file_size:3d} "
|
|
s += f"({percent_translated:5.1f}% translated)"
|
|
s += f", {fuzzy_nb} fuzzy" if fuzzy_nb else ""
|
|
|
|
if reserved_by is not None:
|
|
s += f", réservé par {reserved_by}"
|
|
if show_reservation_dates:
|
|
s += f" ({reservation_date})"
|
|
|
|
buffer.append(s)
|
|
|
|
# Add the percent translated to the folder statistics
|
|
folder_stats["translated"] += po_file_stats.translated_nb
|
|
folder_stats["total"] += po_file_stats.entries_count
|
|
# Indicate to print the file
|
|
printed_list.append(True)
|
|
|
|
|
|
def main() -> None:
|
|
parser = argparse.ArgumentParser(
|
|
prog="potodo",
|
|
description="List and prettify the po files left to translate.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-p",
|
|
"--path",
|
|
help="execute Potodo in path",
|
|
metavar="path",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-e",
|
|
"--exclude",
|
|
nargs="+",
|
|
default=[],
|
|
help="gitignore-style patterns to exclude from search.",
|
|
metavar="path",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-a",
|
|
"--above",
|
|
default=0,
|
|
metavar="X",
|
|
type=int,
|
|
help="list all TODOs above given X%% completion",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-b",
|
|
"--below",
|
|
default=100,
|
|
metavar="X",
|
|
type=int,
|
|
help="list all TODOs below given X%% completion",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-f",
|
|
"--only-fuzzy",
|
|
dest="only_fuzzy",
|
|
action="store_true",
|
|
help="print only files marked as fuzzys",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-u",
|
|
"--api-url",
|
|
help=(
|
|
"API URL to retrieve reservation tickets (https://api.github.com/repos/ORGANISATION/REPOSITORY/issues?state=open or https://git.afpy.org/api/v1/repos/ORGANISATION/REPOSITORY/issues?state=open&type=issues)"
|
|
),
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-n",
|
|
"--no-reserved",
|
|
dest="hide_reserved",
|
|
action="store_true",
|
|
help="don't print info about reserved files",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-c",
|
|
"--counts",
|
|
action="store_true",
|
|
help="render list with the count of remaining entries "
|
|
"(translate or review) rather than percentage done",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-j",
|
|
"--json",
|
|
action="store_true",
|
|
dest="json_format",
|
|
help="format output as JSON",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--exclude-fuzzy",
|
|
action="store_true",
|
|
dest="exclude_fuzzy",
|
|
help="select only files without fuzzy entries",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--exclude-reserved",
|
|
action="store_true",
|
|
dest="exclude_reserved",
|
|
help="select only files that aren't reserved",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--only-reserved",
|
|
action="store_true",
|
|
dest="only_reserved",
|
|
help="select only only reserved files",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--show-reservation-dates",
|
|
action="store_true",
|
|
dest="show_reservation_dates",
|
|
help="show issue creation dates",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--no-cache",
|
|
action="store_true",
|
|
dest="no_cache",
|
|
help="Disables cache (Cache is disabled when files are modified)",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-i",
|
|
"--interactive",
|
|
action="store_true",
|
|
dest="is_interactive",
|
|
help="Activates the interactive menu",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-l",
|
|
"--matching-files",
|
|
action="store_true",
|
|
dest="matching_files",
|
|
help="Suppress normal output; instead print the name of each matching po file from which output would normally "
|
|
"have been printed.",
|
|
)
|
|
|
|
parser.add_argument(
|
|
"--version", action="version", version="%(prog)s " + __version__
|
|
)
|
|
|
|
parser.add_argument(
|
|
"-v", "--verbose", action="count", default=0, help="Increases output verbosity"
|
|
)
|
|
|
|
# Initialize args and check consistency
|
|
args = vars(parser.parse_args())
|
|
args.update(check_args(**args))
|
|
|
|
if args["logging_level"]:
|
|
setup_logging(args["logging_level"])
|
|
|
|
logging.info("Logging activated.")
|
|
logging.debug("Executing potodo with args %s", args)
|
|
|
|
# Removing useless args before running the process
|
|
del args["verbose"]
|
|
del args["logging_level"]
|
|
|
|
# Launch the processing itself
|
|
exec_potodo(**args)
|