├── .gitignore ├── LICENSE ├── README.md ├── hintgen ├── ChangeVector.py ├── State.py ├── SyntaxEdit.py ├── __init__.py ├── admin.py ├── analysis.py ├── apps.py ├── astTools.py ├── canonicalize │ ├── __init__.py │ └── transformations.py ├── display.py ├── generate_message │ └── __init__.py ├── getHint.py ├── getSyntaxHint.py ├── individualize │ └── __init__.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20170113_1447.py │ ├── 0003_auto_20170113_1452.py │ ├── 0004_auto_20170113_1625.py │ ├── 0005_auto_20170113_1629.py │ ├── 0006_auto_20170114_1328.py │ ├── 0007_auto_20170114_1837.py │ ├── 0008_auto_20170114_1837.py │ ├── 0009_auto_20170114_1839.py │ ├── 0010_auto_20170114_1848.py │ ├── 0011_auto_20170114_1849.py │ ├── 0012_auto_20170114_1850.py │ ├── 0013_auto_20170114_1854.py │ ├── 0014_auto_20170114_1919.py │ ├── 0015_auto_20170114_1928.py │ ├── 0016_auto_20170114_1929.py │ ├── 0017_auto_20170114_2016.py │ ├── 0018_test_test_extra.py │ ├── 0019_canonicalstate_orig_tree_source.py │ ├── 0020_auto_20170117_1246.py │ ├── 0021_auto_20170117_1257.py │ ├── 0022_auto_20170117_1717.py │ ├── 0023_auto_20170117_1924.py │ ├── 0024_auto_20170126_1442.py │ ├── 0025_auto_20170126_1453.py │ ├── 0026_auto_20170126_1455.py │ ├── 0027_auto_20170126_1457.py │ ├── 0028_auto_20170126_1458.py │ ├── 0029_auto_20170127_1722.py │ └── __init__.py ├── models.py ├── namesets.py ├── path_construction │ ├── __init__.py │ ├── diffAsts.py │ └── generateNextStates.py ├── paths.py ├── test │ ├── __init__.py │ └── testHarness.py ├── tests.py ├── tools.py ├── urls.py └── views.py ├── manage.py └── testsite ├── __init__.py ├── settings.py ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | local_settings.py 55 | 56 | # Flask stuff: 57 | instance/ 58 | .webassets-cache 59 | 60 | # Scrapy stuff: 61 | .scrapy 62 | 63 | # Sphinx documentation 64 | docs/_build/ 65 | 66 | # PyBuilder 67 | target/ 68 | 69 | # IPython Notebook 70 | .ipynb_checkpoints 71 | 72 | # pyenv 73 | .python-version 74 | 75 | # celery beat schedule file 76 | celerybeat-schedule 77 | 78 | # dotenv 79 | .env 80 | 81 | # virtualenv 82 | venv/ 83 | ENV/ 84 | 85 | # Spyder project settings 86 | .spyderproject 87 | 88 | # Rope project settings 89 | .ropeproject 90 | 91 | datadump.json 92 | 93 | db 94 | 95 | db.sqlite3 96 | 97 | hintgen/data/ 98 | hintgen/combined_data/ 99 | hintgen/log/ 100 | hintgen/test/tmp/ 101 | 102 | hintgen/parser/ 103 | hintgen/lexerSyntaxHint.py 104 | 105 | mydatabase 106 | 107 | 108 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Kelly Rivers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ITAP-django 2 | 3 | Usage instructions forthcoming! -------------------------------------------------------------------------------- /hintgen/State.py: -------------------------------------------------------------------------------- 1 | from .astTools import deepcopy 2 | 3 | # The State class holds all the relevent information for a solution state 4 | class State: 5 | id = None 6 | name = None 7 | score = None 8 | feedback = None 9 | 10 | fun = None 11 | loadedFun = None 12 | tree = None 13 | 14 | def __cmp__(this, other): 15 | if not isinstance(other, State): 16 | return -1 17 | c1 = cmp(this.fun, other.fun) 18 | c2 = cmp(this.name, other.name) 19 | c3 = cmp(this.id, other.id) 20 | return c1 if c1 != 0 else c2 if c2 != 0 else c3 21 | 22 | def deepcopy(this): 23 | s = State() 24 | s.id = this.id 25 | s.name = this.name 26 | s.score = this.score 27 | s.fun = this.fun 28 | s.tree = deepcopy(this.tree) 29 | 30 | properties = ["count", "goal", "goal_id", "goalDist", 31 | "next", "next_id", "edit", "hint", "treeWeight"] 32 | for prop in properties: 33 | if hasattr(this, prop): 34 | setattr(s, prop, getattr(this, prop)) 35 | return s 36 | 37 | class OriginalState(State): 38 | canonicalId = None 39 | 40 | def deepcopy(this): 41 | s = OriginalState() 42 | s.id = this.id 43 | s.canonicalId = this.canonicalId 44 | s.name = this.name 45 | s.score = this.score 46 | s.fun = this.fun 47 | s.tree = deepcopy(this.tree) 48 | 49 | properties = ["count", "goal", "goal_id", "goalDist", 50 | "next", "next_id", "edit", "hint", "treeWeight"] 51 | for prop in properties: 52 | if hasattr(this, prop): 53 | setattr(s, prop, getattr(this, prop)) 54 | return s 55 | 56 | class CanonicalState(State): 57 | count = 0 # how many students have submitted this state before? 58 | 59 | goal = None # the eventual goal state for this student 60 | goalDist = -1 61 | goal_id = None 62 | 63 | next = None # the next state in the solution space 64 | next_id = None 65 | edit = None # the changes on the edge to the next state 66 | 67 | def deepcopy(this): 68 | s = CanonicalState() 69 | s.id = this.id 70 | s.name = this.name 71 | s.score = this.score 72 | s.fun = this.fun 73 | s.tree = deepcopy(this.tree) 74 | 75 | s.count = this.count 76 | s.goal = this.goal 77 | s.goal_id = this.goal_id 78 | s.goalDist = this.goalDist 79 | s.next = this.next 80 | s.next_id = this.next_id 81 | s.edit = this.edit 82 | 83 | if hasattr(this, "hint"): 84 | s.hint = this.hint 85 | if hasattr(this, "treeWeight"): 86 | s.treeWeight = this.treeWeight 87 | return s -------------------------------------------------------------------------------- /hintgen/SyntaxEdit.py: -------------------------------------------------------------------------------- 1 | class SyntaxEdit: 2 | line = -1 3 | col = -1 4 | totalCol = -1 5 | editType = None 6 | text = "" 7 | newText = "" 8 | 9 | def __init__(self, line, col, totalCol, editType, text, newText=None): 10 | self.line = line 11 | self.col = col 12 | self.totalCol = totalCol 13 | self.editType = editType 14 | self.text = text 15 | if newText != None: 16 | self.newText = newText 17 | 18 | def __repr__(self): 19 | return self.editType + " Edit: " + repr(self.text) + ((" - " + repr(self.newText)) if self.newText != "" else "") + \ 20 | " : (" + str(self.line) + ", " + str(self.col) + ", " + str(self.totalCol) + ")" 21 | 22 | def __cmp__(self, other): 23 | if not isinstance(other, SyntaxEdit): 24 | return cmp("SyntaxEdit", type(other)) 25 | for field in ["editType", "line", "col", "totalCol", "text", "newText"]: 26 | if getattr(self, field) != getattr(other, field): 27 | return cmp(getattr(self, field), getattr(other, field)) 28 | return 0 29 | 30 | def textDiff(self): 31 | if self.editType in ["deindent", "-"]: 32 | return len(self.text) 33 | elif self.editType in ["indent", "+"]: 34 | return -len(self.text) 35 | else: 36 | return len(self.text) - len(self.newText) -------------------------------------------------------------------------------- /hintgen/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krivers/ITAP-django/dd6af07b02897f5b11305a1888a0b8df1f0f3d8e/hintgen/__init__.py -------------------------------------------------------------------------------- /hintgen/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | from .models import * 4 | 5 | # Inlines 6 | 7 | class StudentsInline(admin.TabularInline): 8 | model = Student 9 | 10 | class ProblemsInline(admin.TabularInline): 11 | model = Course.problems.through 12 | 13 | class TestcasesInline(admin.TabularInline): 14 | model = Testcase 15 | 16 | class SourceStateInline(admin.TabularInline): 17 | model = SourceState 18 | 19 | 20 | # Admins 21 | 22 | @admin.register(Course) 23 | class CourseAdmin(admin.ModelAdmin): 24 | list_display = ('year', 'semester', 'name') 25 | inlines = [ 26 | ProblemsInline, 27 | StudentsInline, 28 | ] 29 | 30 | @admin.register(Student) 31 | class StudentAdmin(admin.ModelAdmin): 32 | list_display = ('course', 'name', 'condition', 'id') 33 | inlines = [ 34 | SourceStateInline, 35 | ] 36 | 37 | @admin.register(Problem) 38 | class ProblemAdmin(admin.ModelAdmin): 39 | list_display = ('name',) 40 | inlines = [ 41 | TestcasesInline, 42 | # SourceStateInline, 43 | ] 44 | 45 | @admin.register(Hint) 46 | class HintAdmin(admin.ModelAdmin): 47 | list_display = ('level', 'message', 'id') 48 | #inlines = [ 49 | # SourceStateInline, 50 | #] 51 | 52 | @admin.register(Testcase) 53 | class TestcaseAdmin(admin.ModelAdmin): 54 | list_display = ('problem', 'id') 55 | 56 | @admin.register(State) 57 | class StateAdmin(admin.ModelAdmin): 58 | list_display = ('problem', 'score', 'id') 59 | 60 | @admin.register(SourceState) 61 | class SourceStateAdmin(admin.ModelAdmin): 62 | list_display = ('problem', 'student', 'score', 'id') 63 | 64 | @admin.register(CleanedState) 65 | class CleanedStateAdmin(admin.ModelAdmin): 66 | list_display = ('problem', 'count', 'score', 'id') 67 | 68 | @admin.register(AnonState) 69 | class AnonStateAdmin(admin.ModelAdmin): 70 | list_display = ('problem', 'count', 'score', 'id') 71 | 72 | 73 | @admin.register(CanonicalState) 74 | class CanonicalStateAdmin(admin.ModelAdmin): 75 | list_display = ('problem', 'count', 'score', 'id') 76 | -------------------------------------------------------------------------------- /hintgen/analysis.py: -------------------------------------------------------------------------------- 1 | from .getHint import * 2 | 3 | def clear_solution_space(problem, keep_starter=True): 4 | old_states = State.objects.filter(problem=problem.id) 5 | if len(old_states) > 1: 6 | # Clean out the old states 7 | starter_code = list(old_states)[0].code 8 | #log("Deleting " + str(len(old_states)) + " old states...", "bug") 9 | old_states.delete() 10 | 11 | if keep_starter: 12 | # But save the instructor solution! 13 | starter_state = SourceState(code=starter_code, problem=problem, count=1, student=Student.objects.get(id=1)) 14 | starter_state = get_hint(starter_state) 15 | starter_state.save() 16 | problem.solution = starter_state 17 | problem.save() 18 | 19 | stats_problem_set = [ 20 | "all_three_chars", "any_divisible", "any_first_chars", 21 | "any_lowercase", "can_drink_alcohol", "can_make_breakfast", 22 | "convert_to_degrees", "count_all_empty_strings", 23 | "create_number_block", "factorial", "find_root", 24 | "find_the_circle", "first_and_last", "get_extra_bagel", 25 | "go_to_gym", "has_balanced_parentheses", "has_extra_fee", 26 | "has_two_digits", "hello_world", "how_many_egg_cartons", 27 | "is_even_positive_int", "is_leap_month", "is_prime", 28 | "is_punctuation", "is_substring", "kth_digit", "last_index", 29 | "list_of_lists", "multiply_numbers", "nearest_bus_stop", 30 | "no_positive_even", "one_to_n", "over_nine_thousand", 31 | "reduce_to_positive", "second_largest", "single_pig_latin", 32 | "sum_all_even_numbers", "sum_of_digits", "sum_of_odd_digits", 33 | "was_lincoln_alive", "wear_a_coat", 34 | ] 35 | 36 | def run_all_problems(): 37 | problems = stats_problem_set 38 | for problem in problems: 39 | log("Running " + problem, "bug") 40 | import_code_as_states("hintgen/combined_data/"+problem+".csv", 1, 41 | problem, clear_space=True, run_profiler=False, run_hint_chain=False) 42 | 43 | def test_solution_space(): 44 | problems = stats_problem_set 45 | for problem in problems: 46 | for count in range(20): 47 | log("Running " + problem + " " + str(count), "bug") 48 | run_solution_space_improvement("hintgen/combined_data/" + problem + ".csv", problem, "random") 49 | os.rename(LOG_PATH + problem + "_" + "random" + ".csv", 50 | LOG_PATH + problem + "_" + "random" + "_" + str(count) + ".csv") 51 | 52 | 53 | def run_all_spaces(keyword): 54 | problems = stats_problem_set 55 | for problem in problems: 56 | log("Running " + problem, "bug") 57 | run_solution_space_improvement("hintgen/combined_data/" + problem + ".csv", problem, keyword) 58 | 59 | def run_canonical_space_reduction(): 60 | problems = stats_problem_set 61 | log("problem\tnum_syntax_errors\tnum_semantic_errors\tnum_correct\t" + \ 62 | "all_submissions\tall_source_states\tall_cleaned_states\tall_anon_states\tall_canonical_states\t" + \ 63 | "correct_submissions\tcorrect_source_states\tcorrect_cleaned_states\tcorrect_anon_states\tcorrect_canonical_states\n", "bug") 64 | for problem_name in problems: 65 | problem = Problem.objects.get(name=problem_name) 66 | clear_solution_space(problem) 67 | exact_text = { } 68 | correct_exact_text = { } 69 | last_seen = { } 70 | 71 | table = parse_table("hintgen/combined_data/" + problem_name + ".csv") 72 | header = table[0] 73 | table = table[1:] 74 | student_index = header.index("student_id") 75 | code_index = header.index("fun") 76 | for i in range(len(table)): 77 | line = table[i] 78 | student_name = line[student_index] 79 | code = line[code_index] 80 | if i > 0 and student_name in last_seen and \ 81 | last_seen[student_name] == code: 82 | continue # skip for now 83 | students = Student.objects.filter(name=student_name) 84 | if len(students) == 1: 85 | student = students[0] 86 | else: 87 | student = Student(course=course, name=student_name) 88 | student.save() 89 | 90 | state = SourceState(code=code, problem=problem, count=1, student=student) 91 | state = run_tests(state) 92 | state.save() 93 | last_seen[student_name] = code 94 | if state.tree != None: 95 | if code in exact_text: 96 | exact_text[code] += 1 97 | else: 98 | exact_text[code] = 1 99 | 100 | if state.score == 1: 101 | if code in correct_exact_text: 102 | correct_exact_text[code] += 1 103 | else: 104 | correct_exact_text[code] = 1 105 | for k in correct_exact_text: 106 | print(str(correct_exact_text[k]) + "\n" + k) 107 | 108 | source_states = SourceState.objects.filter(problem=problem) 109 | syntax_errors = source_states.filter(treeWeight__isnull=True) 110 | semantic_errors = source_states.filter(score__lt=1, treeWeight__isnull=False) 111 | correct_states = source_states.filter(score=1) 112 | 113 | cleaned_states = CleanedState.objects.filter(problem=problem, treeWeight__isnull=False) 114 | correct_cleaned_states = cleaned_states.filter(score=1) 115 | anon_states = AnonState.objects.filter(problem=problem, treeWeight__isnull=False) 116 | correct_anon_states = anon_states.filter(score=1) 117 | canonical_states = CanonicalState.objects.filter(problem=problem, treeWeight__isnull=False) 118 | correct_canonical_states = canonical_states.filter(score=1) 119 | 120 | log(problem_name + "\t" + str(len(syntax_errors)) + "\t" + str(len(semantic_errors)) + "\t" + str(len(correct_states)) + "\t" + \ 121 | str(len(semantic_errors) + len(correct_states)) + "\t" + str(len(exact_text.keys())) + "\t" + str(len(cleaned_states)) + "\t" + str(len(anon_states)) + "\t" + str(len(canonical_states)) + "\t" + \ 122 | str(len(correct_states)) + "\t" + str(len(correct_exact_text.keys())) + "\t" + str(len(correct_cleaned_states)) + "\t" + str(len(correct_anon_states)) + "\t" + str(len(correct_canonical_states)), "bug") 123 | 124 | def import_code_as_states(f, course_id, problem_name, clear_space=False, run_profiler=False, run_hint_chain=False): 125 | if run_profiler: 126 | # Set up the profiler 127 | out = sys.stdout 128 | outStream = io.StringIO() 129 | sys.stdout = outStream 130 | pr = cProfile.Profile() 131 | pr.enable() 132 | 133 | course = Course.objects.get(id=course_id) 134 | problem = Problem.objects.get(name=problem_name)#course.problems.get(name=problem_name) 135 | 136 | if clear_space: 137 | clear_solution_space(problem) 138 | 139 | # Import a CSV file of code into the database 140 | table = parse_table(f) 141 | header = table[0] 142 | table = table[1:] 143 | results = "" 144 | for line in table: 145 | if line[0] == "0": # we already have the instructor solutions 146 | continue 147 | student_name = line[header.index("student_id")] 148 | students = Student.objects.filter(name=student_name) 149 | if len(students) == 1: 150 | student = students[0] 151 | else: 152 | student = Student(course=course, name=student_name) 153 | student.save() 154 | code = line[header.index("fun")] 155 | 156 | if run_hint_chain: 157 | start_time = time.time() 158 | result, step_count, syntax_edits, semantic_edits, start_state, goal_state = do_hint_chain(code, student, problem) 159 | end_time = time.time() 160 | results += str(line[header.index("id")]) + "\t" + str(start_state.score) + "\t" + str(end_time - start_time) + "\t" + \ 161 | str(result) + "\t" + str(step_count) + "\t" + str(syntax_edits) + "\t" + str(semantic_edits) + "\n" 162 | else: 163 | start_time = time.time() 164 | state = SourceState(code=code, problem=problem, count=1, student=student) 165 | state = get_hint(state) 166 | state.save() 167 | end_time = time.time() 168 | results += str(line[header.index("id")]) + "\t" + str(state.score) + "\t" + str(end_time - start_time) + "\n" 169 | 170 | filename = LOG_PATH + problem_name + "_" + ("chain" if run_hint_chain else "results") + ".log" 171 | with open(filename, "w") as f: 172 | f.write(results) 173 | 174 | if run_profiler: 175 | # Check the profiler results 176 | sys.stdout = out 177 | pr.disable() 178 | s = io.StringIO() 179 | ps = pstats.Stats(pr, stream=s).sort_stats('cumulative') 180 | ps.print_stats() 181 | with open(LOG_PATH + problem_name + "_profile.log", "w") as f: 182 | f.write(outStream.getvalue() + s.getvalue()) 183 | print('\a') 184 | 185 | def generate_space(table, problem, keyword): 186 | # First, clear the solution space 187 | clear_solution_space(problem, keep_starter=False) 188 | 189 | header = table[0] 190 | starter = table[1] 191 | table = table[2:] 192 | 193 | # Add back in the starter code 194 | starter_state = SourceState(code=starter[header.index("fun")], problem=problem, count=1, student=Student.objects.get(id=1)) 195 | starter_state = get_hint(starter_state) 196 | starter_state.save() 197 | problem.solution = starter_state 198 | problem.save() 199 | 200 | if keyword == "optimal": 201 | for line in table: 202 | code_id = line[header.index("id")] 203 | student_name = line[header.index("student_id")] 204 | students = Student.objects.filter(name=student_name) 205 | if len(students) == 1: 206 | student = students[0] 207 | else: 208 | student = Student(course=Course.objects.get(id=1), name=student_name) 209 | student.save() 210 | code = line[header.index("fun")] 211 | if int(code_id) % 10 == 0: 212 | log("Generating space: " + code_id, "bug") 213 | # Now for each piece of code, find the distance between that piece of code and the starter goal 214 | state = SourceState(code=code, problem=problem, count=1, student=student) 215 | state = get_hint(state) 216 | state.save() 217 | 218 | def run_solution_space_improvement(f, problem_name, keyword): 219 | problem = Problem.objects.get(name=problem_name) 220 | table = parse_table(f) 221 | generate_space(table, problem, keyword) 222 | 223 | last_state = State.objects.latest('id') 224 | all_info = "id,correct_states,all_states,syntax_edit_weight,edit_weight,state_weight,goal_weight\n" 225 | correct_states = 1 226 | all_states = 1 227 | 228 | if keyword == "optimal": 229 | all_states = len(SourceState.objects.filter(problem=problem)) 230 | correct_states = len(SourceState.objects.filter(problem=problem, score=1)) 231 | 232 | header = table[0] 233 | table = table[2:] 234 | 235 | if keyword == "random": 236 | random.shuffle(table) 237 | 238 | for i in range(len(table)): 239 | line = table[i] 240 | code_id = line[header.index("id")] 241 | student_name = line[header.index("student_id")] 242 | students = Student.objects.filter(name=student_name) 243 | if len(students) == 1: 244 | student = students[0] 245 | else: 246 | student = Student(course=Course.objects.get(id=1), name=student_name) 247 | student.save() 248 | code = line[header.index("fun")] 249 | if i % 10 == 0: 250 | log("Checking distance: " + str(i), "bug") 251 | 252 | result, step_count, syntax_edits, semantic_edits, start_state, goal_state = do_hint_chain(code, student, problem) 253 | start_weight = diffAsts.getWeight(start_state) 254 | goal_weight = diffAsts.getWeight(goal_state) if goal_state != None else -1 255 | goal_code = goal_state.code if goal_state != None else "" 256 | all_info += str(code_id) + "," + str(correct_states) + "," + \ 257 | str(all_states) + "," + str(syntax_edits) + \ 258 | "," + str(semantic_edits) + "," + str(start_weight) + \ 259 | "," + str(goal_weight) + "," + '"' + start_state.code + \ 260 | '"' + "," + '"' + goal_code + '"' + "," + "\n" 261 | 262 | if keyword == "random": 263 | # Update the counts 264 | if start_state.score == 1: 265 | correct_states += 1 266 | all_states += 1 267 | else: 268 | # And after that, clear out all new states, unless we're building a space 269 | new_states = State.objects.filter(id__gt=last_state.id) 270 | new_states.delete() 271 | 272 | with open(LOG_PATH + problem_name + "_" + keyword + ".csv", "w") as f: 273 | f.write(all_info) 274 | print('\a') -------------------------------------------------------------------------------- /hintgen/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HintgenConfig(AppConfig): 5 | name = 'hintgen' 6 | -------------------------------------------------------------------------------- /hintgen/canonicalize/__init__.py: -------------------------------------------------------------------------------- 1 | import ast, copy, uuid 2 | from .transformations import * 3 | from ..namesets import allPythonFunctions 4 | from ..display import printFunction 5 | from ..test import test 6 | from ..astTools import tree_to_str, deepcopy 7 | from ..tools import log 8 | 9 | def runGiveIds(a): 10 | global idCounter 11 | idCounter = 0 12 | giveIds(a) 13 | 14 | idCounter = 0 15 | def giveIds(a): 16 | global idCounter 17 | if isinstance(a, ast.AST): 18 | if type(a) in [ast.Load, ast.Store, ast.Del, ast.AugLoad, ast.AugStore, ast.Param]: 19 | return # skip these 20 | a.global_id = uuid.uuid1() 21 | idCounter += 1 22 | for field in a._fields: 23 | child = getattr(a, field) 24 | if type(child) == list: 25 | for i in range(len(child)): 26 | # Get rid of aliased items 27 | if hasattr(child[i], "global_id"): 28 | child[i] = copy.deepcopy(child[i]) 29 | giveIds(child[i]) 30 | else: 31 | # Get rid of aliased items 32 | if hasattr(child, "global_id"): 33 | child = copy.deepcopy(child) 34 | setattr(a, field, child) 35 | giveIds(child) 36 | 37 | # def exists(d): 38 | # for k in d: 39 | # if "parsed" not in d[k] or d[k]["parsed"] == False: 40 | # return True 41 | # return False 42 | 43 | # def findGlobalValues(a, globalVars, variables=None): 44 | # if variables == None: 45 | # variables = set() 46 | # if not isinstance(a, ast.AST): 47 | # return 48 | 49 | # if type(a) == ast.FunctionDef: 50 | # variables |= set(gatherAssignedVarIds(a.args.args)) 51 | # elif type(a) == ast.Assign: 52 | # variables |= set(gatherAssignedVarIds(a.targets)) 53 | # elif type(a) == ast.Name: 54 | # if a.id not in variables: 55 | # globalVars.add(a.id) 56 | # return 57 | # for child in ast.iter_child_nodes(a): 58 | # findGlobalValues(child, globalVars, variables) 59 | 60 | # def reduceTree(a, t): 61 | # helperD = { a.name : { "tree" : a, "parsed" : False } } 62 | # toKeep = [] 63 | # while exists(helperD): 64 | # # Find all necessary external names in the helper dictionary 65 | # keys = helperD.keys() 66 | # for k in keys: 67 | # if "parsed" not in helperD[k]: 68 | # # If it isn't in the python namespace... 69 | # if k not in (builtInTypes + libraries + allPythonFunctions.keys()): 70 | # log("canonicalize\treduceTree\tmissing parsed: " + str(k), "bug") 71 | # helperD[k]["parsed"] = True 72 | # elif not helperD[k]["parsed"]: 73 | # globalNames = set() 74 | # findGlobalValues(helperD[k]["tree"], globalNames) 75 | # for name in globalNames: 76 | # if name not in helperD: 77 | # helperD[name] = { } 78 | # helperD[k]["parsed"] = True 79 | 80 | # # go through t and extract all corresponding values 81 | # for i in range(len(t.body)): 82 | # if i in toKeep: 83 | # continue # already checked this one 84 | # item = t.body[i] 85 | # if item == a: 86 | # toKeep.append(i) 87 | # elif type(item) in [ast.ImportFrom]: 88 | # toKeep.append(i) 89 | # elif type(item) in [ast.Assert, ast.Expr]: 90 | # continue 91 | # elif type(item) in [ast.FunctionDef, ast.ClassDef]: 92 | # if item.name in helperD: 93 | # toKeep.append(i) 94 | # helperD[item.name]["tree"] = item 95 | # helperD[item.name]["parsed"] = False 96 | # elif type(item) in [ast.Assign, ast.Import]: 97 | # if type(item) == ast.Assign: 98 | # names = gatherAssignedVarIds(item.targets) 99 | # else: 100 | # names = map(lambda x : x.asname if x.asname != None else x.name, a.names) 101 | # keep = False 102 | # for name in names: 103 | # if name in helperD: 104 | # keep = True 105 | # helperD[name]["tree"] = item 106 | # helperD[name]["parsed"] = False 107 | # if keep: 108 | # toKeep.append(i) 109 | # elif type(item) in [ast.Delete, ast.AugAssign]: 110 | # if type(item) == ast.Delete: 111 | # names = gatherAssignedVarIds(item.targets) 112 | # else: 113 | # names = gatherAssignedVarIds([item.target]) 114 | # for name in names: 115 | # if name in helperD: 116 | # toKeep.append(i) 117 | # break 118 | # else: 119 | # log("canonicalize\treduceTree\tMissing type: " + str(type(item)), "bug") 120 | # toKeep.sort() 121 | # newTree = ast.Module([]) 122 | # for i in toKeep: 123 | # newTree.body.append(t.body[i]) 124 | # return newTree 125 | 126 | def checkGlobalIds(a, l): 127 | if not isinstance(a, ast.AST): 128 | return 129 | elif type(a) in [ ast.Load, ast.Store, ast.Del, ast.AugLoad, ast.AugStore, ast.Param ]: 130 | return 131 | if not hasattr(a, "global_id"): 132 | addedNodes = ["propagatedVariable", "orderedBinOp", 133 | "augAssignVal", "augAssignBinOp", 134 | "combinedConditional", "combinedConditionalOp", 135 | "multiCompPart", "multiCompOp", 136 | "second_global_id", "moved_line", 137 | # above this line has individualize functions. below does not. 138 | "addedNot", "addedNotOp", "addedOther", "addedOtherOp", 139 | "collapsedExpr", "removedLines", 140 | "helperVar", "helperReturn", 141 | "typeCastFunction", ] 142 | for t in addedNodes: 143 | if hasattr(a, t): 144 | break 145 | else: # only enter the else if none of the provided types are an attribute of a 146 | log("canonicalize\tcheckGlobalIds\tNo global id: " + str(l) + "," + str(a.__dict__) + "," + printFunction(a, 0), "bug") 147 | for f in ast.iter_child_nodes(a): 148 | checkGlobalIds(f, l + [type(a)]) 149 | 150 | def stateDiff(s, funName): 151 | return 152 | checkGlobalIds(s.tree, []) 153 | old_score = s.score 154 | old_feedback = s.feedback 155 | old_code = s.code 156 | s = test(s, forceRetest=True) 157 | if abs(old_score - s.score) > 0.001: 158 | log("canonicalize\tstateDiff\tScore mismatch: " + funName + "," + str(old_score) + "," + str(s.score), "bug") 159 | log(old_feedback + "," + s.feedback, "bug") 160 | log(old_code, "bug") 161 | log(printFunction(s.tree), "bug") 162 | log(printFunction(s.orig_tree), "bug") 163 | 164 | def getCanonicalForm(s, given_names=None, argTypes=None, imports=None): 165 | #s.tree = deepcopy(s.tree) # no shallow copying! We need to leave the old tree alone 166 | 167 | #giveIds(s.tree) 168 | #s.orig_tree = deepcopy(s.tree) 169 | #s.orig_tree_source = tree_to_str(s.tree) 170 | orig_score = s.score 171 | orig_feedback = s.feedback 172 | if imports == None: 173 | imports = [] 174 | 175 | #crashableCopyPropagation 176 | transformations = [ 177 | constantFolding, 178 | 179 | cleanupEquals, 180 | cleanupBoolOps, 181 | cleanupRanges, 182 | cleanupSlices, 183 | cleanupTypes, 184 | cleanupNegations, 185 | 186 | conditionalRedundancy, 187 | combineConditionals, 188 | collapseConditionals, 189 | 190 | copyPropagation, 191 | 192 | deMorganize, 193 | orderCommutativeOperations, 194 | 195 | deadCodeRemoval 196 | ] 197 | 198 | s.tree = propogateMetadata(s.tree, argTypes, {}, [0]) 199 | stateDiff(s, "propogateMetadata") 200 | s.tree = simplify(s.tree) 201 | stateDiff(s, "simplify") 202 | s.tree = anonymizeNames(s.tree, given_names, imports) 203 | stateDiff(s, "anonymizeNames") 204 | oldTree = None 205 | while compareASTs(oldTree, s.tree, checkEquality=True) != 0: 206 | oldTree = deepcopy(s.tree) 207 | helperFolding(s.tree, s.problem.name, imports) 208 | stateDiff(s, "helperFolding") 209 | for t in transformations: 210 | s.tree = t(s.tree) # modify in place 211 | stateDiff(s, str(t).split()[1]) 212 | s.code = printFunction(s.tree) 213 | s.score = orig_score 214 | s.feedback = orig_feedback 215 | return s -------------------------------------------------------------------------------- /hintgen/getHint.py: -------------------------------------------------------------------------------- 1 | import ast, sys, io, pstats, cProfile, time, random, os 2 | from .canonicalize import getAllImports, getAllImportStatements, runGiveIds, anonymizeNames, getCanonicalForm, propogateMetadata, propogateNameMetadata 3 | from .path_construction import diffAsts, generateNextStates 4 | from .individualize import mapEdit 5 | from .generate_message import formatHints 6 | from .getSyntaxHint import getSyntaxHint, applyChanges 7 | 8 | from .test import test 9 | from .display import printFunction 10 | from .astTools import deepcopy, tree_to_str, str_to_tree 11 | from .tools import log, parse_table 12 | from .paths import LOG_PATH 13 | 14 | from .models import * 15 | from .ChangeVector import * 16 | from .SyntaxEdit import * 17 | 18 | def check_repeating_edits(state, allEdits, printedStates): 19 | # First check for infinitely-looping edits 20 | # If we've seen this exact change before, it's a loop 21 | if isinstance(state.edit[0], ChangeVector): 22 | if state.edit in allEdits: 23 | s = "REPEATING EDITS" 24 | log(s + "\n" + printedStates, "bug") 25 | return s 26 | elif isinstance(state.edit[0], SyntaxEdit): 27 | if len(allEdits) > 0: 28 | current = state.edit[0] 29 | prev = allEdits[-1][0] 30 | if not isinstance(prev, SyntaxEdit): 31 | log("getHint\tcheck_repeating_edits\tSyntax hint after semantic hint?\n" + 32 | str(allEdits) + "\n" + str(state.edit), "bug") 33 | log(printedStates, "bug") 34 | else: 35 | # If we're adding and then deleting the same thing, it's a loop 36 | if current.editType == "-" and prev.editType == "+" and \ 37 | current.line == prev.line and current.col == prev.col and \ 38 | current.text == prev.text and current.newText == prev.newText: 39 | s = "REPEATING EDITS" 40 | log(s + "\n" + printedStates, "bug") 41 | return s 42 | else: 43 | log("Unknown edit type?" + repr(state.edit[0]), filename="bug") 44 | 45 | def do_hint_chain(code, user, problem, interactive=False): 46 | orig_state = state = SourceState(code=code, problem=problem, count=1, student=user) 47 | failedTests = True 48 | stepCount = editCount = chrCount = 0 49 | cutoff = 20 50 | printedStates = "************************" 51 | allEdits = [] 52 | # Keep applying hints until either the state is correct or 53 | # you've reached the cutoff. The cap is there to cut off infinite loops. 54 | while stepCount < cutoff: 55 | stepCount += 1 56 | printedStates += "State: \n" + state.code + "\n" 57 | state = get_hint(state, hint_level="next_step") 58 | if state.goal != None: 59 | printedStates += "Goal: \n" + state.goal.code + "\n" 60 | 61 | if hasattr(state, "edit") and state.edit != None and len(state.edit) > 0: 62 | printedStates += state.hint.message + "\n" + str(state.edit) + "\n" 63 | 64 | repeatingCheck = check_repeating_edits(state, allEdits, printedStates) 65 | if repeatingCheck != None: 66 | return repeatingCheck, stepCount, chrCount, editCount, orig_state, None 67 | 68 | if isinstance(state.edit[0], ChangeVector): 69 | editCount += diffAsts.getChangesWeight(state.edit, False) 70 | newTree = state.tree 71 | for e in state.edit: 72 | e.start = newTree 73 | newTree = e.applyChange() 74 | 75 | if newTree == None: 76 | s = "EDIT BROKE" 77 | log(s + "\n" + printedStates, "bug") 78 | return s, stepCount, chrCount, editCount, orig_state, None 79 | newFun = printFunction(newTree) 80 | else: # Fixing a syntax error 81 | newFun = applyChanges(state.code, state.edit) 82 | chrCount += sum(len(c.text) + len(c.newText) for c in state.edit) 83 | 84 | allEdits.append(state.edit) 85 | state = SourceState(code=newFun, problem=problem, count=1, student=user) 86 | elif state.score != 1: 87 | s = "NO NEXT STEP" 88 | log(s + "\n" + printedStates, "bug") 89 | log("Scores: " + str(state.score), "bug") 90 | log("Feedback: " + str(state.feedback), "bug") 91 | if state.goal != None: 92 | log("THERE'S A GOAL", "bug") 93 | log("Score: " + str(state.goal.score), "bug") 94 | log("Feedback: " + str(state.goal.feedback), "bug") 95 | log("DIFF: " + str(diffAsts.diffAsts(state.tree, state.goal.tree)), "bug") 96 | return s, stepCount, chrCount, editCount, orig_state, None 97 | else: # break out when the score reaches 1 98 | break 99 | if interactive: 100 | input("") 101 | 102 | if state.score == 1: 103 | if stepCount == 1: 104 | s = "Started Correct" 105 | else: # Got through the hints! Woo! 106 | s = "Success" 107 | else: # These are the bad cases 108 | state = None # no goal state! 109 | if stepCount >= cutoff: 110 | s = "TOO LONG" 111 | log(s + "\n" + printedStates, "bug") 112 | else: 113 | s = "BROKEN" 114 | log(s + "\n" + printedStates, "bug") 115 | return s, stepCount, chrCount, editCount, orig_state, state 116 | 117 | def find_example_solutions(s, goals): 118 | most_common = [None, -1] 119 | furthest = [None, -1] 120 | closest = [None, 2] 121 | 122 | for g in goals: 123 | dist, _ = diffAsts.distance(s, g) 124 | if dist != 0: 125 | if g.count > most_common[1]: 126 | most_common = [g, g.count] 127 | if dist > furthest[1]: 128 | furthest = [g, dist] 129 | if dist < closest[1]: 130 | closest = [g, dist] 131 | if most_common[0] == None: 132 | examples = [] 133 | else: 134 | examples = [most_common[0]] 135 | if furthest[0].code != most_common[0].code: 136 | examples.append(furthest[0]) 137 | if closest[0].code != furthest[0].code and closest[0].code != most_common[0].code: 138 | examples.append(closest[0]) 139 | return examples 140 | 141 | def generate_cleaned_state(source_state): 142 | # First level of abstraction: convert to an AST and back. Cleans the code by removing comments, whitespace, etc. 143 | cleaned_code = printFunction(source_state.tree) 144 | prior_cleaned = list(CleanedState.objects.filter(problem=source_state.problem, code=cleaned_code)) 145 | if len(prior_cleaned) == 0: 146 | cleaned_state = CleanedState(code=cleaned_code, problem=source_state.problem, 147 | score=source_state.score, count=1, 148 | feedback=source_state.feedback) 149 | cleaned_state.tree_source = source_state.tree_source 150 | cleaned_state.treeWeight = source_state.treeWeight 151 | elif len(prior_cleaned) == 1: 152 | cleaned_state = prior_cleaned[0] 153 | cleaned_state.count += 1 154 | if cleaned_code != cleaned_state.code: 155 | log("getHint\tgenerate_cleaned_state\tCode mismatch: \n" + cleaned_state.code + "\n" + cleaned_code, "bug") 156 | else: 157 | log("getHint\tgenerate_cleaned_state\tDuplicate code entries in cleaned: " + cleaned_code, "bug") 158 | cleaned_state.tree = source_state.tree 159 | cleaned_state = test(cleaned_state, forceRetest=True) 160 | if cleaned_state.score != source_state.score: 161 | log("getHint\tgenerate_cleaned_state\tScore mismatch: " + \ 162 | str(source_state.score) + "," + str(cleaned_state.score) + "\n" + \ 163 | source_state.code + "\n" + cleaned_state.code, "bug") 164 | return cleaned_state 165 | 166 | def generate_anon_state(cleaned_state, given_names, imports): 167 | # Mid-level: just anonymize the variable names TODO variableMap 168 | orig_tree = deepcopy(cleaned_state.tree) 169 | runGiveIds(orig_tree) 170 | anon_tree = deepcopy(orig_tree) 171 | anon_tree = anonymizeNames(anon_tree, given_names, imports) 172 | if cleaned_state.count > 1 and cleaned_state.anon != None: 173 | anon_state = cleaned_state.anon 174 | anon_state.count += 1 175 | else: 176 | anon_code = printFunction(anon_tree) 177 | prior_anon = list(AnonState.objects.filter(problem=cleaned_state.problem, code=anon_code)) 178 | if len(prior_anon) == 0: 179 | anon_state = AnonState(code=anon_code, problem=cleaned_state.problem, 180 | score=cleaned_state.score, count=1, 181 | feedback=cleaned_state.feedback) 182 | anon_state.treeWeight = diffAsts.getWeight(anon_tree) 183 | else: 184 | if len(prior_anon) > 1: 185 | log("getHint\tgenerate_anon_state\tDuplicate code entries in anon: " + anon_code, "bug") 186 | anon_state = prior_anon[0] 187 | anon_state.count += 1 188 | anon_state.tree = anon_tree 189 | anon_state.tree_source = tree_to_str(anon_tree) 190 | anon_state = test(anon_state, forceRetest=True) 191 | if anon_state.score != cleaned_state.score: 192 | log("getHint\tgenerate_anon_state\tScore mismatch: " + \ 193 | str(cleaned_state.score) + "," + str(anon_state.score) + "\n" + \ 194 | cleaned_state.code + "\n" + anon_state.code, "bug") 195 | anon_state.orig_tree = orig_tree 196 | anon_state.orig_tree_source = tree_to_str(orig_tree) 197 | return anon_state 198 | 199 | def generate_canonical_state(cleaned_state, anon_state, given_names, imports): 200 | # Second level of abstraction: canonicalize the AST. Gets rid of redundancies. 201 | args = eval(anon_state.problem.arguments) 202 | orig_tree = deepcopy(cleaned_state.tree) 203 | runGiveIds(orig_tree) 204 | if type(args) != dict: 205 | log("getHint\tgenerate_canonical_state\tBad args format: " + anon_state.problem.arguments, "bug") 206 | args = { } 207 | if anon_state.count > 1 and anon_state.canonical != None: 208 | canonical_state = anon_state.canonical 209 | canonical_state.orig_tree = orig_tree 210 | canonical_state.orig_tree_source = tree_to_str(canonical_state.orig_tree) 211 | canonical_state.tree = deepcopy(canonical_state.orig_tree) 212 | canonical_state = getCanonicalForm(canonical_state, given_names, args, imports) 213 | canonical_state.count += 1 214 | else: 215 | canonical_state = CanonicalState(code=cleaned_state.code, problem=cleaned_state.problem, 216 | score=cleaned_state.score, count=1, 217 | feedback=cleaned_state.feedback) 218 | canonical_state.orig_tree = orig_tree 219 | canonical_state.orig_tree_source = tree_to_str(canonical_state.orig_tree) 220 | canonical_state.tree = deepcopy(canonical_state.orig_tree) 221 | canonical_state = getCanonicalForm(canonical_state, given_names, args, imports) 222 | canonical_state = test(canonical_state, forceRetest=True) 223 | if canonical_state.score != cleaned_state.score: 224 | log("getHint\tgenerate_canonical_state\tScore mismatch: " + str(cleaned_state.score) + "," + str(canonical_state.score) + "\n" + cleaned_state.code + "\n" + canonical_state.code, "bug") 225 | prior_canon = list(CanonicalState.objects.filter(problem=cleaned_state.problem, code=canonical_state.code)) 226 | if len(prior_canon) == 0: 227 | canonical_state.tree_source = tree_to_str(canonical_state.tree) 228 | canonical_state.treeWeight = diffAsts.getWeight(canonical_state.tree) 229 | else: 230 | if len(prior_canon) > 1: 231 | log("getHint\tgenerate_canonical_state\tDuplicate code entries in canon: " + canonical_state.code, "bug") 232 | prev_tree = canonical_state.tree 233 | canonical_state = prior_canon[0] 234 | canonical_state.count += 1 235 | canonical_state.tree = prev_tree 236 | canonical_state.orig_tree = orig_tree 237 | canonical_state.orig_tree_source = tree_to_str(orig_tree) 238 | return canonical_state 239 | 240 | def generate_states(source_state, given_names, imports): 241 | # Convert to cleaned, anonymous, and canonical states 242 | 243 | cleaned_state = generate_cleaned_state(source_state) 244 | cleaned_state.save() 245 | anon_state = generate_anon_state(cleaned_state, given_names, imports) 246 | anon_state.save() 247 | canonical_state = generate_canonical_state(cleaned_state, anon_state, given_names, imports) 248 | canonical_state.save() 249 | 250 | source_state.cleaned = cleaned_state 251 | cleaned_state.anon = anon_state 252 | anon_state.canonical = canonical_state 253 | 254 | return (cleaned_state, anon_state, canonical_state) 255 | 256 | def save_states(source, cleaned, anon, canonical): 257 | for s in [anon, canonical]: 258 | g = s.goal 259 | if g != None: 260 | g.save() 261 | s.goal = g 262 | next_chain = [s] 263 | while s.next != None: 264 | s = s.next 265 | next_chain.append(s) 266 | for i in range(len(next_chain)-1, 0, -1): 267 | n = next_chain[i] 268 | g = n.goal 269 | if g != None: 270 | if g.goal != None: 271 | log("getHint\tsave_states\tWeird goal goal: " + str(g.score) + "," + g.code, "bug") 272 | log("getHint\tsave_states\tWeird goal goal: " + str(g.goal.score) + "," + g.goal.code, "bug") 273 | g.save() 274 | n.goal = g 275 | n.save() 276 | next_chain[i-1].next = n 277 | 278 | if source.hint != None: 279 | source.hint.save() 280 | canonical.save() 281 | anon.save() 282 | cleaned.save() 283 | source.save() 284 | 285 | def test_code(source_state): 286 | # Parse the code, get tree and treeWeight 287 | try: 288 | source_state.tree = ast.parse(source_state.code) 289 | source_state.tree_source = tree_to_str(source_state.tree) 290 | source_state.treeWeight = diffAsts.getWeight(source_state.tree) 291 | except Exception as e: 292 | # Couldn't parse 293 | source_state.tree = None 294 | 295 | # Test the code, get score and feedback 296 | source_state = test(source_state) 297 | return source_state 298 | 299 | def run_tests(source_state): 300 | orig_code = source_state.code 301 | source_state = test_code(source_state) 302 | source_state.code = orig_code 303 | 304 | args = eval(source_state.problem.arguments) 305 | given_code = ast.parse(source_state.problem.given_code) 306 | importNames = getAllImports(source_state.tree) + getAllImports(given_code) 307 | inp = importNames + (list(args.keys()) if type(args) == dict else []) 308 | given_names = [str(x) for x in inp] 309 | imports = getAllImportStatements(source_state.tree) + getAllImportStatements(given_code) 310 | 311 | if source_state.tree != None: 312 | (cleaned_state, anon_state, canonical_state) = generate_states(source_state, given_names, imports) 313 | save_states(source_state, cleaned_state, anon_state, canonical_state) 314 | else: 315 | source_state.save() 316 | return source_state 317 | 318 | def get_hint(source_state, hint_level="default"): 319 | orig_code = source_state.code 320 | source_state = test_code(source_state) 321 | source_state.code = orig_code 322 | 323 | # If we can't parse their solution, use a simplified version of the algorithm with textual edits 324 | if source_state.tree == None: 325 | return getSyntaxHint(source_state, "syntax_" + hint_level) 326 | 327 | args = eval(source_state.problem.arguments) 328 | given_code = ast.parse(source_state.problem.given_code) 329 | importNames = getAllImports(source_state.tree) + getAllImports(given_code) 330 | inp = importNames + (list(args.keys()) if type(args) == dict else []) 331 | given_names = [str(x) for x in inp] 332 | imports = getAllImportStatements(source_state.tree) + getAllImportStatements(given_code) 333 | 334 | # Setup the correct states we need for future work 335 | goals = list(AnonState.objects.filter(problem=source_state.problem, score=1)) + \ 336 | list(CanonicalState.objects.filter(problem=source_state.problem, score=1)) 337 | for goal in goals: 338 | goal.tree = str_to_tree(goal.tree_source) 339 | 340 | (cleaned_state, anon_state, canonical_state) = generate_states(source_state, given_names, imports) 341 | 342 | states = list(AnonState.objects.filter(problem=source_state.problem)) + \ 343 | list(CanonicalState.objects.filter(problem=source_state.problem)) 344 | 345 | if source_state.score == 1: 346 | examples = find_example_solutions(source_state, goals) 347 | hint = Hint(level="examples") 348 | hint.message = "Your solution is already correct!" 349 | if len(examples) > 0: 350 | hint.message += " If you're interested, here are some other correct solutions:\n" 351 | for example in examples: 352 | hint.message += "" + example.code + "\n\n" 353 | hint.save() 354 | source_state.hint = hint 355 | else: 356 | # Determine the hint level 357 | if hint_level == "default": 358 | submissions = list(SourceState.objects.filter(student=source_state.student)) 359 | if len(submissions) > 0 and submissions[-1].hint != None and submissions[-1].problem == source_state.problem and \ 360 | submissions[-1].code == source_state.code: 361 | if submissions[-1].hint.level == "next_step": 362 | hint_level = "structure" 363 | elif submissions[-1].hint.level == "structure": 364 | hint_level = "half_steps" 365 | elif submissions[-1].hint.level in ["half_steps", "solution"]: 366 | hint_level = "solution" 367 | else: 368 | hint_level = "next_step" 369 | else: 370 | hint_level = "next_step" 371 | 372 | # If necessary, generate next/goal states for the anon and canonical states 373 | if anon_state.goal == None: 374 | generateNextStates.getNextState(anon_state, goals, states) 375 | else: 376 | # Is there a better goal available now? 377 | best_goal = generateNextStates.chooseGoal(anon_state, goals, states) 378 | if anon_state.goal != best_goal: 379 | generateNextStates.getNextState(anon_state, goals, states, best_goal) 380 | if canonical_state.goal == None: 381 | generateNextStates.getNextState(canonical_state, goals, states) 382 | else: 383 | # Is there a better goal available now? 384 | best_goal = generateNextStates.chooseGoal(canonical_state, goals, states) 385 | if canonical_state.goal != best_goal: 386 | generateNextStates.getNextState(canonical_state, goals, states, best_goal) 387 | 388 | # Then choose the best path to use 389 | anon_distance, _ = diffAsts.distance(anon_state, anon_state.goal, forceReweight=True) 390 | canonical_distance, _ = diffAsts.distance(canonical_state, canonical_state.goal, forceReweight=True) 391 | if anon_distance <= canonical_distance: 392 | used_state = anon_state 393 | other_state = canonical_state 394 | else: 395 | used_state = canonical_state 396 | other_state = anon_state 397 | 398 | switched_already = False 399 | while True: 400 | if used_state.next == None: 401 | log("getHint\tget_hint\tCould not find next state for state " + str(used_state.id), "bug") 402 | break 403 | next_state = used_state.next 404 | if not hasattr(next_state, "tree"): 405 | next_state.tree = str_to_tree(next_state.tree_source) 406 | edit = diffAsts.diffAsts(used_state.tree, next_state.tree) 407 | edit, _ = generateNextStates.updateChangeVectors(edit, used_state.tree, used_state.tree) 408 | if not hasattr(used_state, "orig_tree"): 409 | if hasattr(used_state, "orig_tree_source") and used_state.orig_tree_source != "": 410 | used_state.orig_tree = str_to_tree(used_state.orig_tree_source) 411 | else: 412 | log("getHint\tgetHint\tWhy no orig_tree?!?!" + str(used_state), "bug") 413 | edit = mapEdit(used_state.tree, used_state.orig_tree, edit) 414 | if len(edit) == 0: 415 | if next_state.next != None: 416 | # Replace used_state's next with next_state's 417 | used_state.next = next_state.next 418 | continue 419 | else: 420 | log("Reached dead end: " + printFunction(used_state.orig_tree) + "\n" + str(used_state.code) + "\n" + str(used_state.goal.code), "bug") 421 | if not switched_already: 422 | used_state = other_state # just try the other version 423 | switched_already = True 424 | continue 425 | else: 426 | source_state.edit = None 427 | source_state.hint = Hint(message="No hint could be generated") 428 | break 429 | hint = formatHints(used_state, edit, hint_level, used_state.orig_tree) # generate the right level of hint 430 | source_state.edit = edit 431 | source_state.hint = hint 432 | source_state.goal = used_state.goal 433 | break 434 | 435 | # Save all the states! 436 | save_states(source_state, cleaned_state, anon_state, canonical_state) 437 | return source_state 438 | 439 | -------------------------------------------------------------------------------- /hintgen/getSyntaxHint.py: -------------------------------------------------------------------------------- 1 | import ast, copy, difflib, os, random 2 | from .tools import log, powerSet, fastPowerSet 3 | from .astTools import structureTree 4 | from .paths import TEST_PATH 5 | from .display import printFunction 6 | #from .lexerSyntaxHint import getLexerSyntaxHint 7 | from .SyntaxEdit import SyntaxEdit 8 | from .models import SourceState, Hint 9 | 10 | def smartSplit(code): 11 | tokens = [] 12 | i = 0 13 | currentText = "" 14 | line = currentLine = 1 15 | col = currentCol = 1 16 | totalCol = currentTotalCol = 0 17 | while i < len(code): 18 | chr = code[i] 19 | if len(currentText) == 0: 20 | if chr == "\n" or chr == "\t" or chr == " ": # TODO: how to find the right number of columns for tabs? 21 | tokens.append([chr, line, col, totalCol]) 22 | else: 23 | currentText = chr 24 | currentLine = line 25 | currentCol = col 26 | currentTotalCol = totalCol 27 | else: 28 | if chr == "\n" or chr == "\t" or chr == " ": 29 | tokens.append([currentText, currentLine, currentCol, currentTotalCol]) 30 | tokens.append([chr, line, col, totalCol]) 31 | currentText = "" 32 | else: 33 | currentText += chr 34 | col += 1 35 | totalCol += 1 36 | if chr == "\n": 37 | line += 1 38 | col = 1 39 | i += 1 40 | if currentText != "": 41 | tokens.append([currentText, currentLine, currentCol, currentTotalCol]) 42 | return tokens 43 | 44 | def getTextDiff(code1, code2): 45 | differ = difflib.Differ() 46 | changes = [] 47 | codeTokens1 = smartSplit(code1) 48 | tokens1 = [t[0] for t in codeTokens1] 49 | codeTokens2 = smartSplit(code2) 50 | tokens2 = [t[0] for t in codeTokens2] 51 | dif = differ.compare(tokens1, tokens2) 52 | j = 0 53 | type = "" 54 | text = "" 55 | line = 0 56 | col = 0 57 | totalCol = 0 58 | for chr in dif: 59 | changeType = chr[0] 60 | changeChr = chr[2:] 61 | if changeType != type or (len(text) > 0 and text[-1] == "\n"): 62 | if text != "": 63 | changes.append(SyntaxEdit(line, col, totalCol, type, text)) 64 | text = "" 65 | type = changeType 66 | if changeType in ["-", "+"]: 67 | text = changeChr 68 | if j < len(codeTokens1): 69 | line = codeTokens1[j][1] 70 | col = codeTokens1[j][2] 71 | totalCol = codeTokens1[j][3] 72 | else: 73 | line = codeTokens1[j-1][1] 74 | col = codeTokens1[j-1][2] + len(codeTokens1[j-1][0]) 75 | totalCol = codeTokens1[j-1][3] + len(codeTokens1[j-1][0]) 76 | else: 77 | if changeType in ["-", "+"]: 78 | text += changeChr 79 | if changeType in ["-", " "]: 80 | j += 1 # move forward in the codeTokens list 81 | if text != "": 82 | changes.append(SyntaxEdit(line, col, totalCol, type, text)) 83 | return changes 84 | 85 | def combineSameLocationChanges(changes): 86 | """Destructively modifies changes""" 87 | j = 1 88 | while j < len(changes): 89 | # if possible, combine - and + changes at the same location into -+ changes 90 | if (changes[j-1].totalCol == changes[j].totalCol) and ((changes[j-1].editType + changes[j].editType) == "+-"): 91 | changes[j-1].editType = "-+" 92 | changes[j-1].newText = changes[j-1].text 93 | changes[j-1].text = changes[j].text 94 | changes[j:] = changes[j+1:] 95 | elif (changes[j-1].totalCol == changes[j].totalCol - len(changes[j-1].text)) and \ 96 | (changes[j-1].editType + changes[j].editType) == "-+": 97 | changes[j-1].editType = "-+" 98 | changes[j-1].newText = changes[j].text 99 | changes[j:] = changes[j+1:] 100 | j += 1 101 | return changes 102 | 103 | def combineSmartSyntaxEdits(changes): 104 | j = 1 105 | while j < len(changes): 106 | if j < 1: 107 | j = 1 108 | if len(changes) == 1: 109 | break 110 | if changes[j-1].totalCol == changes[j].totalCol: 111 | if changes[j-1].editType == changes[j].editType and changes[j-1].editType in ["indent", "deindent", "-"]: 112 | changes[j-1].text += changes[j].text 113 | changes.pop(j) 114 | continue 115 | elif changes[j-1].editType in ["deindent", "-"] and changes[j].editType in ["deindent", "-"]: 116 | changes[j-1].text += changes[j].text 117 | changes[j-1].editType = "-" # combining them, so it isn't deindent anymore 118 | changes.pop(j) 119 | j -= 1 120 | continue 121 | elif changes[j-1].editType in ["deindent", "-"] and changes[j].editType in ["indent", "+"]: 122 | changes[j-1].editType = "+-" 123 | changes[j-1].newText = changes[j].text 124 | changes.pop(j) 125 | continue 126 | elif changes[j-1].editType == "+-" and changes[j].editType in ["deindent", "-"]: 127 | if changes[j-1].newText == changes[j].text: 128 | changes[j-1].editType = "deindent" if changes[j-1].text.replace(" ", "").replace("\t", "") == "" else "-" 129 | changes[j-1].newText = "" 130 | changes.pop(j) 131 | j -= 1 132 | continue 133 | elif changes[j].text in changes[j-1].newText and changes[j-1].newText.find(changes[j].text) == 0: 134 | # the next edit is at the start- just remove it now 135 | changes[j-1].newText = changes[j-1].newText[len(changes[j].text):] 136 | changes.pop(j) 137 | continue 138 | elif changes[j-1].newText in changes[j].text and changes[j].text.find(changes[j-1].newText) == 0: 139 | changes[j-1].editType = "deindent" if changes[j-1].text.replace(" ", "").replace("\t", "") == "" else "-" 140 | changes[j].text = changes[j].text[len(changes[j-1].newText):] 141 | changes[j-1].newText = "" 142 | j -= 1 143 | continue 144 | elif changes[j-1].editType in ["+", "indent"] and changes[j].editType in ["+", "indent"]: 145 | if changes[j-1].totalCol + len(changes[j-1].text) == changes[j].totalCol: 146 | changes[j-1].text += changes[j].text 147 | changes[j-1].editType = "indent" if changes[j-1].text.replace(" ", "").replace("\t", "") == "" else "+" 148 | changes.pop(j) 149 | j -= 1 150 | continue 151 | elif changes[j-1].editType == "+-" and changes[j].editType in ["+", "indent"]: 152 | if changes[j-1].totalCol + len(changes[j-1].newText) - len(changes[j-1].text) + 1 == changes[j].totalCol: 153 | changes[j-1].newText += changes[j].text 154 | changes.pop(j) 155 | continue 156 | elif changes[j].editType == "indent" and changes[j-1].line == changes[j].line: 157 | if (changes[j-1].newText.strip(" ") == changes[j].text.strip(" ") == "") or \ 158 | (changes[j-1].newText.strip("\t") == changes[j].text.strip("\t") == ""): 159 | changes[j-1].newText += changes[j].text 160 | changes.pop(j) 161 | continue 162 | elif changes[j-1].editType == "+-" and changes[j].editType == "deindent" and changes[j-1].line == changes[j].line: 163 | # Special case: it's okay to combine if the +- merged text with the old text even if totalCol isn't the same 164 | # if we're just trimming whitespace 165 | if (changes[j-1].newText.strip(" ") == changes[j].text.strip(" ") == "") or \ 166 | (changes[j-1].newText.strip("\t") == changes[j].text.strip("\t") == ""): 167 | if len(changes[j-1].newText) == len(changes[j].text): 168 | changes[j-1].editType = "deindent" if changes[j-1].text.replace(" ", "").replace("\t", "") == "" else "-" 169 | changes[j-1].newText = "" 170 | changes.pop(j) 171 | j -= 1 172 | continue 173 | elif len(changes[j-1].newText) < len(changes[j].text): 174 | changes[j].text = changes[j].text[len(changes[j-1].newText):] 175 | changes[j-1].editType = "deindent" if changes[j-1].text.replace(" ", "").replace("\t", "") == "" else "-" 176 | changes[j-1].newText = "" 177 | j -= 1 178 | continue 179 | else: 180 | changes[j-1].newText = changes[j-1].newText[len(changes[j].text):] 181 | changes.pop(j) 182 | continue 183 | j += 1 184 | return changes 185 | 186 | def getMinimalChanges(changes, code, cutoff=None): 187 | # Only do the power set if the # of changes is reasonable 188 | if len(changes) < 8: 189 | changesPowerSet = powerSet(changes) 190 | elif len(changes) < 50: 191 | changesPowerSet = fastPowerSet(changes) 192 | else: # too large, we can't even do the fast power set because it will overwhelm memory 193 | changesPowerSet = [changes] 194 | sortList = list(map(lambda x : (x, sum(len(item.text) + len(item.newText) for item in x)), changesPowerSet)) 195 | if cutoff != None: 196 | sortList = list(filter(lambda x : x[1] < cutoff, sortList)) 197 | sortList.sort(key=lambda x : x[1]) 198 | usedChange = combineSameLocationChanges(changes) 199 | usedCode = applyChanges(code, usedChange) 200 | for (change, l) in sortList: 201 | change = combineSameLocationChanges(change) 202 | tmpSource = applyChanges(code, change) 203 | try: 204 | ast.parse(tmpSource) 205 | usedChange = change 206 | usedCode = tmpSource 207 | break 208 | except: 209 | pass 210 | return (usedChange, usedCode) 211 | 212 | def applyChanges(code, changes): 213 | colOffset = 0 214 | for change in changes: 215 | totalCol = change.totalCol 216 | if totalCol < 0: 217 | log("getSyntaxHint\tapplyChanges\tBad totalCol: " + str(changes) + "\n" + code, "bug") 218 | changeType = change.editType 219 | changeText = change.text 220 | updatedTotal = totalCol + colOffset 221 | if changeType in ["-", "deindent"]: 222 | code = code[:updatedTotal] + code[updatedTotal + len(changeText):] 223 | colOffset -= len(changeText) 224 | elif changeType in ["+", "indent"]: 225 | code = code[:updatedTotal] + changeText + code[updatedTotal:] 226 | colOffset += len(changeText) 227 | elif changeType in ["-+", "+-"]: 228 | changeText2 = change.newText 229 | code = code[:updatedTotal] + changeText2 + code[updatedTotal + len(changeText):] 230 | colOffset = colOffset - len(changeText) + len(changeText2) 231 | else: 232 | log("getSyntaxHint\tapplyChanges\tMissing change type: " + str(changeType), filename="bug") 233 | return code 234 | 235 | def generateEditHint(edit): 236 | hint = "" 237 | if edit.editType in ["-", "+"]: 238 | if "\t" in edit.text and edit.text.replace("\t", "") == "": 239 | text = str(len(edit.text)) + " tab" + ("s" if len(edit.text) > 1 else "") 240 | elif " " in edit.text and edit.text.replace(" ", "") == "": 241 | text = str(len(edit.text)) + " space" + ("s" if len(edit.text) > 1 else "") 242 | else: 243 | text = "\"" + edit.text + "\"" 244 | hint += "On line " + str(edit.line) + " in column " + str(edit.col) + ", " + \ 245 | ("delete " if edit.editType == "-" else "add ") + "" + text + "" 246 | elif edit.editType in ["-+", "+-"]: 247 | if "\t" in edit.text and edit.text.replace("\t", "") == "": 248 | oldText = str(len(edit.text)) + " tab" + ("s" if len(edit.text) > 1 else "") 249 | elif " " in edit.text and edit.text.replace(" ", "") == "": 250 | oldText = str(len(edit.text)) + " space" + ("s" if len(edit.text) > 1 else "") 251 | else: 252 | oldText = "\"" + edit.text + "\"" 253 | if "\t" in edit.newText and edit.newText.replace("\t", "") == "": 254 | newText = str(len(edit.newText)) + " tab" + ("s" if len(edit.newText) > 1 else "") 255 | elif " " in edit.text and edit.text.replace(" ", "") == "": 256 | newText = str(len(edit.newText)) + " space" + ("s" if len(edit.newText) > 1 else "") 257 | else: 258 | newText = "\"" + edit.newText + "\"" 259 | hint += "On line " + str(edit.line) + " in column " + str(edit.col) + ", replace " + \ 260 | "" + oldText + " with " + newText + "" 261 | elif edit.editType in ["deindent", "indent"]: 262 | length = len(edit.text) 263 | hint += "On line " + str(edit.line) + " in column " + str(edit.col) + \ 264 | (" unindent " if edit.editType == "deindent" else " indent ") + "the code by " + \ 265 | "" + str(length) + " " + ("space" if edit.text[0] == " " else "tab") + ("s" if length > 1 else "") + "" 266 | return hint 267 | 268 | def generateHintText(hint_level, sourceState, bestChange, bestCode): 269 | if hint_level == "syntax_next_step": 270 | hint = "To help your code parse, make this change: " + generateEditHint(bestChange[0]) 271 | hint += "\nIf you need more help, ask for feedback again." 272 | elif hint_level == "syntax_structure": 273 | # generate structure hint 274 | try: 275 | t = ast.parse(bestCode) 276 | structure = structureTree(t) 277 | hint = "To help your code parse, aim for the following code structure:\n" + printFunction(structure, 0) + "" 278 | except Exception as e: 279 | log("getSyntaxHint\tgenerateHintText\tCouldn't parse: " + str(e), "bug") 280 | hint = "Sorry, an error occurred." 281 | hint += "\nIf you need more help, ask for feedback again." 282 | elif hint_level == "syntax_half_steps": 283 | if len(bestChange)//2 > 1: 284 | numChanges = len(bestChange)//2 285 | else: 286 | numChanges = len(bestChange) 287 | # include half of the edits 288 | hint = "To help your code parse, make the following changes: \n" 289 | for i in range(numChanges): 290 | hint += generateEditHint(bestChange[i]) + "\n" 291 | hint += "\nIf you need more help, ask for feedback again." 292 | elif hint_level == "syntax_solution": 293 | hint = "Here is a new version of your program that should be able to compile: \n" + bestCode + "" 294 | else: 295 | log("getSyntaxHint\tgenerateHintText\tUnrecognized hint level: " + hint_level, "bug") 296 | return hint 297 | 298 | 299 | def getSyntaxHint(source_state, hint_level): 300 | # First, try Aayush's approach 301 | # f = TEST_PATH + "tmp/temporarycode" + str(random.randint(0,100000)) + ".py" 302 | # currentCode = source_state.code 303 | # treeParses = False 304 | # allChanges = [] 305 | # failedString = "" 306 | # while not treeParses and len(allChanges) < 10: 307 | # with open(f, "w") as tmp: 308 | # tmp.write(currentCode) 309 | 310 | # bestChange = getLexerSyntaxHint(f) 311 | # if bestChange == None or len(bestChange) == 0: #treeParses will still be False 312 | # if source_state.score == 1: 313 | # return 314 | # s = "No change found\n" 315 | # s += repr(currentCode) + "\n" 316 | # failedString += s 317 | # break 318 | # else: 319 | # bestCode = applyChanges(currentCode, bestChange) 320 | # s = repr(bestChange) + "\n" + repr(currentCode) + "\n" + repr(bestCode) + "\n" 321 | # try: 322 | # a = ast.parse(bestCode) 323 | # allChanges += bestChange 324 | # currentCode = bestCode 325 | # treeParses = True 326 | # s += "Successful parse!\n" 327 | # failedString += s 328 | # except Exception as e: 329 | # # you can do subtract/deindent multiple times, 330 | # # but if you're changing/adding in the same location multiple times, 331 | # # while interchanging with something else, we have an infinite loop 332 | # if len(bestChange) == 1 and bestChange[0].editType not in ["-", "deindent"] and \ 333 | # bestChange[0] in allChanges and bestChange[0] != allChanges[-1]: 334 | # s += "Infinite loop!\n" 335 | # failedString += s 336 | # break 337 | # else: 338 | # s += "Failed parse: " + str(e) + "\n" 339 | # failedString += s 340 | # allChanges += bestChange 341 | # currentCode = bestCode 342 | 343 | # if os.path.exists(f): 344 | # os.remove(f) 345 | treeParses = False 346 | if treeParses: 347 | bestChange = combineSmartSyntaxEdits(allChanges) 348 | bestCode = currentCode 349 | else: 350 | # If that fails, do the basic path construction approach 351 | allSources = SourceState.objects.filter(problem=source_state.problem).exclude(tree_source="") 352 | codes = list(set([state.code for state in allSources])) 353 | allChanges = [] 354 | bestChange = None 355 | bestCode = None 356 | bestLength = None 357 | for state in codes: 358 | changes = getTextDiff(source_state.code, state) 359 | # Now generate all possible combinations of these changes 360 | (usedChange, usedCode) = getMinimalChanges(changes, source_state.code, cutoff=bestLength) 361 | l = sum(len(usedChange[i].text) + len(usedChange[i].newText) for i in range(len(usedChange))) 362 | if bestLength == None or l < bestLength: 363 | bestChange, bestCode, bestLength = usedChange, usedCode, l 364 | if bestLength == 1: 365 | break 366 | # Only apply one change at a time 367 | if bestChange == None: # no correct states available 368 | log("syntaxHint\tgetSyntaxHint\tNo parsing states in " + source_state.problem.name, "bug") 369 | hint = Hint(message="No parsing states found for this problem", level="syntax error") 370 | hint.save() 371 | source_state.hint = hint 372 | source_state.save() 373 | return source_state 374 | 375 | 376 | # Determine the hint level 377 | if hint_level == "syntax_default": 378 | submissions = list(SourceState.objects.filter(student=source_state.student)) 379 | if len(submissions) > 0 and submissions[-1].hint != None and submissions[-1].problem == source_state.problem and \ 380 | submissions[-1].code == source_state.code: 381 | prev_hint_level = submissions[-1].hint.level 382 | if "syntax" in prev_hint_level: 383 | if prev_hint_level == "syntax_next_step": 384 | hint_level = "syntax_structure" 385 | elif prev_hint_level == "syntax_structure": 386 | hint_level = "syntax_half_steps" 387 | elif prev_hint_level in ["syntax_half_steps", "syntax_solution"]: 388 | hint_level = "syntax_solution" 389 | else: 390 | hint_level = "syntax_next_step" 391 | else: 392 | hint_level = "syntax_next_step" 393 | else: 394 | hint_level = "syntax_next_step" 395 | 396 | message = generateHintText(hint_level, source_state, bestChange, bestCode) 397 | firstEdit = bestChange[0] 398 | hint = Hint(message=message, level=hint_level, line=firstEdit.line, col=firstEdit.col) 399 | hint.save() 400 | source_state.edit = bestChange 401 | source_state.hint = hint 402 | source_state.save() 403 | return source_state 404 | -------------------------------------------------------------------------------- /hintgen/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-08 17:52 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | initial = True 12 | 13 | dependencies = [ 14 | ] 15 | 16 | operations = [ 17 | migrations.CreateModel( 18 | name='Course', 19 | fields=[ 20 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 21 | ('name', models.CharField(max_length=100)), 22 | ('semester', models.CharField(max_length=50)), 23 | ('year', models.IntegerField()), 24 | ], 25 | ), 26 | migrations.CreateModel( 27 | name='Problem', 28 | fields=[ 29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('name', models.CharField(max_length=50)), 31 | ('solution', models.TextField()), 32 | ('arguments', models.TextField()), 33 | ('given_code', models.TextField()), 34 | ('course_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='hintgen.Course')), 35 | ], 36 | ), 37 | migrations.CreateModel( 38 | name='State', 39 | fields=[ 40 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 41 | ('code', models.TextField()), 42 | ('score', models.FloatField(default=-1)), 43 | ('count', models.IntegerField(default=0)), 44 | ('feedback', models.TextField()), 45 | ('tree', models.TextField()), 46 | ('treeWeight', models.IntegerField(default=-1)), 47 | ], 48 | ), 49 | migrations.CreateModel( 50 | name='Student', 51 | fields=[ 52 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 53 | ('condition', models.CharField(default='control', max_length=50)), 54 | ('course_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='hintgen.Course')), 55 | ], 56 | ), 57 | migrations.CreateModel( 58 | name='Test', 59 | fields=[ 60 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 61 | ('test_input', models.TextField()), 62 | ('test_output', models.TextField()), 63 | ('problem_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='hintgen.Problem')), 64 | ], 65 | ), 66 | migrations.CreateModel( 67 | name='AnonState', 68 | fields=[ 69 | ('state_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='hintgen.State')), 70 | ('variable_map', models.TextField()), 71 | ], 72 | bases=('hintgen.state',), 73 | ), 74 | migrations.CreateModel( 75 | name='CanonicalState', 76 | fields=[ 77 | ('state_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='hintgen.State')), 78 | ], 79 | bases=('hintgen.state',), 80 | ), 81 | migrations.CreateModel( 82 | name='CleanedState', 83 | fields=[ 84 | ('state_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='hintgen.State')), 85 | ('anon_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='hintgen.AnonState')), 86 | ], 87 | bases=('hintgen.state',), 88 | ), 89 | migrations.CreateModel( 90 | name='SourceState', 91 | fields=[ 92 | ('state_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='hintgen.State')), 93 | ('timestamp', models.DateTimeField(auto_now_add=True)), 94 | ('hint', models.TextField()), 95 | ('hint_level', models.IntegerField(default=1)), 96 | ('cleaned_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='hintgen.CleanedState')), 97 | ('student_state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='hintgen.Student')), 98 | ], 99 | bases=('hintgen.state',), 100 | ), 101 | migrations.AddField( 102 | model_name='state', 103 | name='problem_id', 104 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='hintgen.Problem'), 105 | ), 106 | migrations.AddField( 107 | model_name='canonicalstate', 108 | name='goal_state', 109 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='canonical_feeder_state', to='hintgen.State'), 110 | ), 111 | migrations.AddField( 112 | model_name='canonicalstate', 113 | name='next_state', 114 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='canonical_prev_state', to='hintgen.State'), 115 | ), 116 | migrations.AddField( 117 | model_name='anonstate', 118 | name='canonical_state', 119 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', to='hintgen.CanonicalState'), 120 | ), 121 | migrations.AddField( 122 | model_name='anonstate', 123 | name='goal_state', 124 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='anon_feeder_state', to='hintgen.State'), 125 | ), 126 | migrations.AddField( 127 | model_name='anonstate', 128 | name='next_state', 129 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='anon_prev_state', to='hintgen.State'), 130 | ), 131 | ] 132 | -------------------------------------------------------------------------------- /hintgen/migrations/0002_auto_20170113_1447.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-13 19:47 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='anonstate', 17 | name='variable_map', 18 | field=models.TextField(default=''), 19 | ), 20 | migrations.AlterField( 21 | model_name='course', 22 | name='semester', 23 | field=models.CharField(default='', max_length=50), 24 | ), 25 | migrations.AlterField( 26 | model_name='problem', 27 | name='given_code', 28 | field=models.TextField(default=''), 29 | ), 30 | migrations.AlterField( 31 | model_name='sourcestate', 32 | name='hint', 33 | field=models.TextField(default=''), 34 | ), 35 | migrations.AlterField( 36 | model_name='state', 37 | name='feedback', 38 | field=models.TextField(default=''), 39 | ), 40 | migrations.AlterField( 41 | model_name='state', 42 | name='tree', 43 | field=models.TextField(default=''), 44 | ), 45 | ] 46 | -------------------------------------------------------------------------------- /hintgen/migrations/0003_auto_20170113_1452.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-13 19:52 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0002_auto_20170113_1447'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='anonstate', 17 | name='variable_map', 18 | field=models.TextField(blank=True), 19 | ), 20 | migrations.AlterField( 21 | model_name='course', 22 | name='semester', 23 | field=models.CharField(blank=True, max_length=50), 24 | ), 25 | migrations.AlterField( 26 | model_name='problem', 27 | name='given_code', 28 | field=models.TextField(blank=True), 29 | ), 30 | migrations.AlterField( 31 | model_name='sourcestate', 32 | name='hint', 33 | field=models.TextField(blank=True), 34 | ), 35 | migrations.AlterField( 36 | model_name='state', 37 | name='feedback', 38 | field=models.TextField(blank=True), 39 | ), 40 | migrations.AlterField( 41 | model_name='state', 42 | name='tree', 43 | field=models.TextField(blank=True), 44 | ), 45 | migrations.AlterField( 46 | model_name='student', 47 | name='condition', 48 | field=models.CharField(blank=True, max_length=50), 49 | ), 50 | ] 51 | -------------------------------------------------------------------------------- /hintgen/migrations/0004_auto_20170113_1625.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-13 21:25 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0003_auto_20170113_1452'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RemoveField( 16 | model_name='problem', 17 | name='course_id', 18 | ), 19 | migrations.AddField( 20 | model_name='problem', 21 | name='courses', 22 | field=models.ManyToManyField(related_name='courses', to='hintgen.Course'), 23 | ), 24 | ] 25 | -------------------------------------------------------------------------------- /hintgen/migrations/0005_auto_20170113_1629.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-13 21:29 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0004_auto_20170113_1625'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='problem', 18 | name='courses', 19 | field=models.ManyToManyField(related_name='problems', to='hintgen.Course'), 20 | ), 21 | migrations.AlterField( 22 | model_name='state', 23 | name='problem_id', 24 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='states', to='hintgen.Problem'), 25 | ), 26 | migrations.AlterField( 27 | model_name='student', 28 | name='course_id', 29 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='students', to='hintgen.Course'), 30 | ), 31 | migrations.AlterField( 32 | model_name='test', 33 | name='problem_id', 34 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='tests', to='hintgen.Problem'), 35 | ), 36 | ] 37 | -------------------------------------------------------------------------------- /hintgen/migrations/0006_auto_20170114_1328.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-14 18:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0005_auto_20170113_1629'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='state', 17 | old_name='problem_id', 18 | new_name='problem', 19 | ), 20 | migrations.RenameField( 21 | model_name='student', 22 | old_name='course_id', 23 | new_name='course', 24 | ), 25 | migrations.RenameField( 26 | model_name='test', 27 | old_name='problem_id', 28 | new_name='problem', 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /hintgen/migrations/0007_auto_20170114_1837.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-14 23:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0006_auto_20170114_1328'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RemoveField( 17 | model_name='anonstate', 18 | name='goal_state', 19 | ), 20 | migrations.RemoveField( 21 | model_name='anonstate', 22 | name='next_state', 23 | ), 24 | migrations.RemoveField( 25 | model_name='canonicalstate', 26 | name='goal_state', 27 | ), 28 | migrations.RemoveField( 29 | model_name='canonicalstate', 30 | name='next_state', 31 | ), 32 | migrations.RemoveField( 33 | model_name='cleanedstate', 34 | name='anon_state', 35 | ), 36 | migrations.RemoveField( 37 | model_name='sourcestate', 38 | name='cleaned_state', 39 | ), 40 | migrations.RemoveField( 41 | model_name='sourcestate', 42 | name='student_state', 43 | ), 44 | migrations.AddField( 45 | model_name='anonstate', 46 | name='goal', 47 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='anon_feeder', to='hintgen.State'), 48 | ), 49 | migrations.AddField( 50 | model_name='anonstate', 51 | name='nextstep', 52 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='anon_prev', to='hintgen.State'), 53 | ), 54 | migrations.AddField( 55 | model_name='canonicalstate', 56 | name='goal', 57 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='canonical_feeder', to='hintgen.State'), 58 | ), 59 | migrations.AddField( 60 | model_name='canonicalstate', 61 | name='nextstep', 62 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='canonical_prev', to='hintgen.State'), 63 | ), 64 | migrations.AddField( 65 | model_name='cleanedstate', 66 | name='anon', 67 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cleaned_states', to='hintgen.AnonState'), 68 | ), 69 | migrations.AddField( 70 | model_name='sourcestate', 71 | name='cleaned', 72 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='source_states', to='hintgen.CleanedState'), 73 | ), 74 | migrations.AddField( 75 | model_name='sourcestate', 76 | name='student', 77 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='code_states', to='hintgen.Student'), 78 | ), 79 | migrations.AlterField( 80 | model_name='anonstate', 81 | name='canonical_state', 82 | field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='anon_states', to='hintgen.CanonicalState'), 83 | ), 84 | ] 85 | -------------------------------------------------------------------------------- /hintgen/migrations/0008_auto_20170114_1837.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-14 23:37 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0007_auto_20170114_1837'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='sourcestate', 18 | name='student', 19 | field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='code_states', to='hintgen.Student'), 20 | ), 21 | ] 22 | -------------------------------------------------------------------------------- /hintgen/migrations/0009_auto_20170114_1839.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-14 23:39 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0008_auto_20170114_1837'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='sourcestate', 17 | name='hint_level', 18 | field=models.IntegerField(null=True), 19 | ), 20 | migrations.AlterField( 21 | model_name='state', 22 | name='score', 23 | field=models.FloatField(null=True), 24 | ), 25 | migrations.AlterField( 26 | model_name='state', 27 | name='treeWeight', 28 | field=models.IntegerField(null=True), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /hintgen/migrations/0010_auto_20170114_1848.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-14 23:48 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0009_auto_20170114_1839'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='anonstate', 18 | name='canonical_state', 19 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='anon_states', to='hintgen.CanonicalState'), 20 | ), 21 | migrations.AlterField( 22 | model_name='anonstate', 23 | name='goal', 24 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='anon_feeder', to='hintgen.State'), 25 | ), 26 | migrations.AlterField( 27 | model_name='anonstate', 28 | name='nextstep', 29 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='anon_prev', to='hintgen.State'), 30 | ), 31 | migrations.AlterField( 32 | model_name='canonicalstate', 33 | name='goal', 34 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='canonical_feeder', to='hintgen.State'), 35 | ), 36 | migrations.AlterField( 37 | model_name='canonicalstate', 38 | name='nextstep', 39 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='canonical_prev', to='hintgen.State'), 40 | ), 41 | migrations.AlterField( 42 | model_name='cleanedstate', 43 | name='anon', 44 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='cleaned_states', to='hintgen.AnonState'), 45 | ), 46 | migrations.AlterField( 47 | model_name='sourcestate', 48 | name='cleaned', 49 | field=models.ForeignKey(blank=True, on_delete=django.db.models.deletion.CASCADE, related_name='source_states', to='hintgen.CleanedState'), 50 | ), 51 | migrations.AlterField( 52 | model_name='sourcestate', 53 | name='hint_level', 54 | field=models.IntegerField(blank=True), 55 | ), 56 | migrations.AlterField( 57 | model_name='state', 58 | name='score', 59 | field=models.FloatField(blank=True), 60 | ), 61 | migrations.AlterField( 62 | model_name='state', 63 | name='treeWeight', 64 | field=models.IntegerField(blank=True), 65 | ), 66 | ] 67 | -------------------------------------------------------------------------------- /hintgen/migrations/0011_auto_20170114_1849.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-14 23:49 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0010_auto_20170114_1848'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='sourcestate', 17 | name='hint_level', 18 | field=models.IntegerField(blank=True, null=True), 19 | ), 20 | migrations.AlterField( 21 | model_name='state', 22 | name='score', 23 | field=models.FloatField(blank=True, null=True), 24 | ), 25 | migrations.AlterField( 26 | model_name='state', 27 | name='treeWeight', 28 | field=models.IntegerField(blank=True, null=True), 29 | ), 30 | ] 31 | -------------------------------------------------------------------------------- /hintgen/migrations/0012_auto_20170114_1850.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-14 23:50 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0011_auto_20170114_1849'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='anonstate', 18 | name='canonical_state', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='anon_states', to='hintgen.CanonicalState'), 20 | ), 21 | migrations.AlterField( 22 | model_name='anonstate', 23 | name='goal', 24 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='anon_feeder', to='hintgen.State'), 25 | ), 26 | migrations.AlterField( 27 | model_name='anonstate', 28 | name='nextstep', 29 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='anon_prev', to='hintgen.State'), 30 | ), 31 | migrations.AlterField( 32 | model_name='canonicalstate', 33 | name='goal', 34 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='canonical_feeder', to='hintgen.State'), 35 | ), 36 | migrations.AlterField( 37 | model_name='canonicalstate', 38 | name='nextstep', 39 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='canonical_prev', to='hintgen.State'), 40 | ), 41 | migrations.AlterField( 42 | model_name='cleanedstate', 43 | name='anon', 44 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='cleaned_states', to='hintgen.AnonState'), 45 | ), 46 | migrations.AlterField( 47 | model_name='sourcestate', 48 | name='cleaned', 49 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='source_states', to='hintgen.CleanedState'), 50 | ), 51 | ] 52 | -------------------------------------------------------------------------------- /hintgen/migrations/0013_auto_20170114_1854.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-14 23:54 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0012_auto_20170114_1850'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='student', 17 | name='name', 18 | field=models.CharField(blank=True, max_length=100), 19 | ), 20 | migrations.AlterField( 21 | model_name='problem', 22 | name='arguments', 23 | field=models.CharField(max_length=500), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /hintgen/migrations/0014_auto_20170114_1919.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-15 00:19 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0013_auto_20170114_1854'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='anonstate', 18 | name='canonical_state', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='anon_states', to='hintgen.CanonicalState'), 20 | ), 21 | migrations.AlterField( 22 | model_name='anonstate', 23 | name='goal', 24 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='anon_feeder', to='hintgen.State'), 25 | ), 26 | migrations.AlterField( 27 | model_name='anonstate', 28 | name='nextstep', 29 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='anon_prev', to='hintgen.State'), 30 | ), 31 | migrations.AlterField( 32 | model_name='canonicalstate', 33 | name='goal', 34 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='canonical_feeder', to='hintgen.State'), 35 | ), 36 | migrations.AlterField( 37 | model_name='canonicalstate', 38 | name='nextstep', 39 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='canonical_prev', to='hintgen.State'), 40 | ), 41 | migrations.AlterField( 42 | model_name='cleanedstate', 43 | name='anon', 44 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cleaned_states', to='hintgen.AnonState'), 45 | ), 46 | migrations.AlterField( 47 | model_name='problem', 48 | name='solution', 49 | field=models.TextField(blank=True), 50 | ), 51 | migrations.AlterField( 52 | model_name='sourcestate', 53 | name='cleaned', 54 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='source_states', to='hintgen.CleanedState'), 55 | ), 56 | ] 57 | -------------------------------------------------------------------------------- /hintgen/migrations/0015_auto_20170114_1928.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-15 00:28 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0014_auto_20170114_1919'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RemoveField( 17 | model_name='problem', 18 | name='solution', 19 | ), 20 | migrations.AddField( 21 | model_name='problem', 22 | name='teacher_solution', 23 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='hintgen.SourceState'), 24 | ), 25 | ] 26 | -------------------------------------------------------------------------------- /hintgen/migrations/0016_auto_20170114_1929.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-15 00:29 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0015_auto_20170114_1928'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='problem', 17 | old_name='teacher_solution', 18 | new_name='solution', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /hintgen/migrations/0017_auto_20170114_2016.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-15 01:16 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0016_auto_20170114_1929'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='state', 17 | old_name='tree', 18 | new_name='tree_source', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /hintgen/migrations/0018_test_test_extra.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-15 17:59 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0017_auto_20170114_2016'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='test', 17 | name='test_extra', 18 | field=models.TextField(blank=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /hintgen/migrations/0019_canonicalstate_orig_tree_source.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-16 21:12 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0018_test_test_extra'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AddField( 16 | model_name='canonicalstate', 17 | name='orig_tree_source', 18 | field=models.TextField(blank=True), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /hintgen/migrations/0020_auto_20170117_1246.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-17 17:46 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0019_canonicalstate_orig_tree_source'), 13 | ] 14 | 15 | operations = [ 16 | migrations.CreateModel( 17 | name='Hint', 18 | fields=[ 19 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 20 | ('message', models.TextField(blank=True)), 21 | ('level', models.CharField(blank=True, max_length=50, null=True)), 22 | ('line', models.IntegerField(blank=True, null=True)), 23 | ('col', models.IntegerField(blank=True, null=True)), 24 | ], 25 | ), 26 | migrations.RenameField( 27 | model_name='anonstate', 28 | old_name='nextstep', 29 | new_name='next', 30 | ), 31 | migrations.RenameField( 32 | model_name='canonicalstate', 33 | old_name='nextstep', 34 | new_name='next', 35 | ), 36 | migrations.RemoveField( 37 | model_name='anonstate', 38 | name='variable_map', 39 | ), 40 | migrations.RemoveField( 41 | model_name='sourcestate', 42 | name='hint', 43 | ), 44 | migrations.RemoveField( 45 | model_name='sourcestate', 46 | name='hint_level', 47 | ), 48 | migrations.AddField( 49 | model_name='anonstate', 50 | name='orig_tree_source', 51 | field=models.TextField(blank=True), 52 | ), 53 | migrations.AddField( 54 | model_name='sourcestate', 55 | name='hintmsg', 56 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='code_state', to='hintgen.Hint'), 57 | ), 58 | ] 59 | -------------------------------------------------------------------------------- /hintgen/migrations/0021_auto_20170117_1257.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-17 17:57 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0020_auto_20170117_1246'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='sourcestate', 17 | old_name='hintmsg', 18 | new_name='hint', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /hintgen/migrations/0022_auto_20170117_1717.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-17 22:17 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0021_auto_20170117_1257'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameField( 16 | model_name='anonstate', 17 | old_name='canonical_state', 18 | new_name='canonical', 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /hintgen/migrations/0023_auto_20170117_1924.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-18 00:24 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0022_auto_20170117_1717'), 12 | ] 13 | 14 | operations = [ 15 | migrations.RenameModel( 16 | old_name='Test', 17 | new_name='Testcase', 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /hintgen/migrations/0024_auto_20170126_1442.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-26 19:42 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0023_auto_20170117_1924'), 13 | ] 14 | 15 | operations = [ 16 | migrations.AlterField( 17 | model_name='sourcestate', 18 | name='hint', 19 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='code_state', to='hintgen.Hint'), 20 | ), 21 | migrations.AlterField( 22 | model_name='sourcestate', 23 | name='student', 24 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='code_states', to='hintgen.Student'), 25 | ), 26 | ] 27 | -------------------------------------------------------------------------------- /hintgen/migrations/0025_auto_20170126_1453.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-26 19:53 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0024_auto_20170126_1442'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='problem', 17 | options={'ordering': ['name']}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /hintgen/migrations/0026_auto_20170126_1455.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-26 19:55 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0025_auto_20170126_1453'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='course', 17 | options={'ordering': ['-year', '-semester', 'name']}, 18 | ), 19 | migrations.AlterModelOptions( 20 | name='hint', 21 | options={'ordering': ['level', 'id']}, 22 | ), 23 | migrations.AlterModelOptions( 24 | name='state', 25 | options={'ordering': ['problem', 'id']}, 26 | ), 27 | migrations.AlterModelOptions( 28 | name='testcase', 29 | options={'ordering': ['problem']}, 30 | ), 31 | ] 32 | -------------------------------------------------------------------------------- /hintgen/migrations/0027_auto_20170126_1457.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-26 19:57 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0026_auto_20170126_1455'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='hint', 17 | options={'ordering': ['level', '-id']}, 18 | ), 19 | migrations.AlterModelOptions( 20 | name='testcase', 21 | options={'ordering': ['problem', '-id']}, 22 | ), 23 | ] 24 | -------------------------------------------------------------------------------- /hintgen/migrations/0028_auto_20170126_1458.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-26 19:58 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('hintgen', '0027_auto_20170126_1457'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterModelOptions( 16 | name='testcase', 17 | options={'ordering': ['problem', 'id']}, 18 | ), 19 | ] 20 | -------------------------------------------------------------------------------- /hintgen/migrations/0029_auto_20170127_1722.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10 on 2017-01-27 22:22 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | import django.db.models.deletion 7 | 8 | 9 | class Migration(migrations.Migration): 10 | 11 | dependencies = [ 12 | ('hintgen', '0028_auto_20170126_1458'), 13 | ] 14 | 15 | operations = [ 16 | migrations.RemoveField( 17 | model_name='anonstate', 18 | name='goal', 19 | ), 20 | migrations.RemoveField( 21 | model_name='anonstate', 22 | name='next', 23 | ), 24 | migrations.RemoveField( 25 | model_name='canonicalstate', 26 | name='goal', 27 | ), 28 | migrations.RemoveField( 29 | model_name='canonicalstate', 30 | name='next', 31 | ), 32 | migrations.AddField( 33 | model_name='state', 34 | name='goal', 35 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='feeder', to='hintgen.State'), 36 | ), 37 | migrations.AddField( 38 | model_name='state', 39 | name='next', 40 | field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='prev', to='hintgen.State'), 41 | ), 42 | ] 43 | -------------------------------------------------------------------------------- /hintgen/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/krivers/ITAP-django/dd6af07b02897f5b11305a1888a0b8df1f0f3d8e/hintgen/migrations/__init__.py -------------------------------------------------------------------------------- /hintgen/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Course(models.Model): 4 | name = models.CharField(max_length=100) 5 | semester = models.CharField(max_length=50, blank=True) 6 | year = models.IntegerField() 7 | def __str__(self): 8 | return str(self.year) + " " + self.semester + ": " + self.name 9 | 10 | class Meta: 11 | ordering = ['-year', '-semester', 'name'] 12 | 13 | class Student(models.Model): 14 | course = models.ForeignKey('Course', on_delete=models.CASCADE, related_name="students") 15 | name = models.CharField(max_length=100, blank=True) 16 | condition = models.CharField(max_length=50, blank=True) 17 | def __str__(self): 18 | if self.name != "": 19 | return self.name 20 | return "Student " + str(self.id) 21 | 22 | class Problem(models.Model): 23 | courses = models.ManyToManyField(Course, related_name="problems") 24 | name = models.CharField(max_length=50) 25 | solution = models.ForeignKey('SourceState', on_delete=models.SET_NULL, related_name="+", blank=True, null=True) 26 | arguments = models.CharField(max_length=500) # should be interpreted by pickle 27 | given_code = models.TextField(blank=True) # should be interpreted by pickle 28 | def __str__(self): 29 | return self.name 30 | 31 | class Meta: 32 | ordering = ['name'] 33 | 34 | class Testcase(models.Model): 35 | problem = models.ForeignKey('Problem', on_delete=models.CASCADE, related_name="tests") 36 | test_input = models.TextField() 37 | test_output = models.TextField() 38 | test_extra = models.TextField(blank=True) # specific keywords specify extra tests. For example, 'checkCopy' checks if the input is modified 39 | def __str__(self): 40 | return "Test " + str(self.id) + " for " + str(self.problem) 41 | 42 | class Meta: 43 | ordering = ['problem', 'id'] 44 | 45 | class State(models.Model): 46 | code = models.TextField() 47 | problem = models.ForeignKey('Problem', on_delete=models.CASCADE, related_name="states") 48 | score = models.FloatField(blank=True, null=True) 49 | count = models.IntegerField(default=0) 50 | feedback = models.TextField(blank=True) 51 | tree_source = models.TextField(blank=True) # should be interpreted by pickle 52 | treeWeight = models.IntegerField(blank=True, null=True) 53 | next = models.ForeignKey('State', on_delete=models.SET_NULL, related_name="prev", blank=True, null=True) 54 | goal = models.ForeignKey('State', on_delete=models.SET_NULL, related_name="feeder", blank=True, null=True) 55 | def __str__(self): 56 | return str(self.problem) + " State " + str(self.id) 57 | 58 | class Meta: 59 | ordering = ['problem', 'id'] 60 | 61 | class SourceState(State): 62 | timestamp = models.DateTimeField(auto_now_add=True) 63 | student = models.ForeignKey('Student', on_delete=models.SET_NULL, related_name="code_states", blank=True, null=True) 64 | cleaned = models.ForeignKey('CleanedState', on_delete=models.SET_NULL, related_name="source_states", blank=True, null=True) 65 | hint = models.ForeignKey('Hint', on_delete=models.SET_NULL, related_name="code_state", blank=True, null=True) 66 | 67 | class CleanedState(State): 68 | anon = models.ForeignKey('AnonState', on_delete=models.SET_NULL, related_name="cleaned_states", blank=True, null=True) 69 | 70 | class AnonState(State): 71 | canonical = models.ForeignKey('CanonicalState', on_delete=models.SET_NULL, related_name="anon_states", blank=True, null=True) 72 | orig_tree_source = models.TextField(blank=True) 73 | 74 | class CanonicalState(State): 75 | orig_tree_source = models.TextField(blank=True) 76 | 77 | class Hint(models.Model): 78 | message = models.TextField(blank=True) 79 | level = models.CharField(max_length=50, blank=True, null=True) 80 | line = models.IntegerField(blank=True, null=True) 81 | col = models.IntegerField(blank=True, null=True) 82 | 83 | def __str__(self): 84 | return str(self.level) + " " + str(self.id) 85 | 86 | class Meta: 87 | ordering = ['level', '-id'] 88 | -------------------------------------------------------------------------------- /hintgen/namesets.py: -------------------------------------------------------------------------------- 1 | import ast, collections 2 | 3 | supportedLibraries = [ "string", "math", "random", "__future__", "copy" ] 4 | 5 | builtInTypes = [ bool, bytes, complex, dict, float, int, list, 6 | set, str, tuple, type ] 7 | 8 | staticTypeCastBuiltInFunctions = { 9 | "bool" : { (object,) : bool }, 10 | "bytes" : bytes, 11 | "complex" : { (str, int) : complex, (str, float) : complex, (int, int) : complex, 12 | (int, float) : complex, (float, int) : complex, (float, float) : complex }, 13 | "dict" : { (collections.Iterable,) : dict }, 14 | "enumerate" : { (collections.Iterable,) : enumerate }, 15 | "float" : { (str,) : float, (int,) : float, (float,) : float }, 16 | "frozenset" : frozenset, 17 | "int" : { (str,) : int, (float,) : int, (int,) : int, (str, int) : int }, 18 | "list" : { (collections.Iterable,) : list }, 19 | "memoryview" : None, #TODO 20 | "object" : { () : object }, 21 | "property" : property, #TODO 22 | "reversed" : { (str,) : reversed, (list,) : reversed }, 23 | "set" : { () : None, (collections.Iterable,) : None }, #TODO 24 | "slice" : { (int,) : slice, (int, int) : slice, (int, int, int) : slice }, 25 | "str" : { (object,) : str }, 26 | "tuple" : { () : tuple, (collections.Iterable,) : tuple }, 27 | "type" : { (object,) : type }, 28 | } 29 | 30 | mutatingTypeCastBuiltInFunctions = { 31 | "bytearray" : { () : list }, 32 | "classmethod" : None, 33 | "file" : None, 34 | "staticmethod" : None, #TOOD 35 | "super" : None 36 | } 37 | 38 | builtInNames = [ "None", "True", "False", "NotImplemented", "Ellipsis" ] 39 | 40 | staticBuiltInFunctions = { 41 | "abs" : { (int,) : int, (float,) : float }, 42 | "all" : { (collections.Iterable,) : bool }, 43 | "any" : { (collections.Iterable,) : bool }, 44 | "bin" : { (int,) : str }, 45 | "callable" : { (object,) : bool }, 46 | "chr" : { (int,) : str }, 47 | "cmp" : { (object, object) : int }, 48 | "coerce" : tuple, #TODO 49 | "compile" : { (str, str, str) : ast, (ast, str, str) : ast }, 50 | "dir" : { () : list }, 51 | "divmod" : { (int, int) : tuple, (int, float) : tuple, 52 | (float, int) : tuple, (float, float) : tuple }, 53 | "filter" : { (type(lambda x : x), collections.Iterable) : list }, 54 | "getattr" : None, 55 | "globals" : dict, 56 | "hasattr" : bool, #TODO 57 | "hash" : int, 58 | "hex" : str, 59 | "id" : int, #TODO 60 | "isinstance" : { (None, None) : bool }, 61 | "issubclass" : bool, #TODO 62 | "iter" : { (collections.Iterable,) : None, (None, object) : None }, 63 | "len" : { (str,) : int, (tuple,) : int, (list,) : int, (dict,) : int }, 64 | "locals" : dict, 65 | "map" : { (None, collections.Iterable) : list }, #TODO 66 | "max" : { (collections.Iterable,) : None }, 67 | "min" : { (collections.Iterable,) : None }, 68 | "oct" : { (int,) : str }, 69 | "ord" : { (str,) : int }, 70 | "pow" : { (int, int) : int, (int, float) : float, 71 | (float, int) : float, (float, float) : float }, 72 | "print" : None, 73 | "range" : { (int,) : list, (int, int) : list, (int, int, int) : list }, 74 | "repr" : {(object,) : str }, 75 | "round" : { (int,) : float, (float,) : float, (int, int) : float, (float, int) : float }, 76 | "sorted" : { (collections.Iterable,) : list }, 77 | "sum" : { (collections.Iterable,) : None }, 78 | "vars" : dict, #TODO 79 | "zip" : { () : list, (collections.Iterable,) : list} 80 | } 81 | 82 | mutatingBuiltInFunctions = { 83 | "__import__" : None, 84 | "apply" : None, 85 | "delattr" : { (object, str) : None }, 86 | "eval" : { (str,) : None }, 87 | "execfile" : None, 88 | "format" : None, 89 | "input" : { () : None, (object,) : None }, 90 | "intern" : str, #TOOD 91 | "next" : { () : None, (None,) : None }, 92 | "open" : None, 93 | "raw_input" : { () : str, (object,) : str }, 94 | "reduce" : None, 95 | "reload" : None, 96 | "setattr" : None 97 | } 98 | 99 | builtInSafeFunctions = [ 100 | "abs", "all", "any", "bin", "bool", "cmp", "len", 101 | "list", "max", "min", "pow", "repr", "round", "slice", "str", "type" 102 | ] 103 | 104 | 105 | exceptionClasses = [ 106 | "ArithmeticError", 107 | "AssertionError", 108 | "AttributeError", 109 | "BaseException", 110 | "BufferError", 111 | "BytesWarning", 112 | "DeprecationWarning", 113 | "EOFError", 114 | "EnvironmentError", 115 | "Exception", 116 | "FloatingPointError", 117 | "FutureWarning", 118 | "GeneratorExit", 119 | "IOError", 120 | "ImportError", 121 | "ImportWarning", 122 | "IndentationError", 123 | "IndexError", 124 | "KeyError", 125 | "KeyboardInterrupt", 126 | "LookupError", 127 | "MemoryError", 128 | "NameError", 129 | "NotImplementedError", 130 | "OSError", 131 | "OverflowError", 132 | "PendingDeprecationWarning", 133 | "ReferenceError", 134 | "RuntimeError", 135 | "RuntimeWarning", 136 | "StandardError", 137 | "StopIteration", 138 | "SyntaxError", 139 | "SyntaxWarning", 140 | "SystemError", 141 | "SystemExit", 142 | "TabError", 143 | "TypeError", 144 | "UnboundLocalError", 145 | "UnicodeDecodeError", 146 | "UnicodeEncodeError", 147 | "UnicodeError", 148 | "UnicodeTranslateError", 149 | "UnicodeWarning", 150 | "UserWarning", 151 | "ValueError", 152 | "Warning", 153 | "ZeroDivisionError", 154 | 155 | "WindowsError", "BlockingIOError", "ChildProcessError", 156 | "ConnectionError", "BrokenPipeError", "ConnectionAbortedError", 157 | "ConnectionRefusedError", "ConnectionResetError", "FileExistsError", 158 | "FileNotFoundError", "InterruptedError", "IsADirectoryError", "NotADirectoryError", 159 | "PermissionError", "ProcessLookupError", "TimeoutError", 160 | "ResourceWarning", "RecursionError", "StopAsyncIteration" ] 161 | 162 | builtInFunctions = dict(list(staticBuiltInFunctions.items()) + \ 163 | list(mutatingBuiltInFunctions.items()) + \ 164 | list(staticTypeCastBuiltInFunctions.items()) + \ 165 | list(mutatingTypeCastBuiltInFunctions.items())) 166 | 167 | # All string functions do not mutate the caller, they return copies instead 168 | builtInStringFunctions = { 169 | "capitalize" : { () : str }, 170 | "center" : { (int,) : str, (int, str) : str }, 171 | "count" : { (str,) : int, (str, int) : int, (str, int, int) : int }, 172 | "decode" : { () : str }, 173 | "encode" : { () : str }, 174 | "endswith" : { (str,) : bool }, 175 | "expandtabs" : { () : str, (int,) : str }, 176 | "find" : { (str,) : int, (str, int) : int, (str, int, int) : int }, 177 | "format" : { (list, list) : str }, 178 | "index" : { (str,) : int, (str,int) : int, (str,int,int) : int }, 179 | "isalnum" : { () : bool }, 180 | "isalpha" : { () : bool }, 181 | "isdecimal" : { () : bool }, 182 | "isdigit" : { () : bool }, 183 | "islower" : { () : bool }, 184 | "isnumeric" : { () : bool }, 185 | "isspace" : { () : bool }, 186 | "istitle" : { () : bool }, 187 | "isupper" : { () : bool }, 188 | "join" : { (collections.Iterable,) : str, (collections.Iterable,str) : str }, 189 | "ljust" : { (int,) : str }, 190 | "lower" : { () : str }, 191 | "lstrip" : { () : str, (str,) : str }, 192 | "partition" : { (str,) : tuple }, 193 | "replace" : { (str, str) : str, (str, str, int) : str }, 194 | "rfind" : { (str,) : int, (str,int) : int, (str,int,int) : int }, 195 | "rindex" : { (str,) : int }, 196 | "rjust" : { (int,) : str }, 197 | "rpartition" : { (str,) : tuple }, 198 | "rsplit" : { () : list }, 199 | "rstrip" : { () : str }, 200 | "split" : { () : list, (str,) : list, (str, int) : list }, 201 | "splitlines" : { () : list }, 202 | "startswith" : { (str,) : bool }, 203 | "strip" : { () : str, (str,) : str }, 204 | "swapcase" : { () : str }, 205 | "title" : { () : str }, 206 | "translate" : { (str,) : str }, 207 | "upper" : { () : str }, 208 | "zfill" : { (int,) : str } 209 | } 210 | 211 | safeStringFunctions = [ 212 | "capitalize", "center", "count", "endswith", "expandtabs", "find", 213 | "isalnum", "isalpha", "isdigit", "islower", "isspace", "istitle", 214 | "isupper", "join", "ljust", "lower", "lstrip", "partition", "replace", 215 | "rfind", "rjust", "rpartition", "rsplit", "rstrip", "split", "splitlines", 216 | "startswith", "strip", "swapcase", "title", "translate", "upper", "zfill", 217 | "isdecimal", "isnumeric"] 218 | 219 | mutatingListFunctions = { 220 | "append" : { (object,) : None }, 221 | "extend" : { (list,) : None }, 222 | "insert" : { (int, object) : None }, 223 | "remove" : { (object,) : None }, 224 | "pop" : { () : None, (int,) : None }, 225 | "sort" : { () : None }, 226 | "reverse" : { () : None } 227 | } 228 | 229 | staticListFunctions = { 230 | "index" : { (object,) : int, (object,int) : int, (object,int,int) : int }, 231 | "count" : { (object,) : int, (object,int) : int, (object,int,int) : int } 232 | } 233 | 234 | safeListFunctions = [ "append", "extend", "insert", "count", "sort", "reverse"] 235 | 236 | builtInListFunctions = dict(list(mutatingListFunctions.items()) + list(staticListFunctions.items())) 237 | 238 | staticDictFunctions = { 239 | "get" : { (object,) : object, (object, object) : object }, 240 | "items" : { () : list } 241 | } 242 | 243 | builtInDictFunctions = staticDictFunctions 244 | 245 | mathFunctions = { 246 | "ceil" : { (int,) : float, (float,) : float }, 247 | "copysign" : { (int, int) : float, (int, float) : float, 248 | (float, int) : float, (float, float) : float }, 249 | "fabs" : { (int,) : float, (float,) : float }, 250 | "factorial" : { (int,) : int, (float,) : int }, 251 | "floor" : { (int,) : float, (float,) : float }, 252 | "fmod" : { (int, int) : float, (int, float) : float, 253 | (float, int) : float, (float, float) : float }, 254 | "frexp" : int, 255 | "fsum" : int, #TODO 256 | "isinf" : { (int,) : bool, (float,) : bool }, 257 | "isnan" : { (int,) : bool, (float,) : bool }, 258 | "ldexp" : int, 259 | "modf" : tuple, 260 | "trunc" : None, #TODO 261 | "exp" : { (int,) : float, (float,) : float }, 262 | "expm1" : { (int,) : float, (float,) : float }, 263 | "log" : { (int,) : float, (float,) : float, 264 | (int,int) : float, (int,float) : float, 265 | (float, int) : float, (float, float) : float }, 266 | "log1p" : { (int,) : float, (float,) : float }, 267 | "log10" : { (int,) : float, (float,) : float }, 268 | "pow" : { (int, int) : float, (int, float) : float, 269 | (float, int) : float, (float, float) : float }, 270 | "sqrt" : { (int,) : float, (float,) : float }, 271 | "acos" : { (int,) : float, (float,) : float }, 272 | "asin" : { (int,) : float, (float,) : float }, 273 | "atan" : { (int,) : float, (float,) : float }, 274 | "atan2" : { (int,) : float, (float,) : float }, 275 | "cos" : { (int,) : float, (float,) : float }, 276 | "hypot" : { (int, int) : float, (int, float) : float, 277 | (float, int) : float, (float, float) : float }, 278 | "sin" : { (int,) : float, (float,) : float }, 279 | "tan" : { (int,) : float, (float,) : float }, 280 | "degrees" : { (int,) : float, (float,) : float }, 281 | "radians" : { (int,) : float, (float,) : float }, 282 | "acosh" : int, 283 | "asinh" : int, 284 | "atanh" : int, 285 | "cosh" : int, 286 | "sinh" : int, 287 | "tanh" : int,#TODO 288 | "erf" : int, 289 | "erfc" : int, 290 | "gamma" : int, 291 | "lgamma" : int #TODO 292 | } 293 | 294 | safeMathFunctions = [ 295 | "ceil", "copysign", "fabs", "floor", "fmod", "isinf", 296 | "isnan", "exp", "expm1", "cos", "hypot", "sin", "tan", 297 | "degrees", "radians" ] 298 | 299 | randomFunctions = { 300 | "seed" : { () : None, (collections.Hashable,) : None }, 301 | "getstate" : { () : object }, 302 | "setstate" : { (object,) : None }, 303 | "jumpahead" : { (int,) : None }, 304 | "getrandbits" : { (int,) : int }, 305 | "randrange" : { (int,) : int, (int, int) : int, (int, int, int) : int }, 306 | "randint" : { (int, int) : int }, 307 | "choice" : { (collections.Iterable,) : object }, 308 | "shuffle" : { (collections.Iterable,) : None, 309 | (collections.Iterable, type(lambda x : x)) : None }, 310 | "sample" : { (collections.Iterable, int) : list }, 311 | "random" : { () : float }, 312 | "uniform" : { (float, float) : float } 313 | } 314 | 315 | futureFunctions = { 316 | "nested_scopes" : None, 317 | "generators" : None, 318 | "division" : None, 319 | "absolute_import" : None, 320 | "with_statement" : None, 321 | "print_function" : None, 322 | "unicode_literals" : None 323 | } 324 | 325 | copyFunctions = { 326 | "copy" : None, 327 | "deepcopy" : None 328 | } 329 | 330 | timeFunctions = { 331 | "clock" : { () : float }, 332 | "time" : { () : float } 333 | } 334 | 335 | errorFunctions = { 336 | "AssertionError" : { (str,) : object } 337 | } 338 | 339 | allStaticFunctions = dict(list(staticBuiltInFunctions.items()) + list(staticTypeCastBuiltInFunctions.items()) + \ 340 | list(builtInStringFunctions.items()) + list(staticListFunctions.items()) + \ 341 | list(staticDictFunctions.items()) + list(mathFunctions.items())) 342 | 343 | allMutatingFunctions = dict(list(mutatingBuiltInFunctions.items()) + list(mutatingTypeCastBuiltInFunctions.items()) + \ 344 | list(mutatingListFunctions.items()) + list(randomFunctions.items()) + list(timeFunctions.items())) 345 | 346 | allPythonFunctions = dict(list(allStaticFunctions.items()) + list(allMutatingFunctions.items())) 347 | 348 | safeLibraryMap = { "string" : [ "ascii_letters", "ascii_lowercase", "ascii_uppercase", 349 | "digits", "hexdigits", "letters", "lowercase", "octdigits", 350 | "punctuation", "printable", "uppercase", "whitespace", 351 | "capitalize", "expandtabs", "find", "rfind", "count", 352 | "lower", "split", "rsplit", "splitfields", "join", 353 | "joinfields", "lstrip", "rstrip", "strip", "swapcase", 354 | "upper", "ljust", "rjust", "center", "zfill", "replace"], 355 | "math" : [ "ceil", "copysign", "fabs", "floor", "fmod", 356 | "frexp", "fsum", "isinf", "isnan", "ldexp", "modf", "trunc", "exp", 357 | "expm1", "log", "log1p", "log10", "sqrt", "acos", "asin", 358 | "atan", "atan2", "cos", "hypot", "sin", "tan", "degrees", "radians", 359 | "acosh", "asinh", "atanh", "cosh", "sinh", "tanh", "erf", "erfc", 360 | "gamma", "lgamma", "pi", "e" ], 361 | "random" : [ ], 362 | "__future__" : ["nested_scopes", "generators", "division", "absolute_import", 363 | "with_statement", "print_function", "unicode_literals"] } 364 | 365 | libraryMap = { "string" : [ "ascii_letters", "ascii_lowercase", "ascii_uppercase", 366 | "digits", "hexdigits", "letters", "lowercase", "octdigits", 367 | "punctuation", "printable", "uppercase", "whitespace", 368 | "capwords", "maketrans", "atof", "atoi", "atol", "capitalize", 369 | "expandtabs", "find", "rfind", "index", "rindex", "count", 370 | "lower", "split", "rsplit", "splitfields", "join", 371 | "joinfields", "lstrip", "rstrip", "strip", "swapcase", 372 | "translate", "upper", "ljust", "rjust", "center", "zfill", 373 | "replace", "Template", "Formatter" ], 374 | "math" : [ "ceil", "copysign", "fabs", "factorial", "floor", "fmod", 375 | "frexp", "fsum", "isinf", "isnan", "ldexp", "modf", "trunc", "exp", 376 | "expm1", "log", "log1p", "log10", "pow", "sqrt", "acos", "asin", 377 | "atan", "atan2", "cos", "hypot", "sin", "tan", "degrees", "radians", 378 | "acosh", "asinh", "atanh", "cosh", "sinh", "tanh", "erf", "erfc", 379 | "gamma", "lgamma", "pi", "e" ], 380 | "random" : ["seed", "getstate", "setstate", "jumpahead", "getrandbits", 381 | "randrange", "randrange", "randint", "choice", "shuffle", "sample", 382 | "random", "uniform", "triangular", "betavariate", "expovariate", 383 | "gammavariate", "gauss", "lognormvariate", "normalvariate", 384 | "vonmisesvariate", "paretovariate", "weibullvariate", "WichmannHill", 385 | "whseed", "SystemRandom" ], 386 | "__future__" : ["nested_scopes", "generators", "division", "absolute_import", 387 | "with_statement", "print_function", "unicode_literals"], 388 | "copy" : ["copy", "deepcopy"] } 389 | 390 | libraryDictMap = { "string" : builtInStringFunctions, 391 | "math" : mathFunctions, 392 | "random" : randomFunctions, 393 | "__future__" : futureFunctions, 394 | "copy" : copyFunctions } 395 | 396 | typeMethodMap = {"string" : ["capitalize", "center", "count", "decode", "encode", "endswith", 397 | "expandtabs", "find", "format", "index", "isalnum", "isalpha", 398 | "isdigit", "islower", "isspace", "istitle", "isupper", "join", 399 | "ljust", "lower", "lstrip", "partition", "replace", "rfind", 400 | "rindex", "rjust", "rpartition", "rsplit", "rstrip", "split", 401 | "splitlines", "startswith", "strip", "swapcase", "title", 402 | "translate", "upper", "zfill"], 403 | "list" : [ "append", "extend", "count", "index", "insert", "pop", "remove", 404 | "reverse", "sort"], 405 | "set" : [ "isdisjoint", "issubset", "issuperset", "union", "intersection", 406 | "difference", "symmetric_difference", "update", "intersection_update", 407 | "difference_update", "symmetric_difference_update", "add", 408 | "remove", "discard", "pop", "clear"], 409 | "dict" : [ "iter", "clear", "copy", "fromkeys", "get", "has_key", "items", 410 | "iteritems", "iterkeys", "itervalues", "keys", "pop", "popitem", 411 | "setdefault", "update", "values", "viewitems", "viewkeys", 412 | "viewvalues"] } 413 | 414 | astNames = { 415 | ast.Module : "Module", ast.Interactive : "Interactive Module", 416 | ast.Expression : "Expression Module", ast.Suite : "Suite", 417 | 418 | ast.FunctionDef : "Function Definition", 419 | ast.ClassDef : "Class Definition", ast.Return : "Return", 420 | ast.Delete : "Delete", ast.Assign : "Assign", 421 | ast.AugAssign : "AugAssign", ast.For : "For", 422 | ast.While : "While", ast.If : "If", ast.With : "With", 423 | ast.Raise : "Raise", 424 | ast.Try : "Try", ast.Assert : "Assert", 425 | ast.Import : "Import", ast.ImportFrom : "Import From", 426 | ast.Global : "Global", ast.Expr : "Expression", 427 | ast.Pass : "Pass", ast.Break : "Break", ast.Continue : "Continue", 428 | 429 | ast.BoolOp : "Boolean Operation", ast.BinOp : "Binary Operation", 430 | ast.UnaryOp : "Unary Operation", ast.Lambda : "Lambda", 431 | ast.IfExp : "Ternary", ast.Dict : "Dictionary", ast.Set : "Set", 432 | ast.ListComp : "List Comprehension", ast.SetComp : "Set Comprehension", 433 | ast.DictComp : "Dict Comprehension", 434 | ast.GeneratorExp : "Generator", ast.Yield : "Yield", 435 | ast.Compare : "Compare", ast.Call : "Call", 436 | ast.Num : "Number", ast.Str : "String", ast.Bytes : "Bytes", 437 | ast.NameConstant : "Name Constant", 438 | ast.Attribute : "Attribute", 439 | ast.Subscript : "Subscript", ast.Name : "Name", ast.List : "List", 440 | ast.Tuple : "Tuple", ast.Starred : "Starred", 441 | 442 | ast.Load : "Load", ast.Store : "Store", ast.Del : "Delete", 443 | ast.AugLoad : "AugLoad", ast.AugStore : "AugStore", 444 | ast.Param : "Parameter", 445 | 446 | ast.Ellipsis : "Ellipsis", ast.Slice : "Slice", 447 | ast.ExtSlice : "ExtSlice", ast.Index : "Index", 448 | 449 | ast.And : "And", ast.Or : "Or", ast.Add : "Add", ast.Sub : "Subtract", 450 | ast.Mult : "Multiply", ast.Div : "Divide", ast.Mod : "Modulo", 451 | ast.Pow : "Power", ast.LShift : "Left Shift", 452 | ast.RShift : "Right Shift", ast.BitOr : "|", ast.BitXor : "^", 453 | ast.BitAnd : "&", ast.FloorDiv : "Integer Divide", 454 | ast.Invert : "Invert", ast.Not : "Not", ast.UAdd : "Unsigned Add", 455 | ast.USub : "Unsigned Subtract", ast.Eq : "==", ast.NotEq : "!=", 456 | ast.Lt : "<", ast.LtE : "<=", ast.Gt : ">", ast.GtE : ">=", 457 | ast.Is : "Is", ast.IsNot : "Is Not", ast.In : "In", 458 | ast.NotIn : "Not In", 459 | 460 | ast.comprehension: "Comprehension", 461 | ast.ExceptHandler : "Except Handler", ast.arguments : "Arguments", ast.arg : "Argument", 462 | ast.keyword : "Keyword", ast.alias : "Alias", ast.withitem : "With item" 463 | } -------------------------------------------------------------------------------- /hintgen/path_construction/__init__.py: -------------------------------------------------------------------------------- 1 | from ..generate_message import formatHints 2 | from .generateNextStates import getNextState 3 | 4 | def generatePaths(states): 5 | """Generate all the paths within this solution space""" 6 | i = 0 7 | while i < len(states): 8 | state = states[i] 9 | if state.score != 1: 10 | getNextState(state, states) # can add more states 11 | if state.edit != None: 12 | state.hint = formatHints(state) 13 | if state.goal == None: # Fix these for logging purposes 14 | state.goal = "" 15 | state.goalDist = "" 16 | i += 1 17 | -------------------------------------------------------------------------------- /hintgen/path_construction/diffAsts.py: -------------------------------------------------------------------------------- 1 | import ast 2 | from ..tools import log 3 | from ..astTools import * 4 | from ..namesets import astNames 5 | from ..ChangeVector import * 6 | from ..State import * 7 | 8 | def getWeight(a, countTokens=True): 9 | """Get the size of the given tree""" 10 | if a == None: 11 | return 0 12 | elif type(a) == list: 13 | return sum(map(lambda x : getWeight(x, countTokens), a)) 14 | elif not isinstance(a, ast.AST): 15 | return 1 16 | else: # Otherwise, it's an AST node 17 | if hasattr(a, "treeWeight"): 18 | return a.treeWeight 19 | weight = 0 20 | if type(a) in [ast.Module, ast.Interactive, ast.Suite]: 21 | weight = getWeight(a.body, countTokens=countTokens) 22 | elif type(a) == ast.Expression: 23 | weight = getWeight(a.body, countTokens=countTokens) 24 | elif type(a) == ast.FunctionDef: 25 | # add 1 for function name 26 | weight = 1 + getWeight(a.args, countTokens=countTokens) + \ 27 | getWeight(a.body, countTokens=countTokens) + \ 28 | getWeight(a.decorator_list, countTokens=countTokens) + \ 29 | getWeight(a.returns, countTokens=countTokens) 30 | elif type(a) == ast.ClassDef: 31 | # add 1 for class name 32 | weight = 1 + sumWeight(a.bases, countTokens=countTokens) + \ 33 | sumWeight(a.keywords, countTokens=countTokens) + \ 34 | getWeight(a.body, countTokens=countTokens) + \ 35 | getWeight(a.decorator_list, countTokens=countTokens) 36 | elif type(a) in [ast.Return, ast.Yield, ast.Attribute, ast.Starred]: 37 | # add 1 for action name 38 | weight = 1 + getWeight(a.value, countTokens=countTokens) 39 | elif type(a) == ast.Delete: # add 1 for del 40 | weight = 1 + getWeight(a.targets, countTokens=countTokens) 41 | elif type(a) == ast.Assign: # add 1 for = 42 | weight = 1 + getWeight(a.targets, countTokens=countTokens) + \ 43 | getWeight(a.value, countTokens=countTokens) 44 | elif type(a) == ast.AugAssign: 45 | weight = getWeight(a.target, countTokens=countTokens) + \ 46 | getWeight(a.op, countTokens=countTokens) + \ 47 | getWeight(a.value, countTokens=countTokens) 48 | elif type(a) == ast.For: # add 1 for 'for' and 1 for 'in' 49 | weight = 2 + getWeight(a.target, countTokens=countTokens) + \ 50 | getWeight(a.iter, countTokens=countTokens) + \ 51 | getWeight(a.body, countTokens=countTokens) + \ 52 | getWeight(a.orelse, countTokens=countTokens) 53 | elif type(a) in [ast.While, ast.If]: 54 | # add 1 for while/if 55 | weight = 1 + getWeight(a.test, countTokens=countTokens) + \ 56 | getWeight(a.body, countTokens=countTokens) 57 | if len(a.orelse) > 0: # add 1 for else 58 | weight += 1 + getWeight(a.orelse, countTokens=countTokens) 59 | elif type(a) == ast.With: # add 1 for with 60 | weight = 1 + getWeight(a.items, countTokens=countTokens) + \ 61 | getWeight(a.body, countTokens=countTokens) 62 | elif type(a) == ast.Raise: # add 1 for raise 63 | weight = 1 + getWeight(a.exc, countTokens=countTokens) + \ 64 | getWeight(a.cause, countTokens=countTokens) 65 | elif type(a) == ast.Try: # add 1 for try 66 | weight = 1 + getWeight(a.body, countTokens=countTokens) + \ 67 | getWeight(a.handlers, countTokens=countTokens) 68 | if len(a.orelse) > 0: # add 1 for else 69 | weight += 1 + getWeight(a.orelse, countTokens=countTokens) 70 | if len(a.finalbody) > 0: # add 1 for finally 71 | weight += 1 + getWeight(a.finalbody, countTokens=countTokens) 72 | elif type(a) == ast.Assert: # add 1 for assert 73 | weight = 1 + getWeight(a.test, countTokens=countTokens) + \ 74 | getWeight(a.msg, countTokens=countTokens) 75 | elif type(a) in [ast.Import, ast.Global]: # add 1 for function name 76 | weight = 1 + getWeight(a.names, countTokens=countTokens) 77 | elif type(a) == ast.ImportFrom: # add 3 for from module import 78 | weight = 3 + getWeight(a.names, countTokens=countTokens) 79 | elif type(a) in [ast.Expr, ast.Index]: 80 | weight = getWeight(a.value, countTokens=countTokens) 81 | if weight == 0: 82 | weight = 1 83 | elif type(a) == ast.BoolOp: # add 1 for each op 84 | weight = (len(a.values) - 1) + \ 85 | getWeight(a.values, countTokens=countTokens) 86 | elif type(a) == ast.BinOp: # add 1 for op 87 | weight = 1 + getWeight(a.left, countTokens=countTokens) + \ 88 | getWeight(a.right, countTokens=countTokens) 89 | elif type(a) == ast.UnaryOp: # add 1 for operator 90 | weight = 1 + getWeight(a.operand, countTokens=countTokens) 91 | elif type(a) == ast.Lambda: # add 1 for lambda 92 | weight = 1 + getWeight(a.args, countTokens=countTokens) + \ 93 | getWeight(a.body, countTokens=countTokens) 94 | elif type(a) == ast.IfExp: # add 2 for if and else 95 | weight = 2 + getWeight(a.test, countTokens=countTokens) + \ 96 | getWeight(a.body, countTokens=countTokens) + \ 97 | getWeight(a.orelse, countTokens=countTokens) 98 | elif type(a) == ast.Dict: # return 1 if empty dictionary 99 | weight = 1 + getWeight(a.keys, countTokens=countTokens) + \ 100 | getWeight(a.values, countTokens=countTokens) 101 | elif type(a) in [ast.Set, ast.List, ast.Tuple]: 102 | weight = 1 + getWeight(a.elts, countTokens=countTokens) 103 | elif type(a) in [ast.ListComp, ast.SetComp, ast.GeneratorExp]: 104 | weight = 1 + getWeight(a.elt, countTokens=countTokens) + \ 105 | getWeight(a.generators, countTokens=countTokens) 106 | elif type(a) == ast.DictComp: 107 | weight = 1 + getWeight(a.key, countTokens=countTokens) + \ 108 | getWeight(a.value, countTokens=countTokens) + \ 109 | getWeight(a.generators, countTokens=countTokens) 110 | elif type(a) == ast.Compare: 111 | weight = len(a.ops) + getWeight(a.left, countTokens=countTokens) + \ 112 | getWeight(a.comparators, countTokens=countTokens) 113 | elif type(a) == ast.Call: 114 | functionWeight = getWeight(a.func, countTokens=countTokens) 115 | functionWeight = functionWeight if functionWeight > 0 else 1 116 | argsWeight = getWeight(a.args, countTokens=countTokens) + \ 117 | getWeight(a.keywords, countTokens=countTokens) 118 | argsWeight = argsWeight if argsWeight > 0 else 1 119 | weight = functionWeight + argsWeight 120 | elif type(a) == ast.Subscript: 121 | valueWeight = getWeight(a.value, countTokens=countTokens) 122 | valueWeight = valueWeight if valueWeight > 0 else 1 123 | sliceWeight = getWeight(a.slice, countTokens=countTokens) 124 | sliceWeight = sliceWeight if sliceWeight > 0 else 1 125 | weight = valueWeight + sliceWeight 126 | 127 | elif type(a) == ast.Slice: 128 | weight = getWeight(a.lower, countTokens=countTokens) + \ 129 | getWeight(a.upper, countTokens=countTokens) + \ 130 | getWeight(a.step, countTokens=countTokens) 131 | if weight == 0: 132 | weight = 1 133 | elif type(a) == ast.ExtSlice: 134 | weight = getWeight(a.dims, countTokens=countTokens) 135 | 136 | elif type(a) == ast.comprehension: # add 2 for for and in 137 | # and each of the if tokens 138 | weight = 2 + len(a.ifs) + \ 139 | getWeight(a.target, countTokens=countTokens) + \ 140 | getWeight(a.iter, countTokens=countTokens) + \ 141 | getWeight(a.ifs, countTokens=countTokens) 142 | elif type(a) == ast.ExceptHandler: # add 1 for except 143 | weight = 1 + getWeight(a.type, countTokens=countTokens) 144 | # add 1 for as (if needed) 145 | weight += (1 if a.name != None else 0) + \ 146 | getWeight(a.name, countTokens=countTokens) 147 | weight += getWeight(a.body, countTokens=countTokens) 148 | elif type(a) == ast.arguments: 149 | weight = getWeight(a.args, countTokens=countTokens) + \ 150 | getWeight(a.vararg, countTokens=countTokens) + \ 151 | getWeight(a.kwonlyargs, countTokens=countTokens) + \ 152 | getWeight(a.kw_defaults, countTokens=countTokens) + \ 153 | getWeight(a.kwarg, countTokens=countTokens) + \ 154 | getWeight(a.defaults, countTokens=countTokens) 155 | elif type(a) == ast.arg: 156 | weight = 1 + getWeight(a.annotation, countTokens=countTokens) 157 | elif type(a) == ast.keyword: # add 1 for identifier 158 | weight = 1 + getWeight(a.value, countTokens=countTokens) 159 | elif type(a) == ast.alias: # 1 for name, 1 for as, 1 for asname 160 | weight = 1 + (2 if a.asname != None else 0) 161 | elif type(a) == ast.withitem: 162 | weight = getWeight(a.context_expr, countTokens=countTokens) + \ 163 | getWeight(a.optional_vars, countTokens=countTokens) 164 | elif type(a) == ast.Str: 165 | if countTokens: 166 | weight = 1 167 | elif len(a.s) >= 2 and a.s[0] == "~" and a.s[-1] == "~": 168 | weight = 0 169 | else: 170 | weight = 1 171 | 172 | elif type(a) in [ast.Pass, ast.Break, ast.Continue, ast.Num, ast.Bytes, 173 | ast.NameConstant, ast.Name, 174 | ast.Ellipsis]: 175 | weight = 1 176 | elif type(a) in [ast.And, ast.Or, 177 | ast.Add, ast.Sub, ast.Mult, ast.Div, ast.Mod, ast.Pow, 178 | ast.LShift, ast.RShift, ast.BitOr, ast.BitXor, 179 | ast.BitAnd, ast.FloorDiv, 180 | ast.Invert, ast.Not, ast.UAdd, ast.USub, 181 | ast.Eq, ast.NotEq, ast.Lt, ast.LtE, ast.Gt, ast.GtE, 182 | ast.Is, ast.IsNot, ast.In, ast.NotIn, 183 | ast.Load, ast.Store, ast.Del, ast.AugLoad, 184 | ast.AugStore, ast.Param ]: 185 | weight = 1 186 | else: 187 | log("diffAsts\tgetWeight\tMissing type in diffAsts: " + str(type(a)), "bug") 188 | return 1 189 | setattr(a, "treeWeight", weight) 190 | return weight 191 | 192 | def matchLists(x, y): 193 | """For each line in x, determine which line it best maps to in y""" 194 | x = [ (x[i], i) for i in range(len(x)) ] 195 | y = [ (y[i], i) for i in range(len(y)) ] 196 | # First, separate out all the lines based on their types, as we only match between types 197 | typeMap = { } 198 | for i in range(len(x)): 199 | t = type(x[i][0]) 200 | if (t in typeMap): 201 | pass 202 | xSubset = list(filter(lambda tmp : type(tmp[0]) == t, x)) 203 | ySubset = list(filter(lambda tmp : type(tmp[0]) == t, y)) 204 | typeMap[t] = (xSubset, ySubset) 205 | for j in range(len(y)): 206 | t = type(y[j][0]) 207 | if t in typeMap: 208 | pass 209 | xSubset = list(filter(lambda tmp : type(tmp[0]) == t, x)) 210 | ySubset = list(filter(lambda tmp : type(tmp[0]) == t, y)) 211 | typeMap[t] = (xSubset, ySubset) 212 | 213 | mapSet = {} 214 | for t in typeMap: 215 | # For each type, find the optimal matching 216 | (xSubset, ySubset) = typeMap[t] 217 | # First, find exact matches and remove them 218 | # Give preference to items on the same line- then we won't need to do an edit 219 | i = 0 220 | while i < len(xSubset): 221 | j = 0 222 | while j < len(ySubset): 223 | if xSubset[i][1] == ySubset[j][1]: 224 | if compareASTs(xSubset[i][0], ySubset[j][0], checkEquality=True) == 0: 225 | mapSet[ySubset[j][1]] = xSubset[i][1] 226 | xSubset.pop(i) 227 | ySubset.pop(j) 228 | break 229 | j += 1 230 | else: 231 | i += 1 232 | # Then look for matches anywhere 233 | i = 0 234 | while i < len(xSubset): 235 | j = 0 236 | while j < len(ySubset): 237 | if compareASTs(xSubset[i][0], ySubset[j][0], checkEquality=True) == 0: 238 | mapSet[ySubset[j][1]] = xSubset[i][1] 239 | xSubset.pop(i) 240 | ySubset.pop(j) 241 | break 242 | j += 1 243 | else: 244 | i += 1 # if we break, don't increment! 245 | # TODO - check for subsets/supersets in here? 246 | # Then, look for the 'best we can do' matches 247 | distanceList = [ ] 248 | for i in range(len(xSubset)): # Identify the best matches across all pairs 249 | st1 = State() 250 | st1.tree = xSubset[i][0] 251 | for j in range(len(ySubset)): 252 | st2 = State() 253 | st2.tree = ySubset[j][0] 254 | d, _ = distance(st1, st2) 255 | d = int(d * 1000) 256 | distanceList.append((d, xSubset[i][1], ySubset[j][1])) 257 | # Compare first based on distance, then based on how close the lines are to each other 258 | distanceList.sort(key=lambda x : (x[0], x[1] - x[2])) 259 | l = min(len(xSubset), len(ySubset)) 260 | # Now pick the best pairs 'til we run out of them 261 | while l > 0: 262 | (d, xLine, yLine) = distanceList[0] 263 | mapSet[yLine] = xLine 264 | distanceList = list(filter(lambda x : x[1] != xLine and x[2] != yLine, distanceList)) 265 | l -= 1 266 | # Now, look for matches across different types 267 | leftoverY = list(filter(lambda tmp : tmp not in mapSet, range(len(y)))) 268 | leftoverX = list(filter(lambda tmp : tmp not in mapSet.values(), range(len(x)))) 269 | # First, look for exact line matches 270 | i = 0 271 | while i < len(leftoverX): 272 | line = leftoverX[i] 273 | if line in leftoverY: 274 | mapSet[line] = line 275 | leftoverX.remove(line) 276 | leftoverY.remove(line) 277 | else: 278 | i += 1 279 | # Then, just put the rest in place 280 | for i in range(min(len(leftoverY), len(leftoverX))): # map together all equal parts 281 | mapSet[leftoverY[i]] = leftoverX[i] 282 | if len(leftoverX) > len(leftoverY): # if X greater, map all leftover x's to -1 283 | mapSet[-1] = leftoverX[len(leftoverY):] 284 | elif len(leftoverY) > len(leftoverX): # if Y greater, map all leftover y's to -1 285 | for i in range(len(leftoverX), len(leftoverY)): 286 | mapSet[leftoverY[i]] = -1 287 | # if equal, there are none left to map! 288 | return mapSet 289 | 290 | def findKey(d, val): 291 | for k in d: 292 | if d[k] == val: 293 | return k 294 | return None 295 | 296 | def xOffset(line, deletedLines): 297 | offset = 0 298 | for l in deletedLines: 299 | if l <= line: 300 | offset += 1 301 | return offset 302 | 303 | def yOffset(line, addedLines): 304 | offset = 0 305 | for l in addedLines: 306 | if l <= line: 307 | offset += 1 308 | return offset 309 | 310 | def findSwap(startList, endList): 311 | for i in range(len(startList)): 312 | if startList[i] == endList[i]: 313 | pass 314 | for j in range(i+1, len(startList)): 315 | if startList[i] == endList[j] and endList[i] == startList[j]: 316 | return SwapVector([-1], startList[i], startList[j]) 317 | return None 318 | 319 | # Recursively generate all moves by working from the outside of the list inwards. 320 | # This should be optimal for lists of up to size four, and once you get to size five, your program is too 321 | # large and I don't care anymore. 322 | def generateMovePairs(startList, endList): 323 | if len(startList) <= 1: 324 | return [] 325 | elif startList[0] == endList[0]: 326 | return generateMovePairs(startList[1:], endList[1:]) 327 | elif startList[-1] == endList[-1]: 328 | return generateMovePairs(startList[:-1], endList[:-1]) 329 | elif startList[0] == endList[-1] and startList[-1] == endList[0]: 330 | # swap the two ends 331 | return [("swap", startList[0], startList[-1])] + generateMovePairs(startList[1:-1], endList[1:-1]) 332 | elif startList[0] == endList[-1]: 333 | # move the smallest element from back to front 334 | return [("move", startList[0])] + generateMovePairs(startList[1:], endList[:-1]) 335 | elif startList[-1] == endList[0]: 336 | # move the largest element from front to back 337 | return [("move", startList[-1])] + generateMovePairs(startList[:-1], endList[1:]) 338 | else: 339 | i = endList.index(startList[0]) # find the position in endList 340 | return [("move", startList[0])] + generateMovePairs(startList[1:], endList[:i] + endList[i+1:]) 341 | 342 | def findMoveVectors(mapSet, x, y, add, delete): 343 | """We'll find all the moved lines by recreating the mapSet from a tmpSet using actions""" 344 | startList = list(range(len(x))) 345 | endList = [mapSet[i] for i in range(len(y))] 346 | # Remove deletes from startList and adds from endList 347 | for line in delete: 348 | startList.remove(line) 349 | while -1 in endList: 350 | endList.remove(-1) 351 | if len(startList) != len(endList): 352 | log("diffAsts\tfindMovedLines\tUnequal lists: " + str(len(startList)) + "," + str(len(endList)), "bug") 353 | return [] 354 | moveActions = [] 355 | if startList != endList: 356 | movePairs = generateMovePairs(startList, endList) 357 | for pair in movePairs: 358 | if pair[0] == "move": 359 | moveActions.append(MoveVector([-1], pair[1], endList.index(pair[1]))) 360 | elif pair[0] == "swap": 361 | moveActions.append(SwapVector([-1], pair[1], pair[2])) 362 | else: 363 | log("Missing movePair type: " + str(pair[0]), "bug") 364 | # We need to make sure the indicies start at the appropriate numbers, since they're referring to the original tree 365 | if len(delete) > 0: 366 | for action in moveActions: 367 | if isinstance(action, MoveVector): 368 | addToCount = 0 369 | for deleteAction in delete: 370 | if deleteAction <= action.newSubtree: 371 | addToCount += 1 372 | action.newSubtree += addToCount 373 | return moveActions 374 | 375 | def diffLists(x, y, ignoreVariables=False): 376 | mapSet = matchLists(x, y) 377 | changeVectors = [] 378 | 379 | # First, get all the added and deleted lines 380 | deletedLines = mapSet[-1] if -1 in mapSet else [] 381 | for line in sorted(deletedLines): 382 | changeVectors.append(DeleteVector([line], x[line], None)) 383 | 384 | addedLines = list(filter(lambda tmp : mapSet[tmp] == -1, mapSet.keys())) 385 | addedOffset = 0 # Because added lines don't start in the list, we need 386 | # to offset their positions for each new one that's added 387 | for line in sorted(addedLines): 388 | changeVectors.append(AddVector([line - addedOffset], None, y[line])) 389 | addedOffset += 1 390 | 391 | # Now, find all the required moves 392 | changeVectors += findMoveVectors(mapSet, x, y, addedLines, deletedLines) 393 | 394 | # Finally, for each pair of lines (which have already been moved appropriately, 395 | # find if they need a normal ChangeVector 396 | for j in mapSet: 397 | i = mapSet[j] 398 | # Not a delete or an add 399 | if j != -1 and i != -1: 400 | tempVectors = diffAsts(x[i], y[j], ignoreVariables=ignoreVariables) 401 | for change in tempVectors: 402 | change.path.append(i) 403 | changeVectors += tempVectors 404 | return changeVectors 405 | 406 | def diffAsts(x, y, ignoreVariables=False): 407 | """Find all change vectors between x and y""" 408 | xAST = isinstance(x, ast.AST) 409 | yAST = isinstance(y, ast.AST) 410 | if xAST and yAST: 411 | if type(x) != type(y): # different node types 412 | if occursIn(x, y): 413 | return [SubVector([], x, y)] 414 | elif occursIn(y, x): 415 | return [SuperVector([], x, y)] 416 | else: 417 | return [ChangeVector([], x, y)] 418 | elif ignoreVariables and type(x) == type(y) == ast.Name: 419 | if not builtInName(x.id) and not builtInName(y.id): 420 | return [] # ignore the actual IDs 421 | 422 | result = [] 423 | for field in x._fields: 424 | currentDiffs = diffAsts(getattr(x, field), getattr(y, field), ignoreVariables=ignoreVariables) 425 | if currentDiffs != []: # add the next step in the path 426 | for change in currentDiffs: 427 | change.path.append((field, astNames[type(x)])) 428 | result += currentDiffs 429 | return result 430 | elif (not xAST) and (not yAST): 431 | if type(x) == list and type(y) == list: 432 | return diffLists(x, y, ignoreVariables=ignoreVariables) 433 | elif x != y or type(x) != type(y): # need the type check to distinguish ints from floats 434 | return [ChangeVector([], x, y)] # they're primitive, so just switch them 435 | else: # equal values 436 | return [] 437 | else: # Two mismatched types 438 | return [ChangeVector([], x, y)] 439 | 440 | def getChanges(s, t, ignoreVariables=False): 441 | changes = diffAsts(s, t, ignoreVariables=ignoreVariables) 442 | for change in changes: 443 | change.start = s # WARNING: should maybe have a deepcopy here? It will alias s 444 | return changes 445 | 446 | def getChangesWeight(changes, countTokens=True): 447 | weight = 0 448 | for change in changes: 449 | if isinstance(change, AddVector): 450 | weight += getWeight(change.newSubtree, countTokens=countTokens) 451 | elif isinstance(change, DeleteVector): 452 | weight += getWeight(change.oldSubtree, countTokens=countTokens) 453 | elif isinstance(change, SwapVector): 454 | weight += 2 # only changing the positions 455 | elif isinstance(change, MoveVector): 456 | weight += 1 # only moving one item 457 | elif isinstance(change, SubVector): 458 | weight += abs(getWeight(change.newSubtree, countTokens=countTokens) - \ 459 | getWeight(change.oldSubtree, countTokens=countTokens)) 460 | elif isinstance(change, SuperVector): 461 | weight += abs(getWeight(change.oldSubtree, countTokens=countTokens) - \ 462 | getWeight(change.newSubtree, countTokens=countTokens)) 463 | else: 464 | weight += max(getWeight(change.oldSubtree, countTokens=countTokens), 465 | getWeight(change.newSubtree, countTokens=countTokens)) 466 | return weight 467 | 468 | def distance(s, t, givenChanges=None, forceReweight=False, ignoreVariables=False): 469 | """A method for comparing solution states, which returns a number between 470 | 0 (identical solutions) and 1 (completely different)""" 471 | # First weigh the trees, to propogate metadata 472 | if s == None or t == None: 473 | return 1 # can't compare to a None state 474 | if forceReweight: 475 | baseWeight = max(getWeight(s.tree), getWeight(t.tree)) 476 | else: 477 | if not hasattr(s, "treeWeight"): 478 | s.treeWeight = getWeight(s.tree) 479 | if not hasattr(t, "treeWeight"): 480 | t.treeWeight = getWeight(t.tree) 481 | baseWeight = max(s.treeWeight, t.treeWeight) 482 | 483 | if givenChanges != None: 484 | changes = givenChanges 485 | else: 486 | changes = getChanges(s.tree, t.tree, ignoreVariables=ignoreVariables) 487 | 488 | changeWeight = getChangesWeight(changes) 489 | return (1.0 * changeWeight / baseWeight, changes) -------------------------------------------------------------------------------- /hintgen/path_construction/generateNextStates.py: -------------------------------------------------------------------------------- 1 | import copy 2 | from ..tools import * 3 | from ..astTools import * 4 | from ..display import * 5 | from ..test import test as codetest 6 | from .diffAsts import * 7 | from ..State import * 8 | from ..ChangeVector import * 9 | from ..models import AnonState, CanonicalState 10 | 11 | def getNextId(states, idStart): 12 | count = 0 13 | for s in states: 14 | if idStart in s.id: 15 | count += 1 16 | return idStart + str(count) 17 | 18 | def filterChanges(combinations, changes, oldStart, newStart): 19 | # Remove any combos which do not include the given changes 20 | i = 0 21 | while i < len(combinations): 22 | (nc,nn) = combinations[i] 23 | isLegal = True 24 | for change in changes: 25 | if change in nc: 26 | nc.remove(change) 27 | else: 28 | isLegal = False 29 | break 30 | if isLegal and len(nc) > 0: # empty sets aren't allowed 31 | i += 1 32 | else: 33 | combinations.pop(i) 34 | return combinations 35 | 36 | def desirability(s, n, g): 37 | """Scores the state n based on the four desirable properties. Returns a number 38 | between 0 (not desirable) and 1 (very desirable).""" 39 | # Original metric: 2 - 4 - 1 - 2 40 | score = 0 41 | # First: has the state been visited before? 42 | a = int(n.count > 0) 43 | n.timesVisted = a 44 | score += 4 * a 45 | 46 | # Second: minimize the distance from current to next 47 | b = 1 - distance(s, n)[0] 48 | n.nearCurrent = b 49 | score += 2 * b 50 | 51 | # Third: maximize the performance on test cases 52 | c = n.score 53 | n.test = c 54 | score += 1 * c 55 | 56 | # Forth: minimize the distance from the next state to the final state 57 | #if n != g and not hasattr(n, "goalDist"): 58 | # n.goalDist = distance(n, g)[0] 59 | #goalDist = n.goalDist if n != g else 0 60 | #d = 1 - goalDist 61 | #n.nearGoal = d 62 | #score += 2 * d 63 | 64 | score /= 7.0 65 | return score 66 | 67 | def mapDifferences(start, end): 68 | d = { "start" : { } } 69 | allChanges = getChanges(start, end) 70 | s = deepcopy(start) 71 | for change in allChanges: 72 | change.update(s, d) 73 | s = change.applyChange() 74 | return d 75 | 76 | def quickDeepCopy(cv): 77 | # Doesn't copy start because it will get replaced anyway 78 | # the old subtree and new subtree can be aliases because we never modify them 79 | path = cv.path[:] 80 | old, new = cv.oldSubtree, cv.newSubtree 81 | if isinstance(cv, AddVector): 82 | return AddVector(path, old, new) 83 | elif isinstance(cv, DeleteVector): 84 | return DeleteVector(path, old, new) 85 | elif isinstance(cv, SwapVector): 86 | tmp = SwapVector(path, old, new) 87 | if cv.oldPath != None: 88 | tmp.oldPath = cv.oldPath 89 | tmp.newPath = cv.newPath 90 | return tmp 91 | elif isinstance(cv, MoveVector): 92 | return MoveVector(path, old, new) 93 | elif isinstance(cv, SubVector): 94 | return SubVector(path, old, new) 95 | elif isinstance(cv, SuperVector): 96 | return SuperVector(path, old, new) 97 | elif isinstance(cv, ChangeVector): 98 | return ChangeVector(path, old, new) 99 | else: 100 | log("generateNextSteps\tquickDeepCopy\tMissing type: " + str(type(cv)), "bug") 101 | return cv 102 | 103 | def updateChangeVectors(changes, oldStart, newStart): 104 | if len(changes) == 0: 105 | return changes, newStart 106 | # We need new CVs here because they're going to change 107 | changes = [quickDeepCopy(x) for x in changes] 108 | mapDict = mapDifferences(oldStart, newStart) 109 | newState = deepcopy(newStart) 110 | for change in changes: 111 | change.update(newState, mapDict) # mapDict gets updated each time 112 | newState = change.applyChange() 113 | return changes, newState 114 | 115 | def applyChangeVectors(s, changes, states, goals): 116 | """Attempt to apply all the changes listed to the solution state s""" 117 | if len(changes) == 0: 118 | return s 119 | tup = updateChangeVectors(changes, changes[0].start, s.tree) 120 | if tup == None: 121 | return None 122 | 123 | changes, newState = tup 124 | 125 | # Now, make the new state! 126 | newFun = printFunction(newState) 127 | matches = list(filter(lambda x : x.code==newFun, states)) 128 | if len(matches) > 0: 129 | matches = sorted(matches, key=lambda x : getattr(x, "count")) 130 | tmpN = matches[-1] 131 | tmpN.tree = str_to_tree(tmpN.tree_source) 132 | return tmpN 133 | else: 134 | n = CanonicalState(code=newFun, problem=s.problem, count=0) 135 | n.tree = newState 136 | n.tree_source = tree_to_str(newState) 137 | n.treeWeight = getWeight(newState) 138 | n = codetest(n) 139 | states.append(n) 140 | if n.score == 1: 141 | goals.append(n) 142 | return n 143 | 144 | def chooseGoal(s, goals, states): 145 | # First, find the closest goal state and the changes required to get to it 146 | goalDist = 2 # the max dist is 1 147 | goal = origGoal = None 148 | changes = None 149 | # First, find the program whose structure best matches the state 150 | for g in goals: 151 | (tempD, tempChanges) = distance(s, g, ignoreVariables=True) 152 | # prefer more common goals over less common ones 153 | if (tempD < goalDist) or (tempD == goalDist and g.count > goal.count): 154 | (goal, goalDist, changes) = (g, tempD, tempChanges) 155 | # Then do variable matching between the two programs 156 | if goal != None: 157 | # First, do helper function mapping, if it's necessary 158 | helperDistributions = generateHelperDistributions(s, goal, goals, states) 159 | if len(helperDistributions) > 0: 160 | goalDist = 2 # reset because now we're going to count variables 161 | origGoal = goal 162 | for modG in helperDistributions: 163 | (tempD, tempChanges) = distance(s, modG) 164 | # prefer more common goals over less common ones 165 | if (tempD < goalDist) or (tempD == goalDist and modG.count > goal.count): 166 | (goal, goalDist, changes) = (modG, tempD, tempChanges) 167 | 168 | goalDist = 2 # reset because now we're going to count variables 169 | origGoal = goal 170 | allDistributions = generateVariableDistributions(s, goal, goals, states) 171 | for modG in allDistributions: 172 | (tempD, tempChanges) = distance(s, modG) 173 | # prefer more common goals over less common ones 174 | if (tempD < goalDist) or (tempD == goalDist and modG.count > goal.count): 175 | (goal, goalDist, changes) = (modG, tempD, tempChanges) 176 | return goal 177 | 178 | def generateHelperDistributions(s, g, goals, states): 179 | restricted_names = list(eval(s.problem.arguments).keys()) 180 | sHelpers = gatherAllHelpers(s.tree, restricted_names) 181 | gHelpers = gatherAllHelpers(g.tree, restricted_names) 182 | nonMappableHelpers = gatherAllFunctionNames(g.tree) 183 | for pair in gHelpers: # make sure to remove all matches, regardless of whether the second part matches! 184 | for item in nonMappableHelpers: 185 | if pair[0] == item[0]: 186 | nonMappableHelpers.remove(item) 187 | break 188 | randomCount = nCount = newRandomCount = 0 189 | if len(sHelpers) > len(gHelpers): 190 | gHelpers |= set([("random_fun" + str(i), None) for i in range(len(sHelpers) - len(gHelpers))]) 191 | randomCount = newRandomCount = len(sHelpers) - len(gHelpers) 192 | elif len(gHelpers) > len(sHelpers): 193 | sHelpers |= set([("new_fun" + str(i), None) for i in range(len(gHelpers) - len(sHelpers))]) 194 | nCount = len(gHelpers) - len(sHelpers) 195 | 196 | # First, track down vars which are going to conflict with built-in names in the goal state 197 | starterPairs = [] 198 | sList, gList, nList = list(sHelpers), list(gHelpers), list(nonMappableHelpers) 199 | i = 0 200 | while i < len(sList): 201 | for j in range(len(nList)): 202 | if sList[i][1] == nList[j][0]: # if the variable will conflict with a built-in name 203 | if randomCount > 0: # match the last random var to this var 204 | starterPairs.append((sList[i][0], "random_fun" + str(randomCount-1))) 205 | sList.pop(i) 206 | gList.remove(("random_fun" + str(randomCount-1), None)) 207 | randomCount -= 1 208 | i -= 1 # since we're popping, make sure to check the next one 209 | break 210 | else: # generate a new random var and replace the current pos with a new n 211 | starterPairs.append((sList[i][0], "random_fun" + str(newRandomCount))) 212 | sList[i] = ("new_fun" + str(nCount), None) 213 | newRandomCount += 1 214 | nCount += 1 215 | break 216 | i += 1 217 | # Get rid of the original names now 218 | sList = [x[0] for x in sList] 219 | gList = [x[0] for x in gList] 220 | 221 | listOfMaps = generateMappings(sList, gList) 222 | allMaps = [] 223 | for map in listOfMaps: 224 | d = { } 225 | for tup in map: 226 | d[tup[1]] = tup[0] 227 | allMaps.append(d) 228 | allFuns = [] 229 | for map in allMaps: 230 | tmpTree = deepcopy(g.tree) 231 | tmpTree = applyHelperMap(tmpTree, map) 232 | tmpCode = printFunction(tmpTree) 233 | 234 | matches = list(filter(lambda x : x.code==tmpCode, goals)) 235 | if len(matches) > 0: 236 | matches = sorted(matches, key=lambda s: getattr(s, "count")) 237 | tmpG = matches[-1] 238 | tmpG.tree = str_to_tree(tmpG.tree_source) 239 | allFuns.append(tmpG) 240 | else: 241 | tmpG = CanonicalState(code=tmpCode, problem=s.problem, count=0) 242 | tmpG.tree = tmpTree 243 | tmpG.tree_source = tree_to_str(tmpTree) 244 | tmpG.treeWeight = g.treeWeight 245 | tmpG = codetest(tmpG) 246 | if tmpG.score != 1: 247 | log("generateNextStates\tgenerateHelperDistributions\tBad helper remapping: " + str(map), "bug") 248 | log(s.code, "bug") 249 | log(printFunction(s.orig_tree), "bug") 250 | log(g.code, "bug") 251 | log(tmpCode, "bug") 252 | allFuns.append(tmpG) 253 | goals.append(tmpG) 254 | states.append(tmpG) 255 | return allFuns 256 | 257 | def generateVariableDistributions(s, g, goals, states): 258 | sParameters = gatherAllParameters(s.tree) 259 | gParameters = gatherAllParameters(g.tree, keep_orig=False) 260 | restricted_names = list(eval(s.problem.arguments).keys()) + getAllImports(s.tree) + getAllImports(g.tree) 261 | sHelpers = gatherAllHelpers(s.tree, restricted_names) 262 | gHelpers = gatherAllHelpers(g.tree, restricted_names) 263 | sVariables = gatherAllVariables(s.tree) 264 | gVariables = gatherAllVariables(g.tree, keep_orig=False) 265 | # First, just make extra sure none of the restricted names are included 266 | for name in restricted_names: 267 | for item in sVariables: 268 | if name == item[0]: 269 | sVariables.remove(item) 270 | break 271 | for item in gVariables: 272 | if name == item[0]: 273 | gVariables.remove(item) 274 | break 275 | for pair in sParameters | sHelpers: # make sure to remove all matches, regardless of whether the second part matches! 276 | for item in sVariables: 277 | if pair[0] == item[0]: 278 | sVariables.remove(item) 279 | break 280 | for pair in gParameters | gHelpers: # make sure to remove all matches, regardless of whether the second part matches! 281 | for item in gVariables: 282 | if pair[0] == item[0]: 283 | gVariables.remove(item) 284 | break 285 | nonMappableVariables = gatherAllNames(g.tree, keep_orig=False) 286 | for pair in gVariables | gParameters | gHelpers: # make sure to remove all matches, regardless of whether the second part matches! 287 | for item in nonMappableVariables: 288 | if pair[0] == item[0]: 289 | nonMappableVariables.remove(item) 290 | break 291 | randomCount = nCount = newRandomCount = 0 292 | if len(sVariables) > len(gVariables): 293 | randomCount = newRandomCount = len(sVariables) - len(gVariables) 294 | gVariables |= set([("random" + str(i), None) for i in range(len(sVariables) - len(gVariables))]) 295 | elif len(gVariables) > len(sVariables): 296 | nCount = len(gVariables) - len(sVariables) 297 | sVariables |= set([("n" + str(i) + "_global", None) for i in range(len(gVariables) - len(sVariables))]) 298 | 299 | # First, track down vars which are going to conflict with built-in names in the goal state 300 | starterPairs = [] 301 | sList, gList, nList = list(sVariables), list(gVariables), list(nonMappableVariables) 302 | i = 0 303 | while i < len(sList): 304 | for j in range(len(nList)): 305 | if sList[i][1] == nList[j][0]: # if the variable will conflict with a built-in name 306 | if randomCount > 0: # match the last random var to this var 307 | starterPairs.append((sList[i][0], "random" + str(randomCount-1))) 308 | sList.pop(i) 309 | gList.remove(("random" + str(randomCount-1), None)) 310 | randomCount -= 1 311 | i -= 1 # since we're popping, make sure to check the next one 312 | break 313 | else: # generate a new random var and replace the current pos with a new n 314 | starterPairs.append((sList[i][0], "random" + str(newRandomCount))) 315 | sList[i] = ("n" + str(nCount) + "_global", None) 316 | newRandomCount += 1 317 | nCount += 1 318 | break 319 | i += 1 320 | # Get rid of the original names now 321 | sList = [x[0] for x in sList] 322 | gList = [x[0] for x in gList] 323 | if max(len(sVariables), len(gVariables)) > 6: 324 | # If it's too large, just do the obvious one-to-one mapping. 325 | listOfMaps = [[(sList[i], gList[i]) for i in range(len(sList))]] 326 | else: 327 | listOfMaps = generateMappings(sList, gList) 328 | allMaps = [] 329 | placeholdCount = 0 330 | badMatches = set() 331 | for map in listOfMaps: 332 | d = { } 333 | for pair in starterPairs: # these apply to all of them 334 | d[pair[1]] = pair[0] 335 | for tup in map: 336 | # Don't allow variable matching across functions!!! This just messes things up. 337 | if getParentFunction(tup[0]) != getParentFunction(tup[1]) and getParentFunction(tup[0]) != None and getParentFunction(tup[1]) != None: 338 | badMatches.add(tup[0]) 339 | badMatches.add(tup[1]) 340 | d["z" + str(placeholdCount) + "_newvar"] = tup[0] 341 | placeholdCount += 1 342 | d[tup[1]] = "z" + str(placeholdCount) + "_newvar" 343 | placeholdCount += 1 344 | else: 345 | d[tup[1]] = tup[0] 346 | placeholdCount = 0 347 | allMaps.append(d) 348 | allFuns = [] 349 | for map in allMaps: 350 | tmpTree = deepcopy(g.tree) 351 | tmpTree = applyVariableMap(tmpTree, map) 352 | tmpCode = printFunction(tmpTree) 353 | 354 | matches = list(filter(lambda x : x.code==tmpCode, goals)) 355 | if len(matches) > 0: 356 | matches = sorted(matches, key=lambda s: getattr(s, "count")) 357 | tmpG = matches[-1] 358 | tmpG.tree = str_to_tree(tmpG.tree_source) 359 | allFuns.append(tmpG) 360 | else: 361 | tmpG = CanonicalState(code=tmpCode, problem=s.problem, count=0) 362 | tmpG.tree = tmpTree 363 | tmpG.tree_source = tree_to_str(tmpTree) 364 | tmpG.treeWeight = g.treeWeight 365 | tmpG = codetest(tmpG) 366 | if tmpG.score != 1: 367 | log("generateNextStates\tgenerateVariablesDistributions\tBad variable remapping: " + str(map), "bug") 368 | log(s.code, "bug") 369 | log(printFunction(s.orig_tree), "bug") 370 | log(g.code, "bug") 371 | log(tmpCode, "bug") 372 | allFuns.append(tmpG) 373 | goals.append(tmpG) 374 | states.append(tmpG) 375 | return allFuns 376 | 377 | def generateMappings(s, g): 378 | if len(s) == 0: 379 | return [[]] 380 | allMaps = [] 381 | for i in range(len(g)): 382 | thisMap = (s[0], g[i]) 383 | restMaps = generateMappings(s[1:], g[:i] + g[i+1:]) 384 | if s[0] != g[i]: # only need to include maps that aren't changing the variables 385 | for map in restMaps: 386 | map.append(copy.deepcopy(thisMap)) 387 | allMaps += restMaps 388 | return allMaps 389 | 390 | def optimizeGoal(s, changes, states, goals): 391 | """To optimize the goal, we will work our way up through possible combinations of edits, stopping when we reach 392 | a distance that we know is optimal""" 393 | currentGoal, currentDiff, currentEdits = s.goal, s.goalDist, changes # set up values that will change 394 | allChanges = [] 395 | class Branch: # use this to hold branches 396 | def __init__(self, edits, next, state): 397 | self.edits = edits 398 | self.next = next 399 | self.state = state 400 | 401 | treeLevel = [Branch([], changes, s)] 402 | # Until you've run out of possible goal states... 403 | while len(treeLevel) != 0: 404 | nextLevel = [] 405 | # Look at each number of combinations of edits 406 | for branch in treeLevel: 407 | # Apply each possible next edit 408 | for i in range(len(branch.next)): 409 | newChanges = branch.edits + [branch.next[i]] 410 | # If our current best is in this, don't bother 411 | if isStrictSubset(currentEdits, newChanges): continue 412 | 413 | # Check to see that the state exists and that it isn't too far away 414 | newState = applyChangeVectors(s, newChanges, states, goals) 415 | if newState == None: # shouldn't happen 416 | log("generateNextStates\toptimizeGoal\tBroken edit: " + str(newChanges), "bug") 417 | continue 418 | newDistance, _ = distance(s, newState, givenChanges=newChanges) 419 | 420 | allChanges.append((newChanges, newState)) # just in case we need the final goal 421 | 422 | if newState.score == 1 and newDistance <= currentDiff: # it's a new goal! 423 | # We know that it's closer because we just tested distance 424 | currentGoal, currentDiff, currentEdits = newState, newDistance, newChanges 425 | else: 426 | # Only include changes happening after this one to avoid ordering effects! 427 | # We only add a state here if it's closer than the current goal 428 | nextLevel.append(Branch(newChanges, branch.next[i+1:], newState)) 429 | treeLevel = nextLevel 430 | 431 | if s.goal.code == currentGoal.code: 432 | return allChanges # optimize so we don't need to do the power set twice 433 | else: 434 | s.goal, s.goalDist = currentGoal, currentDiff # otherwise, put in the new goal 435 | 436 | def fastOptimizeGoal(s, changes, states, goals, includeSmallSets=False): 437 | # Only try out one, two, all but two, all but one 438 | fastChanges = fastPowerSet(changes, includeSmallSets) 439 | currentGoal, currentDiff, currentEdits = s.goal, s.goalDist, changes 440 | for changeSet in fastChanges: 441 | if isStrictSubset(currentEdits, changeSet): continue 442 | newState = applyChangeVectors(s, changeSet, states, goals) 443 | if newState == None: continue 444 | newDistance, _ = distance(s, newState, givenChanges=changeSet) 445 | if newDistance <= currentDiff and newState.score == 1: 446 | # Just take the first one we find 447 | currentGoal, currentDiff, currentEdits = newState, newDistance, changeSet 448 | break 449 | if s.goal.code == currentGoal.code: 450 | return None 451 | else: 452 | s.goal, s.goalDist = currentGoal, currentDiff 453 | return currentEdits 454 | 455 | def isValidNextState(s, n, g): 456 | """Checks the three rules for valid next states""" 457 | 458 | # We can't use the state itself! 459 | if s == n: 460 | return False 461 | # First: is the state well-formed? 462 | if n == None: 463 | return False 464 | 465 | # Now test loadable 466 | try: 467 | ast.parse(n.code) 468 | except Exception as e: 469 | return False # didn't load properly 470 | 471 | # Third: is test.test(n) >= test.test(s)? 472 | n = codetest(n) 473 | if n.score < s.score and abs(n.score - s.score) > 0.001: 474 | return False 475 | 476 | # Loadable technically falls here, but it takes a while 477 | # so filter with diff first 478 | # Second: is diff(n, g) < diff(s, g)? 479 | if n.score != 1 and n != g: 480 | n.goal = g 481 | n.goalDist, _ = distance(n, g) 482 | if n.goalDist >= s.goalDist: 483 | return False 484 | 485 | # If we pass all the checks, it's a valid state 486 | return True 487 | 488 | def generateStatesInPath(s, goals, states, validCombinations): 489 | # Now we need to find the desirability of each state and take the best one 490 | # We'll keep cycling here to find the whole path of states 'til we get to the correct solution 491 | originalS = s 492 | while s.score != 1: 493 | bestScore, bestState = -1, None 494 | for (c,n) in validCombinations: 495 | score = desirability(s, n, s.goal) 496 | if score > bestScore: 497 | bestScore, bestState = score, (c,n) 498 | 499 | if bestState == None: 500 | log("Path Construction\tgetNextState\t" + str(s.id) + " could not find best next out of " + str(len(validCombinations)) + " combinations", "bug") 501 | if s != originalS: 502 | getNextState(s, goals, states) # start over with the broken state- resetting the diff will probably help 503 | else: 504 | log("Path Construction\tgetNextState\tPermanently stuck", "bug") 505 | break 506 | (s.edit, s.next) = bestState 507 | if s.next.score != 1: 508 | validCombinations.remove(bestState) 509 | validCombinations = filterChanges(validCombinations, s.edit, s, s.next) 510 | s = s.next 511 | 512 | def getAllCombinations(s, changes, states, goals): 513 | allChanges = powerSet(changes) 514 | # Also find the solution states associated with the changes 515 | allCombinations = [] 516 | for x in allChanges: 517 | allCombinations.append((x, applyChangeVectors(s, x, states, goals))) 518 | return allCombinations 519 | 520 | def getNextState(s, goals, states, given_goal=None): 521 | """Generate the best next state for s, so that it will produce a desirable hint""" 522 | s.goal = chooseGoal(s, goals, states) if given_goal == None else given_goal 523 | if s.goal == None: 524 | log("Path Construction\tgetNextState\tno goal found\t" + s.problem.name, "bug") 525 | return 526 | (s.goalDist, changes) = distance(s, s.goal) # now get the actual changes 527 | 528 | firstRound = True 529 | while len(changes) > 3: # might as well go with this while we can, since it's faster 530 | fastChanges = fastOptimizeGoal(s, changes, states, goals, includeSmallSets=firstRound) 531 | firstRound = False 532 | if fastChanges == None: 533 | if len(changes) > 6: # Cut off at 6 because 2^6 = 64 * 0.1s per test = 6 seconds at worst 534 | # Just say that the next state is the goal. 535 | s.next = s.goal 536 | return 537 | else: 538 | break 539 | else: 540 | changes = fastChanges 541 | 542 | # Now, update the goal by optimizing for it 543 | allCombinations = optimizeGoal(s, changes, states, goals) 544 | if allCombinations == None: # There's an optimized goal 545 | # Let's get the new change vectors! 546 | changes = getChanges(s.tree, s.goal.tree) 547 | allCombinations = getAllCombinations(s, changes, states, goals) 548 | 549 | s.changesToGoal = len(changes) 550 | 551 | # Now check for the required properties of a next state. Filter before sorting to save time 552 | validCombinations = filter(lambda x : isValidNextState(s, x[1], s.goal), allCombinations) 553 | # Order based on the longest-changes first, but with edits in order 554 | validCombinations = sorted(validCombinations, key=lambda x: len(x)) 555 | 556 | if len(validCombinations) == 0: 557 | # No possible changes can be made 558 | log("Path Construction\tgetNextState\t" + str(s.code) + "\t" + str(s.goal.code) + " no valid combinations", "bug") 559 | s.next = None 560 | return 561 | 562 | generateStatesInPath(s, goals, states, validCombinations) 563 | -------------------------------------------------------------------------------- /hintgen/paths.py: -------------------------------------------------------------------------------- 1 | DATA_PATH = "hintgen/data/" 2 | LOG_PATH = "hintgen/log/" 3 | TEST_PATH = "hintgen/test/" -------------------------------------------------------------------------------- /hintgen/test/__init__.py: -------------------------------------------------------------------------------- 1 | import ast, traceback 2 | from .testHarness import * 3 | from ..display import * 4 | from ..namesets import * 5 | from ..models import * 6 | 7 | loaded_pairs = { } # Keep track of pairs for efficiency 8 | 9 | def test(s, forceRetest=False): 10 | """A method for testing solution states, which returns a number between 11 | 0 (totally wrong) and 1 (correct)""" 12 | if forceRetest: 13 | if hasattr(s, "loadedFun"): 14 | del s.loadedFun 15 | s.score = None 16 | s.feedback = "" 17 | if (s.score != None and s.feedback != ""): 18 | return s 19 | 20 | if s.tree != None: 21 | replaceHazards(s.tree) 22 | s.code = printFunction(s.tree, 0) 23 | 24 | # If necessary, load the tests 25 | if s.problem.id not in loaded_pairs: 26 | tests = s.problem.tests.all() 27 | for i in range(len(tests)): 28 | # Need to interpret from repr 29 | try: 30 | tests[i].input = eval(tests[i].test_input) 31 | tests[i].output = eval(tests[i].test_output) 32 | except: 33 | s.score = 0 34 | if not hasattr(tests[i], "input"): 35 | s.feedback = "Broken test case input: " + tests[i].test_input + "\nExpecting a tuple of values." 36 | else: 37 | s.feedback = "Broken test case output: " + tests[i].test_output + "\nExpecting a legal Python value." 38 | return s 39 | loaded_pairs[s.problem.id] = tests 40 | 41 | tests = loaded_pairs[s.problem.id] 42 | s.num_pairs = len(tests) 43 | try: 44 | ast.parse(s.code) 45 | s.score, s.feedback = score(s, tests, returnFeedback=True) 46 | except Exception as e: # if the code doesn't parse, create a compiler error message 47 | s.score = 0 48 | trace = traceback.format_exc() 49 | lines = trace.split("\n") 50 | lines = lines[lines.index(" return compile(source, filename, mode, PyCF_ONLY_AST)")+1:] 51 | s.feedback = "COMPILER ERROR:\n" + str("\n".join(lines)) 52 | return s 53 | 54 | def replaceHazards(a): 55 | if not isinstance(a, ast.AST): 56 | return 57 | for field in ast.walk(a): 58 | if type(a) == ast.Import: 59 | for i in range(len(a.names)): 60 | if a.names[i].name not in supportedLibraries: 61 | if not (a.names[i].name[0] == "r" and a.names[i].name[1] in "0123456789") and not ("NotAllowed" in a.names[i].name): 62 | a.names[i].name = a.names[i].name + "NotAllowed" 63 | elif type(a) == ast.ImportFrom: 64 | if a.module not in supportedLibraries: 65 | if not (a.module[0] == "r" and a.module[1] in "0123456789") and not ("NotAllowed" in a.module): 66 | a.module = a.module + "NotAllowed" 67 | elif type(a) == ast.Call: 68 | if type(a.func) == ast.Name and a.func.id in ["compile", "eval", "execfile", "file", "open", "__import__", "apply"]: 69 | a.func.id = a.func.id + "NotAllowed" 70 | -------------------------------------------------------------------------------- /hintgen/test/testHarness.py: -------------------------------------------------------------------------------- 1 | import copy, ctypes, multiprocessing, os, random, io, sys, threading, time, ast 2 | import importlib.util 3 | from ..tools import log 4 | from ..paths import TEST_PATH 5 | 6 | done = False 7 | msg_length = 400 8 | 9 | def timerDone(): 10 | global done 11 | done = True 12 | 13 | def manageException(e, errors, input, output, actual): 14 | if type(e) == AssertionError: 15 | i = repr(input) 16 | i = i if len(i) < 100 else i[:96] + "...]" 17 | i = i[1:] if (i[0] == 'u' and i[1] == "'" and i[-1] == "'") else i 18 | o = repr(output) 19 | o = o if len(o) < 100 else o[:97] + "..." 20 | o = o[1:] if (o[0] == 'u' and o[1] == "'" and o[-1] == "'") else o 21 | a = repr(actual) 22 | a = a if len(a) < 100 else a[:97] + "..." 23 | errors.append("Failed assertion: given input (" + i[1:-1] + "), expected output " + o + ", actual output " + a ) # 364 24 | else: 25 | i = repr(input) 26 | i = i if len(i) < 100 else i[:97] + "..." 27 | emsg = str(e) 28 | emsg = emsg if len(emsg) < 100 else emsg[:97] + "..." 29 | errors.append("Test function with input (" + i[1:-1] + ") broke with error: " + emsg) # 245 30 | 31 | def checkCopy(f, input): 32 | cp = copy.deepcopy(input) 33 | f(*cp) 34 | return cp == input 35 | 36 | def load_file(tmpFile, tmpFull): 37 | failed = False 38 | # Then try to load the function 39 | try: 40 | spec = importlib.util.spec_from_file_location(tmpFile, tmpFull + ".py") 41 | mod = importlib.util.module_from_spec(spec) 42 | spec.loader.exec_module(mod) 43 | except Exception as e: 44 | mod = None 45 | failed = True 46 | return mod, failed 47 | 48 | def textToFunction(s): 49 | if s.loadedFun != None: 50 | return s.loadedFun 51 | instructorFunctions = s.problem.given_code 52 | # Create a file that can be loaded, with the function in it 53 | tmpFile = "tmp" + str(random.randint(0,100000)) 54 | tmpPath = TEST_PATH + "tmp" 55 | tmpFull = tmpPath + "/" + tmpFile 56 | tmpCache = tmpPath + "/" + "__pycache__" + "/" + tmpFile 57 | try: 58 | f = open(tmpFull + ".py", "w") 59 | except: 60 | s.feedback = "ERROR: could not write file, please change permissions in the test/tmp folder" 61 | return None 62 | # Set this up to look like Python 3 63 | #f.write("from __future__ import (absolute_import, division, print_function)\n") 64 | f.write(s.code) 65 | if len(instructorFunctions) != 0: 66 | f.write("\n\n" + instructorFunctions) 67 | f.close() 68 | 69 | # Try loading the file in a timed thread in case the file calls infinitely-looping code 70 | p = multiprocessing.Process(target=load_file, args=(tmpFile, tmpFull)) 71 | result = run_in_timer(p, 0.1) 72 | if result != "Success": 73 | log("testHarness\ttextToFunction\tTimer problem: " + result + "\n" + s.code, "bug") 74 | s.feedback = result 75 | return None 76 | else: 77 | out = sys.stdout 78 | err = sys.stderr 79 | sys.stdout = io.StringIO() 80 | sys.stderr = io.StringIO() 81 | mod, failed = load_file(tmpFile, tmpFull) 82 | sys.stdout = out 83 | sys.stderr = err 84 | 85 | # Clean up the extra files 86 | if os.path.exists(tmpFull + ".py"): 87 | os.remove(tmpFull + ".py") 88 | if os.path.exists(tmpFull + ".pyc"): 89 | os.remove(tmpFull + ".pyc") 90 | for version in range(10): # Python 3 caches in a separate folder and includes the version number 91 | name = tmpCache + ".cpython-3" + str(version) + ".pyc" 92 | if os.path.exists(name): 93 | os.remove(name) 94 | 95 | if failed: 96 | s.feedback = "ERROR: could not load function, possibly due to compiler error in instructorFunctions" 97 | return None 98 | 99 | # Load the resulting function from the file. It will have references to all necessary helpers 100 | if hasattr(mod, s.problem.name): 101 | loaded = getattr(mod, s.problem.name) 102 | s.loadedFun = loaded 103 | else: 104 | s.feedback = "ERROR: could not find required function in code" 105 | return None 106 | 107 | return s.loadedFun 108 | 109 | def __genericTest__(f, input, output): 110 | errors = [] 111 | answer = None 112 | input_copy = copy.deepcopy(input) 113 | try: 114 | answer = f(*input) 115 | if type(output) == float: 116 | assert(abs(answer - output) < 0.001) 117 | else: 118 | assert(answer == output) 119 | except Exception as e: 120 | manageException(e, errors, input_copy, output, answer) 121 | return errors 122 | 123 | def runFunction(s, tests, score, feedback=None): 124 | f = textToFunction(s) # first, load the function 125 | for i in range(len(tests)): 126 | test = tests[i] 127 | if test.test_extra == "check_copy": 128 | inp = [f] + [test.input] 129 | elif test.test_extra == "": 130 | inp = test.input 131 | else: 132 | log("testHarness\trunFunction\tDid not recognize special function " + test.test_extra, "bug") 133 | return 134 | 135 | input_copy = copy.deepcopy(inp) 136 | errors = __genericTest__(f, inp, test.output) 137 | if len(errors) == 0: # if no problems occurred 138 | score.value = score.value + 1 139 | inp = repr(input_copy) 140 | inp = inp if len(inp) < 100 else inp[:97] + "..." 141 | inp = inp[1:] if (inp[0] == 'u' and inp[1] == "'" and inp[-1] == "'") else inp 142 | o = repr(test.output) 143 | o = o if len(o) < 100 else o[:97] + "..." 144 | o = o[1:] if (o[0] == 'u' and o[1] == "'" and o[-1] == "'") else o 145 | s = "Test passed on input (" + inp[1:-1] + "), expected output " + o + "\n" # 240 146 | else: 147 | s = errors[0] + "\n" 148 | if feedback != None: 149 | for j in range(len(s)): 150 | feedback[i*msg_length + j] = ord(s[j]) # add the string into the array 151 | 152 | def run_in_timer(proc, timerTime): 153 | global done 154 | out = sys.stdout 155 | err = sys.stderr 156 | sys.stdout = io.StringIO() 157 | sys.stderr = io.StringIO() 158 | timer = threading.Timer(timerTime, timerDone) 159 | timeout = False 160 | try: 161 | proc.start() 162 | timer.start() 163 | while (proc.is_alive()) and (not done): 164 | continue 165 | timer.cancel() 166 | # If the process is still running, kill it 167 | if proc.is_alive(): 168 | timeout = True 169 | try: 170 | proc.terminate() 171 | time.sleep(0.01) 172 | if proc.is_alive(): 173 | os.system('kill -9 ' + str(proc.pid)) 174 | except: 175 | log("testHarness\tscore\tThread is still alive!", "bug") 176 | while proc.is_alive(): 177 | time.sleep(0.01) 178 | sys.stdout = out 179 | sys.stderr = err 180 | done = False 181 | except Exception as e: 182 | log("testHarness\tscore\tBroken process: " + str(e), "bug") 183 | return "Broken Process" 184 | if timeout: 185 | return "Infinite loop! Code timed out after " + str(timerTime) + " seconds" 186 | else: 187 | return "Success" 188 | 189 | def contains_function(a, problem_name): 190 | for line in a.body: 191 | if type(line) == ast.FunctionDef and line.name == problem_name: 192 | return True 193 | return False 194 | 195 | def score(s, tests, returnFeedback=False): 196 | # Note that now, infinite loops will break all test cases that come after that. We're OK with this as long as we order test cases properly. 197 | if not hasattr(s, "loadedFun"): 198 | if not hasattr(s, "tree") or s.tree == None: 199 | try: 200 | tmpTree = ast.parse(s.code) 201 | except: 202 | s.feedback = "Could not load code" 203 | return (0, s.feedback) if returnFeedback else 0 204 | else: 205 | tmpTree = s.tree 206 | 207 | if contains_function(tmpTree, s.problem.name): 208 | s.loadedFun = None 209 | s.loadedFun = textToFunction(s) 210 | else: 211 | s.feedback = "ERROR: could not find required function in code" 212 | return (0, s.feedback) if returnFeedback else 0 213 | f = s.loadedFun 214 | 215 | if f == None: 216 | return (0, s.feedback) if returnFeedback else 0 217 | 218 | score = multiprocessing.Value(ctypes.c_float, 0.0, lock=False) 219 | if returnFeedback: 220 | # Allocate 250 chars for each feedback line 221 | feedback = multiprocessing.Array(ctypes.c_int, msg_length * len(tests), lock=False) 222 | p = multiprocessing.Process(target=runFunction, args=(s, tests, score, feedback)) 223 | else: 224 | p = multiprocessing.Process(target=runFunction, args=(s, tests, score)) 225 | 226 | test_result = run_in_timer(p, 0.1) 227 | if test_result != "Success": 228 | #log("testHarness\tscore\tTimer problem: " + test_result, "bug") 229 | result = 0 230 | msg = test_result 231 | else: 232 | if returnFeedback: 233 | msg = "" 234 | for i in range(len(tests)): 235 | j = 0 236 | while j < msg_length and feedback[i*msg_length + j] != 0: 237 | msg += chr(int(feedback[i*msg_length + j])) 238 | j += 1 239 | result = score.value / len(tests) 240 | return (result, msg) if returnFeedback else result 241 | 242 | -------------------------------------------------------------------------------- /hintgen/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /hintgen/tools.py: -------------------------------------------------------------------------------- 1 | """This is a file of useful functions used throughout the hint generation program""" 2 | import time, os.path, ast, json 3 | from .paths import * 4 | 5 | def log(msg, filename="main", newline=True): 6 | txt = "" 7 | if newline: 8 | t = time.strftime("%d %b %Y %H:%M:%S") 9 | txt += t + "\t" 10 | txt += msg 11 | if newline: 12 | txt += "\n" 13 | f = open(LOG_PATH + filename + ".log", "a") 14 | f.write(txt) 15 | f.close() 16 | 17 | def parse_table(filename): 18 | with open(filename, "r") as f: 19 | txt = f.read() 20 | return smart_parse(txt) 21 | 22 | def smart_parse(t, sep=","): 23 | """Parse a string into a 2d spreadsheet""" 24 | # First, fix stupid carriage return errors 25 | t = t.replace("\r\n", "\n").replace("\n\r", "\n").replace("\r", "\n") 26 | 27 | # A sheet is made of lines, which are made of tokens 28 | sheet, line, token = [], [], "" 29 | inString = False 30 | for i in range(len(t)): 31 | if t[i] == '"': 32 | # Keep track of strings so they can be parsed correctly 33 | inString = not inString 34 | continue 35 | 36 | if (not inString) and (t[i] == sep): 37 | line.append(token) 38 | token = "" 39 | elif (not inString) and (t[i] == "\n"): 40 | if len(token) > 0: 41 | line.append(token) 42 | token = "" 43 | sheet.append(line) 44 | line = [] 45 | else: 46 | token += t[i] 47 | 48 | # Catch any stragglers 49 | if len(token) > 0: 50 | line.append(token) 51 | if len(line) > 0: 52 | sheet.append(line) 53 | return sheet 54 | 55 | def powerSet(l): 56 | if len(l) == 0: return [[]] 57 | head = l[-1] 58 | rest = powerSet(l[:-1]) 59 | newL = [] 60 | for x in rest: 61 | newL.append(x) 62 | newL.append(x + [head]) 63 | return newL 64 | 65 | # Power Set for really large sets- just look at groups of 1, 2, n-1, and n-2 66 | def fastPowerSet(changes, includeSmallSets=True): 67 | single, allButOne = [], [] 68 | for i in range(len(changes)): 69 | if includeSmallSets: 70 | single.append([changes[i]]) 71 | allButOne.append(changes[:i] + changes[i+1:]) 72 | return single + allButOne 73 | 74 | def isStrictSubset(s1, s2): 75 | """A strict subset must be smaller than its superset""" 76 | if len(s1) == len(s2): return False 77 | return isSubset(s1, s2) 78 | 79 | def isSubset(s1, s2): 80 | """Returns whether s1 is a subset of s2""" 81 | if len(s1) == 0: return True 82 | if s1[0] in s2: 83 | i = s2.index(s1[0]) 84 | return isSubset(s1[1:], s2[:i] + s2[i+1:]) 85 | else: 86 | return False -------------------------------------------------------------------------------- /hintgen/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | url(r'^$', views.index, name='index'), 7 | url(r'^feedback/([0-9]+)/([0-9]+)/$', views.feedback, name="feedback"), 8 | url(r'^hint/([0-9]+)/([0-9]+)/$', views.hint, name="hint"), 9 | ] -------------------------------------------------------------------------------- /hintgen/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | from django.http import HttpResponse, HttpResponseBadRequest 3 | import json, random 4 | from .models import * 5 | from .getHint import get_hint, run_tests 6 | 7 | """ 8 | TESTING 9 | Use the following code to test the hint/feedback functions: 10 | 11 | from django.test import Client 12 | import json 13 | c = Client() 14 | course_id = 1 15 | problem_id = 4 16 | data = {'student_id' : 'tester', 'code' : "def canDrinkAlcohol(age, isDriving):\n return age > 21 and not isDriving\n" } 17 | response = c.post('/hintgen/hint/' + str(course_id) + '/' + str(problem_id) + '/', data=data) 18 | """ 19 | 20 | RUNNING_STUDY = False 21 | 22 | # Create your views here. 23 | def index(request): 24 | return HttpResponse("Hello, world. You've reached the hint generation index!") 25 | 26 | def check_work(request, course_id): 27 | data = unpack_student_json(request, course_id) 28 | 29 | if isinstance(data, HttpResponse): 30 | return data 31 | 32 | problems_attempted = 0 33 | problems_solved = 0 34 | time_spent = 0 35 | 36 | # Find the number of problems attempted and solved by the student 37 | course_problems = Problem.objects.filter(courses__in=[data["course"]]) 38 | for problem in course_problems: 39 | student_submissions = SourceState.objects.filter(student=data["student"], problem=problem) 40 | if len(student_submissions) > 0: 41 | problems_attempted += 1 42 | student_solves = SourceState.objects.filter(student=data["student"], problem=problem, score=1.0) 43 | if len(student_solves) > 0: 44 | problems_solved += 1 45 | 46 | # Find the amount of time spent by the student 47 | all_student_submissions = SourceState.objects.filter(student=data["student"]) 48 | ordered_submissions = SourceState.objects.order_by('timestamp') 49 | time_cutoff = 60*10 # ten minutes 50 | for i in range(1, len(ordered_submissions)): 51 | time_diff = (ordered_submissions[i].timestamp - 52 | ordered_submissions[i-1].timestamp).total_seconds() 53 | if time_diff > time_cutoff: 54 | time_spent += time_cutoff 55 | else: 56 | time_spent += time_diff 57 | 58 | if problems_solved >= 25: 59 | message = "You have solved all the problems; you're done!" 60 | elif time_spent >= 60*60*2: 61 | message = "You have spent at least two hours in the system; you're done!" 62 | else: 63 | message = "You have not spent at least two hours or completed all the problems yet. Keep working!" 64 | 65 | full_message = "
" + message + "
" 66 | full_message += "Problems Solved: " + str(problems_solved) + "/25
" 67 | full_message += "Problems Attempted: " + str(problems_attempted) + "/25
" 68 | full_message += "Time Spent: " + str(time_spent//60) + " minutes
" 69 | return HttpResponse(full_message) 70 | 71 | def unpack_student_json(request, course_name): 72 | data = request.POST 73 | if 'student_id' not in data: 74 | return HttpResponseBadRequest("Need to include a reference to 'student_id' in the json object") 75 | 76 | try: 77 | course_id = int(course_name) 78 | course = Course.objects.filter(id=course_id) 79 | if len(course) != 1: 80 | return HttpResponseBadRequest("No course exists with that ID") 81 | except: 82 | course = Course.objects.filter(name=course_name) 83 | if len(course) != 1: 84 | return HttpResponseBadRequest("No course exists with that name") 85 | course = course[0] 86 | 87 | student = Student.objects.filter(name=data["student_id"]) 88 | if len(student) == 0: 89 | # We haven't seen this student before, but it's okay; we can add them 90 | 91 | # SORRY HARDCODED STUDY STUFF 92 | condition = "hints_first" if random.random() < 0.5 else "hints_second" 93 | 94 | student = Student(course=course, name=data["student_id"], condition=condition) 95 | student.save() 96 | elif len(student) > 1: 97 | # Multiple students with the same name! Uh oh. 98 | return HttpResponseBadRequest("Could not disambiguate student name; please modify database") 99 | else: 100 | student = student[0] 101 | 102 | if student.condition not in ["hints_first", "hints_second"]: 103 | condition = "hints_first" if random.random() < 0.5 else "hints_second" 104 | student.condition = condition 105 | student.save() 106 | 107 | 108 | data["course"] = course 109 | data["student"] = student 110 | return data 111 | 112 | def unpack_code_json(request, course_name, problem_name): 113 | # request_body = request.body.decode('utf-8') 114 | # if len(request_body) == 0: 115 | # return HttpResponseBadRequest("Empty request body; need to include a json object") 116 | # data = json.loads(request_body) 117 | data = request.POST 118 | if 'code' not in data: 119 | return HttpResponseBadRequest("Need to include a reference to 'code' in the json object") 120 | 121 | if 'student_id' not in data: 122 | return HttpResponseBadRequest("Need to include a reference to 'student_id' in the json object") 123 | 124 | try: 125 | course_id = int(course_name) 126 | course = Course.objects.filter(id=course_id) 127 | if len(course) != 1: 128 | return HttpResponseBadRequest("No course exists with that ID") 129 | except: 130 | course = Course.objects.filter(name=course_name) 131 | if len(course) != 1: 132 | return HttpResponseBadRequest("No course exists with that name") 133 | course = course[0] 134 | 135 | try: 136 | problem_id = int(problem_name) 137 | problem = Problem.objects.filter(id=problem_id) 138 | if len(problem) != 1: 139 | return HttpResponseBadRequest("No problem exists with that ID") 140 | except: 141 | problem = Problem.objects.filter(name=problem_name) 142 | if len(problem) != 1: 143 | return HttpResponseBadRequest("No problem exists with that name") 144 | problem = problem[0] 145 | 146 | student = Student.objects.filter(name=data["student_id"]) 147 | if len(student) == 0: 148 | # We haven't seen this student before, but it's okay; we can add them 149 | 150 | # SORRY HARDCODED STUDY STUFF 151 | condition = "hints_first" if random.random() < 0.5 else "hints_second" 152 | 153 | student = Student(course=course, name=data["student_id"], condition=condition) 154 | student.save() 155 | elif len(student) > 1: 156 | # Multiple students with the same name! Uh oh. 157 | return HttpResponseBadRequest("Could not disambiguate student name; please modify database") 158 | else: 159 | student = student[0] 160 | 161 | if student.condition not in ["hints_first", "hints_second"]: 162 | condition = "hints_first" if random.random() < 0.5 else "hints_second" 163 | student.condition = condition 164 | student.save() 165 | 166 | 167 | data["course"] = course 168 | data["problem"] = problem 169 | data["student"] = student 170 | # Clean up return carriages 171 | data["code"] = data["code"].replace("\r\n", "\n").replace("\n\r", "\n").replace("\r", "\n") 172 | return data 173 | 174 | """ 175 | Test the given code and generate feedback for it. 176 | 177 | USAGE 178 | In the url, map: 179 | course_id -> the course ID for this submission 180 | problem_id -> the problem ID for this submission 181 | In the request content, include a json object mapping: 182 | student_id -> the student ID for this submission 183 | code -> the code being submitted 184 | 185 | RETURNS 186 | A json object mapping: 187 | score -> the resulting score for this submission 188 | feedback -> the resulting feedback message for this submission 189 | """ 190 | def feedback(request, course_id, problem_id): 191 | data = unpack_code_json(request, course_id, problem_id) 192 | 193 | if isinstance(data, HttpResponse): 194 | return data 195 | 196 | code_state = SourceState(code=data["code"], problem=data["problem"], 197 | student=data["student"], count=1) 198 | 199 | code_state = run_tests(code_state) 200 | test_results = code_state.feedback.replace("\n", "