├── .gitignore ├── .gitmodules ├── CONTRIBUTING.rst ├── EasyEuler ├── __init__.py ├── cli.py ├── commands │ ├── __init__.py │ ├── create.py │ ├── generate_resources.py │ ├── list.py │ ├── show.py │ └── verify.py ├── config.json ├── data.py ├── paths.py ├── templates │ ├── c │ ├── description │ ├── haskell │ ├── javascript │ ├── python │ └── ruby └── types.py ├── LICENSE ├── MANIFEST.in ├── README.rst ├── requirements.txt ├── setup.py ├── test.py └── tests ├── test_commands.py └── test_data.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg-info 3 | __pycache__ 4 | tmp 5 | build 6 | dist 7 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "EasyEuler/data"] 2 | path = EasyEuler/data 3 | url = https://github.com/Encrylize/project-euler-data.git 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | **NOTE:** If you wish to contribute by adding more problems and the 5 | likes, please go to the 6 | `project-euler-data `__ 7 | repository. 8 | 9 | When contributing code, please try to follow the `PEP 8 Style 10 | Guide `__ and mimic the style 11 | of existing code as closely as possible. 12 | 13 | To clone this repository to your own machine, run the following command: 14 | 15 | .. code:: bash 16 | 17 | $ git clone --recursive https://github.com/Encrylize/EasyEuler 18 | -------------------------------------------------------------------------------- /EasyEuler/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfblomgren/EasyEuler/4b6bf8f4d599e1e2182524ef88bef3fa8387adc5/EasyEuler/__init__.py -------------------------------------------------------------------------------- /EasyEuler/cli.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import click 4 | 5 | from EasyEuler import paths 6 | 7 | 8 | class CommandLineInterface(click.MultiCommand): 9 | def list_commands(self, ctx): 10 | commands = [] 11 | 12 | for filename in os.listdir(paths.COMMANDS): 13 | if filename.endswith('.py') and filename != '__init__.py': 14 | commands.append(filename[:-3].replace('_', '-')) 15 | 16 | commands.sort() 17 | return commands 18 | 19 | def get_command(self, ctx, name): 20 | # We don't want foo_bar to be interpreted as a valid command, 21 | # but we still want foo-bar to be. 22 | name = name.replace('_', '').replace('-', '_') 23 | 24 | try: 25 | command = __import__('EasyEuler.commands.%s' % name, 26 | None, None, ['cli']) 27 | except ImportError: 28 | return 29 | return command.cli 30 | 31 | 32 | cli = CommandLineInterface() 33 | -------------------------------------------------------------------------------- /EasyEuler/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfblomgren/EasyEuler/4b6bf8f4d599e1e2182524ef88bef3fa8387adc5/EasyEuler/commands/__init__.py -------------------------------------------------------------------------------- /EasyEuler/commands/create.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | import click 5 | 6 | from EasyEuler import data 7 | from EasyEuler.types import LanguageType, ProblemType 8 | from EasyEuler.commands.generate_resources import generate_resources 9 | 10 | 11 | @click.command() 12 | @click.option('--path', '-p', type=click.Path(), 13 | help='Writes the file to PATH.') 14 | @click.argument('problem', type=ProblemType()) 15 | @click.argument('language', type=LanguageType(), 16 | required=False, default=data.config['default language']) 17 | def cli(problem, language, path): 18 | """ 19 | Create the file for a problem. 20 | 21 | Simply specify a valid problem ID and the file will be created at 22 | euler_. (if the PATH option isn't specified). 23 | 24 | Optionally, the LANGUAGE argument can be specified, which will then 25 | be used to identify an appropriate template for the file. 26 | 27 | """ 28 | 29 | if path is None: 30 | filename_format = data.config['filename format'] 31 | path = filename_format.format(id=problem['id'], 32 | extension=language['extension']) 33 | 34 | if os.path.exists(path) and not \ 35 | click.confirm('%s already exists. Do you want to overwrite it?' % 36 | click.format_filename(path)): 37 | return 38 | 39 | try: 40 | write_to_file(problem, language, path) 41 | except (FileNotFoundError, PermissionError) as exception: 42 | sys.exit('An exception occurred: %s' % exception) 43 | 44 | click.echo('Written to %s' % click.format_filename(path)) 45 | 46 | if 'resources' in problem and \ 47 | click.confirm('Generate resources for this problem?'): 48 | resource_path = click.prompt('Path (default: current directory)', 49 | default='.', show_default=False, 50 | type=click.Path(writable=True, 51 | readable=False)) 52 | generate_resources(problem['resources'], resource_path) 53 | 54 | 55 | def write_to_file(problem, language, path): 56 | template_name = language.get('template', language['name']) 57 | template = data.templates.get_template(template_name) 58 | 59 | with open(path, 'w') as problem_file: 60 | problem_file.write(template.render(**problem)) 61 | -------------------------------------------------------------------------------- /EasyEuler/commands/generate_resources.py: -------------------------------------------------------------------------------- 1 | import os 2 | import shutil 3 | import sys 4 | 5 | import click 6 | 7 | from EasyEuler import paths 8 | from EasyEuler.types import ProblemType 9 | 10 | 11 | @click.command('generate-resources') 12 | @click.option('--path', '-p', type=click.Path(writable=True, readable=False), 13 | default='.', help='Creates the file(s) at PATH.') 14 | @click.argument('problem', type=ProblemType(), required=False) 15 | def cli(problem, path): 16 | """ 17 | Generate the resource files for problems. 18 | 19 | These resources are either images - serving as helpful illustrations - 20 | or text files containing specific data - referenced in the problem. 21 | 22 | If the PROBLEM argument isn't specified, all resources will be 23 | generated. 24 | 25 | """ 26 | 27 | if problem is None: 28 | resources = os.listdir('%s/resources' % paths.DATA) 29 | else: 30 | if 'resources' not in problem: 31 | sys.exit('Problem %s has no resource files' % problem['id']) 32 | resources = problem['resources'] 33 | 34 | generate_resources(resources, path) 35 | 36 | 37 | def generate_resources(resources, path): 38 | if len(resources) > 1 and not os.path.isdir(path): 39 | if os.path.exists(path): 40 | sys.exit('%s needs to be a directory to create multiple ' 41 | 'resource files' % click.format_filename(path)) 42 | os.mkdir(path) 43 | 44 | for resource in resources: 45 | if len(resources) > 1 or os.path.isdir(path): 46 | resource_path = '%s/%s' % (path, resource) 47 | else: 48 | resource_path = path 49 | 50 | if os.path.exists(resource_path) and not \ 51 | click.confirm('%s already exists. Do you want to overwrite it?' % 52 | click.format_filename(resource_path)): 53 | continue 54 | 55 | shutil.copy('%s/resources/%s' % (paths.DATA, resource), path) 56 | click.echo('Created %s at path %s' % (resource, 57 | click.format_filename(path))) 58 | -------------------------------------------------------------------------------- /EasyEuler/commands/list.py: -------------------------------------------------------------------------------- 1 | import click 2 | from tabulate import tabulate 3 | 4 | from EasyEuler import data 5 | 6 | 7 | TABLE_HEADERS = ('ID', 'Name', 'Difficulty') 8 | 9 | 10 | @click.command() 11 | @click.option('--sort', '-s', type=click.Choice(('id', 'difficulty')), 12 | default='id', help='Sort the list by problem attribute.') 13 | def cli(sort): 14 | """ Lists all available problems. """ 15 | 16 | problems = sorted(data.problems, key=lambda problem: problem[sort.lower()]) 17 | problem_list = ((problem['id'], problem['name'], 18 | '%d%%' % problem['difficulty']) for problem in problems) 19 | 20 | table = tabulate(problem_list, TABLE_HEADERS, tablefmt='fancy_grid') 21 | click.echo_via_pager(table) 22 | -------------------------------------------------------------------------------- /EasyEuler/commands/show.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from EasyEuler import data 4 | from EasyEuler.types import ProblemType 5 | 6 | 7 | @click.command() 8 | @click.argument('problem', type=ProblemType()) 9 | def cli(problem): 10 | """ Show a problems description. """ 11 | 12 | template = data.templates.get_template('description') 13 | description = template.render(**problem) 14 | click.echo(description) 15 | -------------------------------------------------------------------------------- /EasyEuler/commands/verify.py: -------------------------------------------------------------------------------- 1 | import math 2 | import os 3 | import re 4 | import subprocess 5 | import sys 6 | import time 7 | 8 | import click 9 | 10 | from EasyEuler import data 11 | from EasyEuler.types import LanguageType 12 | 13 | 14 | PROBLEM_ID_REGEX = re.compile(r'\D*([1-9]\d{0,2}).*') 15 | STAGES = ('build', 'execute', 'cleanup') 16 | 17 | 18 | @click.command() 19 | @click.option('--time', '-t', is_flag=True, 20 | help='Time the execution of files.') 21 | @click.option('--errors', '-e', is_flag=True, 22 | help='Show errors.') 23 | @click.option('--recursive', '-r', is_flag=True, 24 | help='Verify files in specified directory paths.') 25 | @click.option('--language', '-l', type=LanguageType(), 26 | help='The language of the file(s).') 27 | @click.argument('paths', type=click.Path(exists=True, readable=True), nargs=-1, 28 | metavar='[PATH]...') 29 | def cli(paths, language, time, errors, recursive): 30 | """ 31 | Verify the solution to a problem. 32 | 33 | Runs the appropriate command for a language (specified in the 34 | configuration file) with the file path(s) as arguments. 35 | 36 | If the LANGUAGE option isn't specified, it will be identified based 37 | on the file extension. Similarly, the problem ID will be identified 38 | based on the file name. 39 | 40 | """ 41 | 42 | for path in paths: 43 | if os.path.isdir(path): 44 | if recursive: 45 | validate_directory(path, language, time, errors) 46 | else: 47 | click.echo('Skipping %s because it is a directory ' 48 | 'and --recursive was not specified' % 49 | click.format_filename(path)) 50 | else: 51 | validate_file(path, language, time, errors) 52 | 53 | 54 | def validate_directory(path, language, time_execution, show_errors): 55 | for root, _, filenames in os.walk(path): 56 | for filename in filenames: 57 | file_path = os.path.join(root, filename) 58 | validate_file(file_path, language, time_execution, show_errors) 59 | 60 | 61 | def validate_file(path, language, time_execution, show_errors): 62 | problem = get_problem_from_path(path) 63 | if problem is None: 64 | click.echo('Skipping %s because it does not contain ' 65 | 'a valid problem ID' % click.format_filename(path)) 66 | return 67 | 68 | if language is None: 69 | language = get_language_from_path(path) or {} 70 | 71 | click.echo('Checking output of %s: ' % click.format_filename(path), 72 | nl=False) 73 | result = verify_solution(path, language, time_execution, problem) 74 | print_result(result, show_errors, time_execution) 75 | 76 | 77 | def print_result(result, show_errors, show_time): 78 | if result['error'] != 'none': 79 | if show_errors: 80 | error_message = result[result['error']]['output'] 81 | else: 82 | error_message = '[error during %s]' % result['error'] 83 | click.secho('\n%s' % error_message, fg='red') 84 | return 85 | 86 | click.secho(result['execute']['output'] or '[no output]', 87 | fg='green' if result['correct'] else 'red') 88 | 89 | if show_time: 90 | print_execution_time(result['execute']['execution_time']) 91 | 92 | 93 | def print_execution_time(execution_time): 94 | if 'user' in execution_time: 95 | execution_time_msg = 'CPU times - user: {user}, ' \ 96 | 'system: {system}, total: {total}\n' \ 97 | 'Wall time: {wall}\n' 98 | else: 99 | execution_time_msg = 'Time: {wall}\n' 100 | click.secho(execution_time_msg.format(**execution_time), fg='cyan') 101 | 102 | 103 | def get_problem_from_path(path): 104 | problem_id = get_problem_id_from_path(path) 105 | if problem_id is None: 106 | return None 107 | return data.problems.get(problem_id) 108 | 109 | 110 | def get_language_from_path(path): 111 | file_extension = os.path.splitext(path)[1].replace('.', '') 112 | return data.config.get_language('extension', file_extension) 113 | 114 | 115 | def get_problem_id_from_path(path): 116 | problem_id = PROBLEM_ID_REGEX.findall(path) 117 | return int(problem_id[0]) if len(problem_id) > 0 else None 118 | 119 | 120 | def verify_solution(path, language, time_execution, problem): 121 | commands = get_commands(path, language) 122 | result = {'error': 'none'} 123 | 124 | for stage in STAGES: 125 | if commands[stage] is None: 126 | continue 127 | 128 | if stage == 'execute': 129 | result[stage] = execute_process(commands[stage], time_execution) 130 | result['correct'] = result[stage]['output'] == problem['answer'] 131 | else: 132 | result[stage] = execute_process(commands[stage], False) 133 | 134 | if result[stage]['error']: 135 | result['error'] = stage 136 | break 137 | 138 | return result 139 | 140 | 141 | def get_process_output(process): 142 | if process.returncode != 0: 143 | return str(process.stderr, encoding='UTF-8'), True 144 | return str(process.stdout, encoding='UTF-8').rstrip(), False 145 | 146 | 147 | def get_commands(path, language): 148 | commands = {'build': None, 'cleanup': None} 149 | commands['execute'] = language.get('execute', './{path}').format(path=path) 150 | 151 | if 'build' in language: 152 | commands['build'] = language['build'].format(path=path) 153 | if 'cleanup' in language: 154 | commands['cleanup'] = language['cleanup'].format(path=path) 155 | 156 | return commands 157 | 158 | 159 | def execute_process(command, time_execution): 160 | if time_execution: 161 | start_time = get_time() 162 | process = subprocess.run(command, shell=True, stdout=subprocess.PIPE, 163 | stderr=subprocess.PIPE) 164 | end_time = get_time() 165 | 166 | execution_time = {key: format_time(end_time[key] - start_time[key]) 167 | for key in end_time} 168 | else: 169 | process = subprocess.run(command, shell=True, stdout=subprocess.PIPE, 170 | stderr=subprocess.PIPE) 171 | execution_time = None 172 | 173 | output, error = get_process_output(process) 174 | return {'output': output, 'error': error, 'execution_time': execution_time} 175 | 176 | 177 | try: 178 | import resource 179 | 180 | def get_time(): 181 | rs = resource.getrusage(resource.RUSAGE_CHILDREN) 182 | return {'user': rs.ru_utime, 'system': rs.ru_stime, 183 | 'total': rs.ru_stime + rs.ru_utime, 'wall': time.time()} 184 | except ImportError: 185 | # The resource module only exists on Unix-based platforms. 186 | # This is a different platform, so we can't provide user 187 | # and system times. 188 | def get_time(): 189 | return {'wall': time.time()} 190 | 191 | 192 | def format_long_time(timespan): 193 | """ 194 | Formats a long timespan in a human-readable form with a 195 | precision of a 100th of a second. 196 | 197 | """ 198 | 199 | formatted_time = [] 200 | units = (('d', 24 * 60 * 60), ('h', 60 * 60), ('m', 60), ('s', 1)) 201 | 202 | for unit, length in units: 203 | value = int(timespan / length) 204 | 205 | if value > 0: 206 | timespan %= length 207 | formatted_time.append('%i%s' % (value, unit)) 208 | 209 | if timespan < 1: 210 | break 211 | 212 | return ' '.join(formatted_time) 213 | 214 | 215 | def format_short_time(timespan): 216 | """ 217 | Formats a short timespan in a human-readable form with a 218 | precision of a billionth of a second. 219 | 220 | """ 221 | 222 | scaling = (1, 1e3, 1e6, 1e9) 223 | units = ['s', 'ms', 'us', 'ns'] 224 | 225 | # Attempt to change 'u' to the micro symbol if it's supported. 226 | if hasattr(sys.stdout, 'encoding') and sys.stdout.encoding: 227 | try: 228 | '\xb5'.encode(sys.stdout.encoding) 229 | units[2] = '\xb5s' 230 | except UnicodeEncodeError: 231 | pass 232 | 233 | if timespan > 0: 234 | order = min(-int(math.floor(math.log10(timespan)) // 3), 3) 235 | else: 236 | order = 3 237 | 238 | return '%.*g%s' % (3, timespan * scaling[order], units[order]) 239 | 240 | 241 | def format_time(timespan): 242 | """ 243 | Formats a timespan in a human-readable form. 244 | Courtesy of IPython. 245 | 246 | """ 247 | 248 | if timespan >= 60: 249 | # If the time is greater than one minute, 250 | # precision is reduced to a 100th of a second. 251 | return format_long_time(timespan) 252 | return format_short_time(timespan) 253 | -------------------------------------------------------------------------------- /EasyEuler/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "filename format": "euler_{id:0>3}.{extension}", 3 | "default language": "python", 4 | "languages": { 5 | "python": 6 | { 7 | "extension": "py", 8 | "template": "python", 9 | "execute": "python {path}" 10 | }, 11 | "c": { 12 | "extension": "c", 13 | "template": "c", 14 | "build": "gcc -o {path}.out {path}", 15 | "execute": "./{path}.out", 16 | "cleanup": "rm {path}.out" 17 | }, 18 | "ruby": { 19 | "extension": "rb", 20 | "template": "ruby", 21 | "execute": "ruby {path}" 22 | }, 23 | "javascript": { 24 | "extension": "js", 25 | "template": "javascript", 26 | "execute": "node {path}" 27 | }, 28 | "haskell": { 29 | "extension": "hs", 30 | "template": "haskell", 31 | "execute": "runhaskell {path}" 32 | }, 33 | "c++": { 34 | "extension": "cpp", 35 | "template": "c", 36 | "build": "g++ -o {path}.out {path}", 37 | "execute": "./{path}.out", 38 | "cleanup": "rm {path}.out" 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /EasyEuler/data.py: -------------------------------------------------------------------------------- 1 | import collections 2 | import json 3 | import os 4 | 5 | from jinja2 import Environment, FileSystemLoader 6 | 7 | from EasyEuler import paths 8 | 9 | 10 | class ProblemList(collections.Sequence): 11 | def __init__(self, problems): 12 | self._problems = problems 13 | 14 | def get(self, problem_id): 15 | if problem_id < 1 or len(self) < problem_id: 16 | # We don't want a negative index, 17 | # because it'll wrap back around. 18 | return None 19 | return self[problem_id] 20 | 21 | def __getitem__(self, problem_id): 22 | return self._problems[problem_id - 1] 23 | 24 | def __len__(self): 25 | return len(self._problems) 26 | 27 | 28 | class ConfigurationDictionary(collections.Mapping): 29 | def __init__(self, configs): 30 | self._config = {} 31 | 32 | for config in configs: 33 | self._config = self._update(self._config, config) 34 | 35 | def _update(self, config, updates): 36 | for key, value in updates.items(): 37 | if isinstance(value, collections.Mapping): 38 | updated = self._update(config.get(key, {}), value) 39 | config[key] = updated 40 | else: 41 | config[key] = value 42 | return config 43 | 44 | def get_language(self, key, value): 45 | for name, options in self._config['languages'].items(): 46 | if options[key] == value: 47 | return {'name': name, **options} 48 | return None 49 | 50 | def __getitem__(self, key): 51 | return self._config[key] 52 | 53 | def __iter__(self): 54 | raise NotImplementedError 55 | 56 | def __len__(self): 57 | raise NotImplementedError 58 | 59 | 60 | CONFIG_LIST = [] 61 | for CONFIG_PATH in paths.CONFIGS: 62 | if not os.path.exists(CONFIG_PATH): 63 | continue 64 | 65 | with open(CONFIG_PATH) as conf: 66 | CONFIG_LIST.append(json.load(conf)) 67 | 68 | with open(paths.PROBLEMS) as f: 69 | PROBLEM_LIST = json.load(f) 70 | 71 | config = ConfigurationDictionary(CONFIG_LIST) 72 | problems = ProblemList(PROBLEM_LIST) 73 | templates = Environment(loader=FileSystemLoader(paths.TEMPLATES)) 74 | -------------------------------------------------------------------------------- /EasyEuler/paths.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | 4 | BASE = os.path.abspath(os.path.dirname(__file__)) 5 | DATA = os.path.join(BASE, 'data') 6 | 7 | COMMANDS = os.path.join(BASE, 'commands') 8 | RESOURCES = os.path.join(DATA, 'resources') 9 | PROBLEMS = os.path.join(DATA, 'problems.json') 10 | 11 | CONFIGS = [os.path.join(BASE, 'config.json')] 12 | TEMPLATES = [os.path.join(BASE, 'templates')] 13 | 14 | HOME = os.environ.get('HOME') 15 | if HOME is not None: 16 | DEFAULT_XDG_CONFIG_HOME = os.path.join(HOME, '.config') 17 | XDG_CONFIG_HOME = os.environ.get('XDG_CONFIG_HOME', DEFAULT_XDG_CONFIG_HOME) 18 | XDG_CONFIG_DIRS = os.environ.get('XDG_CONFIG_DIRS', '/etc/xdg').split(':') 19 | CONFIG_DIRS = list(reversed(XDG_CONFIG_DIRS)) + [XDG_CONFIG_HOME] 20 | 21 | for config_dir in CONFIG_DIRS: 22 | if os.path.isabs(config_dir): 23 | config_path = os.path.join(config_dir, 'EasyEuler/config.json') 24 | template_path = os.path.join(config_dir, 'EasyEuler/templates') 25 | CONFIGS.append(config_path) 26 | TEMPLATES.append(template_path) 27 | -------------------------------------------------------------------------------- /EasyEuler/templates/c: -------------------------------------------------------------------------------- 1 | /* 2 | {% include 'description' %} 3 | 4 | */ 5 | 6 | #include 7 | 8 | int main(void) 9 | { 10 | return 0; 11 | } 12 | -------------------------------------------------------------------------------- /EasyEuler/templates/description: -------------------------------------------------------------------------------- 1 | Problem {{ id }}: {{ name }} 2 | 3 | {{ description }} 4 | 5 | {%- if resources %} 6 | 7 | This problem references the following resources: 8 | {% for resource in resources %} 9 | {{ resource }} 10 | {%- endfor -%} 11 | {%- endif -%} 12 | -------------------------------------------------------------------------------- /EasyEuler/templates/haskell: -------------------------------------------------------------------------------- 1 | {- 2 | {% include 'description' %} 3 | -} 4 | -------------------------------------------------------------------------------- /EasyEuler/templates/javascript: -------------------------------------------------------------------------------- 1 | /* 2 | {% include 'description' %} 3 | */ 4 | -------------------------------------------------------------------------------- /EasyEuler/templates/python: -------------------------------------------------------------------------------- 1 | """ 2 | {% include 'description' %} 3 | 4 | """ 5 | -------------------------------------------------------------------------------- /EasyEuler/templates/ruby: -------------------------------------------------------------------------------- 1 | =begin 2 | {% include 'description' %} 3 | =end 4 | -------------------------------------------------------------------------------- /EasyEuler/types.py: -------------------------------------------------------------------------------- 1 | import click 2 | 3 | from EasyEuler import data 4 | 5 | 6 | class ProblemType(click.ParamType): 7 | name = 'integer' 8 | 9 | def convert(self, value, param, ctx): 10 | if value is None: 11 | return None 12 | 13 | try: 14 | problem = data.problems.get(int(value)) 15 | except ValueError: 16 | self.fail('%s is not a valid integer' % value, param, ctx) 17 | 18 | if problem is None: 19 | self.fail('A problem with ID %s does not exist' % value, param, ctx) 20 | 21 | return problem 22 | 23 | 24 | class LanguageType(click.ParamType): 25 | name = 'string' 26 | 27 | def convert(self, value, param, ctx): 28 | if value is None: 29 | return None 30 | 31 | language = data.config['languages'].get(value) 32 | 33 | if language is None: 34 | self.fail('Could not find language %s' % value, param, ctx) 35 | 36 | return {'name': value, **language} 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Encrylize 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst requirements.txt LICENSE 2 | include EasyEuler/config.json 3 | recursive-include EasyEuler/data problems.json resources/* 4 | graft EasyEuler/templates 5 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | EasyEuler 2 | ========= 3 | 4 | EasyEuler is a configurable command line tool for working with Project 5 | Euler. It provides support for many languages out of the box and adding 6 | more is easy. 7 | 8 | This project was inspired by 9 | `EulerPy `__ and intends to provide 10 | the same functionality for a larger variety of languages. 11 | 12 | Installation 13 | ============ 14 | 15 | EasyEuler can be installed from PyPI using 16 | `pip `__: 17 | 18 | .. code:: bash 19 | 20 | $ pip install easyeuler 21 | 22 | Usage 23 | ===== 24 | 25 | Use ``create`` to create a new problem file: 26 | 27 | .. code:: bash 28 | 29 | $ easyeuler create 1 python 30 | Written to euler_001.py 31 | 32 | $ cat euler_001.py 33 | """ 34 | Problem 1: Multiples of 3 and 5 35 | 36 | If we list all the natural numbers below 10 that are multiples of 3 or 5, 37 | we get 3, 5, 6 and 9. The sum of these multiples is 23. 38 | 39 | Find the sum of all the multiples of 3 or 5 below 1000. 40 | 41 | """ 42 | 43 | Once you've come up with a solution, output the result and check if it's 44 | correct with ``verify``: 45 | 46 | .. code:: bash 47 | 48 | $ easyeuler verify euler_001.py 49 | Checking output of euler_001.py: [no output] # output in red 50 | 51 | $ echo print(12345) > euler_001.py 52 | $ easyeuler verify euler_001.py 53 | Checking output of euler_001.py: 12345 # incorrect solution, output in red 54 | 55 | $ echo print(42) > euler_001.py 56 | $ easyeuler verify euler_001.py 57 | Checking output of euler_001.py: 42 # correct solution, output in green 58 | 59 | You can even time the execution of your solutions with the ``time`` 60 | flag: 61 | 62 | .. code:: bash 63 | 64 | $ easyeuler verify --time euler_001.py 65 | Checking output of euler_001.py: 42 66 | CPU times - user: 16.7ms, system: 3.33ms, total: 20ms 67 | Wall time: 1.02s 68 | 69 | ...and execute multiple at once: 70 | 71 | .. code:: bash 72 | 73 | $ easyeuler verify * 74 | Checking output of euler_001.py: 42 75 | Checking output of euler_002.c: 12345 76 | Checking output of euler_003.py: [error] # [error] is displayed if an error occurs during execution 77 | 78 | Some problems come with additional files, use ``generate-resources`` to 79 | generate those: 80 | 81 | .. code:: bash 82 | 83 | $ easyeuler create 22 python 84 | Written to euler_022.py 85 | $ cat euler_022.py 86 | """ 87 | Problem 22: Names scores 88 | 89 | [....] 90 | 91 | This problem references the following resources: 92 | 93 | names.txt 94 | 95 | """ 96 | 97 | $ easyeuler generate-resources 22 # specify the problem ID to generate problem-specific resources 98 | Created names.txt at path . 99 | 100 | $ easyeuler generate-resources # or leave it empty to generate all resources 101 | [....] 102 | Created 326_formula2.gif at path . 103 | Created 326_formula1.gif at path . 104 | Created 327_rooms_of_doom.gif at path . 105 | Created 330_formula.gif at path . 106 | 107 | Use ``list`` and ``show`` to browse problems: 108 | 109 | .. code:: bash 110 | 111 | $ easyeuler list 112 | ╒══════╤════════════════════════════════════╤══════════════╕ 113 | │ ID │ Name │ Difficulty │ 114 | ╞══════╪════════════════════════════════════╪══════════════╡ 115 | │ 1 │ Multiples of 3 and 5 │ 5% │ 116 | ├──────┼────────────────────────────────────┼──────────────┤ 117 | │ 2 │ Even Fibonacci numbers │ 5% │ 118 | ├──────┼────────────────────────────────────┼──────────────┤ 119 | │ 3 │ Largest prime factor │ 5% │ 120 | ├──────┼────────────────────────────────────┼──────────────┤ 121 | [....] 122 | 123 | $ easyeuler show 2 124 | Problem 2: Even Fibonacci numbers 125 | 126 | Each new term in the Fibonacci sequence is generated by adding the 127 | previous two terms. By starting with 1 and 2, the first 10 terms will be: 128 | 129 | 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, ... 130 | 131 | Find the sum of all the even-valued terms in the sequence which do not 132 | exceed four million. 133 | 134 | Configuration 135 | ============= 136 | 137 | EasyEuler is designed to be configurable and adaptable to any language 138 | you may want to use it with. It follows the `XDG Base Directory Specification 139 | `__ 140 | for locating configuration files. The default location is 141 | ``$HOME/.config/EasyEuler``. 142 | To see examples of configuration, look at ``config.json`` and the ``templates`` 143 | directory inside the package. 144 | 145 | Languages 146 | ~~~~~~~~~ 147 | Adding a new language is as easy as adding a few lines to the ``config.json`` 148 | file. 149 | 150 | A language has the following general attributes: 151 | 152 | - ``name`` - name of the language. (required) 153 | - ``extension`` - file extension for the language. (required) 154 | - ``template`` - name of the template file. (default: ``name``) 155 | 156 | These commands are executed in order when using the ``verify`` command: 157 | 158 | - ``build`` - build the file if required. 159 | - ``execute`` - time this command and compare the output to the solution. (default: ``./{path}``) 160 | - ``cleanup`` - remove binary files after execution, etc. 161 | 162 | Templates 163 | ~~~~~~~~~ 164 | Templates use the `Jinja2 `__ templating engine. 165 | New templates should go in the ``templates`` directory inside the configuration 166 | directory. 167 | 168 | Requirements 169 | ============ 170 | 171 | EasyEuler requires `Python 172 | 3.5+ `__, along 173 | with the `Click `__, 174 | `Jinja2 `__ and 175 | `tabulate `__ modules. 176 | It has been tested on Windows and Linux and it should work on any other 177 | Unix-based platforms, including macOS. 178 | 179 | Contributing 180 | ============ 181 | Please see `CONTRIBUTING.rst 182 | `__ 183 | for information on how to contribute to this project. 184 | 185 | Acknowledgements 186 | ================ 187 | 188 | The problem descriptions are courtesy of the 189 | `EulerPy `__ project, which 190 | formatted the descriptions from Kyle Keen's `Local 191 | Euler `__ project into a human-readable 192 | form. 193 | 194 | License 195 | ======= 196 | 197 | EasyEuler is licensed under the `MIT 198 | license `__. 199 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | click==6.6 2 | Jinja2==2.8 3 | tabulate=0.7.5 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | from setuptools import setup 4 | 5 | requirements = ['Click', 'Jinja2', 'tabulate'] 6 | 7 | if 'win32' in sys.platform.lower(): 8 | # Windows needs colorama for the terminal colors to work. 9 | requirements.append('colorama') 10 | 11 | 12 | def get_readme(): 13 | with open('README.rst', encoding='UTF-8') as readme: 14 | return readme.read() 15 | 16 | setup( 17 | name='EasyEuler', 18 | version='1.2.1', 19 | description='A command line tool for Project Euler', 20 | long_description=get_readme(), 21 | license='MIT', 22 | author='Encrylize', 23 | author_email='encrylize@gmail.com', 24 | url='https://github.com/Encrylize/EasyEuler', 25 | keywords=['EasyEuler', 'ProjectEuler', 'euler', 'Project-Euler'], 26 | classifiers=[ 27 | 'License :: OSI Approved :: MIT License', 28 | 'Topic :: Utilities', 29 | 'Environment :: Console', 30 | 'Intended Audience :: Developers', 31 | 'Development Status :: 5 - Production/Stable', 32 | 'Programming Language :: Python :: 3.5', 33 | 'Operating System :: Microsoft :: Windows', 34 | 'Operating System :: Unix', 35 | 'Natural Language :: English' 36 | ], 37 | packages=['EasyEuler', 'EasyEuler.commands'], 38 | install_requires=requirements, 39 | entry_points=''' 40 | [console_scripts] 41 | easyeuler=EasyEuler.cli:cli 42 | ''', 43 | include_package_data=True, 44 | zip_safe=False 45 | ) 46 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | import coverage 5 | 6 | tests = unittest.TestLoader().discover('tests') 7 | cov = coverage.coverage(branch=True, include='EasyEuler/*.py') 8 | BASE_PATH = os.path.abspath(os.path.dirname(__file__)) 9 | COVERAGE_PATH = os.path.join(BASE_PATH, 'tmp/coverage') 10 | 11 | cov.start() 12 | unittest.TextTestRunner(verbosity=2).run(tests) 13 | cov.stop() 14 | cov.save() 15 | 16 | print('Coverage Summary:') 17 | cov.report() 18 | cov.html_report(directory=COVERAGE_PATH) 19 | print('HTML version: %s/index.html' % COVERAGE_PATH) 20 | cov.erase() 21 | -------------------------------------------------------------------------------- /tests/test_commands.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | 4 | from click.testing import CliRunner 5 | 6 | from EasyEuler import data, paths 7 | from EasyEuler.cli import cli 8 | 9 | 10 | class CommandTestCase(unittest.TestCase): 11 | def setUp(self): 12 | self.runner = CliRunner() 13 | 14 | 15 | class TestCreateCommand(CommandTestCase): 16 | def test_file_creation(self): 17 | with self.runner.isolated_filesystem(): 18 | self.runner.invoke(cli, ['create', '1']) 19 | self.runner.invoke(cli, ['create', '1', 'c']) 20 | 21 | self.assertTrue(os.path.exists('euler_001.py')) 22 | self.assertTrue(os.path.exists('euler_001.c')) 23 | 24 | def test_file_already_exists(self): 25 | with self.runner.isolated_filesystem(): 26 | open('euler_001.py', 'a').close() 27 | result = self.runner.invoke(cli, ['create', '1', 'python'], 28 | input='n\n') 29 | 30 | self.assertTrue(os.path.getsize('euler_001.py') == 0) 31 | 32 | def test_overwrite(self): 33 | with self.runner.isolated_filesystem(): 34 | open('euler_001.py', 'a').close() 35 | result = self.runner.invoke(cli, ['create', '1', 'python'], 36 | input='y\n') 37 | 38 | self.assertTrue(os.path.getsize('euler_001.py') > 0) 39 | 40 | def test_path(self): 41 | with self.runner.isolated_filesystem(): 42 | self.runner.invoke(cli, ['create', '--path', 'test.py', 43 | '1', 'python']) 44 | 45 | self.assertTrue(os.path.exists('test.py')) 46 | 47 | def test_invalid_problem_id(self): 48 | result = self.runner.invoke(cli, ['create', '0']) 49 | self.assertEqual(result.exit_code, 2) 50 | 51 | 52 | class TestGenerateResourcesCommand(CommandTestCase): 53 | def test_generate_problem_resources(self): 54 | with self.runner.isolated_filesystem(): 55 | self.runner.invoke(cli, ['generate-resources', '22']) 56 | self.assertTrue(os.path.exists('names.txt')) 57 | 58 | def test_generate_all_resources(self): 59 | with self.runner.isolated_filesystem(): 60 | self.runner.invoke(cli, ['generate-resources']) 61 | 62 | for filename in os.listdir(paths.RESOURCES): 63 | self.assertTrue(os.path.exists(filename)) 64 | 65 | def test_problem_with_no_resources(self): 66 | result = self.runner.invoke(cli, ['generate-resources', '1']) 67 | self.assertEqual(result.exit_code, 1) 68 | 69 | 70 | class TestVerifyCommand(CommandTestCase): 71 | def test_problem_verification_with_execution_only(self): 72 | with self.runner.isolated_filesystem(): 73 | problem1 = data.problems[1] 74 | problem2 = data.problems[2] 75 | 76 | with open('euler_001.py', 'w') as f: 77 | f.write('print(%s)' % problem1['answer']) 78 | 79 | with open('euler_002.py', 'w') as f: 80 | f.write('print(%s)' % problem2['answer']) 81 | 82 | result = self.runner.invoke(cli, ['verify', 83 | 'euler_001.py', 'euler_002.py']) 84 | output = str(result.output_bytes, encoding='UTF-8') 85 | 86 | self.assertIn(problem1['answer'], output) 87 | self.assertIn(problem2['answer'], output) 88 | 89 | def test_problem_verification_with_build_and_cleanup(self): 90 | problem = data.problems[1] 91 | with self.runner.isolated_filesystem(): 92 | with open('euler_001.c', 'w') as f: 93 | f.writelines(['#include \n', 94 | 'int main(void) {\n', 95 | 'printf("%s");\n' % problem['answer'], 96 | 'return 0;\n', 97 | '}\n']) 98 | 99 | result = self.runner.invoke(cli, ['verify', '-e', 'euler_001.c']) 100 | output = str(result.output_bytes, encoding='UTF-8') 101 | 102 | self.assertIn(problem['answer'], output) 103 | self.assertFalse(os.path.exists('euler_001.c.out')) 104 | 105 | def test_recursive_verification(self): 106 | with self.runner.isolated_filesystem(): 107 | os.mkdir('test') 108 | problem = data.problems[1] 109 | 110 | with open('test/euler_001.py', 'w') as f: 111 | f.write('print(%s)' % problem['answer']) 112 | 113 | result = self.runner.invoke(cli, ['verify', '--recursive', 114 | 'test']) 115 | output = str(result.output_bytes, encoding='UTF-8') 116 | 117 | self.assertIn(problem['answer'], output) 118 | 119 | def test_show_execute_errors(self): 120 | with self.runner.isolated_filesystem(): 121 | with open('euler_001.py', 'w') as f: 122 | f.write('print(') 123 | 124 | result = self.runner.invoke(cli, ['verify', 'euler_001.py']) 125 | output = str(result.output_bytes, encoding='UTF-8') 126 | result_with_errors = self.runner.invoke(cli, ['verify', '--errors', 127 | 'euler_001.py']) 128 | output_with_errors = str(result_with_errors.output_bytes, 129 | encoding='UTF-8') 130 | 131 | self.assertIn('[error during execute]', output) 132 | self.assertIn('SyntaxError', output_with_errors) 133 | 134 | def test_show_build_errors(self): 135 | with self.runner.isolated_filesystem(): 136 | with open('euler_001.c', 'w') as f: 137 | f.write('#include ') 138 | 139 | result = self.runner.invoke(cli, ['verify', 'euler_001.c']) 140 | output = str(result.output_bytes, encoding='UTF-8') 141 | result_with_errors = self.runner.invoke(cli, ['verify', '--errors', 142 | 'euler_001.c']) 143 | output_with_errors = str(result_with_errors.output_bytes, 144 | encoding='UTF-8') 145 | 146 | self.assertIn('[error during build]', output) 147 | self.assertIn('fatal error: invalid_header.h', output_with_errors) 148 | -------------------------------------------------------------------------------- /tests/test_data.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | from EasyEuler.data import ProblemList, ConfigurationDictionary 4 | 5 | 6 | class TestProblemList(unittest.TestCase): 7 | def setUp(self): 8 | problem_list = [1, 2, 3] 9 | self.problems = ProblemList(problem_list) 10 | 11 | def test_get_item(self): 12 | problem = self.problems[1] 13 | self.assertEqual(problem, 1) 14 | 15 | def test_get_valid_id(self): 16 | problem = self.problems.get(3) 17 | self.assertIsNotNone(problem) 18 | 19 | def test_get_invalid_id(self): 20 | problem1 = self.problems.get(0) 21 | problem2 = self.problems.get(99999) 22 | 23 | self.assertIsNone(problem1) 24 | self.assertIsNone(problem2) 25 | 26 | 27 | class TestConfigurationDictionary(unittest.TestCase): 28 | def setUp(self): 29 | config_dict = { 30 | 'foo': 'bar', 31 | 'fuz': 'baz', 32 | 'languages': { 33 | 'python': { 34 | 'extension': 'py', 35 | 'template': 'python' 36 | }, 37 | 'c': { 38 | 'extension': 'c', 39 | 'template': 'c' 40 | }, 41 | 'ruby': { 42 | 'extension': 'rb', 43 | 'template': 'ruby' 44 | } 45 | } 46 | } 47 | overriding_dict = { 48 | 'fuz': 'qux', 49 | 'languages': { 50 | 'c': { 51 | 'template': 'cpp' 52 | }, 53 | 'c++': { 54 | 'extension': 'cpp', 55 | 'template': 'cpp' 56 | } 57 | } 58 | } 59 | self.config = ConfigurationDictionary([config_dict, overriding_dict]) 60 | 61 | def test_update(self): 62 | self.assertEqual(self.config['fuz'], 'qux') 63 | self.assertEqual(self.config['languages']['c']['template'], 'cpp') 64 | self.assertEqual(self.config['languages']['c']['extension'], 'c') 65 | self.assertEqual(self.config['languages']['c++']['template'], 'cpp') 66 | self.assertEqual(self.config['languages']['c++']['extension'], 'cpp') 67 | 68 | def test_get_item(self): 69 | self.assertEqual(self.config['foo'], 'bar') 70 | 71 | def test_get_language(self): 72 | python = self.config.get_language('extension', 'py') 73 | ruby = self.config.get_language('template', 'ruby') 74 | invalid_language = self.config.get_language('template', 'foobar') 75 | 76 | self.assertEqual(python['name'], 'python') 77 | self.assertEqual(ruby['extension'], 'rb') 78 | self.assertIsNone(invalid_language) 79 | --------------------------------------------------------------------------------