diff --git a/conservancy_beancount/config.py b/conservancy_beancount/config.py index 6d1157ad4d2a5e9c6332fd348e4f7f5d32bd369f..9acab353331cf3e227b4921ca4c73e4cd3dba855 100644 --- a/conservancy_beancount/config.py +++ b/conservancy_beancount/config.py @@ -15,9 +15,11 @@ # along with this program. If not, see . import configparser +import datetime import decimal import functools import os +import re import urllib.parse as urlparse import requests.auth @@ -27,6 +29,7 @@ from pathlib import Path from typing import ( NamedTuple, Optional, + Tuple, Type, ) @@ -84,6 +87,9 @@ class Config: with config_path.open() as config_file: self.file_config.read_file(config_file) + def load_string(self, config_str: str) -> None: + self.file_config.read_string(config_str) + def _dir_or_none(self, path: Path) -> Optional[Path]: try: path.mkdir(exist_ok=True) @@ -124,6 +130,24 @@ class Config: config_root = self._path_from_environ('XDG_CONFIG_HOME') return Path(config_root, name, 'config.ini') + def fiscal_year_begin(self) -> Tuple[int, int]: + s = self.file_config.get('Beancount', 'fiscal year begin', fallback='3 1') + match = re.match(r'([01]?[0-9])(?:\s*[-./ ]\s*([0-3]?[0-9]))?$', s.strip()) + if match is None: + raise ValueError(f"fiscal year begin {s!r} has unknown format") + try: + month = int(match.group(1)) + day = int(match.group(2) or 1) + # To check date validity we use an arbitrary year that's + # 1. definitely using the modern calendar + # 2. far enough in the past to not have books (pre-Unix epoch) + # 3. not a leap year + datetime.date(1959, month, day) + except ValueError as e: + raise ValueError(f"fiscal year begin {s!r} is invalid date: {e.args[0]}") + else: + return (month, day) + def payment_threshold(self) -> decimal.Decimal: return decimal.Decimal(0) diff --git a/tests/test_config.py b/tests/test_config.py index 65743b39e1308619f8d2eb21b8dbf8d49d46501d..3faea8721b264fa0fc655e4f6048ec2711612c3f 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -342,3 +342,40 @@ def test_load_file_error(tmp_path, path_func): def test_no_books_path(): config = config_mod.Config() assert config.books_path() is None + +@pytest.mark.parametrize('value,month,day', [ + ('2', 2, 1), + ('3 ', 3, 1), + (' 4', 4, 1), + (' 5 ', 5, 1), + ('6 1', 6, 1), + (' 06 03 ', 6, 3), + ('6-05', 6, 5), + ('06 - 10', 6, 10), + ('6/15', 6, 15), + ('06 / 20', 6, 20), + ('10.25', 10, 25), + (' 10 . 30 ', 10, 30), +]) +def test_fiscal_year_begin(value, month, day): + config = config_mod.Config() + config.load_string(f'[Beancount]\nfiscal year begin = {value}\n') + assert config.fiscal_year_begin() == (month, day) + +@pytest.mark.parametrize('value', [ + 'text', + '1900', + '13', + '010', + '2 30', + '4-31', +]) +def test_bad_fiscal_year_begin(value): + config = config_mod.Config() + config.load_string(f'[Beancount]\nfiscal year begin = {value}\n') + with pytest.raises(ValueError): + config.fiscal_year_begin() + +def test_default_fiscal_year_begin(): + config = config_mod.Config() + assert config.fiscal_year_begin() == (3, 1)