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.
This commit is contained in:
		
							parent
							
								
									6e0c31f0ab
								
							
						
					
					
						commit
						c3c19b0ad2
					
				
					 1 changed files with 60 additions and 0 deletions
				
			
		| 
						 | 
					@ -19,10 +19,22 @@ import re
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from . import errors as errormod
 | 
					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)
 | 
					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)
 | 
					DEFAULT_STOP_DATE = datetime.date(datetime.MAXYEAR, 1, 1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class _GenericRange:
 | 
					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):
 | 
					    def __init__(self, start, stop):
 | 
				
			||||||
        self.start = start
 | 
					        self.start = start
 | 
				
			||||||
        self.stop = stop
 | 
					        self.stop = stop
 | 
				
			||||||
| 
						 | 
					@ -38,7 +50,26 @@ class _GenericRange:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MetadataEnum:
 | 
					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):
 | 
					    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.key = key
 | 
				
			||||||
        self._stdvalues = frozenset(standard_values)
 | 
					        self._stdvalues = frozenset(standard_values)
 | 
				
			||||||
        self._aliases = dict(aliases_map)
 | 
					        self._aliases = dict(aliases_map)
 | 
				
			||||||
| 
						 | 
					@ -49,15 +80,26 @@ class MetadataEnum:
 | 
				
			||||||
        return "{}<{}>".format(type(self).__name__, self.key)
 | 
					        return "{}<{}>".format(type(self).__name__, self.key)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __contains__(self, key):
 | 
					    def __contains__(self, key):
 | 
				
			||||||
 | 
					        """Returns true if `key` is a standard value or alias."""
 | 
				
			||||||
        return key in self._aliases
 | 
					        return key in self._aliases
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __getitem__(self, key):
 | 
					    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]
 | 
					        return self._aliases[key]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def __iter__(self):
 | 
					    def __iter__(self):
 | 
				
			||||||
 | 
					        """Iterate over standard values."""
 | 
				
			||||||
        return iter(self._stdvalues)
 | 
					        return iter(self._stdvalues)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def get(self, key, default_key=None):
 | 
					    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:
 | 
					        try:
 | 
				
			||||||
            return self[key]
 | 
					            return self[key]
 | 
				
			||||||
        except KeyError:
 | 
					        except KeyError:
 | 
				
			||||||
| 
						 | 
					@ -68,6 +110,17 @@ class MetadataEnum:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PostingChecker:
 | 
					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'])
 | 
					    HOOK_GROUPS = frozenset(['Posting', 'metadata'])
 | 
				
			||||||
    ACCOUNTS = ('',)
 | 
					    ACCOUNTS = ('',)
 | 
				
			||||||
    TXN_DATE_RANGE = _GenericRange(DEFAULT_START_DATE, DEFAULT_STOP_DATE)
 | 
					    TXN_DATE_RANGE = _GenericRange(DEFAULT_START_DATE, DEFAULT_STOP_DATE)
 | 
				
			||||||
| 
						 | 
					@ -84,9 +137,16 @@ class PostingChecker:
 | 
				
			||||||
            post.meta = {}
 | 
					            post.meta = {}
 | 
				
			||||||
        post.meta[key] = value
 | 
					        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):
 | 
					    def _default_value(self, txn, post):
 | 
				
			||||||
        raise errormod.InvalidMetadataError(txn, post, self.METADATA_KEY)
 | 
					        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):
 | 
					    def _should_check(self, txn, post):
 | 
				
			||||||
        ok = txn.date in self.TXN_DATE_RANGE
 | 
					        ok = txn.date in self.TXN_DATE_RANGE
 | 
				
			||||||
        if isinstance(self.ACCOUNTS, tuple):
 | 
					        if isinstance(self.ACCOUNTS, tuple):
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue