""" Module implementing message related functions and classes. """ import pathlib from typing import Type, TypeVar, Optional, Any from dataclasses import dataclass, asdict from .tags import Tag QuestionInst = TypeVar('QuestionInst', bound='Question') AnswerInst = TypeVar('AnswerInst', bound='Answer') MessageInst = TypeVar('MessageInst', bound='Message') class MessageError(Exception): pass def source_code(text: str, include_delims: bool = False) -> list[str]: """ Extract all source code sections from the given text, i. e. all lines surrounded by lines tarting with '```'. If 'include_delims' is True, the surrounding lines are included, otherwise they are omitted. The result list contains every source code section as a single string. The order in the list represents the order of the sections in the text. """ code_sections: list[str] = [] code_lines: list[str] = [] in_code_block = False for line in text.split('\n'): if line.strip().startswith('```'): if include_delims: code_lines.append(line) if in_code_block: code_sections.append('\n'.join(code_lines) + '\n') code_lines.clear() in_code_block = not in_code_block elif in_code_block: code_lines.append(line) return code_sections class Question(str): """ A single question with a defined header. """ header = '=== QUESTION ===' def __new__(cls: Type[QuestionInst], string: str) -> QuestionInst: """ Make sure the question string does not contain the header. """ if cls.header in string: raise MessageError(f"Question '{string}' contains the header '{cls.header}'") instance = super().__new__(cls, string) return instance def source_code(self, include_delims: bool = False) -> list[str]: """ Extract and return all source code sections. """ return source_code(self, include_delims) class Answer(str): """ A single answer with a defined header. """ header = '=== ANSWER ===' def __new__(cls: Type[AnswerInst], string: str) -> AnswerInst: """ Make sure the answer string does not contain the header. """ if cls.header in string: raise MessageError(f"Answer '{string}' contains the header '{cls.header}'") instance = super().__new__(cls, string) return instance def source_code(self, include_delims: bool = False) -> list[str]: """ Extract and return all source code sections. """ return source_code(self, include_delims) @dataclass class Message(): """ Single message. Consists of a question and optionally an answer, a set of tags and a file path. """ question: Question answer: Optional[Answer] tags: Optional[set[Tag]] path: Optional[pathlib.Path] # @classmethod # def from_file(cls: Type[MessageInst], path: pathlib.Path) -> MessageInst: # """ # Create a Message from the given file. Expects the following file structure: # * TagLine (from 'self.tags') # * Question.Header # * Question # * Answer.Header # """ # pass def to_file(self, path: Optional[pathlib.Path]) -> None: """ Write Message to the given file. Creates the following file structure: * TagLine (from 'self.tags') * Question.Header * Question * Answer.Header * Answer """ if not path and not self.path: raise MessageError('Got no valid path to write message') pass def asdict(self) -> dict[str, Any]: return asdict(self)