from bdb import Bdb import inspect from itertools import imap, dropwhile, permutations import pydoc import re import sys import traceback import timeit import wrapt import funcfinder.answers import funcfinder.questions from utils import TryImportError def search_questions(terms): if isinstance(terms, basestring): terms = terms.split() terms = map(str.lower, terms) print found = False for question in funcfinder.questions.functions.itervalues(): search_string = (question.__name__ + question.__doc__).lower() if all(imap(search_string.__contains__, terms)): found = True print question.__name__ + ":\n" print pydoc.getdoc(question) print "\n-----------------------\n" if not found: print "No questions found" def _show_dependencies(dependencies, existing_sources): found_dependency = False if dependencies: for dependency in dependencies: dependency_source = _get_source(dependency) if dependency_source in existing_sources: continue spaceless_dependency_source = re.sub("\s+", "", dependency_source) found_matching_source = False for existing_source in existing_sources: spaceless_existing_source = re.sub("\s+", "", existing_source) if spaceless_dependency_source in spaceless_existing_source: found_matching_source = True break if found_matching_source: continue if not found_dependency: print "Dependencies:" print found_dependency = True _show_source(dependency, dependency_source) def show_question(question, time_answers=True): print if isinstance(question, basestring): try: question = funcfinder.questions.functions[question] except KeyError: print "No question with name %s found" % question return sources = set() dependencies = set() _show_source_and_add_to_set(question, sources) if hasattr(question, "answers"): print "Answers:" print correct_answers = [] for answer in question.answers: dependencies.update(_CodeDetector.detect(question, answer, include_questions=True)) _show_source_and_add_to_set(answer, sources) try: question(answer) print "Passed tests successfully." print "--------------------------" print correct_answers.append(answer) except Exception as e: print "Failed tests with exception:" if not isinstance(e, TryImportError): tb_list = traceback.extract_tb(sys.exc_info()[2]) tb_list = list(dropwhile(lambda entry: entry[2] != question.__name__, tb_list)) print "".join(traceback.format_list(tb_list)).rstrip() print "".join(traceback.format_exception_only(*sys.exc_info()[:2])) if time_answers: _time_answers(question, correct_answers) _show_dependencies(dependencies, sources) else: print "No answers have been marked as solving this question, which is a problem." print "The question will now be asked manually. If any solutions are found, please contribute by adding:" print print "@solves(q.%s)" % question.__name__ print print "to each solution." print ask(question, time_answers=time_answers) def _get_source(func, index_permutation=None): try: name = func.__name__ except AttributeError: name = func.co_name pattern = r"(def\s+%s\(.+)" % name regex = re.compile(pattern, re.DOTALL) source = inspect.getsource(func).strip() match = regex.search(source) if match is not None: source = match.group(1) if index_permutation and len(index_permutation) > 1 and list(index_permutation) != sorted(index_permutation): pattern = r"(def\s+%s)\((.+?)\)(.+)" % name regex = re.compile(pattern, re.DOTALL) match = regex.search(source) if match is None: raise Exception("Failed to extract arguments from function definition:\n" + source) args = re.split(r"\s*,\s*", match.group(2), flags=re.DOTALL) args = _permute(args, index_permutation) args = ", ".join(args) source = "{start}({args}){rest}".format(start=match.group(1), args=args, rest=match.group(3)) return source def _show_source(func, source): print inspect.getsourcefile(func), ":", inspect.getsourcelines(func)[1] print source print def _show_source_and_add_to_set(func, sources_set, index_permutation=None): source = _get_source(func, index_permutation) sources_set.add(source) _show_source(func, source) def _time(question, answer, number=1): def stmt(): question(answer) if number == 1: time_taken = 0 while time_taken < 1: number *= 2 time_taken = timeit.timeit(stmt, number=number) return min(timeit.repeat(stmt, number=number, repeat=5)), number def _time_answers(question, correct_answers): if len(correct_answers) > 1: print "Best times per answer:" number = 1 for answer in correct_answers: time_taken, number = _time(question, answer, number) print "%s: %.3f s" % (answer.__name__, time_taken) print "(among 5 sets of %i repetitions)" % number print def ask(question, time_answers=True): num_args_holder = [] def count_expected_args(*args): num_args_holder.append(len(args)) exc_info = None try: question(count_expected_args) except Exception: exc_info = sys.exc_info() pass if not num_args_holder: if exc_info: raise exc_info[0], exc_info[1], exc_info[2] else: raise AssertionError("Failed to find the number of arguments the answer must have. " "Did you call the given function?") del exc_info num_args = num_args_holder[0] index_permutations = list(permutations(range(num_args))) correct_answers = [] dependencies = set() sources = set() for answer in funcfinder.answers.functions.itervalues(): if not getattr(answer, "ask_ignore", False) and answer.func_code.co_argcount == num_args: for index_permutation in index_permutations: try: permuted_answer = _permute_args(index_permutation)(answer) question(permuted_answer) except (_ForbiddenKwargs, _WrongNumberOfArgs) as e: print e.message return except Exception: pass else: _show_source_and_add_to_set(answer, sources, index_permutation) solved_questions = getattr(answer, "solved_questions") if solved_questions: print "Solves the question%s %s" % ( "s" * (len(solved_questions) > 1), ", ".join(q.__name__ for q in solved_questions)) print print "-------------------------" print correct_answers.append(permuted_answer) dependencies.update(_CodeDetector.detect(question, permuted_answer, include_questions=False)) dependencies.discard(answer.func_code) break if not correct_answers: print "Sorry, no correct answers found. If you find one, please consider contributing it!" return if time_answers: _time_answers(question, correct_answers) _show_dependencies(dependencies, sources) def _permute_args(index_permutation): @wrapt.decorator def wrapper(wrapped, _, args, kwargs): if kwargs: raise _ForbiddenKwargs if len(index_permutation) != len(args): raise _WrongNumberOfArgs return wrapped(*_permute(args, index_permutation)) return wrapper def _permute(it, index_permutation): return (it[i] for i in index_permutation) class _ForbiddenKwargs(Exception): message = "You cannot ask for a function with keyword arguments." class _WrongNumberOfArgs(Exception): message = "The function you ask for must always have the same number of arguments." class _CodeDetector(Bdb): def __init__(self, *args): Bdb.__init__(self, *args) self.codes = set() def do_clear(self, arg): pass def user_call(self, frame, argument_list): self.codes.add(frame.f_code) @classmethod def detect(cls, question, answer, include_questions): detector = cls() detector.set_trace() try: question(answer) except Exception: pass detector.set_quit() for func in (question, answer): detector.codes.discard(func.func_code) filtered_codes = set() for code in detector.codes: filename = code.co_filename def from_package(package): return ("funcfinder/%s/" % package) in filename and not filename.endswith("__init__.py") # noinspection PyTypeChecker if from_package("answers") or include_questions and from_package("questions"): filtered_codes.add(code) return filtered_codes