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
![]() |
|||
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", "problems", 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 |