Changeset - ca12496880d6
[Not reviewed]
0 10 0
Brett Smith - 4 years ago 2021-02-26 21:13:02
brettcsmith@brettcsmith.org
typing: Updates to pass type checking under mypy>=0.800.

Most of these account for the fact that mypy now reports that Hashable is
not an allowed return type for sort key functions.

That, plus the new ignore for the regression in config.py.
10 files changed with 22 insertions and 18 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/beancount_types.py
Show inline comments
...
 
@@ -19,24 +19,27 @@ from typing import (
 
    List,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Set,
 
    Tuple,
 
    Type,
 
    Union,
 
)
 

	
 
if TYPE_CHECKING:
 
    from . import errors as errormod
 
    from _typeshed import SupportsLessThan as Sortable
 
else:
 
    from typing import Hashable as Sortable
 

	
 
Account = bc_data.Account
 
Currency = bc_data.Currency
 
Meta = bc_data.Meta
 
MetaKey = str
 
MetaValue = Any
 
MetaValueEnum = str
 
Posting = bc_data.Posting
 

	
 
class Directive(NamedTuple):
 
    meta: Meta
 
    date: datetime.date
conservancy_beancount/cliutil.py
Show inline comments
...
 
@@ -33,40 +33,40 @@ import yaml
 
from . import data
 
from . import errors
 
from . import filters
 
from . import rtutil
 

	
 
from typing import (
 
    cast,
 
    Any,
 
    BinaryIO,
 
    Callable,
 
    Container,
 
    Generic,
 
    Hashable,
 
    IO,
 
    Iterable,
 
    Iterator,
 
    List,
 
    NamedTuple,
 
    NoReturn,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Type,
 
    TypeVar,
 
    Union,
 
)
 
from .beancount_types import (
 
    MetaKey,
 
    Sortable,
 
)
 

	
 
ET = TypeVar('ET', bound=enum.Enum)
 
OutputFile = Union[int, IO]
 

	
 
CPU_COUNT = len(os.sched_getaffinity(0))
 
STDSTREAM_PATH = Path('-')
 
VERSION = pkg_resources.require(PKGNAME)[0].version
 

	
 
class EnumArgument(Generic[ET]):
 
    """Wrapper class to use an enum as argument values
 

	
...
 
@@ -105,33 +105,33 @@ class EnumArgument(Generic[ET]):
 
            return next(iter(matches))
 
        elif count:
 
            names = ', '.join(repr(choice.name) for choice in matches)
 
            raise ValueError(f"ambiguous argument {arg!r}: matches {names}")
 
        else:
 
            raise ValueError(f"unknown argument {arg!r}")
 

	
 
    def value_type(self, arg: str) -> Any:
 
        return self.enum_type(arg).value
 

	
 
    def choices_str(self, sep: str=', ', fmt: str='{!r}') -> str:
 
        """Return a user-formatted string of enum names"""
 
        sortkey: Callable[[ET], Hashable] = getattr(
 
        sortkey: Callable[[ET], Sortable] = getattr(
 
            self.base, '_choices_sortkey', self._choices_sortkey,
 
        )
 
        return sep.join(
 
            fmt.format(choice.name.lower())
 
            for choice in sorted(self.base, key=sortkey)
 
        )
 

	
 
    def _choices_sortkey(self, choice: ET) -> Hashable:
 
    def _choices_sortkey(self, choice: ET) -> Sortable:
 
        return choice.name
 

	
 

	
 
class ExceptHook:
 
    def __init__(self, logger: Optional[logging.Logger]=None) -> None:
 
        if logger is None:
 
            logger = logging.getLogger()
 
        self.logger = logger
 

	
 
    def __call__(self,
 
                 exc_type: Type[BaseException],
 
                 exc_value: BaseException,
...
 
@@ -198,25 +198,25 @@ class ExitCode(enum.IntEnum):
 

	
 
class ExtendAction(argparse.Action):
 
    """argparse action to let a user build a list from a string
 

	
 
    This is a fancier version of argparse's built-in ``action='append'``.
 
    The user's input is turned into a list of strings, split by a regexp
 
    pattern you provide. Typical usage looks like::
 

	
 
        parser = argparse.ArgumentParser()
 
        parser.add_argument(
 
          '--option', ...,
 
          action=ExtendAction,
 
          const=regexp_pattern,  # default is r'\s*,\s*'
 
          const=regexp_pattern,  # default is '\\s*,\\s*'
 
          ...,
 
        )
 
    """
 
    DEFAULT_PATTERN = r'\s*,\s*'
 

	
 
    def __call__(self,
 
                 parser: argparse.ArgumentParser,
 
                 namespace: argparse.Namespace,
 
                 values: Union[Sequence[Any], str, None]=None,
 
                 option_string: Optional[str]=None,
 
    ) -> None:
 
        pattern: str = self.const or self.DEFAULT_PATTERN
...
 
@@ -249,25 +249,25 @@ class InfoAction(argparse.Action):
 

	
 

	
 
class LogLevel(enum.IntEnum):
 
    DEBUG = logging.DEBUG
 
    INFO = logging.INFO
 
    WARNING = logging.WARNING
 
    ERROR = logging.ERROR
 
    CRITICAL = logging.CRITICAL
 
    WARN = WARNING
 
    ERR = ERROR
 
    CRIT = CRITICAL
 

	
 
    def _choices_sortkey(self) -> Hashable:
 
    def _choices_sortkey(self) -> Sortable:
 
        return self.value
 

	
 

	
 
class SearchTerm(NamedTuple):
 
    """NamedTuple representing a user's metadata filter
 

	
 
    SearchTerm knows how to parse and store posting metadata filters provided
 
    by the user in `key=value` format. Reporting tools can use this to filter
 
    postings that match the user's criteria, to report on subsets of the books.
 

	
 
    Typical usage looks like::
 

	
conservancy_beancount/config.py
Show inline comments
...
 
@@ -219,13 +219,14 @@ class Config:
 
            except OSError:
 
                # RTLinkCache.setup() will handle the problem.
 
                pass
 
            cache_db = rtutil.RTLinkCache.setup(cache_path)
 
        return rtutil.RT(wrapper_client, cache_db)
 

	
 
    def rt_wrapper(self,
 
                  credentials: RTCredentials=None,
 
                  client: Type[rt.Rt]=rt.Rt,
 
    ) -> Optional[rtutil.RT]:
 
        if credentials is None:
 
            credentials = self.rt_credentials()
 
        return self._rt_wrapper(credentials, client)
 
        # type ignore for <https://github.com/python/typeshed/issues/4638>
 
        return self._rt_wrapper(credentials, client)  # type:ignore[arg-type]
conservancy_beancount/reconcile/paypal.py
Show inline comments
...
 
@@ -25,36 +25,38 @@ import odf.table  # type:ignore[import]
 

	
 
from beancount.parser import printer as bc_printer
 

	
 
from .. import books
 
from .. import cliutil
 
from .. import config as configmod
 
from .. import data
 
from ..ranges import DateRange
 
from ..reports import core
 

	
 
from typing import (
 
    Any,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Tuple,
 
    Union,
 
)
 
from ..beancount_types import (
 
    Sortable,
 
)
 

	
 
PROGNAME = 'reconcile-paypal'
 
logger = logging.getLogger('conservancy_beancount.reconcile.paypal')
 

	
 
class ReconcileProblems(enum.IntFlag):
 
    NOT_IN_STATEMENT = enum.auto()
 
    DUP_IN_STATEMENT = enum.auto()
 
    AMOUNT_DIFF = enum.auto()
 
    MONTH_DIFF = enum.auto()
 
    DATE_DIFF = enum.auto()
 

	
 

	
...
 
@@ -155,25 +157,25 @@ class PayPalReconciler:
 
        else:
 
            balance = core.Balance(post.amount for post in self.books_posts)
 
            if not (balance - self.statement_posts[0].amount).is_zero():
 
                return ReconcileProblems.AMOUNT_DIFF
 
            worst_problem = 0
 
            for _, problems in self.post_problems():
 
                if problems & ~ReconcileProblems.DATE_DIFF:
 
                    return problems
 
                else:
 
                    worst_problem = worst_problem or problems
 
            return worst_problem
 

	
 
    def sort_key(self) -> Hashable:
 
    def sort_key(self) -> Sortable:
 
        try:
 
            post = self.statement_posts[0]
 
        except IndexError:
 
            post = self.books_posts[0]
 
        return post.date
 

	
 

	
 
class PayPalReconciliationReport(core.BaseODS[PayPalPosting, str]):
 
    COLUMN_WIDTHS = {
 
        'Source': 1.5,
 
        'Transaction ID': 1.5,
 
        'Date': None,
conservancy_beancount/reports/balance_sheet.py
Show inline comments
...
 
@@ -12,25 +12,24 @@ import enum
 
import logging
 
import operator
 
import sys
 

	
 
from decimal import Decimal
 
from pathlib import Path
 

	
 
from typing import (
 
    Any,
 
    Callable,
 
    Collection,
 
    Dict,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Tuple,
 
    Union,
 
)
 

	
conservancy_beancount/reports/budget.py
Show inline comments
...
 
@@ -18,25 +18,24 @@ import enum
 
import logging
 
import operator
 
import sys
 

	
 
from decimal import Decimal
 
from pathlib import Path
 

	
 
from typing import (
 
    Any,
 
    Callable,
 
    Collection,
 
    Dict,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Tuple,
 
    Union,
 
)
 

	
conservancy_beancount/reports/core.py
Show inline comments
...
 
@@ -43,42 +43,42 @@ from .. import filters
 
from .. import ranges
 
from .. import rtutil
 

	
 
from typing import (
 
    cast,
 
    overload,
 
    Any,
 
    BinaryIO,
 
    Callable,
 
    Collection,
 
    Dict,
 
    Generic,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    MutableMapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    Set,
 
    Tuple,
 
    Type,
 
    TypeVar,
 
    Union,
 
)
 
from ..beancount_types import (
 
    MetaKey,
 
    MetaValue,
 
    Sortable,
 
)
 

	
 
OPENING_BALANCE_NAME = "OPENING BALANCE"
 
ENDING_BALANCE_NAME = "ENDING BALANCE"
 

	
 
DecimalCompat = data.DecimalCompat
 
BalanceType = TypeVar('BalanceType', bound='Balance')
 
ElementType = Callable[..., odf.element.Element]
 
LinkType = Union[str, Tuple[str, Optional[str]]]
 
RelatedType = TypeVar('RelatedType', bound='RelatedPostings')
 
RT = TypeVar('RT', bound=Sequence)
 
ST = TypeVar('ST')
...
 
@@ -430,25 +430,25 @@ class Balances:
 
                sort_period = Period.ANY
 
        class_bals: Mapping[data.Account, MutableBalance] \
 
            = collections.defaultdict(MutableBalance)
 
        for key, balance in self.balances.items():
 
            if not key.account.is_under(account):
 
                pass
 
            elif key.period & sort_period:
 
                class_bals[key.classification] += balance
 
            else:
 
                # Ensure the balance exists in the mapping
 
                class_bals[key.classification]
 
        norm_func = normalize_amount_func(f'{account}:RootsOK')
 
        def sortkey(acct: data.Account) -> Hashable:
 
        def sortkey(acct: data.Account) -> Sortable:
 
            prefix, _, _ = acct.rpartition(':')
 
            balance = norm_func(class_bals[acct])
 
            try:
 
                max_bal = max(amount.number for amount in balance.values())
 
            except ValueError:
 
                max_bal = Decimal(0)
 
            return prefix, -max_bal
 
        return sorted(class_bals, key=sortkey)
 

	
 
    def iter_accounts(self, root: Optional[str]=None) -> Sequence[data.Account]:
 
        """Return a sequence of accounts open during the reporting period
 

	
conservancy_beancount/reports/ledger.py
Show inline comments
...
 
@@ -37,39 +37,39 @@ import argparse
 
import collections
 
import datetime
 
import enum
 
import itertools
 
import operator
 
import logging
 
import sys
 

	
 
from typing import (
 
    Any,
 
    Callable,
 
    Dict,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    List,
 
    Mapping,
 
    Optional,
 
    Sequence,
 
    Set,
 
    TextIO,
 
    Tuple,
 
    Type,
 
    Union,
 
    cast,
 
)
 
from ..beancount_types import (
 
    Sortable,
 
    Transaction,
 
)
 

	
 
from pathlib import Path
 

	
 
import odf.table  # type:ignore[import]
 
import odf.text  # type:ignore[import]
 

	
 
from beancount.core import data as bc_data
 
from beancount.parser import printer as bc_printer
 

	
 
from . import core
...
 
@@ -521,25 +521,25 @@ class ReportType(enum.IntFlag):
 

	
 
    @classmethod
 
    def post_flag(cls, post: data.Posting) -> int:
 
        norm_func = core.normalize_amount_func(post.account)
 
        number = norm_func(post.units.number)
 
        if not number:
 
            return cls.ZERO_TRANSACTIONS
 
        elif number > 0:
 
            return cls.CREDIT_TRANSACTIONS
 
        else:
 
            return cls.DEBIT_TRANSACTIONS
 

	
 
    def _choices_sortkey(self) -> Hashable:
 
    def _choices_sortkey(self) -> Sortable:
 
        subtype, _, maintype = self.name.partition('_')
 
        return (maintype, subtype)
 

	
 

	
 
class TransactionODS(LedgerODS):
 
    CORE_COLUMNS: Sequence[str] = [
 
        'Date',
 
        'Description',
 
        'Account',
 
        data.Metadata.human_name('entity'),
 
        'Original Amount',
 
        'Booked Amount',
conservancy_beancount/tools/opening_balances.py
Show inline comments
...
 
@@ -18,38 +18,38 @@ even a specific date (which can be helpful for testing or debugging).
 

	
 
import argparse
 
import collections
 
import copy
 
import datetime
 
import enum
 
import locale
 
import logging
 
import sys
 

	
 
from typing import (
 
    Dict,
 
    Hashable,
 
    Iterable,
 
    Iterator,
 
    Mapping,
 
    NamedTuple,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
    Tuple,
 
)
 
from ..beancount_types import (
 
    Error,
 
    MetaKey,
 
    MetaValue,
 
    Sortable,
 
    Transaction,
 
)
 

	
 
from decimal import Decimal, ROUND_HALF_EVEN, ROUND_HALF_UP
 

	
 
from .. import books
 
from .. import cliutil
 
from .. import config as configmod
 
from .. import data
 
from ..reports.core import Balance
 

	
 
from beancount.core import data as bc_data
...
 
@@ -67,25 +67,25 @@ logger = logging.getLogger('conservancy_beancount.tools.opening_balances')
 

	
 
def quantize_amount(
 
        amount: data.Amount,
 
        exp: Decimal=Decimal('.01'),
 
        rounding: str=ROUND_HALF_EVEN,
 
) -> data.Amount:
 
    return amount._replace(number=amount.number.quantize(exp, rounding=rounding))
 

	
 
class AccountWithFund(NamedTuple):
 
    account: data.Account
 
    fund: Optional[MetaValue]
 

	
 
    def sortkey(self) -> Hashable:
 
    def sortkey(self) -> Sortable:
 
        account, fund = self
 
        return (
 
            0 if fund is None else 1,
 
            locale.strxfrm(account),
 
            locale.strxfrm(str(fund).casefold()),
 
        )
 

	
 

	
 
class Posting(data.Posting):
 
    @staticmethod
 
    def _position_sortkey(position: Position) -> str:
 
        units, cost = position
conservancy_beancount/tools/sort_entries.py
Show inline comments
...
 
@@ -13,33 +13,33 @@ tools like ``diff``.
 
import argparse
 
import logging
 
import os
 
import sys
 

	
 
from pathlib import Path
 

	
 
from beancount.core import display_context as bc_dcontext
 
from beancount import loader as bc_loader
 
from beancount.parser import printer as bc_printer
 

	
 
from typing import (
 
    Hashable,
 
    Optional,
 
    Sequence,
 
    TextIO,
 
)
 
from ..beancount_types import (
 
    Directive,
 
    Entries,
 
    Errors,
 
    Sortable,
 
)
 

	
 
from .. import cliutil
 

	
 
PROGNAME = 'bean-sort'
 
logger = logging.getLogger('conservancy_beancount.tools.sort_entries')
 

	
 
def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace:
 
    parser = argparse.ArgumentParser(prog=PROGNAME)
 
    cliutil.add_version_argument(parser)
 
    cliutil.add_loglevel_argument(parser)
 
    parser.add_argument(
...
 
@@ -47,25 +47,25 @@ def parse_arguments(arglist: Optional[Sequence[str]]=None) -> argparse.Namespace
 
        action='store_true',
 
        help="""Suppress Beancount errors
 
""")
 
    parser.add_argument(
 
        'paths',
 
        metavar='PATH',
 
        type=Path,
 
        nargs=argparse.ONE_OR_MORE,
 
        help="""Beancount path(s) to read entries from
 
""")
 
    return parser.parse_args(arglist)
 

	
 
def entry_sorter(entry: Directive) -> Hashable:
 
def entry_sorter(entry: Directive) -> Sortable:
 
    type_name = type(entry).__name__
 
    if type_name == 'Transaction':
 
        return (entry.date, type_name, entry.narration, entry.payee or '')  # type:ignore[attr-defined]
 
    else:
 
        return (entry.date, type_name)
 

	
 
def main(arglist: Optional[Sequence[str]]=None,
 
         stdout: TextIO=sys.stdout,
 
         stderr: TextIO=sys.stderr,
 
) -> int:
 
    args = parse_arguments(arglist)
 
    cliutil.set_loglevel(logger, args.loglevel)
0 comments (0 inline, 0 general)