Rewrite markup

This version's AttributedText stores attributes for each char instead of
using chunks. I hoped this would be more performant since last time,
joining ATs appeared to be pretty slow. It led to no performace increase
though.

Keeping this change in a separate branch just so it doesn't get lost.
This commit is contained in:
Joscha 2019-06-25 11:57:45 +00:00
parent 1faee1b550
commit 7aafb11d3f

View file

@ -1,99 +1,30 @@
from typing import Any, Iterable, List, Mapping, Optional, Tuple, Union from typing import Any, Iterable, List, Mapping, Optional, Tuple, Union
from dataclasses import dataclass
__all__ = ["Attributes", "Chunk", "AttributedText", "AT"] __all__ = ["Attributes", "AttributedText", "AT"]
Attributes = Mapping[str, Any] Attributes = Mapping[str, Any]
class Chunk: @dataclass
class Char:
@staticmethod char: str
def join_chunks(chunks: List["Chunk"]) -> List["Chunk"]: attrs: Attributes
if not chunks:
return []
new_chunks: List[Chunk] = [] def set(self, name: str, value: Any) -> "Char":
new_attrs = dict(self.attrs)
new_attrs[name] = value
return Char(self.char, new_attrs)
current_chunk = chunks[0] def remove(self, name: str) -> "Char":
for chunk in chunks[1:]: new_attrs = dict(self.attrs)
if not chunk.text:
continue
joined_chunk = current_chunk._join(chunk)
if joined_chunk is None:
new_chunks.append(current_chunk)
current_chunk = chunk
else:
current_chunk = joined_chunk
new_chunks.append(current_chunk)
return new_chunks
# Common special methods
def __init__(self, text: str, attributes: Attributes = {}) -> None:
self._text = text
self._attributes = dict(attributes)
def __str__(self) -> str:
return self.text
def __repr__(self) -> str:
return f"Chunk({self.text!r}, {self._attributes!r})"
# Uncommon special methods
def __eq__(self, other: object) -> bool:
if not isinstance(other, Chunk):
return NotImplemented
return (self._text == other._text and
self._attributes == other._attributes)
def __getitem__(self, key: Union[int, slice]) -> "Chunk":
return Chunk(self.text[key], self._attributes)
def __len__(self) -> int:
return len(self.text)
# Properties
@property
def text(self) -> str:
return self._text
@property
def attributes(self) -> Attributes:
return dict(self._attributes)
# Private methods
def _join(self, chunk: "Chunk") -> Optional["Chunk"]:
if self._attributes == chunk._attributes:
return Chunk(self.text + chunk.text, self._attributes)
return None
# Public methods
def get(self, name: str, default: Any = None) -> Any:
return self.attributes.get(name, default)
def set(self, name: str, value: Any) -> "Chunk":
new_attributes = dict(self._attributes)
new_attributes[name] = value
return Chunk(self.text, new_attributes)
def remove(self, name: str) -> "Chunk":
new_attributes = dict(self._attributes)
# This removes the value with that key, if it exists, and does nothing # This removes the value with that key, if it exists, and does nothing
# if it doesn't exist. (Since we give a default value, no KeyError is # if it doesn't exist. (Since we give a default value, no KeyError is
# raised if the key isn't found.) # raised if the key isn't found.)
new_attributes.pop(name, None) new_attrs.pop(name, None)
return Chunk(self.text, new_attributes) return Char(self.char, new_attrs)
class AttributedText: class AttributedText:
""" """
@ -102,9 +33,9 @@ class AttributedText:
""" """
@classmethod @classmethod
def from_chunks(cls, chunks: Iterable[Chunk]) -> "AttributedText": def from_chars(cls, chars: Iterable[Char]) -> "AttributedText":
new = cls() new = cls()
new._chunks = Chunk.join_chunks(list(chunks)) new._chars = list(chars)
return new return new
# Common special methods # Common special methods
@ -128,117 +59,60 @@ class AttributedText:
attributes = dict(attributes) attributes = dict(attributes)
attributes.update(kwargs) attributes.update(kwargs)
self._chunks: List[Chunk] = [] self._chars: List[Char] = []
if text is not None: for char in text or "":
self._chunks.append(Chunk(text, attributes=attributes)) self._chars.append(Char(char, attributes))
def __str__(self) -> str: def __str__(self) -> str:
return self.text return self.text
def __repr__(self) -> str: def __repr__(self) -> str:
return f"AttributedText.from_chunks({self._chunks!r})" return "N/A"
# Uncommon special methods # Uncommon special methods
def __add__(self, other: "AttributedText") -> "AttributedText": def __add__(self, other: "AttributedText") -> "AttributedText":
return AttributedText.from_chunks(self._chunks + other._chunks) return AttributedText.from_chars(self._chars + other._chars)
def __eq__(self, other: object) -> bool: def __eq__(self, other: object) -> bool:
if not isinstance(other, AttributedText): if not isinstance(other, AttributedText):
return NotImplemented return NotImplemented
return self._chunks == other._chunks return self._chars == other._chars
def __getitem__(self, key: Union[int, slice]) -> "AttributedText": def __getitem__(self, key: Union[int, slice]) -> "AttributedText":
chunks: List[Chunk] chars: List[Char]
if isinstance(key, slice): if isinstance(key, slice):
chunks = Chunk.join_chunks(self._slice(key)) chars = self._chars[key]
else: else:
chunks = [self._at(key)] chars = [self._chars[key]]
return AttributedText.from_chunks(chunks) return AttributedText.from_chars(chars)
def __len__(self) -> int: def __len__(self) -> int:
return sum(map(len, self._chunks)) return len(self._chars)
def __mul__(self, other: int) -> "AttributedText": def __mul__(self, other: int) -> "AttributedText":
if not isinstance(other, int): if not isinstance(other, int):
return NotImplemented return NotImplemented
return self.from_chunks(self.chunks * other) return self.from_chars(self._chars * other)
# Properties # Properties
@property @property
def text(self) -> str: def text(self) -> str:
return "".join(chunk.text for chunk in self._chunks) return "".join(char.char for char in self._chars)
@property @property
def chunks(self) -> List[Chunk]: def chars(self) -> List[Char]:
return list(self._chunks) return list(self._chars)
# Private methods
def _at(self, key: int) -> Chunk:
if key < 0:
key = len(self) + key
pos = 0
for chunk in self._chunks:
chunk_key = key - pos
if 0 <= chunk_key < len(chunk):
return chunk[chunk_key]
pos += len(chunk)
# We haven't found the chunk
raise KeyError
def _slice(self, key: slice) -> List[Chunk]:
start, stop, step = key.start, key.stop, key.step
if start is None:
start = 0
elif start < 0:
start = len(self) + start
if stop is None:
stop = len(self)
elif stop < 0:
stop = len(self) + stop
pos = 0 # cursor position
resulting_chunks = []
for chunk in self._chunks:
chunk_start = start - pos
chunk_stop = stop - pos
offset: Optional[int] = None
if step is not None:
offset = (start - pos) % step
if chunk_stop <= 0 or chunk_start >= len(chunk):
pass
elif chunk_start < 0 and chunk_stop > len(chunk):
resulting_chunks.append(chunk[offset::step])
elif chunk_start < 0:
resulting_chunks.append(chunk[offset:chunk_stop:step])
elif chunk_stop > len(chunk):
resulting_chunks.append(chunk[chunk_start::step])
else:
resulting_chunks.append(chunk[chunk_start:chunk_stop:step])
pos += len(chunk)
return resulting_chunks
# Public methods # Public methods
def at(self, pos: int) -> Attributes: def at(self, pos: int) -> Attributes:
return self._at(pos).attributes return self._chars[pos].attrs
def get(self, def get(self,
pos: int, pos: int,
@ -246,7 +120,7 @@ class AttributedText:
default: Any = None, default: Any = None,
) -> Any: ) -> Any:
return self._at(pos).get(name, default) return self.at(pos).get(name, default)
def split_by(self, def split_by(self,
attribute_name: str, attribute_name: str,
@ -254,26 +128,26 @@ class AttributedText:
blocks = [] blocks = []
chunks: List[Chunk] = [] chars: List[Char] = []
attribute: Any = None attribute: Any = None
for chunk in self._chunks: for char in self._chars:
chunk_attr = chunk.attributes.get(attribute_name) char_attr = char.attrs.get(attribute_name)
if chunks: if chars:
if attribute == chunk_attr: if attribute == char_attr:
chunks.append(chunk) chars.append(char)
else: else:
blocks.append((self.from_chunks(chunks), attribute)) blocks.append((self.from_chars(chars), attribute))
chunks = [chunk] chars = [char]
attribute = chunk_attr attribute = char_attr
else: else:
chunks.append(chunk) chars.append(char)
attribute = chunk_attr attribute = char_attr
if chunks: if chars:
blocks.append((self.from_chunks(chunks), attribute)) blocks.append((self.from_chars(chars), attribute))
return blocks return blocks
@ -287,11 +161,11 @@ class AttributedText:
interspersed.append(self) interspersed.append(self)
interspersed.append(segment) interspersed.append(segment)
chunks = [] chars = []
for segment in interspersed: for segment in interspersed:
chunks.extend(segment.chunks) chars.extend(segment.chars)
return self.from_chunks(chunks) return self.from_chars(chars)
def set(self, def set(self,
name: str, name: str,
@ -301,8 +175,8 @@ class AttributedText:
) -> "AttributedText": ) -> "AttributedText":
if start is None and stop is None: if start is None and stop is None:
chunks = (chunk.set(name, value) for chunk in self._chunks) chars = (char.set(name, value) for char in self._chars)
return AttributedText.from_chunks(chunks) return AttributedText.from_chars(chars)
elif start is None: elif start is None:
return self[:stop].set(name, value) + self[stop:] return self[:stop].set(name, value) + self[stop:]
elif stop is None: elif stop is None:
@ -324,8 +198,8 @@ class AttributedText:
) -> "AttributedText": ) -> "AttributedText":
if start is None and stop is None: if start is None and stop is None:
chunks = (chunk.remove(name) for chunk in self._chunks) chars = (char.remove(name) for char in self._chars)
return AttributedText.from_chunks(chunks) return AttributedText.from_chars(chars)
elif start is None: elif start is None:
return self[:stop].remove(name) + self[stop:] return self[:stop].remove(name) + self[stop:]
elif stop is None: elif stop is None: