@@ -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-
1115826class 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
19541665class 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