• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

renatahodovan / grammarinator / 12298256429

12 Dec 2024 02:17PM UTC coverage: 85.518% (-0.5%) from 86.052%
12298256429

Pull #259

github

web-flow
Merge dd42a94be into f5de911fe
Pull Request #259: Don't import importlib_metadata since Py3.8 already has it as importlib.metadata

1 of 1 new or added line in 1 file covered. (100.0%)

14 existing lines in 3 files now uncovered.

1990 of 2327 relevant lines covered (85.52%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

65.1
/grammarinator/runtime/rule.py
1
# Copyright (c) 2017-2024 Renata Hodovan, Akos Kiss.
2
#
3
# Licensed under the BSD 3-Clause License
4
# <LICENSE.rst or https://opensource.org/licenses/BSD-3-Clause>.
5
# This file may not be copied, modified, or distributed except
6
# according to those terms.
7

8
from copy import deepcopy
1✔
9
from math import inf
1✔
10
from textwrap import indent
1✔
11

12

13
class RuleSize:
1✔
14
    """
15
    Size information about a grammar rule or a (sub)tree (generated based on a
16
    grammar rule), expressed using different metrics (both vertical and
17
    horizontal).
18

19
    This class can be used to describe various size-related concepts, e.g.:
20

21
      - The minimum depth of derivation of a grammar rule and the minimum
22
        number of tokens generated by a grammar rule.
23
      - The actual depth of derivation of a grammar rule and the actual
24
        number of tokens generated by a grammar rule.
25
      - The maximum allowed depth of derivation in a tree and the maximum number
26
        of tokens allowed in a tree.
27
      - The depth of derivation reaching a node from the root of the tree and
28
        the number of tokens in the tree outside the subtree of the node.
29

30
    The class supports additive arithmetics (``+``, ``+=``, ``-``, ``-=``) and
31
    comparisons (``==``, ``<=``, ``<``). Note, however, that the type is not
32
    totally but partially ordered, i.e., a size object is less than another size
33
    object iff all its metrics compare less than the corresponding metrics of
34
    the other size object.
35
    """
36

37
    def __init__(self, depth=0, tokens=0):
1✔
38
        """
39
        :param int or float depth: Derivation length (default: 0).
40
        :param int or float tokens: Token count (default: 0).
41

42
        :ivar int or float depth: Derivation length.
43
        :ivar int or float tokens: Token count.
44

45
        :cvar RuleSize max: All size metrics set to ``inf``.
46
        """
47
        self.depth = depth
1✔
48
        self.tokens = tokens
1✔
49

50
    def __add__(self, other):
1✔
51
        return RuleSize(depth=self.depth + other.depth, tokens=self.tokens + other.tokens)
1✔
52

53
    def __iadd__(self, other):
1✔
54
        self.depth += other.depth
×
55
        self.tokens += other.tokens
×
56

57
    def __sub__(self, other):
1✔
58
        return RuleSize(depth=self.depth - other.depth, tokens=self.tokens - other.tokens)
1✔
59

60
    def __isub__(self, other):
1✔
61
        self.depth -= other.depth
×
62
        self.tokens -= other.tokens
×
63

64
    def __eq__(self, other):
1✔
65
        return self.depth == other.depth and self.tokens == other.tokens
×
66

67
    def __le__(self, other):
1✔
68
        # This defines a partial order (i.e., reflexive, antisymmetric, and transitive).
69
        # Not every pair of objects are comparable.
70
        return self.depth <= other.depth and self.tokens <= other.tokens
1✔
71

72
    def __lt__(self, other):
1✔
73
        # This defines a strict partial order (i.e., irreflexive, asymmetric, and transitive).
74
        # Not every pair of objects are comparable.
75
        return self.depth < other.depth and self.tokens < other.tokens
×
76

77
    def __repr__(self):
1✔
78
        return f'{self.__class__.__name__}(depth={self.depth!r}, tokens={self.tokens!r})'
×
79

80

81
RuleSize.max = RuleSize(inf, inf)
1✔
82

83

84
class Rule:
1✔
85
    """
86
    Abstract base class of tree nodes.
87

88
    Tree nodes support deep copying via :func:`copy.deepcopy` (but no shallow
89
    copying via :func:`copy.copy`). A deep copy of a node is the copy of the
90
    whole subtree rooted at that node. However, the parent of a copy node is
91
    always None, even if the original node had a parent.
92

93
    Tree nodes support various string representations:
94

95
    - The "informal" representation of a node consists of the concatenation of
96
      the string contents of all tokens found in the tree rooted at the node in
97
      depth-first order. No extra whitespace is inserted for separation.
98
    - The "official" representation of a node is an (almost) valid Python
99
      expression to recreate the tree rooted at the node. If the node has any
100
      children, the representation consists of multiple lines.
101
    - The "debug" representation of a node is a multi-line string with the most
102
      important attributes of the tree rooted at the node, using vertical bars
103
      for visual guidance.
104

105
    The builtins :class:`str` and :func:`repr` can be used to compute the
106
    "informal" and "official" representations, respectively, while
107
    :func:`format` can produce all of the above. When string formatting is used,
108
    the following format specifiers are recognized:
109

110
    ================= ==========================
111
    Specifier         Meaning
112
    ================= ==========================
113
    ``''`` or ``'s'`` "Informal" representation.
114
    ``'r'``           "Official" representation.
115
    ``'|'``           "Debug" representation.
116
    ================= ==========================
117

118
    Thus, if ``n`` is a node, the following expressions give equivalent results:
119

120
    - ``str(n)``, ``f'{n}'``, ``f'{n!s}'``, ``f'{n:s}'``,
121
      ``'{}'.format(n)``, ``'{!s}'.format(n)``, and ``'{:s}'.format(n)``
122
    - ``repr(n)``, ``f'{n!r}'``, ``f'{n:r}'``, ``'{!r}'.format(n)``, and
123
      ``'{:r}'.format(n)``
124
    - ``f'{n:|}'`` and ``'{:|}'.format(n)``
125
    """
126

127
    def __init__(self, *, name):
1✔
128
        """
129
        :param str name: Name of the node, i.e., name of the corresponding parser or lexer rule in the grammar.
130

131
        :ivar str name: Name of the node, i.e., name of the corresponding parser or lexer rule in the grammar.
132
        :ivar UnparserRule parent: Parent node object.
133
        """
134
        self.name = name
1✔
135
        self.parent = None
1✔
136

137
    @property
1✔
138
    def left_sibling(self):
1✔
139
        """
140
        Get the left sibling of the node if any. Return ``None`` if the node has
141
        no parent or is the leftmost child of its parent.
142

143
        :return: The left sibling of the current node or ``None``.
144
        :rtype: Rule
145
        """
146
        if not self.parent:
×
147
            return None
×
148
        self_idx = self.parent.children.index(self)
×
149
        if self_idx == 0:
×
150
            return None
×
151
        return self.parent.children[self_idx - 1]
×
152

153
    @property
1✔
154
    def right_sibling(self):
1✔
155
        """
156
        Get the right sibling of the node if any. Return ``None`` if the node
157
        has no parent or is the rightmost child of its parent.
158

159
        :return: The right sibling of the current node or ``None``.
160
        :rtype: Rule
161
        """
162
        if not self.parent:
×
163
            return None
×
164
        self_idx = self.parent.children.index(self)
×
165
        if self_idx == len(self.parent.children) - 1:
×
166
            return None
×
167
        return self.parent.children[self_idx + 1]
×
168

169
    @property
1✔
170
    def root(self):
1✔
171
        """
172
        Get the root of the node, i.e., the node at the top of the parent chain.
173

174
        :return: The root of the current node.
175
        :rtype: Rule
176
        """
UNCOV
177
        node = self
×
UNCOV
178
        while node.parent:
×
UNCOV
179
            node = node.parent
×
UNCOV
180
        return node
×
181

182
    def replace(self, node):
1✔
183
        """
184
        Replace the current node with ``node`` among its siblings.
185

186
        :param Rule node: The replacement node.
187
        :return: ``node``
188
        :rtype: Rule
189
        """
190
        node.remove()
1✔
191
        if self.parent and node is not self:
1✔
192
            self.parent.children[self.parent.children.index(self)] = node
1✔
193
            node.parent = self.parent
1✔
194
            self.parent = None
1✔
195
        return node
1✔
196

197
    def remove(self):
1✔
198
        """
199
        Remove the current node from its parent.
200
        """
201
        if self.parent:
1✔
202
            self.parent.children.remove(self)
1✔
203
            self.parent = None
1✔
204

205
    def equals(self, other):
1✔
206
        """
207
        Compare two nodes (potentially including any children) for equality.
208
        The comparison is not implemented within ``__eq__`` to ensure that
209
        nodes can be added to collections based on identity.
210

211
        :param Rule other: The node to compare the current node to.
212
        :return: Whether the two nodes are equal.
213
        :rtype: bool
214
        """
215
        return self.__class__ == other.__class__ and self.name == other.name
1✔
216

217
    def _dbg_(self):
1✔
218
        """
219
        Called by :meth:`__format__` to compute the "debug" string
220
        representation of a node.
221
        """
222
        raise NotImplementedError()
×
223

224
    def __format__(self, format_spec):
1✔
225
        if format_spec in ['', 's']:
×
226
            return str(self)
×
227
        if format_spec == 'r':
×
228
            return repr(self)
×
229
        if format_spec == '|':
×
230
            return self._dbg_()
×
231
        raise TypeError
×
232

233
    def __copy__(self):
1✔
234
        raise TypeError('shallow copy not supported')
×
235

236
    def __deepcopy__(self, memo):
1✔
237
        raise NotImplementedError()
×
238

239

240
class ParentRule(Rule):
1✔
241
    """
242
    Abstract base class of tree nodes that can have children.
243
    """
244

245
    def __init__(self, *, name, children=None):
1✔
246
        """
247
        :param str name: Name of the corresponding parser rule in the grammar.
248
        :param list[Rule] children: Children of the rule (default: no children).
249

250
        :ivar list[Rule] children: Children of the rule.
251
        """
252
        super().__init__(name=name)
1✔
253
        self.children = []
1✔
254
        if children:
1✔
255
            self.add_children(children)
1✔
256

257
    @property
1✔
258
    def last_child(self):
1✔
259
        """
260
        Get the last child of the current node if any. Return ``None`` if the
261
        node has no children.
262
        """
263
        return self.children[-1] if self.children else None
1✔
264

265
    def insert_child(self, idx, node):
1✔
266
        """
267
        Insert node as child at position.
268

269
        :param int idx: Index of position to insert ``node`` at.
270
        :param Rule node: Node to be inserted.
271
        """
272
        if not node:
1✔
273
            return
×
274
        node.remove()
1✔
275
        self.children.insert(idx, node)
1✔
276
        node.parent = self
1✔
277

278
    def add_child(self, node):
1✔
279
        """
280
        Add node to the end of the list of the children.
281

282
        :param Rule node: Node to be added to children.
283
        """
284
        if node is None:
1✔
285
            return
×
286
        node.remove()
1✔
287
        self.children.append(node)
1✔
288
        node.parent = self
1✔
289

290
    def add_children(self, nodes):
1✔
291
        """
292
        Add mulitple nodes to the end of the list of the children.
293

294
        :param list[Rule] nodes: List of nodes to be added to children.
295
        """
296
        for node in nodes:
1✔
297
            self.add_child(node)
1✔
298

299
    def __iadd__(self, item):
1✔
300
        """
301
        Support for ``+=`` operation to add one or more children to the current node. An alias to
302
        :meth:`add_child` or :meth:`add_children` depending on the type of ``child``.
303

304
        :param Rule or list[Rule] item: The node(s) to be added as child.
305
        :return: The current node with extended children.
306
        :rtype: Rule
307
        """
308
        if isinstance(item, list):
1✔
309
            self.add_children(item)
1✔
310
        else:
311
            self.add_child(item)
1✔
312
        return self
1✔
313

314
    def equals(self, other):
1✔
315
        return super().equals(other) and len(self.children) == len(other.children) and all(child.equals(other.children[i]) for i, child in enumerate(self.children))
1✔
316

317
    def __str__(self):
1✔
318
        return ''.join(str(child) for child in self.children)
1✔
319

320
    def __repr__(self):
1✔
321
        parts = [
×
322
            f'name={self.name!r}',
323
        ]
324
        if self.children:
×
325
            parts.append('children=[\n{children}\n]'.format(children=indent(',\n'.join(repr(child) for child in self.children), '  ')))
×
326
        return f'{self.__class__.__name__}({", ".join(parts)})'
×
327

328
    def _dbg_(self):
1✔
329
        return '{name}\n{children}'.format(name=self.name or self.__class__.__name__, children=indent('\n'.join(child._dbg_() for child in self.children), '|  '))
×
330

331

332
class UnparserRule(ParentRule):
1✔
333
    """
334
    Tree node representing a parser rule. It can have zero or more
335
    :class:`UnparserRule`, :class:`UnlexerRule`, :class:`UnparserRuleQuantifier`
336
    or :class:`UnparserRuleAlternative` children.
337
    """
338

339
    def __getattr__(self, item):
1✔
340
        # This check is needed to avoid infinite recursions when loading a tree
341
        # with pickle. In such cases, the loaded instance is prepared by
342
        # creating an empty object with the expected ``__class__`` and by
343
        # restoring the saved attributes (without calling ``__init__``).
344
        # During this operation, the ``__set_state__`` method of the target
345
        # class is tried to be called, if it exists. Otherwise, ``__getattr__``
346
        # throws an ``AttributeError``. However, if the instantiation of this
347
        # error object tries to access any field that is not yet added by
348
        # pickle, then it throws another ``AttributeError``, causing an
349
        # infinite recursion. Filtering for the field names, that are used
350
        # later in this method, eliminates the issue.
351
        if item in ['name', 'children']:
1✔
352
            raise AttributeError()
1✔
353

354
        result = [child for child in self.children if child.name == item]
1✔
355

356
        if not result:
×
357
            raise AttributeError(f'[{self.name}] No child with name {item!r} {[child.name for child in self.children]}.')
×
358

359
        return result[0] if len(result) == 1 else result
×
360

361
    def __deepcopy__(self, memo):
1✔
362
        return UnparserRule(name=deepcopy(self.name, memo), children=[deepcopy(child, memo) for child in self.children])
1✔
363

364

365
class UnlexerRule(Rule):
1✔
366
    """
367
    Tree node representing a lexer rule or token. It has a string constant set in its ``src`` field.
368
    """
369

370
    def __init__(self, *, name=None, src=None, size=None, immutable=False):
1✔
371
        """
372
        :param str name: Name of the corresponding lexer rule in the grammar.
373
        :param str src: String content of the lexer rule (default: "").
374
        :param RuleSize size: Size of the lexer rule (default: (1,1) if ``src``
375
            is not empty, (0,0) otherwise).
376
        :param bool immutable: Boolean to mark literal Unlexer nodes as immutable.
377

378
        :ivar str src: String content of the lexer rule.
379
        :ivar RuleSize size: Size of the lexer rule, aggregated from the
380
            token fragments it is composed of.
381
        """
382
        super().__init__(name=name)
1✔
383
        self.src = src or ''
1✔
384
        self.size = size or (RuleSize(depth=1, tokens=1) if src else RuleSize(depth=0, tokens=0))
1✔
385
        self.immutable = immutable
1✔
386

387
    def equals(self, other):
1✔
388
        return super().equals(other) and self.src == other.src
1✔
389

390
    def __str__(self):
1✔
391
        return self.src
1✔
392

393
    def __repr__(self):
1✔
394
        parts = []
×
395
        if self.name:
×
396
            parts.append(f'name={self.name!r}')
×
397
        if self.src:
×
398
            parts.append(f'src={self.src!r}')
×
399
        if (self.src and self.size != RuleSize(1, 1)) or (not self.src and self.size != RuleSize(0, 0)):
×
400
            parts.append(f'size={self.size!r}')
×
401
        if self.immutable:
×
402
            parts.append(f'immutable={self.immutable}')
×
403
        return f'{self.__class__.__name__}({", ".join(parts)})'
×
404

405
    def _dbg_(self):
1✔
406
        return f'{self.name or ""}{":" if self.name else ""}{self.src!r}'
×
407

408
    def __deepcopy__(self, memo):
1✔
409
        return UnlexerRule(name=deepcopy(self.name, memo), src=deepcopy(self.src, memo), size=deepcopy(self.size, memo), immutable=deepcopy(self.immutable, memo))
1✔
410

411

412
class UnparserRuleQuantifier(ParentRule):
1✔
413
    """
414
    Tree node representing the root of a quantified sub-tree. It can have one
415
    or more :class:`UnparserRuleQuantified` children.
416
    """
417

418
    def __init__(self, *, idx, start, stop, children=None):
1✔
419
        super().__init__(name=None, children=children)
1✔
420
        self.idx = idx
1✔
421
        self.start = start
1✔
422
        self.stop = stop
1✔
423

424
    def equals(self, other):
1✔
425
        return super().equals(other) and self.idx == other.idx and self.start == other.start and self.stop == other.stop
1✔
426

427
    def __repr__(self):
1✔
428
        parts = [
×
429
            f'idx={self.idx}',
430
            f'start={self.start}',
431
            f'stop={self.stop}',
432
        ]
433
        if self.children:
×
434
            parts.append('children=[\n{children}\n]'.format(children=indent(',\n'.join(repr(child) for child in self.children), '  ')))
×
435
        return f'{self.__class__.__name__}({", ".join(parts)})'
×
436

437
    def __deepcopy__(self, memo):
1✔
438
        return UnparserRuleQuantifier(idx=deepcopy(self.idx, memo), start=deepcopy(self.start, memo), stop=deepcopy(self.stop, memo), children=[deepcopy(child, memo) for child in self.children])
×
439

440
    def _dbg_(self):
1✔
441
        return '{name}:[{idx}]\n{children}'.format(idx=self.idx, name=self.__class__.__name__, children=indent('\n'.join(child._dbg_() for child in self.children), '|  '))
×
442

443

444
class UnparserRuleQuantified(ParentRule):
1✔
445
    """
446
    Tree node representing a single instance of quantified sub-tree. It can
447
    have one or more :class:`UnparserRule`, :class:`UnlexerRule`,
448
    :class:`UnparserRuleQuantifier` or :class:`UnparserRuleAlternative`
449
    children.
450
    """
451

452
    def __init__(self, *, children=None):
1✔
453
        super().__init__(name=None, children=children)
1✔
454

455
    def __deepcopy__(self, memo):
1✔
456
        return UnparserRuleQuantified(children=[deepcopy(child, memo) for child in self.children])
×
457

458

459
class UnparserRuleAlternative(ParentRule):
1✔
460
    """
461
    Tree node representing a sub-tree of an alternative. It can
462
    have zero or more :class:`UnparserRule`, :class:`UnlexerRule`,
463
    :class:`UnparserRuleQuantifier` or :class:`UnparserRuleAlternative`
464
    children.
465
    """
466

467
    def __init__(self, *, alt_idx, idx, children=None):
1✔
468
        super().__init__(name=None, children=children)
1✔
469
        self.alt_idx = alt_idx
1✔
470
        self.idx = idx
1✔
471

472
    def equals(self, other):
1✔
473
        return super().equals(other) and self.alt_idx == other.alt_idx and self.idx == other.idx
1✔
474

475
    def __repr__(self):
1✔
476
        parts = [
×
477
            f'alt_idx={self.alt_idx}',
478
            f'idx={self.idx}',
479
        ]
480
        if self.children:
×
481
            parts.append('children=[\n{children}\n]'.format(
×
482
                children=indent(',\n'.join(repr(child) for child in self.children), '  ')))
483
        return f'{self.__class__.__name__}({", ".join(parts)})'
×
484

485
    def __deepcopy__(self, memo):
1✔
486
        return UnparserRuleAlternative(alt_idx=deepcopy(self.alt_idx, memo),
×
487
                                       idx=deepcopy(self.idx, memo),
488
                                       children=[deepcopy(child, memo) for child in self.children])
489

490
    def _dbg_(self):
1✔
491
        return '{name}:[{alt_idx}/{idx}]\n{children}'.format(name=self.__class__.__name__, alt_idx=self.alt_idx, idx=self.idx, children=indent('\n'.join(child._dbg_() for child in self.children), '|  '))
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc