-
Ashwin Maran authoredAshwin Maran authored
Code owners
Assign users and groups as approvers for specific file changes. Learn more.
public_tests.py 100.54 KiB
#!/usr/bin/python
# +
import os, json, math, copy
from collections import namedtuple
from bs4 import BeautifulSoup
HIDDEN_FILE = os.path.join("hidden", "hidden_tests.py")
if os.path.exists(HIDDEN_FILE):
import hidden.hidden_tests as hidn
# -
MAX_FILE_SIZE = 750 # units - KB
REL_TOL = 6e-04 # relative tolerance for floats
ABS_TOL = 15e-03 # absolute tolerance for floats
TOTAL_SCORE = 100 # total score for the project
DF_FILE = 'expected_dfs.html'
PLOT_FILE = 'expected_plots.json'
PASS = "All test cases passed!"
TEXT_FORMAT = "TEXT_FORMAT" # question type when expected answer is a type, str, int, float, or bool
TEXT_FORMAT_UNORDERED_LIST = "TEXT_FORMAT_UNORDERED_LIST" # question type when the expected answer is a list or a set where the order does *not* matter
TEXT_FORMAT_ORDERED_LIST = "TEXT_FORMAT_ORDERED_LIST" # question type when the expected answer is a list or tuple where the order does matter
TEXT_FORMAT_DICT = "TEXT_FORMAT_DICT" # question type when the expected answer is a dictionary
TEXT_FORMAT_SPECIAL_ORDERED_LIST = "TEXT_FORMAT_SPECIAL_ORDERED_LIST" # question type when the expected answer is a list where order does matter, but with possible ties. Elements are ordered according to values in special_ordered_json (with ties allowed)
TEXT_FORMAT_NAMEDTUPLE = "TEXT_FORMAT_NAMEDTUPLE" # question type when expected answer is a namedtuple
PNG_FORMAT_SCATTER = "PNG_FORMAT_SCATTER" # question type when the expected answer is a scatter plot
HTML_FORMAT = "HTML_FORMAT" # question type when the expected answer is a DataFrame
FILE_JSON_FORMAT = "FILE_JSON_FORMAT" # question type when the expected answer is a JSON file
SLASHES = " SLASHES" # question SUFFIX when expected answer contains paths with slashes
def get_expected_format():
"""get_expected_format() returns a dict mapping each question to the format
of the expected answer."""
expected_format = {'q1': 'TEXT_FORMAT',
'q2': 'TEXT_FORMAT',
'q3': 'TEXT_FORMAT',
'q4': 'TEXT_FORMAT',
'q5': 'TEXT_FORMAT',
'q6': 'TEXT_FORMAT',
'q7': 'TEXT_FORMAT',
'q8': 'TEXT_FORMAT',
'q9': 'TEXT_FORMAT',
'q10': 'TEXT_FORMAT',
'q11': 'TEXT_FORMAT',
'q12': 'TEXT_FORMAT',
'q13': 'TEXT_FORMAT',
'q14': 'TEXT_FORMAT',
'q15': 'TEXT_FORMAT',
'q16': 'TEXT_FORMAT',
'q17': 'TEXT_FORMAT_DICT',
'q18': 'TEXT_FORMAT_DICT',
'q19': 'TEXT_FORMAT',
'q20': 'TEXT_FORMAT',
'q21': 'TEXT_FORMAT_DICT',
'q22': 'TEXT_FORMAT',
'q23': 'TEXT_FORMAT',
'q24': 'TEXT_FORMAT_DICT',
'q25': 'TEXT_FORMAT_DICT',
'q26': 'TEXT_FORMAT',
'q27': 'TEXT_FORMAT',
'q28': 'TEXT_FORMAT_DICT',
'q29': 'TEXT_FORMAT_DICT',
'q30': 'TEXT_FORMAT_DICT',
'q31': 'TEXT_FORMAT_DICT',
'q32': 'TEXT_FORMAT_DICT',
'q33': 'TEXT_FORMAT_DICT',
'q34': 'TEXT_FORMAT_DICT',
'q35': 'TEXT_FORMAT_DICT',
'q36': 'TEXT_FORMAT_DICT',
'q37': 'TEXT_FORMAT_DICT',
'q38': 'TEXT_FORMAT',
'q39': 'TEXT_FORMAT',
'q40': 'TEXT_FORMAT'}
return expected_format
def get_expected_json():
"""get_expected_json() returns a dict mapping each question to the expected
answer (if the format permits it)."""
expected_json = {'q1': 'E. Haaland',
'q2': 'Paris Saint Germain',
'q3': 32,
'q4': '188cm',
'q5': 188,
'q6': '€250K',
'q7': '€158.5M',
'q8': 250000,
'q9': 158500000,
'q10': 0,
'q11': 500,
'q12': 44,
'q13': 63.0,
'q14': 185,
'q15': 'Right',
'q16': 'Arsenal',
'q17': {'ID': 239085,
'Name': 'E. Haaland',
'Age': 22,
'Nationality': 'Norway',
'Team': 'Manchester City',
'League': 'Premier League (England)',
'Value': 185000000,
'Wage': 340000,
'Attacking': 78.6,
'Movement': 83.6,
'Defending': 38.0,
'Goalkeeping': 10.4,
'Overall rating': 91,
'Position': 'ST',
'Height': 195,
'Preferred foot': 'Left'},
'q18': {'ID': 231747,
'Name': 'K. Mbappé',
'Age': 24,
'Nationality': 'France',
'Team': 'Paris Saint Germain',
'League': 'Ligue 1 (France)',
'Value': 181500000,
'Wage': 230000,
'Attacking': 83.0,
'Movement': 92.4,
'Defending': 30.7,
'Goalkeeping': 8.4,
'Overall rating': 91,
'Position': 'ST',
'Height': 182,
'Preferred foot': 'Right'},
'q19': 'ST',
'q20': 182,
'q21': {239085: 83.6,
231747: 92.4,
192985: 77.6,
202126: 74.0,
192119: 58.0,
188545: 80.8,
165153: 79.6,
158023: 87.0,
239818: 65.6,
238794: 90.8,
231866: 66.8,
209331: 90.2,
203376: 70.0,
200145: 67.4,
190871: 87.4,
212622: 79.6,
212198: 78.0,
230621: 58.2,
228702: 83.4,
222665: 79.2,
200104: 83.4,
193080: 56.4,
177003: 82.8,
256790: 88.2,
253163: 70.2,
252371: 79.8,
251854: 84.4,
246669: 85.2,
235243: 66.4,
232411: 85.2,
231443: 87.6,
215914: 79.4,
211110: 83.4,
208722: 86.4,
204485: 84.4,
199556: 76.6,
186942: 73.0,
182521: 65.0,
20801: 75.4,
237692: 85.2,
268965: 55.8,
268963: 54.0,
268280: 38.4,
262760: 49.8,
258802: 58.2,
269032: 60.4,
269038: 61.0,
268279: 36.6,
213799: 45.0,
183162: 34.0},
'q22': 65.6,
'q23': 90.2,
'q24': {239085: {'ID': 239085,
'Name': 'E. Haaland',
'Age': 22,
'Nationality': 'Norway',
'Team': 'Manchester City',
'League': 'Premier League (England)',
'Value': 185000000,
'Wage': 340000,
'Attacking': 78.6,
'Movement': 83.6,
'Defending': 38.0,
'Goalkeeping': 10.4,
'Overall rating': 91,
'Position': 'ST',
'Height': 195,
'Preferred foot': 'Left'},
231747: {'ID': 231747,
'Name': 'K. Mbappé',
'Age': 24,
'Nationality': 'France',
'Team': 'Paris Saint Germain',
'League': 'Ligue 1 (France)',
'Value': 181500000,
'Wage': 230000,
'Attacking': 83.0,
'Movement': 92.4,
'Defending': 30.7,
'Goalkeeping': 8.4,
'Overall rating': 91,
'Position': 'ST',
'Height': 182,
'Preferred foot': 'Right'},
192985: {'ID': 192985,
'Name': 'K. De Bruyne',
'Age': 32,
'Nationality': 'Belgium',
'Team': 'Manchester City',
'League': 'Premier League (England)',
'Value': 103000000,
'Wage': 350000,
'Attacking': 82.4,
'Movement': 77.6,
'Defending': 63.0,
'Goalkeeping': 11.2,
'Overall rating': 91,
'Position': 'CM',
'Height': 181,
'Preferred foot': 'Right'},
202126: {'ID': 202126,
'Name': 'H. Kane',
'Age': 29,
'Nationality': 'England',
'Team': 'FC Bayern München',
'League': 'Bundesliga (Germany)',
'Value': 119500000,
'Wage': 170000,
'Attacking': 88.0,
'Movement': 74.0,
'Defending': 43.3,
'Goalkeeping': 10.8,
'Overall rating': 90,
'Position': 'ST',
'Height': 188,
'Preferred foot': 'Right'},
192119: {'ID': 192119,
'Name': 'T. Courtois',
'Age': 31,
'Nationality': 'Belgium',
'Team': 'Real Madrid',
'League': 'La Liga (Spain)',
'Value': 63000000,
'Wage': 250000,
'Attacking': 17.2,
'Movement': 58.0,
'Defending': 18.0,
'Goalkeeping': 86.6,
'Overall rating': 90,
'Position': 'GK',
'Height': 199,
'Preferred foot': 'Left'},
188545: {'ID': 188545,
'Name': 'R. Lewandowski',
'Age': 34,
'Nationality': 'Poland',
'Team': 'FC Barcelona',
'League': 'La Liga (Spain)',
'Value': 58000000,
'Wage': 340000,
'Attacking': 86.6,
'Movement': 80.8,
'Defending': 32.0,
'Goalkeeping': 10.2,
'Overall rating': 90,
'Position': 'ST',
'Height': 185,
'Preferred foot': 'Right'},
165153: {'ID': 165153,
'Name': 'K. Benzema',
'Age': 35,
'Nationality': 'France',
'Team': 'Al Ittihad',
'League': 'Pro League (Saudi Arabia)',
'Value': 51000000,
'Wage': 95000,
'Attacking': 86.6,
'Movement': 79.6,
'Defending': 28.3,
'Goalkeeping': 8.2,
'Overall rating': 90,
'Position': 'CF',
'Height': 185,
'Preferred foot': 'Right'},
158023: {'ID': 158023,
'Name': 'L. Messi',
'Age': 36,
'Nationality': 'Argentina',
'Team': 'Inter Miami',
'League': 'Major League Soccer (United States)',
'Value': 41000000,
'Wage': 23000,
'Attacking': 81.8,
'Movement': 87.0,
'Defending': 26.3,
'Goalkeeping': 10.8,
'Overall rating': 90,
'Position': 'CAM',
'Height': 169,
'Preferred foot': 'Left'},
239818: {'ID': 239818,
'Name': 'Rúben Dias',
'Age': 26,
'Nationality': 'Portugal',
'Team': 'Manchester City',
'League': 'Premier League (England)',
'Value': 106500000,
'Wage': 250000,
'Attacking': 57.0,
'Movement': 65.6,
'Defending': 89.7,
'Goalkeeping': 9.4,
'Overall rating': 89,
'Position': 'CB',
'Height': 187,
'Preferred foot': 'Right'},
238794: {'ID': 238794,
'Name': 'Vini Jr.',
'Age': 22,
'Nationality': 'Brazil',
'Team': 'Real Madrid',
'League': 'La Liga (Spain)',
'Value': 158500000,
'Wage': 310000,
'Attacking': 73.8,
'Movement': 90.8,
'Defending': 25.0,
'Goalkeeping': 7.2,
'Overall rating': 89,
'Position': 'LW',
'Height': 176,
'Preferred foot': 'Right'},
231866: {'ID': 231866,
'Name': 'Rodri',
'Age': 27,
'Nationality': 'Spain',
'Team': 'Manchester City',
'League': 'Premier League (England)',
'Value': 105500000,
'Wage': 250000,
'Attacking': 71.2,
'Movement': 66.8,
'Defending': 84.3,
'Goalkeeping': 9.8,
'Overall rating': 89,
'Position': 'CDM',
'Height': 191,
'Preferred foot': 'Right'},
209331: {'ID': 209331,
'Name': 'M. Salah',
'Age': 31,
'Nationality': 'Egypt',
'Team': 'Liverpool',
'League': 'Premier League (England)',
'Value': 85500000,
'Wage': 260000,
'Attacking': 79.8,
'Movement': 90.2,
'Defending': 40.7,
'Goalkeeping': 12.4,
'Overall rating': 89,
'Position': 'RW',
'Height': 175,
'Preferred foot': 'Left'},
203376: {'ID': 203376,
'Name': 'V. van Dijk',
'Age': 31,
'Nationality': 'Netherlands',
'Team': 'Liverpool',
'League': 'Premier League (England)',
'Value': 70500000,
'Wage': 220000,
'Attacking': 63.0,
'Movement': 70.0,
'Defending': 89.0,
'Goalkeeping': 11.6,
'Overall rating': 89,
'Position': 'CB',
'Height': 193,
'Preferred foot': 'Right'},
200145: {'ID': 200145,
'Name': 'Casemiro',
'Age': 31,
'Nationality': 'Brazil',
'Team': 'Manchester United',
'League': 'Premier League (England)',
'Value': 72000000,
'Wage': 240000,
'Attacking': 74.6,
'Movement': 67.4,
'Defending': 89.0,
'Goalkeeping': 13.4,
'Overall rating': 89,
'Position': 'CDM',
'Height': 185,
'Preferred foot': 'Right'},
190871: {'ID': 190871,
'Name': 'Neymar Jr',
'Age': 31,
'Nationality': 'Brazil',
'Team': 'Al Hilal',
'League': 'Pro League (Saudi Arabia)',
'Value': 85500000,
'Wage': 115000,
'Attacking': 80.0,
'Movement': 87.4,
'Defending': 32.0,
'Goalkeeping': 11.8,
'Overall rating': 89,
'Position': 'LW',
'Height': 175,
'Preferred foot': 'Right'},
212622: {'ID': 212622,
'Name': 'J. Kimmich',
'Age': 28,
'Nationality': 'Germany',
'Team': 'FC Bayern München',
'League': 'Bundesliga (Germany)',
'Value': 88000000,
'Wage': 130000,
'Attacking': 77.6,
'Movement': 79.6,
'Defending': 81.3,
'Goalkeeping': 12.0,
'Overall rating': 88,
'Position': 'CDM',
'Height': 177,
'Preferred foot': 'Right'},
212198: {'ID': 212198,
'Name': 'Bruno Fernandes',
'Age': 28,
'Nationality': 'Portugal',
'Team': 'Manchester United',
'League': 'Premier League (England)',
'Value': 92000000,
'Wage': 260000,
'Attacking': 82.6,
'Movement': 78.0,
'Defending': 69.3,
'Goalkeeping': 12.6,
'Overall rating': 88,
'Position': 'CAM',
'Height': 179,
'Preferred foot': 'Right'},
230621: {'ID': 230621,
'Name': 'G. Donnarumma',
'Age': 24,
'Nationality': 'Italy',
'Team': 'Paris Saint Germain',
'League': 'Ligue 1 (France)',
'Value': 85000000,
'Wage': 90000,
'Attacking': 16.0,
'Movement': 58.2,
'Defending': 16.7,
'Goalkeeping': 84.6,
'Overall rating': 87,
'Position': 'GK',
'Height': 196,
'Preferred foot': 'Right'},
228702: {'ID': 228702,
'Name': 'F. de Jong',
'Age': 26,
'Nationality': 'Netherlands',
'Team': 'FC Barcelona',
'League': 'La Liga (Spain)',
'Value': 103500000,
'Wage': 240000,
'Attacking': 76.6,
'Movement': 83.4,
'Defending': 76.3,
'Goalkeeping': 9.8,
'Overall rating': 87,
'Position': 'CM',
'Height': 181,
'Preferred foot': 'Right'},
222665: {'ID': 222665,
'Name': 'M. Ødegaard',
'Age': 24,
'Nationality': 'Norway',
'Team': 'Arsenal',
'League': 'Premier League (England)',
'Value': 109000000,
'Wage': 170000,
'Attacking': 78.2,
'Movement': 79.2,
'Defending': 58.7,
'Goalkeeping': 12.4,
'Overall rating': 87,
'Position': 'CAM',
'Height': 178,
'Preferred foot': 'Left'},
200104: {'ID': 200104,
'Name': 'H. Son',
'Age': 30,
'Nationality': 'Korea Republic',
'Team': 'Tottenham Hotspur',
'League': 'Premier League (England)',
'Value': 77000000,
'Wage': 170000,
'Attacking': 80.2,
'Movement': 83.4,
'Defending': 38.0,
'Goalkeeping': 10.6,
'Overall rating': 87,
'Position': 'LW',
'Height': 183,
'Preferred foot': 'Right'},
193080: {'ID': 193080,
'Name': 'De Gea',
'Age': 31,
'Nationality': 'Spain',
'Team': 'Manchester United',
'League': 'Premier League (England)',
'Value': 42000000,
'Wage': 150000,
'Attacking': 20.6,
'Movement': 56.4,
'Defending': 19.0,
'Goalkeeping': 82.2,
'Overall rating': 87,
'Position': 'GK',
'Height': 192,
'Preferred foot': 'Right'},
177003: {'ID': 177003,
'Name': 'L. Modrić',
'Age': 37,
'Nationality': 'Croatia',
'Team': 'Real Madrid',
'League': 'La Liga (Spain)',
'Value': 25000000,
'Wage': 190000,
'Attacking': 76.0,
'Movement': 82.8,
'Defending': 71.7,
'Goalkeeping': 10.4,
'Overall rating': 87,
'Position': 'CM',
'Height': 172,
'Preferred foot': 'Right'},
256790: {'ID': 256790,
'Name': 'J. Musiala',
'Age': 20,
'Nationality': 'Germany',
'Team': 'FC Bayern München',
'League': 'Bundesliga (Germany)',
'Value': 134500000,
'Wage': 79000,
'Attacking': 69.4,
'Movement': 88.2,
'Defending': 65.0,
'Goalkeeping': 8.4,
'Overall rating': 86,
'Position': 'CAM',
'Height': 184,
'Preferred foot': 'Right'},
253163: {'ID': 253163,
'Name': 'R. Araujo',
'Age': 24,
'Nationality': 'Uruguay',
'Team': 'FC Barcelona',
'League': 'La Liga (Spain)',
'Value': 93000000,
'Wage': 175000,
'Attacking': 62.6,
'Movement': 70.2,
'Defending': 86.0,
'Goalkeeping': 10.6,
'Overall rating': 86,
'Position': 'CB',
'Height': 188,
'Preferred foot': 'Right'},
252371: {'ID': 252371,
'Name': 'J. Bellingham',
'Age': 20,
'Nationality': 'England',
'Team': 'Real Madrid',
'League': 'La Liga (Spain)',
'Value': 100500000,
'Wage': 175000,
'Attacking': 74.8,
'Movement': 79.8,
'Defending': 77.7,
'Goalkeeping': 9.6,
'Overall rating': 86,
'Position': 'CM',
'Height': 186,
'Preferred foot': 'Right'},
251854: {'ID': 251854,
'Name': 'Pedri',
'Age': 20,
'Nationality': 'Spain',
'Team': 'FC Barcelona',
'League': 'La Liga (Spain)',
'Value': 105000000,
'Wage': 165000,
'Attacking': 66.8,
'Movement': 84.4,
'Defending': 70.0,
'Goalkeeping': 9.2,
'Overall rating': 86,
'Position': 'CM',
'Height': 174,
'Preferred foot': 'Right'},
246669: {'ID': 246669,
'Name': 'B. Saka',
'Age': 21,
'Nationality': 'England',
'Team': 'Arsenal',
'League': 'Premier League (England)',
'Value': 99000000,
'Wage': 150000,
'Attacking': 74.2,
'Movement': 85.2,
'Defending': 60.7,
'Goalkeeping': 10.0,
'Overall rating': 86,
'Position': 'RW',
'Height': 178,
'Preferred foot': 'Left'},
235243: {'ID': 235243,
'Name': 'M. de Ligt',
'Age': 23,
'Nationality': 'Netherlands',
'Team': 'FC Bayern München',
'League': 'Bundesliga (Germany)',
'Value': 83000000,
'Wage': 84000,
'Attacking': 62.2,
'Movement': 66.4,
'Defending': 86.7,
'Goalkeeping': 11.2,
'Overall rating': 86,
'Position': 'CB',
'Height': 189,
'Preferred foot': 'Right'},
232411: {'ID': 232411,
'Name': 'C. Nkunku',
'Age': 25,
'Nationality': 'France',
'Team': 'Chelsea',
'League': 'Premier League (England)',
'Value': 86500000,
'Wage': 185000,
'Attacking': 76.6,
'Movement': 85.2,
'Defending': 60.0,
'Goalkeeping': 8.6,
'Overall rating': 86,
'Position': 'CAM',
'Height': 175,
'Preferred foot': 'Right'},
231443: {'ID': 231443,
'Name': 'O. Dembélé',
'Age': 26,
'Nationality': 'France',
'Team': 'Paris Saint Germain',
'League': 'Ligue 1 (France)',
'Value': 80000000,
'Wage': 150000,
'Attacking': 72.0,
'Movement': 87.6,
'Defending': 35.0,
'Goalkeeping': 9.8,
'Overall rating': 86,
'Position': 'RW',
'Height': 178,
'Preferred foot': 'Left'},
215914: {'ID': 215914,
'Name': 'N. Kanté',
'Age': 32,
'Nationality': 'France',
'Team': 'Al Ittihad',
'League': 'Pro League (Saudi Arabia)',
'Value': 45000000,
'Wage': 73000,
'Attacking': 64.4,
'Movement': 79.4,
'Defending': 87.7,
'Goalkeeping': 10.8,
'Overall rating': 86,
'Position': 'CDM',
'Height': 168,
'Preferred foot': 'Right'},
211110: {'ID': 211110,
'Name': 'P. Dybala',
'Age': 29,
'Nationality': 'Argentina',
'Team': 'Roma',
'League': 'Serie A (Italy)',
'Value': 68000000,
'Wage': 130000,
'Attacking': 79.8,
'Movement': 83.4,
'Defending': 37.3,
'Goalkeeping': 5.2,
'Overall rating': 86,
'Position': 'CAM',
'Height': 177,
'Preferred foot': 'Left'},
208722: {'ID': 208722,
'Name': 'S. Mané',
'Age': 31,
'Nationality': 'Senegal',
'Team': 'Al Nassr',
'League': 'Pro League (Saudi Arabia)',
'Value': 56500000,
'Wage': 80000,
'Attacking': 80.0,
'Movement': 86.4,
'Defending': 40.7,
'Goalkeeping': 11.2,
'Overall rating': 86,
'Position': 'LM',
'Height': 174,
'Preferred foot': 'Right'},
204485: {'ID': 204485,
'Name': 'R. Mahrez',
'Age': 32,
'Nationality': 'Algeria',
'Team': 'Al Ahli Jeddah',
'League': 'Pro League (Saudi Arabia)',
'Value': 54000000,
'Wage': 72000,
'Attacking': 75.2,
'Movement': 84.4,
'Defending': 32.7,
'Goalkeeping': 10.8,
'Overall rating': 86,
'Position': 'RM',
'Height': 179,
'Preferred foot': 'Left'},
199556: {'ID': 199556,
'Name': 'M. Verratti',
'Age': 30,
'Nationality': 'Italy',
'Team': 'Paris Saint Germain',
'League': 'Ligue 1 (France)',
'Value': 65000000,
'Wage': 135000,
'Attacking': 70.6,
'Movement': 76.6,
'Defending': 81.3,
'Goalkeeping': 12.8,
'Overall rating': 86,
'Position': 'CM',
'Height': 165,
'Preferred foot': 'Right'},
186942: {'ID': 186942,
'Name': 'İ. Gündoğan',
'Age': 32,
'Nationality': 'Germany',
'Team': 'FC Barcelona',
'League': 'La Liga (Spain)',
'Value': 53500000,
'Wage': 220000,
'Attacking': 74.4,
'Movement': 73.0,
'Defending': 73.0,
'Goalkeeping': 9.6,
'Overall rating': 86,
'Position': 'CM',
'Height': 180,
'Preferred foot': 'Right'},
182521: {'ID': 182521,
'Name': 'T. Kroos',
'Age': 33,
'Nationality': 'Germany',
'Team': 'Real Madrid',
'League': 'La Liga (Spain)',
'Value': 42000000,
'Wage': 240000,
'Attacking': 79.2,
'Movement': 65.0,
'Defending': 67.0,
'Goalkeeping': 10.2,
'Overall rating': 86,
'Position': 'CM',
'Height': 183,
'Preferred foot': 'Right'},
20801: {'ID': 20801,
'Name': 'Cristiano Ronaldo',
'Age': 38,
'Nationality': 'Portugal',
'Team': 'Al Nassr',
'League': 'Pro League (Saudi Arabia)',
'Value': 23000000,
'Wage': 66000,
'Attacking': 81.8,
'Movement': 75.4,
'Defending': 26.7,
'Goalkeeping': 11.6,
'Overall rating': 86,
'Position': 'ST',
'Height': 187,
'Preferred foot': 'Right'},
237692: {'ID': 237692,
'Name': 'P. Foden',
'Age': 23,
'Nationality': 'England',
'Team': 'Manchester City',
'League': 'Premier League (England)',
'Value': 81500000,
'Wage': 180000,
'Attacking': 70.2,
'Movement': 85.2,
'Defending': 55.3,
'Goalkeeping': 10.4,
'Overall rating': 85,
'Position': 'CAM',
'Height': 171,
'Preferred foot': 'Left'},
268965: {'ID': 268965,
'Name': 'Zhang Yujun',
'Age': 19,
'Nationality': 'China PR',
'Team': 'Hebei',
'League': 'Super League (China PR)',
'Value': 100000,
'Wage': 1000,
'Attacking': 41.4,
'Movement': 55.8,
'Defending': 43.0,
'Goalkeeping': 10.8,
'Overall rating': 46,
'Position': 'CM',
'Height': 176,
'Preferred foot': 'Right'},
268963: {'ID': 268963,
'Name': 'Zhang Jiahui',
'Age': 19,
'Nationality': 'China PR',
'Team': 'Hebei',
'League': 'Super League (China PR)',
'Value': 100000,
'Wage': 900,
'Attacking': 41.2,
'Movement': 54.0,
'Defending': 43.7,
'Goalkeeping': 10.0,
'Overall rating': 46,
'Position': 'CM',
'Height': 182,
'Preferred foot': 'Right'},
268280: {'ID': 268280,
'Name': 'L. Hebbelmann',
'Age': 22,
'Nationality': 'Germany',
'Team': 'Meppen',
'League': '3. Liga (Germany)',
'Value': 60000,
'Wage': 500,
'Attacking': 35.6,
'Movement': 38.4,
'Defending': 44.3,
'Goalkeeping': 10.4,
'Overall rating': 46,
'Position': 'CDM',
'Height': 181,
'Preferred foot': 'Right'},
262760: {'ID': 262760,
'Name': 'N. Logue',
'Age': 22,
'Nationality': 'Republic of Ireland',
'Team': 'Finn Harps',
'League': 'Premier Division (Republic of Ireland)',
'Value': 100000,
'Wage': 500,
'Attacking': 40.0,
'Movement': 49.8,
'Defending': 43.3,
'Goalkeeping': 7.4,
'Overall rating': 46,
'Position': 'CAM',
'Height': 178,
'Preferred foot': 'Right'},
258802: {'ID': 258802,
'Name': 'B. Singh',
'Age': 22,
'Nationality': 'India',
'Team': 'Jamshedpur',
'League': 'Indian Super League (India)',
'Value': 110000,
'Wage': 500,
'Attacking': 41.4,
'Movement': 58.2,
'Defending': 22.7,
'Goalkeeping': 10.8,
'Overall rating': 46,
'Position': 'ST',
'Height': 172,
'Preferred foot': 'Right'},
269032: {'ID': 269032,
'Name': 'Chen Zeshi',
'Age': 16,
'Nationality': 'China PR',
'Team': 'Shandong Taishan',
'League': 'Super League (China PR)',
'Value': 100000,
'Wage': 500,
'Attacking': 41.0,
'Movement': 60.4,
'Defending': 39.7,
'Goalkeeping': 9.6,
'Overall rating': 45,
'Position': 'RM',
'Height': 180,
'Preferred foot': 'Right'},
269038: {'ID': 269038,
'Name': 'Zhang Wenxuan',
'Age': 16,
'Nationality': 'China PR',
'Team': 'Guangzhou',
'League': 'Super League (China PR)',
'Value': 110000,
'Wage': 500,
'Attacking': 37.2,
'Movement': 61.0,
'Defending': 42.7,
'Goalkeeping': 11.6,
'Overall rating': 44,
'Position': 'CB',
'Height': 175,
'Preferred foot': 'Right'},
268279: {'ID': 268279,
'Name': 'J. Looschen',
'Age': 24,
'Nationality': 'Germany',
'Team': 'Meppen',
'League': '3. Liga (Germany)',
'Value': 60000,
'Wage': 500,
'Attacking': 38.0,
'Movement': 36.6,
'Defending': 23.7,
'Goalkeeping': 9.8,
'Overall rating': 44,
'Position': 'CAM',
'Height': 178,
'Preferred foot': 'Right'},
213799: {'ID': 213799,
'Name': 'H. McFadden',
'Age': 19,
'Nationality': 'Republic of Ireland',
'Team': 'Sligo Rovers',
'League': 'Premier Division (Republic of Ireland)',
'Value': 20000,
'Wage': 500,
'Attacking': 29.4,
'Movement': 45.0,
'Defending': 46.3,
'Goalkeeping': 11.6,
'Overall rating': 44,
'Position': 'CB',
'Height': 182,
'Preferred foot': 'Right'},
183162: {'ID': 183162,
'Name': 'J. Yates',
'Age': 19,
'Nationality': 'England',
'Team': 'Rotherham United',
'League': 'Championship (England)',
'Value': 0,
'Wage': 0,
'Attacking': 26.2,
'Movement': 34.0,
'Defending': 14.0,
'Goalkeeping': 13.8,
'Overall rating': 40,
'Position': 'ST',
'Height': 170,
'Preferred foot': 'Right'}},
'q25': {'ID': 204485,
'Name': 'R. Mahrez',
'Age': 32,
'Nationality': 'Algeria',
'Team': 'Al Ahli Jeddah',
'League': 'Pro League (Saudi Arabia)',
'Value': 54000000,
'Wage': 72000,
'Attacking': 75.2,
'Movement': 84.4,
'Defending': 32.7,
'Goalkeeping': 10.8,
'Overall rating': 86,
'Position': 'RM',
'Height': 179,
'Preferred foot': 'Left'},
'q26': 'M. de Ligt',
'q27': 86,
'q28': {239085: 78.6,
231747: 83.0,
192985: 82.4,
202126: 88.0,
192119: 17.2,
188545: 86.6,
165153: 86.6,
158023: 81.8,
239818: 57.0,
238794: 73.8,
231866: 71.2,
209331: 79.8,
203376: 63.0,
200145: 74.6,
190871: 80.0,
212622: 77.6,
212198: 82.6,
230621: 16.0,
228702: 76.6,
222665: 78.2,
200104: 80.2,
193080: 20.6,
177003: 76.0,
256790: 69.4,
253163: 62.6,
252371: 74.8,
251854: 66.8,
246669: 74.2,
235243: 62.2,
232411: 76.6,
231443: 72.0,
215914: 64.4,
211110: 79.8,
208722: 80.0,
204485: 75.2,
199556: 70.6,
186942: 74.4,
182521: 79.2,
20801: 81.8,
237692: 70.2,
268965: 41.4,
268963: 41.2,
268280: 35.6,
262760: 40.0,
258802: 41.4,
269032: 41.0,
269038: 37.2,
268279: 38.0,
213799: 29.4,
183162: 26.2},
'q29': {'E. Haaland': 239085,
'K. Mbappé': 231747,
'K. De Bruyne': 192985,
'H. Kane': 202126,
'T. Courtois': 192119,
'R. Lewandowski': 188545,
'K. Benzema': 165153,
'L. Messi': 158023,
'Rúben Dias': 239818,
'Vini Jr.': 238794,
'Rodri': 231866,
'M. Salah': 209331,
'V. van Dijk': 203376,
'Casemiro': 200145,
'Neymar Jr': 190871,
'J. Kimmich': 212622,
'Bruno Fernandes': 212198,
'G. Donnarumma': 230621,
'F. de Jong': 228702,
'M. Ødegaard': 222665,
'H. Son': 200104,
'De Gea': 193080,
'L. Modrić': 177003,
'J. Musiala': 256790,
'R. Araujo': 253163,
'J. Bellingham': 252371,
'Pedri': 251854,
'B. Saka': 246669,
'M. de Ligt': 235243,
'C. Nkunku': 232411,
'O. Dembélé': 231443,
'N. Kanté': 215914,
'P. Dybala': 211110,
'S. Mané': 208722,
'R. Mahrez': 204485,
'M. Verratti': 199556,
'İ. Gündoğan': 186942,
'T. Kroos': 182521,
'Cristiano Ronaldo': 20801,
'P. Foden': 237692,
'Zhang Yujun': 268965,
'Zhang Jiahui': 268963,
'L. Hebbelmann': 268280,
'N. Logue': 262760,
'B. Singh': 258802,
'Chen Zeshi': 269032,
'Zhang Wenxuan': 269038,
'J. Looschen': 268279,
'H. McFadden': 213799,
'J. Yates': 183162},
'q30': {'E. Haaland': 'Norway',
'K. Mbappé': 'France',
'K. De Bruyne': 'Belgium',
'H. Kane': 'England',
'T. Courtois': 'Belgium',
'R. Lewandowski': 'Poland',
'K. Benzema': 'France',
'L. Messi': 'Argentina',
'Rúben Dias': 'Portugal',
'Vini Jr.': 'Brazil',
'Rodri': 'Spain',
'M. Salah': 'Egypt',
'V. van Dijk': 'Netherlands',
'Casemiro': 'Brazil',
'Neymar Jr': 'Brazil',
'J. Kimmich': 'Germany',
'Bruno Fernandes': 'Portugal',
'G. Donnarumma': 'Italy',
'F. de Jong': 'Netherlands',
'M. Ødegaard': 'Norway',
'H. Son': 'Korea Republic',
'De Gea': 'Spain',
'L. Modrić': 'Croatia',
'J. Musiala': 'Germany',
'R. Araujo': 'Uruguay',
'J. Bellingham': 'England',
'Pedri': 'Spain',
'B. Saka': 'England',
'M. de Ligt': 'Netherlands',
'C. Nkunku': 'France',
'O. Dembélé': 'France',
'N. Kanté': 'France',
'P. Dybala': 'Argentina',
'S. Mané': 'Senegal',
'R. Mahrez': 'Algeria',
'M. Verratti': 'Italy',
'İ. Gündoğan': 'Germany',
'T. Kroos': 'Germany',
'Cristiano Ronaldo': 'Portugal',
'P. Foden': 'England',
'Zhang Yujun': 'China PR',
'Zhang Jiahui': 'China PR',
'L. Hebbelmann': 'Germany',
'N. Logue': 'Republic of Ireland',
'B. Singh': 'India',
'Chen Zeshi': 'China PR',
'Zhang Wenxuan': 'China PR',
'J. Looschen': 'Germany',
'H. McFadden': 'Republic of Ireland',
'J. Yates': 'England'},
'q31': {'Left': 10, 'Right': 40},
'q32': {'Norway': 2,
'France': 5,
'Belgium': 2,
'England': 5,
'Poland': 1,
'Argentina': 2,
'Portugal': 3,
'Brazil': 3,
'Spain': 3,
'Egypt': 1,
'Netherlands': 3,
'Germany': 6,
'Italy': 2,
'Korea Republic': 1,
'Croatia': 1,
'Uruguay': 1,
'Senegal': 1,
'Algeria': 1,
'China PR': 4,
'Republic of Ireland': 2,
'India': 1},
'q33': {'Norway': 156.8,
'France': 382.6,
'Belgium': 99.60000000000001,
'England': 333.4,
'Poland': 86.6,
'Argentina': 161.6,
'Portugal': 221.39999999999998,
'Brazil': 228.39999999999998,
'Spain': 158.60000000000002,
'Egypt': 79.8,
'Netherlands': 201.8,
'Germany': 374.20000000000005,
'Italy': 86.6,
'Korea Republic': 80.2,
'Croatia': 76.0,
'Uruguay': 62.6,
'Senegal': 80.0,
'Algeria': 75.2,
'China PR': 160.8,
'Republic of Ireland': 69.4,
'India': 41.4},
'q34': {'E. Haaland': 83.6,
'K. Mbappé': 92.4,
'K. De Bruyne': 77.6,
'H. Kane': 74.0,
'T. Courtois': 58.0,
'R. Lewandowski': 80.8,
'K. Benzema': 79.6,
'L. Messi': 87.0,
'Rúben Dias': 65.6,
'Vini Jr.': 90.8,
'Rodri': 66.8,
'M. Salah': 90.2,
'V. van Dijk': 70.0,
'Casemiro': 67.4,
'Neymar Jr': 87.4,
'J. Kimmich': 79.6,
'Bruno Fernandes': 78.0,
'G. Donnarumma': 58.2,
'F. de Jong': 83.4,
'M. Ødegaard': 79.2,
'H. Son': 83.4,
'De Gea': 56.4,
'L. Modrić': 82.8,
'J. Musiala': 88.2,
'R. Araujo': 70.2,
'J. Bellingham': 79.8,
'Pedri': 84.4,
'B. Saka': 85.2,
'M. de Ligt': 66.4,
'C. Nkunku': 85.2,
'O. Dembélé': 87.6,
'N. Kanté': 79.4,
'P. Dybala': 83.4,
'S. Mané': 86.4,
'R. Mahrez': 84.4,
'M. Verratti': 76.6,
'İ. Gündoğan': 73.0,
'T. Kroos': 65.0,
'Cristiano Ronaldo': 75.4,
'P. Foden': 85.2,
'Zhang Yujun': 55.8,
'Zhang Jiahui': 54.0,
'L. Hebbelmann': 38.4,
'N. Logue': 49.8,
'B. Singh': 58.2,
'Chen Zeshi': 60.4,
'Zhang Wenxuan': 61.0,
'J. Looschen': 36.6,
'H. McFadden': 45.0,
'J. Yates': 34.0},
'q35': {'E. Haaland': 78.6,
'K. Mbappé': 83.0,
'K. De Bruyne': 82.4,
'H. Kane': 88.0,
'T. Courtois': 17.2,
'R. Lewandowski': 86.6,
'K. Benzema': 86.6,
'L. Messi': 81.8,
'Rúben Dias': 57.0,
'Vini Jr.': 73.8,
'Rodri': 71.2,
'M. Salah': 79.8,
'V. van Dijk': 63.0,
'Casemiro': 74.6,
'Neymar Jr': 80.0,
'J. Kimmich': 77.6,
'Bruno Fernandes': 82.6,
'G. Donnarumma': 16.0,
'F. de Jong': 76.6,
'M. Ødegaard': 78.2,
'H. Son': 80.2,
'De Gea': 20.6,
'L. Modrić': 76.0,
'J. Musiala': 69.4,
'R. Araujo': 62.6,
'J. Bellingham': 74.8,
'Pedri': 66.8,
'B. Saka': 74.2,
'M. de Ligt': 62.2,
'C. Nkunku': 76.6,
'O. Dembélé': 72.0,
'N. Kanté': 64.4,
'P. Dybala': 79.8,
'S. Mané': 80.0,
'R. Mahrez': 75.2,
'M. Verratti': 70.6,
'İ. Gündoğan': 74.4,
'T. Kroos': 79.2,
'Cristiano Ronaldo': 81.8,
'P. Foden': 70.2,
'Zhang Yujun': 41.4,
'Zhang Jiahui': 41.2,
'L. Hebbelmann': 35.6,
'N. Logue': 40.0,
'B. Singh': 41.4,
'Chen Zeshi': 41.0,
'Zhang Wenxuan': 37.2,
'J. Looschen': 38.0,
'H. McFadden': 29.4,
'J. Yates': 26.2},
'q36': {'E. Haaland': 162.2,
'K. Mbappé': 175.4,
'K. De Bruyne': 160.0,
'H. Kane': 162.0,
'T. Courtois': 75.2,
'R. Lewandowski': 167.39999999999998,
'K. Benzema': 166.2,
'L. Messi': 168.8,
'Rúben Dias': 122.6,
'Vini Jr.': 164.6,
'Rodri': 138.0,
'M. Salah': 170.0,
'V. van Dijk': 133.0,
'Casemiro': 142.0,
'Neymar Jr': 167.4,
'J. Kimmich': 157.2,
'Bruno Fernandes': 160.6,
'G. Donnarumma': 74.2,
'F. de Jong': 160.0,
'M. Ødegaard': 157.4,
'H. Son': 163.60000000000002,
'De Gea': 77.0,
'L. Modrić': 158.8,
'J. Musiala': 157.60000000000002,
'R. Araujo': 132.8,
'J. Bellingham': 154.6,
'Pedri': 151.2,
'B. Saka': 159.4,
'M. de Ligt': 128.60000000000002,
'C. Nkunku': 161.8,
'O. Dembélé': 159.6,
'N. Kanté': 143.8,
'P. Dybala': 163.2,
'S. Mané': 166.4,
'R. Mahrez': 159.60000000000002,
'M. Verratti': 147.2,
'İ. Gündoğan': 147.4,
'T. Kroos': 144.2,
'Cristiano Ronaldo': 157.2,
'P. Foden': 155.4,
'Zhang Yujun': 97.19999999999999,
'Zhang Jiahui': 95.2,
'L. Hebbelmann': 74.0,
'N. Logue': 89.8,
'B. Singh': 99.6,
'Chen Zeshi': 101.4,
'Zhang Wenxuan': 98.2,
'J. Looschen': 74.6,
'H. McFadden': 74.4,
'J. Yates': 60.2},
'q37': {'Norway': 78.4,
'France': 76.52000000000001,
'Belgium': 49.800000000000004,
'England': 66.67999999999999,
'Poland': 86.6,
'Argentina': 80.8,
'Portugal': 73.8,
'Brazil': 76.13333333333333,
'Spain': 52.866666666666674,
'Egypt': 79.8,
'Netherlands': 67.26666666666667,
'Germany': 62.366666666666674,
'Italy': 43.3,
'Korea Republic': 80.2,
'Croatia': 76.0,
'Uruguay': 62.6,
'Senegal': 80.0,
'Algeria': 75.2,
'China PR': 40.2,
'Republic of Ireland': 34.7,
'India': 41.4},
'q38': 'Poland',
'q39': 'H. Kane',
'q40': 'J. Yates'}
return expected_json
def get_special_json():
"""get_special_json() returns a dict mapping each question to the expected
answer stored in a special format as a list of tuples. Each tuple contains
the element expected in the list, and its corresponding value. Any two
elements with the same value can appear in any order in the actual list,
but if two elements have different values, then they must appear in the
same order as in the expected list of tuples."""
special_json = {}
return special_json
def compare(expected, actual, q_format=TEXT_FORMAT):
"""compare(expected, actual) is used to compare when the format of
the expected answer is known for certain."""
try:
if q_format == TEXT_FORMAT:
return simple_compare(expected, actual)
elif q_format == TEXT_FORMAT_UNORDERED_LIST:
return list_compare_unordered(expected, actual)
elif q_format == TEXT_FORMAT_ORDERED_LIST:
return list_compare_ordered(expected, actual)
elif q_format == TEXT_FORMAT_DICT:
return dict_compare(expected, actual)
elif q_format == TEXT_FORMAT_SPECIAL_ORDERED_LIST:
return list_compare_special(expected, actual)
elif q_format == TEXT_FORMAT_NAMEDTUPLE:
return namedtuple_compare(expected, actual)
elif q_format == PNG_FORMAT_SCATTER:
return compare_flip_dicts(expected, actual)
elif q_format == HTML_FORMAT:
return compare_cell_html(expected, actual)
elif q_format == FILE_JSON_FORMAT:
return compare_json(expected, actual)
else:
if expected != actual:
return "expected %s but found %s " % (repr(expected), repr(actual))
except:
if expected != actual:
return "expected %s" % (repr(expected))
return PASS
def print_message(expected, actual, complete_msg=True):
"""print_message(expected, actual) displays a simple error message."""
msg = "expected %s" % (repr(expected))
if complete_msg:
msg = msg + " but found %s" % (repr(actual))
return msg
def simple_compare(expected, actual, complete_msg=True):
"""simple_compare(expected, actual) is used to compare when the expected answer
is a type/Nones/str/int/float/bool. When the expected answer is a float,
the actual answer is allowed to be within the tolerance limit. Otherwise,
the values must match exactly, or a very simple error message is displayed."""
msg = PASS
if 'numpy' in repr(type((actual))):
actual = actual.item()
if isinstance(expected, type):
if expected != actual:
if isinstance(actual, type):
msg = "expected %s but found %s" % (expected.__name__, actual.__name__)
else:
msg = "expected %s but found %s" % (expected.__name__, repr(actual))
elif not isinstance(actual, type(expected)) and not (isinstance(expected, (float, int)) and isinstance(actual, (float, int))):
msg = "expected to find type %s but found type %s" % (type(expected).__name__, type(actual).__name__)
elif isinstance(expected, float):
if not math.isclose(actual, expected, rel_tol=REL_TOL, abs_tol=ABS_TOL):
msg = print_message(expected, actual, complete_msg)
elif isinstance(expected, (list, tuple)) or is_namedtuple(expected):
new_msg = print_message(expected, actual, complete_msg)
if len(expected) != len(actual):
return new_msg
for i in range(len(expected)):
val = simple_compare(expected[i], actual[i])
if val != PASS:
return new_msg
elif isinstance(expected, dict):
new_msg = print_message(expected, actual, complete_msg)
if len(expected) != len(actual):
return new_msg
val = simple_compare(list(expected.keys()), list(actual.keys()))
if val != PASS:
return new_msg
for key in expected:
val = simple_compare(expected[key], actual[key])
if val != PASS:
return new_msg
else:
if expected != actual:
msg = print_message(expected, actual, complete_msg)
return msg
def intelligent_compare(expected, actual, obj=None):
"""intelligent_compare(expected, actual) is used to compare when the
data type of the expected answer is not known for certain, and default
assumptions need to be made."""
if obj == None:
obj = type(expected).__name__
if is_namedtuple(expected):
msg = namedtuple_compare(expected, actual)
elif isinstance(expected, (list, tuple)):
msg = list_compare_ordered(expected, actual, obj)
elif isinstance(expected, set):
msg = list_compare_unordered(expected, actual, obj)
elif isinstance(expected, (dict)):
msg = dict_compare(expected, actual)
else:
msg = simple_compare(expected, actual)
msg = msg.replace("CompDict", "dict").replace("CompSet", "set").replace("NewNone", "None")
return msg
def is_namedtuple(obj, init_check=True):
"""is_namedtuple(obj) returns True if `obj` is a namedtuple object
defined in the test file."""
bases = type(obj).__bases__
if len(bases) != 1 or bases[0] != tuple:
return False
fields = getattr(type(obj), '_fields', None)
if not isinstance(fields, tuple):
return False
if init_check and not type(obj).__name__ in [nt.__name__ for nt in _expected_namedtuples]:
return False
return True
def list_compare_ordered(expected, actual, obj=None):
"""list_compare_ordered(expected, actual) is used to compare when the
expected answer is a list/tuple, where the order of the elements matters."""
msg = PASS
if not isinstance(actual, type(expected)):
msg = "expected to find type %s but found type %s" % (type(expected).__name__, type(actual).__name__)
return msg
if obj == None:
obj = type(expected).__name__
for i in range(len(expected)):
if i >= len(actual):
msg = "at index %d of the %s, expected missing %s" % (i, obj, repr(expected[i]))
break
val = intelligent_compare(expected[i], actual[i], "sub" + obj)
if val != PASS:
msg = "at index %d of the %s, " % (i, obj) + val
break
if len(actual) > len(expected) and msg == PASS:
msg = "at index %d of the %s, found unexpected %s" % (len(expected), obj, repr(actual[len(expected)]))
if len(expected) != len(actual):
msg = msg + " (found %d entries in %s, but expected %d)" % (len(actual), obj, len(expected))
if len(expected) > 0:
try:
if msg != PASS and list_compare_unordered(expected, actual, obj) == PASS:
msg = msg + " (%s may not be ordered as required)" % (obj)
except:
pass
return msg
def list_compare_helper(larger, smaller):
"""list_compare_helper(larger, smaller) is a helper function which takes in
two lists of possibly unequal sizes and finds the item that is not present
in the smaller list, if there is such an element."""
msg = PASS
j = 0
for i in range(len(larger)):
if i == len(smaller):
msg = "expected %s" % (repr(larger[i]))
break
found = False
while not found:
if j == len(smaller):
val = simple_compare(larger[i], smaller[j - 1], complete_msg=False)
break
val = simple_compare(larger[i], smaller[j], complete_msg=False)
j += 1
if val == PASS:
found = True
break
if not found:
msg = val
break
return msg
class NewNone():
"""alternate class in place of None, which allows for comparison with
all other data types."""
def __str__(self):
return 'None'
def __repr__(self):
return 'None'
def __lt__(self, other):
return True
def __le__(self, other):
return True
def __gt__(self, other):
return False
def __ge__(self, other):
return other == None
def __eq__(self, other):
return other == None
def __ne__(self, other):
return other != None
class CompDict(dict):
"""subclass of dict, which allows for comparison with other dicts."""
def __init__(self, vals):
super(self.__class__, self).__init__(vals)
if type(vals) == CompDict:
self.val = vals.val
elif isinstance(vals, dict):
self.val = self.get_equiv(vals)
else:
raise TypeError("'%s' object cannot be type casted to CompDict class" % type(vals).__name__)
def get_equiv(self, vals):
val = []
for key in sorted(list(vals.keys())):
val.append((key, vals[key]))
return val
def __str__(self):
return str(dict(self.val))
def __repr__(self):
return repr(dict(self.val))
def __lt__(self, other):
return self.val < CompDict(other).val
def __le__(self, other):
return self.val <= CompDict(other).val
def __gt__(self, other):
return self.val > CompDict(other).val
def __ge__(self, other):
return self.val >= CompDict(other).val
def __eq__(self, other):
return self.val == CompDict(other).val
def __ne__(self, other):
return self.val != CompDict(other).val
class CompSet(set):
"""subclass of set, which allows for comparison with other sets."""
def __init__(self, vals):
super(self.__class__, self).__init__(vals)
if type(vals) == CompSet:
self.val = vals.val
elif isinstance(vals, set):
self.val = self.get_equiv(vals)
else:
raise TypeError("'%s' object cannot be type casted to CompSet class" % type(vals).__name__)
def get_equiv(self, vals):
return sorted(list(vals))
def __str__(self):
return str(set(self.val))
def __repr__(self):
return repr(set(self.val))
def __getitem__(self, index):
return self.val[index]
def __lt__(self, other):
return self.val < CompSet(other).val
def __le__(self, other):
return self.val <= CompSet(other).val
def __gt__(self, other):
return self.val > CompSet(other).val
def __ge__(self, other):
return self.val >= CompSet(other).val
def __eq__(self, other):
return self.val == CompSet(other).val
def __ne__(self, other):
return self.val != CompSet(other).val
def make_sortable(item):
"""make_sortable(item) replaces all Nones in `item` with an alternate
class that allows for comparison with str/int/float/bool/list/set/tuple/dict.
It also replaces all dicts (and sets) with a subclass that allows for
comparison with other dicts (and sets)."""
if item == None:
return NewNone()
elif isinstance(item, (type, str, int, float, bool)):
return item
elif isinstance(item, (list, set, tuple)):
new_item = []
for subitem in item:
new_item.append(make_sortable(subitem))
if is_namedtuple(item):
return type(item)(*new_item)
elif isinstance(item, set):
return CompSet(new_item)
else:
return type(item)(new_item)
elif isinstance(item, dict):
new_item = {}
for key in item:
new_item[key] = make_sortable(item[key])
return CompDict(new_item)
return item
def list_compare_unordered(expected, actual, obj=None):
"""list_compare_unordered(expected, actual) is used to compare when the
expected answer is a list/set where the order of the elements does not matter."""
msg = PASS
if not isinstance(actual, type(expected)):
msg = "expected to find type %s but found type %s" % (type(expected).__name__, type(actual).__name__)
return msg
if obj == None:
obj = type(expected).__name__
try:
sort_expected = sorted(make_sortable(expected))
sort_actual = sorted(make_sortable(actual))
except:
return "unexpected datatype found in %s; expected entries of type %s" % (obj, obj, type(expected[0]).__name__)
if len(actual) == 0 and len(expected) > 0:
msg = "in the %s, missing" % (obj) + sort_expected[0]
elif len(actual) > 0 and len(expected) > 0:
val = intelligent_compare(sort_expected[0], sort_actual[0])
if val.startswith("expected to find type"):
msg = "in the %s, " % (obj) + simple_compare(sort_expected[0], sort_actual[0])
else:
if len(expected) > len(actual):
msg = "in the %s, missing " % (obj) + list_compare_helper(sort_expected, sort_actual)
elif len(expected) < len(actual):
msg = "in the %s, found un" % (obj) + list_compare_helper(sort_actual, sort_expected)
if len(expected) != len(actual):
msg = msg + " (found %d entries in %s, but expected %d)" % (len(actual), obj, len(expected))
return msg
else:
val = list_compare_helper(sort_expected, sort_actual)
if val != PASS:
msg = "in the %s, missing " % (obj) + val + ", but found un" + list_compare_helper(sort_actual,
sort_expected)
return msg
def namedtuple_compare(expected, actual):
"""namedtuple_compare(expected, actual) is used to compare when the
expected answer is a namedtuple defined in the test file."""
msg = PASS
if is_namedtuple(actual, False):
msg = "expected namedtuple but found %s" % (type(actual).__name__)
return msg
if type(expected).__name__ != type(actual).__name__:
return "expected namedtuple %s but found namedtuple %s" % (type(expected).__name__, type(actual).__name__)
expected_fields = expected._fields
actual_fields = actual._fields
msg = list_compare_ordered(list(expected_fields), list(actual_fields), "namedtuple attributes")
if msg != PASS:
return msg
for field in expected_fields:
val = intelligent_compare(getattr(expected, field), getattr(actual, field))
if val != PASS:
msg = "at attribute %s of namedtuple %s, " % (field, type(expected).__name__) + val
return msg
return msg
def clean_slashes(item):
"""clean_slashes()"""
if isinstance(item, str):
return item.replace("\\", "/").replace("/", os.path.sep)
elif item == None or isinstance(item, (type, int, float, bool)):
return item
elif isinstance(item, (list, tuple, set)) or is_namedtuple(item):
new_item = []
for subitem in item:
new_item.append(clean_slashes(subitem))
if is_namedtuple(item):
return type(item)(*new_item)
else:
return type(item)(new_item)
elif isinstance(item, dict):
new_item = {}
for key in item:
new_item[clean_slashes(key)] = clean_slashes(item[key])
return item
def list_compare_special_initialize(special_expected):
"""list_compare_special_initialize(special_expected) takes in the special
ordering stored as a sorted list of items, and returns a list of lists
where the ordering among the inner lists does not matter."""
latest_val = None
clean_special = []
for row in special_expected:
if latest_val == None or row[1] != latest_val:
clean_special.append([])
latest_val = row[1]
clean_special[-1].append(row[0])
return clean_special
def list_compare_special(special_expected, actual):
"""list_compare_special(special_expected, actual) is used to compare when the
expected answer is a list with special ordering defined in `special_expected`."""
msg = PASS
expected_list = []
special_order = list_compare_special_initialize(special_expected)
for expected_item in special_order:
expected_list.extend(expected_item)
val = list_compare_unordered(expected_list, actual)
if val != PASS:
return val
i = 0
for expected_item in special_order:
j = len(expected_item)
actual_item = actual[i: i + j]
val = list_compare_unordered(expected_item, actual_item)
if val != PASS:
if j == 1:
msg = "at index %d " % (i) + val
else:
msg = "between indices %d and %d " % (i, i + j - 1) + val
msg = msg + " (list may not be ordered as required)"
break
i += j
return msg
def dict_compare(expected, actual, obj=None):
"""dict_compare(expected, actual) is used to compare when the expected answer
is a dict."""
msg = PASS
if not isinstance(actual, type(expected)):
msg = "expected to find type %s but found type %s" % (type(expected).__name__, type(actual).__name__)
return msg
if obj == None:
obj = type(expected).__name__
expected_keys = list(expected.keys())
actual_keys = list(actual.keys())
val = list_compare_unordered(expected_keys, actual_keys, obj)
if val != PASS:
msg = "bad keys in %s: " % (obj) + val
if msg == PASS:
for key in expected:
new_obj = None
if isinstance(expected[key], (list, tuple, set)):
new_obj = 'value'
elif isinstance(expected[key], dict):
new_obj = 'sub' + obj
val = intelligent_compare(expected[key], actual[key], new_obj)
if val != PASS:
msg = "incorrect value for key %s in %s: " % (repr(key), obj) + val
return msg
def is_flippable(item):
"""is_flippable(item) determines if the given dict of lists has lists of the
same length and is therefore flippable."""
item_lens = set(([str(len(item[key])) for key in item]))
if len(item_lens) == 1:
return PASS
else:
return "found lists of lengths %s" % (", ".join(list(item_lens)))
def flip_dict_of_lists(item):
"""flip_dict_of_lists(item) flips a dict of lists into a list of dicts if the
lists are of same length."""
new_item = []
length = len(list(item.values())[0])
for i in range(length):
new_dict = {}
for key in item:
new_dict[key] = item[key][i]
new_item.append(new_dict)
return new_item
def compare_flip_dicts(expected, actual, obj="lists"):
"""compare_flip_dicts(expected, actual) flips a dict of lists (or dicts) into
a list of dicts (or dict of dicts) and then compares the list ignoring order."""
msg = PASS
example_item = list(expected.values())[0]
if isinstance(example_item, (list, tuple)):
val = is_flippable(actual)
if val != PASS:
msg = "expected to find lists of length %d, but " % (len(example_item)) + val
return msg
msg = list_compare_unordered(flip_dict_of_lists(expected), flip_dict_of_lists(actual), "lists")
elif isinstance(example_item, dict):
expected_keys = list(example_item.keys())
for key in actual:
val = list_compare_unordered(expected_keys, list(actual[key].keys()), "dictionary %s" % key)
if val != PASS:
return val
for cat_key in expected_keys:
expected_category = {}
actual_category = {}
for key in expected:
expected_category[key] = expected[key][cat_key]
actual_category[key] = actual[key][cat_key]
val = list_compare_unordered(flip_dict_of_lists(expected), flip_dict_of_lists(actual), "category " + repr(cat_key))
if val != PASS:
return val
return msg
def get_expected_tables():
"""get_expected_tables() reads the html file with the expected DataFrames
and returns a dict mapping each question to a html table."""
if not os.path.exists(DF_FILE):
return None
expected_tables = {}
f = open(DF_FILE, encoding='utf-8')
soup = BeautifulSoup(f.read(), 'html.parser')
f.close()
tables = soup.find_all('table')
for table in tables:
expected_tables[table.get("data-question")] = table
return expected_tables
def parse_df_html_table(table):
"""parse_df_html_table(table) takes in a table as a html string and returns
a dict mapping each row and column index to the value at that position."""
rows = []
for tr in table.find_all('tr'):
rows.append([])
for cell in tr.find_all(['td', 'th']):
rows[-1].append(cell.get_text().strip("\n "))
cells = {}
for r in range(1, len(rows)):
for c in range(1, len(rows[0])):
rname = rows[r][0]
cname = rows[0][c]
cells[(rname,cname)] = rows[r][c]
return cells
def get_expected_namedtuples():
"""get_expected_namedtuples() defines the required namedtuple objects
globally. It also returns a tuple of the classes."""
expected_namedtuples = []
return tuple(expected_namedtuples)
_expected_namedtuples = get_expected_namedtuples()
def compare_cell_html(expected, actual):
"""compare_cell_html(expected, actual) is used to compare when the
expected answer is a DataFrame stored in the `expected_dfs` html file."""
expected_cells = parse_df_html_table(expected)
try:
actual_cells = parse_df_html_table(BeautifulSoup(actual, 'html.parser').find('table'))
except Exception as e:
return "expected to find type DataFrame but found type %s instead" % type(actual).__name__
expected_cols = list(set(["column %s" % (loc[1]) for loc in expected_cells]))
actual_cols = list(set(["column %s" % (loc[1]) for loc in actual_cells]))
msg = list_compare_unordered(expected_cols, actual_cols, "DataFrame")
if msg != PASS:
return msg
expected_rows = list(set(["row index %s" % (loc[0]) for loc in expected_cells]))
actual_rows = list(set(["row index %s" % (loc[0]) for loc in actual_cells]))
msg = list_compare_unordered(expected_rows, actual_rows, "DataFrame")
if msg != PASS:
return msg
for location, expected in expected_cells.items():
location_name = "column {} at index {}".format(location[1], location[0])
actual = actual_cells.get(location, None)
if actual == None:
return "in %s, expected to find %s" % (location_name, repr(expected))
try:
actual_ans = float(actual)
expected_ans = float(expected)
if math.isnan(actual_ans) and math.isnan(expected_ans):
continue
except Exception as e:
actual_ans, expected_ans = actual, expected
msg = simple_compare(expected_ans, actual_ans)
if msg != PASS:
return "in %s, " % location_name + msg
return PASS
def get_expected_plots():
"""get_expected_plots() reads the json file with the expected plot data
and returns a dict mapping each question to a dictionary with the plots data."""
if not os.path.exists(PLOT_FILE):
return None
f = open(PLOT_FILE, encoding='utf-8')
expected_plots = json.load(f)
f.close()
return expected_plots
def compare_file_json(expected, actual):
"""compare_file_json(expected, actual) is used to compare when the
expected answer is a JSON file."""
msg = PASS
if not os.path.isfile(expected):
return "file %s not found; make sure it is downloaded and stored in the correct directory" % (expected)
elif not os.path.isfile(actual):
return "file %s not found; make sure that you have created the file with the correct name" % (actual)
try:
e = open(expected, encoding='utf-8')
expected_data = json.load(e)
e.close()
except json.JSONDecodeError:
return "file %s is broken and cannot be parsed; please delete and redownload the file correctly" % (expected)
try:
a = open(actual, encoding='utf-8')
actual_data = json.load(a)
a.close()
except json.JSONDecodeError:
return "file %s is broken and cannot be parsed" % (actual)
if type(expected_data) == list:
msg = list_compare_ordered(expected_data, actual_data, 'file ' + actual)
elif type(expected_data) == dict:
msg = dict_compare(expected_data, actual_data)
return msg
_expected_json = get_expected_json()
_special_json = get_special_json()
_expected_plots = get_expected_plots()
_expected_tables = get_expected_tables()
_expected_format = get_expected_format()
def check(qnum, actual):
"""check(qnum, actual) is used to check if the answer in the notebook is
the correct answer, and provide useful feedback if the answer is incorrect."""
msg = PASS
error_msg = "<b style='color: red;'>ERROR:</b> "
q_format = _expected_format[qnum]
if q_format == TEXT_FORMAT_SPECIAL_ORDERED_LIST:
expected = _special_json[qnum]
elif q_format == PNG_FORMAT_SCATTER:
if _expected_plots == None:
msg = error_msg + "file %s not parsed; make sure it is downloaded and stored in the correct directory" % (PLOT_FILE)
else:
expected = _expected_plots[qnum]
elif q_format == HTML_FORMAT:
if _expected_tables == None:
msg = error_msg + "file %s not parsed; make sure it is downloaded and stored in the correct directory" % (DF_FILE)
else:
expected = _expected_tables[qnum]
else:
expected = _expected_json[qnum]
if SLASHES in q_format:
q_format = q_format.replace(SLASHES, "")
expected = clean_slashes(expected)
actual = clean_slashes(actual)
if msg != PASS:
print(msg)
else:
msg = compare(expected, actual, q_format)
if msg != PASS:
msg = error_msg + msg
print(msg)
def check_file_size(path):
"""check_file_size(path) throws an error if the file is too big to display
on Gradescope."""
size = os.path.getsize(path)
assert size < MAX_FILE_SIZE * 10**3, "Your file is too big to be displayed by Gradescope; please delete unnecessary output cells so your file size is < %s KB" % MAX_FILE_SIZE
def reset_hidden_tests():
"""reset_hidden_tests() resets all hidden tests on the Gradescope autograder where the hidden test file exists"""
if not os.path.exists(HIDDEN_FILE):
return
hidn.reset_hidden_tests()
def rubric_check(rubric_point, ignore_past_errors=True):
"""rubric_check(rubric_point) uses the hidden test file on the Gradescope autograder to grade the `rubric_point`"""
if not os.path.exists(HIDDEN_FILE):
print(PASS)
return
error_msg_1 = "ERROR: "
error_msg_2 = "TEST DETAILS: "
try:
msg = hidn.rubric_check(rubric_point, ignore_past_errors)
except:
msg = "hidden tests crashed before execution"
if msg != PASS:
hidn.make_deductions(rubric_point)
if msg == "public tests failed":
comment = "The public tests have failed, so you will not receive any points for this question."
comment += "\nPlease confirm that the public tests pass locally before submitting."
elif msg == "answer is hardcoded":
comment = "In the datasets for testing hardcoding, all numbers are replaced with random values."
comment += "\nIf the answer is the same as in the original dataset for all these datasets"
comment += "\ndespite this, that implies that the answer in the notebook is hardcoded."
comment += "\nYou will not receive any points for this question."
else:
comment = hidn.get_comment(rubric_point)
msg = error_msg_1 + msg
if comment != "":
msg = msg + "\n" + error_msg_2 + comment
print(msg)
def get_summary():
"""get_summary() returns the summary of the notebook using the hidden test file on the Gradescope autograder"""
if not os.path.exists(HIDDEN_FILE):
print("Total Score: %d/%d" % (TOTAL_SCORE, TOTAL_SCORE))
return
score = min(TOTAL_SCORE, hidn.get_score(TOTAL_SCORE))
display_msg = "Total Score: %d/%d" % (score, TOTAL_SCORE)
if score != TOTAL_SCORE:
display_msg += "\n" + hidn.get_deduction_string()
print(display_msg)
def get_score_digit(digit):
"""get_score_digit(digit) returns the `digit` of the score using the hidden test file on the Gradescope autograder"""
if not os.path.exists(HIDDEN_FILE):
score = TOTAL_SCORE
else:
score = hidn.get_score(TOTAL_SCORE)
digits = bin(score)[2:]
digits = "0"*(7 - len(digits)) + digits
return int(digits[6 - digit])