Changeset - 15becebf5c4f
[Not reviewed]
0 2 0
Brett Smith - 4 years ago 2020-06-28 03:08:23
brettcsmith@brettcsmith.org
reports: Add BaseODS.border_style() method.
2 files changed with 78 insertions and 3 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/reports/core.py
Show inline comments
...
 
@@ -15,12 +15,13 @@
 
# 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 urllib.parse as urlparse
 

	
 
import babel.core  # type:ignore[import]
...
 
@@ -515,12 +516,20 @@ class BaseSpreadsheet(Generic[RT, ST], metaclass=abc.ABCMeta):
 
            should_end = False
 
        if should_end:
 
            self.end_section(section)
 
        self.end_spreadsheet()
 

	
 

	
 
class Border(enum.IntFlag):
 
    TOP = 1
 
    RIGHT = 2
 
    BOTTOM = 4
 
    LEFT = 8
 
    # in CSS order, clockwise from top
 

	
 

	
 
class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
    """Abstract base class to help write OpenDocument spreadsheets
 

	
 
    This class provides the very core logic to write an arbitrary set of data
 
    rows to an OpenDocument spreadsheet. It provides helper methods for
 
    building sheets, rows, and cells.
...
 
@@ -545,13 +554,13 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 

	
 
    def __init__(self, rt_wrapper: Optional[rtutil.RT]=None) -> None:
 
        self.rt_wrapper = rt_wrapper
 
        self.locale = babel.core.Locale.default('LC_MONETARY')
 
        self.currency_fmt_key = 'accounting'
 
        self._name_counter = itertools.count(1)
 
        self._currency_style_cache: MutableMapping[str, odf.style.Style] = {}
 
        self._style_cache: MutableMapping[str, odf.style.Style] = {}
 
        self.document = odf.opendocument.OpenDocumentSpreadsheet()
 
        self.init_settings()
 
        self.init_styles()
 
        self.sheet = self.use_sheet("Report")
 

	
 
    ### Low-level document tree manipulation
...
 
@@ -663,12 +672,38 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
            root, odf.config.ConfigItem, name=name, type=config_type,
 
        )
 
        item.addText(value_s)
 

	
 
    ### Styles
 

	
 
    def border_style(self,
 
                     edges: int,
 
                     width: str='1px',
 
                     style: str='solid',
 
                     color: str='#000000',
 
    ) -> odf.style.Style:
 
        flags = [edge for edge in Border if edges & edge]
 
        if not flags:
 
            raise ValueError(f"no valid edges in {edges!r}")
 
        border_attr = f'{width} {style} {color}'
 
        key = f'{",".join(f.name for f in flags)} {border_attr}'
 
        try:
 
            retval = self._style_cache[key]
 
        except KeyError:
 
            props = odf.style.TableCellProperties()
 
            for flag in flags:
 
                props.setAttribute(f'border{flag.name.lower()}', border_attr)
 
            retval = odf.style.Style(
 
                name=f'Border{next(self._name_counter)}',
 
                family='table-cell',
 
            )
 
            retval.addElement(props)
 
            self.document.styles.addElement(retval)
 
            self._style_cache[key] = retval
 
        return retval
 

	
 
    def column_style(self, width: Union[float, str], **attrs: Any) -> odf.style.Style:
 
        if not isinstance(width, str) or (width and not width[-1].isalpha()):
 
            width = f'{width}in'
 
        match = self.MEASUREMENT_RE.fullmatch(width)
 
        if match is None:
 
            raise ValueError(f"invalid width {width!r}")
...
 
@@ -782,13 +817,13 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
        if positive_properties is not None:
 
            cache_parts.append('')
 
            for key, value in self.iter_attributes(positive_properties):
 
                cache_parts.append(f'{key}={value}')
 
        cache_key = '\0'.join(cache_parts)
 
        try:
 
            style = self._currency_style_cache[cache_key]
 
            style = self._style_cache[cache_key]
 
        except KeyError:
 
            pos_style = self._build_currency_style(
 
                root, locale, code, 0, positive_properties, volatile=True,
 
            )
 
            curr_style = self._build_currency_style(
 
                root, locale, code, 1, negative_properties,
...
 
@@ -800,13 +835,13 @@ class BaseODS(BaseSpreadsheet[RT, ST], metaclass=abc.ABCMeta):
 
                self.document.styles,
 
                odf.style.Style,
 
                name=f'{curr_style.getAttribute("name")}Cell',
 
                family='table-cell',
 
                datastylename=curr_style,
 
            )
 
            self._currency_style_cache[cache_key] = style
 
            self._style_cache[cache_key] = style
 
        return style
 

	
 
    def _merge_style_iter_names(
 
            self,
 
            styles: Sequence[Union[str, odf.style.Style, None]],
 
    ) -> Iterator[str]:
tests/test_reports_spreadsheet.py
Show inline comments
...
 
@@ -310,12 +310,52 @@ def test_ods_writer_style(ods_writer, attr_name, child_type, checked_attr):
 
    style = getattr(ods_writer, attr_name)
 
    actual = get_child(root, odf.style.Style, name=style.getAttribute('name'))
 
    assert actual is style
 
    child = get_child(actual, child_type)
 
    assert child.getAttribute(checked_attr)
 

	
 
@pytest.mark.parametrize('edges,width,style,color', [
 
    (core.Border.TOP,
 
     '5px', 'solid', '#ff0000'),
 
    (core.Border.RIGHT | core.Border.LEFT,
 
     '2pt', 'dashed', '#00ff00'),
 
    (core.Border.BOTTOM | core.Border.RIGHT | core.Border.LEFT,
 
     '1em', 'dotted', '#0000ff'),
 
    (core.Border.TOP | core.Border.BOTTOM | core.Border.RIGHT | core.Border.LEFT,
 
     '1cm', 'thick', '#aaaaaa'),
 
])
 
def test_ods_writer_border_style(ods_writer, edges, width, style, color):
 
    actual = ods_writer.border_style(edges, width, style, color)
 
    props, = actual.childNodes
 
    attr_s = f'{width} {style} {color}'
 
    for edge_exp, edge_name in enumerate(['top', 'right', 'bottom', 'left']):
 
        expected = attr_s if edges & (2 ** edge_exp) else None
 
        assert props.getAttribute(f'border{edge_name}') == expected
 

	
 
def test_ods_writer_border_style_caches(ods_writer):
 
    expected = ods_writer.border_style(core.Border.TOP)
 
    width, style, color = expected.childNodes[0].getAttribute('bordertop').split()
 
    actual = ods_writer.border_style(core.Border.TOP, width, style, color)
 
    assert actual is expected
 

	
 
@pytest.mark.parametrize('argname,val1,val2', [
 
    ('edges', core.Border.TOP, core.Border.LEFT),
 
    ('edges', core.Border.TOP, core.Border.TOP | core.Border.BOTTOM),
 
    ('style', 'solid', 'dashed'),
 
    ('width', '1px', '1em'),
 
    ('width', '1px', '2px'),
 
    ('color', '#0000fe', '#0000ff'),
 
])
 
def test_ods_writer_border_no_caching(ods_writer, argname, val1, val2):
 
    kwargs = {'edges': core.Border.TOP}
 
    kwargs[argname] = val1
 
    style1 = ods_writer.border_style(**kwargs)
 
    kwargs[argname] = val2
 
    style2 = ods_writer.border_style(**kwargs)
 
    assert style1 is not style2
 

	
 
def test_ods_writer_merge_styles(ods_writer):
 
    style = ods_writer.merge_styles(ods_writer.style_bold, ods_writer.style_dividerline)
 
    actual = get_child(
 
        ods_writer.document.styles,
 
        odf.style.Style,
 
        name=style.getAttribute('name'),
0 comments (0 inline, 0 general)