From c3c19b0ad21c61070751524e6e7609b362b09fa5 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Fri, 6 Mar 2020 10:37:54 -0500 Subject: [PATCH] plugin.core: Document base classes. Since so much other code is built on these, I wanted to make sure I wrote this while it was all fresh in my head. --- conservancy_beancount/plugin/core.py | 60 ++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/conservancy_beancount/plugin/core.py b/conservancy_beancount/plugin/core.py index d91fbac..fc8a130 100644 --- a/conservancy_beancount/plugin/core.py +++ b/conservancy_beancount/plugin/core.py @@ -19,10 +19,22 @@ import re from . import errors as errormod +# I expect these will become configurable in the future, which is why I'm +# keeping them outside of a class, but for now constants will do. DEFAULT_START_DATE = datetime.date(2020, 3, 1) +# The default stop date leaves a little room after so it's easy to test +# dates past the far end of the range. DEFAULT_STOP_DATE = datetime.date(datetime.MAXYEAR, 1, 1) class _GenericRange: + """Convenience class to check whether a value is within a range. + + `foo in generic_range` is equivalent to `start <= foo < stop`. + Since we have multiple user-configurable ranges, having the check + encapsulated in an object helps implement the check consistently, and + makes it easier for subclasses to override. + """ + def __init__(self, start, stop): self.start = start self.stop = stop @@ -38,7 +50,26 @@ class _GenericRange: class MetadataEnum: + """Map acceptable metadata values to their normalized forms. + + When a piece of metadata uses a set of allowed values, use this class to + define them. You can also specify aliases that hooks will normalize to + the primary values. + """ + def __init__(self, key, standard_values, aliases_map): + """Specify allowed values and aliases for this metadata. + + Arguments: + + * key: The name of the metadata key that uses this enum. + * standard_values: A sequence of strings that enumerate the standard + values for this metadata. + * aliases_map: A mapping of strings to strings. The keys are + additional allowed metadata values. The values are standard values + that each key will evaluate to. The code asserts that all values are + in standard_values. + """ self.key = key self._stdvalues = frozenset(standard_values) self._aliases = dict(aliases_map) @@ -49,15 +80,26 @@ class MetadataEnum: return "{}<{}>".format(type(self).__name__, self.key) def __contains__(self, key): + """Returns true if `key` is a standard value or alias.""" return key in self._aliases def __getitem__(self, key): + """Return the standard value for `key`. + + Raises KeyError if `key` is not a known value or alias. + """ return self._aliases[key] def __iter__(self): + """Iterate over standard values.""" return iter(self._stdvalues) def get(self, key, default_key=None): + """Return self[key], or a default fallback if that doesn't exist. + + default_key is another key to look up, *not* a default value to return. + This helps ensure you always get a standard value. + """ try: return self[key] except KeyError: @@ -68,6 +110,17 @@ class MetadataEnum: class PostingChecker: + """Base class to normalize posting metadata from an enum.""" + # This class provides basic functionality to filter postings, normalize + # metadata values, and set default values. + # Subclasses should set: + # * METADATA_KEY: A string with the name of the metadata key to normalize. + # * ACCOUNTS: Only check postings that match these account names. + # Can be a tuple of account prefix strings, or a regexp. + # * VALUES_ENUM: A MetadataEnum with allowed values and aliases. + # Subclasses may wish to override _default_value and _should_check. + # See below. + HOOK_GROUPS = frozenset(['Posting', 'metadata']) ACCOUNTS = ('',) TXN_DATE_RANGE = _GenericRange(DEFAULT_START_DATE, DEFAULT_STOP_DATE) @@ -84,9 +137,16 @@ class PostingChecker: post.meta = {} post.meta[key] = value + # If the posting does not specify METADATA_KEY, the hook calls + # _default_value to get a default. This method should either return + # a value string from METADATA_ENUM, or else raise InvalidMetadataError. + # This base implementation does the latter. def _default_value(self, txn, post): raise errormod.InvalidMetadataError(txn, post, self.METADATA_KEY) + # The hook calls _should_check on every posting and only checks postings + # when the method returns true. This base method checks the transaction + # date is in TXN_DATE_RANGE, and the posting account name matches ACCOUNTS. def _should_check(self, txn, post): ok = txn.date in self.TXN_DATE_RANGE if isinstance(self.ACCOUNTS, tuple):