data: Add Account.is_account and Account.load_options_map.

These work in concert to distinguish account names from other
colon-separated strings.
This commit is contained in:
Brett Smith 2020-07-16 10:11:39 -04:00
parent 6a7815090c
commit fff9e37bf8
3 changed files with 67 additions and 0 deletions

View file

@ -30,6 +30,7 @@ from beancount.core import amount as bc_amount
from beancount.core import convert as bc_convert
from beancount.core import data as bc_data
from beancount.core import position as bc_position
from beancount.parser import options as bc_options
from typing import (
cast,
@ -40,6 +41,7 @@ from typing import (
Iterator,
MutableMapping,
Optional,
Pattern,
Sequence,
TypeVar,
Union,
@ -53,6 +55,7 @@ from .beancount_types import (
MetaKey,
MetaValue,
Open,
OptionsMap,
Posting as BasePosting,
Transaction,
)
@ -153,8 +156,19 @@ class Account(str):
"""
__slots__ = ()
ACCOUNT_RE: Pattern
SEP = bc_account.sep
_meta_map: MutableMapping[str, AccountMeta] = {}
_options_map: OptionsMap
@classmethod
def load_options_map(cls, options_map: OptionsMap) -> None:
cls._options_map = options_map
roots: Sequence[str] = bc_options.get_account_types(options_map)
cls.ACCOUNT_RE = re.compile(
r'^(?:{})(?:{}[A-Z0-9][-A-Za-z0-9]*)+$'.format(
'|'.join(roots), cls.SEP,
))
@classmethod
def load_opening(cls, opening: Open) -> None:
@ -178,6 +192,10 @@ class Account(str):
elif isinstance(entry, bc_data.Close):
cls.load_closing(entry) # type:ignore[arg-type]
@classmethod
def is_account(cls, s: str) -> bool:
return cls.ACCOUNT_RE.fullmatch(s) is not None
@property
def meta(self) -> AccountMeta:
return self._meta_map[self]
@ -286,6 +304,7 @@ class Account(str):
return self
else:
return self[:stop]
Account.load_options_map(bc_options.OPTIONS_DEFAULTS)
class Amount(bc_amount.Amount):

View file

@ -21,6 +21,7 @@ from . import testutil
from datetime import date as Date
from beancount.core.data import Open, Close, Booking
from beancount.parser import options as bc_options
from conservancy_beancount import data
@ -267,3 +268,48 @@ def test_load_openings_and_closings(clean_account_meta):
check_account_meta('Income:Donations', entries[0])
check_account_meta('Income:Other', entries[1])
check_account_meta('Assets:Checking', entries[2], entries[-1])
@pytest.mark.parametrize('account_s', [
'Assets:Bank:Checking',
'Equity:Funds:Restricted',
'Expenses:Other',
'Income:Donations',
'Liabilities:CreditCard:Visa',
])
def test_is_account(account_s):
assert data.Account.is_account(account_s)
@pytest.mark.parametrize('account_s', [
'Assets:Bank:12-345',
'Equity:Funds:Restricted',
'Expenses:Other',
'Income:Donations',
'Liabilities:CreditCard:Visa0123',
])
def test_is_account(clean_account_meta, account_s):
assert data.Account.is_account(account_s)
@pytest.mark.parametrize('account_s', [
'Assets:checking',
'Assets::Cash',
'Equity',
'Liabilities:Credit Card',
'income:Donations',
'Expenses:Banking_Fees',
'Revenue:Grants',
])
def test_is_not_account(clean_account_meta, account_s):
assert not data.Account.is_account(account_s)
@pytest.mark.parametrize('account_s,expected', [
('Revenue:Donations', True),
('Costs:Other', True),
('Income:Donations', False),
('Expenses:Other', False),
])
def test_is_account_respects_configured_roots(clean_account_meta, account_s, expected):
config = bc_options.OPTIONS_DEFAULTS.copy()
config['name_expenses'] = 'Costs'
config['name_income'] = 'Revenue'
data.Account.load_options_map(config)
assert data.Account.is_account(account_s) == expected

View file

@ -21,6 +21,7 @@ import re
import beancount.core.amount as bc_amount
import beancount.core.data as bc_data
import beancount.loader as bc_loader
import beancount.parser.options as bc_options
import odf.element
import odf.opendocument
@ -43,6 +44,7 @@ TESTS_DIR = Path(__file__).parent
# it with different scopes. Typical usage looks like:
# clean_account_meta = pytest.fixture([options])(testutil.clean_account_meta)
def clean_account_meta():
data.Account.load_options_map(bc_options.OPTIONS_DEFAULTS)
data.Account._meta_map.clear()
def _ods_cell_value_type(cell):