Skip to content

Commit bdcd26a

Browse files
committed
plugins: get rid of PluginManager singleton
This rids us of some tech debt and will facilitate configuration of the plugin manager in a future commit.
1 parent 3a989b0 commit bdcd26a

File tree

10 files changed

+71
-74
lines changed

10 files changed

+71
-74
lines changed

src/tclint/checks.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import re
22

3-
from tclint.commands import get_commands
3+
from tclint.commands.plugins import PluginManager
44
from tclint.config import Config
55
from tclint.syntax_tree import (
66
BracedExpression,
@@ -82,11 +82,14 @@ class RedefinedBuiltinChecker(Visitor):
8282
Reports 'redefined-builtin' violations.
8383
"""
8484

85+
def __init__(self, plugin_manager: PluginManager):
86+
self._plugin_manager = plugin_manager
87+
8588
def check(self, _, tree: Script, config: Config) -> list[Violation]:
8689
self._violations: list[Violation] = []
8790

8891
plugins = [config.commands] if config.commands is not None else []
89-
commands = get_commands(plugins)
92+
commands = self._plugin_manager.get_commands(plugins)
9093
self._commands = commands.keys()
9194

9295
tree.accept(self, recurse=True)
@@ -216,9 +219,9 @@ def visit_function(self, function):
216219
self._check_operand(arg)
217220

218221

219-
def get_checkers():
222+
def get_checkers(plugin_manager: PluginManager):
220223
checkers = (
221-
RedefinedBuiltinChecker(),
224+
RedefinedBuiltinChecker(plugin_manager),
222225
UnbracedExprChecker(),
223226
RedundantExprChecker(),
224227
LineLengthChecker(),

src/tclint/cli/tclfmt.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
from tclint.cli.resolver import Resolver
88
from tclint.cli.utils import register_codec_warning
9+
from tclint.commands.plugins import PluginManager
910
from tclint.config import Config, ConfigError, setup_tclfmt_config_cli_args
1011
from tclint.format import Formatter, FormatterOpts
1112
from tclint.parser import Parser, TclSyntaxError
@@ -22,9 +23,11 @@
2223
EXIT_INPUT_ERROR = 4
2324

2425

25-
def format(script: str, config: Config, debug=False, partial=False) -> str:
26-
plugins = [config.commands] if config.commands is not None else []
27-
parser = Parser(debug=debug, command_plugins=plugins)
26+
def format(
27+
script: str, config: Config, plugins: PluginManager, debug=False, partial=False
28+
) -> str:
29+
_plugins = [config.commands] if config.commands is not None else []
30+
parser = Parser(debug=debug, commands=plugins.get_commands(_plugins))
2831

2932
formatter = Formatter(
3033
FormatterOpts(
@@ -125,6 +128,8 @@ def main():
125128
print(f"Invalid config file: {e}")
126129
return EXIT_INPUT_ERROR
127130

131+
plugin_manager = PluginManager()
132+
128133
retcode = EXIT_OK
129134

130135
register_codec_warning("replace_with_warning")
@@ -143,6 +148,7 @@ def main():
143148
formatted = format(
144149
script,
145150
config,
151+
plugin_manager,
146152
debug=(args.debug > 1),
147153
partial=args.partial,
148154
)

src/tclint/cli/tclint.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from tclint.checks import get_checkers
99
from tclint.cli.resolver import Resolver
1010
from tclint.cli.utils import register_codec_warning
11+
from tclint.commands.plugins import PluginManager
1112
from tclint.comments import CommentVisitor
1213
from tclint.config import Config, ConfigError, setup_config_cli_args
1314
from tclint.parser import Parser, TclSyntaxError
@@ -47,11 +48,12 @@ def filter_violations(
4748
def lint(
4849
script: str,
4950
config: Config,
51+
plugins: PluginManager,
5052
path: Optional[pathlib.Path],
5153
debug=0,
5254
) -> list[Violation]:
53-
plugins = [config.commands] if config.commands is not None else []
54-
parser = Parser(debug=(debug > 0), command_plugins=plugins)
55+
_plugins = [config.commands] if config.commands is not None else []
56+
parser = Parser(debug=(debug > 0), commands=plugins.get_commands(_plugins))
5557

5658
violations = []
5759
tree = parser.parse(script)
@@ -60,7 +62,7 @@ def lint(
6062
if debug > 0:
6163
print(tree.pretty(positions=(debug > 1)))
6264

63-
for checker in get_checkers():
65+
for checker in get_checkers(plugins):
6466
violations += checker.check(script, tree, config)
6567

6668
v = CommentVisitor()
@@ -126,6 +128,8 @@ def main():
126128
print(f"Invalid config file: {e}")
127129
return EXIT_INPUT_ERROR
128130

131+
plugin_manager = PluginManager()
132+
129133
retcode = EXIT_OK
130134

131135
register_codec_warning("replace_with_warning")
@@ -143,6 +147,7 @@ def main():
143147
violations = lint(
144148
script,
145149
config,
150+
plugin_manager,
146151
path,
147152
debug=args.debug,
148153
)

src/tclint/cli/tclsp.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from pygls.workspace import TextDocument
1212

1313
from tclint.cli import tclint, utils
14+
from tclint.commands.plugins import PluginManager
1415
from tclint.config import DEFAULT_CONFIGS, Config, ConfigError, load_config_at
1516
from tclint.format import Formatter, FormatterOpts
1617
from tclint.lexer import TclSyntaxError
@@ -26,11 +27,11 @@
2627
_DEFAULT_CONFIG = Config()
2728

2829

29-
def lint(source, config, path):
30+
def lint(source, config, plugin_manager, path):
3031
diagnostics = []
3132

3233
try:
33-
violations = tclint.lint(source, config, path)
34+
violations = tclint.lint(source, config, plugin_manager, path)
3435
except TclSyntaxError as e:
3536
return [
3637
lsp.Diagnostic(
@@ -99,6 +100,8 @@ def __init__(self, *args, **kwargs):
99100
self.global_settings = ExtensionSettings()
100101
self.workspace_settings: dict[Path, ExtensionSettings] = {}
101102

103+
self.plugin_manager = PluginManager()
104+
102105
def get_roots(self) -> list[Path]:
103106
"""Returns root folders currently open in the workspace."""
104107
roots = []
@@ -221,7 +224,7 @@ def _compute_diagnostics(self, document: TextDocument) -> list[lsp.Diagnostic]:
221224
if is_excluded(path):
222225
return []
223226

224-
return lint(document.source, config, path)
227+
return lint(document.source, config, self.plugin_manager, path)
225228

226229
def compute_diagnostics(self, document: TextDocument):
227230
# `None` sentinel ensures that `diagnostics` gets updated if the URI is not

src/tclint/commands/__init__.py

Lines changed: 2 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,4 @@
1-
import pathlib
2-
from collections.abc import Sequence
3-
4-
from tclint.commands import builtin as _builtin
5-
6-
# import to expose in package
1+
# Import to expose in package.
72
from tclint.commands.checks import CommandArgError
8-
from tclint.commands.plugins import PluginManager
9-
10-
__all__ = ["CommandArgError", "validate_command_plugins", "get_commands"]
11-
12-
13-
def validate_command_plugins(plugins: list[str]) -> list[str]:
14-
valid_plugins = []
15-
for plugin in set(plugins):
16-
if PluginManager.load(plugin) is not None:
17-
valid_plugins.append(plugin)
18-
19-
return valid_plugins
20-
21-
22-
def get_commands(plugins: Sequence[str | pathlib.Path]) -> dict:
23-
commands = {}
24-
commands.update(_builtin.commands)
25-
26-
for plugin in plugins:
27-
if isinstance(plugin, str):
28-
plugin_commands = PluginManager.load(plugin)
29-
elif isinstance(plugin, pathlib.Path):
30-
if plugin.suffix == ".py":
31-
plugin_commands = PluginManager.load_from_py(plugin)
32-
else:
33-
plugin_commands = PluginManager.load_from_spec(plugin)
34-
else:
35-
raise TypeError(f"Plugins must be strings or paths, got {type(plugin)}")
36-
37-
if plugin_commands is not None:
38-
commands.update(plugin_commands)
393

40-
return commands
4+
__all__ = ["CommandArgError"]

src/tclint/commands/plugins.py

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import json
22
import pathlib
3+
from collections.abc import Sequence
34
from importlib.util import module_from_spec, spec_from_file_location
45
from types import ModuleType
56
from typing import Optional
67

78
import voluptuous
89
from importlib_metadata import entry_points
910

11+
from tclint.commands import builtin as _builtin
1012
from tclint.commands import schema
1113

1214

13-
class _PluginManager:
15+
class PluginManager:
1416
def __init__(self):
1517
self._loaded = {}
1618
self._installed = {}
@@ -123,8 +125,22 @@ def _load_from_py(self, path: pathlib.Path) -> Optional[dict]:
123125

124126
return self._load_module(name, mod)
125127

126-
127-
# TODO: we'll probably want to construct this in the tclint entry point and pass
128-
# it around rather than using a singleton instance, but this made for an easier
129-
# refactor.
130-
PluginManager = _PluginManager()
128+
def get_commands(self, plugins: Sequence[str | pathlib.Path]) -> dict:
129+
commands = {}
130+
commands.update(_builtin.commands)
131+
132+
for plugin in plugins:
133+
if isinstance(plugin, str):
134+
plugin_commands = self.load(plugin)
135+
elif isinstance(plugin, pathlib.Path):
136+
if plugin.suffix == ".py":
137+
plugin_commands = self.load_from_py(plugin)
138+
else:
139+
plugin_commands = self.load_from_spec(plugin)
140+
else:
141+
raise TypeError(f"Plugins must be strings or paths, got {type(plugin)}")
142+
143+
if plugin_commands is not None:
144+
commands.update(plugin_commands)
145+
146+
return commands

src/tclint/parser.py

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import io
22
import re
33
import string
4-
from collections.abc import Sequence
5-
from pathlib import Path
64
from typing import Optional, Tuple
75

8-
from tclint.commands import CommandArgError, get_commands
6+
from tclint.commands import CommandArgError
7+
from tclint.commands import builtin as _builtin
98
from tclint.commands.checks import check_command
109
from tclint.lexer import (
1110
STATE_BRACEDWORD,
@@ -103,17 +102,15 @@ def resolve(self, end_pos):
103102

104103

105104
class Parser:
106-
def __init__(
107-
self, debug=False, command_plugins: Optional[Sequence[str | Path]] = None
108-
):
105+
def __init__(self, debug=False, commands: Optional[dict] = None):
109106
self._debug = debug
110107
self._debug_indent = 0
111108
# TODO: better way to handle this?
112109
self.violations: list[Violation] = []
113110

114-
if command_plugins is None:
115-
command_plugins = []
116-
self._commands = get_commands(command_plugins)
111+
if commands is None:
112+
commands = _builtin.commands
113+
self._commands = commands
117114

118115
# Used to normalize newlines consistently with open()'s universal newlines mode.
119116
self._decoder = io.IncrementalNewlineDecoder(None, True)

tests/commands/test_plugin.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
from pathlib import Path
22

3-
# Not clean, but we import the _PluginManager class instead of using the singleton so we
4-
# have isolation between tests.
5-
from tclint.commands.plugins import _PluginManager as PluginManager
3+
from tclint.commands.plugins import PluginManager
64

75
MY_DIR = Path(__file__).parent.resolve()
86
TEST_DATA_DIR = MY_DIR / "data"

tests/plugins/test_expect.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import pytest
22

3-
# Not clean, but we import the _PluginManager class instead of using the singleton so we
4-
# have isolation between tests.
5-
from tclint.commands.plugins import _PluginManager as PluginManager
3+
from tclint.commands.plugins import PluginManager
64
from tclint.parser import Parser
75

86

@@ -13,6 +11,7 @@ def test_load():
1311

1412
@pytest.mark.parametrize("command", ["close", "close -i $spawn_id"])
1513
def test_parse_valid(command):
16-
parser = Parser(command_plugins=["expect"])
14+
plugins = PluginManager()
15+
parser = Parser(commands=plugins.get_commands(["expect"]))
1716
parser.parse(command)
1817
assert len(parser.violations) == 0, f"unexpected violation: {parser.violations[0]}"

tests/test_lint.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,17 @@
11
import json
22
from pathlib import Path
33

4-
from tclint.cli.tclint import lint
4+
from tclint.cli.tclint import lint as _lint
5+
from tclint.commands.plugins import PluginManager
56
from tclint.config import Config
67
from tclint.violations import Rule
78

89

10+
def lint(script, config, path):
11+
plugin_manager = PluginManager()
12+
return _lint(script, config, plugin_manager, path)
13+
14+
915
def test_cmd_args_in_sub():
1016
"""Ensure command args get checked in cmd sub."""
1117
script = "[puts]"

0 commit comments

Comments
 (0)