diff options
Diffstat (limited to 'mwlib/sanitychecker.py')
-rw-r--r-- | mwlib/sanitychecker.py | 205 |
1 files changed, 205 insertions, 0 deletions
diff --git a/mwlib/sanitychecker.py b/mwlib/sanitychecker.py new file mode 100644 index 0000000..78d25b8 --- /dev/null +++ b/mwlib/sanitychecker.py @@ -0,0 +1,205 @@ +# Copyright (c) 2007-2008 PediaPress GmbH +# See README.txt for additional licensing information. +""" +class for defining DTD-Like Rules for the tree +""" +from advtree import Article + +from mwlib.log import Log +log = Log("sanitychecker") + +# ----------------------------------------------------------- +# Constraints +# ----------------------------------------------------------- + +class ConstraintBase(object): + def __init__(self, *klasses): + self.klasses = klasses + + def test(self, nodes): + return True,None # passed + + def __repr__(self): + return "%s(%s)" %(self.__class__.__name__, ", ".join(k.__name__ for k in self.klasses)) + + +class Forbid(ConstraintBase): + "forbid any of the classes" + def test(self, nodes): + for n in nodes: + if n.__class__ in self.klasses: + return False, n + return True, None + + +class Allow(ConstraintBase): + "allow only these classes" + def test(self, nodes): + for n in nodes: + if not n.__class__ in self.klasses: + return False, n + return True, None + + +class Require(ConstraintBase): + "require any of these classes" + def test(self, nodes): + for n in nodes: + if n.__class__ in self.klasses: + return True, n + return False, None + +class Equal(ConstraintBase): + "node classes and their order must be equal to these klasses" + def test(self, nodes): + if len(nodes) != len(self.klasses): + return False, None # FIXME what could we report? + for i,n in enumerate(nodes): + if n.__class__ != self.klasses[i]: + return False, n + return True, None + + +# ----------------------------------------------------------- +# Rules regarding [Children, AllChildren, Parents, ...] +# ----------------------------------------------------------- + +class RuleBase: + def __init__(self, klass, constraint): + self.klass = klass + self.constraint = constraint + + def _tocheck(self, node): + return [] + + def test(self, node): + if node.__class__ == self.klass: + return self.constraint.test( self._tocheck(node) ) + return True, None + + def __repr__(self): + return "%s(%s, %r)" %(self.__class__.__name__, self.klass.__name__, self.constraint) + +class ChildrenOf(RuleBase): + def _tocheck(self, node): + return node.children + +class AllChildrenOf(RuleBase): + def _tocheck(self, node): + return node.getAllChildren() + +class ParentsOf(RuleBase): + def _tocheck(self, node): + return node.parents + +class ParentOf(RuleBase): + def _tocheck(self, node): + if node.parent: + return [node.parent] + return [] + +class SiblingsOf(RuleBase): + def _tocheck(self, node): + return node.siblings + + + +# example custom rules + +class RequireChild(RuleBase): + + def __init__(self, klass): + self.klass = klass + + def __repr__(self): + return "%s(%s)" %(self.__class__.__name__, self.klass.__name__) + + def test(self, node): + if node.__class__ == self.klass: + if not len(node.children): + return False, node + return True, None + + + + +# ----------------------------------------------------------- +# Callbacks +# ----------------------------------------------------------- +""" +callbacks get called if added to rules +callback return values should be: + * True if it modified the tree and the sanity check needs to restart + * False if the tree is left unmodified +""" +class SanityException(Exception): + pass + +def exceptioncb(rule, node=None, parentnode=None): + raise SanityException("%r err:%r" %(rule, node or parentnode) ) + +def warncb(rule, node=None, parentnode=None): + log.warn("%r node:%r parent:%r" %(rule, node, parentnode)) + return False + +def removecb(rule, node=None, parentnode=None): + assert node and node.parent + node.parent.removeChild(node) + return True + + + +# ----------------------------------------------------------- +# Container for sanity rules +# ----------------------------------------------------------- + +class SanityChecker(object): + + def __init__(self): + self.rules = [] + + def addRule(self, rule, actioncb=exceptioncb): + self.rules.append((rule, actioncb)) + + def check(self, tree): + """ + check each node with each rule + on failure call callback + """ + modified = True + while modified: + modified = False + for node in tree.allchildren(): + #if node.__class__ == Article: + # log.info("checking article:", node.caption.encode('utf-8')) + for r,cb in self.rules: + passed, errnode = r.test(node) + if not passed and cb: + if cb(r, errnode or node): + modified = True + break + if modified: + break + +def demo(tree): + "for documentation only, see tests for more demos" + from mwlib.advtree import Table, Row, Cell, Text, ImageLink, PreFormatted + + sc = SanityChecker() + rules = [ChildrenOf(Table, Allow(Row)), + ChildrenOf(Row, Allow(Cell)), + AllChildrenOf(Cell, Require(Text, ImageLink)), + AllChildrenOf(Cell, Forbid(PreFormatted)), + ChildrenOf(PreFormatted, Equal(Text)), + ] + + def mycb(rule, node=None, parentnode=None): + print "failed", rule, node or parentnode + modifiedtree = False + return modifiedtree + + for r in rules: + sc.addRule( r, mycb) + #sc.check(anytree) + + |