Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
104 commits
Select commit Hold shift + click to select a range
852e30c
initial commit: calc.py can calculate all valid expressinos, except e…
misha-sauchuk Apr 14, 2019
ace43c3
feat: add calculation of two arguments separated by commo in the math…
misha-sauchuk Apr 14, 2019
62abf56
refactor: extract main function "calculate",
misha-sauchuk Apr 14, 2019
47c3166
fix: operators with two elements "//, ==, !=, >=, <=" now is valid
misha-sauchuk Apr 14, 2019
c110200
fix: resolve problem in converter function when we have sub function
misha-sauchuk Apr 21, 2019
bec2b2a
feat: add argparse module, make distribution package wtih setuptools …
misha-sauchuk Apr 22, 2019
2025e62
feat: add class Error and function check_expression to make validatio…
misha-sauchuk Apr 25, 2019
4c79af9
feat: add exception for blank about two operands
misha-sauchuk Apr 25, 2019
4737325
feat: update method function_parser with tau as a constant; extract A…
misha-sauchuk May 10, 2019
f789ce8
refactor: update long lines in the code, add some whitespaces
misha-sauchuk May 10, 2019
164b049
feat: update method split_operators to calculate functions with sever…
misha-sauchuk May 10, 2019
9aa5958
feat: remove class Errors, raise SyntaxError inside code
misha-sauchuk May 10, 2019
5302999
fix: return ValueError in number_parser function
misha-sauchuk May 10, 2019
765b59a
refactor: put try-except method into main function
misha-sauchuk May 10, 2019
4587c42
feat: add function to raise an exception if expression consist of fun…
misha-sauchuk May 10, 2019
4fe0ddc
feat: add exception if there is more than two arguments in the function
misha-sauchuk May 10, 2019
ab26ff7
feat: add expression check if expression start with operator, if func…
misha-sauchuk May 12, 2019
c692c36
fix: remove print method
misha-sauchuk May 12, 2019
9f2bef9
fix: remove one line for debug
misha-sauchuk May 12, 2019
5b20da7
fix: add symbol * to check if there is blank symbol betwwen twice ope…
misha-sauchuk May 12, 2019
8cf47b2
refactor: update the max-line-length
misha-sauchuk May 12, 2019
e35b482
feat: add operator_parser to check if the math operator is correct; r…
misha-sauchuk May 13, 2019
953073e
feat: add operator_parser to check if the math operator is correct; r…
misha-sauchuk May 13, 2019
94124bb
feat: update operator_dict with ** operator; add checking if there ar…
misha-sauchuk May 13, 2019
cc24c2d
git: mistake in git ammend
misha-sauchuk May 13, 2019
951cfe3
fix: remove extra lines in converter function
misha-sauchuk May 13, 2019
486cab9
fix: remove raise the exception when last of two consecutive operator…
misha-sauchuk May 13, 2019
c178130
remove raise the exception when last of two consecutive operators is …
misha-sauchuk May 13, 2019
bafd158
feat: add unary_dict with inary operator "-", update priority to all …
misha-sauchuk May 14, 2019
031417b
fix: update if statment for sub operation in converter; feat: add ext…
misha-sauchuk May 14, 2019
367eb44
style: remove long line
misha-sauchuk May 14, 2019
ea57e21
feat: add test case for operation_priority and error_cases
misha-sauchuk May 14, 2019
c1778ed
feat: extract function split_operators into class SplitOperator, extr…
misha-sauchuk May 15, 2019
07e2add
fix: resolve typo in package name
misha-sauchuk May 15, 2019
a1f94cd
fix: in main function add expressin_line to call in split_operators
misha-sauchuk May 15, 2019
b61dba0
fix: update import my modules
misha-sauchuk May 15, 2019
6fe197a
fix: update import my modules
misha-sauchuk May 15, 2019
fdb1b63
feat: create packege pycalc and put there calc modules
misha-sauchuk May 15, 2019
bb7f75d
fix: add package pycalc in entry_points
misha-sauchuk May 15, 2019
3142b89
fix: update import in expression_parser.py, update main function with…
misha-sauchuk May 15, 2019
9685348
fix: now check_manager return list
misha-sauchuk May 15, 2019
4f3a030
fix: add if statmet to check if theres a duble math operator
misha-sauchuk May 15, 2019
8ac1b50
feat: extract metohds to parse numbers, functions, operators from spl…
misha-sauchuk May 15, 2019
a245b70
fix: correct typo in method name
misha-sauchuk May 15, 2019
332f78a
feat: extract function converter into class Converter and move it int…
misha-sauchuk May 16, 2019
0e6db61
fix: divide function check_expressino into two function check_express…
misha-sauchuk May 16, 2019
c415e31
fix: changed if statment in method _function_converter to check when …
misha-sauchuk May 16, 2019
d4d85e0
feat: extract fucntion calculcate into module calculator, update Spli…
misha-sauchuk May 17, 2019
011ba7a
fix: include return to method calc_on_stack to avoid lost of the curr…
misha-sauchuk May 17, 2019
75ac4b8
feat: extract agr_parser into module, rename module expression_parser…
misha-sauchuk May 18, 2019
6f4df28
feat: add test module to test check_manager module, finish unittest i…
misha-sauchuk May 19, 2019
e5b7154
fix: in test_check_manager add import check_parsing_list, operator_di…
misha-sauchuk May 19, 2019
dadeec9
fix: add check constant tau in test_function_check_function_dict; fea…
misha-sauchuk May 19, 2019
bb1eee8
feat: add unittest for module converter, refactor: update method func…
misha-sauchuk May 20, 2019
4900fae
fix: update import module
misha-sauchuk May 20, 2019
fd104b0
fix: remove point in import module from pycalc; refactor: delete unus…
misha-sauchuk May 20, 2019
fd404ab
fix: update method function_converter: return if statment only with !…
misha-sauchuk May 20, 2019
006f403
fix: upadte unitest for funct i on_converter when we have minus after…
misha-sauchuk May 20, 2019
3641d09
feat: add unittests fot module calculator; update method calc_on_stac…
misha-sauchuk May 21, 2019
d9ad6a5
style: align list parameters
misha-sauchuk May 21, 2019
efc3b05
feat: extract function to creat a dict with math functions; feat: add…
misha-sauchuk May 22, 2019
bfb6f74
fix: update imports
misha-sauchuk May 22, 2019
0147aa3
fix: add import class Calculator
misha-sauchuk May 22, 2019
107d675
style: add description for every functions in the app
misha-sauchuk May 22, 2019
d9e9781
feat: add users_modules to ar g_parser, update class SplitOperators, …
misha-sauchuk May 27, 2019
7c225c9
style: made four spaces out
misha-sauchuk May 27, 2019
1c1b875
fix: update unit tests incase adding one required arguments during in…
misha-sauchuk May 27, 2019
bf531a9
fix: update function_check by removing the excess fist if statment; a…
misha-sauchuk May 27, 2019
b9b3e65
feat: add chechikg if there is closed bracket before open bracket
misha-sauchuk May 27, 2019
5969be8
fix: change elif to if statment to append brackets into parsing_list
misha-sauchuk May 27, 2019
9fcc8c4
fix: add "(" to self.brackets in the test_simple_operator_add_closed_…
misha-sauchuk May 27, 2019
352d09c
feat: add collect_function_arguments method into class SplitOperators…
misha-sauchuk May 28, 2019
7730f18
style: add empty line in the end
misha-sauchuk May 28, 2019
beb7025
feat: creaer unittests for method collect_function_arguments; update …
misha-sauchuk May 28, 2019
e976ce0
feat: add logic to calculate function arguments into module calculato…
misha-sauchuk May 29, 2019
3ad8c06
feat: update unittests for module calculator acording new functionali…
misha-sauchuk May 29, 2019
e3d0d6d
fix: update unittests acording functions in modules
misha-sauchuk May 29, 2019
ed3f7e9
fix: add whitespace to actula result, remove blank lines in the end o…
misha-sauchuk May 29, 2019
6a123b5
feat: add unittest with mock for arg_parser function
misha-sauchuk May 30, 2019
c44cb80
fix: update path to arg_parser in test_pycalc
misha-sauchuk May 30, 2019
a4d0cfd
fix: upade import for patch in unittest, feat: add posibility to send…
misha-sauchuk May 30, 2019
8df0444
fix: add try"/"except to send iterable to math function; delete blank…
misha-sauchuk May 30, 2019
374662c
fix: add MagicMock to unittest of calling arg_parser
misha-sauchuk May 30, 2019
5662e5b
fix: correct typo in module naname
misha-sauchuk May 30, 2019
361f325
fix: remove mock from unittests
misha-sauchuk May 30, 2019
ff1771c
fix: add import main function to unittest
misha-sauchuk May 30, 2019
b5f09dc
style: add some additional commetns to the docstings
misha-sauchuk Jun 3, 2019
0e661c6
refactor: replaced three "if" statements in the end of calculate func…
misha-sauchuk Jun 4, 2019
2bff82d
feat: update FUNCIONS_AND_CONSTANTS tests with iteriable arguments "f…
misha-sauchuk Jun 4, 2019
d531321
feat: add unittest for math function with iterable arguments in test_…
misha-sauchuk Jun 4, 2019
a72cf15
fix: add brackets "()" to call _calc_on_stack function in test_calcu…
misha-sauchuk Jun 4, 2019
8a1b434
feat: create unittest for argument_parser module
misha-sauchuk Jun 4, 2019
e55dfc4
fix: add mock_args into definition of test_expression_parser
misha-sauchuk Jun 4, 2019
331c0f2
feat: add unittest for arg_parser with user modules
misha-sauchuk Jun 4, 2019
20a8990
fix: add the . to the name of user module to make relative path; styl…
misha-sauchuk Jun 4, 2019
622f711
fix: repalce importlib.import_module by __import__
misha-sauchuk Jun 4, 2019
1b47d24
fix: add package name to import user module
misha-sauchuk Jun 4, 2019
e1e9c62
fix: update path to user_module by adding "pycalc."
misha-sauchuk Jun 4, 2019
f29720e
fix: use os.path.commonprefix to get realtive path for user module
misha-sauchuk Jun 4, 2019
da41b6e
fix: add ..mod to the path of user_module
misha-sauchuk Jun 4, 2019
3e4644e
fix: create use_module_test in tests package, upadte operator_manager…
misha-sauchuk Jun 4, 2019
9356d67
fix: remove unittest for parsing user_expression with user_modules
misha-sauchuk Jun 4, 2019
03117d0
style: remove blank lines
misha-sauchuk Jun 4, 2019
955f642
feat: remove test_argument_parser.py for removing unittest/mock.py fr…
misha-sauchuk Jun 5, 2019
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Empty file added __init__.py
Empty file.
Empty file added final_task/pycalc/__init__.py
Empty file.
37 changes: 37 additions & 0 deletions final_task/pycalc/argument_parser.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
"""Argument parser module"""

import argparse
from collections import namedtuple
from .operator_manager import create_func_dict, find_user_functions


def arg_parser():
"""
This function gather positional arguments from users,
create a function_dict with users and built_in math functions if there is users_modules,
otherwise create a function_dict only with built_in math functions
:return: line as namedtuple(expression, function_dict)
"""
parser = argparse.ArgumentParser(
description='Pure-python command-line calculator.',
prog='pycalc'
)
parser.add_argument(
'-m',
'--use-modules',
help='additional modules to use',
metavar='MODULE [MODULE ...]'
)
parser.add_argument('EXPRESSION', help='expression string to evaluate')
args = parser.parse_args()
expression = args.EXPRESSION
if args.use_modules:
user_functions = find_user_functions(args.use_modules)
function_dict = create_func_dict(user_functions)
expression_line = namedtuple('expression_line', 'expression functions')
line = expression_line(expression, function_dict)
else:
function_dict = create_func_dict()
expression_line = namedtuple('expression_line', 'expression functions')
line = expression_line(expression, function_dict)
return line
118 changes: 118 additions & 0 deletions final_task/pycalc/calculator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
"""Calculator module"""

from .stack_manager import Stack
from .operator_manager import operator_dict, unary_dict
from .split_operators import SplitOperators
from .converter import Converter


class Calculator:
"""Calculator class"""
def __init__(self, expression, functions):
"""
Generates an instance of the Calculator class,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Этот докстринг в точности повторяет код строчка за строчкой :)
( + __init__ не генерирует объект, а инициализирует его)

take an expression_line from user,
create an instance of the SplitOperators,
create an instance of the Converter,
create a Stack to put all operands,
create a Stack to put all operations
self.current_operator need to check the priority of current_operator
and the operator on stack after one calculation
self.arg_result_lst need to put calculated arguments and to make a tuple
of arguments
"""
self.expression_line = expression
self.function_dict = functions
self.parser = SplitOperators(self.expression_line, self.function_dict).split_operators()
self.converted_list = Converter(self.parser, self.function_dict).converter()
self.current_result = ""
self.operands = Stack()
self.function = Stack()
self.current_operator = ""
self.arg_result_lst = []

def _calc_on_stack(self):
"""
Encapsulate function
Takes an item from function_stack and operands from the operands_stack
and performs calculation.
Try to send arguments to math function as variables,
or if it impossible send an iterable (e.g. for math.fsum),
or Raise an Exception if there is two many arguments for current function
If it possible to make next calculation - make recursion
Returns result of calculation to the current_result.
"""
operator_on_stack = self.function.take_from_stack()
if operator_on_stack in self.function_dict.values():
func_args = self.operands.take_from_stack()
try:
self.current_result = operator_on_stack['operator'](*func_args)
except TypeError:
try:
self.current_result = operator_on_stack['operator'](func_args)
except TypeError as err:
raise SyntaxError(err)
elif operator_on_stack in operator_dict.values() or operator_on_stack in unary_dict.values():
if len(self.operands.stack) == 1:
second_operand = self.operands.take_from_stack()
first_operand = 0
else:
second_operand = self.operands.take_from_stack()
first_operand = self.operands.take_from_stack()
self.current_result = operator_on_stack['operator'](first_operand, second_operand)
self.operands.put_on_stack(self.current_result)
if self.function.stack and self.function.top() is not '(':
if self.current_operator['priority'] >= self.function.top()['priority']:
self.current_result = self._calc_on_stack()
return self.current_result

def calculate(self):
"""
For each item in converted_list using Reverse Polish notation, method
check a type and put it on either operands or function stack,
and invokes calc_on_stack method to perform calculation itself.
If item is tuple (function arguments) generate a new instance of
Calculator to calculate it and after that put results as tuple to
the operand stack. After calculating current arguments arg_result_lst become
an empty list. Returns result of calculation received from calc_on_stack method
"""
for item in self.converted_list:
if isinstance(item, tuple):
for argument in item:
arg_calculate = Calculator(argument, self.function_dict)
arg_result = arg_calculate.calculate()
self.arg_result_lst.append(arg_result)
self.operands.put_on_stack(tuple(self.arg_result_lst))
self.arg_result_lst = []
elif isinstance(item, float) or isinstance(item, int):
self.operands.put_on_stack(item)
elif item in operator_dict.values() \
or item in self.function_dict.values() \
or item in unary_dict.values():
self.current_operator = item
if self.function.is_empty():
self.function.put_on_stack(self.current_operator)
else:
if self.function.top() is '(' \
or self.current_operator['priority'] < self.function.top()['priority'] \
or self.current_operator == operator_dict['^'] \
and self.function.top() == operator_dict['^']:
self.function.put_on_stack(self.current_operator)
else:
self._calc_on_stack()
self.function.put_on_stack(self.current_operator)
elif item is '(':
self.function.put_on_stack(item)
elif item is ')' and self.function.top() == '(':
self.function.take_from_stack()
else:
for i in self.function.stack:
self._calc_on_stack()
if item is ')' and self.function.stack:
if self.function.top() is '(':
self.function.take_from_stack()
break
while self.function.stack:
self._calc_on_stack()
self.current_result = self.operands.take_from_stack()
return self.current_result
93 changes: 93 additions & 0 deletions final_task/pycalc/check_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Check manager module"""

import sys
from .operator_manager import operator_dict


def check_expression(expression_line):
"""
Check if the expression_line is not empty and the brackets are correct,
otherwise raise an Exception
:param expression_line: string from the user
:return: clear expression line as str
"""
if not expression_line:
raise SyntaxError('Expression cannot be empty')
if expression_line.count('(') < expression_line.count(')'):
raise SyntaxError('Opening bracket required!')
elif expression_line.count('(') > expression_line.count(')'):
raise SyntaxError('Closing bracket required!')
return expression_line


def check_parsing_list(parsing_list, function_dict):
"""
Check if the parsing list is valid otherwise raise an Exception for next reasons:
- expression starts with math operators (except "+" and "-")
- there is no operand in the expression line
- expression line ends with math operator or math function
:param parsing_list: list from instance of SplitOperators class
:param function_dict: dict with all functions {'operator': function, 'priority': 0}
:return: clear parsing_list as str
"""
if parsing_list[0] in operator_dict.keys():
if parsing_list[0] is not '+' and parsing_list[0] is not '-':
raise SyntaxError('Expression cannot start with "{}"'.format(parsing_list[0]))
if len(parsing_list) == 1:
if isinstance(parsing_list[0], int) or isinstance(parsing_list[0], float):
return parsing_list
raise SyntaxError('Expression must include at list one operand!')
if parsing_list[-1] in operator_dict.keys():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

вызов метода .keys можно опустить

if parsing_list[-1] in operator_dict:
   ...

raise SyntaxError('Extra operator "{}" at the end of an expression!'.format(parsing_list[-1]))
if parsing_list[-1] in function_dict.keys():
raise SyntaxError('Function "{}" without argument'.format(parsing_list[-1]))
return parsing_list


def operator_check(operator_symbol):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Мне кажется, что это лишняя функция и можно было легко обойтись без нее, просто проверять наличие ключа в словаре вместо вызова функции

"""
Check if there is an operator with this symbol in the operators_dict
:param operator_symbol: last_symbol (str) from instance of SplitOperators class
:return: clear operator_symbol as str
"""
if operator_symbol in operator_dict.keys():
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

и все же если оставлять эту функцию именно в таком виде, то я бы лучше ее написал вот так

if operator_symbol not in operator_dict:
    raise SyntaxError('Typo in math operator!')
return operator_symbol

return operator_symbol
raise SyntaxError('Typo in math operator!')


def number_check(number):
"""
Check if number is int or float
:param number: last_number (str) from instance of SplitOperators class
:return: number as int or float
"""
try:
return int(number)
except ValueError:
return float(number)


def function_check(function_name, function_dict):
"""
Check if function_name is a key in function_dict.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If function_name is "pi", "e", "tau", "inf" or "nan" convert it into float
Если говорить о константах, то это точно не функции :)
скорее это проверка токена, чем функции

Check the python version to add constant "tau".
If function_name is "pi", "e", "tau", "inf" or "nan" convert it into float
If there is no such name in function_dict Raise an Exception
:param function_name: last_letter (str) from instance of SplitOperators class
:param function_dict: dict with all functions {'operator': function, 'priority': 0}
:return: float or clear function_name as str
"""
if function_name == 'tau':
if sys.version_info >= (3, 6):
return function_dict[function_name]['operator']
else:
return 2 * function_dict['e']['operator']
elif function_name in function_dict.keys():
if isinstance(function_dict[function_name]['operator'], int) \
or isinstance(function_dict[function_name]['operator'], float):
return function_dict[function_name]['operator']
return function_name
else:
raise SyntaxError(
'There is no function with this name {}!'.format(function_name)
)
133 changes: 133 additions & 0 deletions final_task/pycalc/converter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Converter module"""

from .operator_manager import operator_dict, unary_dict
from .check_manager import check_parsing_list


class Converter:
"""Converter class"""
def __init__(self, parsing_list, functions):
"""
Generates an instance of the Converter class,
take an parsing_list as list from instance of SplitOperators class
create an empty list to put the result of converting
self.last_item is a place to put all successive "+" and "-"
and after appending any other element into self.converted_list
self.last_item become an empty str
"""
self.parsing_list = parsing_list
self.function_dict = functions
self.last_item = ""
self.converted_list = []

def _clean_add_sub_operators(self):
"""
Encapsulate function
Take all the successive of "+" and "-" (self.last)item)
and return "-" if count("-") is odd or "+" if count is even
"""
if self.last_item.count('-') % 2 == 0:
if self.converted_list[-1] == '(':
self.last_item = ""
else:
self.last_item = '+'
else:
self.last_item = '-'

def _append_to_converted_list(self, *args):
"""
Encapsulate function
Append all converted elements into self.converted_list
and update successive of "+" and "-" to empty value
"""
[self.converted_list.append(i) for i in args]
self.last_item = ""

def _number_converter(self, number):
"""
Encapsulate function
Add unary operator if number is the first element in the self.parsing_list,
add "-" to number if self.last_symbol is "-", otherwise add "+"
And if there is no self.last_symbol
:param number: is int or float object from self.parsing_list
"""
if self.last_item == '-':
if self.converted_list[-1] == 0:
self._append_to_converted_list(unary_dict[self.last_item], number)
elif self.last_item == '-' and self.converted_list[-1] != '(' \
and self.converted_list[-1] not in operator_dict.values():
self._append_to_converted_list(operator_dict[self.last_item], number)
else:
self._append_to_converted_list(-number)
else:
self._append_to_converted_list(number)

def _operator_converter(self, operator_str):
"""
Encapsulate function
Convert math operator symbol into dictionary with built-in math operator
Raise an exception if there is no operand between two math operators
:param operator_str: is one math symbol from self.parsing_list
"""
if operator_str == '-' or operator_str == '+':
self.last_item += operator_str
else:
try:
if self.converted_list[-1]['operator']:
raise SyntaxError('Missing operand between two math operators!')
except TypeError:
self._append_to_converted_list(operator_dict[operator_str])

def _function_converter(self, function):
"""
Encapsulate function
Convert math function name into dictionary with built-in math function
Check necessary of "-" before the math function
:param function_str: is math function name from self.parsing_list
"""
if self.last_item:
if self.last_item == '-' and self.converted_list[-1] != '(':
self._append_to_converted_list(
operator_dict['+'],
-1,
operator_dict['*'],
self.function_dict[function]
)
else:
self._append_to_converted_list(
-1,
operator_dict['*'],
self.function_dict[function]
)
else:
self._append_to_converted_list(self.function_dict[function])

def converter(self):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Название функции должно показывать действие. По смыслу больше подходит convert

"""
The main function of the Converter class
Call function to check if the parsing_list is valid
Go thought the parsing_list and call necessary encapsulate function
for every number, operator or function
:return: converted_list
"""
check_parsing_list(self.parsing_list, self.function_dict)
if self.parsing_list[0] in operator_dict.keys(): # parsing_list[0] == '+' or '-'
self.converted_list.append(0)
for i in self.parsing_list:
if i == " ":
continue
if i != '-' and i != '+' and self.last_item:
self._clean_add_sub_operators()
if self.last_item == '+':
self._append_to_converted_list(operator_dict[self.last_item])
if isinstance(i, float) or isinstance(i, int):
self._number_converter(i)
elif i in operator_dict.keys():
self._operator_converter(i)
elif i in self.function_dict.keys():
self._function_converter(i)
else:
if self.last_item:
self._append_to_converted_list(operator_dict['-'])
self._append_to_converted_list(i)
return self.converted_list
Loading