forked from AFPy/potodo
Compare commits
10 Commits
ebd2a2271c
...
0ffab39591
Author | SHA1 | Date |
---|---|---|
Julien Palard | 0ffab39591 | |
Julien Palard | 372cbf1c85 | |
Julien Palard | 8b4a6d7dec | |
Julien Palard | 2bf68dc6e7 | |
Julien Palard | 10c21bbc44 | |
Julien Palard | d620496b61 | |
Julien Palard | e85286bf63 | |
Julien Palard | edecf0fa4c | |
Julien Palard | 6c9994705a | |
Julien Palard | e8fcd50d22 |
|
@ -1,20 +1,173 @@
|
|||
import argparse
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from argparse import Namespace
|
||||
from pathlib import Path
|
||||
|
||||
from potodo import __version__
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
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 = parser.parse_args()
|
||||
check_args(args)
|
||||
return args
|
||||
|
||||
|
||||
def check_args(args: Namespace) -> None:
|
||||
# If below is lower than above, raise an error
|
||||
if args.below < args.above:
|
||||
print("Potodo: 'below' value must be greater than 'above' value.")
|
||||
exit(1)
|
||||
print(
|
||||
"Potodo: 'below' value must be greater than 'above' value.", file=sys.stderr
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if args.json_format and args.is_interactive:
|
||||
print(
|
||||
"Potodo: Json format and interactive modes cannot be activated at the same time."
|
||||
"Potodo: Json format and interactive modes cannot be activated at the same time.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
if args.is_interactive:
|
||||
try:
|
||||
|
@ -23,20 +176,24 @@ def check_args(args: Namespace) -> None:
|
|||
import platform
|
||||
|
||||
print(
|
||||
'Potodo: "{}" is not supported for interactive mode'.format(
|
||||
platform.system()
|
||||
)
|
||||
f'Potodo: "{platform.system()}" is not supported for interactive mode',
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if args.exclude_fuzzy and args.only_fuzzy:
|
||||
print("Potodo: Cannot pass --exclude-fuzzy and --only-fuzzy at the same time.")
|
||||
exit(1)
|
||||
print(
|
||||
"Potodo: Cannot pass --exclude-fuzzy and --only-fuzzy at the same time.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
|
||||
if args.exclude_reserved and args.only_reserved:
|
||||
print(
|
||||
"Potodo: Cannot pass --exclude-reserved and --only-reserved at the same time."
|
||||
"Potodo: Cannot pass --exclude-reserved and --only-reserved at the same time.",
|
||||
file=sys.stderr,
|
||||
)
|
||||
exit(1)
|
||||
sys.exit(1)
|
||||
|
||||
# If no path is specified, use current directory
|
||||
if not args.path:
|
||||
|
|
|
@ -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, Set, cast
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, cast
|
||||
|
||||
import polib
|
||||
|
||||
|
@ -134,22 +134,16 @@ class PoDirectoryStats:
|
|||
|
||||
|
||||
class PoProjectStats:
|
||||
"""Represents a hierarchy of `.po` files."""
|
||||
"""Represents the root of the hierarchy of `.po` files."""
|
||||
|
||||
def __init__(
|
||||
self, path: Path, filter_function: Optional[Callable[[str], bool]] = None
|
||||
):
|
||||
"""filter_function is a function to include/exclude po files
|
||||
or directories, it should return True for the file to be
|
||||
included.
|
||||
"""
|
||||
def __init__(self, path: Path):
|
||||
self.path = path
|
||||
if filter_function is None:
|
||||
filter_function = self.allow_all
|
||||
self.filter_function = filter_function
|
||||
# self.files can be persisted on disk
|
||||
# using `.write_cache()` and `.read_cache()
|
||||
self.files: Dict[Path, PoFileStats] = {}
|
||||
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:
|
||||
|
@ -166,41 +160,21 @@ class PoProjectStats:
|
|||
"""Return % of completion of this project."""
|
||||
return 100 * self.translated / self.entries
|
||||
|
||||
@staticmethod
|
||||
def allow_all(path: str) -> bool:
|
||||
"""Default filtering function: allow all files."""
|
||||
return True
|
||||
def rescan(self) -> None:
|
||||
"""Scan disk to search for po files.
|
||||
|
||||
def find_all_files(self) -> List[Path]:
|
||||
"""Get all the files matching `**/*.po`.
|
||||
File can be filtered using `self.filter_function`, see __init__.
|
||||
This is the only function that hit the disk.
|
||||
"""
|
||||
return [
|
||||
file for file in self.path.rglob("*.po") if self.filter_function(str(file))
|
||||
]
|
||||
|
||||
def files_by_directory(self) -> Dict[Path, Set[Path]]:
|
||||
return {
|
||||
name: set(files)
|
||||
# We assume the output of rglob to be sorted,
|
||||
# so each 'name' is unique within groupby
|
||||
for name, files in itertools.groupby(
|
||||
self.find_all_files(), key=lambda path: path.parent
|
||||
)
|
||||
}
|
||||
|
||||
def stats_for_file(self, path: Path) -> PoFileStats:
|
||||
"""Get a PoFileStats for a given Path."""
|
||||
if path not in self.files:
|
||||
self.files[path] = PoFileStats(path)
|
||||
return self.files[path]
|
||||
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, [self.stats_for_file(po_file) for po_file in po_files]
|
||||
PoDirectoryStats(directory, list(po_files))
|
||||
for directory, po_files in itertools.groupby(
|
||||
self.files, key=lambda po_file: po_file.path.parent
|
||||
)
|
||||
for directory, po_files in self.files_by_directory().items()
|
||||
]
|
||||
|
||||
def read_cache(
|
||||
|
@ -222,9 +196,9 @@ class PoProjectStats:
|
|||
if data.get("version") != VERSION:
|
||||
logging.info("Found old cache, ignored it.")
|
||||
return
|
||||
for path, stats in cast(Dict[Path, PoFileStats], data["data"]).items():
|
||||
if os.path.getmtime(path.resolve()) == stats.mtime:
|
||||
self.files[path] = stats
|
||||
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."""
|
||||
|
|
343
potodo/potodo.py
343
potodo/potodo.py
|
@ -1,13 +1,11 @@
|
|||
import argparse
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, List
|
||||
from typing import Any, Callable, Dict, List
|
||||
|
||||
from gitignore_parser import rule_from_pattern
|
||||
|
||||
from potodo import __version__
|
||||
from potodo.arguments_handling import check_args
|
||||
from potodo.arguments_handling import parse_args
|
||||
from potodo.forge_api import get_issue_reservations
|
||||
from potodo.json import json_dateconv
|
||||
from potodo.logging import setup_logging
|
||||
|
@ -18,11 +16,10 @@ def scan_path(
|
|||
path: Path,
|
||||
no_cache: bool,
|
||||
hide_reserved: bool,
|
||||
ignore_matches: Callable[[str], bool],
|
||||
api_url: str,
|
||||
) -> PoProjectStats:
|
||||
logging.debug("Finding po files in %s", path)
|
||||
po_project = PoProjectStats(path, lambda file: not ignore_matches(file))
|
||||
po_project = PoProjectStats(path)
|
||||
cache_path = path.resolve() / ".potodo" / "cache.pickle"
|
||||
|
||||
if no_cache:
|
||||
|
@ -30,12 +27,14 @@ def scan_path(
|
|||
else:
|
||||
po_project.read_cache(cache_path)
|
||||
|
||||
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.values():
|
||||
for po_file_stats in po_project.files:
|
||||
reserved_by, reservation_date = issue_reservations.get(
|
||||
po_file_stats.filename_dir.lower(), (None, None)
|
||||
)
|
||||
|
@ -46,100 +45,45 @@ def scan_path(
|
|||
return po_project
|
||||
|
||||
|
||||
def non_interactive_output(
|
||||
path: Path,
|
||||
hide_reserved: bool,
|
||||
counts: bool,
|
||||
json_format: bool,
|
||||
select: Callable[[PoFileStats], bool],
|
||||
show_reservation_dates: bool,
|
||||
no_cache: bool,
|
||||
is_interactive: bool,
|
||||
matching_files: bool,
|
||||
ignore_matches: Callable[[str], bool],
|
||||
api_url: str,
|
||||
) -> None:
|
||||
po_project = scan_path(path, no_cache, hide_reserved, ignore_matches, api_url)
|
||||
if matching_files:
|
||||
print_matching_files(po_project, select)
|
||||
elif json_format:
|
||||
print_po_project_as_json(
|
||||
po_project,
|
||||
select,
|
||||
)
|
||||
else:
|
||||
print_po_project(
|
||||
po_project,
|
||||
counts,
|
||||
select,
|
||||
show_reservation_dates,
|
||||
)
|
||||
|
||||
|
||||
def print_matching_files(
|
||||
po_project: PoProjectStats,
|
||||
select: Callable[[PoFileStats], bool],
|
||||
) -> None:
|
||||
def print_matching_files(po_project: PoProjectStats) -> None:
|
||||
for directory in sorted(po_project.stats_by_directory()):
|
||||
for po_file in sorted(directory.files):
|
||||
if select(po_file):
|
||||
print(po_file.path)
|
||||
print(po_file.path)
|
||||
|
||||
|
||||
def print_po_project(
|
||||
po_project: PoProjectStats,
|
||||
counts: bool,
|
||||
select: Callable[[PoFileStats], bool],
|
||||
show_reservation_dates: bool,
|
||||
po_project: PoProjectStats, counts: bool, show_reservation_dates: bool
|
||||
) -> None:
|
||||
for directory in sorted(po_project.stats_by_directory()):
|
||||
if any(select(po_file) for po_file in directory.files):
|
||||
print(f"\n\n# {directory.path.name} ({directory.completion:.2f}% done)\n")
|
||||
print(f"\n\n# {directory.path.name} ({directory.completion:.2f}% done)\n")
|
||||
|
||||
for po_file in sorted(directory.files):
|
||||
if select(po_file):
|
||||
line = f"- {po_file.filename:<30} "
|
||||
if counts:
|
||||
line += f"{po_file.missing:3d} to do"
|
||||
else:
|
||||
line += (
|
||||
f"{po_file.translated_nb:3d} / {po_file.entries:3d}"
|
||||
f" ({po_file.percent_translated:5.1f}% translated)"
|
||||
)
|
||||
if po_file.fuzzy_nb:
|
||||
line += f", {po_file.fuzzy_nb} fuzzy"
|
||||
if po_file.reserved_by is not None:
|
||||
line += ", " + po_file.reservation_str(show_reservation_dates)
|
||||
print(line + ".")
|
||||
line = f"- {po_file.filename:<30} "
|
||||
if counts:
|
||||
line += f"{po_file.missing:3d} to do"
|
||||
else:
|
||||
line += f"{po_file.translated_nb:3d} / {po_file.entries:3d}"
|
||||
line += f" ({po_file.percent_translated:5.1f}% translated)"
|
||||
if po_file.fuzzy_nb:
|
||||
line += f", {po_file.fuzzy_nb} fuzzy"
|
||||
if po_file.reserved_by is not None:
|
||||
line += ", " + po_file.reservation_str(show_reservation_dates)
|
||||
print(line + ".")
|
||||
|
||||
if po_project.entries != 0:
|
||||
print(f"\n\n# TOTAL ({po_project.completion:.2f}% done)\n")
|
||||
|
||||
|
||||
def print_po_project_as_json(
|
||||
po_project: PoProjectStats,
|
||||
select: Callable[[PoFileStats], bool],
|
||||
) -> None:
|
||||
dir_stats: List[Any] = []
|
||||
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()):
|
||||
buffer: List[Any] = []
|
||||
for po_file in sorted(directory.files):
|
||||
if not select(po_file):
|
||||
continue
|
||||
buffer.append(po_file.as_dict())
|
||||
|
||||
# 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 buffer:
|
||||
folder_completion = 100 * directory.translated / directory.entries
|
||||
dir_stats.append(
|
||||
{
|
||||
"name": f"{directory.path.name}/",
|
||||
"percent_translated": float(f"{folder_completion:.2f}"),
|
||||
"files": buffer,
|
||||
}
|
||||
)
|
||||
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,
|
||||
|
@ -166,198 +110,16 @@ def build_ignore_matcher(path: Path, exclude: List[str]) -> Callable[[str], bool
|
|||
return lambda file_path: any(r.match(file_path) for r in rules)
|
||||
|
||||
|
||||
def exec_potodo(
|
||||
path: Path,
|
||||
exclude: List[str],
|
||||
hide_reserved: bool,
|
||||
counts: bool,
|
||||
json_format: bool,
|
||||
select: Callable[[PoFileStats], 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 hide_reserved: Will not show the reserved files
|
||||
:param counts: Render list with counts not percentage
|
||||
:param json_format: Format output as JSON.
|
||||
: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,
|
||||
hide_reserved,
|
||||
counts,
|
||||
json_format,
|
||||
select,
|
||||
show_reservation_dates,
|
||||
no_cache,
|
||||
is_interactive,
|
||||
matching_files,
|
||||
ignore_matches,
|
||||
api_url,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="potodo",
|
||||
description="List and prettify the po files left to translate.",
|
||||
)
|
||||
args = parse_args()
|
||||
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--path",
|
||||
help="execute Potodo in path",
|
||||
metavar="path",
|
||||
)
|
||||
if args.logging_level:
|
||||
setup_logging(args.logging_level)
|
||||
|
||||
parser.add_argument(
|
||||
"-e",
|
||||
"--exclude",
|
||||
nargs="+",
|
||||
default=[],
|
||||
help="gitignore-style patterns to exclude from search.",
|
||||
metavar="path",
|
||||
)
|
||||
logging.info("Logging activated.")
|
||||
logging.debug("Executing potodo with args %s", args)
|
||||
|
||||
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 = parser.parse_args()
|
||||
check_args(args)
|
||||
ignore_matches = build_ignore_matcher(args.path, args.exclude)
|
||||
|
||||
def select(po_file: PoFileStats) -> bool:
|
||||
"""Return True if the po_file should be displayed, False otherwise."""
|
||||
|
@ -378,25 +140,22 @@ def main() -> None:
|
|||
if args.only_reserved and not po_file.reserved_by:
|
||||
return False
|
||||
|
||||
if ignore_matches(str(po_file.path)):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
if args.logging_level:
|
||||
setup_logging(args.logging_level)
|
||||
if args.is_interactive:
|
||||
from potodo.interactive import interactive_output
|
||||
|
||||
logging.info("Logging activated.")
|
||||
logging.debug("Executing potodo with args %s", args)
|
||||
interactive_output(args.path, ignore_matches)
|
||||
return
|
||||
|
||||
# Launch the processing itself
|
||||
exec_potodo(
|
||||
args.path,
|
||||
args.exclude,
|
||||
args.hide_reserved,
|
||||
args.counts,
|
||||
args.json_format,
|
||||
select,
|
||||
args.show_reservation_dates,
|
||||
args.no_cache,
|
||||
args.is_interactive,
|
||||
args.matching_files,
|
||||
args.api_url,
|
||||
)
|
||||
po_project = scan_path(args.path, args.no_cache, args.hide_reserved, args.api_url)
|
||||
po_project.filter(select)
|
||||
if args.matching_files:
|
||||
print_matching_files(po_project)
|
||||
elif args.json_format:
|
||||
print_po_project_as_json(po_project)
|
||||
else:
|
||||
print_po_project(po_project, args.counts, args.show_reservation_dates)
|
||||
|
|
|
@ -2,32 +2,24 @@ from pathlib import Path
|
|||
|
||||
import pytest
|
||||
|
||||
from potodo.potodo import main
|
||||
|
||||
@pytest.fixture
|
||||
def repo_dir():
|
||||
|
||||
@pytest.fixture(name="repo_dir")
|
||||
def _repo_dir():
|
||||
return Path(__file__).resolve().parent / "fixtures" / "repository"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def base_config(repo_dir):
|
||||
def select(po_file) -> bool:
|
||||
"""Return True if the po_file should be displayed, False otherwise."""
|
||||
return not (
|
||||
po_file.percent_translated == 100
|
||||
or po_file.percent_translated < 0
|
||||
or po_file.percent_translated > 100
|
||||
def run_potodo(repo_dir, capsys, monkeypatch):
|
||||
def run_it(argv):
|
||||
monkeypatch.setattr(
|
||||
"sys.argv", ["potodo", "--no-cache", "-p", str(repo_dir)] + argv
|
||||
)
|
||||
try:
|
||||
main()
|
||||
except SystemExit:
|
||||
pass
|
||||
return capsys.readouterr()
|
||||
|
||||
return {
|
||||
"path": repo_dir,
|
||||
"exclude": ["excluded/", "excluded.po"],
|
||||
"hide_reserved": False,
|
||||
"counts": False,
|
||||
"json_format": False,
|
||||
"select": select,
|
||||
"show_reservation_dates": False,
|
||||
"no_cache": True,
|
||||
"is_interactive": False,
|
||||
"matching_files": False,
|
||||
"api_url": "",
|
||||
}
|
||||
return run_it
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
from potodo.potodo import exec_potodo
|
||||
from pathlib import Path
|
||||
|
||||
from potodo.potodo import main
|
||||
|
||||
REPO_DIR = Path(__file__).resolve().parent / "fixtures" / "repository"
|
||||
|
||||
|
||||
def test_no_exclude(capsys, base_config):
|
||||
exec_potodo(**base_config)
|
||||
def test_no_exclude(capsys, monkeypatch):
|
||||
monkeypatch.setattr("sys.argv", ["potodo", "-p", str(REPO_DIR)])
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert not err
|
||||
assert "file1" in out
|
||||
|
@ -10,9 +15,11 @@ def test_no_exclude(capsys, base_config):
|
|||
assert "file3" in out
|
||||
|
||||
|
||||
def test_exclude_file(capsys, base_config):
|
||||
base_config["exclude"] = ["file*"]
|
||||
exec_potodo(**base_config)
|
||||
def test_exclude_file(capsys, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"sys.argv", ["potodo", "-p", str(REPO_DIR), "--exclude", "file*"]
|
||||
)
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert not err
|
||||
assert "file1" not in out
|
||||
|
@ -21,9 +28,11 @@ def test_exclude_file(capsys, base_config):
|
|||
assert "excluded" in out # The only one not being named file
|
||||
|
||||
|
||||
def test_exclude_directory(capsys, base_config):
|
||||
base_config["exclude"] = ["excluded/*"]
|
||||
exec_potodo(**base_config)
|
||||
def test_exclude_directory(capsys, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"sys.argv", ["potodo", "-p", str(REPO_DIR), "--exclude", "excluded/*"]
|
||||
)
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert not err
|
||||
assert "file1" in out
|
||||
|
@ -33,9 +42,11 @@ def test_exclude_directory(capsys, base_config):
|
|||
assert "excluded/" not in out
|
||||
|
||||
|
||||
def test_exclude_single_file(capsys, base_config):
|
||||
base_config["exclude"] = ["file2.po"]
|
||||
exec_potodo(**base_config)
|
||||
def test_exclude_single_file(capsys, monkeypatch):
|
||||
monkeypatch.setattr(
|
||||
"sys.argv", ["potodo", "-p", str(REPO_DIR), "--exclude", "file2.po"]
|
||||
)
|
||||
main()
|
||||
out, err = capsys.readouterr()
|
||||
assert not err
|
||||
assert "file1" in out
|
||||
|
|
|
@ -1,11 +1,8 @@
|
|||
import json
|
||||
|
||||
from potodo.potodo import exec_potodo
|
||||
|
||||
|
||||
def test_txt_output(capsys, base_config):
|
||||
exec_potodo(**base_config)
|
||||
captured = capsys.readouterr()
|
||||
def test_txt_output(run_potodo):
|
||||
captured = run_potodo(["--exclude", "excluded/", "excluded.po"])
|
||||
|
||||
assert "file1.po" in captured.out
|
||||
assert "file2.po" in captured.out
|
||||
|
@ -16,10 +13,10 @@ def test_txt_output(capsys, base_config):
|
|||
assert "excluded" not in captured.out
|
||||
|
||||
|
||||
def test_output(capsys, base_config, repo_dir):
|
||||
base_config["json_format"] = True
|
||||
exec_potodo(**base_config)
|
||||
output = json.loads(capsys.readouterr().out)
|
||||
def test_output(run_potodo, repo_dir):
|
||||
output = json.loads(
|
||||
run_potodo(["--json", "--exclude", "excluded/", "excluded.po"]).out
|
||||
)
|
||||
|
||||
expected_folder = {
|
||||
"name": "folder/",
|
||||
|
|
|
@ -1,78 +1,41 @@
|
|||
import sys
|
||||
from subprocess import check_output, CalledProcessError
|
||||
def test_potodo_help(run_potodo):
|
||||
output = run_potodo(["--help"]).out
|
||||
output_short = run_potodo(["-h"]).out
|
||||
assert output == output_short
|
||||
assert "-h, --help show this help message and exit" in output
|
||||
|
||||
|
||||
class TestPotodoArgsErrors:
|
||||
def test_potodo_help(self):
|
||||
output = check_output([sys.executable, "-m", "potodo", "--help"]).decode(
|
||||
"utf-8"
|
||||
)
|
||||
output_short = check_output([sys.executable, "-m", "potodo", "-h"]).decode(
|
||||
"utf-8"
|
||||
)
|
||||
assert output == output_short
|
||||
assert "-h, --help show this help message and exit" in output
|
||||
# TODO: Find a better way of testing help output
|
||||
def test_potodo_above_below_conflict(run_potodo):
|
||||
output = run_potodo(["--above", "50", "--below", "40"]).err
|
||||
output_short = run_potodo(["-a", "50", "-b", "40"]).err
|
||||
assert (
|
||||
output
|
||||
== output_short
|
||||
== "Potodo: 'below' value must be greater than 'above' value.\n"
|
||||
)
|
||||
|
||||
def test_potodo_above_below_conflict(self):
|
||||
try:
|
||||
check_output(
|
||||
[sys.executable, "-m", "potodo", "--above", "50", "--below", "40"]
|
||||
).decode("utf-8")
|
||||
except CalledProcessError as e:
|
||||
output = e.output
|
||||
try:
|
||||
check_output(
|
||||
[sys.executable, "-m", "potodo", "-a", "50", "-b", "40"]
|
||||
).decode("utf-8")
|
||||
except CalledProcessError as e:
|
||||
output_short = e.output
|
||||
assert output == output_short
|
||||
assert output == b"Potodo: 'below' value must be greater than 'above' value.\n"
|
||||
|
||||
def test_potodo_json_interactive_conflict(self):
|
||||
try:
|
||||
check_output(
|
||||
[sys.executable, "-m", "potodo", "--json", "--interactive"]
|
||||
).decode("utf-8")
|
||||
except CalledProcessError as e:
|
||||
output = e.output
|
||||
try:
|
||||
check_output([sys.executable, "-m", "potodo", "-j", "-i"]).decode("utf-8")
|
||||
except CalledProcessError as e:
|
||||
output_short = e.output
|
||||
assert output == output_short
|
||||
assert (
|
||||
output
|
||||
== b"Potodo: Json format and interactive modes cannot be activated at the same time.\n"
|
||||
)
|
||||
def test_potodo_json_interactive_conflict(run_potodo):
|
||||
output = run_potodo(["--json", "--interactive"]).err
|
||||
output_short = run_potodo(["-j", "-i"]).err
|
||||
assert (
|
||||
output
|
||||
== output_short
|
||||
== "Potodo: Json format and interactive modes cannot be activated at the same time.\n"
|
||||
)
|
||||
|
||||
def test_potodo_exclude_and_only_fuzzy_conflict(self):
|
||||
try:
|
||||
check_output(
|
||||
[sys.executable, "-m", "potodo", "--exclude-fuzzy", "--only-fuzzy"]
|
||||
).decode("utf-8")
|
||||
except CalledProcessError as e:
|
||||
output = e.output
|
||||
assert (
|
||||
output
|
||||
== b"Potodo: Cannot pass --exclude-fuzzy and --only-fuzzy at the same time.\n"
|
||||
)
|
||||
|
||||
def test_potodo_exclude_and_only_reserved_conflict(self):
|
||||
try:
|
||||
check_output(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"potodo",
|
||||
"--exclude-reserved",
|
||||
"--only-reserved",
|
||||
]
|
||||
).decode("utf-8")
|
||||
except CalledProcessError as e:
|
||||
output = e.output
|
||||
assert (
|
||||
output
|
||||
== b"Potodo: Cannot pass --exclude-reserved and --only-reserved at the same time.\n"
|
||||
)
|
||||
def test_potodo_exclude_and_only_fuzzy_conflict(run_potodo):
|
||||
output = run_potodo(["--exclude-fuzzy", "--only-fuzzy"]).err
|
||||
assert (
|
||||
output
|
||||
== "Potodo: Cannot pass --exclude-fuzzy and --only-fuzzy at the same time.\n"
|
||||
)
|
||||
|
||||
|
||||
def test_potodo_exclude_and_only_reserved_conflict(run_potodo):
|
||||
output = run_potodo(["--exclude-reserved", "--only-reserved"]).err
|
||||
assert (
|
||||
output
|
||||
== "Potodo: Cannot pass --exclude-reserved and --only-reserved at the same time.\n"
|
||||
)
|
||||
|
|
|
@ -1,187 +1,103 @@
|
|||
import sys
|
||||
from subprocess import check_output
|
||||
from pathlib import Path
|
||||
|
||||
HERE = Path(__file__).parent.resolve()
|
||||
FIXTURES = HERE / "fixtures"
|
||||
def test_potodo_no_args(run_potodo):
|
||||
output = run_potodo([]).out
|
||||
assert "# excluded (50.00% done)" in output
|
||||
assert "# folder (33.33% done)" in output
|
||||
assert "- excluded.po 1 / 2 ( 50.0% translated)" in output
|
||||
assert "- file3.po 0 / 1 ( 0.0% translated)" in output
|
||||
assert "# repository (25.00% done)" in output
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
in output
|
||||
)
|
||||
|
||||
|
||||
class TestPotodoCLI:
|
||||
def test_potodo_no_args(self):
|
||||
output = check_output(
|
||||
[sys.executable, "-m", "potodo", "-p", str(FIXTURES)], encoding="UTF-8"
|
||||
)
|
||||
assert "# excluded (50.00% done)" in output
|
||||
assert "# folder (33.33% done)" in output
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)" in output
|
||||
)
|
||||
assert (
|
||||
"- file3.po 0 / 1 ( 0.0% translated)" in output
|
||||
)
|
||||
assert "# repository (25.00% done)" in output
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
in output
|
||||
)
|
||||
def test_potodo_exclude(run_potodo):
|
||||
output = run_potodo(["--exclude", "excluded/", "excluded.po"]).out
|
||||
output_short = run_potodo(["-e", "excluded/", "excluded.po"]).out
|
||||
assert output == output_short
|
||||
assert "# excluded (50.00% done)" not in output
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)" not in output
|
||||
)
|
||||
assert "# repository (25.00% done)" in output
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
in output
|
||||
)
|
||||
|
||||
def test_potodo_exclude(self, base_config):
|
||||
output = check_output(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"potodo",
|
||||
"--exclude",
|
||||
base_config["exclude"][0],
|
||||
base_config["exclude"][1],
|
||||
"-p",
|
||||
str(FIXTURES),
|
||||
],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
output_short = check_output(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"potodo",
|
||||
"-e",
|
||||
base_config["exclude"][0],
|
||||
base_config["exclude"][1],
|
||||
"-p",
|
||||
str(FIXTURES),
|
||||
],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
assert output == output_short
|
||||
assert "# excluded (50.00% done)" not in output
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)"
|
||||
not in output
|
||||
)
|
||||
assert "# repository (25.00% done)" in output
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
in output
|
||||
)
|
||||
|
||||
def test_potodo_above(self):
|
||||
output = check_output(
|
||||
[sys.executable, "-m", "potodo", "--above", "40", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
output_short = check_output(
|
||||
[sys.executable, "-m", "potodo", "-a", "40"]
|
||||
).decode("utf-8")
|
||||
assert output == output_short
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
not in output
|
||||
)
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)" in output
|
||||
)
|
||||
def test_potodo_above(run_potodo):
|
||||
output = run_potodo(["--above", "40"]).out
|
||||
output_short = run_potodo(["-a", "40"]).out
|
||||
assert output == output_short
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
not in output
|
||||
)
|
||||
assert "- excluded.po 1 / 2 ( 50.0% translated)" in output
|
||||
|
||||
def test_potodo_below(self):
|
||||
output = check_output(
|
||||
[sys.executable, "-m", "potodo", "--below", "40", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
|
||||
output_short = check_output(
|
||||
[sys.executable, "-m", "potodo", "-b", "40", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
assert output == output_short
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
in output
|
||||
)
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)"
|
||||
not in output
|
||||
)
|
||||
def test_potodo_below(run_potodo):
|
||||
output = run_potodo(["--below", "40"]).out
|
||||
output_short = run_potodo(["-b", "40"]).out
|
||||
assert output == output_short
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
in output
|
||||
)
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)" not in output
|
||||
)
|
||||
|
||||
def test_potodo_onlyfuzzy(self):
|
||||
output = check_output(
|
||||
[sys.executable, "-m", "potodo", "--only-fuzzy", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
output_short = check_output(
|
||||
[sys.executable, "-m", "potodo", "-f", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
assert output == output_short
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
in output
|
||||
)
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)"
|
||||
not in output
|
||||
)
|
||||
|
||||
def test_potodo_counts(self):
|
||||
output = check_output(
|
||||
[sys.executable, "-m", "potodo", "--counts", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
output_short = check_output(
|
||||
[sys.executable, "-m", "potodo", "-c", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
assert output == output_short
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)"
|
||||
not in output
|
||||
)
|
||||
assert "- file4.po 1 to do" in output
|
||||
assert "# repository (25.00% done)" in output
|
||||
assert "- file1.po 2 to do, 1 fuzzy." in output
|
||||
def test_potodo_onlyfuzzy(run_potodo):
|
||||
output = run_potodo(["--only-fuzzy"]).out
|
||||
output_short = run_potodo(["-f"]).out
|
||||
assert output == output_short
|
||||
assert (
|
||||
"- file1.po 1 / 3 ( 33.0% translated), 1 fuzzy"
|
||||
in output
|
||||
)
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)" not in output
|
||||
)
|
||||
|
||||
def test_potodo_exclude_fuzzy(self):
|
||||
output = check_output(
|
||||
[sys.executable, "-m", "potodo", "--exclude-fuzzy", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)" in output
|
||||
)
|
||||
assert "- file1.po 2 to do, 1 fuzzy." not in output
|
||||
|
||||
def test_potodo_matching_files_solo(self):
|
||||
output = check_output(
|
||||
[sys.executable, "-m", "potodo", "--matching-files", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
output_short = check_output(
|
||||
[sys.executable, "-m", "potodo", "-l", "-p", str(FIXTURES)],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
assert output == output_short
|
||||
assert "excluded/file4.po" in output
|
||||
assert "folder/excluded.po" in output
|
||||
assert "folder/file3.po" in output
|
||||
assert "file1.po" in output
|
||||
assert "file2.po" in output
|
||||
def test_potodo_counts(run_potodo):
|
||||
output = run_potodo(["--counts"]).out
|
||||
output_short = run_potodo(["-c"]).out
|
||||
assert output == output_short
|
||||
assert (
|
||||
"- excluded.po 1 / 2 ( 50.0% translated)" not in output
|
||||
)
|
||||
assert "- file4.po 1 to do" in output
|
||||
assert "# repository (25.00% done)" in output
|
||||
assert "- file1.po 2 to do, 1 fuzzy." in output
|
||||
|
||||
def test_potodo_matching_files_fuzzy(self):
|
||||
output = check_output(
|
||||
[
|
||||
sys.executable,
|
||||
"-m",
|
||||
"potodo",
|
||||
"--matching-files",
|
||||
"--only-fuzzy",
|
||||
"-p",
|
||||
str(FIXTURES),
|
||||
],
|
||||
encoding="UTF-8",
|
||||
)
|
||||
output_short = check_output(
|
||||
[sys.executable, "-m", "potodo", "-l", "-f"]
|
||||
).decode("utf-8")
|
||||
assert output == output_short
|
||||
assert "file1.po" in output
|
||||
|
||||
# TODO: Test hide_reserved, offline options, only_reserved, exclude_reserved, show_reservation_dates
|
||||
# TODO: Test verbose output levels
|
||||
def test_potodo_exclude_fuzzy(run_potodo):
|
||||
output = run_potodo(["--exclude-fuzzy"]).out
|
||||
assert "- excluded.po 1 / 2 ( 50.0% translated)" in output
|
||||
assert "- file1.po 2 to do, 1 fuzzy." not in output
|
||||
|
||||
|
||||
def test_potodo_matching_files_solo(run_potodo):
|
||||
output = run_potodo(["--matching-files"]).out
|
||||
output_short = run_potodo(["-l"]).out
|
||||
assert output == output_short
|
||||
assert "excluded/file4.po" in output
|
||||
assert "folder/excluded.po" in output
|
||||
assert "folder/file3.po" in output
|
||||
assert "file1.po" in output
|
||||
assert "file2.po" in output
|
||||
|
||||
|
||||
def test_potodo_matching_files_fuzzy(run_potodo):
|
||||
output = run_potodo(["--matching-files", "--only-fuzzy"]).out
|
||||
output_short = run_potodo(["-l", "-f"]).out
|
||||
assert output == output_short
|
||||
assert "file1.po" in output
|
||||
|
||||
|
||||
# TODO: Test hide_reserved, offline options, only_reserved, exclude_reserved, show_reservation_dates
|
||||
# TODO: Test verbose output levels
|
||||
|
|
Loading…
Reference in New Issue