Skip to content

Commit dd638e1

Browse files
authored
Feat/allow relative imports in custom rule folder (#1150)
* Changed `unused-keyword` introduction version to 5.3.0 * Added workaround for allowing relative imports within custom rule folders * Added test for relative imports in custom rules folder * Simplified F string * Added docs on how to import from a rules directory * Renamed utilities.py to constants.py * Fixed some more code blocks * Even mode rule description code block fixes
1 parent aa82a0b commit dd638e1

File tree

10 files changed

+103
-4
lines changed

10 files changed

+103
-4
lines changed

docs/external_rules.rst

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,63 @@ Dotted syntax is also supported::
170170
``rules`` dictionary should be available at the same level as checker that is using it. It could be either defined
171171
or imported from other files.
172172

173+
Import from rules directory
174+
----------------------------
175+
176+
Robocop rules can discovered from a directory. For example, using the following directory
177+
structure::
178+
179+
external_rules/
180+
external_rules/some_rule.py
181+
external_rules/constants.py
182+
setup.py
183+
184+
Inside ``some_rules.py``:
185+
186+
.. code-block:: python
187+
:caption: some_rules.py
188+
189+
from robocop.checkers import VisitorChecker
190+
from robocop.rules import Rule, RuleSeverity
191+
from external_rules.constants import DISALLOWED_KEYWORDS
192+
193+
194+
rules = {
195+
"9903": Rule(rule_id="9903", name="external-rule", msg="This is an external rule", severity=RuleSeverity.INFO)
196+
}
197+
198+
199+
class CustomRule(VisitorChecker):
200+
""" Checker for missing keyword name. """
201+
reports = ("external-rule",)
202+
203+
def visit_KeywordCall(self, node): # noqa: N802
204+
if node.keyword and node.keyword not in DISALLOWED_KEYWORDS:
205+
self.report("external-rule", node=node)
206+
207+
Inside ``constants.py``:
208+
209+
.. code-block:: python
210+
:caption: constants.py
211+
212+
DISALLOWED_KEYWORDS = ['Some Keyword', 'Another Keyword']
213+
214+
Note how you can import other files from the directory:
215+
216+
.. code-block:: python
217+
218+
from external_rules.constants import DISALLOWED_KEYWORDS
219+
220+
You can also import relative to the external rules directory:
221+
222+
.. code-block:: python
223+
224+
from constants import DISALLOWED_KEYWORDS
225+
226+
You can import this rule directory using a relative path to the directory::
227+
228+
robocop --ext-rules ./external_rules .
229+
173230
Rules disabled by default
174231
-------------------------
175232

robocop/checkers/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import ast
3939
import importlib.util
4040
import inspect
41+
import sys
4142
from collections import defaultdict
4243
from importlib import import_module
4344
from pathlib import Path
@@ -202,6 +203,11 @@ def get_community_modules(self):
202203
return self.modules_from_paths([self.community_checkers_dir], recursive=True)
203204

204205
def get_external_modules(self):
206+
for ext_rule_path in self.external_rules_paths:
207+
# Allow relative imports in external rules folder
208+
sys.path.append(ext_rule_path)
209+
sys.path.append(str(Path(ext_rule_path).parent))
210+
205211
return self.modules_from_paths([*self.external_rules_paths], recursive=True)
206212

207213
def _get_checkers_from_modules(self, modules, is_community):

robocop/checkers/community_rules/keywords.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def comma_separated_list(value: str) -> set[str]:
111111
Example:
112112
113113
Using a keyword with one embedded argument. Buying the drink and the size of the drink are
114-
jumbled together.
114+
jumbled together::
115115
116116
*** Test Cases ***
117117
Prepare for an amazing movie
@@ -123,7 +123,7 @@ def comma_separated_list(value: str) -> set[str]:
123123
124124
Change the embedded argument to a normal argument. Now buying the drink is separate from the
125125
size of the drink. In this approach, it's easier to see that you can change the size of your
126-
drink.
126+
drink::
127127
128128
*** Test Cases ***
129129
Prepare for an amazing movie

robocop/checkers/community_rules/misc.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
added_in_version="5.2.0",
1717
enabled=False,
1818
docs="""
19-
Example of rule violation:
19+
Example of rule violation::
2020
2121
*** Settings ***
2222
Library Collections
@@ -33,7 +33,7 @@
3333
added_in_version="5.2.0",
3434
enabled=False,
3535
docs="""
36-
Example of rule violation:
36+
Example of rule violation::
3737
3838
*** Settings ***
3939
Resource CustomResource.resource
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Import sibling file
2+
from foo import EXPECTED_KEYWORD
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Import inside rules folder
2+
from sub_dir.baz import EXPECTED_KEYWORD
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from robocop.checkers import VisitorChecker
2+
from robocop.rules import Rule, RuleSeverity
3+
# Import starting with rules folder
4+
from ext_rule_module_with_relative_import.bar import EXPECTED_KEYWORD
5+
6+
rules = {
7+
"9905": Rule(
8+
rule_id="9905",
9+
name="external-rule",
10+
msg="This is external rule with {{ parameter }} in msg",
11+
severity=RuleSeverity.INFO,
12+
)
13+
}
14+
15+
16+
class CustomRuleChecker(VisitorChecker):
17+
"""Checker for missing keyword name."""
18+
19+
reports = ("external-rule",)
20+
21+
def visit_KeywordCall(self, node): # noqa: N802
22+
if node.keyword and EXPECTED_KEYWORD not in node.keyword:
23+
self.report("external-rule", node=node)

tests/test_data/ext_rules/ext_rule_module_with_relative_import/sub_dir/__init__.py

Whitespace-only changes.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
EXPECTED_KEYWORD = 'Example'

tests/utest/test_external_rules.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
EXT_MODULE = str(TEST_DATA / "ext_rule_module")
1515
EXT_MODULE_SIMPLE = str(TEST_DATA / "ext_rule_module_simple_import")
1616
EXT_MODULE_ROBOCOP_IMPORT = str(TEST_DATA / "ext_rule_module_import_robocop")
17+
EXT_MODULE_WITH_RELATIVE_IMPORT = str(TEST_DATA / "ext_rule_module_with_relative_import")
1718

1819

1920
@contextmanager
@@ -90,6 +91,13 @@ def test_loading_external_rule_from_dotted_module(self, robocop_pre_load):
9091
assert "9904" in robocop_pre_load.rules
9192
assert "9903" not in robocop_pre_load.rules
9293

94+
def test_loading_external_rule_including_relative_import(self, robocop_pre_load):
95+
clear_imported_module("RobocopRules")
96+
with add_sys_path(EXT_MODULE_WITH_RELATIVE_IMPORT):
97+
robocop_pre_load.config.ext_rules = {EXT_MODULE_WITH_RELATIVE_IMPORT}
98+
robocop_pre_load.load_checkers()
99+
assert "9905" in robocop_pre_load.rules
100+
93101
@pytest.mark.parametrize("config_dir", ["config_robocop", "config_pyproject"])
94102
def test_loading_external_rule_in_robocop_config(self, robocop_pre_load, config_dir):
95103
clear_imported_module("RobocopRules")

0 commit comments

Comments
 (0)