3232
3333import logging
3434import re
35+ from typing import TypedDict , NotRequired
3536from abc import ABCMeta , abstractmethod
3637
3738from cms import FEEDBACK_LEVEL_RESTRICTED
@@ -192,26 +193,38 @@ class ScoreTypeAlone(ScoreType):
192193 pass
193194
194195
196+ class ScoreTypeGroupParametersDict (TypedDict ):
197+ max_score : float
198+ testcases : int | str | list [str ]
199+ threshold : NotRequired [float ]
200+ always_show_testcases : NotRequired [bool ]
201+
202+
203+ # the format of parameters is impossible to type-hint correctly, it seems...
204+ # this hint is (mostly) correct for the methods this base class implements,
205+ # subclasses might need a longer tuple.
206+ ScoreTypeGroupParameters = tuple [float , int | str | list [str ]] | ScoreTypeGroupParametersDict
207+
208+
195209class ScoreTypeGroup (ScoreTypeAlone ):
196210 """Intermediate class to manage tasks whose testcases are
197211 subdivided in groups (or subtasks). The score type parameters must
198212 be in the form [[m, t, ...], [...], ...], where m is the maximum
199213 score for the given subtask and t is the parameter for specifying
200- testcases.
214+ testcases, or be a list of dicts matching ScoreTypeGroupParametersDict .
201215
202216 If t is int, it is interpreted as the number of testcases
203217 comprising the subtask (that are consumed from the first to the
204218 last, sorted by num). If t is unicode, it is interpreted as the regular
205- expression of the names of target testcases. All t must have the same type.
219+ expression of the names of target testcases. If t is a list of strings,
220+ it is interpreted as a list of testcase codenames. All t must have the
221+ same type.
206222
207223 A subclass must implement the method 'get_public_outcome' and
208224 'reduce'.
209225
210226 """
211- # the format of parameters is impossible to type-hint correctly, it seems...
212- # this hint is (mostly) correct for the methods this base class implements,
213- # subclasses might need a longer tuple.
214- parameters : list [tuple [float , int | str ]]
227+ parameters : list [ScoreTypeGroupParameters ]
215228
216229 # Mark strings for localization.
217230 N_ ("Subtask %(index)s" )
@@ -333,6 +346,24 @@ class ScoreTypeGroup(ScoreTypeAlone):
333346</div>
334347{% endfor %}"""
335348
349+ def get_max_score (self , group_parameter : ScoreTypeGroupParameters ) -> float :
350+ if isinstance (group_parameter , tuple ) or isinstance (group_parameter , list ):
351+ return group_parameter [0 ]
352+ else :
353+ return group_parameter ["max_score" ]
354+
355+ def get_testcases (self , group_parameter : ScoreTypeGroupParameters ) -> int | str | list [str ]:
356+ if isinstance (group_parameter , tuple ) or isinstance (group_parameter , list ):
357+ return group_parameter [1 ]
358+ else :
359+ return group_parameter ["testcases" ]
360+
361+ def get_always_show_testcases (self , group_parameter : ScoreTypeGroupParameters ) -> bool :
362+ if isinstance (group_parameter , tuple ) or isinstance (group_parameter , list ):
363+ return False
364+ else :
365+ return group_parameter .get ("always_show_testcases" , False )
366+
336367 def retrieve_target_testcases (self ) -> list [list [str ]]:
337368 """Return the list of the target testcases for each subtask.
338369
@@ -345,7 +376,7 @@ def retrieve_target_testcases(self) -> list[list[str]]:
345376
346377 """
347378
348- t_params = [p [ 1 ] for p in self .parameters ]
379+ t_params = [self . get_testcases ( p ) for p in self .parameters ]
349380
350381 if all (isinstance (t , int ) for t in t_params ):
351382
@@ -379,9 +410,12 @@ def retrieve_target_testcases(self) -> list[list[str]]:
379410
380411 return targets
381412
413+ elif all (isinstance (t , list ) for t in t_params ) and all (all (isinstance (t , str ) for t in s ) for s in t_params ):
414+ return t_params
415+
382416 raise ValueError (
383417 "In the score type parameters, the second value of each element "
384- "must have the same type (int or unicode )" )
418+ "must have the same type (int, unicode or list of strings )" )
385419
386420 def max_scores (self ):
387421 """See ScoreType.max_score."""
@@ -393,10 +427,10 @@ def max_scores(self):
393427
394428 for st_idx , parameter in enumerate (self .parameters ):
395429 target = targets [st_idx ]
396- score += parameter [ 0 ]
430+ score += self . get_max_score ( parameter )
397431 if all (self .public_testcases [tc_idx ] for tc_idx in target ):
398- public_score += parameter [ 0 ]
399- headers += ["Subtask %d (%g)" % (st_idx , parameter [ 0 ] )]
432+ public_score += self . get_max_score ( parameter )
433+ headers += ["Subtask %d (%g)" % (st_idx , self . get_max_score ( parameter ) )]
400434
401435 return score , public_score , headers
402436
@@ -461,10 +495,10 @@ def compute_score(self, submission_result):
461495 st_score_fraction = self .reduce (
462496 [float (evaluations [tc_idx ].outcome ) for tc_idx in target ],
463497 parameter )
464- st_score = st_score_fraction * parameter [ 0 ]
498+ st_score = st_score_fraction * self . get_max_score ( parameter )
465499 rounded_score = round (st_score , score_precision )
466500
467- if tc_first_lowest_idx is not None and st_score_fraction < 1.0 :
501+ if tc_first_lowest_idx is not None and st_score_fraction < 1.0 and not self . get_always_show_testcases ( parameter ) :
468502 for tc in testcases :
469503 if not self .public_testcases [tc ["idx" ]]:
470504 continue
@@ -482,7 +516,7 @@ def compute_score(self, submission_result):
482516 "score_fraction" : st_score_fraction ,
483517 # But we also want the properly rounded score for display.
484518 "score" : rounded_score ,
485- "max_score" : parameter [ 0 ] ,
519+ "max_score" : self . get_max_score ( parameter ) ,
486520 "testcases" : testcases })
487521 if all (self .public_testcases [tc_idx ] for tc_idx in target ):
488522 public_score += st_score
@@ -495,7 +529,7 @@ def compute_score(self, submission_result):
495529 return score , subtasks , public_score , public_subtasks , ranking_details
496530
497531 @abstractmethod
498- def get_public_outcome (self , outcome : float , parameter : list ) -> str :
532+ def get_public_outcome (self , outcome : float , parameter : ScoreTypeGroupParameters ) -> str :
499533 """Return a public outcome from an outcome.
500534
501535 The public outcome is shown to the user, and this method
@@ -512,7 +546,7 @@ def get_public_outcome(self, outcome: float, parameter: list) -> str:
512546 pass
513547
514548 @abstractmethod
515- def reduce (self , outcomes : list [float ], parameter : list ) -> float :
549+ def reduce (self , outcomes : list [float ], parameter : ScoreTypeGroupParameters ) -> float :
516550 """Return the score of a subtask given the outcomes.
517551
518552 outcomes: the outcomes of the submission in
0 commit comments