Skip to content

Commit

Permalink
[pylint] dedent rule (#9669)
Browse files Browse the repository at this point in the history
* dedent rule

* update

* readme

* copilot

* update

* register
  • Loading branch information
l0lawrence authored Jan 22, 2025
1 parent 1a44261 commit 5144d2e
Show file tree
Hide file tree
Showing 4 changed files with 193 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,5 @@ In the case of a false positive, use the disable command to remove the pylint er
| invalid-use-of-overload | Do not mix async and synchronous overloads | pylint:disable=invalid-use-of-overload | No Link. |
| do-not-hardcode-connection-verify | Do not hardcode a boolean value to connection_verify | pylint:disable=do-not-hardcode-connection-verify | No LInk. |
| do-not-log-exceptions | Do not log exceptions in levels other than debug, otherwise it can reveal sensitive information | pylint:disable=do-not-log-exceptions | [link](https://azure.github.io/azure-sdk/python_implementation.html#python-logging-sensitive-info) |
| unapproved-client-method-name-prefix | Clients should use preferred verbs for method names | pylint:disable=unapproved-client-method-name-prefix | [link](https://azure.github.io/azure-sdk/python_design.html#naming) |
| unapproved-client-method-name-prefix | Clients should use preferred verbs for method names | pylint:disable=unapproved-client-method-name-prefix | [link](https://azure.github.io/azure-sdk/python_design.html#naming) |
| do-not-hardcode-dedent | Sphinx will automatically dedent examples. | pylint:disable=do-not-hardcode-dedent | No Link. |
Original file line number Diff line number Diff line change
Expand Up @@ -3079,6 +3079,80 @@ def visit_annassign(self, node):
except:
pass

class DoNotDedentDocstring(BaseChecker):

"""Rule to check that developers do not hardcode `dedent` in their docstring. Sphinx will handle this automatically."""

name = "do-not-hardcode-dedent"
priority = -1
msgs = {
"C4768": (
"Do not hardcode dedent value in docstring",
"do-not-hardcode-dedent",
"Do not hardcode dedent value in docstring. It's up to sphinx to handle this automatically",
),
}

def __init__(self, linter=None):
super(DoNotDedentDocstring, self).__init__(linter)

def check_for_dedent(self, node):
"""Parse the docstring for a dedent.
If found, checks that the dedent does not have a value set.
:param node: ast.ClassDef or ast.FunctionDef
:return: None
"""

try:
# not every class/method will have a docstring so don't crash here, just return
# don't fail if there is no dedent in the docstring, be lenient
if (
node.doc_node.value.find(":dedent") != -1
):
dedent_value = node.doc_node.value.split(":dedent:")[1].split("\n")[0].strip()
try:
int(dedent_value)
self.add_message(
"do-not-hardcode-dedent",
node=node,
confidence=None,
)
except:
pass
except Exception:
return

def visit_classdef(self, node):
"""Visits every class docstring.
:param node: ast.ClassDef
:return: None
"""
try:
for func in node.body:
if isinstance(func, astroid.FunctionDef) and func.name == "__init__":
self.check_for_dedent(node)
except Exception:
logger.debug("Pylint custom checker failed to check docstrings.")
pass

def visit_functiondef(self, node):
"""Visits every method docstring.
:param node: ast.FunctionDef
:return: None
"""
try:
if node.name == "__init__":
return
self.check_for_dedent(node)
except Exception:
logger.debug("Pylint custom checker failed to check docstrings.")
pass

# this line makes it work for async functions
visit_asyncfunctiondef = visit_functiondef

# if a linter is registered in this function then it will be checked with pylint
def register(linter):
Expand Down Expand Up @@ -3117,8 +3191,8 @@ def register(linter):
linter.register_checker(DoNotLogErrorsEndUpRaising(linter))
linter.register_checker(InvalidUseOfOverload(linter))
linter.register_checker(DoNotLogExceptions(linter))

linter.register_checker(DoNotHardcodeConnectionVerify(linter))
linter.register_checker(DoNotDedentDocstring(linter))

# disabled by default, use pylint --enable=check-docstrings if you want to use it
linter.register_checker(CheckDocstringParameters(linter))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# test_ignores_correct_dedent_in_function
def function_foo(x, y, z):
"""docstring
.. admonition:: Example:
.. literalinclude:: ../samples/sample_authentication.py
:start-after: [START auth_from_connection_string]
:end-before: [END auth_from_connection_string]
:language: python
:dedent:
:caption: Authenticate with a connection string
"""
pass


# test_failure_dedent_in_function
def function_foo1(x, y, z):
"""docstring
.. admonition:: Example:
This is Example content.
Should support multi-line.
Can also include file:
.. literalinclude:: ../samples/sample_authentication.py
:start-after: [START auth_from_connection_string]
:end-before: [END auth_from_connection_string]
:language: python
:dedent: 8
"""


# test_ignores_correct_dedent_in_class
class SomeClient(object):
"""docstring
.. admonition:: Example:
.. literalinclude:: ../samples/sample_authentication.py
:start-after: [START auth_from_connection_string]
:end-before: [END auth_from_connection_string]
:language: python
:dedent:
:caption: Authenticate with a connection string
"""

def __init__(self):
pass


# test_failure_dedent_in_class
class Some1Client(): # @
"""docstring
.. admonition:: Example:
This is Example content.
Should support multi-line.
Can also include file:
.. literalinclude:: ../samples/sample_authentication.py
:start-after: [START auth_from_connection_string]
:end-before: [END auth_from_connection_string]
:language: python
:dedent: 8
"""

def __init__(self):
pass
Original file line number Diff line number Diff line change
Expand Up @@ -3901,3 +3901,55 @@ def test_invalid_connection_verify(self):
self.checker.visit_annassign(annotated_assignment)
self.checker.visit_annassign(annotated_self_assignment)


class TestDedent(pylint.testutils.CheckerTestCase):
"""Test that we are checking the dedent is not set in the docstring"""

CHECKER_CLASS = checker.DoNotDedentDocstring

@pytest.fixture(scope="class")
def setup(self):
file = open(
os.path.join(TEST_FOLDER, "test_files", "dedent_failure.py")
)
node = astroid.parse(file.read())
file.close()
return node

def test_ignores_correct_dedent_in_function(self, setup):
function_node = setup.body[0]
with self.assertNoMessages():
self.checker.visit_functiondef(function_node)

def test_bad_dedent_in_function(self, setup):
function_node = setup.body[1]
with self.assertAddsMessages(
pylint.testutils.MessageTest(
msg_id="do-not-hardcode-dedent",
line=17,
node=function_node,
col_offset=0,
end_line=17,
end_col_offset=17,
)
):
self.checker.visit_functiondef(function_node)

def test_ignores_correct_dedent_in_class(self, setup):
function_node = setup.body[2]
with self.assertNoMessages():
self.checker.visit_classdef(function_node)

def test_bad_dedent_in_class(self, setup):
function_node = setup.body[3]
with self.assertAddsMessages(
pylint.testutils.MessageTest(
msg_id="do-not-hardcode-dedent",
line=49,
node=function_node,
col_offset=0,
end_line=49,
end_col_offset=17,
)
):
self.checker.visit_classdef(function_node)

0 comments on commit 5144d2e

Please sign in to comment.