Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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/fragments/4725.new_check
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Added ``unguarded-next-without-default`` checker which warns about calls to ``next()``
without a default value or handling of possible ``StopIteration`` in the same scope.

Closes #4725
27 changes: 27 additions & 0 deletions pylint/checkers/stdlib.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,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 should "
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
"exception. This exception should be caught or a default value should "
"exception when there's nothing to iterate anymore. It's generally not a trivial task to prove that it's possible to call next safely so this exception should be caught or a default value should "

"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 @@ -533,6 +540,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 @@ -557,6 +565,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 @@ -794,6 +803,24 @@ def _check_env_function(self, node: nodes.Call, infer: nodes.FunctionDef) -> Non
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: nodes.Call,
Expand Down
2 changes: 2 additions & 0 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,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."""


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
2 changes: 1 addition & 1 deletion tests/functional/s/stop_iteration_inside_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Test that no StopIteration is raised inside a generator
"""
# pylint: disable=missing-docstring,invalid-name,import-error, try-except-raise, wrong-import-position
# pylint: disable=not-callable,raise-missing-from,broad-exception-raised
# pylint: disable=not-callable,raise-missing-from,broad-exception-raised, unguarded-next-without-default
import asyncio

class RebornStopIteration(StopIteration):
Expand Down
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