Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 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)) # [unguarded-next-without-default]
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)) # [unguarded-next-without-default]
def display(animals):
iterator = iter(animals)
while True:
print(next(iterator)) # [unguarded-next-without-default]

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
3 changes: 2 additions & 1 deletion 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
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
3 changes: 2 additions & 1 deletion tests/functional/s/stop_iteration_inside_generator.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
"""
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):
Expand Down
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
2 changes: 1 addition & 1 deletion tests/functional/u/undefined/undefined_variable_py38.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"""Tests for undefined variable with assignment expressions"""
# pylint: disable=using-constant-test, expression-not-assigned
# pylint: disable=using-constant-test, expression-not-assigned, unguarded-next-without-default

# Tests for annotation of variables and potentially undefinition

Expand Down
46 changes: 46 additions & 0 deletions tests/functional/u/unguarded_next_without_default.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
"""Warnings for using next() without specifying a default value."""
# pylint: disable=missing-class-docstring, too-few-public-methods, missing-function-docstring
# pylint: disable=inconsistent-return-statements

next((i for i in (1, 2)), None)
next(i for i in (1, 2)) # [unguarded-next-without-default]
var = next(i for i in (1, 2)) # [unguarded-next-without-default]

try:
next(i for i in (1, 2))
except StopIteration:
pass

try:
next(i for i in (1, 2)) # [unguarded-next-without-default]
except ValueError:
pass

try:
next(i for i in (1, 2))
except (ValueError, StopIteration):
pass

try:
next(i for i in (1, 2))
except ValueError:
pass
except StopIteration:
pass

redefined_next = next
redefined_next(i for i in (1, 2)) # [unguarded-next-without-default]


class MyClass:
def __next__(self):
return next(i for i in (1, 2))
Comment on lines +35 to +37
Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas Feb 5, 2023

Choose a reason for hiding this comment

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

Suggested change
class MyClass:
def __next__(self):
return next(i for i in (1, 2))
class MyClass:
def __next__(self):
return next(i for i in (1, 2))
def expected_to_raise_stop_iteration(self):
"""Custom iterator function that will raise StopIteration by design.
Raises:
StopIteration: If the end of the sequence has been reached.
"""
return next(i for i in (1, 2))

Just a thought. Maybe we don't need to check that it's a proper docstring in numpy style or whatever. If there's StopIteration in the docstring we do not raise.



# Example based on astroid code
def func(keywords, context):
for value in keywords:
try:
return next(value.infer(context=context))
except StopIteration:
continue
4 changes: 4 additions & 0 deletions tests/functional/u/unguarded_next_without_default.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
unguarded-next-without-default:6:0:6:23::Using next without explicitly specifying a default value or catching the StopIteration:INFERENCE
unguarded-next-without-default:7:6:7:29::Using next without explicitly specifying a default value or catching the StopIteration:INFERENCE
unguarded-next-without-default:15:4:15:27::Using next without explicitly specifying a default value or catching the StopIteration:INFERENCE
unguarded-next-without-default:32:0:32:33::Using next without explicitly specifying a default value or catching the StopIteration:INFERENCE