Skip to content

Tatsiana Kavalchuk #42

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 41 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
6b776f1
Add files via upload
p4elopuh May 29, 2019
7285b24
Update __init__.py
p4elopuh May 29, 2019
d13aa1d
Update setup.py
p4elopuh May 29, 2019
74fa1a6
Add files via upload
p4elopuh May 29, 2019
d9ec494
Add files via upload
p4elopuh May 29, 2019
c55c813
Delete pycalc.py
p4elopuh May 29, 2019
6175f35
Update setup.py
p4elopuh May 29, 2019
0700309
Update pycalc.py
p4elopuh May 29, 2019
4af3546
Update __init__.py
p4elopuh May 29, 2019
fb127ba
Add files via upload
p4elopuh May 29, 2019
5364371
Change setup
p4elopuh May 29, 2019
59250ce
Update __init__
p4elopuh May 29, 2019
d082f67
Add files via upload
p4elopuh May 29, 2019
a5b7474
Change parsing function
p4elopuh May 29, 2019
93323fc
Remove blanc spaces
p4elopuh May 29, 2019
cb00b2c
Change parse logic
p4elopuh May 30, 2019
6ca42f7
Add files via upload
p4elopuh May 30, 2019
9ea6e16
Add files via upload
p4elopuh Jun 3, 2019
8594cba
Delete test.py
p4elopuh Jun 3, 2019
ab336a5
Add files via upload
p4elopuh Jun 3, 2019
dc6f276
Delete test.py
p4elopuh Jun 3, 2019
d3d6280
Add files via upload
p4elopuh Jun 4, 2019
2bee0c5
Update __init__.py
p4elopuh Jun 4, 2019
8ddfab5
Add files via upload
p4elopuh Jun 4, 2019
4871ee0
Update test.py
p4elopuh Jun 4, 2019
0c77a3a
Delete test.py
p4elopuh Jun 4, 2019
cda5c65
Update __init__.py
p4elopuh Jun 4, 2019
ac0a368
Add files via upload
p4elopuh Jun 4, 2019
5e94371
Add files via upload
p4elopuh Jun 4, 2019
05b9951
Update test.py
p4elopuh Jun 4, 2019
7ddb7fc
Update test.py
p4elopuh Jun 4, 2019
ad7a25b
Add test
p4elopuh Jun 4, 2019
d2ba285
Add files via upload
p4elopuh Jun 4, 2019
84612d7
Add tests
p4elopuh Jun 4, 2019
a5a89fd
Add files via upload
p4elopuh Jun 4, 2019
b60c3ff
Add tests
p4elopuh Jun 4, 2019
18bd110
Delete pycalc.py
p4elopuh Jun 4, 2019
dd180ca
Add files via upload
p4elopuh Jun 4, 2019
b5004fe
Add tests
p4elopuh Jun 4, 2019
09864e8
Add files via upload
p4elopuh Jun 6, 2019
5cb89ac
Add PEP8, modules
p4elopuh Jun 6, 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
1 change: 1 addition & 0 deletions final_task/pycalc/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name = "pycalc"
9 changes: 9 additions & 0 deletions final_task/pycalc/mag.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env python3

mag = 42


def magn(arg1, arg2, arg3):
if not arg1:
arg1 = mag
return arg1 + arg2 + arg3
5 changes: 5 additions & 0 deletions final_task/pycalc/my_module.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env python3


def sin(number):
return 42
343 changes: 343 additions & 0 deletions final_task/pycalc/pycalc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,343 @@
#!/usr/bin/env python3

import argparse
import operator
import math
import importlib

OPERATORS = '+-*/^%<>=! ()'
CONSTANTS = {attr: getattr(math, attr) for attr in dir(math) if isinstance(getattr(math, attr), (int, float))}
OPERATIONS = {
'+': (operator.add, 3),
'-': (operator.sub, 3),
'*': (operator.mul, 4),
'//': (operator.floordiv, 4),
'^': (operator.pow, 5),
'/': (operator.truediv, 4),
'%': (operator.mod, 4),
'<': (operator.lt, 2),
'<=': (operator.le, 2),
'==': (operator.eq, 1),
'!=': (operator.ne, 1),
'>=': (operator.ge, 2),
'>': (operator.gt, 2),
'-u': (operator.neg, 5),
'+u': (lambda x: x, 5),
}
FUNCTIONS = {attr: getattr(math, attr) for attr in dir(math) if not attr[0] == '_'}
FUNCTIONS['abs'] = abs
FUNCTIONS['round'] = round


def parse_command_line():
parser = argparse.ArgumentParser(description='Pure-python command-line calculator.')
parser.add_argument("EXPRESSION", type=str, help='expression string to evaluate')
parser.add_argument('-m', '--use-modules', nargs='*', dest="MODULE", type=str, help='additional modules to use')
args = parser.parse_args()
expression = args.EXPRESSION
modules = args.MODULE
return expression, modules


def parse_to_list(exprstr):
""" Converting the expression from string to list by moving tokens from the left side of the string
with their simultaneous assignment to numbers, operators or functions
"""
temp = ''
expr_list = []
while exprstr:
symbol = exprstr[0]
if symbol in OPERATIONS or (len(exprstr) > 1 and exprstr[:2] in OPERATIONS): # if operator
if exprstr[:2] in OPERATIONS: # if two symbol operator
symbol = exprstr[:2]
expr_list.append(symbol)
exprstr = exprstr[2:]
continue
expr_list.append(symbol)
exprstr = exprstr[1:]
elif symbol.isdigit() or symbol == '.': # if digit or start float number
for symbol_temp in exprstr: # check next symbols to find whole number
if symbol_temp.isdigit() or symbol_temp == '.': # if float
temp += symbol_temp
else:
break
if '.' in temp:
try:
expr_list.append(float(temp))
except ValueError: # if impossible to convert to float
raise ValueError('error in input number {0}'.format(temp))
else:
expr_list.append(int(temp))
exprstr = exprstr[len(temp):]
temp = ''
elif symbol.isalpha(): # if character is alphabetic
for symbol_temp in exprstr: # check the next symbols to find func or constant
if symbol_temp.isalpha() or symbol_temp.isdigit():
temp += symbol_temp # if alphabetic or digit (for funcs with digit in name)
else:
break # function name must contain only letters and digits
if temp in CONSTANTS:
expr_list.append(temp)
elif temp in FUNCTIONS:
try:
if exprstr[len(temp)] == '(':
expr_list.append(temp)
else:
raise SyntaxError('missed argument(s) for function {0}'.format(temp))
except IndexError:
raise IndexError('missed argument(s) for function {0}'.format(temp))
else: # if temp not in constants or function_dict
raise ValueError('unknown function or constant {0}'.format(temp))
exprstr = exprstr[len(temp):]
temp = ''
elif symbol in ('(', ')', ','): # if bracket or comma - move to list
expr_list.append(symbol)
exprstr = exprstr[1:]
elif symbol == ' ':
exprstr = exprstr[1:] # ignore space
else: # if different symbol - raise Error
raise SyntaxError('unsupported symbol "{0}" in expression'.format(symbol))
return expr_list


def check_unary_operator(expr_list):
""" Checking the '+' and '-' operators for unary conditions and their modifying.
Conditions for assignment to the unary group - '-' or '+' stay on first position in equation or
after another arithmetic operator or opening parentheses or comma.
"""
if expr_list[0] in ('-', '+'):
expr_list[0] += 'u'
for index in range(1, len(expr_list)):
if expr_list[index] in ('-', '+') and (expr_list[index - 1] in ('(', ',') or
expr_list[index-1] in OPERATIONS):
expr_list[index] += 'u'


def precedence(oper1, oper2):
""" Determining of operator precedence """
left_associated = ['+', '-', '*', '//', '/', '%', '<', '<=', '==', '!=', '>=', '>']
right_associated = ['^', '-u', '+u']
if oper1 in left_associated:
return OPERATIONS[oper1][1] <= OPERATIONS[oper2][1]
elif oper1 in right_associated:
return OPERATIONS[oper1][1] < OPERATIONS[oper2][1]


def shunting_yard_alg(expr_list):
""" Parsing mathematical expressions specified in infix notation to Reverse Polish Notation
by Shunting-Yard algorithm.
RPN - notation of equation in which operator or function stay after corresponding operand(s).
Operators are added according their precedence. No parentheses are needed.
Resulting notation is ready for calculation (from left to right).
"""
output_list = []
stack = []
while expr_list:
item = expr_list[0]
if isinstance(item, (int, float)) or item in CONSTANTS:
output_list.append(item)
expr_list = expr_list[1:]
elif item in FUNCTIONS:
stack.append(item)
expr_list = expr_list[1:]
elif item == ',':
expr_list = expr_list[1:]
if '(' in stack:
while stack[-1] != '(':
output_list.append(stack.pop())
else:
raise SyntaxError('the opening parentheses or comma is missed')
output_list.append(item) # comma will indicate the presence of multi parameters for the func
if expr_list and expr_list[0] == ')':
raise SyntaxError('error in the data inside the parentheses')
elif item in OPERATIONS:
if stack and (stack[-1] in OPERATIONS):
while stack and (stack[-1] in OPERATIONS):
if precedence(item, stack[-1]):
output_list.append(stack.pop())
else:
break
stack.append(item)
elif stack == [] or '(' in stack:
stack.append(item)
expr_list = expr_list[1:]
elif item == '(':
stack.append(item)
expr_list = expr_list[1:]
elif item == ')':
while stack and stack[-1] != '(':
output_list.append(stack.pop())
if stack and stack[-1] == '(':
stack.pop()
elif not stack:
raise SyntaxError('opening parentheses is missed')
if stack and (stack[-1] in FUNCTIONS):
output_list.append(stack.pop())
expr_list = expr_list[1:]
else:
while stack:
if stack[-1] == '(':
raise SyntaxError('closing parentheses is missed')
output_list.append(stack.pop())
return output_list # returning expression in Reverse Polish Notation


def perform_operation(operator, operand_1, operand_2):
""" Perform binary operator for operands """
return OPERATIONS[operator][0](operand_1, operand_2)


def perform_unary_operation(operator, operand):
""" Perform unary operator for operand """
return OPERATIONS[operator][0](operand)


def perform_function(function, *operand):
""" Perform function with one or more arguments """
try:
result = FUNCTIONS[function](*operand)
except ValueError:
raise ValueError('error in entered data for function {0}'.format(function))
# except TypeError:
# result = function_dict[function](operand)
# raise TypeError('unsupported data in function {0} parentheses'.format(function))
return result


def calculation_from_rpn(rev_pol_not_list):
""" Sequential calculation from left to right of expression in Reverse Polish Notation.
Operators and functions are applied to corresponding previous staying operand(s)
"""
stack = []
arg_stack = []
for item in rev_pol_not_list:
if item in CONSTANTS:
stack.append(CONSTANTS[item])
elif isinstance(item, (int, float)):
stack.append(item)
elif item == ',':
arg_stack.append(stack.pop())
stack.append(item)
elif item in FUNCTIONS:
if len(stack) > 1 and stack[-2] == ',':
commas = 0 # cheking the presence of commas and sum all successive ones
for index in range(-2, -len(stack)-1, -1):
if stack[index] == ',':
commas += 1
else:
break
while commas:
params = arg_stack[-commas:]
params.append(stack[-1])
try:
intermediate_result = perform_function(item, *params)
for index in range(commas + 1):
stack.pop()
for index in range(commas):
arg_stack.pop()
stack.append(intermediate_result)
break
except TypeError:
commas -= 1
continue
else:
try:
intermediate_result = perform_function(item, stack.pop())
stack.append(intermediate_result)
except TypeError:
raise TypeError("unsupported operand(s)")
else:
try:
intermediate_result = perform_function(item, stack.pop())
stack.append(intermediate_result)
except TypeError:
raise TypeError("unsupported operand for function '{0}'".format(item))
except OverflowError:
raise OverflowError(f'result of {item} too large to be represented')
elif item in OPERATIONS:
if item in ('-u', '+u'):
intermediate_result = perform_unary_operation(item, stack.pop())
else:
try:
operand_2, operand_1 = stack.pop(), stack.pop()
intermediate_result = perform_operation(item, operand_1, operand_2)
except ZeroDivisionError:
raise ZeroDivisionError('division by zero')
except IndexError:
raise IndexError('insufficient amount of operands')
except OverflowError:
raise OverflowError(f'result of {item} too large to be represented')
stack.append(intermediate_result)
if len(stack) == 1:
result = stack[0]
return result
else:
raise SyntaxError('insufficient amount of operators or function or too many operands/arguments')


def check_empty_operators(exprstr):
""" Checking expression for empty string or string containing only operators or parentheses """
if len(exprstr) == 0:
raise SyntaxError('empty expression')
elif set(exprstr).issubset(set(OPERATORS)):
raise SyntaxError('no digits, constants or functions in expression')
else:
return False


def check_brackets(exprstr):
""" Checking for brackets balance """
if ('(' and ')') in exprstr:
Copy link
Collaborator

Choose a reason for hiding this comment

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

В чём назначение этой проверки?

Copy link
Author

Choose a reason for hiding this comment

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

Проверить, если в выражении есть скобки, то сбалансировано ли их количество, а также нет ли закрывающей скобки раньше открывающей.
Но после вопроса уже сомневаюсь в необходимости.

Copy link
Author

Choose a reason for hiding this comment

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

В условиях цейтнота добавила возможность импорта модулей и загрузила тестовые файлы для проверки приоритета функций и констант, и не обратила внимания, что эта функциональность не проверяется скриптом.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Выражение ('(' and ')') всегда будет возвращать ')'. Таким образом if ('(' and ')') in exprstr проверяет есть ли в строке )

if exprstr.count('(') == exprstr.count(')'):
if exprstr.index(')') < exprstr.index('('):
raise SyntaxError('brackets are not balanced')
return True
else:
raise SyntaxError('brackets are not balanced')
else:
return True


def calculation(exprstr):
""" Calculation of string type argument from command line """
if (not check_empty_operators(exprstr)) and check_brackets(exprstr):
expr_list = parse_to_list(exprstr)
check_unary_operator(expr_list)
return calculation_from_rpn(shunting_yard_alg(expr_list))


def import_new_module(module_name):
""" Trying to import module provided with -m (--use-modules) flag and
updating the constants and functions dictionaries.
Functions and constants from user defined modules have higher priority.
in case of name conflict then stuff from math module or built-in functions
If the module cannot be imported info about this will be printed
but calculation will be tried to execute
"""
try:
module = importlib.import_module(module_name)
try:
CONSTANTS.update({attr: getattr(module, attr) for attr in dir(module)
if isinstance(getattr(module, attr), (int, float))})
FUNCTIONS.update({attr: getattr(module, attr) for attr in dir(module) if not attr[0] == '_'})
except Exception:
print(f'Smth bad with new module {module_name}')
pass
# raise Exception('Smth bad with new module')
except ImportError:
print(f'Unable to import the module {module_name}')


def main():
try:
expression, modules = parse_command_line()
if modules:
for module in modules:
import_new_module(module)
print(calculation(expression))
except Exception as err:
print('ERROR: ', err)


if __name__ == '__main__':
main()
1 change: 1 addition & 0 deletions final_task/pycalc/test/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name = 'pycalc'
21 changes: 21 additions & 0 deletions final_task/setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import setuptools


setuptools.setup(
name="pycalc",
version="0.1",
author="Tatsiana Kavalchuk",
author_email="p4elopuh@gmail.com",
description="Pure-python command-line calculator",
packages=setuptools.find_packages(),
entry_points={
'console_scripts': [
'pycalc=pycalc.pycalc:main',
],
},
classifiers=[
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
],
)
Loading