Skip to content
Merged
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
4 changes: 4 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ What's New in astroid 4.0.0?
============================
Release date: TBA

* Add support for boolean truthiness constraints (`x`, `not x`) in inference.

Closes pylint-dev/pylint#9515

* Fix false positive `invalid-name` on `attrs` classes with `ClassVar` annotated variables.

Closes pylint-dev/pylint#10525
Expand Down
48 changes: 47 additions & 1 deletion astroid/constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,47 @@ def satisfied_by(self, inferred: InferenceResult) -> bool:
return self.negate ^ _matches(inferred, self.CONST_NONE)


class BooleanConstraint(Constraint):
"""Represents an "x" or "not x" constraint."""

@classmethod
def match(
cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False
) -> Self | None:
"""Return a new constraint for node if expr matches one of these patterns:

- direct match (expr == node): use given negate value
- negated match (expr == `not node`): flip negate value

Return None if no pattern matches.
"""
if _matches(expr, node):
return cls(node=node, negate=negate)

if (
isinstance(expr, nodes.UnaryOp)
and expr.op == "not"
and _matches(expr.operand, node)
):
return cls(node=node, negate=not negate)

return None

def satisfied_by(self, inferred: InferenceResult) -> bool:
"""Return True for uninferable results, or depending on negate flag:

- negate=False: satisfied if boolean value is True
- negate=True: satisfied if boolean value is False
"""
inferred_booleaness = inferred.bool_value()
if isinstance(inferred, util.UninferableBase) or isinstance(
inferred_booleaness, util.UninferableBase
):
return True

return self.negate ^ inferred_booleaness


def get_constraints(
expr: _NameNodes, frame: nodes.LocalsDictNodeNG
) -> dict[nodes.If, set[Constraint]]:
Expand Down Expand Up @@ -114,7 +155,12 @@ def get_constraints(
return constraints_mapping


ALL_CONSTRAINT_CLASSES = frozenset((NoneConstraint,))
ALL_CONSTRAINT_CLASSES = frozenset(
(
NoneConstraint,
BooleanConstraint,
)
)
"""All supported constraint types."""


Expand Down
2 changes: 2 additions & 0 deletions tests/test_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ def common_params(node: str) -> pytest.MarkDecorator:
(
(f"{node} is None", None, 3),
(f"{node} is not None", 3, None),
(f"{node}", 3, None),
(f"not {node}", None, 3),
),
)

Expand Down
2 changes: 1 addition & 1 deletion tests/test_inference.py
Original file line number Diff line number Diff line change
Expand Up @@ -5491,7 +5491,7 @@ def add(x, y):
else:
kwargs = {}

if nums:
if nums is not None:
add(*nums)
print(**kwargs)
"""
Expand Down
Loading