Skip to content

Commit 80a1768

Browse files
authored
Merge pull request #284 from square-cylinder/develop
Remove deprecated functionality of legacy format
2 parents 081b5be + c7b0365 commit 80a1768

File tree

2 files changed

+3
-300
lines changed

2 files changed

+3
-300
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
/problemtools.egg-info/
55
/support/default_validator/default_validator
66
/support/interactive/interactive
7+
build/

problemtools/verifyproblem.py

Lines changed: 2 additions & 300 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,6 @@ def __init__(self, args: argparse.Namespace, executor: ThreadPoolExecutor|None)
9999
self.data_filter: Pattern[str] = args.data_filter
100100
self.submission_filter: Pattern[str] = args.submission_filter
101101
self.fixed_timelim: int|None = args.fixed_timelim
102-
self.compile_generators: bool = ('compile_generators' not in args or args.compile_generators)
103102
self.executor = executor
104103
self._background_work: list[concurrent.futures.Future[object]] = []
105104

@@ -729,7 +728,6 @@ def __init__(self, problem: Problem):
729728
elif param == 'interactive':
730729
pass
731730

732-
self._data['languages'] = self._data['languages'].split()
733731

734732
def __str__(self) -> str:
735733
return 'problem configuration'
@@ -819,299 +817,12 @@ def check(self, context: Context) -> bool:
819817
self.error('Limits key in problem.yaml must specify a dict')
820818
self._data['limits'] = ProblemConfig._OPTIONAL_CONFIG['limits']
821819

822-
if self._data['languages'] != '':
823-
for lang_id in self._data['languages']:
824-
if lang_id != 'all' and self._problem.language_config.get(lang_id) is None:
825-
self.error("Unrecognized language id '%s'" % lang_id)
826-
827820
# Some things not yet implemented
828821
if self._data['libraries'] != '':
829822
self.error("Libraries not yet supported")
830823

831824
return self._check_res
832825

833-
834-
class Generators(ProblemAspect):
835-
_TESTCASE_OPTIONS = ['input', 'solution', 'visualizer', 'random_salt']
836-
_NULLABLE_OPTIONS = ['input', 'solution', 'visualizer']
837-
_DATA_DIRECTORIES = {'sample', 'secret'}
838-
_VISUALIZER_EXTENSIONS = ['png', 'jpg', 'jpeg', 'svg', 'interaction', 'desc', 'hint']
839-
840-
def __init__(self, problem: Problem):
841-
super().__init__(f"{problem.shortname}.generators")
842-
self.debug(' Loading generators')
843-
self._problem = problem
844-
self.configfile = os.path.join(problem.probdir, 'generators', 'generators.yaml')
845-
self._data = None
846-
self._generators: dict[str, str|list[str]|run.Program] = {}
847-
848-
if os.path.isfile(self.configfile):
849-
try:
850-
with open(self.configfile) as f:
851-
self._data = yaml.safe_load(f)
852-
# Loading empty yaml yields None, for no apparent reason...
853-
if self._data is None:
854-
self._data = {}
855-
except Exception as e:
856-
self.error(str(e))
857-
858-
if isinstance(self._data, dict):
859-
# The top-level dict always represents a directory, even if there
860-
# is no type key
861-
self._data['type'] = 'directory'
862-
863-
def __str__(self) -> str:
864-
return 'generators'
865-
866-
def _parse_command(self, key: str, state: dict) -> tuple[str, list[str]]|None:
867-
command = state[key]
868-
name = os.path.basename(state['path'])
869-
random_salt = str(state['random_salt'])
870-
871-
def err() -> None:
872-
self.error('Invalid %s key for path %s in generators.yaml' % (key, state['path']))
873-
874-
if not isinstance(command, str):
875-
err()
876-
return None
877-
878-
seed = str(int(hashlib.sha512((random_salt + command).encode('utf-8')).hexdigest(), 16) % (2**31))
879-
880-
parts = shlex.split(command)
881-
if not parts:
882-
err()
883-
return None
884-
885-
for i, part in enumerate(parts):
886-
new = ''
887-
for j, group in enumerate(part.split('{')):
888-
if group.count('}') != (0 if j == 0 else 1):
889-
err()
890-
return None
891-
if j == 0:
892-
new += group
893-
else:
894-
group, rest = group.split('}')
895-
if group.startswith('seed'):
896-
new += seed
897-
elif group == 'name':
898-
new += name
899-
else:
900-
err()
901-
return None
902-
new += rest
903-
parts[i] = new
904-
905-
program, arguments = parts[0], parts[1:]
906-
if program not in self._generators:
907-
self._generators[program] = program
908-
909-
return (program, arguments)
910-
911-
def _parse_testcase(self, data: dict, state: dict) -> None:
912-
if state['input'] is None:
913-
self.error('Path %s in generators.yaml must contain an input key' % state['path'])
914-
for key in ['input', 'solution', 'visualizer']:
915-
if state[key] is not None:
916-
state[key] = self._parse_command(key, state)
917-
918-
def _parse_directory(self, data: dict, state: dict) -> None:
919-
# TODO: Process includes
920-
921-
if 'testdata.yaml' in data:
922-
content = data['testdata.yaml']
923-
if content is None:
924-
content = {}
925-
926-
cases = data.get('data', {})
927-
ordered = True
928-
if not isinstance(cases, list):
929-
ordered = False
930-
cases = [cases]
931-
932-
case_counter = 0
933-
case_format = '%%0%dd' % len(str(len(cases)))
934-
for case in cases:
935-
if not isinstance(case, dict):
936-
self.error('Path %s/data in generators.yaml must contain a dict or a list of dicts' % state['path'])
937-
continue
938-
939-
if ordered:
940-
case_counter += 1
941-
942-
for name, value in sorted(case.items(), key=lambda kv: str(kv[0])):
943-
if ordered:
944-
num = case_format % case_counter
945-
name = num + ('' if name is None else '-' + str(name))
946-
else:
947-
name = str(name)
948-
949-
next_state = copy.deepcopy(state)
950-
next_state['path'] = '%s/%s' % (state['path'], name)
951-
self._parse_element(value, next_state)
952-
953-
def _parse_element(self, data: dict, state: dict) -> None:
954-
if data is None:
955-
data = '/%s.in' % state['path']
956-
state['manual'] = True
957-
if isinstance(data, str):
958-
data = { 'input': data }
959-
if not isinstance(data, dict):
960-
self.error("Path %s in generators.yaml must specify a dict" % state['path'])
961-
return
962-
963-
state.update({
964-
key: data[key]
965-
for key in Generators._TESTCASE_OPTIONS
966-
if key in data
967-
})
968-
969-
if data.get('type', 'testcase') == 'testcase':
970-
self._parse_testcase(data, state)
971-
else:
972-
if data['type'] != 'directory':
973-
self.error("Type of %s in generators.yaml must be 'directory'" % state['path'])
974-
self._parse_directory(data, state)
975-
976-
def _resolve_path(self, path: str) -> str:
977-
base_path = self._problem.probdir
978-
if path.startswith('/'):
979-
path = path[1:]
980-
else:
981-
base_path = os.path.join(base_path, 'generators')
982-
return os.path.join(*([base_path] + path.split('/')))
983-
984-
def _compile_generators(self) -> None:
985-
for gen, files in list(self._generators.items()):
986-
implicit = True
987-
manual = False
988-
if isinstance(files, str):
989-
path = files
990-
files = []
991-
implicit = False
992-
if path.endswith('.in'):
993-
manual = True
994-
for ext in ['ans'] + Generators._VISUALIZER_EXTENSIONS:
995-
other_path = path[:-2] + ext
996-
if os.path.isfile(self._resolve_path(other_path)):
997-
files.append(other_path)
998-
# Always add original file last, to ensure it is chosen as
999-
# the representative file
1000-
files.append(path)
1001-
if not isinstance(files, list) or not files:
1002-
self.error('Invalid generator %s in generators.yaml' % gen)
1003-
continue
1004-
tmpdir = tempfile.mkdtemp(prefix='generator', dir=self._problem.tmpdir)
1005-
ok = True
1006-
for opath in files:
1007-
if not isinstance(opath, str) or not opath:
1008-
self.error('Invalid generator %s in generators.yaml' % gen)
1009-
ok = False
1010-
break
1011-
1012-
name = os.path.basename(opath)
1013-
if implicit and opath == files[0]:
1014-
# In implicit generators, the first listed file should
1015-
# be the entry point. problemtools usually picks the
1016-
# lexicographically smallest filename as the entry
1017-
# point, unless there exists a file that starts with
1018-
# "main.". Thus the following renames the file that
1019-
# should be the entry point to "main.old.extension".
1020-
# TODO: Make problemtools support passing a different
1021-
# entry point than "main.", and remove this hack.
1022-
name = 'main' + os.path.splitext(name)[1]
1023-
1024-
fpath = self._resolve_path(opath)
1025-
dest = os.path.join(tmpdir, name)
1026-
if os.path.exists(dest):
1027-
self.error('Duplicate entry for filename %s in generator %s' % (name, gen))
1028-
ok = False
1029-
elif not os.path.exists(fpath):
1030-
self.error('Generator %s does not exist' % opath)
1031-
ok = False
1032-
else:
1033-
try:
1034-
if os.path.isdir(fpath):
1035-
shutil.copytree(fpath, dest)
1036-
else:
1037-
shutil.copy2(fpath, dest)
1038-
except Exception as e:
1039-
self.error(str(e))
1040-
ok = False
1041-
if ok:
1042-
if manual:
1043-
self._generators[gen] = dest
1044-
else:
1045-
prog = run.get_program(tmpdir if implicit else dest,
1046-
language_config=self._problem.language_config,
1047-
work_dir=self._problem.tmpdir)
1048-
if prog is None:
1049-
self.error('Could not load generator %s' % gen)
1050-
ok = False
1051-
else:
1052-
self._generators[gen] = prog
1053-
success, msg = prog.compile()
1054-
if not success:
1055-
self.error('Compile error for generator %s' % gen, msg)
1056-
ok = False
1057-
if not ok and gen in self._generators:
1058-
del self._generators[gen]
1059-
1060-
def check(self, context: Context) -> bool:
1061-
if self._check_res is not None:
1062-
return self._check_res
1063-
self._check_res = True
1064-
1065-
if self._data is None:
1066-
return self._check_res
1067-
if not isinstance(self._data, dict):
1068-
self.error('generators.yaml must specify a dict')
1069-
return self._check_res
1070-
1071-
self._generators = self._data.get('generators') or {}
1072-
if not isinstance(self._generators, dict):
1073-
self.error('Generators key in generators.yaml must specify a dict')
1074-
self._generators = {}
1075-
1076-
# Check the shape of the top-level data dict
1077-
if isinstance(self._data.get('data'), list):
1078-
self.error('Top-level data key in generators.yaml must specify a dict')
1079-
self._data['data'] = {}
1080-
1081-
if isinstance(self._data.get('data'), dict):
1082-
invalid = []
1083-
for key, value in self._data['data'].items():
1084-
valid = False
1085-
if key not in Generators._DATA_DIRECTORIES:
1086-
self.warning("Invalid key '%s' in generators.yaml, expected one of %s" % (key, Generators._DATA_DIRECTORIES))
1087-
elif not isinstance(value, dict):
1088-
self.warning("Key '%s' in generators.yaml must specify a dict" % key)
1089-
elif value.get('type') != 'directory':
1090-
self.warning("Type of %s in generators.yaml must be 'directory'" % key)
1091-
else:
1092-
valid = True
1093-
if not valid:
1094-
invalid.append(key)
1095-
for key in invalid:
1096-
del self._data['data'][key]
1097-
1098-
# Run a depth-first search through generators.yaml and generate a
1099-
# flattened list of testcases
1100-
default_state: dict[str, str|bool|None] = { key: None for key in Generators._TESTCASE_OPTIONS }
1101-
default_state.update({
1102-
'path': 'data',
1103-
'manual': False,
1104-
'random_salt': '',
1105-
})
1106-
1107-
self._parse_element(self._data, default_state)
1108-
1109-
if context.compile_generators:
1110-
self._compile_generators()
1111-
1112-
return self._check_res
1113-
1114-
1115826
class ProblemStatement(ProblemAspect):
1116827
def __init__(self, problem: Problem):
1117828
super().__init__(f"{problem.shortname}.statement")
@@ -1949,7 +1660,7 @@ def check(self, context: Context) -> bool:
19491660

19501661
return self._check_res
19511662

1952-
PROBLEM_PARTS = ['config', 'statement', 'validators', 'graders', 'generators', 'data', 'submissions']
1663+
PROBLEM_PARTS = ['config', 'statement', 'validators', 'graders', 'data', 'submissions']
19531664

19541665
class Problem(ProblemAspect):
19551666
def __init__(self, probdir: str):
@@ -1968,14 +1679,7 @@ def __enter__(self) -> Problem:
19681679
self.statement = ProblemStatement(self)
19691680
self.attachments = Attachments(self)
19701681
self.config = ProblemConfig(self)
1971-
available_languages = self.config.get('languages')
1972-
if 'all' not in available_languages:
1973-
language_config = languages.Languages()
1974-
for lang_id in available_languages:
1975-
lang_spec = self.language_config.get(lang_id)
1976-
if lang_spec is not None:
1977-
language_config.update({lang_id: self.language_config.get(lang_id)})
1978-
self.language_config = language_config
1682+
self.available_languages = languages.load_language_config()
19791683

19801684
self.is_interactive = 'interactive' in self.config.get('validation-params')
19811685
self.is_scoring = (self.config.get('type') == 'scoring')
@@ -1985,7 +1689,6 @@ def __enter__(self) -> Problem:
19851689
self.testcase_by_infile: dict[str, TestCase] = {}
19861690
self.testdata = TestCaseGroup(self, os.path.join(self.probdir, 'data'))
19871691
self.submissions = Submissions(self)
1988-
self.generators = Generators(self)
19891692
return self
19901693

19911694
def __exit__(self, exc_type, exc_value, exc_traceback) -> None:
@@ -2012,7 +1715,6 @@ def check(self, args: argparse.Namespace) -> tuple[int, int]:
20121715
'statement': [self.statement, self.attachments],
20131716
'validators': [self.input_validators, self.output_validators],
20141717
'graders': [self.graders],
2015-
'generators': [self.generators],
20161718
'data': [self.testdata],
20171719
'submissions': [self.submissions],
20181720
}

0 commit comments

Comments
 (0)