Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pylint] dedent rule #9669

Merged
merged 6 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,74 @@ 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 an 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
if (
node.doc_node.value.find(":dedent") != -1
):
self.add_message(
"do-not-hardcode-dedent",
node=node,
confidence=None,
)
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# 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
: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
: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=16,
node=function_node,
col_offset=0,
end_line=16,
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=47,
node=function_node,
col_offset=0,
end_line=47,
end_col_offset=17,
)
):
self.checker.visit_classdef(function_node)
Loading