Skip to content

Commit 4c2eb2d

Browse files
Create separate documentation page for each message (#5396)
Co-authored-by: Pierre Sassoulas <pierre.sassoulas@gmail.com>
1 parent 0a2bf37 commit 4c2eb2d

File tree

13 files changed

+764
-9
lines changed

13 files changed

+764
-9
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
/pylint.egg-info/
1010
.tox
1111
*.sw[a-z]
12+
doc/messages/
1213
doc/technical_reference/extensions.rst
1314
doc/technical_reference/features.rst
1415
pyve

doc/Makefile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ features.rst: $(shell find ../pylint/checkers -type f -regex '.*\.py')
150150
rm -f features.rst
151151
PYTHONPATH=$(PYTHONPATH) $(PYTHON) ./exts/pylint_features.py
152152

153+
messages: $(PYTHONPATH) $(PYTHON) ./exts/pylint_messages.py
154+
153155
gen-examples:
154156
chmod u+w ../examples/pylintrc
155157
pylint --rcfile=/dev/null --generate-rcfile > ../examples/pylintrc

doc/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
extensions = [
3737
"pylint_features",
3838
"pylint_extensions",
39+
"pylint_messages",
3940
"sphinx.ext.autosectionlabel",
4041
"sphinx.ext.intersphinx",
4142
]

doc/exts/pylint_messages.py

Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
2+
# For details: https://github.com/PyCQA/pylint/blob/main/LICENSE
3+
4+
"""Script used to generate the messages files."""
5+
6+
import os
7+
from collections import defaultdict
8+
from pathlib import Path
9+
from typing import DefaultDict, Dict, List, NamedTuple, Optional, Tuple
10+
11+
from sphinx.application import Sphinx
12+
13+
from pylint.checkers import initialize as initialize_checkers
14+
from pylint.constants import MSG_TYPES
15+
from pylint.extensions import initialize as initialize_extensions
16+
from pylint.lint import PyLinter
17+
from pylint.message import MessageDefinition
18+
from pylint.utils import get_rst_title
19+
20+
PYLINT_BASE_PATH = Path(__file__).resolve().parent.parent.parent
21+
"""Base path to the project folder"""
22+
23+
PYLINT_MESSAGES_PATH = PYLINT_BASE_PATH / "doc" / "messages"
24+
"""Path to the messages documentation folder"""
25+
26+
27+
MSG_TYPES_DOC = {k: v if v != "info" else "information" for k, v in MSG_TYPES.items()}
28+
29+
30+
class MessageData(NamedTuple):
31+
checker: str
32+
id: str
33+
name: str
34+
definition: MessageDefinition
35+
36+
37+
MessagesDict = Dict[str, List[MessageData]]
38+
OldMessagesDict = Dict[str, DefaultDict[Tuple[str, str], List[Tuple[str, str]]]]
39+
"""DefaultDict is indexed by tuples of (old name symbol, old name id) and values are
40+
tuples of (new name symbol, new name category)
41+
"""
42+
43+
44+
def _register_all_checkers_and_extensions(linter: PyLinter) -> None:
45+
"""Registers all checkers and extensions found in the default folders."""
46+
initialize_checkers(linter)
47+
initialize_extensions(linter)
48+
49+
50+
def _get_all_messages(
51+
linter: PyLinter,
52+
) -> Tuple[MessagesDict, OldMessagesDict]:
53+
"""Get all messages registered to a linter and return a dictionary indexed by message
54+
type.
55+
Also return a dictionary of old message and the new messages they can be mapped to.
56+
"""
57+
messages_dict: MessagesDict = {
58+
"fatal": [],
59+
"error": [],
60+
"warning": [],
61+
"convention": [],
62+
"refactor": [],
63+
"information": [],
64+
}
65+
old_messages: OldMessagesDict = {
66+
"fatal": defaultdict(list),
67+
"error": defaultdict(list),
68+
"warning": defaultdict(list),
69+
"convention": defaultdict(list),
70+
"refactor": defaultdict(list),
71+
"information": defaultdict(list),
72+
}
73+
for message in linter.msgs_store.messages:
74+
message_data = MessageData(
75+
message.checker_name, message.msgid, message.symbol, message
76+
)
77+
messages_dict[MSG_TYPES_DOC[message.msgid[0]]].append(message_data)
78+
79+
if message.old_names:
80+
for old_name in message.old_names:
81+
category = MSG_TYPES_DOC[old_name[0][0]]
82+
old_messages[category][(old_name[1], old_name[0])].append(
83+
(message.symbol, MSG_TYPES_DOC[message.msgid[0]])
84+
)
85+
86+
return messages_dict, old_messages
87+
88+
89+
def _write_message_page(messages_dict: MessagesDict) -> None:
90+
"""Create or overwrite the file for each message."""
91+
for category, messages in messages_dict.items():
92+
category_dir = PYLINT_MESSAGES_PATH / category
93+
if not category_dir.exists():
94+
category_dir.mkdir(parents=True, exist_ok=True)
95+
for message in messages:
96+
messages_file = os.path.join(category_dir, f"{message.name}.rst")
97+
with open(messages_file, "w", encoding="utf-8") as stream:
98+
stream.write(
99+
f""".. _{message.name}:
100+
101+
{get_rst_title(f"{message.name} / {message.id}", "=")}
102+
**Message emitted:**
103+
104+
{message.definition.msg}
105+
106+
**Description:**
107+
108+
*{message.definition.description}*
109+
110+
Created by ``{message.checker}`` checker
111+
"""
112+
)
113+
114+
115+
def _write_messages_list_page(
116+
messages_dict: MessagesDict, old_messages_dict: OldMessagesDict
117+
) -> None:
118+
"""Create or overwrite the page with the list of all messages."""
119+
messages_file = os.path.join(PYLINT_MESSAGES_PATH, "messages_list.rst")
120+
with open(messages_file, "w", encoding="utf-8") as stream:
121+
# Write header of file
122+
stream.write(
123+
f""".. _messages-list:
124+
125+
{get_rst_title("Pylint Messages", "=")}
126+
Pylint can emit the following messages:
127+
128+
"""
129+
)
130+
131+
# Iterate over tuple to keep same order
132+
for category in (
133+
"fatal",
134+
"error",
135+
"warning",
136+
"convention",
137+
"refactor",
138+
"information",
139+
):
140+
messages = sorted(messages_dict[category], key=lambda item: item.name)
141+
old_messages = sorted(old_messages_dict[category], key=lambda item: item[0])
142+
messages_string = "".join(
143+
f" {category}/{message.name}.rst\n" for message in messages
144+
)
145+
old_messages_string = "".join(
146+
f" {category}/{old_message[0]}.rst\n" for old_message in old_messages
147+
)
148+
149+
# Write list per category
150+
stream.write(
151+
f"""{get_rst_title(category.capitalize(), "-")}
152+
All messages in the {category} category:
153+
154+
.. toctree::
155+
:maxdepth: 2
156+
:titlesonly:
157+
158+
{messages_string}
159+
All renamed messages in the {category} category:
160+
161+
.. toctree::
162+
:maxdepth: 1
163+
:titlesonly:
164+
165+
{old_messages_string}
166+
167+
"""
168+
)
169+
170+
171+
def _write_redirect_pages(old_messages: OldMessagesDict) -> None:
172+
"""Create redirect pages for old-messages."""
173+
for category, old_names in old_messages.items():
174+
category_dir = PYLINT_MESSAGES_PATH / category
175+
if not os.path.exists(category_dir):
176+
os.makedirs(category_dir)
177+
for old_name, new_names in old_names.items():
178+
old_name_file = os.path.join(category_dir, f"{old_name[0]}.rst")
179+
with open(old_name_file, "w", encoding="utf-8") as stream:
180+
new_names_string = "".join(
181+
f" ../{new_name[1]}/{new_name[0]}.rst\n" for new_name in new_names
182+
)
183+
stream.write(
184+
f""".. _{old_name[0]}:
185+
186+
{get_rst_title(" / ".join(old_name), "=")}
187+
"{old_name[0]} has been renamed. The new message can be found at:
188+
189+
.. toctree::
190+
:maxdepth: 2
191+
:titlesonly:
192+
193+
{new_names_string}
194+
"""
195+
)
196+
197+
198+
# pylint: disable-next=unused-argument
199+
def build_messages_pages(app: Optional[Sphinx]) -> None:
200+
"""Overwrite messages files by printing the documentation to a stream.
201+
Documentation is written in ReST format.
202+
"""
203+
# Create linter, register all checkers and extensions and get all messages
204+
linter = PyLinter()
205+
_register_all_checkers_and_extensions(linter)
206+
messages, old_messages = _get_all_messages(linter)
207+
208+
# Write message and category pages
209+
_write_message_page(messages)
210+
_write_messages_list_page(messages, old_messages)
211+
212+
# Write redirect pages
213+
_write_redirect_pages(old_messages)
214+
215+
216+
def setup(app: Sphinx) -> None:
217+
"""Connects the extension to the Sphinx process"""
218+
# Register callback at the builder-inited Sphinx event
219+
# See https://www.sphinx-doc.org/en/master/extdev/appapi.html
220+
app.connect("builder-inited", build_messages_pages)
221+
222+
223+
if __name__ == "__main__":
224+
pass
225+
# Uncomment to allow running this script by your local python interpreter
226+
# build_messages_pages(None)

doc/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ refactored and can offer you details about the code's complexity.
1717

1818
user_guide/index.rst
1919
how_tos/index.rst
20+
messages/index.rst
2021
technical_reference/index.rst
2122
development_guide/index.rst
2223
additional_commands/index.rst

doc/messages/index.rst

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
.. _messages:
2+
3+
Messages
4+
===================
5+
6+
.. toctree::
7+
:maxdepth: 1
8+
:titlesonly:
9+
10+
messages_introduction
11+
messages_list
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
.. _messages-introduction:
2+
3+
Pylint messages
4+
================
5+
6+
Pylint can emit various messages. These are categorized according to categories::
7+
8+
Convention
9+
Error
10+
Fatal
11+
Information
12+
Refactor
13+
Warning
14+
15+
A list of these messages can be found here: :ref:`messages-list`

0 commit comments

Comments
 (0)