# Copyright (C) 2018-'24 Frank Sachsenheim## This program is free software: you can redistribute it and/or modify# it under the terms of the GNU Affero General Public License as published# by the Free Software Foundation, either version 3 of the License, or# (at your option) any later version.## This program is distributed in the hope that it will be useful,# but WITHOUT ANY WARRANTY; without even the implied warranty of# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the# GNU Affero General Public License for more details.## You should have received a copy of the GNU Affero General Public License# along with this program. If not, see <https://www.gnu.org/licenses/>."""This module offers a canonical interface with the aim to make re-use of transformingalgorithms easier.Let's look at it with examples:: from delb.transform import Transformation class ResolveCopyOf(Transformation): def transform(self): for node in self.root.css_select( "*[copyOf]", namespaces={None: "http://www.tei-c.org/ns/1.0"} ): source_id = node["copyOf"] source_node = self.origin_document.xpath( f'//*[@xml:id="{source_id[1:]}"]', namespaces={} ).first cloned_node = source_node.clone(deep=True) cloned_node.id = None node.replace_with(cloned_node)From such defined transformations instances can be called with a (sub-)tree and anoptional document where that tree originates from:: resolve_copy_of = ResolveCopyOf() tree = resolve_copy_of(tree, origin_document=document):class:`typing.NamedTuple` are used to define options for transformations:: from typing import Final, NamedTuple, TypedDict class NamespacesKWArgs(TypedDict): namespaces: dict[str | None, str] TEI: Final[NamespacesKWArgs] = {"namespaces": {None: TEI_NAMESPACE}} class ResolveChoiceOptions(NamedTuple): corr: bool = True reg: bool = True class ResolveChoice(Transformation): options_class = ResolveChoiceOptions def __init__(self, options): super().__init__(options) self.keep_selector = ",".join( ( "corr" if self.options.corr else "sic", "reg" if self.options.reg else "orig" ) ) self.drop_selector = ",".join( ( "sic" if self.options.corr else "corr", "orig" if self.options.reg else "reg" ) ) def transform(self): for choice_node in self.root.css_select("choice", **TEI): node_to_drop = choice_node.css_select(self.drop_selector, **TEI).first node_to_drop.detach() node_to_keep = choice_node.css_select(self.keep_selector, **TEI).first node_to_keep.detach(retain_child_nodes=True) choice_node.detach(retain_child_nodes=True)A transformation class that defines an ``option_class`` property can then either be usedwith its defaults or with alternate options:: resolve_choice = ResolveChoice() tree = resolve_choice(tree) resolve_choice = ResolveChoice(ResolveChoiceOptions(reg=False)) tree = resolve_choice(tree)Finally, concrete transformations can be chained, both as classes or instances. Theinterface allows also to chain multiple chains:: from delb.transform import TransformationSequence tidy_up = TransformationSequence(ResolveCopyOf, resolve_choice) tree = tidy_up(tree).. attention:: This is an experimental feature. It might change significantly in the future or be removed altogether."""from__future__importannotationsfromabcimportABC,abstractmethodfromtypingimportNamedTuple,Optionalfromdelbimportnew_tag_node,Document,TagNode#classTransformationBase(ABC):"""This base class defines the calling interface of transformations."""@abstractmethoddef__call__(self,root:TagNode,origin_document:Optional[Document]=None)->TagNode:pass
[docs]classTransformation(TransformationBase):"""This is a base class for any transformation algorithm."""options_class:Optional[type]=Nonedef__init__(self,options:Optional[NamedTuple]=None):self.root:TagNodeifoptionsisNoneandself.options_classisnotNone:options=self.options_class()self.options=optionsself.__set_placeholder()def__call__(self,root:TagNode,origin_document:Optional[Document]=None)->TagNode:self.root=rootself.origin_document=origin_documentself.transform()result=self.rootself.__set_placeholder()returnresultdef__set_placeholder(self):self.root=new_tag_node("transformation-placeholder")
[docs]@abstractmethoddeftransform(self):""" This method needs to implement the transformation logic. When it is called, the instance has two attributes assigned from its call: ``root`` is the node that the transformation was called to transform with. ``origin_document`` is the document that was possibly passed as second argument. """pass
[docs]classTransformationSequence(TransformationBase):""" A transformation sequence can be used to combine any number of both :class:`Transformation` (provided as class or instantiated with options) and other :class:`TransformationSequence` instances or classes. """def__init__(self,*transformations:TransformationBase|type[TransformationBase],):_transformations=[]fortransformationintransformations:ifisinstance(transformation,type)andissubclass(transformation,TransformationBase):_transformations.append(transformation())elifisinstance(transformation,TransformationBase):_transformations.append(transformation)else:raiseTypeError("Only subclasses of TransformationBase or instances of such are ""allowed.")self.transformations=tuple(_transformations)def__call__(self,root:TagNode,origin_document:Optional[Document]=None)->TagNode:fortransformationinself.transformations:root=transformation(root,origin_document=origin_document)returnroot