| 1 | """ |
||
| 2 | :mod:`euler` -- Command Line Interface |
||
| 3 | ====================================== |
||
| 4 | |||
| 5 | .. module:: euler |
||
| 6 | :synopsis: Project's command line interface (CLI). |
||
| 7 | |||
| 8 | .. moduleauthor:: Bill Maroney <[email protected]> |
||
| 9 | """ |
||
| 10 | |||
| 11 | import argparse |
||
| 12 | import importlib |
||
| 13 | import os |
||
| 14 | import pytest |
||
| 15 | import urllib.request |
||
| 16 | from bs4 import BeautifulSoup |
||
| 17 | from time import time |
||
| 18 | from typing import Dict |
||
| 19 | |||
| 20 | from lib.util import wrap |
||
| 21 | from tests.validation_test import SOLUTION_MODULE_ROOT, SOLUTION_MODULE_PATH |
||
| 22 | |||
| 23 | TEMPLATE_PATH = "problem_template" # template used to start new problem |
||
| 24 | RST_TEMPLATE_PATH = "rst_template" # template used to document new problem |
||
| 25 | BASE_URL = "https://projecteuler.net/" # URL of Project Euler web-site |
||
| 26 | |||
| 27 | |||
| 28 | def start_problem(selection): |
||
| 29 | """ Create a template solution Python file for a selected Project Euler problem |
||
| 30 | |||
| 31 | The Python file will be populated by the problem description and boilerplate needed by this project. |
||
| 32 | """ |
||
| 33 | |||
| 34 | template = _load_templates() # load templates for Python and rst files |
||
| 35 | |||
| 36 | # Ask user for the problem number (if it wasn't specified on command line) |
||
| 37 | if selection is None: |
||
| 38 | selection = input("Select a problem: ") |
||
| 39 | |||
| 40 | # Parse the specified problem number |
||
| 41 | err_msg = "Error: you must enter a positive decimal number." |
||
| 42 | try: |
||
| 43 | problem_number = int(selection) |
||
| 44 | except ValueError: |
||
| 45 | print(err_msg) |
||
| 46 | return |
||
| 47 | if problem_number <= 0: |
||
| 48 | print(err_msg) |
||
| 49 | return |
||
| 50 | |||
| 51 | # Fetch the problem URL |
||
| 52 | url = "{}problem={}".format(BASE_URL, problem_number) |
||
| 53 | with urllib.request.urlopen(url) as fp: |
||
| 54 | soup = BeautifulSoup(fp, "html.parser") |
||
| 55 | |||
| 56 | # Extract the problem title, capitalise it |
||
| 57 | title = soup.find("h2").text |
||
| 58 | title = " ".join([word if word.isupper() else word.title() for word in title.split(" ")]) |
||
| 59 | |||
| 60 | # Extract the problem statement |
||
| 61 | problem = soup.find("div", attrs={"class": "problem_content"}) |
||
| 62 | paragraphs = problem.text.split("\n") |
||
| 63 | problem_statement = "" |
||
| 64 | for paragraph in [paragraph for paragraph in paragraphs if paragraph != ""]: |
||
| 65 | problem_statement += wrap(paragraph, 0, 120) |
||
| 66 | problem_statement += "\n\n" |
||
| 67 | problem_statement = problem_statement.rstrip("\n") |
||
| 68 | |||
| 69 | problem_statement = problem_statement.encode("ascii", "ignore").decode() |
||
| 70 | |||
| 71 | # Write the problem templates (if they don't already exist) |
||
| 72 | path = _build_template_paths(problem_number) |
||
| 73 | if not os.path.exists(path["py"]): |
||
| 74 | with open(path["py"], "w") as op: |
||
| 75 | uline = "=" * len("Project Euler Problem {id}: {title}".format(id=problem_number, title=title)) |
||
| 76 | op.write(template["py"].format(id=problem_number, title=title, underline=uline, problem=problem_statement)) |
||
| 77 | else: |
||
| 78 | print("Error: {} already exists.".format(path["py"])) |
||
| 79 | return |
||
| 80 | if not os.path.exists(path["rst"]): |
||
| 81 | with open(path["rst"], "w") as op: |
||
| 82 | op.write(template["rst"].format(id=problem_number)) |
||
| 83 | else: |
||
| 84 | print("Error: {} already exists.".format(path["rst"])) |
||
| 85 | return |
||
| 86 | |||
| 87 | _fetch_downloads(problem) |
||
| 88 | |||
| 89 | |||
| 90 | def solve_problem(selection): |
||
| 91 | """ Dynamically load and run solution for a selected Project Euler problem """ |
||
| 92 | |||
| 93 | # Ask user for the problem number (if it wasn't specified on command line) |
||
| 94 | if selection is None: |
||
| 95 | selection = input("Select a problem: ") |
||
| 96 | |||
| 97 | # Parse the specified problem number |
||
| 98 | err_msg = "Error: you must enter a positive decimal number." |
||
| 99 | try: |
||
| 100 | problem_number = int(selection) |
||
| 101 | except ValueError: |
||
| 102 | print(err_msg) |
||
| 103 | return |
||
| 104 | if problem_number <= 0: |
||
| 105 | print(err_msg) |
||
| 106 | return |
||
| 107 | |||
| 108 | if isinstance(problem_number, int): |
||
| 109 | # Attempt to dynamically load the solution module |
||
| 110 | try: |
||
| 111 | mod = importlib.import_module(SOLUTION_MODULE_PATH.format(problem_number)) |
||
| 112 | except ModuleNotFoundError: |
||
|
0 ignored issues
–
show
Comprehensibility
Best Practice
introduced
by
Loading history...
|
|||
| 113 | print("Error: solution for problem {} doesn't currently exist.".format(problem_number)) |
||
| 114 | return # cannot proceed without the problem module |
||
| 115 | |||
| 116 | # Attempt to compute the solution and check its correctness |
||
| 117 | try: |
||
| 118 | t0 = time() |
||
| 119 | answer = mod.solve() |
||
| 120 | t1 = time() |
||
| 121 | except AttributeError as err: |
||
| 122 | print("Error: {}.".format(err)) |
||
| 123 | return # cannot proceed with problem modules solve function |
||
| 124 | |||
| 125 | # Attempt to retrieve the expected answer |
||
| 126 | try: |
||
| 127 | expected_answer = mod.expected_answer |
||
| 128 | except AttributeError as err: |
||
| 129 | expected_answer = None |
||
| 130 | print("Warning: {}.".format(err)) |
||
| 131 | |||
| 132 | # Report the results and runtime |
||
| 133 | if expected_answer is None: |
||
| 134 | suffix = "cannot be checked." |
||
| 135 | elif expected_answer == answer: |
||
| 136 | suffix = "is correct." |
||
| 137 | else: |
||
| 138 | suffix = "is incorrect.\nExpected solution is {}.".format(expected_answer) |
||
| 139 | print("Solution is {}, which {}".format(answer, suffix)) |
||
| 140 | print("Solution took {:.2f} seconds to compute.".format(t1 - t0)) |
||
| 141 | |||
| 142 | |||
| 143 | def _build_args_parser(): |
||
| 144 | parser = argparse.ArgumentParser() |
||
| 145 | subparsers = parser.add_subparsers(dest="command") |
||
| 146 | |||
| 147 | parser_start = subparsers.add_parser("start", help="Start a new Project Euler problem") |
||
| 148 | parser_start.add_argument("n", nargs="?", type=int, help="problem number (default: prompt user for input)") |
||
| 149 | |||
| 150 | parser_solve = subparsers.add_parser("solve", help="Compute the answer to a single Project Euler problem") |
||
| 151 | parser_solve.add_argument("n", nargs="?", type=int, help="problem number (default: prompt user for input)") |
||
| 152 | |||
| 153 | parser_validate = subparsers.add_parser("validate", help="Compute answers to all Project Euler problems") |
||
| 154 | |||
| 155 | return parser |
||
| 156 | |||
| 157 | |||
| 158 | def _load_templates() -> Dict[str, str]: |
||
| 159 | # Load the problem template to be instantiated with this new problem |
||
| 160 | with open(os.path.join(os.path.dirname(__file__), TEMPLATE_PATH), "r") as fp: |
||
| 161 | py_template = fp.read() |
||
| 162 | |||
| 163 | # Load the reStructuredText template to be instantiated with this new problem |
||
| 164 | with open(os.path.join(os.path.dirname(__file__), RST_TEMPLATE_PATH), "r") as fp: |
||
| 165 | rst_template = fp.read() |
||
| 166 | |||
| 167 | return {"py": py_template, "rst": rst_template} |
||
| 168 | |||
| 169 | |||
| 170 | def _build_template_paths(problem_number: int) -> Dict[str, str]: |
||
| 171 | base_path = os.path.dirname(__file__) |
||
| 172 | py_path = os.path.join(base_path, "solutions", "problem{}.py".format(problem_number)) |
||
| 173 | rst_path = os.path.join(base_path, "docs", "solutions", "{}.rst".format(problem_number)) |
||
| 174 | return {"py": py_path, "rst": rst_path} |
||
| 175 | |||
| 176 | |||
| 177 | def _fetch_downloads(problem: str) -> None: |
||
| 178 | # Search for possible file attachments and prompt for optional downloads |
||
| 179 | files = problem.find_all("a") |
||
| 180 | for file in files: |
||
| 181 | file_url = "{}{}".format(BASE_URL, file["href"]) |
||
| 182 | filename = file["href"].split("/")[-1] |
||
| 183 | file_path = os.path.join("data", filename) |
||
| 184 | |||
| 185 | if os.path.exists(file_path): |
||
| 186 | print("Warning: {} already exists.".format(file_path)) |
||
| 187 | continue |
||
| 188 | |||
| 189 | choice = input("Download '{}'? [y/N]: ".format(filename)) |
||
| 190 | if choice == 'y': |
||
| 191 | with urllib.request.urlopen(file_url) as ffp: |
||
| 192 | data = ffp.read() |
||
| 193 | open(file_path, "w").write(data.decode("utf8")) |
||
| 194 | |||
| 195 | |||
| 196 | def main(): |
||
| 197 | """ Main program entry point """ |
||
| 198 | |||
| 199 | # Build an argument parser and apply it to the command-line arguments |
||
| 200 | parser = _build_args_parser() |
||
| 201 | args = parser.parse_args() |
||
| 202 | |||
| 203 | # Perform the requested action |
||
| 204 | if args.command == "start": |
||
| 205 | # Start a problem |
||
| 206 | start_problem(args.n) |
||
| 207 | elif args.command == "solve": |
||
| 208 | # Execute a single solution displaying answer (for submission to Project Euler) |
||
| 209 | solve_problem(args.n) |
||
| 210 | elif args.command == "validate": |
||
| 211 | # Validate all existing solutions |
||
| 212 | pytest.main(["tests/validation_test.py"]) # run the pytest unit-testing framework |
||
| 213 | else: |
||
| 214 | parser.print_usage() # invalid command |
||
| 215 | |||
| 216 | |||
| 217 | if __name__ == "__main__": |
||
| 218 | main() |
||
| 219 |