I'm working on making a program to read a chart of accounts and turn it into a tree of Account
objects, which I will be doing stuff with later. The chart of accounts in question has several levels of sub-accounts, with the name of each account being indented three spaces more than its parent account. For example:
Current Assets
Cash
1000000.00 - Cash
1000010.00 - Bank Account #1
...
Other Current Assets
Receivables
100800.00 - Accounts Receivable
...
I've created a class to represent an account, as follows:
class Account:
parent = None
num: int | None
name: str # Name after stripping whitespace and splitting off account number
raw: str # Name prior to ^
... # Assorted irrelevant class variables
acct_type: str | None
detail_type: str | None
_header: bool = False # Flags whether an account's a header account
descendants: list = list() # Holds account's children. Should be empty if _header == False.
def __init__(self, instr: str, parent=None, acct_type: str = None, detail_type: str = None):
''' Initialize an Account.
- parent: Account. `None` if top-level.
- instr: raw string from ACS CoA.
- acct_type: QBO Account Type
- detail_type: QBO Detail Type
'''
self.raw = instr
self.parent = parent
self.acct_type = acct_type
self.detail_type = detail_type
stripped = instr.strip()
... # Assorted initialization code, incl. manipulating stripped
# to get values for self.num and self.name
... # Assorted irrelevant methods
def is_header(self) -> bool:
return self._header
def depth(self) -> int:
return (len(self.raw) - len(self.raw.strip())) / 3 # self.raw
def add_descendant(self, instr, acct_t: str = None, detail: str = None):
'''Add descendant to account.
If `acct_type` and `detail_type` are supplied, will override parent account type.
'''
if len(self.descendants) > 0:
if calc_depth(instr) > self.descendants[-1].depth():
self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
else:
self.descendants.append(Account(
instr,
parent=self,
acct_type = self.acct_type if acct_t is None else acct_t,
detail_type = self.detail_type if detail is None else detail
))
else:
self.descendants.append(Account(
instr,
parent=self,
acct_type = self.acct_type if acct_t is None else acct_t,
detail_type = self.detail_type if detail is None else detail
))
return
if __name__ == "__main__":
accounts: list[Account] = list()
for (acs_name, account_type, detail_type) in get_data(): # Gets data as list[tuple[str, str, str]]
d = len(acs_name) - len(acs_name.lstrip())) / 3 # Depth of current row's account
if d == 0: # Overall header
accounts.append(Account(acs_name, parent=None, acct_type=account_type, detail_type=detail_type))
else:
if account_type is not None:
# Account metadata supplied in CoA spreadsheet
accounts[-1].add_descendant(
acs_name,
acct_t = account_type,
detail = detail_type)
else:
# Impute account metadata from parent
accounts[-1].add_descendant(acs_name)
However, when I run this, I get a rather nasty RecursionError:
Exception has occurred: RecursionError
maximum recursion depth exceeded
KeyError: 'C:\\Users\\...\\Account.py'
During handling of the above exception, another exception occurred:
KeyError: 'C:\\Users\\...\\Account.py'
During handling of the above exception, another exception occurred:
KeyError: 'C:\\Users\\...\\Account.py'
During handling of the above exception, another exception occurred:
KeyError: 'C:\\Users\\...\\Account.py'
During handling of the above exception, another exception occurred:
File "C:\Users\...\Account.py", line 104, in add_descendant
self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\...\Account.py", line 104, in add_descendant
self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Users\...\Account.py", line 104, in add_descendant
self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
[Previous line repeated 980 more times]
File "C:\Users\...\Account.py", line 155, in <module>
acs_name,
^^^^^^^^^
...<3 lines>...
detail=detail_type
RecursionError: maximum recursion depth exceeded
I've spent the last 1.5 hours trying to fix this, but to no avail. Looking in my debugger, it appears that while the first two levels get added properly, that second level makes itself its own descendant ad infinitum. For example, in my most recent debugging run the tree looks like this:
<__main__.Account object at 0x0000015631511BE0> # Current Assets
<__main__.Account object at 0x0000015631796710> # Cash
<__main__.Account object at 0x0000015631796710> # Cash
<__main__.Account object at 0x0000015631796710> # Yet more Cash
... # 980-ish more Cash
For reference, it should actually look like this:
<__main__.Account object at 0x0000015631511BE0> # Current Assets
<__main__.Account object at 0x0000015631796710> # Cash
<__main__.Account object at 0x0000015631XXXXXX> # 1000000.00 - Cash
What am I doing wrong? As far as I can tell, there isn't anywhere where I am adding self
to self.descendants
.
(As a final note, this isn't my full code; irrelevant bits have been omitted or simplified.)
In this code:
def add_descendant(self, instr, acct_t: str = None, detail: str = None):
'''Add descendant to account.
If `acct_type` and `detail_type` are supplied, will override parent account type.
'''
if len(self.descendants) > 0:
if calc_depth(instr) > self.descendants[-1].depth():
self.descendants[-1].add_descendant(instr, acct_t = acct_t, detail = detail)
If the first two conditions are true, it calls itself recursively.
Then in the recursive call, those same two conditions are still true (because, as @Blckknght pointed out, self.descendants
is a shared class attribute, not a per-instance attribute). So it's going to call itself again.
And again.
And again.