From 65b8046fe08a19df937068e5fe5ad15f9b0a785a Mon Sep 17 00:00:00 2001 From: Emmanuel Gil Peyrot Date: Sat, 12 Dec 2020 18:44:37 +0100 Subject: from __future__ import annotations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that our baseline is Python 3.7, we can rely on type annotations to be lazily evaluated. --- poezio/bookmarks.py | 2 +- poezio/colors.py | 2 +- poezio/common.py | 2 +- poezio/config.py | 5 ++--- poezio/contact.py | 10 +++++----- poezio/core/commands.py | 2 +- poezio/core/completions.py | 2 +- poezio/core/core.py | 2 +- poezio/core/tabs.py | 19 +++++++++---------- poezio/decorators.py | 5 ++++- poezio/events.py | 2 +- poezio/keyboard.py | 4 ++-- poezio/logger.py | 2 +- poezio/mam.py | 4 ++-- poezio/multiuserchat.py | 8 +++++--- poezio/pep.py | 8 ++++---- poezio/plugin_e2ee.py | 18 +++++++++--------- poezio/tabs/adhoc_commands_list.py | 4 ++-- poezio/tabs/basetabs.py | 20 +++++++++++--------- poezio/tabs/bookmarkstab.py | 8 ++++---- poezio/tabs/confirmtab.py | 4 ++-- poezio/tabs/conversationtab.py | 14 +++++++------- poezio/tabs/data_forms.py | 4 ++-- poezio/tabs/listtab.py | 4 ++-- poezio/tabs/muclisttab.py | 4 ++-- poezio/tabs/muctab.py | 30 ++++++++++++++++-------------- poezio/tabs/privatetab.py | 6 +++--- poezio/tabs/rostertab.py | 4 ++-- poezio/text_buffer.py | 16 +++++++++------- poezio/theming.py | 7 +++---- poezio/timed_events.py | 8 ++++---- poezio/ui/render.py | 20 +++++++++++--------- poezio/user.py | 8 ++++---- poezio/windows/base_wins.py | 6 ++++-- poezio/windows/bookmark_forms.py | 2 +- poezio/windows/image.py | 4 ++-- poezio/windows/info_wins.py | 8 +++++--- poezio/windows/input_placeholders.py | 2 +- poezio/windows/inputs.py | 16 ++++++++-------- poezio/windows/list.py | 8 ++++---- poezio/windows/misc.py | 2 +- poezio/windows/muc.py | 2 +- poezio/windows/roster_win.py | 4 ++-- poezio/windows/text_win.py | 10 +++++----- poezio/xdg.py | 4 ++-- poezio/xhtml.py | 20 ++++++++++---------- 46 files changed, 180 insertions(+), 166 deletions(-) diff --git a/poezio/bookmarks.py b/poezio/bookmarks.py index d842d2dd..4ce06cf0 100644 --- a/poezio/bookmarks.py +++ b/poezio/bookmarks.py @@ -152,7 +152,7 @@ class Bookmark: class BookmarkList: def __init__(self): - self.bookmarks = [] # type: List[Bookmark] + self.bookmarks: List[Bookmark] = [] preferred = config.get('use_bookmarks_method').lower() if preferred not in ('pep', 'privatexml'): preferred = 'privatexml' diff --git a/poezio/colors.py b/poezio/colors.py index c1019145..adb2ca40 100644 --- a/poezio/colors.py +++ b/poezio/colors.py @@ -37,7 +37,7 @@ def ncurses_color_to_rgb(color: int) -> Tuple[float, float, float]: def generate_ccg_palette(curses_palette: List[int], reference_y: float) -> Palette: - cbcr_palette = {} # type: Dict[float, Tuple[float, int]] + cbcr_palette: Dict[float, Tuple[float, int]] = {} for curses_color in curses_palette: r, g, b = ncurses_color_to_rgb(curses_color) # drop grayscale diff --git a/poezio/common.py b/poezio/common.py index 315d5b9e..5332d408 100644 --- a/poezio/common.py +++ b/poezio/common.py @@ -250,7 +250,7 @@ def find_delayed_tag(message: Message) -> Tuple[bool, Optional[datetime]]: find_delay = message.xml.find delay_tag = find_delay('{urn:xmpp:delay}delay') - date = None # type: Optional[datetime] + date: Optional[datetime] = None if delay_tag is not None: delayed = True date = _datetime_tuple(delay_tag.attrib['stamp']) diff --git a/poezio/config.py b/poezio/config.py index 9a51e53f..78581775 100644 --- a/poezio/config.py +++ b/poezio/config.py @@ -383,8 +383,7 @@ class Config(RawConfigParser): if file_ok(self.file_name): try: with self.file_name.open('r', encoding='utf-8') as df: - lines_before = [line.strip() - for line in df] # type: List[str] + lines_before: List[str] = [line.strip() for line in df] except OSError: log.error( 'Unable to read the config file %s', @@ -394,7 +393,7 @@ class Config(RawConfigParser): else: lines_before = [] - sections = {} # type: Dict[str, List[int]] + sections: Dict[str, List[int]] = {} duplicate_section = False current_section = '' current_line = 0 diff --git a/poezio/contact.py b/poezio/contact.py index 50ccab1f..3330a2a6 100644 --- a/poezio/contact.py +++ b/poezio/contact.py @@ -29,8 +29,8 @@ class Resource: data: the dict to use as a source """ # Full JID - self._jid = jid # type: str - self._data = data # type: Dict[str, Union[str, int]] + self._jid: str = jid + self._data: Dict[str, Union[str, int]] = data @property def jid(self) -> str: @@ -69,12 +69,12 @@ class Contact: item: a slixmpp RosterItem pointing to that contact """ self.__item = item - self.folded_states = defaultdict(lambda: True) # type: Dict[str, bool] + self.folded_states: Dict[str, bool] = defaultdict(lambda: True) self._name = '' self.avatar = None self.error = None - self.tune = {} # type: Dict[str, str] - self.gaming = {} # type: Dict[str, str] + self.tune: Dict[str, str] = {} + self.gaming: Dict[str, str] = {} self.mood = '' self.activity = '' diff --git a/poezio/core/commands.py b/poezio/core/commands.py index e926dba5..cd957002 100644 --- a/poezio/core/commands.py +++ b/poezio/core/commands.py @@ -335,7 +335,7 @@ class CommandCore: except InvalidJID: return (None, None) - set_nick = '' # type: Optional[str] + set_nick: Optional[str] = '' if len(jid_string) > 1 and jid_string.startswith('/'): set_nick = jid_string[1:] elif info.resource: diff --git a/poezio/core/completions.py b/poezio/core/completions.py index ada8d2b9..98ca9ba0 100644 --- a/poezio/core/completions.py +++ b/poezio/core/completions.py @@ -490,7 +490,7 @@ class CompletionCore: tabs.StaticConversationTab, tabs.DynamicConversationTab, ) - tabjid = [] # type: List[JID] + tabjid: List[JID] = [] if isinstance(current_tab, chattabs): tabjid = [current_tab.jid.bare] diff --git a/poezio/core/core.py b/poezio/core/core.py index 3a13d4c3..2151600f 100644 --- a/poezio/core/core.py +++ b/poezio/core/core.py @@ -1171,7 +1171,7 @@ class Core: """ # shortcut priority = tabs.STATE_PRIORITY - tab_refs = {} # type: Dict[str, List[tabs.Tab]] + tab_refs: Dict[str, List[tabs.Tab]] = {} # put all the active tabs in a dict of lists by state for tab in self.tabs.get_tabs(): if not tab: diff --git a/poezio/core/tabs.py b/poezio/core/tabs.py index 61bad6f2..a789100d 100644 --- a/poezio/core/tabs.py +++ b/poezio/core/tabs.py @@ -54,16 +54,15 @@ class Tabs: once. Otherwise, mayhem is expected. """ # cursor - self._current_index = 0 # type: int - self._current_tab = None # type: Optional[tabs.Tab] - - self._previous_tab = None # type: Optional[tabs.Tab] - self._tabs = [] # type: List[tabs.Tab] - self._tab_jids = dict() # type: Dict[JID, tabs.Tab] - self._tab_types = defaultdict( - list) # type: Dict[Type[tabs.Tab], List[tabs.Tab]] - self._tab_names = dict() # type: Dict[str, tabs.Tab] - self._events = events # type: EventHandler + self._current_index: int = 0 + self._current_tab: Optional[tabs.Tab] = None + + self._previous_tab: Optional[tabs.Tab] = None + self._tabs: List[tabs.Tab] = [] + self._tab_jids: Dict[JID, tabs.Tab] = dict() + self._tab_types: Dict[Type[tabs.Tab], List[tabs.Tab]] = defaultdict(list) + self._tab_names: Dict[str, tabs.Tab] = dict() + self._events: EventHandler = events def __len__(self): return len(self._tabs) diff --git a/poezio/decorators.py b/poezio/decorators.py index 62724ecd..6a853446 100644 --- a/poezio/decorators.py +++ b/poezio/decorators.py @@ -1,6 +1,9 @@ """ Module containing various decorators """ + +from __future__ import annotations + from typing import ( cast, Any, @@ -179,7 +182,7 @@ command_args_parser = CommandArgParser() def deny_anonymous(func: Callable) -> Callable: """Decorator to disable commands when using an anonymous account.""" - def wrap(self: 'RosterInfoTab', *args: Any, **kwargs: Any) -> Any: + def wrap(self: RosterInfoTab, *args: Any, **kwargs: Any) -> Any: if self.core.xmpp.anon: return self.core.information( 'This command is not available for anonymous accounts.', diff --git a/poezio/events.py b/poezio/events.py index 5213f663..63782836 100644 --- a/poezio/events.py +++ b/poezio/events.py @@ -48,7 +48,7 @@ class EventHandler: 'ignored_private', 'tab_change', ] - self.events = {} # type: Dict[str, OrderedDict[int, List[Callable]]] + self.events: Dict[str, OrderedDict[int, List[Callable]]] = {} for event in events: self.events[event] = OrderedDict() diff --git a/poezio/keyboard.py b/poezio/keyboard.py index 3d8e8d5c..23da2e37 100755 --- a/poezio/keyboard.py +++ b/poezio/keyboard.py @@ -26,7 +26,7 @@ log = logging.getLogger(__name__) # shortcuts or inserting text in the current output. The callback # is always reset to None afterwards (to resume the normal # processing of keys) -continuation_keys_callback = None # type: Optional[Callable] +continuation_keys_callback: Optional[Callable] = None def get_next_byte(s) -> Tuple[Optional[int], Optional[bytes]]: @@ -46,7 +46,7 @@ def get_next_byte(s) -> Tuple[Optional[int], Optional[bytes]]: def get_char_list(s) -> List[str]: - ret_list = [] # type: List[str] + ret_list: List[str] = [] while True: try: key = s.get_wch() diff --git a/poezio/logger.py b/poezio/logger.py index 14882f00..579639e3 100644 --- a/poezio/logger.py +++ b/poezio/logger.py @@ -76,7 +76,7 @@ class Logger: def __init__(self): self._roster_logfile = None # Optional[IO[Any]] # a dict of 'groupchatname': file-object (opened) - self._fds = {} # type: Dict[str, IO[Any]] + self._fds: Dict[str, IO[Any]] = {} def __del__(self): for opened_file in self._fds.values(): diff --git a/poezio/mam.py b/poezio/mam.py index 31ca2f0c..b40ddc4d 100644 --- a/poezio/mam.py +++ b/poezio/mam.py @@ -111,10 +111,10 @@ async def get_mam_iterator( if 'urn:xmpp:mam:2' not in iq['disco_info'].get_features(): raise NoMAMSupportException() - args = { + args: Dict[str, Any] = { 'iterator': True, 'reverse': reverse, - } # type: Dict[str, Any] + } if groupchat: args['jid'] = remote_jid diff --git a/poezio/multiuserchat.py b/poezio/multiuserchat.py index f4383176..71b5cceb 100644 --- a/poezio/multiuserchat.py +++ b/poezio/multiuserchat.py @@ -10,6 +10,8 @@ Add some facilities that are not available on the XEP_0045 slix plugin """ +from __future__ import annotations + from xml.etree import ElementTree as ET from typing import ( Callable, @@ -103,7 +105,7 @@ def change_subject(xmpp: ClientXMPP, jid: JID, subject: str) -> None: def change_nick( - core: 'Core', + core: Core, jid: JID, nick: str, status: Optional[str] = None, @@ -120,14 +122,14 @@ def change_nick( def join_groupchat( - core: 'Core', + core: Core, jid: JID, nick: str, passwd: str = '', status: Optional[str] = None, show: Optional[str] = None, seconds: Optional[int] = None, - tab: Optional['Tab'] = None + tab: Optional[Tab] = None ) -> None: xmpp = core.xmpp stanza = xmpp.make_presence( diff --git a/poezio/pep.py b/poezio/pep.py index 52cc4cd5..dde97ed6 100644 --- a/poezio/pep.py +++ b/poezio/pep.py @@ -5,7 +5,7 @@ extracted directly from the XEP from typing import Dict -MOODS = { +MOODS: Dict[str, str] = { 'afraid': 'Afraid', 'amazed': 'Amazed', 'angry': 'Angry', @@ -86,9 +86,9 @@ MOODS = { 'undefined': 'Undefined', 'weak': 'Weak', 'worried': 'Worried' -} # type: Dict[str, str] +} -ACTIVITIES = { +ACTIVITIES: Dict[str, Dict[str, str]] = { 'doing_chores': { 'category': 'Doing_chores', 'buying_groceries': 'Buying groceries', @@ -204,4 +204,4 @@ ACTIVITIES = { 'studying': 'Studying', 'other': 'Other', } -} # type: Dict[str, Dict[str, str]] +} diff --git a/poezio/plugin_e2ee.py b/poezio/plugin_e2ee.py index 9d1d4903..a0856957 100644 --- a/poezio/plugin_e2ee.py +++ b/poezio/plugin_e2ee.py @@ -97,30 +97,30 @@ class E2EEPlugin(BasePlugin): #: Encryption name, used in command descriptions, and logs. At least one #: of `encryption_name` and `encryption_short_name` must be set. - encryption_name = None # type: Optional[str] + encryption_name: Optional[str] = None #: Encryption short name, used as command name, and also to display #: encryption status in a tab. At least one of `encryption_name` and #: `encryption_short_name` must be set. - encryption_short_name = None # type: Optional[str] + encryption_short_name: Optional[str] = None #: Required. https://xmpp.org/extensions/xep-0380.html. - eme_ns = None # type: Optional[str] + eme_ns: Optional[str] = None #: Used to figure out what messages to attempt decryption for. Also used #: in combination with `tag_whitelist` to avoid removing encrypted tags #: before sending. - encrypted_tags = None # type: Optional[List[Tuple[str, str]]] + encrypted_tags: Optional[List[Tuple[str, str]]] = None # Static map, to be able to limit to one encryption mechanism per tab at a # time - _enabled_tabs = {} # type: Dict[JID, Callable] + _enabled_tabs: Dict[JID, Callable] = {} # Tabs that support this encryption mechanism - supported_tab_types = tuple() # type: Tuple[ChatTabs] + supported_tab_types: Tuple[ChatTabs] = tuple() # States for each remote entity - trust_states = {'accepted': set(), 'rejected': set()} # type: Dict[str, Set[str]] + trust_states: Dict[str, Set[str]] = {'accepted': set(), 'rejected': set()} def init(self): self._all_trust_states = self.trust_states['accepted'].union( @@ -229,7 +229,7 @@ class E2EEPlugin(BasePlugin): return "" def _toggle_tab(self, _input: str) -> None: - jid = self.api.current_tab().jid # type: JID + jid: JID = self.api.current_tab().jid if self._encryption_enabled(jid): del self._enabled_tabs[jid] @@ -394,7 +394,7 @@ class E2EEPlugin(BasePlugin): # Find who to encrypt to. If in a groupchat this can be multiple JIDs. # It is possible that we are not able to find a jid (e.g., semi-anon # MUCs). Let the plugin decide what to do with this information. - jids = [message['to']] # type: Optional[List[JID]] + jids: Optional[List[JID]] = [message['to']] tab = self.core.tabs.by_jid(message['to']) if tab is None: # When does that ever happen? log.debug('Attempting to encrypt a message to \'%s\' ' diff --git a/poezio/tabs/adhoc_commands_list.py b/poezio/tabs/adhoc_commands_list.py index b62166b0..3b6bc1db 100644 --- a/poezio/tabs/adhoc_commands_list.py +++ b/poezio/tabs/adhoc_commands_list.py @@ -16,8 +16,8 @@ log = logging.getLogger(__name__) class AdhocCommandsListTab(ListTab): - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} def __init__(self, core, jid): ListTab.__init__( diff --git a/poezio/tabs/basetabs.py b/poezio/tabs/basetabs.py index e4cdc3af..3907d7bc 100644 --- a/poezio/tabs/basetabs.py +++ b/poezio/tabs/basetabs.py @@ -13,6 +13,8 @@ This module also defines ChatTabs, the parent class for all tabs revolving around chats. """ +from __future__ import annotations + import copy import logging import string @@ -111,13 +113,13 @@ SHOW_NAME = { class Tab: - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} # Placeholder values, set on resize height = 1 width = 1 - def __init__(self, core: 'Core'): + def __init__(self, core: Core): self.core = core self.nb = 0 if not hasattr(self, 'name'): @@ -133,7 +135,7 @@ class Tab: self.commands = {} # and their own commands @property - def size(self) -> 'SizeManager': + def size(self) -> SizeManager: return self.core.size @staticmethod @@ -196,7 +198,7 @@ class Tab: self._state = 'normal' @staticmethod - def initial_resize(scr: '_CursesWindow'): + def initial_resize(scr: _CursesWindow): Tab.height, Tab.width = scr.getmaxyx() windows.base_wins.TAB_WIN = scr @@ -479,8 +481,8 @@ class ChatTab(Tab): Also, ^M is already bound to on_enter And also, add the /say command """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} message_type = 'chat' def __init__(self, core, jid: Union[JID, str]): @@ -492,7 +494,7 @@ class ChatTab(Tab): self._jid = jid #: Is the tab currently requesting MAM data? self.query_status = False - self._name = jid.full # type: Optional[str] + self._name: Optional[str] = jid.full self.text_win = windows.TextWin() self.directed_presence = None self._text_buffer = TextBuffer() @@ -532,7 +534,7 @@ class ChatTab(Tab): desc='Fix the last message with whatever you want.', shortdesc='Correct the last message.', completion=self.completion_correct) - self.chat_state = None # type: Optional[str] + self.chat_state: Optional[str] = None self.update_commands() self.update_keys() diff --git a/poezio/tabs/bookmarkstab.py b/poezio/tabs/bookmarkstab.py index eb390bd3..c4fdadd3 100644 --- a/poezio/tabs/bookmarkstab.py +++ b/poezio/tabs/bookmarkstab.py @@ -21,15 +21,15 @@ class BookmarksTab(Tab): A tab displaying lines of bookmarks, each bookmark having a 4 widgets to set the jid/password/autojoin/storage method """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} def __init__(self, core, bookmarks: BookmarkList): Tab.__init__(self, core) self.name = "Bookmarks" self.bookmarks = bookmarks - self.new_bookmarks = [] # type: List[Bookmark] - self.removed_bookmarks = [] # type: List[Bookmark] + self.new_bookmarks: List[Bookmark] = [] + self.removed_bookmarks: List[Bookmark] = [] self.header_win = windows.ColumnHeaderWin( ('name', 'room@server/nickname', 'password', 'autojoin', 'storage')) diff --git a/poezio/tabs/confirmtab.py b/poezio/tabs/confirmtab.py index c13de4a6..89d0daf5 100644 --- a/poezio/tabs/confirmtab.py +++ b/poezio/tabs/confirmtab.py @@ -13,8 +13,8 @@ log = logging.getLogger(__name__) class ConfirmTab(Tab): - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} def __init__(self, core, diff --git a/poezio/tabs/conversationtab.py b/poezio/tabs/conversationtab.py index 5950e4cb..5d62fa5f 100644 --- a/poezio/tabs/conversationtab.py +++ b/poezio/tabs/conversationtab.py @@ -38,9 +38,9 @@ class ConversationTab(OneToOneTab): The tab containing a normal conversation (not from a MUC) Must not be instantiated, use Static or Dynamic version only. """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] - additional_information = {} # type: Dict[str, Callable[[str], str]] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} + additional_information: Dict[str, Callable[[str], str]] = {} message_type = 'chat' def __init__(self, core, jid): @@ -377,8 +377,8 @@ class DynamicConversationTab(ConversationTab): bad idea so it has been removed. Only one DynamicConversationTab can be opened for a given jid. """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} def __init__(self, core, jid, resource=None): self.locked_resource = None @@ -447,8 +447,8 @@ class StaticConversationTab(ConversationTab): A conversation tab associated with one Full JID. It cannot be locked to an different resource or unlocked. """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} def __init__(self, core, jid): ConversationTab.__init__(self, core, jid) diff --git a/poezio/tabs/data_forms.py b/poezio/tabs/data_forms.py index f4ed63e5..8e13a84c 100644 --- a/poezio/tabs/data_forms.py +++ b/poezio/tabs/data_forms.py @@ -17,8 +17,8 @@ class DataFormsTab(Tab): A tab containing various window type, displaying a form that the user needs to fill. """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} def __init__(self, core, form, on_cancel, on_send, kwargs): Tab.__init__(self, core) diff --git a/poezio/tabs/listtab.py b/poezio/tabs/listtab.py index 87e7d9f4..3489eb9c 100644 --- a/poezio/tabs/listtab.py +++ b/poezio/tabs/listtab.py @@ -18,8 +18,8 @@ log = logging.getLogger(__name__) class ListTab(Tab): - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} def __init__(self, core, name, help_message, header_text, cols): """Parameters: diff --git a/poezio/tabs/muclisttab.py b/poezio/tabs/muclisttab.py index 4c1e492f..f6b3fc35 100644 --- a/poezio/tabs/muclisttab.py +++ b/poezio/tabs/muclisttab.py @@ -20,8 +20,8 @@ class MucListTab(ListTab): A tab listing rooms from a specific server, displaying various information, scrollable, and letting the user join them, etc """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} def __init__(self, core, server): ListTab.__init__(self, core, server.full, "“j”: join room.", diff --git a/poezio/tabs/muctab.py b/poezio/tabs/muctab.py index 182e7145..88fb5419 100644 --- a/poezio/tabs/muctab.py +++ b/poezio/tabs/muctab.py @@ -7,6 +7,8 @@ It keeps track of many things such as part/joins, maintains an user list, and updates private tabs when necessary. """ +from __future__ import annotations + import asyncio import bisect import curses @@ -78,38 +80,38 @@ class MucTab(ChatTab): It contains a userlist, an input, a topic, an information and a chat zone """ message_type = 'groupchat' - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable[..., Any]] - additional_information = {} # type: Dict[str, Callable[[str], str]] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable[..., Any]] = {} + additional_information: Dict[str, Callable[[str], str]] = {} lagged = False - def __init__(self, core: 'Core', jid: JID, nick: str, password: Optional[str] = None) -> None: + def __init__(self, core: Core, jid: JID, nick: str, password: Optional[str] = None) -> None: ChatTab.__init__(self, core, jid) self.joined = False self._state = 'disconnected' # our nick in the MUC self.own_nick = nick # self User object - self.own_user = None # type: Optional[User] + self.own_user: Optional[User] = None self.password = password # buffered presences - self.presence_buffer = [] # type: List[Presence] + self.presence_buffer: List[Presence] = [] # userlist - self.users = [] # type: List[User] + self.users: List[User] = [] # private conversations - self.privates = [] # type: List[Tab] + self.privates: List[Tab] = [] self.topic = '' self.topic_from = '' # Self ping event, so we can cancel it when we leave the room - self.self_ping_event = None # type: Optional[timed_events.DelayedEvent] + self.self_ping_event: Optional[timed_events.DelayedEvent] = None # UI stuff self.topic_win = windows.Topic() self.v_separator = windows.VerticalSeparator() self.user_win = windows.UserList() self.info_header = windows.MucInfoWin() - self.input = windows.MessageInput() # type: windows.MessageInput + self.input: windows.MessageInput = windows.MessageInput() # List of ignored users - self.ignores = [] # type: List[User] + self.ignores: List[User] = [] # keys self.register_keys() self.update_keys() @@ -149,14 +151,14 @@ class MucTab(ChatTab): """ del MucTab.additional_information[plugin_name] - def cancel_config(self, form: 'Form') -> None: + def cancel_config(self, form: Form) -> None: """ The user do not want to send their config, send an iq cancel """ asyncio.ensure_future(self.core.xmpp['xep_0045'].cancel_config(self.jid.bare)) self.core.close_tab() - def send_config(self, form: 'Form') -> None: + def send_config(self, form: Form) -> None: """ The user sends their config to the server """ @@ -1406,7 +1408,7 @@ class MucTab(ChatTab): /configure """ - def on_form_received(form: 'Form') -> None: + def on_form_received(form: Form) -> None: if not form: self.core.information( 'Could not retrieve the configuration form', 'Error') diff --git a/poezio/tabs/privatetab.py b/poezio/tabs/privatetab.py index f29e302c..54b9a15d 100644 --- a/poezio/tabs/privatetab.py +++ b/poezio/tabs/privatetab.py @@ -35,10 +35,10 @@ class PrivateTab(OneToOneTab): """ The tab containing a private conversation (someone from a MUC) """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} message_type = 'chat' - additional_information = {} # type: Dict[str, Callable[[str], str]] + additional_information: Dict[str, Callable[[str], str]] = {} def __init__(self, core, jid, nick): OneToOneTab.__init__(self, core, jid) diff --git a/poezio/tabs/rostertab.py b/poezio/tabs/rostertab.py index 072e0776..15a59455 100644 --- a/poezio/tabs/rostertab.py +++ b/poezio/tabs/rostertab.py @@ -36,8 +36,8 @@ class RosterInfoTab(Tab): """ A tab, split in two, containing the roster and infos """ - plugin_commands = {} # type: Dict[str, Command] - plugin_keys = {} # type: Dict[str, Callable] + plugin_commands: Dict[str, Command] = {} + plugin_keys: Dict[str, Callable] = {} def __init__(self, core): Tab.__init__(self, core) diff --git a/poezio/text_buffer.py b/poezio/text_buffer.py index 6ef8e3d4..89bae3a2 100644 --- a/poezio/text_buffer.py +++ b/poezio/text_buffer.py @@ -8,6 +8,8 @@ Each text buffer can be linked to multiple windows, that will be rendered independently by their TextWins. """ +from __future__ import annotations + import logging log = logging.getLogger(__name__) @@ -62,23 +64,23 @@ class TextBuffer: if messages_nb_limit is None: messages_nb_limit = cast(int, config.get('max_messages_in_memory')) - self._messages_nb_limit = messages_nb_limit # type: int + self._messages_nb_limit: int = messages_nb_limit # Message objects - self.messages = [] # type: List[BaseMessage] + self.messages: List[BaseMessage] = [] # COMPAT: Correction id -> Original message id. - self.correction_ids = {} # type: Dict[str, str] + self.correction_ids: Dict[str, str] = {} # we keep track of one or more windows # so we can pass the new messages to them, as they are added, so # they (the windows) can build the lines from the new message - self._windows = [] # type: List[TextWin] + self._windows: List[TextWin] = [] def add_window(self, win) -> None: self._windows.append(win) def find_last_gap_muc(self) -> Optional[HistoryGap]: """Find the last known history gap contained in buffer""" - leave = None # type:Optional[Tuple[int, BaseMessage]] - join = None # type:Optional[Tuple[int, BaseMessage]] + leave: Optional[Tuple[int, BaseMessage]] = None + join: Optional[Tuple[int, BaseMessage]] = None for i, item in enumerate(reversed(self.messages)): if isinstance(item, MucOwnLeaveMessage): leave = (len(self.messages) - i - 1, item) @@ -250,7 +252,7 @@ class TextBuffer: new_id: str, highlight: bool = False, time: Optional[datetime] = None, - user: Optional['User'] = None, + user: Optional[User] = None, jid: Optional[str] = None) -> Message: """ Correct a message in a text buffer. diff --git a/poezio/theming.py b/poezio/theming.py index 7752fe15..6245a48d 100755 --- a/poezio/theming.py +++ b/poezio/theming.py @@ -291,7 +291,7 @@ class Theme: (224, -1), (225, -1), (226, -1), (227, -1)] # XEP-0392 consistent color generation palette placeholder # it’s generated on first use when accessing the ccg_palette property - CCG_PALETTE = None # type: Optional[Dict[float, int]] + CCG_PALETTE: Optional[Dict[float, int]] = None CCG_Y = 0.5**0.45 # yapf: enable @@ -393,8 +393,7 @@ theme = Theme() # Each time we use a color tuple, we check if it has already been used. # If not we create a new color_pair and keep it in that dict, to use it # the next time. -curses_colors_dict = { -} # type: Dict[Union[Tuple[int, int], Tuple[int, int, str]], int] +curses_colors_dict: Dict[Union[Tuple[int, int], Tuple[int, int, str]], int] = {} # yapf: disable @@ -418,7 +417,7 @@ table_256_to_16 = [ ] # yapf: enable -load_path = [] # type: List[str] +load_path: List[str] = [] def color_256_to_16(color): diff --git a/poezio/timed_events.py b/poezio/timed_events.py index cd7659e2..3354443a 100644 --- a/poezio/timed_events.py +++ b/poezio/timed_events.py @@ -32,11 +32,11 @@ class DelayedEvent: :param function callback: The handler that will be executed. :param args: Optional arguments passed to the handler. """ - self.callback = callback # type: Callable - self.args = args # type: Tuple[Any, ...] - self.delay = delay # type: Union[int, float] + self.callback: Callable = callback + self.args: Tuple[Any, ...] = args + self.delay: Union[int, float] = delay # An asyncio handler, as returned by call_later() or call_at() - self.handler = None # type: Optional[Handle] + self.handler: Optional[Handle] = None class TimedEvent(DelayedEvent): diff --git a/poezio/ui/render.py b/poezio/ui/render.py index f37ea39e..13e493f1 100644 --- a/poezio/ui/render.py +++ b/poezio/ui/render.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import curses from datetime import ( @@ -52,7 +54,7 @@ LinePos = Tuple[int, int] def generate_lines(lines: List[LinePos], msg: BaseMessage, default_color: str = '') -> List[Line]: line_objects = [] - attrs = [] # type: List[str] + attrs: List[str] = [] prepend = default_color if default_color else '' for line in lines: saved = Line( @@ -114,7 +116,7 @@ def build_xmllog(msg: XMLLog, width: int, timestamp: bool, nick_size: int = 10) @singledispatch -def write_pre(msg: BaseMessage, win: 'Win', with_timestamps: bool, nick_size: int) -> int: +def write_pre(msg: BaseMessage, win: Win, with_timestamps: bool, nick_size: int) -> int: """Write the part before text (only the timestamp)""" if with_timestamps: return PreMessageHelpers.write_time(win, False, msg.time) @@ -122,7 +124,7 @@ def write_pre(msg: BaseMessage, win: 'Win', with_timestamps: bool, nick_size: in @write_pre.register(Message) -def write_pre_message(msg: Message, win: 'Win', with_timestamps: bool, nick_size: int) -> int: +def write_pre_message(msg: Message, win: Win, with_timestamps: bool, nick_size: int) -> int: """Write the part before the body: - timestamp (short or long) - ack/nack @@ -165,7 +167,7 @@ def write_pre_message(msg: Message, win: 'Win', with_timestamps: bool, nick_size @write_pre.register(XMLLog) -def write_pre_xmllog(msg: XMLLog, win: 'Win', with_timestamps: bool, nick_size: int) -> int: +def write_pre_xmllog(msg: XMLLog, win: Win, with_timestamps: bool, nick_size: int) -> int: """Write the part before the stanza (timestamp + IN/OUT)""" offset = 0 if with_timestamps: @@ -186,7 +188,7 @@ def write_pre_xmllog(msg: XMLLog, win: 'Win', with_timestamps: bool, nick_size: class PreMessageHelpers: @staticmethod - def write_revisions(buffer: 'Win', msg: Message) -> int: + def write_revisions(buffer: Win, msg: Message) -> int: if msg.revisions: color = get_theme().COLOR_REVISIONS_MESSAGE with buffer.colored_text(color=color): @@ -195,7 +197,7 @@ class PreMessageHelpers: return 0 @staticmethod - def write_ack(buffer: 'Win') -> int: + def write_ack(buffer: Win) -> int: theme = get_theme() color = theme.COLOR_CHAR_ACK with buffer.colored_text(color=color): @@ -204,7 +206,7 @@ class PreMessageHelpers: return poopt.wcswidth(theme.CHAR_ACK_RECEIVED) + 1 @staticmethod - def write_nack(buffer: 'Win') -> int: + def write_nack(buffer: Win) -> int: theme = get_theme() color = theme.COLOR_CHAR_NACK with buffer.colored_text(color=color): @@ -213,7 +215,7 @@ class PreMessageHelpers: return poopt.wcswidth(theme.CHAR_NACK) + 1 @staticmethod - def write_nickname(buffer: 'Win', nickname: str, color, highlight=False) -> None: + def write_nickname(buffer: Win, nickname: str, color, highlight=False) -> None: """ Write the nickname, using the user's color and return the number of written characters @@ -231,7 +233,7 @@ class PreMessageHelpers: buffer.addstr(nickname) @staticmethod - def write_time(buffer: 'Win', history: bool, time: datetime) -> int: + def write_time(buffer: Win, history: bool, time: datetime) -> int: """ Write the date on the yth line of the window """ diff --git a/poezio/user.py b/poezio/user.py index 5aed5031..3724d229 100644 --- a/poezio/user.py +++ b/poezio/user.py @@ -41,12 +41,12 @@ class User: deterministic=True, color='') -> None: # The oldest possible time - self.last_talked = datetime(1, 1, 1) # type: datetime + self.last_talked: datetime = datetime(1, 1, 1) self.update(affiliation, show, status, role) self.change_nick(nick) - self.jid = jid # type: JID - self.chatstate = None # type: Optional[str] - self.color = (1, 1) # type: Tuple[int, int] + self.jid: JID = jid + self.chatstate: Optional[str] = None + self.color: Tuple[int, int] = (1, 1) if color != '': self.change_color(color, deterministic) else: diff --git a/poezio/windows/base_wins.py b/poezio/windows/base_wins.py index f371c106..1347553a 100644 --- a/poezio/windows/base_wins.py +++ b/poezio/windows/base_wins.py @@ -7,7 +7,9 @@ the text window, the roster window, etc. A Tab (see the poezio.tabs module) is composed of multiple Windows """ -TAB_WIN = None # type: _CursesWindow +from __future__ import annotations + +TAB_WIN: _CursesWindow = None import logging log = logging.getLogger(__name__) @@ -41,7 +43,7 @@ class Win: __slots__ = ('_win', 'height', 'width', 'y', 'x') def __init__(self) -> None: - self._win = None # type: _CursesWindow + self._win: _CursesWindow = None self.height, self.width = 0, 0 def _resize(self, height: int, width: int, y: int, x: int) -> None: diff --git a/poezio/windows/bookmark_forms.py b/poezio/windows/bookmark_forms.py index d538e6a2..f1e737fd 100644 --- a/poezio/windows/bookmark_forms.py +++ b/poezio/windows/bookmark_forms.py @@ -161,7 +161,7 @@ class BookmarksWin(Win): self._current_input = 0 self.current_horizontal_input = 0 self._bookmarks = list(bookmarks) - self.lines = [] # type: List[Tuple[BookmarkNameInput, BookmarkJIDInput, BookmarkPasswordInput, BookmarkAutojoinWin, BookmarkMethodInput]] + self.lines: List[Tuple[BookmarkNameInput, BookmarkJIDInput, BookmarkPasswordInput, BookmarkAutojoinWin, BookmarkMethodInput]] = [] for bookmark in sorted(self._bookmarks, key=lambda x: str(x.jid)): self.lines.append((BookmarkNameInput(bookmark), BookmarkJIDInput(bookmark), diff --git a/poezio/windows/image.py b/poezio/windows/image.py index ebecb5ad..79ecf7d9 100644 --- a/poezio/windows/image.py +++ b/poezio/windows/image.py @@ -69,10 +69,10 @@ class ImageWin(Win): __slots__ = ('_image', '_display_avatar') def __init__(self) -> None: - self._image = None # type: Optional[Image.Image] + self._image: Optional[Image.Image] = None Win.__init__(self) if config.get('image_use_half_blocks'): - self._display_avatar = self._display_avatar_half_blocks # type: Callable[[int, int], None] + self._display_avatar: Callable[[int, int], None] = self._display_avatar_half_blocks else: self._display_avatar = self._display_avatar_full_blocks diff --git a/poezio/windows/info_wins.py b/poezio/windows/info_wins.py index c3975c8c..cd775e33 100644 --- a/poezio/windows/info_wins.py +++ b/poezio/windows/info_wins.py @@ -3,6 +3,8 @@ Module defining all the "info wins", ie the bar which is on top of the info buffer in normal tabs """ +from __future__ import annotations + from typing import Optional, Dict, TYPE_CHECKING, Any import logging @@ -272,9 +274,9 @@ class MucInfoWin(InfoWin): def refresh( self, - room: 'MucTab', - window: Optional['TextWin'] = None, - user: Optional['User'] = None, + room: MucTab, + window: Optional[TextWin] = None, + user: Optional[User] = None, information: Optional[Dict[str, Any]] = None ) -> None: log.debug('Refresh: %s', self.__class__.__name__) diff --git a/poezio/windows/input_placeholders.py b/poezio/windows/input_placeholders.py index 4d414636..3ec57583 100644 --- a/poezio/windows/input_placeholders.py +++ b/poezio/windows/input_placeholders.py @@ -23,7 +23,7 @@ class HelpText(Win): def __init__(self, text: str = '') -> None: Win.__init__(self) - self.txt = text # type: str + self.txt: str = text def refresh(self, txt: Optional[str] = None) -> None: log.debug('Refresh: %s', self.__class__.__name__) diff --git a/poezio/windows/inputs.py b/poezio/windows/inputs.py index 5cca8803..b3601913 100644 --- a/poezio/windows/inputs.py +++ b/poezio/windows/inputs.py @@ -41,7 +41,7 @@ class Input(Win): # it easy cut and paste text between various input def __init__(self) -> None: - self.key_func = { + self.key_func: Dict[str, Callable] = { "KEY_LEFT": self.key_left, "KEY_RIGHT": self.key_right, "KEY_END": self.key_end, @@ -66,7 +66,7 @@ class Input(Win): '^?': self.key_backspace, "M-^?": self.delete_word, # '^J': self.add_line_break, - } # type: Dict[str, Callable] + } Win.__init__(self) self.text = '' self.pos = 0 # The position of the “cursor” in the text @@ -76,8 +76,8 @@ class Input(Win): # screen self.on_input = DEFAULT_ON_INPUT # callback called on any key pressed self.color = None # use this color on addstr - self.last_completion = None # type: Optional[str] - self.hit_list = [] # type: List[str] + self.last_completion: Optional[str] = None + self.hit_list: List[str] = [] def on_delete(self) -> None: """ @@ -592,7 +592,7 @@ class HistoryInput(Input): """ __slots__ = ('help_message', 'histo_pos', 'current_completed', 'search') - history = [] # type: List[str] + history: List[str] = [] def __init__(self) -> None: Input.__init__(self) @@ -603,7 +603,7 @@ class HistoryInput(Input): self.search = False if config.get('separate_history'): # pylint: disable=assigning-non-slot - self.history = [] # type: List[str] + self.history: List[str] = [] def toggle_search(self) -> None: if self.help_message: @@ -680,7 +680,7 @@ class MessageInput(HistoryInput): Also letting the user enter colors or other text markups """ # The history is common to all MessageInput - history = [] # type: List[str] + history: List[str] = [] def __init__(self) -> None: HistoryInput.__init__(self) @@ -726,7 +726,7 @@ class CommandInput(HistoryInput): HelpMessage when a command is started The on_input callback """ - history = [] # type: List[str] + history: List[str] = [] def __init__(self, help_message: str, on_abort, on_success, on_input=None) -> None: HistoryInput.__init__(self) diff --git a/poezio/windows/list.py b/poezio/windows/list.py index c427a79e..1c5d834f 100644 --- a/poezio/windows/list.py +++ b/poezio/windows/list.py @@ -24,10 +24,10 @@ class ListWin(Win): def __init__(self, columns: Dict[str, int], with_headers: bool = True) -> None: Win.__init__(self) - self._columns = columns # type: Dict[str, int] - self._columns_sizes = {} # type: Dict[str, int] + self._columns: Dict[str, int] = columns + self._columns_sizes: Dict[str, int] = {} self.sorted_by = (None, None) # for example: ('name', '↑') - self.lines = [] # type: List[str] + self.lines: List[str] = [] self._selected_row = 0 self._starting_pos = 0 # The column number from which we start the refresh @@ -173,7 +173,7 @@ class ColumnHeaderWin(Win): def __init__(self, columns: List[str]) -> None: Win.__init__(self) self._columns = columns - self._columns_sizes = {} # type: Dict[str, int] + self._columns_sizes: Dict[str, int] = {} self._column_sel = '' self._column_order = '' self._column_order_asc = False diff --git a/poezio/windows/misc.py b/poezio/windows/misc.py index 6c04b814..8739db0c 100644 --- a/poezio/windows/misc.py +++ b/poezio/windows/misc.py @@ -37,7 +37,7 @@ class SimpleTextWin(Win): def __init__(self, text) -> None: Win.__init__(self) self._text = text - self.built_lines = [] # type: List[str] + self.built_lines: List[str] = [] def rebuild_text(self) -> None: """ diff --git a/poezio/windows/muc.py b/poezio/windows/muc.py index 951940e1..05fe683e 100644 --- a/poezio/windows/muc.py +++ b/poezio/windows/muc.py @@ -33,7 +33,7 @@ class UserList(Win): def __init__(self) -> None: Win.__init__(self) self.pos = 0 - self.cache = [] # type: List[CachedUser] + self.cache: List[CachedUser] = [] def scroll_up(self) -> bool: self.pos += self.height - 1 diff --git a/poezio/windows/roster_win.py b/poezio/windows/roster_win.py index 2efdd324..c4a1c30b 100644 --- a/poezio/windows/roster_win.py +++ b/poezio/windows/roster_win.py @@ -26,8 +26,8 @@ class RosterWin(Win): Win.__init__(self) self.pos = 0 # cursor position in the contact list self.start_pos = 1 # position of the start of the display - self.selected_row = None # type: Optional[Row] - self.roster_cache = [] # type: List[Row] + self.selected_row: Optional[Row] = None + self.roster_cache: List[Row] = [] @property def roster_len(self) -> int: diff --git a/poezio/windows/text_win.py b/poezio/windows/text_win.py index 2cb75271..ac60dee7 100644 --- a/poezio/windows/text_win.py +++ b/poezio/windows/text_win.py @@ -30,17 +30,17 @@ class TextWin(Win): Win.__init__(self) if lines_nb_limit is None: lines_nb_limit = config.get('max_lines_in_memory') - self.lines_nb_limit = lines_nb_limit # type: int + self.lines_nb_limit: int = lines_nb_limit self.pos = 0 # Each new message is built and kept here. # on resize, we rebuild all the messages - self.built_lines = [] # type: List[Union[None, Line]] + self.built_lines: List[Union[None, Line]] = [] self.lock = False - self.lock_buffer = [] # type: List[Union[None, Line]] - self.separator_after = None # type: Optional[Line] + self.lock_buffer: List[Union[None, Line]] = [] + self.separator_after: Optional[Line] = None # the Lines of the highlights in that buffer - self.highlights = [] # type: List[Line] + self.highlights: List[Line] = [] # the current HL position in that list NaN means that we’re not on # an hl. -1 is a valid position (it's before the first hl of the # list. i.e the separator, in the case where there’s no hl before diff --git a/poezio/xdg.py b/poezio/xdg.py index 0b63998c..d4ce0538 100644 --- a/poezio/xdg.py +++ b/poezio/xdg.py @@ -15,11 +15,11 @@ from os import environ from typing import Dict # $HOME has already been checked to not be None in test_env(). -DEFAULT_PATHS = { +DEFAULT_PATHS: Dict[str, Path] = { 'XDG_CONFIG_HOME': Path.home() / '.config', 'XDG_DATA_HOME': Path.home() / '.local' / 'share', 'XDG_CACHE_HOME': Path.home() / '.cache', -} # type: Dict[str, Path] +} def _get_directory(variable: str) -> Path: diff --git a/poezio/xhtml.py b/poezio/xhtml.py index 0b234c29..ebbffb02 100644 --- a/poezio/xhtml.py +++ b/poezio/xhtml.py @@ -32,7 +32,7 @@ digits = '0123456789' # never trust the modules XHTML_NS = 'http://www.w3.org/1999/xhtml' # HTML named colors -colors = { +colors: Dict[str, int] = { 'aliceblue': 231, 'antiquewhite': 231, 'aqua': 51, @@ -180,7 +180,7 @@ colors = { 'whitesmoke': 255, 'yellow': 226, 'yellowgreen': 149 -} # type: Dict[str, int] +} whitespace_re = re.compile(r'\s+') @@ -302,11 +302,11 @@ def get_hash(data: bytes) -> str: class XHTMLHandler(sax.ContentHandler): def __init__(self, force_ns=False, tmp_image_dir: Optional[Path] = None) -> None: - self.builder = [] # type: List[str] - self.formatting = [] # type: List[str] - self.attrs = [] # type: List[Dict[str, str]] - self.list_state = [] # type: List[Union[str, int]] - self.cids = {} # type: Dict[str, Optional[str]] + self.builder: List[str] = [] + self.formatting: List[str] = [] + self.attrs: List[Dict[str, str]] = [] + self.list_state: List[Union[str, int]] = [] + self.cids: Dict[str, Optional[str]] = {} self.is_pre = False self.a_start = 0 # do not care about xhtml-in namespace @@ -512,7 +512,7 @@ def convert_simple_to_full_colors(text: str) -> str: return re.sub(xhtml_simple_attr_re, add_curly_bracket, text) -number_to_color_names = { +number_to_color_names: Dict[int, str] = { 1: 'red', 2: 'green', 3: 'yellow', @@ -520,7 +520,7 @@ number_to_color_names = { 5: 'violet', 6: 'turquoise', 7: 'white' -} # type: Dict[int, str] +} def format_inline_css(_dict: Dict[str, str]) -> str: @@ -535,7 +535,7 @@ def poezio_colors_to_html(string: str) -> str: # Maintain a list of the current css attributes used # And check if a tag is open (by design, we only open # spans tag, and they cannot be nested. - current_attrs = {} # type: Dict[str, str] + current_attrs: Dict[str, str] = {} tag_open = False next_attr_char = string.find('\x19') build = ["

"] -- cgit v1.2.3