Skip to content

Commit

Permalink
refactor: use pydantic to validate settings
Browse files Browse the repository at this point in the history
Signed-off-by: Jack Cherng <[email protected]>
  • Loading branch information
jfcherng committed Nov 3, 2024
1 parent ad41ac1 commit 589fb61
Show file tree
Hide file tree
Showing 8 changed files with 138 additions and 121 deletions.
28 changes: 10 additions & 18 deletions plugin/commands/auto_set_syntax_debug_information.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@

from typing import Any, Mapping

import sublime
import sublime_plugin

from ..constants import PLUGIN_NAME, PY_VERSION, ST_CHANNEL, ST_PLATFORM_ARCH, ST_VERSION, VERSION, VIEW_KEY_IS_CREATED
from ..constants import PLUGIN_NAME, PY_VERSION, ST_CHANNEL, ST_PLATFORM_ARCH, ST_VERSION, VERSION
from ..helpers import copy_or_new_file
from ..rules.constraint import get_constraints
from ..rules.match import get_matches
from ..settings import get_merged_plugin_settings
from ..shared import G
from ..utils import find_syntax_by_syntax_like, get_fqcn, stringify
from ..utils import get_fqcn, stringify

TEMPLATE = f"""
# === [{PLUGIN_NAME}] Debug Information === #
Expand Down Expand Up @@ -68,18 +68,10 @@ def run(self, *, copy_only: bool = False) -> None:

content = TEMPLATE.format_map(_pythonize(info))

if copy_only:
sublime.set_clipboard(content)
sublime.message_dialog(f"[{PLUGIN_NAME}] The result has been copied to the clipboard.")
return

view = self.window.new_file()
view.set_name(self.description())
view.set_scratch(True)
view.run_command("append", {"characters": content})
view.settings().update({
VIEW_KEY_IS_CREATED: True,
})

if syntax_ := find_syntax_by_syntax_like("scope:source.python"):
view.assign_syntax(syntax_)
copy_or_new_file(
content,
copy_only=False,
window=self.window,
view_name=self.description(),
view_syntax="scope:source.python",
)
62 changes: 20 additions & 42 deletions plugin/commands/auto_set_syntax_syntax_rules_summary.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,22 +5,10 @@
import sublime
import sublime_plugin

from ..constants import PLUGIN_NAME, VIEW_KEY_IS_CREATED
from ..rules import SyntaxRule
from ..constants import PLUGIN_NAME
from ..helpers import copy_or_new_file
from ..shared import G
from ..utils import find_syntax_by_syntax_like, stringify

TEMPLATE = f"""
# === [{PLUGIN_NAME}] Syntax Rules Summary === #
# You may use the following website to beautify this debug information.
# @link https://play.ruff.rs/?secondary=Format
########################
# Syntax Rules Summary #
########################
{{content}}
""".lstrip()
from ..types import StSyntaxRule


class AutoSetSyntaxSyntaxRulesSummaryCommand(sublime_plugin.WindowCommand):
Expand All @@ -31,31 +19,21 @@ def run(self, *, copy_only: bool = False) -> None:
if not (rule_collection := G.syntax_rule_collections.get(self.window)):
return

summary: defaultdict[sublime.Syntax, list[SyntaxRule]] = defaultdict(list)
summary: defaultdict[sublime.Syntax, list[StSyntaxRule]] = defaultdict(list)
for rule in rule_collection.rules:
if rule.syntax:
summary[rule.syntax].append(rule)

content = ""
for syntax_, rules in sorted(summary.items(), key=lambda x: x[0].name.casefold()):
content += f"# Syntax: {syntax_.name}\n"
for rule in rules:
content += f"{stringify(rule)}\n"
content += "\n"
content = TEMPLATE.format(content=content)

if copy_only:
sublime.set_clipboard(content)
sublime.message_dialog(f"[{PLUGIN_NAME}] The result has been copied to the clipboard.")
return

view = self.window.new_file()
view.set_name(self.description())
view.set_scratch(True)
view.run_command("append", {"characters": content})
view.settings().update({
VIEW_KEY_IS_CREATED: True,
})

if syntax := find_syntax_by_syntax_like("scope:source.python"):
view.assign_syntax(syntax)
if rule.syntax and rule.src_setting:
summary[rule.syntax].append(rule.src_setting)

content = f"// [{PLUGIN_NAME}] Syntax Rules Summary\n\n"
for syntax, rules in sorted(summary.items(), key=lambda x: x[0].name.casefold()):
content += "/" * 80 + "\n" + f"// Syntax: {syntax.name}\n" + "/" * 80 + "\n"
content += "\n".join(st_rule.model_dump_json(indent=4) for st_rule in rules)
content += "\n\n"

copy_or_new_file(
content,
copy_only=False,
window=self.window,
view_name=self.description(),
view_syntax="scope:source.json",
)
33 changes: 32 additions & 1 deletion plugin/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@

import sublime

from .constants import PLUGIN_NAME, VIEW_KEY_IS_CREATED
from .settings import get_st_setting
from .utils import is_plaintext_syntax, is_transient_view
from .utils import find_syntax_by_syntax_like, is_plaintext_syntax, is_transient_view


def is_syntaxable_view(view: sublime.View, *, must_plaintext: bool = False) -> bool:
Expand Down Expand Up @@ -44,3 +45,33 @@ def resolve_magika_label_with_syntax_map(label: str, syntax_map: dict[str, list[
res[scope] = True # parsed

return [scope for scope, is_parsed in res.items() if is_parsed]


def copy_or_new_file(
content: str,
*,
copy_only: bool = True,
window: sublime.Window | None = None,
view_name: str = "Untitled",
view_syntax: str | sublime.Syntax | None = None,
) -> sublime.View | None:
"""Copies the content to the clipboard or a new view."""

if copy_only:
sublime.set_clipboard(content)
sublime.message_dialog(f"[{PLUGIN_NAME}] The result has been copied to the clipboard.")
return None

window = window or sublime.active_window()
view = window.new_file()
view.set_name(view_name)
view.set_scratch(True)
view.run_command("append", {"characters": content})
view.settings().update({
VIEW_KEY_IS_CREATED: True,
})

if view_syntax and (syntax := find_syntax_by_syntax_like(view_syntax)):
view.assign_syntax(syntax)

return view
23 changes: 12 additions & 11 deletions plugin/rules/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

from ..cache import clearable_lru_cache
from ..constants import PLUGIN_NAME, ST_PLATFORM
from ..logger import Logger
from ..snapshot import ViewSnapshot
from ..types import Optimizable, StConstraintRule
from ..utils import (
Expand Down Expand Up @@ -46,7 +47,10 @@ class ConstraintRule(Optimizable):
constraint_name: str = ""
args: tuple[Any, ...] = tuple()
kwargs: dict[str, Any] = field(default_factory=dict)
inverted: bool = False # whether the test result should be inverted
inverted: bool = False

src_setting: StConstraintRule | None = None
"""The source setting object."""

def is_droppable(self) -> bool:
return not (self.constraint and not self.constraint.is_droppable())
Expand Down Expand Up @@ -74,21 +78,18 @@ def test(self, view_snapshot: ViewSnapshot) -> bool:
def make(cls, constraint_rule: StConstraintRule) -> Self:
"""Build this object with the `constraint_rule`."""
obj = cls()
obj.src_setting = constraint_rule

if args := constraint_rule.get("args"):
# make sure args is always a tuple
obj.args = tuple(args) if isinstance(args, list) else (args,)

if kwargs := constraint_rule.get("kwargs"):
obj.kwargs = kwargs

if (inverted := constraint_rule.get("inverted")) is not None:
obj.inverted = bool(inverted)
obj.args = tuple(constraint_rule.args)
obj.kwargs = constraint_rule.kwargs
obj.inverted = constraint_rule.inverted

if constraint := constraint_rule.get("constraint"):
if constraint := constraint_rule.constraint:
obj.constraint_name = constraint
if constraint_class := find_constraint(constraint):
obj.constraint = constraint_class(*obj.args, **obj.kwargs)
else:
Logger.log(f"Unsupported constraint rule: {constraint}")

return obj

Expand Down
27 changes: 14 additions & 13 deletions plugin/rules/match.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
from typing_extensions import Self

from ..cache import clearable_lru_cache
from ..logger import Logger
from ..snapshot import ViewSnapshot
from ..types import Optimizable, StMatchRule
from ..types import Optimizable, StConstraintRule, StMatchRule
from ..utils import camel_to_snake, list_all_subclasses, remove_suffix
from .constraint import ConstraintRule

Expand All @@ -30,14 +31,15 @@ def list_matches() -> Generator[type[AbstractMatch], None, None]:

@dataclass
class MatchRule(Optimizable):
DEFAULT_MATCH_NAME = "any"

match: AbstractMatch | None = None
match_name: str = ""
args: tuple[Any, ...] = tuple()
kwargs: dict[str, Any] = field(default_factory=dict)
rules: tuple[MatchableRule, ...] = tuple()

src_setting: StMatchRule | None = None
"""The source setting object."""

def is_droppable(self) -> bool:
return not (self.rules and self.match and not self.match.is_droppable(self.rules))

Expand All @@ -62,25 +64,24 @@ def test(self, view_snapshot: ViewSnapshot) -> bool:
def make(cls, match_rule: StMatchRule) -> Self:
"""Build this object with the `match_rule`."""
obj = cls()
obj.src_setting = match_rule

if args := match_rule.get("args"):
# make sure args is always a tuple
obj.args = tuple(args) if isinstance(args, list) else (args,)

if kwargs := match_rule.get("kwargs"):
obj.kwargs = kwargs
obj.args = tuple(match_rule.args)
obj.kwargs = match_rule.kwargs

match = match_rule.get("match", cls.DEFAULT_MATCH_NAME)
match = match_rule.match
if match_class := find_match(match):
obj.match_name = match
obj.match = match_class(*obj.args, **obj.kwargs)
else:
Logger.log(f"Unsupported match rule: {match}")

rules_compiled: list[MatchableRule] = []
for rule in match_rule.get("rules", []):
for rule in match_rule.rules:
rule_class: type[MatchableRule] | None = None
if "constraint" in rule:
if isinstance(rule, StConstraintRule):
rule_class = ConstraintRule
elif "rules" in rule: # nested MatchRule
elif isinstance(rule, StMatchRule):
rule_class = MatchRule
if rule_class and (rule_compiled := rule_class.make(rule)): # type: ignore
rules_compiled.append(rule_compiled)
Expand Down
21 changes: 8 additions & 13 deletions plugin/rules/syntax.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ class SyntaxRule(Optimizable):
"""`None` = no restriction, empty = no event = never triggered."""
root_rule: MatchRule | None = None

src_setting: StSyntaxRule | None = None
"""The source setting object."""

def is_droppable(self) -> bool:
return not (self.syntax and (self.on_events is None or self.on_events) and self.root_rule)

Expand Down Expand Up @@ -56,24 +59,16 @@ def test(self, view_snapshot: ViewSnapshot, event: ListenerEvent | None = None)
def make(cls, syntax_rule: StSyntaxRule) -> Self:
"""Build this object with the `syntax_rule`."""
obj = cls()
obj.src_setting = syntax_rule

if comment := syntax_rule.get("comment"):
obj.comment = str(comment)

syntaxes = syntax_rule.get("syntaxes", [])
if isinstance(syntaxes, str):
syntaxes = [syntaxes]
obj.syntaxes_name = tuple(syntaxes)
if target_syntax := find_syntax_by_syntax_likes(syntaxes):
obj.syntaxes_name = tuple(syntax_rule.syntaxes)
if target_syntax := find_syntax_by_syntax_likes(syntax_rule.syntaxes):
obj.syntax = target_syntax

# note that an empty string selector should match any scope
if (selector := syntax_rule.get("selector")) is not None:
obj.selector = selector
obj.selector = syntax_rule.selector

if (on_events := syntax_rule.get("on_events")) is not None:
if isinstance(on_events, str):
on_events = [on_events]
if (on_events := syntax_rule.on_events) is not None:
obj.on_events = set(drop_falsy(map(ListenerEvent.from_value, on_events)))

if match_rule_compiled := MatchRule.make(syntax_rule):
Expand Down
11 changes: 6 additions & 5 deletions plugin/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@

from collections import ChainMap
from itertools import chain
from typing import Any, Callable, Mapping, MutableMapping
from typing import Any, Callable, List, Mapping, MutableMapping

import sublime
import sublime_plugin
from more_itertools import unique_everseen
from pydantic import TypeAdapter

from .types import StSyntaxRule
from .utils import drop_falsy
Expand Down Expand Up @@ -34,7 +35,7 @@ def get_st_settings() -> sublime.Settings:


def pref_syntax_rules(*, window: sublime.Window | None = None) -> list[StSyntaxRule]:
return get_merged_plugin_setting("syntax_rules", [], window=window)
return TypeAdapter(List[StSyntaxRule]).validate_python(get_merged_plugin_setting("syntax_rules", [], window=window))


def pref_trim_suffixes(*, window: sublime.Window | None = None) -> tuple[str]:
Expand Down Expand Up @@ -194,15 +195,15 @@ def _on_settings_change(cls, windows: list[sublime.Window] | None = None, run_ca
@classmethod
def _update_plugin_settings(cls) -> None:
assert cls._plugin_settings_object
cls._plugin_settings = {"_comment": "plugin_settings"}
cls._plugin_settings = {"__comment": "plugin_settings"}
cls._plugin_settings.update(cls._plugin_settings_object.to_dict())
if cls._settings_normalizer:
cls._settings_normalizer(cls._plugin_settings)

@classmethod
def _update_project_plugin_settings(cls, window: sublime.Window) -> None:
window_id = window.id()
cls._project_plugin_settings[window_id] = {"_comment": "project_settings"}
cls._project_plugin_settings[window_id] = {"__comment": "project_settings"}
cls._project_plugin_settings[window_id].update(
(window.project_data() or {}).get("settings", {}).get(cls.plugin_name, {})
)
Expand All @@ -218,7 +219,7 @@ def _update_merged_plugin_settings(cls, window: sublime.Window) -> None:
cls._plugin_settings,
)

produced = {"_comment": "produced_settings"}
produced = {"__comment": "produced_settings"}
if cls._settings_producer:
produced.update(cls._settings_producer(merged))

Expand Down
Loading

0 comments on commit 589fb61

Please sign in to comment.