diff --git a/CHANGES.md b/CHANGES.md index 43d7d7c9912..0bd22c29401 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -120,6 +120,7 @@ splicing the unchanged blocks into each parent's child list in a single pass rather than removing and re-inserting each one, which rescanned and shifted the whole child list on every conversion (#5213) +- Reduce memory requirement for Node and Leaf class (#5222) ### Output diff --git a/src/black/brackets.py b/src/black/brackets.py index 6093eb739da..12611e32a5e 100644 --- a/src/black/brackets.py +++ b/src/black/brackets.py @@ -56,7 +56,7 @@ class BracketMatchError(Exception): """Raised when an opening bracket is unable to be matched to a closing bracket.""" -@dataclass +@dataclass(slots=True, eq=False) class BracketTracker: """Keeps track of brackets on a line.""" @@ -156,7 +156,7 @@ def delimiter_count_with_priority(self, priority: Priority = 0) -> int: return 0 priority = priority or self.max_delimiter_priority() - return sum(1 for p in self.delimiters.values() if p == priority) + return sum(p == priority for p in self.delimiters.values()) def maybe_increment_for_loop_variable(self, leaf: Leaf) -> bool: """In a for loop, or comprehension, the variables are often unpacks. diff --git a/src/black/comments.py b/src/black/comments.py index f5fe98b7ea6..c09be19e3c7 100644 --- a/src/black/comments.py +++ b/src/black/comments.py @@ -2,7 +2,7 @@ from collections.abc import Collection, Iterator from dataclasses import dataclass from functools import lru_cache -from typing import Final, Union +from typing import Final from black.mode import Mode from black.nodes import ( @@ -22,7 +22,7 @@ from blib2to3.pytree import Leaf, Node # types -LN = Union[Leaf, Node] +LN = Leaf | Node FMT_OFF: Final = {"# fmt: off", "# fmt:off", "# yapf: disable"} FMT_SKIP: Final = {"# fmt: skip", "# fmt:skip"} @@ -37,7 +37,7 @@ _COMMENT_LIST_SEPARATOR = ";" -@dataclass +@dataclass(slots=True, eq=False, repr=False) class ProtoComment: """Describes a piece of syntax that is a comment. diff --git a/src/black/linegen.py b/src/black/linegen.py index c9db9d8b4e9..82f810e2255 100644 --- a/src/black/linegen.py +++ b/src/black/linegen.py @@ -1054,13 +1054,12 @@ def _first_right_hand_split( len(str(leaf)) for leaf in hugged_opening_leaves + hugged_closing_leaves ) - if is_line_short_enough( + + # Do not hug if it fits on a single line. + should_hug = not is_line_short_enough( inner_body, mode=replace(line.mode, line_length=line_length) - ): - # Do not hug if it fits on a single line. - should_hug = False - else: - should_hug = True + ) + if should_hug: body_leaves = inner_body_leaves head_leaves.extend(hugged_opening_leaves) diff --git a/src/black/lines.py b/src/black/lines.py index e04f5b1b135..383f8dcd025 100644 --- a/src/black/lines.py +++ b/src/black/lines.py @@ -38,7 +38,7 @@ LN = Union[Leaf, Node] -@dataclass +@dataclass(slots=True, eq=False) class Line: """Holds leaves and comments. Can be printed with `str(line)`.""" @@ -197,11 +197,7 @@ def _is_triple_quoted_string(self) -> bool: if not self or self.leaves[0].type != token.STRING: return False value = self.leaves[0].value - if value.startswith(('"""', "'''")): - return True - if value.startswith(("r'''", 'r"""', "R'''", 'R"""')): - return True - return False + return value.startswith(('"""', "'''", "r'''", 'r"""', "R'''", 'R"""')) @property def is_docstring(self) -> bool: @@ -483,22 +479,21 @@ def __str__(self) -> str: return "\n" indent = " " * self.depth - leaves = iter(self.leaves) - first = next(leaves) - res = f"{first.prefix}{indent}{first.value}" - res += "".join(str(leaf) for leaf in leaves) - comments_iter = itertools.chain.from_iterable(self.comments.values()) - comments = [str(comment) for comment in comments_iter] - res += "".join(comments) + first = self.leaves[0] + + rest_str = "".join(map(str, self.leaves[1:])) + comments_str = "".join( + str(comment) for group in self.comments.values() for comment in group + ) - return res + "\n" + return f"{first.prefix}{indent}{first.value}{rest_str}{comments_str}\n" def __bool__(self) -> bool: """Return True if the line has leaves or comments.""" return bool(self.leaves or self.comments) -@dataclass +@dataclass(slots=True, eq=False, repr=False) class RHSResult: """Intermediate split result from a right hand split.""" @@ -509,7 +504,7 @@ class RHSResult: closing_bracket: Leaf -@dataclass +@dataclass(slots=True, eq=False, repr=False) class LinesBlock: """Class that holds information about a block of formatted lines. @@ -549,7 +544,7 @@ class _DecoratedFuncInfo(NamedTuple): is_multi: bool -@dataclass +@dataclass(slots=True, eq=False, repr=False) class EmptyLineTracker: """Provides a stateful method that returns the number of potential extra empty lines needed before and after the currently processed line. @@ -1610,7 +1605,6 @@ def can_omit_invisible_parens( # a leading opening bracket and a trailing closing bracket. If the # opening bracket doesn't match our rule, maybe the closing will. - penultimate = line.leaves[-2] last = line.leaves[-1] if ( @@ -1624,6 +1618,7 @@ def can_omit_invisible_parens( and last.parent.type != syms.trailer ) ): + penultimate = line.leaves[-2] if penultimate.type in OPENING_BRACKETS: # Empty brackets don't help. return False diff --git a/src/black/output.py b/src/black/output.py index 61d712e626e..62d81461cd9 100644 --- a/src/black/output.py +++ b/src/black/output.py @@ -89,8 +89,7 @@ def diff(a: str, b: str, a_name: str, b_name: str) -> str: if line[-1] == "\n": diff_lines.append(line) else: - diff_lines.append(line + "\n") - diff_lines.append("\\ No newline at end of file\n") + diff_lines.extend((line + "\n", "\\ No newline at end of file\n")) return "".join(diff_lines) diff --git a/src/black/ranges.py b/src/black/ranges.py index 16db00bdfca..d7c7b2cecd1 100644 --- a/src/black/ranges.py +++ b/src/black/ranges.py @@ -525,7 +525,7 @@ def _get_line_range(node_or_nodes: LN | list[LN]) -> set[int]: return set() -@dataclass +@dataclass(slots=True, eq=False, repr=False) class _LinesMapping: """1-based lines mapping from original source to modified source. diff --git a/src/black/trans.py b/src/black/trans.py index 8d1bd276ded..ff92153b7ff 100644 --- a/src/black/trans.py +++ b/src/black/trans.py @@ -360,7 +360,7 @@ def _get_key(string: str) -> CustomSplitMapKey: A unique identifier that is used internally to map @string to a group of custom splits. """ - return (id(string), string) + return id(string), string def add_custom_splits( self, string: str, custom_splits: Iterable[CustomSplit] @@ -422,6 +422,8 @@ class StringMerger(StringTransformer, CustomSplitMapMixin): StringMerger provides custom split information to StringSplitter. """ + __slots__ = () + def do_match(self, line: Line) -> TMatchResult: LL = line.leaves @@ -1016,13 +1018,11 @@ def do_transform( string_parser = StringParser() rpar_idx = string_parser.parse(LL, string_idx) - should_transform = True - for leaf in (LL[string_idx - 1], LL[rpar_idx]): - if line.comments_after(leaf): - # Should not strip parentheses which have comments attached - # to them. - should_transform = False - break + # Should not strip parentheses which have comments attached + # to them. + should_transform = not any( + line.comments_after(leaf) for leaf in (LL[string_idx - 1], LL[rpar_idx]) + ) if should_transform: string_and_rpar_indices.extend((string_idx, rpar_idx)) @@ -1358,7 +1358,7 @@ def iter_fexpr_spans(s: str) -> Iterator[tuple[int, int]]: j = stack.pop() # we've made it back out of the expression! yield the span if not stack: - yield (j, i + 1) + yield j, i + 1 i += 1 continue @@ -1397,11 +1397,13 @@ def _toggle_fexpr_quotes(fstring: str, old_quote: str) -> str: escaping, once Black figures out how to parse the new grammar. """ new_quote = "'" if old_quote == '"' else '"' - parts = [] + parts: list[str] = [] previous_index = 0 for start, end in iter_fexpr_spans(fstring): - parts.append(fstring[previous_index:start]) - parts.append(fstring[start:end].replace(old_quote, new_quote)) + parts.extend(( + fstring[previous_index:start], + fstring[start:end].replace(old_quote, new_quote), + )) previous_index = end parts.append(fstring[previous_index:]) return "".join(parts) @@ -2257,10 +2259,7 @@ def do_transform( insert_str_child = insert_str_child_factory(LL[string_idx]) comma_idx = -1 - ends_with_comma = False - if LL[comma_idx].type == token.COMMA: - ends_with_comma = True - + ends_with_comma = LL[comma_idx].type == token.COMMA leaves_to_steal_comments_from = [LL[string_idx]] if ends_with_comma: leaves_to_steal_comments_from.append(LL[comma_idx]) diff --git a/src/blib2to3/pgen2/driver.py b/src/blib2to3/pgen2/driver.py index 9893c20d971..6847c55c991 100644 --- a/src/blib2to3/pgen2/driver.py +++ b/src/blib2to3/pgen2/driver.py @@ -16,7 +16,6 @@ __all__ = ["Driver", "load_grammar"] # Python imports -import io import logging import os import pkgutil @@ -37,7 +36,7 @@ Path = Union[str, "os.PathLike[str]"] -@dataclass +@dataclass(slots=True, eq=False, repr=False) class ReleaseRange: start: int end: int | None = None @@ -49,6 +48,8 @@ def lock(self) -> None: class TokenProxy: + __slots__ = ("_tokens", "_counter", "_release_ranges", "logger") + def __init__(self, generator: Any) -> None: self._tokens = generator self._counter = 0 diff --git a/src/blib2to3/pgen2/parse.py b/src/blib2to3/pgen2/parse.py index 3ee003a828a..bca34ed3d7d 100644 --- a/src/blib2to3/pgen2/parse.py +++ b/src/blib2to3/pgen2/parse.py @@ -157,6 +157,17 @@ class Parser: """ + __slots__ = ( + "convert", + "grammar", + "is_backtracking", + "last_token", + "proxy", + "rootnode", + "stack", + "used_names", + ) + def __init__(self, grammar: Grammar, convert: Convert | None = None) -> None: """Constructor. @@ -392,4 +403,3 @@ def pop(self) -> None: node[-1].append(newnode) else: self.rootnode = newnode - self.rootnode.used_names = self.used_names diff --git a/src/blib2to3/pgen2/pgen.py b/src/blib2to3/pgen2/pgen.py index 231dd91b951..f8d58da8b92 100644 --- a/src/blib2to3/pgen2/pgen.py +++ b/src/blib2to3/pgen2/pgen.py @@ -2,17 +2,17 @@ # Licensed to PSF under a Contributor Agreement. import os -from collections.abc import Iterator, Sequence -from typing import IO, Any, NoReturn, Union +from collections.abc import Iterator +from typing import IO, Any, NoReturn from blib2to3.pgen2 import grammar, token, tokenize from blib2to3.pgen2.tokenize import TokenInfo -Path = Union[str, "os.PathLike[str]"] +Path = str | os.PathLike[str] class PgenGrammar(grammar.Grammar): - pass + __slots__ = () class ParserGenerator: @@ -37,8 +37,7 @@ def __init__(self, filename: Path, stream: IO[str] | None = None) -> None: def make_grammar(self) -> PgenGrammar: c = PgenGrammar() - names = list(self.dfas.keys()) - names.sort() + names = sorted(self.dfas.keys()) names.remove(self.startsymbol) names.insert(0, self.startsymbol) for name in names: @@ -49,9 +48,10 @@ def make_grammar(self) -> PgenGrammar: dfa = self.dfas[name] states = [] for state in dfa: - arcs = [] - for label, next in sorted(state.arcs.items()): - arcs.append((self.make_label(c, label), dfa.index(next))) + arcs: list[tuple[int, int]] = [ + (self.make_label(c, label), dfa.index(next_)) + for label, next_ in sorted(state.arcs.items()) + ] if state.isfinal: arcs.append((0, dfa.index(state))) states.append(arcs) @@ -122,8 +122,7 @@ def make_label(self, c: PgenGrammar, label: str) -> int: return ilabel def addfirstsets(self) -> None: - names = list(self.dfas.keys()) - names.sort() + names = sorted(self.dfas.keys()) for name in names: if name not in self.first: self.calcfirst(name) @@ -145,7 +144,7 @@ def calcfirst(self, name: str) -> None: self.calcfirst(label) fset = self.first[label] assert fset is not None - totalset.update(fset) + totalset |= fset overlapcheck[label] = fset else: totalset[label] = 1 @@ -375,10 +374,7 @@ def __eq__(self, other: Any) -> bool: # would invoke this method recursively, with cycles... if len(self.arcs) != len(other.arcs): return False - for label, next in self.arcs.items(): - if next is not other.arcs.get(label): - return False - return True + return all(next_ is other.arcs.get(label) for label, next_ in self.arcs.items()) __hash__: Any = None # For Py3 compatibility. diff --git a/src/blib2to3/pytree.py b/src/blib2to3/pytree.py index 152e5734be0..d4ba5871a2c 100644 --- a/src/blib2to3/pytree.py +++ b/src/blib2to3/pytree.py @@ -12,6 +12,7 @@ # mypy: allow-untyped-defs, allow-incomplete-defs +from abc import ABC, abstractmethod from collections.abc import Iterable, Iterator from typing import Any, Optional, TypeVar, Union @@ -39,7 +40,7 @@ def type_repr(type_num: int) -> str | int: # from .pgen2 import token // token.__dict__.items(): for name in dir(pygram.python_symbols): val = getattr(pygram.python_symbols, name) - if type(val) == int: + if type(val) is int: _type_reprs[val] = name return _type_reprs.setdefault(type_num, type_num) @@ -51,7 +52,7 @@ def type_repr(type_num: int) -> str | int: RawNode = tuple[int, Optional[str], Optional[Context], Optional[list[NL]]] -class Base: +class Base(ABC): """ Abstract base class for Node and Leaf. @@ -61,16 +62,13 @@ class Base: A node may be a subnode of at most one parent. """ - # Default values for instance variables - type: int # int: token number (< 256) or symbol number (>= 256) - parent: Optional["Node"] = None # Parent node pointer, or None - children: list[NL] # List of subnodes - was_changed: bool = False + __slots__ = ("type", "parent", "children", "was_changed") - def __new__(cls, *args, **kwds): - """Constructor that prevents Base from being instantiated.""" - assert cls is not Base, "Cannot instantiate Base" - return object.__new__(cls) + def __init__(self, type_id: int, children: Optional[list[NL]] = None): + self.type = type_id # int: token number (< 256) or symbol number (>= 256) + self.children = children or [] # List of subnodes + self.parent: Optional["Node"] = None # Parent node pointer, or None + self.was_changed: bool = False def __eq__(self, other: Any) -> bool: """ @@ -83,9 +81,10 @@ def __eq__(self, other: Any) -> bool: return self._eq(other) @property - def prefix(self) -> str: - raise NotImplementedError + @abstractmethod + def prefix(self) -> str: ... + @abstractmethod def _eq(self: _P, other: _P) -> bool: """ Compare two nodes for equality. @@ -95,34 +94,33 @@ def _eq(self: _P, other: _P) -> bool: Nodes should be considered equal if they have the same structure, ignoring the prefix string and other context information. """ - raise NotImplementedError def __deepcopy__(self: _P, memo: Any) -> _P: return self.clone() + @abstractmethod def clone(self: _P) -> _P: """ Return a cloned (deep) copy of self. This must be implemented by the concrete subclass. """ - raise NotImplementedError + @abstractmethod def post_order(self) -> Iterator[NL]: """ Return a post-order iterator for the tree. This must be implemented by the concrete subclass. """ - raise NotImplementedError + @abstractmethod def pre_order(self) -> Iterator[NL]: """ Return a pre-order iterator for the tree. This must be implemented by the concrete subclass. """ - raise NotImplementedError def replace(self, new: NL | list[NL]) -> None: """Replace this node with a new one in the parent.""" @@ -230,8 +228,7 @@ def get_suffix(self) -> str: class Node(Base): """Concrete implementation for interior nodes.""" - fixers_applied: list[Any] | None - used_names: set[str] | None + __slots__ = ("prev_sibling_map", "next_sibling_map") def __init__( self, @@ -250,18 +247,14 @@ def __init__( As a side effect, the parent pointers of the children are updated. """ assert type >= 256, type - self.type = type - self.children = list(children) + + super().__init__(type, children) for ch in self.children: assert ch.parent is None, repr(ch) ch.parent = self self.invalidate_sibling_maps() if prefix is not None: self.prefix = prefix - if fixers_applied: - self.fixers_applied = fixers_applied[:] - else: - self.fixers_applied = None def __repr__(self) -> str: """Return a canonical string representation.""" @@ -286,7 +279,6 @@ def clone(self) -> "Node": return Node( self.type, [ch.clone() for ch in self.children], - fixers_applied=self.fixers_applied, ) def post_order(self) -> Iterator[NL]: @@ -426,35 +418,31 @@ def _replace_child_in_sibling_maps(self, old: NL, new_children: list[NL]) -> Non prev_map[id(after)] = previous def update_sibling_maps(self) -> None: - _prev: dict[int, NL | None] = {} - _next: dict[int, NL | None] = {} - self.prev_sibling_map = _prev - self.next_sibling_map = _next + self.prev_sibling_map = {id(x): None for x in self.children} + self.next_sibling_map = self.prev_sibling_map.copy() previous: NL | None = None + for current in self.children: - _prev[id(current)] = previous - _next[id(previous)] = current + self.prev_sibling_map[id(current)] = previous + self.next_sibling_map[id(previous)] = current previous = current - _next[id(current)] = None + + if previous is not None: # if self.children is empty! + self.next_sibling_map[id(previous)] = None class Leaf(Base): """Concrete implementation for leaf nodes.""" - # Default values for instance variables - value: str - fixers_applied: list[Any] - bracket_depth: int - # Changed later in brackets.py - opening_bracket: Optional["Leaf"] = None - used_names: set[str] | None - _prefix = "" # Whitespace and comments preceding this token in the input - lineno: int = 0 # Line where this token starts in the input - column: int = 0 # Column where this token starts in the input - # If not None, this Leaf is created by converting a block of fmt off/skip - # code, and `fmt_pass_converted_first_leaf` points to the first Leaf in the - # converted code. - fmt_pass_converted_first_leaf: Optional["Leaf"] = None + __slots__ = ( + "_prefix", + "bracket_depth", + "column", + "fmt_pass_converted_first_leaf", + "lineno", + "opening_bracket", + "value", + ) def __init__( self, @@ -472,19 +460,25 @@ def __init__( Takes a type constant (a token number < 256), a string value, and an optional context keyword argument. """ - assert 0 <= type < 256, type - if context is not None: - self._prefix, (self.lineno, self.column) = context - self.type = type + super().__init__(type) + self.value = value - if prefix is not None: - self._prefix = prefix - self.fixers_applied: list[Any] | None = fixers_applied[:] - self.children = [] self.opening_bracket = opening_bracket + self.bracket_depth: int = 0 + + # If not None, this Leaf is created by converting a block of fmt off/skip + # code, and `fmt_pass_converted_first_leaf` points to the first Leaf in the + # converted code. self.fmt_pass_converted_first_leaf = fmt_pass_converted_first_leaf + self._prefix = "" # Whitespace and comments preceding this token in the input + self.lineno = 0 # Line where this token starts in the input + self.column = 0 # Column where this token starts in the input + if context is not None: + self._prefix, (self.lineno, self.column) = context + self._prefix = prefix or self._prefix + def __repr__(self) -> str: """Return a canonical string representation.""" from .pgen2.token import tok_name @@ -514,7 +508,6 @@ def clone(self) -> "Leaf": self.type, self.value, (self.prefix, (self.lineno, self.column)), - fixers_applied=self.fixers_applied, ) def leaves(self) -> Iterator["Leaf"]: @@ -564,7 +557,7 @@ def convert(gr: Grammar, raw_node: RawNode) -> NL: _Results = dict[str, NL] -class BasePattern: +class BasePattern(ABC): """ A pattern is a tree matching pattern. @@ -579,16 +572,14 @@ class BasePattern: - WildcardPattern matches a sequence of nodes of variable length. """ - # Defaults for instance variables - type: int | None - type = None # Node type (token if < 256, symbol if >= 256) - content: Any = None # Optional content matching pattern - name: str | None = None # Optional name used to store match in results dict + __slots__ = ("type", "content", "name") - def __new__(cls, *args, **kwds): - """Constructor that prevents BasePattern from being instantiated.""" - assert cls is not BasePattern, "Cannot instantiate BasePattern" - return object.__new__(cls) + def __init__( + self, type_id: int | None = None, content: Any = None, name: str | None = None + ): + self.type = type_id # Node type (token if < 256, symbol if >= 256) + self.content = content # Optional content matching pattern + self.name = name # Optional name used to store match in results dict def __repr__(self) -> str: assert self.type is not None @@ -622,14 +613,12 @@ def match(self, node: NL, results: _Results | None = None) -> bool: if self.type is not None and node.type != self.type: return False if self.content is not None: - r: _Results | None = None - if results is not None: - r = {} + r: _Results | None = {} if results is not None else None if not self._submatch(node, r): return False if r: assert results is not None - results.update(r) + results |= r if results is not None and self.name: results[self.name] = node return True @@ -656,6 +645,8 @@ def generate_matches(self, nodes: list[NL]) -> Iterator[tuple[int, _Results]]: class LeafPattern(BasePattern): + __slots__ = () + def __init__( self, type: int | None = None, @@ -677,9 +668,7 @@ def __init__( assert 0 <= type < 256, type if content is not None: assert isinstance(content, str), repr(content) - self.type = type - self.content = content - self.name = name + super().__init__(type, content=content, name=name) def match(self, node: NL, results=None) -> bool: """Override match() to insist on a leaf node.""" @@ -704,7 +693,7 @@ def _submatch(self, node, results=None): class NodePattern(BasePattern): - wildcards: bool = False + __slots__ = ("wildcards",) def __init__( self, @@ -727,21 +716,21 @@ def __init__( If a name is given, the matching node is stored in the results dict under that key. """ + self.wildcards: bool = False if type is not None: assert type >= 256, type + newcontent = None if content is not None: assert not isinstance(content, str), repr(content) newcontent = list(content) for i, item in enumerate(newcontent): - assert isinstance(item, BasePattern), (i, item) + assert isinstance(item, BasePattern), (i, item) # type: ignore[unreachable] # I don't even think this code is used anywhere, but it does cause # unreachable errors from mypy. This function's signature does look # odd though *shrug*. if isinstance(item, WildcardPattern): # type: ignore[unreachable] - self.wildcards = True # type: ignore[unreachable] - self.type = type - self.content = newcontent # TODO: this is unbound when content is None - self.name = name + self.wildcards = True + super().__init__(type, content=newcontent, name=name) def _submatch(self, node, results=None) -> bool: """ @@ -760,7 +749,7 @@ def _submatch(self, node, results=None) -> bool: for c, r in generate_matches(self.content, node.children): if c == len(node.children): if results is not None: - results.update(r) + results |= r return True return False if len(self.content) != len(node.children): @@ -784,8 +773,7 @@ class WildcardPattern(BasePattern): except it always uses non-greedy matching. """ - min: int - max: int + __slots__ = ("min", "max") def __init__( self, @@ -817,19 +805,19 @@ def __init__( list of alternatives, e.g. (a b c | d e | f g h)* """ assert 0 <= min <= max <= HUGE, (min, max) + wrapped_content = None if content is not None: - f = lambda s: tuple(s) - wrapped_content = tuple(map(f, content)) # Protect against alterations + wrapped_content = tuple(map(tuple, content)) # Protect against alterations # Check sanity of alternatives assert len(wrapped_content), repr( wrapped_content ) # Can't have zero alternatives for alt in wrapped_content: assert len(alt), repr(alt) # Can have empty alternatives - self.content = wrapped_content + self.min = min self.max = max - self.name = name + super().__init__(content=wrapped_content, name=name) def optimize(self) -> Any: """Optimize certain stacked wildcard patterns.""" @@ -868,7 +856,7 @@ def match_seq(self, nodes, results=None) -> bool: for c, r in self.generate_matches(nodes): if c == len(nodes): if results is not None: - results.update(r) + results |= r if self.name: results[self.name] = list(nodes) return True @@ -941,9 +929,7 @@ def _iterative_matches(self, nodes) -> Iterator[tuple[int, _Results]]: for alt in self.content: for c1, r1 in generate_matches(alt, nodes[c0:]): if c1 > 0: - r = {} - r.update(r0) - r.update(r1) + r = r0 | r1 yield c0 + c1, r new_results.append((c0 + c1, r)) results = new_results @@ -974,13 +960,13 @@ def _recursive_matches(self, nodes, count) -> Iterator[tuple[int, _Results]]: for alt in self.content: for c0, r0 in generate_matches(alt, nodes): for c1, r1 in self._recursive_matches(nodes[c0:], count + 1): - r = {} - r.update(r0) - r.update(r1) + r = r0 | r1 yield c0 + c1, r class NegatedPattern(BasePattern): + __slots__ = () + def __init__(self, content: BasePattern | None = None) -> None: """ Initializer. @@ -992,7 +978,7 @@ def __init__(self, content: BasePattern | None = None) -> None: """ if content is not None: assert isinstance(content, BasePattern), repr(content) - self.content = content + super().__init__(content=content) def match(self, node, results=None) -> bool: # We never match a node in its entirety @@ -1038,7 +1024,5 @@ def generate_matches( yield c0, r0 else: for c1, r1 in generate_matches(rest, nodes[c0:]): - r = {} - r.update(r0) - r.update(r1) + r = r0 | r1 yield c0 + c1, r