def generate(num, prompt_default=True): """Generates Python file for a problem.""" p = Problem(num) problem_text = p.text msg = "Generate file for problem %i?" % num click.confirm(msg, default=prompt_default, abort=True) # Allow skipped problem files to be recreated if p.glob: filename = str(p.file) msg = '"{}" already exists. Overwrite?'.format(filename) click.confirm(click.style(msg, fg='red'), abort=True) else: # Try to keep prefix consistent with existing files previous_file = Problem(num - 1).file prefix = previous_file.prefix if previous_file else '' filename = p.filename(prefix=prefix) header = 'Project Euler Problem %i' % num divider = '=' * len(header) text = '\n'.join([header, divider, '', problem_text]) content = '\n'.join(['"""', text, '"""']) with open(filename, 'w') as f: f.write(content + '\n\n\n') click.secho('Successfully created "{}".'.format(filename), fg='green') # Copy over problem resources if required if p.resources: p.copy_resources()
def verify(num, filename=None, exit=True): """Verifies the solution to a problem.""" p = Problem(num) filename = filename or p.filename() if not os.path.isfile(filename): # Attempt to verify the first problem file matched by glob if p.glob: filename = str(p.file) else: click.secho('No file found for problem %i.' % p.num, fg='red') sys.exit(1) solution = p.solution click.echo('Checking "{}" against solution: '.format(filename), nl=False) cmd = (sys.executable or 'python', filename) start = clock() proc = subprocess.Popen(cmd, stdout=subprocess.PIPE) stdout = proc.communicate()[0] end = clock() time_info = format_time(start, end) # Return value of anything other than 0 indicates an error if proc.poll() != 0: click.secho('Error calling "{}".'.format(filename), fg='red') click.secho(time_info, fg='cyan') # Return None if option is not --verify-all, otherwise exit return sys.exit(1) if exit else None # Decode output if returned as bytes (Python 3) if isinstance(stdout, bytes): output = stdout.decode('ascii') # Split output lines into array; make empty output more readable output_lines = output.splitlines() if output else ['[no output]'] # If output is multi-lined, print the first line of the output on a # separate line from the "checking against solution" message, and # skip the solution check (multi-line solution won't be correct) if len(output_lines) > 1: is_correct = False click.echo() # force output to start on next line click.secho('\n'.join(output_lines), bold=True, fg='red') else: is_correct = output_lines[0] == solution fg_colour = 'green' if is_correct else 'red' click.secho(output_lines[0], bold=True, fg=fg_colour) click.secho(time_info, fg='cyan') # Remove any suffix from the filename if its solution is correct if is_correct: p.file.change_suffix('') # Exit here if answer was incorrect, otherwise return is_correct value return sys.exit(1) if exit and not is_correct else is_correct
def generateFile(problem, filename=None, content=None, correct=False): """ Uses Problem.solution to generate a problem file. The correct argument controls whether the generated file is correct or not. """ p = Problem(problem) filename = filename or p.filename() with open(filename, 'w') as f: if correct: f.write('print({})'.format(p.solution)) elif content: f.write(content)
def test_expected_problem(self): """Check that problem #1 returns the correct problem text""" problem_one = textwrap.dedent(""" If we list all the natural numbers below 10 that are multiples of 3 or 5, we get 3, 5, 6 and 9. The sum of these multiples is 23. Find the sum of all the multiples of 3 or 5 below 1000. """) self.assertEqual(problem_one.strip(), Problem(1).text)
def generateFile(problem, filename=None, correct=False): """ Uses Problem().solution to generate a problem file. The correct argument controls whether the generated file is correct or not. """ p = Problem(problem) filename = filename or p.filename with open(filename, 'a') as file: if correct: file.write('print({0})\n'.format(p.solution))
def main(option, problem): """Python-based Project Euler command line tool.""" # No problem given (or given option ignores the problem argument) if problem == 0 or option in (skip, verify_all): # Determine the highest problem number in the current directory files = problem_glob() problem = max(int(file[:3]) for file in files) if files else 0 # No Project Euler files in current directory (no glob results) if problem == 0: # Generate the first problem file if option is appropriate if option not in (cheat, preview, verify_all): msg = "No Project Euler files found in the current directory." click.echo(msg) option = generate # Set problem number to 1 problem = 1 # --preview and no problem; preview the next problem elif option is preview: problem += 1 # No option and no problem; generate next file if answer is # correct (verify() will exit if the solution is incorrect) if option is None: verify(Problem(problem)) problem += 1 option = generate # Problem given but no option; decide between generate and verify elif option is None: option = verify if any(Problem(problem).iglob) else generate # Execute function based on option (pass Problem object as argument) option(Problem(problem)) sys.exit(0)
def test_problem_format(self): """ Ensure each parsed problem only contains one problem (that one problem does not "bleed" into the next one due to an issue with line breaks) """ # Determine largest problem in problems.txt problems_file = os.path.join(EULER_DATA, 'problems.txt') with open(problems_file) as f: for line in f: if line.startswith('Problem '): largest_problem = line.split(' ')[1] for problem in range(1, int(largest_problem) + 1): problemText = Problem(problem).text msg = "Error encountered when parsing problem {}.".format(problem) self.assertFalse('========='in problemText, msg=msg) self.assertFalse('\n\n\n' in problemText, msg=msg)
def test_problem_format(self): """ Ensure each parsed problem only contains one problem (that one problem does not "bleed" into the next one due to an issue with line breaks) """ # Determine largest problem in problems.txt problemsFile = os.path.join(os.path.dirname(__file__), 'data', 'problems.txt') with open(problemsFile) as file: largest = '' for line in file: if line.startswith('Problem'): largest = line.strip() largestProblem = int(largest.split(' ')[1]) for problem in range(1, largestProblem + 1): problemText = Problem(problem).text msg = "Error encountered when parsing problem {0}.".format(problem) self.assertFalse('=========' in problemText, msg=msg) self.assertFalse('\n\n\n' in problemText, msg=msg)
def test_filename_format(self): """Check that filenames are being formatted correctly""" self.assertEqual(Problem(1).filename(), "001.py") self.assertEqual(Problem(10).filename(), "010.py") self.assertEqual(Problem(100).filename(), "100.py")
def verify_all(current_p): """ Verifies all problem files in the current directory and prints an overview of the status of each problem. """ # Define various problem statuses statuses = ( ('correct', 'C', 'green'), ('incorrect', 'I', 'red'), ('error', 'E', 'yellow'), ('skipped', 'S', 'cyan'), ('missing', '.', 'white'), ) status = OrderedDict( (key, click.style(symbol, fg=colour, bold=True)) for key, symbol, colour in statuses ) overview = {} # Search through problem files using glob module for filename in glob.glob('[0-9][0-9][0-9]*.py'): p = Problem(int(filename[:3])) # Catch KeyboardInterrupt during verification to allow the user # to skip the verification of a problem if it takes too long try: is_correct = verify(p, filename=filename, exit=False) except KeyboardInterrupt: overview[p.num] = status['skipped'] else: if is_correct is None: # error was returned by problem file overview[p.num] = status['error'] elif is_correct: overview[p.num] = status['correct'] elif not is_correct: overview[p.num] = status['incorrect'] # Attempt to add "skipped" suffix to the filename if the # problem file is not the current problem. This is useful # when the --verify-all is used in a directory containing # files generated pre-v1.1 (before files with suffixes) if p.num != current_p.num: rename_file(filename, p.suf_name('skipped')) # Separate each verification with a newline click.echo() # No Project Euler files in the current directory if not overview: click.echo("No Project Euler files found in the current directory.") sys.exit(1) # Print overview of the status of each problem legend = ', '.join('{0} = {1}'.format(v, k) for k, v in status.items()) click.echo('-' * 63) click.echo(legend + '\n') # Rows needed for overview is based on the current problem number num_of_rows = (current_p.num + 19) // 20 for row in range(1, num_of_rows + 1): low, high = (row * 20) - 19, (row * 20) click.echo("Problems {0:03d}-{1:03d}: ".format(low, high), nl=False) for problem in range(low, high + 1): # Add missing status to problems with no corresponding file status = overview[problem] if problem in overview else '.' # Separate problem indicators into groups of 5 spacer = ' ' if (problem % 5 == 0) else ' ' # Start a new line at the end of each row click.secho(status + spacer, nl=(problem % 20 == 0)) click.echo()
def verify_all(current_p): """ Verifies all problem files in the current directory and prints an overview of the status of each problem. """ overview = {} # Search through problem files using glob module for filename in glob.glob("[0-9][0-9][0-9]*.py"): p = Problem(int(filename[:3])) # Catch KeyboardInterrupt during verification to allow the user # to skip the verification of a problem if it takes too long try: is_correct = verify(p, filename=filename, exit=False) except KeyboardInterrupt: overview[p.num] = click.style("S", fg="cyan") else: if is_correct is None: # error was returned by problem file overview[p.num] = click.style("E", fg="yellow") elif is_correct: overview[p.num] = click.style("C", fg="green") elif not is_correct: overview[p.num] = click.style("I", fg="red") # Attempt to add "skipped" suffix to the filename if the # problem file is not the current problem. This is useful # when the --verify-all is used in a directory containing # files generated pre-v1.1 (before files with suffixes) if p.num != current_p.num: skipped_name = p.suf_name("skipped") rename_file(filename, skipped_name) # Separate each verification with a newline click.echo() # No Project Euler files in the current directory if not overview: click.echo("No Project Euler files found in the current directory.") sys.exit(1) # Print overview of the status of each problem click.echo("-" * 63) legend = ", ".join( "{0} = {1}".format(click.style(symbol, bold=True, fg=colour), name) for symbol, name, colour in ( ("C", "correct", "green"), ("I", "incorrect", "red"), ("E", "error", "yellow"), ("S", "skipped", "cyan"), (".", "missing", "white"), ) ) click.echo(legend + "\n") # Rows needed for overview is based on the current problem number num_of_rows = (current_p.num + 19) // 20 for row in range(1, num_of_rows + 1): low, high = (row * 20) - 19, (row * 20) click.echo("Problems {0:03d}-{1:03d}: ".format(low, high), nl=False) for problem in range(low, high + 1): # Add missing status to problems with no problem file status = overview[problem] if problem in overview else "." click.secho(status, bold=True, nl=False) # Separate problem indicators into groups of 5 click.echo(" " if problem % 5 == 0 else " ", nl=False) # Start a new line at the end of each row click.echo() click.echo()
def generate(num, prompt_default=True): """Generates Python file for a problem.""" def wrap(text, wrap_len): """Wraps text at given character width. Args: text (str): text to wrap wrap_len (int): character width at which to wrap Returns: String with newlines inserted as appropriate """ if len(text) > wrap_len: words = iter(text.split()) wrapped_lines = [] wrapped_line = next(words) # assume 1st word is shorter than wrap_len for word in words: if len(wrapped_line) + len(word) < wrap_len: # < rather than <= to account for 1 char space wrapped_line = ' '.join([wrapped_line, word]) else: wrapped_lines.append(wrapped_line) wrapped_line = word wrapped_lines.append(wrapped_line) return '\n'.join(wrapped_lines) else: return text p = Problem(num) problem_text = p.text msg = "Generate file for problem %i?" % num click.confirm(msg, default=prompt_default, abort=True) # Allow skipped problem files to be recreated if p.glob: filename = str(p.file) msg = '"{}" already exists. Overwrite?'.format(filename) click.confirm(click.style(msg, fg='red'), abort=True) else: # Try to keep prefix consistent with existing files previous_file = Problem(num - 1).file prefix = previous_file.prefix if previous_file else '' filename = p.filename(prefix=prefix) header = 'Project Euler Problem {}\n\n{}'.format(num, wrap(p.title, 76)) divider = '=' * len(header.split('\n')[-1]) text = '\n'.join([header, divider, '', problem_text]) content = '\n'.join([ '#!/usr/bin/env python', '# -*- coding: utf-8 -*-', '', '"""', text, '"""', '', 'def problem_{}():'.format(num), ' response = 0', ' print(response)', '', "if __name__ == '__main__':", ' problem_{}()'.format(num) ]) with open(filename, 'w') as f: f.write(content) click.secho('Successfully created "{}".'.format(filename), fg='green') # Copy over problem resources if required if p.resources: p.copy_resources()
def skip(num): """Generates Python file for the next problem.""" click.echo("Current problem is problem %i." % num) generate(num + 1, prompt_default=False) Problem(num).file.change_suffix('-skipped')
def preview(num): """Prints the text of a problem.""" # Define problem_text before echoing in case problem does not exist problem_text = Problem(num).text click.secho("Project Euler Problem %i" % num, bold=True) click.echo(problem_text)
def cheat(num): """View the answer to a problem.""" # Define solution before echoing in case solution does not exist solution = click.style(Problem(num).solution, bold=True) click.confirm("View answer to problem %i?" % num, abort=True) click.echo("The answer to problem {} is {}.".format(num, solution))
def verify_all(current_p): """ Verifies all problem files in the current directory and prints an overview of the status of each problem. """ # Define various problem statuses keys = ('correct', 'incorrect', 'error', 'skipped', 'missing') symbols = ('C', 'I', 'E', 'S', '.') colours = ('green', 'red', 'yellow', 'cyan', 'white') status = OrderedDict( (key, click.style(symbol, fg=colour, bold=True)) for key, symbol, colour in zip(keys, symbols, colours)) overview = {} # Search through problem files using glob module files = problem_glob() # No Project Euler files in the current directory if not files: click.echo("No Project Euler files found in the current directory.") sys.exit(1) for file in files: p = Problem(int(file[:3])) # Catch KeyboardInterrupt during verification to allow the user # to skip the verification of a problem if it takes too long try: is_correct = verify(p, filename=file, exit=False) except KeyboardInterrupt: overview[p.num] = status['skipped'] else: if is_correct is None: # error was returned by problem file overview[p.num] = status['error'] elif is_correct: overview[p.num] = status['correct'] elif not is_correct: overview[p.num] = status['incorrect'] # Attempt to add "skipped" suffix to the filename if the # problem file is not the current problem. This is useful # when the --verify-all is used in a directory containing # files generated pre-v1.1 (before files with suffixes) if p.num != current_p.num: rename_file(file, p.suf_name('skipped')) # Separate each verification with a newline click.echo() # Print overview of the status of each problem legend = ', '.join('{0} = {1}'.format(v, k) for k, v in status.items()) click.echo('-' * 63) click.echo(legend + '\n') # Rows needed for overview is based on the current problem number num_of_rows = (current_p.num + 19) // 20 for row in range(1, num_of_rows + 1): low, high = (row * 20) - 19, (row * 20) click.echo("Problems {0:03d}-{1:03d}: ".format(low, high), nl=False) for problem in range(low, high + 1): # Add missing status to problems with no corresponding file status = overview[problem] if problem in overview else '.' # Separate problem indicators into groups of 5 spacer = ' ' if (problem % 5 == 0) else ' ' # Start a new line at the end of each row click.secho(status + spacer, nl=(problem % 20 == 0)) click.echo()
def skip(p): """Generates Python file for the next problem.""" click.echo("Current problem is problem %i." % p.num) next_p = Problem(p.num + 1) generate(next_p, prompt_default=False) rename_file(p.filename, p.suf_name('skipped'))