Source code for pyexlatex.table.models.texgen.alignment

import re
from typing import List, Tuple, Optional

from mixins.repr import ReprMixin
from pyexlatex.models.item import ItemBase
from pyexlatex.models.containeritem import ContainerItem
from pyexlatex.texgen.packages.columntypes import ColumnTypesPackage


[docs]class ColumnAlignment(ReprMixin, ItemBase): """ Alignment of a single column in a table """ repr_cols = ['align']
[docs] def __init__(self, align_str: str): self._validate_align_str(align_str) self.align = align_str
def __str__(self): return self.align def __add__(self, other): return self.align + other.align def __radd__(self, other): return other.align + self.align def _validate_align_str(self, align_str): basic_pattern = re.compile(r'[lcr|.]') length_pattern = re.compile(r'[LCRd]\{[\d\w\s.]+\}') dcolumn_pattern = re.compile(r'D(\{.+\})?') siunitx_pattern = re.compile(r'[sS](\[.+\])?') spacing_pattern = re.compile(r'[@!]\{.*\}') basic_match = basic_pattern.fullmatch(align_str) length_match = length_pattern.fullmatch(align_str) dcolumn_match = dcolumn_pattern.fullmatch(align_str) siunitx_match = siunitx_pattern.fullmatch(align_str) spacing_match = spacing_pattern.fullmatch(align_str) if length_match or dcolumn_match: self._add_requirements_for_length_match() if siunitx_match: self._add_requirements_for_s_column_types() if not (basic_match or length_match or dcolumn_match or siunitx_match or spacing_match): raise ValueError(f'expected alignment of l, c, r, ., |, s, S, ' f'L{{size}}, C{{size}}, R{{size}}, d{{decimal format}}, ' f'or D{{in sep}}{{out sep}}{{decimal format}}. Got {align_str}') def _add_requirements_for_length_match(self): self.add_package(ColumnTypesPackage()) def _add_requirements_for_s_column_types(self): self.add_package('siunitx') @property def col_length(self) -> int: if self.align == '|': return 0 spacing_adjust_pattern = re.compile(r'[@!]\{.*\}') if spacing_adjust_pattern.fullmatch(self.align): return 0 return 1
[docs]class ColumnsAlignment(ReprMixin, ContainerItem): """ A set of column alignments, usually for the whole table """ repr_cols = ['aligns']
[docs] def __init__(self, aligns: List[ColumnAlignment] = None, num_columns: int=None): self.aligns = ColumnsAlignment._get_aligns(aligns, num_columns) self.add_data_from_content(self.aligns)
def __str__(self): return ''.join(str(align) for align in self.aligns) def __iter__(self): for align in self.aligns: yield align @staticmethod def _get_col_length(aligns: List[ColumnAlignment]) -> int: return sum([align.col_length for align in aligns]) @staticmethod def _get_aligns(aligns: List[ColumnAlignment] = None, num_columns: int=None): if aligns is None and num_columns is None: raise ValueError('must pass aligns or num columns') # default align is first column left, rest centered if aligns is None and num_columns is not None: return [ColumnAlignment('l')] + [ColumnAlignment('c')] * (num_columns - 1) # if we don't know how many columns, must assume passed number of aligns is correct if num_columns is None: return aligns # Shouldn't hit this block, but needed for typing if aligns is None: raise ValueError('must pass aligns or num columns') # number of alignments matches number of columns. no extra processing needed if ColumnsAlignment._get_col_length(aligns) == num_columns: return aligns # if one alignment is passed with many columns, use that align with all columns if len(aligns) == 1: return [aligns[0]] * num_columns else: raise ValueError(f'got {len(aligns)} alignments for {num_columns} columns. unclear how to apply')
[docs] @classmethod def from_alignment_str(cls, align_str: str, num_columns: int=None): align_str_list = _full_align_str_to_align_str_list(align_str) aligns = [ColumnAlignment(align) for align in align_str_list] return cls(aligns, num_columns=num_columns)
def _full_align_str_to_align_str_list(align_str: str): split_letters = ['l', 'c', 'r', '|', 'L', 'C', 'R', '.', 's', 'S', '@', '!', 'd', 'D'] out_list = [] collected_letters = '' escape_pairs: List[Tuple[str, str]] = [('{', '}'), ('[', ']')] escape_begins = [pair[0] for pair in escape_pairs] escape_ends = [pair[1] for pair in escape_pairs] split = True current_escape_end: str = '' for letter in align_str: # beginning inside of length str. don't split while inside if letter in escape_begins: split = False # Determine escape ending character for (beg, end) in escape_pairs: if letter == beg: current_escape_end = end break if not current_escape_end: raise ValueError(f'matched {letter} as an escape character but no ' f'end character. escape pairs: {escape_pairs}') # end of inside of length str. turn splitting back on if letter == current_escape_end: split = True current_escape_end = '' # if splitting, output what we've got so far and start a new item if split and letter in split_letters: out_list.append(collected_letters) collected_letters = '' # if not splitting, add to current item collected_letters += letter # Clean up list from loop. Add last entry, and combine first two entries out_list.append(collected_letters) first_val = out_list.pop(0) out_list[0] = first_val + out_list[0] return out_list