From d3fb6acd198748fd44d69a9ee79df0a1fcf15881 Mon Sep 17 00:00:00 2001 From: Akuli Date: Sat, 5 Jun 2021 15:22:06 +0300 Subject: [PATCH 01/38] configure black and isort --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..460c34082 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[tool.black] +line-length = 100 + +[tool.isort] +profile = "black" +multi_line_output = 3 From b8d7b427eb6f18977ffd898c06e6a4998a12ae28 Mon Sep 17 00:00:00 2001 From: Akuli Date: Sat, 5 Jun 2021 15:23:28 +0300 Subject: [PATCH 02/38] apply black -S and isort --- more_plugins/pythonprompt.py | 20 +-- more_plugins/terminal.py | 13 +- more_plugins/tetris.py | 75 +++++------ porcupine/__init__.py | 4 +- porcupine/__main__.py | 116 +++++++++++------ porcupine/_ipc.py | 8 +- porcupine/_logs.py | 17 ++- porcupine/_state.py | 17 +-- porcupine/menubar.py | 58 ++++++--- porcupine/pluginloader.py | 11 +- porcupine/plugins/aboutdialog.py | 20 +-- porcupine/plugins/autocomplete.py | 139 ++++++++++---------- porcupine/plugins/autoindent.py | 8 +- porcupine/plugins/black.py | 23 ++-- porcupine/plugins/comment_block.py | 15 ++- porcupine/plugins/directory_tree.py | 90 +++++++++---- porcupine/plugins/drop_to_open.py | 10 +- porcupine/plugins/editorconfig.py | 14 ++- porcupine/plugins/filetypes.py | 85 ++++++++----- porcupine/plugins/find.py | 48 ++++--- porcupine/plugins/fold.py | 15 ++- porcupine/plugins/gotoline.py | 6 +- porcupine/plugins/highlight.py | 30 +++-- porcupine/plugins/indent_block.py | 2 +- porcupine/plugins/keybindings.py | 6 +- porcupine/plugins/langserver.py | 182 +++++++++++++++------------ porcupine/plugins/linenumbers.py | 20 +-- porcupine/plugins/longlinemarker.py | 13 +- porcupine/plugins/mergeconflict.py | 51 +++++--- porcupine/plugins/minimap.py | 28 +++-- porcupine/plugins/pastebin.py | 59 ++++++--- porcupine/plugins/pluginmanager.py | 47 ++++--- porcupine/plugins/poppingtabs.py | 63 ++++++---- porcupine/plugins/pygments_style.py | 14 ++- porcupine/plugins/reload.py | 2 +- porcupine/plugins/restart.py | 3 +- porcupine/plugins/run/__init__.py | 16 ++- porcupine/plugins/run/no_terminal.py | 27 ++-- porcupine/plugins/run/terminal.py | 39 +++--- porcupine/plugins/statusbar.py | 5 +- porcupine/plugins/tab_closing.py | 18 ++- porcupine/plugins/tab_order.py | 23 ++-- porcupine/plugins/ttk_themes.py | 8 +- porcupine/plugins/underlines.py | 7 +- porcupine/plugins/urls.py | 41 +++--- porcupine/plugins/welcome.py | 16 ++- porcupine/plugins/windowicon.py | 2 +- porcupine/settings.py | 115 +++++++++-------- porcupine/tabs.py | 108 ++++++++++------ porcupine/textwidget.py | 96 ++++++++------ porcupine/utils.py | 138 ++++++++++++-------- tests/conftest.py | 12 +- tests/test_about_dialog.py | 4 +- tests/test_comment_block_plugin.py | 2 +- tests/test_directory_tree_plugin.py | 13 +- tests/test_editorconfig_plugin.py | 27 ++-- tests/test_filetypes_plugin.py | 44 ++++--- tests/test_find_plugin.py | 138 +++++++++++--------- tests/test_fold_plugin.py | 2 +- tests/test_highlight_plugin.py | 2 +- tests/test_indent_dedent.py | 47 ++++--- tests/test_logs.py | 8 +- tests/test_matching_paren_plugin.py | 19 ++- tests/test_menubar.py | 5 +- tests/test_mergeconflict_plugin.py | 14 ++- tests/test_pastebin_plugin.py | 19 +-- tests/test_settings.py | 17 ++- tests/test_sort_plugin.py | 24 +++- tests/test_statusbar_plugin.py | 2 +- tests/test_tabs.py | 4 +- tests/test_text_contentchanged.py | 29 +++-- tests/test_urls_plugin.py | 19 +-- tests/test_utils.py | 19 ++- 73 files changed, 1509 insertions(+), 952 deletions(-) diff --git a/more_plugins/pythonprompt.py b/more_plugins/pythonprompt.py index ec71896b6..e17477a24 100644 --- a/more_plugins/pythonprompt.py +++ b/more_plugins/pythonprompt.py @@ -24,7 +24,6 @@ def _tupleindex(index: str) -> Tuple[int, int]: class PythonPrompt: - def __init__(self, textwidget: tkinter.Text, close_callback: Callable[[], None]): self.widget = textwidget self.close_callback = close_callback @@ -37,8 +36,12 @@ def __init__(self, textwidget: tkinter.Text, close_callback: Callable[[], None]) # without -u python buffers stdout and everything is one enter # press late :( see python --help self.process = subprocess.Popen( - [sys.executable, '-i', '-u'], stdin=subprocess.PIPE, - stdout=subprocess.PIPE, stderr=subprocess.STDOUT, bufsize=0) + [sys.executable, '-i', '-u'], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=0, + ) # the queuer thread is a daemon thread because it makes exiting # porcupine easier and interrupting it isn't a problem @@ -88,7 +91,7 @@ def _on_return(self, junk: object) -> utils.BreakOrNone: # this needs to return 'break' to allow pressing enter with the # cursor anywhere on the line - text = self.widget.get('%d.%d' % end_of_output, 'end') # ends with \n + text = self.widget.get('%d.%d' % end_of_output, 'end') # ends with \n self.widget.insert('end', '\n') self.widget.mark_set('insert', 'end') assert self.process.stdin is not None @@ -121,16 +124,16 @@ def _queue_clearer(self) -> None: self.close_callback() else: self.widget.insert( - 'end', "\n\n***********************\n" + - f"the subprocess exited with code {value!r}") + 'end', + "\n\n***********************\n" + f"the subprocess exited with code {value!r}", + ) self.widget.config(state='disabled') return assert state == 'output' and isinstance(value, bytes) if sys.platform == 'win32': value = value.replace(b'\r\n', b'\n') - self.widget.insert( - 'end-1c', value.decode('utf-8', errors='replace'), 'output') + self.widget.insert('end-1c', value.decode('utf-8', errors='replace'), 'output') self.widget.see('end-1c') # we got something, let's try again as soon as possible @@ -138,7 +141,6 @@ def _queue_clearer(self) -> None: class PromptTab(tabs.Tab): - def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self.title_choices = ["Interactive Prompt"] diff --git a/more_plugins/terminal.py b/more_plugins/terminal.py index 14d5faa8e..9df6440c6 100644 --- a/more_plugins/terminal.py +++ b/more_plugins/terminal.py @@ -14,14 +14,15 @@ def start_xterm() -> None: tab = tabs.Tab(get_tab_manager()) tab.title_choices = ["Terminal"] content = tkinter.Frame(tab, container=True) - content.pack(fill='both', expand=True) # FIXME: doesn't stretch correctly? + content.pack(fill='both', expand=True) # FIXME: doesn't stretch correctly? try: process = subprocess.Popen(['xterm', '-into', str(content.winfo_id())]) except FileNotFoundError: - messagebox.showerror("xterm not found", ( - "Seems like xterm is not installed. " + - "Please install it and try again.")) + messagebox.showerror( + "xterm not found", + ("Seems like xterm is not installed. " + "Please install it and try again."), + ) return def terminal_wants_to_exit(junk: object) -> None: @@ -38,8 +39,8 @@ def setup() -> None: if get_tab_manager().tk.call('tk', 'windowingsystem') != 'x11': # TODO: more noob-friendly "u have the wrong os lel" message? messagebox.showerror( - "Unsupported windowing system", - "Sorry, the terminal plugin only works on X11 :(") + "Unsupported windowing system", "Sorry, the terminal plugin only works on X11 :(" + ) return menubar.get_menu("Tools").add_command(label="Terminal", command=start_xterm) diff --git a/more_plugins/tetris.py b/more_plugins/tetris.py index 900011449..defa866d5 100644 --- a/more_plugins/tetris.py +++ b/more_plugins/tetris.py @@ -12,7 +12,7 @@ WIDTH = 10 HEIGHT = 20 -SCALE = 20 # each square is 20x20 pixels +SCALE = 20 # each square is 20x20 pixels Point = Tuple[int, int] ShapeLetter = str @@ -22,26 +22,13 @@ # shape is added to it # y is like in math, so more y means higher SHAPES: Dict[ShapeLetter, List[Point]] = { - 'I': [(0, 2), - (0, 1), - (0, 0), - (0, -1)], - 'O': [(-1, 0), (0, 0), - (-1, 1), (0, 1)], - 'T': [(-1, 0), (0, 0), (1, 0), - (0, -1)], - 'L': [(0, 1), - (0, 0), - (0, -1), (1, -1)], - 'J': [ - (0, 1), # noqa - (0, 0), - (-1, -1), (0, -1)], # noqa - 'S': [ - (0, 1), (1, 1), # noqa - (-1, 0), (0, 0)], # noqa - 'Z': [(-1, 1), (0, 1), - (0, 0), (1, 0)], + 'I': [(0, 2), (0, 1), (0, 0), (0, -1)], + 'O': [(-1, 0), (0, 0), (-1, 1), (0, 1)], + 'T': [(-1, 0), (0, 0), (1, 0), (0, -1)], + 'L': [(0, 1), (0, 0), (0, -1), (1, -1)], + 'J': [(0, 1), (0, 0), (-1, -1), (0, -1)], # noqa # noqa + 'S': [(0, 1), (1, 1), (-1, 0), (0, 0)], # noqa # noqa + 'Z': [(-1, 1), (0, 1), (0, 0), (1, 0)], } @@ -67,7 +54,7 @@ def get_coords(self) -> Iterator[Point]: yield (self.x + shapex, self.y + shapey) def bumps(self, x: int, y: int) -> bool: - return (x not in range(WIDTH) or y < 0 or (x, y) in self._game.frozen_squares) + return x not in range(WIDTH) or y < 0 or (x, y) in self._game.frozen_squares def _move(self, deltax: int, deltay: int) -> bool: for x, y in self.get_coords(): @@ -99,7 +86,6 @@ def rotate(self) -> bool: class NonRotatingBlock(Block): - def rotate(self) -> bool: return False @@ -138,14 +124,14 @@ class Game: def __init__(self) -> None: self.frozen_squares: Dict[Point, str] = {} - self.score = 0 # each new block increments score - self.add_block() # creates self.moving_block - self.paused = False # only used outside this class definition + self.score = 0 # each new block increments score + self.add_block() # creates self.moving_block + self.paused = False # only used outside this class definition @property def level(self) -> int: # levels start at 1 - return self.score//30 + 1 # noqa + return self.score // 30 + 1 # noqa @property def delay(self) -> int: @@ -178,8 +164,7 @@ def delete_full_lines(self) -> None: # this is much easier with a nested list lines = [] for y in range(HEIGHT): - line = [self.frozen_squares.pop((x, y), None) - for x in range(WIDTH)] + line = [self.frozen_squares.pop((x, y), None) for x in range(WIDTH)] if None in line: # it's not full, we can keep it lines.append(line) @@ -215,7 +200,6 @@ def game_over(self) -> bool: class TetrisTab(tabs.Tab): - def __init__(self, manager: tabs.TabManager) -> None: super().__init__(manager) self.title_choices = ["Tetris"] @@ -223,18 +207,25 @@ def __init__(self, manager: tabs.TabManager) -> None: # the takefocus thing is important, it's hard to bind the keys # correctly without it self._canvas = tkinter.Canvas( - self, width=WIDTH*SCALE, height=HEIGHT*SCALE, - relief='ridge', bg='black', takefocus=True) + self, + width=WIDTH * SCALE, + height=HEIGHT * SCALE, + relief='ridge', + bg='black', + takefocus=True, + ) self._canvas.pack() self._score_label = ttk.Label(self, justify='center') self._score_label.pack() - help_text = ' '.join(f''' + help_text = ' '.join( + f''' You can move the blocks with arrow keys. Press {utils.get_binding('<>')} to pause or {utils.get_binding('<>')} to start a new game. - '''.split()) + '''.split() + ) ttk.Label( self, text=help_text, @@ -254,8 +245,8 @@ def __init__(self, manager: tabs.TabManager) -> None: left = x * SCALE bottom = (HEIGHT - y) * SCALE self._canvas_content[(x, y)] = self._canvas.create_rectangle( - left, bottom - SCALE, left + SCALE, bottom, - outline='black', fill='black') + left, bottom - SCALE, left + SCALE, bottom, outline='black', fill='black' + ) self._timeout_id: Optional[str] = None self._game_over_id: Optional[int] = None @@ -298,7 +289,8 @@ def _refresh(self) -> None: self._canvas.itemconfig(item_id, fill=color) self._score_label['text'] = f"Score {self._game.score}, level {self._game.level}\n" + ( - "Paused" if self._game.paused else "") + "Paused" if self._game.paused else "" + ) def new_game(self) -> None: if self._timeout_id is not None: @@ -322,10 +314,11 @@ def _on_timeout(self) -> None: font_size = 18 self._canvas.create_rectangle( - 0, centery - font_size, self._canvas['width'], centery + font_size, - fill='black') + 0, centery - font_size, self._canvas['width'], centery + font_size, fill='black' + ) self._game_over_id = self._canvas.create_text( - centerx, centery, + centerx, + centery, anchor='center', text="Game Over :(", font=('', font_size, 'bold'), @@ -336,7 +329,7 @@ def _on_timeout(self) -> None: self._timeout_id = self.after(self._game.delay, self._on_timeout) def get_state(self) -> Game: - return self._game # it should be picklable + return self._game # it should be picklable @classmethod def from_state(cls, manager: tabs.TabManager, game: Game) -> 'TetrisTab': diff --git a/porcupine/__init__.py b/porcupine/__init__.py index 6a2ec5c50..dc87dbd50 100644 --- a/porcupine/__init__.py +++ b/porcupine/__init__.py @@ -11,7 +11,7 @@ import appdirs # type: ignore[import] -version_info = (0, 92, 3) # this is updated with scripts/release.py +version_info = (0, 92, 3) # this is updated with scripts/release.py __version__ = '%d.%d.%d' % version_info __author__ = 'Akuli' __copyright__ = 'Copyright (c) 2017-2021 Akuli' @@ -29,7 +29,7 @@ get_main_window = _state.get_main_window get_parsed_args = _state.get_parsed_args -get_paned_window = _state.get_paned_window # TODO: document this +get_paned_window = _state.get_paned_window # TODO: document this get_tab_manager = _state.get_tab_manager filedialog_kwargs = _state.filedialog_kwargs quit = _state.quit diff --git a/porcupine/__main__.py b/porcupine/__main__.py index 88158bf6c..ab1172c59 100644 --- a/porcupine/__main__.py +++ b/porcupine/__main__.py @@ -5,23 +5,34 @@ # imports spread across multiple lines to keep sane line lengths and make it greppable from porcupine import __version__ as porcupine_version -from porcupine import (_logs, _state, dirs, get_main_window, get_tab_manager, menubar, pluginloader, plugins, settings, - tabs) +from porcupine import ( + _logs, + _state, + dirs, + get_main_window, + get_tab_manager, + menubar, + pluginloader, + plugins, + settings, + tabs, +) log = logging.getLogger(__name__) # see the --help action in argparse's source code class _PrintPlugindirAction(argparse.Action): - - def __init__( # type: ignore[no-untyped-def] - self, option_strings, dest=argparse.SUPPRESS, - default=argparse.SUPPRESS, help=None): - super().__init__(option_strings=option_strings, dest=dest, - default=default, nargs=0, help=help) - - def __call__( # type: ignore[no-untyped-def] - self, parser, namespace, values, option_string=None): + def __init__( # type: ignore[no-untyped-def] + self, option_strings, dest=argparse.SUPPRESS, default=argparse.SUPPRESS, help=None + ): + super().__init__( + option_strings=option_strings, dest=dest, default=default, nargs=0, help=help + ) + + def __call__( # type: ignore[no-untyped-def] + self, parser, namespace, values, option_string=None + ): print(f"You can install plugins here:\n\n {plugins.__path__[0]}\n") parser.exit() @@ -48,35 +59,59 @@ def main() -> None: add_help=False, # help in step 1 wouldn't show options added by plugins ) parser.add_argument( - '--version', action='version', + '--version', + action='version', version=f"Porcupine {porcupine_version}", - help="display the Porcupine version number and exit") + help="display the Porcupine version number and exit", + ) parser.add_argument( - '--print-plugindir', action=_PrintPlugindirAction, - help="find out where to install custom plugins") + '--print-plugindir', + action=_PrintPlugindirAction, + help="find out where to install custom plugins", + ) verbose_group = parser.add_mutually_exclusive_group() verbose_group.add_argument( - '-v', '--verbose', dest='verbose_logger', action='store_const', const='', - help=("print all logging messages to stderr, only warnings and errors " - "are printed by default (but all messages always go to a log " - "file as well)")) + '-v', + '--verbose', + dest='verbose_logger', + action='store_const', + const='', + help=( + "print all logging messages to stderr, only warnings and errors " + "are printed by default (but all messages always go to a log " + "file as well)" + ), + ) verbose_group.add_argument( '--verbose-logger', - help=("increase verbosity for just one logger only, e.g. " - "--verbose-logger=porcupine.plugins.highlight " - "to see messages from highlight plugin")) + help=( + "increase verbosity for just one logger only, e.g. " + "--verbose-logger=porcupine.plugins.highlight " + "to see messages from highlight plugin" + ), + ) plugingroup = parser.add_argument_group("plugin loading options") plugingroup.add_argument( - '--no-plugins', action='store_false', dest='use_plugins', - help=("don't load any plugins, this is useful for " - "understanding how much can be done with plugins")) + '--no-plugins', + action='store_false', + dest='use_plugins', + help=( + "don't load any plugins, this is useful for " + "understanding how much can be done with plugins" + ), + ) plugingroup.add_argument( - '--without-plugins', metavar='PLUGINS', default='', - help=("don't load PLUGINS (see --print-plugindir), " - "e.g. --without-plugins=highlight disables syntax highlighting, " - "multiple plugin names can be given comma-separated")) + '--without-plugins', + metavar='PLUGINS', + default='', + help=( + "don't load PLUGINS (see --print-plugindir), " + "e.g. --without-plugins=highlight disables syntax highlighting, " + "multiple plugin names can be given comma-separated" + ), + ) args_parsed_in_first_step, junk = parser.parse_known_args() @@ -101,15 +136,22 @@ def main() -> None: parser.add_argument('--help', action='help', help="show this message") pluginloader.run_setup_argument_parser_functions(parser) parser.add_argument( - 'files', metavar='FILES', nargs=argparse.ZERO_OR_MORE, - help="open these files when Porcupine starts, - means stdin") + 'files', + metavar='FILES', + nargs=argparse.ZERO_OR_MORE, + help="open these files when Porcupine starts, - means stdin", + ) plugingroup.add_argument( - '--shuffle-plugins', action='store_true', - help=("respect setup_before and setup_after, but otherwise setup the " - "plugins in a random order instead of sorting by name " - "alphabetically, useful for making sure that your plugin's " - "setup_before and setup_after define everything needed; usually " - "plugins are not shuffled in order to make the UI consistent")) + '--shuffle-plugins', + action='store_true', + help=( + "respect setup_before and setup_after, but otherwise setup the " + "plugins in a random order instead of sorting by name " + "alphabetically, useful for making sure that your plugin's " + "setup_before and setup_after define everything needed; usually " + "plugins are not shuffled in order to make the UI consistent" + ), + ) args = parser.parse_args() _state.init(args) diff --git a/porcupine/_ipc.py b/porcupine/_ipc.py index 31b18b296..8c8a44a5c 100644 --- a/porcupine/_ipc.py +++ b/porcupine/_ipc.py @@ -37,8 +37,7 @@ def send(objects: List[Any]) -> None: client.send(message) -def _listener2queue(listener: connection.Listener, - object_queue: queue.Queue[Any]) -> None: +def _listener2queue(listener: connection.Listener, object_queue: queue.Queue[Any]) -> None: """Accept connections. Receive and queue objects.""" while True: try: @@ -70,8 +69,9 @@ def session() -> Iterator['queue.Queue[Any]']: with connection.Listener() as listener: with _ADDRESS_FILE.open('w') as file: print(listener.address, file=file) - thread = threading.Thread(target=_listener2queue, - args=[listener, message_queue], daemon=True) + thread = threading.Thread( + target=_listener2queue, args=[listener, message_queue], daemon=True + ) thread.start() yield message_queue diff --git a/porcupine/_logs.py b/porcupine/_logs.py index 06b1a14b7..877bcf3de 100644 --- a/porcupine/_logs.py +++ b/porcupine/_logs.py @@ -37,9 +37,9 @@ def _remove_old_logs() -> None: def _run_command(command: str) -> None: try: - output = subprocess.check_output( - shlex.split(command), stderr=subprocess.STDOUT - ).decode('utf-8', errors='replace') + output = subprocess.check_output(shlex.split(command), stderr=subprocess.STDOUT).decode( + 'utf-8', errors='replace' + ) log.info(f"output from '{command}':\n{output}") except FileNotFoundError as e: log.info(f"cannot run '{command}': {e}") @@ -50,8 +50,7 @@ def _run_command(command: str) -> None: def _open_log_file() -> TextIO: timestamp = datetime.now().strftime(FILENAME_FIRST_PART_FORMAT) filenames = ( - f'{timestamp}.txt' if i == 0 else f'{timestamp}_{i}.txt' - for i in itertools.count() + f'{timestamp}.txt' if i == 0 else f'{timestamp}_{i}.txt' for i in itertools.count() ) for filename in filenames: try: @@ -73,8 +72,9 @@ def setup(verbose_logger: Optional[str]) -> None: file_handler = logging.StreamHandler(log_file) file_handler.setLevel(logging.DEBUG) - file_handler.setFormatter(logging.Formatter( - '[%(asctime)s] %(name)s %(levelname)s: %(message)s')) + file_handler.setFormatter( + logging.Formatter('[%(asctime)s] %(name)s %(levelname)s: %(message)s') + ) handlers.append(file_handler) if sys.stderr is not None: @@ -85,8 +85,7 @@ def setup(verbose_logger: Optional[str]) -> None: else: print_handler.setLevel(logging.DEBUG) print_handler.addFilter(logging.Filter(verbose_logger)) - print_handler.setFormatter(logging.Formatter( - '%(name)s %(levelname)s: %(message)s')) + print_handler.setFormatter(logging.Formatter('%(name)s %(levelname)s: %(message)s')) handlers.append(print_handler) # don't know why level must be specified here diff --git a/porcupine/_state.py b/porcupine/_state.py index 5c2f882af..735da94d7 100644 --- a/porcupine/_state.py +++ b/porcupine/_state.py @@ -18,7 +18,9 @@ filedialog_kwargs: Dict[str, Any] = {} -def _log_tkinter_error(exc: Type[BaseException], val: BaseException, tb: types.TracebackType) -> Any: +def _log_tkinter_error( + exc: Type[BaseException], val: BaseException, tb: types.TracebackType +) -> Any: log.error("Error in tkinter callback", exc_info=(exc, val, tb)) @@ -34,15 +36,17 @@ def init(args: Any) -> None: log.debug("init() starts") _parsed_args = args - _root = tkinter.Tk(className="Porcupine") # class name shows up in my alt+tab list + _root = tkinter.Tk(className="Porcupine") # class name shows up in my alt+tab list _root.protocol('WM_DELETE_WINDOW', quit) _root.report_callback_exception = _log_tkinter_error _paned_window = ttk.Panedwindow(_root, orient='horizontal') settings.remember_divider_positions(_paned_window, 'main_panedwindow_dividers', [250]) - _root.bind('<>', ( - lambda event: get_paned_window().event_generate('<>') - ), add=True) + _root.bind( + '<>', + (lambda event: get_paned_window().event_generate('<>')), + add=True, + ) _paned_window.pack(fill='both', expand=True) _tab_manager = tabs.TabManager(_paned_window) @@ -59,8 +63,7 @@ def get_main_window() -> tkinter.Tk: def get_tab_manager() -> tabs.TabManager: - """Return the :class:`porcupine.tabs.TabManager` widget in the main window. - """ # these are on a separate line because pep-8 line length + """Return the :class:`porcupine.tabs.TabManager` widget in the main window.""" # these are on a separate line because pep-8 line length if _tab_manager is None: raise RuntimeError("Porcupine is not running") return _tab_manager diff --git a/porcupine/menubar.py b/porcupine/menubar.py index c08db8a5c..c41a7e426 100644 --- a/porcupine/menubar.py +++ b/porcupine/menubar.py @@ -57,7 +57,9 @@ def _fix_text_widget_bindings(event: tkinter.Event[tkinter.Misc]) -> None: if virtual_event.startswith('< None: def _find_item(menu: tkinter.Menu, label: str) -> Optional[int]: last_index = menu.index('end') - if last_index is not None: # menu not empty + if last_index is not None: # menu not empty for index in range(last_index + 1): - if menu.type(index) in _MENU_ITEM_TYPES_WITH_LABEL and menu.entrycget(index, 'label') == label: + if ( + menu.type(index) in _MENU_ITEM_TYPES_WITH_LABEL + and menu.entrycget(index, 'label') == label + ): return index return None @@ -129,7 +134,9 @@ def add_config_file_button(path: pathlib.Path) -> None: """ get_menu("Settings/Config Files").add_command( label=path.name, - command=(lambda: get_tab_manager().add_tab(tabs.FileTab.open_file(get_tab_manager(), path))), + command=( + lambda: get_tab_manager().add_tab(tabs.FileTab.open_file(get_tab_manager(), path)) + ), ) @@ -142,7 +149,7 @@ def _walk_menu_contents( menu = get_menu(None) last_index = menu.index('end') - if last_index is not None: # menu not empty + if last_index is not None: # menu not empty for index in range(last_index + 1): if menu.type(index) == 'cascade': submenu: tkinter.Menu = menu.nametowidget(menu.entrycget(index, 'menu')) @@ -153,7 +160,9 @@ def _walk_menu_contents( yield (path, menu, index) -def _menu_event_handler(menu: tkinter.Menu, index: int, junk: tkinter.Event[tkinter.Misc]) -> utils.BreakOrNone: +def _menu_event_handler( + menu: tkinter.Menu, index: int, junk: tkinter.Event[tkinter.Misc] +) -> utils.BreakOrNone: menu.invoke(index) return 'break' @@ -209,7 +218,9 @@ def update_keyboard_shortcuts() -> None: _update_shortcuts_for_opening_submenus() -def set_enabled_based_on_tab(path: str, callback: Callable[[Optional[tabs.Tab]], bool]) -> Callable[..., None]: +def set_enabled_based_on_tab( + path: str, callback: Callable[[Optional[tabs.Tab]], bool] +) -> Callable[..., None]: """Use this for disabling menu items depending on the currently selected tab. When the selected :class:`~porcupine.tabs.Tab` changes, ``callback`` will @@ -240,6 +251,7 @@ def setup(): ignores all arguments given to it, which makes using it with ``.bind()`` easier. """ + def update_enabledness(*junk: object) -> None: tab = get_tab_manager().select() menu = get_menu(path.rsplit('/', 1)[0] if '/' in path else None) @@ -257,7 +269,7 @@ def update_enabledness(*junk: object) -> None: def _fill_menus_with_default_stuff() -> None: # Make sure to get the order of menus right: # File, Edit, , Help - get_menu("Help") # handled specially in get_menu + get_menu("Help") # handled specially in get_menu get_menu("File") get_menu("Edit") @@ -324,9 +336,15 @@ def change_font_size(how: Literal['bigger', 'smaller', 'reset']) -> None: settings.set_('font_size', size) - get_menu("View").add_command(label="Bigger Font", command=functools.partial(change_font_size, 'bigger')) - get_menu("View").add_command(label="Smaller Font", command=functools.partial(change_font_size, 'smaller')) - get_menu("View").add_command(label="Reset Font Size", command=functools.partial(change_font_size, 'reset')) + get_menu("View").add_command( + label="Bigger Font", command=functools.partial(change_font_size, 'bigger') + ) + get_menu("View").add_command( + label="Smaller Font", command=functools.partial(change_font_size, 'smaller') + ) + get_menu("View").add_command( + label="Reset Font Size", command=functools.partial(change_font_size, 'reset') + ) set_enabled_based_on_tab("View/Bigger Font", (lambda tab: tab is not None)) set_enabled_based_on_tab("View/Smaller Font", (lambda tab: tab is not None)) set_enabled_based_on_tab("View/Reset Font Size", (lambda tab: tab is not None)) @@ -339,7 +357,19 @@ def add_link(menu_path: str, label: str, url: str) -> None: # TODO: porcupine starring button # TODO: does ##learnpython IRC link still work? add_link("Help", "Porcupine Wiki", "https://github.com/Akuli/porcupine/wiki") - add_link("Help", "Report a problem or request a feature", "https://github.com/Akuli/porcupine/issues/new") - add_link("Help/Python", "Free help chat", "https://kiwiirc.com/nextclient/irc.libera.chat/##learnpython") - add_link("Help/Python", "My Python tutorial", "https://github.com/Akuli/python-tutorial/blob/master/README.md") + add_link( + "Help", + "Report a problem or request a feature", + "https://github.com/Akuli/porcupine/issues/new", + ) + add_link( + "Help/Python", + "Free help chat", + "https://kiwiirc.com/nextclient/irc.libera.chat/##learnpython", + ) + add_link( + "Help/Python", + "My Python tutorial", + "https://github.com/Akuli/python-tutorial/blob/master/README.md", + ) add_link("Help/Python", "Official documentation", "https://docs.python.org/") diff --git a/porcupine/pluginloader.py b/porcupine/pluginloader.py index 9e65e2779..029ebf6d6 100644 --- a/porcupine/pluginloader.py +++ b/porcupine/pluginloader.py @@ -64,6 +64,7 @@ class Status(enum.Enum): should be set up before *A*, then *A*, *B* and *C* will all fail with ``CIRCULAR_DEPENDENCY_ERROR``. """ + LOADING = enum.auto() ACTIVE = enum.auto() DISABLED_BY_SETTINGS = enum.auto() @@ -94,6 +95,7 @@ class PluginInfo: * If *status* is ``CIRCULAR_DEPENDENCY_ERROR``, then *error* is a user-readable one-line message. """ + name: str came_with_porcupine: bool status: Status @@ -120,7 +122,7 @@ def _run_setup_argument_parser_function(info: PluginInfo, parser: argparse.Argum info.error = traceback.format_exc() duration = time.perf_counter() - start - log.debug("ran %s.setup_argument_parser() in %.3f milliseconds", info.name, duration*1000) + log.debug("ran %s.setup_argument_parser() in %.3f milliseconds", info.name, duration * 1000) def _import_plugin(info: PluginInfo) -> None: @@ -147,7 +149,7 @@ def _import_plugin(info: PluginInfo) -> None: _dependencies[dep_info].add(info) duration = time.perf_counter() - start - log.debug("imported porcupine.plugins.%s in %.3f milliseconds", info.name, duration*1000) + log.debug("imported porcupine.plugins.%s in %.3f milliseconds", info.name, duration * 1000) # Remember to generate <> when this succeeds @@ -166,7 +168,7 @@ def _run_setup(info: PluginInfo) -> None: info.status = Status.ACTIVE duration = time.perf_counter() - start - log.debug("ran %s.setup() in %.3f milliseconds", info.name, duration*1000) + log.debug("ran %s.setup() in %.3f milliseconds", info.name, duration * 1000) def _did_plugin_come_with_porcupine(finder: object) -> bool: @@ -274,8 +276,7 @@ def can_setup_while_running(info: PluginInfo) -> bool: return False return not any( - info.status == Status.ACTIVE and info in deps - for info, deps in _dependencies.items() + info.status == Status.ACTIVE and info in deps for info, deps in _dependencies.items() ) diff --git a/porcupine/plugins/aboutdialog.py b/porcupine/plugins/aboutdialog.py index 9a191df26..ae1e162fe 100644 --- a/porcupine/plugins/aboutdialog.py +++ b/porcupine/plugins/aboutdialog.py @@ -46,13 +46,11 @@ def show_huge_logo(junk: object = None) -> None: class _AboutDialogContent(ttk.Frame): - def __init__(self, *args: Any, **kwargs: Any): super().__init__(*args, **kwargs) # TODO: calculate height automagically, instead of hard-coding - self._textwidget = textwidget.create_passive_text_widget( - self, width=60, height=25) + self._textwidget = textwidget.create_passive_text_widget(self, width=60, height=25) self._textwidget.pack(fill='both', expand=True, padx=5, pady=5) # http://effbot.org/zone/tkinter-text-hyperlink.htm @@ -66,12 +64,13 @@ def __init__(self, *args: Any, **kwargs: Any): for text_chunk in _BORING_TEXT.strip().split('\n\n'): self._add_minimal_markdown(text_chunk) self._textwidget.insert('end', '\n\n') - self._add_directory_link("Porcupine is installed to", pathlib.Path(__file__).absolute().parent.parent.parent) + self._add_directory_link( + "Porcupine is installed to", pathlib.Path(__file__).absolute().parent.parent.parent + ) self._add_directory_link("You can install plugins to", pathlib.Path(plugins.__path__[0])) self._textwidget.config(state='disabled') - label = ttk.Label(self, image=images.get('logo-200x200'), - cursor='hand2') + label = ttk.Label(self, image=images.get('logo-200x200'), cursor='hand2') label.pack(anchor='e') utils.set_tooltip(label, "Click to view in full size") label.bind('', show_huge_logo, add=True) @@ -81,7 +80,7 @@ def _add_minimal_markdown(self, text: str) -> None: previous_end = 0 for link in re.finditer(r'\[(.+?)\]\((.+?)\)', text): - parts.append(text[previous_end:link.start()]) + parts.append(text[previous_end : link.start()]) parts.append(link) previous_end = link.end() parts.append(text[previous_end:]) @@ -93,8 +92,9 @@ def _add_minimal_markdown(self, text: str) -> None: # a link text, href = part.groups() tag = next(self._link_tag_names) - self._textwidget.tag_bind( # bindcheck: ignore - tag, '', functools.partial(self._open_link, href)) + self._textwidget.tag_bind( # bindcheck: ignore + tag, '', functools.partial(self._open_link, href) + ) self._textwidget.insert('end', text, ['link', tag]) def _add_directory_link(self, description: str, path: pathlib.Path) -> None: @@ -128,7 +128,7 @@ def show_about_dialog() -> None: content = _AboutDialogContent(dialog) content.pack(fill='both', expand=True) - content.update() # make sure that the winfo stuff works + content.update() # make sure that the winfo stuff works dialog.minsize(content.winfo_reqwidth(), content.winfo_reqheight()) dialog.title(f"About Porcupine {porcupine_version}") dialog.transient(get_main_window()) diff --git a/porcupine/plugins/autocomplete.py b/porcupine/plugins/autocomplete.py index 91f1e8a02..1834b7927 100644 --- a/porcupine/plugins/autocomplete.py +++ b/porcupine/plugins/autocomplete.py @@ -17,7 +17,7 @@ from porcupine import get_tab_manager, settings, tabs, textwidget, utils -setup_before = ['tabs2spaces'] # see tabs2spaces.py +setup_before = ['tabs2spaces'] # see tabs2spaces.py log = logging.getLogger(__name__) @@ -45,9 +45,8 @@ def _pack_with_scrollbar(widget: Union[ttk.Treeview, tkinter.Text]) -> ttk.Scrol def _calculate_popup_geometry(textwidget: tkinter.Text) -> str: bbox = textwidget.bbox('insert') - assert bbox is not None # cursor must be visible - (cursor_x, cursor_y, - cursor_width, cursor_height) = bbox + assert bbox is not None # cursor must be visible + (cursor_x, cursor_y, cursor_width, cursor_height) = bbox # make coordinates relative to screen cursor_x += textwidget.winfo_rootx() @@ -79,7 +78,6 @@ def _calculate_popup_geometry(textwidget: tkinter.Text) -> str: class _Popup: - def __init__(self) -> None: self._completion_list: Optional[List[Completion]] = None @@ -93,8 +91,7 @@ def __init__(self) -> None: # # I'm using Panedwindow here in case the PanedWindow alias is deleted # in a future version of python. - self._panedwindow = ttk.Panedwindow( - self.toplevel, orient='horizontal') + self._panedwindow = ttk.Panedwindow(self.toplevel, orient='horizontal') settings.remember_divider_positions(self._panedwindow, 'autocomplete_dividers', [200]) self._panedwindow.pack(fill='both', expand=True) @@ -103,14 +100,14 @@ def __init__(self) -> None: self._panedwindow.add(left_pane) self._panedwindow.add(right_pane) - self.treeview = ttk.Treeview( - left_pane, show='tree', selectmode='browse') + self.treeview = ttk.Treeview(left_pane, show='tree', selectmode='browse') self.treeview.bind('', self._on_mouse_move, add=True) self.treeview.bind('<>', self._on_select, add=True) self._left_scrollbar = _pack_with_scrollbar(self.treeview) self._doc_text = textwidget.create_passive_text_widget( - right_pane, width=50, height=15, wrap='word') + right_pane, width=50, height=15, wrap='word' + ) self._right_scrollbar = _pack_with_scrollbar(self._doc_text) self._resize_handle = ttk.Sizegrip(self.toplevel) @@ -135,12 +132,11 @@ def _on_anything_resized(self, junk: object) -> None: # state where it's completing but not showing. def is_completing(self) -> bool: - return (self._completion_list is not None) + return self._completion_list is not None def is_showing(self) -> bool: # don't know how only one of them could be mapped, checking to be sure - return bool(self.toplevel.winfo_ismapped() and - self._panedwindow.winfo_ismapped()) + return bool(self.toplevel.winfo_ismapped() and self._panedwindow.winfo_ismapped()) def _select_item(self, item_id: str) -> None: self.treeview.selection_set(item_id) @@ -158,7 +154,9 @@ def _get_selected_completion(self) -> Optional[Completion]: [the_id] = selected_ids return self._completion_list[int(the_id)] - def start_completing(self, completion_list: List[Completion], geometry: Optional[str] = None) -> None: + def start_completing( + self, completion_list: List[Completion], geometry: Optional[str] = None + ) -> None: if self.is_completing(): self.stop_completing(withdraw=False) @@ -167,8 +165,7 @@ def start_completing(self, completion_list: List[Completion], geometry: Optional if completion_list: for index, completion in enumerate(completion_list): # id=str(index) is used in the rest of this class - self.treeview.insert('', 'end', id=str(index), - text=completion.display_text) + self.treeview.insert('', 'end', id=str(index), text=completion.display_text) self._select_item('0') else: self._doc_text.config(state='normal') @@ -202,16 +199,14 @@ def select_previous(self) -> None: selected_ids = self.treeview.selection() if selected_ids: [the_id] = selected_ids - self._select_item( - self.treeview.prev(the_id) or self.treeview.get_children()[-1]) + self._select_item(self.treeview.prev(the_id) or self.treeview.get_children()[-1]) def select_next(self) -> None: assert self.is_completing() selected_ids = self.treeview.selection() if selected_ids: [the_id] = selected_ids - self._select_item( - self.treeview.next(the_id) or self.treeview.get_children()[0]) + self._select_item(self.treeview.next(the_id) or self.treeview.get_children()[0]) def on_page_up_down(self, event: tkinter.Event[tkinter.Misc]) -> utils.BreakOrNone: if not self.is_showing(): @@ -225,8 +220,7 @@ def on_arrow_key_up_down(self, event: tkinter.Event[tkinter.Misc]) -> utils.Brea if not self.is_showing(): return None - method = {'Up': self.select_previous, - 'Down': self.select_next}[event.keysym] + method = {'Up': self.select_previous, 'Down': self.select_next}[event.keysym] method() return 'break' @@ -263,7 +257,7 @@ class Response(utils.EventDataclass): def text_index_less_than(index1: str, index2: str) -> bool: tuple1 = tuple(map(int, index1.split('.'))) tuple2 = tuple(map(int, index2.split('.'))) - return (tuple1 < tuple2) + return tuple1 < tuple2 # stupid fallback @@ -274,11 +268,15 @@ def _all_words_in_file_completions(textwidget: tkinter.Text) -> List[Completion] replace_start = textwidget.index(f'insert - {len(before_cursor)} chars') replace_end = textwidget.index('insert') - counts = dict(collections.Counter([ - word - for word in re.findall(r'\w+', textwidget.get('1.0', 'end')) - if before_cursor.casefold() in word.casefold() - ])) + counts = dict( + collections.Counter( + [ + word + for word in re.findall(r'\w+', textwidget.get('1.0', 'end')) + if before_cursor.casefold() in word.casefold() + ] + ) + ) if counts.get(before_cursor, 0) == 1: del counts[before_cursor] @@ -296,7 +294,6 @@ def _all_words_in_file_completions(textwidget: tkinter.Text) -> List[Completion] class AutoCompleter: - def __init__(self, tab: tabs.FileTab) -> None: self._tab = tab self._orig_cursorpos: Optional[str] = None @@ -304,9 +301,11 @@ def __init__(self, tab: tabs.FileTab) -> None: self._waiting_for_response_id: Optional[int] = None self.popup = _Popup() utils.bind_with_data( - tab, '<>', + tab, + '<>', lambda event: self.receive_completions(event.data_class(Response)), - add=True) + add=True, + ) def _request_completions(self) -> None: log.debug("requesting completions") @@ -319,16 +318,21 @@ def _request_completions(self) -> None: if self._tab.bind('<>'): # an event handler is bound, use that - self._tab.event_generate('<>', data=Request( - id=the_id, - cursor_pos=self._orig_cursorpos, - )) + self._tab.event_generate( + '<>', + data=Request( + id=the_id, + cursor_pos=self._orig_cursorpos, + ), + ) else: # fall back to "all words in file" autocompleting - self.receive_completions(Response( - id=the_id, - completions=_all_words_in_file_completions(self._tab.textwidget), - )) + self.receive_completions( + Response( + id=the_id, + completions=_all_words_in_file_completions(self._tab.textwidget), + ) + ) def _user_wants_to_see_popup(self) -> bool: assert self._orig_cursorpos is not None @@ -343,9 +347,11 @@ def _user_wants_to_see_popup(self) -> bool: # # Moving the cursor forward to filter through the list is allowed as # long as the cursor stays on the same line. - return (self._tab.focus_get() == self._tab.textwidget and - current_line == initial_line and - current_column >= initial_column) + return ( + self._tab.focus_get() == self._tab.textwidget + and current_line == initial_line + and current_column >= initial_column + ) # this might not run for all requests if e.g. langserver not configured def receive_completions(self, response: Response) -> None: @@ -358,8 +364,8 @@ def receive_completions(self, response: Response) -> None: if self._user_wants_to_see_popup(): self.unfiltered_completions = response.completions self.popup.start_completing( - self._get_filtered_completions(), - _calculate_popup_geometry(self._tab.textwidget)) + self._get_filtered_completions(), _calculate_popup_geometry(self._tab.textwidget) + ) # this doesn't work perfectly. After get, getar_u matches # getchar_unlocked but getch_u doesn't. @@ -369,7 +375,8 @@ def _get_filtered_completions(self) -> List[Completion]: filter_text = self._tab.textwidget.get(self._orig_cursorpos, 'insert') return [ - completion for completion in self.unfiltered_completions + completion + for completion in self.unfiltered_completions if filter_text.lower() in completion.filter_text.lower() ] @@ -417,8 +424,8 @@ def _accept(self) -> None: assert self._orig_cursorpos is not None self._tab.textwidget.delete(self._orig_cursorpos, 'insert') self._tab.textwidget.replace( - completion.replace_start, completion.replace_end, - completion.replace_text) + completion.replace_start, completion.replace_end, completion.replace_text + ) self._waiting_for_response_id = None self._orig_cursorpos = None @@ -445,9 +452,9 @@ def on_any_key(self, event: tkinter.Event[tkinter.Misc]) -> None: self._tab.textwidget.after_idle(self._filter_through_completions) elif event.char in self._tab.settings.get('autocomplete_chars', List[str]): + def do_request() -> None: - if ((not self.popup.is_completing()) - and self._can_complete_here()): + if (not self.popup.is_completing()) and self._can_complete_here(): self._request_completions() # Tiny delay added to make sure that langserver's change events @@ -468,12 +475,11 @@ def _filter_through_completions(self) -> None: # if cursor has moved back more since requesting completions: User # has backspaced away a lot and likely doesn't want completions. - if text_index_less_than( - self._tab.textwidget.index('insert'), self._orig_cursorpos): + if text_index_less_than(self._tab.textwidget.index('insert'), self._orig_cursorpos): self._reject() return - self.popup.stop_completing(withdraw=False) # TODO: is this needed? + self.popup.stop_completing(withdraw=False) # TODO: is this needed? self.popup.start_completing(self._get_filtered_completions()) def on_enter(self, event: tkinter.Event[tkinter.Misc]) -> utils.BreakOrNone: @@ -498,33 +504,28 @@ def on_new_tab(tab: tabs.Tab) -> None: completer = AutoCompleter(tab) # no idea why backspace has to be bound separately - utils.bind_with_data( - tab.textwidget, '', completer.on_any_key, add=True) + utils.bind_with_data(tab.textwidget, '', completer.on_any_key, add=True) tab.textwidget.bind('', completer.on_any_key, add=True) utils.bind_tab_key(tab.textwidget, completer.on_tab, add=True) tab.textwidget.bind('', completer.on_enter, add=True) tab.textwidget.bind('', completer.on_escape, add=True) - tab.textwidget.bind( - '', completer.popup.on_page_up_down, add=True) - tab.textwidget.bind( - '', completer.popup.on_page_up_down, add=True) - tab.textwidget.bind( - '', completer.popup.on_arrow_key_up_down, add=True) - tab.textwidget.bind( - '', completer.popup.on_arrow_key_up_down, add=True) - completer.popup.treeview.bind( - '', (lambda event: completer._accept()), add=True) + tab.textwidget.bind('', completer.popup.on_page_up_down, add=True) + tab.textwidget.bind('', completer.popup.on_page_up_down, add=True) + tab.textwidget.bind('', completer.popup.on_arrow_key_up_down, add=True) + tab.textwidget.bind('', completer.popup.on_arrow_key_up_down, add=True) + completer.popup.treeview.bind('', (lambda event: completer._accept()), add=True) # avoid weird corner cases - tab.winfo_toplevel().bind( - '', (lambda event: completer._reject()), add=True) + tab.winfo_toplevel().bind('', (lambda event: completer._reject()), add=True) tab.textwidget.bind( # any mouse button - '