From 43fc5e7750b90062d302606310d854a4be5f5c3e Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 28 Apr 2019 13:45:43 +0300 Subject: [PATCH 01/19] feat: implement code --- final_task/calc/pycalc.py | 499 ++++++++++++++++++++++++++++++++++++++ final_task/calc/tests.py | 324 +++++++++++++++++++++++++ final_task/setup.py | 8 + 3 files changed, 831 insertions(+) create mode 100755 final_task/calc/pycalc.py create mode 100644 final_task/calc/tests.py diff --git a/final_task/calc/pycalc.py b/final_task/calc/pycalc.py new file mode 100755 index 00000000..c223fae5 --- /dev/null +++ b/final_task/calc/pycalc.py @@ -0,0 +1,499 @@ +# -*- coding: utf-8 -*- +""" +The module is designed to work with mathematical expressions. + +Example: + $ python pycalc.py -h + $ python pycalc.py 'expretion' + $ python pycalc.py 'expretion' -m 'module1' 'module2' + +Attributes: + LEFT_BRACKET (str): possible representation of the bracket ( in the expression. + RIGHT_BRACKET (str): possible representation of the bracket ) in the expression. + MULTIPLE (str): possible representation of the operation * in the expression. + POWER (str): possible representation of the operation ** in the expression. + TRUE_DIVISION (str): possible representation of the operation / in the expression. + FLOOR_DIVISION (str): possible representation of the operation // in the expression. + MODULE (str): possible representation of the operation % in the expression. + PLUS (str): possible representation of the operation + in the expression. + MINUS (str): possible representation of the operation - in the expression. + LESS (str): possible representation of the operation < in the expression. + LESS_OR_EQUAL (str): possible representation of the operation <= in the expression. + GREAT (str): possible representation of the operation > in the expression. + GREAT_OR_EQUAL (str): possible representation of the operation >= in the expression. + EQUAL (str): possible representation of the operation == in the expression. + NOT_EQUAL (str): possible representation of the operation != in the expression. + REGEXP_DIGIT (rstr): regular expressions for finding numbers. + REGEXP_SIMPLE_DIGIT (rstr): regular expressions for checking common digits. + REGEXP_SCREENING (rstr): regular expressions for operation screening. + REGEX_NAME (rstr): regular expressions for finding names. + REGEXP_BACKETS (rstr): regular expressions for finding brackets. + REGEXP_FUNCTION (rstr): regular expressions for finding functons. + REGEXP_CONSTANT (rstr): regular expressions for finding constant names. + REGEXP_UNARY (rstr): regular expressions for finding unary operation. + REGEXP_BYNARY (rstr): regular expressions for finding bynary operation. + REGEXP_COMPARE (rstr): regular expressions for finding compare operation. + REGEXP_INCORECT_EXPRETION (rstr): regular expressions for defining invalid expressions. + HAS_COMPARE (bool): determines whether the expression has a comparison operation. + LIBRARY (dict): library of available operations. +""" + +import argparse +import re +from collections import namedtuple +from functools import reduce +from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt + +LEFT_BRACKET = '(' +RIGHT_BRACKET = ')' +MULTIPLE = '*' +POWER = '^' +TRUE_DIVISION = '/' +FLOOR_DIVISION = '//' +MODULE = '%' +PLUS = '+' +MINUS = '-' +LESS = '<' +LESS_OR_EQUAL = '<=' +GREAT = '>' +GREAT_OR_EQUAL = '>=' +EQUAL = '==' +NOT_EQUAL = '!=' + +REGEXP_DIGIT = r'[+-]?\d+\.\d+e\+\d+|[+-]?\d+\.?\d*|[+-]?\d*\.?\d+' +REGEXP_SIMPLE_DIGIT = rf'^({REGEXP_DIGIT})$' +REGEXP_SCREENING = rf'\{{operation}}' +REGEX_NAME = r'\w+' +REGEXP_BACKETS = r'(?:^|\W)(\([^)(]+\))' +REGEXP_FUNCTION = rf'(?P(?P{REGEX_NAME})\((?P(?:{REGEXP_DIGIT})(?:,(?:{REGEXP_DIGIT})+)*|)\))' +REGEXP_CONSTANT = rf'(?P{REGEXP_DIGIT}|{REGEX_NAME}\(?)' +REGEXP_UNARY = rf'([-+]{{2,}})' +REGEXP_BYNARY = rf'((?:{REGEXP_DIGIT})(?:{{operation}}(?:{REGEXP_DIGIT}))+)' +REGEXP_COMPARE = rf'^{REGEXP_BYNARY}$'.format(operation='[=!<>]{1,2}') +REGEXP_INCORECT_EXPRETION = ( + r'.?\W\d+\s*\(|' + r'^\d+\s*\(|' + r'^\W*$|' + r'\d+[)(<=!>][<>!]\d+|' + r'\W\d+[)(<=!>][]\d+|' + r'\w+\s+\w+|' + r'[-+*^\/%<=!>]+\s+[\/*^%<=!>]+|' + r'^[\/*^%<=!>]|' + r'[-+*^\/%<=!>]$' +) + +HAS_COMPARE = False +LIBRARY = { + 'abs': abs, + 'round': round, +} + + +def exec_operation(x, y, operation=MULTIPLE): + """Executes the operation and returns the result. + + Args: + x (str): String representation of a number. + y (str): String representation of a number. + + Returns: + str: result of calculations. + + Raises: + ValueError: If `operation` is not found`. + """ + if operation == POWER and y[0] == MINUS: + a, b = float(y[1:]), float(x) + if operation == POWER: + a, b = float(y), float(x) + else: + a, b = float(x), float(y) + + result = None + # arithmetic operation + if operation == MULTIPLE: + result = mul(a, b) + elif operation == POWER: + result = pow(a, b) + elif operation == TRUE_DIVISION: + result = truediv(a, b) + elif operation == FLOOR_DIVISION: + result = floordiv(a, b) + elif operation == MODULE: + result = mod(a, b) + elif operation == PLUS: + result = add(a, b) + elif operation == MINUS: + result = sub(a, b) + + if operation == POWER and y[0] == MINUS: + return f'{MINUS}{result}' + if result is not None: + return f'{PLUS}{result}' if result > 0 else str(result) + + # comparison operation + if operation == LESS: + result = lt(a, b) + elif operation == LESS_OR_EQUAL: + result = le(a, b) + elif operation == EQUAL: + result = eq(a, b) + elif operation == NOT_EQUAL: + result = ne(a, b) + elif operation == GREAT_OR_EQUAL: + result = ge(a, b) + elif operation == GREAT: + result = gt(a, b) + + if result is not None: + return str(int(result)) + + raise ValueError('operation was not found') + + +def replace_constant(expr): + """ + Calculates constant operations. + + Args: + expr (str): String mathematical expression. + + Returns: + str: Updated expression. + """ + results = re.finditer(REGEXP_CONSTANT, expr) + + for m in results: + name = m.group('name') + + if name[-1] == LEFT_BRACKET or re.match(REGEXP_DIGIT, name): + continue + + answer = str(LIBRARY[name]) + arr = expr.split(name) + + for idx, piece in enumerate(arr[:-1]): + if piece and piece[-1].isalnum(): + arr[idx] = f'{piece}{name}' + elif piece or not idx: + arr[idx] = f'{piece}{answer}' + + expr = ''.join(arr) + + return expr + + +def replace_fanction(expr): + """ + Calculates function operations. + + Args: + expr (str): String mathematical expression. + + Returns: + str: Updated expression. + """ + results = re.finditer(REGEXP_FUNCTION, expr) + + for m in results: + func = m.group('name') + pattern = m.group('pattern') + args = filter(bool, m.group('args').split(',')) + args = [float(v) for v in args] + answer = str(LIBRARY[func](*args)) + expr = expr.replace(pattern, answer) + + return expr + + +def replace_unary_operator(expr): + """ + Calculates unary operations. + + Args: + expr (str): String mathematical expression. + + Returns: + str: Updated expression. + """ + results = re.findall(REGEXP_UNARY, expr) + results.sort(key=len, reverse=True) + + for m in results: + answer = MINUS if m.count(MINUS) % 2 else PLUS + expr = expr.replace(m, answer) + + return expr + + +def replace_compare_operator(expr, *operations): + """ + Calculates compare operations. + + Args: + expr (str): String mathematical expression. + *operations (list): List of operations that need to be done on the expression. + + Returns: + str: Updated expression. + """ + if re.search(REGEXP_COMPARE, expr): + return replace_bynary_operator(expr, *operations) + + return expr + + +def replace_bynary_operator(expr, *operations): + """ + Calculates binary operations. + + Args: + expr (str): String mathematical expression. + *operations (list): List of operations that need to be done on the expression. + + Returns: + str: Updated expression. + """ + for o in operations: + delimeter = o + if o == PLUS or o == MULTIPLE or o == POWER: + delimeter = REGEXP_SCREENING.format(operation=o) + + regexp = REGEXP_BYNARY.format(operation=delimeter) + results = re.findall(regexp, expr) + for m in results: + arr = list(filter(bool, m.split(o))) + if o == MINUS and m[0] == MINUS: + arr[0] = f'{MINUS}{arr[0]}' + if o == POWER: + arr = arr[::-1] + + answer = reduce(lambda a, b: exec_operation(a, b, operation=o), arr) + expr = expr.replace(m, answer) + + return expr + + +def replace_brackets(expr): + """ + Calculates the expression in brackets. + + Args: + expr (str): String mathematical expression. + + Returns: + str: Updated expression. + """ + results = re.findall(REGEXP_BACKETS, expr) + + for m in results: + answer = calc(m[1:-1]) + expr = expr.replace(m, answer) + + return expr + + +def calc(expr): + """ + Calculates the result from the getting expression. + + Args: + expr (str): String mathematical expression. + + Returns: + str: result of calculations. + """ + Operation = namedtuple('Operation', 'func args') + OPERATION_PRIORITY = [ + Operation(replace_constant, []), + Operation(replace_fanction, []), + Operation(replace_brackets, []), + Operation(replace_unary_operator, []), + Operation(replace_bynary_operator, [POWER]), + Operation(replace_bynary_operator, [MULTIPLE, TRUE_DIVISION, FLOOR_DIVISION, MODULE]), + Operation(replace_bynary_operator, [PLUS, MINUS]), + Operation(replace_compare_operator, [EQUAL, NOT_EQUAL, GREAT, GREAT_OR_EQUAL, LESS, LESS_OR_EQUAL]), + ] + + pattern = re.compile(REGEXP_SIMPLE_DIGIT) + while True: + for inst in OPERATION_PRIORITY: + expr = inst.func(expr, *inst.args) + if pattern.match(expr): + return expr + + return expr + + +def import_modules(*modules): + """Imports the modules from the list to the global field LIBRARY.""" + for module in modules: + LIBRARY.update(__import__(module).__dict__) + + +def check_spaces(expr): + """ + Checks if an expression has the wrong elements. + + Args: + expr (str): String mathematical expression. + + Returns: + str: cleared expression from spaces. + + Raises: + ValueError: If `expr` is not correct`. + """ + res = re.findall(REGEXP_INCORECT_EXPRETION, expr) + if res: + raise ValueError('expression is not correct') + + return expr.replace(' ', '') + + +def check_brackets(expr): + """ + Checks if all brackets have a pair. + + Args: + expr (str): String mathematical expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + stack = [] + for c in expr: + if c == LEFT_BRACKET: + stack.append(c) + elif c == RIGHT_BRACKET and (not stack or stack.pop() != LEFT_BRACKET): + raise ValueError('brackets are not balanced') + + if stack: + raise ValueError('brackets are not balanced') + + +def check_constant(expr): + """ + Checks if all constants in the expression are available. + + Args: + expr (str): String mathematical expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + results = re.finditer(REGEXP_CONSTANT, expr) + + for m in results: + name = m.group('name') + + if name[-1] == LEFT_BRACKET or re.match(REGEXP_DIGIT, name): + continue + + if name[0].isdigit(): + raise ValueError(f'constant {name} can not start with digit') + + if name not in LIBRARY or callable(LIBRARY[name]): + raise ValueError(f'there is no such constant {name}') + + +def check_function(expr): + """ + Checks if all functions in the expression are available. + + Args: + expr (str): String mathematical expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + results = re.finditer(REGEXP_FUNCTION, expr) + for m in results: + name = m.group('name') + pattern = m.group('pattern') + + if name[0].isdigit(): + raise ValueError(f'function {pattern} can not start with digit') + + if name not in LIBRARY or not callable(LIBRARY[name]): + raise ValueError(f'there is no such function {pattern}') + + +def check_expression(expr): + """ + Checks the expression for correctness. + + Args: + expr (str): String mathematical expression. + + Returns: + str: cleared expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + expr = check_spaces(expr) + check_brackets(expr) + check_constant(expr) + check_function(expr) + + global HAS_COMPARE + HAS_COMPARE = True if re.search(r'[=!<>]', expr) else False + + return expr + + +def parse_query(): + """ + Convert argument strings to objects and assign them as attributes of the namespace. + + Returns: + Namespace: If there is no fractional part. + """ + parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') + parser.add_argument('expr', metavar='EXPRESSION', help='expression string to evaluate') + parser.add_argument('-m', + '--use-modules', + default=[], + dest='modules', + metavar='MODULE', + nargs='+', + help='additional modules to use') + + return parser.parse_args() + + +def convert_answer(string): + """ + Converts the resulting string to the desired type. + + Args: + string (str): String representation of a number. + + Returns: + int: If there is no fractional part. + float: If there is a fractional part. + bool: If the expression contained some equality. + """ + num = float(string) + match = re.search(r'\.0*[1-9]', string) + num = num if match else int(num) + + return bool(num) if HAS_COMPARE else num + + +def main(): + """Performs processing and calculation of the request from the command line and displays it on the screen.""" + try: + args = parse_query() + import_modules('math', *args.modules) + expr = check_expression(args.expr) + answer = calc(expr) + answer = convert_answer(answer) + print(answer) + except Exception as e: + print(f'ERROR: {e}') + + +if __name__ == '__main__': + main() diff --git a/final_task/calc/tests.py b/final_task/calc/tests.py new file mode 100644 index 00000000..646e2d55 --- /dev/null +++ b/final_task/calc/tests.py @@ -0,0 +1,324 @@ +import io +import unittest +from unittest.mock import patch +import pycalc + + +class TestCalcFunction(unittest.TestCase): + def test_import_modules(self): + pycalc.import_modules() + self.assertFalse('path' in pycalc.LIBRARY) + + pycalc.import_modules('os') + self.assertTrue('path' in pycalc.LIBRARY) + + pycalc.import_modules('sys', 'time') + self.assertTrue('stdin' in pycalc.LIBRARY) + self.assertTrue('clock' in pycalc.LIBRARY) + + with self.assertRaises(ModuleNotFoundError): + pycalc.import_modules('bad_module') + + def test_exec_operation(self): + a, b = '3', '7' + + with self.subTest("Arithmetic operations return currect sting value"): + self.assertEqual(pycalc.exec_operation(a, b, pycalc.MULTIPLE), '+21.0') + self.assertEqual(pycalc.exec_operation(b, a, pycalc.POWER), '+2187.0') + self.assertEqual(pycalc.exec_operation(a, b, pycalc.TRUE_DIVISION), '+0.42857142857142855') + self.assertEqual(pycalc.exec_operation(a, b, pycalc.FLOOR_DIVISION), '0.0') + self.assertEqual(pycalc.exec_operation(a, b, pycalc.MODULE), '+3.0') + self.assertEqual(pycalc.exec_operation(a, b, pycalc.PLUS), '+10.0') + self.assertEqual(pycalc.exec_operation(a, b, pycalc.MINUS), '-4.0') + + with self.subTest("Comparison operations return currect sting value 1 (True) or 0 (False)"): + self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.LESS)), a < b) + self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.LESS_OR_EQUAL)), a <= b) + self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.EQUAL)), a == b) + self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.NOT_EQUAL)), a != b) + self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.GREAT_OR_EQUAL)), a >= b) + self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.GREAT)), a > b) + + with self.subTest("If don't have operation throw error"): + self.assertRaises(ValueError, lambda: pycalc.exec_operation(a, b, '**')) + self.assertRaises(ValueError, lambda: pycalc.exec_operation(a, b, '&&')) + self.assertRaises(ValueError, lambda: pycalc.exec_operation(a, b, '||')) + + def test_replace_constant(self): + with self.subTest("Replaces constant name to constant value"): + self.assertEqual(pycalc.replace_constant('e'), '2.718281828459045') + self.assertEqual(pycalc.replace_constant('e + e'), '2.718281828459045 + 2.718281828459045') + + with self.subTest("Does not touch function and digit"): + self.assertEqual(pycalc.replace_constant('log()'), 'log()') + self.assertEqual(pycalc.replace_constant('log(e)'), 'log(2.718281828459045)') + self.assertEqual(pycalc.replace_constant('log(e) + e'), 'log(2.718281828459045) + 2.718281828459045') + self.assertEqual(pycalc.replace_constant('2.161727821137838e+16'), '2.161727821137838e+16') + self.assertEqual(pycalc.replace_constant('time(e) + e'), 'time(2.718281828459045) + 2.718281828459045') + + def test_replace_fanction(self): + with self.subTest("Replaces function expression to function result"): + self.assertEqual(pycalc.replace_fanction('log10(100)'), '2.0') + self.assertEqual(pycalc.replace_fanction('log10(100) + log10(100)'), '2.0 + 2.0') + self.assertEqual(pycalc.replace_fanction('log(100,10)'), '2.0') + + with self.subTest("Does not touch constants"): + self.assertEqual(pycalc.replace_fanction('log10(100) + log(e)'), '2.0 + log(e)') + self.assertEqual(pycalc.replace_fanction('log10(100) + e'), '2.0 + e') + self.assertEqual(pycalc.replace_fanction('log10(e)'), 'log10(e)') + self.assertEqual(pycalc.replace_fanction('log10(e) + 1'), 'log10(e) + 1') + + with self.subTest("Can receive seveeral arguments"): + self.assertEqual(pycalc.replace_fanction('log(100,10)'), '2.0') + self.assertEqual(pycalc.replace_fanction('hypot(-2,0)'), '2.0') + self.assertEqual(pycalc.replace_fanction('hypot(-2,0) + log(100,10)'), '2.0 + 2.0') + + def test_replace_unary_operator(self): + with self.subTest("Replaces sequence of unary operators"): + self.assertEqual(pycalc.replace_unary_operator('+---+1'), '-1') + self.assertEqual(pycalc.replace_unary_operator('+--+1'), '+1') + self.assertEqual(pycalc.replace_unary_operator('-13'), '-13') + self.assertEqual(pycalc.replace_unary_operator('-+---+-1'), '-1') + + def test_replace_bynary_operator(self): + with self.subTest("Replaces sequence of bynary operators"): + self.assertEqual(float(pycalc.replace_bynary_operator('1*2*3*4', pycalc.MULTIPLE)), eval('1*2*3*4')) + self.assertEqual(float(pycalc.replace_bynary_operator('2^3^4', pycalc.POWER)), eval('2**3**4')) + self.assertEqual(float(pycalc.replace_bynary_operator('1/2/3/4', pycalc.TRUE_DIVISION)), eval('1/2/3/4')) + self.assertEqual(float(pycalc.replace_bynary_operator('1//2//3', pycalc.FLOOR_DIVISION)), eval('1//2//3')) + self.assertEqual(float(pycalc.replace_bynary_operator('1%2%3%4', pycalc.MODULE)), eval('1%2%3%4')) + self.assertEqual(float(pycalc.replace_bynary_operator('1+2+3+4', pycalc.PLUS)), eval('1+2+3+4')) + self.assertEqual(float(pycalc.replace_bynary_operator('1-2-3-4', pycalc.MINUS)), eval('1-2-3-4')) + self.assertEqual(float(pycalc.replace_bynary_operator('-1-2-3-4', pycalc.MINUS)), eval('-1-2-3-4')) + + with self.subTest("May receive several operators"): + val = '1*2*3+1+2+3' + self.assertEqual(float(pycalc.replace_bynary_operator(val, pycalc.MULTIPLE, pycalc.PLUS)), eval(val)) + val = '-1-2-3-4+1+2+3+4' + self.assertEqual(float(pycalc.replace_bynary_operator(val, pycalc.MINUS, pycalc.PLUS)), eval(val)) + + def test_replace_brackets(self): + with self.subTest("Replaces inner brackets to result"): + self.assertEqual(pycalc.replace_brackets('(1*2*3*4)'), '+24.0') + self.assertEqual(pycalc.replace_brackets('1+(2+3*2)*3'), '1++8.0*3') + self.assertEqual(pycalc.replace_brackets('10*(2+1)'), '10*+3.0') + self.assertEqual(pycalc.replace_brackets('(100)'), '100') + self.assertEqual(pycalc.replace_brackets('(((100)))'), '((100))') + + with self.subTest("Does not touch function brakets"): + self.assertEqual(pycalc.replace_brackets('log(1*2*3*4)'), 'log(1*2*3*4)') + self.assertEqual(pycalc.replace_brackets('log((5+95),10)'), 'log(+100.0,10)') + + def test_calc(self): + pycalc.import_modules('math', 'time') + + with self.subTest("Calculates unary operations"): + self.assertEqual(pycalc.calc('-13'), '-13') + self.assertEqual(pycalc.calc('6-(-13)'), '+19.0') + self.assertEqual(pycalc.calc('1---1'), '0.0') + self.assertEqual(pycalc.calc('-+---+-1'), '-1') + + with self.subTest("Calculates priority operations"): + self.assertEqual(pycalc.calc('1+2*2'), '+5.0') + self.assertEqual(pycalc.calc('1+(2+3*2)*3'), '+25.0') + self.assertEqual(pycalc.calc('10*(2+1)'), '+30.0') + self.assertEqual(pycalc.calc('10^(2+1)'), '+1000.0') + self.assertEqual(pycalc.calc('100/3^2'), '+11.11111111111111') + self.assertEqual(pycalc.calc('100/3%2^2'), '+1.3333333333333357') + + with self.subTest("Calculates constants and functions"): + self.assertEqual(pycalc.calc('pi+e'), '+5.859874482048838') + self.assertEqual(pycalc.calc('log(e)'), '1.0') + self.assertEqual(pycalc.calc('sin(pi/2)'), '1.0') + self.assertEqual(pycalc.calc('log10(100)'), '2.0') + self.assertEqual(pycalc.calc('sin(pi/2)*111*6'), '+666.0') + self.assertEqual(pycalc.calc('2*sin(pi/2)'), '+2.0') + + with self.subTest("Calculates assotiacive operations"): + self.assertEqual(pycalc.calc('102%12%7'), '+6.0') + self.assertEqual(pycalc.calc('100/4/3'), '+8.333333333333334') + self.assertIn(pycalc.calc('2^3^4'), ['2417851639229258349412352.0', '+2.4178516392292583e+24']) + + with self.subTest("Calculates comparation operations"): + self.assertEqual(pycalc.calc('1+2*3==1+2*3'), '1') + self.assertEqual(pycalc.calc('e^5>=e^5+1'), '0') + self.assertEqual(pycalc.calc('1+2*4/3+1!=1+2*4/3+2'), '1') + + with self.subTest("Calculates common operations"): + self.assertEqual(pycalc.calc('(100)'), '100') + self.assertEqual(pycalc.calc('666'), '666') + self.assertEqual(pycalc.calc('-.1'), '-.1') + self.assertEqual(pycalc.calc('1/3'), '+0.3333333333333333') + self.assertEqual(pycalc.calc('1.0/3.0'), '+0.3333333333333333') + self.assertEqual(pycalc.calc('.1*2.0^56.0'), '+7205759403792794.0') + self.assertEqual(pycalc.calc('e^34'), '+583461742527453.9') + self.assertEqual(pycalc.calc('(2.0^(pi/pi+e/e+2.0^0.0))'), '+8.0') + self.assertEqual(pycalc.calc('(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)'), '+2.0') + self.assertEqual(pycalc.calc('sin(pi/2^1)+log(1*4+2^2+1,3^2)'), '+2.0') + self.assertEqual(pycalc.calc('10*e^0*log10(.4-5/-0.1-10)--abs(-53/10)+-5'), '+16.36381365110605') + self.assertEqual(pycalc.calc('2.0^(2.0^2.0*2.0^2.0)'), '+65536.0') + self.assertEqual(pycalc.calc('sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))'), '0.76638122986603') + + val = ('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)') + self.assertEqual(pycalc.calc(val), '0.5361064001012783') + + def test_check_spaces(self): + with self.subTest("Throws error if spaces is not correct"): + self.assertRaises(ValueError, lambda: pycalc.check_spaces('')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('------')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('-')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('+')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('1-')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('1 + 1 2 3 4 5 6 ')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('* *')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('/ /')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('/ *')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('+ *')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('1 2')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('= =')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('! =')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('<-+!')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('==7')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('1 + 2(3 * 4))')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('1 = = 2')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('1<>2')) + self.assertRaises(ValueError, lambda: pycalc.check_spaces('1><2')) + + with self.subTest("removes spaces and returns new expretion"): + self.assertEqual(pycalc.check_spaces('1 + 2'), '1+2') + self.assertEqual(pycalc.check_spaces('1-2'), '1-2') + self.assertEqual(pycalc.check_spaces('1 * - 2'), '1*-2') + self.assertEqual(pycalc.check_spaces('1 == 2'), '1==2') + self.assertEqual(pycalc.check_spaces('1 <= 2'), '1<=2') + self.assertEqual(pycalc.check_spaces('1 - sin (1, 2, 3) + - 2'), '1-sin(1,2,3)+-2') + self.assertEqual(pycalc.check_spaces('sin(pi/2)'), 'sin(pi/2)') + self.assertTrue(pycalc.check_spaces('sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))')) + self.assertTrue(pycalc.check_spaces('time()-e-(1+1)/60+1-1*1//10000%1000^2==1==1<=3>=5<1>1')) + + val = ('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)') + self.assertTrue(pycalc.check_spaces(val)) + + def test_check_brackets(self): + with self.subTest("Throws error if brackets are not unpaired"): + self.assertRaises(ValueError, lambda: pycalc.check_brackets('(')) + self.assertRaises(ValueError, lambda: pycalc.check_brackets(')')) + self.assertRaises(ValueError, lambda: pycalc.check_brackets('())(')) + self.assertRaises(ValueError, lambda: pycalc.check_brackets('(()))')) + self.assertRaises(ValueError, lambda: pycalc.check_brackets(')()(')) + + with self.subTest("returns nothing if expretion is good"): + self.assertIsNone(pycalc.check_brackets('')) + self.assertIsNone(pycalc.check_brackets('()')) + self.assertIsNone(pycalc.check_brackets('((()))()')) + self.assertIsNone(pycalc.check_brackets('()()()()')) + self.assertIsNone(pycalc.check_brackets('(()(()())())')) + + def test_check_constant(self): + with self.subTest("Throws error if environment does not have constant"): + self.assertRaises(ValueError, lambda: pycalc.check_constant('constant')) + self.assertRaises(ValueError, lambda: pycalc.check_constant('constant + 5')) + self.assertRaises(ValueError, lambda: pycalc.check_constant('sin(1) + constant + 7')) + + with self.subTest("Throws error if constant name starts with digit"): + self.assertRaises(ValueError, lambda: pycalc.check_constant('10constant')) + self.assertRaises(ValueError, lambda: pycalc.check_constant('10constant + 5')) + self.assertRaises(ValueError, lambda: pycalc.check_constant('sin(1) + 10constant + 7')) + + with self.subTest("returns nothing if expretion is good"): + pycalc.import_modules('math') + + self.assertIsNone(pycalc.check_constant('')) + self.assertIsNone(pycalc.check_constant('e')) + self.assertIsNone(pycalc.check_constant('sin(21)')) + self.assertIsNone(pycalc.check_constant('sin(21) + e')) + self.assertIsNone(pycalc.check_constant('2.4178516392292583e+24 + 5')) + + def test_check_function(self): + with self.subTest("Throws error if environment does not have function"): + self.assertRaises(ValueError, lambda: pycalc.check_function('multiply()')) + self.assertRaises(ValueError, lambda: pycalc.check_function('multiply(5,7)')) + self.assertRaises(ValueError, lambda: pycalc.check_function('multiply() + 7')) + + with self.subTest("Throws error if function name starts with digit"): + self.assertRaises(ValueError, lambda: pycalc.check_function('10log()')) + self.assertRaises(ValueError, lambda: pycalc.check_function('10log(1)')) + self.assertRaises(ValueError, lambda: pycalc.check_function('10log(5,7)')) + self.assertRaises(ValueError, lambda: pycalc.check_function('10log() + 7')) + + with self.subTest("returns nothing if expretion is good"): + pycalc.import_modules('math') + + self.assertIsNone(pycalc.check_function('')) + self.assertIsNone(pycalc.check_function('e')) + self.assertIsNone(pycalc.check_function('sin(21)')) + self.assertIsNone(pycalc.check_function('sin(21) + e')) + self.assertIsNone(pycalc.check_function('2.4178516392292583e+24 + 5')) + + def test_check_expression(self): + with self.subTest("calls methods check_spaces, check_brackets, check_constant, check_function"), \ + patch('pycalc.check_spaces', return_value='1') as check_spaces, \ + patch('pycalc.check_brackets') as check_brackets, \ + patch('pycalc.check_constant') as check_constant, \ + patch('pycalc.check_function') as check_function: + + pycalc.check_expression('1') + + self.assertTrue(check_spaces.called) + self.assertTrue(check_brackets.called) + self.assertTrue(check_constant.called) + self.assertTrue(check_function.called) + + with self.subTest("returns expression without spaces"): + self.assertEqual(pycalc.check_expression('1 + 2'), '1+2') + self.assertEqual(pycalc.check_expression('1-2'), '1-2') + self.assertEqual(pycalc.check_expression('1 * - 2'), '1*-2') + self.assertEqual(pycalc.check_expression('1 - sin (1, 2, 3) + - 2'), '1-sin(1,2,3)+-2') + + def test_parse_query(self): + with self.subTest("return currect value"): + import sys + + sys.argv = ['pycalc.py', 'time()/60', '-m', 'time', 'os', 'math'] + args = pycalc.parse_query() + self.assertEqual(args.expr, 'time()/60') + self.assertEqual(args.modules, ['time', 'os', 'math']) + + def test_convert_answer(self): + with self.subTest("returns correct answer"): + pycalc.HAS_COMPARE = False + self.assertEqual(pycalc.convert_answer('-1'), -1) + self.assertEqual(pycalc.convert_answer('0'), 0) + pycalc.HAS_COMPARE = True + self.assertEqual(pycalc.convert_answer('-1'), True) + self.assertEqual(pycalc.convert_answer('0'), False) + + def test_main(self): + with self.subTest("calls methods parse_query, import_modules, check_expression, calc, convert_answer"), \ + patch('pycalc.parse_query') as parse_query, \ + patch('pycalc.import_modules') as import_modules, \ + patch('pycalc.check_expression') as check_expression, \ + patch('pycalc.calc') as calc, \ + patch('pycalc.convert_answer', return_value='1') as convert_answer, \ + patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: + + pycalc.main() + self.assertEqual(mock_stdout.getvalue(), '1\n') + + self.assertTrue(parse_query.called) + self.assertTrue(import_modules.called) + self.assertTrue(calc.called) + self.assertTrue(check_expression.called) + self.assertTrue(convert_answer.called) + + with self.subTest("catchs exception"), \ + patch('pycalc.parse_query') as parse_query, \ + patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: + + pycalc.main() + self.assertTrue(mock_stdout.getvalue().startswith('ERROR: ')) + + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/setup.py b/final_task/setup.py index e69de29b..73772a62 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -0,0 +1,8 @@ +from setuptools import setup, find_packages +from os.path import join, dirname + +setup( + name='pycalc', + version='1.0', + packages=find_packages(), +) From 729d1190095c7ea0bb5458050e95115a2449845a Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 28 Apr 2019 14:09:01 +0300 Subject: [PATCH 02/19] fix: change 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 73772a62..706935eb 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -4,5 +4,5 @@ setup( name='pycalc', version='1.0', - packages=find_packages(), + packages=['calc'], ) From 721d11667d73a448dd9114e0dd06d508df68dfdd Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 28 Apr 2019 14:21:24 +0300 Subject: [PATCH 03/19] fix: change setup.py --- final_task/setup.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/final_task/setup.py b/final_task/setup.py index 706935eb..256000a7 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,8 +1,11 @@ from setuptools import setup, find_packages -from os.path import join, dirname setup( - name='pycalc', - version='1.0', - packages=['calc'], + name='pycalc', + version='1.0', + packages=find_packages(), + description='Pure-python command-line calculator.', + py_modules=['pycalc'], + entry_points={'console_scripts': ['pycalc = calc.pycalc:main', ], }, + platforms='any', ) From 775ccccbbb751892e904bfd4e0e42c92c74737f1 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 28 Apr 2019 14:59:33 +0300 Subject: [PATCH 04/19] fix: change setup.py --- final_task/{calc => calculator}/pycalc.py | 0 final_task/{calc => calculator}/tests.py | 0 final_task/setup.py | 17 +++++++++-------- 3 files changed, 9 insertions(+), 8 deletions(-) rename final_task/{calc => calculator}/pycalc.py (100%) rename final_task/{calc => calculator}/tests.py (100%) diff --git a/final_task/calc/pycalc.py b/final_task/calculator/pycalc.py similarity index 100% rename from final_task/calc/pycalc.py rename to final_task/calculator/pycalc.py diff --git a/final_task/calc/tests.py b/final_task/calculator/tests.py similarity index 100% rename from final_task/calc/tests.py rename to final_task/calculator/tests.py diff --git a/final_task/setup.py b/final_task/setup.py index 256000a7..14411d4b 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,11 +1,12 @@ -from setuptools import setup, find_packages +from setuptools import setup setup( - name='pycalc', - version='1.0', - packages=find_packages(), - description='Pure-python command-line calculator.', - py_modules=['pycalc'], - entry_points={'console_scripts': ['pycalc = calc.pycalc:main', ], }, - platforms='any', + name='pycalc', + version='1.0', + packages=["calculator"], + description='Pure-python command-line calculator.', + entry_points={ + "console_scripts": ["pycalc=calculator.pycalc3:main"] + }, + platforms='any', ) From c1d5a209d73c3bc3ff9913ded74d17876ee7e50a Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 28 Apr 2019 14:59:33 +0300 Subject: [PATCH 05/19] fix: change setup.py --- final_task/{calc => calculator}/pycalc.py | 0 final_task/{calc => calculator}/tests.py | 0 final_task/setup.py | 17 +++++++++-------- 3 files changed, 9 insertions(+), 8 deletions(-) rename final_task/{calc => calculator}/pycalc.py (100%) rename final_task/{calc => calculator}/tests.py (100%) diff --git a/final_task/calc/pycalc.py b/final_task/calculator/pycalc.py similarity index 100% rename from final_task/calc/pycalc.py rename to final_task/calculator/pycalc.py diff --git a/final_task/calc/tests.py b/final_task/calculator/tests.py similarity index 100% rename from final_task/calc/tests.py rename to final_task/calculator/tests.py diff --git a/final_task/setup.py b/final_task/setup.py index 256000a7..ccb32e08 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,11 +1,12 @@ -from setuptools import setup, find_packages +from setuptools import setup setup( - name='pycalc', - version='1.0', - packages=find_packages(), - description='Pure-python command-line calculator.', - py_modules=['pycalc'], - entry_points={'console_scripts': ['pycalc = calc.pycalc:main', ], }, - platforms='any', + name='pycalc', + version='1.0', + packages=["calculator"], + description='Pure-python command-line calculator.', + entry_points={ + "console_scripts": ["pycalc=calculator.pycalc:main"] + }, + platforms='any', ) From 108747366e241836edc2d52b6b4e8c578bee6870 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 28 Apr 2019 18:17:18 +0300 Subject: [PATCH 06/19] refactor: make regular expressions in separate file --- final_task/calculator/pycalc.py | 41 +++++------------------------ final_task/calculator/regexps.py | 44 ++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 35 deletions(-) create mode 100644 final_task/calculator/regexps.py diff --git a/final_task/calculator/pycalc.py b/final_task/calculator/pycalc.py index c223fae5..2e6b323a 100755 --- a/final_task/calculator/pycalc.py +++ b/final_task/calculator/pycalc.py @@ -23,17 +23,6 @@ GREAT_OR_EQUAL (str): possible representation of the operation >= in the expression. EQUAL (str): possible representation of the operation == in the expression. NOT_EQUAL (str): possible representation of the operation != in the expression. - REGEXP_DIGIT (rstr): regular expressions for finding numbers. - REGEXP_SIMPLE_DIGIT (rstr): regular expressions for checking common digits. - REGEXP_SCREENING (rstr): regular expressions for operation screening. - REGEX_NAME (rstr): regular expressions for finding names. - REGEXP_BACKETS (rstr): regular expressions for finding brackets. - REGEXP_FUNCTION (rstr): regular expressions for finding functons. - REGEXP_CONSTANT (rstr): regular expressions for finding constant names. - REGEXP_UNARY (rstr): regular expressions for finding unary operation. - REGEXP_BYNARY (rstr): regular expressions for finding bynary operation. - REGEXP_COMPARE (rstr): regular expressions for finding compare operation. - REGEXP_INCORECT_EXPRETION (rstr): regular expressions for defining invalid expressions. HAS_COMPARE (bool): determines whether the expression has a comparison operation. LIBRARY (dict): library of available operations. """ @@ -43,6 +32,10 @@ from collections import namedtuple from functools import reduce from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt +from regexps import REGEXP_DIGIT, REGEXP_SIMPLE_DIGIT, REGEXP_SCREENING, REGEX_NAME, REGEXP_BACKETS, \ + REGEXP_FUNCTION, REGEXP_CONSTANT, REGEXP_UNARY, REGEXP_BYNARY, REGEXP_COMPARE, \ + REGEXP_INCORECT_EXPRETION, REGEXP_NON_ZERO_FRACTION_PART, REGEXP_COMPARATOR + LEFT_BRACKET = '(' RIGHT_BRACKET = ')' @@ -60,28 +53,6 @@ EQUAL = '==' NOT_EQUAL = '!=' -REGEXP_DIGIT = r'[+-]?\d+\.\d+e\+\d+|[+-]?\d+\.?\d*|[+-]?\d*\.?\d+' -REGEXP_SIMPLE_DIGIT = rf'^({REGEXP_DIGIT})$' -REGEXP_SCREENING = rf'\{{operation}}' -REGEX_NAME = r'\w+' -REGEXP_BACKETS = r'(?:^|\W)(\([^)(]+\))' -REGEXP_FUNCTION = rf'(?P(?P{REGEX_NAME})\((?P(?:{REGEXP_DIGIT})(?:,(?:{REGEXP_DIGIT})+)*|)\))' -REGEXP_CONSTANT = rf'(?P{REGEXP_DIGIT}|{REGEX_NAME}\(?)' -REGEXP_UNARY = rf'([-+]{{2,}})' -REGEXP_BYNARY = rf'((?:{REGEXP_DIGIT})(?:{{operation}}(?:{REGEXP_DIGIT}))+)' -REGEXP_COMPARE = rf'^{REGEXP_BYNARY}$'.format(operation='[=!<>]{1,2}') -REGEXP_INCORECT_EXPRETION = ( - r'.?\W\d+\s*\(|' - r'^\d+\s*\(|' - r'^\W*$|' - r'\d+[)(<=!>][<>!]\d+|' - r'\W\d+[)(<=!>][]\d+|' - r'\w+\s+\w+|' - r'[-+*^\/%<=!>]+\s+[\/*^%<=!>]+|' - r'^[\/*^%<=!>]|' - r'[-+*^\/%<=!>]$' -) - HAS_COMPARE = False LIBRARY = { 'abs': abs, @@ -438,7 +409,7 @@ def check_expression(expr): check_function(expr) global HAS_COMPARE - HAS_COMPARE = True if re.search(r'[=!<>]', expr) else False + HAS_COMPARE = True if re.search(REGEXP_COMPARATOR, expr) else False return expr @@ -476,7 +447,7 @@ def convert_answer(string): bool: If the expression contained some equality. """ num = float(string) - match = re.search(r'\.0*[1-9]', string) + match = re.search(REGEXP_NON_ZERO_FRACTION_PART, string) num = num if match else int(num) return bool(num) if HAS_COMPARE else num diff --git a/final_task/calculator/regexps.py b/final_task/calculator/regexps.py new file mode 100644 index 00000000..2b8e73f5 --- /dev/null +++ b/final_task/calculator/regexps.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" +The module is a regular expression library for searching math expressions. + +Attributes: + REGEXP_DIGIT (rstr): regular expressions for finding numbers. + REGEXP_SIMPLE_DIGIT (rstr): regular expressions for checking common digits. + REGEXP_SCREENING (rstr): regular expressions for operation screening. + REGEX_NAME (rstr): regular expressions for finding names. + REGEXP_BACKETS (rstr): regular expressions for finding brackets. + REGEXP_FUNCTION (rstr): regular expressions for finding functons. + REGEXP_CONSTANT (rstr): regular expressions for finding constant names. + REGEXP_UNARY (rstr): regular expressions for finding unary operation. + REGEXP_BYNARY (rstr): regular expressions for finding bynary operation. + REGEXP_COMPARE (rstr): regular expressions for finding compare operation. + REGEXP_NON_ZERO_FRACTION_PART (rstr): regular expressions for finding non-zero fraction part. + REGEXP_COMPARATOR (rstr): regular expressions for finding comparator. + REGEXP_INCORECT_EXPRETION (rstr): regular expressions for defining invalid expressions. + +""" + +REGEXP_DIGIT = r'[+-]?\d+\.\d+e\+\d+|[+-]?\d+\.?\d*|[+-]?\d*\.?\d+' +REGEXP_SIMPLE_DIGIT = rf'^({REGEXP_DIGIT})$' +REGEXP_SCREENING = rf'\{{operation}}' +REGEX_NAME = r'\w+' +REGEXP_BACKETS = r'(?:^|\W)(\([^)(]+\))' +REGEXP_FUNCTION = rf'(?P(?P{REGEX_NAME})\((?P(?:{REGEXP_DIGIT})(?:,(?:{REGEXP_DIGIT})+)*|)\))' +REGEXP_CONSTANT = rf'(?P{REGEXP_DIGIT}|{REGEX_NAME}\(?)' +REGEXP_UNARY = rf'([-+]{{2,}})' +REGEXP_BYNARY = rf'((?:{REGEXP_DIGIT})(?:{{operation}}(?:{REGEXP_DIGIT}))+)' +REGEXP_COMPARE = rf'^{REGEXP_BYNARY}$'.format(operation='[=!<>]{1,2}') +REGEXP_NON_ZERO_FRACTION_PART = r'\.0*[1-9]' +REGEXP_COMPARATOR = r'[=!<>]{1,2}' +REGEXP_INCORECT_EXPRETION = ( + r'.?\W\d+\s*\(|' + r'^\d+\s*\(|' + r'^\W*$|' + r'\d+[)(<=!>][<>!]\d+|' + r'\W\d+[)(<=!>][]\d+|' + r'\w+\s+\w+|' + r'[-+*^\/%<=!>]+\s+[\/*^%<=!>]+|' + r'^[\/*^%<=!>]|' + r'[-+*^\/%<=!>]$' +) From 3c8094bcc2ec3b240cfe6c69a69f080241b648dd Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 28 Apr 2019 18:33:43 +0300 Subject: [PATCH 07/19] fix: return to its former state --- final_task/calculator/pycalc.py | 41 ++++++++++++++++++++++++++--- final_task/calculator/regexps.py | 44 -------------------------------- 2 files changed, 37 insertions(+), 48 deletions(-) delete mode 100644 final_task/calculator/regexps.py diff --git a/final_task/calculator/pycalc.py b/final_task/calculator/pycalc.py index 2e6b323a..0dd9b15f 100755 --- a/final_task/calculator/pycalc.py +++ b/final_task/calculator/pycalc.py @@ -23,6 +23,19 @@ GREAT_OR_EQUAL (str): possible representation of the operation >= in the expression. EQUAL (str): possible representation of the operation == in the expression. NOT_EQUAL (str): possible representation of the operation != in the expression. + REGEXP_DIGIT (rstr): regular expressions for finding numbers. + REGEXP_SIMPLE_DIGIT (rstr): regular expressions for checking common digits. + REGEXP_SCREENING (rstr): regular expressions for operation screening. + REGEX_NAME (rstr): regular expressions for finding names. + REGEXP_BACKETS (rstr): regular expressions for finding brackets. + REGEXP_FUNCTION (rstr): regular expressions for finding functons. + REGEXP_CONSTANT (rstr): regular expressions for finding constant names. + REGEXP_UNARY (rstr): regular expressions for finding unary operation. + REGEXP_BYNARY (rstr): regular expressions for finding bynary operation. + REGEXP_COMPARE (rstr): regular expressions for finding compare operation. + REGEXP_INCORECT_EXPRETION (rstr): regular expressions for defining invalid expressions. + REGEXP_NON_ZERO_FRACTION_PART (rstr): regular expressions for finding non-zero fraction part. + REGEXP_COMPARATOR (rstr): regular expressions for finding comparator. HAS_COMPARE (bool): determines whether the expression has a comparison operation. LIBRARY (dict): library of available operations. """ @@ -32,10 +45,6 @@ from collections import namedtuple from functools import reduce from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt -from regexps import REGEXP_DIGIT, REGEXP_SIMPLE_DIGIT, REGEXP_SCREENING, REGEX_NAME, REGEXP_BACKETS, \ - REGEXP_FUNCTION, REGEXP_CONSTANT, REGEXP_UNARY, REGEXP_BYNARY, REGEXP_COMPARE, \ - REGEXP_INCORECT_EXPRETION, REGEXP_NON_ZERO_FRACTION_PART, REGEXP_COMPARATOR - LEFT_BRACKET = '(' RIGHT_BRACKET = ')' @@ -53,6 +62,30 @@ EQUAL = '==' NOT_EQUAL = '!=' +REGEXP_DIGIT = r'[+-]?\d+\.\d+e\+\d+|[+-]?\d+\.?\d*|[+-]?\d*\.?\d+' +REGEXP_SIMPLE_DIGIT = rf'^({REGEXP_DIGIT})$' +REGEXP_SCREENING = rf'\{{operation}}' +REGEX_NAME = r'\w+' +REGEXP_BACKETS = r'(?:^|\W)(\([^)(]+\))' +REGEXP_FUNCTION = rf'(?P(?P{REGEX_NAME})\((?P(?:{REGEXP_DIGIT})(?:,(?:{REGEXP_DIGIT})+)*|)\))' +REGEXP_CONSTANT = rf'(?P{REGEXP_DIGIT}|{REGEX_NAME}\(?)' +REGEXP_UNARY = rf'([-+]{{2,}})' +REGEXP_BYNARY = rf'((?:{REGEXP_DIGIT})(?:{{operation}}(?:{REGEXP_DIGIT}))+)' +REGEXP_COMPARE = rf'^{REGEXP_BYNARY}$'.format(operation='[=!<>]{1,2}') +REGEXP_NON_ZERO_FRACTION_PART = r'\.0*[1-9]' +REGEXP_COMPARATOR = r'[=!<>]{1,2}' +REGEXP_INCORECT_EXPRETION = ( + r'.?\W\d+\s*\(|' + r'^\d+\s*\(|' + r'^\W*$|' + r'\d+[)(<=!>][<>!]\d+|' + r'\W\d+[)(<=!>][]\d+|' + r'\w+\s+\w+|' + r'[-+*^\/%<=!>]+\s+[\/*^%<=!>]+|' + r'^[\/*^%<=!>]|' + r'[-+*^\/%<=!>]$' +) + HAS_COMPARE = False LIBRARY = { 'abs': abs, diff --git a/final_task/calculator/regexps.py b/final_task/calculator/regexps.py deleted file mode 100644 index 2b8e73f5..00000000 --- a/final_task/calculator/regexps.py +++ /dev/null @@ -1,44 +0,0 @@ -# -*- coding: utf-8 -*- -""" -The module is a regular expression library for searching math expressions. - -Attributes: - REGEXP_DIGIT (rstr): regular expressions for finding numbers. - REGEXP_SIMPLE_DIGIT (rstr): regular expressions for checking common digits. - REGEXP_SCREENING (rstr): regular expressions for operation screening. - REGEX_NAME (rstr): regular expressions for finding names. - REGEXP_BACKETS (rstr): regular expressions for finding brackets. - REGEXP_FUNCTION (rstr): regular expressions for finding functons. - REGEXP_CONSTANT (rstr): regular expressions for finding constant names. - REGEXP_UNARY (rstr): regular expressions for finding unary operation. - REGEXP_BYNARY (rstr): regular expressions for finding bynary operation. - REGEXP_COMPARE (rstr): regular expressions for finding compare operation. - REGEXP_NON_ZERO_FRACTION_PART (rstr): regular expressions for finding non-zero fraction part. - REGEXP_COMPARATOR (rstr): regular expressions for finding comparator. - REGEXP_INCORECT_EXPRETION (rstr): regular expressions for defining invalid expressions. - -""" - -REGEXP_DIGIT = r'[+-]?\d+\.\d+e\+\d+|[+-]?\d+\.?\d*|[+-]?\d*\.?\d+' -REGEXP_SIMPLE_DIGIT = rf'^({REGEXP_DIGIT})$' -REGEXP_SCREENING = rf'\{{operation}}' -REGEX_NAME = r'\w+' -REGEXP_BACKETS = r'(?:^|\W)(\([^)(]+\))' -REGEXP_FUNCTION = rf'(?P(?P{REGEX_NAME})\((?P(?:{REGEXP_DIGIT})(?:,(?:{REGEXP_DIGIT})+)*|)\))' -REGEXP_CONSTANT = rf'(?P{REGEXP_DIGIT}|{REGEX_NAME}\(?)' -REGEXP_UNARY = rf'([-+]{{2,}})' -REGEXP_BYNARY = rf'((?:{REGEXP_DIGIT})(?:{{operation}}(?:{REGEXP_DIGIT}))+)' -REGEXP_COMPARE = rf'^{REGEXP_BYNARY}$'.format(operation='[=!<>]{1,2}') -REGEXP_NON_ZERO_FRACTION_PART = r'\.0*[1-9]' -REGEXP_COMPARATOR = r'[=!<>]{1,2}' -REGEXP_INCORECT_EXPRETION = ( - r'.?\W\d+\s*\(|' - r'^\d+\s*\(|' - r'^\W*$|' - r'\d+[)(<=!>][<>!]\d+|' - r'\W\d+[)(<=!>][]\d+|' - r'\w+\s+\w+|' - r'[-+*^\/%<=!>]+\s+[\/*^%<=!>]+|' - r'^[\/*^%<=!>]|' - r'[-+*^\/%<=!>]$' -) From a33d56bfa12ea2cfcdb6f31d5c4dea40fde3217f Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 12 May 2019 08:50:34 +0300 Subject: [PATCH 08/19] refactor: make regular expressions in separate file --- final_task/calculator/math_regexp.py | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 final_task/calculator/math_regexp.py diff --git a/final_task/calculator/math_regexp.py b/final_task/calculator/math_regexp.py new file mode 100644 index 00000000..2b8e73f5 --- /dev/null +++ b/final_task/calculator/math_regexp.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" +The module is a regular expression library for searching math expressions. + +Attributes: + REGEXP_DIGIT (rstr): regular expressions for finding numbers. + REGEXP_SIMPLE_DIGIT (rstr): regular expressions for checking common digits. + REGEXP_SCREENING (rstr): regular expressions for operation screening. + REGEX_NAME (rstr): regular expressions for finding names. + REGEXP_BACKETS (rstr): regular expressions for finding brackets. + REGEXP_FUNCTION (rstr): regular expressions for finding functons. + REGEXP_CONSTANT (rstr): regular expressions for finding constant names. + REGEXP_UNARY (rstr): regular expressions for finding unary operation. + REGEXP_BYNARY (rstr): regular expressions for finding bynary operation. + REGEXP_COMPARE (rstr): regular expressions for finding compare operation. + REGEXP_NON_ZERO_FRACTION_PART (rstr): regular expressions for finding non-zero fraction part. + REGEXP_COMPARATOR (rstr): regular expressions for finding comparator. + REGEXP_INCORECT_EXPRETION (rstr): regular expressions for defining invalid expressions. + +""" + +REGEXP_DIGIT = r'[+-]?\d+\.\d+e\+\d+|[+-]?\d+\.?\d*|[+-]?\d*\.?\d+' +REGEXP_SIMPLE_DIGIT = rf'^({REGEXP_DIGIT})$' +REGEXP_SCREENING = rf'\{{operation}}' +REGEX_NAME = r'\w+' +REGEXP_BACKETS = r'(?:^|\W)(\([^)(]+\))' +REGEXP_FUNCTION = rf'(?P(?P{REGEX_NAME})\((?P(?:{REGEXP_DIGIT})(?:,(?:{REGEXP_DIGIT})+)*|)\))' +REGEXP_CONSTANT = rf'(?P{REGEXP_DIGIT}|{REGEX_NAME}\(?)' +REGEXP_UNARY = rf'([-+]{{2,}})' +REGEXP_BYNARY = rf'((?:{REGEXP_DIGIT})(?:{{operation}}(?:{REGEXP_DIGIT}))+)' +REGEXP_COMPARE = rf'^{REGEXP_BYNARY}$'.format(operation='[=!<>]{1,2}') +REGEXP_NON_ZERO_FRACTION_PART = r'\.0*[1-9]' +REGEXP_COMPARATOR = r'[=!<>]{1,2}' +REGEXP_INCORECT_EXPRETION = ( + r'.?\W\d+\s*\(|' + r'^\d+\s*\(|' + r'^\W*$|' + r'\d+[)(<=!>][<>!]\d+|' + r'\W\d+[)(<=!>][]\d+|' + r'\w+\s+\w+|' + r'[-+*^\/%<=!>]+\s+[\/*^%<=!>]+|' + r'^[\/*^%<=!>]|' + r'[-+*^\/%<=!>]$' +) From 2c0dbecfe2626e47c0c5166b7646c0ead7b8f97d Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 12 May 2019 08:54:05 +0300 Subject: [PATCH 09/19] refactor: carry out operators in the dictionary --- final_task/calculator/pycalc.py | 189 ++++++++++++-------------------- final_task/calculator/tests.py | 37 +++++-- 2 files changed, 95 insertions(+), 131 deletions(-) diff --git a/final_task/calculator/pycalc.py b/final_task/calculator/pycalc.py index 0dd9b15f..465b95d3 100755 --- a/final_task/calculator/pycalc.py +++ b/final_task/calculator/pycalc.py @@ -23,19 +23,6 @@ GREAT_OR_EQUAL (str): possible representation of the operation >= in the expression. EQUAL (str): possible representation of the operation == in the expression. NOT_EQUAL (str): possible representation of the operation != in the expression. - REGEXP_DIGIT (rstr): regular expressions for finding numbers. - REGEXP_SIMPLE_DIGIT (rstr): regular expressions for checking common digits. - REGEXP_SCREENING (rstr): regular expressions for operation screening. - REGEX_NAME (rstr): regular expressions for finding names. - REGEXP_BACKETS (rstr): regular expressions for finding brackets. - REGEXP_FUNCTION (rstr): regular expressions for finding functons. - REGEXP_CONSTANT (rstr): regular expressions for finding constant names. - REGEXP_UNARY (rstr): regular expressions for finding unary operation. - REGEXP_BYNARY (rstr): regular expressions for finding bynary operation. - REGEXP_COMPARE (rstr): regular expressions for finding compare operation. - REGEXP_INCORECT_EXPRETION (rstr): regular expressions for defining invalid expressions. - REGEXP_NON_ZERO_FRACTION_PART (rstr): regular expressions for finding non-zero fraction part. - REGEXP_COMPARATOR (rstr): regular expressions for finding comparator. HAS_COMPARE (bool): determines whether the expression has a comparison operation. LIBRARY (dict): library of available operations. """ @@ -45,6 +32,13 @@ from collections import namedtuple from functools import reduce from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt +import math_regexp as mre + +HAS_COMPARE = False +LIBRARY = { + 'abs': abs, + 'round': round, +} LEFT_BRACKET = '(' RIGHT_BRACKET = ')' @@ -62,38 +56,28 @@ EQUAL = '==' NOT_EQUAL = '!=' -REGEXP_DIGIT = r'[+-]?\d+\.\d+e\+\d+|[+-]?\d+\.?\d*|[+-]?\d*\.?\d+' -REGEXP_SIMPLE_DIGIT = rf'^({REGEXP_DIGIT})$' -REGEXP_SCREENING = rf'\{{operation}}' -REGEX_NAME = r'\w+' -REGEXP_BACKETS = r'(?:^|\W)(\([^)(]+\))' -REGEXP_FUNCTION = rf'(?P(?P{REGEX_NAME})\((?P(?:{REGEXP_DIGIT})(?:,(?:{REGEXP_DIGIT})+)*|)\))' -REGEXP_CONSTANT = rf'(?P{REGEXP_DIGIT}|{REGEX_NAME}\(?)' -REGEXP_UNARY = rf'([-+]{{2,}})' -REGEXP_BYNARY = rf'((?:{REGEXP_DIGIT})(?:{{operation}}(?:{REGEXP_DIGIT}))+)' -REGEXP_COMPARE = rf'^{REGEXP_BYNARY}$'.format(operation='[=!<>]{1,2}') -REGEXP_NON_ZERO_FRACTION_PART = r'\.0*[1-9]' -REGEXP_COMPARATOR = r'[=!<>]{1,2}' -REGEXP_INCORECT_EXPRETION = ( - r'.?\W\d+\s*\(|' - r'^\d+\s*\(|' - r'^\W*$|' - r'\d+[)(<=!>][<>!]\d+|' - r'\W\d+[)(<=!>][]\d+|' - r'\w+\s+\w+|' - r'[-+*^\/%<=!>]+\s+[\/*^%<=!>]+|' - r'^[\/*^%<=!>]|' - r'[-+*^\/%<=!>]$' -) - -HAS_COMPARE = False -LIBRARY = { - 'abs': abs, - 'round': round, +class Type: + ARITHMETIC = 0 + COMPARISON = 1 + +Operator = namedtuple('Operator', 'func type') +OPERATORS = { + MULTIPLE: Operator(mul, Type.ARITHMETIC), + POWER: Operator(pow, Type.ARITHMETIC), + TRUE_DIVISION: Operator(truediv, Type.ARITHMETIC), + FLOOR_DIVISION: Operator(floordiv, Type.ARITHMETIC), + MODULE: Operator(mod, Type.ARITHMETIC), + PLUS: Operator(add, Type.ARITHMETIC), + MINUS: Operator(sub, Type.ARITHMETIC), + LESS: Operator(lt, Type.COMPARISON), + LESS_OR_EQUAL: Operator(le, Type.COMPARISON), + EQUAL: Operator(eq, Type.COMPARISON), + NOT_EQUAL: Operator(ne, Type.COMPARISON), + GREAT_OR_EQUAL: Operator(ge, Type.COMPARISON), + GREAT: Operator(gt, Type.COMPARISON), } - -def exec_operation(x, y, operation=MULTIPLE): +def exec_operation(x: str, y: str, operation=MULTIPLE) -> str: """Executes the operation and returns the result. Args: @@ -106,6 +90,9 @@ def exec_operation(x, y, operation=MULTIPLE): Raises: ValueError: If `operation` is not found`. """ + if operation not in OPERATORS: + raise ValueError('operation was not found') + if operation == POWER and y[0] == MINUS: a, b = float(y[1:]), float(x) if operation == POWER: @@ -113,49 +100,19 @@ def exec_operation(x, y, operation=MULTIPLE): else: a, b = float(x), float(y) - result = None - # arithmetic operation - if operation == MULTIPLE: - result = mul(a, b) - elif operation == POWER: - result = pow(a, b) - elif operation == TRUE_DIVISION: - result = truediv(a, b) - elif operation == FLOOR_DIVISION: - result = floordiv(a, b) - elif operation == MODULE: - result = mod(a, b) - elif operation == PLUS: - result = add(a, b) - elif operation == MINUS: - result = sub(a, b) + operator = OPERATORS[operation] + result = operator.func(a, b) - if operation == POWER and y[0] == MINUS: - return f'{MINUS}{result}' - if result is not None: + if operator.type == Type.ARITHMETIC: + if operation == POWER and y[0] == MINUS: + return f'{MINUS}{result}' return f'{PLUS}{result}' if result > 0 else str(result) - # comparison operation - if operation == LESS: - result = lt(a, b) - elif operation == LESS_OR_EQUAL: - result = le(a, b) - elif operation == EQUAL: - result = eq(a, b) - elif operation == NOT_EQUAL: - result = ne(a, b) - elif operation == GREAT_OR_EQUAL: - result = ge(a, b) - elif operation == GREAT: - result = gt(a, b) - - if result is not None: + if operator.type == Type.COMPARISON: return str(int(result)) - raise ValueError('operation was not found') - -def replace_constant(expr): +def replace_constant(expr: str) -> str: """ Calculates constant operations. @@ -165,12 +122,12 @@ def replace_constant(expr): Returns: str: Updated expression. """ - results = re.finditer(REGEXP_CONSTANT, expr) + results = re.finditer(mre.REGEXP_CONSTANT, expr) for m in results: name = m.group('name') - if name[-1] == LEFT_BRACKET or re.match(REGEXP_DIGIT, name): + if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): continue answer = str(LIBRARY[name]) @@ -187,7 +144,7 @@ def replace_constant(expr): return expr -def replace_fanction(expr): +def replace_fanction(expr: str) -> str: """ Calculates function operations. @@ -197,7 +154,7 @@ def replace_fanction(expr): Returns: str: Updated expression. """ - results = re.finditer(REGEXP_FUNCTION, expr) + results = re.finditer(mre.REGEXP_FUNCTION, expr) for m in results: func = m.group('name') @@ -210,7 +167,7 @@ def replace_fanction(expr): return expr -def replace_unary_operator(expr): +def replace_unary_operator(expr: str) -> str: """ Calculates unary operations. @@ -220,7 +177,7 @@ def replace_unary_operator(expr): Returns: str: Updated expression. """ - results = re.findall(REGEXP_UNARY, expr) + results = re.findall(mre.REGEXP_UNARY, expr) results.sort(key=len, reverse=True) for m in results: @@ -230,7 +187,7 @@ def replace_unary_operator(expr): return expr -def replace_compare_operator(expr, *operations): +def replace_compare_operator(expr: str, *operations: list) -> str: """ Calculates compare operations. @@ -241,13 +198,13 @@ def replace_compare_operator(expr, *operations): Returns: str: Updated expression. """ - if re.search(REGEXP_COMPARE, expr): + if re.search(mre.REGEXP_COMPARE, expr): return replace_bynary_operator(expr, *operations) return expr -def replace_bynary_operator(expr, *operations): +def replace_bynary_operator(expr: str, *operations: list) -> str: """ Calculates binary operations. @@ -261,9 +218,9 @@ def replace_bynary_operator(expr, *operations): for o in operations: delimeter = o if o == PLUS or o == MULTIPLE or o == POWER: - delimeter = REGEXP_SCREENING.format(operation=o) + delimeter = mre.REGEXP_SCREENING.format(operation=o) - regexp = REGEXP_BYNARY.format(operation=delimeter) + regexp = mre.REGEXP_BYNARY.format(operation=delimeter) results = re.findall(regexp, expr) for m in results: arr = list(filter(bool, m.split(o))) @@ -278,7 +235,7 @@ def replace_bynary_operator(expr, *operations): return expr -def replace_brackets(expr): +def replace_brackets(expr: str) -> str: """ Calculates the expression in brackets. @@ -288,7 +245,7 @@ def replace_brackets(expr): Returns: str: Updated expression. """ - results = re.findall(REGEXP_BACKETS, expr) + results = re.findall(mre.REGEXP_BACKETS, expr) for m in results: answer = calc(m[1:-1]) @@ -297,7 +254,7 @@ def replace_brackets(expr): return expr -def calc(expr): +def calc(expr: str) -> str: """ Calculates the result from the getting expression. @@ -319,7 +276,7 @@ def calc(expr): Operation(replace_compare_operator, [EQUAL, NOT_EQUAL, GREAT, GREAT_OR_EQUAL, LESS, LESS_OR_EQUAL]), ] - pattern = re.compile(REGEXP_SIMPLE_DIGIT) + pattern = re.compile(mre.REGEXP_SIMPLE_DIGIT) while True: for inst in OPERATION_PRIORITY: expr = inst.func(expr, *inst.args) @@ -329,13 +286,13 @@ def calc(expr): return expr -def import_modules(*modules): +def import_modules(*modules: list): """Imports the modules from the list to the global field LIBRARY.""" for module in modules: LIBRARY.update(__import__(module).__dict__) -def check_spaces(expr): +def check_spaces(expr: str) -> str: """ Checks if an expression has the wrong elements. @@ -348,14 +305,14 @@ def check_spaces(expr): Raises: ValueError: If `expr` is not correct`. """ - res = re.findall(REGEXP_INCORECT_EXPRETION, expr) + res = re.findall(mre.REGEXP_INCORECT_EXPRETION, expr) if res: raise ValueError('expression is not correct') return expr.replace(' ', '') -def check_brackets(expr): +def check_brackets(expr: str): """ Checks if all brackets have a pair. @@ -376,7 +333,7 @@ def check_brackets(expr): raise ValueError('brackets are not balanced') -def check_constant(expr): +def check_constant(expr: str): """ Checks if all constants in the expression are available. @@ -386,12 +343,11 @@ def check_constant(expr): Raises: ValueError: If `expr` is not correct`. """ - results = re.finditer(REGEXP_CONSTANT, expr) - + results = re.finditer(mre.REGEXP_CONSTANT, expr) for m in results: name = m.group('name') - if name[-1] == LEFT_BRACKET or re.match(REGEXP_DIGIT, name): + if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): continue if name[0].isdigit(): @@ -401,7 +357,7 @@ def check_constant(expr): raise ValueError(f'there is no such constant {name}') -def check_function(expr): +def check_function(expr: str): """ Checks if all functions in the expression are available. @@ -411,7 +367,7 @@ def check_function(expr): Raises: ValueError: If `expr` is not correct`. """ - results = re.finditer(REGEXP_FUNCTION, expr) + results = re.finditer(mre.REGEXP_FUNCTION, expr) for m in results: name = m.group('name') pattern = m.group('pattern') @@ -423,7 +379,7 @@ def check_function(expr): raise ValueError(f'there is no such function {pattern}') -def check_expression(expr): +def check_expression(expr: str) -> str: """ Checks the expression for correctness. @@ -442,7 +398,7 @@ def check_expression(expr): check_function(expr) global HAS_COMPARE - HAS_COMPARE = True if re.search(REGEXP_COMPARATOR, expr) else False + HAS_COMPARE = True if re.search(mre.REGEXP_COMPARATOR, expr) else False return expr @@ -452,7 +408,7 @@ def parse_query(): Convert argument strings to objects and assign them as attributes of the namespace. Returns: - Namespace: If there is no fractional part. + Namespace: got data from command line. """ parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') parser.add_argument('expr', metavar='EXPRESSION', help='expression string to evaluate') @@ -467,23 +423,19 @@ def parse_query(): return parser.parse_args() -def convert_answer(string): +def print_answer(string: str): """ - Converts the resulting string to the desired type. + Converts the resulting string to the desired type and prints it. Args: string (str): String representation of a number. - - Returns: - int: If there is no fractional part. - float: If there is a fractional part. - bool: If the expression contained some equality. """ num = float(string) - match = re.search(REGEXP_NON_ZERO_FRACTION_PART, string) + match = re.search(mre.REGEXP_NON_ZERO_FRACTION_PART, string) num = num if match else int(num) - return bool(num) if HAS_COMPARE else num + answer = bool(num) if HAS_COMPARE else num + print(answer) def main(): @@ -493,8 +445,7 @@ def main(): import_modules('math', *args.modules) expr = check_expression(args.expr) answer = calc(expr) - answer = convert_answer(answer) - print(answer) + print_answer(answer) except Exception as e: print(f'ERROR: {e}') diff --git a/final_task/calculator/tests.py b/final_task/calculator/tests.py index 646e2d55..52f84957 100644 --- a/final_task/calculator/tests.py +++ b/final_task/calculator/tests.py @@ -285,32 +285,45 @@ def test_parse_query(self): self.assertEqual(args.expr, 'time()/60') self.assertEqual(args.modules, ['time', 'os', 'math']) - def test_convert_answer(self): - with self.subTest("returns correct answer"): + def test_print_answer(self): + with self.subTest("print correct answer"), \ + patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: pycalc.HAS_COMPARE = False - self.assertEqual(pycalc.convert_answer('-1'), -1) - self.assertEqual(pycalc.convert_answer('0'), 0) + pycalc.print_answer('-1') + self.assertEqual(mock_stdout.getvalue(), '-1\n') + + mock_stdout.truncate(0) + mock_stdout.seek(0) + pycalc.print_answer('0') + self.assertEqual(mock_stdout.getvalue(), '0\n') + pycalc.HAS_COMPARE = True - self.assertEqual(pycalc.convert_answer('-1'), True) - self.assertEqual(pycalc.convert_answer('0'), False) + + mock_stdout.truncate(0) + mock_stdout.seek(0) + pycalc.print_answer('-1') + self.assertEqual(mock_stdout.getvalue(), 'True\n') + + mock_stdout.truncate(0) + mock_stdout.seek(0) + pycalc.print_answer('0') + self.assertEqual(mock_stdout.getvalue(), 'False\n') def test_main(self): - with self.subTest("calls methods parse_query, import_modules, check_expression, calc, convert_answer"), \ + with self.subTest("calls methods parse_query, import_modules, check_expression, calc, print_answer"), \ patch('pycalc.parse_query') as parse_query, \ patch('pycalc.import_modules') as import_modules, \ patch('pycalc.check_expression') as check_expression, \ - patch('pycalc.calc') as calc, \ - patch('pycalc.convert_answer', return_value='1') as convert_answer, \ - patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: + patch('pycalc.calc', return_value='1') as calc, \ + patch('pycalc.print_answer') as print_answer: pycalc.main() - self.assertEqual(mock_stdout.getvalue(), '1\n') self.assertTrue(parse_query.called) self.assertTrue(import_modules.called) self.assertTrue(calc.called) self.assertTrue(check_expression.called) - self.assertTrue(convert_answer.called) + self.assertTrue(print_answer.called) with self.subTest("catchs exception"), \ patch('pycalc.parse_query') as parse_query, \ From 308bc292304a5037e76923985bf450c8c81485c4 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 12 May 2019 08:58:24 +0300 Subject: [PATCH 10/19] refactor: change file name math_regexp to regexp --- final_task/calculator/pycalc.py | 2 +- final_task/calculator/{math_regexp.py => regexp.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename final_task/calculator/{math_regexp.py => regexp.py} (100%) diff --git a/final_task/calculator/pycalc.py b/final_task/calculator/pycalc.py index 465b95d3..3e68202f 100755 --- a/final_task/calculator/pycalc.py +++ b/final_task/calculator/pycalc.py @@ -32,7 +32,7 @@ from collections import namedtuple from functools import reduce from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt -import math_regexp as mre +import regexp as mre HAS_COMPARE = False LIBRARY = { diff --git a/final_task/calculator/math_regexp.py b/final_task/calculator/regexp.py similarity index 100% rename from final_task/calculator/math_regexp.py rename to final_task/calculator/regexp.py From bc75852cfdc4302a9ba934ca8aac9c8c3dd52de1 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 12 May 2019 20:12:50 +0300 Subject: [PATCH 11/19] refactor: split code into multiple files --- final_task/calculator/checker.py | 129 +++++++ final_task/calculator/converter.py | 16 + final_task/calculator/library.py | 14 + final_task/calculator/operators.py | 99 ++++++ final_task/calculator/parser.py | 20 ++ final_task/calculator/pycalc.py | 444 +----------------------- final_task/calculator/regexp.py | 10 + final_task/calculator/replacer.py | 200 +++++++++++ final_task/calculator/test_checker.py | 121 +++++++ final_task/calculator/test_converter.py | 13 + final_task/calculator/test_library.py | 36 ++ final_task/calculator/test_operators.py | 32 ++ final_task/calculator/test_parser.py | 15 + final_task/calculator/test_pycalc.py | 35 ++ final_task/calculator/test_regexp.py | 21 ++ final_task/calculator/test_replacer.py | 130 +++++++ final_task/calculator/tests.py | 337 ------------------ 17 files changed, 904 insertions(+), 768 deletions(-) create mode 100644 final_task/calculator/checker.py create mode 100644 final_task/calculator/converter.py create mode 100644 final_task/calculator/library.py create mode 100644 final_task/calculator/operators.py create mode 100644 final_task/calculator/parser.py create mode 100644 final_task/calculator/replacer.py create mode 100644 final_task/calculator/test_checker.py create mode 100644 final_task/calculator/test_converter.py create mode 100644 final_task/calculator/test_library.py create mode 100644 final_task/calculator/test_operators.py create mode 100644 final_task/calculator/test_parser.py create mode 100644 final_task/calculator/test_pycalc.py create mode 100644 final_task/calculator/test_regexp.py create mode 100644 final_task/calculator/test_replacer.py delete mode 100644 final_task/calculator/tests.py diff --git a/final_task/calculator/checker.py b/final_task/calculator/checker.py new file mode 100644 index 00000000..3eec2e8f --- /dev/null +++ b/final_task/calculator/checker.py @@ -0,0 +1,129 @@ +# -*- coding: utf-8 -*- +""" +The module is designed to test the mathematical expression for correctness. + +Example: + check_spaces(' 1+ 3 / 2') + >>> '1+3/2' + + check_brackets('2*(3+5)') + + lib = { + 'e': 2.718281828459045, + 'sum': sum + } + check_constant('e', lib) + check_function('sum(100, 50)', lib) +""" + +import re +import regexp as mre +from operators import LEFT_BRACKET, RIGHT_BRACKET +from library import Library + +def check_spaces(expr: str) -> str: + """ + Checks if an expression has the wrong elements. + + Args: + expr (str): String mathematical expression. + + Returns: + str: cleared expression from spaces. + + Raises: + ValueError: If `expr` is not correct`. + """ + res = re.findall(mre.REGEXP_INCORECT_EXPRETION, expr) + if res: + raise ValueError('expression is not correct') + + return expr.replace(' ', '') + + +def check_brackets(expr: str): + """ + Checks if all brackets have a pair. + + Args: + expr (str): String mathematical expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + stack = [] + for c in expr: + if c == LEFT_BRACKET: + stack.append(c) + elif c == RIGHT_BRACKET and (not stack or stack.pop() != LEFT_BRACKET): + raise ValueError('brackets are not balanced') + + if stack: + raise ValueError('brackets are not balanced') + + +def check_constant(expr: str, library: Library): + """ + Checks if all constants in the expression are available. + + Args: + expr (str): String mathematical expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + results = re.finditer(mre.REGEXP_CONSTANT, expr) + for m in results: + name = m.group('name') + + if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): + continue + + if name[0].isdigit(): + raise ValueError(f'constant {name} can not start with digit') + + if name not in library or callable(library[name]): + raise ValueError(f'there is no such constant {name}') + + +def check_function(expr: str, library: Library): + """ + Checks if all functions in the expression are available. + + Args: + expr (str): String mathematical expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + results = re.finditer(mre.REGEXP_FUNCTION, expr) + for m in results: + name = m.group('name') + pattern = m.group('pattern') + + if name[0].isdigit(): + raise ValueError(f'function {pattern} can not start with digit') + + if name not in library or not callable(library[name]): + raise ValueError(f'there is no such function {pattern}') + + +def check_expression(expr: str, library: Library) -> str: + """ + Checks the expression for correctness. + + Args: + expr (str): String mathematical expression. + + Returns: + str: cleared expression. + + Raises: + ValueError: If `expr` is not correct`. + """ + expr = check_spaces(expr) + check_brackets(expr) + check_constant(expr, library) + check_function(expr, library) + + return expr diff --git a/final_task/calculator/converter.py b/final_task/calculator/converter.py new file mode 100644 index 00000000..2a04fe7f --- /dev/null +++ b/final_task/calculator/converter.py @@ -0,0 +1,16 @@ +from regexp import has_non_zero_fraction_part + +def convert_answer(expr: str, has_compare: bool) -> str: + """ + Converts the resulting string to the desired type. + + Args: + expr (str): String representation of a number. + """ + num = float(expr) + match = has_non_zero_fraction_part(expr) + num = num if match else int(num) + + answer = bool(num) if has_compare else num + + return str(answer) diff --git a/final_task/calculator/library.py b/final_task/calculator/library.py new file mode 100644 index 00000000..9ef41f02 --- /dev/null +++ b/final_task/calculator/library.py @@ -0,0 +1,14 @@ +class Library(dict): + """Class is designed to work with modules.""" + def __init__(self, *modules: list): + super().__init__() + + self['abs'] = abs + self['round'] = round + + self.update(*modules) + + def update(self, *modules: list): + """Adds functions and veriables from got module names to dictionary.""" + for module in modules: + super().update(__import__(module).__dict__) \ No newline at end of file diff --git a/final_task/calculator/operators.py b/final_task/calculator/operators.py new file mode 100644 index 00000000..937a4a3b --- /dev/null +++ b/final_task/calculator/operators.py @@ -0,0 +1,99 @@ +# -*- coding: utf-8 -*- +""" +The module is designed to calculate mathematical operations. +Also contains string representations of operations. + +Attributes: + LEFT_BRACKET (str): possible representation of the bracket ( in the expression. + RIGHT_BRACKET (str): possible representation of the bracket ) in the expression. + MULTIPLE (str): possible representation of the operation * in the expression. + POWER (str): possible representation of the operation ** in the expression. + TRUE_DIVISION (str): possible representation of the operation / in the expression. + FLOOR_DIVISION (str): possible representation of the operation // in the expression. + MODULE (str): possible representation of the operation % in the expression. + PLUS (str): possible representation of the operation + in the expression. + MINUS (str): possible representation of the operation - in the expression. + LESS (str): possible representation of the operation < in the expression. + LESS_OR_EQUAL (str): possible representation of the operation <= in the expression. + GREAT (str): possible representation of the operation > in the expression. + GREAT_OR_EQUAL (str): possible representation of the operation >= in the expression. + EQUAL (str): possible representation of the operation == in the expression. + NOT_EQUAL (str): possible representation of the operation != in the expression. + Type (class): static class containing types of operations. + OPERATORS (dict): key is string representation of operations, and value is namedtuple(func, type). +""" + +import re +from collections import namedtuple +from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt + +LEFT_BRACKET = '(' +RIGHT_BRACKET = ')' +MULTIPLE = '*' +POWER = '^' +TRUE_DIVISION = '/' +FLOOR_DIVISION = '//' +MODULE = '%' +PLUS = '+' +MINUS = '-' +LESS = '<' +LESS_OR_EQUAL = '<=' +GREAT = '>' +GREAT_OR_EQUAL = '>=' +EQUAL = '==' +NOT_EQUAL = '!=' + +class Type: + ARITHMETIC = 0 + COMPARISON = 1 + +Operator = namedtuple('Operator', 'func type') +OPERATORS = { + MULTIPLE: Operator(mul, Type.ARITHMETIC), + POWER: Operator(pow, Type.ARITHMETIC), + TRUE_DIVISION: Operator(truediv, Type.ARITHMETIC), + FLOOR_DIVISION: Operator(floordiv, Type.ARITHMETIC), + MODULE: Operator(mod, Type.ARITHMETIC), + PLUS: Operator(add, Type.ARITHMETIC), + MINUS: Operator(sub, Type.ARITHMETIC), + LESS: Operator(lt, Type.COMPARISON), + LESS_OR_EQUAL: Operator(le, Type.COMPARISON), + EQUAL: Operator(eq, Type.COMPARISON), + NOT_EQUAL: Operator(ne, Type.COMPARISON), + GREAT_OR_EQUAL: Operator(ge, Type.COMPARISON), + GREAT: Operator(gt, Type.COMPARISON), +} + +def exec_operation(x: str, y: str, operation=MULTIPLE) -> str: + """Executes the operation and returns the result. + + Args: + x (str): String representation of a number. + y (str): String representation of a number. + + Returns: + str: result of calculations. + + Raises: + ValueError: If `operation` is not found`. + """ + if operation not in OPERATORS: + raise ValueError('operation was not found') + + if operation == POWER and y[0] == MINUS: + a, b = float(y[1:]), float(x) + if operation == POWER: + a, b = float(y), float(x) + else: + a, b = float(x), float(y) + + operator = OPERATORS[operation] + result = operator.func(a, b) + + if operator.type == Type.ARITHMETIC: + if operation == POWER and y[0] == MINUS: + return f'{MINUS}{result}' + return f'{PLUS}{result}' if result > 0 else str(result) + + if operator.type == Type.COMPARISON: + return str(int(result)) diff --git a/final_task/calculator/parser.py b/final_task/calculator/parser.py new file mode 100644 index 00000000..2afb3b22 --- /dev/null +++ b/final_task/calculator/parser.py @@ -0,0 +1,20 @@ +import argparse + +def parse_query(): + """ + Convert argument strings to objects and assign them as attributes of the namespace. + + Returns: + Namespace: got data from command line. + """ + parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') + parser.add_argument('expr', metavar='EXPRESSION', help='expression string to evaluate') + parser.add_argument('-m', + '--use-modules', + default=[], + dest='modules', + metavar='MODULE', + nargs='+', + help='additional modules to use') + + return parser.parse_args() diff --git a/final_task/calculator/pycalc.py b/final_task/calculator/pycalc.py index 3e68202f..afb37d07 100755 --- a/final_task/calculator/pycalc.py +++ b/final_task/calculator/pycalc.py @@ -6,446 +6,28 @@ $ python pycalc.py -h $ python pycalc.py 'expretion' $ python pycalc.py 'expretion' -m 'module1' 'module2' - -Attributes: - LEFT_BRACKET (str): possible representation of the bracket ( in the expression. - RIGHT_BRACKET (str): possible representation of the bracket ) in the expression. - MULTIPLE (str): possible representation of the operation * in the expression. - POWER (str): possible representation of the operation ** in the expression. - TRUE_DIVISION (str): possible representation of the operation / in the expression. - FLOOR_DIVISION (str): possible representation of the operation // in the expression. - MODULE (str): possible representation of the operation % in the expression. - PLUS (str): possible representation of the operation + in the expression. - MINUS (str): possible representation of the operation - in the expression. - LESS (str): possible representation of the operation < in the expression. - LESS_OR_EQUAL (str): possible representation of the operation <= in the expression. - GREAT (str): possible representation of the operation > in the expression. - GREAT_OR_EQUAL (str): possible representation of the operation >= in the expression. - EQUAL (str): possible representation of the operation == in the expression. - NOT_EQUAL (str): possible representation of the operation != in the expression. - HAS_COMPARE (bool): determines whether the expression has a comparison operation. - LIBRARY (dict): library of available operations. """ -import argparse import re -from collections import namedtuple -from functools import reduce -from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt -import regexp as mre - -HAS_COMPARE = False -LIBRARY = { - 'abs': abs, - 'round': round, -} - -LEFT_BRACKET = '(' -RIGHT_BRACKET = ')' -MULTIPLE = '*' -POWER = '^' -TRUE_DIVISION = '/' -FLOOR_DIVISION = '//' -MODULE = '%' -PLUS = '+' -MINUS = '-' -LESS = '<' -LESS_OR_EQUAL = '<=' -GREAT = '>' -GREAT_OR_EQUAL = '>=' -EQUAL = '==' -NOT_EQUAL = '!=' - -class Type: - ARITHMETIC = 0 - COMPARISON = 1 - -Operator = namedtuple('Operator', 'func type') -OPERATORS = { - MULTIPLE: Operator(mul, Type.ARITHMETIC), - POWER: Operator(pow, Type.ARITHMETIC), - TRUE_DIVISION: Operator(truediv, Type.ARITHMETIC), - FLOOR_DIVISION: Operator(floordiv, Type.ARITHMETIC), - MODULE: Operator(mod, Type.ARITHMETIC), - PLUS: Operator(add, Type.ARITHMETIC), - MINUS: Operator(sub, Type.ARITHMETIC), - LESS: Operator(lt, Type.COMPARISON), - LESS_OR_EQUAL: Operator(le, Type.COMPARISON), - EQUAL: Operator(eq, Type.COMPARISON), - NOT_EQUAL: Operator(ne, Type.COMPARISON), - GREAT_OR_EQUAL: Operator(ge, Type.COMPARISON), - GREAT: Operator(gt, Type.COMPARISON), -} - -def exec_operation(x: str, y: str, operation=MULTIPLE) -> str: - """Executes the operation and returns the result. - - Args: - x (str): String representation of a number. - y (str): String representation of a number. - - Returns: - str: result of calculations. - - Raises: - ValueError: If `operation` is not found`. - """ - if operation not in OPERATORS: - raise ValueError('operation was not found') - - if operation == POWER and y[0] == MINUS: - a, b = float(y[1:]), float(x) - if operation == POWER: - a, b = float(y), float(x) - else: - a, b = float(x), float(y) - - operator = OPERATORS[operation] - result = operator.func(a, b) - - if operator.type == Type.ARITHMETIC: - if operation == POWER and y[0] == MINUS: - return f'{MINUS}{result}' - return f'{PLUS}{result}' if result > 0 else str(result) - - if operator.type == Type.COMPARISON: - return str(int(result)) - - -def replace_constant(expr: str) -> str: - """ - Calculates constant operations. - - Args: - expr (str): String mathematical expression. - - Returns: - str: Updated expression. - """ - results = re.finditer(mre.REGEXP_CONSTANT, expr) - - for m in results: - name = m.group('name') - - if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): - continue - - answer = str(LIBRARY[name]) - arr = expr.split(name) - - for idx, piece in enumerate(arr[:-1]): - if piece and piece[-1].isalnum(): - arr[idx] = f'{piece}{name}' - elif piece or not idx: - arr[idx] = f'{piece}{answer}' - - expr = ''.join(arr) - - return expr - - -def replace_fanction(expr: str) -> str: - """ - Calculates function operations. - - Args: - expr (str): String mathematical expression. - - Returns: - str: Updated expression. - """ - results = re.finditer(mre.REGEXP_FUNCTION, expr) - - for m in results: - func = m.group('name') - pattern = m.group('pattern') - args = filter(bool, m.group('args').split(',')) - args = [float(v) for v in args] - answer = str(LIBRARY[func](*args)) - expr = expr.replace(pattern, answer) - - return expr - - -def replace_unary_operator(expr: str) -> str: - """ - Calculates unary operations. - - Args: - expr (str): String mathematical expression. - - Returns: - str: Updated expression. - """ - results = re.findall(mre.REGEXP_UNARY, expr) - results.sort(key=len, reverse=True) - - for m in results: - answer = MINUS if m.count(MINUS) % 2 else PLUS - expr = expr.replace(m, answer) - - return expr - - -def replace_compare_operator(expr: str, *operations: list) -> str: - """ - Calculates compare operations. - - Args: - expr (str): String mathematical expression. - *operations (list): List of operations that need to be done on the expression. - - Returns: - str: Updated expression. - """ - if re.search(mre.REGEXP_COMPARE, expr): - return replace_bynary_operator(expr, *operations) - - return expr - - -def replace_bynary_operator(expr: str, *operations: list) -> str: - """ - Calculates binary operations. - - Args: - expr (str): String mathematical expression. - *operations (list): List of operations that need to be done on the expression. - - Returns: - str: Updated expression. - """ - for o in operations: - delimeter = o - if o == PLUS or o == MULTIPLE or o == POWER: - delimeter = mre.REGEXP_SCREENING.format(operation=o) - - regexp = mre.REGEXP_BYNARY.format(operation=delimeter) - results = re.findall(regexp, expr) - for m in results: - arr = list(filter(bool, m.split(o))) - if o == MINUS and m[0] == MINUS: - arr[0] = f'{MINUS}{arr[0]}' - if o == POWER: - arr = arr[::-1] - - answer = reduce(lambda a, b: exec_operation(a, b, operation=o), arr) - expr = expr.replace(m, answer) - - return expr - - -def replace_brackets(expr: str) -> str: - """ - Calculates the expression in brackets. - - Args: - expr (str): String mathematical expression. - - Returns: - str: Updated expression. - """ - results = re.findall(mre.REGEXP_BACKETS, expr) - - for m in results: - answer = calc(m[1:-1]) - expr = expr.replace(m, answer) - - return expr - - -def calc(expr: str) -> str: - """ - Calculates the result from the getting expression. - - Args: - expr (str): String mathematical expression. - - Returns: - str: result of calculations. - """ - Operation = namedtuple('Operation', 'func args') - OPERATION_PRIORITY = [ - Operation(replace_constant, []), - Operation(replace_fanction, []), - Operation(replace_brackets, []), - Operation(replace_unary_operator, []), - Operation(replace_bynary_operator, [POWER]), - Operation(replace_bynary_operator, [MULTIPLE, TRUE_DIVISION, FLOOR_DIVISION, MODULE]), - Operation(replace_bynary_operator, [PLUS, MINUS]), - Operation(replace_compare_operator, [EQUAL, NOT_EQUAL, GREAT, GREAT_OR_EQUAL, LESS, LESS_OR_EQUAL]), - ] - - pattern = re.compile(mre.REGEXP_SIMPLE_DIGIT) - while True: - for inst in OPERATION_PRIORITY: - expr = inst.func(expr, *inst.args) - if pattern.match(expr): - return expr - - return expr - - -def import_modules(*modules: list): - """Imports the modules from the list to the global field LIBRARY.""" - for module in modules: - LIBRARY.update(__import__(module).__dict__) - - -def check_spaces(expr: str) -> str: - """ - Checks if an expression has the wrong elements. - - Args: - expr (str): String mathematical expression. - - Returns: - str: cleared expression from spaces. - - Raises: - ValueError: If `expr` is not correct`. - """ - res = re.findall(mre.REGEXP_INCORECT_EXPRETION, expr) - if res: - raise ValueError('expression is not correct') - - return expr.replace(' ', '') - - -def check_brackets(expr: str): - """ - Checks if all brackets have a pair. - - Args: - expr (str): String mathematical expression. - - Raises: - ValueError: If `expr` is not correct`. - """ - stack = [] - for c in expr: - if c == LEFT_BRACKET: - stack.append(c) - elif c == RIGHT_BRACKET and (not stack or stack.pop() != LEFT_BRACKET): - raise ValueError('brackets are not balanced') - - if stack: - raise ValueError('brackets are not balanced') - - -def check_constant(expr: str): - """ - Checks if all constants in the expression are available. - - Args: - expr (str): String mathematical expression. - - Raises: - ValueError: If `expr` is not correct`. - """ - results = re.finditer(mre.REGEXP_CONSTANT, expr) - for m in results: - name = m.group('name') - - if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): - continue - - if name[0].isdigit(): - raise ValueError(f'constant {name} can not start with digit') - - if name not in LIBRARY or callable(LIBRARY[name]): - raise ValueError(f'there is no such constant {name}') - - -def check_function(expr: str): - """ - Checks if all functions in the expression are available. - - Args: - expr (str): String mathematical expression. - - Raises: - ValueError: If `expr` is not correct`. - """ - results = re.finditer(mre.REGEXP_FUNCTION, expr) - for m in results: - name = m.group('name') - pattern = m.group('pattern') - - if name[0].isdigit(): - raise ValueError(f'function {pattern} can not start with digit') - - if name not in LIBRARY or not callable(LIBRARY[name]): - raise ValueError(f'there is no such function {pattern}') - - -def check_expression(expr: str) -> str: - """ - Checks the expression for correctness. - - Args: - expr (str): String mathematical expression. - - Returns: - str: cleared expression. - - Raises: - ValueError: If `expr` is not correct`. - """ - expr = check_spaces(expr) - check_brackets(expr) - check_constant(expr) - check_function(expr) - - global HAS_COMPARE - HAS_COMPARE = True if re.search(mre.REGEXP_COMPARATOR, expr) else False - - return expr - - -def parse_query(): - """ - Convert argument strings to objects and assign them as attributes of the namespace. - - Returns: - Namespace: got data from command line. - """ - parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') - parser.add_argument('expr', metavar='EXPRESSION', help='expression string to evaluate') - parser.add_argument('-m', - '--use-modules', - default=[], - dest='modules', - metavar='MODULE', - nargs='+', - help='additional modules to use') - - return parser.parse_args() - - -def print_answer(string: str): - """ - Converts the resulting string to the desired type and prints it. - - Args: - string (str): String representation of a number. - """ - num = float(string) - match = re.search(mre.REGEXP_NON_ZERO_FRACTION_PART, string) - num = num if match else int(num) - - answer = bool(num) if HAS_COMPARE else num - print(answer) +from library import Library +from parser import parse_query +from checker import check_expression +from replacer import replace_all_mathes +from regexp import has_comparator +from converter import convert_answer def main(): """Performs processing and calculation of the request from the command line and displays it on the screen.""" try: + lib = Library('math') args = parse_query() - import_modules('math', *args.modules) - expr = check_expression(args.expr) - answer = calc(expr) - print_answer(answer) + lib.update(*args.modules) + expr = check_expression(args.expr, lib) + has_compare = has_comparator(expr) + answer = replace_all_mathes(expr, lib) + answer = convert_answer(answer, has_compare) + print(answer) except Exception as e: print(f'ERROR: {e}') diff --git a/final_task/calculator/regexp.py b/final_task/calculator/regexp.py index 2b8e73f5..b17fa32d 100644 --- a/final_task/calculator/regexp.py +++ b/final_task/calculator/regexp.py @@ -19,6 +19,8 @@ """ +import re + REGEXP_DIGIT = r'[+-]?\d+\.\d+e\+\d+|[+-]?\d+\.?\d*|[+-]?\d*\.?\d+' REGEXP_SIMPLE_DIGIT = rf'^({REGEXP_DIGIT})$' REGEXP_SCREENING = rf'\{{operation}}' @@ -42,3 +44,11 @@ r'^[\/*^%<=!>]|' r'[-+*^\/%<=!>]$' ) + +def has_comparator(expr): + match = re.search(REGEXP_COMPARATOR, expr) + return bool(match) + +def has_non_zero_fraction_part(expr): + match = re.search(REGEXP_NON_ZERO_FRACTION_PART, expr) + return bool(match) \ No newline at end of file diff --git a/final_task/calculator/replacer.py b/final_task/calculator/replacer.py new file mode 100644 index 00000000..72f8fd55 --- /dev/null +++ b/final_task/calculator/replacer.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +""" +The module is intended to replace mathematical expressions with their result. + +Example: + replace_unary_operator('1+-+-+-3') + >>> '1-3' + + lib = { + 'e': 2.718281828459045, + 'sum': sum + } + replace_constant('1+e', lib) + >>> '1+2.718281828459045' + + replace_fanction('sum(100,50)', lib) + >>> '150' +""" + +import re +from collections import namedtuple +from functools import reduce +import regexp as mre +from library import Library +from operators import LEFT_BRACKET, MINUS, PLUS, MULTIPLE, POWER, TRUE_DIVISION, FLOOR_DIVISION, MODULE, \ + EQUAL, NOT_EQUAL, GREAT, GREAT_OR_EQUAL, LESS, LESS_OR_EQUAL, exec_operation + +def replace_constant(expr: str, library: Library) -> str: + """ + Calculates constant operations. + + Args: + expr (str): String mathematical expression. + + Returns: + str: Updated expression. + """ + results = re.finditer(mre.REGEXP_CONSTANT, expr) + + for m in results: + name = m.group('name') + + if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): + continue + + answer = str(library[name]) + arr = expr.split(name) + + for idx, piece in enumerate(arr[:-1]): + if piece and piece[-1].isalnum(): + arr[idx] = f'{piece}{name}' + elif piece or not idx: + arr[idx] = f'{piece}{answer}' + + expr = ''.join(arr) + + return expr + + +def replace_fanction(expr: str, library: Library) -> str: + """ + Calculates function operations. + + Args: + expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. + + Returns: + str: Updated expression. + """ + results = re.finditer(mre.REGEXP_FUNCTION, expr) + + for m in results: + func = m.group('name') + pattern = m.group('pattern') + args = filter(bool, m.group('args').split(',')) + args = [float(v) for v in args] + answer = str(library[func](*args)) + expr = expr.replace(pattern, answer) + + return expr + + +def replace_unary_operator(expr: str) -> str: + """ + Calculates unary operations. + + Args: + expr (str): String mathematical expression. + + Returns: + str: Updated expression. + """ + results = re.findall(mre.REGEXP_UNARY, expr) + results.sort(key=len, reverse=True) + + for m in results: + answer = MINUS if m.count(MINUS) % 2 else PLUS + expr = expr.replace(m, answer) + + return expr + + +def replace_compare_operator(expr: str, *operations: list) -> str: + """ + Calculates compare operations. + + Args: + expr (str): String mathematical expression. + *operations (list): List of operations that need to be done on the expression. + + Returns: + str: Updated expression. + """ + if re.search(mre.REGEXP_COMPARE, expr): + return replace_bynary_operator(expr, *operations) + + return expr + + +def replace_bynary_operator(expr: str, *operations: list) -> str: + """ + Calculates binary operations. + + Args: + expr (str): String mathematical expression. + *operations (list): List of operations that need to be done on the expression. + + Returns: + str: Updated expression. + """ + for o in operations: + delimeter = o + if o == PLUS or o == MULTIPLE or o == POWER: + delimeter = mre.REGEXP_SCREENING.format(operation=o) + + regexp = mre.REGEXP_BYNARY.format(operation=delimeter) + results = re.findall(regexp, expr) + for m in results: + arr = list(filter(bool, m.split(o))) + if o == MINUS and m[0] == MINUS: + arr[0] = f'{MINUS}{arr[0]}' + if o == POWER: + arr = arr[::-1] + + answer = reduce(lambda a, b: exec_operation(a, b, operation=o), arr) + expr = expr.replace(m, answer) + + return expr + + +def replace_brackets(expr: str, library: Library) -> str: + """ + Calculates the expression in brackets. + + Args: + expr (str): String mathematical expression. + + Returns: + str: Updated expression. + """ + results = re.findall(mre.REGEXP_BACKETS, expr) + + for m in results: + answer = replace_all_mathes(m[1:-1], library) + expr = expr.replace(m, answer) + + return expr + + +def replace_all_mathes(expr: str, library: Library) -> str: + """ + Calculates the result from the getting expression. + + Args: + expr (str): String mathematical expression. + + Returns: + str: result of calculations. + """ + Operation = namedtuple('Operation', 'func args') + OPERATION_PRIORITY = [ + Operation(replace_constant, [library]), + Operation(replace_fanction, [library]), + Operation(replace_brackets, [library]), + Operation(replace_unary_operator, []), + Operation(replace_bynary_operator, [POWER]), + Operation(replace_bynary_operator, [MULTIPLE, TRUE_DIVISION, FLOOR_DIVISION, MODULE]), + Operation(replace_bynary_operator, [PLUS, MINUS]), + Operation(replace_compare_operator, [EQUAL, NOT_EQUAL, GREAT, GREAT_OR_EQUAL, LESS, LESS_OR_EQUAL]), + ] + + pattern = re.compile(mre.REGEXP_SIMPLE_DIGIT) + while True: + for inst in OPERATION_PRIORITY: + expr = inst.func(expr, *inst.args) + if pattern.match(expr): + return expr + + return expr diff --git a/final_task/calculator/test_checker.py b/final_task/calculator/test_checker.py new file mode 100644 index 00000000..c84ba9d1 --- /dev/null +++ b/final_task/calculator/test_checker.py @@ -0,0 +1,121 @@ +import unittest +from unittest.mock import patch +from library import Library +from checker import check_brackets, check_constant, check_expression, check_function, check_spaces + +class TestCheckFunction(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.lib = Library('math') + + def test_check_spaces(self): + with self.subTest("Throws error if spaces is not correct"): + self.assertRaises(ValueError, lambda: check_spaces('')) + self.assertRaises(ValueError, lambda: check_spaces('------')) + self.assertRaises(ValueError, lambda: check_spaces('-')) + self.assertRaises(ValueError, lambda: check_spaces('+')) + self.assertRaises(ValueError, lambda: check_spaces('1-')) + self.assertRaises(ValueError, lambda: check_spaces('1 + 1 2 3 4 5 6 ')) + self.assertRaises(ValueError, lambda: check_spaces('* *')) + self.assertRaises(ValueError, lambda: check_spaces('/ /')) + self.assertRaises(ValueError, lambda: check_spaces('/ *')) + self.assertRaises(ValueError, lambda: check_spaces('+ *')) + self.assertRaises(ValueError, lambda: check_spaces('1 2')) + self.assertRaises(ValueError, lambda: check_spaces('= =')) + self.assertRaises(ValueError, lambda: check_spaces('! =')) + self.assertRaises(ValueError, lambda: check_spaces('<-+!')) + self.assertRaises(ValueError, lambda: check_spaces('==7')) + self.assertRaises(ValueError, lambda: check_spaces('1 + 2(3 * 4))')) + self.assertRaises(ValueError, lambda: check_spaces('1 = = 2')) + self.assertRaises(ValueError, lambda: check_spaces('1<>2')) + self.assertRaises(ValueError, lambda: check_spaces('1><2')) + + with self.subTest("removes spaces and returns new expretion"): + self.assertEqual(check_spaces('1 + 2'), '1+2') + self.assertEqual(check_spaces('1-2'), '1-2') + self.assertEqual(check_spaces('1 * - 2'), '1*-2') + self.assertEqual(check_spaces('1 == 2'), '1==2') + self.assertEqual(check_spaces('1 <= 2'), '1<=2') + self.assertEqual(check_spaces('1 - sin (1, 2, 3) + - 2'), '1-sin(1,2,3)+-2') + self.assertEqual(check_spaces('sin(pi/2)'), 'sin(pi/2)') + self.assertTrue(check_spaces('sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))')) + self.assertTrue(check_spaces('time()-e-(1+1)/60+1-1*1//10000%1000^2==1==1<=3>=5<1>1')) + + val = ('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)') + self.assertTrue(check_spaces(val)) + + def test_check_brackets(self): + with self.subTest("Throws error if brackets are not unpaired"): + self.assertRaises(ValueError, lambda: check_brackets('(')) + self.assertRaises(ValueError, lambda: check_brackets(')')) + self.assertRaises(ValueError, lambda: check_brackets('())(')) + self.assertRaises(ValueError, lambda: check_brackets('(()))')) + self.assertRaises(ValueError, lambda: check_brackets(')()(')) + + with self.subTest("returns nothing if expretion is good"): + self.assertIsNone(check_brackets('')) + self.assertIsNone(check_brackets('()')) + self.assertIsNone(check_brackets('((()))()')) + self.assertIsNone(check_brackets('()()()()')) + self.assertIsNone(check_brackets('(()(()())())')) + + def test_check_constant(self): + with self.subTest("Throws error if environment does not have constant"): + self.assertRaises(ValueError, lambda: check_constant('constant', self.lib)) + self.assertRaises(ValueError, lambda: check_constant('constant + 5', self.lib)) + self.assertRaises(ValueError, lambda: check_constant('sin(1) + constant + 7', self.lib)) + + with self.subTest("Throws error if constant name starts with digit"): + self.assertRaises(ValueError, lambda: check_constant('10constant', self.lib)) + self.assertRaises(ValueError, lambda: check_constant('10constant + 5', self.lib)) + self.assertRaises(ValueError, lambda: check_constant('sin(1) + 10constant + 7', self.lib)) + + with self.subTest("returns nothing if expretion is good"): + self.assertIsNone(check_constant('', self.lib)) + self.assertIsNone(check_constant('e', self.lib)) + self.assertIsNone(check_constant('sin(21)', self.lib)) + self.assertIsNone(check_constant('sin(21) + e', self.lib)) + self.assertIsNone(check_constant('2.4178516392292583e+24 + 5', self.lib)) + + def test_check_function(self): + with self.subTest("Throws error if environment does not have function"): + self.assertRaises(ValueError, lambda: check_function('multiply()', self.lib)) + self.assertRaises(ValueError, lambda: check_function('multiply(5,7)', self.lib)) + self.assertRaises(ValueError, lambda: check_function('multiply() + 7', self.lib)) + + with self.subTest("Throws error if function name starts with digit"): + self.assertRaises(ValueError, lambda: check_function('10log()', self.lib)) + self.assertRaises(ValueError, lambda: check_function('10log(1)', self.lib)) + self.assertRaises(ValueError, lambda: check_function('10log(5,7)', self.lib)) + self.assertRaises(ValueError, lambda: check_function('10log() + 7', self.lib)) + + with self.subTest("returns nothing if expretion is good"): + self.assertIsNone(check_function('', self.lib)) + self.assertIsNone(check_function('e', self.lib)) + self.assertIsNone(check_function('sin(21)', self.lib)) + self.assertIsNone(check_function('sin(21) + e', self.lib)) + self.assertIsNone(check_function('2.4178516392292583e+24 + 5', self.lib)) + + def test_check_expression(self): + with self.subTest("calls methods check_spaces, check_brackets, check_constant, check_function"), \ + patch('checker.check_spaces', return_value='1') as check_spaces, \ + patch('checker.check_brackets') as check_brackets, \ + patch('checker.check_constant') as check_constant, \ + patch('checker.check_function') as check_function: + + check_expression('1', self.lib) + + self.assertTrue(check_spaces.called) + self.assertTrue(check_brackets.called) + self.assertTrue(check_constant.called) + self.assertTrue(check_function.called) + + with self.subTest("returns expression without spaces"): + self.assertEqual(check_expression('1 + 2', self.lib), '1+2') + self.assertEqual(check_expression('1-2', self.lib), '1-2') + self.assertEqual(check_expression('1 * - 2', self.lib), '1*-2') + self.assertEqual(check_expression('1 - sin (1, 2, 3) + - 2', self.lib), '1-sin(1,2,3)+-2') + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/test_converter.py b/final_task/calculator/test_converter.py new file mode 100644 index 00000000..671c39d7 --- /dev/null +++ b/final_task/calculator/test_converter.py @@ -0,0 +1,13 @@ +import unittest +from converter import convert_answer + +class TestConverterFunction(unittest.TestCase): + def test_convert_answer(self): + with self.subTest("returns correct answer"): + self.assertEqual(convert_answer('-1', False), '-1') + self.assertEqual(convert_answer('0', False), '0') + self.assertEqual(convert_answer('-1', True), 'True') + self.assertEqual(convert_answer('0', True), 'False') + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/test_library.py b/final_task/calculator/test_library.py new file mode 100644 index 00000000..3fe25a6a --- /dev/null +++ b/final_task/calculator/test_library.py @@ -0,0 +1,36 @@ +import unittest +from library import Library + +class TestLibraryClass(unittest.TestCase): + def test__init__(self): + with self.subTest("contains around and abs functon by default"): + lib = Library() + + self.assertTrue('round' in lib) + self.assertTrue('abs' in lib) + + with self.subTest("can get module names and adds their variables to your own dictionary"): + lib = Library('math', 'os', 'time') + + self.assertTrue('path' in lib) + self.assertTrue('clock' in lib) + self.assertTrue('pi' in lib) + + def test_update(self): + with self.subTest("get module names and adds their variables to your own dictionary"): + lib = Library() + self.assertFalse('path' in lib) + + lib.update('os') + self.assertTrue('path' in lib) + + lib.update('sys', 'time') + self.assertTrue('stdin' in lib) + self.assertTrue('clock' in lib) + + with self.subTest("raises error if veriable is not found"): + self.assertRaises(ModuleNotFoundError, lambda: lib.update('bad_module')) + self.assertRaises(ModuleNotFoundError, lambda: lib.update('new_math')) + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/test_operators.py b/final_task/calculator/test_operators.py new file mode 100644 index 00000000..e6156740 --- /dev/null +++ b/final_task/calculator/test_operators.py @@ -0,0 +1,32 @@ +import unittest +from operators import MULTIPLE, POWER, TRUE_DIVISION, FLOOR_DIVISION, MODULE, PLUS, MINUS, LESS, GREAT, EQUAL, \ + LESS_OR_EQUAL, GREAT_OR_EQUAL, NOT_EQUAL, exec_operation + +class TestOperatorFunction(unittest.TestCase): + def test_exec_operation(self): + a, b = '3', '7' + + with self.subTest("Arithmetic operations return currect sting value"): + self.assertEqual(exec_operation(a, b, MULTIPLE), '+21.0') + self.assertEqual(exec_operation(b, a, POWER), '+2187.0') + self.assertEqual(exec_operation(a, b, TRUE_DIVISION), '+0.42857142857142855') + self.assertEqual(exec_operation(a, b, FLOOR_DIVISION), '0.0') + self.assertEqual(exec_operation(a, b, MODULE), '+3.0') + self.assertEqual(exec_operation(a, b, PLUS), '+10.0') + self.assertEqual(exec_operation(a, b, MINUS), '-4.0') + + with self.subTest("Comparison operations return currect sting value 1 (True) or 0 (False)"): + self.assertEqual(float(exec_operation(a, b, LESS)), a < b) + self.assertEqual(float(exec_operation(a, b, LESS_OR_EQUAL)), a <= b) + self.assertEqual(float(exec_operation(a, b, EQUAL)), a == b) + self.assertEqual(float(exec_operation(a, b, NOT_EQUAL)), a != b) + self.assertEqual(float(exec_operation(a, b, GREAT_OR_EQUAL)), a >= b) + self.assertEqual(float(exec_operation(a, b, GREAT)), a > b) + + with self.subTest("If don't have operation throw error"): + self.assertRaises(ValueError, lambda: exec_operation(a, b, '**')) + self.assertRaises(ValueError, lambda: exec_operation(a, b, '&&')) + self.assertRaises(ValueError, lambda: exec_operation(a, b, '||')) + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/test_parser.py b/final_task/calculator/test_parser.py new file mode 100644 index 00000000..30005295 --- /dev/null +++ b/final_task/calculator/test_parser.py @@ -0,0 +1,15 @@ +import unittest +from parser import parse_query + +class TestParserFunction(unittest.TestCase): + def test_parse_query(self): + with self.subTest("return currect value"): + import sys + + sys.argv = ['pycalc.py', 'time()/60', '-m', 'time', 'os', 'math'] + args = parse_query() + self.assertEqual(args.expr, 'time()/60') + self.assertEqual(args.modules, ['time', 'os', 'math']) + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/test_pycalc.py b/final_task/calculator/test_pycalc.py new file mode 100644 index 00000000..fa1b05ef --- /dev/null +++ b/final_task/calculator/test_pycalc.py @@ -0,0 +1,35 @@ +import io +import unittest +from unittest.mock import patch +from pycalc import main + +class TestPycalcFunction(unittest.TestCase): + def test_main(self): + with self.subTest("calls methods parse_query, Library.update, check_expression, replace_all_mathes, convert_answer"), \ + patch('pycalc.parse_query') as parse_query, \ + patch('pycalc.Library.update') as import_modules, \ + patch('pycalc.check_expression') as check_expression, \ + patch('pycalc.has_comparator') as has_comparator, \ + patch('pycalc.replace_all_mathes') as replace_all_mathes, \ + patch('pycalc.convert_answer', return_value='1') as convert_answer, \ + patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: + + main() + self.assertEqual(mock_stdout.getvalue(), '1\n') + + self.assertTrue(parse_query.called) + self.assertTrue(import_modules.called) + self.assertTrue(replace_all_mathes.called) + self.assertTrue(has_comparator.called) + self.assertTrue(check_expression.called) + self.assertTrue(convert_answer.called) + + with self.subTest("catchs exception"), \ + patch('pycalc.parse_query') as parse_query, \ + patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: + + main() + self.assertTrue(mock_stdout.getvalue().startswith('ERROR: ')) + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/test_regexp.py b/final_task/calculator/test_regexp.py new file mode 100644 index 00000000..7f7397b0 --- /dev/null +++ b/final_task/calculator/test_regexp.py @@ -0,0 +1,21 @@ +import unittest +from regexp import has_comparator, has_non_zero_fraction_part + +class TestRegexpFunction(unittest.TestCase): + def test_has_comparator(self): + with self.subTest("returns correct answer"): + self.assertFalse(has_comparator('1')) + self.assertFalse(has_comparator('1+1', )) + self.assertTrue(has_comparator('1==1')) + self.assertTrue(has_comparator('1>=1')) + + def test_has_non_zero_fraction_part(self): + with self.subTest("returns correct answer"): + self.assertFalse(has_non_zero_fraction_part('1')) + self.assertFalse(has_non_zero_fraction_part('1.0')) + self.assertTrue(has_non_zero_fraction_part('1.9')) + self.assertTrue(has_non_zero_fraction_part('1.09')) + self.assertTrue(has_non_zero_fraction_part('1.00000000000001')) + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/test_replacer.py b/final_task/calculator/test_replacer.py new file mode 100644 index 00000000..cce6fae1 --- /dev/null +++ b/final_task/calculator/test_replacer.py @@ -0,0 +1,130 @@ +import unittest +from library import Library +from replacer import replace_constant, replace_fanction, replace_brackets, replace_unary_operator, \ + replace_bynary_operator, replace_compare_operator, replace_all_mathes +from operators import MULTIPLE, POWER, TRUE_DIVISION, FLOOR_DIVISION, MODULE, PLUS, MINUS + +class TestReplaceFunction(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.lib = Library('math', 'time') + + def test_replace_constant(self): + with self.subTest("Replaces constant name to constant value"): + self.assertEqual(replace_constant('e', self.lib), '2.718281828459045') + self.assertEqual(replace_constant('e + e', self.lib), '2.718281828459045 + 2.718281828459045') + + with self.subTest("Does not touch function and digit"): + self.assertEqual(replace_constant('log()', self.lib), 'log()') + self.assertEqual(replace_constant('log(e)', self.lib), 'log(2.718281828459045)') + self.assertEqual(replace_constant('log(e) + e', self.lib), 'log(2.718281828459045) + 2.718281828459045') + self.assertEqual(replace_constant('2.161727821137838e+16', self.lib), '2.161727821137838e+16') + self.assertEqual(replace_constant('time(e) + e', self.lib), 'time(2.718281828459045) + 2.718281828459045') + + def test_replace_fanction(self): + with self.subTest("Replaces function expression to function result"): + self.assertEqual(replace_fanction('log10(100)', self.lib), '2.0') + self.assertEqual(replace_fanction('log10(100) + log10(100)', self.lib), '2.0 + 2.0') + self.assertEqual(replace_fanction('log(100,10)', self.lib), '2.0') + + with self.subTest("Does not touch constants"): + self.assertEqual(replace_fanction('log10(100) + log(e)', self.lib), '2.0 + log(e)') + self.assertEqual(replace_fanction('log10(100) + e', self.lib), '2.0 + e') + self.assertEqual(replace_fanction('log10(e)', self.lib), 'log10(e)') + self.assertEqual(replace_fanction('log10(e) + 1', self.lib), 'log10(e) + 1') + + with self.subTest("Can receive seveeral arguments"): + self.assertEqual(replace_fanction('log(100,10)', self.lib), '2.0') + self.assertEqual(replace_fanction('hypot(-2,0)', self.lib), '2.0') + self.assertEqual(replace_fanction('hypot(-2,0) + log(100,10)', self.lib), '2.0 + 2.0') + + def test_replace_unary_operator(self): + with self.subTest("Replaces sequence of unary operators"): + self.assertEqual(replace_unary_operator('+---+1'), '-1') + self.assertEqual(replace_unary_operator('+--+1'), '+1') + self.assertEqual(replace_unary_operator('-13'), '-13') + self.assertEqual(replace_unary_operator('-+---+-1'), '-1') + + def test_replace_bynary_operator(self): + with self.subTest("Replaces sequence of bynary operators"): + self.assertEqual(float(replace_bynary_operator('1*2*3*4', MULTIPLE)), eval('1*2*3*4')) + self.assertEqual(float(replace_bynary_operator('2^3^4', POWER)), eval('2**3**4')) + self.assertEqual(float(replace_bynary_operator('1/2/3/4', TRUE_DIVISION)), eval('1/2/3/4')) + self.assertEqual(float(replace_bynary_operator('1//2//3', FLOOR_DIVISION)), eval('1//2//3')) + self.assertEqual(float(replace_bynary_operator('1%2%3%4', MODULE)), eval('1%2%3%4')) + self.assertEqual(float(replace_bynary_operator('1+2+3+4', PLUS)), eval('1+2+3+4')) + self.assertEqual(float(replace_bynary_operator('1-2-3-4', MINUS)), eval('1-2-3-4')) + self.assertEqual(float(replace_bynary_operator('-1-2-3-4', MINUS)), eval('-1-2-3-4')) + + with self.subTest("May receive several operators"): + val = '1*2*3+1+2+3' + self.assertEqual(float(replace_bynary_operator(val, MULTIPLE, PLUS)), eval(val)) + val = '-1-2-3-4+1+2+3+4' + self.assertEqual(float(replace_bynary_operator(val, MINUS, PLUS)), eval(val)) + + def test_replace_brackets(self): + with self.subTest("Replaces inner brackets to result"): + self.assertEqual(replace_brackets('(1*2*3*4)', self.lib), '+24.0') + self.assertEqual(replace_brackets('1+(2+3*2)*3', self.lib), '1++8.0*3') + self.assertEqual(replace_brackets('10*(2+1)', self.lib), '10*+3.0') + self.assertEqual(replace_brackets('(100)', self.lib), '100') + self.assertEqual(replace_brackets('(((100)))', self.lib), '((100))') + + with self.subTest("Does not touch function brakets"): + self.assertEqual(replace_brackets('log(1*2*3*4)', self.lib), 'log(1*2*3*4)') + self.assertEqual(replace_brackets('log((5+95),10)', self.lib), 'log(+100.0,10)') + + def test_replace_all_mathes(self): + with self.subTest("Calculates unary operations"): + self.assertEqual(replace_all_mathes('-13', self.lib), '-13') + self.assertEqual(replace_all_mathes('6-(-13)', self.lib), '+19.0') + self.assertEqual(replace_all_mathes('1---1', self.lib), '0.0') + self.assertEqual(replace_all_mathes('-+---+-1', self.lib), '-1') + + with self.subTest("Calculates priority operations"): + self.assertEqual(replace_all_mathes('1+2*2', self.lib), '+5.0') + self.assertEqual(replace_all_mathes('1+(2+3*2)*3', self.lib), '+25.0') + self.assertEqual(replace_all_mathes('10*(2+1)', self.lib), '+30.0') + self.assertEqual(replace_all_mathes('10^(2+1)', self.lib), '+1000.0') + self.assertEqual(replace_all_mathes('100/3^2', self.lib), '+11.11111111111111') + self.assertEqual(replace_all_mathes('100/3%2^2', self.lib), '+1.3333333333333357') + + with self.subTest("Calculates constants and functions"): + self.assertEqual(replace_all_mathes('pi+e', self.lib), '+5.859874482048838') + self.assertEqual(replace_all_mathes('log(e)', self.lib), '1.0') + self.assertEqual(replace_all_mathes('sin(pi/2)', self.lib), '1.0') + self.assertEqual(replace_all_mathes('log10(100)', self.lib), '2.0') + self.assertEqual(replace_all_mathes('sin(pi/2)*111*6', self.lib), '+666.0') + self.assertEqual(replace_all_mathes('2*sin(pi/2)', self.lib), '+2.0') + + with self.subTest("Calculates assotiacive operations"): + self.assertEqual(replace_all_mathes('102%12%7', self.lib), '+6.0') + self.assertEqual(replace_all_mathes('100/4/3', self.lib), '+8.333333333333334') + self.assertIn(replace_all_mathes('2^3^4', self.lib), ['2417851639229258349412352.0', '+2.4178516392292583e+24']) + + with self.subTest("Calculates comparation operations"): + self.assertEqual(replace_all_mathes('1+2*3==1+2*3', self.lib), '1') + self.assertEqual(replace_all_mathes('e^5>=e^5+1', self.lib), '0') + self.assertEqual(replace_all_mathes('1+2*4/3+1!=1+2*4/3+2', self.lib), '1') + + with self.subTest("Calculates common operations"): + self.assertEqual(replace_all_mathes('(100)', self.lib), '100') + self.assertEqual(replace_all_mathes('666', self.lib), '666') + self.assertEqual(replace_all_mathes('-.1', self.lib), '-.1') + self.assertEqual(replace_all_mathes('1/3', self.lib), '+0.3333333333333333') + self.assertEqual(replace_all_mathes('1.0/3.0', self.lib), '+0.3333333333333333') + self.assertEqual(replace_all_mathes('.1*2.0^56.0', self.lib), '+7205759403792794.0') + self.assertEqual(replace_all_mathes('e^34', self.lib), '+583461742527453.9') + self.assertEqual(replace_all_mathes('(2.0^(pi/pi+e/e+2.0^0.0))', self.lib), '+8.0') + self.assertEqual(replace_all_mathes('(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)', self.lib), '+2.0') + self.assertEqual(replace_all_mathes('sin(pi/2^1)+log(1*4+2^2+1,3^2)', self.lib), '+2.0') + self.assertEqual(replace_all_mathes('10*e^0*log10(.4-5/-0.1-10)--abs(-53/10)+-5', self.lib), '+16.36381365110605') + self.assertEqual(replace_all_mathes('2.0^(2.0^2.0*2.0^2.0)', self.lib), '+65536.0') + self.assertEqual(replace_all_mathes('sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))', self.lib), '0.76638122986603') + + val = ('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)') + self.assertEqual(replace_all_mathes(val, self.lib), '0.5361064001012783') + +if __name__ == '__main__': + unittest.main() diff --git a/final_task/calculator/tests.py b/final_task/calculator/tests.py deleted file mode 100644 index 52f84957..00000000 --- a/final_task/calculator/tests.py +++ /dev/null @@ -1,337 +0,0 @@ -import io -import unittest -from unittest.mock import patch -import pycalc - - -class TestCalcFunction(unittest.TestCase): - def test_import_modules(self): - pycalc.import_modules() - self.assertFalse('path' in pycalc.LIBRARY) - - pycalc.import_modules('os') - self.assertTrue('path' in pycalc.LIBRARY) - - pycalc.import_modules('sys', 'time') - self.assertTrue('stdin' in pycalc.LIBRARY) - self.assertTrue('clock' in pycalc.LIBRARY) - - with self.assertRaises(ModuleNotFoundError): - pycalc.import_modules('bad_module') - - def test_exec_operation(self): - a, b = '3', '7' - - with self.subTest("Arithmetic operations return currect sting value"): - self.assertEqual(pycalc.exec_operation(a, b, pycalc.MULTIPLE), '+21.0') - self.assertEqual(pycalc.exec_operation(b, a, pycalc.POWER), '+2187.0') - self.assertEqual(pycalc.exec_operation(a, b, pycalc.TRUE_DIVISION), '+0.42857142857142855') - self.assertEqual(pycalc.exec_operation(a, b, pycalc.FLOOR_DIVISION), '0.0') - self.assertEqual(pycalc.exec_operation(a, b, pycalc.MODULE), '+3.0') - self.assertEqual(pycalc.exec_operation(a, b, pycalc.PLUS), '+10.0') - self.assertEqual(pycalc.exec_operation(a, b, pycalc.MINUS), '-4.0') - - with self.subTest("Comparison operations return currect sting value 1 (True) or 0 (False)"): - self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.LESS)), a < b) - self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.LESS_OR_EQUAL)), a <= b) - self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.EQUAL)), a == b) - self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.NOT_EQUAL)), a != b) - self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.GREAT_OR_EQUAL)), a >= b) - self.assertEqual(float(pycalc.exec_operation(a, b, pycalc.GREAT)), a > b) - - with self.subTest("If don't have operation throw error"): - self.assertRaises(ValueError, lambda: pycalc.exec_operation(a, b, '**')) - self.assertRaises(ValueError, lambda: pycalc.exec_operation(a, b, '&&')) - self.assertRaises(ValueError, lambda: pycalc.exec_operation(a, b, '||')) - - def test_replace_constant(self): - with self.subTest("Replaces constant name to constant value"): - self.assertEqual(pycalc.replace_constant('e'), '2.718281828459045') - self.assertEqual(pycalc.replace_constant('e + e'), '2.718281828459045 + 2.718281828459045') - - with self.subTest("Does not touch function and digit"): - self.assertEqual(pycalc.replace_constant('log()'), 'log()') - self.assertEqual(pycalc.replace_constant('log(e)'), 'log(2.718281828459045)') - self.assertEqual(pycalc.replace_constant('log(e) + e'), 'log(2.718281828459045) + 2.718281828459045') - self.assertEqual(pycalc.replace_constant('2.161727821137838e+16'), '2.161727821137838e+16') - self.assertEqual(pycalc.replace_constant('time(e) + e'), 'time(2.718281828459045) + 2.718281828459045') - - def test_replace_fanction(self): - with self.subTest("Replaces function expression to function result"): - self.assertEqual(pycalc.replace_fanction('log10(100)'), '2.0') - self.assertEqual(pycalc.replace_fanction('log10(100) + log10(100)'), '2.0 + 2.0') - self.assertEqual(pycalc.replace_fanction('log(100,10)'), '2.0') - - with self.subTest("Does not touch constants"): - self.assertEqual(pycalc.replace_fanction('log10(100) + log(e)'), '2.0 + log(e)') - self.assertEqual(pycalc.replace_fanction('log10(100) + e'), '2.0 + e') - self.assertEqual(pycalc.replace_fanction('log10(e)'), 'log10(e)') - self.assertEqual(pycalc.replace_fanction('log10(e) + 1'), 'log10(e) + 1') - - with self.subTest("Can receive seveeral arguments"): - self.assertEqual(pycalc.replace_fanction('log(100,10)'), '2.0') - self.assertEqual(pycalc.replace_fanction('hypot(-2,0)'), '2.0') - self.assertEqual(pycalc.replace_fanction('hypot(-2,0) + log(100,10)'), '2.0 + 2.0') - - def test_replace_unary_operator(self): - with self.subTest("Replaces sequence of unary operators"): - self.assertEqual(pycalc.replace_unary_operator('+---+1'), '-1') - self.assertEqual(pycalc.replace_unary_operator('+--+1'), '+1') - self.assertEqual(pycalc.replace_unary_operator('-13'), '-13') - self.assertEqual(pycalc.replace_unary_operator('-+---+-1'), '-1') - - def test_replace_bynary_operator(self): - with self.subTest("Replaces sequence of bynary operators"): - self.assertEqual(float(pycalc.replace_bynary_operator('1*2*3*4', pycalc.MULTIPLE)), eval('1*2*3*4')) - self.assertEqual(float(pycalc.replace_bynary_operator('2^3^4', pycalc.POWER)), eval('2**3**4')) - self.assertEqual(float(pycalc.replace_bynary_operator('1/2/3/4', pycalc.TRUE_DIVISION)), eval('1/2/3/4')) - self.assertEqual(float(pycalc.replace_bynary_operator('1//2//3', pycalc.FLOOR_DIVISION)), eval('1//2//3')) - self.assertEqual(float(pycalc.replace_bynary_operator('1%2%3%4', pycalc.MODULE)), eval('1%2%3%4')) - self.assertEqual(float(pycalc.replace_bynary_operator('1+2+3+4', pycalc.PLUS)), eval('1+2+3+4')) - self.assertEqual(float(pycalc.replace_bynary_operator('1-2-3-4', pycalc.MINUS)), eval('1-2-3-4')) - self.assertEqual(float(pycalc.replace_bynary_operator('-1-2-3-4', pycalc.MINUS)), eval('-1-2-3-4')) - - with self.subTest("May receive several operators"): - val = '1*2*3+1+2+3' - self.assertEqual(float(pycalc.replace_bynary_operator(val, pycalc.MULTIPLE, pycalc.PLUS)), eval(val)) - val = '-1-2-3-4+1+2+3+4' - self.assertEqual(float(pycalc.replace_bynary_operator(val, pycalc.MINUS, pycalc.PLUS)), eval(val)) - - def test_replace_brackets(self): - with self.subTest("Replaces inner brackets to result"): - self.assertEqual(pycalc.replace_brackets('(1*2*3*4)'), '+24.0') - self.assertEqual(pycalc.replace_brackets('1+(2+3*2)*3'), '1++8.0*3') - self.assertEqual(pycalc.replace_brackets('10*(2+1)'), '10*+3.0') - self.assertEqual(pycalc.replace_brackets('(100)'), '100') - self.assertEqual(pycalc.replace_brackets('(((100)))'), '((100))') - - with self.subTest("Does not touch function brakets"): - self.assertEqual(pycalc.replace_brackets('log(1*2*3*4)'), 'log(1*2*3*4)') - self.assertEqual(pycalc.replace_brackets('log((5+95),10)'), 'log(+100.0,10)') - - def test_calc(self): - pycalc.import_modules('math', 'time') - - with self.subTest("Calculates unary operations"): - self.assertEqual(pycalc.calc('-13'), '-13') - self.assertEqual(pycalc.calc('6-(-13)'), '+19.0') - self.assertEqual(pycalc.calc('1---1'), '0.0') - self.assertEqual(pycalc.calc('-+---+-1'), '-1') - - with self.subTest("Calculates priority operations"): - self.assertEqual(pycalc.calc('1+2*2'), '+5.0') - self.assertEqual(pycalc.calc('1+(2+3*2)*3'), '+25.0') - self.assertEqual(pycalc.calc('10*(2+1)'), '+30.0') - self.assertEqual(pycalc.calc('10^(2+1)'), '+1000.0') - self.assertEqual(pycalc.calc('100/3^2'), '+11.11111111111111') - self.assertEqual(pycalc.calc('100/3%2^2'), '+1.3333333333333357') - - with self.subTest("Calculates constants and functions"): - self.assertEqual(pycalc.calc('pi+e'), '+5.859874482048838') - self.assertEqual(pycalc.calc('log(e)'), '1.0') - self.assertEqual(pycalc.calc('sin(pi/2)'), '1.0') - self.assertEqual(pycalc.calc('log10(100)'), '2.0') - self.assertEqual(pycalc.calc('sin(pi/2)*111*6'), '+666.0') - self.assertEqual(pycalc.calc('2*sin(pi/2)'), '+2.0') - - with self.subTest("Calculates assotiacive operations"): - self.assertEqual(pycalc.calc('102%12%7'), '+6.0') - self.assertEqual(pycalc.calc('100/4/3'), '+8.333333333333334') - self.assertIn(pycalc.calc('2^3^4'), ['2417851639229258349412352.0', '+2.4178516392292583e+24']) - - with self.subTest("Calculates comparation operations"): - self.assertEqual(pycalc.calc('1+2*3==1+2*3'), '1') - self.assertEqual(pycalc.calc('e^5>=e^5+1'), '0') - self.assertEqual(pycalc.calc('1+2*4/3+1!=1+2*4/3+2'), '1') - - with self.subTest("Calculates common operations"): - self.assertEqual(pycalc.calc('(100)'), '100') - self.assertEqual(pycalc.calc('666'), '666') - self.assertEqual(pycalc.calc('-.1'), '-.1') - self.assertEqual(pycalc.calc('1/3'), '+0.3333333333333333') - self.assertEqual(pycalc.calc('1.0/3.0'), '+0.3333333333333333') - self.assertEqual(pycalc.calc('.1*2.0^56.0'), '+7205759403792794.0') - self.assertEqual(pycalc.calc('e^34'), '+583461742527453.9') - self.assertEqual(pycalc.calc('(2.0^(pi/pi+e/e+2.0^0.0))'), '+8.0') - self.assertEqual(pycalc.calc('(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)'), '+2.0') - self.assertEqual(pycalc.calc('sin(pi/2^1)+log(1*4+2^2+1,3^2)'), '+2.0') - self.assertEqual(pycalc.calc('10*e^0*log10(.4-5/-0.1-10)--abs(-53/10)+-5'), '+16.36381365110605') - self.assertEqual(pycalc.calc('2.0^(2.0^2.0*2.0^2.0)'), '+65536.0') - self.assertEqual(pycalc.calc('sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))'), '0.76638122986603') - - val = ('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)') - self.assertEqual(pycalc.calc(val), '0.5361064001012783') - - def test_check_spaces(self): - with self.subTest("Throws error if spaces is not correct"): - self.assertRaises(ValueError, lambda: pycalc.check_spaces('')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('------')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('-')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('+')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('1-')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('1 + 1 2 3 4 5 6 ')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('* *')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('/ /')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('/ *')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('+ *')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('1 2')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('= =')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('! =')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('<-+!')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('==7')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('1 + 2(3 * 4))')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('1 = = 2')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('1<>2')) - self.assertRaises(ValueError, lambda: pycalc.check_spaces('1><2')) - - with self.subTest("removes spaces and returns new expretion"): - self.assertEqual(pycalc.check_spaces('1 + 2'), '1+2') - self.assertEqual(pycalc.check_spaces('1-2'), '1-2') - self.assertEqual(pycalc.check_spaces('1 * - 2'), '1*-2') - self.assertEqual(pycalc.check_spaces('1 == 2'), '1==2') - self.assertEqual(pycalc.check_spaces('1 <= 2'), '1<=2') - self.assertEqual(pycalc.check_spaces('1 - sin (1, 2, 3) + - 2'), '1-sin(1,2,3)+-2') - self.assertEqual(pycalc.check_spaces('sin(pi/2)'), 'sin(pi/2)') - self.assertTrue(pycalc.check_spaces('sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))')) - self.assertTrue(pycalc.check_spaces('time()-e-(1+1)/60+1-1*1//10000%1000^2==1==1<=3>=5<1>1')) - - val = ('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)') - self.assertTrue(pycalc.check_spaces(val)) - - def test_check_brackets(self): - with self.subTest("Throws error if brackets are not unpaired"): - self.assertRaises(ValueError, lambda: pycalc.check_brackets('(')) - self.assertRaises(ValueError, lambda: pycalc.check_brackets(')')) - self.assertRaises(ValueError, lambda: pycalc.check_brackets('())(')) - self.assertRaises(ValueError, lambda: pycalc.check_brackets('(()))')) - self.assertRaises(ValueError, lambda: pycalc.check_brackets(')()(')) - - with self.subTest("returns nothing if expretion is good"): - self.assertIsNone(pycalc.check_brackets('')) - self.assertIsNone(pycalc.check_brackets('()')) - self.assertIsNone(pycalc.check_brackets('((()))()')) - self.assertIsNone(pycalc.check_brackets('()()()()')) - self.assertIsNone(pycalc.check_brackets('(()(()())())')) - - def test_check_constant(self): - with self.subTest("Throws error if environment does not have constant"): - self.assertRaises(ValueError, lambda: pycalc.check_constant('constant')) - self.assertRaises(ValueError, lambda: pycalc.check_constant('constant + 5')) - self.assertRaises(ValueError, lambda: pycalc.check_constant('sin(1) + constant + 7')) - - with self.subTest("Throws error if constant name starts with digit"): - self.assertRaises(ValueError, lambda: pycalc.check_constant('10constant')) - self.assertRaises(ValueError, lambda: pycalc.check_constant('10constant + 5')) - self.assertRaises(ValueError, lambda: pycalc.check_constant('sin(1) + 10constant + 7')) - - with self.subTest("returns nothing if expretion is good"): - pycalc.import_modules('math') - - self.assertIsNone(pycalc.check_constant('')) - self.assertIsNone(pycalc.check_constant('e')) - self.assertIsNone(pycalc.check_constant('sin(21)')) - self.assertIsNone(pycalc.check_constant('sin(21) + e')) - self.assertIsNone(pycalc.check_constant('2.4178516392292583e+24 + 5')) - - def test_check_function(self): - with self.subTest("Throws error if environment does not have function"): - self.assertRaises(ValueError, lambda: pycalc.check_function('multiply()')) - self.assertRaises(ValueError, lambda: pycalc.check_function('multiply(5,7)')) - self.assertRaises(ValueError, lambda: pycalc.check_function('multiply() + 7')) - - with self.subTest("Throws error if function name starts with digit"): - self.assertRaises(ValueError, lambda: pycalc.check_function('10log()')) - self.assertRaises(ValueError, lambda: pycalc.check_function('10log(1)')) - self.assertRaises(ValueError, lambda: pycalc.check_function('10log(5,7)')) - self.assertRaises(ValueError, lambda: pycalc.check_function('10log() + 7')) - - with self.subTest("returns nothing if expretion is good"): - pycalc.import_modules('math') - - self.assertIsNone(pycalc.check_function('')) - self.assertIsNone(pycalc.check_function('e')) - self.assertIsNone(pycalc.check_function('sin(21)')) - self.assertIsNone(pycalc.check_function('sin(21) + e')) - self.assertIsNone(pycalc.check_function('2.4178516392292583e+24 + 5')) - - def test_check_expression(self): - with self.subTest("calls methods check_spaces, check_brackets, check_constant, check_function"), \ - patch('pycalc.check_spaces', return_value='1') as check_spaces, \ - patch('pycalc.check_brackets') as check_brackets, \ - patch('pycalc.check_constant') as check_constant, \ - patch('pycalc.check_function') as check_function: - - pycalc.check_expression('1') - - self.assertTrue(check_spaces.called) - self.assertTrue(check_brackets.called) - self.assertTrue(check_constant.called) - self.assertTrue(check_function.called) - - with self.subTest("returns expression without spaces"): - self.assertEqual(pycalc.check_expression('1 + 2'), '1+2') - self.assertEqual(pycalc.check_expression('1-2'), '1-2') - self.assertEqual(pycalc.check_expression('1 * - 2'), '1*-2') - self.assertEqual(pycalc.check_expression('1 - sin (1, 2, 3) + - 2'), '1-sin(1,2,3)+-2') - - def test_parse_query(self): - with self.subTest("return currect value"): - import sys - - sys.argv = ['pycalc.py', 'time()/60', '-m', 'time', 'os', 'math'] - args = pycalc.parse_query() - self.assertEqual(args.expr, 'time()/60') - self.assertEqual(args.modules, ['time', 'os', 'math']) - - def test_print_answer(self): - with self.subTest("print correct answer"), \ - patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: - pycalc.HAS_COMPARE = False - pycalc.print_answer('-1') - self.assertEqual(mock_stdout.getvalue(), '-1\n') - - mock_stdout.truncate(0) - mock_stdout.seek(0) - pycalc.print_answer('0') - self.assertEqual(mock_stdout.getvalue(), '0\n') - - pycalc.HAS_COMPARE = True - - mock_stdout.truncate(0) - mock_stdout.seek(0) - pycalc.print_answer('-1') - self.assertEqual(mock_stdout.getvalue(), 'True\n') - - mock_stdout.truncate(0) - mock_stdout.seek(0) - pycalc.print_answer('0') - self.assertEqual(mock_stdout.getvalue(), 'False\n') - - def test_main(self): - with self.subTest("calls methods parse_query, import_modules, check_expression, calc, print_answer"), \ - patch('pycalc.parse_query') as parse_query, \ - patch('pycalc.import_modules') as import_modules, \ - patch('pycalc.check_expression') as check_expression, \ - patch('pycalc.calc', return_value='1') as calc, \ - patch('pycalc.print_answer') as print_answer: - - pycalc.main() - - self.assertTrue(parse_query.called) - self.assertTrue(import_modules.called) - self.assertTrue(calc.called) - self.assertTrue(check_expression.called) - self.assertTrue(print_answer.called) - - with self.subTest("catchs exception"), \ - patch('pycalc.parse_query') as parse_query, \ - patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: - - pycalc.main() - self.assertTrue(mock_stdout.getvalue().startswith('ERROR: ')) - - -if __name__ == '__main__': - unittest.main() From cdb9a8fefe2fbb2f72d29bf2a8c680fa8268bf99 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 12 May 2019 20:31:19 +0300 Subject: [PATCH 12/19] refactor: change style of code to pycodestyle --- final_task/calculator/checker.py | 1 + final_task/calculator/converter.py | 1 + final_task/calculator/library.py | 4 ++-- final_task/calculator/operators.py | 5 ++++- final_task/calculator/parser.py | 1 + final_task/calculator/regexp.py | 5 +++-- final_task/calculator/replacer.py | 1 + final_task/calculator/test_checker.py | 2 ++ final_task/calculator/test_converter.py | 2 ++ final_task/calculator/test_library.py | 6 ++++-- final_task/calculator/test_operators.py | 2 ++ final_task/calculator/test_parser.py | 2 ++ final_task/calculator/test_pycalc.py | 4 +++- final_task/calculator/test_regexp.py | 2 ++ final_task/calculator/test_replacer.py | 14 ++++++++++---- 15 files changed, 40 insertions(+), 12 deletions(-) diff --git a/final_task/calculator/checker.py b/final_task/calculator/checker.py index 3eec2e8f..6a1ab2c7 100644 --- a/final_task/calculator/checker.py +++ b/final_task/calculator/checker.py @@ -21,6 +21,7 @@ from operators import LEFT_BRACKET, RIGHT_BRACKET from library import Library + def check_spaces(expr: str) -> str: """ Checks if an expression has the wrong elements. diff --git a/final_task/calculator/converter.py b/final_task/calculator/converter.py index 2a04fe7f..acc846fd 100644 --- a/final_task/calculator/converter.py +++ b/final_task/calculator/converter.py @@ -1,5 +1,6 @@ from regexp import has_non_zero_fraction_part + def convert_answer(expr: str, has_compare: bool) -> str: """ Converts the resulting string to the desired type. diff --git a/final_task/calculator/library.py b/final_task/calculator/library.py index 9ef41f02..326ad48e 100644 --- a/final_task/calculator/library.py +++ b/final_task/calculator/library.py @@ -7,8 +7,8 @@ def __init__(self, *modules: list): self['round'] = round self.update(*modules) - + def update(self, *modules: list): """Adds functions and veriables from got module names to dictionary.""" for module in modules: - super().update(__import__(module).__dict__) \ No newline at end of file + super().update(__import__(module).__dict__) diff --git a/final_task/calculator/operators.py b/final_task/calculator/operators.py index 937a4a3b..2a2b178a 100644 --- a/final_task/calculator/operators.py +++ b/final_task/calculator/operators.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -The module is designed to calculate mathematical operations. +The module is designed to calculate mathematical operations. Also contains string representations of operations. Attributes: @@ -43,10 +43,12 @@ EQUAL = '==' NOT_EQUAL = '!=' + class Type: ARITHMETIC = 0 COMPARISON = 1 + Operator = namedtuple('Operator', 'func type') OPERATORS = { MULTIPLE: Operator(mul, Type.ARITHMETIC), @@ -64,6 +66,7 @@ class Type: GREAT: Operator(gt, Type.COMPARISON), } + def exec_operation(x: str, y: str, operation=MULTIPLE) -> str: """Executes the operation and returns the result. diff --git a/final_task/calculator/parser.py b/final_task/calculator/parser.py index 2afb3b22..8d890279 100644 --- a/final_task/calculator/parser.py +++ b/final_task/calculator/parser.py @@ -1,5 +1,6 @@ import argparse + def parse_query(): """ Convert argument strings to objects and assign them as attributes of the namespace. diff --git a/final_task/calculator/regexp.py b/final_task/calculator/regexp.py index b17fa32d..7bad3f98 100644 --- a/final_task/calculator/regexp.py +++ b/final_task/calculator/regexp.py @@ -16,7 +16,6 @@ REGEXP_NON_ZERO_FRACTION_PART (rstr): regular expressions for finding non-zero fraction part. REGEXP_COMPARATOR (rstr): regular expressions for finding comparator. REGEXP_INCORECT_EXPRETION (rstr): regular expressions for defining invalid expressions. - """ import re @@ -45,10 +44,12 @@ r'[-+*^\/%<=!>]$' ) + def has_comparator(expr): match = re.search(REGEXP_COMPARATOR, expr) return bool(match) + def has_non_zero_fraction_part(expr): match = re.search(REGEXP_NON_ZERO_FRACTION_PART, expr) - return bool(match) \ No newline at end of file + return bool(match) diff --git a/final_task/calculator/replacer.py b/final_task/calculator/replacer.py index 72f8fd55..d13c5386 100644 --- a/final_task/calculator/replacer.py +++ b/final_task/calculator/replacer.py @@ -25,6 +25,7 @@ from operators import LEFT_BRACKET, MINUS, PLUS, MULTIPLE, POWER, TRUE_DIVISION, FLOOR_DIVISION, MODULE, \ EQUAL, NOT_EQUAL, GREAT, GREAT_OR_EQUAL, LESS, LESS_OR_EQUAL, exec_operation + def replace_constant(expr: str, library: Library) -> str: """ Calculates constant operations. diff --git a/final_task/calculator/test_checker.py b/final_task/calculator/test_checker.py index c84ba9d1..2c329067 100644 --- a/final_task/calculator/test_checker.py +++ b/final_task/calculator/test_checker.py @@ -3,6 +3,7 @@ from library import Library from checker import check_brackets, check_constant, check_expression, check_function, check_spaces + class TestCheckFunction(unittest.TestCase): @classmethod def setUpClass(cls): @@ -117,5 +118,6 @@ def test_check_expression(self): self.assertEqual(check_expression('1 * - 2', self.lib), '1*-2') self.assertEqual(check_expression('1 - sin (1, 2, 3) + - 2', self.lib), '1-sin(1,2,3)+-2') + if __name__ == '__main__': unittest.main() diff --git a/final_task/calculator/test_converter.py b/final_task/calculator/test_converter.py index 671c39d7..8158cee7 100644 --- a/final_task/calculator/test_converter.py +++ b/final_task/calculator/test_converter.py @@ -1,6 +1,7 @@ import unittest from converter import convert_answer + class TestConverterFunction(unittest.TestCase): def test_convert_answer(self): with self.subTest("returns correct answer"): @@ -9,5 +10,6 @@ def test_convert_answer(self): self.assertEqual(convert_answer('-1', True), 'True') self.assertEqual(convert_answer('0', True), 'False') + if __name__ == '__main__': unittest.main() diff --git a/final_task/calculator/test_library.py b/final_task/calculator/test_library.py index 3fe25a6a..cd155d22 100644 --- a/final_task/calculator/test_library.py +++ b/final_task/calculator/test_library.py @@ -1,11 +1,12 @@ import unittest from library import Library + class TestLibraryClass(unittest.TestCase): def test__init__(self): with self.subTest("contains around and abs functon by default"): lib = Library() - + self.assertTrue('round' in lib) self.assertTrue('abs' in lib) @@ -31,6 +32,7 @@ def test_update(self): with self.subTest("raises error if veriable is not found"): self.assertRaises(ModuleNotFoundError, lambda: lib.update('bad_module')) self.assertRaises(ModuleNotFoundError, lambda: lib.update('new_math')) - + + if __name__ == '__main__': unittest.main() diff --git a/final_task/calculator/test_operators.py b/final_task/calculator/test_operators.py index e6156740..a68b4944 100644 --- a/final_task/calculator/test_operators.py +++ b/final_task/calculator/test_operators.py @@ -2,6 +2,7 @@ from operators import MULTIPLE, POWER, TRUE_DIVISION, FLOOR_DIVISION, MODULE, PLUS, MINUS, LESS, GREAT, EQUAL, \ LESS_OR_EQUAL, GREAT_OR_EQUAL, NOT_EQUAL, exec_operation + class TestOperatorFunction(unittest.TestCase): def test_exec_operation(self): a, b = '3', '7' @@ -28,5 +29,6 @@ def test_exec_operation(self): self.assertRaises(ValueError, lambda: exec_operation(a, b, '&&')) self.assertRaises(ValueError, lambda: exec_operation(a, b, '||')) + if __name__ == '__main__': unittest.main() diff --git a/final_task/calculator/test_parser.py b/final_task/calculator/test_parser.py index 30005295..c0293134 100644 --- a/final_task/calculator/test_parser.py +++ b/final_task/calculator/test_parser.py @@ -1,6 +1,7 @@ import unittest from parser import parse_query + class TestParserFunction(unittest.TestCase): def test_parse_query(self): with self.subTest("return currect value"): @@ -11,5 +12,6 @@ def test_parse_query(self): self.assertEqual(args.expr, 'time()/60') self.assertEqual(args.modules, ['time', 'os', 'math']) + if __name__ == '__main__': unittest.main() diff --git a/final_task/calculator/test_pycalc.py b/final_task/calculator/test_pycalc.py index fa1b05ef..2e5e6872 100644 --- a/final_task/calculator/test_pycalc.py +++ b/final_task/calculator/test_pycalc.py @@ -3,9 +3,10 @@ from unittest.mock import patch from pycalc import main + class TestPycalcFunction(unittest.TestCase): def test_main(self): - with self.subTest("calls methods parse_query, Library.update, check_expression, replace_all_mathes, convert_answer"), \ + with self.subTest("calls methods parse_query, update, check_expression, replace_all_mathes, convert_answer"), \ patch('pycalc.parse_query') as parse_query, \ patch('pycalc.Library.update') as import_modules, \ patch('pycalc.check_expression') as check_expression, \ @@ -31,5 +32,6 @@ def test_main(self): main() self.assertTrue(mock_stdout.getvalue().startswith('ERROR: ')) + if __name__ == '__main__': unittest.main() diff --git a/final_task/calculator/test_regexp.py b/final_task/calculator/test_regexp.py index 7f7397b0..03b8c1fa 100644 --- a/final_task/calculator/test_regexp.py +++ b/final_task/calculator/test_regexp.py @@ -1,6 +1,7 @@ import unittest from regexp import has_comparator, has_non_zero_fraction_part + class TestRegexpFunction(unittest.TestCase): def test_has_comparator(self): with self.subTest("returns correct answer"): @@ -17,5 +18,6 @@ def test_has_non_zero_fraction_part(self): self.assertTrue(has_non_zero_fraction_part('1.09')) self.assertTrue(has_non_zero_fraction_part('1.00000000000001')) + if __name__ == '__main__': unittest.main() diff --git a/final_task/calculator/test_replacer.py b/final_task/calculator/test_replacer.py index cce6fae1..f03bbbd9 100644 --- a/final_task/calculator/test_replacer.py +++ b/final_task/calculator/test_replacer.py @@ -4,11 +4,12 @@ replace_bynary_operator, replace_compare_operator, replace_all_mathes from operators import MULTIPLE, POWER, TRUE_DIVISION, FLOOR_DIVISION, MODULE, PLUS, MINUS + class TestReplaceFunction(unittest.TestCase): @classmethod def setUpClass(cls): cls.lib = Library('math', 'time') - + def test_replace_constant(self): with self.subTest("Replaces constant name to constant value"): self.assertEqual(replace_constant('e', self.lib), '2.718281828459045') @@ -100,7 +101,7 @@ def test_replace_all_mathes(self): with self.subTest("Calculates assotiacive operations"): self.assertEqual(replace_all_mathes('102%12%7', self.lib), '+6.0') self.assertEqual(replace_all_mathes('100/4/3', self.lib), '+8.333333333333334') - self.assertIn(replace_all_mathes('2^3^4', self.lib), ['2417851639229258349412352.0', '+2.4178516392292583e+24']) + self.assertEqual(replace_all_mathes('2^3^4', self.lib), '+2.4178516392292583e+24') with self.subTest("Calculates comparation operations"): self.assertEqual(replace_all_mathes('1+2*3==1+2*3', self.lib), '1') @@ -118,13 +119,18 @@ def test_replace_all_mathes(self): self.assertEqual(replace_all_mathes('(2.0^(pi/pi+e/e+2.0^0.0))', self.lib), '+8.0') self.assertEqual(replace_all_mathes('(2.0^(pi/pi+e/e+2.0^0.0))^(1.0/3.0)', self.lib), '+2.0') self.assertEqual(replace_all_mathes('sin(pi/2^1)+log(1*4+2^2+1,3^2)', self.lib), '+2.0') - self.assertEqual(replace_all_mathes('10*e^0*log10(.4-5/-0.1-10)--abs(-53/10)+-5', self.lib), '+16.36381365110605') self.assertEqual(replace_all_mathes('2.0^(2.0^2.0*2.0^2.0)', self.lib), '+65536.0') - self.assertEqual(replace_all_mathes('sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))', self.lib), '0.76638122986603') + + val = '10*e^0*log10(.4-5/-0.1-10)--abs(-53/10)+-5' + self.assertEqual(replace_all_mathes(val, self.lib), '+16.36381365110605') + + val = 'sin(e^log(e^e^sin(23.0),45.0)+cos(3.0+log10(e^-e)))' + self.assertEqual(replace_all_mathes(val, self.lib), '0.76638122986603') val = ('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)') self.assertEqual(replace_all_mathes(val, self.lib), '0.5361064001012783') + if __name__ == '__main__': unittest.main() From 3d20b3aebed9c4f0da10c0aeddd475f75f12bda5 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Sun, 12 May 2019 21:24:16 +0300 Subject: [PATCH 13/19] docs: add docs for files --- final_task/calculator/checker.py | 6 ++++- final_task/calculator/converter.py | 19 +++++++++++++++ final_task/calculator/library.py | 16 +++++++++++++ final_task/calculator/mtypes.py | 11 +++++++++ final_task/calculator/operators.py | 38 +++++++++++++----------------- final_task/calculator/parser.py | 6 ++--- final_task/calculator/regexp.py | 25 ++++++++++++++++++-- final_task/calculator/replacer.py | 3 +++ 8 files changed, 97 insertions(+), 27 deletions(-) create mode 100644 final_task/calculator/mtypes.py diff --git a/final_task/calculator/checker.py b/final_task/calculator/checker.py index 6a1ab2c7..13e33c1c 100644 --- a/final_task/calculator/checker.py +++ b/final_task/calculator/checker.py @@ -7,13 +7,14 @@ >>> '1+3/2' check_brackets('2*(3+5)') + >>> '2*(3+5)' lib = { 'e': 2.718281828459045, 'sum': sum } - check_constant('e', lib) check_function('sum(100, 50)', lib) + >>> 'sum(100, 50)' """ import re @@ -69,6 +70,7 @@ def check_constant(expr: str, library: Library): Args: expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. Raises: ValueError: If `expr` is not correct`. @@ -93,6 +95,7 @@ def check_function(expr: str, library: Library): Args: expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. Raises: ValueError: If `expr` is not correct`. @@ -115,6 +118,7 @@ def check_expression(expr: str, library: Library) -> str: Args: expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. Returns: str: cleared expression. diff --git a/final_task/calculator/converter.py b/final_task/calculator/converter.py index acc846fd..88976c8f 100644 --- a/final_task/calculator/converter.py +++ b/final_task/calculator/converter.py @@ -1,3 +1,21 @@ +# -*- coding: utf-8 -*- +""" +The module is designed to convert the expression to the desired type. + +Example: + convert_answer('-1', False) + >>> '-1' + + convert_answer('-1', False) + >>> '0' + + convert_answer('-1', True) + >>> 'True' + + convert_answer('0', True) + >>> 'False' +""" + from regexp import has_non_zero_fraction_part @@ -7,6 +25,7 @@ def convert_answer(expr: str, has_compare: bool) -> str: Args: expr (str): String representation of a number. + has_compare (bool): whether the expression contains boolean logic """ num = float(expr) match = has_non_zero_fraction_part(expr) diff --git a/final_task/calculator/library.py b/final_task/calculator/library.py index 326ad48e..9c93795a 100644 --- a/final_task/calculator/library.py +++ b/final_task/calculator/library.py @@ -1,3 +1,19 @@ +# -*- coding: utf-8 -*- +""" +The module is designed for dynamic creation of module libraries. + +Example: + lib = Library('math', 'os') + lib.update('time', 'os') + + lib['e'] + >>> 2.718281828459045 + + lib['sum'](5, 10) + >>> 15 +""" + + class Library(dict): """Class is designed to work with modules.""" def __init__(self, *modules: list): diff --git a/final_task/calculator/mtypes.py b/final_task/calculator/mtypes.py new file mode 100644 index 00000000..2a443e19 --- /dev/null +++ b/final_task/calculator/mtypes.py @@ -0,0 +1,11 @@ +# -*- coding: utf-8 -*- +""" +The module contains operation type constants. + +Attributes: + ARITHMETIC (int): constant type arithmetic operators. + COMPARISON (int): constant type comparison operators. +""" + +ARITHMETIC = 0 +COMPARISON = 1 diff --git a/final_task/calculator/operators.py b/final_task/calculator/operators.py index 2a2b178a..d8c17554 100644 --- a/final_task/calculator/operators.py +++ b/final_task/calculator/operators.py @@ -19,13 +19,14 @@ GREAT_OR_EQUAL (str): possible representation of the operation >= in the expression. EQUAL (str): possible representation of the operation == in the expression. NOT_EQUAL (str): possible representation of the operation != in the expression. - Type (class): static class containing types of operations. OPERATORS (dict): key is string representation of operations, and value is namedtuple(func, type). """ import re from collections import namedtuple from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt +from mtypes import ARITHMETIC, COMPARISON + LEFT_BRACKET = '(' RIGHT_BRACKET = ')' @@ -44,26 +45,21 @@ NOT_EQUAL = '!=' -class Type: - ARITHMETIC = 0 - COMPARISON = 1 - - Operator = namedtuple('Operator', 'func type') OPERATORS = { - MULTIPLE: Operator(mul, Type.ARITHMETIC), - POWER: Operator(pow, Type.ARITHMETIC), - TRUE_DIVISION: Operator(truediv, Type.ARITHMETIC), - FLOOR_DIVISION: Operator(floordiv, Type.ARITHMETIC), - MODULE: Operator(mod, Type.ARITHMETIC), - PLUS: Operator(add, Type.ARITHMETIC), - MINUS: Operator(sub, Type.ARITHMETIC), - LESS: Operator(lt, Type.COMPARISON), - LESS_OR_EQUAL: Operator(le, Type.COMPARISON), - EQUAL: Operator(eq, Type.COMPARISON), - NOT_EQUAL: Operator(ne, Type.COMPARISON), - GREAT_OR_EQUAL: Operator(ge, Type.COMPARISON), - GREAT: Operator(gt, Type.COMPARISON), + MULTIPLE: Operator(mul, ARITHMETIC), + POWER: Operator(pow, ARITHMETIC), + TRUE_DIVISION: Operator(truediv, ARITHMETIC), + FLOOR_DIVISION: Operator(floordiv, ARITHMETIC), + MODULE: Operator(mod, ARITHMETIC), + PLUS: Operator(add, ARITHMETIC), + MINUS: Operator(sub, ARITHMETIC), + LESS: Operator(lt, COMPARISON), + LESS_OR_EQUAL: Operator(le, COMPARISON), + EQUAL: Operator(eq, COMPARISON), + NOT_EQUAL: Operator(ne, COMPARISON), + GREAT_OR_EQUAL: Operator(ge, COMPARISON), + GREAT: Operator(gt, COMPARISON), } @@ -93,10 +89,10 @@ def exec_operation(x: str, y: str, operation=MULTIPLE) -> str: operator = OPERATORS[operation] result = operator.func(a, b) - if operator.type == Type.ARITHMETIC: + if operator.type == ARITHMETIC: if operation == POWER and y[0] == MINUS: return f'{MINUS}{result}' return f'{PLUS}{result}' if result > 0 else str(result) - if operator.type == Type.COMPARISON: + if operator.type == COMPARISON: return str(int(result)) diff --git a/final_task/calculator/parser.py b/final_task/calculator/parser.py index 8d890279..e199aecd 100644 --- a/final_task/calculator/parser.py +++ b/final_task/calculator/parser.py @@ -1,14 +1,14 @@ -import argparse +from argparse import ArgumentParser, Namespace -def parse_query(): +def parse_query() -> Namespace: """ Convert argument strings to objects and assign them as attributes of the namespace. Returns: Namespace: got data from command line. """ - parser = argparse.ArgumentParser(description='Pure-python command-line calculator.') + parser = ArgumentParser(description='Pure-python command-line calculator.') parser.add_argument('expr', metavar='EXPRESSION', help='expression string to evaluate') parser.add_argument('-m', '--use-modules', diff --git a/final_task/calculator/regexp.py b/final_task/calculator/regexp.py index 7bad3f98..28560173 100644 --- a/final_task/calculator/regexp.py +++ b/final_task/calculator/regexp.py @@ -2,6 +2,19 @@ """ The module is a regular expression library for searching math expressions. +Example: + has_comparator('1==3') + >>> True + + has_comparator('1+2') + >>> False + + has_non_zero_fraction_part('1.0') + >>> False + + has_non_zero_fraction_part('1.01') + >>> True + Attributes: REGEXP_DIGIT (rstr): regular expressions for finding numbers. REGEXP_SIMPLE_DIGIT (rstr): regular expressions for checking common digits. @@ -45,11 +58,19 @@ ) -def has_comparator(expr): +def has_comparator(expr: str) -> bool: + """ + Checks if expression has a comparator. + Returns True if the expression contains, otherwise False. + """ match = re.search(REGEXP_COMPARATOR, expr) return bool(match) -def has_non_zero_fraction_part(expr): +def has_non_zero_fraction_part(expr: str) -> bool: + """ + Checks if expression has a non zero fraction part + Returns True if the expression contains, otherwise False. + """ match = re.search(REGEXP_NON_ZERO_FRACTION_PART, expr) return bool(match) diff --git a/final_task/calculator/replacer.py b/final_task/calculator/replacer.py index d13c5386..7fc6f228 100644 --- a/final_task/calculator/replacer.py +++ b/final_task/calculator/replacer.py @@ -32,6 +32,7 @@ def replace_constant(expr: str, library: Library) -> str: Args: expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. Returns: str: Updated expression. @@ -156,6 +157,7 @@ def replace_brackets(expr: str, library: Library) -> str: Args: expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. Returns: str: Updated expression. @@ -175,6 +177,7 @@ def replace_all_mathes(expr: str, library: Library) -> str: Args: expr (str): String mathematical expression. + library (Library): dictionary of functions and constant. Returns: str: result of calculations. From 0d4c4942a16783966e0b08d06ba58e97af5def63 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Mon, 13 May 2019 07:15:17 +0300 Subject: [PATCH 14/19] refactor: rename variables --- final_task/calculator/checker.py | 26 +++++------ final_task/calculator/converter.py | 4 +- final_task/calculator/library.py | 5 ++- final_task/calculator/operators.py | 18 ++++---- final_task/calculator/pycalc.py | 11 +++-- final_task/calculator/replacer.py | 72 +++++++++++++++--------------- 6 files changed, 71 insertions(+), 65 deletions(-) diff --git a/final_task/calculator/checker.py b/final_task/calculator/checker.py index 13e33c1c..6e01ac36 100644 --- a/final_task/calculator/checker.py +++ b/final_task/calculator/checker.py @@ -36,8 +36,8 @@ def check_spaces(expr: str) -> str: Raises: ValueError: If `expr` is not correct`. """ - res = re.findall(mre.REGEXP_INCORECT_EXPRETION, expr) - if res: + matches = re.findall(mre.REGEXP_INCORECT_EXPRETION, expr) + if matches: raise ValueError('expression is not correct') return expr.replace(' ', '') @@ -54,10 +54,10 @@ def check_brackets(expr: str): ValueError: If `expr` is not correct`. """ stack = [] - for c in expr: - if c == LEFT_BRACKET: - stack.append(c) - elif c == RIGHT_BRACKET and (not stack or stack.pop() != LEFT_BRACKET): + for symbol in expr: + if symbol == LEFT_BRACKET: + stack.append(symbol) + elif symbol == RIGHT_BRACKET and (not stack or stack.pop() != LEFT_BRACKET): raise ValueError('brackets are not balanced') if stack: @@ -75,9 +75,9 @@ def check_constant(expr: str, library: Library): Raises: ValueError: If `expr` is not correct`. """ - results = re.finditer(mre.REGEXP_CONSTANT, expr) - for m in results: - name = m.group('name') + matches = re.finditer(mre.REGEXP_CONSTANT, expr) + for match in matches: + name = match.group('name') if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): continue @@ -100,10 +100,10 @@ def check_function(expr: str, library: Library): Raises: ValueError: If `expr` is not correct`. """ - results = re.finditer(mre.REGEXP_FUNCTION, expr) - for m in results: - name = m.group('name') - pattern = m.group('pattern') + matches = re.finditer(mre.REGEXP_FUNCTION, expr) + for match in matches: + name = match.group('name') + pattern = match.group('pattern') if name[0].isdigit(): raise ValueError(f'function {pattern} can not start with digit') diff --git a/final_task/calculator/converter.py b/final_task/calculator/converter.py index 88976c8f..bcbdad6d 100644 --- a/final_task/calculator/converter.py +++ b/final_task/calculator/converter.py @@ -31,6 +31,6 @@ def convert_answer(expr: str, has_compare: bool) -> str: match = has_non_zero_fraction_part(expr) num = num if match else int(num) - answer = bool(num) if has_compare else num + result = bool(num) if has_compare else num - return str(answer) + return str(result) diff --git a/final_task/calculator/library.py b/final_task/calculator/library.py index 9c93795a..1255af76 100644 --- a/final_task/calculator/library.py +++ b/final_task/calculator/library.py @@ -15,7 +15,10 @@ class Library(dict): - """Class is designed to work with modules.""" + """ + Class is designed to work with modules. + It is a dictionary of functions and constants. + """ def __init__(self, *modules: list): super().__init__() diff --git a/final_task/calculator/operators.py b/final_task/calculator/operators.py index d8c17554..9dc4400b 100644 --- a/final_task/calculator/operators.py +++ b/final_task/calculator/operators.py @@ -63,12 +63,12 @@ } -def exec_operation(x: str, y: str, operation=MULTIPLE) -> str: +def exec_operation(val1: str, val2: str, operation=MULTIPLE) -> str: """Executes the operation and returns the result. Args: - x (str): String representation of a number. - y (str): String representation of a number. + val1 (str): String representation of a number. + val2 (str): String representation of a number. Returns: str: result of calculations. @@ -79,18 +79,18 @@ def exec_operation(x: str, y: str, operation=MULTIPLE) -> str: if operation not in OPERATORS: raise ValueError('operation was not found') - if operation == POWER and y[0] == MINUS: - a, b = float(y[1:]), float(x) + if operation == POWER and val2[0] == MINUS: + converted_val1, converted_val2 = float(val2[1:]), float(val1) if operation == POWER: - a, b = float(y), float(x) + converted_val1, converted_val2 = float(val2), float(val1) else: - a, b = float(x), float(y) + converted_val1, converted_val2 = float(val1), float(val2) operator = OPERATORS[operation] - result = operator.func(a, b) + result = operator.func(converted_val1, converted_val2) if operator.type == ARITHMETIC: - if operation == POWER and y[0] == MINUS: + if operation == POWER and val2[0] == MINUS: return f'{MINUS}{result}' return f'{PLUS}{result}' if result > 0 else str(result) diff --git a/final_task/calculator/pycalc.py b/final_task/calculator/pycalc.py index afb37d07..efd5574b 100755 --- a/final_task/calculator/pycalc.py +++ b/final_task/calculator/pycalc.py @@ -18,16 +18,19 @@ def main(): - """Performs processing and calculation of the request from the command line and displays it on the screen.""" + """ + Performs processing and calculation of the request + from the command line and displays it on the screen. + """ try: lib = Library('math') args = parse_query() lib.update(*args.modules) expr = check_expression(args.expr, lib) has_compare = has_comparator(expr) - answer = replace_all_mathes(expr, lib) - answer = convert_answer(answer, has_compare) - print(answer) + result = replace_all_mathes(expr, lib) + result = convert_answer(result, has_compare) + print(result) except Exception as e: print(f'ERROR: {e}') diff --git a/final_task/calculator/replacer.py b/final_task/calculator/replacer.py index 7fc6f228..0b4fee8e 100644 --- a/final_task/calculator/replacer.py +++ b/final_task/calculator/replacer.py @@ -37,22 +37,22 @@ def replace_constant(expr: str, library: Library) -> str: Returns: str: Updated expression. """ - results = re.finditer(mre.REGEXP_CONSTANT, expr) + matches = re.finditer(mre.REGEXP_CONSTANT, expr) - for m in results: - name = m.group('name') + for match in matches: + name = match.group('name') if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): continue - answer = str(library[name]) + result = str(library[name]) arr = expr.split(name) for idx, piece in enumerate(arr[:-1]): if piece and piece[-1].isalnum(): arr[idx] = f'{piece}{name}' elif piece or not idx: - arr[idx] = f'{piece}{answer}' + arr[idx] = f'{piece}{result}' expr = ''.join(arr) @@ -70,15 +70,15 @@ def replace_fanction(expr: str, library: Library) -> str: Returns: str: Updated expression. """ - results = re.finditer(mre.REGEXP_FUNCTION, expr) + matches = re.finditer(mre.REGEXP_FUNCTION, expr) - for m in results: - func = m.group('name') - pattern = m.group('pattern') - args = filter(bool, m.group('args').split(',')) + for match in matches: + func = match.group('name') + pattern = match.group('pattern') + args = filter(bool, match.group('args').split(',')) args = [float(v) for v in args] - answer = str(library[func](*args)) - expr = expr.replace(pattern, answer) + result = str(library[func](*args)) + expr = expr.replace(pattern, result) return expr @@ -93,12 +93,12 @@ def replace_unary_operator(expr: str) -> str: Returns: str: Updated expression. """ - results = re.findall(mre.REGEXP_UNARY, expr) - results.sort(key=len, reverse=True) + matches = re.findall(mre.REGEXP_UNARY, expr) + matches.sort(key=len, reverse=True) - for m in results: - answer = MINUS if m.count(MINUS) % 2 else PLUS - expr = expr.replace(m, answer) + for match in matches: + result = MINUS if match.count(MINUS) % 2 else PLUS + expr = expr.replace(match, result) return expr @@ -131,22 +131,22 @@ def replace_bynary_operator(expr: str, *operations: list) -> str: Returns: str: Updated expression. """ - for o in operations: - delimeter = o - if o == PLUS or o == MULTIPLE or o == POWER: - delimeter = mre.REGEXP_SCREENING.format(operation=o) + for operation in operations: + delimeter = operation + if operation == PLUS or operation == MULTIPLE or operation == POWER: + delimeter = mre.REGEXP_SCREENING.format(operation=operation) regexp = mre.REGEXP_BYNARY.format(operation=delimeter) - results = re.findall(regexp, expr) - for m in results: - arr = list(filter(bool, m.split(o))) - if o == MINUS and m[0] == MINUS: - arr[0] = f'{MINUS}{arr[0]}' - if o == POWER: - arr = arr[::-1] + matches = re.findall(regexp, expr) + for match in matches: + operands = list(filter(bool, match.split(operation))) + if operation == MINUS and match[0] == MINUS: + operands[0] = f'{MINUS}{operands[0]}' + if operation == POWER: + operands = operands[::-1] - answer = reduce(lambda a, b: exec_operation(a, b, operation=o), arr) - expr = expr.replace(m, answer) + result = reduce(lambda acc, val: exec_operation(acc, val, operation=operation), operands) + expr = expr.replace(match, result) return expr @@ -162,11 +162,11 @@ def replace_brackets(expr: str, library: Library) -> str: Returns: str: Updated expression. """ - results = re.findall(mre.REGEXP_BACKETS, expr) + matches = re.findall(mre.REGEXP_BACKETS, expr) - for m in results: - answer = replace_all_mathes(m[1:-1], library) - expr = expr.replace(m, answer) + for match in matches: + result = replace_all_mathes(match[1:-1], library) + expr = expr.replace(match, result) return expr @@ -196,8 +196,8 @@ def replace_all_mathes(expr: str, library: Library) -> str: pattern = re.compile(mre.REGEXP_SIMPLE_DIGIT) while True: - for inst in OPERATION_PRIORITY: - expr = inst.func(expr, *inst.args) + for operation in OPERATION_PRIORITY: + expr = operation.func(expr, *operation.args) if pattern.match(expr): return expr From d7abb7f0f860e656c3528c995545f97e2b25be68 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Mon, 13 May 2019 07:41:19 +0300 Subject: [PATCH 15/19] feat: update setup.py --- final_task/setup.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/final_task/setup.py b/final_task/setup.py index ccb32e08..703c9d39 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,12 +1,20 @@ -from setuptools import setup +import os +from setuptools import setup, find_packages + +def read(fname): + return open(os.path.join(os.path.dirname(__file__), fname)).read() setup( name='pycalc', version='1.0', - packages=["calculator"], description='Pure-python command-line calculator.', + long_description=read('README.md'), + packages=find_packages(), + python_requires='>=3.6', entry_points={ - "console_scripts": ["pycalc=calculator.pycalc:main"] + "console_scripts": [ + "pycalc=calculator.pycalc:main", + ] }, platforms='any', ) From b64f2aedccf829dfba7f48e502172c3885798ccf Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Mon, 13 May 2019 07:47:46 +0300 Subject: [PATCH 16/19] refactor: add blank lines and __init__ file --- final_task/calculator/__init__.py | 0 final_task/setup.py | 2 ++ 2 files changed, 2 insertions(+) create mode 100644 final_task/calculator/__init__.py diff --git a/final_task/calculator/__init__.py b/final_task/calculator/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/final_task/setup.py b/final_task/setup.py index 703c9d39..893d57fb 100644 --- a/final_task/setup.py +++ b/final_task/setup.py @@ -1,9 +1,11 @@ import os from setuptools import setup, find_packages + def read(fname): return open(os.path.join(os.path.dirname(__file__), fname)).read() + setup( name='pycalc', version='1.0', From 21fccf79378d71b0d986ddf8c33b4ff41eb697fe Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Mon, 13 May 2019 09:24:01 +0300 Subject: [PATCH 17/19] refactor: change style of file import --- final_task/calculator/checker.py | 22 +++++++---- final_task/calculator/converter.py | 2 +- final_task/calculator/operators.py | 2 +- final_task/calculator/pycalc.py | 12 +++--- final_task/calculator/replacer.py | 51 ++++++++++++++++++------- final_task/calculator/test_checker.py | 23 ++++------- final_task/calculator/test_converter.py | 2 +- final_task/calculator/test_library.py | 2 +- final_task/calculator/test_operators.py | 18 ++++++++- final_task/calculator/test_parser.py | 2 +- final_task/calculator/test_pycalc.py | 37 ------------------ final_task/calculator/test_regexp.py | 2 +- final_task/calculator/test_replacer.py | 23 +++++++++-- 13 files changed, 108 insertions(+), 90 deletions(-) delete mode 100644 final_task/calculator/test_pycalc.py diff --git a/final_task/calculator/checker.py b/final_task/calculator/checker.py index 6e01ac36..9ac8217b 100644 --- a/final_task/calculator/checker.py +++ b/final_task/calculator/checker.py @@ -18,9 +18,17 @@ """ import re -import regexp as mre -from operators import LEFT_BRACKET, RIGHT_BRACKET -from library import Library +from .library import Library +from .operators import ( + LEFT_BRACKET, + RIGHT_BRACKET, +) +from .regexp import ( + REGEXP_INCORECT_EXPRETION, + REGEXP_CONSTANT, + REGEXP_DIGIT, + REGEXP_FUNCTION, +) def check_spaces(expr: str) -> str: @@ -36,7 +44,7 @@ def check_spaces(expr: str) -> str: Raises: ValueError: If `expr` is not correct`. """ - matches = re.findall(mre.REGEXP_INCORECT_EXPRETION, expr) + matches = re.findall(REGEXP_INCORECT_EXPRETION, expr) if matches: raise ValueError('expression is not correct') @@ -75,11 +83,11 @@ def check_constant(expr: str, library: Library): Raises: ValueError: If `expr` is not correct`. """ - matches = re.finditer(mre.REGEXP_CONSTANT, expr) + matches = re.finditer(REGEXP_CONSTANT, expr) for match in matches: name = match.group('name') - if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): + if name[-1] == LEFT_BRACKET or re.match(REGEXP_DIGIT, name): continue if name[0].isdigit(): @@ -100,7 +108,7 @@ def check_function(expr: str, library: Library): Raises: ValueError: If `expr` is not correct`. """ - matches = re.finditer(mre.REGEXP_FUNCTION, expr) + matches = re.finditer(REGEXP_FUNCTION, expr) for match in matches: name = match.group('name') pattern = match.group('pattern') diff --git a/final_task/calculator/converter.py b/final_task/calculator/converter.py index bcbdad6d..0a404013 100644 --- a/final_task/calculator/converter.py +++ b/final_task/calculator/converter.py @@ -16,7 +16,7 @@ >>> 'False' """ -from regexp import has_non_zero_fraction_part +from .regexp import has_non_zero_fraction_part def convert_answer(expr: str, has_compare: bool) -> str: diff --git a/final_task/calculator/operators.py b/final_task/calculator/operators.py index 9dc4400b..8eb6e948 100644 --- a/final_task/calculator/operators.py +++ b/final_task/calculator/operators.py @@ -25,7 +25,7 @@ import re from collections import namedtuple from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt -from mtypes import ARITHMETIC, COMPARISON +from .mtypes import ARITHMETIC, COMPARISON LEFT_BRACKET = '(' diff --git a/final_task/calculator/pycalc.py b/final_task/calculator/pycalc.py index efd5574b..b019c241 100755 --- a/final_task/calculator/pycalc.py +++ b/final_task/calculator/pycalc.py @@ -9,12 +9,12 @@ """ import re -from library import Library -from parser import parse_query -from checker import check_expression -from replacer import replace_all_mathes -from regexp import has_comparator -from converter import convert_answer +from .library import Library +from .parser import parse_query +from .checker import check_expression +from .replacer import replace_all_mathes +from .regexp import has_comparator +from .converter import convert_answer def main(): diff --git a/final_task/calculator/replacer.py b/final_task/calculator/replacer.py index 0b4fee8e..ad91ec6a 100644 --- a/final_task/calculator/replacer.py +++ b/final_task/calculator/replacer.py @@ -20,10 +20,35 @@ import re from collections import namedtuple from functools import reduce -import regexp as mre -from library import Library -from operators import LEFT_BRACKET, MINUS, PLUS, MULTIPLE, POWER, TRUE_DIVISION, FLOOR_DIVISION, MODULE, \ - EQUAL, NOT_EQUAL, GREAT, GREAT_OR_EQUAL, LESS, LESS_OR_EQUAL, exec_operation +from .library import Library +from .operators import ( + LEFT_BRACKET, + MINUS, + PLUS, + MULTIPLE, + POWER, + TRUE_DIVISION, + FLOOR_DIVISION, + MODULE, + EQUAL, + NOT_EQUAL, + GREAT, + GREAT_OR_EQUAL, + LESS, + LESS_OR_EQUAL, + exec_operation, +) +from .regexp import ( + REGEXP_BACKETS, + REGEXP_CONSTANT, + REGEXP_DIGIT, + REGEXP_SCREENING, + REGEXP_BYNARY, + REGEXP_COMPARE, + REGEXP_UNARY, + REGEXP_FUNCTION, + REGEXP_SIMPLE_DIGIT, +) def replace_constant(expr: str, library: Library) -> str: @@ -37,12 +62,12 @@ def replace_constant(expr: str, library: Library) -> str: Returns: str: Updated expression. """ - matches = re.finditer(mre.REGEXP_CONSTANT, expr) + matches = re.finditer(REGEXP_CONSTANT, expr) for match in matches: name = match.group('name') - if name[-1] == LEFT_BRACKET or re.match(mre.REGEXP_DIGIT, name): + if name[-1] == LEFT_BRACKET or re.match(REGEXP_DIGIT, name): continue result = str(library[name]) @@ -70,7 +95,7 @@ def replace_fanction(expr: str, library: Library) -> str: Returns: str: Updated expression. """ - matches = re.finditer(mre.REGEXP_FUNCTION, expr) + matches = re.finditer(REGEXP_FUNCTION, expr) for match in matches: func = match.group('name') @@ -93,7 +118,7 @@ def replace_unary_operator(expr: str) -> str: Returns: str: Updated expression. """ - matches = re.findall(mre.REGEXP_UNARY, expr) + matches = re.findall(REGEXP_UNARY, expr) matches.sort(key=len, reverse=True) for match in matches: @@ -114,7 +139,7 @@ def replace_compare_operator(expr: str, *operations: list) -> str: Returns: str: Updated expression. """ - if re.search(mre.REGEXP_COMPARE, expr): + if re.search(REGEXP_COMPARE, expr): return replace_bynary_operator(expr, *operations) return expr @@ -134,9 +159,9 @@ def replace_bynary_operator(expr: str, *operations: list) -> str: for operation in operations: delimeter = operation if operation == PLUS or operation == MULTIPLE or operation == POWER: - delimeter = mre.REGEXP_SCREENING.format(operation=operation) + delimeter = REGEXP_SCREENING.format(operation=operation) - regexp = mre.REGEXP_BYNARY.format(operation=delimeter) + regexp = REGEXP_BYNARY.format(operation=delimeter) matches = re.findall(regexp, expr) for match in matches: operands = list(filter(bool, match.split(operation))) @@ -162,7 +187,7 @@ def replace_brackets(expr: str, library: Library) -> str: Returns: str: Updated expression. """ - matches = re.findall(mre.REGEXP_BACKETS, expr) + matches = re.findall(REGEXP_BACKETS, expr) for match in matches: result = replace_all_mathes(match[1:-1], library) @@ -194,7 +219,7 @@ def replace_all_mathes(expr: str, library: Library) -> str: Operation(replace_compare_operator, [EQUAL, NOT_EQUAL, GREAT, GREAT_OR_EQUAL, LESS, LESS_OR_EQUAL]), ] - pattern = re.compile(mre.REGEXP_SIMPLE_DIGIT) + pattern = re.compile(REGEXP_SIMPLE_DIGIT) while True: for operation in OPERATION_PRIORITY: expr = operation.func(expr, *operation.args) diff --git a/final_task/calculator/test_checker.py b/final_task/calculator/test_checker.py index 2c329067..efbcaa09 100644 --- a/final_task/calculator/test_checker.py +++ b/final_task/calculator/test_checker.py @@ -1,7 +1,13 @@ import unittest from unittest.mock import patch -from library import Library -from checker import check_brackets, check_constant, check_expression, check_function, check_spaces +from .library import Library +from .checker import ( + check_brackets, + check_constant, + check_expression, + check_function, + check_spaces, +) class TestCheckFunction(unittest.TestCase): @@ -99,19 +105,6 @@ def test_check_function(self): self.assertIsNone(check_function('2.4178516392292583e+24 + 5', self.lib)) def test_check_expression(self): - with self.subTest("calls methods check_spaces, check_brackets, check_constant, check_function"), \ - patch('checker.check_spaces', return_value='1') as check_spaces, \ - patch('checker.check_brackets') as check_brackets, \ - patch('checker.check_constant') as check_constant, \ - patch('checker.check_function') as check_function: - - check_expression('1', self.lib) - - self.assertTrue(check_spaces.called) - self.assertTrue(check_brackets.called) - self.assertTrue(check_constant.called) - self.assertTrue(check_function.called) - with self.subTest("returns expression without spaces"): self.assertEqual(check_expression('1 + 2', self.lib), '1+2') self.assertEqual(check_expression('1-2', self.lib), '1-2') diff --git a/final_task/calculator/test_converter.py b/final_task/calculator/test_converter.py index 8158cee7..e217834f 100644 --- a/final_task/calculator/test_converter.py +++ b/final_task/calculator/test_converter.py @@ -1,5 +1,5 @@ import unittest -from converter import convert_answer +from .converter import convert_answer class TestConverterFunction(unittest.TestCase): diff --git a/final_task/calculator/test_library.py b/final_task/calculator/test_library.py index cd155d22..7eaedc86 100644 --- a/final_task/calculator/test_library.py +++ b/final_task/calculator/test_library.py @@ -1,5 +1,5 @@ import unittest -from library import Library +from .library import Library class TestLibraryClass(unittest.TestCase): diff --git a/final_task/calculator/test_operators.py b/final_task/calculator/test_operators.py index a68b4944..8576c025 100644 --- a/final_task/calculator/test_operators.py +++ b/final_task/calculator/test_operators.py @@ -1,6 +1,20 @@ import unittest -from operators import MULTIPLE, POWER, TRUE_DIVISION, FLOOR_DIVISION, MODULE, PLUS, MINUS, LESS, GREAT, EQUAL, \ - LESS_OR_EQUAL, GREAT_OR_EQUAL, NOT_EQUAL, exec_operation +from .operators import ( + PLUS, + MINUS, + POWER, + MODULE, + MULTIPLE, + TRUE_DIVISION, + FLOOR_DIVISION, + LESS, + GREAT, + EQUAL, + NOT_EQUAL, + LESS_OR_EQUAL, + GREAT_OR_EQUAL, + exec_operation, +) class TestOperatorFunction(unittest.TestCase): diff --git a/final_task/calculator/test_parser.py b/final_task/calculator/test_parser.py index c0293134..b820bf4a 100644 --- a/final_task/calculator/test_parser.py +++ b/final_task/calculator/test_parser.py @@ -1,5 +1,5 @@ import unittest -from parser import parse_query +from .parser import parse_query class TestParserFunction(unittest.TestCase): diff --git a/final_task/calculator/test_pycalc.py b/final_task/calculator/test_pycalc.py deleted file mode 100644 index 2e5e6872..00000000 --- a/final_task/calculator/test_pycalc.py +++ /dev/null @@ -1,37 +0,0 @@ -import io -import unittest -from unittest.mock import patch -from pycalc import main - - -class TestPycalcFunction(unittest.TestCase): - def test_main(self): - with self.subTest("calls methods parse_query, update, check_expression, replace_all_mathes, convert_answer"), \ - patch('pycalc.parse_query') as parse_query, \ - patch('pycalc.Library.update') as import_modules, \ - patch('pycalc.check_expression') as check_expression, \ - patch('pycalc.has_comparator') as has_comparator, \ - patch('pycalc.replace_all_mathes') as replace_all_mathes, \ - patch('pycalc.convert_answer', return_value='1') as convert_answer, \ - patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: - - main() - self.assertEqual(mock_stdout.getvalue(), '1\n') - - self.assertTrue(parse_query.called) - self.assertTrue(import_modules.called) - self.assertTrue(replace_all_mathes.called) - self.assertTrue(has_comparator.called) - self.assertTrue(check_expression.called) - self.assertTrue(convert_answer.called) - - with self.subTest("catchs exception"), \ - patch('pycalc.parse_query') as parse_query, \ - patch('sys.stdout', new_callable=io.StringIO) as mock_stdout: - - main() - self.assertTrue(mock_stdout.getvalue().startswith('ERROR: ')) - - -if __name__ == '__main__': - unittest.main() diff --git a/final_task/calculator/test_regexp.py b/final_task/calculator/test_regexp.py index 03b8c1fa..e0e74319 100644 --- a/final_task/calculator/test_regexp.py +++ b/final_task/calculator/test_regexp.py @@ -1,5 +1,5 @@ import unittest -from regexp import has_comparator, has_non_zero_fraction_part +from .regexp import has_comparator, has_non_zero_fraction_part class TestRegexpFunction(unittest.TestCase): diff --git a/final_task/calculator/test_replacer.py b/final_task/calculator/test_replacer.py index f03bbbd9..a347f338 100644 --- a/final_task/calculator/test_replacer.py +++ b/final_task/calculator/test_replacer.py @@ -1,8 +1,23 @@ import unittest -from library import Library -from replacer import replace_constant, replace_fanction, replace_brackets, replace_unary_operator, \ - replace_bynary_operator, replace_compare_operator, replace_all_mathes -from operators import MULTIPLE, POWER, TRUE_DIVISION, FLOOR_DIVISION, MODULE, PLUS, MINUS +from .library import Library +from .replacer import ( + replace_constant, + replace_fanction, + replace_brackets, + replace_unary_operator, + replace_bynary_operator, + replace_compare_operator, + replace_all_mathes +) +from .operators import ( + MULTIPLE, + POWER, + TRUE_DIVISION, + FLOOR_DIVISION, + MODULE, + PLUS, + MINUS, +) class TestReplaceFunction(unittest.TestCase): From f3ed670717041a2cf41b92431cc50488c3c847d3 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Mon, 13 May 2019 09:33:58 +0300 Subject: [PATCH 18/19] refactor: remove unnecessary imports --- final_task/calculator/test_checker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/final_task/calculator/test_checker.py b/final_task/calculator/test_checker.py index efbcaa09..5e1c9170 100644 --- a/final_task/calculator/test_checker.py +++ b/final_task/calculator/test_checker.py @@ -1,5 +1,4 @@ import unittest -from unittest.mock import patch from .library import Library from .checker import ( check_brackets, From 7412ff4d9aa4226d84cf8972ce4fee8bbd2e2d90 Mon Sep 17 00:00:00 2001 From: PythonProjectDeveloper Date: Mon, 13 May 2019 13:50:09 +0300 Subject: [PATCH 19/19] refactor: decompose files into folders --- final_task/calculator/checker/__init__.py | 7 +++++++ .../calculator/{ => checker}/checker.py | 6 +++--- .../calculator/{ => checker}/test_checker.py | 2 +- final_task/calculator/converter/__init__.py | 1 + .../calculator/{ => converter}/converter.py | 2 +- .../{ => converter}/test_converter.py | 0 final_task/calculator/library/__init__.py | 1 + .../calculator/{ => library}/library.py | 0 .../calculator/{ => library}/test_library.py | 0 final_task/calculator/operators/__init__.py | 19 +++++++++++++++++++ .../calculator/{ => operators}/operators.py | 2 +- .../{ => operators}/test_operators.py | 0 .../calculator/operators/types/__init__.py | 4 ++++ .../{mtypes.py => operators/types/types.py} | 0 final_task/calculator/parser/__init__.py | 1 + final_task/calculator/{ => parser}/parser.py | 0 .../calculator/{ => parser}/test_parser.py | 0 final_task/calculator/regexp/__init__.py | 17 +++++++++++++++++ final_task/calculator/{ => regexp}/regexp.py | 0 .../calculator/{ => regexp}/test_regexp.py | 0 final_task/calculator/replacer/__init__.py | 9 +++++++++ .../calculator/{ => replacer}/replacer.py | 6 +++--- .../{ => replacer}/test_replacer.py | 4 ++-- 23 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 final_task/calculator/checker/__init__.py rename final_task/calculator/{ => checker}/checker.py (97%) rename final_task/calculator/{ => checker}/test_checker.py (99%) create mode 100644 final_task/calculator/converter/__init__.py rename final_task/calculator/{ => converter}/converter.py (94%) rename final_task/calculator/{ => converter}/test_converter.py (100%) create mode 100644 final_task/calculator/library/__init__.py rename final_task/calculator/{ => library}/library.py (100%) rename final_task/calculator/{ => library}/test_library.py (100%) create mode 100644 final_task/calculator/operators/__init__.py rename final_task/calculator/{ => operators}/operators.py (98%) rename final_task/calculator/{ => operators}/test_operators.py (100%) create mode 100644 final_task/calculator/operators/types/__init__.py rename final_task/calculator/{mtypes.py => operators/types/types.py} (100%) create mode 100644 final_task/calculator/parser/__init__.py rename final_task/calculator/{ => parser}/parser.py (100%) rename final_task/calculator/{ => parser}/test_parser.py (100%) create mode 100644 final_task/calculator/regexp/__init__.py rename final_task/calculator/{ => regexp}/regexp.py (100%) rename final_task/calculator/{ => regexp}/test_regexp.py (100%) create mode 100644 final_task/calculator/replacer/__init__.py rename final_task/calculator/{ => replacer}/replacer.py (98%) rename final_task/calculator/{ => replacer}/test_replacer.py (99%) diff --git a/final_task/calculator/checker/__init__.py b/final_task/calculator/checker/__init__.py new file mode 100644 index 00000000..867c39ab --- /dev/null +++ b/final_task/calculator/checker/__init__.py @@ -0,0 +1,7 @@ +from .checker import ( + check_spaces, + check_brackets, + check_constant, + check_function, + check_expression, +) diff --git a/final_task/calculator/checker.py b/final_task/calculator/checker/checker.py similarity index 97% rename from final_task/calculator/checker.py rename to final_task/calculator/checker/checker.py index 9ac8217b..2ece1ba3 100644 --- a/final_task/calculator/checker.py +++ b/final_task/calculator/checker/checker.py @@ -18,12 +18,12 @@ """ import re -from .library import Library -from .operators import ( +from ..library import Library +from ..operators import ( LEFT_BRACKET, RIGHT_BRACKET, ) -from .regexp import ( +from ..regexp import ( REGEXP_INCORECT_EXPRETION, REGEXP_CONSTANT, REGEXP_DIGIT, diff --git a/final_task/calculator/test_checker.py b/final_task/calculator/checker/test_checker.py similarity index 99% rename from final_task/calculator/test_checker.py rename to final_task/calculator/checker/test_checker.py index 5e1c9170..ada278e8 100644 --- a/final_task/calculator/test_checker.py +++ b/final_task/calculator/checker/test_checker.py @@ -1,5 +1,5 @@ import unittest -from .library import Library +from ..library import Library from .checker import ( check_brackets, check_constant, diff --git a/final_task/calculator/converter/__init__.py b/final_task/calculator/converter/__init__.py new file mode 100644 index 00000000..78622ef8 --- /dev/null +++ b/final_task/calculator/converter/__init__.py @@ -0,0 +1 @@ +from .converter import convert_answer diff --git a/final_task/calculator/converter.py b/final_task/calculator/converter/converter.py similarity index 94% rename from final_task/calculator/converter.py rename to final_task/calculator/converter/converter.py index 0a404013..7ef04aa3 100644 --- a/final_task/calculator/converter.py +++ b/final_task/calculator/converter/converter.py @@ -16,7 +16,7 @@ >>> 'False' """ -from .regexp import has_non_zero_fraction_part +from ..regexp import has_non_zero_fraction_part def convert_answer(expr: str, has_compare: bool) -> str: diff --git a/final_task/calculator/test_converter.py b/final_task/calculator/converter/test_converter.py similarity index 100% rename from final_task/calculator/test_converter.py rename to final_task/calculator/converter/test_converter.py diff --git a/final_task/calculator/library/__init__.py b/final_task/calculator/library/__init__.py new file mode 100644 index 00000000..ff2f4e5f --- /dev/null +++ b/final_task/calculator/library/__init__.py @@ -0,0 +1 @@ +from .library import Library diff --git a/final_task/calculator/library.py b/final_task/calculator/library/library.py similarity index 100% rename from final_task/calculator/library.py rename to final_task/calculator/library/library.py diff --git a/final_task/calculator/test_library.py b/final_task/calculator/library/test_library.py similarity index 100% rename from final_task/calculator/test_library.py rename to final_task/calculator/library/test_library.py diff --git a/final_task/calculator/operators/__init__.py b/final_task/calculator/operators/__init__.py new file mode 100644 index 00000000..c3797f53 --- /dev/null +++ b/final_task/calculator/operators/__init__.py @@ -0,0 +1,19 @@ +from .operators import ( + LEFT_BRACKET, + RIGHT_BRACKET, + MULTIPLE, + POWER, + TRUE_DIVISION, + FLOOR_DIVISION, + MODULE, + PLUS, + MINUS, + LESS, + LESS_OR_EQUAL, + GREAT, + GREAT_OR_EQUAL, + EQUAL, + NOT_EQUAL, + OPERATORS, + exec_operation, +) diff --git a/final_task/calculator/operators.py b/final_task/calculator/operators/operators.py similarity index 98% rename from final_task/calculator/operators.py rename to final_task/calculator/operators/operators.py index 8eb6e948..671f90d9 100644 --- a/final_task/calculator/operators.py +++ b/final_task/calculator/operators/operators.py @@ -25,7 +25,7 @@ import re from collections import namedtuple from operator import mul, truediv, floordiv, mod, add, sub, lt, le, eq, ne, ge, gt -from .mtypes import ARITHMETIC, COMPARISON +from .types import ARITHMETIC, COMPARISON LEFT_BRACKET = '(' diff --git a/final_task/calculator/test_operators.py b/final_task/calculator/operators/test_operators.py similarity index 100% rename from final_task/calculator/test_operators.py rename to final_task/calculator/operators/test_operators.py diff --git a/final_task/calculator/operators/types/__init__.py b/final_task/calculator/operators/types/__init__.py new file mode 100644 index 00000000..ce587534 --- /dev/null +++ b/final_task/calculator/operators/types/__init__.py @@ -0,0 +1,4 @@ +from .types import ( + ARITHMETIC, + COMPARISON, +) diff --git a/final_task/calculator/mtypes.py b/final_task/calculator/operators/types/types.py similarity index 100% rename from final_task/calculator/mtypes.py rename to final_task/calculator/operators/types/types.py diff --git a/final_task/calculator/parser/__init__.py b/final_task/calculator/parser/__init__.py new file mode 100644 index 00000000..f7cefe76 --- /dev/null +++ b/final_task/calculator/parser/__init__.py @@ -0,0 +1 @@ +from .parser import parse_query diff --git a/final_task/calculator/parser.py b/final_task/calculator/parser/parser.py similarity index 100% rename from final_task/calculator/parser.py rename to final_task/calculator/parser/parser.py diff --git a/final_task/calculator/test_parser.py b/final_task/calculator/parser/test_parser.py similarity index 100% rename from final_task/calculator/test_parser.py rename to final_task/calculator/parser/test_parser.py diff --git a/final_task/calculator/regexp/__init__.py b/final_task/calculator/regexp/__init__.py new file mode 100644 index 00000000..0e6b28d3 --- /dev/null +++ b/final_task/calculator/regexp/__init__.py @@ -0,0 +1,17 @@ +from .regexp import ( + REGEXP_DIGIT, + REGEXP_SIMPLE_DIGIT, + REGEXP_SCREENING, + REGEX_NAME, + REGEXP_BACKETS, + REGEXP_FUNCTION, + REGEXP_CONSTANT, + REGEXP_UNARY, + REGEXP_BYNARY, + REGEXP_COMPARE, + REGEXP_NON_ZERO_FRACTION_PART, + REGEXP_COMPARATOR, + REGEXP_INCORECT_EXPRETION, + has_comparator, + has_non_zero_fraction_part, +) diff --git a/final_task/calculator/regexp.py b/final_task/calculator/regexp/regexp.py similarity index 100% rename from final_task/calculator/regexp.py rename to final_task/calculator/regexp/regexp.py diff --git a/final_task/calculator/test_regexp.py b/final_task/calculator/regexp/test_regexp.py similarity index 100% rename from final_task/calculator/test_regexp.py rename to final_task/calculator/regexp/test_regexp.py diff --git a/final_task/calculator/replacer/__init__.py b/final_task/calculator/replacer/__init__.py new file mode 100644 index 00000000..a4f758c6 --- /dev/null +++ b/final_task/calculator/replacer/__init__.py @@ -0,0 +1,9 @@ +from .replacer import ( + replace_constant, + replace_fanction, + replace_unary_operator, + replace_compare_operator, + replace_bynary_operator, + replace_brackets, + replace_all_mathes, +) diff --git a/final_task/calculator/replacer.py b/final_task/calculator/replacer/replacer.py similarity index 98% rename from final_task/calculator/replacer.py rename to final_task/calculator/replacer/replacer.py index ad91ec6a..0c605e41 100644 --- a/final_task/calculator/replacer.py +++ b/final_task/calculator/replacer/replacer.py @@ -20,8 +20,8 @@ import re from collections import namedtuple from functools import reduce -from .library import Library -from .operators import ( +from ..library import Library +from ..operators import ( LEFT_BRACKET, MINUS, PLUS, @@ -38,7 +38,7 @@ LESS_OR_EQUAL, exec_operation, ) -from .regexp import ( +from ..regexp import ( REGEXP_BACKETS, REGEXP_CONSTANT, REGEXP_DIGIT, diff --git a/final_task/calculator/test_replacer.py b/final_task/calculator/replacer/test_replacer.py similarity index 99% rename from final_task/calculator/test_replacer.py rename to final_task/calculator/replacer/test_replacer.py index a347f338..10067e5e 100644 --- a/final_task/calculator/test_replacer.py +++ b/final_task/calculator/replacer/test_replacer.py @@ -1,5 +1,5 @@ import unittest -from .library import Library +from ..library import Library from .replacer import ( replace_constant, replace_fanction, @@ -9,7 +9,7 @@ replace_compare_operator, replace_all_mathes ) -from .operators import ( +from ..operators import ( MULTIPLE, POWER, TRUE_DIVISION,