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 convert as bc_convert
from beancount.core import data as bc_data from beancount.core import data as bc_data
from beancount.core import position as bc_position from beancount.core import position as bc_position
from beancount.parser import options as bc_options
from typing import ( from typing import (
cast, cast,
@ -40,6 +41,7 @@ from typing import (
Iterator, Iterator,
MutableMapping, MutableMapping,
Optional, Optional,
Pattern,
Sequence, Sequence,
TypeVar, TypeVar,
Union, Union,
@ -53,6 +55,7 @@ from .beancount_types import (
MetaKey, MetaKey,
MetaValue, MetaValue,
Open, Open,
OptionsMap,
Posting as BasePosting, Posting as BasePosting,
Transaction, Transaction,
) )
@ -153,8 +156,19 @@ class Account(str):
""" """
__slots__ = () __slots__ = ()
ACCOUNT_RE: Pattern
SEP = bc_account.sep SEP = bc_account.sep
_meta_map: MutableMapping[str, AccountMeta] = {} _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 @classmethod
def load_opening(cls, opening: Open) -> None: def load_opening(cls, opening: Open) -> None:
@ -178,6 +192,10 @@ class Account(str):
elif isinstance(entry, bc_data.Close): elif isinstance(entry, bc_data.Close):
cls.load_closing(entry) # type:ignore[arg-type] 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 @property
def meta(self) -> AccountMeta: def meta(self) -> AccountMeta:
return self._meta_map[self] return self._meta_map[self]
@ -286,6 +304,7 @@ class Account(str):
return self return self
else: else:
return self[:stop] return self[:stop]
Account.load_options_map(bc_options.OPTIONS_DEFAULTS)
class Amount(bc_amount.Amount): class Amount(bc_amount.Amount):

View file

@ -21,6 +21,7 @@ from . import testutil
from datetime import date as Date from datetime import date as Date
from beancount.core.data import Open, Close, Booking from beancount.core.data import Open, Close, Booking
from beancount.parser import options as bc_options
from conservancy_beancount import data 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:Donations', entries[0])
check_account_meta('Income:Other', entries[1]) check_account_meta('Income:Other', entries[1])
check_account_meta('Assets:Checking', entries[2], 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.amount as bc_amount
import beancount.core.data as bc_data import beancount.core.data as bc_data
import beancount.loader as bc_loader import beancount.loader as bc_loader
import beancount.parser.options as bc_options
import odf.element import odf.element
import odf.opendocument import odf.opendocument
@ -43,6 +44,7 @@ TESTS_DIR = Path(__file__).parent
# it with different scopes. Typical usage looks like: # it with different scopes. Typical usage looks like:
# clean_account_meta = pytest.fixture([options])(testutil.clean_account_meta) # clean_account_meta = pytest.fixture([options])(testutil.clean_account_meta)
def clean_account_meta(): def clean_account_meta():
data.Account.load_options_map(bc_options.OPTIONS_DEFAULTS)
data.Account._meta_map.clear() data.Account._meta_map.clear()
def _ods_cell_value_type(cell): def _ods_cell_value_type(cell):