From 047559909c1d47959a8be0c4ecfb24202cebc2e2 Mon Sep 17 00:00:00 2001 From: "R. David Murray" Date: Wed, 3 Aug 2016 13:50:40 -0400 Subject: [PATCH 01/10] Fake pmemobj and doctest skeleton for testing accounts. --- nvm/fake_pmemobj.py | 51 +++++++++++++++++++++++ nvm/pmemobj/__init__.py | 2 + tests/accounts.doctest | 89 +++++++++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+) create mode 100644 nvm/fake_pmemobj.py create mode 100644 tests/accounts.doctest diff --git a/nvm/fake_pmemobj.py b/nvm/fake_pmemobj.py new file mode 100644 index 0000000..8abc725 --- /dev/null +++ b/nvm/fake_pmemobj.py @@ -0,0 +1,51 @@ +""" +A fake PersistentObjectPool. It does the persistence magic by using json +on the root object to store it in a file, and the transactions are fake. But +it allows for testing the "this persists" logic of a program without dealing +with any bugs that may exist in the real PersistentObjectPool. + +""" + +import os +import json + +from contextlib import contextmanager + +from nvm.pmemobj import PersistentList, PersistentDict + +class PersistentObjectPool: + def __init__(self, filename, flag='w', *args, **kw): + self.filename = filename + exists = os.path.exists(filename) + if flag == 'w' or (flag == 'c' and exists): + with open(filename) as f: + self.root = json.load(f)[0] + elif flag == 'x' or (flag == 'c' and not exists): + with open(filename, 'w') as f: + self.root = None + json.dump([None], f) + elif flag == 'r': + raise ValueError("Read-only mode is not supported") + else: + raise ValueError("Invalid flag value {}".format(flag)) + + def new(self, typ, *args, **kw): + if typ == PersistentList: + return list(*args, **kw) + if typ == PersistentDict: + return dict(*args, **kw) + + @contextmanager + def transaction(self): + yield None + + def close(self): + with open(self.filename+'.tmp', 'w') as f: + json.dump([self.root], f) + os.rename(self.filename+'.tmp', self.filename) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() diff --git a/nvm/pmemobj/__init__.py b/nvm/pmemobj/__init__.py index b889d0d..7bdd113 100644 --- a/nvm/pmemobj/__init__.py +++ b/nvm/pmemobj/__init__.py @@ -1,2 +1,4 @@ from .pool import open, create, MIN_POOL_SIZE, PersistentObjectPool from .list import PersistentList +class PersistentDict(object): + pass diff --git a/tests/accounts.doctest b/tests/accounts.doctest new file mode 100644 index 0000000..65e1707 --- /dev/null +++ b/tests/accounts.doctest @@ -0,0 +1,89 @@ +This file tests and demonstrates the 'accounts' example program. We're using +doctest here for two reasons: (1) accounts is a command line program that +produces output on the terminal, so doctest's "show the command and check the +output" approach is a natural fit, and (2) accounts is demonstrating the +*persistence* of data between command runs, so a long string of examples that +progressively modify the data is a better fit than the unit test approach. To +do the same thing in unit test would be harder to read, more verbose (because +each test would need multiple commands to test the persistence), and not really +*unit* tests in the sense the unit test framework is designed for writing. + +To run these tests you should have your current directory be the directory +containing the 'nvm' package, and run the following command: + + python -m doctest tests/accounts.doctest + +The doctest will only run if the 'accounts' demo is in 'examples/accounts' +relative to that same current directory. + +The persistent data used by the tests is backed by a file. For current testing +purposes we're using an ordinary file system file in the temporary directory +using libpmem's pmem emulation support. For a test of a demo program more +than that is not really needed. + +If a test run aborts, that file will be left behind, so the first thing +we need to do is remove the temp file: + + >>> import tempfile + >>> import os + >>> PMEM_FILE = os.path.join(tempfile.gettempdir(), 'accounts.pmem') + >>> if os.path.exists(PMEM_FILE): + ... os.remove(PMEM_FILE) + +The tests below use a common format: we call the accounts demo program using +a set of arguments, and see the output that produces. Since we're running this +from inside doctest, which executes python code, we need a helper function to +run the command: + + >>> import sys + >>> from subprocess import Popen, PIPE + >>> def run(cmd): + ... cmd, _, args = cmd.partition(' ') + ... cmd = (sys.executable + ' ' + os.path.join('examples', cmd) + ... + ' -f' + PMEM_FILE + ' ' + args) + ... p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, + ... universal_newlines=True) + ... rc = p.wait() + ... for line in p.stdout: + ... print(line.rstrip('\n')) + ... errs = p.stderr.read().splitlines() + ... if errs: + ... print('--------- error output --------') + ... for line in p.stderr: + ... print(line) + +This function takes care of the boilerplate of turning the command name +'accounts' into a call to it using the same interpreter used to run the +doctests, in its expected location in the examples directory, and passing to it +the -f option to specify the location of our test database instead of using its +default. It also prints a line dividing normal output from error output if +there is any error output, allowing us to check that error output goes to the +correct standard stream. + +Initially, the file holding the data does not exist: + + >>> os.path.exists(PMEM_FILE) + False + +The default action of the 'accounts' command is to show a summary of the +current accounts. Initially that will just be a message that there are +no accounts: + + >>> run('accounts') + No accounts currently exist. Add an account using 'account create'. + +But now the (empty) persistent memory file will exist: + + >>> os.path.exists(PMEM_FILE) + True + +If we create an account, by default it starts with a zero balance: + + >>> run('accounts create checking') + Created account 'checking'. + >>> run('accounts') # doctest: +ELLIPSIS + Account ... Balance + ------- ... ------- + checking ... 0.00 + ... _______ + Net Worth: ... 0.00 From efa34714a7e45e7a57fef3e060aea808401e2fe8 Mon Sep 17 00:00:00 2001 From: Isabel Tripp Date: Tue, 9 Aug 2016 11:12:24 -0400 Subject: [PATCH 02/10] Doctest file renamed, started 'create' subcommand --- examples/accounts.py | 39 ++++++++++++++++++++++++ nvm/fake_pmemobj.py | 7 ++++- tests/{accounts.doctest => accounts.txt} | 19 +++++++++--- 3 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 examples/accounts.py rename tests/{accounts.doctest => accounts.txt} (90%) diff --git a/examples/accounts.py b/examples/accounts.py new file mode 100644 index 0000000..999535e --- /dev/null +++ b/examples/accounts.py @@ -0,0 +1,39 @@ +#demo account management program +import argparse +from nvm.fake_pmemobj import PersistentObjectPool, PersistentDict, PersistentList +#initial account creation module + + +#top-level parser +parser = argparse.ArgumentParser() +parser.add_argument('--foo', action = 'store_true', help = 'foo help') +subparsers = parser.add_subparsers(help = 'sub-command help', dest = 'subcommand') +parser.add_argument('-f', '--filename', default='accounts.pmem', help="filename to store data in") + +#create the parser for the 'account create' command +parser_create = subparsers.add_parser('create', description= 'account creation') +#(specify type of account) +parser_create.add_argument('account', help = 'create specific type of bank account') +#(establish initial balance in accnt) +parser_create.add_argument('amount', help = "establish initial balance", type=float, default = 0, nargs = '?') +args_create = parser.parse_args() + +#temporary +#print(args_create) + +#***use name of acct to create dictionary + +with PersistentObjectPool(args_create.filename, flag='c') as pop: + if pop.root is None: + pop.root = pop.new(PersistentDict) + accounts = pop.root + if args_create.subcommand == 'create': + accounts[args_create.account] = args_create.amount + print(accounts) + #if subcommand = 'transfer": + else: + #check for accounts + #if .. + print() + print("No accounts currently exist. Add an account using 'account create'.") + diff --git a/nvm/fake_pmemobj.py b/nvm/fake_pmemobj.py index 8abc725..51c6b45 100644 --- a/nvm/fake_pmemobj.py +++ b/nvm/fake_pmemobj.py @@ -11,7 +11,12 @@ from contextlib import contextmanager -from nvm.pmemobj import PersistentList, PersistentDict +#from nvm.pmemobj import PersistentList, PersistentDict + +class PersistentList(object): + pass +class PersistentDict(object): + pass class PersistentObjectPool: def __init__(self, filename, flag='w', *args, **kw): diff --git a/tests/accounts.doctest b/tests/accounts.txt similarity index 90% rename from tests/accounts.doctest rename to tests/accounts.txt index 65e1707..e57f6d5 100644 --- a/tests/accounts.doctest +++ b/tests/accounts.txt @@ -11,7 +11,7 @@ each test would need multiple commands to test the persistence), and not really To run these tests you should have your current directory be the directory containing the 'nvm' package, and run the following command: - python -m doctest tests/accounts.doctest + python3 -m doctest tests/accounts.txt The doctest will only run if the 'accounts' demo is in 'examples/accounts' relative to that same current directory. @@ -38,18 +38,20 @@ run the command: >>> import sys >>> from subprocess import Popen, PIPE >>> def run(cmd): + ... env = os.environ + ... env['PYTHONPATH'] = '.' ... cmd, _, args = cmd.partition(' ') - ... cmd = (sys.executable + ' ' + os.path.join('examples', cmd) - ... + ' -f' + PMEM_FILE + ' ' + args) + ... cmd = (sys.executable + ' ' + os.path.join('examples', cmd + '.py') + ... + ' -f ' + PMEM_FILE + ' ' + args) ... p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, - ... universal_newlines=True) + ... universal_newlines=True, env=env) ... rc = p.wait() ... for line in p.stdout: ... print(line.rstrip('\n')) ... errs = p.stderr.read().splitlines() ... if errs: ... print('--------- error output --------') - ... for line in p.stderr: + ... for line in errs: ... print(line) This function takes care of the boilerplate of turning the command name @@ -87,3 +89,10 @@ If we create an account, by default it starts with a zero balance: checking ... 0.00 ... _______ Net Worth: ... 0.00 + + + +Cleanup: + >>> if os.path.exists(PMEM_FILE): + ... os.remove(PMEM_FILE) + From 089732e0479a3b93700293ba3db3e1bff7c33931 Mon Sep 17 00:00:00 2001 From: "R. David Murray" Date: Tue, 9 Aug 2016 13:05:25 -0400 Subject: [PATCH 03/10] Use pickle instead of json so we can work with Decimal objects. --- nvm/fake_pmemobj.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/nvm/fake_pmemobj.py b/nvm/fake_pmemobj.py index 51c6b45..45a3b7e 100644 --- a/nvm/fake_pmemobj.py +++ b/nvm/fake_pmemobj.py @@ -7,7 +7,7 @@ """ import os -import json +import pickle from contextlib import contextmanager @@ -23,12 +23,12 @@ def __init__(self, filename, flag='w', *args, **kw): self.filename = filename exists = os.path.exists(filename) if flag == 'w' or (flag == 'c' and exists): - with open(filename) as f: - self.root = json.load(f)[0] + with open(filename, 'rb') as f: + self.root = pickle.load(f)[0] elif flag == 'x' or (flag == 'c' and not exists): - with open(filename, 'w') as f: + with open(filename, 'wb') as f: self.root = None - json.dump([None], f) + pickle.dump([None], f) elif flag == 'r': raise ValueError("Read-only mode is not supported") else: @@ -45,8 +45,8 @@ def transaction(self): yield None def close(self): - with open(self.filename+'.tmp', 'w') as f: - json.dump([self.root], f) + with open(self.filename+'.tmp', 'wb') as f: + pickle.dump([self.root], f) os.rename(self.filename+'.tmp', self.filename) def __enter__(self): From 148b6a337b2e01981f94c838cad06decc1b00c45 Mon Sep 17 00:00:00 2001 From: Isabel Tripp Date: Tue, 9 Aug 2016 14:03:11 -0400 Subject: [PATCH 04/10] accounts complies to first doctest/displays accnts, doctest incl. spacing --- examples/accounts.py | 32 ++++++++++++++++++++++---------- tests/accounts.txt | 12 ++++++------ 2 files changed, 28 insertions(+), 16 deletions(-) diff --git a/examples/accounts.py b/examples/accounts.py index 999535e..c9b3dc4 100644 --- a/examples/accounts.py +++ b/examples/accounts.py @@ -1,8 +1,11 @@ #demo account management program import argparse from nvm.fake_pmemobj import PersistentObjectPool, PersistentDict, PersistentList +import decimal #initial account creation module +#decimal precision +decimal.getcontext().prec = 2 #top-level parser parser = argparse.ArgumentParser() @@ -10,30 +13,39 @@ subparsers = parser.add_subparsers(help = 'sub-command help', dest = 'subcommand') parser.add_argument('-f', '--filename', default='accounts.pmem', help="filename to store data in") -#create the parser for the 'account create' command +#create the parser for the 'accounts create' command parser_create = subparsers.add_parser('create', description= 'account creation') #(specify type of account) parser_create.add_argument('account', help = 'create specific type of bank account') #(establish initial balance in accnt) -parser_create.add_argument('amount', help = "establish initial balance", type=float, default = 0, nargs = '?') +parser_create.add_argument('amount', help = "establish initial balance", type=decimal.Decimal, default = decimal.Decimal('0.00'), nargs = '?') args_create = parser.parse_args() #temporary #print(args_create) -#***use name of acct to create dictionary - with PersistentObjectPool(args_create.filename, flag='c') as pop: if pop.root is None: pop.root = pop.new(PersistentDict) accounts = pop.root if args_create.subcommand == 'create': accounts[args_create.account] = args_create.amount - print(accounts) - #if subcommand = 'transfer": + print("Created account '" + args_create.account + "'.") + #if subcommand == 'transfer": else: #check for accounts - #if .. - print() - print("No accounts currently exist. Add an account using 'account create'.") - + if accounts: + m1= ("Account Balance\n" + "------- -------") + print(m1) + accntTotal = 0 + for specificAccount in accounts: + accntInfo = "{} {}".format(specificAccount, accounts[specificAccount]) + print(accntInfo) + accntTotal = accntTotal + accounts[specificAccount] + m2=(" _______\n" + " Net Worth: {}").format(accntTotal) + print(m2) + #print message if no accounts exist + else: + print("No accounts currently exist. Add an account using 'account create'.") diff --git a/tests/accounts.txt b/tests/accounts.txt index e57f6d5..ecde6c9 100644 --- a/tests/accounts.txt +++ b/tests/accounts.txt @@ -83,12 +83,12 @@ If we create an account, by default it starts with a zero balance: >>> run('accounts create checking') Created account 'checking'. - >>> run('accounts') # doctest: +ELLIPSIS - Account ... Balance - ------- ... ------- - checking ... 0.00 - ... _______ - Net Worth: ... 0.00 + >>> run('accounts') + Account Balance + ------- ------- + checking 0.00 + _______ + Net Worth: 0.00 From 36bbd07319a706db9320b8236f030fc3904d3872 Mon Sep 17 00:00:00 2001 From: "R. David Murray" Date: Tue, 9 Aug 2016 15:51:51 -0400 Subject: [PATCH 05/10] Add some more doctests (list, transfer, check). --- tests/accounts.txt | 62 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/tests/accounts.txt b/tests/accounts.txt index ecde6c9..7fcacbc 100644 --- a/tests/accounts.txt +++ b/tests/accounts.txt @@ -90,6 +90,68 @@ If we create an account, by default it starts with a zero balance: _______ Net Worth: 0.00 +When we create an account, we can specify an initial balance: + + >>> run('accounts create savings 119.00') + Created account 'savings'. + >>> run('accounts') + Account Balance + ------- ------- + checking 0.00 + savings 119.00 + _______ + Net Worth: 119.00 + +The only transaction in a newly created account is the initial balance: + + >>> run('accounts list checking') + Date Amount Balance Memo + ---------- ------- ------- ---- + 2015-08-09 0.00 0.00 Initial account balance + + >>> run('accounts list savings') + Date Amount Balance Memo + ---------- ------- ------- ---- + 2015-08-09 119.00 119.00 Initial account balance + +If we transfer money between accounts, that will add a transaction to +each account: + + >>> run('accounts transfer 50.50 savings checking For check 115') + Transferred 50.50 from savings (new balance 68.50) to savings (new balance 50.50) + + >>> run('accounts list checking') + Date Amount Balance Memo + ---------- ------- ------- ---- + 2015-08-09 50.50 50.50 For check 115 + 2015-08-09 0.00 0.00 Initial account balance + + >>> run('accounts list savings') + Date Amount Balance Memo + ---------- ------- ------- ---- + 2015-08-09 -50.50 68.50 For check 115 + 2015-08-09 119.00 119.00 Initial account balance + +When check 115 clears we can do: + + >>> run('accounts check 115 checking 50.50 Vet bill') + Check 115 for 50.50 debited from checking (new balance 0.00) + + >>> run('accounts list checking') + Date Amount Balance Memo + ---------- ------- ------- ---- + 2015-08-09 -50.50 0.00 Check 115: Vet bill + 2015-08-09 50.50 50.50 For check 115 + 2015-08-09 0.00 0.00 Initial account balance + +(Aside: it would be nice to have the account name in the previous command +have a default value that could be established by an 'accounts set +default-checking checking' command so that we could just type 'accounts +check 115 50.50 Vet bill'), but that is a bit complicated to code with +argparse so we won't bother with doing that for this demo program.) + + + Cleanup: From 4663e55253782e6fd941ea7b138188213aee2396 Mon Sep 17 00:00:00 2001 From: Isabel Tripp Date: Wed, 31 Aug 2016 10:15:43 -0400 Subject: [PATCH 06/10] progress on list, transfer, check subcommands and small edit to doctest --- examples/accounts.py | 90 +++++++++++++++++++++++++++++++++----------- tests/accounts.txt | 2 +- 2 files changed, 69 insertions(+), 23 deletions(-) diff --git a/examples/accounts.py b/examples/accounts.py index c9b3dc4..19fcda4 100644 --- a/examples/accounts.py +++ b/examples/accounts.py @@ -1,11 +1,13 @@ #demo account management program import argparse from nvm.fake_pmemobj import PersistentObjectPool, PersistentDict, PersistentList -import decimal +import decimal +import time + #initial account creation module #decimal precision -decimal.getcontext().prec = 2 +decimal.getcontext().prec = 12 #top-level parser parser = argparse.ArgumentParser() @@ -15,37 +17,81 @@ #create the parser for the 'accounts create' command parser_create = subparsers.add_parser('create', description= 'account creation') -#(specify type of account) parser_create.add_argument('account', help = 'create specific type of bank account') -#(establish initial balance in accnt) -parser_create.add_argument('amount', help = "establish initial balance", type=decimal.Decimal, default = decimal.Decimal('0.00'), nargs = '?') -args_create = parser.parse_args() +parser_create.add_argument('amount', help = 'establish initial balance', type=decimal.Decimal, default = decimal.Decimal('0.00'), nargs = '?') +#list account info., incl. past transactions +parser_create = subparsers.add_parser('list', description = 'individual account information display') +parser_create.add_argument('account', help = 'specify account') +#transfer money between accounts +parser_create = subparsers.add_parser('transfer', description = 'transer funds between accounts') +parser_create.add_argument('transferAmount', help ='quantity of money to transfer', type=decimal.Decimal, default = decimal.Decimal('0.00')) +parser_create.add_argument('pastLocation', help ='account from which funds are withdrawn') +parser_create.add_argument('futureLocation', help = 'account to which funds are deposited') +parser_create.add_argument('memo', help = 'explanation of transfer', nargs = argparse.REMAINDER) +#withdraw money from account +parser_create = subparsers.add_parser('check', description = 'withdraw money via check') +parser_create.add_argument('checkNumber', help = 'number of check') +parser_create.add_argument('account', help = 'account from which to withdraw money') +parser_create.add_argument('amount', help = 'amount withdrawn', type = decimal.Decimal, default = decimal.Decimal('0.00')) +parser_create.add_argument('memo', help = 'check memo', nargs = argparse.REMAINDER) +args = parser.parse_args() -#temporary -#print(args_create) -with PersistentObjectPool(args_create.filename, flag='c') as pop: +with PersistentObjectPool(args.filename, flag='c') as pop: if pop.root is None: pop.root = pop.new(PersistentDict) accounts = pop.root - if args_create.subcommand == 'create': - accounts[args_create.account] = args_create.amount - print("Created account '" + args_create.account + "'.") - #if subcommand == 'transfer": + if args.subcommand == 'create': + accounts[args.account] = [['2015-08-09', decimal.Decimal(args.amount), 'Initial account balance']] + print("Created account '" + args.account + "'.") + elif args.subcommand == 'list': + L1 = ("Date Amount Balance Memo\n" + "---------- ------- ------- ----") + print(L1) + accntBalance = decimal.Decimal(0) + for x in accounts[args.account]: + accntBalance = accntBalance + x[1] + for transaction in accounts[args.account]: + L2= "{:<10}{:>9}{:>9} {}".format(x[0], transaction[1], accntBalance, x[2]) + print(L2) + elif args.subcommand == 'transfer': + s= " " + memo = s.join(args.memo) #'Initial account balance' doesn't satisfy s.join(args.memo) + accounts[args.pastLocation].append(['2015-08-09', -decimal.Decimal(args.transferAmount), memo]) #problem for create transaction + accounts[args.futureLocation].append(['2015-08-09', decimal.Decimal(args.transferAmount), memo]) + pBalance = decimal.Decimal(0) + for transaction in accounts[args.pastLocation]: + pBalance = pBalance + transaction[1] + fBalance = decimal.Decimal(0) + for transaction in accounts[args.futureLocation]: + fBalance = fBalance + transaction[1] + print("Transferred {} from {} (new balance {}) to {} (new balance {})".format(args.transferAmount, args.pastLocation, str(pBalance), args.futureLocation, str(fBalance))) + elif args.subcommand == 'check': + c = " " + memo = c.join(args.memo) + accounts[args.account].append(['2015-08-09', -decimal.Decimal(args.amount), (memo + "(check " + checkNumber + ")")]) + newBalance = decimal.Decimal(0) + for transaction in accounts[args.account]: + newBalance = newBalance + transaction[1] + print("Check {} for {} debited from {} (new balance {})".format(checkNumber, amount, account, newBalance)) else: #check for accounts if accounts: - m1= ("Account Balance\n" - "------- -------") + #25 character spaces + m1= ("Account Balance\n" + "------- -------") print(m1) - accntTotal = 0 - for specificAccount in accounts: - accntInfo = "{} {}".format(specificAccount, accounts[specificAccount]) + accntTotal = decimal.Decimal(0) + for specificAccount in sorted(accounts): + balance= decimal.Decimal(0.0) + for transaction in accounts[specificAccount]: + balance = balance + transaction[1] + accntInfo = "{:<20}{:>6}".format(specificAccount, balance) print(accntInfo) - accntTotal = accntTotal + accounts[specificAccount] - m2=(" _______\n" - " Net Worth: {}").format(accntTotal) - print(m2) + accntTotal = accntTotal + balance + m2=(" _______\n" + " Net Worth: {:>6.2f}").format(accntTotal) + print (m2) #print message if no accounts exist else: print("No accounts currently exist. Add an account using 'account create'.") diff --git a/tests/accounts.txt b/tests/accounts.txt index 7fcacbc..ef5c24f 100644 --- a/tests/accounts.txt +++ b/tests/accounts.txt @@ -118,7 +118,7 @@ If we transfer money between accounts, that will add a transaction to each account: >>> run('accounts transfer 50.50 savings checking For check 115') - Transferred 50.50 from savings (new balance 68.50) to savings (new balance 50.50) + Transferred 50.50 from savings (new balance 68.50) to checking (new balance 50.50) >>> run('accounts list checking') Date Amount Balance Memo From 694563a724b964f381500f26ce7c553e77acfe55 Mon Sep 17 00:00:00 2001 From: Isabel Tripp Date: Wed, 31 Aug 2016 12:34:06 -0400 Subject: [PATCH 07/10] doctest 2 satisfied -- check function complete --- examples/accounts.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/accounts.py b/examples/accounts.py index 19fcda4..52c27d2 100644 --- a/examples/accounts.py +++ b/examples/accounts.py @@ -51,13 +51,14 @@ accntBalance = decimal.Decimal(0) for x in accounts[args.account]: accntBalance = accntBalance + x[1] - for transaction in accounts[args.account]: - L2= "{:<10}{:>9}{:>9} {}".format(x[0], transaction[1], accntBalance, x[2]) + for transaction in reversed(accounts[args.account]): + L2= "{:<10}{:>9}{:>9} {}".format(transaction[0], transaction[1], accntBalance, transaction[2]) print(L2) + accntBalance = accntBalance - transaction[1] elif args.subcommand == 'transfer': s= " " - memo = s.join(args.memo) #'Initial account balance' doesn't satisfy s.join(args.memo) - accounts[args.pastLocation].append(['2015-08-09', -decimal.Decimal(args.transferAmount), memo]) #problem for create transaction + memo = s.join(args.memo) + accounts[args.pastLocation].append(['2015-08-09', -decimal.Decimal(args.transferAmount), memo]) accounts[args.futureLocation].append(['2015-08-09', decimal.Decimal(args.transferAmount), memo]) pBalance = decimal.Decimal(0) for transaction in accounts[args.pastLocation]: @@ -69,11 +70,11 @@ elif args.subcommand == 'check': c = " " memo = c.join(args.memo) - accounts[args.account].append(['2015-08-09', -decimal.Decimal(args.amount), (memo + "(check " + checkNumber + ")")]) + accounts[args.account].append(['2015-08-09', -decimal.Decimal(args.amount), "Check {}: {}".format(args.checkNumber, memo)]) newBalance = decimal.Decimal(0) for transaction in accounts[args.account]: newBalance = newBalance + transaction[1] - print("Check {} for {} debited from {} (new balance {})".format(checkNumber, amount, account, newBalance)) + print("Check {} for {} debited from {} (new balance {})".format(args.checkNumber, args.amount, args.account, newBalance)) else: #check for accounts if accounts: From fdfb4ce8be5a9d5f78db380da5ed589b77877783 Mon Sep 17 00:00:00 2001 From: Isabel Tripp Date: Wed, 31 Aug 2016 13:16:17 -0400 Subject: [PATCH 08/10] withdraw and deposit function --- examples/accounts.py | 34 ++++++++++++++++++++++++++++++++-- tests/accounts.txt | 25 +++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/examples/accounts.py b/examples/accounts.py index 52c27d2..c2d7e00 100644 --- a/examples/accounts.py +++ b/examples/accounts.py @@ -28,12 +28,22 @@ parser_create.add_argument('pastLocation', help ='account from which funds are withdrawn') parser_create.add_argument('futureLocation', help = 'account to which funds are deposited') parser_create.add_argument('memo', help = 'explanation of transfer', nargs = argparse.REMAINDER) -#withdraw money from account +#withdraw money from account (check) parser_create = subparsers.add_parser('check', description = 'withdraw money via check') parser_create.add_argument('checkNumber', help = 'number of check') parser_create.add_argument('account', help = 'account from which to withdraw money') parser_create.add_argument('amount', help = 'amount withdrawn', type = decimal.Decimal, default = decimal.Decimal('0.00')) parser_create.add_argument('memo', help = 'check memo', nargs = argparse.REMAINDER) +#deposit money into account +parser_create = subparsers.add_parser('deposit', description = 'deposit money into account') +parser_create.add_argument('account', help = 'account in which to deposit money') +parser_create.add_argument('amount', help = 'total deposit', type = decimal.Decimal, default = decimal.Decimal('0.00')) +parser_create.add_argument('memo', help = 'source of deposit', nargs = argparse.REMAINDER) +#withdraw money from account (cash) +parser_create = subparsers.add_parser('withdraw', description = 'withdraw amount of cash from account') +parser_create.add_argument('account', help = 'account from which to withdraw money') +parser_create.add_argument('amount', help = 'total withdrawl', type = decimal.Decimal, default = decimal.Decimal('0.00')) +parser_create.add_argument('memo', help = 'reason for withdrawl', nargs = argparse.REMAINDER) args = parser.parse_args() @@ -57,7 +67,8 @@ accntBalance = accntBalance - transaction[1] elif args.subcommand == 'transfer': s= " " - memo = s.join(args.memo) + memo = s.join(args.memo) + memo = memo[0].upper() + memo[1:] accounts[args.pastLocation].append(['2015-08-09', -decimal.Decimal(args.transferAmount), memo]) accounts[args.futureLocation].append(['2015-08-09', decimal.Decimal(args.transferAmount), memo]) pBalance = decimal.Decimal(0) @@ -70,11 +81,30 @@ elif args.subcommand == 'check': c = " " memo = c.join(args.memo) + memo = memo[0].upper() + memo[1:] accounts[args.account].append(['2015-08-09', -decimal.Decimal(args.amount), "Check {}: {}".format(args.checkNumber, memo)]) newBalance = decimal.Decimal(0) for transaction in accounts[args.account]: newBalance = newBalance + transaction[1] print("Check {} for {} debited from {} (new balance {})".format(args.checkNumber, args.amount, args.account, newBalance)) + elif args.subcommand == 'deposit': + c = " " + memo = c.join(args.memo) + memo = memo[0].upper() + memo[1:] + accounts[args.account].append(['2015-08-09', decimal.Decimal(args.amount), memo]) + newBalance = decimal.Decimal(0) + for transaction in accounts[args.account]: + newBalance = newBalance + transaction[1] + print("{} added to {} (new balance {})".format(args.amount, args.account, newBalance)) + elif args.subcommand == 'withdraw': + c = " " + memo = c.join(args.memo) + memo = memo[0].upper() + memo[1:] + accounts[args.account].append(['2015-08-09', -decimal.Decimal(args.amount), memo]) + newBalance = decimal.Decimal(0) + for transaction in accounts[args.account]: + newBalance = newBalance + transaction[1] + print("{} withdrawn from {} (new balance {})".format(args.amount, args.account, newBalance)) else: #check for accounts if accounts: diff --git a/tests/accounts.txt b/tests/accounts.txt index ef5c24f..fc00ddb 100644 --- a/tests/accounts.txt +++ b/tests/accounts.txt @@ -144,6 +144,31 @@ When check 115 clears we can do: 2015-08-09 50.50 50.50 For check 115 2015-08-09 0.00 0.00 Initial account balance +Next we can deposit funds into one of the accounts: + + >>> run('accounts deposit savings 30.00 Monday paycheck') + 30.00 added to savings (new balance 98.50) + + >>> run('accounts list savings') + Date Amount Balance Memo + ---------- ------- ------- ---- + 2015-08-09 30.00 98.50 Monday paycheck + 2015-08-09 -50.50 68.50 For check 115 + 2015-08-09 119.00 119.00 Initial account balance + +Conversely, we can withdraw funds: + + >>> run('accounts withdraw savings 15.00 lunch money') + 15.00 withdrawn from savings (new balance 83.50) + + >>> run('accounts list savings') + Date Amount Balance Memo + ---------- ------- ------- ---- + 2015-08-09 -15.00 83.50 Lunch money + 2015-08-09 30.00 98.50 Monday paycheck + 2015-08-09 -50.50 68.50 For check 115 + 2015-08-09 119.00 119.00 Initial account balance + (Aside: it would be nice to have the account name in the previous command have a default value that could be established by an 'accounts set default-checking checking' command so that we could just type 'accounts From 06b29bf93808fa99f376701a3029de606e0b93ef Mon Sep 17 00:00:00 2001 From: Isabel Tripp Date: Wed, 31 Aug 2016 14:03:31 -0400 Subject: [PATCH 09/10] Date & time no longer replaced by fake strings in program & doctest --- examples/accounts.py | 22 +++++++++++++--------- tests/accounts.txt | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/examples/accounts.py b/examples/accounts.py index c2d7e00..f2b6868 100644 --- a/examples/accounts.py +++ b/examples/accounts.py @@ -2,7 +2,7 @@ import argparse from nvm.fake_pmemobj import PersistentObjectPool, PersistentDict, PersistentList import decimal -import time +import datetime #initial account creation module @@ -14,6 +14,7 @@ parser.add_argument('--foo', action = 'store_true', help = 'foo help') subparsers = parser.add_subparsers(help = 'sub-command help', dest = 'subcommand') parser.add_argument('-f', '--filename', default='accounts.pmem', help="filename to store data in") +parser.add_argument('-d', '--date', default = datetime.date.today(), help = 'specify date for this transaction') #create the parser for the 'accounts create' command parser_create = subparsers.add_parser('create', description= 'account creation') @@ -46,13 +47,16 @@ parser_create.add_argument('memo', help = 'reason for withdrawl', nargs = argparse.REMAINDER) args = parser.parse_args() - +#check to see if args.date is a string -- convert to datetime object if so +if isinstance(args.date, str) == True: + args.date = datetime.datetime.strptime(args.date, '%Y-%m-%d') + with PersistentObjectPool(args.filename, flag='c') as pop: if pop.root is None: pop.root = pop.new(PersistentDict) accounts = pop.root if args.subcommand == 'create': - accounts[args.account] = [['2015-08-09', decimal.Decimal(args.amount), 'Initial account balance']] + accounts[args.account] = [[args.date, decimal.Decimal(args.amount), 'Initial account balance']] print("Created account '" + args.account + "'.") elif args.subcommand == 'list': L1 = ("Date Amount Balance Memo\n" @@ -62,15 +66,15 @@ for x in accounts[args.account]: accntBalance = accntBalance + x[1] for transaction in reversed(accounts[args.account]): - L2= "{:<10}{:>9}{:>9} {}".format(transaction[0], transaction[1], accntBalance, transaction[2]) + L2= "{:%Y-%m-%d}{:>9}{:>9} {}".format(transaction[0], transaction[1], accntBalance, transaction[2]) print(L2) accntBalance = accntBalance - transaction[1] elif args.subcommand == 'transfer': s= " " memo = s.join(args.memo) memo = memo[0].upper() + memo[1:] - accounts[args.pastLocation].append(['2015-08-09', -decimal.Decimal(args.transferAmount), memo]) - accounts[args.futureLocation].append(['2015-08-09', decimal.Decimal(args.transferAmount), memo]) + accounts[args.pastLocation].append([args.date, -decimal.Decimal(args.transferAmount), memo]) + accounts[args.futureLocation].append([args.date, decimal.Decimal(args.transferAmount), memo]) pBalance = decimal.Decimal(0) for transaction in accounts[args.pastLocation]: pBalance = pBalance + transaction[1] @@ -82,7 +86,7 @@ c = " " memo = c.join(args.memo) memo = memo[0].upper() + memo[1:] - accounts[args.account].append(['2015-08-09', -decimal.Decimal(args.amount), "Check {}: {}".format(args.checkNumber, memo)]) + accounts[args.account].append([args.date, -decimal.Decimal(args.amount), "Check {}: {}".format(args.checkNumber, memo)]) newBalance = decimal.Decimal(0) for transaction in accounts[args.account]: newBalance = newBalance + transaction[1] @@ -91,7 +95,7 @@ c = " " memo = c.join(args.memo) memo = memo[0].upper() + memo[1:] - accounts[args.account].append(['2015-08-09', decimal.Decimal(args.amount), memo]) + accounts[args.account].append([args.date, decimal.Decimal(args.amount), memo]) newBalance = decimal.Decimal(0) for transaction in accounts[args.account]: newBalance = newBalance + transaction[1] @@ -100,7 +104,7 @@ c = " " memo = c.join(args.memo) memo = memo[0].upper() + memo[1:] - accounts[args.account].append(['2015-08-09', -decimal.Decimal(args.amount), memo]) + accounts[args.account].append([args.date, -decimal.Decimal(args.amount), memo]) newBalance = decimal.Decimal(0) for transaction in accounts[args.account]: newBalance = newBalance + transaction[1] diff --git a/tests/accounts.txt b/tests/accounts.txt index fc00ddb..37b27a2 100644 --- a/tests/accounts.txt +++ b/tests/accounts.txt @@ -42,7 +42,7 @@ run the command: ... env['PYTHONPATH'] = '.' ... cmd, _, args = cmd.partition(' ') ... cmd = (sys.executable + ' ' + os.path.join('examples', cmd + '.py') - ... + ' -f ' + PMEM_FILE + ' ' + args) + ... + ' -f ' + PMEM_FILE + ' ' + '-d 2015-08-09 ' + args) ... p = Popen(cmd, shell=True, stdout=PIPE, stderr=PIPE, ... universal_newlines=True, env=env) ... rc = p.wait() From 8c206376bb33886bbed1bf1fb9ad85b0c249d9b3 Mon Sep 17 00:00:00 2001 From: Isabel Tripp Date: Wed, 31 Aug 2016 14:19:02 -0400 Subject: [PATCH 10/10] dec precision deleted -- default of 27 is sufficient --- examples/accounts.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/examples/accounts.py b/examples/accounts.py index f2b6868..fc04499 100644 --- a/examples/accounts.py +++ b/examples/accounts.py @@ -6,9 +6,6 @@ #initial account creation module -#decimal precision -decimal.getcontext().prec = 12 - #top-level parser parser = argparse.ArgumentParser() parser.add_argument('--foo', action = 'store_true', help = 'foo help')