From fff9e37bf83e7f9461df2ad2d35e179c9cbc82f5 2020-07-16 14:11:39 From: Brett Smith Date: 2020-07-16 14:11:39 Subject: [PATCH] data: Add Account.is_account and Account.load_options_map. These work in concert to distinguish account names from other colon-separated strings. --- diff --git a/conservancy_beancount/data.py b/conservancy_beancount/data.py index 614034ef7ae897814fd7af26a4ef13c9e9285609..28c587f14ce2ff9b323e5c45e892deb71894e5b6 100644 --- a/conservancy_beancount/data.py +++ b/conservancy_beancount/data.py @@ -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): diff --git a/tests/test_data_account.py b/tests/test_data_account.py index 5c312eb254e1285763e12d5e9979056c529047a1..3b92fcde6de1c214858452c16dfdd48203c2360c 100644 --- a/tests/test_data_account.py +++ b/tests/test_data_account.py @@ -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 diff --git a/tests/testutil.py b/tests/testutil.py index 12331707d42f4d50425f62de2c6881c53b6a6aa3..e90b3e6bb00798cdc0d25b32c63a8c8d53cd0470 100644 --- a/tests/testutil.py +++ b/tests/testutil.py @@ -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):