hyperbase 0.8.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,39 @@
1
+ from importlib.metadata import entry_points, EntryPoint
2
+ from typing import Any
3
+
4
+ from hyperbase.parsers.parser import Parser
5
+
6
+
7
+ def list_parsers() -> dict[str, EntryPoint]:
8
+ """Return all installed parser plugins.
9
+
10
+ Each plugin registers via the ``hyperbase.parsers`` entry-point group
11
+ in its ``pyproject.toml``::
12
+
13
+ [project.entry-points."hyperbase.parsers"]
14
+ alphabeta = "hyperparser_alphabeta:ParserAlphaBeta"
15
+ """
16
+ eps = entry_points(group="hyperbase.parsers")
17
+ return {ep.name: ep for ep in eps}
18
+
19
+
20
+ def get_parser(name: str, **kwargs: Any) -> Parser:
21
+ """Instantiate a parser plugin by name.
22
+
23
+ Looks up *name* in the ``hyperbase.parsers`` entry-point group and
24
+ returns an instance of the registered :class:`Parser` subclass.
25
+
26
+ Raises :class:`ValueError` if the parser is not installed.
27
+ """
28
+ parsers = list_parsers()
29
+ if name not in parsers:
30
+ available = ", ".join(sorted(parsers)) or "(none)"
31
+ raise ValueError(
32
+ f"Parser {name!r} is not installed. "
33
+ f"Available parsers: {available}"
34
+ )
35
+ cls = parsers[name].load()
36
+ return cls(**kwargs) # type: ignore[no-any-return]
37
+
38
+
39
+ __all__ = ["Parser", "get_parser", "list_parsers"]
@@ -0,0 +1,265 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import Counter
4
+ from typing import Any
5
+
6
+ from hyperbase.hyperedge import Hyperedge
7
+ from hyperbase.parsers.utils import filter_alphanumeric_strings
8
+
9
+
10
+ def check_structural_quality(edge: Hyperedge) -> dict[Hyperedge, list[tuple[str, str, int]]]:
11
+ errors: dict[Hyperedge, list[tuple[str, str, int]]] = {}
12
+
13
+ def _visit(current_edge: Hyperedge) -> None:
14
+ if not current_edge or current_edge.atom:
15
+ return
16
+
17
+ current_errors: list[tuple[str, str, int]] = []
18
+
19
+ # Argrole checks
20
+ try:
21
+ ars = current_edge.argroles()
22
+ ar_counts: Counter[str] = Counter()
23
+ for ar in ars:
24
+ if ar not in 'mspaoixtjrc':
25
+ current_errors.append(('bad-argrole', f"Bad argument role '{ar}'. Should be one of 'mspaoixtjrc'.", 2))
26
+ ar_counts[ar] += 1
27
+
28
+ for role in 'spoiamc':
29
+ if ar_counts[role] > 1:
30
+ current_errors.append((f'duplicate-argrole-{role}', f"Argument role '{role}' should only be used once.", 2))
31
+ except Exception:
32
+ pass
33
+
34
+ # Junction checks
35
+ try:
36
+ if current_edge[0].mt == 'J':
37
+ types = set([child.mt for child in current_edge[1:]])
38
+ if types != {'R'} and types != {'C'} and types != {'R', 'S'}:
39
+ current_errors.append(('bad-junction-types', "Junction arguments should ideally be all of type 'R[S]' or all of type 'C'.", 3))
40
+ except Exception:
41
+ pass
42
+
43
+ if current_errors:
44
+ errors[current_edge] = current_errors
45
+
46
+ for child in current_edge:
47
+ _visit(child)
48
+
49
+ if edge:
50
+ _visit(edge)
51
+ return errors
52
+
53
+
54
+ def badness_check(
55
+ edge: Hyperedge,
56
+ tokens: list[str]
57
+ ) -> dict[Any, list[tuple[str, str, int]]]:
58
+
59
+ raw_errors = edge.check_correctness()
60
+ errors: dict[Any, list[tuple[str, str, int]]] = {}
61
+ for k, v in raw_errors.items():
62
+ errors[k] = [(err_type, err_msg, 0) for err_type, err_msg in v]
63
+
64
+ structural_errors = check_structural_quality(edge)
65
+ for k, v2 in structural_errors.items():
66
+ if k in errors:
67
+ errors[k].extend(v2)
68
+ else:
69
+ errors[k] = v2
70
+
71
+ # Only check token matching if we have a valid edge
72
+ if edge:
73
+ try:
74
+ tokens = filter_alphanumeric_strings(tokens)
75
+ roots = filter_alphanumeric_strings([atom.label() for atom in edge.all_atoms()])
76
+
77
+
78
+ # Track which tokens and roots have been matched
79
+ matched_tokens: set[int] = set()
80
+ matched_roots: set[int] = set()
81
+
82
+ # Count remaining unmatched instances of each root
83
+ def count_unmatched_roots(root_value: str) -> int:
84
+ count = 0
85
+ for root_idx, root in enumerate(roots):
86
+ if root == root_value and root_idx not in matched_roots:
87
+ count += 1
88
+ return count
89
+
90
+ # Go through each token and try to find matching roots
91
+ for token_idx, token in enumerate(tokens):
92
+ if token_idx in matched_tokens:
93
+ continue # Already matched this token
94
+
95
+ # Try exact match first
96
+ unmatched_root_count = count_unmatched_roots(token)
97
+ if unmatched_root_count > 0:
98
+ matched_tokens.add(token_idx)
99
+ # Find an unmatched instance of this root
100
+ for root_idx, root in enumerate(roots):
101
+ if root == token and root_idx not in matched_roots:
102
+ matched_roots.add(root_idx)
103
+ break
104
+
105
+ else:
106
+ # Try to find a root that matches this token exactly (case (a))
107
+ for root_idx, root in enumerate(roots):
108
+ if root_idx in matched_roots:
109
+ continue # Already matched this root
110
+
111
+ if root == token:
112
+ matched_tokens.add(token_idx)
113
+ matched_roots.add(root_idx)
114
+ break
115
+
116
+ # If no exact match, try to find combination of roots that form this token (case (b))
117
+ if token_idx not in matched_tokens:
118
+ # Look for sequence of consecutive roots that concatenate to form the token
119
+ for root_start_idx in range(len(roots)):
120
+ if root_start_idx in matched_roots:
121
+ continue # This root is already matched
122
+
123
+ concatenated = ""
124
+ root_sequence: list[int] = []
125
+
126
+ for root_idx in range(root_start_idx, len(roots)):
127
+ if root_idx in matched_roots:
128
+ # Can't use matched roots in sequence
129
+ break
130
+
131
+ root = roots[root_idx]
132
+ concatenated += root
133
+ root_sequence.append(root_idx)
134
+
135
+ if concatenated == token:
136
+ # Found a matching sequence
137
+ matched_tokens.add(token_idx)
138
+ for idx in root_sequence:
139
+ matched_roots.add(idx)
140
+ break
141
+
142
+ if len(concatenated) >= len(token):
143
+ # Gone too far or exact match found
144
+ break
145
+
146
+ if token_idx in matched_tokens:
147
+ break # Found a match, no need to try other starting positions
148
+
149
+ # If still no match, try case (c): root that matches this token and subsequent tokens
150
+ if token_idx not in matched_tokens:
151
+ # Look for a root that can match this token plus some following tokens
152
+ for root_idx, root in enumerate(roots):
153
+ if root_idx in matched_roots:
154
+ continue # Already matched
155
+
156
+ concatenated = ""
157
+ token_sequence: list[int] = []
158
+
159
+ for next_token_idx in range(token_idx, len(tokens)):
160
+ if next_token_idx in matched_tokens:
161
+ continue # Already matched
162
+
163
+ concatenated += tokens[next_token_idx]
164
+ token_sequence.append(next_token_idx)
165
+
166
+ if concatenated == root:
167
+ # Found a root that matches multiple tokens
168
+ matched_roots.add(root_idx)
169
+ for idx in token_sequence:
170
+ matched_tokens.add(idx)
171
+ break
172
+
173
+ if len(concatenated) >= len(root):
174
+ break
175
+
176
+ # If still no match, try case (d): multi-token to multi-root concatenation matching
177
+ if token_idx not in matched_tokens:
178
+ # First, try positional matching (existing logic)
179
+ for root_start_idx in range(len(roots)):
180
+ if root_start_idx in matched_roots:
181
+ continue # This root is already matched
182
+
183
+ tokens_concatenated = ""
184
+ roots_concatenated = ""
185
+ token_sequence = []
186
+ root_sequence = []
187
+
188
+ max_tokens = min(len(tokens) - token_idx, len(roots) - root_start_idx)
189
+
190
+ for i in range(max_tokens):
191
+ current_token_idx = token_idx + i
192
+ current_root_idx = root_start_idx + i
193
+
194
+ if current_token_idx in matched_tokens or current_root_idx in matched_roots:
195
+ break # Can't use already matched items
196
+
197
+ tokens_concatenated += tokens[current_token_idx]
198
+ roots_concatenated += roots[current_root_idx]
199
+ token_sequence.append(current_token_idx)
200
+ root_sequence.append(current_root_idx)
201
+
202
+ # Check if concatenations match
203
+ if tokens_concatenated == roots_concatenated and tokens_concatenated:
204
+ # Found a match - mark all as matched
205
+ for idx in token_sequence:
206
+ matched_tokens.add(idx)
207
+ for idx in root_sequence:
208
+ matched_roots.add(idx)
209
+ break
210
+
211
+ # Stop if we've gone too far (tokens longer than reasonable)
212
+ if len(tokens_concatenated) > 10 or len(roots_concatenated) > 10:
213
+ break
214
+
215
+ if token_idx in matched_tokens:
216
+ break # Found a match, no need to try other root positions
217
+
218
+ # If still no match, try non-positional contraction matching (new logic)
219
+ if token_idx not in matched_tokens:
220
+ # Look for contractions by trying to combine this token with the next one
221
+ # and matching against any two available roots in the roots list (not necessarily consecutive)
222
+ if token_idx + 1 < len(tokens) and token_idx + 1 not in matched_tokens:
223
+ token_concat = tokens[token_idx] + tokens[token_idx + 1]
224
+
225
+ # Try to find any two available roots (not necessarily consecutive) that concatenate to the same value
226
+ for root_idx1 in range(len(roots)):
227
+ if root_idx1 in matched_roots:
228
+ continue # Can't use already matched roots
229
+
230
+ for root_idx2 in range(len(roots)):
231
+ if root_idx2 in matched_roots or root_idx2 == root_idx1:
232
+ continue # Can't use already matched roots or same root
233
+
234
+ root_concat = roots[root_idx1] + roots[root_idx2]
235
+
236
+ if token_concat == root_concat:
237
+ # Found a contraction match!
238
+ matched_tokens.add(token_idx)
239
+ matched_tokens.add(token_idx + 1)
240
+ matched_roots.add(root_idx1)
241
+ matched_roots.add(root_idx2)
242
+ break
243
+
244
+ if token_idx in matched_tokens:
245
+ break # Found a match, no need to try other combinations
246
+
247
+ token_matching_errors: list[tuple[str, str, int]] = []
248
+ # Report unmatched roots
249
+ for root_idx, root in enumerate(roots):
250
+ if root_idx not in matched_roots:
251
+ token_matching_errors.append(('root-without-token', f"Atom root '{root}' is used more times than it appears in the original text.", 1))
252
+
253
+ # Report unmatched tokens
254
+ for token_idx, token in enumerate(tokens):
255
+ if token_idx not in matched_tokens:
256
+ token_matching_errors.append(('token-unused', f"Atom root '{token}' is not used, but it appears in the original text.", 1))
257
+
258
+ if len(token_matching_errors) > 0:
259
+ errors['token-matching'] = token_matching_errors
260
+
261
+ except (AttributeError, Exception):
262
+ # If token counting fails (e.g., edge is invalid), skip it
263
+ pass
264
+
265
+ return errors
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Iterator
4
+ from typing import Any
5
+
6
+
7
+ class Parser:
8
+ def sentensize(self, text: str) -> list[str]:
9
+ raise NotImplementedError
10
+
11
+ def parse(self, text: str) -> Iterator[dict[str, Any]]:
12
+ for sentence in self.sentensize(text):
13
+ for parse in self.parse_sentence(sentence):
14
+ yield parse
15
+
16
+ def parse_sentence(self, sentence: str) -> list[dict[str, Any]]:
17
+ raise NotImplementedError
18
+
19
+ def parse_batch(self, sentences: list[str]) -> list[list[dict[str, Any]]]:
20
+ """Parse multiple sentences. Subclasses may override with a
21
+ true batched implementation (e.g. a single CT2 call)."""
22
+ return [self.parse_sentence(sentence) for sentence in sentences]
23
+
24
+ def parse_text(
25
+ self, text: str, batch_size: int = 8, progress: bool = False
26
+ ) -> list[dict[str, Any]]:
27
+ """Sentensize text, then parse all sentences in batches.
28
+
29
+ Returns a flat list of parse results across all sentences.
30
+ """
31
+ sentences = [s for s in self.sentensize(text) if len(s.split()) > 1]
32
+ batch_range = range(0, len(sentences), batch_size)
33
+ if progress:
34
+ from tqdm import tqdm # type: ignore[import-untyped]
35
+ batch_range = tqdm(batch_range, desc="Parsing batches", leave=False)
36
+ results: list[dict[str, Any]] = []
37
+ for i in batch_range:
38
+ batch = sentences[i:i + batch_size]
39
+ for sentence_results in self.parse_batch(batch):
40
+ results.extend(sentence_results)
41
+ return results
@@ -0,0 +1,19 @@
1
+ def filter_alphanumeric_strings(strings: list[str]) -> list[str]:
2
+ """
3
+ Filter a list of strings to include only those containing alphanumeric characters,
4
+ and remove all non-alphanumeric characters from each string.
5
+
6
+ Args:
7
+ strings: List of strings to filter
8
+
9
+ Returns:
10
+ Filtered list containing only lowercased alphanumeric characters
11
+ """
12
+ filtered: list[str] = []
13
+ for s in strings:
14
+ # Remove non-alphanumeric characters and lowercase
15
+ cleaned = ''.join(c.lower() for c in s if c.isalnum())
16
+ # Only include if result is non-empty
17
+ if cleaned:
18
+ filtered.append(cleaned)
19
+ return filtered
@@ -0,0 +1,29 @@
1
+ from hyperbase.patterns.common import common_pattern
2
+ from hyperbase.patterns.entrypoints import match_pattern, edge_matches_pattern
3
+ from hyperbase.patterns.merge import merge_patterns
4
+ from hyperbase.patterns.properties import (is_wildcard, is_pattern, is_full_pattern, is_fun_pattern,
5
+ is_unordered_pattern)
6
+ from hyperbase.patterns.utils import more_general
7
+ from hyperbase.patterns.variables import (all_variables, apply_vars, apply_variables, extract_vars_map, is_variable,
8
+ contains_variable, remove_variables)
9
+
10
+
11
+ __all__ = [
12
+ 'all_variables',
13
+ 'apply_vars',
14
+ 'apply_variables',
15
+ 'common_pattern',
16
+ 'contains_variable',
17
+ 'edge_matches_pattern',
18
+ 'extract_vars_map',
19
+ 'is_full_pattern',
20
+ 'is_fun_pattern',
21
+ 'is_pattern',
22
+ 'is_unordered_pattern',
23
+ 'is_variable',
24
+ 'is_wildcard',
25
+ 'match_pattern',
26
+ 'merge_patterns',
27
+ 'more_general',
28
+ 'remove_variables'
29
+ ]
@@ -0,0 +1,142 @@
1
+ from __future__ import annotations
2
+
3
+ import itertools
4
+ from collections.abc import Iterator, Mapping, Sequence
5
+ from typing import TYPE_CHECKING
6
+
7
+ from hyperbase.hyperedge import Hyperedge, hedge
8
+ from hyperbase.patterns.utils import _defun_pattern_argroles
9
+
10
+ if TYPE_CHECKING:
11
+ from hyperbase.patterns.matcher import Matcher
12
+
13
+
14
+ def _match_by_argroles(
15
+ matcher: Matcher,
16
+ edge: Hyperedge,
17
+ pattern: Hyperedge,
18
+ role_counts: list[tuple[str, int]],
19
+ min_vars: int,
20
+ matched: tuple[Hyperedge, ...] = (),
21
+ curvars: dict[str, Hyperedge] | None = None,
22
+ tok_pos: list[int] | None = None
23
+ ) -> list[dict[str, Hyperedge]]:
24
+ if curvars is None:
25
+ curvars = {}
26
+
27
+ if len(role_counts) == 0:
28
+ return [curvars]
29
+
30
+ argrole, n = role_counts[0]
31
+
32
+ # match connector
33
+ if argrole == 'X':
34
+ eitems = [edge[0]]
35
+ pitems = [pattern[0]]
36
+ # match any argrole
37
+ elif argrole == '*':
38
+ eitems = [e for e in edge if e not in matched]
39
+ pitems = list(pattern[-n:])
40
+ # match specific argrole
41
+ else:
42
+ eitems = edge.edges_with_argrole(argrole)
43
+ pitems = _defun_pattern_argroles(pattern).edges_with_argrole(argrole)
44
+
45
+ if len(eitems) < n:
46
+ if len(curvars) >= min_vars:
47
+ return [curvars]
48
+ else:
49
+ return []
50
+
51
+ result: list[dict[str, Hyperedge]] = []
52
+
53
+ if tok_pos:
54
+ tok_pos_items = [tok_pos[i] for i, subedge in enumerate(edge) if subedge in eitems]
55
+ tok_pos_perms = tuple(itertools.permutations(tok_pos_items, r=n))
56
+
57
+ for perm_n, perm in enumerate(tuple(itertools.permutations(eitems, r=n))):
58
+ if tok_pos:
59
+ tok_pos_perm = tok_pos_perms[perm_n]
60
+ perm_result: list[dict[str, Hyperedge]] = [{}]
61
+ for i, eitem in enumerate(perm):
62
+ pitem = pitems[i]
63
+ tok_pos_item = tok_pos_perm[i] if tok_pos else None
64
+ item_result: list[dict[str, Hyperedge]] = []
65
+ for variables in perm_result:
66
+ item_result += matcher.match(
67
+ eitem,
68
+ pitem,
69
+ {**curvars, **variables},
70
+ tok_pos=tok_pos_item
71
+ )
72
+ perm_result = item_result
73
+ if len(item_result) == 0:
74
+ break
75
+
76
+ for variables in perm_result:
77
+ result += _match_by_argroles(
78
+ matcher,
79
+ edge,
80
+ pattern,
81
+ role_counts[1:],
82
+ min_vars,
83
+ matched + perm,
84
+ {**curvars, **variables},
85
+ tok_pos=tok_pos
86
+ )
87
+
88
+ return result
89
+
90
+
91
+ def edge2rolemap(edge: Hyperedge) -> dict[str, list[Hyperedge]]:
92
+ argroles = edge[0].argroles()
93
+ if argroles[0] == '{':
94
+ argroles = argroles[1:-1]
95
+ args = list(zip(argroles, edge[1:]))
96
+ rolemap: dict[str, list[Hyperedge]] = {}
97
+ for role, subedge in args:
98
+ if role not in rolemap:
99
+ rolemap[role] = []
100
+ rolemap[role].append(subedge)
101
+ return rolemap
102
+
103
+
104
+ def rolemap2edge(pred: Hyperedge, rm: Mapping[str, Sequence[Hyperedge]]) -> Hyperedge:
105
+ roles = list(rm.keys())
106
+ argroles = ''
107
+ subedges: list[Hyperedge] = [pred]
108
+ for role in roles:
109
+ for arg in rm[role]:
110
+ argroles += role
111
+ subedges.append(arg)
112
+ result = hedge(subedges)
113
+ assert result is not None
114
+ return result.replace_argroles(argroles)
115
+
116
+
117
+ def rolemap_pairings(
118
+ rm1: dict[str, list[Hyperedge]],
119
+ rm2: dict[str, list[Hyperedge]]
120
+ ) -> Iterator[tuple[dict[str, tuple[Hyperedge, ...]], dict[str, tuple[Hyperedge, ...]]]]:
121
+ roles = list(set(rm1.keys()) & set(rm2.keys()))
122
+ role_counts: dict[str, int] = {}
123
+ for role in roles:
124
+ role_counts[role] = min(len(rm1[role]), len(rm2[role]))
125
+
126
+ pairings: list[list[tuple[tuple[Hyperedge, ...], tuple[Hyperedge, ...]]]] = []
127
+ for role in roles:
128
+ role_pairings: list[tuple[tuple[Hyperedge, ...], tuple[Hyperedge, ...]]] = []
129
+ n = role_counts[role]
130
+ for args1_combs in itertools.combinations(rm1[role], n):
131
+ for args1 in itertools.permutations(args1_combs):
132
+ for args2 in itertools.combinations(rm2[role], n):
133
+ role_pairings.append((args1, args2))
134
+ pairings.append(role_pairings)
135
+
136
+ for pairing in itertools.product(*pairings):
137
+ rm1_: dict[str, tuple[Hyperedge, ...]] = {}
138
+ rm2_: dict[str, tuple[Hyperedge, ...]] = {}
139
+ for role, role_pairing in zip(roles, pairing):
140
+ rm1_[role] = role_pairing[0]
141
+ rm2_[role] = role_pairing[1]
142
+ yield rm1_, rm2_
@@ -0,0 +1,98 @@
1
+ from hyperbase.hyperedge import Hyperedge
2
+
3
+
4
+ def _matches_atomic_pattern(edge: Hyperedge, atomic_pattern: Hyperedge) -> bool:
5
+ ap_parts = atomic_pattern.parts() # type: ignore[attr-defined]
6
+
7
+ if len(ap_parts) == 0 or len(ap_parts[0]) == 0:
8
+ return False
9
+
10
+ # structural match
11
+ struct_code = ap_parts[0][0]
12
+ if struct_code == '.':
13
+ if edge.not_atom:
14
+ return False
15
+ elif atomic_pattern.parens: # type: ignore[attr-defined]
16
+ if edge.atom:
17
+ return False
18
+ elif struct_code != '*' and not struct_code.isupper():
19
+ if edge.not_atom:
20
+ return False
21
+ if edge.root() != atomic_pattern.root(): # type: ignore[attr-defined]
22
+ return False
23
+
24
+ # role match
25
+ if len(ap_parts) > 1:
26
+ pos = 1
27
+
28
+ # type match
29
+ ap_role = atomic_pattern.role() # type: ignore[attr-defined]
30
+ ap_type = ap_role[0]
31
+ e_type = edge.type()
32
+ n = len(ap_type)
33
+ if len(e_type) < n or e_type[:n] != ap_type:
34
+ return False
35
+
36
+ e_atom = edge.inner_atom()
37
+
38
+ if len(ap_role) > 1:
39
+ e_role = e_atom.role()
40
+ # check if edge role has enough parts to satisfy the wildcard
41
+ # specification
42
+ if len(e_role) < len(ap_role):
43
+ return False
44
+
45
+ # argroles match
46
+ if ap_type[0] in {'B', 'P'}:
47
+ ap_argroles_parts = ap_role[1].split('-')
48
+ if len(ap_argroles_parts) == 1:
49
+ ap_argroles_parts.append('')
50
+ ap_negroles = ap_argroles_parts[1]
51
+
52
+ # fixed order?
53
+ ap_argroles_posopt = ap_argroles_parts[0]
54
+ e_argroles = e_role[1]
55
+ if len(ap_argroles_posopt) > 0 and ap_argroles_posopt[0] == '{':
56
+ ap_argroles_posopt = ap_argroles_posopt[1:-1]
57
+ else:
58
+ ap_argroles_posopt = ap_argroles_posopt.replace(',', '')
59
+ if len(e_argroles) > len(ap_argroles_posopt):
60
+ return False
61
+ else:
62
+ return ap_argroles_posopt.startswith(e_argroles) # type: ignore[no-any-return]
63
+
64
+ ap_argroles_parts = ap_argroles_posopt.split(',')
65
+ ap_posroles = ap_argroles_parts[0]
66
+ ap_argroles = set(ap_posroles) | set(ap_negroles)
67
+ for argrole in ap_argroles:
68
+ min_count = ap_posroles.count(argrole)
69
+ # if there are argrole exclusions
70
+ fixed = ap_negroles.count(argrole) > 0
71
+ count = e_argroles.count(argrole)
72
+ if count < min_count:
73
+ return False
74
+ # deal with exclusions
75
+ if fixed and count > min_count:
76
+ return False
77
+ pos = 2
78
+
79
+ # match rest of role
80
+ while pos < len(ap_role):
81
+ if e_role[pos] != ap_role[pos]:
82
+ return False
83
+ pos += 1
84
+
85
+ # match rest of atom
86
+ if len(ap_parts) > 2:
87
+ e_parts = e_atom.parts()
88
+ # check if edge role has enough parts to satisfy the wildcard
89
+ # specification
90
+ if len(e_parts) < len(ap_parts):
91
+ return False
92
+
93
+ while pos < len(ap_parts):
94
+ if e_parts[pos] != ap_parts[pos]:
95
+ return False
96
+ pos += 1
97
+
98
+ return True