From 6ba32bc72b24b5b4ef4590ecfc7bf1cf02147c69 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sat, 27 Sep 2025 22:38:52 +0200 Subject: [PATCH 01/10] copy from #166 --- bin/contest.py | 2 + bin/tools.py | 118 +++++++++++++++++++++++-------------------------- 2 files changed, 58 insertions(+), 62 deletions(-) diff --git a/bin/contest.py b/bin/contest.py index 4801d3f5..376b38f5 100644 --- a/bin/contest.py +++ b/bin/contest.py @@ -38,6 +38,8 @@ def problems_yaml() -> Optional[list[dict[str, Any]]]: _problems_yaml = False return None _problems_yaml = read_yaml(problemsyaml_path) + if not isinstance(_problems_yaml, list): + fatal("problems.yaml must contain a list of problems") return cast(list[dict[str, Any]], _problems_yaml) diff --git a/bin/tools.py b/bin/tools.py index fa22784a..5d3018db 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -27,7 +27,7 @@ from collections import Counter from colorama import Style from pathlib import Path -from typing import cast, Literal, Optional +from typing import cast, Optional # Local imports import config @@ -75,47 +75,42 @@ fatal("BAPCtools requires at least Python 3.10.") -# Get the list of relevant problems. -# Either use the problems.yaml, or check the existence of problem.yaml and sort -# by shortname. -def get_problems(): - def is_problem_directory(path): - return (path / "problem.yaml").is_file() - - contest: Optional[Path] = None - problem: Optional[Path] = None - level: Optional[Literal["problem", "problemset"]] = None +# A path is a problem directory if it contains a `problem.yaml` file. +def is_problem_directory(path: Path) -> bool: + return (path / "problem.yaml").is_file() + + +# Changes the working directory to the root of the contest. +# Returns the "level" of the current command (either 'problem' or 'problemset') +# and, if `level == 'problem'`, the directory of the problem. +def change_directory() -> Optional[Path]: + problem_dir: Optional[Path] = None + config.level = "problemset" if config.args.contest: # TODO #102: replace cast with typed Namespace field - contest = cast(Path, config.args.contest).resolve() - os.chdir(contest) - level = "problemset" + contest_dir = cast(Path, config.args.contest).absolute() + os.chdir(contest_dir) if config.args.problem: # TODO #102: replace cast with typed Namespace field - problem = cast(Path, config.args.problem).resolve() - level = "problem" - os.chdir(problem.parent) - elif is_problem_directory(Path(".")): - problem = Path().cwd() - level = "problem" - os.chdir("..") - else: - level = "problemset" + problem_dir = cast(Path, config.args.problem).absolute() + elif is_problem_directory(Path().cwd()): + problem_dir = Path().cwd().absolute() + if problem_dir is not None: + config.level = "problem" + os.chdir(problem_dir.parent) + return problem_dir + +# Get the list of relevant problems. +# Either use the problems.yaml, +# or check the existence of problem.yaml and sort by shortname. +def get_problems(problem_dir: Optional[Path]) -> tuple[list[Problem], Path]: # We create one tmpdir per contest. h = hashlib.sha256(bytes(Path().cwd())).hexdigest()[-6:] tmpdir = Path(tempfile.gettempdir()) / ("bapctools_" + h) tmpdir.mkdir(parents=True, exist_ok=True) - def parse_problems_yaml(problemlist): - if problemlist is None: - fatal(f"Did not find any problem in {problemsyaml}.") - problemlist = problemlist - if problemlist is None: - problemlist = [] - if not isinstance(problemlist, list): - fatal("problems.yaml must contain a problems: list.") - + def parse_problems_yaml(problemlist: list[dict[str, Any]]) -> list[tuple[str, str]]: labels = dict[str, str]() # label -> shortname problems = [] for p in problemlist: @@ -136,7 +131,7 @@ def parse_problems_yaml(problemlist): error(f"No directory found for problem {shortname} mentioned in problems.yaml.") return problems - def fallback_problems(): + def fallback_problems() -> list[tuple[Path, str]]: problem_paths = list(filter(is_problem_directory, glob(Path("."), "*/"))) label = ( chr(ord("Z") - len(problem_paths) + 1) if contest_yaml().get("test_session") else "A" @@ -148,25 +143,24 @@ def fallback_problems(): return problems problems = [] - if level == "problem": - assert problem + if config.level == "problem": + assert problem_dir # If the problem is mentioned in problems.yaml, use that ID. problemsyaml = problems_yaml() if problemsyaml: problem_labels = parse_problems_yaml(problemsyaml) for shortname, label in problem_labels: - if shortname == problem.name: - problems = [Problem(Path(problem.name), tmpdir, label)] + if shortname == problem_dir.name: + problems = [Problem(Path(problem_dir.name), tmpdir, label)] break if len(problems) == 0: found_label = None for path, label in fallback_problems(): - if path.name == problem.name: + if path.name == problem_dir.name: found_label = label - problems = [Problem(Path(problem.name), tmpdir, found_label)] - else: - level = "problemset" + problems = [Problem(Path(problem_dir.name), tmpdir, found_label)] + else: # config.level == 'problemset' # If problems.yaml is available, use it. problemsyaml = problems_yaml() if problemsyaml: @@ -264,10 +258,8 @@ def key(self) -> tuple[int, int]: else: error("ruamel.yaml library not found. Update the order manually.") - contest_name = Path().cwd().name - # Filter problems by submissions/testcases, if given. - if level == "problemset" and (config.args.submissions or config.args.testcases): + if config.level == "problemset" and (config.args.submissions or config.args.testcases): submissions = config.args.submissions or [] testcases = config.args.testcases or [] @@ -286,8 +278,7 @@ def keep_problem(problem): problems = [p for p in problems if keep_problem(p)] - config.level = level - return problems, level, contest_name, tmpdir + return problems, tmpdir # NOTE: This is one of the few places that prints to stdout instead of stderr. @@ -1045,8 +1036,11 @@ def run_parsed_arguments(args): skel.new_problem() return - # Get problem_paths and cd to contest - problems, level, contest, tmpdir = get_problems() + # Get problems list and cd to contest directory + problem_dir = change_directory() + level = config.level + contest_name = Path().cwd().name + problems, tmpdir = get_problems(problem_dir) # Check non unique uuid # TODO: check this even more globally? @@ -1136,15 +1130,15 @@ def run_parsed_arguments(args): return if action == "gitlabci": - skel.create_gitlab_jobs(contest, problems) + skel.create_gitlab_jobs(contest_name, problems) return if action == "forgejo_actions": - skel.create_forgejo_actions(contest, problems) + skel.create_forgejo_actions(contest_name, problems) return if action == "github_actions": - skel.create_github_actions(contest, problems) + skel.create_github_actions(contest_name, problems) return if action == "skel": @@ -1293,15 +1287,15 @@ def run_parsed_arguments(args): export.export_contest_and_problems(problems, languages) if level == "problemset": - print(f"{Style.BRIGHT}CONTEST {contest}{Style.RESET_ALL}", file=sys.stderr) + print(f"{Style.BRIGHT}CONTEST {contest_name}{Style.RESET_ALL}", file=sys.stderr) # build pdf for the entire contest if action in ["pdf"]: - success &= latex.build_contest_pdfs(contest, problems, tmpdir, web=config.args.web) + success &= latex.build_contest_pdfs(contest_name, problems, tmpdir, web=config.args.web) if action in ["solutions"]: success &= latex.build_contest_pdfs( - contest, + contest_name, problems, tmpdir, build_type=latex.PdfType.SOLUTION, @@ -1310,7 +1304,7 @@ def run_parsed_arguments(args): if action in ["problem_slides"]: success &= latex.build_contest_pdfs( - contest, + contest_name, problems, tmpdir, build_type=latex.PdfType.PROBLEM_SLIDE, @@ -1329,20 +1323,20 @@ def run_parsed_arguments(args): ) for language in languages: - success &= latex.build_contest_pdfs(contest, problems, tmpdir, language) + success &= latex.build_contest_pdfs(contest_name, problems, tmpdir, language) success &= latex.build_contest_pdfs( - contest, problems, tmpdir, language, web=True + contest_name, problems, tmpdir, language, web=True ) if not config.args.no_solutions: success &= latex.build_contest_pdf( - contest, + contest_name, problems, tmpdir, language, build_type=latex.PdfType.SOLUTION, ) success &= latex.build_contest_pdf( - contest, + contest_name, problems, tmpdir, language, @@ -1351,7 +1345,7 @@ def run_parsed_arguments(args): ) if build_problem_slides: success &= latex.build_contest_pdf( - contest, + contest_name, problems, tmpdir, language, @@ -1361,9 +1355,9 @@ def run_parsed_arguments(args): if not build_problem_slides: log(f"No problem has {slideglob.name}, skipping problem slides") - outfile = contest + ".zip" + outfile = contest_name + ".zip" if config.args.kattis: - outfile = contest + "-kattis.zip" + outfile = contest_name + "-kattis.zip" export.build_contest_zip(problems, problem_zips, outfile, languages) if action in ["update_problems_yaml"]: From 3a797d5e722c2d98330d99281642e9837652fa0e Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 28 Sep 2025 00:43:21 +0200 Subject: [PATCH 02/10] add more types --- bin/tools.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/bin/tools.py b/bin/tools.py index 5d3018db..d0735162 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -27,7 +27,7 @@ from collections import Counter from colorama import Style from pathlib import Path -from typing import cast, Optional +from typing import Any, cast, Optional # Local imports import config @@ -193,7 +193,7 @@ def fallback_problems() -> list[tuple[Path, str]]: warn(f"{p.label} does not appear in 'order'") # Sort by position of id in order - def get_pos(id): + def get_pos(id: Optional[str]) -> int: if id in order: return order.index(id) else: @@ -211,7 +211,7 @@ def __init__(self): self.teams_submitted = 0 self.teams_pending = 0 - def update(self, team_stats: dict[str, Any]): + def update(self, team_stats: dict[str, Any]) -> None: if team_stats["solved"]: self.solved += 1 if team_stats["num_judged"]: @@ -263,7 +263,7 @@ def key(self) -> tuple[int, int]: submissions = config.args.submissions or [] testcases = config.args.testcases or [] - def keep_problem(problem): + def keep_problem(problem: Problem) -> bool: for s in submissions: x = resolve_path_argument(problem, s, "submissions") if x: @@ -282,12 +282,12 @@ def keep_problem(problem): # NOTE: This is one of the few places that prints to stdout instead of stderr. -def print_sorted(problems): +def print_sorted(problems: list[Problem]) -> None: for problem in problems: print(f"{problem.label:<2}: {problem.path}") -def split_submissions_and_testcases(s): +def split_submissions_and_testcases(s: list[Path]) -> tuple[list[Path], list[Path]]: # Everything containing data/, .in, or .ans goes into testcases. submissions = [] testcases = [] @@ -313,7 +313,7 @@ def __init__(self, **kwargs): super(SuppressingParser, self).__init__(**kwargs, argument_default=argparse.SUPPRESS) -def build_parser(): +def build_parser() -> SuppressingParser: parser = SuppressingParser( description=""" Tools for ICPC style problem sets. @@ -1005,8 +1005,7 @@ def build_parser(): return parser -# Takes a Namespace object returned by argparse.parse_args(). -def run_parsed_arguments(args): +def run_parsed_arguments(args: argparse.Namespace) -> None: # Process arguments config.args = args config.set_default_args() @@ -1410,12 +1409,12 @@ def read_personal_config(): # Takes command line arguments def main(): - def interrupt_handler(sig, frame): + def interrupt_handler(sig: Any, frame: Any) -> None: fatal("Running interrupted") signal.signal(signal.SIGINT, interrupt_handler) - # Don't zero newly allocated memory for this any any subprocess + # Don't zero newly allocated memory for this and any subprocess # Will likely only work on linux os.environ["MALLOC_PERTURB_"] = str(0b01011001) From c74aba089dfa98c47c6dc340d6bdd8433e107998 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 28 Sep 2025 01:21:46 +0200 Subject: [PATCH 03/10] improve error handling --- bin/contest.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/bin/contest.py b/bin/contest.py index 376b38f5..87e9d178 100644 --- a/bin/contest.py +++ b/bin/contest.py @@ -38,6 +38,9 @@ def problems_yaml() -> Optional[list[dict[str, Any]]]: _problems_yaml = False return None _problems_yaml = read_yaml(problemsyaml_path) + if _problems_yaml is None: + _problems_yaml = False + return None if not isinstance(_problems_yaml, list): fatal("problems.yaml must contain a list of problems") return cast(list[dict[str, Any]], _problems_yaml) From 71595fcf396d31301db39a6171595ca13e0ca815 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 28 Sep 2025 14:07:35 +0200 Subject: [PATCH 04/10] cd before reading personal config --- bin/config.py | 6 ++- bin/tools.py | 129 +++++++++++++++++++++++++------------------------ bin/upgrade.py | 31 ++++++------ bin/util.py | 5 ++ 4 files changed, 93 insertions(+), 78 deletions(-) diff --git a/bin/config.py b/bin/config.py index e7cb8026..76873e27 100644 --- a/bin/config.py +++ b/bin/config.py @@ -125,14 +125,18 @@ # fmt: on -def set_default_args() -> None: +def set_default_args() -> list[str]: # Set default argument values. + missing = [] for arg in ARGS_LIST: if not hasattr(args, arg): setattr(args, arg, None) + missing.append(arg) for arg, value in DEFAULT_ARGS.items(): if not hasattr(args, arg): setattr(args, arg, value) + missing.append(arg) + return missing level: Optional[Literal["problem", "problemset"]] = None diff --git a/bin/tools.py b/bin/tools.py index d0735162..7481980c 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -75,11 +75,6 @@ fatal("BAPCtools requires at least Python 3.10.") -# A path is a problem directory if it contains a `problem.yaml` file. -def is_problem_directory(path: Path) -> bool: - return (path / "problem.yaml").is_file() - - # Changes the working directory to the root of the contest. # Returns the "level" of the current command (either 'problem' or 'problemset') # and, if `level == 'problem'`, the directory of the problem. @@ -160,7 +155,8 @@ def fallback_problems() -> list[tuple[Path, str]]: if path.name == problem_dir.name: found_label = label problems = [Problem(Path(problem_dir.name), tmpdir, found_label)] - else: # config.level == 'problemset' + else: + assert config.level == "problemset" # If problems.yaml is available, use it. problemsyaml = problems_yaml() if problemsyaml: @@ -1005,42 +1001,90 @@ def build_parser() -> SuppressingParser: return parser -def run_parsed_arguments(args: argparse.Namespace) -> None: +def find_personal_config() -> Optional[Path]: + if is_windows(): + app_data = os.getenv("AppData") + return Path(app_data) if app_data else None + else: + home = os.getenv("HOME") + xdg_config_home = os.getenv("XDG_CONFIG_HOME") + return ( + Path(xdg_config_home) if xdg_config_home else Path(home) / ".config" if home else None + ) + + +def read_personal_config() -> dict[str, Any]: + args = {} + home_config = find_personal_config() + + for config_file in [ + # Highest prio: contest directory + Path() / ".bapctools.yaml", + Path() / ".." / ".bapctools.yaml", + ] + ( + # Lowest prio: user config directory + [home_config / "bapctools" / "config.yaml"] if home_config else [] + ): + if not config_file.is_file(): + continue + config_data = read_yaml(config_file) or {} + for arg, value in config_data.items(): + if arg not in args: + args[arg] = value + + return args + + +def run_parsed_arguments(args: argparse.Namespace, personal_config: bool = True) -> None: + # Don't zero newly allocated memory for this and any subprocess + # Will likely only have an effect on linux + os.environ["MALLOC_PERTURB_"] = str(0b01011001) + # Process arguments config.args = args - config.set_default_args() + missing_args = config.set_default_args() - action = config.args.action + # cd to contest directory + problem_dir = change_directory() + level = config.level + contest_name = Path().cwd().name - # Split submissions and testcases when needed. - if action in ["run", "fuzz", "time_limit"]: - if config.args.submissions: - config.args.submissions, config.args.testcases = split_submissions_and_testcases( - config.args.submissions - ) - else: - config.args.testcases = [] + if personal_config: + personal_args = read_personal_config() + for arg in missing_args: + if arg in personal_args: + setattr(config.args, arg, personal_args[arg]) + + action = config.args.action # upgrade commands. if action == "upgrade": - upgrade.upgrade() + upgrade.upgrade(problem_dir) return # Skel commands. if action == "new_contest": + os.chdir(config.current_working_directory) skel.new_contest() return if action == "new_problem": + os.chdir(config.current_working_directory) skel.new_problem() return - # Get problems list and cd to contest directory - problem_dir = change_directory() - level = config.level - contest_name = Path().cwd().name + # get problems list problems, tmpdir = get_problems(problem_dir) + # Split submissions and testcases when needed. + if action in ["run", "fuzz", "time_limit"]: + if config.args.submissions: + config.args.submissions, config.args.testcases = split_submissions_and_testcases( + config.args.submissions + ) + else: + config.args.testcases = [] + # Check non unique uuid # TODO: check this even more globally? uuids: dict[str, Problem] = {} @@ -1373,40 +1417,6 @@ def run_parsed_arguments(args: argparse.Namespace) -> None: sys.exit(1) -def find_personal_config() -> Optional[Path]: - if is_windows(): - app_data = os.getenv("AppData") - return Path(app_data) if app_data else None - else: - home = os.getenv("HOME") - xdg_config_home = os.getenv("XDG_CONFIG_HOME") - return ( - Path(xdg_config_home) if xdg_config_home else Path(home) / ".config" if home else None - ) - - -def read_personal_config(): - args = {} - home_config = find_personal_config() - - for config_file in [ - # Highest prio: contest directory - Path() / ".bapctools.yaml", - Path() / ".." / ".bapctools.yaml", - ] + ( - # Lowest prio: user config directory - [home_config / "bapctools" / "config.yaml"] if home_config else [] - ): - if not config_file.is_file(): - continue - config_data = read_yaml(config_file) or {} - for arg, value in config_data.items(): - if arg not in args: - args[arg] = value - - return args - - # Takes command line arguments def main(): def interrupt_handler(sig: Any, frame: Any) -> None: @@ -1414,13 +1424,8 @@ def interrupt_handler(sig: Any, frame: Any) -> None: signal.signal(signal.SIGINT, interrupt_handler) - # Don't zero newly allocated memory for this and any subprocess - # Will likely only work on linux - os.environ["MALLOC_PERTURB_"] = str(0b01011001) - try: parser = build_parser() - parser.set_defaults(**read_personal_config()) run_parsed_arguments(parser.parse_args()) except AbortException: fatal("Running interrupted") @@ -1442,7 +1447,7 @@ def test(args): contest._problems_yaml = None try: parser = build_parser() - run_parsed_arguments(parser.parse_args(args)) + run_parsed_arguments(parser.parse_args(), personal_config=False) finally: os.chdir(original_directory) ProgressBar.current_bar = None diff --git a/bin/upgrade.py b/bin/upgrade.py index 12aeb4c3..c56a1bb6 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -609,26 +609,27 @@ def _upgrade(problem_path: Path, bar: ProgressBar) -> None: bar.done() -def upgrade() -> None: +def upgrade(problem_dir: Optional[Path]) -> None: if not has_ryaml: error("upgrade needs the ruamel.yaml python3 library. Install python[3]-ruamel.yaml.") return - cwd = Path().cwd() - def is_problem_directory(path: Path) -> bool: - return (path / "problem.yaml").is_file() - - if is_problem_directory(cwd): - paths = [cwd] + if config.level == "problem": + assert problem_dir + if not is_problem_directory(problem_dir): + fatal(f"{problem_dir} does not contain a problem.yaml") + paths = [problem_dir] + bar = ProgressBar("upgrade", items=paths) else: - paths = [p for p in cwd.iterdir() if is_problem_directory(p)] - - bar = ProgressBar("upgrade", items=["contest.yaml", *paths]) - - bar.start("contest.yaml") - if (cwd / "contest.yaml").is_file(): - upgrade_contest_yaml(cwd / "contest.yaml", bar) - bar.done() + assert config.level == "problemset" + contest_dir = Path().cwd() + paths = [p for p in contest_dir.iterdir() if is_problem_directory(p)] + bar = ProgressBar("upgrade", items=["contest.yaml", *paths]) + + bar.start("contest.yaml") + if (contest_dir / "contest.yaml").is_file(): + upgrade_contest_yaml(contest_dir / "contest.yaml", bar) + bar.done() for path in paths: _upgrade(path, bar) diff --git a/bin/util.py b/bin/util.py index 89d278df..5acc57e0 100644 --- a/bin/util.py +++ b/bin/util.py @@ -1461,6 +1461,11 @@ def inc_label(label: str) -> str: return "A" + label +# A path is a problem directory if it contains a `problem.yaml` file. +def is_problem_directory(path: Path) -> bool: + return (path / "problem.yaml").is_file() + + def combine_hashes(values: Sequence[str]) -> str: hasher = hashlib.sha512(usedforsecurity=False) for item in sorted(values): From 72ad72e82575dcfdcb48dfe5f09d15aec42287be Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 28 Sep 2025 14:16:32 +0200 Subject: [PATCH 05/10] pass args for test --- bin/tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/tools.py b/bin/tools.py index 7481980c..bd39190e 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -1418,7 +1418,7 @@ def run_parsed_arguments(args: argparse.Namespace, personal_config: bool = True) # Takes command line arguments -def main(): +def main() -> None: def interrupt_handler(sig: Any, frame: Any) -> None: fatal("Running interrupted") @@ -1435,7 +1435,7 @@ def interrupt_handler(sig: Any, frame: Any) -> None: main() -def test(args): +def test(args: list[str]) -> None: config.RUNNING_TEST = True # Make sure to cd back to the original directory before returning. @@ -1447,7 +1447,7 @@ def test(args): contest._problems_yaml = None try: parser = build_parser() - run_parsed_arguments(parser.parse_args(), personal_config=False) + run_parsed_arguments(parser.parse_args(args), personal_config=False) finally: os.chdir(original_directory) ProgressBar.current_bar = None From 854c64f6436dc585541a6d5a10dead373eb27855 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 28 Sep 2025 14:31:19 +0200 Subject: [PATCH 06/10] fix cwd? --- bin/tools.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/bin/tools.py b/bin/tools.py index bd39190e..054218c1 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -1045,6 +1045,7 @@ def run_parsed_arguments(args: argparse.Namespace, personal_config: bool = True) missing_args = config.set_default_args() # cd to contest directory + call_cwd = Path().cwd() problem_dir = change_directory() level = config.level contest_name = Path().cwd().name @@ -1064,12 +1065,12 @@ def run_parsed_arguments(args: argparse.Namespace, personal_config: bool = True) # Skel commands. if action == "new_contest": - os.chdir(config.current_working_directory) + os.chdir(call_cwd) skel.new_contest() return if action == "new_problem": - os.chdir(config.current_working_directory) + os.chdir(call_cwd) skel.new_problem() return From 677903892bbb32bea6d86fc59700e7b2da5b9355 Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 5 Oct 2025 17:24:02 +0200 Subject: [PATCH 07/10] only read .bapctools config inside contest and problem --- bin/tools.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/bin/tools.py b/bin/tools.py index 054218c1..2dd3e427 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -1013,18 +1013,18 @@ def find_personal_config() -> Optional[Path]: ) -def read_personal_config() -> dict[str, Any]: +def read_personal_config(problem_dir: Optional[Path]) -> dict[str, Any]: args = {} home_config = find_personal_config() - - for config_file in [ - # Highest prio: contest directory - Path() / ".bapctools.yaml", - Path() / ".." / ".bapctools.yaml", - ] + ( - # Lowest prio: user config directory - [home_config / "bapctools" / "config.yaml"] if home_config else [] - ): + # possible config files, sorted by priority + config_files = [] + if problem_dir: + config_files.append(problem_dir / ".bapctools.yaml") + config_files.append(Path().cwd() / ".bapctools.yaml") + if home_config: + config_files.append(home_config / "bapctools" / "config.yaml") + + for config_file in config_files: if not config_file.is_file(): continue config_data = read_yaml(config_file) or {} @@ -1051,7 +1051,7 @@ def run_parsed_arguments(args: argparse.Namespace, personal_config: bool = True) contest_name = Path().cwd().name if personal_config: - personal_args = read_personal_config() + personal_args = read_personal_config(problem_dir) for arg in missing_args: if arg in personal_args: setattr(config.args, arg, personal_args[arg]) From 7ff2ecb7c5c18c8a861dab4c8f254265069b46fb Mon Sep 17 00:00:00 2001 From: mzuenni Date: Sun, 5 Oct 2025 17:43:40 +0200 Subject: [PATCH 08/10] dont use string find to identify testcases --- bin/tools.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/bin/tools.py b/bin/tools.py index 2dd3e427..334bb765 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -284,17 +284,19 @@ def print_sorted(problems: list[Problem]) -> None: def split_submissions_and_testcases(s: list[Path]) -> tuple[list[Path], list[Path]]: - # Everything containing data/, .in, or .ans goes into testcases. + # We try to identify testcases by common directory names and common suffixes submissions = [] testcases = [] for p in s: - ps = str(p) - if "data" in ps or "sample" in ps or "secret" in ps or ".in" in ps or ".ans" in ps: - # Strip potential .ans and .in - if p.suffix in [".ans", ".in"]: - testcases.append(p.with_suffix("")) - else: - testcases.append(p) + testcase_dirs = ["data", "sample", "secret", "fuzz"] + if ( + any(part in testcase_dirs for part in p.parts) + or p.suffix in config.KNOWN_DATA_EXTENSIONS + ): + # Strip potential suffix + if p.suffix in config.KNOWN_DATA_EXTENSIONS: + p = p.with_suffix("") + testcases.append(p) else: submissions.append(p) return (submissions, testcases) From 7d1eccfae80ecb3ac3e6fb2b69e22ad84ac542af Mon Sep 17 00:00:00 2001 From: MZuenni Date: Fri, 10 Oct 2025 13:04:05 +0200 Subject: [PATCH 09/10] make call_cwd absolute --- bin/tools.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/tools.py b/bin/tools.py index 334bb765..936c2f7d 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -76,8 +76,8 @@ # Changes the working directory to the root of the contest. -# Returns the "level" of the current command (either 'problem' or 'problemset') -# and, if `level == 'problem'`, the directory of the problem. +# sets the "level" of the current command (either 'problem' or 'problemset') +# and, if `level == 'problem'` returns the directory of the problem. def change_directory() -> Optional[Path]: problem_dir: Optional[Path] = None config.level = "problemset" @@ -1047,7 +1047,7 @@ def run_parsed_arguments(args: argparse.Namespace, personal_config: bool = True) missing_args = config.set_default_args() # cd to contest directory - call_cwd = Path().cwd() + call_cwd = Path().cwd().absolute() problem_dir = change_directory() level = config.level contest_name = Path().cwd().name From ad34218825583e563026a4e9ff7dae68c881c8e8 Mon Sep 17 00:00:00 2001 From: MZuenni Date: Fri, 10 Oct 2025 13:12:29 +0200 Subject: [PATCH 10/10] use class method properly --- bin/stats.py | 2 +- bin/tools.py | 14 +++++++------- bin/upgrade.py | 2 +- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/bin/stats.py b/bin/stats.py index 0ce1681b..a65187fd 100644 --- a/bin/stats.py +++ b/bin/stats.py @@ -380,7 +380,7 @@ def get_submissions_row( values = [] for problem in problems: directory = ( - Path().cwd() / "submissions" / problem.name + Path.cwd() / "submissions" / problem.name if team_submissions else problem.path / "submissions" ) diff --git a/bin/tools.py b/bin/tools.py index 936c2f7d..08633a84 100755 --- a/bin/tools.py +++ b/bin/tools.py @@ -88,8 +88,8 @@ def change_directory() -> Optional[Path]: if config.args.problem: # TODO #102: replace cast with typed Namespace field problem_dir = cast(Path, config.args.problem).absolute() - elif is_problem_directory(Path().cwd()): - problem_dir = Path().cwd().absolute() + elif is_problem_directory(Path.cwd()): + problem_dir = Path.cwd().absolute() if problem_dir is not None: config.level = "problem" os.chdir(problem_dir.parent) @@ -101,7 +101,7 @@ def change_directory() -> Optional[Path]: # or check the existence of problem.yaml and sort by shortname. def get_problems(problem_dir: Optional[Path]) -> tuple[list[Problem], Path]: # We create one tmpdir per contest. - h = hashlib.sha256(bytes(Path().cwd())).hexdigest()[-6:] + h = hashlib.sha256(bytes(Path.cwd())).hexdigest()[-6:] tmpdir = Path(tempfile.gettempdir()) / ("bapctools_" + h) tmpdir.mkdir(parents=True, exist_ok=True) @@ -1022,7 +1022,7 @@ def read_personal_config(problem_dir: Optional[Path]) -> dict[str, Any]: config_files = [] if problem_dir: config_files.append(problem_dir / ".bapctools.yaml") - config_files.append(Path().cwd() / ".bapctools.yaml") + config_files.append(Path.cwd() / ".bapctools.yaml") if home_config: config_files.append(home_config / "bapctools" / "config.yaml") @@ -1047,10 +1047,10 @@ def run_parsed_arguments(args: argparse.Namespace, personal_config: bool = True) missing_args = config.set_default_args() # cd to contest directory - call_cwd = Path().cwd().absolute() + call_cwd = Path.cwd().absolute() problem_dir = change_directory() level = config.level - contest_name = Path().cwd().name + contest_name = Path.cwd().name if personal_config: personal_args = read_personal_config(problem_dir) @@ -1443,7 +1443,7 @@ def test(args: list[str]) -> None: # Make sure to cd back to the original directory before returning. # Needed to stay in the same directory in tests. - original_directory = Path().cwd() + original_directory = Path.cwd() config.n_warn = 0 config.n_error = 0 contest._contest_yaml = None diff --git a/bin/upgrade.py b/bin/upgrade.py index c56a1bb6..5362e9c4 100644 --- a/bin/upgrade.py +++ b/bin/upgrade.py @@ -622,7 +622,7 @@ def upgrade(problem_dir: Optional[Path]) -> None: bar = ProgressBar("upgrade", items=paths) else: assert config.level == "problemset" - contest_dir = Path().cwd() + contest_dir = Path.cwd() paths = [p for p in contest_dir.iterdir() if is_problem_directory(p)] bar = ProgressBar("upgrade", items=["contest.yaml", *paths])