Changeset - 837fcec8f0db
[Not reviewed]
0 3 0
Brett Smith - 4 years ago 2020-07-29 21:22:09
brettcsmith@brettcsmith.org
reports: Add BaseODS.set_common_properties() method.
3 files changed with 47 insertions and 0 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/core.py
Show inline comments
 
"""core.py - Common data classes for reporting functionality"""
 
# Copyright © 2020  Brett Smith
 
#
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU Affero General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU Affero General Public License for more details.
 
#
 
# You should have received a copy of the GNU Affero General Public License
 
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 

	
 
import abc
 
import collections
 
import copy
 
import datetime
 
import enum
 
import itertools
 
import operator
 
import re
 
import shlex
 
import sys
 
import urllib.parse as urlparse
 

	
 
import babel.core  # type:ignore[import]
 
import babel.numbers  # type:ignore[import]
 

	
 
import git  # type:ignore[import]
 

	
 
import odf.config  # type:ignore[import]
 
import odf.element  # type:ignore[import]
 
import odf.meta  # type:ignore[import]
 
import odf.number  # type:ignore[import]
 
import odf.opendocument  # type:ignore[import]
 
import odf.style  # type:ignore[import]
 
import odf.table  # type:ignore[import]
 
import odf.text  # type:ignore[import]
 

	
 
from decimal import Decimal
 
from pathlib import Path
 

	
 
from beancount.core import amount as bc_amount
 
from odf.namespaces import TOOLSVERSION  # type:ignore[import]
 

	
 
from ..cliutil import VERSION
 
from .. import data
 
from .. import filters
 
from .. import rtutil
 

	
 
from typing import (
 
    cast,
 
    overload,
 
    Any,
...
 
@@ -1031,48 +1035,64 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
        date_style.addElement(odf.number.Month(style='long'))
 
        date_style.addElement(odf.number.Text(text='-'))
 
        date_style.addElement(odf.number.Day(style='long'))
 
        self.style_date = self.ensure_child(
 
            styles,
 
            odf.style.Style,
 
            name=f'{date_style.getAttribute("name")}Cell',
 
            family='table-cell',
 
            datastylename=date_style,
 
        )
 

	
 
        self.style_starttext: odf.style.Style
 
        self.style_centertext: odf.style.Style
 
        self.style_endtext: odf.style.Style
 
        for textalign in ['start', 'center', 'end']:
 
            aligned_style = self.replace_child(
 
                styles, odf.style.Style, name=f'{textalign.title()}Text',
 
            )
 
            aligned_style.setAttribute('family', 'table-cell')
 
            aligned_style.addElement(odf.style.ParagraphProperties(textalign=textalign))
 
            setattr(self, f'style_{textalign}text', aligned_style)
 

	
 
    ### Properties
 

	
 
    def set_common_properties(self,
 
                              repo: Optional[git.Repo]=None,
 
                              command: Optional[Sequence[str]]=sys.argv,
 
    ) -> None:
 
        if repo is None:
 
            git_shahex = '<none>'
 
            git_dirty = True
 
        else:
 
            git_shahex = repo.head.commit.hexsha
 
            git_dirty = repo.is_dirty()
 
        self.set_custom_property('GitSHA', git_shahex)
 
        self.set_custom_property('GitDirty', git_dirty, 'boolean')
 
        if command is not None:
 
            command_s = ' '.join(shlex.quote(s) for s in command)
 
            self.set_custom_property('ReportCommand', command_s)
 

	
 
    def set_custom_property(self,
 
                            name: str,
 
                            value: Any,
 
                            valuetype: Optional[str]=None,
 
    ) -> odf.meta.UserDefined:
 
        if valuetype is None:
 
            if isinstance(value, bool):
 
                valuetype = 'boolean'
 
            elif isinstance(value, (datetime.date, datetime.datetime)):
 
                valuetype = 'date'
 
            elif isinstance(value, (int, float, Decimal)):
 
                valuetype = 'float'
 
        if not isinstance(value, str):
 
            if valuetype == 'boolean':
 
                value = 'true' if value else 'false'
 
            elif valuetype == 'date':
 
                value = value.isoformat()
 
            else:
 
                value = str(value)
 
        retval = self.ensure_child(self.document.meta, odf.meta.UserDefined, name=name)
 
        if valuetype is None:
 
            try:
 
                retval.removeAttribute('valuetype')
 
            except KeyError:
tests/test_reports_spreadsheet.py
Show inline comments
...
 
@@ -727,24 +727,42 @@ def test_ods_writer_set_properties(ods_writer):
 
    generator = get_child(meta, odf.meta.Generator)
 
    assert re.match(r'testgen/\d+\.\d+', generator.text)
 

	
 
@pytest.mark.parametrize('value,exptype', [
 
    (1, 'float'),
 
    (12.34, 'float'),
 
    (Decimal('5.99'), 'float'),
 
    (datetime.date(2009, 8, 17), 'date'),
 
    (datetime.datetime(2009, 8, 17, 18, 38, 58), 'date'),
 
    (True, 'boolean'),
 
    (False, 'boolean'),
 
    ('foo', None)
 
])
 
def test_ods_writer_set_custom_property(ods_writer, value, exptype):
 
    cprop = ods_writer.set_custom_property('cprop', value)
 
    assert cprop.getAttribute('name') == 'cprop'
 
    assert cprop.getAttribute('valuetype') == exptype
 
    if exptype == 'boolean':
 
        expected = str(value).lower()
 
    elif exptype == 'date':
 
        expected = value.isoformat()
 
    else:
 
        expected = str(value)
 
    assert cprop.text == expected
 

	
 
def test_ods_writer_set_common_properties(ods_writer):
 
    ods_writer.set_common_properties()
 
    get_child(ods_writer.document.meta, odf.meta.UserDefined, name='ReportCommand')
 

	
 
def test_ods_writer_common_repo_properties(ods_writer):
 
    repo = testutil.TestRepo('abcd1234', False)
 
    ods_writer.set_common_properties(repo)
 
    meta = ods_writer.document.meta
 
    sha_prop = get_child(meta, odf.meta.UserDefined, name='GitSHA')
 
    assert sha_prop.text == 'abcd1234'
 
    dirty_prop = get_child(meta, odf.meta.UserDefined, name='GitDirty')
 
    assert dirty_prop.text == 'false'
 

	
 
def test_ods_writer_common_command(ods_writer):
 
    ods_writer.set_common_properties(command=['testcmd', 'testarg*'])
 
    cmd_prop = get_child(ods_writer.document.meta, odf.meta.UserDefined, name='ReportCommand')
 
    assert cmd_prop.text == 'testcmd \'testarg*\''
tests/testutil.py
Show inline comments
 
"""Mock Beancount objects for testing"""
 
# Copyright © 2020  Brett Smith
 
#
 
# This program is free software: you can redistribute it and/or modify
 
# it under the terms of the GNU Affero General Public License as published by
 
# the Free Software Foundation, either version 3 of the License, or
 
# (at your option) any later version.
 
#
 
# This program is distributed in the hope that it will be useful,
 
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
# GNU Affero General Public License for more details.
 
#
 
# You should have received a copy of the GNU Affero General Public License
 
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 

	
 
import datetime
 
import itertools
 
import re
 
import unittest.mock
 

	
 
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 git
 
import odf.element
 
import odf.opendocument
 
import odf.table
 

	
 
from decimal import Decimal
 
from pathlib import Path
 
from typing import Any, Optional, NamedTuple
 

	
 
from conservancy_beancount import books, data, rtutil
 

	
 
EXTREME_FUTURE_DATE = datetime.date(datetime.MAXYEAR, 12, 30)
 
FUTURE_DATE = datetime.date.today() + datetime.timedelta(days=365 * 99)
 
FY_START_DATE = datetime.date(2020, 3, 1)
 
FY_MID_DATE = datetime.date(2020, 9, 1)
 
PAST_DATE = datetime.date(2000, 1, 1)
 
TESTS_DIR = Path(__file__).parent
 

	
 
# This function is a teardown fixture, but different test files use
 
# it with different scopes. Typical usage looks like:
 
#   clean_account_meta = pytest.fixture([options])(testutil.clean_account_meta)
 
def clean_account_meta():
 
    try:
 
        yield
 
    finally:
...
 
@@ -267,48 +269,55 @@ class TestConfig:
 
            self._rt_wrapper = rtutil.RT(rt_client)
 

	
 
    def books_loader(self):
 
        return self._books_loader
 

	
 
    def config_file_path(self):
 
        return test_path('userconfig/conservancy_beancount/config.ini')
 

	
 
    def fiscal_year_begin(self):
 
        return books.FiscalYear(*self.fiscal_year)
 

	
 
    def payment_threshold(self):
 
        return self._payment_threshold
 

	
 
    def repository_path(self):
 
        return self.repo_path
 

	
 
    def rt_client(self):
 
        return self._rt_client
 

	
 
    def rt_wrapper(self):
 
        return self._rt_wrapper
 

	
 

	
 
def TestRepo(head_hexsha='abcd1234', dirty=False):
 
    retval = unittest.mock.Mock(spec=git.Repo)
 
    retval.is_dirty.return_value = dirty
 
    retval.head.commit.hexsha = head_hexsha
 
    return retval
 

	
 

	
 
class _TicketBuilder:
 
    MESSAGE_ATTACHMENTS = [
 
        ('(Unnamed)', 'multipart/alternative', '0b'),
 
        ('(Unnamed)', 'text/plain', '1.2k'),
 
        ('(Unnamed)', 'text/html', '1.4k'),
 
    ]
 
    MISC_ATTACHMENTS = [
 
        ('Forwarded Message.eml', 'message/rfc822', '3.1k'),
 
        ('photo.jpg', 'image/jpeg', '65.2k'),
 
        ('ConservancyInvoice-301.pdf', 'application/pdf', '326k'),
 
        ('Company_invoice-2020030405_as-sent.pdf', 'application/pdf', '50k'),
 
        ('statement.txt', 'text/plain', '652b'),
 
        ('screenshot.png', 'image/png', '1.9m'),
 
    ]
 

	
 
    def __init__(self):
 
        self.id_seq = itertools.count(1)
 
        self.misc_attchs = itertools.cycle(self.MISC_ATTACHMENTS)
 

	
 
    def new_attch(self, attch):
 
        return (str(next(self.id_seq)), *attch)
 

	
 
    def new_msg_with_attachments(self, attachments_count=1):
 
        for attch in self.MESSAGE_ATTACHMENTS:
0 comments (0 inline, 0 general)