Skip to content

Commit e2dcbb9

Browse files
Merge branch 'working-main' into exception-logging
2 parents 7e7256b + 18ea2fa commit e2dcbb9

8 files changed

Lines changed: 843 additions & 325 deletions

tools/pylint-extensions/azure-pylint-guidelines-checker/README.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,11 @@ In the case of a false positive, use the disable command to remove the pylint er
9393
| docstring-keyword-should-match-keyword-only | Docstring keyword arguments and keyword-only method arguments should match. | pylint:disable=docstring-keyword-should-match-keyword-only | [link](https://azure.github.io/azure-sdk/python_documentation.html#docstrings) |
9494
| docstring-type-do-not-use-class | Docstring type is formatted incorrectly. Do not use `:class` in docstring type. | pylint:disable=docstring-type-do-not-use-class | [link](https://sphinx-rtd-tutorial.readthedocs.io/en/latest/docstrings.html) |
9595
| no-typing-import-in-type-check | Do not import typing under TYPE_CHECKING. | pylint:disable=no-typing-import-in-type-check | No Link. |
96+
| invalid-use-of-overload | Do not mix async and synchronous overloads | pylint:disable=invalid-use-of-overload | No Link. |
9697
| do-not-log-raised-errors | Do not log errors at `error` or `warning` level when error is raised in an exception block. | pylint:disable=do-not-log-raised-errors | No Link. |
97-
| do-not-use-legacy-typing | Do not use legacy (<Python 3.8) type hinting comments | pylint:disable=do-not-use-legacy-typing | No Link.
98+
| do-not-use-legacy-typing | Do not use legacy (<Python 3.8) type hinting comments | pylint:disable=do-not-use-legacy-typing | No Link. |
9899
| do-not-import-asyncio | Do not import asyncio directly. | pylint:disable=do-not-import-asyncio | No Link. |
99-
| 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) |
100+
| 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) | | | |
101+
| 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) |
102+
| do-not-hardcode-connection-verify | Do not hardcode a boolean value to connection_verify | pylint:disable=do-not-hardcode-connection-verify | No LInk. |
103+
| TODO | Address Commented out Pylint Custom Plugin Checkers #3228 | | |

tools/pylint-extensions/azure-pylint-guidelines-checker/pylint_guidelines_checker.py

Lines changed: 221 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -161,79 +161,93 @@ class ClientHasApprovedMethodNamePrefix(BaseChecker):
161161
" https://azure.github.io/azure-sdk/python_design.html#service-operations",
162162
"unapproved-client-method-name-prefix",
163163
"All clients should use the preferred verbs for method names.",
164-
)
165-
}
166-
options = (
167-
(
168-
"ignore-unapproved-client-method-name-prefix",
169-
{
170-
"default": False,
171-
"type": "yn",
172-
"metavar": "<y_or_n>",
173-
"help": "Allow clients to not use preferred method name prefixes",
174-
},
175164
),
176-
)
165+
}
177166

178-
ignore_clients = [
167+
ignore_clients = [
179168
"PipelineClient",
180169
"AsyncPipelineClient",
181170
"ARMPipelineClient",
182171
"AsyncARMPipelineClient",
183172
]
184173

174+
approved_prefixes = [
175+
"get",
176+
"list",
177+
"create",
178+
"upsert",
179+
"set",
180+
"update",
181+
"replace",
182+
"append",
183+
"add",
184+
"delete",
185+
"remove",
186+
"begin",
187+
"upload",
188+
"download", # standard verbs
189+
"close", # very common verb
190+
"cancel",
191+
"clear",
192+
"subscribe",
193+
"send",
194+
"query", # common verbs
195+
"analyze",
196+
"train",
197+
"detect", # future proofing
198+
"from", # special case
199+
]
200+
201+
ignored_decorators = [
202+
"property",
203+
]
204+
185205
def __init__(self, linter=None):
186206
super(ClientHasApprovedMethodNamePrefix, self).__init__(linter)
207+
self.process_class = None
208+
self.namespace = None
209+
210+
def _check_decorators(self, node):
211+
if not node.decorators:
212+
return False
213+
for decorator in node.decorators.nodes:
214+
if isinstance(decorator, astroid.nodes.Name) and decorator.name in self.ignored_decorators:
215+
return True
216+
return False
217+
218+
def visit_module(self, node):
219+
self.namespace = node.name
187220

188221
def visit_classdef(self, node):
189-
"""Visits every class in file and checks if it is a client. If it is a client, checks
190-
that approved method name prefixes are present.
222+
if all((
223+
node.name.endswith("Client"),
224+
node.name not in self.ignore_clients,
225+
not node.name.startswith("_"),
226+
not '._' in self.namespace,
227+
)):
228+
self.process_class = node
191229

192-
:param node: class node
193-
:type node: ast.ClassDef
194-
:return: None
195-
"""
196-
try:
197-
if node.name.endswith("Client") and node.name not in self.ignore_clients:
198-
client_methods = [
199-
child for child in node.get_children() if child.is_function
200-
]
201-
202-
approved_prefixes = [
203-
"get",
204-
"list",
205-
"create",
206-
"upsert",
207-
"set",
208-
"update",
209-
"replace",
210-
"append",
211-
"add",
212-
"delete",
213-
"remove",
214-
"begin",
215-
]
216-
for idx, method in enumerate(client_methods):
217-
if (
218-
method.name.startswith("__")
219-
or "_exists" in method.name
220-
or method.name.startswith("_")
221-
or method.name.startswith("from")
222-
):
223-
continue
224-
prefix = method.name.split("_")[0]
225-
if prefix.lower() not in approved_prefixes:
226-
self.add_message(
227-
msgid="unapproved-client-method-name-prefix",
228-
node=client_methods[idx],
229-
confidence=None,
230-
)
231-
except AttributeError:
232-
logger.debug(
233-
"Pylint custom checker failed to check if client has approved method name prefix."
230+
def visit_functiondef(self, node):
231+
if any((
232+
self.process_class is None, # not in a client class
233+
node.name.startswith("_"), # private method
234+
node.name.endswith("_exists"), # special case
235+
self._check_decorators(node), # property
236+
node.parent != self.process_class, # nested method
237+
)):
238+
return
239+
240+
# check for approved prefix
241+
parts = node.name.split("_")
242+
if parts[0].lower() not in self.approved_prefixes:
243+
self.add_message(
244+
msgid="unapproved-client-method-name-prefix",
245+
node=node,
246+
confidence=None,
234247
)
235-
pass
236248

249+
def leave_classdef(self, node):
250+
self.process_class = None
237251

238252
class ClientMethodsUseKwargsWithMultipleParameters(BaseChecker):
239253
name = "client-method-multiple-parameters"
@@ -2805,6 +2819,75 @@ def visit_import(self, node):
28052819
pass
28062820

28072821

2822+
class InvalidUseOfOverload(BaseChecker):
2823+
"""Rule to check that use of the @overload decorator matches the async/sync nature of the underlying function"""
2824+
2825+
name = "invalid-use-of-overload"
2826+
priority = -1
2827+
msgs = {
2828+
"C4765": (
2829+
"Do not mix async and synchronous overloads",
2830+
"invalid-use-of-overload",
2831+
"Functions and their overloads must be either all async or all synchronous.",
2832+
),
2833+
}
2834+
2835+
def visit_classdef(self, node):
2836+
"""Check that use of the @overload decorator matches the async/sync nature of the underlying function"""
2837+
2838+
# Obtain a list of all functions and function names
2839+
functions = []
2840+
try:
2841+
node.body
2842+
for item in node.body:
2843+
if hasattr(item, 'name'):
2844+
functions.append(item)
2845+
2846+
# Dictionary of lists of all functions by name
2847+
overloadedfunctions = {}
2848+
for item in functions:
2849+
if item.name in overloadedfunctions:
2850+
overloadedfunctions[item.name].append(item)
2851+
else:
2852+
overloadedfunctions[item.name] = [item]
2853+
2854+
2855+
# Loop through the overloaded functions and check they are the same type
2856+
for funct in overloadedfunctions.values():
2857+
if len(funct) > 1: # only need to check if there is more than 1 function with the same name
2858+
function_is_async = None
2859+
2860+
for item in funct:
2861+
if function_is_async is None:
2862+
function_is_async = self.is_function_async(item)
2863+
2864+
else:
2865+
if function_is_async != self.is_function_async(item):
2866+
self.add_message(
2867+
msgid=f"invalid-use-of-overload",
2868+
node=item,
2869+
confidence=None,
2870+
)
2871+
except:
2872+
pass
2873+
2874+
2875+
def is_function_async(self, node):
2876+
try:
2877+
str(node.__class__).index("Async")
2878+
return True
2879+
except:
2880+
if node.returns is None:
2881+
return False
2882+
try:
2883+
if node.returns.value.name == "Awaitable":
2884+
return True
2885+
else:
2886+
return False
2887+
except:
2888+
return False
2889+
2890+
28082891
class DoNotUseLegacyTyping(BaseChecker):
28092892

28102893
""" Rule to check that we aren't using legacy typing using comments. """
@@ -2835,7 +2918,6 @@ class DoNotImportAsyncio(BaseChecker):
28352918

28362919
name = "do-not-import-asyncio"
28372920
priority = -1
2838-
# TODO Find message number
28392921
msgs = {
28402922
"C4763": (
28412923
"Do not import the asyncio package directly in your library",
@@ -2864,8 +2946,6 @@ def visit_import(self, node):
28642946
)
28652947

28662948

2867-
# [Pylint] custom linter check for invalid use of @overload #3229
2868-
28692949

28702950
class DoNotLogExceptions(BaseChecker):
28712951

@@ -2940,8 +3020,83 @@ def check_for_logging(self, node, exception_name):
29403020

29413021

29423022
# [Pylint] Address Commented out Pylint Custom Plugin Checkers #3228
2943-
# [Pylint] Add a check for connection_verify hardcoded settings #35355
2944-
# [Pylint] Investigate pylint rule around missing dependency #3231
3023+
3024+
3025+
class DoNotHardcodeConnectionVerify(BaseChecker):
3026+
3027+
"""Rule to check that developers do not hardcode a boolean to connection_verify."""
3028+
3029+
name = "do-not-hardcode-connection-verify"
3030+
priority = -1
3031+
msgs = {
3032+
"C4767": (
3033+
"Do not hardcode a boolean value to connection_verify",
3034+
"do-not-hardcode-connection-verify",
3035+
"Do not hardcode a boolean value to connection_verify. It's up to customers who use the code to be able to set it",
3036+
),
3037+
}
3038+
3039+
3040+
def visit_call(self, node):
3041+
"""Visit function calls to ensure it isn't used as a parameter"""
3042+
try:
3043+
for keyword in node.keywords:
3044+
if keyword.arg == "connection_verify":
3045+
if type(keyword.value.value) == bool:
3046+
self.add_message(
3047+
msgid=f"do-not-hardcode-connection-verify",
3048+
node=keyword,
3049+
confidence=None,
3050+
)
3051+
except:
3052+
pass
3053+
3054+
3055+
def visit_assign(self, node):
3056+
"""Visiting variable Assignments"""
3057+
try: # Picks up self.connection_verify = True
3058+
if node.targets[0].attrname == "connection_verify":
3059+
if type(node.value.value) == bool:
3060+
self.add_message(
3061+
msgid=f"do-not-hardcode-connection-verify",
3062+
node=node,
3063+
confidence=None,
3064+
)
3065+
except:
3066+
try: # connection_verify = True
3067+
if node.targets[0].name == "connection_verify":
3068+
if type(node.value.value) == bool:
3069+
self.add_message(
3070+
msgid=f"do-not-hardcode-connection-verify",
3071+
node=node,
3072+
confidence=None,
3073+
)
3074+
except:
3075+
pass
3076+
3077+
3078+
def visit_annassign(self, node):
3079+
"""Visiting variable annotated assignments"""
3080+
try: # self.connection_verify: bool = True
3081+
if node.target.attrname == "connection_verify":
3082+
if type(node.value.value) == bool:
3083+
self.add_message(
3084+
msgid=f"do-not-hardcode-connection-verify",
3085+
node=node,
3086+
confidence=None,
3087+
)
3088+
except: # Picks up connection_verify: bool = True
3089+
try:
3090+
if node.target.name == "connection_verify":
3091+
if type(node.value.value) == bool:
3092+
self.add_message(
3093+
msgid=f"do-not-hardcode-connection-verify",
3094+
node=node,
3095+
confidence=None,
3096+
)
3097+
except:
3098+
pass
3099+
29453100

29463101

29473102
# if a linter is registered in this function then it will be checked with pylint
@@ -2977,22 +3132,20 @@ def register(linter):
29773132
linter.register_checker(DoNotImportAsyncio(linter))
29783133
linter.register_checker(NoLegacyAzureCoreHttpResponseImport(linter))
29793134
linter.register_checker(NoImportTypingFromTypeCheck(linter))
3135+
linter.register_checker(InvalidUseOfOverload(linter))
29803136
linter.register_checker(DoNotUseLegacyTyping(linter))
29813137
linter.register_checker(DoNotLogErrorsEndUpRaising(linter))
29823138
linter.register_checker(DoNotLogExceptions(linter))
2983-
2984-
# [Pylint] custom linter check for invalid use of @overload #3229
29853139
# [Pylint] Address Commented out Pylint Custom Plugin Checkers #3228
2986-
# [Pylint] Add a check for connection_verify hardcoded settings #35355
2987-
# [Pylint] Investigate pylint rule around missing dependency #3231
3140+
linter.register_checker(DoNotHardcodeConnectionVerify(linter))
29883141

29893142
# disabled by default, use pylint --enable=check-docstrings if you want to use it
29903143
linter.register_checker(CheckDocstringParameters(linter))
29913144

29923145

29933146
# Rules are disabled until false positive rate improved
29943147
# linter.register_checker(CheckForPolicyUse(linter))
2995-
# linter.register_checker(ClientHasApprovedMethodNamePrefix(linter))
3148+
linter.register_checker(ClientHasApprovedMethodNamePrefix(linter))
29963149

29973150
# linter.register_checker(ClientDocstringUsesLiteralIncludeForCodeExample(linter))
29983151
# linter.register_checker(ClientLROMethodsUseCorePolling(linter))

0 commit comments

Comments
 (0)