blob: 7ec9baa26e31ba5d6e86596d26da26318f07b11f [file] [log] [blame]
# Licensed under the GPL: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
# For details: https://github.com/pylint-dev/pylint/blob/main/LICENSE
# Copyright (c) https://github.com/pylint-dev/pylint/blob/main/CONTRIBUTORS.txt
"""Script used to generate the options page."""
from __future__ import annotations
import re
from collections import defaultdict
from inspect import getmodule
from pathlib import Path
from typing import NamedTuple
import tomlkit
from sphinx.application import Sphinx
from pylint.checkers import initialize as initialize_checkers
from pylint.checkers.base_checker import BaseChecker
from pylint.extensions import initialize as initialize_extensions
from pylint.lint import PyLinter
from pylint.typing import OptionDict
from pylint.utils import get_rst_title
class OptionsData(NamedTuple):
name: str
optdict: OptionDict
checker: BaseChecker
extension: bool
OptionsDataDict = dict[str, list[OptionsData]]
PYLINT_BASE_PATH = Path(__file__).resolve().parent.parent.parent
"""Base path to the project folder."""
PYLINT_USERGUIDE_PATH = PYLINT_BASE_PATH / "doc" / "user_guide"
"""Path to the messages documentation folder."""
DYNAMICALLY_DEFINED_OPTIONS: dict[str, dict[str, str]] = {
# Option name, key / values we want to modify
"py-version": {"default": "sys.version_info[:2]"},
"spelling-dict": {
"choices": "Values from 'enchant.Broker().list_dicts()' depending on your local enchant installation",
"help": "Spelling dictionary name. Available dictionaries depends on your local enchant installation",
},
}
def _register_all_checkers_and_extensions(linter: PyLinter) -> None:
"""Registers all checkers and extensions found in the default folders."""
initialize_checkers(linter)
initialize_extensions(linter)
def _get_all_options(linter: PyLinter) -> OptionsDataDict:
"""Get all options registered to a linter and return the data."""
all_options: OptionsDataDict = defaultdict(list)
for checker in sorted(linter.get_checkers()):
for option_name, option_info in checker.options:
changes_to_do = DYNAMICALLY_DEFINED_OPTIONS.get(option_name, {})
if changes_to_do:
for key_to_change, new_value in changes_to_do.items():
print(
f"Doc value for {option_name!r}['{key_to_change}'] changed to "
f"{new_value!r} (instead of {option_info[key_to_change]!r})"
)
option_info[key_to_change] = new_value
all_options[checker.name].append(
OptionsData(
option_name,
option_info,
checker,
getmodule(checker).__name__.startswith("pylint.extensions."), # type: ignore[union-attr]
)
)
return all_options
def _create_checker_section(
checker: str, options: list[OptionsData], linter: PyLinter
) -> str:
checker_string = f".. _{checker}-options:\n\n"
checker_string += get_rst_title(f"``{checker.capitalize()}`` **Checker**", "-")
toml_doc = tomlkit.document()
tool_table = tomlkit.table(is_super_table=True)
toml_doc.add(tomlkit.key("tool"), tool_table)
pylint_tool_table = tomlkit.table(is_super_table=True)
tool_table.add(tomlkit.key("pylint"), pylint_tool_table)
checker_table = tomlkit.table()
for option in sorted(options, key=lambda x: x.name):
checker_string += get_rst_title(f"--{option.name}", '"')
checker_string += f"*{option.optdict.get('help')}*\n\n"
if option.optdict.get("default") == "":
checker_string += '**Default:** ``""``\n\n\n'
else:
checker_string += f"**Default:** ``{option.optdict.get('default')}``\n\n\n"
# Start adding the option to the toml example
if option.optdict.get("hide_from_config_file"):
continue
# Get current value of option
try:
value = DYNAMICALLY_DEFINED_OPTIONS[option.name]["default"]
except KeyError:
value = getattr(linter.config, option.name.replace("-", "_"))
# Create a comment if the option has no value
if value is None:
checker_table.add(tomlkit.comment(f"{option.name} ="))
checker_table.add(tomlkit.nl())
continue
# Display possible choices
choices = option.optdict.get("choices", "")
if choices:
checker_table.add(tomlkit.comment(f"Possible choices: {choices}"))
# Tomlkit doesn't support regular expressions
if isinstance(value, re.Pattern):
value = value.pattern
elif (
isinstance(value, (list, tuple))
and value
and isinstance(value[0], re.Pattern)
):
value = [i.pattern for i in value]
# Sorting in order for the output to be the same on all interpreters
# Don't sort everything here, alphabetical order do not make a lot of sense
# for options most of the time. Only dict based 'unstable' options need this
if isinstance(value, (list, tuple)) and option.name in ["disable"]:
value = sorted(value, key=lambda x: str(x))
# Add to table
checker_table.add(option.name, value)
checker_table.add(tomlkit.nl())
pylint_tool_table.add(options[0].checker.name.lower(), checker_table)
toml_string = "\n".join(
f" {i}" if i else "" for i in tomlkit.dumps(toml_doc).split("\n")
)
checker_string += f"""
.. raw:: html
<details>
<summary><a>Example configuration section</a></summary>
**Note:** Only ``tool.pylint`` is required, the section title is not. These are the default values.
.. code-block:: toml
{toml_string}
.. raw:: html
</details>
"""
return checker_string
def _write_options_page(options: OptionsDataDict, linter: PyLinter) -> None:
"""Create or overwrite the options page."""
sections: list[str] = [
".. This file is auto-generated. Make any changes to the associated\n"
".. docs extension in 'doc/exts/pylint_options.py'.\n\n"
".. _all-options:",
get_rst_title("Standard Checkers", "^"),
]
found_extensions = False
# We can't sort without using the 'key' keyword because if keys in 'options' were
# checkers then it wouldn't be possible to have a checker with the same name
# spanning multiple classes. It would make pylint plugin code less readable by
# forcing to use a single class / file.
for checker_name, checker_options in sorted(
options.items(), key=lambda x: x[1][0].checker
):
if not found_extensions and checker_options[0].extension:
sections.append(get_rst_title("Extensions", "^"))
found_extensions = True
sections.append(_create_checker_section(checker_name, checker_options, linter))
all_options_path = PYLINT_USERGUIDE_PATH / "configuration" / "all-options.rst"
sections_string = "\n\n".join(sections)
with open(all_options_path, "w", encoding="utf-8") as stream:
stream.write(f"\n\n{sections_string}")
# pylint: disable-next=unused-argument
def build_options_page(app: Sphinx | None) -> None:
"""Overwrite messages files by printing the documentation to a stream.
Documentation is written in ReST format.
"""
# Create linter, register all checkers and extensions and get all options
linter = PyLinter()
_register_all_checkers_and_extensions(linter)
options = _get_all_options(linter)
# Write options page
_write_options_page(options, linter)
def setup(app: Sphinx) -> dict[str, bool]:
"""Connects the extension to the Sphinx process."""
# Register callback at the builder-inited Sphinx event
# See https://www.sphinx-doc.org/en/master/extdev/appapi.html
app.connect("builder-inited", build_options_page)
return {"parallel_read_safe": True}
if __name__ == "__main__":
print("Uncomment the following line to allow running this script directly.")
# build_options_page(None)