euler   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 219
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 136
dl 0
loc 219
rs 9.36
c 0
b 0
f 0
wmc 38

7 Functions

Rating   Name   Duplication   Size   Complexity  
A main() 0 19 4
A _build_args_parser() 0 13 1
C solve_problem() 0 51 10
A _load_templates() 0 10 3
A _build_template_paths() 0 5 1
F start_problem() 0 60 14
A _fetch_downloads() 0 17 5
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
The variable ModuleNotFoundError does not seem to be defined.
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", "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