import re from pathlib import Path from typing import Optional, Dict, TextIO from .constants import * from .formatterif import FormatterIF from .formatting_config import FormattingConfig from .git_version_info import GitVersionInfo from .generic_text import GenericText from ..logger import logger from abc import ABC, abstractmethod from .enums import * from datetime import * class PyTeXFormatter(FormatterIF, ABC): def __init__( self, input_file: Optional[Path] = None, config: Optional[FormattingConfig] = None, git_version_info: Optional[GitVersionInfo] = None, locate_file_config: bool = True, allow_infile_config: bool = True ): super().__init__( input_file=input_file, config=config ) self._config: Optional[FormattingConfig] = self._config # for type-hinting self._git_version_info: Optional[GitVersionInfo] = git_version_info self._allow_infile_config: bool = allow_infile_config self._header: Optional[GenericText] = None if locate_file_config: file_config = self.parse_file_config() if allow_infile_config: infile_config = self.parse_infile_config() self._config = \ file_config.merge_with( infile_config, strict=True ).merge_with(self.config, strict=False) else: self._config = file_config.merge_with(self.config) else: if allow_infile_config: infile_config = self.parse_infile_config() self._config = infile_config.merge_with(self.config) self._output_file: Optional[TextIO] = None # This may change over time in case of multiple output files self._attribute_dict: Optional[Dict] = None def parse_file_config(self) -> FormattingConfig: config_file = self.input_file.with_name(self.input_file.name + PYTEX_CONFIG_FILE_EXTENSION) if config_file.exists(): try: return FormattingConfig.from_yaml(config_file) except: raise NotImplementedError # Invalid yaml file format else: return FormattingConfig() def parse_infile_config(self) -> FormattingConfig: if self._input_file is None: raise NotImplementedError # no file initialised yet with open(self._input_file, "r") as file: line = file.readline() if re.match(self.config.escape_character + INFILE_CONFIG_BEGIN_CONFIG, line): if not line.strip().lstrip('%').strip() == self.config.escape_character + INFILE_CONFIG_BEGIN_CONFIG: logger.warning( "File {file}: Start of infile config invalid." ) config = [] while True: line = file.readline() if re.match(self.config.escape_character + INFILE_CONFIG_END_CONFIG, line): if not line.strip().lstrip( '%').strip() == self.config.escape_character + INFILE_CONFIG_END_CONFIG: logger.warning( "File {file}: End of infile config invalid." ) break if line == '': raise NotImplementedError # No matching end block config.append(line.lstrip('%').rstrip()) try: return FormattingConfig.from_yaml('\n'.join(config)) except: raise NotImplementedError # Invalid yaml file format else: return FormattingConfig() @property def config(self) -> FormattingConfig: if self._config is None: return FormattingConfig() return self._config @config.setter def config(self, formatting_config: FormattingConfig): self._config = formatting_config @property def git_version_info(self) -> GitVersionInfo: if self._git_version_info is None: return GitVersionInfo() else: return self._git_version_info @property def header(self) -> GenericText: if self._header is None: if not ( self.config.include_extra_header or self.config.include_time or self.config.include_pytex_version or self.config.include_pytex_info_text or self.config.include_repo_version or self.config.include_repo_info_text ): self._header = GenericText() else: self._header = GenericText() # TODO: handle license if self.config.include_extra_header: self._header += self.config.extra_header + [''] if self.config.include_repo_info_text: self._header += self.config.repo_info_text if self.config.include_pytex_info_text: self._header += self.config.pytex_info_text + [''] ## TODO handle rest return self._header def _update_attribute_dict(self): self._attribute_dict: Dict[str, str] = { FormatterProperty.author.value: self.config.author, FormatterProperty.shortauthor.value: self.shortauthor, FormatterProperty.date.value: datetime.now().strftime('%Y/%m/%d'), FormatterProperty.year.value: datetime.now().strftime('%Y'), FormatterProperty.raw_name.value: self.raw_name, FormatterProperty.name.value: self.name, FormatterProperty.version.value: self.config.version, FormatterProperty.file_name.value: self.current_file_name, FormatterProperty.source_file_name.value: self._input_file.name, FormatterProperty.repo_version.value: self.git_version_info.repo_version.version, FormatterProperty.repo_branch.value: self.git_version_info.repo_version.branch, FormatterProperty.repo_commit.value: self.git_version_info.repo_version.commit_hash, FormatterProperty.repo_dirty.value: self.git_version_info.repo_version.dirty, FormatterProperty.pytex_version.value: self.git_version_info.pytex_version.version, FormatterProperty.pytex_branch.value: self.git_version_info.pytex_version.branch, FormatterProperty.pytex_commit.value: self.git_version_info.pytex_version.commit_hash, FormatterProperty.pytex_dirty.value: self.git_version_info.pytex_version.dirty, FormatterProperty.tex_type.value: self.config.tex_type.value, FormatterProperty.tex_flavour.value: self.config.tex_flavour.value, FormatterProperty.file_prefix.value: self.file_prefix } @property def attribute_dict(self) -> Dict: if self._attribute_dict is None: self._update_attribute_dict() return self._attribute_dict @property def shortauthor(self) -> str: parts = self.config.author.lower().replace('ß', 'ss').split(' ') # TODO: better non-alphanumeric handling if len(parts) == 1: return parts[0] else: return parts[0][0] + parts[-1] @property def raw_name(self) -> str: parts = self._input_file.name.split('.', maxsplit=1) if not len(parts) == 2: raise NotImplementedError # invalid file name else: return parts[0] @property def file_prefix(self) -> str: if self.config.naming_scheme == NamingScheme.prepend_author: if self.config.tex_flavour == TeXFlavour.LaTeX2e: return self.shortauthor + '@' + self.raw_name elif self.config.tex_flavour == TeXFlavour.LaTeX3: return self.shortauthor + '_' + self.raw_name else: raise NotImplementedError else: return self.raw_name @property def name(self): if self.config.naming_scheme == NamingScheme.prepend_author: return self.shortauthor + '-' + self.raw_name else: return self.raw_name def current_file_name(self): return self._output_file.name def make_header(self, **kwargs) -> str: try: return '\n'.join( [ '%' * 80, self.header.format(**self.attribute_dict), '%' * 80, '' ] ) except KeyError: raise NotImplementedError