Skip to content

Commit 24e9d35

Browse files
committed
Start adding a protocol checker for the async features added in PEP 492:
* this patch adds the basis of a new checker, 'async', which deals with problems that can occur when working with async features added in Python with PEP 492. * We're also adding a new error, 'yield-inside-async-function', emitted on Python 3.5 and upwards when the `yield` statement is found inside a new coroutine function (PEP 492). * Another new error is added, 'not-async-context-manager', emitted when an async context manager block is used with an object which doesn't support this protocol (PEP 492).
1 parent 615a182 commit 24e9d35

8 files changed

+188
-0
lines changed

ChangeLog

+10
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,16 @@ ChangeLog for Pylint
309309
This fixes a false positive related to abstract-class-instantiated.
310310
Closes issue #648.
311311

312+
* Add a new checker for the async features added by PEP 492.
313+
314+
* Add a new error, 'yield-inside-async-function', emitted on
315+
Python 3.5 and upwards when the `yield` statement is found inside
316+
a new coroutine function (PEP 492).
317+
318+
* Add a new error, 'not-async-context-manager', emitted when
319+
an async context manager block is used with an object which doesn't
320+
support this protocol (PEP 492).
321+
312322

313323

314324
2015-03-14 -- 1.4.3

pylint/checkers/async.py

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
# Copyright (c) 2003-2015 LOGILAB S.A. (Paris, FRANCE).
2+
# http://www.logilab.fr/ -- mailto:[email protected]
3+
# Copyright (c) 2009-2010 Arista Networks, Inc.
4+
#
5+
# This program is free software; you can redistribute it and/or modify it under
6+
# the terms of the GNU General Public License as published by the Free Software
7+
# Foundation; either version 2 of the License, or (at your option) any later
8+
# version.
9+
#
10+
# This program is distributed in the hope that it will be useful, but WITHOUT
11+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12+
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU General Public License along with
15+
# this program; if not, write to the Free Software Foundation, Inc.,
16+
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17+
"""Checker for anything related to the async protocol (PEP 492)."""
18+
19+
import astroid
20+
from astroid import exceptions
21+
from astroid import helpers
22+
23+
from pylint import checkers
24+
from pylint.checkers import utils as checker_utils
25+
from pylint import interfaces
26+
from pylint import utils
27+
28+
29+
class AsyncChecker(checkers.BaseChecker):
30+
__implements__ = interfaces.IAstroidChecker
31+
name = 'async'
32+
msgs = {
33+
'E1700': ('Yield inside async function',
34+
'yield-inside-async-function',
35+
'Used when an `yield` or `yield from` statement is '
36+
'found inside an async function.',
37+
{'minversion': (3, 5)}),
38+
'E1701': ("Async context manager '%s' doesn't implement __aenter__ and __aexit__.",
39+
'not-async-context-manager',
40+
'Used when an async context manager is used with an object '
41+
'that does not implement the async context management protocol.',
42+
{'minversion': (3, 5)}),
43+
}
44+
45+
def open(self):
46+
self._ignore_mixin_members = utils.get_global_option(self, 'ignore-mixin-members')
47+
48+
@checker_utils.check_messages('yield-inside-async-function')
49+
def visit_asyncfunctiondef(self, node):
50+
for child in node.nodes_of_class(astroid.Yield):
51+
if child.scope() is node:
52+
self.add_message('yield-inside-async-function', node=child)
53+
54+
@checker_utils.check_messages('not-async-context-manager')
55+
def visit_asyncwith(self, node):
56+
for ctx_mgr, _ in node.items:
57+
infered = helpers.safe_infer(ctx_mgr)
58+
if infered is None or infered is astroid.YES:
59+
continue
60+
61+
if isinstance(infered, astroid.Instance):
62+
try:
63+
infered.getattr('__aenter__')
64+
infered.getattr('__aexit__')
65+
except exceptions.NotFoundError:
66+
if isinstance(infered, astroid.Instance):
67+
# If we do not know the bases of this class,
68+
# just skip it.
69+
if not helpers.has_known_bases(infered):
70+
continue
71+
# Just ignore mixin classes.
72+
if self._ignore_mixin_members:
73+
if infered.name[-5:].lower() == 'mixin':
74+
continue
75+
else:
76+
continue
77+
78+
self.add_message('not-async-context-manager',
79+
node=node, args=(infered.name, ))
80+
81+
82+
def register(linter):
83+
"""required method to auto register this checker"""
84+
linter.register_checker(AsyncChecker(linter))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
"""Test that an async context manager receives a proper object."""
2+
# pylint: disable=missing-docstring, import-error, too-few-public-methods
3+
import contextlib
4+
5+
from ala import Portocala
6+
7+
8+
@contextlib.contextmanager
9+
def ctx_manager():
10+
yield
11+
12+
13+
class ContextManager(object):
14+
def __enter__(self):
15+
pass
16+
def __exit__(self, *args):
17+
pass
18+
19+
class PartialAsyncContextManager(object):
20+
def __aenter__(self):
21+
pass
22+
23+
class SecondPartialAsyncContextManager(object):
24+
def __aexit__(self, *args):
25+
pass
26+
27+
class UnknownBases(Portocala):
28+
def __aenter__(self):
29+
pass
30+
31+
32+
class AsyncManagerMixin(object):
33+
pass
34+
35+
class GoodAsyncManager(object):
36+
def __aenter__(self):
37+
pass
38+
def __aexit__(self, *args):
39+
pass
40+
41+
class InheritExit(object):
42+
def __aexit__(self, *args):
43+
pass
44+
45+
class SecondGoodAsyncManager(InheritExit):
46+
def __aenter__(self):
47+
pass
48+
49+
50+
async def bad_coro():
51+
async with 42: # [not-async-context-manager]
52+
pass
53+
async with ctx_manager(): # [not-async-context-manager]
54+
pass
55+
async with ContextManager(): # [not-async-context-manager]
56+
pass
57+
async with PartialAsyncContextManager(): # [not-async-context-manager]
58+
pass
59+
async with SecondPartialAsyncContextManager(): # [not-async-context-manager]
60+
pass
61+
62+
63+
async def good_coro():
64+
async with UnknownBases():
65+
pass
66+
async with AsyncManagerMixin():
67+
pass
68+
async with GoodAsyncManager():
69+
pass
70+
async with SecondGoodAsyncManager():
71+
pass
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
min_pyver=3.5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
not-async-context-manager:51:bad_coro:Async context manager 'int' doesn't implement __aenter__ and __aexit__.
2+
not-async-context-manager:53:bad_coro:Async context manager 'generator' doesn't implement __aenter__ and __aexit__.
3+
not-async-context-manager:55:bad_coro:Async context manager 'ContextManager' doesn't implement __aenter__ and __aexit__.
4+
not-async-context-manager:57:bad_coro:Async context manager 'PartialAsyncContextManager' doesn't implement __aenter__ and __aexit__.
5+
not-async-context-manager:59:bad_coro:Async context manager 'SecondPartialAsyncContextManager' doesn't implement __aenter__ and __aexit__.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Test that `yield` or `yield from` can't be used inside an async function."""
2+
# pylint: disable=missing-docstring
3+
4+
async def good_coro():
5+
def _inner():
6+
yield 42
7+
yield from [1, 2, 3]
8+
9+
10+
async def bad_coro():
11+
yield 42 # [yield-inside-async-function]
12+
yield from [1, 2, 3] # [yield-inside-async-function]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[testoptions]
2+
min_pyver=3.5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
yield-inside-async-function:11:bad_coro:Yield inside async function
2+
yield-inside-async-function:12:bad_coro:Yield inside async function

0 commit comments

Comments
 (0)