Skip to content
Closed
Show file tree
Hide file tree
Changes from 5 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
1 change: 1 addition & 0 deletions doc/data/messages/u/unguarded-next-without-default/bad.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
next(i for i in (1, 2) if isinstance(i, str)) # [unspecified-default-for-next]
1 change: 1 addition & 0 deletions doc/data/messages/u/unguarded-next-without-default/good.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
next((i for i in (1, 2) if isinstance(i, str)), None)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
next((i for i in (1, 2) if isinstance(i, str)), None)
def display(animals):
iterator = iter(animals)
while True:
next_animal = next(iterator, None)
if next_animal is None:
break
print(next_animal)

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- `PEP 479 <https://peps.python.org/pep-0479/>`_
4 changes: 4 additions & 0 deletions doc/whatsnew/2/2.15/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Summary -- Release highlights
New checkers
============

* Added ``unguarded-next-without-default`` checker which warns about calls to ``next()``
without a default value.

Closes #4725

Removed checkers
================
Expand Down
27 changes: 27 additions & 0 deletions pylint/checkers/stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,13 @@ class StdlibChecker(DeprecatedMixin, BaseChecker):
]
},
),
"W1519": (
"Using next without explicitly specifying a default value or catching the StopIteration",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Using next without explicitly specifying a default value or catching the StopIteration",
"Using next without specifying a default value or catching the StopIteration",

There's no implicit default, it's going to raise a StopIteration if you don't set the default right ?

"unguarded-next-without-default",
"Without a default value calls to next() can raise a StopIteration "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"Without a default value calls to next() can raise a StopIteration "
"Without a default value calls to next() will raise a ``StopIteration`` "

"exception. This exception should be caught or a default value can "
"be provided.",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"be provided.",
"be provided, unless you're in a function that is expected to raise ``StopIteration``.",

),
}

def __init__(self, linter: PyLinter) -> None:
Expand Down Expand Up @@ -498,6 +505,7 @@ def _check_shallow_copy_environ(self, node: nodes.Call) -> None:
"deprecated-class",
"unspecified-encoding",
"forgotten-debug-statement",
"unguarded-next-without-default",
)
def visit_call(self, node: nodes.Call) -> None:
"""Visit a Call node."""
Expand All @@ -522,6 +530,7 @@ def visit_call(self, node: nodes.Call) -> None:
self._check_for_preexec_fn_in_popen(node)
elif isinstance(inferred, nodes.FunctionDef):
name = inferred.qname()
self._check_next_call(node, name)
if name == COPY_COPY:
self._check_shallow_copy_environ(node)
elif name in ENV_GETTERS:
Expand Down Expand Up @@ -720,6 +729,24 @@ def _check_env_function(self, node, infer):
allow_none=True,
)

def _check_next_call(self, node: nodes.Call, name: str) -> None:
if name != "builtins.next":
return
# We don't care about this call if there are zero arguments
if len(node.args) != 1:
return
if utils.get_exception_handlers(node, StopIteration):
return
# Raising is fine within __next__
func_def = utils.get_node_first_ancestor_of_type(node, nodes.FunctionDef)
if func_def and func_def.name == "__next__":
return
self.add_message(
"unguarded-next-without-default",
node=node,
confidence=interfaces.INFERENCE,
)

def _check_invalid_envvar_value(self, node, infer, message, call_arg, allow_none):
if call_arg in (astroid.Uninferable, None):
return
Expand Down
2 changes: 2 additions & 0 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ disable=
format,
# We anticipate #3512 where it will become optional
fixme,
# TODO: 2.15: Fix and enable.
unguarded-next-without-default


[REPORTS]
Expand Down
31 changes: 17 additions & 14 deletions tests/functional/c/cellvar_escaping_loop.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# pylint: disable=unnecessary-comprehension,missing-docstring,too-few-public-methods,unnecessary-direct-lambda-call
# pylint: disable=unnecessary-comprehension, missing-docstring, too-few-public-methods
# pylint: disable=unnecessary-direct-lambda-call, unguarded-next-without-default
"""Tests for loopvar-in-closure."""
from __future__ import print_function

Expand Down Expand Up @@ -55,9 +56,11 @@ def good_case8():
"""Another eager binding of the cell variable."""
funs = []
for i in range(10):

def func(bound_i=i):
"""Ignore."""
return bound_i

funs.append(func)
return funs

Expand All @@ -75,8 +78,10 @@ def good_case10():
"""Ignore when a loop variable is shadowed by an inner function"""
lst = []
for i in range(10): # pylint: disable=unused-variable

def func():
i = 100

def func2(arg=i):
return arg

Expand All @@ -87,8 +92,7 @@ def func2(arg=i):


def good_case_issue3107():
"""Eager binding of cell variable when used in a non-trivial default argument expression.
"""
"""Eager binding of cell variable when used in a non-trivial default argument expression."""
for i in [[2], [3]]:
next(filter(lambda j, ix=i[0]: j == ix, [1, 3]))

Expand All @@ -99,16 +103,20 @@ def good_case_issue_5012():
"""
funs = []
for i in range(5):

def func(*, _i=i):
print(_i)

funs.append(func)

def func2(_i=i):
print(_i)

funs.append(func2)

return funs


def bad_case():
"""Closing over a loop variable."""
lst = []
Expand All @@ -135,9 +143,11 @@ def bad_case4():
"""Closing over variable defined in loop."""
lst = []
for i in range(10):

def nested():
"""Nested function."""
return i**2 # [cell-var-from-loop]

lst.append(nested)
return lst

Expand Down Expand Up @@ -173,9 +183,7 @@ def bad_case6():
def bad_case7():
"""Multiple variables unpacked in comprehension."""
return [
lambda: (
x # [cell-var-from-loop]
+ y) # [cell-var-from-loop]
lambda: (x + y) # [cell-var-from-loop] # [cell-var-from-loop]
for x, y in ((1, 2), (3, 4), (5, 6))
]

Expand All @@ -202,6 +210,7 @@ def bad_case10():
"""Detect when a loop variable is the default argument for a nested function"""
lst = []
for i in range(10):

def func():
def func2(arg=i): # [cell-var-from-loop]
return arg
Expand All @@ -214,15 +223,9 @@ def func2(arg=i): # [cell-var-from-loop]

def bad_case_issue2846():
"""Closing over variable that is used within a comprehension in the function body."""
lst_a = [
(lambda: n) # [cell-var-from-loop]
for n in range(3)
]
lst_a = [(lambda: n) for n in range(3)] # [cell-var-from-loop]

lst_b = [
(lambda: [n for _ in range(3)]) # [cell-var-from-loop]
for n in range(3)
]
lst_b = [(lambda: [n for _ in range(3)]) for n in range(3)] # [cell-var-from-loop]

return lst_a, lst_b

Expand Down
26 changes: 13 additions & 13 deletions tests/functional/c/cellvar_escaping_loop.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
cell-var-from-loop:117:27:117:28:bad_case.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:122:20:122:21:bad_case2.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:130:27:130:28:bad_case3.<lambda>:Cell variable j defined in loop:UNDEFINED
cell-var-from-loop:140:19:140:20:bad_case4.nested:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:161:20:161:21:bad_case5.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:169:27:169:28:bad_case6.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:177:12:177:13:bad_case7.<lambda>:Cell variable x defined in loop:UNDEFINED
cell-var-from-loop:178:14:178:15:bad_case7.<lambda>:Cell variable y defined in loop:UNDEFINED
cell-var-from-loop:187:27:187:28:bad_case8.<lambda>:Cell variable j defined in loop:UNDEFINED
cell-var-from-loop:197:27:197:28:bad_case9.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:206:26:206:27:bad_case10.func.func2:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:218:17:218:18:bad_case_issue2846.<lambda>:Cell variable n defined in loop:UNDEFINED
cell-var-from-loop:223:18:223:19:bad_case_issue2846.<lambda>:Cell variable n defined in loop:UNDEFINED
cell-var-from-loop:118:27:118:28:bad_case.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:123:20:123:21:bad_case2.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:131:27:131:28:bad_case3.<lambda>:Cell variable j defined in loop:UNDEFINED
cell-var-from-loop:141:19:141:20:bad_case4.nested:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:162:20:162:21:bad_case5.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:170:27:170:28:bad_case6.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:178:12:178:13:bad_case7.<lambda>:Cell variable x defined in loop:UNDEFINED
cell-var-from-loop:179:14:179:15:bad_case7.<lambda>:Cell variable y defined in loop:UNDEFINED
cell-var-from-loop:188:27:188:28:bad_case8.<lambda>:Cell variable j defined in loop:UNDEFINED
cell-var-from-loop:198:27:198:28:bad_case9.<lambda>:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:207:26:207:27:bad_case10.func.func2:Cell variable i defined in loop:UNDEFINED
cell-var-from-loop:219:17:219:18:bad_case_issue2846.<lambda>:Cell variable n defined in loop:UNDEFINED
cell-var-from-loop:224:18:224:19:bad_case_issue2846.<lambda>:Cell variable n defined in loop:UNDEFINED
23 changes: 18 additions & 5 deletions tests/functional/s/stop_iteration_inside_generator.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
"""
Test that no StopIteration is raised inside a generator
"""
# pylint: disable=missing-docstring,invalid-name,import-error, try-except-raise, wrong-import-position,not-callable,raise-missing-from
# pylint: disable=missing-docstring, invalid-name, import-error, try-except-raise, wrong-import-position
# pylint: disable=not-callable, raise-missing-from, unguarded-next-without-default
import asyncio


class RebornStopIteration(StopIteration):
"""
A class inheriting from StopIteration exception
"""


# This one is ok
def gen_ok():
yield 1
yield 2
yield 3


# pylint should warn about this one
# because of a direct raising of StopIteration inside generator
def gen_stopiter():
Expand All @@ -23,6 +27,7 @@ def gen_stopiter():
yield 3
raise StopIteration # [stop-iteration-return]


# pylint should warn about this one
# because of a direct raising of an exception inheriting from StopIteration inside generator
def gen_stopiterchild():
Expand All @@ -31,13 +36,15 @@ def gen_stopiterchild():
yield 3
raise RebornStopIteration # [stop-iteration-return]


# pylint should warn here
# because of the possibility that next raises a StopIteration exception
def gen_next_raises_stopiter():
g = gen_ok()
while True:
yield next(g) # [stop-iteration-return]


# This one is the same as gen_next_raises_stopiter
# but is ok because the next function is inside
# a try/except block handling StopIteration
Expand All @@ -49,6 +56,7 @@ def gen_next_inside_try_except():
except StopIteration:
return


# This one is the same as gen_next_inside_try_except
# but is not ok because the next function is inside
# a try/except block that don't handle StopIteration
Expand All @@ -60,6 +68,7 @@ def gen_next_inside_wrong_try_except():
except ValueError:
return


# This one is the same as gen_next_inside_try_except
# but is not ok because the next function is inside
# a try/except block that handle StopIteration but reraise it
Expand All @@ -71,11 +80,13 @@ def gen_next_inside_wrong_try_except2():
except StopIteration:
raise StopIteration # [stop-iteration-return]


# Those two last are ok
def gen_in_for():
for el in gen_ok():
yield el


def gen_yield_from():
yield from gen_ok()

Expand All @@ -84,7 +95,7 @@ def gen_dont_crash_on_no_exception():
g = gen_ok()
while True:
try:
yield next(g) # [stop-iteration-return]
yield next(g) # [stop-iteration-return]
except ValueError:
raise

Expand All @@ -97,11 +108,12 @@ def gen_dont_crash_on_uninferable():

# https://github.com/PyCQA/pylint/issues/1830
def gen_next_with_sentinel():
yield next([], 42) # No bad return
yield next([], 42) # No bad return


from itertools import count


# https://github.com/PyCQA/pylint/issues/2158
def generator_using_next():
counter = count()
Expand All @@ -113,6 +125,7 @@ def generator_using_next():
class SomeClassWithNext:
def next(self):
return iter([1, 2, 3])

def some_gen(self):
for value in self.next():
yield value
Expand All @@ -122,8 +135,8 @@ def some_gen(self):


def something_invalid():
raise Exception('cannot iterate this')
raise Exception("cannot iterate this")


def invalid_object_passed_to_next():
yield next(something_invalid()) # [stop-iteration-return]
yield next(something_invalid()) # [stop-iteration-return]
14 changes: 7 additions & 7 deletions tests/functional/s/stop_iteration_inside_generator.txt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
stop-iteration-return:24:4:24:23:gen_stopiter:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:32:4:32:29:gen_stopiterchild:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:39:14:39:21:gen_next_raises_stopiter:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:59:18:59:25:gen_next_inside_wrong_try_except:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:72:12:72:31:gen_next_inside_wrong_try_except2:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:87:18:87:25:gen_dont_crash_on_no_exception:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:129:10:129:35:invalid_object_passed_to_next:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:25:4:25:23:gen_stopiter:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:33:4:33:29:gen_stopiterchild:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:40:14:40:21:gen_next_raises_stopiter:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:60:18:60:25:gen_next_inside_wrong_try_except:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:73:12:73:31:gen_next_inside_wrong_try_except2:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:88:18:88:25:gen_dont_crash_on_no_exception:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
stop-iteration-return:130:10:130:35:invalid_object_passed_to_next:Do not raise StopIteration in generator, use return statement instead:UNDEFINED
Loading