mirror of
https://github.com/unclebob/dryer.git
synced 2026-11-06 18:33:49 +00:00
Bring every CRAP score under 10 except the option parser.
The reader, the normalizer, the TypeScript picker, and the fingerprint printer are each a small function. parse_args stays the option switch.
This commit is contained in:
+46
-26
@@ -178,11 +178,25 @@ def _without_type_args(nodes):
|
|||||||
return rest, type_args
|
return rest, type_args
|
||||||
|
|
||||||
|
|
||||||
|
def _callee_leaf(node):
|
||||||
|
if node.type in _IDENTIFIERS:
|
||||||
|
return [K("symbol"), _text(node)]
|
||||||
|
return _PENDING
|
||||||
|
|
||||||
|
|
||||||
|
def _named_leaf(node, head: bool):
|
||||||
|
if node.type in _IDENTIFIERS:
|
||||||
|
if head:
|
||||||
|
return [K("symbol"), _text(node)]
|
||||||
|
return K("symbol")
|
||||||
|
if _is_literal(node):
|
||||||
|
return K("literal")
|
||||||
|
return _PENDING
|
||||||
|
|
||||||
|
|
||||||
def _try_leaf(node, mode: str, head: bool):
|
def _try_leaf(node, mode: str, head: bool):
|
||||||
if mode == "callee":
|
if mode == "callee":
|
||||||
if node.type in _IDENTIFIERS:
|
return _callee_leaf(node)
|
||||||
return [K("symbol"), _text(node)]
|
|
||||||
return _PENDING
|
|
||||||
if mode == "path":
|
if mode == "path":
|
||||||
return _PENDING
|
return _PENDING
|
||||||
if node.type in _SKIP:
|
if node.type in _SKIP:
|
||||||
@@ -191,13 +205,7 @@ def _try_leaf(node, mode: str, head: bool):
|
|||||||
return [K("symbol"), node.type]
|
return [K("symbol"), node.type]
|
||||||
if not node.is_named:
|
if not node.is_named:
|
||||||
return None
|
return None
|
||||||
if node.type in _IDENTIFIERS:
|
return _named_leaf(node, head)
|
||||||
if head:
|
|
||||||
return [K("symbol"), _text(node)]
|
|
||||||
return K("symbol")
|
|
||||||
if _is_literal(node):
|
|
||||||
return K("literal")
|
|
||||||
return _PENDING
|
|
||||||
|
|
||||||
|
|
||||||
def _call_jobs(node):
|
def _call_jobs(node):
|
||||||
@@ -241,13 +249,17 @@ def _path_jobs(node):
|
|||||||
return jobs
|
return jobs
|
||||||
|
|
||||||
|
|
||||||
|
def _callee_jobs(node):
|
||||||
|
if node.type in _ATTRIBUTES:
|
||||||
|
return _attr_jobs(node, True)
|
||||||
|
if node.type in _SCOPED or node.type == "generic_type":
|
||||||
|
return _path_jobs(node)
|
||||||
|
return [(node, "norm", True)]
|
||||||
|
|
||||||
|
|
||||||
def _jobs(node, mode: str, head: bool):
|
def _jobs(node, mode: str, head: bool):
|
||||||
if mode == "callee":
|
if mode == "callee":
|
||||||
if node.type in _ATTRIBUTES:
|
return _callee_jobs(node)
|
||||||
return _attr_jobs(node, True)
|
|
||||||
if node.type in _SCOPED or node.type == "generic_type":
|
|
||||||
return _path_jobs(node)
|
|
||||||
return [(node, "norm", True)]
|
|
||||||
if mode == "path":
|
if mode == "path":
|
||||||
return _path_jobs(node)
|
return _path_jobs(node)
|
||||||
if node.type == "parenthesized_expression" and len(node.named_children) == 1:
|
if node.type == "parenthesized_expression" and len(node.named_children) == 1:
|
||||||
@@ -325,13 +337,26 @@ def _assemble_path(node, cache):
|
|||||||
return parts
|
return parts
|
||||||
|
|
||||||
|
|
||||||
|
def _callee_value(node, cache):
|
||||||
|
if node.type in _ATTRIBUTES:
|
||||||
|
return _assemble_attr(node, True, cache)
|
||||||
|
if node.type in _SCOPED or node.type == "generic_type":
|
||||||
|
return _assemble_path(node, cache)
|
||||||
|
return cache[(id(node), "norm", True)]
|
||||||
|
|
||||||
|
|
||||||
|
def _assemble_children(node, cache):
|
||||||
|
children = []
|
||||||
|
for child in node.children:
|
||||||
|
norm = cache[(id(child), "norm", False)]
|
||||||
|
if norm is not None:
|
||||||
|
children.append(norm)
|
||||||
|
return [K(node.type), *children]
|
||||||
|
|
||||||
|
|
||||||
def _assemble(node, mode: str, head: bool, cache):
|
def _assemble(node, mode: str, head: bool, cache):
|
||||||
if mode == "callee":
|
if mode == "callee":
|
||||||
if node.type in _ATTRIBUTES:
|
return _callee_value(node, cache)
|
||||||
return _assemble_attr(node, True, cache)
|
|
||||||
if node.type in _SCOPED or node.type == "generic_type":
|
|
||||||
return _assemble_path(node, cache)
|
|
||||||
return cache[(id(node), "norm", True)]
|
|
||||||
if mode == "path":
|
if mode == "path":
|
||||||
return _assemble_path(node, cache)
|
return _assemble_path(node, cache)
|
||||||
if node.type == "parenthesized_expression" and len(node.named_children) == 1:
|
if node.type == "parenthesized_expression" and len(node.named_children) == 1:
|
||||||
@@ -341,12 +366,7 @@ def _assemble(node, mode: str, head: bool, cache):
|
|||||||
return _assemble_call(node, cache)
|
return _assemble_call(node, cache)
|
||||||
if node.type in _ATTRIBUTES:
|
if node.type in _ATTRIBUTES:
|
||||||
return _assemble_attr(node, head, cache)
|
return _assemble_attr(node, head, cache)
|
||||||
children = []
|
return _assemble_children(node, cache)
|
||||||
for child in node.children:
|
|
||||||
norm = cache[(id(child), "norm", False)]
|
|
||||||
if norm is not None:
|
|
||||||
children.append(norm)
|
|
||||||
return [K(node.type), *children]
|
|
||||||
|
|
||||||
|
|
||||||
def _normalize(node, head: bool = False):
|
def _normalize(node, head: bool = False):
|
||||||
|
|||||||
+121
-87
@@ -17,6 +17,9 @@ from dryer.shape import K
|
|||||||
|
|
||||||
_WHITESPACE = set(" \t\n\r,")
|
_WHITESPACE = set(" \t\n\r,")
|
||||||
_TERMINATORS = set("()[]{}\";'@^`~\\,#:")
|
_TERMINATORS = set("()[]{}\";'@^`~\\,#:")
|
||||||
|
_COLLECTIONS = {"(": (")", "list"), "[": ("]", "vector"), "{": ("}", "map")}
|
||||||
|
_WRAPPERS = {"'": "quote", "@": "deref", "`": "syntax-quote"}
|
||||||
|
_ATOMS = set("\"\\:")
|
||||||
_NUMBER = re.compile(
|
_NUMBER = re.compile(
|
||||||
r"^[+-]?(?:"
|
r"^[+-]?(?:"
|
||||||
r"\d+/\d+"
|
r"\d+/\d+"
|
||||||
@@ -190,25 +193,31 @@ class Reader:
|
|||||||
raise ReadError(self.line, "unexpected end of file")
|
raise ReadError(self.line, "unexpected end of file")
|
||||||
return self._open(stack)
|
return self._open(stack)
|
||||||
|
|
||||||
|
def _deliver_coll(self, frame, produced):
|
||||||
|
items = frame[4]
|
||||||
|
if produced is None:
|
||||||
|
return _NEED
|
||||||
|
if isinstance(produced, Splice):
|
||||||
|
items.extend(produced.items)
|
||||||
|
else:
|
||||||
|
items.append(produced)
|
||||||
|
return _NEED
|
||||||
|
|
||||||
|
def _deliver_wrap(self, frame, produced):
|
||||||
|
_tag, name, line = frame
|
||||||
|
items = [Sym(name, line)]
|
||||||
|
if produced is not None and not isinstance(produced, Splice):
|
||||||
|
items.append(produced)
|
||||||
|
return Coll("list", line, items=items)
|
||||||
|
|
||||||
def _deliver(self, stack: list[tuple], produced):
|
def _deliver(self, stack: list[tuple], produced):
|
||||||
frame = stack[-1]
|
frame = stack[-1]
|
||||||
tag = frame[0]
|
tag = frame[0]
|
||||||
if tag == "coll":
|
if tag == "coll":
|
||||||
items = frame[4]
|
return self._deliver_coll(frame, produced)
|
||||||
if produced is None:
|
|
||||||
return _NEED
|
|
||||||
if isinstance(produced, Splice):
|
|
||||||
items.extend(produced.items)
|
|
||||||
else:
|
|
||||||
items.append(produced)
|
|
||||||
return _NEED
|
|
||||||
stack.pop()
|
stack.pop()
|
||||||
if tag == "wrap":
|
if tag == "wrap":
|
||||||
_tag, name, line = frame
|
return self._deliver_wrap(frame, produced)
|
||||||
items = [Sym(name, line)]
|
|
||||||
if produced is not None and not isinstance(produced, Splice):
|
|
||||||
items.append(produced)
|
|
||||||
return Coll("list", line, items=items)
|
|
||||||
if tag == "discard":
|
if tag == "discard":
|
||||||
return None
|
return None
|
||||||
if tag == "meta":
|
if tag == "meta":
|
||||||
@@ -232,97 +241,122 @@ class Reader:
|
|||||||
raise ReadError(self.line, "splicing reader conditional needs a collection")
|
raise ReadError(self.line, "splicing reader conditional needs a collection")
|
||||||
return spliced
|
return spliced
|
||||||
|
|
||||||
def _open(self, stack: list[tuple]):
|
def _open_collection(self, stack: list[tuple], ch: str, line: int):
|
||||||
ch = self.peek()
|
self.get()
|
||||||
line = self.line
|
end, kind = _COLLECTIONS[ch]
|
||||||
if ch == "(":
|
stack.append(("coll", end, kind, line, [], False))
|
||||||
self.get()
|
return _NEED
|
||||||
stack.append(("coll", ")", "list", line, [], False))
|
|
||||||
return _NEED
|
def _open_atom(self, ch: str):
|
||||||
if ch == "[":
|
|
||||||
self.get()
|
|
||||||
stack.append(("coll", "]", "vector", line, [], False))
|
|
||||||
return _NEED
|
|
||||||
if ch == "{":
|
|
||||||
self.get()
|
|
||||||
stack.append(("coll", "}", "map", line, [], False))
|
|
||||||
return _NEED
|
|
||||||
if ch == '"':
|
if ch == '"':
|
||||||
return self.read_string()
|
return self.read_string()
|
||||||
if ch == "\\":
|
if ch == "\\":
|
||||||
return self.read_char()
|
return self.read_char()
|
||||||
if ch == ":":
|
return self.read_keyword()
|
||||||
return self.read_keyword()
|
|
||||||
if ch == "'":
|
def _open_unquote(self, stack: list[tuple], line: int):
|
||||||
|
name = "unquote"
|
||||||
|
if self.peek() == "@":
|
||||||
self.get()
|
self.get()
|
||||||
stack.append(("wrap", "quote", line))
|
name = "unquote-splicing"
|
||||||
return _NEED
|
stack.append(("wrap", name, line))
|
||||||
if ch == "@":
|
return _NEED
|
||||||
self.get()
|
|
||||||
stack.append(("wrap", "deref", line))
|
def _open_wrapper(self, stack: list[tuple], ch: str, line: int):
|
||||||
return _NEED
|
self.get()
|
||||||
if ch == "`":
|
if ch in _WRAPPERS:
|
||||||
self.get()
|
stack.append(("wrap", _WRAPPERS[ch], line))
|
||||||
stack.append(("wrap", "syntax-quote", line))
|
|
||||||
return _NEED
|
return _NEED
|
||||||
if ch == "~":
|
if ch == "~":
|
||||||
self.get()
|
return self._open_unquote(stack, line)
|
||||||
name = "unquote"
|
stack.append(("meta",))
|
||||||
if self.peek() == "@":
|
return _NEED
|
||||||
self.get()
|
|
||||||
name = "unquote-splicing"
|
def _open_token(self, line: int):
|
||||||
stack.append(("wrap", name, line))
|
|
||||||
return _NEED
|
|
||||||
if ch == "^":
|
|
||||||
self.get()
|
|
||||||
stack.append(("meta",))
|
|
||||||
return _NEED
|
|
||||||
if ch == "#":
|
|
||||||
return self._dispatch(stack, line)
|
|
||||||
token = self.read_token()
|
token = self.read_token()
|
||||||
if _NUMBER.match(token):
|
if _NUMBER.match(token):
|
||||||
return Lit(line)
|
return Lit(line)
|
||||||
return Sym(token, line)
|
return Sym(token, line)
|
||||||
|
|
||||||
|
def _open(self, stack: list[tuple]):
|
||||||
|
ch = self.peek()
|
||||||
|
line = self.line
|
||||||
|
if ch in _COLLECTIONS:
|
||||||
|
return self._open_collection(stack, ch, line)
|
||||||
|
if ch in _ATOMS:
|
||||||
|
return self._open_atom(ch)
|
||||||
|
if ch in _WRAPPERS or ch in "~^":
|
||||||
|
return self._open_wrapper(stack, ch, line)
|
||||||
|
if ch == "#":
|
||||||
|
return self._dispatch(stack, line)
|
||||||
|
return self._open_token(line)
|
||||||
|
|
||||||
|
def _dispatch_discard(self, stack: list[tuple], _line: int):
|
||||||
|
self.get()
|
||||||
|
stack.append(("discard",))
|
||||||
|
return _NEED
|
||||||
|
|
||||||
|
def _dispatch_set(self, stack: list[tuple], _line: int):
|
||||||
|
brace = self.line
|
||||||
|
self.get()
|
||||||
|
stack.append(("coll", "}", "set", brace, [], False))
|
||||||
|
return _NEED
|
||||||
|
|
||||||
|
def _dispatch_fn(self, stack: list[tuple], line: int):
|
||||||
|
self.get()
|
||||||
|
stack.append(("coll", ")", "list", line, [], True))
|
||||||
|
return _NEED
|
||||||
|
|
||||||
|
def _dispatch_string(self, _stack: list[tuple], line: int):
|
||||||
|
self.read_string()
|
||||||
|
return Lit(line)
|
||||||
|
|
||||||
|
def _dispatch_cond(self, stack: list[tuple], _line: int):
|
||||||
|
self.get()
|
||||||
|
splicing = self.peek() == "@"
|
||||||
|
if splicing:
|
||||||
|
self.get()
|
||||||
|
stack.append(("cond", splicing))
|
||||||
|
return _NEED
|
||||||
|
|
||||||
|
def _read_map_qualifier(self) -> None:
|
||||||
|
peeked = self.peek()
|
||||||
|
if peeked == ":":
|
||||||
|
self.get()
|
||||||
|
return
|
||||||
|
if peeked is None or peeked == "{" or peeked in _WHITESPACE:
|
||||||
|
return
|
||||||
|
self.read_token()
|
||||||
|
|
||||||
|
def _dispatch_map(self, _stack: list[tuple], _line: int):
|
||||||
|
self.get()
|
||||||
|
self._read_map_qualifier()
|
||||||
|
return _NEED
|
||||||
|
|
||||||
|
def _dispatch_extension(self, stack: list[tuple], line: int):
|
||||||
|
self.read_token()
|
||||||
|
stack.append(("lit", line))
|
||||||
|
return _NEED
|
||||||
|
|
||||||
def _dispatch(self, stack: list[tuple], line: int):
|
def _dispatch(self, stack: list[tuple], line: int):
|
||||||
self.get()
|
self.get()
|
||||||
ch = self.peek()
|
ch = self.peek()
|
||||||
if ch is None:
|
if ch is None:
|
||||||
raise ReadError(line, "incomplete dispatch")
|
raise ReadError(line, "incomplete dispatch")
|
||||||
if ch == "_":
|
handler = _DISPATCH.get(ch)
|
||||||
self.get()
|
if handler is None:
|
||||||
stack.append(("discard",))
|
return self._dispatch_extension(stack, line)
|
||||||
return _NEED
|
return handler(self, stack, line)
|
||||||
if ch == "{":
|
|
||||||
brace = self.line
|
|
||||||
self.get()
|
_DISPATCH = {
|
||||||
stack.append(("coll", "}", "set", brace, [], False))
|
"_": Reader._dispatch_discard,
|
||||||
return _NEED
|
"{": Reader._dispatch_set,
|
||||||
if ch == "(":
|
"(": Reader._dispatch_fn,
|
||||||
self.get()
|
'"': Reader._dispatch_string,
|
||||||
stack.append(("coll", ")", "list", line, [], True))
|
"?": Reader._dispatch_cond,
|
||||||
return _NEED
|
":": Reader._dispatch_map,
|
||||||
if ch == '"':
|
}
|
||||||
self.read_string()
|
|
||||||
return Lit(line)
|
|
||||||
if ch == "?":
|
|
||||||
self.get()
|
|
||||||
splicing = False
|
|
||||||
if self.peek() == "@":
|
|
||||||
self.get()
|
|
||||||
splicing = True
|
|
||||||
stack.append(("cond", splicing))
|
|
||||||
return _NEED
|
|
||||||
if ch == ":":
|
|
||||||
self.get()
|
|
||||||
if self.peek() == ":":
|
|
||||||
self.get()
|
|
||||||
elif self.peek() not in (None, "{") and self.peek() not in _WHITESPACE:
|
|
||||||
self.read_token()
|
|
||||||
return _NEED
|
|
||||||
self.read_token()
|
|
||||||
stack.append(("lit", line))
|
|
||||||
return _NEED
|
|
||||||
|
|
||||||
|
|
||||||
def _branch_name(feature):
|
def _branch_name(feature):
|
||||||
|
|||||||
+18
-11
@@ -104,22 +104,29 @@ def _python_nodes(data: bytes, root) -> list:
|
|||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def _keep_typescript_function(node) -> bool:
|
||||||
|
return _has(node, "statement_block") and not _inside(node, _TS_FUNCTION)
|
||||||
|
|
||||||
|
|
||||||
|
def _typescript_parent(node):
|
||||||
|
parent = node.parent
|
||||||
|
if parent is None or parent.type not in _DEFINITION_PARENTS:
|
||||||
|
return None
|
||||||
|
if _inside(parent, _TS_FUNCTION):
|
||||||
|
return None
|
||||||
|
return parent
|
||||||
|
|
||||||
|
|
||||||
def _typescript_nodes(data: bytes, root) -> list:
|
def _typescript_nodes(data: bytes, root) -> list:
|
||||||
found = []
|
found = []
|
||||||
for node in descendants(root):
|
for node in descendants(root):
|
||||||
if node.type == "function_declaration":
|
if node.type in {"function_declaration", "method_definition"}:
|
||||||
if _has(node, "statement_block") and not _inside(node, _TS_FUNCTION):
|
if _keep_typescript_function(node):
|
||||||
found.append(node)
|
|
||||||
elif node.type == "method_definition":
|
|
||||||
if _has(node, "statement_block") and not _inside(node, _TS_FUNCTION):
|
|
||||||
found.append(node)
|
found.append(node)
|
||||||
elif node.type in {"arrow_function", "function_expression"}:
|
elif node.type in {"arrow_function", "function_expression"}:
|
||||||
parent = node.parent
|
parent = _typescript_parent(node)
|
||||||
if parent is None or parent.type not in _DEFINITION_PARENTS:
|
if parent is not None:
|
||||||
continue
|
found.append(parent)
|
||||||
if _inside(parent, _TS_FUNCTION):
|
|
||||||
continue
|
|
||||||
found.append(parent)
|
|
||||||
return found
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+35
-39
@@ -27,11 +27,23 @@ def _pr_atom(node) -> str:
|
|||||||
raise TypeError(f"cannot print {node!r}")
|
raise TypeError(f"cannot print {node!r}")
|
||||||
|
|
||||||
|
|
||||||
def pr(node) -> str:
|
def _print_list(current: list, done: dict[int, str]) -> str:
|
||||||
"""Print a normalized node the way Clojure `pr-str` prints it."""
|
pieces = []
|
||||||
|
for child in current:
|
||||||
|
if isinstance(child, list):
|
||||||
|
pieces.append(done[id(child)])
|
||||||
|
else:
|
||||||
|
pieces.append(_pr_atom(child))
|
||||||
|
return "[" + " ".join(pieces) + "]"
|
||||||
|
|
||||||
if not isinstance(node, list):
|
|
||||||
return _pr_atom(node)
|
def _queue_lists(stack: list, current: list) -> None:
|
||||||
|
for child in reversed(current):
|
||||||
|
if isinstance(child, list):
|
||||||
|
stack.append((child, False))
|
||||||
|
|
||||||
|
|
||||||
|
def _printed(node: list) -> dict[int, str]:
|
||||||
# Post-order. A 1,200-deep tree must not raise RecursionError.
|
# Post-order. A 1,200-deep tree must not raise RecursionError.
|
||||||
done: dict[int, str] = {}
|
done: dict[int, str] = {}
|
||||||
stack: list[tuple[list, bool]] = [(node, False)]
|
stack: list[tuple[list, bool]] = [(node, False)]
|
||||||
@@ -39,21 +51,21 @@ def pr(node) -> str:
|
|||||||
current, expanded = stack.pop()
|
current, expanded = stack.pop()
|
||||||
key = id(current)
|
key = id(current)
|
||||||
if expanded:
|
if expanded:
|
||||||
pieces = []
|
done[key] = _print_list(current, done)
|
||||||
for child in current:
|
|
||||||
if isinstance(child, list):
|
|
||||||
pieces.append(done[id(child)])
|
|
||||||
else:
|
|
||||||
pieces.append(_pr_atom(child))
|
|
||||||
done[key] = "[" + " ".join(pieces) + "]"
|
|
||||||
continue
|
continue
|
||||||
if key in done:
|
if key in done:
|
||||||
continue
|
continue
|
||||||
stack.append((current, True))
|
stack.append((current, True))
|
||||||
for child in reversed(current):
|
_queue_lists(stack, current)
|
||||||
if isinstance(child, list):
|
return done
|
||||||
stack.append((child, False))
|
|
||||||
return done[id(node)]
|
|
||||||
|
def pr(node) -> str:
|
||||||
|
"""Print a normalized node the way Clojure `pr-str` prints it."""
|
||||||
|
|
||||||
|
if not isinstance(node, list):
|
||||||
|
return _pr_atom(node)
|
||||||
|
return _printed(node)[id(node)]
|
||||||
|
|
||||||
|
|
||||||
def node_count(node) -> int:
|
def node_count(node) -> int:
|
||||||
@@ -71,29 +83,7 @@ def node_count(node) -> int:
|
|||||||
return total
|
return total
|
||||||
|
|
||||||
|
|
||||||
def fingerprints(node) -> frozenset[str]:
|
def _atom_prints(node) -> set[str]:
|
||||||
if not isinstance(node, list):
|
|
||||||
return frozenset({pr(node)})
|
|
||||||
done: dict[int, str] = {}
|
|
||||||
stack: list[tuple[list, bool]] = [(node, False)]
|
|
||||||
while stack:
|
|
||||||
current, expanded = stack.pop()
|
|
||||||
key = id(current)
|
|
||||||
if expanded:
|
|
||||||
pieces = []
|
|
||||||
for child in current:
|
|
||||||
if isinstance(child, list):
|
|
||||||
pieces.append(done[id(child)])
|
|
||||||
else:
|
|
||||||
pieces.append(_pr_atom(child))
|
|
||||||
done[key] = "[" + " ".join(pieces) + "]"
|
|
||||||
continue
|
|
||||||
if key in done:
|
|
||||||
continue
|
|
||||||
stack.append((current, True))
|
|
||||||
for child in reversed(current):
|
|
||||||
if isinstance(child, list):
|
|
||||||
stack.append((child, False))
|
|
||||||
atoms: set[str] = set()
|
atoms: set[str] = set()
|
||||||
pending: list = [node]
|
pending: list = [node]
|
||||||
while pending:
|
while pending:
|
||||||
@@ -102,7 +92,13 @@ def fingerprints(node) -> frozenset[str]:
|
|||||||
pending.extend(current)
|
pending.extend(current)
|
||||||
else:
|
else:
|
||||||
atoms.add(_pr_atom(current))
|
atoms.add(_pr_atom(current))
|
||||||
return frozenset(set(done.values()) | atoms)
|
return atoms
|
||||||
|
|
||||||
|
|
||||||
|
def fingerprints(node) -> frozenset[str]:
|
||||||
|
if not isinstance(node, list):
|
||||||
|
return frozenset({pr(node)})
|
||||||
|
return frozenset(set(_printed(node).values()) | _atom_prints(node))
|
||||||
|
|
||||||
|
|
||||||
def jaccard(left: frozenset[str], right: frozenset[str]) -> float:
|
def jaccard(left: frozenset[str], right: frozenset[str]) -> float:
|
||||||
|
|||||||
Reference in New Issue
Block a user