diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index adddd5d5..839681b9 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -26,3 +26,20 @@ jobs: with: skip: '*.po' ignore_words_list: ba,te,deltion + + mypy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install mypy + pip install -r requirements.txt + pip install urwid twisted watchdog "jedi >=0.16" babel "sphinx >=1.5" numpy + pip install types-backports types-requests types-setuptools types-toml types-pygments + - name: Check with mypy + # for now only run on a few files to avoid slipping backward + run: mypy diff --git a/bpython/__init__.py b/bpython/__init__.py index f9048afa..adc00c06 100644 --- a/bpython/__init__.py +++ b/bpython/__init__.py @@ -23,7 +23,7 @@ import os.path try: - from ._version import __version__ as version + from ._version import __version__ as version # type: ignore except ImportError: version = "unknown" diff --git a/bpython/args.py b/bpython/args.py index 79ddcc67..1ab61d26 100644 --- a/bpython/args.py +++ b/bpython/args.py @@ -21,12 +21,17 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# To gradually migrate to mypy we aren't setting these globally yet +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True + """ Module to handle command line argument parsing, for all front-ends. """ import argparse -from typing import Tuple +from typing import Tuple, List, Optional, NoReturn, Callable +import code import curtsies import cwcwidth import greenlet @@ -50,11 +55,11 @@ class ArgumentParserFailed(ValueError): class RaisingArgumentParser(argparse.ArgumentParser): - def error(self, msg): + def error(self, msg: str) -> NoReturn: raise ArgumentParserFailed() -def version_banner(base="bpython") -> str: +def version_banner(base: str = "bpython") -> str: return _("{} version {} on top of Python {} {}").format( base, __version__, @@ -67,7 +72,14 @@ def copyright_banner() -> str: return _("{} See AUTHORS.rst for details.").format(__copyright__) -def parse(args, extras=None, ignore_stdin=False) -> Tuple: +Options = Tuple[str, str, Callable[[argparse._ArgumentGroup], None]] + + +def parse( + args: Optional[List[str]], + extras: Options = None, + ignore_stdin: bool = False, +) -> Tuple: """Receive an argument list - if None, use sys.argv - parse all args and take appropriate action. Also receive optional extra argument: this should be a tuple of (title, description, callback) @@ -197,7 +209,7 @@ def callback(group): logger.info(f"curtsies: {curtsies.__version__}") logger.info(f"cwcwidth: {cwcwidth.__version__}") logger.info(f"greenlet: {greenlet.__version__}") - logger.info(f"pygments: {pygments.__version__}") + logger.info(f"pygments: {pygments.__version__}") # type: ignore logger.info(f"requests: {requests.__version__}") logger.info( "environment:\n{}".format( @@ -214,7 +226,9 @@ def callback(group): return Config(options.config), options, options.args -def exec_code(interpreter, args): +def exec_code( + interpreter: code.InteractiveInterpreter, args: List[str] +) -> None: """ Helper to execute code in a given interpreter, e.g. to implement the behavior of python3 [-i] file.py @@ -230,9 +244,10 @@ def exec_code(interpreter, args): old_argv, sys.argv = sys.argv, args sys.path.insert(0, os.path.abspath(os.path.dirname(args[0]))) spec = importlib.util.spec_from_loader("__console__", loader=None) + assert spec mod = importlib.util.module_from_spec(spec) sys.modules["__console__"] = mod - interpreter.locals.update(mod.__dict__) - interpreter.locals["__file__"] = args[0] + interpreter.locals.update(mod.__dict__) # type: ignore # TODO use a more specific type that has a .locals attribute + interpreter.locals["__file__"] = args[0] # type: ignore # TODO use a more specific type that has a .locals attribute interpreter.runsource(source, args[0], "exec") sys.argv = old_argv diff --git a/bpython/autocomplete.py b/bpython/autocomplete.py index 3ec2a457..fd91ea4a 100644 --- a/bpython/autocomplete.py +++ b/bpython/autocomplete.py @@ -21,6 +21,9 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# To gradually migrate to mypy we aren't setting these globally yet +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True import __main__ import abc @@ -33,12 +36,26 @@ import builtins from enum import Enum -from typing import Any, Dict, Iterator, List, Match, NoReturn, Set, Union, Tuple +from typing import ( + Any, + cast, + Dict, + Iterator, + List, + Match, + Optional, + Set, + Union, + Tuple, + Type, + Sequence, +) from . import inspection from . import line as lineparts from .line import LinePart from .lazyre import LazyReCompile from .simpleeval import safe_eval, evaluate_current_expression, EvaluationError +from .importcompletion import ModuleGatherer # Autocomplete modes @@ -49,7 +66,7 @@ class AutocompleteModes(Enum): FUZZY = "fuzzy" @classmethod - def from_string(cls, value) -> Union[Any, None]: + def from_string(cls, value: str) -> Optional[Any]: if value.upper() in cls.__members__: return cls.__members__[value.upper()] return None @@ -166,7 +183,7 @@ def after_last_dot(name: str) -> str: return name.rstrip(".").rsplit(".")[-1] -def few_enough_underscores(current, match) -> bool: +def few_enough_underscores(current: str, match: str) -> bool: """Returns whether match should be shown based on current if current is _, True if match starts with 0 or 1 underscore @@ -180,19 +197,19 @@ def few_enough_underscores(current, match) -> bool: return not match.startswith("_") -def method_match_none(word, size, text) -> bool: +def method_match_none(word: str, size: int, text: str) -> bool: return False -def method_match_simple(word, size, text) -> bool: +def method_match_simple(word: str, size: int, text: str) -> bool: return word[:size] == text -def method_match_substring(word, size, text) -> bool: +def method_match_substring(word: str, size: int, text: str) -> bool: return text in word -def method_match_fuzzy(word, size, text) -> Union[Match, None]: +def method_match_fuzzy(word: str, size: int, text: str) -> Optional[Match]: s = r".*%s.*" % ".*".join(list(text)) return re.search(s, word) @@ -209,15 +226,17 @@ class BaseCompletionType: """Describes different completion types""" def __init__( - self, shown_before_tab: bool = True, mode=AutocompleteModes.SIMPLE + self, + shown_before_tab: bool = True, + mode: AutocompleteModes = AutocompleteModes.SIMPLE, ) -> None: self._shown_before_tab = shown_before_tab self.method_match = MODES_MAP[mode] @abc.abstractmethod def matches( - self, cursor_offset: int, line: str, **kwargs - ) -> Union[Set[str], None]: + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set[str]]: """Returns a list of possible matches given a line and cursor, or None if this completion type isn't applicable. @@ -236,7 +255,7 @@ def matches( raise NotImplementedError @abc.abstractmethod - def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: """Returns a Linepart namedtuple instance or None given cursor and line A Linepart namedtuple contains a start, stop, and word. None is @@ -244,10 +263,12 @@ def locate(self, cursor_offset: int, line: str) -> Union[LinePart, None]: the cursor.""" raise NotImplementedError - def format(self, word): + def format(self, word: str) -> str: return word - def substitute(self, cursor_offset, line, match) -> Tuple[int, str]: + def substitute( + self, cursor_offset: int, line: str, match: str + ) -> Tuple[int, str]: """Returns a cursor offset and line with match swapped in""" lpart = self.locate(cursor_offset, line) assert lpart @@ -265,28 +286,32 @@ def shown_before_tab(self) -> bool: class CumulativeCompleter(BaseCompletionType): """Returns combined matches from several completers""" - def __init__(self, completers, mode=AutocompleteModes.SIMPLE) -> None: + def __init__( + self, + completers: Sequence[BaseCompletionType], + mode: AutocompleteModes = AutocompleteModes.SIMPLE, + ) -> None: if not completers: raise ValueError( "CumulativeCompleter requires at least one completer" ) - self._completers = completers + self._completers: Sequence[BaseCompletionType] = completers super().__init__(True, mode) - def locate(self, current_offset: int, line: str) -> Union[None, NoReturn]: + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: for completer in self._completers: - return_value = completer.locate(current_offset, line) + return_value = completer.locate(cursor_offset, line) if return_value is not None: return return_value return None - def format(self, word): + def format(self, word: str) -> str: return self._completers[0].format(word) def matches( - self, cursor_offset: int, line: str, **kwargs - ) -> Union[None, Set]: + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: return_value = None all_matches = set() for completer in self._completers: @@ -301,28 +326,36 @@ def matches( class ImportCompletion(BaseCompletionType): - def __init__(self, module_gatherer, mode=AutocompleteModes.SIMPLE): + def __init__( + self, + module_gatherer: ModuleGatherer, + mode: AutocompleteModes = AutocompleteModes.SIMPLE, + ): super().__init__(False, mode) self.module_gatherer = module_gatherer - def matches(self, cursor_offset, line, **kwargs): + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: return self.module_gatherer.complete(cursor_offset, line) - def locate(self, current_offset, line): - return lineparts.current_word(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_word(cursor_offset, line) - def format(self, word): + def format(self, word: str) -> str: return after_last_dot(word) class FilenameCompletion(BaseCompletionType): - def __init__(self, mode=AutocompleteModes.SIMPLE): + def __init__(self, mode: AutocompleteModes = AutocompleteModes.SIMPLE): super().__init__(False, mode) - def safe_glob(self, pathname) -> Iterator: + def safe_glob(self, pathname: str) -> Iterator[str]: return glob.iglob(glob.escape(pathname) + "*") - def matches(self, cursor_offset, line, **kwargs) -> Union[None, set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: cs = lineparts.current_string(cursor_offset, line) if cs is None: return None @@ -337,10 +370,10 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, set]: matches.add(filename) return matches - def locate(self, current_offset, line): - return lineparts.current_string(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_string(cursor_offset, line) - def format(self, filename): + def format(self, filename: str) -> str: filename.rstrip(os.sep).rsplit(os.sep)[-1] if os.sep in filename[:-1]: return filename[filename.rindex(os.sep, 0, -1) + 1 :] @@ -352,10 +385,12 @@ class AttrCompletion(BaseCompletionType): attr_matches_re = LazyReCompile(r"(\w+(\.\w+)*)\.(\w*)") - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "locals_" not in kwargs: return None - locals_ = kwargs["locals_"] + locals_ = cast(Dict[str, Any], kwargs["locals_"]) r = self.locate(cursor_offset, line) if r is None: @@ -382,13 +417,13 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: if few_enough_underscores(r.word.split(".")[-1], m.split(".")[-1]) } - def locate(self, current_offset, line): - return lineparts.current_dotted_attribute(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_dotted_attribute(cursor_offset, line) - def format(self, word): + def format(self, word: str) -> str: return after_last_dot(word) - def attr_matches(self, text, namespace) -> List: + def attr_matches(self, text: str, namespace: Dict[str, Any]) -> List: """Taken from rlcompleter.py and bent to my will.""" m = self.attr_matches_re.match(text) @@ -407,7 +442,7 @@ def attr_matches(self, text, namespace) -> List: matches = self.attr_lookup(obj, expr, attr) return matches - def attr_lookup(self, obj, expr, attr) -> List: + def attr_lookup(self, obj: Any, expr: str, attr: str) -> List: """Second half of attr_matches.""" words = self.list_attributes(obj) if inspection.hasattr_safe(obj, "__class__"): @@ -427,7 +462,7 @@ def attr_lookup(self, obj, expr, attr) -> List: matches.append(f"{expr}.{word}") return matches - def list_attributes(self, obj) -> List[str]: + def list_attributes(self, obj: Any) -> List[str]: # TODO: re-implement dir using getattr_static to avoid using # AttrCleaner here? with inspection.AttrCleaner(obj): @@ -435,7 +470,9 @@ def list_attributes(self, obj) -> List[str]: class DictKeyCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -458,15 +495,17 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: else: return None - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_dict_key(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_dict_key(cursor_offset, line) - def format(self, match): + def format(self, match: str) -> str: return match[:-1] class MagicMethodCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "current_block" not in kwargs: return None current_block = kwargs["current_block"] @@ -478,12 +517,14 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: return None return {name for name in MAGIC_METHODS if name.startswith(r.word)} - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_method_definition_name(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_method_definition_name(cursor_offset, line) class GlobalCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: """Compute matches when text is a simple name. Return a list of all keywords, built-in functions and names currently defined in self.namespace that match. @@ -513,12 +554,14 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: matches.add(_callable_postfix(val, word)) return matches if matches else None - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_single_word(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_single_word(cursor_offset, line) class ParameterNameCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "argspec" not in kwargs: return None argspec = kwargs["argspec"] @@ -539,16 +582,18 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: ) return matches if matches else None - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_word(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_word(cursor_offset, line) class ExpressionAttributeCompletion(AttrCompletion): # could replace attr completion as a more general case with some work - def locate(self, current_offset, line) -> Union[LinePart, None]: - return lineparts.current_expression_attribute(current_offset, line) + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: + return lineparts.current_expression_attribute(cursor_offset, line) - def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "locals_" not in kwargs: return None locals_ = kwargs["locals_"] @@ -573,17 +618,24 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: import jedi except ImportError: - class MultilineJediCompletion(BaseCompletionType): - def matches(self, cursor_offset, line, **kwargs) -> None: + class MultilineJediCompletion(BaseCompletionType): # type: ignore [no-redef] + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: + return None + + def locate(self, cursor_offset: int, line: str) -> Optional[LinePart]: return None else: class JediCompletion(BaseCompletionType): - _orig_start: Union[int, None] + _orig_start: Optional[int] - def matches(self, cursor_offset, line, **kwargs) -> Union[None, Set]: + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "history" not in kwargs: return None history = kwargs["history"] @@ -630,8 +682,10 @@ def locate(self, cursor_offset: int, line: str) -> LinePart: end = cursor_offset return LinePart(start, end, line[start:end]) - class MultilineJediCompletion(JediCompletion): - def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: + class MultilineJediCompletion(JediCompletion): # type: ignore [no-redef] + def matches( + self, cursor_offset: int, line: str, **kwargs: Any + ) -> Optional[Set]: if "current_block" not in kwargs or "history" not in kwargs: return None current_block = kwargs["current_block"] @@ -648,7 +702,12 @@ def matches(self, cursor_offset, line, **kwargs) -> Union[Set, None]: return None -def get_completer(completers, cursor_offset, line, **kwargs): +def get_completer( + completers: Sequence[BaseCompletionType], + cursor_offset: int, + line: str, + **kwargs: Any, +) -> Tuple[List[str], Optional[BaseCompletionType]]: """Returns a list of matches and an applicable completer If no matches available, returns a tuple of an empty list and None @@ -683,7 +742,9 @@ def get_completer(completers, cursor_offset, line, **kwargs): return [], None -def get_default_completer(mode=AutocompleteModes.SIMPLE, module_gatherer=None): +def get_default_completer( + mode: AutocompleteModes, module_gatherer: ModuleGatherer +) -> Tuple[BaseCompletionType, ...]: return ( ( DictKeyCompletion(mode=mode), @@ -706,7 +767,7 @@ def get_default_completer(mode=AutocompleteModes.SIMPLE, module_gatherer=None): ) -def _callable_postfix(value, word): +def _callable_postfix(value: Any, word: str) -> str: """rlcompleter's _callable_postfix done right.""" if callable(value): word += "(" diff --git a/bpython/cli.py b/bpython/cli.py index baa2059d..28cc67c7 100644 --- a/bpython/cli.py +++ b/bpython/cli.py @@ -50,7 +50,7 @@ import struct import sys import time -from typing import Iterator, NoReturn +from typing import Iterator, NoReturn, List import unicodedata from dataclasses import dataclass @@ -144,7 +144,7 @@ def writelines(self, l) -> None: for s in l: self.write(s) - def isatty(self) -> True: + def isatty(self) -> bool: # some third party (amongst them mercurial) depend on this return True @@ -161,7 +161,7 @@ def __init__(self, interface) -> None: self.encoding = getpreferredencoding() self.interface = interface - self.buffer = list() + self.buffer: List[str] = list() def __iter__(self) -> Iterator: return iter(self.readlines()) @@ -175,7 +175,7 @@ def write(self, value) -> NoReturn: # others, so here's a hack to keep them happy raise OSError(errno.EBADF, "sys.stdin is read-only") - def isatty(self) -> True: + def isatty(self) -> bool: return True def readline(self, size=-1): diff --git a/bpython/config.py b/bpython/config.py index 48686610..0127b0cb 100644 --- a/bpython/config.py +++ b/bpython/config.py @@ -21,6 +21,10 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# To gradually migrate to mypy we aren't setting these globally yet +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True + import os import sys import locale diff --git a/bpython/curtsies.py b/bpython/curtsies.py index 10fab77f..86d33cf3 100644 --- a/bpython/curtsies.py +++ b/bpython/curtsies.py @@ -1,3 +1,9 @@ +# To gradually migrate to mypy we aren't setting these globally yet +# mypy: disallow_untyped_defs=True +# mypy: disallow_untyped_calls=True + +import argparse +import code import collections import logging import sys @@ -16,11 +22,43 @@ from .repl import extract_exit_value from .translations import _ +from typing import ( + Any, + Dict, + List, + Callable, + Union, + Sequence, + Tuple, + Optional, + Generator, +) +from typing_extensions import Literal, Protocol + logger = logging.getLogger(__name__) +class SupportsEventGeneration(Protocol): + def send( + self, timeout: Optional[float] + ) -> Union[str, curtsies.events.Event, None]: + ... + + def __iter__(self) -> "SupportsEventGeneration": + ... + + def __next__(self) -> Union[str, curtsies.events.Event, None]: + ... + + class FullCurtsiesRepl(BaseRepl): - def __init__(self, config, locals_, banner, interp=None) -> None: + def __init__( + self, + config: Config, + locals_: Optional[Dict[str, Any]], + banner: Optional[str], + interp: code.InteractiveInterpreter = None, + ) -> None: self.input_generator = curtsies.input.Input( keynames="curtsies", sigint_event=True, paste_threshold=None ) @@ -32,13 +70,13 @@ def __init__(self, config, locals_, banner, interp=None) -> None: extra_bytes_callback=self.input_generator.unget_bytes, ) - self._request_refresh_callback = self.input_generator.event_trigger( - events.RefreshRequestEvent - ) - self._schedule_refresh_callback = ( - self.input_generator.scheduled_event_trigger( - events.ScheduledRefreshRequestEvent - ) + self._request_refresh_callback: Callable[ + [], None + ] = self.input_generator.event_trigger(events.RefreshRequestEvent) + self._schedule_refresh_callback: Callable[ + [float], None + ] = self.input_generator.scheduled_event_trigger( + events.ScheduledRefreshRequestEvent ) self._request_reload_callback = ( self.input_generator.threadsafe_event_trigger(events.ReloadEvent) @@ -61,40 +99,42 @@ def __init__(self, config, locals_, banner, interp=None) -> None: orig_tcattrs=self.input_generator.original_stty, ) - def _request_refresh(self): + def _request_refresh(self) -> None: return self._request_refresh_callback() - def _schedule_refresh(self, when="now"): + def _schedule_refresh(self, when: float) -> None: return self._schedule_refresh_callback(when) - def _request_reload(self, files_modified=("?",)): + def _request_reload(self, files_modified: Sequence[str] = ("?",)) -> None: return self._request_reload_callback(files_modified) - def interrupting_refresh(self): + def interrupting_refresh(self) -> None: return self._interrupting_refresh_callback() - def request_undo(self, n=1): + def request_undo(self, n: int = 1) -> None: return self._request_undo_callback(n=n) - def get_term_hw(self): + def get_term_hw(self) -> Tuple[int, int]: return self.window.get_term_hw() - def get_cursor_vertical_diff(self): + def get_cursor_vertical_diff(self) -> int: return self.window.get_cursor_vertical_diff() - def get_top_usable_line(self): + def get_top_usable_line(self) -> int: return self.window.top_usable_row - def on_suspend(self): + def on_suspend(self) -> None: self.window.__exit__(None, None, None) self.input_generator.__exit__(None, None, None) - def after_suspend(self): + def after_suspend(self) -> None: self.input_generator.__enter__() self.window.__enter__() self.interrupting_refresh() - def process_event_and_paint(self, e): + def process_event_and_paint( + self, e: Union[str, curtsies.events.Event, None] + ) -> None: """If None is passed in, just paint the screen""" try: if e is not None: @@ -112,7 +152,11 @@ def process_event_and_paint(self, e): scrolled = self.window.render_to_terminal(array, cursor_pos) self.scroll_offset += scrolled - def mainloop(self, interactive=True, paste=None): + def mainloop( + self, + interactive: bool = True, + paste: Optional[curtsies.events.PasteEvent] = None, + ) -> None: if interactive: # Add custom help command # TODO: add methods to run the code @@ -137,14 +181,19 @@ def mainloop(self, interactive=True, paste=None): self.process_event_and_paint(e) -def main(args=None, locals_=None, banner=None, welcome_message=None): +def main( + args: List[str] = None, + locals_: Dict[str, Any] = None, + banner: str = None, + welcome_message: str = None, +) -> Any: """ banner is displayed directly after the version information. welcome_message is passed on to Repl and displayed in the statusbar. """ translations.init() - def curtsies_arguments(parser): + def curtsies_arguments(parser: argparse._ArgumentGroup) -> None: parser.add_argument( "--paste", "-p", @@ -163,10 +212,10 @@ def curtsies_arguments(parser): interp = None paste = None + exit_value: Tuple[Any, ...] = () if exec_args: if not options: raise ValueError("don't pass in exec_args without options") - exit_value = () if options.paste: paste = curtsies.events.PasteEvent() encoding = inspection.get_encoding_file(exec_args[0]) @@ -196,16 +245,18 @@ def curtsies_arguments(parser): with repl.window as win: with repl: repl.height, repl.width = win.t.height, win.t.width - exit_value = repl.mainloop(True, paste) + repl.mainloop(True, paste) except (SystemExitFromCodeRunner, SystemExit) as e: exit_value = e.args return extract_exit_value(exit_value) -def _combined_events(event_provider, paste_threshold): +def _combined_events( + event_provider: "SupportsEventGeneration", paste_threshold: int +) -> Generator[Union[str, curtsies.events.Event, None], Optional[float], None]: """Combines consecutive keypress events into paste events.""" timeout = yield "nonsense_event" # so send can be used immediately - queue = collections.deque() + queue: collections.deque = collections.deque() while True: e = event_provider.send(timeout) if isinstance(e, curtsies.events.Event): @@ -230,7 +281,9 @@ def _combined_events(event_provider, paste_threshold): timeout = yield queue.popleft() -def combined_events(event_provider, paste_threshold=3): +def combined_events( + event_provider: SupportsEventGeneration, paste_threshold: int = 3 +) -> SupportsEventGeneration: g = _combined_events(event_provider, paste_threshold) next(g) return g diff --git a/bpython/curtsiesfrontend/filewatch.py b/bpython/curtsiesfrontend/filewatch.py index 314767a5..e3607180 100644 --- a/bpython/curtsiesfrontend/filewatch.py +++ b/bpython/curtsiesfrontend/filewatch.py @@ -14,7 +14,7 @@ def ModuleChangedEventHandler(*args): else: - class ModuleChangedEventHandler(FileSystemEventHandler): + class ModuleChangedEventHandler(FileSystemEventHandler): # type: ignore [no-redef] def __init__(self, paths, on_change): self.dirs = defaultdict(set) self.on_change = on_change diff --git a/bpython/curtsiesfrontend/interaction.py b/bpython/curtsiesfrontend/interaction.py index 51fd28d3..79622d14 100644 --- a/bpython/curtsiesfrontend/interaction.py +++ b/bpython/curtsiesfrontend/interaction.py @@ -78,7 +78,7 @@ def _check_for_expired_message(self): ): self._message = "" - def process_event(self, e): + def process_event(self, e) -> None: """Returns True if shutting down""" assert self.in_prompt or self.in_confirm or self.waiting_for_refresh if isinstance(e, RefreshRequestEvent): diff --git a/bpython/curtsiesfrontend/interpreter.py b/bpython/curtsiesfrontend/interpreter.py index a48bc429..7f7c2fcc 100644 --- a/bpython/curtsiesfrontend/interpreter.py +++ b/bpython/curtsiesfrontend/interpreter.py @@ -1,4 +1,5 @@ import sys +from typing import Any, Dict from pygments.token import Generic, Token, Keyword, Name, Comment, String from pygments.token import Error, Literal, Number, Operator, Punctuation @@ -59,7 +60,7 @@ def format(self, tokensource, outfile): class Interp(ReplInterpreter): - def __init__(self, locals=None, encoding=None): + def __init__(self, locals: Dict[str, Any] = None, encoding=None): """Constructor. We include an argument for the outfile to pass to the formatter for it @@ -75,7 +76,7 @@ def write(err_line): Accepts FmtStrs so interpreters can output them""" sys.stderr.write(str(err_line)) - self.write = write + self.write = write # type: ignore self.outfile = self def writetb(self, lines): diff --git a/bpython/curtsiesfrontend/repl.py b/bpython/curtsiesfrontend/repl.py index 21b606e6..85965f4f 100644 --- a/bpython/curtsiesfrontend/repl.py +++ b/bpython/curtsiesfrontend/repl.py @@ -1,3 +1,4 @@ +import code import contextlib import errno import itertools @@ -12,6 +13,8 @@ import unicodedata from enum import Enum +from typing import Dict, Any, List, Optional, Tuple, Union, cast + import blessings import cwcwidth import greenlet @@ -32,6 +35,7 @@ from pygments.lexers import Python3Lexer from . import events as bpythonevents, sitefix, replpainter as paint +from ..config import Config from .coderunner import ( CodeRunner, FakeOutput, @@ -98,7 +102,7 @@ def __init__(self, coderunner, repl, configured_edit_keys=None): else: self.rl_char_sequences = edit_keys - def process_event(self, e): + def process_event(self, e: Union[events.Event, str]) -> None: assert self.has_focus logger.debug("fake input processing event %r", e) @@ -116,8 +120,6 @@ def process_event(self, e): self.current_line = "" self.cursor_offset = 0 self.repl.run_code_and_maybe_finish() - elif e in ("",): - self.get_last_word() elif e in ("",): pass elif e in ("",): @@ -281,7 +283,7 @@ def _process_ps(ps, default_ps: str): if not isinstance(ps, str): return ps - return ps if cwcwidth.wcswidth(ps) >= 0 else default_ps + return ps if cwcwidth.wcswidth(ps, None) >= 0 else default_ps class BaseRepl(Repl): @@ -308,11 +310,11 @@ class BaseRepl(Repl): def __init__( self, - config, - locals_=None, - banner=None, - interp=None, - orig_tcattrs=None, + config: Config, + locals_: Dict[str, Any] = None, + banner: str = None, + interp: code.InteractiveInterpreter = None, + orig_tcattrs: List[Any] = None, ): """ locals_ is a mapping of locals to pass into the interpreter @@ -330,7 +332,7 @@ def __init__( if interp is None: interp = Interp(locals=locals_) - interp.write = self.send_to_stdouterr + interp.write = self.send_to_stdouterr # type: ignore if banner is None: if config.help_key: banner = ( @@ -372,7 +374,7 @@ def __init__( # this is every line that's been displayed (input and output) # as with formatting applied. Logical lines that exceeded the terminal width # at the time of output are split across multiple entries in this list. - self.display_lines = [] + self.display_lines: List[FmtStr] = [] # this is every line that's been executed; it gets smaller on rewind self.history = [] @@ -382,12 +384,12 @@ def __init__( # Entries are tuples, where # - the first element the line (string, not fmtsr) # - the second element is one of 2 global constants: "input" or "output" - # (use LineTypeTranslator.INPUT or LineTypeTranslator.OUTPUT to avoid typing these strings) - self.all_logical_lines = [] + # (use LineType.INPUT or LineType.OUTPUT to avoid typing these strings) + self.all_logical_lines: List[Tuple[str, LineType]] = [] # formatted version of lines in the buffer kept around so we can # unhighlight parens using self.reprint_line as called by bpython.Repl - self.display_buffer = [] + self.display_buffer: List[FmtStr] = [] # how many times display has been scrolled down # because there wasn't room to display everything @@ -396,7 +398,7 @@ def __init__( # cursor position relative to start of current_line, 0 is first char self._cursor_offset = 0 - self.orig_tcattrs = orig_tcattrs + self.orig_tcattrs: Optional[List[Any]] = orig_tcattrs self.coderunner = CodeRunner(self.interp, self.request_refresh) @@ -428,7 +430,7 @@ def __init__( # some commands act differently based on the prev event # this list doesn't include instances of event.Event, # only keypress-type events (no refresh screen events etc.) - self.last_events = [None] * 50 + self.last_events: List[Optional[str]] = [None] * 50 # displays prev events in a column on the right hand side self.presentation_mode = False @@ -446,8 +448,10 @@ def __init__( self.original_modules = set(sys.modules.keys()) - self.width = None - self.height = None + # as long as the first event received is a window resize event, + # this works fine... + self.width: int = cast(int, None) + self.height: int = cast(int, None) self.status_bar.message(banner) @@ -470,7 +474,7 @@ def get_term_hw(self): """Returns the current width and height of the display area.""" return (50, 10) - def _schedule_refresh(self, when="now"): + def _schedule_refresh(self, when: float): """Arrange for the bpython display to be refreshed soon. This method will be called when the Repl wants the display to be @@ -613,7 +617,7 @@ def clean_up_current_line_for_exit(self): self.unhighlight_paren() # Event handling - def process_event(self, e): + def process_event(self, e: Union[events.Event, str]) -> Optional[bool]: """Returns True if shutting down, otherwise returns None. Mostly mutates state of Repl object""" @@ -623,9 +627,10 @@ def process_event(self, e): else: self.last_events.append(e) self.last_events.pop(0) - return self.process_key_event(e) + self.process_key_event(e) + return None - def process_control_event(self, e): + def process_control_event(self, e) -> Optional[bool]: if isinstance(e, bpythonevents.ScheduledRefreshRequestEvent): # This is a scheduled refresh - it's really just a refresh (so nop) @@ -640,7 +645,7 @@ def process_control_event(self, e): self.run_code_and_maybe_finish() elif self.status_bar.has_focus: - return self.status_bar.process_event(e) + self.status_bar.process_event(e) # handles paste events for both stdin and repl elif isinstance(e, events.PasteEvent): @@ -680,12 +685,11 @@ def process_control_event(self, e): self.undo(n=e.n) elif self.stdin.has_focus: - return self.stdin.process_event(e) + self.stdin.process_event(e) elif isinstance(e, events.SigIntEvent): logger.debug("received sigint event") self.keyboard_interrupt() - return elif isinstance(e, bpythonevents.ReloadEvent): if self.watching_files: @@ -697,8 +701,9 @@ def process_control_event(self, e): else: raise ValueError("Don't know how to handle event type: %r" % e) + return None - def process_key_event(self, e): + def process_key_event(self, e: str) -> None: # To find the curtsies name for a keypress, try # python -m curtsies.events if self.status_bar.has_focus: @@ -753,7 +758,7 @@ def process_key_event(self, e): elif e in key_dispatch[self.config.reimport_key]: self.clear_modules_and_reevaluate() elif e in key_dispatch[self.config.toggle_file_watch_key]: - return self.toggle_file_watch() + self.toggle_file_watch() elif e in key_dispatch[self.config.clear_screen_key]: self.request_paint_to_clear_screen = True elif e in key_dispatch[self.config.show_source_key]: @@ -1429,7 +1434,7 @@ def paint( user_quit=False, try_preserve_history_height=30, min_infobox_height=5, - ): + ) -> Tuple[FSArray, Tuple[int, int]]: """Returns an array of min_height or more rows and width columns, plus cursor position @@ -1587,11 +1592,12 @@ def move_screen_up(current_line_start_row): type(self.current_stdouterr_line), self.current_stdouterr_line, ) - stdouterr_width = ( - self.current_stdouterr_line.width - if isinstance(self.current_stdouterr_line, FmtStr) - else wcswidth(self.current_stdouterr_line) - ) + # mypy can't do ternary type guards yet + stdouterr = self.current_stdouterr_line + if isinstance(stdouterr, FmtStr): + stdouterr_width = stdouterr.width + else: + stdouterr_width = len(stdouterr) cursor_row, cursor_column = divmod( stdouterr_width + wcswidth( @@ -1620,8 +1626,8 @@ def move_screen_up(current_line_start_row): ) else: # Common case for determining cursor position cursor_row, cursor_column = divmod( - wcswidth(self.current_cursor_line_without_suggestion.s) - - wcswidth(self.current_line) + wcswidth(self.current_cursor_line_without_suggestion.s, None) + - wcswidth(self.current_line, None) + wcswidth(self.current_line, max(0, self.cursor_offset)) + self.number_of_padding_chars_on_current_cursor_line(), width, @@ -1817,7 +1823,7 @@ def echo(self, msg, redraw=True): @property def cpos(self): - "many WATs were had - it's the pos from the end of the line back" "" + "many WATs were had - it's the pos from the end of the line back" return len(self.current_line) - self.cursor_offset def reprint_line(self, lineno, tokens): @@ -1932,7 +1938,7 @@ def reevaluate(self, new_code=False): self._cursor_offset = 0 self.current_line = "" - def initialize_interp(self): + def initialize_interp(self) -> None: self.coderunner.interp.locals["_repl"] = self self.coderunner.interp.runsource( "from bpython.curtsiesfrontend._internal import _Helper\n" diff --git a/bpython/inspection.py b/bpython/inspection.py index 4c0dbada..7f95fbd2 100644 --- a/bpython/inspection.py +++ b/bpython/inspection.py @@ -336,7 +336,7 @@ def get_encoding(obj): return "utf8" -def get_encoding_file(fname): +def get_encoding_file(fname: str) -> str: """Try to obtain encoding information from a Python source file.""" with open(fname, encoding="ascii", errors="ignore") as f: for unused in range(2): @@ -347,7 +347,7 @@ def get_encoding_file(fname): return "utf8" -def getattr_safe(obj, name): +def getattr_safe(obj: Any, name: str): """side effect free getattr (calls getattr_static).""" result = inspect.getattr_static(obj, name) # Slots are a MemberDescriptorType @@ -356,7 +356,7 @@ def getattr_safe(obj, name): return result -def hasattr_safe(obj, name): +def hasattr_safe(obj: Any, name: str) -> bool: try: getattr_safe(obj, name) return True diff --git a/bpython/lazyre.py b/bpython/lazyre.py index fbbdd38d..8f1e7099 100644 --- a/bpython/lazyre.py +++ b/bpython/lazyre.py @@ -21,12 +21,12 @@ # THE SOFTWARE. import re -from typing import Optional, Iterator +from typing import Optional, Iterator, Pattern, Match, Optional try: from functools import cached_property except ImportError: - from backports.cached_property import cached_property # type: ignore + from backports.cached_property import cached_property # type: ignore [no-redef] class LazyReCompile: @@ -40,16 +40,16 @@ def __init__(self, regex: str, flags: int = 0) -> None: self.flags = flags @cached_property - def compiled(self): + def compiled(self) -> Pattern[str]: return re.compile(self.regex, self.flags) def finditer(self, *args, **kwargs): return self.compiled.finditer(*args, **kwargs) - def search(self, *args, **kwargs): + def search(self, *args, **kwargs) -> Optional[Match[str]]: return self.compiled.search(*args, **kwargs) - def match(self, *args, **kwargs): + def match(self, *args, **kwargs) -> Optional[Match[str]]: return self.compiled.match(*args, **kwargs) def sub(self, *args, **kwargs) -> str: diff --git a/bpython/patch_linecache.py b/bpython/patch_linecache.py index daf7251d..e1d94a15 100644 --- a/bpython/patch_linecache.py +++ b/bpython/patch_linecache.py @@ -64,7 +64,7 @@ def _bpython_clear_linecache(): # Monkey-patch the linecache module so that we're able # to hold our command history there and have it persist -linecache.cache = BPythonLinecache(linecache.cache) +linecache.cache = BPythonLinecache(linecache.cache) # type: ignore linecache.clearcache = _bpython_clear_linecache diff --git a/bpython/repl.py b/bpython/repl.py index ba9acf4d..e261e61a 100644 --- a/bpython/repl.py +++ b/bpython/repl.py @@ -33,11 +33,13 @@ import textwrap import time import traceback +from abc import abstractmethod from itertools import takewhile from pathlib import Path from pygments.lexers import Python3Lexer from pygments.token import Token from types import ModuleType +from typing import cast, Tuple, Any have_pyperclip = True try: @@ -451,11 +453,11 @@ def __init__(self, interp, config): @property def ps1(self) -> str: - return getattr(sys, "ps1", ">>> ") + return cast(str, getattr(sys, "ps1", ">>> ")) @property def ps2(self) -> str: - return getattr(sys, "ps2", "... ") + return cast(str, getattr(sys, "ps2", "... ")) def startup(self): """ @@ -769,6 +771,10 @@ def line_is_empty(line): indentation = 0 return indentation + @abstractmethod + def getstdout(self) -> str: + raise NotImplementedError() + def get_session_formatted_for_file(self) -> str: """Format the stdout buffer to something suitable for writing to disk, i.e. without >>> and ... at input lines and with "# OUT: " prepended to @@ -1214,7 +1220,7 @@ def token_is_any_of(token): return token_is_any_of -def extract_exit_value(args): +def extract_exit_value(args: Tuple[Any, ...]) -> Any: """Given the arguments passed to `SystemExit`, return the value that should be passed to `sys.exit`. """ diff --git a/bpython/simpleeval.py b/bpython/simpleeval.py index ef6dd53b..c5e14cf5 100644 --- a/bpython/simpleeval.py +++ b/bpython/simpleeval.py @@ -28,6 +28,7 @@ import ast import sys import builtins +from typing import Dict, Any from . import line as line_properties from .inspection import getattr_safe @@ -44,7 +45,7 @@ class EvaluationError(Exception): """Raised if an exception occurred in safe_eval.""" -def safe_eval(expr, namespace): +def safe_eval(expr: str, namespace: Dict[str, Any]) -> Any: """Not all that safe, just catches some errors""" try: return eval(expr, namespace) @@ -214,7 +215,9 @@ def find_attribute_with_name(node, name): return r -def evaluate_current_expression(cursor_offset, line, namespace=None): +def evaluate_current_expression( + cursor_offset: int, line: str, namespace: Dict[str, Any] = None +): """ Return evaluated expression to the right of the dot of current attribute. diff --git a/bpython/test/test_crashers.py b/bpython/test/test_crashers.py index 64abff3e..051e5b69 100644 --- a/bpython/test/test_crashers.py +++ b/bpython/test/test_crashers.py @@ -17,10 +17,10 @@ from twisted.trial.unittest import TestCase as TrialTestCase except ImportError: - class TrialTestCase: + class TrialTestCase: # type: ignore [no-redef] pass - reactor = None + reactor = None # type: ignore try: import urwid diff --git a/bpython/test/test_curtsies_repl.py b/bpython/test/test_curtsies_repl.py index 2ed33097..a6e4c786 100644 --- a/bpython/test/test_curtsies_repl.py +++ b/bpython/test/test_curtsies_repl.py @@ -86,8 +86,7 @@ def test_last_word(self): self.assertEqual(curtsiesrepl._last_word("a"), "a") self.assertEqual(curtsiesrepl._last_word("a b"), "b") - # this is the behavior of bash - not currently implemented - @unittest.skip + @unittest.skip("this is the behavior of bash - not currently implemented") def test_get_last_word_with_prev_line(self): self.repl.rl_history.entries = ["1", "2 3", "4 5 6"] self.repl._set_current_line("abcde") @@ -300,7 +299,7 @@ def test_simple(self): self.assertEqual(self.repl.predicted_indent("def asdf():"), 4) self.assertEqual(self.repl.predicted_indent("def asdf(): return 7"), 0) - @unittest.skip + @unittest.skip("This would be interesting") def test_complex(self): self.assertEqual(self.repl.predicted_indent("[a, "), 1) self.assertEqual(self.repl.predicted_indent("reduce(asdfasdf, "), 7) diff --git a/bpython/test/test_inspection.py b/bpython/test/test_inspection.py index fecb848b..50be0c3a 100644 --- a/bpython/test/test_inspection.py +++ b/bpython/test/test_inspection.py @@ -12,7 +12,7 @@ try: import numpy except ImportError: - numpy = None + numpy = None # type: ignore foo_ascii_only = '''def foo(): @@ -191,7 +191,7 @@ def __mro__(self): a = 1 -member_descriptor = type(Slots.s1) +member_descriptor = type(Slots.s1) # type: ignore class TestSafeGetAttribute(unittest.TestCase): diff --git a/bpython/translations/__init__.py b/bpython/translations/__init__.py index c2e23f80..13c49802 100644 --- a/bpython/translations/__init__.py +++ b/bpython/translations/__init__.py @@ -2,13 +2,14 @@ import locale import os.path import sys +from typing import cast, List from .. import package_dir -translator = None +translator: gettext.NullTranslations = cast(gettext.NullTranslations, None) -def _(message): +def _(message) -> str: return translator.gettext(message) @@ -16,7 +17,7 @@ def ngettext(singular, plural, n): return translator.ngettext(singular, plural, n) -def init(locale_dir=None, languages=None): +def init(locale_dir: str = None, languages: List[str] = None) -> None: try: locale.setlocale(locale.LC_ALL, "") except locale.Error: diff --git a/bpython/urwid.py b/bpython/urwid.py index 1f63b137..e3aab75c 100644 --- a/bpython/urwid.py +++ b/bpython/urwid.py @@ -20,6 +20,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. +# This whole file typing TODO +# type: ignore """bpython backend based on Urwid. diff --git a/setup.cfg b/setup.cfg index 89b6cde6..1efed779 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,3 +65,15 @@ msgid_bugs_address = https://github.com/bpython/bpython/issues builder = man source_dir = doc/sphinx/source build_dir = build + +[mypy] +warn_return_any = True +warn_unused_configs = True +mypy_path=stubs +files=bpython + +[mypy-jedi] +ignore_missing_imports = True + +[mypy-urwid] +ignore_missing_imports = True diff --git a/stubs/blessings.pyi b/stubs/blessings.pyi new file mode 100644 index 00000000..66fd9621 --- /dev/null +++ b/stubs/blessings.pyi @@ -0,0 +1,47 @@ +from typing import ContextManager, Text, IO + +class Terminal: + def __init__(self, stream=None, force_styling=False): + # type: (IO, bool) -> None + pass + def location(self, x=None, y=None): + # type: (int, int) -> ContextManager + pass + @property + def hide_cursor(self): + # type: () -> Text + pass + @property + def normal_cursor(self): + # type: () -> Text + pass + @property + def height(self): + # type: () -> int + pass + @property + def width(self): + # type: () -> int + pass + def fullscreen(self): + # type: () -> ContextManager + pass + def move(self, y, x): + # type: (int, int) -> Text + pass + @property + def clear_eol(self): + # type: () -> Text + pass + @property + def clear_bol(self): + # type: () -> Text + pass + @property + def clear_eos(self): + # type: () -> Text + pass + @property + def clear_eos(self): + # type: () -> Text + pass diff --git a/stubs/greenlet.pyi b/stubs/greenlet.pyi new file mode 100644 index 00000000..778c827e --- /dev/null +++ b/stubs/greenlet.pyi @@ -0,0 +1,9 @@ +from typing import Any, Callable + +__version__: str + +def getcurrent() -> None: ... + +class greenlet: + def __init__(self, func: Callable[[], Any]): ... + def switch(self, value: Any = None) -> Any: ... diff --git a/stubs/msvcrt.pyi b/stubs/msvcrt.pyi new file mode 100644 index 00000000..2e99c900 --- /dev/null +++ b/stubs/msvcrt.pyi @@ -0,0 +1,7 @@ +# The real types seem only available on the Windows platform, +# but it seems annoying to need to run typechecking once per platform +# https://github.com/python/typeshed/blob/master/stdlib/msvcrt.pyi +def locking(__fd: int, __mode: int, __nbytes: int) -> None: ... + +LK_NBLCK: int +LK_UNLCK: int diff --git a/stubs/pyperclip.pyi b/stubs/pyperclip.pyi new file mode 100644 index 00000000..3968c20a --- /dev/null +++ b/stubs/pyperclip.pyi @@ -0,0 +1,3 @@ +def copy(content: str): ... + +class PyperclipException(Exception): ... diff --git a/stubs/rlcompleter.pyi b/stubs/rlcompleter.pyi new file mode 100644 index 00000000..bbc871ad --- /dev/null +++ b/stubs/rlcompleter.pyi @@ -0,0 +1,3 @@ +from typing import Any + +def get_class_members(class_: Any): ... diff --git a/stubs/watchdog/__init__.pyi b/stubs/watchdog/__init__.pyi new file mode 100644 index 00000000..e69de29b diff --git a/stubs/watchdog/events.pyi b/stubs/watchdog/events.pyi new file mode 100644 index 00000000..6e17bd6d --- /dev/null +++ b/stubs/watchdog/events.pyi @@ -0,0 +1 @@ +class FileSystemEventHandler: ... diff --git a/stubs/watchdog/observers.pyi b/stubs/watchdog/observers.pyi new file mode 100644 index 00000000..7db3099f --- /dev/null +++ b/stubs/watchdog/observers.pyi @@ -0,0 +1,4 @@ +class Observer: + def start(self): ... + def schedule(self, dirname: str, recursive: bool): ... + def unschedule_all(self): ... diff --git a/stubs/xdg.pyi b/stubs/xdg.pyi new file mode 100644 index 00000000..db7d63e0 --- /dev/null +++ b/stubs/xdg.pyi @@ -0,0 +1,4 @@ +from typing import ClassVar + +class BaseDirectory: + xdg_config_home: ClassVar[str]