Source code for gptables.core.theme
from xlsxwriter.format import Format
from gptables.core.gptable import GPTable
import yaml
from functools import wraps
def validate_single_format(f):
@wraps(f)
def wrapper(cls, format_dict):
"""
Decorator to validate that input is a dictionary dictionary.
"""
if not isinstance(format_dict, dict):
actual_type = type(format_dict)
msg = ("Formats must be supplied as a dictionary, not"
f"{actual_type}")
raise ValueError(msg)
for fmt in format_dict.keys():
cls._validate_format_label(fmt)
return f(cls, format_dict)
return wrapper
[docs]class Theme:
"""
A class that defines a set of format attributes for use in xlsxwriter.
This class associates a dict of format attributes with table elements.
See XlsxWriter
`format properties <https://xlsxwriter.readthedocs.io/format.html#format-methods-and-format-properties>`_
for valid options.
Attributes
----------
cover_title_format : dict
cover_subtitle_format : dict
cover_text_format : dict
title_format : dict
subtitle_format : dict
instructions_format : dict
scope_format : dict
column_heading_format : dict
index_1_format : dict
index_2_format : dict
index_3_format : dict
data_format : dict
source_format : dict
legend_format : dict
description_order : list
"""
def __init__(
self,
config=None,
):
"""
Initialise theme object.
Parameters
----------
config : dict or .yaml/.yml file
theme specification
"""
## Formats
self._format_attributes = [
"cover_title_format",
"cover_subtitle_format",
"cover_text_format",
"title_format",
"subtitle_format",
"instructions_format",
"scope_format",
"column_heading_format",
"index_1_format",
"index_2_format",
"index_3_format",
"data_format",
"source_format",
"legend_format",
]
for attr in self._format_attributes:
setattr(self, attr, {})
## Other attributes
self.description_order = []
# Valid Them format attributes
self._valid_attrs = [
x.replace("_format", "")
for x in self._format_attributes
] + ["global"]
# Valid XlsxWriter Format attributes
self._valid_format_labels = [
attr.replace("set_", "")
for attr in Format().__dir__()
if attr.startswith('set_')
and callable(getattr(Format(), attr))
]
if config:
self.apply_config(config)
@staticmethod
def _parse_config(config):
"""
Parse yaml configuration to dictionary.
"""
if isinstance(config, str):
if not config.endswith((".yml", ".yaml")):
raise ValueError("Theme configuration files must be YAML")
with open(config, "r") as file:
cfg = yaml.safe_load(file)
elif isinstance(config, dict):
cfg = config
else:
raise ValueError("Theme configuration must be a dict or YAML file")
return cfg
def _validate_config(self, config):
"""
Assert that format dictionary lower level keys are valid XlsxWriter
Format attributes.
"""
for attr in config.keys():
if attr in self._valid_attrs:
attr_config = config[attr] or {}
for fmt in attr_config.keys():
self._validate_format_label(fmt)
def _validate_format_label(self, format_name):
"""
Assert that format is a valid XlsxWriter Format attribute.
"""
if format_name not in self._valid_format_labels:
raise ValueError(f"`{format_name}` is not a valid format label")
[docs] def apply_config(self, config):
"""
Update multiple Theme attributes using a YAML or dictionary config.
This enables extension of build in Themes.
"""
cfg = self._parse_config(config)
self._validate_config(cfg)
# Update all when global used
if "global" in cfg.keys():
default_format = cfg.pop("global")
self._update_all_formats(default_format)
# Update with individual methods
for key, value in cfg.items():
if key == "description_order":
getattr(self, "update_" + key)(value)
elif key in self._valid_attrs:
if value is not None:
getattr(self, "update_" + key + "_format")(value)
else:
raise ValueError(f"`{key}` is not a valid Theme attribute")
def _update_all_formats(self, global_dict):
"""
Updates all theme attributes with a global format dictionary.
"""
for attr in self._format_attributes:
if attr.endswith("_format"):
getattr(self, "update_" + attr)(global_dict)
[docs] @validate_single_format
def update_column_heading_format(self, format_dict):
"""
Update the `column_heading_format` attribute. Where keys already exist,
existing items are replaced.
"""
self.column_heading_format.update(format_dict)
[docs] @validate_single_format
def update_index_1_format(self, format_dict):
"""
Update the `index_1_format` attribute. Where keys already exist, existing
items are replaced.
"""
self.index_1_format.update(format_dict)
[docs] @validate_single_format
def update_index_2_format(self, format_dict):
"""
Update the `index_2_format` attribute. Where keys already exist, existing
items are replaced.
"""
self.index_2_format.update(format_dict)
[docs] @validate_single_format
def update_index_3_format(self, format_dict):
"""
Update the `index_3_format` attribute. Where keys already exist, existing
items are replaced.
"""
self.index_3_format.update(format_dict)
[docs] @validate_single_format
def update_data_format(self, format_dict):
"""
Update the `data_format` attribute. Where keys already exist,
existing items are replaced.
"""
self.data_format.update(format_dict)
[docs] @validate_single_format
def update_cover_title_format(self, format_dict):
"""
Update the `cover_title_format` attribute. Where keys already exist, existing
items are replaced.
"""
self.cover_title_format.update(format_dict)
[docs] @validate_single_format
def update_cover_subtitle_format(self, format_dict):
"""
Update the `cover_subtitle_format` attribute. Where keys already exist, existing
items are replaced.
"""
self.cover_subtitle_format.update(format_dict)
[docs] @validate_single_format
def update_cover_text_format(self, format_dict):
"""
Update the `cover_text_format` attribute. Where keys already exist, existing
items are replaced.
"""
self.cover_text_format.update(format_dict)
[docs] @validate_single_format
def update_title_format(self, format_dict):
"""
Update the `title_format` attribute. Where keys already exist, existing
items are replaced.
"""
self.title_format.update(format_dict)
[docs] @validate_single_format
def update_subtitle_format(self, format_dict):
"""
Update the `subtitle_format` attribute. Where keys already exist,
existing items are replaced.
"""
self.subtitle_format.update(format_dict)
[docs] @validate_single_format
def update_instructions_format(self, format_dict):
"""
Update the `instructions_format` attribute. Where keys already exist,
existing items are replaced.
"""
self.instructions_format.update(format_dict)
[docs] @validate_single_format
def update_scope_format(self, format_dict):
"""
Update the `scope_format` attribute. Where keys already exist, existing
items are replaced.
"""
self.scope_format.update(format_dict)
[docs] @validate_single_format
def update_location_format(self, format_dict):
"""
Update the `location_format` attribute. Where keys already exist,
existing items are replaced.
"""
self.location_format.update(format_dict)
[docs] @validate_single_format
def update_source_format(self, format_dict):
"""
Update the `source_format` attribute. Where keys already exist,
existing items are replaced.
"""
self.source_format.update(format_dict)
[docs] @validate_single_format
def update_legend_format(self, format_dict):
"""
Update the `legend_format` attribute. Where keys already exist,
existing items are replaced.
"""
self.legend_format.update(format_dict)
[docs] def update_description_order(self, order_list):
"""
Update the `description_order` attribute. Overrides existing order.
"""
if not isinstance(order_list, list):
msg = ("`description_order` must be a list of description element names")
raise TypeError(msg)
valid_elements = ["instructions", "source", "legend", "scope"]
if not all(element in valid_elements for element in order_list):
msg = (f"`description_order` elements must be in {valid_elements}")
raise ValueError(msg)
self.description_order = order_list
[docs] def print_attributes(self):
"""
Print all current format attributes and values to the console.
"""
obj_attr = [
attr for attr in self.__dir__()
if not attr.startswith('_')
and not callable(getattr(self, attr))
]
for attr in obj_attr:
print(attr, ":", getattr(self, attr))
def __eq__(self, other):
"""
Comparison operator, for testing.
"""
# don't attempt to compare against unrelated types
if not isinstance(other, Theme):
return False
obj_attr = [
attr for attr in self.__dir__()
if not attr.startswith('_')
and not callable(getattr(self, attr))
]
return all([
getattr(self, attr) == getattr(other, attr)
for attr in obj_attr
])