I have implemented a console calculator.

-   It has support for variables over a set of integers and the following operations: multiplication, integer division, exponentiation, addition and subtraction. You can use parentheses in expressions.

-   The `SyntacticalAnalizer` class implements parsing and syntactic validation of a string entered by the user, converting the expression into reverse polish notation.

-   The `Interpreter` class checks for `NameError` errors and calculates the result of the expression.

-   The `SmartCalculator` class contains the `SyntacticAnalizer` and `Interpreter` classes and provides the user interface.

The code passed the tests, but the state processing turned out to be very noodle-like. I would be grateful for advice on improving the code architecture. Source on [GitHub][1]

    from string import ascii_letters
    from collections import deque
    
    
    class SyntacticalAnalyzer:
        `"""
        The name of a variable (identifier) can contain only Latin letters.
    A variable can have a name consisting of more than one letter.
    The case is also important; for example, n is not the same as N.
    The value can be an integer number or a value of another variable.
    Addition and subtraction operations are allowed.
    Commands begin with a slash and can be: /exit and /help
        """`
        _digits_tags: str = '1234567890'
        _commands = ['/exit', '/help']
        left_part: str = None
        expression_stack: list = []
        operator_priority: dict = {
            '(': 0,
            '+': 2,
            '-': 2,
            '*': 3,
            '/': 3,
            '^': 4,
            ')': 0
        }
        rpn_stack: deque = deque()
    
        @staticmethod
        def check_ascii(name):
            for letter in name:
                if letter not in ascii_letters:
                    return False
            return True
    
        @property
        def checked_string(self) -> str:
            return self.__checked_string
    
        @checked_string.setter
        def checked_string(self, value: str):
            self.__checked_string = value
    
        @property
        def check_result(self):
            return self.res
    
        def __init__(self):
            self.__checked_string: str = ''
            self._state: str = 'assignment operator'
            # scan chain bypass rules
            self.chain_rules: dict = dict(skip=False,
                                          off=False
                                          )
            # check status
            self._status = dict(checker='',
                                error=None,
                                check_res=False,
                                )
            # object passed to the wrapper class
            self.res = dict(error=None,
                            state=None,
                            command=None,
                            left=None,
                            rpn_expression=None
                            )
            # list of test functions
            self._check_chain: list = [self.check_not_empty,
                                       self.check_command_tag,
                                       self.check_command_incorrectness,
                                       self.check_equality_tag,
                                       self.check_left_part,
                                       self.check_right_part,
                                       self.to_rpn
                                       ]
    
        def notify(self, checker: str, check_res: bool):
            """
           Passes the function name and the result of its work to the self._status object
         @param checker: name of the function passed
         @type checker: str
         @param check_res: result of the function passed
         @type check_res: bool
         @return: None
            """
            self._status['checker'] = checker
            self._status['check_res'] = check_res
    
        def check_status_handler(self):
            """
            Reads the modified self._status object, sets self._state
             and modifies the self.chain_rules object
            @return: None
            """
    
            if self._status['checker'] == 'check_not_empty' and not self._status['check_res']:
                self._status['error'] = 'empty'
                self._state = 'empty'
            if self._status['checker'] == 'check_command_tag':
                if self._status['check_res']:
                    self._state = 'command'
                else:
                    self.chain_rules['skip'] = True
    
            if self._status['checker'] == 'check_command_incorrectness':
                if not self._status['check_res']:
                    self._status['error'] = self.add_command()
                    self.chain_rules['off'] = True
                else:
                    self.chain_rules['off'] = True
            if self._status['checker'] == 'check_equality_tag':
                if not self._status['check_res']:
                    self._state = 'expression'
                    self.chain_rules['skip'] = True
    
            if self._status['checker'] == 'check_left_part':
                if not self._status['check_res']:
                    self._status['error'] = 'Invalid identifier'
    
            if self._status['checker'] == 'check_right_part':
                if not self._status['check_res']:
                    if self._state == 'assignment operator':
                        self._status['error'] = 'Invalid assignment'
                        self.chain_rules['skip'] = True
                    else:
                        self._status['error'] = 'Invalid identifier'
                        self.chain_rules['skip'] = True
            if self._status['checker'] == 'to_rpn':
                if not self._status['check_res']:
                    if self._state == 'assignment operator':
                        self._status['error'] = 'Invalid assignment'
                    else:
                        self._status['error'] = 'Invalid expression'
    
        def perform_res(self):
            """
            Checks self._state and self._status. Fills out the dictionary self.res
            @return: None
            """
            self.res['state'] = self._state
            if self._state == 'empty':
                self.res['error'] = 'empty'
                self.chain_rules['off'] = True
            if self._state == 'command':
                if self._status['error'] is None:
                    self.res['command'] = self.add_command()
                else:
                    self.res['error'] = self._status['error']
            if self._state == 'assignment operator':
                if self._status['error'] is None:
                    self.res['left'] = self.left_part
                    self.res['rpn_expression'] = self.rpn_stack
                else:
                    self.res['error'] = self._status['error']
            if self._state == 'expression':
                if self._status['error'] is None:
                    self.res['rpn_expression'] = self.rpn_stack
                else:
                    self.res['error'] = self._status['error']
    
        def clear_init_fields(self):
            """
            Clears all constructor fields before checking for a new line
            @return: None
            """
            self._state = 'assignment operator'
            self._status['checker'] = ''
            self._status['error'] = None
            self._status['check_res'] = False
            self.chain_rules['skip'] = False
            self.chain_rules['off'] = False
    
            for key, value in self.res.items():
                if type(value) != dict:
                    self.res[key] = None
            self.expression_stack = []
    
        def run_check_chain(self):
            """
            Starts a string check chain. Reads objects self.chain_rules and
            self._status, if the skip == True property skips the next check,
             if the property off == True or one of the checks has completed
             with an error, terminates its work
            @return: None
            """
            j = -1
            self.clear_init_fields()
    
            for i, check in enumerate(self._check_chain):
                if j == i:
                    self.chain_rules['skip'] = False
                if self.chain_rules['skip']:
                    j = i + 1 if i + 1 < len(self._check_chain) else -1
                    continue
                self.run_check(check)
                self.check_status_handler()
                if self._status['error'] is not None:
                    break
                if self.chain_rules['off']:
                    break
            self.perform_res()
    
        def run_check(self, check_func):
            """
            @type check_func: function
            """
            result = check_func()
            self.notify(check_func.__name__, result)
    
        def check_not_empty(self):
            return self.checked_string != ''
    
        def check_command_tag(self):
            return self.checked_string.startswith('/')
    
        def check_command_incorrectness(self):
            return self.checked_string in self._commands
    
        def add_command(self) -> str:
            for _command in self._commands:
                if self.checked_string == _command:
                    return _command
            return 'Unknown command'
    
        def check_equality_tag(self) -> bool:
            return '=' in self.checked_string
    
        def is_variable(self, name: str) -> bool:
            return all([len(name) >= 1, self.check_ascii(name)])
    
        def check_left_part(self):
            if self._state == 'assignment operator':
                self.left_part = self.checked_string.split('=')[0].strip()
                return self.is_variable(self.left_part)
    
        @staticmethod
        def get_fragment_params(value: str, end):
            out_str = ''
            pos = 0
            sym = value[0]
            while sym not in end:
                out_str += sym
                try:
                    pos += 1
                    sym = value[pos]
                except IndexError:
                    return out_str, None
            return out_str, pos
    
        @staticmethod
        def is_operator(item: str):
            item_list: list = item.strip().split(' ')
            my_str = ''.join(item_list)
            if my_str[0] in '+-':
                for el in my_str:
                    if el not in '+-':
                        return False
            if my_str[0] in '/*^':
                if len(my_str) > 1:
                    return False
            return True
    
        @staticmethod
        def is_digit(item: str):
            if item[0] == '0':
                if len(item) != 1:
                    return False
                return True
            for el in item:
                if el not in '1234567890':
                    return False
            return True
    
        @staticmethod
        def is_left_parenthesis(item: str):
            for el in item:
                if el not in '(':
                    return False
            return True
    
        @staticmethod
        def is_right_parenthesis(item: str):
            for el in item:
                if el not in ')':
                    return False
            return True
    
        @staticmethod
        def get_first(value: str):
            return value[0] if value else None
    
        @staticmethod
        def get_tag(letter: str):
            if letter in ascii_letters:
                return 'variable'
            if letter in '-+/*^':
                return 'operator'
            if letter in '1234567890':
                return 'digit'
            if letter in '()':
                return 'left parenthesis' if letter == '(' else 'right parenthesis'
    
        @staticmethod
        def get_end_tag(tag: str) -> str:
            if tag == 'variable':
                return ' )+-/*^'
            if tag == 'operator':
                return '(0123456789' + ascii_letters
            if tag == 'digit':
                return ' )+-/*^'
            if tag == 'left parenthesis':
                return ' 0123456789' + ascii_letters + '+-'
            if tag == 'right parenthesis':
                return ' +-/*^'
    
        @staticmethod
        def transform_operator(el: str):
            if '-' in el or '+' in el:
                minus_cnt = el.count('-')
                if minus_cnt:
                    return '-' if minus_cnt % 2 != 0 else '+'
                return '+'
            return el
    
        @staticmethod
        def transform_parenthesis(el: str):
            return list(el)
    
        def transform_element(self, el: str, tag: str):
            if tag == 'operator':
                return self.transform_operator(el)
            if tag in ['left parenthesis', 'right parenthesis']:
                return self.transform_parenthesis(el.rstrip())
            return el.rstrip()
    
        @staticmethod
        def add_el(container: list, el):
            if type(el) == list:
                container += el
            else:
                container.append(el.rstrip())
    
        def check_right_part(self):
            next_pos = 0
            if self._state == 'assignment operator':
                input_str = self.checked_string.split('=', 1)[1].strip()
            else:
                input_str = self.checked_string.strip()
            if not input_str:
                return False
            while True:
                current: str = input_str[next_pos:]
                sym: str = self.get_first(current)
                name = self.get_tag(sym)
                end_tag = self.get_end_tag(name)
                el, offset = self.get_fragment_params(value=current, end=end_tag)
    
                conditions = [
                    self.is_variable(el),
                    self.is_operator(el),
                    self.is_digit(el),
                    self.is_left_parenthesis(el),
                    self.is_right_parenthesis(el)
                ]
                if not any(conditions):
                    return False
                el = self.transform_element(el=el, tag=name)
                if not self.expression_stack or self.expression_stack[-1] == '(':
                    if el in '+-':
                        self.expression_stack.append('0')
                self.add_el(self.expression_stack, el)
                if offset is None:
                    return True
                temp = current[offset:]
                offset += temp.find(temp.lstrip())
                next_pos += offset
    
        def to_rpn(self):
            f = False
            operators: list = []
            for item in self.expression_stack:
                if self.is_digit(item) or self.is_variable(item):
                    self.rpn_stack.append(item)
                else:
                    if not operators:
                        operators.append(item)
                    else:
                        if item == '(' or self.operator_priority[item] > self.operator_priority[operators[-1]]:
                            operators.append(item)
                        else:
                            if not operators:
                                return False
                            while operators:
                                operator = operators.pop()
                                if operator == '(':
                                    f = True
                                    break
                                self.rpn_stack.append(operator)
                            if item == ')' and not f:
                                return False
                            if item != ')':
                                operators.append(item)
    
            if operators:
                if '(' in operators:
                    return False
                else:
                    while operators:
                        self.rpn_stack.append(operators.pop())
            return True
    
        # End of class SyntacticalAnalyzer
    
    
    class Interpreter:
        bye_string = 'Bye!'
        help_string = 'The program calculates expressions using addition, subtraction, multiplication, integer division' \
                      ' and exponentiation over a set of integers, and also uses variables.'
    
        def __init__(self, obj):
            self.variables: dict = {}
            self.obj = obj
            self.error: str = None
            self.res: int = None
            self.rpn_stack: deque = deque()
    
        def execute(self):
            if not self.analysis_handler():
                return False
            return True
    
        def analysis_handler(self):
            """
            Читает self.obj.
            @return:
            """
            self.rpn_stack = deque()
            self.res = None
            self.error = None
            if self.obj['state'] == 'empty':
                pass
            if self.obj['state'] == 'command':
                if not self.command_handler(self.obj['command']):
                    return False
            if self.obj['state'] == 'expression':
                if not self.expression_handler():
                    print(self.error)
                else:
                    print(self.res)
    
            if self.obj['state'] == 'assignment operator':
                if not self.assignment_handler():
                    print(self.error)
            return True
    
        def command_handler(self, param: str) -> bool:
            if param == '/exit':
                print(self.bye_string)
                return False
            if param == '/help':
                print(self.help_string)
                return True
    
        def expression_handler(self):
            if not self.check_variables():
                return False
            self.res = self.get_expression_result()
            return True
    
        @staticmethod
        def calculate_this(one, two, sign):
            one, two = [int(x) for x in [one, two]]
            if sign == '+':
                return one + two
            if sign == '-':
                return one - two
            if sign == '*':
                return one * two
            if sign == '/':
                return one // two
            if sign == '^':
                return one ** two
    
        @staticmethod
        def is_digit(item: str):
            if item[0] == '0':
                if len(item) != 1:
                    return False
                return True
            for el in item:
                if el not in '1234567890':
                    return False
            return True
    
        def get_expression_result(self):
            result_stack: list = []
    
            while self.rpn_stack:
                item = self.rpn_stack.popleft()
                if self.is_digit(item):
                    result_stack.append(item)
                else:
                    second, first = result_stack.pop(), result_stack.pop()
                    result_stack.append(self.calculate_this(first, second, item))
            return result_stack[0]
    
        def assignment_handler(self):
            if not self.expression_handler():
                return False
            left = self.obj['left']
            self.variables[left] = self.res
            return True
    
        def check_variables(self):
            self.rpn_stack = self.obj['rpn_expression']
            for i, item in enumerate(self.rpn_stack):
                if item in self.variables:
                    self.rpn_stack[i] = self.variables[item]
                else:
                    if item[0] in ascii_letters:
                        return False
            return True
    
    
    class SmartCalculator:
        """
        The name of a variable (identifier) can contain only Latin letters.
    A variable can have a name consisting of more than one letter.
    The case is also important; for example, n is not the same as N.
    The value can be an integer number or a value of another variable.
    It should be possible to set a new value to an existing variable.
    To print the value of a variable you should just type its name.
        """
        _analyzer_methods = ['run_check_chain']
        _interpreter_methods = ['execute', 'analysis_handler']
    
        def __init__(self):
            self._analyzer: SyntacticalAnalyzer = SyntacticalAnalyzer()
            self.analyzer_result: dict = self._analyzer.check_result
            self._interpreter: Interpreter = Interpreter(self.analyzer_result)
    
        def __getattr__(self, item):
            for item in self._analyzer_methods + self._interpreter_methods:
                if item in self._analyzer_methods:
                    return getattr(self._analyzer, item)
                if item in self._interpreter_methods:
                    return getattr(self._interpreter, item)
    
        def run(self):
            while True:
                self._analyzer.checked_string = input().strip()
                self._analyzer.run_check_chain()
                if self._analyzer.res['error'] is not None and self._analyzer.res['error'] != 'empty':
                    print(self._analyzer.res['error'])
                else:
                    if not self._interpreter.execute():
                        return None
    
    
    calculator = SmartCalculator()
    calculator.run()


  [1]: https://github.com/sergeVe/smart-calculator/blob/master/calculator.py