Skip to content

Commit

Permalink
refactoring for more centralized calculator state management
Browse files Browse the repository at this point in the history
  • Loading branch information
newmanrs committed Feb 18, 2022
1 parent fc2719d commit e4f4b7e
Show file tree
Hide file tree
Showing 11 changed files with 93 additions and 56 deletions.
22 changes: 11 additions & 11 deletions rpncalc/classes.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import enum
from rpncalc.globals import stack
from rpncalc.state import state
from rpncalc.exceptions import StackDepletedError


class StackAccessor:
def gen_n(self, n):

if n > len(stack):
if n > len(state.stack):
msg = (
f"Empty stack processing {self} trying "
f"to pop {n} values. Stack: {stack}")
raise IndexError(msg)
f"to pop {n} values. Stack: {state.stack}")
raise StackDepletedError(msg)

for i in range(n):
yield stack.pop()
yield state.stack.pop()

def stack_size(self):
return len(stack)
return len(state.stack)

def take_n(self, n):
if n == 1:
Expand All @@ -33,16 +34,15 @@ def take_3(self):
return self.take_n(3)

def take_all(self):
return self.take_n(len(stack))
return self.take_n(len(state.stack))

def push(self, value):
# use of += in if statement creates local binding
# shrug python things
global stack
if isinstance(value, list):
stack += value
state.stack += value
else:
stack.append(value)
state.stack.append(value)


@enum.unique
Expand All @@ -68,7 +68,7 @@ def __new__(cls, *args):
return obj

def action(self):
msg = f"No action method defined in {self}"
msg = f"Default action not implemented for {self}"
raise NotImplementedError(msg)

def help(self):
Expand Down
8 changes: 8 additions & 0 deletions rpncalc/exceptions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
class RpnCalcError(Exception):
"""Calculator Generic Exception"""
pass


class StackDepletedError(RpnCalcError):
""" Stack is out of items """
pass
6 changes: 0 additions & 6 deletions rpncalc/globals.py

This file was deleted.

4 changes: 2 additions & 2 deletions rpncalc/help.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ def help_string():
"operators are:"
]
msg_foot = [
"Use help(cmd) or help_cmd for more detailed help on"
"specific operators such as help_matsq",
"Use help(cmd) or help_cmd for detailed help on specific operators"
" such as help_matsq",
"",
"Flags:",
" --verbose, -v. Print how the stack is processed",
Expand Down
10 changes: 7 additions & 3 deletions rpncalc/idempotentoperator.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import sys

from rpncalc import storedvalues
from rpncalc.classes import ActionEnum
from rpncalc.globals import stack
from rpncalc.state import state


class IdempotentOperator(ActionEnum):
Expand All @@ -12,10 +11,13 @@ class IdempotentOperator(ActionEnum):
" the stack contains only said value"
print_stored_named = 'print_store', \
"Prints the names and values of all stored constants"
print_state = 'print_state', \
"Print all calculator state including stack and storage"
quit = 'quit'
exit = 'exit'

def action(self):
stack = state.stack
o = type(self)
match self:
case o.print_stack:
Expand All @@ -26,7 +28,9 @@ def action(self):
else:
print(f"Stack: {stack}")
case o.print_stored_named:
print(f"Stored Values {storedvalues.storage}")
print(f"Stored Values {state.stored_values}")
case o.print_state:
print(state.to_json())
case o.quit:
sys.exit(0)
case o.exit:
Expand Down
25 changes: 12 additions & 13 deletions rpncalc/parseinput.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@
from rpncalc.history import HistoryOperator
from rpncalc.storedvalues import get_stored_value_class
from rpncalc.help import HelpOperator, HelpCommand, Help
from rpncalc.globals import stack
import rpncalc.state
import traceback


def compute_rpn(expression, verbose=False, return_copy=True):

# Rollback the stack if parsing the expression throws
# to preserve using the calculator in interactive mode
backup = copy.deepcopy(stack)

# Take state snapshot to roll back to if exceptions
# are encountered
snap = rpncalc.state.state.make_snapshot()
try:
for item in expression:

match item:

case _ if isinstance(item, (int | float)):
stack.append(item)
rpncalc.state.state.stack.append(item)

case _ if hasattr(item, 'action'):
if verbose and hasattr(item, 'verbose_mode_message'):
Expand All @@ -37,16 +37,15 @@ def compute_rpn(expression, verbose=False, return_copy=True):
s = f"No known action in rpn parse loop for item '{item}'"
raise ValueError(s)

except Exception as e:
stack.clear()
for item in backup:
stack.append(item)
raise e
except Exception:
print(traceback.print_exc())
print("Encountered above error, rolling back state changes")
rpncalc.state.state.load_snapshot(snap)

if return_copy:
return copy.deepcopy(stack)
return copy.deepcopy(rpncalc.state.state)
else:
return stack
return rpncalc.state.state


def parse_expression(strexp, verbose=False):
Expand Down
6 changes: 4 additions & 2 deletions rpncalc/rpncalc.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,13 @@ def main():
if parser.interactive:
interactive_loop(parser)
else:
# Commmand line input needs to be added manually to
# calculator history file.
readline.add_history(' '.join(parser.expression))
exp = parse_expression(parser.expression, parser.verbose)
ans = compute_rpn(exp, parser.verbose)
if len(ans) > 0:
print(f"Stack: {ans}")
if len(ans.stack) > 0:
print(f"Stack: {ans.stack}")

if parser.debug:
breakpoint()
7 changes: 4 additions & 3 deletions rpncalc/stackoperator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from rpncalc.classes import ActionEnum
from rpncalc.globals import stack
from rpncalc.state import state


class StackOperator(ActionEnum):
clear_stack = 'clear', "Clear entire stack."
clear_store = 'clear_storage', "Clear stored named variables"
swap2 = 'swap2', "Swap last two elements"
reverse_stack = 'reverse', "Reverse stack contents"
pop_last = 'pop', "Remove and discard last item in stack"
Expand All @@ -12,13 +13,13 @@ def action(self):
o = type(self)
match self:
case o.clear_stack:
stack.clear()
state.clear_stack()
case o.swap2:
v1, v0 = self.take_2()
self.push(v1)
self.push(v0)
case o.reverse_stack:
stack.reverse()
state.stack.reverse()
case o.pop_last:
self.take_1()
case _:
Expand Down
31 changes: 31 additions & 0 deletions rpncalc/state.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import dataclasses
import json


@dataclasses.dataclass
class State:

stack: list = dataclasses.field(default_factory=list)
stored_values: dict = dataclasses.field(default_factory=dict)

def clear_stack(self):
self.stack.clear()

def clear_storage(self):
self.stored_values.clear()

def make_snapshot(self):
return dataclasses.asdict(self)

def load_snapshot(self, snapshot):
for k, v in snapshot.items():
setattr(self, k, v)

def to_json(self):
return json.dumps(self.make_snapshot(), indent=None)

def __len__(self):
return len(state)


state = State()
13 changes: 4 additions & 9 deletions rpncalc/storedvalues.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
from rpncalc.classes import StackAccessor

storage = dict()


def clear_storage():
storage.clear()
from rpncalc.state import state


def get_stored_value_class(arg):
Expand All @@ -31,7 +26,7 @@ def __init__(self, name):
self.name = name

def action(self):
storage[self.name] = self.take_1()
state.stored_values[self.name] = self.take_1()

def verbose_mode_message(self):
print(f"Popping stack into stored value {self.name}")
Expand All @@ -43,9 +38,9 @@ def __init__(self, name):

def action(self):
try:
self.push(storage[self.name])
self.push(state.stored_values[self.name])
except KeyError as e:
k = tuple(storage.keys())
k = tuple(state.stored_values.keys())
if len(k) == 0:
avail = "No stored keys"
elif len(k) == 1:
Expand Down
17 changes: 10 additions & 7 deletions tests/test_rpn.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
import unittest
import numpy
from rpncalc.rpncalc import parse_expression, compute_rpn
import rpncalc.storedvalues
import rpncalc.globals
import rpncalc.state


class TestRPNCalc(unittest.TestCase):

def run_from_expr(self, expr, clear_stored=True, clear_stack=True):
# TODO: Fix the state management here

print(f"\n\"{expr}\"")
ans = compute_rpn(parse_expression(expr))
state = compute_rpn(parse_expression(expr))
# Clear the named storage variables to ensure
# that unittests are independent of order
if clear_stored:
rpncalc.storedvalues.clear_storage()
rpncalc.state.state.clear_storage()
if clear_stack:
rpncalc.globals.clear_stack()
if len(ans) == 1:
ans = ans[0]
rpncalc.state.state.clear_stack()
if len(state.stack) == 1:
ans = state.stack[0]
else:
ans = state.stack
print(ans)
return ans

Expand Down

0 comments on commit e4f4b7e

Please sign in to comment.