query.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. # Purpose: Query language and manipulation object for DXF entities
  2. # Created: 27.04.13
  3. # Copyright (C) 2013, Manfred Moitzi
  4. # License: MIT License
  5. from typing import TYPE_CHECKING, Iterable, Callable, Hashable, Dict, List, Any, Sequence, Union
  6. import re
  7. import operator
  8. from collections import abc
  9. from ezdxf.queryparser import EntityQueryParser
  10. from ezdxf.groupby import groupby
  11. if TYPE_CHECKING: # import forward references
  12. from ezdxf.eztypes import DXFEntity
  13. class EntityQuery(abc.Sequence):
  14. """
  15. EntityQuery is a result container, which is filled with dxf entities matching the query string.
  16. It is possible to add entities to the container (extend), remove entities from the container and
  17. to filter the container.
  18. Query String
  19. ============
  20. QueryString := EntityQuery ("[" AttribQuery "]")*
  21. The query string is the combination of two queries, first the required entity query and second the
  22. optional attribute query, enclosed in square brackets.
  23. Entity Query
  24. ------------
  25. The entity query is a whitespace separated list of DXF entity names or the special name ``*``.
  26. Where ``*`` means all DXF entities, all other DXF names have to be uppercase.
  27. Attribute Query
  28. ---------------
  29. The attribute query is used to select DXF entities by its DXF attributes. The attribute query is an addition to the
  30. entity query and matches only if the entity already match the entity query.
  31. The attribute query is a boolean expression, supported operators are:
  32. - not: !term is true, if term is false
  33. - and: term & term is true, if both terms are true
  34. - or: term | term is true, if one term is true
  35. - and arbitrary nested round brackets
  36. Attribute selection is a term: "name comparator value", where name is a DXF entity attribute in lowercase,
  37. value is a integer, float or double quoted string, valid comparators are:
  38. - "==" equal "value"
  39. - "!=" not equal "value"
  40. - "<" lower than "value"
  41. - "<=" lower or equal than "value"
  42. - ">" greater than "value"
  43. - ">=" greater or equal than "value"
  44. - "?" match regular expression "value"
  45. - "!?" does not match regular expression "value"
  46. Query Result
  47. ------------
  48. The EntityQuery() class based on the abstract Sequence() class, contains all DXF entities of the source collection,
  49. which matches one name of the entity query AND the whole attribute query.
  50. If a DXF entity does not have or support a required attribute, the corresponding attribute search term is false.
  51. example: 'LINE[text ? ".*"]' is always empty, because the LINE entity has no text attribute.
  52. examples:
  53. 'LINE CIRCLE[layer=="construction"]' => all LINE and CIRCLE entities on layer "construction"
  54. '*[!(layer=="construction" & color<7)]' => all entities except those on layer == "construction" and color < 7
  55. """
  56. def __init__(self, entities: Iterable['DXFEntity'] = None, query: str = '*'):
  57. """
  58. Setup container with entities matching the initial query.
  59. Args:
  60. entities: sequence of wrapped DXF entities (at least GraphicEntity class)
  61. query: query string, see class documentation
  62. """
  63. if entities is None:
  64. self.entities = []
  65. elif query == '*':
  66. self.entities = list(entities)
  67. else:
  68. match = entity_matcher(query)
  69. self.entities = [entity for entity in entities if match(entity)]
  70. def __len__(self) -> int:
  71. """
  72. Count of result entities.
  73. """
  74. return len(self.entities)
  75. def __getitem__(self, item: int) -> 'DXFEntity':
  76. return self.entities.__getitem__(item)
  77. def extend(self, entities: Iterable['DXFEntity'], query: str = '*', unique: bool = True) -> 'EntityQuery':
  78. """
  79. Extent the query container by entities matching a additional query.
  80. """
  81. self.entities.extend(EntityQuery(entities, query))
  82. if unique:
  83. self.entities = list(unique_entities(self.entities))
  84. return self
  85. def remove(self, query: str = '*') -> None:
  86. """
  87. Remove all entities from result container matching this additional query.
  88. """
  89. handles_of_entities_to_remove = frozenset(entity.dxf.handle for entity in self.query(query))
  90. self.entities = [entity for entity in self.entities if entity.dxf.handle not in handles_of_entities_to_remove]
  91. def query(self, query: str = '*') -> 'EntityQuery':
  92. """
  93. Returns a new result container with all entities matching this additional query.
  94. raises: ParseException (pyparsing.py)
  95. """
  96. return EntityQuery(self.entities, query)
  97. def groupby(self, dxfattrib: str = '', key: Callable[['DXFEntity'], Hashable] = None) \
  98. -> Dict[Hashable, List['DXFEntity']]:
  99. """
  100. Returns a dict of entity lists, where entities are grouped by a dxfattrib or a key function.
  101. Args:
  102. dxfattrib: grouping DXF attribute like 'layer'
  103. key: key function, which accepts a DXFEntity as argument, returns grouping key of this entity or None for
  104. ignore this object. Reason for ignoring: a queried DXF attribute is not supported by this entity
  105. Returns: dict
  106. """
  107. return groupby(self.entities, dxfattrib, key)
  108. def entity_matcher(query: str) -> Callable[['DXFEntity'], bool]:
  109. query_args = EntityQueryParser.parseString(query, parseAll=True)
  110. entity_matcher_ = build_entity_name_matcher(query_args.EntityQuery)
  111. attrib_matcher = build_entity_attributes_matcher(query_args.AttribQuery, query_args.AttribQueryOptions)
  112. def matcher(entity: 'DXFEntity') -> bool:
  113. return entity_matcher_(entity) and attrib_matcher(entity)
  114. return matcher
  115. def build_entity_name_matcher(names: Sequence[str]) -> Callable[['DXFEntity'], bool]:
  116. entity_names = frozenset(names)
  117. if names[0] == '*':
  118. return lambda e: True
  119. else:
  120. return lambda e: e.dxftype() in entity_names
  121. class Relation:
  122. CMP_OPERATORS = {
  123. '==': operator.eq,
  124. '!=': operator.ne,
  125. '<': operator.lt,
  126. '<=': operator.le,
  127. '>': operator.gt,
  128. '>=': operator.ge,
  129. '?': lambda e, regex: regex.match(e) is not None,
  130. '!?': lambda e, regex: regex.match(e) is None,
  131. }
  132. VALID_CMP_OPERATORS = frozenset(CMP_OPERATORS.keys())
  133. def __init__(self, relation: Sequence, ignore_case: bool):
  134. name, op, value = relation
  135. self.dxf_attrib = name
  136. self.compare = Relation.CMP_OPERATORS[op]
  137. self.convert_case = to_lower if ignore_case else lambda x: x
  138. re_flags = re.IGNORECASE if ignore_case else 0
  139. if '?' in op:
  140. self.value = re.compile(value + '$', flags=re_flags) # always match whole pattern
  141. else:
  142. self.value = self.convert_case(value)
  143. def evaluate(self, entity: 'DXFEntity') -> bool:
  144. try:
  145. value = self.convert_case(entity.get_dxf_attrib(self.dxf_attrib))
  146. return self.compare(value, self.value)
  147. except AttributeError: # entity does not support this attribute
  148. return False
  149. except ValueError: # entity supports this attribute, but has no value for it
  150. return False
  151. def to_lower(value):
  152. return value.lower() if hasattr(value, 'lower') else value
  153. class BoolExpression:
  154. OPERATORS = {
  155. '&': operator.and_,
  156. '|': operator.or_,
  157. }
  158. def __init__(self, tokens: Sequence):
  159. self.tokens = tokens
  160. def __iter__(self):
  161. return iter(self.tokens)
  162. def evaluate(self, entity: 'DXFEntity') -> bool:
  163. if isinstance(self.tokens, Relation): # expression is just one relation, no bool operations
  164. return self.tokens.evaluate(entity)
  165. values = [] # first in, first out
  166. operators = [] # first in, first out
  167. for token in self.tokens:
  168. if hasattr(token, 'evaluate'):
  169. values.append(token.evaluate(entity))
  170. else: # bool operator
  171. operators.append(token)
  172. values.reverse() # revert values -> pop() == pop(0) & append(value) == insert(0, value)
  173. for op in operators: # as queue -> first in, first out
  174. if op == '!':
  175. value = not values.pop()
  176. else:
  177. value = BoolExpression.OPERATORS[op](values.pop(), values.pop())
  178. values.append(value)
  179. return values.pop()
  180. def _compile_tokens(tokens: Union[str, Sequence], ignore_case: bool) -> Union[str, Relation, BoolExpression]:
  181. def is_relation(tokens: Sequence) -> bool:
  182. return len(tokens) == 3 and tokens[1] in Relation.VALID_CMP_OPERATORS
  183. if isinstance(tokens, str): # bool operator as string
  184. return tokens
  185. tokens = tuple(tokens)
  186. if is_relation(tokens):
  187. return Relation(tokens, ignore_case)
  188. else:
  189. return BoolExpression([_compile_tokens(token, ignore_case) for token in tokens])
  190. def build_entity_attributes_matcher(tokens: Sequence, options: str) -> Callable[['DXFEntity'], bool]:
  191. if not len(tokens):
  192. return lambda x: True
  193. ignore_case = 'i' == options # at this time just one option is supported
  194. expr = BoolExpression(_compile_tokens(tokens, ignore_case))
  195. def match_bool_expr(entity: 'DXFEntity') -> bool:
  196. return expr.evaluate(entity)
  197. return match_bool_expr
  198. def unique_entities(entities: Iterable['DXFEntity']) -> Iterable['DXFEntity']:
  199. """
  200. Yield all unique entities, order of all entities will be preserved.
  201. """
  202. handles = set()
  203. for entity in entities:
  204. handle = entity.dxf.handle
  205. if handle not in handles:
  206. handles.add(handle)
  207. yield entity
  208. def name_query(names: Iterable[str], query: str = "*") -> Iterable[str]:
  209. def build_regexp_matcher() -> Callable[[str], bool]:
  210. if query == "*":
  211. return lambda n: True
  212. else:
  213. # always match until end of string
  214. matcher = re.compile(query + '$')
  215. return lambda n: matcher.match(n) is not None
  216. match = build_regexp_matcher()
  217. return (name for name in names if match(name))
  218. def new(entities=None, query='*'):
  219. return EntityQuery(entities, query)