Skip to content

Commit b1bc0ea

Browse files
authored
Merge pull request #480 from eduNEXT/MJG/filter-docstring-linter
feat: [FC-0074] add linter for Open edX Filters classes definitions
2 parents 44536e1 + 46ac9a3 commit b1bc0ea

File tree

4 files changed

+162
-1
lines changed

4 files changed

+162
-1
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ Change Log
1313
Unreleased
1414
~~~~~~~~~~
1515

16+
5.6.0 - 2025-01-24
17+
~~~~~~~~~~~~~~~~~~
18+
19+
* Add docstring linter for Open edX filters.
20+
1621
5.5.0 - 2025-01-22
1722
~~~~~~~~~~~~~~~~~~
1823

edx_lint/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@
22
edx_lint standardizes lint configuration and additional plugins for use in
33
Open edX code.
44
"""
5-
__version__ = "5.5.0"
5+
__version__ = "5.6.0"
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
"""
2+
edx_lint filters_docstring module (optional plugin for filters docstrings).
3+
4+
Add this to your pylintrc::
5+
load-plugins=edx_lint.pylint.filters_docstring
6+
"""
7+
8+
from .filters_docstring_check import register_checkers
9+
10+
register = register_checkers
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
"""
2+
Pylint checker for the format of the docstrings of filters.
3+
4+
A filter's docstring should have the following structure:
5+
6+
1. Description: Any non-empty text followed by a blank line.
7+
2. Filter Type: A line that starts with "Filter Type:".
8+
3. Trigger: A line that starts with "Trigger:".
9+
"""
10+
11+
import re
12+
13+
from pylint.checkers import BaseChecker, utils
14+
15+
from edx_lint.pylint.common import BASE_ID
16+
17+
18+
def register_checkers(linter):
19+
"""
20+
Register checkers.
21+
"""
22+
linter.register_checker(FiltersDocstringFormatChecker(linter))
23+
24+
25+
class FiltersDocstringFormatChecker(BaseChecker):
26+
"""Pylint checker for the format of the docstrings of filters."""
27+
28+
name = "filters-docstring-format"
29+
30+
DOCSTRING_MISSING_PURPOSE_OR_BADLY_FORMATTED = "filter-docstring-missing-purpose"
31+
DOCSTRING_MISSING_OR_INCORRECT_TYPE = "filter-docstring-missing-or-incorrect-type"
32+
DOCSTRING_MISSING_TRIGGER_OR_BADLY_FORMATTED = "filter-docstring-missing-trigger"
33+
34+
msgs = {
35+
("E%d91" % BASE_ID): (
36+
"Filter's (%s) docstring is missing the required `Purpose` section or is badly formatted",
37+
DOCSTRING_MISSING_PURPOSE_OR_BADLY_FORMATTED,
38+
"filters docstring is missing the required `Purpose` section or is badly formatted",
39+
),
40+
("E%d93" % BASE_ID): (
41+
"Filter's (%s) docstring `Filter Type` section is missing or incorrect",
42+
DOCSTRING_MISSING_OR_INCORRECT_TYPE,
43+
"filters docstring `Filter Type` section is missing or incorrect",
44+
),
45+
("E%d94" % BASE_ID): (
46+
"Filter's (%s) docstring is missing the required `Trigger` section or is badly formatted",
47+
DOCSTRING_MISSING_TRIGGER_OR_BADLY_FORMATTED,
48+
"filters docstring is missing the required `Trigger` section or is badly formatted",
49+
),
50+
}
51+
52+
@utils.only_required_for_messages(
53+
DOCSTRING_MISSING_PURPOSE_OR_BADLY_FORMATTED,
54+
DOCSTRING_MISSING_OR_INCORRECT_TYPE,
55+
DOCSTRING_MISSING_TRIGGER_OR_BADLY_FORMATTED,
56+
)
57+
def visit_classdef(self, node):
58+
"""
59+
Visit a class definition and check its docstring.
60+
61+
If the class is a subclass of OpenEdxPublicFilter, check the format of its docstring. Skip the
62+
OpenEdxPublicFilter class itself.
63+
64+
"""
65+
if not node.is_subtype_of("openedx_filters.tooling.OpenEdxPublicFilter") or node.name == "OpenEdxPublicFilter":
66+
return
67+
68+
docstring = node.doc_node.value if node.doc_node else ""
69+
if not (error_messages := self._check_docstring_format(node, docstring)):
70+
return
71+
for error_message in error_messages:
72+
self.add_message(error_message, node=node, args=(node.name,))
73+
74+
def _check_docstring_format(self, node, docstring):
75+
"""
76+
Check the format of the docstring for errors and return a list of error messages.
77+
78+
The docstring should have the following structure:
79+
1. Description: Any non-empty text followed by a blank line.
80+
2. Filter Type: A line that starts with "Filter Type:".
81+
3. Trigger: A line that starts with "Trigger:".
82+
83+
For example:
84+
85+
```
86+
Purpose:
87+
Filter used to modify the certificate rendering process.
88+
89+
... (more description)
90+
91+
Filter Type:
92+
org.openedx.learning.certificate.render.started.v1
93+
94+
Trigger:
95+
- Repository: openedx/edx-platform
96+
- Path: lms/djangoapps/certificates/views/webview.py
97+
- Function or Method: render_html_view
98+
```
99+
"""
100+
error_messages = []
101+
if error_message := self._check_purpose_missing_or_badly_formatted(docstring):
102+
error_messages.append(error_message)
103+
if error_message := self._check_filter_type_missing_or_incorrect(node, docstring):
104+
error_messages.append(error_message)
105+
if error_message := self._check_trigger_missing_or_badly_formatted(docstring):
106+
error_messages.append(error_message)
107+
return error_messages
108+
109+
def _check_purpose_missing_or_badly_formatted(self, docstring):
110+
"""
111+
Check if the purpose is missing or badly formatted.
112+
113+
If the purpose is missing or badly formatted, return the error message. Otherwise, return.
114+
"""
115+
if not re.search(r"Purpose:\s*.*\n", docstring):
116+
return self.DOCSTRING_MISSING_PURPOSE_OR_BADLY_FORMATTED
117+
return None
118+
119+
def _check_filter_type_missing_or_incorrect(self, node, docstring):
120+
"""
121+
Check if the filter type is missing or incorrect.
122+
123+
If the filter type is missing or incorrect, return the error message. Otherwise, return.
124+
"""
125+
filter_type = node.locals.get("filter_type")
126+
if not filter_type:
127+
return self.DOCSTRING_MISSING_OR_INCORRECT_TYPE
128+
129+
filter_type = filter_type[0].statement().value.value if filter_type else ""
130+
if not re.search(r"Filter Type:\s*%s" % filter_type, docstring):
131+
return self.DOCSTRING_MISSING_OR_INCORRECT_TYPE
132+
return None
133+
134+
def _check_trigger_missing_or_badly_formatted(self, docstring):
135+
"""
136+
Check if the trigger is missing or badly formatted.
137+
138+
If the trigger is missing or badly formatted, return the error message. Otherwise, return.
139+
"""
140+
if not re.search(
141+
r"Trigger:\s*(NA|-\s*Repository:\s*[^\n]+\s*-\s*Path:\s*[^\n]+\s*-\s*Function\s*or\s*Method:\s*[^\n]+)",
142+
docstring,
143+
re.MULTILINE,
144+
):
145+
return self.DOCSTRING_MISSING_TRIGGER_OR_BADLY_FORMATTED
146+
return None

0 commit comments

Comments
 (0)