Skip to content

Commit e77764c

Browse files
Dynamic imports (#922)
* Change: plugins dynamic discovery * Change: dynamic imports improved comments * Change: dynamic import clean up * Change: dynamic import add subclass filter * Add: dynamic plugin import test case * Change: dynamic plugins disable strategy
1 parent 6e03464 commit e77764c

File tree

3 files changed

+160
-149
lines changed

3 files changed

+160
-149
lines changed

tests/plugins/test_todo_tbd.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
from pathlib import Path
1919

2020
from troubadix.plugin import LinterWarning
21-
from troubadix.plugins import CheckTodoTbd
21+
from troubadix.plugins.todo_tbd import CheckTodoTbd
2222

2323
from . import PluginTestCase
2424

tests/test_plugin_discovery.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import unittest
2+
3+
from troubadix.plugin import FilePlugin, FilesPlugin
4+
from troubadix.plugins import _discover_plugins
5+
6+
7+
class TestPluginDiscovery(unittest.TestCase):
8+
def test_dynamic_plugin_discovery(self):
9+
file_plugins, files_plugins = _discover_plugins()
10+
# Check all discovered plugins are subclasses and have a name
11+
for plugin in file_plugins + files_plugins:
12+
self.assertTrue(
13+
issubclass(plugin, (FilePlugin, FilesPlugin)),
14+
f"{plugin.__name__} is not a valid plugin subclass",
15+
)
16+
self.assertTrue(
17+
hasattr(plugin, "name"), f"{plugin.__name__} does not have a 'name' attribute"
18+
)
19+
20+
def test_disabled_plugins_are_excluded(self):
21+
class DummyDisabledPlugin(FilePlugin):
22+
name = "dummy_disabled"
23+
is_disabled = True
24+
25+
DummyDisabledPlugin.__module__ = "troubadix.plugins.dummy"
26+
27+
file_plugins, _ = _discover_plugins()
28+
self.assertNotIn(DummyDisabledPlugin, file_plugins)
29+
30+
class DummyEnabledPlugin(FilePlugin):
31+
name = "dummy_enabled"
32+
33+
DummyEnabledPlugin.__module__ = "troubadix.plugins.dummy"
34+
35+
file_plugins, _ = _discover_plugins()
36+
self.assertIn(DummyEnabledPlugin, file_plugins)
37+
38+
def test_deep_inheritance_hierarchy(self):
39+
class ParentPlugin(FilePlugin):
40+
name = "parent"
41+
42+
class ChildPlugin(ParentPlugin):
43+
name = "child"
44+
45+
class GrandChildPlugin(ChildPlugin):
46+
name = "grandchild"
47+
48+
ParentPlugin.__module__ = "troubadix.plugins.hierarchy"
49+
ChildPlugin.__module__ = "troubadix.plugins.hierarchy"
50+
GrandChildPlugin.__module__ = "troubadix.plugins.hierarchy"
51+
52+
file_plugins, _ = _discover_plugins()
53+
54+
self.assertIn(ParentPlugin, file_plugins)
55+
self.assertIn(ChildPlugin, file_plugins)
56+
self.assertIn(GrandChildPlugin, file_plugins)
57+
58+
def test_is_disabled_inheritance(self):
59+
class BaseDisabledPlugin(FilePlugin):
60+
name = "base_disabled"
61+
is_disabled = True
62+
63+
class InheritedDisabledPlugin(BaseDisabledPlugin):
64+
name = "inherited_disabled"
65+
66+
BaseDisabledPlugin.__module__ = "troubadix.plugins.test"
67+
InheritedDisabledPlugin.__module__ = "troubadix.plugins.test"
68+
69+
file_plugins, _ = _discover_plugins()
70+
self.assertNotIn(BaseDisabledPlugin, file_plugins)
71+
self.assertNotIn(InheritedDisabledPlugin, file_plugins)
72+
73+
def test_is_disabled_override(self):
74+
class BaseDisabledPlugin(FilePlugin):
75+
name = "base_disabled"
76+
is_disabled = True
77+
78+
class ReEnabledPlugin(BaseDisabledPlugin):
79+
name = "re_enabled"
80+
is_disabled = False
81+
82+
BaseDisabledPlugin.__module__ = "troubadix.plugins.test"
83+
ReEnabledPlugin.__module__ = "troubadix.plugins.test"
84+
85+
file_plugins, _ = _discover_plugins()
86+
self.assertNotIn(BaseDisabledPlugin, file_plugins)
87+
self.assertIn(ReEnabledPlugin, file_plugins)
88+
89+
def test_files_plugin_disabled(self):
90+
class DisabledFilesPlugin(FilesPlugin):
91+
name = "disabled_files"
92+
is_disabled = True
93+
94+
DisabledFilesPlugin.__module__ = "troubadix.plugins.test"
95+
96+
_, files_plugins = _discover_plugins()
97+
self.assertNotIn(DisabledFilesPlugin, files_plugins)
98+
99+
def test_external_plugin_exclusion(self):
100+
class ExternalPlugin(FilePlugin):
101+
name = "external"
102+
103+
# Simulate a plugin defined outside the troubadix.plugins package
104+
ExternalPlugin.__module__ = "some_other_package.plugins"
105+
106+
file_plugins, _ = _discover_plugins()
107+
self.assertNotIn(ExternalPlugin, file_plugins)

troubadix/plugins/__init__.py

Lines changed: 52 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -15,153 +15,57 @@
1515
# You should have received a copy of the GNU General Public License
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

18+
"""
19+
This package contains all linter plugins.
20+
Plugins are discovered dynamically at runtime. To add a new plugin:
21+
1. Create a new .py file in this directory.
22+
2. Define a class that inherits from FilePlugin or FilesPlugin.
23+
24+
The discovery logic only searches the top-level of this package.
25+
Nested sub-packages are currently not supported for plugin discovery,
26+
which excludes plugins in subfolders as a byproduct. However, the
27+
intended way to disable a plugin is to add `is_disabled = True` to its class.
28+
"""
29+
1830
import difflib
19-
from typing import Iterable, List
31+
import importlib
32+
import pkgutil
33+
from typing import Iterable, Type
2034

2135
from troubadix.plugin import FilePlugin, FilesPlugin, Plugin
22-
from troubadix.plugins.spaces_before_dots import CheckSpacesBeforeDots
23-
24-
from .badwords import CheckBadwords
25-
from .copyright_text import CheckCopyrightText
26-
from .copyright_year import CheckCopyrightYear
27-
from .creation_date import CheckCreationDate
28-
from .cve_format import CheckCVEFormat
29-
from .cvss_format import CheckCVSSFormat
30-
from .dependencies import CheckDependencies
31-
from .dependency_category_order import CheckDependencyCategoryOrder
32-
from .deprecated_dependency import CheckDeprecatedDependency
33-
from .deprecated_functions import CheckDeprecatedFunctions
34-
from .double_end_points import CheckDoubleEndPoints
35-
from .duplicate_oid import CheckDuplicateOID
36-
from .duplicated_script_tags import CheckDuplicatedScriptTags
37-
from .encoding import CheckEncoding
38-
from .forking_nasl_functions import CheckForkingNaslFunctions
39-
from .get_kb_on_services import CheckGetKBOnServices
40-
from .grammar import CheckGrammar
41-
from .http_links_in_tags import CheckHttpLinksInTags
42-
from .if_statement_syntax import CheckIfStatementSyntax
43-
from .illegal_characters import CheckIllegalCharacters
44-
from .infos_array_keys import CheckInfosArrayKeys
45-
from .log_messages import CheckLogMessages
46-
from .malformed_dependencies import CheckMalformedDependencies
47-
from .misplaced_compare_in_if import CheckMisplacedCompareInIf
48-
from .missing_desc_exit import CheckMissingDescExit
49-
from .missing_tag_solution import CheckMissingTagSolution
50-
from .multiple_re_parameters import CheckMultipleReParameters
51-
from .newlines import CheckNewlines
52-
from .overlong_description_lines import CheckOverlongDescriptionLines
53-
from .overlong_script_tags import CheckOverlongScriptTags
54-
from .prod_svc_detect_in_vulnvt import CheckProdSvcDetectInVulnvt
55-
from .qod import CheckQod
56-
from .reporting_consistency import CheckReportingConsistency
57-
from .script_add_preference_id import CheckScriptAddPreferenceId
58-
from .script_add_preference_type import CheckScriptAddPreferenceType
59-
from .script_calls_empty_values import CheckScriptCallsEmptyValues
60-
from .script_calls_recommended import CheckScriptCallsRecommended
61-
from .script_category import CheckScriptCategory
62-
from .script_copyright import CheckScriptCopyright
63-
from .script_family import CheckScriptFamily
64-
from .script_tag_form import CheckScriptTagForm
65-
from .script_tag_whitespaces import CheckScriptTagWhitespaces
66-
from .script_tags_mandatory import CheckScriptTagsMandatory
67-
from .script_version_and_last_modification_tags import (
68-
CheckScriptVersionAndLastModificationTags,
69-
)
70-
from .script_xref_form import CheckScriptXrefForm
71-
from .script_xref_url import CheckScriptXrefUrl
72-
from .security_messages import CheckSecurityMessages
73-
from .set_get_kb_calls import CheckWrongSetGetKBCalls
74-
from .severity_date import CheckSeverityDate
75-
from .severity_format import CheckSeverityFormat
76-
from .severity_origin import CheckSeverityOrigin
77-
from .solution_text import CheckSolutionText
78-
from .solution_type import CheckSolutionType
79-
from .spaces_in_filename import CheckSpacesInFilename
80-
from .spelling import CheckSpelling
81-
from .tabs import CheckTabs
82-
from .todo_tbd import CheckTodoTbd
83-
from .trailing_spaces_tabs import CheckTrailingSpacesTabs
84-
from .using_display import CheckUsingDisplay
85-
from .valid_oid import CheckValidOID
86-
from .valid_script_tag_names import CheckValidScriptTagNames
87-
from .variable_assigned_in_if import CheckVariableAssignedInIf
88-
from .variable_redefinition_in_foreach import CheckVariableRedefinitionInForeach
89-
from .vt_file_permissions import CheckVTFilePermissions
90-
from .vt_placement import CheckVTPlacement
91-
92-
# plugins checking single files
93-
_FILE_PLUGINS = [
94-
CheckBadwords,
95-
CheckCopyrightText,
96-
CheckCopyrightYear,
97-
CheckCreationDate,
98-
CheckCVEFormat,
99-
CheckCVSSFormat,
100-
CheckDependencies,
101-
CheckDependencyCategoryOrder,
102-
CheckDeprecatedDependency,
103-
CheckDeprecatedFunctions,
104-
CheckDoubleEndPoints,
105-
CheckDuplicatedScriptTags,
106-
CheckEncoding,
107-
CheckForkingNaslFunctions,
108-
CheckGetKBOnServices,
109-
CheckGrammar,
110-
CheckHttpLinksInTags,
111-
CheckIllegalCharacters,
112-
CheckLogMessages,
113-
CheckMalformedDependencies,
114-
CheckMisplacedCompareInIf,
115-
CheckMissingDescExit,
116-
CheckMissingTagSolution,
117-
CheckMultipleReParameters,
118-
CheckNewlines,
119-
CheckOverlongDescriptionLines,
120-
CheckOverlongScriptTags,
121-
CheckProdSvcDetectInVulnvt,
122-
CheckQod,
123-
CheckReportingConsistency,
124-
CheckScriptAddPreferenceType,
125-
CheckScriptCallsEmptyValues,
126-
CheckScriptCallsRecommended,
127-
CheckScriptCategory,
128-
CheckScriptCopyright,
129-
CheckScriptFamily,
130-
CheckScriptTagForm,
131-
CheckScriptTagsMandatory,
132-
CheckScriptTagWhitespaces,
133-
CheckScriptVersionAndLastModificationTags,
134-
CheckScriptXrefForm,
135-
CheckScriptXrefUrl,
136-
CheckSecurityMessages,
137-
CheckSeverityDate,
138-
CheckSeverityFormat,
139-
CheckSeverityOrigin,
140-
CheckSolutionText,
141-
CheckSolutionType,
142-
CheckSpacesInFilename,
143-
CheckTabs,
144-
CheckTodoTbd,
145-
CheckTrailingSpacesTabs,
146-
CheckUsingDisplay,
147-
CheckValidOID,
148-
CheckValidScriptTagNames,
149-
CheckVariableAssignedInIf,
150-
CheckVariableRedefinitionInForeach,
151-
CheckVTFilePermissions,
152-
CheckVTPlacement,
153-
CheckWrongSetGetKBCalls,
154-
CheckSpacesBeforeDots,
155-
CheckIfStatementSyntax,
156-
CheckScriptAddPreferenceId,
157-
CheckInfosArrayKeys,
158-
]
159-
160-
# plugins checking all files
161-
_FILES_PLUGINS = [
162-
CheckDuplicateOID,
163-
CheckSpelling,
164-
]
36+
37+
38+
def _get_all_subclasses(cls: Type) -> Iterable[Type]:
39+
"""Recursively find all subclasses of a given class."""
40+
for subclass in cls.__subclasses__():
41+
yield subclass
42+
yield from _get_all_subclasses(subclass)
43+
44+
45+
def _discover_plugins() -> tuple[list[Type[FilePlugin]], list[Type[FilesPlugin]]]:
46+
"""
47+
Dynamically discover all concrete plugin classes.
48+
49+
A tuple containing (list of file plugins, list of files plugins).
50+
"""
51+
for _loader, module_name, _is_pkg in pkgutil.iter_modules(__path__):
52+
importlib.import_module(f"{__name__}.{module_name}")
53+
54+
def _is_valid_plugin(cls: Type) -> bool:
55+
return cls.__module__.startswith(__name__) and not getattr(cls, "is_disabled", False)
56+
57+
# Only include plugins defined in this package and that are not disabled.
58+
# excludes the plugins baseclasses and external plugins.
59+
file_plugins = [cls for cls in _get_all_subclasses(FilePlugin) if _is_valid_plugin(cls)]
60+
files_plugins = [cls for cls in _get_all_subclasses(FilesPlugin) if _is_valid_plugin(cls)]
61+
62+
return (
63+
sorted(file_plugins, key=lambda x: x.__name__),
64+
sorted(files_plugins, key=lambda x: x.__name__),
65+
)
66+
67+
68+
_FILE_PLUGINS, _FILES_PLUGINS = _discover_plugins()
16569

16670

16771
class Plugins:
@@ -183,8 +87,8 @@ def __iter__(self) -> Iterable[Plugin]:
18387
class StandardPlugins(Plugins):
18488
def __init__(
18589
self,
186-
excluded_plugins: List[str] = None,
187-
included_plugins: List[str] = None,
90+
excluded_plugins: list[str] = None,
91+
included_plugins: list[str] = None,
18892
) -> None:
18993
file_plugins = _FILE_PLUGINS
19094
files_plugins = _FILES_PLUGINS
@@ -204,15 +108,15 @@ def __init__(
204108
super().__init__(file_plugins=file_plugins, files_plugins=files_plugins)
205109

206110
@staticmethod
207-
def _exclude_plugins(excluded: Iterable[str], plugins: Iterable[Plugin]) -> List[Plugin]:
111+
def _exclude_plugins(excluded: Iterable[str], plugins: Iterable[Plugin]) -> list[Plugin]:
208112
return [
209113
plugin
210114
for plugin in plugins
211115
if plugin.__name__ not in excluded and plugin.name not in excluded
212116
]
213117

214118
@staticmethod
215-
def _include_plugins(included: Iterable[str], plugins: Iterable[Plugin]) -> List[Plugin]:
119+
def _include_plugins(included: Iterable[str], plugins: Iterable[Plugin]) -> list[Plugin]:
216120
return [
217121
plugin for plugin in plugins if plugin.__name__ in included or plugin.name in included
218122
]

0 commit comments

Comments
 (0)