From 79b6f9319188f5391219510f4c7388305a70afb2 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 11 Apr 2019 13:03:07 +0300 Subject: [PATCH 001/144] Add setup.py to the project --- final_task/setup.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/final_task/setup.py b/final_task/setup.py index e69de29b..656cd746 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,20 @@ +from os import path +from setuptools import setup + +here = path.abspath(path.dirname(__file__)) + +with open(path.join(here, 'README.md')) as f: + long_description = f.read() + +setup( + name='calc', + version='0.1.0', + description='A pure-python command-line calculator', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/siarhiejkresik/PythonHomework', + author='Siarhiej Kresik', + author_email='siarhiej.kresik@gmail.com', + keywords='calculator calc cli', + python_requires='>=3.6' +) From 1fb4f8a38d3d40ed662e43ddde1b31103e5fdc1c Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 11 Apr 2019 16:03:57 +0300 Subject: [PATCH 002/144] Add Makefile --- final_task/Makefile | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 final_task/Makefile diff --git a/final_task/Makefile b/final_task/Makefile new file mode 100644 index 00000000..b1abe443 --- /dev/null +++ b/final_task/Makefile @@ -0,0 +1,16 @@ +.DEFAULT_GOAL := run + +PROGRAMM_NAME := pycalc +SRC_FOLDER := $(PROGRAMM_NAME) + + +install-dev: + @echo "Installing in the development mode..." + pip3 install --editable . + +uninstall-dev: + @echo "Uninstalling calc..." + pip3 uninstall $(PROGRAMM_NAME) -y + +run: + python3 -m $(SRC_FOLDER) \ No newline at end of file From e1440e9ce73168b2f0b464d873455160bbbd6a61 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 11 Apr 2019 17:48:24 +0300 Subject: [PATCH 003/144] Change the program name in the setup.py --- final_task/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/final_task/setup.py b/final_task/setup.py index 656cd746..a117a5dc 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -7,7 +7,7 @@ long_description = f.read() setup( - name='calc', + name='pycalc', version='0.1.0', description='A pure-python command-line calculator', long_description=long_description, From ccf69793cd0cf506212595fccee99b3d0c16e994 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 11 Apr 2019 18:31:00 +0300 Subject: [PATCH 004/144] setup.py --- final_task/setup.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/final_task/setup.py b/final_task/setup.py index a117a5dc..0ecb6811 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,5 +1,5 @@ from os import path -from setuptools import setup +from setuptools import setup, find_packages here = path.abspath(path.dirname(__file__)) @@ -13,8 +13,14 @@ long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/siarhiejkresik/PythonHomework', + packages=find_packages(), author='Siarhiej Kresik', author_email='siarhiej.kresik@gmail.com', keywords='calculator calc cli', - python_requires='>=3.6' + python_requires='>=3.6', + entry_points={ + "console_scripts": [ + "pycalc=pycalc.__main__:main", + ] + }, ) From 58f0d9aa359f437bf38976483b4ea2f908e6d6f1 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 11 Apr 2019 18:31:33 +0300 Subject: [PATCH 005/144] Makefile --- final_task/Makefile | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/final_task/Makefile b/final_task/Makefile index b1abe443..343a5e83 100644 --- a/final_task/Makefile +++ b/final_task/Makefile @@ -2,15 +2,30 @@ PROGRAMM_NAME := pycalc SRC_FOLDER := $(PROGRAMM_NAME) +INSTALLED_EXECUTABLE_PATH := ~/.local/bin +PIP_LOCAL := --user + +run: + $(INSTALLED_EXECUTABLE_PATH)/$(PROGRAMM_NAME) + + +# run from the source folder +run-source: + python3 -m $(SRC_FOLDER) + +# locally +install: + @echo "Installing..." + pip3 install $(PIP_LOCAL) . install-dev: @echo "Installing in the development mode..." - pip3 install --editable . + pip3 install $(PIP_LOCAL) --editable . -uninstall-dev: - @echo "Uninstalling calc..." +uninstall: + @echo "Uninstalling..." pip3 uninstall $(PROGRAMM_NAME) -y -run: - python3 -m $(SRC_FOLDER) \ No newline at end of file +show: + pip3 show $(PROGRAMM_NAME) From 355ad34a9492aa84cf1e349041ecae86ebe831db Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 11 Apr 2019 20:14:56 +0300 Subject: [PATCH 006/144] Makefile --- final_task/Makefile | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/final_task/Makefile b/final_task/Makefile index 343a5e83..f9254729 100644 --- a/final_task/Makefile +++ b/final_task/Makefile @@ -2,30 +2,33 @@ PROGRAMM_NAME := pycalc SRC_FOLDER := $(PROGRAMM_NAME) -INSTALLED_EXECUTABLE_PATH := ~/.local/bin +INSTALLED_EXECUTABLE_DIR := ~/.local/bin +PYTHON := python3 + +PIP := pip3 PIP_LOCAL := --user run: - $(INSTALLED_EXECUTABLE_PATH)/$(PROGRAMM_NAME) - + $(INSTALLED_EXECUTABLE_DIR)/$(PROGRAMM_NAME) -# run from the source folder run-source: - python3 -m $(SRC_FOLDER) + $(PYTHON) -m $(SRC_FOLDER) -# locally install: @echo "Installing..." - pip3 install $(PIP_LOCAL) . + $(PIP) install $(PIP_LOCAL) . install-dev: @echo "Installing in the development mode..." - pip3 install $(PIP_LOCAL) --editable . + $(PIP) install $(PIP_LOCAL) --editable . uninstall: @echo "Uninstalling..." - pip3 uninstall $(PROGRAMM_NAME) -y + $(PIP) uninstall $(PROGRAMM_NAME) -y show: - pip3 show $(PROGRAMM_NAME) + $(PIP) show $(PROGRAMM_NAME) + +pycodestyle: + pycodestyle $(PROGRAMM_NAME)/* From 73f6f1f13073005fecf277e7f2fa0e233036b1c6 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 11 Apr 2019 23:09:13 +0300 Subject: [PATCH 007/144] Args --- final_task/pycalc/args.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 final_task/pycalc/args.py diff --git a/final_task/pycalc/args.py b/final_task/pycalc/args.py new file mode 100644 index 00000000..ada68b87 --- /dev/null +++ b/final_task/pycalc/args.py @@ -0,0 +1,37 @@ +import argparse + +PARSER = { + 'description': 'Pure-python command-line calculator.' +} + +EXPRESSION = { + 'name_or_flags': ['expression'], + 'keyword_arguments': { + 'metavar': 'EXPRESSION', + 'type': str, + 'help': 'expression string to evaluate' + } +} + +MODULE = { + 'name_or_flags': ['-m', '--use-modules'], + 'keyword_arguments': { + 'metavar': 'MODULE', + 'type': str, + 'nargs': '+', + 'help': 'additional modules to use', + 'dest': 'modules' + } +} + +arguments = [ + EXPRESSION, + MODULE, +] + +parser = argparse.ArgumentParser(**PARSER) + +for arg in arguments: + parser.add_argument(*arg['name_or_flags'], **arg['keyword_arguments']) + +args = parser.parse_args() From 248bb2c7d13407acb081c9affc7cde521f149fee Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 26 Apr 2019 22:53:13 +0300 Subject: [PATCH 008/144] Implement importing members from a module --- final_task/pycalc/imports.py | 51 ++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) create mode 100644 final_task/pycalc/imports.py diff --git a/final_task/pycalc/imports.py b/final_task/pycalc/imports.py new file mode 100644 index 00000000..c4ae44ba --- /dev/null +++ b/final_task/pycalc/imports.py @@ -0,0 +1,51 @@ +"""""" + +from importlib import import_module +from inspect import getmembers + +NUMERIC_TYPES = (int, float, complex) +UNDERSCORE = '_' + +module_names = ['math'] + + +def is_numeric(obj) -> bool: + """Return `True` if a object is instance of numeric types""" + + return isinstance(obj, (NUMERIC_TYPES)) + + +# def is_callable(obj) -> bool: +# """Return `True` if a object is callable""" + +# return callable(obj) + + +def starts_with_underscore(string: str): + """""" + return string.startswith(UNDERSCORE) + + +def importing_module(name): + """""" + return import_module(name) + + +m = importing_module('math') + + +def get_module_members(module, predicat): + """""" + return [ + member[0] for member in getmembers(module, predicat) + if not starts_with_underscore(member[0]) + ] + + # return members + # members = getmembers(module, predicat) +# def get_members(module_name): + + +from pprint import pprint +pprint(get_module_members(m, is_numeric)) +pprint(get_module_members(m, callable)) From f9adf74975d0985e25dc0a001fddeabc673fba6a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sat, 27 Apr 2019 00:56:59 +0300 Subject: [PATCH 009/144] Update importing from a module --- final_task/pycalc/imports.py | 79 ++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/final_task/pycalc/imports.py b/final_task/pycalc/imports.py index c4ae44ba..340e7462 100644 --- a/final_task/pycalc/imports.py +++ b/final_task/pycalc/imports.py @@ -1,51 +1,88 @@ """""" +# TODO: exception when import fails +from collections import OrderedDict, defaultdict from importlib import import_module from inspect import getmembers NUMERIC_TYPES = (int, float, complex) UNDERSCORE = '_' +DEFAULT_MODULE_NAMES = ('math',) -module_names = ['math'] + +def dedupe_to_list(iterable) -> list: + """""" + + return list(OrderedDict.fromkeys(iterable)) def is_numeric(obj) -> bool: - """Return `True` if a object is instance of numeric types""" + """Return `True` if a object is of numeric types""" return isinstance(obj, (NUMERIC_TYPES)) -# def is_callable(obj) -> bool: -# """Return `True` if a object is callable""" +def merge_module_names(module_names: tuple) -> list: + """""" + + m_names = list(DEFAULT_MODULE_NAMES) + if module_names: + m_names.extend(module_names) -# return callable(obj) + return m_names -def starts_with_underscore(string: str): +def get_module_members_names_by_type(module, type_checker) -> list: """""" - return string.startswith(UNDERSCORE) + return [ + member[0] for member in getmembers(module, type_checker) + if not member[0].startswith(UNDERSCORE) + ] + + +MEMBER_TYPES = { + 'function': callable, + 'constants': is_numeric +} -def importing_module(name): + +def get_module_members_names(module_name: tuple) -> dict: """""" - return import_module(name) + result = defaultdict(set) + + module = import_module(module_name) -m = importing_module('math') + for type_, type_checker in MEMBER_TYPES.items(): + members = get_module_members_names_by_type(module, type_checker) + result[type_].update(members) + return module, result -def get_module_members(module, predicat): + +def get(module_names: tuple = None) -> dict: """""" - return [ - member[0] for member in getmembers(module, predicat) - if not starts_with_underscore(member[0]) - ] - # return members - # members = getmembers(module, predicat) -# def get_members(module_name): + if not module_names: + module_names = [] + + m_names = merge_module_names(module_names) + m_names = dedupe_to_list(m_names) + + result = {} + for module_name in m_names: + module, members = get_module_members_names(module_name) + result[module_name] = {} + result[module_name]['module'] = module + result[module_name]['members'] = members + + return result -from pprint import pprint -pprint(get_module_members(m, is_numeric)) -pprint(get_module_members(m, callable)) +if __name__ == '__main__': + names = ('calendar',) + from pprint import pprint + for key, value in get(names).items(): + print(key) + pprint(value) From 4573b754d0736a789a7aee08b8522c456e87fe43 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sat, 27 Apr 2019 01:01:40 +0300 Subject: [PATCH 010/144] Change members names storing from set to list --- final_task/pycalc/imports.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/final_task/pycalc/imports.py b/final_task/pycalc/imports.py index 340e7462..cc8410b7 100644 --- a/final_task/pycalc/imports.py +++ b/final_task/pycalc/imports.py @@ -50,13 +50,13 @@ def get_module_members_names_by_type(module, type_checker) -> list: def get_module_members_names(module_name: tuple) -> dict: """""" - result = defaultdict(set) + result = defaultdict(list) module = import_module(module_name) for type_, type_checker in MEMBER_TYPES.items(): members = get_module_members_names_by_type(module, type_checker) - result[type_].update(members) + result[type_].extend(members) return module, result @@ -81,7 +81,7 @@ def get(module_names: tuple = None) -> dict: if __name__ == '__main__': - names = ('calendar',) + names = ('calendar', 'pprint') from pprint import pprint for key, value in get(names).items(): print(key) From ecc888b4c2ee147f698c3baceb909bce5b97cba5 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sat, 27 Apr 2019 01:38:23 +0300 Subject: [PATCH 011/144] Refactor, fix a bug, add exception handling --- final_task/pycalc/imports.py | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/final_task/pycalc/imports.py b/final_task/pycalc/imports.py index cc8410b7..28297e13 100644 --- a/final_task/pycalc/imports.py +++ b/final_task/pycalc/imports.py @@ -47,32 +47,31 @@ def get_module_members_names_by_type(module, type_checker) -> list: } -def get_module_members_names(module_name: tuple) -> dict: +def get_module_members_names(module): """""" - result = defaultdict(list) - - module = import_module(module_name) - - for type_, type_checker in MEMBER_TYPES.items(): - members = get_module_members_names_by_type(module, type_checker) - result[type_].extend(members) - - return module, result + return {type_: get_module_members_names_by_type(module, type_checker) + for type_, type_checker in MEMBER_TYPES.items()} def get(module_names: tuple = None) -> dict: """""" if not module_names: - module_names = [] + module_names = tuple() m_names = merge_module_names(module_names) m_names = dedupe_to_list(m_names) result = {} + for module_name in m_names: - module, members = get_module_members_names(module_name) + try: + module = import_module(module_name) + except ModuleNotFoundError: + raise ModuleNotFoundError + + members = get_module_members_names(module) result[module_name] = {} result[module_name]['module'] = module result[module_name]['members'] = members @@ -81,8 +80,8 @@ def get(module_names: tuple = None) -> dict: if __name__ == '__main__': - names = ('calendar', 'pprint') + MODULE_NAMES = ('calendar', 'pprint') from pprint import pprint - for key, value in get(names).items(): + for key, value in get(MODULE_NAMES).items(): print(key) pprint(value) From c7a16ce35ce4825fe04b02e3d7812da7a4dba86a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sat, 27 Apr 2019 02:08:17 +0300 Subject: [PATCH 012/144] Add a function checker --- final_task/pycalc/imports.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/final_task/pycalc/imports.py b/final_task/pycalc/imports.py index 28297e13..e1943173 100644 --- a/final_task/pycalc/imports.py +++ b/final_task/pycalc/imports.py @@ -1,11 +1,14 @@ """""" # TODO: exception when import fails -from collections import OrderedDict, defaultdict +from collections import OrderedDict +from functools import partial from importlib import import_module from inspect import getmembers +from types import BuiltinFunctionType, FunctionType, LambdaType NUMERIC_TYPES = (int, float, complex) +FUNCTION_TYPES = (BuiltinFunctionType, FunctionType, LambdaType, partial) UNDERSCORE = '_' DEFAULT_MODULE_NAMES = ('math',) @@ -17,11 +20,17 @@ def dedupe_to_list(iterable) -> list: def is_numeric(obj) -> bool: - """Return `True` if a object is of numeric types""" + """Return `True` if a object is one of numeric types.""" return isinstance(obj, (NUMERIC_TYPES)) +def is_function(obj) -> bool: + """Return `True` if a object is a function.""" + + return isinstance(obj, (FUNCTION_TYPES)) + + def merge_module_names(module_names: tuple) -> list: """""" @@ -42,7 +51,7 @@ def get_module_members_names_by_type(module, type_checker) -> list: MEMBER_TYPES = { - 'function': callable, + 'function': is_function, 'constants': is_numeric } From 9f9ec2fc358a7a53460a161134e08a5b82fc6842 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sat, 27 Apr 2019 02:16:32 +0300 Subject: [PATCH 013/144] Format code --- final_task/pycalc/imports.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/imports.py b/final_task/pycalc/imports.py index e1943173..26108f35 100644 --- a/final_task/pycalc/imports.py +++ b/final_task/pycalc/imports.py @@ -7,9 +7,11 @@ from inspect import getmembers from types import BuiltinFunctionType, FunctionType, LambdaType + NUMERIC_TYPES = (int, float, complex) FUNCTION_TYPES = (BuiltinFunctionType, FunctionType, LambdaType, partial) UNDERSCORE = '_' + DEFAULT_MODULE_NAMES = ('math',) @@ -51,7 +53,7 @@ def get_module_members_names_by_type(module, type_checker) -> list: MEMBER_TYPES = { - 'function': is_function, + 'functions': is_function, 'constants': is_numeric } From 0a12d15df469bab5463907e5a806b9af4f573244 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sat, 27 Apr 2019 02:21:44 +0300 Subject: [PATCH 014/144] Rename imports.py file to importer.py --- final_task/pycalc/{imports.py => importer.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename final_task/pycalc/{imports.py => importer.py} (100%) diff --git a/final_task/pycalc/imports.py b/final_task/pycalc/importer.py similarity index 100% rename from final_task/pycalc/imports.py rename to final_task/pycalc/importer.py From d05df4d6bd07cdec58ed9f01f0a8ea9f5dbecb56 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 28 Apr 2019 00:55:12 +0300 Subject: [PATCH 015/144] Add lexer --- final_task/pycalc/lexer/lexer.py | 35 ++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 final_task/pycalc/lexer/lexer.py diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py new file mode 100644 index 00000000..aa7e4f50 --- /dev/null +++ b/final_task/pycalc/lexer/lexer.py @@ -0,0 +1,35 @@ +from pycalc.token.builder import build_token + + +class Lexer: + def __init__(self, source): + self.source = source + self.matchers = [] + self.pos = 0 + self.length = len(source) + + def _advance_pos(self, value): + """""" + + self.pos += value + + def get_next_token(self): + """""" + + if self.pos >= self.length: + return EOF + + for matcher in self.matchers: + result = matcher(self.source, self.pos) + + if not result: + continue + + (lexeme, token_type) = result + token = build_token(lexeme, token_type) + + self._advance_pos(len(lexeme)) + + return token + + raise Exception('No match') From 976bfa39d8bf7f39a22570c3fdeb551b93972399 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 29 Apr 2019 11:33:07 +0300 Subject: [PATCH 016/144] Add helpers functions for the matcher package --- final_task/pycalc/matcher/helpers.py | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 final_task/pycalc/matcher/helpers.py diff --git a/final_task/pycalc/matcher/helpers.py b/final_task/pycalc/matcher/helpers.py new file mode 100644 index 00000000..e001f6bc --- /dev/null +++ b/final_task/pycalc/matcher/helpers.py @@ -0,0 +1,56 @@ +"""""" + +import re + + +def list_sorted_by_length(iterable) -> list: + """""" + + return sorted(iterable, key=len, reverse=True) + + +def construct_literals_list(literals): + """""" + + sorted_literals = list_sorted_by_length(literals) + return sorted_literals + + +def construct_regex(literals): + """Return regex string for... . + + >>> construct_regex_string(['🏇', cos', 'arcsin', 'sin', pi()']) + arcsin|pi\(\)|cos|sin|\🏇 + + """ + + literals_list = construct_literals_list(literals) + regex_string = '|'.join(map(re.escape, literals_list)) + regex = re.compile(regex_string) + + return regex + + +def regex_matcher(regex): + """""" + + def matcher(string, pos): + """""" + result = regex.match(string, pos) + if result: + return result.group() + + return matcher + + +def text_matcher(literals): + """""" + + def matcher(string, pos): + """""" + + for literal in literals: + if string.startswith(literal, pos): + return literal + + return matcher From eae42900a77e17500e86802773a70ae4e6c349ad Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 29 Apr 2019 11:35:01 +0300 Subject: [PATCH 017/144] Add the numbers matcher --- final_task/pycalc/matcher/number.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 final_task/pycalc/matcher/number.py diff --git a/final_task/pycalc/matcher/number.py b/final_task/pycalc/matcher/number.py new file mode 100644 index 00000000..f5752725 --- /dev/null +++ b/final_task/pycalc/matcher/number.py @@ -0,0 +1,20 @@ +"""""" + +import re + +from .helpers import regex_matcher + +NUMBER = r''' +( # integers or numbers with a fractional part: 13, 154., 3.44, ... +\d+ # an integer part: 10, 2, 432, ... +(\.\d*)* # a fractional part: .2, .43, .1245, ... or dot: . +) +| +( # numbers that begin with a dot: .12, .59, ... +\.\d+ # a fractional part: .2, .43, .1245, ... +) +''' + +NUMBER_REGEX = re.compile(NUMBER, re.VERBOSE) + +NUMBER_MATCHER = regex_matcher(NUMBER_REGEX) From 126e16e95759af48019a9f1ba00ef024b5ff583c Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 29 Apr 2019 11:38:24 +0300 Subject: [PATCH 018/144] Add the Matchers class --- final_task/pycalc/matcher/matcher.py | 77 ++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) create mode 100644 final_task/pycalc/matcher/matcher.py diff --git a/final_task/pycalc/matcher/matcher.py b/final_task/pycalc/matcher/matcher.py new file mode 100644 index 00000000..2d4f33a1 --- /dev/null +++ b/final_task/pycalc/matcher/matcher.py @@ -0,0 +1,77 @@ + +from collections import namedtuple + +from pycalc.token.constants import TokenType + +from .helpers import construct_regex, regex_matcher +from .number import NUMBER_MATCHER + + +# operation: + - etc. predefined +# constants: pi, e etc. load from module +# function: sin, abs etc. load from module +# numbers: 0, 10, 15., .03, 2.14 etc. dynamicaly matched (no literal) +# other: ( ) , predefined + +Matcher = namedtuple("Matcher", ("token_type", "matcher")) + +PREDEFINED_MATCHERS = { + TokenType.NUMERIC: NUMBER_MATCHER +} + +operators = ['+', '>', '-', '>=', '==', '<=', '!'] +functions = ['sin', 'arcsin', 'abs', 'time'] +constants = ['pi', 'e', 'nan'] +punctuations = ['(', ')', ','] + + +class Matchers: + def __init__(self): + self.matchers = [] + + def __iter__(self): + for matcher in self.matchers: + yield matcher + + def register_matcher(self, token_type, matcher): + """""" + + self.matchers.append(Matcher(token_type, matcher)) + + def create_matcher_from_regex(self, regex): + """""" + + return regex_matcher(regex) + + def create_matcher_from_literals_list(self, literals): + """""" + + return regex_matcher(construct_regex(literals)) + + +matchers = Matchers() + +matchers.register_matcher(TokenType.NUMERIC, + PREDEFINED_MATCHERS[TokenType.NUMERIC]) +matchers.register_matcher(TokenType.OPERATOR, + matchers.create_matcher_from_literals_list(operators)) +matchers.register_matcher(TokenType.CONSTANT, + matchers.create_matcher_from_literals_list(constants)) +matchers.register_matcher(TokenType.FUNCTION, + matchers.create_matcher_from_literals_list(functions)) +matchers.register_matcher(TokenType.PUNCTUATION, + matchers.create_matcher_from_literals_list(punctuations)) + +if __name__ == '__main__': + + source = '1.3>=sin(pi+e)' + pos = 0 + while True: + for token_type, matcher in matchers: + result = matcher(source, pos) + if result: + print(token_type, result) + pos += len(result) + break + if pos >= len(source): + break From 27cab52d1d8b5b7851eb9297b6bcca0c1020a62b Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 29 Apr 2019 11:39:03 +0300 Subject: [PATCH 019/144] Add __init__.py for the matcher package --- final_task/pycalc/matcher/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 final_task/pycalc/matcher/__init__.py diff --git a/final_task/pycalc/matcher/__init__.py b/final_task/pycalc/matcher/__init__.py new file mode 100644 index 00000000..e69de29b From 09638052b8c42e476ded77707bb3f9b3a7718f31 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 11 Apr 2019 13:03:07 +0300 Subject: [PATCH 020/144] Add setup.py to the project --- final_task/setup.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/final_task/setup.py b/final_task/setup.py index e69de29b..0ecb6811 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,26 @@ +from os import path +from setuptools import setup, find_packages + +here = path.abspath(path.dirname(__file__)) + +with open(path.join(here, 'README.md')) as f: + long_description = f.read() + +setup( + name='pycalc', + version='0.1.0', + description='A pure-python command-line calculator', + long_description=long_description, + long_description_content_type='text/markdown', + url='https://github.com/siarhiejkresik/PythonHomework', + packages=find_packages(), + author='Siarhiej Kresik', + author_email='siarhiej.kresik@gmail.com', + keywords='calculator calc cli', + python_requires='>=3.6', + entry_points={ + "console_scripts": [ + "pycalc=pycalc.__main__:main", + ] + }, +) From a953f2853a82635619f2794975643f23b509154a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 11 Apr 2019 16:03:57 +0300 Subject: [PATCH 021/144] Add Makefile --- final_task/Makefile | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 final_task/Makefile diff --git a/final_task/Makefile b/final_task/Makefile new file mode 100644 index 00000000..f9254729 --- /dev/null +++ b/final_task/Makefile @@ -0,0 +1,34 @@ +.DEFAULT_GOAL := run + +PROGRAMM_NAME := pycalc +SRC_FOLDER := $(PROGRAMM_NAME) +INSTALLED_EXECUTABLE_DIR := ~/.local/bin + +PYTHON := python3 + +PIP := pip3 +PIP_LOCAL := --user + +run: + $(INSTALLED_EXECUTABLE_DIR)/$(PROGRAMM_NAME) + +run-source: + $(PYTHON) -m $(SRC_FOLDER) + +install: + @echo "Installing..." + $(PIP) install $(PIP_LOCAL) . + +install-dev: + @echo "Installing in the development mode..." + $(PIP) install $(PIP_LOCAL) --editable . + +uninstall: + @echo "Uninstalling..." + $(PIP) uninstall $(PROGRAMM_NAME) -y + +show: + $(PIP) show $(PROGRAMM_NAME) + +pycodestyle: + pycodestyle $(PROGRAMM_NAME)/* From 26e5dd88dc06352c45e84766eaab215acc3fe950 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 29 Apr 2019 16:33:17 +0300 Subject: [PATCH 022/144] Refactor lexer --- final_task/pycalc/lexer/lexer.py | 63 +++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index aa7e4f50..a650673f 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -1,35 +1,64 @@ -from pycalc.token.builder import build_token +"""""" + +from collections import namedtuple + +from pycalc.token.constants import TokenType +from pycalc.matcher.matcher import matchers + + +Token = namedtuple('Token', ('token_type', 'lexeme')) class Lexer: - def __init__(self, source): + def __init__(self, source, matchers): self.source = source - self.matchers = [] + self.matchers = matchers self.pos = 0 self.length = len(source) - def _advance_pos(self, value): + def get_next_token(self): """""" - self.pos += value + if self._check_source_end(): + raise Exception('EOL') - def get_next_token(self): - """""" + token = self._next_token() - if self.pos >= self.length: - return EOF + if not token: + raise Exception('No match') - for matcher in self.matchers: - result = matcher(self.source, self.pos) + self._advance_pos_by_lexeme(token.lexeme) - if not result: - continue + return token + + def _next_token(self): + """""" - (lexeme, token_type) = result - token = build_token(lexeme, token_type) + for token_type, matcher in self.matchers: + lexeme = matcher(self.source, self.pos) - self._advance_pos(len(lexeme)) + if not lexeme: + continue + + token = Token(token_type, lexeme) return token - raise Exception('No match') + def _advance_pos_by_lexeme(self, lexeme): + """""" + + value = len(lexeme) + self.pos += value + + def _check_source_end(self): + """""" + + return self.pos >= self.length + + +if __name__ == '__main__': + + source = '1.3>=sin(pi+ e)' + l = Lexer(source, matchers) + while True: + print(l.get_next_token()) From 84623825f9aa7f91e534b1ee690782032e6486b4 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 30 Apr 2019 08:19:36 +0300 Subject: [PATCH 023/144] Refactor lexer --- final_task/pycalc/lexer/lexer.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index a650673f..8ae0f565 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -1,5 +1,6 @@ """""" +import re from collections import namedtuple from pycalc.token.constants import TokenType @@ -7,6 +8,7 @@ Token = namedtuple('Token', ('token_type', 'lexeme')) +WHITESPACES = re.compile('\s+') class Lexer: @@ -18,6 +20,7 @@ def __init__(self, source, matchers): def get_next_token(self): """""" + self._skip_whitespaces() if self._check_source_end(): raise Exception('EOL') @@ -44,6 +47,13 @@ def _next_token(self): return token + def _skip_whitespaces(self): + """""" + + whitespaces = WHITESPACES.match(self.source, self.pos) + if whitespaces: + self._advance_pos_by_lexeme(whitespaces.group()) + def _advance_pos_by_lexeme(self, lexeme): """""" @@ -58,7 +68,7 @@ def _check_source_end(self): if __name__ == '__main__': - source = '1.3>=sin(pi+ e)' + source = ' 1.3 >=sin(pi +e) ' l = Lexer(source, matchers) while True: print(l.get_next_token()) From 7f72e35cf2527061c96a5817f10f425a7bafeb8b Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 30 Apr 2019 15:14:39 +0300 Subject: [PATCH 024/144] Add parser --- final_task/pycalc/parser/parser.py | 60 ++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 final_task/pycalc/parser/parser.py diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py new file mode 100644 index 00000000..1c9b170f --- /dev/null +++ b/final_task/pycalc/parser/parser.py @@ -0,0 +1,60 @@ +"""""" +from pycalc.lexer.lexer import Lexer +from pycalc.matcher.matcher import matchers + + +class Parser: + """""" + + def __init__(self, lexer): + self.lexer = lexer + self.token = None + self.next_token = None + + def parse(self, source): + """""" + + self.lexer.init(source) + self.next() + + return self.expression() + + def next(self): + """""" + + self.token = self.next_token + self.next_token = self.lexer.get_next_token() + + def advance(self, token_class=None): + """""" + + if token_class and not self.next_token.is_instance(token_class): + raise SyntaxError(f"Expected: {token_class.__name__}") + + self.next_token = self.lexer.get_next_token() + + def expression(self, rbp=0): + """""" + + self.next() + left = self.token.prefix() + print('in expression start:', self.token, self.next_token, left) + while rbp < self.next_token.lbp: + self.next() + left = self.token.infix(left) + + return left + + +if __name__ == "__main__": + l = Lexer(matchers) + p = Parser(l) + assert p.parse('- - - 2 ** fn ( 1 , ( 2 + 1 ) * 5 , 4 )') == -1048576 + assert p.parse('- - 2') == 2 + assert p.parse('4 ** 3 ** 2') == 262144 + assert p.parse('1 + 2 * 3') == 7 + assert p.parse('( 1 + 2 ) * 3') == 9 + assert p.parse('1 + 2 == 3') is True + assert p.parse('0 == 1') is False + # TODO: + # assert p.parse('0 1') is False From 865675d3be1b34a8d7e266c14c863a1945fb1b9d Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 30 Apr 2019 18:40:24 +0300 Subject: [PATCH 025/144] Update lexer --- final_task/pycalc/lexer/lexer.py | 42 ++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 18 deletions(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index 8ae0f565..ca89c80e 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -4,31 +4,36 @@ from collections import namedtuple from pycalc.token.constants import TokenType -from pycalc.matcher.matcher import matchers +# from pycalc.matcher.matcher import matchers -Token = namedtuple('Token', ('token_type', 'lexeme')) -WHITESPACES = re.compile('\s+') +Token = namedtuple("Token", ("token_type", "lexeme")) +WHITESPACES = re.compile("\s+") class Lexer: - def __init__(self, source, matchers): - self.source = source + def __init__(self, matchers): self.matchers = matchers - self.pos = 0 - self.length = len(source) + self._source = "" + self._pos = 0 + self._length = 0 + + def init(self, source): + self._source = source + self._pos = 0 + self._length = len(source) def get_next_token(self): """""" self._skip_whitespaces() - if self._check_source_end(): - raise Exception('EOL') + if self._is_source_exhausted(): + raise Exception("EOL") token = self._next_token() if not token: - raise Exception('No match') + raise Exception("No match") self._advance_pos_by_lexeme(token.lexeme) @@ -38,7 +43,7 @@ def _next_token(self): """""" for token_type, matcher in self.matchers: - lexeme = matcher(self.source, self.pos) + lexeme = matcher(self._source, self._pos) if not lexeme: continue @@ -50,7 +55,7 @@ def _next_token(self): def _skip_whitespaces(self): """""" - whitespaces = WHITESPACES.match(self.source, self.pos) + whitespaces = WHITESPACES.match(self._source, self._pos) if whitespaces: self._advance_pos_by_lexeme(whitespaces.group()) @@ -58,17 +63,18 @@ def _advance_pos_by_lexeme(self, lexeme): """""" value = len(lexeme) - self.pos += value + self._pos += value - def _check_source_end(self): + def _is_source_exhausted(self): """""" - return self.pos >= self.length + return self._pos >= self._length -if __name__ == '__main__': +if __name__ == "__main__": - source = ' 1.3 >=sin(pi +e) ' - l = Lexer(source, matchers) + source = " 1.3 >=sin(pi +e) " + l = Lexer(matchers) + l.init(source) while True: print(l.get_next_token()) From c22a61b5ab00a2db1fece32a303bbe8d9c891406 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 30 Apr 2019 18:40:56 +0300 Subject: [PATCH 026/144] Add lexer tests --- final_task/tests/lexer/test_lexer.py | 102 +++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 final_task/tests/lexer/test_lexer.py diff --git a/final_task/tests/lexer/test_lexer.py b/final_task/tests/lexer/test_lexer.py new file mode 100644 index 00000000..233009b3 --- /dev/null +++ b/final_task/tests/lexer/test_lexer.py @@ -0,0 +1,102 @@ +import unittest + +from pycalc.lexer.lexer import Lexer + + +class InitTestCase(unittest.TestCase): + + def test_class_init(self): + '''Test class __init__() method''' + + matchers = ['1'] + lexer = Lexer(matchers) + + self.assertEqual(lexer._source, '') + self.assertEqual(lexer.matchers, matchers) + self.assertEqual(lexer._pos, 0) + self.assertEqual(lexer._length, 0) + + def test_init(self): + """""" + + matchers = ['1'] + source = 'non_empty_string' + + lexer = Lexer(matchers) + lexer._source = source * 2 # arbitrary number + lexer._pos += 13 # arbitrary number + lexer._length += 13 # arbitrary number + + lexer.init(source) + self.assertEqual(lexer._source, source) + self.assertEqual(lexer._pos, 0) + self.assertEqual(lexer._length, len(source)) + + @unittest.skip('not implemented') + def test_get_next_token(self): + pass + + @unittest.skip('not implemented') + def test__next_token(self): + pass + + def test__skip_whitespaces(self): + """""" + + lexer = Lexer([]) + test_cases = ( + (' bcd', 0, 1), + (' cd', 0, 2), + ('0 d', 1, 3), + (' ', 0, 1), + ('', 0, 0), + ('\n\tc', 0, 2), + ) + + for source, pos, expected_pos in test_cases: + with self.subTest(source=source, + pos=pos, + expected_pos=expected_pos): + lexer.init(source) + lexer._pos = pos + lexer._skip_whitespaces() + self.assertEqual(lexer._pos, expected_pos) + + def test__advance_pos_by_lexeme(self): + """""" + + lexeme = 'abcde' + pos = 10 + expected_pos = pos + len(lexeme) + + lexer = Lexer([]) + lexer._pos = pos + + lexer._advance_pos_by_lexeme(lexeme) + self.assertEqual(lexer._pos, expected_pos) + + def test__check_source_end(self): + """""" + + lexer = Lexer([]) + test_cases = ( + (0, 0, True), + (3, 3, True), + (1, 2, False), + (2, 1, True), + ) + + for pos, length, expected_result in test_cases: + with self.subTest(pos=pos, + length=length, + expected_result=expected_result + ): + lexer = Lexer([]) + lexer._pos = pos + lexer._length = length + self.assertEqual(lexer._is_source_exhausted(), + expected_result) + + +if __name__ == '__main__': + unittest.main() From efe18dd838b32c0c4ee2d46fb6b930a460e8ad9d Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 30 Apr 2019 19:31:24 +0300 Subject: [PATCH 027/144] Refactor lexer --- final_task/pycalc/lexer/lexer.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index ca89c80e..360dfb07 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -1,17 +1,20 @@ -"""""" +""" +Lexer class. +""" import re from collections import namedtuple -from pycalc.token.constants import TokenType -# from pycalc.matcher.matcher import matchers +from pycalc.matcher.matcher import matchers Token = namedtuple("Token", ("token_type", "lexeme")) -WHITESPACES = re.compile("\s+") +WHITESPACES = re.compile(r"\s+") class Lexer: + """Represents""" + def __init__(self, matchers): self.matchers = matchers self._source = "" @@ -19,12 +22,17 @@ def __init__(self, matchers): self._length = 0 def init(self, source): + """Init a lexer with a source string.""" + self._source = source self._pos = 0 self._length = len(source) def get_next_token(self): - """""" + """Return the next token. + + Raise exceptions when the source is exhausted or there are no matches.""" + self._skip_whitespaces() if self._is_source_exhausted(): @@ -40,7 +48,7 @@ def get_next_token(self): return token def _next_token(self): - """""" + """Return `Token` or `None`.""" for token_type, matcher in self.matchers: lexeme = matcher(self._source, self._pos) @@ -53,20 +61,23 @@ def _next_token(self): return token def _skip_whitespaces(self): - """""" + """Skip whitespaces and advance the position index + to the first non whitespace symbol.""" whitespaces = WHITESPACES.match(self._source, self._pos) if whitespaces: self._advance_pos_by_lexeme(whitespaces.group()) def _advance_pos_by_lexeme(self, lexeme): - """""" + """Advance the position index by lexeme lenght.""" value = len(lexeme) self._pos += value def _is_source_exhausted(self): - """""" + """Return `True` if the position pointer is out of the source string.""" + + assert(self._pos) >= 0 return self._pos >= self._length From 42b4399df2a978cb7e8be69574034b0eff43425f Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 2 May 2019 22:53:31 +0300 Subject: [PATCH 028/144] Move code blocks in the parser class --- final_task/pycalc/parser/parser.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 1c9b170f..66efb68a 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -19,11 +19,17 @@ def parse(self, source): return self.expression() - def next(self): + def expression(self, rbp=0): """""" - self.token = self.next_token - self.next_token = self.lexer.get_next_token() + self.next() + left = self.token.prefix() + print('in expression start:', self.token, self.next_token, left) + while rbp < self.next_token.lbp: + self.next() + left = self.token.infix(left) + + return left def advance(self, token_class=None): """""" @@ -33,17 +39,11 @@ def advance(self, token_class=None): self.next_token = self.lexer.get_next_token() - def expression(self, rbp=0): + def next(self): """""" - self.next() - left = self.token.prefix() - print('in expression start:', self.token, self.next_token, left) - while rbp < self.next_token.lbp: - self.next() - left = self.token.infix(left) - - return left + self.token = self.next_token + self.next_token = self.lexer.get_next_token() if __name__ == "__main__": From 4a9b556d98f9243bad3ef5cdc96b8f017869e367 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 3 May 2019 15:02:43 +0300 Subject: [PATCH 029/144] Refactor lexer with comsumer/peek --- final_task/pycalc/lexer/lexer.py | 76 ++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index 360dfb07..9bcb79b0 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -5,53 +5,64 @@ import re from collections import namedtuple -from pycalc.matcher.matcher import matchers - Token = namedtuple("Token", ("token_type", "lexeme")) WHITESPACES = re.compile(r"\s+") class Lexer: - """Represents""" + """Represents a lexer.""" - def __init__(self, matchers): + def __init__(self, matchers, source): self.matchers = matchers - self._source = "" - self._pos = 0 - self._length = 0 + self.source = source + self.pos = 0 + self.length = len(source) - def init(self, source): - """Init a lexer with a source string.""" + # a wrapper for caching the last peeked token + self._token_wrapper = [] - self._source = source - self._pos = 0 - self._length = len(source) + def is_source_exhausted(self): + """Return `True` if the position pointer is out of the source string.""" - def get_next_token(self): - """Return the next token. + assert(self.pos) >= 0 - Raise exceptions when the source is exhausted or there are no matches.""" + return self.pos >= self.length - self._skip_whitespaces() + def peek(self): + """""" - if self._is_source_exhausted(): - raise Exception("EOL") + if self._token_wrapper: + return self._token_wrapper[-1] token = self._next_token() + self._token_wrapper.append(token) - if not token: - raise Exception("No match") + return token - self._advance_pos_by_lexeme(token.lexeme) + def consume(self): + """""" + + token = self.peek() + self._token_wrapper.pop() + if token: + self._advance_pos_by_lexeme(token.lexeme) return token def _next_token(self): + """Try to match the next token.""" + + self._skip_whitespaces() + token = self._match() + + return token + + def _match(self): """Return `Token` or `None`.""" for token_type, matcher in self.matchers: - lexeme = matcher(self._source, self._pos) + lexeme = matcher(self.source, self.pos) if not lexeme: continue @@ -64,7 +75,7 @@ def _skip_whitespaces(self): """Skip whitespaces and advance the position index to the first non whitespace symbol.""" - whitespaces = WHITESPACES.match(self._source, self._pos) + whitespaces = WHITESPACES.match(self.source, self.pos) if whitespaces: self._advance_pos_by_lexeme(whitespaces.group()) @@ -72,20 +83,17 @@ def _advance_pos_by_lexeme(self, lexeme): """Advance the position index by lexeme lenght.""" value = len(lexeme) - self._pos += value - - def _is_source_exhausted(self): - """Return `True` if the position pointer is out of the source string.""" - - assert(self._pos) >= 0 - - return self._pos >= self._length + self.pos += value if __name__ == "__main__": + from pycalc.matcher.matcher import matchers source = " 1.3 >=sin(pi +e) " - l = Lexer(matchers) - l.init(source) + l = Lexer(matchers, source) while True: - print(l.get_next_token()) + token = l.consume() + if not token: + break + print(token, l.source, l.pos, l.length) + From c93d2edf014aeaec86d8d4630490dc55f593e6c3 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 3 May 2019 15:08:55 +0300 Subject: [PATCH 030/144] Fix tests for lexer --- final_task/tests/lexer/test_lexer.py | 82 +++++++++++++--------------- 1 file changed, 37 insertions(+), 45 deletions(-) diff --git a/final_task/tests/lexer/test_lexer.py b/final_task/tests/lexer/test_lexer.py index 233009b3..76b5d1ec 100644 --- a/final_task/tests/lexer/test_lexer.py +++ b/final_task/tests/lexer/test_lexer.py @@ -9,41 +9,55 @@ def test_class_init(self): '''Test class __init__() method''' matchers = ['1'] - lexer = Lexer(matchers) + source = 'some_source' + lexer = Lexer(matchers, source) - self.assertEqual(lexer._source, '') + self.assertEqual(lexer.source, source) self.assertEqual(lexer.matchers, matchers) - self.assertEqual(lexer._pos, 0) - self.assertEqual(lexer._length, 0) + self.assertEqual(lexer.pos, 0) + self.assertEqual(lexer.length, len(source)) - def test_init(self): + def test_is_source_exhausted(self): """""" - matchers = ['1'] - source = 'non_empty_string' + lexer = Lexer([], '') + test_cases = ( + (0, 0, True), + (3, 3, True), + (1, 2, False), + (2, 1, True), + ) - lexer = Lexer(matchers) - lexer._source = source * 2 # arbitrary number - lexer._pos += 13 # arbitrary number - lexer._length += 13 # arbitrary number + for pos, length, expected_result in test_cases: + with self.subTest(pos=pos, + length=length, + expected_result=expected_result + ): + lexer = Lexer([], '') + lexer.pos = pos + lexer.length = length + self.assertEqual(lexer.is_source_exhausted(), + expected_result) - lexer.init(source) - self.assertEqual(lexer._source, source) - self.assertEqual(lexer._pos, 0) - self.assertEqual(lexer._length, len(source)) + @unittest.skip('not implemented') + def test_peek(self): + pass @unittest.skip('not implemented') - def test_get_next_token(self): + def test_consume(self): pass @unittest.skip('not implemented') def test__next_token(self): pass + @unittest.skip('not implemented') + def test__match(self): + pass + def test__skip_whitespaces(self): """""" - lexer = Lexer([]) test_cases = ( (' bcd', 0, 1), (' cd', 0, 2), @@ -57,10 +71,10 @@ def test__skip_whitespaces(self): with self.subTest(source=source, pos=pos, expected_pos=expected_pos): - lexer.init(source) - lexer._pos = pos + lexer = Lexer([], source) + lexer.pos = pos lexer._skip_whitespaces() - self.assertEqual(lexer._pos, expected_pos) + self.assertEqual(lexer.pos, expected_pos) def test__advance_pos_by_lexeme(self): """""" @@ -69,33 +83,11 @@ def test__advance_pos_by_lexeme(self): pos = 10 expected_pos = pos + len(lexeme) - lexer = Lexer([]) - lexer._pos = pos + lexer = Lexer([], '') + lexer.pos = pos lexer._advance_pos_by_lexeme(lexeme) - self.assertEqual(lexer._pos, expected_pos) - - def test__check_source_end(self): - """""" - - lexer = Lexer([]) - test_cases = ( - (0, 0, True), - (3, 3, True), - (1, 2, False), - (2, 1, True), - ) - - for pos, length, expected_result in test_cases: - with self.subTest(pos=pos, - length=length, - expected_result=expected_result - ): - lexer = Lexer([]) - lexer._pos = pos - lexer._length = length - self.assertEqual(lexer._is_source_exhausted(), - expected_result) + self.assertEqual(lexer.pos, expected_pos) if __name__ == '__main__': From 4c79d2049658b68ade34f43432829391ed33ce10 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 3 May 2019 11:30:11 +0300 Subject: [PATCH 031/144] Refactor a parser with consume, peek methods --- final_task/pycalc/parser/parser.py | 75 ++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 24 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 66efb68a..5dd833a3 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -1,60 +1,87 @@ -"""""" -from pycalc.lexer.lexer import Lexer -from pycalc.matcher.matcher import matchers +""" +Parser. +""" class Parser: """""" - def __init__(self, lexer): + def __init__(self, registry, lexer): + self.registry = registry self.lexer = lexer - self.token = None - self.next_token = None def parse(self, source): """""" self.lexer.init(source) - self.next() + result = self.expression() - return self.expression() + assert self.lexer.source_exhausted(), \ + f'Unparsed part of source left, (pos: {self.lexer._pos}).' - def expression(self, rbp=0): + return result + + def expression(self, power=0): """""" - self.next() - left = self.token.prefix() - print('in expression start:', self.token, self.next_token, left) - while rbp < self.next_token.lbp: - self.next() - left = self.token.infix(left) + token = self.consume() + if not token: + raise Exception('i expect something but nothing finded') + + left = token.nud() + + while True: + token = self.peek() + if not token: + break + + if power >= token.power.led: + break + + self.consume() + left = token.led(left) return left - def advance(self, token_class=None): + def consume(self): """""" - if token_class and not self.next_token.is_instance(token_class): - raise SyntaxError(f"Expected: {token_class.__name__}") + return self.lexer.consume() + + def peek(self): + """""" - self.next_token = self.lexer.get_next_token() + return self.lexer.peek() - def next(self): + def advance(self, token_class=None): """""" - self.token = self.next_token - self.next_token = self.lexer.get_next_token() + token = self.peek() + + if not token or ( + token_class and + not token.is_instance(token_class) + ): + raise SyntaxError(f"Expected: {token_class.__name__}") + + self.consume() if __name__ == "__main__": + from pycalc.lexer.lexer import Lexer + from pycalc.matcher.matcher import matchers + l = Lexer(matchers) - p = Parser(l) - assert p.parse('- - - 2 ** fn ( 1 , ( 2 + 1 ) * 5 , 4 )') == -1048576 + p = Parser(None, l) + assert p.parse('2') == 2 + assert p.parse('- 2') == 2 assert p.parse('- - 2') == 2 + assert p.parse('1 - - 2') == 3 assert p.parse('4 ** 3 ** 2') == 262144 assert p.parse('1 + 2 * 3') == 7 assert p.parse('( 1 + 2 ) * 3') == 9 assert p.parse('1 + 2 == 3') is True assert p.parse('0 == 1') is False + assert p.parse('- - - 2 ** fn ( 1 , ( 2 + 1 ) * 5 , 4 )') == -1048576 # TODO: # assert p.parse('0 1') is False From 201e02766aca282ea3822bbc3ecfa6f5b3794a22 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 3 May 2019 22:23:26 +0300 Subject: [PATCH 032/144] Revert lexer init method --- final_task/pycalc/lexer/lexer.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index 9bcb79b0..5e18917d 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -13,15 +13,23 @@ class Lexer: """Represents a lexer.""" - def __init__(self, matchers, source): + def __init__(self, matchers): self.matchers = matchers - self.source = source + self.source = '' self.pos = 0 - self.length = len(source) + self.length = 0 # a wrapper for caching the last peeked token self._token_wrapper = [] + def init(self, source): + """Init a lexer with a source string.""" + + self.source = source + self.pos = 0 + self.length = len(source) + self._token_wrapper = [] + def is_source_exhausted(self): """Return `True` if the position pointer is out of the source string.""" @@ -96,4 +104,3 @@ def _advance_pos_by_lexeme(self, lexeme): if not token: break print(token, l.source, l.pos, l.length) - From b3fba7895555eb2654aad9b3f7bb7b9b9dc837b7 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sat, 4 May 2019 14:13:17 +0300 Subject: [PATCH 033/144] Refactor parser - introduce spec --- final_task/pycalc/parser/parser.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 5dd833a3..373e5d9e 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -6,8 +6,8 @@ class Parser: """""" - def __init__(self, registry, lexer): - self.registry = registry + def __init__(self, spec, lexer): + self.spec = spec self.lexer = lexer def parse(self, source): @@ -21,25 +21,25 @@ def parse(self, source): return result - def expression(self, power=0): + def expression(self, right_power=0): """""" token = self.consume() if not token: raise Exception('i expect something but nothing finded') - left = token.nud() + left = self._nud(token) while True: token = self.peek() if not token: break - if power >= token.power.led: + if right_power >= self._left_power(token): break self.consume() - left = token.led(left) + left = self._led(token, left) return left @@ -66,6 +66,21 @@ def advance(self, token_class=None): self.consume() + def _nud(self, token): + """""" + + return self.spec.nud.eval(self, token) + + def _led(self, token, left): + """""" + + return self.spec.led.eval(self, token, left) + + def _left_power(self, token): + """""" + + return self.spec.led.power(token) + if __name__ == "__main__": from pycalc.lexer.lexer import Lexer From 6532f01f8241222512a61a04676592ee5083663c Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 09:06:46 +0300 Subject: [PATCH 034/144] Clear token wapper in init method --- final_task/pycalc/lexer/lexer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index 5e18917d..d6b0fced 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -28,7 +28,7 @@ def init(self, source): self.source = source self.pos = 0 self.length = len(source) - self._token_wrapper = [] + self._token_wrapper.clear() def is_source_exhausted(self): """Return `True` if the position pointer is out of the source string.""" From 6b09a298cd6f42be956826637970bd4411642428 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 09:08:06 +0300 Subject: [PATCH 035/144] Add the format method to the lexer --- final_task/pycalc/lexer/lexer.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index d6b0fced..f9689266 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -58,6 +58,15 @@ def consume(self): return token + def format(self): + """""" + + pos = self.pos + begin = self.source[:pos] + end = self.source[pos:] + + return f'{begin}>{end}' + def _next_token(self): """Try to match the next token.""" From 97c136247a5a23ec122230de0e881092f8b00171 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 09:17:08 +0300 Subject: [PATCH 036/144] Modify matchers registering --- final_task/pycalc/matcher/matcher.py | 50 +++++++++++++++++++++------- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/final_task/pycalc/matcher/matcher.py b/final_task/pycalc/matcher/matcher.py index 2d4f33a1..9433d2bc 100644 --- a/final_task/pycalc/matcher/matcher.py +++ b/final_task/pycalc/matcher/matcher.py @@ -19,10 +19,10 @@ TokenType.NUMERIC: NUMBER_MATCHER } -operators = ['+', '>', '-', '>=', '==', '<=', '!'] -functions = ['sin', 'arcsin', 'abs', 'time'] -constants = ['pi', 'e', 'nan'] -punctuations = ['(', ')', ','] +# operators = ['+', '>', '-', '>=', '==', '<=', '!', '^'] +# functions = ['sin', 'arcsin', 'abs', 'time'] +# constants = ['pi', 'e', 'nan'] +# punctuations = ['(', ')', ','] class Matchers: @@ -53,14 +53,40 @@ def create_matcher_from_literals_list(self, literals): matchers.register_matcher(TokenType.NUMERIC, PREDEFINED_MATCHERS[TokenType.NUMERIC]) -matchers.register_matcher(TokenType.OPERATOR, - matchers.create_matcher_from_literals_list(operators)) -matchers.register_matcher(TokenType.CONSTANT, - matchers.create_matcher_from_literals_list(constants)) -matchers.register_matcher(TokenType.FUNCTION, - matchers.create_matcher_from_literals_list(functions)) -matchers.register_matcher(TokenType.PUNCTUATION, - matchers.create_matcher_from_literals_list(punctuations)) + +matchers.register_matcher(TokenType.SUB, + matchers.create_matcher_from_literals_list(['-'])) + +matchers.register_matcher(TokenType.MUL, + matchers.create_matcher_from_literals_list(['*'])) + +matchers.register_matcher(TokenType.POW, + matchers.create_matcher_from_literals_list(['^'])) + +matchers.register_matcher(TokenType.GE, + matchers.create_matcher_from_literals_list(['>='])) + +matchers.register_matcher(TokenType.GT, + matchers.create_matcher_from_literals_list(['>'])) + +matchers.register_matcher(TokenType.LEFT_PARENTHESIS, + matchers.create_matcher_from_literals_list(['('])) + +matchers.register_matcher(TokenType.RIGHT_PARENTHESIS, + matchers.create_matcher_from_literals_list([')'])) + +matchers.register_matcher(TokenType.SUM, + matchers.create_matcher_from_literals_list(['sum'])) + +matchers.register_matcher(TokenType.COMMA, + matchers.create_matcher_from_literals_list([','])) + +# matchers.register_matcher(TokenType.CONSTANT, +# matchers.create_matcher_from_literals_list(constants)) +# matchers.register_matcher(TokenType.FUNCTION, +# matchers.create_matcher_from_literals_list(functions)) +# matchers.register_matcher(TokenType.PUNCTUATION, +# matchers.create_matcher_from_literals_list(punctuations)) if __name__ == '__main__': From d6975d6380209169d90d62e8a615638c59fc0474 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 09:43:17 +0300 Subject: [PATCH 037/144] Add peek and check method to the parser --- final_task/pycalc/parser/parser.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 373e5d9e..9be47918 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -66,6 +66,15 @@ def advance(self, token_class=None): self.consume() + def peek_and_check(self, token_type): + """""" + + token = self.peek() + if not token or token.token_type != token_type: + return False + + return True + def _nud(self, token): """""" From 7623899d1b882942fb3b37dfeda6a7091008d2f4 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 09:44:16 +0300 Subject: [PATCH 038/144] Add some debug info to the parser --- final_task/pycalc/parser/parser.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 9be47918..77145e2e 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -13,12 +13,17 @@ def __init__(self, spec, lexer): def parse(self, source): """""" + print('=' * 30) + print(f'input : {source}') + self.lexer.init(source) result = self.expression() assert self.lexer.source_exhausted(), \ f'Unparsed part of source left, (pos: {self.lexer._pos}).' + print(f'output: {result}') + return result def expression(self, right_power=0): From e09f312c2ccdba55f4f976e66b95dd320aac3df8 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 09:44:45 +0300 Subject: [PATCH 039/144] Refactor advance method of the parser --- final_task/pycalc/parser/parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 77145e2e..1a77fcc0 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -58,16 +58,16 @@ def peek(self): return self.lexer.peek() - def advance(self, token_class=None): + def advance(self, token_type=None): """""" token = self.peek() if not token or ( - token_class and - not token.is_instance(token_class) + token_type and + not token.token_type == token_type ): - raise SyntaxError(f"Expected: {token_class.__name__}") + raise SyntaxError(f"Expected: {token_type}") self.consume() From af3bc3d86fbd4a58711b34946101cf1fbf6514ca Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 09:45:44 +0300 Subject: [PATCH 040/144] Update asserts in the lexer module --- final_task/pycalc/parser/parser.py | 35 ++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 1a77fcc0..30219929 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -99,18 +99,35 @@ def _left_power(self, token): if __name__ == "__main__": from pycalc.lexer.lexer import Lexer from pycalc.matcher.matcher import matchers + from pycalc.specification.specification import spec - l = Lexer(matchers) - p = Parser(None, l) + lexer = Lexer(matchers) + p = Parser(spec, lexer) assert p.parse('2') == 2 - assert p.parse('- 2') == 2 + assert p.parse(' 2') == 2 + assert p.parse('- 2') == - 2 assert p.parse('- - 2') == 2 + assert p.parse('1 - 2') == -1 + assert p.parse(' 1 - 2 ') == -1 assert p.parse('1 - - 2') == 3 - assert p.parse('4 ** 3 ** 2') == 262144 - assert p.parse('1 + 2 * 3') == 7 - assert p.parse('( 1 + 2 ) * 3') == 9 - assert p.parse('1 + 2 == 3') is True - assert p.parse('0 == 1') is False - assert p.parse('- - - 2 ** fn ( 1 , ( 2 + 1 ) * 5 , 4 )') == -1048576 + assert p.parse('1 - - - 2 ') == -1 + # assert p.parse('2 ** 3 ') == 8 + assert p.parse('1 - 2 * 3') == -5 + assert p.parse('3 ^ 2 * 2') == 18 + assert p.parse('3 * 2 ^ 2') == 12 + assert p.parse('4 ^ 3 ^ 2') == 262144 + assert p.parse('6-(-13)') == 19 + assert p.parse('( 7 - 2 ) * 3') == 15 + assert p.parse('(0)') == 0 + # assert p.parse(') 2 ') == 15 + assert p.parse('0 > 1') is False + assert p.parse('0 >= 1') is False + assert p.parse('2 > 1') is True + assert p.parse('1 >= 1') is True + assert p.parse('1 - 2 >= -1') is True + assert p.parse('sum(1 - 3, 2 - 5)') == -5 + # assert p.parse(', 1') is True + # assert p.parse('1 , 2') is True + assert p.parse('- - - 2 ^ sum ( 1 , ( 4 - 1 ) * 5 , 4 )') == -1048576 # TODO: # assert p.parse('0 1') is False From 144245a284a1cff096b0bcb53e40523fdb0990b3 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 09:46:10 +0300 Subject: [PATCH 041/144] Handle exceptions in the parser module --- final_task/pycalc/parser/parser.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 30219929..3dfa1f37 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -17,10 +17,13 @@ def parse(self, source): print(f'input : {source}') self.lexer.init(source) - result = self.expression() + try: + result = self.expression() + except Exception as e: + print(f'{e}: (pos: {self.lexer.pos}), {self.lexer.format()}') assert self.lexer.source_exhausted(), \ - f'Unparsed part of source left, (pos: {self.lexer._pos}).' + f'source not parsed completely, (pos: {self.lexer.pos}), {self.lexer.format()}' print(f'output: {result}') @@ -31,7 +34,7 @@ def expression(self, right_power=0): token = self.consume() if not token: - raise Exception('i expect something but nothing finded') + raise SyntaxError('i expect something but nothing finded') left = self._nud(token) From 1cd983983db016d091c9183008c1e8a1079813a6 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 10:58:26 +0300 Subject: [PATCH 042/144] Add the init file for the specification package --- final_task/pycalc/specification/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 final_task/pycalc/specification/__init__.py diff --git a/final_task/pycalc/specification/__init__.py b/final_task/pycalc/specification/__init__.py new file mode 100644 index 00000000..e69de29b From d631b9523a37d747c13ed80fdaeaf08dd4636536 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 11:09:09 +0300 Subject: [PATCH 043/144] Implement nud and led classes for the parser specification --- final_task/pycalc/specification/denotation.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 final_task/pycalc/specification/denotation.py diff --git a/final_task/pycalc/specification/denotation.py b/final_task/pycalc/specification/denotation.py new file mode 100644 index 00000000..796bdcfd --- /dev/null +++ b/final_task/pycalc/specification/denotation.py @@ -0,0 +1,100 @@ +""" +Denotation. +""" + + +class _Denotation: + """ + The base class for nud- and led-denotation classes. + """ + + def __init__(self): + self.registry = {} + + def register(self, token_type, parselet, **kwargs): + """ + Register token type with an appropriate parselet. + """ + self._check_for_dup(token_type) + self.registry[token_type] = parselet(**kwargs) + + def power(self, token): + """ + Return power for a given token. + """ + + power = self._get_parselet(token).power + + return power + + def _get_parselet(self, token): + """ + Find and return stored parselet for a given token. + """ + + token_type = token.token_type + + try: + parselet = self.registry[token_type] + except KeyError: + raise SyntaxError(f'not in specification: {token_type}') + + return parselet + + def _check_for_dup(self, token_type): + """ + Check if a givent token type is already registered. + """ + + if token_type in self.registry: + raise Exception( + f'Token of {token_type} type is already registered.') + + +class Nud(_Denotation): + """ + Null-Denotation. + + The specification of how an operator consumes to the right with no left-context. + """ + + def eval(self, parser, token): + """ + Evaluate and return result. + """ + + parselet = self._get_parselet(token) + result = parselet.nud(parser, token) + + return result + + +class Led(_Denotation): + """ + Left-Denotation. + + The specification of how an operator consumes to the right with a left-context. + """ + + def eval(self, parser, token, left): + """ + Evaluate and return result. + """ + + parselet = self._get_parselet(token) + result = parselet.led(parser, token, left) + + return result + + +if __name__ == "__main__": + from collections import namedtuple + from pycalc.token.constants import TokenType, Predefined + from pycalc.token.description import PREDEFINED_TOKEN_DESC + from pycalc.token.precedence import Precedence + from pycalc.token.tokens import Number + + Token = namedtuple("Token", ("token_type", "lexeme")) + + token = Token('op', '+') + d = _Denotation() From 556517f1c0fdb7cd0e4a00d2b590feb7328c5b2e Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 11:11:23 +0300 Subject: [PATCH 044/144] Add the parser specification class --- .../pycalc/specification/specification.py | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 final_task/pycalc/specification/specification.py diff --git a/final_task/pycalc/specification/specification.py b/final_task/pycalc/specification/specification.py new file mode 100644 index 00000000..c8e2008b --- /dev/null +++ b/final_task/pycalc/specification/specification.py @@ -0,0 +1,81 @@ +""" +Parser Specification +""" + +from .denotation import Nud, Led + + +class ParserSpecification: + """ + Hold nud and led. + """ + + def __init__(self): + self.nud = Nud() + self.led = Led() + +# + + +from pycalc.token.constants import TokenType +from pycalc.token.tokens import * +from pycalc.token.precedence import Precedence +import operator +from math import pow as math_pow + +spec = ParserSpecification() + +# NUMBERS +spec.nud.register(TokenType.NUMERIC, Number, + power=Precedence.DEFAULT) + +# OPERATIONS + +# negation +spec.nud.register(TokenType.SUB, UnaryPrefix, + power=Precedence.NEGATIVE, + func=lambda x: -x) + +# subtraction +spec.led.register(TokenType.SUB, BinaryInfixLeft, + power=Precedence.SUBTRACTION, + func=operator.sub) + +# exponentiation +spec.led.register(TokenType.POW, BinaryInfixRight, + power=Precedence.EXPONENTIATION, + func=math_pow) + +# multiplication +spec.led.register(TokenType.MUL, BinaryInfixLeft, + power=Precedence.MULTIPLICATION, + func=operator.mul) + +spec.led.register(TokenType.GE, BinaryInfixLeft, + power=Precedence.COMPARISONS, + func=operator.ge) + +spec.led.register(TokenType.GT, BinaryInfixLeft, + power=Precedence.COMPARISONS, + func=operator.gt) + +# FUNCTIONS + +# sum +spec.nud.register(TokenType.SUM, Function, + power=Precedence.CALL, + func=sum, + start=TokenType.LEFT_PARENTHESIS, + stop=TokenType.RIGHT_PARENTHESIS, + sep=TokenType.COMMA) + +# PUNCTUATION +spec.nud.register(TokenType.LEFT_PARENTHESIS, GroupedExpressionStart, + power=Precedence.BINDING, + right_pair=TokenType.RIGHT_PARENTHESIS) + +spec.led.register(TokenType.RIGHT_PARENTHESIS, GroupedExpressionEnd, + power=Precedence.DEFAULT) + +spec.led.register(TokenType.COMMA, Comma, + power=Precedence.DEFAULT) From 594070d3929470266c1f44feb1adfe14d006a19a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 11:16:18 +0300 Subject: [PATCH 045/144] Fix name of calling lexer method --- final_task/pycalc/parser/parser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 3dfa1f37..bf35feed 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -22,7 +22,7 @@ def parse(self, source): except Exception as e: print(f'{e}: (pos: {self.lexer.pos}), {self.lexer.format()}') - assert self.lexer.source_exhausted(), \ + assert self.lexer.is_source_exhausted(), \ f'source not parsed completely, (pos: {self.lexer.pos}), {self.lexer.format()}' print(f'output: {result}') From 0132c6ee7d5fd40110aa8c5f23f16d19d7063d7b Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sun, 5 May 2019 11:53:20 +0300 Subject: [PATCH 046/144] Add the init file for lexer package --- final_task/pycalc/lexer/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 final_task/pycalc/lexer/__init__.py diff --git a/final_task/pycalc/lexer/__init__.py b/final_task/pycalc/lexer/__init__.py new file mode 100644 index 00000000..e69de29b From 04da9398ba305b32cb3d329842dec4d0b6b26fbc Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 12:51:35 +0300 Subject: [PATCH 047/144] Refactor module members importer --- final_task/pycalc/importer.py | 100 ++++++++++------------------------ 1 file changed, 28 insertions(+), 72 deletions(-) diff --git a/final_task/pycalc/importer.py b/final_task/pycalc/importer.py index 26108f35..dbc578be 100644 --- a/final_task/pycalc/importer.py +++ b/final_task/pycalc/importer.py @@ -1,98 +1,54 @@ -"""""" -# TODO: exception when import fails +""" +""" + +# TODO: handle exception when module importing fails from collections import OrderedDict -from functools import partial from importlib import import_module from inspect import getmembers -from types import BuiltinFunctionType, FunctionType, LambdaType - +from itertools import chain -NUMERIC_TYPES = (int, float, complex) -FUNCTION_TYPES = (BuiltinFunctionType, FunctionType, LambdaType, partial) UNDERSCORE = '_' -DEFAULT_MODULE_NAMES = ('math',) - -def dedupe_to_list(iterable) -> list: +def dedupe(iterables): """""" - return list(OrderedDict.fromkeys(iterable)) - - -def is_numeric(obj) -> bool: - """Return `True` if a object is one of numeric types.""" + return (key for key in OrderedDict.fromkeys(chain(iterables))) - return isinstance(obj, (NUMERIC_TYPES)) - -def is_function(obj) -> bool: - """Return `True` if a object is a function.""" - - return isinstance(obj, (FUNCTION_TYPES)) - - -def merge_module_names(module_names: tuple) -> list: +def import_modules(*iterables): """""" - m_names = list(DEFAULT_MODULE_NAMES) - if module_names: - m_names.extend(module_names) + modules = [] - return m_names + for iterable in dedupe(iterables): + for module_name in iterable: + try: + module = import_module(module_name) + modules.append(module) + except ModuleNotFoundError: + raise ModuleNotFoundError -def get_module_members_names_by_type(module, type_checker) -> list: - """""" + return modules - return [ - member[0] for member in getmembers(module, type_checker) - if not member[0].startswith(UNDERSCORE) - ] - -MEMBER_TYPES = { - 'functions': is_function, - 'constants': is_numeric -} - - -def get_module_members_names(module): +def module_members_by_type(module, type_checker, skip_underscored=True): """""" - return {type_: get_module_members_names_by_type(module, type_checker) - for type_, type_checker in MEMBER_TYPES.items()} - - -def get(module_names: tuple = None) -> dict: - """""" - - if not module_names: - module_names = tuple() - - m_names = merge_module_names(module_names) - m_names = dedupe_to_list(m_names) - - result = {} + for name, member in getmembers(module, type_checker): + if skip_underscored and name.startswith(UNDERSCORE): + continue + yield name, member - for module_name in m_names: - try: - module = import_module(module_name) - except ModuleNotFoundError: - raise ModuleNotFoundError - members = get_module_members_names(module) - result[module_name] = {} - result[module_name]['module'] = module - result[module_name]['members'] = members +def collect_members_by_type(modules, type_checker, skip_underscored=True, predefined=None): - return result + accumulator = dict(predefined) if predefined else {} + for module in modules: + for name, member in module_members_by_type(module, type_checker, skip_underscored): + accumulator[name] = member -if __name__ == '__main__': - MODULE_NAMES = ('calendar', 'pprint') - from pprint import pprint - for key, value in get(MODULE_NAMES).items(): - print(key) - pprint(value) + return accumulator From 1d70982f6afc1c55f859834809109683baa31b4d Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 13:01:11 +0300 Subject: [PATCH 048/144] Move importer module into a separate package --- final_task/pycalc/importer/__init__.py | 6 ++++++ final_task/pycalc/{ => importer}/importer.py | 0 2 files changed, 6 insertions(+) create mode 100644 final_task/pycalc/importer/__init__.py rename final_task/pycalc/{ => importer}/importer.py (100%) diff --git a/final_task/pycalc/importer/__init__.py b/final_task/pycalc/importer/__init__.py new file mode 100644 index 00000000..8b12cd8a --- /dev/null +++ b/final_task/pycalc/importer/__init__.py @@ -0,0 +1,6 @@ +""" +Importer package provides functions for +modules importing and collecting module members. +""" + +from .importer import collect_members_by_type, import_modules diff --git a/final_task/pycalc/importer.py b/final_task/pycalc/importer/importer.py similarity index 100% rename from final_task/pycalc/importer.py rename to final_task/pycalc/importer/importer.py From 19105f7a46aa1e1cb08ce4cba658cc728ba2f149 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 13:02:48 +0300 Subject: [PATCH 049/144] Fix package docs --- final_task/pycalc/importer/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/importer/__init__.py b/final_task/pycalc/importer/__init__.py index 8b12cd8a..997c7d7d 100644 --- a/final_task/pycalc/importer/__init__.py +++ b/final_task/pycalc/importer/__init__.py @@ -1,6 +1,6 @@ """ -Importer package provides functions for -modules importing and collecting module members. +Importer package provides functions for +modules importing and module members collecting. """ from .importer import collect_members_by_type, import_modules From 1976bcab11775a6f8f3d024753faab1cd6f40b42 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 15:58:24 +0300 Subject: [PATCH 050/144] Add ERROR to parser error message and propagate exception --- final_task/pycalc/parser/parser.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index bf35feed..f899ba12 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -20,7 +20,9 @@ def parse(self, source): try: result = self.expression() except Exception as e: - print(f'{e}: (pos: {self.lexer.pos}), {self.lexer.format()}') + print( + f'ERROR: {e}: (pos: {self.lexer.pos}), {self.lexer.format()}') + raise e assert self.lexer.is_source_exhausted(), \ f'source not parsed completely, (pos: {self.lexer.pos}), {self.lexer.format()}' From 0f8ce5b86ee0b4ba71768c53cd839d090cbe2263 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 15:59:01 +0300 Subject: [PATCH 051/144] Update parser asserts --- final_task/pycalc/parser/parser.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index f899ba12..32d60579 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -105,9 +105,14 @@ def _left_power(self, token): from pycalc.lexer.lexer import Lexer from pycalc.matcher.matcher import matchers from pycalc.specification.specification import spec + import math lexer = Lexer(matchers) p = Parser(spec, lexer) + # assert p.parse('1 / 0') == 0 + assert p.parse('sin(2)') == math.sin(2) + assert p.parse('sin(2-3)') == math.sin(2 - 3) + # assert p.parse('sin(1,2)') == math.sin(0.5) assert p.parse('2') == 2 assert p.parse(' 2') == 2 assert p.parse('- 2') == - 2 @@ -130,9 +135,9 @@ def _left_power(self, token): assert p.parse('2 > 1') is True assert p.parse('1 >= 1') is True assert p.parse('1 - 2 >= -1') is True - assert p.parse('sum(1 - 3, 2 - 5)') == -5 + assert p.parse('log(1025 - 1, 7 - 5)') == 10 # assert p.parse(', 1') is True # assert p.parse('1 , 2') is True - assert p.parse('- - - 2 ^ sum ( 1 , ( 4 - 1 ) * 5 , 4 )') == -1048576 + # assert p.parse('- - - 2 ^ log ( 1 , ( 4 - 1 ) * 5 , 4 )') == -1048576 # TODO: # assert p.parse('0 1') is False From 40702b62b80323afbc175c1659f6516f8d18ecfc Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 18:25:06 +0300 Subject: [PATCH 052/144] Refactor import package --- final_task/pycalc/importer/importer.py | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/final_task/pycalc/importer/importer.py b/final_task/pycalc/importer/importer.py index dbc578be..da9d7f9d 100644 --- a/final_task/pycalc/importer/importer.py +++ b/final_task/pycalc/importer/importer.py @@ -11,10 +11,12 @@ UNDERSCORE = '_' -def dedupe(iterables): - """""" +def iter_uniq(iterables): + """ + Returns a generator that iterates over unique elements of iterables. + """ - return (key for key in OrderedDict.fromkeys(chain(iterables))) + return (key for key in OrderedDict.fromkeys(chain(*iterables))) def import_modules(*iterables): @@ -22,14 +24,13 @@ def import_modules(*iterables): modules = [] - for iterable in dedupe(iterables): - for module_name in iterable: + for module_name in iter_uniq(iterables): - try: - module = import_module(module_name) - modules.append(module) - except ModuleNotFoundError: - raise ModuleNotFoundError + try: + module = import_module(module_name) + modules.append(module) + except ModuleNotFoundError: + raise ModuleNotFoundError return modules @@ -44,6 +45,7 @@ def module_members_by_type(module, type_checker, skip_underscored=True): def collect_members_by_type(modules, type_checker, skip_underscored=True, predefined=None): + """""" accumulator = dict(predefined) if predefined else {} From 4f54fe8300aee543965a52da361390f47f8e8f64 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 18:48:07 +0300 Subject: [PATCH 053/144] Remove commented code --- final_task/pycalc/lexer/lexer.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index f9689266..538ef2d0 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -101,15 +101,3 @@ def _advance_pos_by_lexeme(self, lexeme): value = len(lexeme) self.pos += value - - -if __name__ == "__main__": - from pycalc.matcher.matcher import matchers - - source = " 1.3 >=sin(pi +e) " - l = Lexer(matchers, source) - while True: - token = l.consume() - if not token: - break - print(token, l.source, l.pos, l.length) From 5122e2d28be1f21ab5d33f037394b546e4840594 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:07:32 +0300 Subject: [PATCH 054/144] Refactor matchers container class --- final_task/pycalc/matcher/matcher.py | 82 +++------------------------- 1 file changed, 8 insertions(+), 74 deletions(-) diff --git a/final_task/pycalc/matcher/matcher.py b/final_task/pycalc/matcher/matcher.py index 9433d2bc..21b8b0b0 100644 --- a/final_task/pycalc/matcher/matcher.py +++ b/final_task/pycalc/matcher/matcher.py @@ -1,31 +1,17 @@ +""" +""" from collections import namedtuple -from pycalc.token.constants import TokenType - from .helpers import construct_regex, regex_matcher -from .number import NUMBER_MATCHER - -# operation: + - etc. predefined -# constants: pi, e etc. load from module -# function: sin, abs etc. load from module -# numbers: 0, 10, 15., .03, 2.14 etc. dynamicaly matched (no literal) -# other: ( ) , predefined Matcher = namedtuple("Matcher", ("token_type", "matcher")) -PREDEFINED_MATCHERS = { - TokenType.NUMERIC: NUMBER_MATCHER -} - -# operators = ['+', '>', '-', '>=', '==', '<=', '!', '^'] -# functions = ['sin', 'arcsin', 'abs', 'time'] -# constants = ['pi', 'e', 'nan'] -# punctuations = ['(', ')', ','] - class Matchers: + """""" + def __init__(self): self.matchers = [] @@ -38,66 +24,14 @@ def register_matcher(self, token_type, matcher): self.matchers.append(Matcher(token_type, matcher)) - def create_matcher_from_regex(self, regex): + @staticmethod + def create_matcher_from_regex(regex): """""" return regex_matcher(regex) - def create_matcher_from_literals_list(self, literals): + @staticmethod + def create_matcher_from_literals_list(literals): """""" return regex_matcher(construct_regex(literals)) - - -matchers = Matchers() - -matchers.register_matcher(TokenType.NUMERIC, - PREDEFINED_MATCHERS[TokenType.NUMERIC]) - -matchers.register_matcher(TokenType.SUB, - matchers.create_matcher_from_literals_list(['-'])) - -matchers.register_matcher(TokenType.MUL, - matchers.create_matcher_from_literals_list(['*'])) - -matchers.register_matcher(TokenType.POW, - matchers.create_matcher_from_literals_list(['^'])) - -matchers.register_matcher(TokenType.GE, - matchers.create_matcher_from_literals_list(['>='])) - -matchers.register_matcher(TokenType.GT, - matchers.create_matcher_from_literals_list(['>'])) - -matchers.register_matcher(TokenType.LEFT_PARENTHESIS, - matchers.create_matcher_from_literals_list(['('])) - -matchers.register_matcher(TokenType.RIGHT_PARENTHESIS, - matchers.create_matcher_from_literals_list([')'])) - -matchers.register_matcher(TokenType.SUM, - matchers.create_matcher_from_literals_list(['sum'])) - -matchers.register_matcher(TokenType.COMMA, - matchers.create_matcher_from_literals_list([','])) - -# matchers.register_matcher(TokenType.CONSTANT, -# matchers.create_matcher_from_literals_list(constants)) -# matchers.register_matcher(TokenType.FUNCTION, -# matchers.create_matcher_from_literals_list(functions)) -# matchers.register_matcher(TokenType.PUNCTUATION, -# matchers.create_matcher_from_literals_list(punctuations)) - -if __name__ == '__main__': - - source = '1.3>=sin(pi+e)' - pos = 0 - while True: - for token_type, matcher in matchers: - result = matcher(source, pos) - if result: - print(token_type, result) - pos += len(result) - break - if pos >= len(source): - break From 09a8e07d8efa796ef25282de1d6fefb95d620dfe Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 13:05:37 +0300 Subject: [PATCH 055/144] Add importer to calculator --- final_task/pycalc/calculator/importer.py | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 final_task/pycalc/calculator/importer.py diff --git a/final_task/pycalc/calculator/importer.py b/final_task/pycalc/calculator/importer.py new file mode 100644 index 00000000..de570c2d --- /dev/null +++ b/final_task/pycalc/calculator/importer.py @@ -0,0 +1,49 @@ +""" +Collect maps of module member’s names +to module member’s objects of specified types. +""" + +# TODO: handle exception when module importing fails + + +from functools import partial +from types import BuiltinFunctionType, FunctionType, LambdaType + +from pycalc.importer import collect_members_by_type, import_modules + + +NUMERIC_TYPES = (int, float, complex) +FUNCTION_TYPES = (BuiltinFunctionType, FunctionType, LambdaType, partial) + +DEFAULT_MODULE_NAMES = ('math',) +DEFAULT_FUNCTIONS = {'abs': abs, 'round': round} + + +def is_numeric(obj) -> bool: + """Return `True` if a object is one of numeric types.""" + + return isinstance(obj, (NUMERIC_TYPES)) + + +def is_function(obj) -> bool: + """Return `True` if a object is a function.""" + + return isinstance(obj, (FUNCTION_TYPES)) + + +def build_modules_registry(modules_names): + """""" + + if not modules_names: + modules_names = tuple() + + modules = import_modules(DEFAULT_MODULE_NAMES, modules_names) + + functions = collect_members_by_type(modules, + is_function, + predefined=DEFAULT_FUNCTIONS) + + constants = collect_members_by_type(modules, + is_numeric) + + return {"functions": functions, 'constants': constants} From 1564b6e36a566141c26b16e13d85187aafcae5ee Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:18:24 +0300 Subject: [PATCH 056/144] Add matchers to calculator --- final_task/pycalc/calculator/matchers.py | 63 ++++++++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 final_task/pycalc/calculator/matchers.py diff --git a/final_task/pycalc/calculator/matchers.py b/final_task/pycalc/calculator/matchers.py new file mode 100644 index 00000000..e46c0e67 --- /dev/null +++ b/final_task/pycalc/calculator/matchers.py @@ -0,0 +1,63 @@ +""" +Build matchers. +""" + +from pycalc.matcher.matcher import Matchers +from pycalc.matcher.number import NUMBER_MATCHER +from pycalc.token.constants import TokenType + + +PREDEFINED_MATCHERS = { + TokenType.NUMERIC: NUMBER_MATCHER +} + + +def build_matchers(registry): + """""" + + matchers = Matchers() + + matchers.register_matcher(TokenType.NUMERIC, + PREDEFINED_MATCHERS[TokenType.NUMERIC]) + + matchers.register_matcher(TokenType.FUNCTION, + matchers.create_matcher_from_literals_list( + registry['functions'].keys() + )) + + matchers.register_matcher(TokenType.ADD, + matchers.create_matcher_from_literals_list(['+'])) + + matchers.register_matcher(TokenType.SUB, + matchers.create_matcher_from_literals_list(['-'])) + + matchers.register_matcher(TokenType.MUL, + matchers.create_matcher_from_literals_list(['*'])) + + matchers.register_matcher(TokenType.TRUEDIV, + matchers.create_matcher_from_literals_list(['/'])) + + matchers.register_matcher(TokenType.MOD, + matchers.create_matcher_from_literals_list(['%'])) + + matchers.register_matcher(TokenType.POW, + matchers.create_matcher_from_literals_list(['^'])) + + matchers.register_matcher(TokenType.EQ, + matchers.create_matcher_from_literals_list(['=='])) + matchers.register_matcher(TokenType.GE, + matchers.create_matcher_from_literals_list(['>='])) + + matchers.register_matcher(TokenType.GT, + matchers.create_matcher_from_literals_list(['>'])) + + matchers.register_matcher(TokenType.LEFT_PARENTHESIS, + matchers.create_matcher_from_literals_list(['('])) + + matchers.register_matcher(TokenType.RIGHT_PARENTHESIS, + matchers.create_matcher_from_literals_list([')'])) + + matchers.register_matcher(TokenType.COMMA, + matchers.create_matcher_from_literals_list([','])) + + return matchers From 981196d8bf3b2eaf680d4af57aab633419daf228 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:19:04 +0300 Subject: [PATCH 057/144] Add number matcher to calculator --- final_task/pycalc/calculator/number.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 final_task/pycalc/calculator/number.py diff --git a/final_task/pycalc/calculator/number.py b/final_task/pycalc/calculator/number.py new file mode 100644 index 00000000..f5752725 --- /dev/null +++ b/final_task/pycalc/calculator/number.py @@ -0,0 +1,20 @@ +"""""" + +import re + +from .helpers import regex_matcher + +NUMBER = r''' +( # integers or numbers with a fractional part: 13, 154., 3.44, ... +\d+ # an integer part: 10, 2, 432, ... +(\.\d*)* # a fractional part: .2, .43, .1245, ... or dot: . +) +| +( # numbers that begin with a dot: .12, .59, ... +\.\d+ # a fractional part: .2, .43, .1245, ... +) +''' + +NUMBER_REGEX = re.compile(NUMBER, re.VERBOSE) + +NUMBER_MATCHER = regex_matcher(NUMBER_REGEX) From e3c9d2580cb46f30b2c21e27d41f6f8b3b13ea6a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:19:26 +0300 Subject: [PATCH 058/144] Add parser to calculator --- final_task/pycalc/calculator/parser.py | 1 + 1 file changed, 1 insertion(+) create mode 100644 final_task/pycalc/calculator/parser.py diff --git a/final_task/pycalc/calculator/parser.py b/final_task/pycalc/calculator/parser.py new file mode 100644 index 00000000..0bf07aac --- /dev/null +++ b/final_task/pycalc/calculator/parser.py @@ -0,0 +1 @@ +from pycalc.parser.parser import Parser \ No newline at end of file From a81744a092d1468ae959be19afd5b2ef150f8ccb Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:19:49 +0300 Subject: [PATCH 059/144] Add specification to calculator --- final_task/pycalc/calculator/specification.py | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 final_task/pycalc/calculator/specification.py diff --git a/final_task/pycalc/calculator/specification.py b/final_task/pycalc/calculator/specification.py new file mode 100644 index 00000000..025db875 --- /dev/null +++ b/final_task/pycalc/calculator/specification.py @@ -0,0 +1,100 @@ +""" + +""" + +from math import pow as math_pow +import math # TODO: remove + +from pycalc.specification.specification import ParserSpecification + +from pycalc.token.constants import TokenType +from pycalc.token.precedence import Precedence +from pycalc.token.tokens import * + +# FIX: operator build and Operator from token same name +import operator + + +def build_specification(registry): + + spec = ParserSpecification() + + # NUMBERS + spec.nud.register(TokenType.NUMERIC, Number, + power=Precedence.DEFAULT) + + # FUNCTIONS + spec.nud.register(TokenType.FUNCTION, Function, + power=Precedence.CALL, + func_registry=registry['functions'], + start=TokenType.LEFT_PARENTHESIS, + stop=TokenType.RIGHT_PARENTHESIS, + sep=TokenType.COMMA) + + # OPERATIONS + + # positive + spec.nud.register(TokenType.ADD, UnaryPrefix, + power=Precedence.POSITIVE, + func=lambda x: +x) + # negation + spec.nud.register(TokenType.SUB, UnaryPrefix, + power=Precedence.NEGATIVE, + func=lambda x: -x) + + # addition + spec.led.register(TokenType.ADD, BinaryInfixLeft, + power=Precedence.ADDITION, + func=operator.add) + # subtraction + spec.led.register(TokenType.SUB, BinaryInfixLeft, + power=Precedence.SUBTRACTION, + func=operator.sub) + + # exponentiation + spec.led.register(TokenType.POW, BinaryInfixRight, + power=Precedence.EXPONENTIATION, + func=math_pow) + + # multiplication + spec.led.register(TokenType.MUL, BinaryInfixLeft, + power=Precedence.MULTIPLICATION, + func=operator.mul) + + # division + spec.led.register(TokenType.TRUEDIV, BinaryInfixLeft, + power=Precedence.DIVISION, + func=operator.truediv) + + # remainder + spec.led.register(TokenType.MOD, BinaryInfixLeft, + power=Precedence.REMAINDER, + func=operator.mod) + + # equal or greater + spec.led.register(TokenType.EQ, BinaryInfixLeft, + power=Precedence.COMPARISONS, + func=operator.eq) + + # greater + spec.led.register(TokenType.GT, BinaryInfixLeft, + power=Precedence.COMPARISONS, + func=operator.gt) + + # equal or greater + spec.led.register(TokenType.GE, BinaryInfixLeft, + power=Precedence.COMPARISONS, + func=operator.ge) + + # PUNCTUATION + spec.nud.register(TokenType.LEFT_PARENTHESIS, GroupedExpressionStart, + power=Precedence.BINDING, + right_pair=TokenType.RIGHT_PARENTHESIS) + + spec.led.register(TokenType.RIGHT_PARENTHESIS, GroupedExpressionEnd, + power=Precedence.DEFAULT) + + spec.led.register(TokenType.COMMA, Comma, + power=Precedence.DEFAULT) + + return spec From fa2c4ed29d232340fb94809b9fcddc0daacf475a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:20:30 +0300 Subject: [PATCH 060/144] Add calculator to calculator --- final_task/pycalc/calculator/calculator.py | 69 ++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 final_task/pycalc/calculator/calculator.py diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py new file mode 100644 index 00000000..b7b8fb20 --- /dev/null +++ b/final_task/pycalc/calculator/calculator.py @@ -0,0 +1,69 @@ +""" +""" + +from pycalc.lexer.lexer import Lexer +from pycalc.parser.parser import Parser + +from .importer import build_modules_registry +from .matchers import build_matchers +from .specification import build_specification + + +def calculator(modules_names=None): + """""" + + # import constants and functions from default and requested modules + modules_registry = build_modules_registry(modules_names) + + # build lexemes matchers + matchers = build_matchers(modules_registry) + + # create a lexer + lexer = Lexer(matchers) + + # build a specification for a parser + spec = build_specification(modules_registry) + + # create a parser + parser = Parser(spec, lexer) + + return parser + + +if __name__ == "__main__": + import math + + p = calculator() + + # assert p.parse('1 / 0') == 0 + assert p.parse('sin(2)') == math.sin(2) + assert p.parse('sin(2-3)') == math.sin(2 - 3) + # assert p.parse('sin(1,2)') == math.sin(0.5) + assert p.parse('2') == 2 + assert p.parse(' 2') == 2 + assert p.parse('- 2') == - 2 + assert p.parse('- - 2') == 2 + assert p.parse('1 - 2') == -1 + assert p.parse(' 1 - 2 ') == -1 + assert p.parse('1 - - 2') == 3 + assert p.parse('1 - - - 2 ') == -1 + # assert p.parse('2 ** 3 ') == 8 + assert p.parse('1 - 2 * 3') == -5 + assert p.parse('3 ^ 2 * 2') == 18 + assert p.parse('3 * 2 ^ 2') == 12 + assert p.parse('4 ^ 3 ^ 2') == 262144 + assert p.parse('6-(-13)') == 19 + assert p.parse('( 7 - 2 ) * 3') == 15 + assert p.parse('(0)') == 0 + # assert p.parse(') 2 ') == 15 + assert p.parse('0 > 1') is False + assert p.parse('0 >= 1') is False + assert p.parse('2 > 1') is True + assert p.parse('1 >= 1') is True + assert p.parse('1 - 2 >= -1') is True + assert p.parse('log(1025 - 1, 7 - 5)') == 10 + # assert p.parse(', 1') is True + # assert p.parse('1 , 2') is True + # assert p.parse('- - - 2 ^ log ( 1 , ( 4 - 1 ) * 5 , 4 )') == -1048576 + # TODO: + # assert p.parse('0 1') is False From 5438c5c9954cc21bbf38103add6e2b3f5a9a961e Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:20:47 +0300 Subject: [PATCH 061/144] Add init file to calculator --- final_task/pycalc/calculator/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 final_task/pycalc/calculator/__init__.py diff --git a/final_task/pycalc/calculator/__init__.py b/final_task/pycalc/calculator/__init__.py new file mode 100644 index 00000000..19d4497d --- /dev/null +++ b/final_task/pycalc/calculator/__init__.py @@ -0,0 +1,5 @@ +""" +Calculator. +""" + +from .calculator import calculator From a96d7ff66ccd1c04636d08f4377375d29f9247c3 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:33:57 +0300 Subject: [PATCH 062/144] Remove unused code --- .../pycalc/specification/specification.py | 70 +------------------ 1 file changed, 2 insertions(+), 68 deletions(-) diff --git a/final_task/pycalc/specification/specification.py b/final_task/pycalc/specification/specification.py index c8e2008b..b9d430f5 100644 --- a/final_task/pycalc/specification/specification.py +++ b/final_task/pycalc/specification/specification.py @@ -1,5 +1,5 @@ """ -Parser Specification +Parser specification. """ from .denotation import Nud, Led @@ -7,75 +7,9 @@ class ParserSpecification: """ - Hold nud and led. + Holds nud and led. """ def __init__(self): self.nud = Nud() self.led = Led() - -# - - -from pycalc.token.constants import TokenType -from pycalc.token.tokens import * -from pycalc.token.precedence import Precedence -import operator -from math import pow as math_pow - -spec = ParserSpecification() - -# NUMBERS -spec.nud.register(TokenType.NUMERIC, Number, - power=Precedence.DEFAULT) - -# OPERATIONS - -# negation -spec.nud.register(TokenType.SUB, UnaryPrefix, - power=Precedence.NEGATIVE, - func=lambda x: -x) - -# subtraction -spec.led.register(TokenType.SUB, BinaryInfixLeft, - power=Precedence.SUBTRACTION, - func=operator.sub) - -# exponentiation -spec.led.register(TokenType.POW, BinaryInfixRight, - power=Precedence.EXPONENTIATION, - func=math_pow) - -# multiplication -spec.led.register(TokenType.MUL, BinaryInfixLeft, - power=Precedence.MULTIPLICATION, - func=operator.mul) - -spec.led.register(TokenType.GE, BinaryInfixLeft, - power=Precedence.COMPARISONS, - func=operator.ge) - -spec.led.register(TokenType.GT, BinaryInfixLeft, - power=Precedence.COMPARISONS, - func=operator.gt) - -# FUNCTIONS - -# sum -spec.nud.register(TokenType.SUM, Function, - power=Precedence.CALL, - func=sum, - start=TokenType.LEFT_PARENTHESIS, - stop=TokenType.RIGHT_PARENTHESIS, - sep=TokenType.COMMA) - -# PUNCTUATION -spec.nud.register(TokenType.LEFT_PARENTHESIS, GroupedExpressionStart, - power=Precedence.BINDING, - right_pair=TokenType.RIGHT_PARENTHESIS) - -spec.led.register(TokenType.RIGHT_PARENTHESIS, GroupedExpressionEnd, - power=Precedence.DEFAULT) - -spec.led.register(TokenType.COMMA, Comma, - power=Precedence.DEFAULT) From 9a723525570ba39c739828b5e0e0413c1ab49481 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:34:20 +0300 Subject: [PATCH 063/144] Remove unused code --- final_task/pycalc/specification/denotation.py | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/final_task/pycalc/specification/denotation.py b/final_task/pycalc/specification/denotation.py index 796bdcfd..94b317e2 100644 --- a/final_task/pycalc/specification/denotation.py +++ b/final_task/pycalc/specification/denotation.py @@ -85,16 +85,3 @@ def eval(self, parser, token, left): result = parselet.led(parser, token, left) return result - - -if __name__ == "__main__": - from collections import namedtuple - from pycalc.token.constants import TokenType, Predefined - from pycalc.token.description import PREDEFINED_TOKEN_DESC - from pycalc.token.precedence import Precedence - from pycalc.token.tokens import Number - - Token = namedtuple("Token", ("token_type", "lexeme")) - - token = Token('op', '+') - d = _Denotation() From 8c8580b69d3afade19518d8827ad196a8245c827 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 18:49:21 +0300 Subject: [PATCH 064/144] Add docs --- final_task/pycalc/lexer/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/final_task/pycalc/lexer/__init__.py b/final_task/pycalc/lexer/__init__.py index e69de29b..8f762a95 100644 --- a/final_task/pycalc/lexer/__init__.py +++ b/final_task/pycalc/lexer/__init__.py @@ -0,0 +1,5 @@ +""" +Lexer performs lexical analysis of a source. +""" + +from .lexer import Lexer From 012ee7f10dd6be3bffeb663bcf8a43984cad6d8f Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:46:46 +0300 Subject: [PATCH 065/144] Fix imports --- final_task/pycalc/calculator/calculator.py | 6 ++++-- final_task/pycalc/calculator/specification.py | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index b7b8fb20..3eba4e16 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -1,8 +1,9 @@ """ +Initializes a calculator and returns a parser object. """ -from pycalc.lexer.lexer import Lexer -from pycalc.parser.parser import Parser +from pycalc.lexer import Lexer +from pycalc.parser import Parser from .importer import build_modules_registry from .matchers import build_matchers @@ -30,6 +31,7 @@ def calculator(modules_names=None): return parser +# TODO: remove if __name__ == "__main__": import math diff --git a/final_task/pycalc/calculator/specification.py b/final_task/pycalc/calculator/specification.py index 025db875..87cb3545 100644 --- a/final_task/pycalc/calculator/specification.py +++ b/final_task/pycalc/calculator/specification.py @@ -5,7 +5,7 @@ from math import pow as math_pow import math # TODO: remove -from pycalc.specification.specification import ParserSpecification +from pycalc.specification import Specification from pycalc.token.constants import TokenType from pycalc.token.precedence import Precedence @@ -17,7 +17,7 @@ def build_specification(registry): - spec = ParserSpecification() + spec = Specification() # NUMBERS spec.nud.register(TokenType.NUMERIC, Number, From ea56e66103e8611ae656e3af80c51ae53e50c09f Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 20:00:40 +0300 Subject: [PATCH 066/144] Refactor calculator package --- final_task/pycalc/calculator/calculator.py | 2 +- final_task/pycalc/calculator/number.py | 20 ------------------- final_task/pycalc/calculator/parser.py | 1 - final_task/pycalc/calculator/specification.py | 5 ++--- 4 files changed, 3 insertions(+), 25 deletions(-) delete mode 100644 final_task/pycalc/calculator/number.py delete mode 100644 final_task/pycalc/calculator/parser.py diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index 3eba4e16..b52a174a 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -1,5 +1,5 @@ """ -Initializes a calculator and returns a parser object. +Initialization of a calculator. Returns a parser object. """ from pycalc.lexer import Lexer diff --git a/final_task/pycalc/calculator/number.py b/final_task/pycalc/calculator/number.py deleted file mode 100644 index f5752725..00000000 --- a/final_task/pycalc/calculator/number.py +++ /dev/null @@ -1,20 +0,0 @@ -"""""" - -import re - -from .helpers import regex_matcher - -NUMBER = r''' -( # integers or numbers with a fractional part: 13, 154., 3.44, ... -\d+ # an integer part: 10, 2, 432, ... -(\.\d*)* # a fractional part: .2, .43, .1245, ... or dot: . -) -| -( # numbers that begin with a dot: .12, .59, ... -\.\d+ # a fractional part: .2, .43, .1245, ... -) -''' - -NUMBER_REGEX = re.compile(NUMBER, re.VERBOSE) - -NUMBER_MATCHER = regex_matcher(NUMBER_REGEX) diff --git a/final_task/pycalc/calculator/parser.py b/final_task/pycalc/calculator/parser.py deleted file mode 100644 index 0bf07aac..00000000 --- a/final_task/pycalc/calculator/parser.py +++ /dev/null @@ -1 +0,0 @@ -from pycalc.parser.parser import Parser \ No newline at end of file diff --git a/final_task/pycalc/calculator/specification.py b/final_task/pycalc/calculator/specification.py index 87cb3545..8b077e5d 100644 --- a/final_task/pycalc/calculator/specification.py +++ b/final_task/pycalc/calculator/specification.py @@ -1,9 +1,8 @@ """ - +Initializatioin of parser specification. """ from math import pow as math_pow -import math # TODO: remove from pycalc.specification import Specification @@ -11,7 +10,7 @@ from pycalc.token.precedence import Precedence from pycalc.token.tokens import * -# FIX: operator build and Operator from token same name +# TODO: operator build and Operator from token same name import operator From 6e0188153d6c29ec65372d5ddc222d465e498f82 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 20:06:48 +0300 Subject: [PATCH 067/144] Add docs --- final_task/pycalc/parser/__init__.py | 8 ++++++++ final_task/pycalc/parser/parser.py | 7 +++++-- 2 files changed, 13 insertions(+), 2 deletions(-) create mode 100644 final_task/pycalc/parser/__init__.py diff --git a/final_task/pycalc/parser/__init__.py b/final_task/pycalc/parser/__init__.py new file mode 100644 index 00000000..0219f706 --- /dev/null +++ b/final_task/pycalc/parser/__init__.py @@ -0,0 +1,8 @@ +""" +Parser package provides a Parser class for +top down operator precedence parcing (Pratt parser). + +https://en.wikipedia.org/wiki/Pratt_parser +""" + +from .parser import Parser diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 32d60579..651b8a9e 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -1,10 +1,13 @@ """ -Parser. +Parser package provides a Parser class for +top down operator precedence parsing (Pratt parser). """ class Parser: - """""" + """ + Parser class for top down operator precedence parsing (Pratt parser). + """ def __init__(self, spec, lexer): self.spec = spec From f32053ea367094ebee7609c6bbe6af2e0ff5bb90 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 20:23:37 +0300 Subject: [PATCH 068/144] Update calculator asserts --- final_task/pycalc/calculator/calculator.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index b52a174a..f6517f0b 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -11,7 +11,7 @@ def calculator(modules_names=None): - """""" + """Initialize of a calculator and return a parser object.""" # import constants and functions from default and requested modules modules_registry = build_modules_registry(modules_names) @@ -37,10 +37,8 @@ def calculator(modules_names=None): p = calculator() - # assert p.parse('1 / 0') == 0 assert p.parse('sin(2)') == math.sin(2) assert p.parse('sin(2-3)') == math.sin(2 - 3) - # assert p.parse('sin(1,2)') == math.sin(0.5) assert p.parse('2') == 2 assert p.parse(' 2') == 2 assert p.parse('- 2') == - 2 @@ -49,7 +47,7 @@ def calculator(modules_names=None): assert p.parse(' 1 - 2 ') == -1 assert p.parse('1 - - 2') == 3 assert p.parse('1 - - - 2 ') == -1 - # assert p.parse('2 ** 3 ') == 8 + assert p.parse('2 ^ 3 ') == 8 assert p.parse('1 - 2 * 3') == -5 assert p.parse('3 ^ 2 * 2') == 18 assert p.parse('3 * 2 ^ 2') == 12 @@ -57,15 +55,18 @@ def calculator(modules_names=None): assert p.parse('6-(-13)') == 19 assert p.parse('( 7 - 2 ) * 3') == 15 assert p.parse('(0)') == 0 - # assert p.parse(') 2 ') == 15 assert p.parse('0 > 1') is False assert p.parse('0 >= 1') is False assert p.parse('2 > 1') is True assert p.parse('1 >= 1') is True assert p.parse('1 - 2 >= -1') is True assert p.parse('log(1025 - 1, 7 - 5)') == 10 - # assert p.parse(', 1') is True - # assert p.parse('1 , 2') is True + + # assert p.parse('1 / 0') + # assert p.parse('sin(1,2)') + # assert p.parse(', 1') + # assert p.parse('1 , 2') + # assert p.parse(') 2 ') == 15 + # assert p.parse('0 1') # assert p.parse('- - - 2 ^ log ( 1 , ( 4 - 1 ) * 5 , 4 )') == -1048576 # TODO: - # assert p.parse('0 1') is False From 282e4330d9bb8d0da7c96d7badec75bc82667b66 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 20:30:17 +0300 Subject: [PATCH 069/144] Add constants to matchers of calculator --- final_task/pycalc/calculator/matchers.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/calculator/matchers.py b/final_task/pycalc/calculator/matchers.py index e46c0e67..5e6e08b9 100644 --- a/final_task/pycalc/calculator/matchers.py +++ b/final_task/pycalc/calculator/matchers.py @@ -22,8 +22,13 @@ def build_matchers(registry): matchers.register_matcher(TokenType.FUNCTION, matchers.create_matcher_from_literals_list( - registry['functions'].keys() - )) + registry['functions'].keys()) + ) + + matchers.register_matcher(TokenType.CONSTANT, + matchers.create_matcher_from_literals_list( + registry['constants'].keys()) + ) matchers.register_matcher(TokenType.ADD, matchers.create_matcher_from_literals_list(['+'])) From 26de9dd434e841f6d509d610b55c3980b65903ba Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 20:34:07 +0300 Subject: [PATCH 070/144] Add constants to specification of calculator --- final_task/pycalc/calculator/specification.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/final_task/pycalc/calculator/specification.py b/final_task/pycalc/calculator/specification.py index 8b077e5d..29e3ca4b 100644 --- a/final_task/pycalc/calculator/specification.py +++ b/final_task/pycalc/calculator/specification.py @@ -30,6 +30,11 @@ def build_specification(registry): stop=TokenType.RIGHT_PARENTHESIS, sep=TokenType.COMMA) + # CONSTANTS + spec.nud.register(TokenType.CONSTANT, Constant, + power=Precedence.DEFAULT, + const_registry=registry['constants']) + # OPERATIONS # positive From 148904cffe05ca5eb4773312dc3cc044f15775f4 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 20:51:00 +0300 Subject: [PATCH 071/144] Add non equal operation to calculator --- final_task/pycalc/calculator/matchers.py | 4 ++++ final_task/pycalc/calculator/specification.py | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/calculator/matchers.py b/final_task/pycalc/calculator/matchers.py index 5e6e08b9..6e2b2f42 100644 --- a/final_task/pycalc/calculator/matchers.py +++ b/final_task/pycalc/calculator/matchers.py @@ -50,6 +50,10 @@ def build_matchers(registry): matchers.register_matcher(TokenType.EQ, matchers.create_matcher_from_literals_list(['=='])) + + matchers.register_matcher(TokenType.NE, + matchers.create_matcher_from_literals_list(['!='])) + matchers.register_matcher(TokenType.GE, matchers.create_matcher_from_literals_list(['>='])) diff --git a/final_task/pycalc/calculator/specification.py b/final_task/pycalc/calculator/specification.py index 29e3ca4b..b2887586 100644 --- a/final_task/pycalc/calculator/specification.py +++ b/final_task/pycalc/calculator/specification.py @@ -75,10 +75,14 @@ def build_specification(registry): power=Precedence.REMAINDER, func=operator.mod) - # equal or greater + # equal spec.led.register(TokenType.EQ, BinaryInfixLeft, power=Precedence.COMPARISONS, func=operator.eq) + # not equal + spec.led.register(TokenType.NE, BinaryInfixLeft, + power=Precedence.COMPARISONS, + func=operator.ne) # greater spec.led.register(TokenType.GT, BinaryInfixLeft, From 2959910aa6fa895628e44c13767c64b0793a56e6 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:45:16 +0300 Subject: [PATCH 072/144] Refactor specification package --- final_task/pycalc/specification/__init__.py | 5 +++ final_task/pycalc/specification/denotation.py | 40 +------------------ final_task/pycalc/specification/led.py | 23 +++++++++++ final_task/pycalc/specification/nud.py | 23 +++++++++++ .../pycalc/specification/specification.py | 5 ++- 5 files changed, 56 insertions(+), 40 deletions(-) create mode 100644 final_task/pycalc/specification/led.py create mode 100644 final_task/pycalc/specification/nud.py diff --git a/final_task/pycalc/specification/__init__.py b/final_task/pycalc/specification/__init__.py index e69de29b..3f624682 100644 --- a/final_task/pycalc/specification/__init__.py +++ b/final_task/pycalc/specification/__init__.py @@ -0,0 +1,5 @@ +""" +Specification for a parser. +""" + +from .specification import Specification diff --git a/final_task/pycalc/specification/denotation.py b/final_task/pycalc/specification/denotation.py index 94b317e2..79285059 100644 --- a/final_task/pycalc/specification/denotation.py +++ b/final_task/pycalc/specification/denotation.py @@ -3,7 +3,7 @@ """ -class _Denotation: +class Denotation: """ The base class for nud- and led-denotation classes. """ @@ -29,7 +29,7 @@ def power(self, token): def _get_parselet(self, token): """ - Find and return stored parselet for a given token. + Find and return appropriate stored parselet for a given token type. """ token_type = token.token_type @@ -49,39 +49,3 @@ def _check_for_dup(self, token_type): if token_type in self.registry: raise Exception( f'Token of {token_type} type is already registered.') - - -class Nud(_Denotation): - """ - Null-Denotation. - - The specification of how an operator consumes to the right with no left-context. - """ - - def eval(self, parser, token): - """ - Evaluate and return result. - """ - - parselet = self._get_parselet(token) - result = parselet.nud(parser, token) - - return result - - -class Led(_Denotation): - """ - Left-Denotation. - - The specification of how an operator consumes to the right with a left-context. - """ - - def eval(self, parser, token, left): - """ - Evaluate and return result. - """ - - parselet = self._get_parselet(token) - result = parselet.led(parser, token, left) - - return result diff --git a/final_task/pycalc/specification/led.py b/final_task/pycalc/specification/led.py new file mode 100644 index 00000000..66154614 --- /dev/null +++ b/final_task/pycalc/specification/led.py @@ -0,0 +1,23 @@ +""" +Left-Denotation. +""" + +from .denotation import Denotation + + +class Led(Denotation): + """ + Left-Denotation. + + The specification of how an operator consumes to the right with a left-context. + """ + + def eval(self, parser, token, left): + """ + Evaluate and return result. + """ + + parselet = self._get_parselet(token) + result = parselet.led(parser, token, left) + + return result diff --git a/final_task/pycalc/specification/nud.py b/final_task/pycalc/specification/nud.py new file mode 100644 index 00000000..f6678dfa --- /dev/null +++ b/final_task/pycalc/specification/nud.py @@ -0,0 +1,23 @@ +""" +Null-Denotation. +""" + +from .denotation import Denotation + + +class Nud(Denotation): + """ + Null-Denotation. + + The specification of how an operator consumes to the right with no left-context. + """ + + def eval(self, parser, token): + """ + Evaluate and return result. + """ + + parselet = self._get_parselet(token) + result = parselet.nud(parser, token) + + return result diff --git a/final_task/pycalc/specification/specification.py b/final_task/pycalc/specification/specification.py index b9d430f5..08c41b60 100644 --- a/final_task/pycalc/specification/specification.py +++ b/final_task/pycalc/specification/specification.py @@ -2,10 +2,11 @@ Parser specification. """ -from .denotation import Nud, Led +from .led import Led +from .nud import Nud -class ParserSpecification: +class Specification: """ Holds nud and led. """ From db263a2fc58f7264035208370a60414b136e52c5 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 20:57:23 +0300 Subject: [PATCH 073/144] Add logging of assert calls --- final_task/pycalc/calculator/calculator.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index f6517f0b..286261d8 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -35,7 +35,17 @@ def calculator(modules_names=None): if __name__ == "__main__": import math + def logger(fn): + def wrap(source): + print('=' * 30) + print(f'input : {source}') + result = fn(source) + print(f'output: {result}') + return result + return wrap + p = calculator() + p.parse = logger(p.parse) assert p.parse('sin(2)') == math.sin(2) assert p.parse('sin(2-3)') == math.sin(2 - 3) From 26a39df762ea56034be5c305a6646be7109488ef Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 20:58:49 +0300 Subject: [PATCH 074/144] Remove unused code --- final_task/pycalc/parser/parser.py | 48 +----------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 651b8a9e..7d02b6df 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -16,10 +16,8 @@ def __init__(self, spec, lexer): def parse(self, source): """""" - print('=' * 30) - print(f'input : {source}') - self.lexer.init(source) + try: result = self.expression() except Exception as e: @@ -30,8 +28,6 @@ def parse(self, source): assert self.lexer.is_source_exhausted(), \ f'source not parsed completely, (pos: {self.lexer.pos}), {self.lexer.format()}' - print(f'output: {result}') - return result def expression(self, right_power=0): @@ -102,45 +98,3 @@ def _left_power(self, token): """""" return self.spec.led.power(token) - - -if __name__ == "__main__": - from pycalc.lexer.lexer import Lexer - from pycalc.matcher.matcher import matchers - from pycalc.specification.specification import spec - import math - - lexer = Lexer(matchers) - p = Parser(spec, lexer) - # assert p.parse('1 / 0') == 0 - assert p.parse('sin(2)') == math.sin(2) - assert p.parse('sin(2-3)') == math.sin(2 - 3) - # assert p.parse('sin(1,2)') == math.sin(0.5) - assert p.parse('2') == 2 - assert p.parse(' 2') == 2 - assert p.parse('- 2') == - 2 - assert p.parse('- - 2') == 2 - assert p.parse('1 - 2') == -1 - assert p.parse(' 1 - 2 ') == -1 - assert p.parse('1 - - 2') == 3 - assert p.parse('1 - - - 2 ') == -1 - # assert p.parse('2 ** 3 ') == 8 - assert p.parse('1 - 2 * 3') == -5 - assert p.parse('3 ^ 2 * 2') == 18 - assert p.parse('3 * 2 ^ 2') == 12 - assert p.parse('4 ^ 3 ^ 2') == 262144 - assert p.parse('6-(-13)') == 19 - assert p.parse('( 7 - 2 ) * 3') == 15 - assert p.parse('(0)') == 0 - # assert p.parse(') 2 ') == 15 - assert p.parse('0 > 1') is False - assert p.parse('0 >= 1') is False - assert p.parse('2 > 1') is True - assert p.parse('1 >= 1') is True - assert p.parse('1 - 2 >= -1') is True - assert p.parse('log(1025 - 1, 7 - 5)') == 10 - # assert p.parse(', 1') is True - # assert p.parse('1 , 2') is True - # assert p.parse('- - - 2 ^ log ( 1 , ( 4 - 1 ) * 5 , 4 )') == -1048576 - # TODO: - # assert p.parse('0 1') is False From e201df6076cd9aea745a2fd65fac43832cc2ed4a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 21:01:17 +0300 Subject: [PATCH 075/144] Format code --- final_task/pycalc/specification/denotation.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/final_task/pycalc/specification/denotation.py b/final_task/pycalc/specification/denotation.py index 79285059..35f52876 100644 --- a/final_task/pycalc/specification/denotation.py +++ b/final_task/pycalc/specification/denotation.py @@ -12,25 +12,20 @@ def __init__(self): self.registry = {} def register(self, token_type, parselet, **kwargs): - """ - Register token type with an appropriate parselet. - """ + """Register token type with an appropriate parselet.""" + self._check_for_dup(token_type) self.registry[token_type] = parselet(**kwargs) def power(self, token): - """ - Return power for a given token. - """ + """Return power for a given token.""" power = self._get_parselet(token).power return power def _get_parselet(self, token): - """ - Find and return appropriate stored parselet for a given token type. - """ + """Find and return appropriate stored parselet for a given token type.""" token_type = token.token_type From 89fd3a8756cf4bd1f11f28c93cecaf1ca673056c Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 21:21:39 +0300 Subject: [PATCH 076/144] Update docs --- final_task/pycalc/specification/led.py | 4 +--- final_task/pycalc/specification/nud.py | 4 +--- final_task/pycalc/specification/specification.py | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/final_task/pycalc/specification/led.py b/final_task/pycalc/specification/led.py index 66154614..e40915e9 100644 --- a/final_task/pycalc/specification/led.py +++ b/final_task/pycalc/specification/led.py @@ -13,9 +13,7 @@ class Led(Denotation): """ def eval(self, parser, token, left): - """ - Evaluate and return result. - """ + """Receive from left, evaluate and return result.""" parselet = self._get_parselet(token) result = parselet.led(parser, token, left) diff --git a/final_task/pycalc/specification/nud.py b/final_task/pycalc/specification/nud.py index f6678dfa..5aaf1ce2 100644 --- a/final_task/pycalc/specification/nud.py +++ b/final_task/pycalc/specification/nud.py @@ -13,9 +13,7 @@ class Nud(Denotation): """ def eval(self, parser, token): - """ - Evaluate and return result. - """ + """Evaluate and return result.""" parselet = self._get_parselet(token) result = parselet.nud(parser, token) diff --git a/final_task/pycalc/specification/specification.py b/final_task/pycalc/specification/specification.py index 08c41b60..7b815bd2 100644 --- a/final_task/pycalc/specification/specification.py +++ b/final_task/pycalc/specification/specification.py @@ -7,9 +7,7 @@ class Specification: - """ - Holds nud and led. - """ + """Holds nud and led specifications.""" def __init__(self): self.nud = Nud() From bb12b72626dac7a21f62c9d039506d06cd1cbf79 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 21:23:00 +0300 Subject: [PATCH 077/144] Fix typo --- final_task/pycalc/calculator/specification.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/final_task/pycalc/calculator/specification.py b/final_task/pycalc/calculator/specification.py index b2887586..0d5cf09e 100644 --- a/final_task/pycalc/calculator/specification.py +++ b/final_task/pycalc/calculator/specification.py @@ -1,5 +1,5 @@ """ -Initializatioin of parser specification. +Initialization of parser specification. """ from math import pow as math_pow From cf35b2e90e8ea96e134f2d7aba713b21cb2a8c41 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 7 May 2019 08:44:41 +0300 Subject: [PATCH 078/144] Raise an exception if a source is not parsed completely --- final_task/pycalc/parser/parser.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 7d02b6df..dc36d01e 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -25,8 +25,9 @@ def parse(self, source): f'ERROR: {e}: (pos: {self.lexer.pos}), {self.lexer.format()}') raise e - assert self.lexer.is_source_exhausted(), \ - f'source not parsed completely, (pos: {self.lexer.pos}), {self.lexer.format()}' + if not self.lexer.is_source_exhausted(): + raise Exception( + f'ERROR: source not parsed completely, (pos: {self.lexer.pos}), {self.lexer.format()}') return result From 638066764c09662b810d875aa227cc3e3815d583 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 7 May 2019 09:18:11 +0300 Subject: [PATCH 079/144] Add docs --- final_task/pycalc/parser/parser.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index dc36d01e..ae0d2e7c 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -5,16 +5,14 @@ class Parser: - """ - Parser class for top down operator precedence parsing (Pratt parser). - """ + """Parser class for top down operator precedence parsing (Pratt parser).""" def __init__(self, spec, lexer): self.spec = spec self.lexer = lexer def parse(self, source): - """""" + """Parse a source and return a result of parsing.""" self.lexer.init(source) @@ -32,7 +30,7 @@ def parse(self, source): return result def expression(self, right_power=0): - """""" + """The main parsing function of Pratt parser.""" token = self.consume() if not token: @@ -54,17 +52,20 @@ def expression(self, right_power=0): return left def consume(self): - """""" + """Return the next token and advance the source position pointer.""" return self.lexer.consume() def peek(self): - """""" + """Return the next token without advancing the source position pointer.""" return self.lexer.peek() def advance(self, token_type=None): - """""" + """ + Consume a next token if that one is of given type. + Raise an exception if types don’t match. + """ token = self.peek() @@ -77,7 +78,7 @@ def advance(self, token_type=None): self.consume() def peek_and_check(self, token_type): - """""" + """Check if the next token is of given type.""" token = self.peek() if not token or token.token_type != token_type: @@ -96,6 +97,6 @@ def _led(self, token, left): return self.spec.led.eval(self, token, left) def _left_power(self, token): - """""" + """Get token binding power.""" return self.spec.led.power(token) From 98f6fabb5693587c5884a4cf8cd9455c57120273 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 7 May 2019 10:38:01 +0300 Subject: [PATCH 080/144] Fix methods names after renaming --- final_task/pycalc/calculator/matchers.py | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/final_task/pycalc/calculator/matchers.py b/final_task/pycalc/calculator/matchers.py index 6e2b2f42..633570c3 100644 --- a/final_task/pycalc/calculator/matchers.py +++ b/final_task/pycalc/calculator/matchers.py @@ -21,52 +21,52 @@ def build_matchers(registry): PREDEFINED_MATCHERS[TokenType.NUMERIC]) matchers.register_matcher(TokenType.FUNCTION, - matchers.create_matcher_from_literals_list( + matchers.create_from.literals_list( registry['functions'].keys()) ) matchers.register_matcher(TokenType.CONSTANT, - matchers.create_matcher_from_literals_list( + matchers.create_from.literals_list( registry['constants'].keys()) ) matchers.register_matcher(TokenType.ADD, - matchers.create_matcher_from_literals_list(['+'])) + matchers.create_from.literals_list(['+'])) matchers.register_matcher(TokenType.SUB, - matchers.create_matcher_from_literals_list(['-'])) + matchers.create_from.literals_list(['-'])) matchers.register_matcher(TokenType.MUL, - matchers.create_matcher_from_literals_list(['*'])) + matchers.create_from.literals_list(['*'])) matchers.register_matcher(TokenType.TRUEDIV, - matchers.create_matcher_from_literals_list(['/'])) + matchers.create_from.literals_list(['/'])) matchers.register_matcher(TokenType.MOD, - matchers.create_matcher_from_literals_list(['%'])) + matchers.create_from.literals_list(['%'])) matchers.register_matcher(TokenType.POW, - matchers.create_matcher_from_literals_list(['^'])) + matchers.create_from.literals_list(['^'])) matchers.register_matcher(TokenType.EQ, - matchers.create_matcher_from_literals_list(['=='])) + matchers.create_from.literals_list(['=='])) matchers.register_matcher(TokenType.NE, - matchers.create_matcher_from_literals_list(['!='])) + matchers.create_from.literals_list(['!='])) matchers.register_matcher(TokenType.GE, - matchers.create_matcher_from_literals_list(['>='])) + matchers.create_from.literals_list(['>='])) matchers.register_matcher(TokenType.GT, - matchers.create_matcher_from_literals_list(['>'])) + matchers.create_from.literals_list(['>'])) matchers.register_matcher(TokenType.LEFT_PARENTHESIS, - matchers.create_matcher_from_literals_list(['('])) + matchers.create_from.literals_list(['('])) matchers.register_matcher(TokenType.RIGHT_PARENTHESIS, - matchers.create_matcher_from_literals_list([')'])) + matchers.create_from.literals_list([')'])) matchers.register_matcher(TokenType.COMMA, - matchers.create_matcher_from_literals_list([','])) + matchers.create_from.literals_list([','])) return matchers From 64cc40927d31e71aab1403c05fc4faf8a62934bc Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 7 May 2019 18:15:46 +0300 Subject: [PATCH 081/144] Refactor matchers creating --- final_task/pycalc/calculator/matchers.py | 77 +++++++----------------- 1 file changed, 23 insertions(+), 54 deletions(-) diff --git a/final_task/pycalc/calculator/matchers.py b/final_task/pycalc/calculator/matchers.py index 633570c3..9c589438 100644 --- a/final_task/pycalc/calculator/matchers.py +++ b/final_task/pycalc/calculator/matchers.py @@ -1,72 +1,41 @@ """ -Build matchers. +Create and register matchers for token types. """ from pycalc.matcher.matcher import Matchers -from pycalc.matcher.number import NUMBER_MATCHER +from pycalc.matcher.number import NUMBER_REGEX from pycalc.token.constants import TokenType +from pycalc.token.lexeme import PREDEFINED -PREDEFINED_MATCHERS = { - TokenType.NUMERIC: NUMBER_MATCHER -} - - -def build_matchers(registry): - """""" +def build_matchers(imports_registry): + """Create and register matchers for token types.""" matchers = Matchers() - matchers.register_matcher(TokenType.NUMERIC, - PREDEFINED_MATCHERS[TokenType.NUMERIC]) - - matchers.register_matcher(TokenType.FUNCTION, - matchers.create_from.literals_list( - registry['functions'].keys()) - ) - - matchers.register_matcher(TokenType.CONSTANT, - matchers.create_from.literals_list( - registry['constants'].keys()) - ) - - matchers.register_matcher(TokenType.ADD, - matchers.create_from.literals_list(['+'])) - - matchers.register_matcher(TokenType.SUB, - matchers.create_from.literals_list(['-'])) - - matchers.register_matcher(TokenType.MUL, - matchers.create_from.literals_list(['*'])) - - matchers.register_matcher(TokenType.TRUEDIV, - matchers.create_from.literals_list(['/'])) - - matchers.register_matcher(TokenType.MOD, - matchers.create_from.literals_list(['%'])) - - matchers.register_matcher(TokenType.POW, - matchers.create_from.literals_list(['^'])) - - matchers.register_matcher(TokenType.EQ, - matchers.create_from.literals_list(['=='])) + # create matchers for token types with dynamically created lexemes - matchers.register_matcher(TokenType.NE, - matchers.create_from.literals_list(['!='])) + numeric_matcher = matchers.create_from.compiled_regex(NUMBER_REGEX) - matchers.register_matcher(TokenType.GE, - matchers.create_from.literals_list(['>='])) + functions_matcher = matchers.create_from.literals_list( + imports_registry['functions'].keys()) - matchers.register_matcher(TokenType.GT, - matchers.create_from.literals_list(['>'])) + constants_matcher = matchers.create_from.literals_list( + imports_registry['constants'].keys()) - matchers.register_matcher(TokenType.LEFT_PARENTHESIS, - matchers.create_from.literals_list(['('])) + # register matchers for token types with dynamically created lexemes + matchers.register_matcher(TokenType.NUMERIC, numeric_matcher) + matchers.register_matcher(TokenType.FUNCTION, functions_matcher) + matchers.register_matcher(TokenType.CONSTANT, constants_matcher) - matchers.register_matcher(TokenType.RIGHT_PARENTHESIS, - matchers.create_from.literals_list([')'])) + # sort predefined lexemes map by lexeme length in reversed order + lexemes_map = sorted(PREDEFINED.items(), + key=lambda kv: len(kv[1]), + reverse=True) - matchers.register_matcher(TokenType.COMMA, - matchers.create_from.literals_list([','])) + # create and register matchers for predefined lexemes + for token_type, lexeme in lexemes_map: + matchers.register_matcher(token_type, + matchers.create_from.literals_list([lexeme])) return matchers From 8662e15fb3ae339a48b1a5550906a318e314bb55 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 7 May 2019 19:42:37 +0300 Subject: [PATCH 082/144] Update calculator specification --- final_task/pycalc/calculator/specification.py | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/final_task/pycalc/calculator/specification.py b/final_task/pycalc/calculator/specification.py index 0d5cf09e..1e9ed3dd 100644 --- a/final_task/pycalc/calculator/specification.py +++ b/final_task/pycalc/calculator/specification.py @@ -55,11 +55,6 @@ def build_specification(registry): power=Precedence.SUBTRACTION, func=operator.sub) - # exponentiation - spec.led.register(TokenType.POW, BinaryInfixRight, - power=Precedence.EXPONENTIATION, - func=math_pow) - # multiplication spec.led.register(TokenType.MUL, BinaryInfixLeft, power=Precedence.MULTIPLICATION, @@ -70,6 +65,16 @@ def build_specification(registry): power=Precedence.DIVISION, func=operator.truediv) + # floor division + spec.led.register(TokenType.FLOORDIV, BinaryInfixLeft, + power=Precedence.FLOOR_DIVISION, + func=operator.floordiv) + + # exponentiation + spec.led.register(TokenType.POW, BinaryInfixRight, + power=Precedence.EXPONENTIATION, + func=math_pow) + # remainder spec.led.register(TokenType.MOD, BinaryInfixLeft, power=Precedence.REMAINDER, @@ -94,6 +99,16 @@ def build_specification(registry): power=Precedence.COMPARISONS, func=operator.ge) + # less + spec.led.register(TokenType.LT, BinaryInfixLeft, + power=Precedence.COMPARISONS, + func=operator.lt) + + # equal or less + spec.led.register(TokenType.LE, BinaryInfixLeft, + power=Precedence.COMPARISONS, + func=operator.le) + # PUNCTUATION spec.nud.register(TokenType.LEFT_PARENTHESIS, GroupedExpressionStart, power=Precedence.BINDING, From 674c77b3a3a5cd59d490beab4110693805fd1ab5 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 11:42:45 +0300 Subject: [PATCH 083/144] Transform constant names to uppercase --- final_task/pycalc/args.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/args.py b/final_task/pycalc/args.py index ada68b87..bd54c765 100644 --- a/final_task/pycalc/args.py +++ b/final_task/pycalc/args.py @@ -24,14 +24,14 @@ } } -arguments = [ +ARGUMENTS = [ EXPRESSION, MODULE, ] parser = argparse.ArgumentParser(**PARSER) -for arg in arguments: +for arg in ARGUMENTS: parser.add_argument(*arg['name_or_flags'], **arg['keyword_arguments']) args = parser.parse_args() From e4591492252b56269fb72dd28f821384aa6b2eeb Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 11:43:01 +0300 Subject: [PATCH 084/144] Add docs --- final_task/pycalc/args.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/final_task/pycalc/args.py b/final_task/pycalc/args.py index bd54c765..c523ec31 100644 --- a/final_task/pycalc/args.py +++ b/final_task/pycalc/args.py @@ -1,3 +1,7 @@ +""" +Parse command-line options. +""" + import argparse PARSER = { From 993440cccd5f48b2bf8ede5b06c2fe4079e0817b Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:12:17 +0300 Subject: [PATCH 085/144] Add exceptions for the importer package --- final_task/pycalc/importer/__init__.py | 1 + final_task/pycalc/importer/errors.py | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 final_task/pycalc/importer/errors.py diff --git a/final_task/pycalc/importer/__init__.py b/final_task/pycalc/importer/__init__.py index 997c7d7d..23567c37 100644 --- a/final_task/pycalc/importer/__init__.py +++ b/final_task/pycalc/importer/__init__.py @@ -4,3 +4,4 @@ """ from .importer import collect_members_by_type, import_modules +from .errors import ModuleImportErrors diff --git a/final_task/pycalc/importer/errors.py b/final_task/pycalc/importer/errors.py new file mode 100644 index 00000000..07d65a71 --- /dev/null +++ b/final_task/pycalc/importer/errors.py @@ -0,0 +1,15 @@ +""" +Exceptions for the importer module. +""" + + +class ModuleImportErrors(ModuleNotFoundError): + """""" + + def __init__(self, module_names): + super().__init__() + self.modules_names = module_names + + def __str__(self): + + return f"no module named {', '.join(self.modules_names)}" From 063c0b06972eb305a84e284af95662733f1052c0 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:13:59 +0300 Subject: [PATCH 086/144] Raise a custom exception on module imports error --- final_task/pycalc/importer/importer.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/importer/importer.py b/final_task/pycalc/importer/importer.py index da9d7f9d..63b6125f 100644 --- a/final_task/pycalc/importer/importer.py +++ b/final_task/pycalc/importer/importer.py @@ -8,6 +8,9 @@ from inspect import getmembers from itertools import chain +from .errors import ModuleImportErrors + + UNDERSCORE = '_' @@ -23,6 +26,7 @@ def import_modules(*iterables): """""" modules = [] + failed_imports = [] for module_name in iter_uniq(iterables): @@ -30,7 +34,10 @@ def import_modules(*iterables): module = import_module(module_name) modules.append(module) except ModuleNotFoundError: - raise ModuleNotFoundError + failed_imports.append(module_name) + + if failed_imports: + raise ModuleImportErrors(failed_imports) return modules From 68a0f75464066b6638e222e1ddacfce817cae4eb Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:34:34 +0300 Subject: [PATCH 087/144] Add the context method to the lexer class --- final_task/pycalc/lexer/lexer.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index 538ef2d0..b2e6e23d 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -7,6 +7,8 @@ Token = namedtuple("Token", ("token_type", "lexeme")) +LexerContext = namedtuple('LexerContext', ('source', 'pos')) + WHITESPACES = re.compile(r"\s+") @@ -17,6 +19,7 @@ def __init__(self, matchers): self.matchers = matchers self.source = '' self.pos = 0 + self.prev_pos = 0 self.length = 0 # a wrapper for caching the last peeked token @@ -27,9 +30,17 @@ def init(self, source): self.source = source self.pos = 0 + self.prev_pos = 0 self.length = len(source) self._token_wrapper.clear() + def context(self, previous=False): + """Return a lexer context.""" + + pos = self.prev_pos if previous else self.pos + + return LexerContext(self.source, pos) + def is_source_exhausted(self): """Return `True` if the position pointer is out of the source string.""" @@ -100,4 +111,5 @@ def _advance_pos_by_lexeme(self, lexeme): """Advance the position index by lexeme lenght.""" value = len(lexeme) + self.prev_pos = self.pos self.pos += value From 4e500a86ac4f4f24eacbae1cdcd8a998201501fc Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:35:18 +0300 Subject: [PATCH 088/144] Remove unused code --- final_task/pycalc/lexer/lexer.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index b2e6e23d..5483ea21 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -69,15 +69,6 @@ def consume(self): return token - def format(self): - """""" - - pos = self.pos - begin = self.source[:pos] - end = self.source[pos:] - - return f'{begin}>{end}' - def _next_token(self): """Try to match the next token.""" From 9f42f680b7863c3234ec3147bddfeeb126ebb4d5 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:36:06 +0300 Subject: [PATCH 089/144] Update docs --- final_task/pycalc/lexer/lexer.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/final_task/pycalc/lexer/lexer.py b/final_task/pycalc/lexer/lexer.py index 5483ea21..329b5178 100644 --- a/final_task/pycalc/lexer/lexer.py +++ b/final_task/pycalc/lexer/lexer.py @@ -49,7 +49,7 @@ def is_source_exhausted(self): return self.pos >= self.length def peek(self): - """""" + """Return the next token without advancing the pointer position.""" if self._token_wrapper: return self._token_wrapper[-1] @@ -60,7 +60,7 @@ def peek(self): return token def consume(self): - """""" + """Return the next token and advance the pointer position.""" token = self.peek() self._token_wrapper.pop() @@ -70,7 +70,7 @@ def consume(self): return token def _next_token(self): - """Try to match the next token.""" + """Skip whitespaces and return the next token.""" self._skip_whitespaces() token = self._match() @@ -78,7 +78,7 @@ def _next_token(self): return token def _match(self): - """Return `Token` or `None`.""" + """Try to match the next token. Return a `Token` instance or `None`.""" for token_type, matcher in self.matchers: lexeme = matcher(self.source, self.pos) @@ -99,7 +99,7 @@ def _skip_whitespaces(self): self._advance_pos_by_lexeme(whitespaces.group()) def _advance_pos_by_lexeme(self, lexeme): - """Advance the position index by lexeme lenght.""" + """Advance the position index by lexeme length.""" value = len(lexeme) self.prev_pos = self.pos From 68a1b8246fc13ce21d933cf474f597d76b5214b9 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:41:39 +0300 Subject: [PATCH 090/144] Implement exceptions for the parser package --- final_task/pycalc/parser/__init__.py | 6 +++++ final_task/pycalc/parser/errors.py | 37 ++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 final_task/pycalc/parser/errors.py diff --git a/final_task/pycalc/parser/__init__.py b/final_task/pycalc/parser/__init__.py index 0219f706..467abe2e 100644 --- a/final_task/pycalc/parser/__init__.py +++ b/final_task/pycalc/parser/__init__.py @@ -6,3 +6,9 @@ """ from .parser import Parser +from .errors import ( + ParserGenericError, + ParserNoTokenReceived, + ParserExpectedTokenAbsent, + ParserSourceNotExhausted +) diff --git a/final_task/pycalc/parser/errors.py b/final_task/pycalc/parser/errors.py new file mode 100644 index 00000000..d65766d3 --- /dev/null +++ b/final_task/pycalc/parser/errors.py @@ -0,0 +1,37 @@ +""" +Exceptions for the parser module. +""" + +GENERIC_PARSER_ERROR = 'encountered an error while parsing' + + +class ParserGenericError(SyntaxError): + """A generic exception for a parser.""" + + def __init__(self, ctx): + super().__init__() + self.ctx = ctx + + def __str__(self): + return GENERIC_PARSER_ERROR + + +class ParserNoTokenReceived(ParserGenericError): + """Raise when a lexer returns `None` instead of `Token` object.""" + + pass + + +class ParserExpectedTokenAbsent(ParserGenericError): + """Raise when a token is not of a given type.""" + + pass + + +class ParserSourceNotExhausted(ParserGenericError): + """ + Raise when a parser finished parsing a source + but a source is not parsed completely. + """ + + pass From 6e02292c0c1722b67ad5b977d194904769834881 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:43:50 +0300 Subject: [PATCH 091/144] Add context method to the parser class --- final_task/pycalc/parser/parser.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index ae0d2e7c..c3a34b98 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -86,6 +86,11 @@ def peek_and_check(self, token_type): return True + def context(self, previous=False): + """Return parsing context.""" + + return self.lexer.context(previous) + def _nud(self, token): """""" From 45811e31fc5fdf69aef1da79a7fdef538ea27d80 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:46:11 +0300 Subject: [PATCH 092/144] Refactor exception raising in the parser class --- final_task/pycalc/parser/parser.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index c3a34b98..2e3d8210 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -4,6 +4,14 @@ """ +from .errors import ( + ParserExpectedTokenAbsent, + ParserNoTokenReceived, + ParserGenericError, + ParserSourceNotExhausted +) + + class Parser: """Parser class for top down operator precedence parsing (Pratt parser).""" @@ -18,14 +26,11 @@ def parse(self, source): try: result = self.expression() - except Exception as e: - print( - f'ERROR: {e}: (pos: {self.lexer.pos}), {self.lexer.format()}') - raise e + except Exception as exc: + raise ParserGenericError(self.context()) from exc if not self.lexer.is_source_exhausted(): - raise Exception( - f'ERROR: source not parsed completely, (pos: {self.lexer.pos}), {self.lexer.format()}') + raise ParserSourceNotExhausted(self.context()) return result @@ -34,7 +39,7 @@ def expression(self, right_power=0): token = self.consume() if not token: - raise SyntaxError('i expect something but nothing finded') + raise ParserNoTokenReceived(self.context()) left = self._nud(token) @@ -64,7 +69,8 @@ def peek(self): def advance(self, token_type=None): """ Consume a next token if that one is of given type. - Raise an exception if types don’t match. + + Raise an `ParserExpectedTokenAbsent` exception if types don’t match. """ token = self.peek() @@ -73,7 +79,7 @@ def advance(self, token_type=None): token_type and not token.token_type == token_type ): - raise SyntaxError(f"Expected: {token_type}") + raise ParserExpectedTokenAbsent(self.context()) self.consume() From 20a4b57b6ffe942c08b06d39e057dfb5b5c8139d Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:52:25 +0300 Subject: [PATCH 093/144] Pass default token power to the parser constructor --- final_task/pycalc/parser/parser.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index 2e3d8210..fa151d9c 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -15,9 +15,10 @@ class Parser: """Parser class for top down operator precedence parsing (Pratt parser).""" - def __init__(self, spec, lexer): + def __init__(self, spec, lexer, default_power): self.spec = spec self.lexer = lexer + self.default_power = default_power def parse(self, source): """Parse a source and return a result of parsing.""" @@ -34,7 +35,7 @@ def parse(self, source): return result - def expression(self, right_power=0): + def expression(self, right_power=None): """The main parsing function of Pratt parser.""" token = self.consume() @@ -48,6 +49,7 @@ def expression(self, right_power=0): if not token: break + right_power = right_power if right_power is not None else self.default_power if right_power >= self._left_power(token): break From a7b5d573002d0ad802465613a48281f0623d6c5c Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 13:05:11 +0300 Subject: [PATCH 094/144] Implement exceptions for the specification package --- final_task/pycalc/specification/__init__.py | 5 +++ final_task/pycalc/specification/errors.py | 46 +++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 final_task/pycalc/specification/errors.py diff --git a/final_task/pycalc/specification/__init__.py b/final_task/pycalc/specification/__init__.py index 3f624682..453bc2ef 100644 --- a/final_task/pycalc/specification/__init__.py +++ b/final_task/pycalc/specification/__init__.py @@ -3,3 +3,8 @@ """ from .specification import Specification +from .errors import ( + DuplicatedTokenType, + NudDenotationError, + LedDenotationError +) diff --git a/final_task/pycalc/specification/errors.py b/final_task/pycalc/specification/errors.py new file mode 100644 index 00000000..a9b8b5ac --- /dev/null +++ b/final_task/pycalc/specification/errors.py @@ -0,0 +1,46 @@ +""" +Exceptions for the specification package. +""" + + +def denotation_err_msg(token, denotation_type): + """Return a formatted message for denotation error excepcions.""" + + return f'{token.token_type} is not registered or can’t be in the {denotation_type}-position.' + + +class DuplicatedTokenType(Exception): + """Raise when a token type is already registered in a registry.""" + + def __init__(self, token_type): + super().__init__() + self.token_type = token_type + + +class DenotationError(SyntaxError): + """The generic exception class for denotation errors.""" + + def __init__(self, ctx, token): + super().__init__() + self.ctx = ctx + self.token = token + + +class NudDenotationError(DenotationError): + """ + Raise when a token type is not registered in a specification + or can’t be in the nud-position. + """ + + def __str__(self): + return denotation_err_msg(self.token, 'nud') + + +class LedDenotationError(DenotationError): + """ + Raise when a token type is not registered in a specification + or can’t be in the led-position. + """ + + def __str__(self): + return denotation_err_msg(self.token, 'led') From b22aaeade78a08d6914e1d863515d3ccf3a61636 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 13:07:50 +0300 Subject: [PATCH 095/144] Refactor exceptions handling in the specification package --- final_task/pycalc/specification/denotation.py | 11 ++++------- final_task/pycalc/specification/led.py | 8 +++++++- final_task/pycalc/specification/nud.py | 8 +++++++- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/final_task/pycalc/specification/denotation.py b/final_task/pycalc/specification/denotation.py index 35f52876..5dc0480c 100644 --- a/final_task/pycalc/specification/denotation.py +++ b/final_task/pycalc/specification/denotation.py @@ -2,6 +2,8 @@ Denotation. """ +from .errors import DuplicatedTokenType + class Denotation: """ @@ -28,11 +30,7 @@ def _get_parselet(self, token): """Find and return appropriate stored parselet for a given token type.""" token_type = token.token_type - - try: - parselet = self.registry[token_type] - except KeyError: - raise SyntaxError(f'not in specification: {token_type}') + parselet = self.registry[token_type] return parselet @@ -42,5 +40,4 @@ def _check_for_dup(self, token_type): """ if token_type in self.registry: - raise Exception( - f'Token of {token_type} type is already registered.') + raise DuplicatedTokenType(token_type) diff --git a/final_task/pycalc/specification/led.py b/final_task/pycalc/specification/led.py index e40915e9..f3af23da 100644 --- a/final_task/pycalc/specification/led.py +++ b/final_task/pycalc/specification/led.py @@ -3,6 +3,7 @@ """ from .denotation import Denotation +from .errors import LedDenotationError class Led(Denotation): @@ -15,7 +16,12 @@ class Led(Denotation): def eval(self, parser, token, left): """Receive from left, evaluate and return result.""" - parselet = self._get_parselet(token) + try: + parselet = self._get_parselet(token) + except KeyError: + ctx = parser.context(previous=True) + raise LedDenotationError(ctx, token) + result = parselet.led(parser, token, left) return result diff --git a/final_task/pycalc/specification/nud.py b/final_task/pycalc/specification/nud.py index 5aaf1ce2..214fe315 100644 --- a/final_task/pycalc/specification/nud.py +++ b/final_task/pycalc/specification/nud.py @@ -3,6 +3,7 @@ """ from .denotation import Denotation +from .errors import NudDenotationError class Nud(Denotation): @@ -15,7 +16,12 @@ class Nud(Denotation): def eval(self, parser, token): """Evaluate and return result.""" - parselet = self._get_parselet(token) + try: + parselet = self._get_parselet(token) + except KeyError: + ctx = parser.context(previous=True) + raise NudDenotationError(ctx, token) + result = parselet.nud(parser, token) return result From 34ebad55dedcbb29ffb94e466239b626202e1a37 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 13:08:07 +0300 Subject: [PATCH 096/144] Update docs --- final_task/pycalc/specification/__init__.py | 2 +- final_task/pycalc/specification/specification.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/specification/__init__.py b/final_task/pycalc/specification/__init__.py index 453bc2ef..185dad80 100644 --- a/final_task/pycalc/specification/__init__.py +++ b/final_task/pycalc/specification/__init__.py @@ -1,5 +1,5 @@ """ -Specification for a parser. +Specification for a Pratt parser. """ from .specification import Specification diff --git a/final_task/pycalc/specification/specification.py b/final_task/pycalc/specification/specification.py index 7b815bd2..3b263dcb 100644 --- a/final_task/pycalc/specification/specification.py +++ b/final_task/pycalc/specification/specification.py @@ -1,5 +1,5 @@ """ -Parser specification. +A generic parser specification. """ from .led import Led From e63cf227b9cf6ad61fb869ec0b4d80d606942ea7 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 13:39:03 +0300 Subject: [PATCH 097/144] Add calculator's messages constants --- final_task/pycalc/calculator/messages.py | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 final_task/pycalc/calculator/messages.py diff --git a/final_task/pycalc/calculator/messages.py b/final_task/pycalc/calculator/messages.py new file mode 100644 index 00000000..3359117a --- /dev/null +++ b/final_task/pycalc/calculator/messages.py @@ -0,0 +1,8 @@ +""" +Text constants for calculator messages. +""" + +CALCULATOR_INITIALIZATION_ERROR = 'calculator initialization error' +ERROR_MSG_PREFIX = 'ERROR: ' +EMPTY_EXPRESSION_PROVIDED = 'empty expression provided' +SYNTAX_ERROR = 'syntax error' From 020b08e889523ca759a75b08667fc13b65061647 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 13:39:33 +0300 Subject: [PATCH 098/144] Add calculator's string formatters --- final_task/pycalc/calculator/formatters.py | 27 ++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 final_task/pycalc/calculator/formatters.py diff --git a/final_task/pycalc/calculator/formatters.py b/final_task/pycalc/calculator/formatters.py new file mode 100644 index 00000000..635fa94a --- /dev/null +++ b/final_task/pycalc/calculator/formatters.py @@ -0,0 +1,27 @@ +""" +Provides functions for string formatting. +""" + +from .messages import ERROR_MSG_PREFIX + + +ERROR_PLACE_INDICATOR = '^' + + +def err_msg_formatter(msg): + """Return an error message with error prefix.""" + + return f'{ERROR_MSG_PREFIX}{msg}' + + +def err_ctx_formatter(ctx): + """ + Return a two-line string with a source in the first string + and a sign in the second one wich indicate + a place where an error occured. + """ + + source = ctx.source + pos = ctx.pos + + return '{}\n{}{}'.format(source, ' ' * (pos), ERROR_PLACE_INDICATOR) From 6e2369710afefdc7ea4ba054ce39749533a65ba4 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 13:41:00 +0300 Subject: [PATCH 099/144] Remove an irrelevant todo comment --- final_task/pycalc/calculator/importer.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/final_task/pycalc/calculator/importer.py b/final_task/pycalc/calculator/importer.py index de570c2d..1894bfdc 100644 --- a/final_task/pycalc/calculator/importer.py +++ b/final_task/pycalc/calculator/importer.py @@ -3,8 +3,6 @@ to module member’s objects of specified types. """ -# TODO: handle exception when module importing fails - from functools import partial from types import BuiltinFunctionType, FunctionType, LambdaType From bc6bbaca4c2a7b033bf301112c002d769cfd2e4a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 13:53:30 +0300 Subject: [PATCH 100/144] Update docs --- final_task/pycalc/calculator/calculator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index 286261d8..aa7048e0 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -1,5 +1,5 @@ """ -Initialization of a calculator. Returns a parser object. +Initialization of a calculator. Returns a calculator instance. """ from pycalc.lexer import Lexer From c25ca31ae28bd7dfc334e0362d4c57d2a286c0ce Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 13:58:48 +0300 Subject: [PATCH 101/144] Implement the calculator class --- final_task/pycalc/calculator/calculator.py | 76 +++++++++++++++++++++- 1 file changed, 73 insertions(+), 3 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index aa7048e0..31d7c16b 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -3,13 +3,79 @@ """ from pycalc.lexer import Lexer -from pycalc.parser import Parser +from pycalc.parser import Parser, ParserGenericError +from pycalc.token.precedence import Precedence +from .formatters import err_msg_formatter, err_ctx_formatter from .importer import build_modules_registry from .matchers import build_matchers +from .messages import EMPTY_EXPRESSION_PROVIDED, SYNTAX_ERROR from .specification import build_specification +class Calculator: + """ + The calculator class. + + Provide a method to calculate an expression from a string. + """ + + def __init__(self, parser): + self._parser = parser + + def calculate(self, expression): + """ + Calculate an expression. + + Return result of a calculation or an error message + if the calculation fails. + """ + + # empty expression + if not expression: + return err_msg_formatter(EMPTY_EXPRESSION_PROVIDED) + + # calculate an expression + try: + result = self._parser.parse(expression) + return result + + # handle calculation errors + except ParserGenericError as exc: + + # ’unwrap’ an original exception if that one has a stored context ?? + + # print('wrapper :', type(exc)) + # print('original:', type(exc.__cause__)) + + exc = exc.__cause__ if hasattr(exc.__cause__, 'ctx') else exc + ctx = exc.ctx + + # print(exc) + # print(ctx) + + # print(exc.__cause__.ctx) + # print(ctx) + # print(exc.__cause__.ctx) + + err_msg = err_msg_formatter(f'{SYNTAX_ERROR}') + ctx_msg = err_ctx_formatter(ctx) + # print(type(exc), exc, exc.ctx) + # print(exc.__cause__) + + return f'{err_msg}\n{ctx_msg}' + + # except (ArithmeticError, ZeroDivisionError) as exc: + # print(type(exc), exc) + # err_msg = err_msg_formatter(exc) + # return err_msg + + except Exception as exc: + print(type(exc), exc) + print(f'CALCULATOR: exception: {exc}') + return 'Calculation failed.' + + def calculator(modules_names=None): """Initialize of a calculator and return a parser object.""" @@ -26,9 +92,13 @@ def calculator(modules_names=None): spec = build_specification(modules_registry) # create a parser - parser = Parser(spec, lexer) + power = Precedence.DEFAULT + parser = Parser(spec, lexer, power) + + # create a calculator + calculator_ = Calculator(parser) - return parser + return calculator_ # TODO: remove From e1b568163cc54f476ea52cb049b2560e06f7cbe6 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 15:17:41 +0300 Subject: [PATCH 102/144] Update calculators asserts --- final_task/pycalc/calculator/calculator.py | 48 +++++++++++++++++----- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index 31d7c16b..6c037e81 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -115,7 +115,7 @@ def wrap(source): return wrap p = calculator() - p.parse = logger(p.parse) + p.parse = logger(p.calculate) assert p.parse('sin(2)') == math.sin(2) assert p.parse('sin(2-3)') == math.sin(2 - 3) @@ -141,12 +141,40 @@ def wrap(source): assert p.parse('1 >= 1') is True assert p.parse('1 - 2 >= -1') is True assert p.parse('log(1025 - 1, 7 - 5)') == 10 - - # assert p.parse('1 / 0') - # assert p.parse('sin(1,2)') - # assert p.parse(', 1') - # assert p.parse('1 , 2') - # assert p.parse(') 2 ') == 15 - # assert p.parse('0 1') - # assert p.parse('- - - 2 ^ log ( 1 , ( 4 - 1 ) * 5 , 4 )') == -1048576 - # TODO: + assert p.parse('1 + tau') == 1 + math.tau + assert p.parse('1 + inf') == math.inf + assert p.parse('1 - inf') == -math.inf + assert p.parse('-inf + 1') == -math.inf + assert str(p.parse('-inf + inf')) == str(math.nan) + + assert p.parse('1 / (1-1) + 1') # ZeroDivisionError + assert p.parse('100^100^100^100') # OverflowError + assert p.parse('sin(1,2)') + assert p.parse(', 1') # from nud + assert p.parse(') 2 ') # from nud + assert p.parse('1 , 2') # not parsed completely + assert p.parse('(2') # expected + assert p.parse('sin2') # expected + assert p.parse('0 1') + assert p.parse('a') # expected token in expr begin + + # asserts from pycalc_checker.py error cases + p.parse('+') + p.parse('1-') + p.parse('1 2') + p.parse('==7') + p.parse('1+2(3*4))') + p.parse('((1+2)') + p.parse('1+1 2 3 4 5 6') + p.parse('log100(100)') + p.parse('------') + p.parse('5> =6') + p.parse('5/ /6') + p.parse('6<=6') + p.parse('6* *6') + p.parse('(((((') + p.parse('abs') + p.parse('pow(2, 3, 4)') + + p.parse('1 / 0') + p.parse('10 ^ 10 ^ 10 ^ 10') From d405e51017bf027db9972c44ca6c35df34306a8e Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 19:46:25 +0300 Subject: [PATCH 103/144] Split parse method of the parse class --- final_task/pycalc/parser/parser.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index fa151d9c..f453fba9 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -26,10 +26,21 @@ def parse(self, source): self.lexer.init(source) try: - result = self.expression() + result = self._parse() except Exception as exc: raise ParserGenericError(self.context()) from exc + return result + + def _parse(self): + """The inner parsing function. + + Splitted from the main parsing function to allow + catching all parsing error exceptions in one place. + """ + + result = self.expression() + if not self.lexer.is_source_exhausted(): raise ParserSourceNotExhausted(self.context()) From 9199aa771508d5f0b78791e116d076dffac29de4 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 11:25:45 +0300 Subject: [PATCH 104/144] Add an exception for unregistered parselets --- final_task/pycalc/specification/errors.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/final_task/pycalc/specification/errors.py b/final_task/pycalc/specification/errors.py index a9b8b5ac..cdc24bb3 100644 --- a/final_task/pycalc/specification/errors.py +++ b/final_task/pycalc/specification/errors.py @@ -17,6 +17,14 @@ def __init__(self, token_type): self.token_type = token_type +class ParseletNotRegistered(Exception): + """Raise when there is no registered parselet for a given token type.""" + + def __init__(self, token_type): + super().__init__() + self.token_type = token_type + + class DenotationError(SyntaxError): """The generic exception class for denotation errors.""" From e77a0ae5116397b78f72eb7ee051b73c20c3cb1b Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 11:28:27 +0300 Subject: [PATCH 105/144] Update docs --- final_task/pycalc/specification/__init__.py | 3 ++- final_task/pycalc/specification/denotation.py | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/final_task/pycalc/specification/__init__.py b/final_task/pycalc/specification/__init__.py index 185dad80..6d70f385 100644 --- a/final_task/pycalc/specification/__init__.py +++ b/final_task/pycalc/specification/__init__.py @@ -1,5 +1,6 @@ """ -Specification for a Pratt parser. +Specification for top down operator +precedence parsing (Pratt parser). """ from .specification import Specification diff --git a/final_task/pycalc/specification/denotation.py b/final_task/pycalc/specification/denotation.py index 5dc0480c..5581ffd1 100644 --- a/final_task/pycalc/specification/denotation.py +++ b/final_task/pycalc/specification/denotation.py @@ -1,5 +1,5 @@ """ -Denotation. +Provides the base class for nud- and led-denotation classes. """ from .errors import DuplicatedTokenType @@ -14,7 +14,7 @@ def __init__(self): self.registry = {} def register(self, token_type, parselet, **kwargs): - """Register token type with an appropriate parselet.""" + """Register a parselet for a given token type.""" self._check_for_dup(token_type) self.registry[token_type] = parselet(**kwargs) @@ -27,7 +27,7 @@ def power(self, token): return power def _get_parselet(self, token): - """Find and return appropriate stored parselet for a given token type.""" + """Find and return a parselet for a given token type.""" token_type = token.token_type parselet = self.registry[token_type] From e5e1707e010f7289fcb0f0ce4ae876f9e3d39e03 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 12:07:41 +0300 Subject: [PATCH 106/144] Raise an custom exception on parselet finding error --- final_task/pycalc/specification/denotation.py | 7 +++++-- final_task/pycalc/specification/nud.py | 4 ++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/final_task/pycalc/specification/denotation.py b/final_task/pycalc/specification/denotation.py index 5581ffd1..83f1565c 100644 --- a/final_task/pycalc/specification/denotation.py +++ b/final_task/pycalc/specification/denotation.py @@ -2,7 +2,7 @@ Provides the base class for nud- and led-denotation classes. """ -from .errors import DuplicatedTokenType +from .errors import DuplicatedTokenType, ParseletNotRegistered class Denotation: @@ -30,7 +30,10 @@ def _get_parselet(self, token): """Find and return a parselet for a given token type.""" token_type = token.token_type - parselet = self.registry[token_type] + try: + parselet = self.registry[token_type] + except KeyError: + raise ParseletNotRegistered(token_type) return parselet diff --git a/final_task/pycalc/specification/nud.py b/final_task/pycalc/specification/nud.py index 214fe315..c18b6493 100644 --- a/final_task/pycalc/specification/nud.py +++ b/final_task/pycalc/specification/nud.py @@ -3,7 +3,7 @@ """ from .denotation import Denotation -from .errors import NudDenotationError +from .errors import NudDenotationError, ParseletNotRegistered class Nud(Denotation): @@ -18,7 +18,7 @@ def eval(self, parser, token): try: parselet = self._get_parselet(token) - except KeyError: + except ParseletNotRegistered: ctx = parser.context(previous=True) raise NudDenotationError(ctx, token) From 7a2d4532c614fea8c816b4b898ebb0ec3a45bb3c Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 12:08:13 +0300 Subject: [PATCH 107/144] Refactor the led denotation class --- final_task/pycalc/specification/denotation.py | 7 ----- final_task/pycalc/specification/led.py | 28 ++++++++++++++----- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/final_task/pycalc/specification/denotation.py b/final_task/pycalc/specification/denotation.py index 83f1565c..0d2d30bf 100644 --- a/final_task/pycalc/specification/denotation.py +++ b/final_task/pycalc/specification/denotation.py @@ -19,13 +19,6 @@ def register(self, token_type, parselet, **kwargs): self._check_for_dup(token_type) self.registry[token_type] = parselet(**kwargs) - def power(self, token): - """Return power for a given token.""" - - power = self._get_parselet(token).power - - return power - def _get_parselet(self, token): """Find and return a parselet for a given token type.""" diff --git a/final_task/pycalc/specification/led.py b/final_task/pycalc/specification/led.py index f3af23da..8772b603 100644 --- a/final_task/pycalc/specification/led.py +++ b/final_task/pycalc/specification/led.py @@ -3,7 +3,7 @@ """ from .denotation import Denotation -from .errors import LedDenotationError +from .errors import LedDenotationError, ParseletNotRegistered class Led(Denotation): @@ -13,15 +13,29 @@ class Led(Denotation): The specification of how an operator consumes to the right with a left-context. """ + def power(self, parser, token): + """Return power for a given token.""" + + parselet = self._parselet(parser, token) + power = parselet.power + + return power + def eval(self, parser, token, left): """Receive from left, evaluate and return result.""" - try: - parselet = self._get_parselet(token) - except KeyError: - ctx = parser.context(previous=True) - raise LedDenotationError(ctx, token) - + parselet = self._parselet(parser, token) result = parselet.led(parser, token, left) return result + + def _parselet(self, parser, token): + """Find and return a stored parselet for a given token type.""" + + try: + parselet = super()._get_parselet(token) + except ParseletNotRegistered: + ctx = parser.context() + raise LedDenotationError(ctx, token) + + return parselet From 88bb1a33a19dae6db701683b84297ca9d47deef3 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 12:12:50 +0300 Subject: [PATCH 108/144] Add the parser syntax error class --- final_task/pycalc/parser/__init__.py | 1 + final_task/pycalc/parser/errors.py | 10 +++++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/parser/__init__.py b/final_task/pycalc/parser/__init__.py index 467abe2e..8c351fd5 100644 --- a/final_task/pycalc/parser/__init__.py +++ b/final_task/pycalc/parser/__init__.py @@ -8,6 +8,7 @@ from .parser import Parser from .errors import ( ParserGenericError, + ParserSyntaxError, ParserNoTokenReceived, ParserExpectedTokenAbsent, ParserSourceNotExhausted diff --git a/final_task/pycalc/parser/errors.py b/final_task/pycalc/parser/errors.py index d65766d3..c6385d7d 100644 --- a/final_task/pycalc/parser/errors.py +++ b/final_task/pycalc/parser/errors.py @@ -1,8 +1,9 @@ """ -Exceptions for the parser module. +Exceptions for the parser package. """ GENERIC_PARSER_ERROR = 'encountered an error while parsing' +GENERIC_PARSER_SYNTAX_ERROR = 'syntax error' class ParserGenericError(SyntaxError): @@ -16,6 +17,13 @@ def __str__(self): return GENERIC_PARSER_ERROR +class ParserSyntaxError(ParserGenericError): + """A generic parser exception that represents a syntax error.""" + + def __str__(self): + return GENERIC_PARSER_SYNTAX_ERROR + + class ParserNoTokenReceived(ParserGenericError): """Raise when a lexer returns `None` instead of `Token` object.""" From ea176b5704ba94a33b18cde1e745c551fe196284 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Thu, 9 May 2019 12:14:25 +0300 Subject: [PATCH 109/144] Add docs --- final_task/pycalc/importer/__init__.py | 2 +- final_task/pycalc/importer/errors.py | 2 +- final_task/pycalc/importer/importer.py | 20 ++++++++++++-------- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/final_task/pycalc/importer/__init__.py b/final_task/pycalc/importer/__init__.py index 23567c37..b056eec8 100644 --- a/final_task/pycalc/importer/__init__.py +++ b/final_task/pycalc/importer/__init__.py @@ -1,5 +1,5 @@ """ -Importer package provides functions for +The importer package provides functions for modules importing and module members collecting. """ diff --git a/final_task/pycalc/importer/errors.py b/final_task/pycalc/importer/errors.py index 07d65a71..cc7ac360 100644 --- a/final_task/pycalc/importer/errors.py +++ b/final_task/pycalc/importer/errors.py @@ -4,7 +4,7 @@ class ModuleImportErrors(ModuleNotFoundError): - """""" + """Raise when at least one of module imports failed.""" def __init__(self, module_names): super().__init__() diff --git a/final_task/pycalc/importer/importer.py b/final_task/pycalc/importer/importer.py index 63b6125f..0883fcc9 100644 --- a/final_task/pycalc/importer/importer.py +++ b/final_task/pycalc/importer/importer.py @@ -1,8 +1,7 @@ """ +Functions for modules importing and module members collecting. """ -# TODO: handle exception when module importing fails - from collections import OrderedDict from importlib import import_module from inspect import getmembers @@ -15,15 +14,17 @@ def iter_uniq(iterables): - """ - Returns a generator that iterates over unique elements of iterables. - """ + """Returns a generator that iterates over unique elements of iterables.""" return (key for key in OrderedDict.fromkeys(chain(*iterables))) def import_modules(*iterables): - """""" + """ + Return a list of imported modules. + + Raise an `ModuleImportErrors` exception if at least one of imports fails. + """ modules = [] failed_imports = [] @@ -43,7 +44,10 @@ def import_modules(*iterables): def module_members_by_type(module, type_checker, skip_underscored=True): - """""" + """ + Create a generator over tuples of names and + members of a certain type in a module. + """ for name, member in getmembers(module, type_checker): if skip_underscored and name.startswith(UNDERSCORE): @@ -52,7 +56,7 @@ def module_members_by_type(module, type_checker, skip_underscored=True): def collect_members_by_type(modules, type_checker, skip_underscored=True, predefined=None): - """""" + """Collect members of modules by types into an dictionary.""" accumulator = dict(predefined) if predefined else {} From 3606210983a2b04f7251ee83009395b0c85911b3 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 12:26:51 +0300 Subject: [PATCH 110/144] Add mapping exceptions to error messages --- final_task/pycalc/calculator/errors.py | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 final_task/pycalc/calculator/errors.py diff --git a/final_task/pycalc/calculator/errors.py b/final_task/pycalc/calculator/errors.py new file mode 100644 index 00000000..1e1f2ea2 --- /dev/null +++ b/final_task/pycalc/calculator/errors.py @@ -0,0 +1,37 @@ +""" +Provides functions to compose an error message +according to an exception type. +""" + +from pycalc.parser import errors as parser_err +from pycalc.specification import errors as spec_err +from pycalc.token.tokens import errors as token_err + +from .messages import SYNTAX_ERROR, CANT_PARSE_EXPRESSION + +SYNTAX_ERROR_EXCEPIONS = ( + parser_err.ParserNoTokenReceived, + parser_err.ParserExpectedTokenAbsent, + parser_err.ParserSourceNotExhausted, + spec_err.NudDenotationError, + spec_err.LedDenotationError +) + + +def get_err_msg(exc): + """Return an error message according to an exception type.""" + + # for function calls and operator applying errors + if isinstance(exc, token_err.CallError): + # get the error message of the original exception + err_msg = str(exc.__cause__) + + # for exceptions that mean a syntax error + elif isinstance(exc, SYNTAX_ERROR_EXCEPIONS): + err_msg = SYNTAX_ERROR + + # for all others exception + else: + err_msg = CANT_PARSE_EXPRESSION + + return err_msg From ea181fbe75ae3313a33a8c947a962d63590d7cc6 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 12:27:07 +0300 Subject: [PATCH 111/144] Update messages constants --- final_task/pycalc/calculator/messages.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/calculator/messages.py b/final_task/pycalc/calculator/messages.py index 3359117a..6a9ae63b 100644 --- a/final_task/pycalc/calculator/messages.py +++ b/final_task/pycalc/calculator/messages.py @@ -3,6 +3,8 @@ """ CALCULATOR_INITIALIZATION_ERROR = 'calculator initialization error' -ERROR_MSG_PREFIX = 'ERROR: ' +CANT_PARSE_EXPRESSION = 'can’t parse this expression' EMPTY_EXPRESSION_PROVIDED = 'empty expression provided' +ERROR_MSG_PREFIX = 'ERROR: ' +MODULES_IMPORT_ERROR = 'no module(s) named' SYNTAX_ERROR = 'syntax error' From 14ea74ef5aa32fee8faff1a474c76aaf71a5eaad Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 12:28:18 +0300 Subject: [PATCH 112/144] Update docs --- final_task/pycalc/calculator/calculator.py | 2 +- final_task/pycalc/calculator/formatters.py | 6 +++--- final_task/pycalc/calculator/importer.py | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index 6c037e81..78688467 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -77,7 +77,7 @@ def calculate(self, expression): def calculator(modules_names=None): - """Initialize of a calculator and return a parser object.""" + """Initialize a calculator and return a parser object.""" # import constants and functions from default and requested modules modules_registry = build_modules_registry(modules_names) diff --git a/final_task/pycalc/calculator/formatters.py b/final_task/pycalc/calculator/formatters.py index 635fa94a..a8798afa 100644 --- a/final_task/pycalc/calculator/formatters.py +++ b/final_task/pycalc/calculator/formatters.py @@ -9,15 +9,15 @@ def err_msg_formatter(msg): - """Return an error message with error prefix.""" + """Return an error message with an error prefix.""" return f'{ERROR_MSG_PREFIX}{msg}' def err_ctx_formatter(ctx): """ - Return a two-line string with a source in the first string - and a sign in the second one wich indicate + Return a two-line string with a source in the first line + and a sign in the second one which indicate a place where an error occured. """ diff --git a/final_task/pycalc/calculator/importer.py b/final_task/pycalc/calculator/importer.py index 1894bfdc..82b111b0 100644 --- a/final_task/pycalc/calculator/importer.py +++ b/final_task/pycalc/calculator/importer.py @@ -30,7 +30,10 @@ def is_function(obj) -> bool: def build_modules_registry(modules_names): - """""" + """ + Collect maps of module member’s names + to module member’s objects of specified types. + """ if not modules_names: modules_names = tuple() From e3ca8f209949388bbaea15beb6128f7a385c645f Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 12:30:12 +0300 Subject: [PATCH 113/144] Add handling of calculation errors --- final_task/pycalc/calculator/calculator.py | 44 +++++++++------------- 1 file changed, 18 insertions(+), 26 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index 78688467..8953057e 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -7,9 +7,13 @@ from pycalc.token.precedence import Precedence from .formatters import err_msg_formatter, err_ctx_formatter +from .errors import get_err_msg from .importer import build_modules_registry from .matchers import build_matchers -from .messages import EMPTY_EXPRESSION_PROVIDED, SYNTAX_ERROR +from .messages import ( + CANT_PARSE_EXPRESSION, + EMPTY_EXPRESSION_PROVIDED, +) from .specification import build_specification @@ -41,39 +45,27 @@ def calculate(self, expression): return result # handle calculation errors - except ParserGenericError as exc: + except ParserGenericError as exc_wrapper: - # ’unwrap’ an original exception if that one has a stored context ?? + # ’unwrap’ an original exception and get context + exc = exc_wrapper.__cause__ + ctx = exc.ctx if hasattr(exc, 'ctx') else exc_wrapper.ctx - # print('wrapper :', type(exc)) - # print('original:', type(exc.__cause__)) + # an error message + err_msg = get_err_msg(exc) + err_msg = err_msg_formatter(err_msg) - exc = exc.__cause__ if hasattr(exc.__cause__, 'ctx') else exc - ctx = exc.ctx - - # print(exc) - # print(ctx) - - # print(exc.__cause__.ctx) - # print(ctx) - # print(exc.__cause__.ctx) - - err_msg = err_msg_formatter(f'{SYNTAX_ERROR}') + # an context message ctx_msg = err_ctx_formatter(ctx) - # print(type(exc), exc, exc.ctx) - # print(exc.__cause__) return f'{err_msg}\n{ctx_msg}' - # except (ArithmeticError, ZeroDivisionError) as exc: - # print(type(exc), exc) - # err_msg = err_msg_formatter(exc) - # return err_msg - + # probably not reacheable code but better save than sorry except Exception as exc: - print(type(exc), exc) - print(f'CALCULATOR: exception: {exc}') - return 'Calculation failed.' + + err_msg = err_msg_formatter(CANT_PARSE_EXPRESSION) + + return err_msg def calculator(modules_names=None): From 906643f6247b02d859b1fa7625ef7c0c7b873c8b Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 12:33:51 +0300 Subject: [PATCH 114/144] Implement cli --- final_task/pycalc/cli.py | 42 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 final_task/pycalc/cli.py diff --git a/final_task/pycalc/cli.py b/final_task/pycalc/cli.py new file mode 100644 index 00000000..e07f9474 --- /dev/null +++ b/final_task/pycalc/cli.py @@ -0,0 +1,42 @@ +""" +Initialize a calculator and calculate +an expression from a command line argument. +""" + +import sys + +from pycalc.args import args +from pycalc.calculator import calculator +from pycalc.calculator.formatters import err_msg_formatter +from pycalc.calculator.messages import ( + CALCULATOR_INITIALIZATION_ERROR, + MODULES_IMPORT_ERROR +) +from pycalc.importer.errors import ModuleImportErrors + + +def main(): + """ + Initialize a calculator and calculate + an expression from a command line argument. + """ + + # initialize a calculator + try: + calc = calculator(args.modules) + + except ModuleImportErrors as exc: + modules_names = ', '.join(exc.modules_names) + err_msg = f'{MODULES_IMPORT_ERROR} {modules_names}' + sys.exit(err_msg_formatter(err_msg)) + + except Exception: + sys.exit(err_msg_formatter(CALCULATOR_INITIALIZATION_ERROR)) + + # make a calculation and print a result + result = calc.calculate(args.expression) + print(result) + + +if __name__ == "__main__": + main() From af92b44a68e4b7b390be5decf8d1817227e0636e Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 12:14:39 +0300 Subject: [PATCH 115/144] Refactor handling of power in the parser class --- final_task/pycalc/parser/parser.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index f453fba9..fc7434bd 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -46,7 +46,7 @@ def _parse(self): return result - def expression(self, right_power=None): + def expression(self, power=None): """The main parsing function of Pratt parser.""" token = self.consume() @@ -60,8 +60,7 @@ def expression(self, right_power=None): if not token: break - right_power = right_power if right_power is not None else self.default_power - if right_power >= self._left_power(token): + if self._right_power(power) >= self._left_power(token): break self.consume() @@ -120,7 +119,15 @@ def _led(self, token, left): return self.spec.led.eval(self, token, left) + def _right_power(self, power): + """Return token binding power.""" + + if power is not None: + return power + + return self.default_power + def _left_power(self, token): """Get token binding power.""" - return self.spec.led.power(token) + return self.spec.led.power(self, token) From ce12efb3ee93a4c87ac1479b353019130bb174fd Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 15:05:22 +0300 Subject: [PATCH 116/144] Update docs --- final_task/pycalc/calculator/__init__.py | 2 +- final_task/pycalc/calculator/specification.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/final_task/pycalc/calculator/__init__.py b/final_task/pycalc/calculator/__init__.py index 19d4497d..b3cdab98 100644 --- a/final_task/pycalc/calculator/__init__.py +++ b/final_task/pycalc/calculator/__init__.py @@ -1,5 +1,5 @@ """ -Calculator. +The calculator package. """ from .calculator import calculator diff --git a/final_task/pycalc/calculator/specification.py b/final_task/pycalc/calculator/specification.py index 1e9ed3dd..90ae5624 100644 --- a/final_task/pycalc/calculator/specification.py +++ b/final_task/pycalc/calculator/specification.py @@ -15,6 +15,7 @@ def build_specification(registry): + """Initialize a parser specification.""" spec = Specification() From 3a90591e801586a9639bc2dfcb426e6afa4e959b Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 15:11:21 +0300 Subject: [PATCH 117/144] Implement parselets for calculator --- .../pycalc/calculator/parselets/__init__.py | 27 ++++++++ .../pycalc/calculator/parselets/base.py | 13 ++++ .../pycalc/calculator/parselets/constant.py | 31 +++++++++ .../pycalc/calculator/parselets/errors.py | 11 +++ .../pycalc/calculator/parselets/function.py | 56 +++++++++++++++ .../pycalc/calculator/parselets/group.py | 30 ++++++++ .../pycalc/calculator/parselets/helpers.py | 16 +++++ .../pycalc/calculator/parselets/number.py | 19 +++++ .../pycalc/calculator/parselets/operators.py | 69 +++++++++++++++++++ .../calculator/parselets/punctuation.py | 14 ++++ 10 files changed, 286 insertions(+) create mode 100644 final_task/pycalc/calculator/parselets/__init__.py create mode 100644 final_task/pycalc/calculator/parselets/base.py create mode 100644 final_task/pycalc/calculator/parselets/constant.py create mode 100644 final_task/pycalc/calculator/parselets/errors.py create mode 100644 final_task/pycalc/calculator/parselets/function.py create mode 100644 final_task/pycalc/calculator/parselets/group.py create mode 100644 final_task/pycalc/calculator/parselets/helpers.py create mode 100644 final_task/pycalc/calculator/parselets/number.py create mode 100644 final_task/pycalc/calculator/parselets/operators.py create mode 100644 final_task/pycalc/calculator/parselets/punctuation.py diff --git a/final_task/pycalc/calculator/parselets/__init__.py b/final_task/pycalc/calculator/parselets/__init__.py new file mode 100644 index 00000000..bb8e7bec --- /dev/null +++ b/final_task/pycalc/calculator/parselets/__init__.py @@ -0,0 +1,27 @@ +""" +Parselet types. +""" + +from .constant import Constant +from .function import Function +from .number import Number +from .operators import (UnaryPrefix, + UnaryPostfix, + BinaryInfixLeft, + BinaryInfixRight) +from .group import GroupedExpressionStart, GroupedExpressionEnd +from .punctuation import Comma + + +__all__ = [ + 'BinaryInfixLeft', + 'BinaryInfixRight', + 'Comma', + 'Constant', + 'Function', + 'GroupedExpressionEnd', + 'GroupedExpressionStart', + 'Number', + 'UnaryPostfix', + 'UnaryPrefix', +] diff --git a/final_task/pycalc/calculator/parselets/base.py b/final_task/pycalc/calculator/parselets/base.py new file mode 100644 index 00000000..39b49a8a --- /dev/null +++ b/final_task/pycalc/calculator/parselets/base.py @@ -0,0 +1,13 @@ +""" +The generic parselet class. +""" + + +class Base(): + """The generic parselet class.""" + + def __init__(self, power): + self.power = power + + def __repr__(self): + return self.__class__.__name__ diff --git a/final_task/pycalc/calculator/parselets/constant.py b/final_task/pycalc/calculator/parselets/constant.py new file mode 100644 index 00000000..81a43d22 --- /dev/null +++ b/final_task/pycalc/calculator/parselets/constant.py @@ -0,0 +1,31 @@ +""" +The parselet class for constants. +""" + +from .base import Base + + +class Constant(Base): + """Parselet parselet to handle constants.""" + + def __init__(self, power, const_registry): + super().__init__(power) + self.const_registry = const_registry + + def nud(self, parser, token): + """""" + + const = self.const(token) + + return const + + def const(self, token): + """""" + + const_name = token.lexeme + try: + const = self.const_registry[const_name] + except KeyError: + raise Exception(f"$'{const_name}' constant is not registered") + + return const diff --git a/final_task/pycalc/calculator/parselets/errors.py b/final_task/pycalc/calculator/parselets/errors.py new file mode 100644 index 00000000..fda77c57 --- /dev/null +++ b/final_task/pycalc/calculator/parselets/errors.py @@ -0,0 +1,11 @@ +""" +The exception class for function calling. +""" + + +class CallError(Exception): + """An exception for function calling.""" + + def __init__(self, ctx): + super().__init__() + self.ctx = ctx diff --git a/final_task/pycalc/calculator/parselets/function.py b/final_task/pycalc/calculator/parselets/function.py new file mode 100644 index 00000000..c07fec25 --- /dev/null +++ b/final_task/pycalc/calculator/parselets/function.py @@ -0,0 +1,56 @@ +""" +The parselet class for functions. +""" + +from .base import Base +from .helpers import call + + +class Function(Base): + """The parselet class for functions.""" + + def __init__(self, power, func_registry, start, stop, sep): + super().__init__(power) + self.func_registry = func_registry + self.start = start + self.stop = stop + self.sep = sep + + def nud(self, parser, token): + """""" + + ctx = parser.context(previous=True) + + parser.advance(self.start) + args = self.args(parser) + parser.advance(self.stop) + + func = self.func(token) + result = call(ctx, func, args) + + return result + + def args(self, parser): + """Advance a parser and collect an arguments list.""" + + args = [] + + if not parser.peek_and_check(self.stop): + while True: + args.append(parser.expression()) + if not parser.peek_and_check(self.sep): + break + parser.advance(self.sep) + + return args + + def func(self, token): + """Get a function from the function registry by a function name.""" + + func_name = token.lexeme + try: + func = self.func_registry[func_name] + except KeyError: + raise Exception(f"$'{func_name}' function is not registered") + + return func diff --git a/final_task/pycalc/calculator/parselets/group.py b/final_task/pycalc/calculator/parselets/group.py new file mode 100644 index 00000000..39c47509 --- /dev/null +++ b/final_task/pycalc/calculator/parselets/group.py @@ -0,0 +1,30 @@ +""" +The parselet classes for grouped expressions. +""" + +from .base import Base + + +class GroupedExpressionStart(Base): + """A grouped expression start parselet.""" + + def __init__(self, power, right_pair): + super().__init__(power) + self.right_pair = right_pair + + def nud(self, parser, token): + """""" + + expr = parser.expression() + parser.advance(self.right_pair) + + return expr + + +class GroupedExpressionEnd(Base): + """A grouped expression end parselet.""" + + def led(self, parser, token, left): + """""" + + return parser.expression(self.power) diff --git a/final_task/pycalc/calculator/parselets/helpers.py b/final_task/pycalc/calculator/parselets/helpers.py new file mode 100644 index 00000000..0b71c44a --- /dev/null +++ b/final_task/pycalc/calculator/parselets/helpers.py @@ -0,0 +1,16 @@ +""" +Provides helper functions. +""" + +from .errors import CallError + + +def call(ctx, func, args): + """Call a function with given arguments.""" + + try: + result = func(*args) + except Exception as exc: + raise CallError(ctx) from exc + + return result diff --git a/final_task/pycalc/calculator/parselets/number.py b/final_task/pycalc/calculator/parselets/number.py new file mode 100644 index 00000000..fdcb3cee --- /dev/null +++ b/final_task/pycalc/calculator/parselets/number.py @@ -0,0 +1,19 @@ +""" +The parselet class for numbers. +""" + +from .base import Base + + +class Number(Base): + """The parselet class for numbers.""" + + def nud(self, parser, token): + """""" + + try: + value = int(token.lexeme) + except ValueError: + value = float(token.lexeme) + + return value diff --git a/final_task/pycalc/calculator/parselets/operators.py b/final_task/pycalc/calculator/parselets/operators.py new file mode 100644 index 00000000..4fabb86e --- /dev/null +++ b/final_task/pycalc/calculator/parselets/operators.py @@ -0,0 +1,69 @@ +""" +The parselet classes for operations. +""" + +from .base import Base +from .helpers import call + + +class Operator(Base): + """The generic parselet class for operations.""" + + def __init__(self, power, func): + super().__init__(power) + self.func = func + + +class UnaryPrefix(Operator): + """The parselet class for unary prefix operations.""" + + def nud(self, parser, token): + """""" + + ctx = parser.context(previous=True) + + right = parser.expression(self.power) + result = call(ctx, self.func, [right]) + + return result + + +class UnaryPostfix(Operator): + """The parselet class for unary postfix operations.""" + + def led(self, parser, token, left): + """""" + + ctx = parser.context(previous=True) + + result = call(ctx, self.func, [left]) + + return result + + +class BinaryInfixLeft(Operator): + """The parselet class for binary prefix operations.""" + + def led(self, parser, token, left): + """""" + + ctx = parser.context(previous=True) + + right = parser.expression(self.power) + result = call(ctx, self.func, [left, right]) + + return result + + +class BinaryInfixRight(Operator): + """The parselet class for binary infix operations.""" + + def led(self, parser, token, left): + """""" + + ctx = parser.context(previous=True) + + right = parser.expression(self.power - 1) + result = call(ctx, self.func, [left, right]) + + return result diff --git a/final_task/pycalc/calculator/parselets/punctuation.py b/final_task/pycalc/calculator/parselets/punctuation.py new file mode 100644 index 00000000..6db880ca --- /dev/null +++ b/final_task/pycalc/calculator/parselets/punctuation.py @@ -0,0 +1,14 @@ +""" +The punctuation parselets. +""" + +from .base import Base + + +class Comma(Base): + """The parselet class for comma.""" + + def nud(self, parser, token): + """""" + + return parser.expression(self.power) From 2f96036c7377437ce7334a7db53b5ba5946026c9 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 15:13:29 +0300 Subject: [PATCH 118/144] Add operator precedences --- final_task/pycalc/calculator/precedence.py | 75 ++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 final_task/pycalc/calculator/precedence.py diff --git a/final_task/pycalc/calculator/precedence.py b/final_task/pycalc/calculator/precedence.py new file mode 100644 index 00000000..04af20eb --- /dev/null +++ b/final_task/pycalc/calculator/precedence.py @@ -0,0 +1,75 @@ +""" +The operator precedence. +""" + +from enum import IntEnum + + +class Precedence(IntEnum): + """ + The operator precedence according to the operator precedence in Python. + + https://docs.python.org/3/reference/expressions.html#operator-precedence + """ + DEFAULT = 0 + + # Lambda expression + LAMBDA = 10 # lambda + # Conditional expression + CONDITIONAL_EXPRESSION = 20 # if – else + # Boolean OR + BOOLEAN_OR = 30 # or + # Boolean AND + BOOLEAN_AND = 40 # and + # Boolean NOT + BOOLEAN_NOT = 50 # not x + + # Comparisons, including membership tests and identity tests + COMPARISONS = 60 # <, <= , > , >= , != , == + MEMBERSHIP_TESTS = 60 # in, not in + IDENTITY_TESTS = 60 # is, is not + + # Bitwise XOR + BITWISE_XOR = 70 # ^ + # Bitwise OR + BITWISE_OR = 80 # | + # Bitwise AND + BITWISE_AND = 90 # & + # Shifts + SHIFTS = 100 # <<, >> + + # Addition and subtraction + ADDITION = 110 # + + SUBTRACTION = 110 # - + + # Multiplication, matrix multiplication, division, floor division, remainder + MULTIPLICATION = 120 # * + MATRIX_MULTIPLICATION = 120 # @ + DIVISION = 120 # / + FLOOR_DIVISION = 120 # // + REMAINDER = 120 # % + + # Positive, negative, bitwise NOT + POSITIVE = 130 # +x + NEGATIVE = 130 # -x + BITWISE_NOT = 130 # ~x + + # Exponentiation + EXPONENTIATION = 140 # ^ + # EXPONENTIATION = 140 # ** + + # Await expression + AWAIT = 150 # await x + + # Subscription, slicing, call, attribute reference + SUBSCRIPTION = 160 # x[index], + SLICING = 160 # x[index:index], + CALL = 160 # x(arguments...), + ATTRIBUTE_REFERENCE = 160 # x.attribute + + # Binding or tuple display, list display, dictionary display, set display + BINDING = 170 + TUPLE = 170 # (expressions...), + LIST = 170 # [expressions...], + DICTIONARY = 170 # {key: value...}, + SET = 170 # {expressions...} From ca593c3791b74d0ac143f82f8468b94b2ebde5de Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 15:16:01 +0300 Subject: [PATCH 119/144] Implement the tokens package --- .../pycalc/calculator/tokens/__init__.py | 3 ++ .../pycalc/calculator/tokens/lexemes.py | 53 +++++++++++++++++++ final_task/pycalc/calculator/tokens/types.py | 40 ++++++++++++++ 3 files changed, 96 insertions(+) create mode 100644 final_task/pycalc/calculator/tokens/__init__.py create mode 100644 final_task/pycalc/calculator/tokens/lexemes.py create mode 100644 final_task/pycalc/calculator/tokens/types.py diff --git a/final_task/pycalc/calculator/tokens/__init__.py b/final_task/pycalc/calculator/tokens/__init__.py new file mode 100644 index 00000000..f8b74f44 --- /dev/null +++ b/final_task/pycalc/calculator/tokens/__init__.py @@ -0,0 +1,3 @@ +""" +Tokens package. Provides token types and predefined lexemes. +""" diff --git a/final_task/pycalc/calculator/tokens/lexemes.py b/final_task/pycalc/calculator/tokens/lexemes.py new file mode 100644 index 00000000..535dcee6 --- /dev/null +++ b/final_task/pycalc/calculator/tokens/lexemes.py @@ -0,0 +1,53 @@ +""" +Lexemes. +""" + +from itertools import chain + +from .types import TokenType + + +PREDEFINED = { + # common operators + TokenType.ADD: '+', + TokenType.SUB: '-', + TokenType.MUL: '*', + TokenType.TRUEDIV: '/', + TokenType.FLOORDIV: '//', + TokenType.POW: '^', + TokenType.MOD: '%', + + # comparison operators + TokenType.EQ: '==', + TokenType.NE: '!=', + TokenType.LT: '<', + TokenType.LE: '<=', + TokenType.GT: '>', + TokenType.GE: '>=', + + # built-in functions + TokenType.ABS: 'abs', + TokenType.ROUND: 'round', + + # punctuation + TokenType.COMMA: ',', + TokenType.LEFT_PARENTHESIS: '(', + TokenType.RIGHT_PARENTHESIS: ')', +} + +# lexemes for this token types are created dynamically at runtime +DYNAMIC = ( + TokenType.NUMERIC, + TokenType.FUNCTION, + TokenType.CONSTANT, +) + +ALL = (PREDEFINED, DYNAMIC) + + +# Simple import time validations (TODO: not for production, to refactor) + +# all non-dynamically created token types should have lexemes +for token_type in TokenType: + if token_type not in chain(*ALL): + raise Exception(f'no lexeme is specified for {token_type}') diff --git a/final_task/pycalc/calculator/tokens/types.py b/final_task/pycalc/calculator/tokens/types.py new file mode 100644 index 00000000..61b4343d --- /dev/null +++ b/final_task/pycalc/calculator/tokens/types.py @@ -0,0 +1,40 @@ +""" +Token types specification. +""" + + +from enum import Enum, auto + + +class TokenType(Enum): + """Token types specification.""" + + NUMERIC = auto() # 0 1 2.3 4. .5, ... (TODO: 3.14e-10 3.14j ?) + CONSTANT = auto() # e, pi, ... + FUNCTION = auto() # abs, sin, ... + + # operators + ADD = auto() # + + SUB = auto() # - + MUL = auto() # * + TRUEDIV = auto() # / + FLOORDIV = auto() # // + POW = auto() # ^ + MOD = auto() # % + + # comparison operators + EQ = auto() # == + NE = auto() # != + LT = auto() # < + LE = auto() # <= + GT = auto() # > + GE = auto() # > + + # built-in functions + ABS = auto() # abs + ROUND = auto() # round + + # punctuation + COMMA = auto() # , + LEFT_PARENTHESIS = auto() # ( + RIGHT_PARENTHESIS = auto() # ) From aadd3af61c958de7b9b393331d742f340c6a03de Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 15:22:05 +0300 Subject: [PATCH 120/144] Fix imports --- final_task/pycalc/calculator/calculator.py | 2 +- final_task/pycalc/calculator/errors.py | 2 +- final_task/pycalc/calculator/matchers.py | 9 +++++---- final_task/pycalc/calculator/specification.py | 10 ++++------ 4 files changed, 11 insertions(+), 12 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index 8953057e..df7966b5 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -4,7 +4,6 @@ from pycalc.lexer import Lexer from pycalc.parser import Parser, ParserGenericError -from pycalc.token.precedence import Precedence from .formatters import err_msg_formatter, err_ctx_formatter from .errors import get_err_msg @@ -14,6 +13,7 @@ CANT_PARSE_EXPRESSION, EMPTY_EXPRESSION_PROVIDED, ) +from .precedence import Precedence from .specification import build_specification diff --git a/final_task/pycalc/calculator/errors.py b/final_task/pycalc/calculator/errors.py index 1e1f2ea2..cf073320 100644 --- a/final_task/pycalc/calculator/errors.py +++ b/final_task/pycalc/calculator/errors.py @@ -5,7 +5,7 @@ from pycalc.parser import errors as parser_err from pycalc.specification import errors as spec_err -from pycalc.token.tokens import errors as token_err +from .parselets import errors as token_err from .messages import SYNTAX_ERROR, CANT_PARSE_EXPRESSION diff --git a/final_task/pycalc/calculator/matchers.py b/final_task/pycalc/calculator/matchers.py index 9c589438..858efb2d 100644 --- a/final_task/pycalc/calculator/matchers.py +++ b/final_task/pycalc/calculator/matchers.py @@ -2,10 +2,11 @@ Create and register matchers for token types. """ -from pycalc.matcher.matcher import Matchers -from pycalc.matcher.number import NUMBER_REGEX -from pycalc.token.constants import TokenType -from pycalc.token.lexeme import PREDEFINED + +from pycalc.matcher import Matchers + +from .tokens.types import TokenType +from .tokens.lexemes import PREDEFINED def build_matchers(imports_registry): diff --git a/final_task/pycalc/calculator/specification.py b/final_task/pycalc/calculator/specification.py index 90ae5624..85ca0b20 100644 --- a/final_task/pycalc/calculator/specification.py +++ b/final_task/pycalc/calculator/specification.py @@ -2,16 +2,14 @@ Initialization of parser specification. """ +import operator from math import pow as math_pow from pycalc.specification import Specification -from pycalc.token.constants import TokenType -from pycalc.token.precedence import Precedence -from pycalc.token.tokens import * - -# TODO: operator build and Operator from token same name -import operator +from .parselets import * +from .precedence import Precedence +from .tokens.types import TokenType def build_specification(registry): From 2d01b3b175372a18accab5870efc85fddccfd92a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 15:26:13 +0300 Subject: [PATCH 121/144] Add number regex object --- final_task/pycalc/calculator/matchers.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/final_task/pycalc/calculator/matchers.py b/final_task/pycalc/calculator/matchers.py index 858efb2d..92dcc466 100644 --- a/final_task/pycalc/calculator/matchers.py +++ b/final_task/pycalc/calculator/matchers.py @@ -2,6 +2,7 @@ Create and register matchers for token types. """ +import re from pycalc.matcher import Matchers @@ -9,6 +10,20 @@ from .tokens.lexemes import PREDEFINED +NUMBER = r""" +( # integers or numbers with a fractional part: 13, 154., 3.44, ... +\d+ # an integer part: 10, 2, 432, ... +(\.\d*)* # a fractional part: .2, .43, .1245, ... or dot: . +) +| +( # numbers that begin with a dot: .12, .59, ... +\.\d+ # a fractional part: .2, .43, .1245, ... +) +""" + +NUMBER_REGEX = re.compile(NUMBER, re.VERBOSE) + + def build_matchers(imports_registry): """Create and register matchers for token types.""" From 1a30d9ce2532600835300f9f2f1f7f0a043698bc Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 6 May 2019 19:08:03 +0300 Subject: [PATCH 122/144] Add docs --- final_task/pycalc/matcher/__init__.py | 6 ++++++ final_task/pycalc/matcher/helpers.py | 19 +++++++++++++------ final_task/pycalc/matcher/matcher.py | 12 ++++++++---- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/final_task/pycalc/matcher/__init__.py b/final_task/pycalc/matcher/__init__.py index e69de29b..339ed1c4 100644 --- a/final_task/pycalc/matcher/__init__.py +++ b/final_task/pycalc/matcher/__init__.py @@ -0,0 +1,6 @@ +""" +Matcher packages provides a matchers container +with methods for creating a matcher from list of lexemes or regex. +""" + +from .matcher import Matchers diff --git a/final_task/pycalc/matcher/helpers.py b/final_task/pycalc/matcher/helpers.py index e001f6bc..42100751 100644 --- a/final_task/pycalc/matcher/helpers.py +++ b/final_task/pycalc/matcher/helpers.py @@ -1,23 +1,30 @@ -"""""" +""" +Helpers function for building matchers. +""" import re def list_sorted_by_length(iterable) -> list: - """""" + """ + Return a sorted list from iterable. + + Result list is sorted by length in reversed order. + """ return sorted(iterable, key=len, reverse=True) def construct_literals_list(literals): - """""" + """Return a list of literals sorted by length in reversed order.""" sorted_literals = list_sorted_by_length(literals) return sorted_literals def construct_regex(literals): - """Return regex string for... . + r""" + Return regex string for... . >>> construct_regex_string(['🏇', cos', 'arcsin', 'sin', pi()']) arcsin|pi\(\)|cos|sin|\🏇 @@ -32,7 +39,7 @@ def construct_regex(literals): def regex_matcher(regex): - """""" + """Return a regex matcher function.""" def matcher(string, pos): """""" @@ -44,7 +51,7 @@ def matcher(string, pos): def text_matcher(literals): - """""" + """Return a matcher function that matchs by iterating over a list of literals.""" def matcher(string, pos): """""" diff --git a/final_task/pycalc/matcher/matcher.py b/final_task/pycalc/matcher/matcher.py index 21b8b0b0..92de674b 100644 --- a/final_task/pycalc/matcher/matcher.py +++ b/final_task/pycalc/matcher/matcher.py @@ -1,4 +1,5 @@ """ +Matchers class. """ from collections import namedtuple @@ -10,7 +11,10 @@ class Matchers: - """""" + """ + Matchers is an iterable container for matchers with methods + for creating matchers from literals list or regex. + """ def __init__(self): self.matchers = [] @@ -20,18 +24,18 @@ def __iter__(self): yield matcher def register_matcher(self, token_type, matcher): - """""" + """Register a matcher with a corresponding token type.""" self.matchers.append(Matcher(token_type, matcher)) @staticmethod def create_matcher_from_regex(regex): - """""" + """Create a matcher from compiled regex object.""" return regex_matcher(regex) @staticmethod def create_matcher_from_literals_list(literals): - """""" + """Create a matcher from a list of literals""" return regex_matcher(construct_regex(literals)) From 1d8f530e52b5b907886f7e92568ce4ad36df05f1 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Tue, 7 May 2019 10:35:11 +0300 Subject: [PATCH 123/144] Refactor matcher package --- final_task/pycalc/matcher/__init__.py | 3 ++- final_task/pycalc/matcher/creator.py | 21 +++++++++++++++++++++ final_task/pycalc/matcher/helpers.py | 19 +++++-------------- final_task/pycalc/matcher/matcher.py | 15 ++------------- 4 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 final_task/pycalc/matcher/creator.py diff --git a/final_task/pycalc/matcher/__init__.py b/final_task/pycalc/matcher/__init__.py index 339ed1c4..718ce07e 100644 --- a/final_task/pycalc/matcher/__init__.py +++ b/final_task/pycalc/matcher/__init__.py @@ -1,6 +1,7 @@ """ Matcher packages provides a matchers container -with methods for creating a matcher from list of lexemes or regex. +with methods for creating matchers from list of +literals or a regex object. """ from .matcher import Matchers diff --git a/final_task/pycalc/matcher/creator.py b/final_task/pycalc/matcher/creator.py new file mode 100644 index 00000000..08803795 --- /dev/null +++ b/final_task/pycalc/matcher/creator.py @@ -0,0 +1,21 @@ +""" +Matcher creator class holds functions for matchers creation. +""" + +from .helpers import regex_matcher, construct_regex + + +class MatcherCreator: + """Matcher creator class holds functions for matchers creation.""" + + @staticmethod + def compiled_regex(regex): + """Create a matcher from compiled regex object.""" + + return regex_matcher(regex) + + @staticmethod + def literals_list(literals): + """Create a matcher from a list of literals.""" + + return regex_matcher(construct_regex(literals)) diff --git a/final_task/pycalc/matcher/helpers.py b/final_task/pycalc/matcher/helpers.py index 42100751..286c1d6b 100644 --- a/final_task/pycalc/matcher/helpers.py +++ b/final_task/pycalc/matcher/helpers.py @@ -42,22 +42,13 @@ def regex_matcher(regex): """Return a regex matcher function.""" def matcher(string, pos): - """""" + """ + Return a substring that match a string from + a given position or `None` if there are no matches. + """ + result = regex.match(string, pos) if result: return result.group() return matcher - - -def text_matcher(literals): - """Return a matcher function that matchs by iterating over a list of literals.""" - - def matcher(string, pos): - """""" - - for literal in literals: - if string.startswith(literal, pos): - return literal - - return matcher diff --git a/final_task/pycalc/matcher/matcher.py b/final_task/pycalc/matcher/matcher.py index 92de674b..e293b745 100644 --- a/final_task/pycalc/matcher/matcher.py +++ b/final_task/pycalc/matcher/matcher.py @@ -4,7 +4,7 @@ from collections import namedtuple -from .helpers import construct_regex, regex_matcher +from .creator import MatcherCreator Matcher = namedtuple("Matcher", ("token_type", "matcher")) @@ -18,6 +18,7 @@ class Matchers: def __init__(self): self.matchers = [] + self.create_from = MatcherCreator def __iter__(self): for matcher in self.matchers: @@ -27,15 +28,3 @@ def register_matcher(self, token_type, matcher): """Register a matcher with a corresponding token type.""" self.matchers.append(Matcher(token_type, matcher)) - - @staticmethod - def create_matcher_from_regex(regex): - """Create a matcher from compiled regex object.""" - - return regex_matcher(regex) - - @staticmethod - def create_matcher_from_literals_list(literals): - """Create a matcher from a list of literals""" - - return regex_matcher(construct_regex(literals)) From a43a92cff63041d13168d16521e3794602656292 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 15:30:17 +0300 Subject: [PATCH 124/144] Remove redundant number module --- final_task/pycalc/matcher/number.py | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 final_task/pycalc/matcher/number.py diff --git a/final_task/pycalc/matcher/number.py b/final_task/pycalc/matcher/number.py deleted file mode 100644 index f5752725..00000000 --- a/final_task/pycalc/matcher/number.py +++ /dev/null @@ -1,20 +0,0 @@ -"""""" - -import re - -from .helpers import regex_matcher - -NUMBER = r''' -( # integers or numbers with a fractional part: 13, 154., 3.44, ... -\d+ # an integer part: 10, 2, 432, ... -(\.\d*)* # a fractional part: .2, .43, .1245, ... or dot: . -) -| -( # numbers that begin with a dot: .12, .59, ... -\.\d+ # a fractional part: .2, .43, .1245, ... -) -''' - -NUMBER_REGEX = re.compile(NUMBER, re.VERBOSE) - -NUMBER_MATCHER = regex_matcher(NUMBER_REGEX) From 7f0bff6ceeef5be4aad115d4277638a044079a73 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 16:39:49 +0300 Subject: [PATCH 125/144] Rename a variable --- final_task/pycalc/calculator/errors.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/calculator/errors.py b/final_task/pycalc/calculator/errors.py index cf073320..790f5327 100644 --- a/final_task/pycalc/calculator/errors.py +++ b/final_task/pycalc/calculator/errors.py @@ -5,7 +5,7 @@ from pycalc.parser import errors as parser_err from pycalc.specification import errors as spec_err -from .parselets import errors as token_err +from .parselets import errors as parselet_err from .messages import SYNTAX_ERROR, CANT_PARSE_EXPRESSION @@ -22,7 +22,7 @@ def get_err_msg(exc): """Return an error message according to an exception type.""" # for function calls and operator applying errors - if isinstance(exc, token_err.CallError): + if isinstance(exc, parselet_err.CallError): # get the error message of the original exception err_msg = str(exc.__cause__) From ab0ca024f47225ed51b9bf0f89963ad6b118e4d8 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 17:09:40 +0300 Subject: [PATCH 126/144] Add top-level init and main files --- final_task/pycalc/__init__.py | 6 ++++++ final_task/pycalc/__main__.py | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 final_task/pycalc/__init__.py create mode 100644 final_task/pycalc/__main__.py diff --git a/final_task/pycalc/__init__.py b/final_task/pycalc/__init__.py new file mode 100644 index 00000000..650d27d8 --- /dev/null +++ b/final_task/pycalc/__init__.py @@ -0,0 +1,6 @@ +""" +Pure-python implementation of a command-line calculator. + +Receives mathematical expression string as an argument +and prints evaluated result. +""" diff --git a/final_task/pycalc/__main__.py b/final_task/pycalc/__main__.py new file mode 100644 index 00000000..d5ae7248 --- /dev/null +++ b/final_task/pycalc/__main__.py @@ -0,0 +1,18 @@ +""" +Pure-python implementation of a command-line calculator. + +Receives mathematical expression string as an argument +and prints evaluated result. +""" + +from pycalc import cli + + +def main(): + """Pure-python implementation of a command-line calculator.""" + + cli.main() + + +if __name__ == "__main__": + main() From 29715f3dfd4ecbbd478e442f7d2d1e6598d98f8a Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 17:44:25 +0300 Subject: [PATCH 127/144] Update docs --- final_task/pycalc/calculator/parselets/__init__.py | 2 +- final_task/pycalc/calculator/parselets/constant.py | 2 +- final_task/pycalc/calculator/parselets/errors.py | 7 ++++++- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/final_task/pycalc/calculator/parselets/__init__.py b/final_task/pycalc/calculator/parselets/__init__.py index bb8e7bec..562141a1 100644 --- a/final_task/pycalc/calculator/parselets/__init__.py +++ b/final_task/pycalc/calculator/parselets/__init__.py @@ -1,5 +1,5 @@ """ -Parselet types. +Parselets for a calculator. """ from .constant import Constant diff --git a/final_task/pycalc/calculator/parselets/constant.py b/final_task/pycalc/calculator/parselets/constant.py index 81a43d22..537c6d15 100644 --- a/final_task/pycalc/calculator/parselets/constant.py +++ b/final_task/pycalc/calculator/parselets/constant.py @@ -6,7 +6,7 @@ class Constant(Base): - """Parselet parselet to handle constants.""" + """The parselet class for constants.""" def __init__(self, power, const_registry): super().__init__(power) diff --git a/final_task/pycalc/calculator/parselets/errors.py b/final_task/pycalc/calculator/parselets/errors.py index fda77c57..8c7305a6 100644 --- a/final_task/pycalc/calculator/parselets/errors.py +++ b/final_task/pycalc/calculator/parselets/errors.py @@ -4,7 +4,12 @@ class CallError(Exception): - """An exception for function calling.""" + """ + Raise when a function call throw an exception. + + Wrap built-in exceptions like `ArithmeticError`, + `OverflowError`, etc. + """ def __init__(self, ctx): super().__init__() From 05e3e74dacc0fd4d3d06163dbc3b47ef696f499e Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Fri, 10 May 2019 17:58:01 +0300 Subject: [PATCH 128/144] Code cleanup --- final_task/pycalc/calculator/calculator.py | 79 ---------------------- 1 file changed, 79 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index df7966b5..72725832 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -91,82 +91,3 @@ def calculator(modules_names=None): calculator_ = Calculator(parser) return calculator_ - - -# TODO: remove -if __name__ == "__main__": - import math - - def logger(fn): - def wrap(source): - print('=' * 30) - print(f'input : {source}') - result = fn(source) - print(f'output: {result}') - return result - return wrap - - p = calculator() - p.parse = logger(p.calculate) - - assert p.parse('sin(2)') == math.sin(2) - assert p.parse('sin(2-3)') == math.sin(2 - 3) - assert p.parse('2') == 2 - assert p.parse(' 2') == 2 - assert p.parse('- 2') == - 2 - assert p.parse('- - 2') == 2 - assert p.parse('1 - 2') == -1 - assert p.parse(' 1 - 2 ') == -1 - assert p.parse('1 - - 2') == 3 - assert p.parse('1 - - - 2 ') == -1 - assert p.parse('2 ^ 3 ') == 8 - assert p.parse('1 - 2 * 3') == -5 - assert p.parse('3 ^ 2 * 2') == 18 - assert p.parse('3 * 2 ^ 2') == 12 - assert p.parse('4 ^ 3 ^ 2') == 262144 - assert p.parse('6-(-13)') == 19 - assert p.parse('( 7 - 2 ) * 3') == 15 - assert p.parse('(0)') == 0 - assert p.parse('0 > 1') is False - assert p.parse('0 >= 1') is False - assert p.parse('2 > 1') is True - assert p.parse('1 >= 1') is True - assert p.parse('1 - 2 >= -1') is True - assert p.parse('log(1025 - 1, 7 - 5)') == 10 - assert p.parse('1 + tau') == 1 + math.tau - assert p.parse('1 + inf') == math.inf - assert p.parse('1 - inf') == -math.inf - assert p.parse('-inf + 1') == -math.inf - assert str(p.parse('-inf + inf')) == str(math.nan) - - assert p.parse('1 / (1-1) + 1') # ZeroDivisionError - assert p.parse('100^100^100^100') # OverflowError - assert p.parse('sin(1,2)') - assert p.parse(', 1') # from nud - assert p.parse(') 2 ') # from nud - assert p.parse('1 , 2') # not parsed completely - assert p.parse('(2') # expected - assert p.parse('sin2') # expected - assert p.parse('0 1') - assert p.parse('a') # expected token in expr begin - - # asserts from pycalc_checker.py error cases - p.parse('+') - p.parse('1-') - p.parse('1 2') - p.parse('==7') - p.parse('1+2(3*4))') - p.parse('((1+2)') - p.parse('1+1 2 3 4 5 6') - p.parse('log100(100)') - p.parse('------') - p.parse('5> =6') - p.parse('5/ /6') - p.parse('6<=6') - p.parse('6* *6') - p.parse('(((((') - p.parse('abs') - p.parse('pow(2, 3, 4)') - - p.parse('1 / 0') - p.parse('10 ^ 10 ^ 10 ^ 10') From 167a9bcf35045369154a9f522184620520265f58 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 3 Jun 2019 16:54:41 +0300 Subject: [PATCH 129/144] Add tests for calculation and calculation errors --- .../tests/integration/test_calculator.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 final_task/tests/integration/test_calculator.py diff --git a/final_task/tests/integration/test_calculator.py b/final_task/tests/integration/test_calculator.py new file mode 100644 index 00000000..7a3420fb --- /dev/null +++ b/final_task/tests/integration/test_calculator.py @@ -0,0 +1,135 @@ +""" +Test a calculator for calculation operation and calculation errors. +""" + +import unittest +from math import * + +from pycalc.calculator import calculator +from pycalc.calculator.messages import ERROR_MSG_PREFIX + + +UNARY_OPERATORS = ( + "-13", + "6-(-13)", + "1---1", + "-+---+-1" +) + +OPERATION_PRIORITY = ( + "1+2*2", + "1+(2+3*2)*3", + "10*(2+1)", + "10^(2+1)", + "100/3^2", + "100/3%2^2") + +FUNCTIONS_AND_CONSTANTS = ( + "pi+e", + "log(e)", + "sin(pi/2)", + "log10(100)", + "sin(pi/2)*111*6", + "2*sin(pi/2)", + "abs(-5)", + "round(123.456789)" +) + +ASSOCIATIVE = ( + "102%12%7", + "100/4/3", + "2^3^4", +) + +COMPARISON_OPERATORS = ( + "1+2*3==1+2*3", + "e^5>=e^5+1", + "1+2*4/3+1!=1+2*4/3+2", +) + +COMMON_TESTS = ( + "(100)", + "666", + "-.1", + "1/3", + "1.0/3.0", + ".1 * 2.0^56.0", + "e^34", + "(2.0^(pi/pi+e/e+2.0^0.0))", + "(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)", + "sin(pi/2^1) + log(1*4+2^2+1, 3^2)", + "10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5", + # a long expression splitted into two lines + "sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))+" + "cos(sin(sin(34.0-2.0^2.0))))--cos(1.0)--cos(0.0)^3.0)", + "2.0^(2.0^2.0*2.0^2.0)", + "sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))", +) + +ERROR_CASES = ( + "", + "+", + "1-", + "1 2", + "ee", + "==7", + "1 + 2(3 * 4))", + "((1+2)", + "1 + 1 2 3 4 5 6 ", + "log100(100)", + "------", + "5 > = 6", + "5 / / 6", + "6 < = 6", + "6 * * 6", + "(((((", + "pow(2, 3, 4)", +) + + +CALCULATION_CASES = ( + UNARY_OPERATORS, + OPERATION_PRIORITY, + FUNCTIONS_AND_CONSTANTS, + ASSOCIATIVE, + COMPARISON_OPERATORS, + COMMON_TESTS +) + + +def replace_power_sign(string): + """Replace the power sign in a string to the python’s power sign.""" + + return string.replace('^', '**') + + +def is_error_message(string): + """Check if a string is an error message.""" + + return string.startswith(ERROR_MSG_PREFIX) + + +class CalculatorTestCase(unittest.TestCase): + """Test a calculator.""" + + @classmethod + def setUpClass(cls): + cls.calculator = calculator() + + def test_calculation(self): + """Test calculation of a calculator.""" + + for cases in CALCULATION_CASES: + for case in cases: + with self.subTest(case=case): + case_ = replace_power_sign(case) + self.assertEqual(self.calculator.calculate( + case), eval(case_), case) + + def test_errors(self): + """Test a calculator returns errors.""" + + for case in ERROR_CASES: + with self.subTest(case=case): + result = str(self.calculator.calculate(case)) + self.assertTrue(is_error_message(result), case) From a2e4432da0f6c712c79ecb5ee0ae2d1aa464a00e Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Sat, 11 May 2019 15:24:37 +0300 Subject: [PATCH 130/144] Update docs --- final_task/pycalc/calculator/calculator.py | 2 +- final_task/pycalc/matcher/helpers.py | 8 +------- final_task/pycalc/parser/parser.py | 3 ++- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index 72725832..93ba8c53 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -1,5 +1,5 @@ """ -Initialization of a calculator. Returns a calculator instance. +Initialization of a calculator. Return a calculator instance. """ from pycalc.lexer import Lexer diff --git a/final_task/pycalc/matcher/helpers.py b/final_task/pycalc/matcher/helpers.py index 286c1d6b..62a356ff 100644 --- a/final_task/pycalc/matcher/helpers.py +++ b/final_task/pycalc/matcher/helpers.py @@ -23,13 +23,7 @@ def construct_literals_list(literals): def construct_regex(literals): - r""" - Return regex string for... . - - >>> construct_regex_string(['🏇', cos', 'arcsin', 'sin', pi()']) - arcsin|pi\(\)|cos|sin|\🏇 - - """ + """Return compiled regex object for a list of string literals.""" literals_list = construct_literals_list(literals) regex_string = '|'.join(map(re.escape, literals_list)) diff --git a/final_task/pycalc/parser/parser.py b/final_task/pycalc/parser/parser.py index fc7434bd..51124027 100644 --- a/final_task/pycalc/parser/parser.py +++ b/final_task/pycalc/parser/parser.py @@ -33,7 +33,8 @@ def parse(self, source): return result def _parse(self): - """The inner parsing function. + """ + The inner parsing function. Splitted from the main parsing function to allow catching all parsing error exceptions in one place. From 73edc6d2cc54aa5500c2f3c3bc0486124fe7579e Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 3 Jun 2019 18:09:12 +0300 Subject: [PATCH 131/144] Refactor context getting of operation parselets --- final_task/pycalc/calculator/parselets/operators.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/final_task/pycalc/calculator/parselets/operators.py b/final_task/pycalc/calculator/parselets/operators.py index 4fabb86e..04bb981f 100644 --- a/final_task/pycalc/calculator/parselets/operators.py +++ b/final_task/pycalc/calculator/parselets/operators.py @@ -13,6 +13,11 @@ def __init__(self, power, func): super().__init__(power) self.func = func + def ctx(self, parser): + """Get parsing context.""" + + return parser.context(previous=True) + class UnaryPrefix(Operator): """The parselet class for unary prefix operations.""" @@ -20,7 +25,7 @@ class UnaryPrefix(Operator): def nud(self, parser, token): """""" - ctx = parser.context(previous=True) + ctx = self.ctx(parser) right = parser.expression(self.power) result = call(ctx, self.func, [right]) @@ -34,7 +39,7 @@ class UnaryPostfix(Operator): def led(self, parser, token, left): """""" - ctx = parser.context(previous=True) + ctx = self.ctx(parser) result = call(ctx, self.func, [left]) @@ -61,7 +66,7 @@ class BinaryInfixRight(Operator): def led(self, parser, token, left): """""" - ctx = parser.context(previous=True) + ctx = self.ctx(parser) right = parser.expression(self.power - 1) result = call(ctx, self.func, [left, right]) From a2a0fbe1dd11d3cc8182abcb4bdf16ca54a5d7ed Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 3 Jun 2019 18:11:12 +0300 Subject: [PATCH 132/144] Convert lists to tuples --- final_task/pycalc/args.py | 4 ++-- final_task/pycalc/calculator/parselets/operators.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/final_task/pycalc/args.py b/final_task/pycalc/args.py index c523ec31..d2d84c2c 100644 --- a/final_task/pycalc/args.py +++ b/final_task/pycalc/args.py @@ -28,10 +28,10 @@ } } -ARGUMENTS = [ +ARGUMENTS = ( EXPRESSION, MODULE, -] +) parser = argparse.ArgumentParser(**PARSER) diff --git a/final_task/pycalc/calculator/parselets/operators.py b/final_task/pycalc/calculator/parselets/operators.py index 04bb981f..819c8014 100644 --- a/final_task/pycalc/calculator/parselets/operators.py +++ b/final_task/pycalc/calculator/parselets/operators.py @@ -28,7 +28,7 @@ def nud(self, parser, token): ctx = self.ctx(parser) right = parser.expression(self.power) - result = call(ctx, self.func, [right]) + result = call(ctx, self.func, (right,)) return result @@ -41,7 +41,7 @@ def led(self, parser, token, left): ctx = self.ctx(parser) - result = call(ctx, self.func, [left]) + result = call(ctx, self.func, (left,)) return result @@ -52,10 +52,10 @@ class BinaryInfixLeft(Operator): def led(self, parser, token, left): """""" - ctx = parser.context(previous=True) + ctx = self.ctx(parser) right = parser.expression(self.power) - result = call(ctx, self.func, [left, right]) + result = call(ctx, self.func, (left, right)) return result @@ -69,6 +69,6 @@ def led(self, parser, token, left): ctx = self.ctx(parser) right = parser.expression(self.power - 1) - result = call(ctx, self.func, [left, right]) + result = call(ctx, self.func, (left, right)) return result From 1d1905518a143045f9ff6de32ac33d46be08bf4d Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 3 Jun 2019 22:31:12 +0300 Subject: [PATCH 133/144] Add test running into the travis config --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index baca1385..7129bf8a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,4 +9,8 @@ script: - nosetests --cover-branches --with-coverage . - pycodestyle --max-line-length=120 . - python ./../pycalc_checker.py + - cd - + # added + - cd final_task + - python -m unittest - cd - \ No newline at end of file From 1fd5bcae3a4f9ff3257f7e63edcf38b72766219d Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Mon, 3 Jun 2019 22:33:37 +0300 Subject: [PATCH 134/144] Refactor lexer tests and move them into a proper directory --- final_task/tests/__init__.py | 0 final_task/tests/integration/__init__.py | 0 final_task/tests/unit/__init__.py | 0 final_task/tests/unit/lexer/__init__.py | 0 .../tests/{ => unit}/lexer/test_lexer.py | 25 +++++++++++-------- 5 files changed, 15 insertions(+), 10 deletions(-) create mode 100644 final_task/tests/__init__.py create mode 100644 final_task/tests/integration/__init__.py create mode 100644 final_task/tests/unit/__init__.py create mode 100644 final_task/tests/unit/lexer/__init__.py rename final_task/tests/{ => unit}/lexer/test_lexer.py (83%) diff --git a/final_task/tests/__init__.py b/final_task/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/final_task/tests/integration/__init__.py b/final_task/tests/integration/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/final_task/tests/unit/__init__.py b/final_task/tests/unit/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/final_task/tests/unit/lexer/__init__.py b/final_task/tests/unit/lexer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/final_task/tests/lexer/test_lexer.py b/final_task/tests/unit/lexer/test_lexer.py similarity index 83% rename from final_task/tests/lexer/test_lexer.py rename to final_task/tests/unit/lexer/test_lexer.py index 76b5d1ec..30618845 100644 --- a/final_task/tests/lexer/test_lexer.py +++ b/final_task/tests/unit/lexer/test_lexer.py @@ -1,26 +1,30 @@ +""" +Test a lexer. +""" + import unittest from pycalc.lexer.lexer import Lexer -class InitTestCase(unittest.TestCase): +class LexerTestCase(unittest.TestCase): + """""" def test_class_init(self): - '''Test class __init__() method''' + """Test the class init method""" matchers = ['1'] - source = 'some_source' - lexer = Lexer(matchers, source) + lexer = Lexer(matchers) - self.assertEqual(lexer.source, source) self.assertEqual(lexer.matchers, matchers) + self.assertEqual(lexer.source, '') self.assertEqual(lexer.pos, 0) - self.assertEqual(lexer.length, len(source)) + self.assertEqual(lexer.prev_pos, 0) + self.assertEqual(lexer.length, 0) def test_is_source_exhausted(self): """""" - lexer = Lexer([], '') test_cases = ( (0, 0, True), (3, 3, True), @@ -33,7 +37,7 @@ def test_is_source_exhausted(self): length=length, expected_result=expected_result ): - lexer = Lexer([], '') + lexer = Lexer([]) lexer.pos = pos lexer.length = length self.assertEqual(lexer.is_source_exhausted(), @@ -71,7 +75,8 @@ def test__skip_whitespaces(self): with self.subTest(source=source, pos=pos, expected_pos=expected_pos): - lexer = Lexer([], source) + lexer = Lexer([]) + lexer.init(source) lexer.pos = pos lexer._skip_whitespaces() self.assertEqual(lexer.pos, expected_pos) @@ -83,7 +88,7 @@ def test__advance_pos_by_lexeme(self): pos = 10 expected_pos = pos + len(lexeme) - lexer = Lexer([], '') + lexer = Lexer([]) lexer.pos = pos lexer._advance_pos_by_lexeme(lexeme) From 0ffe2a87c8af2eccb674f4b96d9bc4af3321a615 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Wed, 5 Jun 2019 10:16:50 +0300 Subject: [PATCH 135/144] Wrap parsing of command line arguments in a function --- final_task/pycalc/args.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/final_task/pycalc/args.py b/final_task/pycalc/args.py index d2d84c2c..1cb49c47 100644 --- a/final_task/pycalc/args.py +++ b/final_task/pycalc/args.py @@ -33,9 +33,15 @@ MODULE, ) -parser = argparse.ArgumentParser(**PARSER) -for arg in ARGUMENTS: - parser.add_argument(*arg['name_or_flags'], **arg['keyword_arguments']) +def get_args(): + """Parse command line arguments.""" -args = parser.parse_args() + parser = argparse.ArgumentParser(**PARSER) + + for arg in ARGUMENTS: + parser.add_argument(*arg['name_or_flags'], **arg['keyword_arguments']) + + args = parser.parse_args() + + return args From 883f4e708f8580a69eed7f95966f95c833fb67f0 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Wed, 5 Jun 2019 10:54:19 +0300 Subject: [PATCH 136/144] Add exceptions for calculator errors --- final_task/pycalc/calculator/errors.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/final_task/pycalc/calculator/errors.py b/final_task/pycalc/calculator/errors.py index 790f5327..0dacaff5 100644 --- a/final_task/pycalc/calculator/errors.py +++ b/final_task/pycalc/calculator/errors.py @@ -18,6 +18,22 @@ ) +class CalculatorError(Exception): + """Raise on calculator’s errors.""" + + def __init__(self, err_msg): + super().__init__() + self.err_msg = err_msg + + +class CalculatorInitializationError(CalculatorError): + """Raise on calculator’s initialization errors.""" + + +class CalculatorCalculationError(CalculatorError): + """Raise on calculator’s calculation errors.""" + + def get_err_msg(exc): """Return an error message according to an exception type.""" From 11d0b016ddc10ffe4583e20fce4b1b8b0f4624a2 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Wed, 5 Jun 2019 10:55:00 +0300 Subject: [PATCH 137/144] =?UTF-8?q?Refactor=20calculator=E2=80=99s=20forma?= =?UTF-8?q?tters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- final_task/pycalc/calculator/formatters.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/calculator/formatters.py b/final_task/pycalc/calculator/formatters.py index a8798afa..03f2d094 100644 --- a/final_task/pycalc/calculator/formatters.py +++ b/final_task/pycalc/calculator/formatters.py @@ -8,13 +8,13 @@ ERROR_PLACE_INDICATOR = '^' -def err_msg_formatter(msg): +def prefix_err_msg(msg): """Return an error message with an error prefix.""" return f'{ERROR_MSG_PREFIX}{msg}' -def err_ctx_formatter(ctx): +def ctx_formatter(ctx): """ Return a two-line string with a source in the first line and a sign in the second one which indicate @@ -25,3 +25,11 @@ def err_ctx_formatter(ctx): pos = ctx.pos return '{}\n{}{}'.format(source, ' ' * (pos), ERROR_PLACE_INDICATOR) + + +def err_msg_with_ctx_formatter(msg, ctx): + """Return an error message with context information.""" + + ctx_msg = ctx_formatter(ctx) + + return f'{msg}\n{ctx_msg}' From f29d3250e3dc3aaaf32cda354cdcf6da4f8f7879 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Wed, 5 Jun 2019 11:15:56 +0300 Subject: [PATCH 138/144] Fix imports --- final_task/pycalc/calculator/__init__.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/final_task/pycalc/calculator/__init__.py b/final_task/pycalc/calculator/__init__.py index b3cdab98..2f7e268f 100644 --- a/final_task/pycalc/calculator/__init__.py +++ b/final_task/pycalc/calculator/__init__.py @@ -3,3 +3,8 @@ """ from .calculator import calculator +from .errors import ( + CalculatorError, + CalculatorInitializationError, + CalculatorCalculationError +) From 76974cd8d8efc8be92bd34d6fadf4684702a9fe7 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Wed, 5 Jun 2019 12:52:19 +0300 Subject: [PATCH 139/144] Refactor formatters of calculator --- final_task/pycalc/calculator/formatters.py | 22 ++++++++++++---------- final_task/pycalc/calculator/messages.py | 3 ++- 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/final_task/pycalc/calculator/formatters.py b/final_task/pycalc/calculator/formatters.py index 03f2d094..f0b1e57e 100644 --- a/final_task/pycalc/calculator/formatters.py +++ b/final_task/pycalc/calculator/formatters.py @@ -2,16 +2,10 @@ Provides functions for string formatting. """ -from .messages import ERROR_MSG_PREFIX - - -ERROR_PLACE_INDICATOR = '^' - - -def prefix_err_msg(msg): - """Return an error message with an error prefix.""" - - return f'{ERROR_MSG_PREFIX}{msg}' +from .messages import ( + ERROR_PLACE_INDICATOR, + MODULES_IMPORT_ERROR, +) def ctx_formatter(ctx): @@ -33,3 +27,11 @@ def err_msg_with_ctx_formatter(msg, ctx): ctx_msg = ctx_formatter(ctx) return f'{msg}\n{ctx_msg}' + + +def err_modules_import_formatter(modules_names): + """Return an error message for module imports errors.""" + + modules_names = ', '.join(modules_names) + + return f'{MODULES_IMPORT_ERROR} {modules_names}' diff --git a/final_task/pycalc/calculator/messages.py b/final_task/pycalc/calculator/messages.py index 6a9ae63b..6093a84e 100644 --- a/final_task/pycalc/calculator/messages.py +++ b/final_task/pycalc/calculator/messages.py @@ -5,6 +5,7 @@ CALCULATOR_INITIALIZATION_ERROR = 'calculator initialization error' CANT_PARSE_EXPRESSION = 'can’t parse this expression' EMPTY_EXPRESSION_PROVIDED = 'empty expression provided' -ERROR_MSG_PREFIX = 'ERROR: ' MODULES_IMPORT_ERROR = 'no module(s) named' SYNTAX_ERROR = 'syntax error' + +ERROR_PLACE_INDICATOR = '^' From 9231382c236c31fe5c868a3dfeba7e9079371a2e Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Wed, 5 Jun 2019 11:14:39 +0300 Subject: [PATCH 140/144] Raise exceptions on calculator errors --- final_task/pycalc/calculator/calculator.py | 67 ++++++++++++---------- 1 file changed, 38 insertions(+), 29 deletions(-) diff --git a/final_task/pycalc/calculator/calculator.py b/final_task/pycalc/calculator/calculator.py index 93ba8c53..780976b7 100644 --- a/final_task/pycalc/calculator/calculator.py +++ b/final_task/pycalc/calculator/calculator.py @@ -4,12 +4,14 @@ from pycalc.lexer import Lexer from pycalc.parser import Parser, ParserGenericError +from pycalc.importer import ModuleImportErrors -from .formatters import err_msg_formatter, err_ctx_formatter -from .errors import get_err_msg +from .formatters import err_msg_with_ctx_formatter, err_modules_import_formatter +from .errors import CalculatorCalculationError, CalculatorInitializationError, get_err_msg from .importer import build_modules_registry from .matchers import build_matchers from .messages import ( + CALCULATOR_INITIALIZATION_ERROR, CANT_PARSE_EXPRESSION, EMPTY_EXPRESSION_PROVIDED, ) @@ -31,13 +33,13 @@ def calculate(self, expression): """ Calculate an expression. - Return result of a calculation or an error message - if the calculation fails. + Return result of calculation or raise a `CalculatorCalculationError` exception + if calculation fails. """ # empty expression if not expression: - return err_msg_formatter(EMPTY_EXPRESSION_PROVIDED) + raise CalculatorCalculationError(EMPTY_EXPRESSION_PROVIDED) # calculate an expression try: @@ -53,41 +55,48 @@ def calculate(self, expression): # an error message err_msg = get_err_msg(exc) - err_msg = err_msg_formatter(err_msg) + err_msg = err_msg_with_ctx_formatter(err_msg, ctx) - # an context message - ctx_msg = err_ctx_formatter(ctx) + # handle all other errors + except Exception as exc: + err_msg = CANT_PARSE_EXPRESSION - return f'{err_msg}\n{ctx_msg}' + raise CalculatorCalculationError(err_msg) - # probably not reacheable code but better save than sorry - except Exception as exc: - err_msg = err_msg_formatter(CANT_PARSE_EXPRESSION) +def calculator(modules_names=None): + """ + Initialize a calculator and return a Calculator instance. + + Raise a `CalculatorInitializationError` exception when initialization fails.""" - return err_msg + try: + # import constants and functions from default and requested modules + modules_registry = build_modules_registry(modules_names) + # build lexemes matchers + matchers = build_matchers(modules_registry) -def calculator(modules_names=None): - """Initialize a calculator and return a parser object.""" + # create a lexer + lexer = Lexer(matchers) - # import constants and functions from default and requested modules - modules_registry = build_modules_registry(modules_names) + # build a specification for a parser + spec = build_specification(modules_registry) - # build lexemes matchers - matchers = build_matchers(modules_registry) + # create a parser + power = Precedence.DEFAULT + parser = Parser(spec, lexer, power) - # create a lexer - lexer = Lexer(matchers) + # create a calculator + calculator_ = Calculator(parser) - # build a specification for a parser - spec = build_specification(modules_registry) + return calculator_ - # create a parser - power = Precedence.DEFAULT - parser = Parser(spec, lexer, power) + except ModuleImportErrors as exc: + modules_names = exc.modules_names + err_msg = err_modules_import_formatter(modules_names) - # create a calculator - calculator_ = Calculator(parser) + except Exception: + err_msg = CALCULATOR_INITIALIZATION_ERROR - return calculator_ + raise CalculatorInitializationError(err_msg) From 31f1f611e7f8c02b13cdcce0de26de61ddde13b9 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Wed, 5 Jun 2019 12:58:23 +0300 Subject: [PATCH 141/144] Refactor cli to class --- final_task/pycalc/cli.py | 98 +++++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 26 deletions(-) diff --git a/final_task/pycalc/cli.py b/final_task/pycalc/cli.py index e07f9474..9e169f94 100644 --- a/final_task/pycalc/cli.py +++ b/final_task/pycalc/cli.py @@ -5,38 +5,84 @@ import sys -from pycalc.args import args -from pycalc.calculator import calculator -from pycalc.calculator.formatters import err_msg_formatter -from pycalc.calculator.messages import ( - CALCULATOR_INITIALIZATION_ERROR, - MODULES_IMPORT_ERROR -) -from pycalc.importer.errors import ModuleImportErrors +from pycalc.args import get_args +from pycalc.calculator import calculator, CalculatorError +ERROR_MSG_PREFIX = 'ERROR: ' -def main(): - """ - Initialize a calculator and calculate - an expression from a command line argument. - """ - # initialize a calculator - try: - calc = calculator(args.modules) +class Cli: + """Command line interface for a calculator.""" - except ModuleImportErrors as exc: - modules_names = ', '.join(exc.modules_names) - err_msg = f'{MODULES_IMPORT_ERROR} {modules_names}' - sys.exit(err_msg_formatter(err_msg)) + def __init__(self): + self.args = get_args() + self.calculator = None - except Exception: - sys.exit(err_msg_formatter(CALCULATOR_INITIALIZATION_ERROR)) + def run(self): + """Initialize a calculator and make a calculation.""" - # make a calculation and print a result - result = calc.calculate(args.expression) - print(result) + modules = self.args.modules + self.init_calculator(modules) + + expression = self.args.expression + self.calculate(expression) + + def init_calculator(self, modules): + """Initialize a calculator.""" + + try: + self.calculator = calculator(modules) + return + + except CalculatorError as exc: + err_msg = exc.err_msg + self.on_error(err_msg) + + def calculate(self, expression): + """Make a calculation.""" + + assert callable( + self.calculator.calculate), 'calculate is not a callable' + + try: + result = self.calculator.calculate(expression) + self.on_success(result) + return + + except CalculatorError as exc: + err_msg = exc.err_msg + + except Exception as exc: + err_msg = str(exc) + + self.on_error(err_msg) + + def on_success(self, result): + """Run if a calculation was succesfull.""" + + self.exit(result) + + def on_error(self, err_msg): + """Run if there were initialization or calculation errors.""" + + message = self.prefix_err_msg(err_msg) + self.exit(message, is_error=True) + + def exit(self, message, is_error=False): + """Print a message and exit.""" + + print(message) + + if is_error: + sys.exit(1) + + sys.exit() + + def prefix_err_msg(self, msg): + """Return an error message with an error prefix.""" + + return f'{ERROR_MSG_PREFIX}{msg}' if __name__ == "__main__": - main() + Cli().run() From 5ee52e88463571de4596b2cc4c757974899c1ccd Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Wed, 5 Jun 2019 12:59:16 +0300 Subject: [PATCH 142/144] Fix the top-level main script --- final_task/pycalc/__main__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/final_task/pycalc/__main__.py b/final_task/pycalc/__main__.py index d5ae7248..61c1b3f4 100644 --- a/final_task/pycalc/__main__.py +++ b/final_task/pycalc/__main__.py @@ -5,13 +5,13 @@ and prints evaluated result. """ -from pycalc import cli +from pycalc.cli import Cli def main(): """Pure-python implementation of a command-line calculator.""" - cli.main() + Cli().run() if __name__ == "__main__": From 2d3276a203166a3faef282dd924d5fffbe7a2f5e Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik Date: Wed, 5 Jun 2019 15:45:28 +0300 Subject: [PATCH 143/144] Remove unrelevant tests after refactoring --- .../tests/integration/test_calculator.py | 135 ------------------ 1 file changed, 135 deletions(-) delete mode 100644 final_task/tests/integration/test_calculator.py diff --git a/final_task/tests/integration/test_calculator.py b/final_task/tests/integration/test_calculator.py deleted file mode 100644 index 7a3420fb..00000000 --- a/final_task/tests/integration/test_calculator.py +++ /dev/null @@ -1,135 +0,0 @@ -""" -Test a calculator for calculation operation and calculation errors. -""" - -import unittest -from math import * - -from pycalc.calculator import calculator -from pycalc.calculator.messages import ERROR_MSG_PREFIX - - -UNARY_OPERATORS = ( - "-13", - "6-(-13)", - "1---1", - "-+---+-1" -) - -OPERATION_PRIORITY = ( - "1+2*2", - "1+(2+3*2)*3", - "10*(2+1)", - "10^(2+1)", - "100/3^2", - "100/3%2^2") - -FUNCTIONS_AND_CONSTANTS = ( - "pi+e", - "log(e)", - "sin(pi/2)", - "log10(100)", - "sin(pi/2)*111*6", - "2*sin(pi/2)", - "abs(-5)", - "round(123.456789)" -) - -ASSOCIATIVE = ( - "102%12%7", - "100/4/3", - "2^3^4", -) - -COMPARISON_OPERATORS = ( - "1+2*3==1+2*3", - "e^5>=e^5+1", - "1+2*4/3+1!=1+2*4/3+2", -) - -COMMON_TESTS = ( - "(100)", - "666", - "-.1", - "1/3", - "1.0/3.0", - ".1 * 2.0^56.0", - "e^34", - "(2.0^(pi/pi+e/e+2.0^0.0))", - "(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)", - "sin(pi/2^1) + log(1*4+2^2+1, 3^2)", - "10*e^0*log10(.4 -5/ -0.1-10) - -abs(-53/10) + -5", - # a long expression splitted into two lines - "sin(-cos(-sin(3.0)-cos(-sin(-3.0*5.0)-sin(cos(log10(43.0))))+" - "cos(sin(sin(34.0-2.0^2.0))))--cos(1.0)--cos(0.0)^3.0)", - "2.0^(2.0^2.0*2.0^2.0)", - "sin(e^log(e^e^sin(23.0),45.0) + cos(3.0+log10(e^-e)))", -) - -ERROR_CASES = ( - "", - "+", - "1-", - "1 2", - "ee", - "==7", - "1 + 2(3 * 4))", - "((1+2)", - "1 + 1 2 3 4 5 6 ", - "log100(100)", - "------", - "5 > = 6", - "5 / / 6", - "6 < = 6", - "6 * * 6", - "(((((", - "pow(2, 3, 4)", -) - - -CALCULATION_CASES = ( - UNARY_OPERATORS, - OPERATION_PRIORITY, - FUNCTIONS_AND_CONSTANTS, - ASSOCIATIVE, - COMPARISON_OPERATORS, - COMMON_TESTS -) - - -def replace_power_sign(string): - """Replace the power sign in a string to the python’s power sign.""" - - return string.replace('^', '**') - - -def is_error_message(string): - """Check if a string is an error message.""" - - return string.startswith(ERROR_MSG_PREFIX) - - -class CalculatorTestCase(unittest.TestCase): - """Test a calculator.""" - - @classmethod - def setUpClass(cls): - cls.calculator = calculator() - - def test_calculation(self): - """Test calculation of a calculator.""" - - for cases in CALCULATION_CASES: - for case in cases: - with self.subTest(case=case): - case_ = replace_power_sign(case) - self.assertEqual(self.calculator.calculate( - case), eval(case_), case) - - def test_errors(self): - """Test a calculator returns errors.""" - - for case in ERROR_CASES: - with self.subTest(case=case): - result = str(self.calculator.calculate(case)) - self.assertTrue(is_error_message(result), case) From 43f8e3b6ab4c6b85f44e5331d16f36ac88518c42 Mon Sep 17 00:00:00 2001 From: Siarhiej Kresik <47978453+siarhiejkresik@users.noreply.github.com> Date: Wed, 5 Jun 2019 18:46:45 +0300 Subject: [PATCH 144/144] Add README file --- README.md | 135 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000..6eb27988 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# pycalc + +[![Build Status](https://travis-ci.org/siarhiejkresik/Epam-2019-Python-Homework.svg?branch=master)](https://travis-ci.org/siarhiejkresik/Epam-2019-Python-Homework) +Python Programming Language Foundation Hometask (EPAM, 2019). +For task description see [link](https://github.com/siarhiejkresik/Epam-2019-Python-Homework/tree/master/final_task). + +`pycalc` is a command-line calculator implemented in pure Python 3 using Top Down Operator Precedence parsing algorithm (Pratt parser). It receives mathematical expression string as an argument and prints evaluated result. + +## Features + +`pycalc` supports: + +- arithmetic operations (`+`, `-`, `*`, `/`, `//`, `%`, `^` (`^` is a power)); +- comparison operations (`<`, `<=`, `==`, `!=`, `>=`, `>`); +- 2 built-in python functions: `abs` and `round`; +- all functions and constants from standard python module `math` (trigonometry, logarithms, etc.); +- functions and constants from the modules provided with `-m` or `--use-modules` command-line option; +- exit with non-zero exit code on errors. + +## How to install + +1. `git clone ` +2. `cd /final_task/` +3. `pip3 install --user .` or `sudo -H pip3 install .` + +## Examples + +### Command line interface: + +```shell +$ pycalc --help +usage: pycalc [-h] EXPRESSION [-m MODULE [MODULE ...]] + +Pure-python command-line calculator. + +positional arguments: + EXPRESSION expression string to evaluate + +optional arguments: + -h, --help show this help message and exit + -m MODULE [MODULE ...], --use-modules MODULE [MODULE ...] + additional modules to use +``` + +### Calculation: + +```shell +$ pycalc '2+2*2' +6 + +$ pycalc '2+sin(pi)^(2-cos(e))' +2.0 +``` + +```shell +$ pycalc '5+3<=1' +False +``` + +```shell +$ pycalc 'e + pi + tau' +12.143059789228424 + +$ pycalc '1 + inf' +inf + +$ pycalc '1 - inf' +-inf + +$ pycalc 'inf - inf' +nan + +$ pycalc 'nan == nan' +False +``` + +### Errors: + +```shell +$ pycalc '15*(25+1' +ERROR: syntax error +15*(25+1 + ^ +$ pycalc 'func' +ERROR: syntax error +func +^ +$ pycalc '10 + 1/0 -3' +ERROR: division by zero +10 + 1/0 -3 + ^ +$ pycalc '1 + sin(1,2) - 2' +ERROR: sin() takes exactly one argument (2 given) +1 + sin(1,2) - 2 + ^ +$ pycalc '10^10^10' +ERROR: math range error +10^10^10 + ^ +$ pycalc '(-1)^0.5' +ERROR: math domain error +(-1)^0.5 + ^ +$ pycalc '' +ERROR: empty expression provided + +$ pycalc '1514' -m fake calendar nonexistent time +ERROR: no module(s) named fake, nonexistent +``` + +### Additional modules: + +```python +# my_module.py +def sin(number): + return 42 +``` + +```shell +$ pycalc 'sin(pi/2)' +1.0 +$ pycalc 'sin(pi/2)' -m my_module +42 +$ pycalc 'THURSDAY' -m calendar +3 +$ pycalc 'sin(pi/2) - THURSDAY * 10' -m my_module calendar +12 +``` + +## References + +- https://en.wikipedia.org/wiki/Pratt_parser +- https://tdop.github.io/ +- http://www.oilshell.org/blog/2017/03/31.html +- https://engineering.desmos.com/articles/pratt-parser