diff --git a/crifx/cli.py b/crifx/cli.py index 2e6d246..27b5c73 100644 --- a/crifx/cli.py +++ b/crifx/cli.py @@ -15,6 +15,7 @@ def _dir_path_argparse_type(path): + """Check that the provided path is a directory.""" if os.path.isdir(path): return path else: diff --git a/crifx/config_parser.py b/crifx/config_parser.py index 102659c..f255a91 100644 --- a/crifx/config_parser.py +++ b/crifx/config_parser.py @@ -80,10 +80,13 @@ class LanguageGroupConfig: required_ac_count: int = 0 @staticmethod - def from_toml_dict( - toml_dict: dict[str, Any], group_identifier: str - ) -> "LanguageGroupConfig": + def from_toml_dict(toml_dict: dict[str, Any]) -> "LanguageGroupConfig": """Initialize a LanguageGroupConfig from a toml dict.""" + group_identifier = toml_dict.get("name") + if group_identifier is None: + raise ValueError( + "Language group in the `crifx.toml` file is missing a 'name'" + ) language_names = toml_dict.get("languages", []) languages = [] for language_name in language_names: @@ -110,13 +113,16 @@ class AliasGroup: aliases: list[str] @staticmethod - def from_toml_dict( - toml_dict: dict[str, Any], group_identifier: str - ) -> "AliasGroup": + def from_toml_dict(toml_dict: dict[str, Any]) -> "AliasGroup": """Parse an AliasGroup from a toml dictionary.""" - aliases = toml_dict.get("aliases", []) + primary_name = toml_dict.get("primary_name") + if primary_name is None: + raise ValueError( + "The `crifx.toml` file is missing a 'primary_name' for one or more 'judge' tables." + ) git_name = toml_dict.get("git_name") - return AliasGroup(group_identifier, git_name, aliases) + aliases = toml_dict.get("aliases", []) + return AliasGroup(primary_name, git_name, aliases) class Config: @@ -129,18 +135,18 @@ def __init__(self, toml_dict): ) self.language_group_configs = [] self.alias_groups = [] - language_groups = toml_dict.get("language_groups", {}) - for group_identifier, language_group_dict in language_groups.items(): + language_groups = toml_dict.get("language_group", []) + for language_group_dict in language_groups: language_group_config = LanguageGroupConfig.from_toml_dict( - language_group_dict, group_identifier + language_group_dict, ) if not language_group_config.language_group.languages: # No languages parsed from the language group. continue self.language_group_configs.append(language_group_config) - alias_groups = toml_dict.get("aliases", {}) - for identifier, alias_group_dict in alias_groups.items(): - alias_group = AliasGroup.from_toml_dict(alias_group_dict, identifier) + alias_groups = toml_dict.get("judge", []) + for alias_group_dict in alias_groups: + alias_group = AliasGroup.from_toml_dict(alias_group_dict) self.alias_groups.append(alias_group) for alias_group_1, alias_group_2 in itertools.product( self.alias_groups, self.alias_groups diff --git a/crifx/problemset_parser.py b/crifx/problemset_parser.py index 8731c94..ee8e83a 100644 --- a/crifx/problemset_parser.py +++ b/crifx/problemset_parser.py @@ -2,6 +2,7 @@ import logging import os +import re import tomllib from typing import Any @@ -26,6 +27,7 @@ TEST_CASE_IMAGE_EXTENSIONS = ["png", "jpg", "jpeg"] PROBLEM_REVIEW_STATUS_FILENAME = "crifx-problem-status.toml" +CRIFX_AUTHOR_PATTERN = "crifx!\(author=([a-zA-Z0-9_ ]+)\)" class ProblemSetParser: @@ -199,19 +201,22 @@ def _parse_submissions_dir( if language is None: continue submission_path = os.path.join(submissions_dir, filename) - git_user_guess = self.git_manager.guess_file_author(submission_path) - filename_guess = self.guess_author_by_filename(filename) - if filename_guess is None: - judge = ( - self.judges_by_name.get(getattr(git_user_guess, "name")) - or UNKNOWN_JUDGE - ) - else: - judge = filename_guess lines_of_code = 0 file_bytes = 0 + author_name_override = None try: with open(submission_path, "r") as submission_file: + submission_lines = submission_file.readlines() + for line_number, line in enumerate(submission_lines): + author_match = re.search(CRIFX_AUTHOR_PATTERN, line) + if author_match is not None: + logging.debug( + "Found author override for file %s on line %d", + submission_path, + line_number + 1, + ) + author_name_override = author_match.group(1) + break lines_of_code = len(submission_file.readlines()) file_bytes = os.stat(submission_path).st_size except (FileExistsError, FileNotFoundError, PermissionError): @@ -219,6 +224,22 @@ def _parse_submissions_dir( "Could not determine size of submission at path '%s'", submission_path, ) + git_user_guess = self.git_manager.guess_file_author(submission_path) + filename_guess = self.guess_author_by_filename(filename) + if author_name_override is not None: + judge = UNKNOWN_JUDGE + for candidate_judge in self.judges_by_name.values(): + if candidate_judge.has_alias(author_name_override): + judge = candidate_judge + break + elif filename_guess is not None: + judge = filename_guess + + else: + judge = ( + self.judges_by_name.get(getattr(git_user_guess, "name")) + or UNKNOWN_JUDGE + ) submission = Submission( judge, filename, diff --git a/examples/example_problemset/addtwonumbers/submissions/accepted/add_two.py b/examples/example_problemset/addtwonumbers/submissions/accepted/add_two_jd.py similarity index 51% rename from examples/example_problemset/addtwonumbers/submissions/accepted/add_two.py rename to examples/example_problemset/addtwonumbers/submissions/accepted/add_two_jd.py index 94d6754..26cd027 100644 --- a/examples/example_problemset/addtwonumbers/submissions/accepted/add_two.py +++ b/examples/example_problemset/addtwonumbers/submissions/accepted/add_two_jd.py @@ -1,5 +1,11 @@ #!/bin/python3 +""" +This file uses 'jd' in the filename. + +This is an alias defined for Jane Doe in the crifx.toml file. +""" + from sys import stdin tokens = stdin.readline().split(" ") diff --git a/examples/example_problemset/crifx.toml b/examples/example_problemset/crifx.toml index a9c9d1f..24adbcd 100644 --- a/examples/example_problemset/crifx.toml +++ b/examples/example_problemset/crifx.toml @@ -22,21 +22,36 @@ validator_reviewers = 2 # There must be at least 2 judges who have reviewed the test data. data_reviewers = 2 -# Section for defining language groups. -[language_groups] -# Non alphanumeric characters can be used in the language group name crifx -# the language group name is written in double quotes "". -[language_groups."c/c++"] +# Define judge names in `[[judge]]` tables. +[[judge]] +# Primary name is required. +primary_name = "Jane Doe" +# git_name is not required. +# git_name = "janedoe" +# Aliases is a list of zero or more alternative names. +aliases = ["jd", "jane"] + +[[judge]] +primary_name = "Homer Simpson" +git_name = "homers123" +aliases = [] + +# Tables for defining language groups. +[[language_group]] +# Non alphanumeric characters can be used in the language group name. +name = "c/c++" # The names of the languages in the group. Language names are cast to # lowercase before comparison to language names known to crifx. languages = ["C", "C++"] # The number of AC submissions required for this group. required_ac_count = 0 -[language_groups."java/kotlin"] +[[language_group]] +name = "java/kotlin" languages = ["Java", "Kotlin"] required_ac_count = 0 -[language_groups.python] +[[language_group]] +name = "python" languages = ["Python"] required_ac_count = 0 diff --git a/examples/example_problemset/helloworld/submissions/accepted/hello.py b/examples/example_problemset/helloworld/submissions/accepted/hello.py index 7c6d7b2..9720602 100644 --- a/examples/example_problemset/helloworld/submissions/accepted/hello.py +++ b/examples/example_problemset/helloworld/submissions/accepted/hello.py @@ -4,7 +4,7 @@ # Use a crifx command to override the author for this submission instead # of relying on git blame information. -# crifx!(author="Homer Simpson") +# crifx!(author=Homer Simpson) from sys import stdin diff --git a/tests/scenarios/sample_contest_1/crifx.toml b/tests/scenarios/sample_contest_1/crifx.toml new file mode 100644 index 0000000..a0ed22a --- /dev/null +++ b/tests/scenarios/sample_contest_1/crifx.toml @@ -0,0 +1,3 @@ +[[judge]] +primary_name = "Jane Doe" +aliases = [] \ No newline at end of file diff --git a/tests/scenarios/sample_contest_1/problem_a/submissions/accepted/hello_world.java b/tests/scenarios/sample_contest_1/problem_a/submissions/accepted/hello_world.java index d2fecf6..6a10580 100644 --- a/tests/scenarios/sample_contest_1/problem_a/submissions/accepted/hello_world.java +++ b/tests/scenarios/sample_contest_1/problem_a/submissions/accepted/hello_world.java @@ -1,3 +1,9 @@ +/** + * Example solution with a name override. + * + * crifx!(author=Jane Doe) + */ + public class hello_world { public static void main(String[] args) { System.out.println("Hello world!"); diff --git a/tests/test_problemset_parsing.py b/tests/test_problemset_parsing.py index 0f1fe6e..2366225 100644 --- a/tests/test_problemset_parsing.py +++ b/tests/test_problemset_parsing.py @@ -2,6 +2,8 @@ import os +from crifx.config_parser import parse_config +from crifx.contest_objects import ProgrammingLanguage from crifx.git_manager import GitManager from crifx.problemset_parser import ProblemSetParser @@ -10,7 +12,8 @@ def test_scenario_1(scenarios_path): """Test the basic scenario 1.""" path = os.path.join(scenarios_path, "sample_contest_1") git_manager = GitManager(scenarios_path) - parser = ProblemSetParser(path, git_manager, []) + config = parse_config(path) + parser = ProblemSetParser(path, git_manager, config.alias_groups) problemset = parser.parse_problemset() problem_a = next( problem for problem in problemset.problems if problem.name == "problem_a" @@ -32,3 +35,10 @@ def test_scenario_1(scenarios_path): assert len(problem_a.ac_submissions) == 2 assert len(problem_a.wa_submissions) == 1 assert len(problem_a.tle_submissions) == 0 + + problem_a_java_ac = next( + sub + for sub in problem_a.ac_submissions + if sub.language is ProgrammingLanguage.JAVA + ) + assert problem_a_java_ac.author.primary_name == "Jane Doe"