#!/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])