Change logical ops names, add tests, general code cleanup

Changed the op names to

and
or
not
exc(ept)

Added more test-cases
Cleanup up the Selectors code

Code should be ready for merging now
This commit is contained in:
adam-urbanczyk
2016-07-10 18:51:45 +02:00
parent f5a91a6980
commit a42cad1c3e
2 changed files with 99 additions and 107 deletions

View File

@ -289,10 +289,6 @@ class DirectionMinMaxSelector(Selector):
CQ(aCube).faces( ">Z" )
Future Enhancements:
provide a nicer way to select in arbitrary directions. IE, a bit more code could
allow '>(0,0,1)' to work.
"""
def __init__(self, vector, directionMax=True, tolerance=0.0001):
self.vector = vector
@ -424,7 +420,7 @@ class InverseSelector(Selector):
def _makeGrammar():
"""
Define the string selector grammar using PyParsing
Define the simple string selector grammar using PyParsing
"""
#float definition
@ -479,42 +475,12 @@ _grammar = _makeGrammar() #make a grammar instance
class _SimpleStringSyntaxSelector(Selector):
"""
Filter lists objects using a simple string syntax. All of the filters available in the string syntax
are also available ( usually with more functionality ) through the creation of full-fledged
selector objects. see :py:class:`Selector` and its subclasses
Filtering works differently depending on the type of object list being filtered.
:param selectorString: A two-part selector string, [selector][axis]
:return: objects that match the specified selector
***Modfiers*** are ``('|','+','-','<','>','%')``
:\|:
parallel to ( same as :py:class:`ParallelDirSelector` ). Can return multiple objects.
:#:
perpendicular to (same as :py:class:`PerpendicularDirSelector` )
:+:
positive direction (same as :py:class:`DirectionSelector` )
:-:
negative direction (same as :py:class:`DirectionSelector` )
:>:
maximize (same as :py:class:`DirectionMinMaxSelector` with directionMax=True)
:<:
minimize (same as :py:class:`DirectionMinMaxSelector` with directionMax=False )
:%:
curve/surface type (same as :py:class:`TypeSelector`)
***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ``
Selectors are a complex topic: see :ref:`selector_reference` for more information
This is a private class that converts a parseResults object into a simple
selector object
"""
def __init__(self,selectorString):
def __init__(self,parseResults):
#define all token to object mappings
self.axes = {
'X': Vector(1,0,0),
'Y': Vector(0,1,0),
@ -546,11 +512,8 @@ class _SimpleStringSyntaxSelector(Selector):
'#' : PerpendicularDirSelector,
'|' : ParallelDirSelector}
self.selectorString = selectorString
#this class is going to be refactored - it will accept ParseResults objects as input i.s.o. strings
parsing_result = selectorString#_grammar.parseString(selectorString,parseAll=True)
self.mySelector = self._chooseSelector(parsing_result)
self.parseResults = parseResults
self.mySelector = self._chooseSelector(parseResults)
def _chooseSelector(self,pr):
"""
@ -597,63 +560,45 @@ class _SimpleStringSyntaxSelector(Selector):
"""
return self.mySelector.filter(objectList)
def _makeSelector(expr):
"""
"""
return _SimpleStringSyntaxSelector(expr)
def _makeInverseSelector(expr):
"""
"""
if 'NOT' in expr:
return InverseSelector(expr[1])
else:
return expr
def _makeExpressionGrammar(atom):
"""
Define the complex string selector grammar using PyParsing (which supports
logical operations and nesting)
"""
and_op = Literal('&').setResultsName('AND')
or_op = Literal('|').setResultsName('OR')
delta_op = Literal('^').setResultsName('DELTA')
not_op = Literal('~').setResultsName('NOT')
binary_op = and_op | or_op | delta_op
#define operators
and_op = Literal('and')
or_op = Literal('or')
delta_op = Literal('exc') | Literal('except')
not_op = Literal('not')
atom.setParseAction(_makeSelector)
atom.setResultsName('atom')
def atom_callback(res):
return _SimpleStringSyntaxSelector(res)
atom_ = atom | not_op + atom
atom_.setParseAction(_makeInverseSelector)
atom.setParseAction(atom_callback) #construct a simple selector from every matched
#infix notation
#define callback functions for all operations
def and_callback(res):
items = res.asList()[0][::2]
items = res.asList()[0][::2] #take every secend items, i.e. all operands
return reduce(AndSelector,items)
def or_callback(res):
items = res.asList()[0][::2]
items = res.asList()[0][::2] #take every secend items, i.e. all operands
return reduce(SumSelector,items)
def delta_callback(res):
items = res.asList()[0][::2]
def exc_callback(res):
items = res.asList()[0][::2] #take every secend items, i.e. all operands
return reduce(SubtractSelector,items)
def not_callback(res):
right = res.asList()[0][1]
right = res.asList()[0][1] #take second item, i.e. the operand
return InverseSelector(right)
#this version does not support nesting
#expr = atom_ + ZeroOrMore(binary_op + atom_)
#this version does support nesting: i.e. A & (B | C)
expr = infixNotation(atom_,
#construct the final grammar and set all the callbacks
expr = infixNotation(atom,
[(and_op,2,opAssoc.LEFT,and_callback),
(or_op,2,opAssoc.LEFT,or_callback),
(delta_op,2,opAssoc.LEFT,delta_callback),
(delta_op,2,opAssoc.LEFT,exc_callback),
(not_op,1,opAssoc.RIGHT,not_callback)])
return expr
@ -662,11 +607,57 @@ _expression_grammar = _makeExpressionGrammar(_grammar)
class StringSyntaxSelector(Selector):
"""
Implment StringExpresionSelector here
Filter lists objects using a simple string syntax. All of the filters available in the string syntax
are also available ( usually with more functionality ) through the creation of full-fledged
selector objects. see :py:class:`Selector` and its subclasses
Filtering works differently depending on the type of object list being filtered.
:param selectorString: A two-part selector string, [selector][axis]
:return: objects that match the specified selector
***Modfiers*** are ``('|','+','-','<','>','%')``
:\|:
parallel to ( same as :py:class:`ParallelDirSelector` ). Can return multiple objects.
:#:
perpendicular to (same as :py:class:`PerpendicularDirSelector` )
:+:
positive direction (same as :py:class:`DirectionSelector` )
:-:
negative direction (same as :py:class:`DirectionSelector` )
:>:
maximize (same as :py:class:`DirectionMinMaxSelector` with directionMax=True)
:<:
minimize (same as :py:class:`DirectionMinMaxSelector` with directionMax=False )
:%:
curve/surface type (same as :py:class:`TypeSelector`)
***axisStrings*** are: ``X,Y,Z,XY,YZ,XZ`` or ``(x,y,z)`` which defines an arbitrary direction
It is possible to combine simple selectors together using logical operations.
The following operations are suuported
:and:
Logical AND, e.g. >X and >Y
:or:
Logical OR, e.g. |X or |Y
:not:
Logical NOT, e.g. not #XY
:exc(ept):
Set difference (equivalent to AND NOT): |X exc >Z
Finally, it is also possible to use even more complex expressions with nesting
and arbitrary number of terms, e.g.
(not >X[0] and #XY) or >XY[0]
Selectors are a complex topic: see :ref:`selector_reference` for more information
"""
def __init__(self,selectorString):
"""
Feed the input string through the parser and construct an relevant complex selector object
"""
self.selectorString = selectorString
parse_result = _expression_grammar.parseString(selectorString,
@ -675,14 +666,6 @@ class StringSyntaxSelector(Selector):
def filter(self,objectList):
"""
Filter give object list through th already constructed complex selector object
"""
return self.mySelector.filter(objectList)
if __name__ == '__main__':
#this code is meant for testing - to be removed!
#parse a simple expression
rs = StringSyntaxSelector('~|(1,1,0) & >(0,0,1) | XY & >(1,1,1)[-1]')
#parse a nested expression
rn = StringSyntaxSelector('(~|(1,1,0) & >(0,0,1)) | XY & (Z | X)')

View File

@ -339,6 +339,10 @@ class TestCQSelectors(BaseTest):
el = c.edges(S('|X') & BS((-2,-2,0.1), (2,2,2))).vals()
self.assertEqual(2, len(el))
# test using extended string syntax
v = c.vertices(">X and >Y").vals()
self.assertEqual(2, len(v))
def testSumSelector(self):
c = CQ(makeUnitCube())
@ -356,15 +360,11 @@ class TestCQSelectors(BaseTest):
self.assertEqual(8, len(el))
# test using extended string syntax
fl = c.faces(">Z | <Z").vals()
fl = c.faces(">Z or <Z").vals()
self.assertEqual(2, len(fl))
el = c.edges("|X | |Y").vals()
el = c.edges("|X or |Y").vals()
self.assertEqual(8, len(el))
# test using extended string syntax
v = c.vertices(">X & >Y").vals()
self.assertEqual(2, len(v))
def testSubtractSelector(self):
c = CQ(makeUnitCube())
@ -378,7 +378,7 @@ class TestCQSelectors(BaseTest):
self.assertEqual(3, len(fl))
# test using extended string syntax
fl = c.faces("#Z ^ >X").vals()
fl = c.faces("#Z exc >X").vals()
self.assertEqual(3, len(fl))
def testInverseSelector(self):
@ -398,11 +398,18 @@ class TestCQSelectors(BaseTest):
self.assertEqual(3, len(el))
# test using extended string syntax
fl = c.faces('~>Z').vals()
fl = c.faces('not >Z').vals()
self.assertEqual(5, len(fl))
el = c.faces('>Z').edges('~>X').vals()
el = c.faces('>Z').edges('not >X').vals()
self.assertEqual(3, len(el))
def testComplexStringSelector(self):
c = CQ(makeUnitCube())
v = c.vertices('(>X and >Y) or (<X and <Y)').vals()
self.assertEqual(4, len(v))
def testFaceCount(self):
c = CQ(makeUnitCube())
self.assertEqual( 6, c.faces().size() )
@ -426,7 +433,7 @@ class TestCQSelectors(BaseTest):
Test if reasonable string selector expressions parse without an error
"""
gram = selectors._makeGrammar()
gram = selectors._expression_grammar
expressions = ['+X ',
'-Y',
@ -443,7 +450,9 @@ class TestCQSelectors(BaseTest):
'left',
'right',
'top',
'bottom']
'bottom',
'not |(1,1,0) and >(0,0,1) or XY except >(1,1,1)[-1]',
'(not |(1,1,0) and >(0,0,1)) exc XY and (Z or X)']
for e in expressions: gram.parseString(e)