-
Notifications
You must be signed in to change notification settings - Fork 1
Accounts example #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
0475599
efa3471
089732e
148b6a3
36bbd07
4663e55
694563a
fdfb4ce
06b29bf
d0328f9
8c20637
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| #demo account management program | ||
| import argparse | ||
| from nvm.fake_pmemobj import PersistentObjectPool, PersistentDict, PersistentList | ||
| import decimal | ||
| import datetime | ||
|
|
||
| #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") | ||
| 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') | ||
| parser_create.add_argument('account', help = 'create specific type of bank account') | ||
| 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 (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() | ||
|
|
||
| #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] = [[args.date, 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] | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is working fine, as we discussed. There's a simpler way you could write this, though, using the 'sum' function and what's called a 'list comprehension'. There's no need to change this, but if you feel like looking those two up and figuring out how to do it you'd be learning something new. |
||
| for transaction in reversed(accounts[args.account]): | ||
| 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) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A string literal in Python (which is what " " is) gets compiled into a 'string object'. Your assignment statement then puts the pointer to this string object that the literal gets turned into into the name space under the name s. When you write "s.join", you are calling the 'join' method of the string object pointed to by s. But '" "' is turned into a pointer to a string object by the compiler, so you can equally well call 'join' directly on it: |
||
| memo = memo[0].upper() + memo[1:] | ||
| accounts[args.pastLocation].append([args.date, -decimal.Decimal(args.transferAmount), memo]) | ||
| accounts[args.futureLocation].append([args.date, decimal.Decimal(args.transferAmount), memo]) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. args.transferAmount is declared in the parser to be type decimal.Decimal. What that means is that the parser will call decimal.Decimal on whatever the user types. So calling decimal.Decimal on it here is redundant, it should already be a Decimal. |
||
| 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))) | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In the summary we had to use the 6.2f format to make sure we got exactly two decimal places. I'm a little surprised that none of the tests failed, given that you are only using str here. It would be nice to come up with a test that did fail (had too few or too many decimal places...or better yet two tests, one for each of those cases). Since we don't have any commands that do division or multiplication, though, making a test for too many digits won't be possible, I think. And maybe the other case just doesn't fail either, so don't worry about the tests much. |
||
| elif args.subcommand == 'check': | ||
| c = " " | ||
| memo = c.join(args.memo) | ||
| memo = memo[0].upper() + memo[1:] | ||
|
Owner
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You do this operation in several of the if cases. That makes it a good candidate to turn into a function. You'd use 'def format_memo(memo_parts):' up above the if and put the lines in the body of the function, and then in each case you can do 'memo = format_memo(args.memo)' instead of repeating your three (or two, see above) lines. |
||
| 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] | ||
| 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([args.date, 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([args.date, -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: | ||
| #25 character spaces | ||
| m1= ("Account Balance\n" | ||
| "------- -------") | ||
| print(m1) | ||
| 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 + 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'.") | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| """ | ||
| 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 pickle | ||
|
|
||
| from contextlib import contextmanager | ||
|
|
||
| #from nvm.pmemobj import PersistentList, PersistentDict | ||
|
|
||
| class PersistentList(object): | ||
| pass | ||
| class PersistentDict(object): | ||
| pass | ||
|
|
||
| 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, 'rb') as f: | ||
| self.root = pickle.load(f)[0] | ||
| elif flag == 'x' or (flag == 'c' and not exists): | ||
| with open(filename, 'wb') as f: | ||
| self.root = None | ||
| pickle.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', 'wb') as f: | ||
| pickle.dump([self.root], f) | ||
| os.rename(self.filename+'.tmp', self.filename) | ||
|
|
||
| def __enter__(self): | ||
| return self | ||
|
|
||
| def __exit__(self, *args): | ||
| self.close() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| 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: | ||
|
|
||
| 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. | ||
|
|
||
| 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): | ||
| ... env = os.environ | ||
| ... env['PYTHONPATH'] = '.' | ||
| ... cmd, _, args = cmd.partition(' ') | ||
| ... cmd = (sys.executable + ' ' + os.path.join('examples', cmd + '.py') | ||
| ... + ' -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() | ||
| ... for line in p.stdout: | ||
| ... print(line.rstrip('\n')) | ||
| ... errs = p.stderr.read().splitlines() | ||
| ... if errs: | ||
| ... print('--------- error output --------') | ||
| ... for line in errs: | ||
| ... 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') | ||
| Account Balance | ||
| ------- ------- | ||
| checking 0.00 | ||
| _______ | ||
| 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 checking (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 | ||
|
|
||
| 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 | ||
| 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: | ||
| >>> if os.path.exists(PMEM_FILE): | ||
| ... os.remove(PMEM_FILE) | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's nothing wrong with what you have here. However, it is a general principle in python that in most places where you could use a variable name you can instead use an expression. This applies particularly to function arguments. You have "L1 = " followed by "print(L1)". You could instead write "print()". That is, the above two lines would become:
This is how you will typically see print statements written in Python programs.