diff --git a/plugin/__init__.py b/plugin/__init__.py index 812038f3..e9511d24 100644 --- a/plugin/__init__.py +++ b/plugin/__init__.py @@ -65,6 +65,7 @@ def plugin_loaded() -> None: + """Executed when this plugin is loaded.""" # somehow "AutoSetSyntaxAppendLogCommand" won't be ready if we don't wait a bit sublime.set_timeout(_plugin_loaded) @@ -86,6 +87,7 @@ def _plugin_loaded() -> None: def plugin_unloaded() -> None: + """Executed when this plugin is unloaded.""" AioSettings.clear_on_change(PLUGIN_NAME) AioSettings.tear_down() diff --git a/plugin/rules/constraint.py b/plugin/rules/constraint.py index 1595a0a9..55cff708 100644 --- a/plugin/rules/constraint.py +++ b/plugin/rules/constraint.py @@ -13,10 +13,18 @@ from ..cache import clearable_lru_cache from ..constants import PLUGIN_NAME, ST_PLATFORM from ..snapshot import ViewSnapshot -from ..types import Optimizable, ST_ConstraintRule -from ..utils import camel_to_snake, compile_regex, list_all_subclasses, merge_regexes, parse_regex_flags, remove_suffix +from ..types import Optimizable, StConstraintRule +from ..utils import ( + camel_to_snake, + compile_regex, + drop_falsy, + list_all_subclasses, + merge_regexes, + parse_regex_flags, + remove_suffix, +) -T = TypeVar("T") +_T = TypeVar("_T") def find_constraint(obj: Any) -> type[AbstractConstraint] | None: @@ -63,7 +71,7 @@ def test(self, view_snapshot: ViewSnapshot) -> bool: return not result if self.inverted else result @classmethod - def make(cls, constraint_rule: ST_ConstraintRule) -> Self: + def make(cls, constraint_rule: StConstraintRule) -> Self: """Build this object with the `constraint_rule`.""" obj = cls() @@ -114,9 +122,9 @@ def test(self, view_snapshot: ViewSnapshot) -> bool: """Tests whether the `view_snapshot` passes this constraint.""" @final - def _handled_args(self, normalizer: Callable[[T], T] | None = None) -> tuple[T, ...]: + def _handled_args(self, normalizer: Callable[[_T], _T] | None = None) -> tuple[_T, ...]: """Filter falsy args and normalize them. Note that `0`, `""` and `None` are falsy.""" - args: Iterable[T] = filter(None, self.args) + args: Iterable[_T] = drop_falsy(self.args) if normalizer: args = map(normalizer, args) return tuple(args) @@ -158,7 +166,10 @@ def _handled_case_insensitive(kwargs: dict[str, Any]) -> bool: @staticmethod def find_parent_with_sibling(base: str | Path, sibling: str, *, use_exists: bool = False) -> Path | None: """Find the first parent directory which contains `sibling`.""" - path = Path(base).resolve() + try: + path = Path(base).resolve() + except Exception: + return None if use_exists: checker = Path.exists diff --git a/plugin/rules/match.py b/plugin/rules/match.py index a6669b0a..f68befe6 100644 --- a/plugin/rules/match.py +++ b/plugin/rules/match.py @@ -10,7 +10,7 @@ from ..cache import clearable_lru_cache from ..snapshot import ViewSnapshot -from ..types import Optimizable, ST_MatchRule +from ..types import Optimizable, StMatchRule from ..utils import camel_to_snake, list_all_subclasses, remove_suffix from .constraint import ConstraintRule @@ -59,7 +59,7 @@ def test(self, view_snapshot: ViewSnapshot) -> bool: return self.match.test(view_snapshot, self.rules) @classmethod - def make(cls, match_rule: ST_MatchRule) -> Self: + def make(cls, match_rule: StMatchRule) -> Self: """Build this object with the `match_rule`.""" obj = cls() diff --git a/plugin/rules/syntax.py b/plugin/rules/syntax.py index 4d7b831c..f395751c 100644 --- a/plugin/rules/syntax.py +++ b/plugin/rules/syntax.py @@ -9,8 +9,8 @@ from ..constants import VERSION from ..snapshot import ViewSnapshot -from ..types import ListenerEvent, Optimizable, ST_SyntaxRule -from ..utils import find_syntax_by_syntax_likes +from ..types import ListenerEvent, Optimizable, StSyntaxRule +from ..utils import drop_falsy, find_syntax_by_syntax_likes from .match import MatchRule @@ -53,7 +53,7 @@ def test(self, view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) return self.root_rule.test(view_snapshot) @classmethod - def make(cls, syntax_rule: ST_SyntaxRule) -> Self: + def make(cls, syntax_rule: StSyntaxRule) -> Self: """Build this object with the `syntax_rule`.""" obj = cls() @@ -74,7 +74,7 @@ def make(cls, syntax_rule: ST_SyntaxRule) -> Self: if (on_events := syntax_rule.get("on_events")) is not None: if isinstance(on_events, str): on_events = [on_events] - obj.on_events = set(filter(None, map(ListenerEvent.from_value, on_events))) + obj.on_events = set(drop_falsy(map(ListenerEvent.from_value, on_events))) if match_rule_compiled := MatchRule.make(syntax_rule): obj.root_rule = match_rule_compiled @@ -107,7 +107,7 @@ def test(self, view_snapshot: ViewSnapshot, event: ListenerEvent | None = None) return first_true(self.rules, pred=lambda rule: rule.test(view_snapshot, event)) @classmethod - def make(cls, syntax_rules: Iterable[ST_SyntaxRule]) -> Self: + def make(cls, syntax_rules: Iterable[StSyntaxRule]) -> Self: """Build this object with the `syntax_rules`.""" obj = cls() obj.rules = tuple(map(SyntaxRule.make, syntax_rules)) diff --git a/plugin/settings.py b/plugin/settings.py index e9fd24ef..78da692e 100644 --- a/plugin/settings.py +++ b/plugin/settings.py @@ -8,7 +8,8 @@ import sublime_plugin from more_itertools import unique_everseen -from .types import ST_SyntaxRule +from .types import StSyntaxRule +from .utils import drop_falsy def get_merged_plugin_setting( @@ -32,7 +33,7 @@ def get_st_settings() -> sublime.Settings: return sublime.load_settings("Preferences.sublime-settings") -def pref_syntax_rules(*, window: sublime.Window | None = None) -> list[ST_SyntaxRule]: +def pref_syntax_rules(*, window: sublime.Window | None = None) -> list[StSyntaxRule]: return get_merged_plugin_setting("syntax_rules", [], window=window) @@ -52,8 +53,7 @@ def extra_settings_producer(settings: MergedSettingsDict) -> dict[str, Any]: # use tuple to freeze setting for better performance (cache-able) ret["trim_suffixes"] = tuple( - filter( - None, # remove falsy values + drop_falsy( unique_everseen( chain( settings.get("project_trim_suffixes", []), diff --git a/plugin/shared.py b/plugin/shared.py index 645b5c6c..81b08960 100644 --- a/plugin/shared.py +++ b/plugin/shared.py @@ -1,22 +1,20 @@ from __future__ import annotations -from typing import List +from typing import TYPE_CHECKING, List +from weakref import WeakKeyDictionary import sublime from .rules import SyntaxRuleCollection from .settings import get_merged_plugin_settings -from .types import Optimizable, WindowKeyedDict +from .types import Optimizable -_DroppedRules = List[Optimizable] - - -class DroppedRulesCollection(WindowKeyedDict[_DroppedRules]): - pass - - -class SyntaxRuleCollections(WindowKeyedDict[SyntaxRuleCollection]): - pass +if TYPE_CHECKING: + DroppedRulesCollection = WeakKeyDictionary[sublime.Window, List[Optimizable]] + SyntaxRuleCollections = WeakKeyDictionary[sublime.Window, SyntaxRuleCollection] +else: + DroppedRulesCollection = WeakKeyDictionary + SyntaxRuleCollections = WeakKeyDictionary class G: @@ -24,10 +22,8 @@ class G: startup_views: set[sublime.View] = set() """Views exist before this plugin is loaded when Sublime Text just starts.""" - syntax_rule_collections = SyntaxRuleCollections() """The compiled per-window top-level plugin rules.""" - dropped_rules_collection = DroppedRulesCollection() """Those per-window rules which are dropped after doing optimizations.""" diff --git a/plugin/snapshot.py b/plugin/snapshot.py index ec367450..8ed892f0 100644 --- a/plugin/snapshot.py +++ b/plugin/snapshot.py @@ -83,9 +83,7 @@ def from_view(cls, view: sublime.View) -> Self: window = view.window() or sublime.active_window() # is real file on a disk? - if (_path := view.file_name()) and (path := Path(_path).resolve()).is_file(): - pass - else: + if not (_path := view.file_name()) or not (path := Path(_path).resolve()).is_file(): path = None return cls( diff --git a/plugin/types.py b/plugin/types.py index be1a5feb..7349ac04 100644 --- a/plugin/types.py +++ b/plugin/types.py @@ -2,58 +2,21 @@ import sys from abc import ABC, abstractmethod -from collections import UserDict as BuiltinUserDict -from collections.abc import Generator, Iterator, KeysView +from collections.abc import Generator from enum import Enum -from typing import Any, Generic, TypedDict, TypeVar, Union, overload +from typing import Any, TypedDict, Union import sublime from typing_extensions import Self SyntaxLike = Union[str, sublime.Syntax] -WindowId = int -WindowIdAble = Union[WindowId, sublime.Window] - -_K = TypeVar("_K") -_T = TypeVar("_T") -_V = TypeVar("_V") - -if sys.version_info < (3, 9): - - class UserDict(BuiltinUserDict, Generic[_K, _V]): - """Workaround class for the fact that `UserDict` is not subscriptable until Python 3.9...""" - - def __init__(self, dict=None, /, **kwargs) -> None: - self.data: dict[_K, _V] = {} - super().__init__(dict, **kwargs) - - def __getitem__(self, key: _K) -> _V: - return super().__getitem__(key) - - def __setitem__(self, key: _K, item: _V) -> None: - super().__setitem__(key, item) - - def __delitem__(self, key: _K) -> None: - super().__delitem__(key) - - def __iter__(self) -> Iterator[_K]: - return super().__iter__() - - @overload - def get(self, key: _K) -> _V | None: ... - @overload - def get(self, key: _K, default: _T) -> _V | _T: ... - - def get(self, key: _K, default: _T | None = None) -> _V | _T | None: - return super().get(key, default) -else: - UserDict = BuiltinUserDict # noqa: F401 if sys.version_info < (3, 11): class StrEnum(str, Enum): __format__ = str.__format__ # type: ignore __str__ = str.__str__ # type: ignore + else: from enum import StrEnum # noqa: F401 @@ -94,7 +57,7 @@ def optimize(self) -> Generator[Any, None, None]: """Does optimizations and returns a generator for dropped objects.""" -class ST_ConstraintRule(TypedDict): +class StConstraintRule(TypedDict): """Typed dict for corresponding ST settings.""" constraint: str @@ -103,40 +66,19 @@ class ST_ConstraintRule(TypedDict): inverted: bool -class ST_MatchRule(TypedDict): +class StMatchRule(TypedDict): """Typed dict for corresponding ST settings.""" match: str args: list[Any] | Any | None kwargs: dict[str, Any] | None - rules: list[ST_MatchRule | ST_ConstraintRule] + rules: list[StMatchRule | StConstraintRule] -class ST_SyntaxRule(ST_MatchRule): +class StSyntaxRule(StMatchRule): """Typed dict for corresponding ST settings.""" comment: str selector: str syntaxes: str | list[str] on_events: str | list[str] | None - - -class WindowKeyedDict(UserDict[WindowIdAble, _T]): - def __setitem__(self, key: WindowIdAble, value: _T) -> None: - key = self._to_window_id(key) - super().__setitem__(key, value) - - def __getitem__(self, key: WindowIdAble) -> _T: - key = self._to_window_id(key) - return super().__getitem__(key) - - def __delitem__(self, key: WindowIdAble) -> None: - key = self._to_window_id(key) - super().__delitem__(key) - - def keys(self) -> KeysView[WindowId]: - return super().keys() - - @staticmethod - def _to_window_id(value: WindowIdAble) -> WindowId: - return value.id() if isinstance(value, sublime.Window) else value diff --git a/plugin/utils.py b/plugin/utils.py index 4aa90b84..90f5bc18 100644 --- a/plugin/utils.py +++ b/plugin/utils.py @@ -63,6 +63,11 @@ def compile_regex(regex: str | Pattern[str], flags: int = 0) -> Pattern[str]: return re.compile(regex, flags) +def drop_falsy(iterable: Iterable[_T | None]) -> Generator[_T, None, None]: + """Drops falsy values from the iterable.""" + yield from filter(None, iterable) + + def get_fqcn(obj: Any) -> str: if obj is None: return "None" @@ -86,7 +91,7 @@ def merge_regexes(regexes: Iterable[str]) -> str: """Merge regex strings into a single regex string.""" regexes = tuple(regexes) if len(regexes) == 0: - return "" + return r"~^(?#match nothing)" if len(regexes) == 1: merged = regexes[0] else: