Source code for ooodev.utils.selection

# coding: utf-8
# Python conversion of Info.java by Andrew Davison, ad@fivedots.coe.psu.ac.th
# See Also: https://fivedots.coe.psu.ac.th/~ad/jlop/
from __future__ import annotations
import contextlib
from typing import cast, overload, TYPE_CHECKING
from enum import IntEnum

from com.sun.star.beans import XPropertySet
from com.sun.star.container import XIndexAccess
from com.sun.star.frame import XModel
from com.sun.star.i18n import XBreakIterator
from com.sun.star.lang import XComponent
from com.sun.star.text import XParagraphCursor
from com.sun.star.text import XSentenceCursor
from com.sun.star.text import XText
from com.sun.star.text import XTextCursor
from com.sun.star.text import XTextDocument
from com.sun.star.text import XTextRange
from com.sun.star.text import XTextRangeCompare
from com.sun.star.text import XTextViewCursor
from com.sun.star.text import XTextViewCursorSupplier
from com.sun.star.text import XWordCursor
from com.sun.star.view import XSelectionSupplier

from ooo.dyn.i18n.word_type import WordTypeEnum as WordTypeEnum
from ooo.dyn.i18n.boundary import Boundary  # struct

from ooodev.events.args.cancel_event_args import CancelEventArgs
from ooodev.events.args.event_args import EventArgs
from ooodev.events.event_singleton import _Events
from ooodev.events.lo_named_event import LoNamedEvent
from ooodev.events.write_named_event import WriteNamedEvent
from ooodev.exceptions import ex as mEx
from ooodev.meta.static_meta import StaticProperty, classproperty
from ooodev.utils import info as mInfo
from ooodev.loader import lo as mLo
from ooodev.utils.type_var import DocOrText, DocOrCursor


# if not _DOCS_BUILDING and not _ON_RTD:


if TYPE_CHECKING:
    from com.sun.star.text import XSimpleText


[docs]class Selection(metaclass=StaticProperty): """Selection Framework"""
[docs] class CompareEnum(IntEnum): """Compare Enumeration""" AFTER = 1 BEFORE = -1 EQUAL = 0
[docs] @staticmethod def is_anything_selected(text_doc: XTextDocument) -> bool: """ Determine if anything is selected. If Write document is not visible this method returns false. |lo_safe| Args: text_doc (XTextDocument): Text Document Returns: bool: True if anything in the document is selected: Otherwise, False Note: Writer must be visible for this method or ``False`` is always returned. """ model = mLo.Lo.qi(XModel, text_doc, True) o_selections = model.getCurrentSelection() if o_selections is None: return False index_access = mLo.Lo.qi(XIndexAccess, o_selections) if index_access is None: return False count = int(index_access.getCount()) if count == 0: return False elif count > 1: return True else: # There is only one selection so obtain the first selection o_sel = mLo.Lo.qi(XTextRange, index_access.getByIndex(0)) if o_sel is None: return False o_text = o_sel.getText() # Create a text cursor that covers the range and then see if it is collapsed o_cursor = o_text.createTextCursorByRange(o_sel) if not o_cursor.isCollapsed(): return True return False
# region get_text_doc() @overload @classmethod def get_text_doc(cls) -> XTextDocument: """ Gets a writer document. When using this method in a macro the ``Lo.get_document()`` value should be passed as ``doc`` arg. |lo_unsafe| Args: doc (XComponent): Component to get writer document from. Raises: TypeError: doc is None. MissingInterfaceError: If doc does not implement XTextDocument interface. Returns: XTextDocument: Writer document. """ ... @overload @classmethod def get_text_doc(cls, doc: XComponent) -> XTextDocument: """ Gets a writer document. When using this method in a macro the ``Lo.get_document()`` value should be passed as ``doc`` arg. |lo_safe| Args: doc (XComponent): Component to get writer document from. Raises: TypeError: doc is None. MissingInterfaceError: If doc does not implement XTextDocument interface. Returns: XTextDocument: Writer document. """ ...
[docs] @classmethod def get_text_doc(cls, doc: XComponent | None = None) -> XTextDocument: """ Gets a writer document When using this method in a macro the ``Lo.get_document()`` value should be passed as ``doc`` arg. Args: doc (XComponent): Component to get writer document from Raises: TypeError: doc is None MissingInterfaceError: If doc does not implement XTextDocument interface Returns: XTextDocument: Writer document :events: .. cssclass:: lo_event - :py:attr:`~.events.write_named_event.WriteNamedEvent.DOC_TEXT` :eventref:`src-docs-event` .. versionchanged:: 0.9.0 Added overload ``get_text_doc()`` """ text_doc = None if doc is None: with contextlib.suppress(AttributeError): text_doc = mLo.Lo.qi(XTextDocument, mLo.Lo.xscript_context.getDocument()) if text_doc is None: # most likely in headless mode with dynamic options set to True text_doc = mLo.Lo.qi(XTextDocument, mLo.Lo.lo_component, True) _Events().trigger(WriteNamedEvent.DOC_TEXT, EventArgs(Selection.get_text_doc.__qualname__)) return text_doc
# endregion get_text_doc()
[docs] @staticmethod def get_selected_text_range(text_doc: XTextDocument) -> XTextRange | None: """ Gets the text range for current selection. |lo_safe| Args: text_doc (XTextDocument): Text Document. Raises: MissingInterfaceError: If unable to obtain required interface. Returns: XTextRange | None: If no selection is made then None is returned; Otherwise, Text Range. Note: Writer must be visible for this method or ``None`` is returned. """ model = mLo.Lo.qi(XModel, text_doc, True) o_selections = cast(XIndexAccess, model.getCurrentSelection()) if not o_selections: return None count = int(o_selections.getCount()) if count == 0: return None return mLo.Lo.qi(XTextRange, o_selections.getByIndex(0), True)
[docs] @classmethod def get_selected_text_str(cls, text_doc: XTextDocument) -> str: """ Gets the first selection text for Document. |lo_safe| Args: text_doc (XTextDocument): Text Document. Returns: str: Selected text or empty string. Note: Writer must be visible for this method or empty string is returned. """ rng = cls.get_selected_text_range(text_doc=text_doc) return "" if rng is None else rng.getString()
[docs] @classmethod def compare_cursor_ends(cls, c1: XTextRange, c2: XTextRange) -> Selection.CompareEnum: """ Compares two cursors ranges end positions. |lo_unsafe| Args: c1 (XTextRange): first cursor range c2 (XTextRange): second cursor range Raises: Exception: if comparison fails Returns: CompareEnum: Compare result. :py:attr:`.CompareEnum.BEFORE` if ``c1`` end position is before ``c2`` end position. :py:attr:`.CompareEnum.EQUAL` if ``c1`` end position is equal to ``c2`` end position. :py:attr:`.CompareEnum.AFTER` if ``c1`` end position is after ``c2`` end position. """ # sourcery skip: raise-specific-error range_compare = cast(XTextRangeCompare, cls.text_range_compare) i = range_compare.compareRegionEnds(c1, c2) if i == 1: return cls.CompareEnum.BEFORE if i == -1: return cls.CompareEnum.AFTER if i == 0: return cls.CompareEnum.EQUAL # if no valid result raise error msg = "get_cursor_compare_ends() unable to get a valid compare result" raise Exception(msg)
[docs] @classmethod def range_len(cls, text_doc: XTextDocument, o_sel: XTextCursor) -> int: """ Gets the distance between range start and range end. |lo_unsafe| Args: o_sel (XTextCursor): first cursor range o_text (object): XText object, usually document text object Returns: int: length of range Note: All characters are counted including paragraph breaks. In Writer it will display selected characters however, paragraph breaks are not counted. """ i = 0 if o_sel.isCollapsed(): return i # o_text = mLo.Lo.qi(XText, text_doc, True) o_text = text_doc.getText() if not o_text: return 0 l_cursor = cls.get_left_cursor(o_sel=o_sel, o_text=o_text) r_cursor = cls.get_right_cursor(o_sel=o_sel, o_text=o_text) if cls.compare_cursor_ends(c1=l_cursor, c2=r_cursor) < cls.CompareEnum.EQUAL: while cls.compare_cursor_ends(c1=l_cursor, c2=r_cursor) != cls.CompareEnum.EQUAL: l_cursor.goRight(1, False) i += 1 return i
# region ------------- model cursor methods ------------------------
[docs] @classmethod def get_text_cursor_props(cls, text_doc: XTextDocument) -> XPropertySet: """ Gets properties for document cursor. |lo_safe| Args: text_doc (XTextDocument): Text Document Raises: MissingInterfaceError: If unable to obtain XPropertySet interface from cursor. Returns: XPropertySet: Properties """ cursor = cls.get_cursor(text_doc) return mLo.Lo.qi(XPropertySet, cursor, True)
# region get_cursor() @overload @staticmethod def get_cursor() -> XTextCursor: """ Gets text cursor from the current document. |lo_unsafe| Returns: XTextCursor: Cursor """ ... @overload @staticmethod def get_cursor(cursor_obj: DocOrCursor) -> XTextCursor: """ Gets text cursor. |lo_safe| Args: cursor_obj (DocOrCursor): Text Document or Text Cursor. Returns: XTextCursor: Cursor. """ ... @overload @staticmethod def get_cursor(rng: XTextRange, txt: XText) -> XTextCursor: """ Gets text cursor. |lo_safe| Args: rng (XTextRange): Text Range Instance. txt (XText): Text Instance. Returns: XTextCursor: Cursor. """ ... @overload @staticmethod def get_cursor(rng: XTextRange, text_doc: XTextDocument) -> XTextCursor: """ Gets text cursor. |lo_safe| Args: rng (XTextRange): Text Range instance. text_doc (XTextDocument): Text Document instance. Returns: XTextCursor: Cursor. """ ...
[docs] @staticmethod def get_cursor(*args, **kwargs) -> XTextCursor | None: """ Gets text cursor. Args: cursor_obj (DocOrCursor): Text Document or Text View Cursor. rng (XTextRange): Text Range Instance. text_doc (XTextDocument): Text Document instance. Raises: CursorError: If Unable to get cursor. Returns: XTextCursor | None: Cursor or ``None`` if unable to get cursor. """ ordered_keys = (1, 2) kargs_len = len(kwargs) count = len(args) + kargs_len def get_kwargs() -> dict: ka = {} if kargs_len == 0: return ka valid_keys = ("cursor_obj", "rng", "txt", "text_doc") check = all(key in valid_keys for key in kwargs) if not check: raise TypeError("get_cursor() got an unexpected keyword argument") keys = ("cursor_obj", "rng") for key in keys: if key in kwargs: ka[1] = kwargs[key] break if count == 1: return ka keys = ("txt", "text_doc") for key in keys: if key in kwargs: ka[2] = kwargs[key] break return ka if count not in (0, 1, 2): raise TypeError("get_cursor() got an invalid number of arguments") kargs = get_kwargs() for i, arg in enumerate(args): kargs[ordered_keys[i]] = arg if count == 0: cursor = Selection._get_cursor_obj(Selection.active_doc) if cursor is None: raise mEx.CursorError("Unable to get cursor") return cursor if count == 1: cursor = Selection._get_cursor_obj(kargs[1]) if cursor is None: raise mEx.CursorError("Unable to get cursor") return cursor txt_doc = mLo.Lo.qi(XTextDocument, kargs[2]) txt = kargs[2] if txt_doc is None else txt_doc.getText() return Selection._get_cursor_txt(rng=kargs[1], txt=txt)
@staticmethod def _get_cursor_txt(rng: XTextRange, txt: XText) -> XTextCursor: """Lo Safe Method.""" # sourcery skip: raise-specific-error try: cursor = txt.createTextCursorByRange(rng) if cursor is None: raise Exception("Cursor is None") return cursor except Exception as e: raise mEx.CursorError(str(e)) from e @staticmethod def _get_cursor_obj(cursor_obj: DocOrCursor) -> XTextCursor | None: """Lo Safe Method.""" # sourcery skip: raise-specific-error try: # https://wiki.openoffice.org/wiki/Writer/API/Text_cursor x_txt_cursor = mLo.Lo.qi(XTextCursor, cursor_obj) if x_txt_cursor is not None: c = x_txt_cursor.getText().createTextCursorByRange(x_txt_cursor) if c is None: raise Exception("XTextViewCursor.createTextCursorByRange() result is null") return c x_doc = mLo.Lo.qi(XTextDocument, cursor_obj) if x_doc is not None: xtext = x_doc.getText() if xtext is None: return None c = xtext.createTextCursor() if c is None: raise Exception("XTextDocument, XText failed to create text cursor") return c raise TypeError("cursor_obj is invalid type. Expected XTextDocument or XTextViewCursor") except Exception as e: raise mEx.CursorError(str(e)) from e # XTextViewCursor # endregion get_cursor()
[docs] @classmethod def get_word_cursor(cls, cursor_obj: DocOrCursor) -> XWordCursor: """ Gets document word cursor. |lo_safe| Args: cursor_obj (DocOrCursor): Text Document or Text Cursor. Raises: WordCursorError: If Unable to get cursor. Returns: XWordCursor: Word Cursor. """ try: if mLo.Lo.is_uno_interfaces(cursor_obj, XTextDocument): cursor = cls.get_cursor(cursor_obj) else: cursor = cursor_obj return mLo.Lo.qi(XWordCursor, cursor, True) except Exception as e: raise mEx.WordCursorError(str(e)) from e
[docs] @classmethod def get_sentence_cursor(cls, cursor_obj: DocOrCursor) -> XSentenceCursor: """ Gets document sentence cursor. |lo_safe| Args: cursor_obj (DocOrCursor): Text Document or Text Cursor. Raises: SentenceCursorError: If Unable to get cursor. Returns: XSentenceCursor: Sentence Cursor. .. versionchanged:: 0.16.0 Now raises ``SentenceCursorError`` instead of returning ``None``. """ try: if mLo.Lo.is_uno_interfaces(cursor_obj, XTextDocument): cursor = cls.get_cursor(cursor_obj) else: cursor = cursor_obj return mLo.Lo.qi(XSentenceCursor, cursor, True) except Exception as e: raise mEx.SentenceCursorError(str(e)) from e
[docs] @classmethod def get_paragraph_cursor(cls, cursor_obj: DocOrCursor) -> XParagraphCursor: """ Gets document paragraph cursor. |lo_safe| Args: cursor_obj (DocOrCursor): Text Document or Text Cursor. Raises: ParagraphCursorError: If Unable to get cursor. Returns: XParagraphCursor: Paragraph cursor. """ try: if mLo.Lo.qi(XTextDocument, cursor_obj) is None: cursor = cursor_obj else: cursor = cls.get_cursor(cursor_obj) return mLo.Lo.qi(XParagraphCursor, cursor, True) except Exception as e: raise mEx.ParagraphCursorError(str(e)) from e
[docs] @classmethod def get_left_cursor(cls, o_sel: XTextRange, o_text: DocOrText) -> XTextCursor: """ Creates a new TextCursor with position left that can travel right. |lo_unsafe| Args: o_sel (XTextRange): Text Range. o_text (DocOrText): Text document or text. Returns: XTextCursor: a new instance of a TextCursor which is located at the specified TextRange to travel in the given text context. """ doc = mLo.Lo.qi(XTextDocument, o_text) text = cast(XText, o_text if doc is None else doc.getText()) range_compare = cls.text_range_compare if range_compare.compareRegionStarts(o_sel.getEnd(), o_sel) >= 0: o_range = o_sel.getEnd() else: o_range = o_sel.getStart() cursor = text.createTextCursorByRange(o_range) cursor.goRight(0, False) return cursor
[docs] @classmethod def get_right_cursor(cls, o_sel: XTextRange, o_text: DocOrText) -> XTextCursor: """ Creates a new TextCursor with position right that can travel left. |lo_unsafe| Args: o_sel (XTextRange): Text Range o_text (DocOrText): Text document or text. Returns: XTextCursor: a new instance of a TextCursor which is located at the specified TextRange to travel in the given text context. """ range_compare = cls.text_range_compare if range_compare.compareRegionStarts(o_sel.getEnd(), o_sel) >= 0: o_range = o_sel.getStart() else: o_range = o_sel.getEnd() cursor = cast("XSimpleText", o_text).createTextCursorByRange(o_range) cursor.goLeft(0, False) return cursor
[docs] @classmethod def get_position(cls, cursor: XTextCursor) -> int: """ Gets position of the cursor. |lo_unsafe| Args: cursor (XTextCursor): Text Cursor. Returns: int: Current Cursor Position. Note: This method is not the most reliable. It attempts to read all the text in a document and move the cursor to the end and then get the position. It would be better to use cursors from relative positions in bigger documents. """ # def get_near_max(l:XTextCursor, r: XTextCursor, jump=10) -> int: # i_max = 0 # if cls.compare_cursor_ends(l, r) == cls.CompareEnum.BEFORE: # l.goRight(jump, False) # i_max = i_max + jump jmp_amt = 25 def get_high(lft: XTextCursor, rgt: XTextCursor, jump=jmp_amt, total=0) -> int: # OPTIMIZE: get_position.get_high() # The idea of this function is to cut down on the number if iterations # needed to get the range from cursors left and right positions. # Most likely there is an even more efficient way to do this. if jump <= 0: return 0 if cls.compare_cursor_ends(lft, rgt) == cls.CompareEnum.BEFORE: j = jump + jmp_amt lft.goRight(j, False) return get_high(lft, rgt, j, jump) else: lft.gotoStart(False) lft.goRight(total, False) return total # else: # return jump, True xcursor = cursor.getText().createTextCursor() xcursor.gotoStart(False) xcursor.gotoEnd(True) xtext = xcursor.getText() l_cursor = cls.get_left_cursor(o_sel=xcursor, o_text=xtext) r_cursor = cls.get_right_cursor(o_sel=xcursor, o_text=xtext) l_cursor.goRight(jmp_amt, False) i = 0 if cls.compare_cursor_ends(l_cursor, r_cursor) != cls.CompareEnum.BEFORE: # cursor has less text then jmp_amt l_cursor.gotoStart(False) l_cursor.goRight(0, False) while cls.compare_cursor_ends(l_cursor, r_cursor) == cls.CompareEnum.BEFORE: i += 1 l_cursor.goRight(1, False) return i l_cursor.gotoStart(False) l_cursor.goRight(0, False) if cls.compare_cursor_ends(c1=l_cursor, c2=r_cursor) < cls.CompareEnum.EQUAL: high = get_high(l_cursor, r_cursor) # l_cursor.gotoStart(False) while cls.compare_cursor_ends(c1=l_cursor, c2=r_cursor) != cls.CompareEnum.EQUAL: l_cursor.goRight(1, False) i += 1 i += high return i
# return len(cursor.getText().getString()) # region ------------- view cursor methods -------------------------
[docs] @classmethod def get_text_view_cursor_prop_set(cls, text_doc: XTextDocument) -> XPropertySet: """ Gets properties for document view cursor. |lo_safe| Args: text_doc (XTextDocument): Text Document. Raises: MissingInterfaceError: If unable to obtain XPropertySet interface from cursor. Returns: XPropertySet: Properties. """ xview_cursor = cls.get_view_cursor(text_doc) return mLo.Lo.qi(XPropertySet, xview_cursor, True)
[docs] @staticmethod def get_view_cursor(text_doc: XTextDocument) -> XTextViewCursor: """ Gets document view cursor. Describes a cursor in a text document's view. |lo_safe| Args: text_doc (XTextDocument): Text Document Raises: ViewCursorError: If Unable to get cursor Returns: XTextViewCursor: Text View Cursor See Also: `LibreOffice API XTextViewCursor <https://api.libreoffice.org/docs/idl/ref/interfacecom_1_1sun_1_1star_1_1text_1_1XTextViewCursor.html>`_ """ # sourcery skip: raise-specific-error # https://wiki.openoffice.org/wiki/Writer/API/Text_cursor try: model = mLo.Lo.qi(XModel, text_doc, True) x_controller = model.getCurrentController() supplier = mLo.Lo.qi(XTextViewCursorSupplier, x_controller, True) vc = supplier.getViewCursor() if vc is None: raise Exception("Supplier return null view cursor") return vc except Exception as e: raise mEx.ViewCursorError(str(e)) from e
# endregion ---------- view cursor methods ------------------------- # endregion ---------- model cursor methods ------------------------ @classproperty def text_range_compare(cls) -> XTextRangeCompare: """ Gets text range for comparison operations |lo_unsafe| Returns: XTextRangeCompare: Text Range Compare instance """ # Note: This class is inherited so is important the attribute be assigned directly # to Selection and not to cls try: if mLo.Lo.current_lo.is_default: return Selection._text_range_compare # type: ignore else: # When in multi document mode do use cached attribute. doc = cls.get_text_doc() text = doc.getText() return mLo.Lo.qi(XTextRangeCompare, text.getText(), True) except AttributeError: doc = cls.get_text_doc() text = doc.getText() Selection._text_range_compare = mLo.Lo.qi(XTextRangeCompare, text.getText(), True) return Selection._text_range_compare # type: ignore
[docs] @staticmethod def get_word_count_ooo(text: str, word_type: WordTypeEnum | None = None, locale_lang: str | None = None) -> int: """ Get the number of word in ooo way. This method takes into account the current Locale. |lo_unsafe| Args: text (str): string to count the word of. word_type (WordTypeEnum, optional): type of words to count. Default ``WordTypeEnum.WORD_COUNT`` Import line ``from ooodev.utils.selection import WordTypeEnum``. locale_lang (str, optional): Language such as 'en-US' used to process word boundaries. Defaults to LO's current language. Raises: CreateInstanceMsfError: If unable to create ``i18n.BreakIterator service``. Returns: int: The number of words. """ if word_type is None: word_type = WordTypeEnum.WORD_COUNT # https://forum.openoffice.org/en/forum/viewtopic.php?f=20&t=82678 next_wd = Boundary() if locale_lang: local = mInfo.Info.parse_language_code(locale_lang) else: local = mInfo.Info.language_locale num_words = 0 start_pos = 0 st = f" {text} " if word_type > WordTypeEnum.ANY_WORD else text brk = mLo.Lo.create_instance_mcf(XBreakIterator, "com.sun.star.i18n.BreakIterator") if brk is None: raise mEx.CreateInstanceMcfError(XBreakIterator, "com.sun.star.i18n.BreakIterator") next_wd = brk.nextWord(st, start_pos, local, word_type.value) # type: ignore while next_wd.startPos != next_wd.endPos: num_words += 1 nw = next_wd.startPos next_wd = brk.nextWord(st, nw, local, word_type.value) # type: ignore return num_words
[docs] @classmethod def select_next_word(cls, text_doc: XTextDocument) -> bool: """ Select the word right from the current cursor position. |lo_safe| Args: text_doc (XTextDocument): Text Document Returns: bool: True if go to next word succeeds; Otherwise, False. :events: .. cssclass:: lo_event - :py:attr:`~.events.write_named_event.WriteNamedEvent.WORD_SELECTING` :eventref:`write_word_selecting` - :py:attr:`~.events.write_named_event.WriteNamedEvent.WORD_SELECTED` :eventref:`write_word_selected` Note: The method returning ``True`` does not necessarily mean that the cursor is located at the next word, or any word at all! This may happen for example if it travels over empty paragraphs. Note: Event args ``event_data`` is a dictionary containing ``text_doc``. """ cargs = CancelEventArgs(Selection.select_next_word) cargs.event_data = {"text_doc": text_doc} _Events().trigger(WriteNamedEvent.WORD_SELECTING, cargs) if cargs.cancel: return False supplier = mLo.Lo.qi(XSelectionSupplier, text_doc.getCurrentController(), True) # clear any current selection view_cursor = cls.get_view_cursor(text_doc=text_doc) # view_cursor.collapseToEnd() view_cursor.goRight(0, False) # see section 7.5.1 of developers' guide index_access = mLo.Lo.qi(XIndexAccess, supplier.getSelection(), True) rng = mLo.Lo.qi(XTextRange, index_access.getByIndex(0), True) # get the XWordCursor and make a selection! xText = rng.getText() word_cursor = cls.get_word_cursor(xText.createTextCursorByRange(rng)) if not word_cursor.isStartOfWord(): word_cursor.gotoStartOfWord(False) result = word_cursor.gotoNextWord(True) supplier.select(word_cursor) _Events().trigger(WriteNamedEvent.WORD_SELECTED, EventArgs.from_args(cargs)) return result
@classproperty def active_doc(cls) -> XTextDocument: """ Gets current active document. |lo_unsafe| Returns: XTextDocument: Text Document .. versionadded:: 0.9.0 """ # note: # It is not permitted to create weak ref to pyuno objects. return Selection.get_text_doc()
# try: # return Selection._active_doc # except AttributeError: # Selection._active_doc = Selection.get_text_doc() # return Selection._active_doc def _del_cache_attrs(source: object, e: EventArgs) -> None: # clears Write Attributes that are dynamically created d_attrs = ( "_text_range_compare", # "_active_doc", ) for attr in d_attrs: if hasattr(Selection, attr): delattr(Selection, attr) # subscribe to events that warrant clearing cached attribs _Events().on(LoNamedEvent.RESET, _del_cache_attrs) __all__ = ("Selection",)