Skip to content

Commit ea3772e

Browse files
authored
Support boolean truthiness constraints in inference (#2823)
1 parent 90188b4 commit ea3772e

File tree

4 files changed

+54
-2
lines changed

4 files changed

+54
-2
lines changed

ChangeLog

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ What's New in astroid 4.0.0?
77
============================
88
Release date: TBA
99

10+
* Add support for boolean truthiness constraints (`x`, `not x`) in inference.
11+
12+
Closes pylint-dev/pylint#9515
13+
1014
* Fix false positive `invalid-name` on `attrs` classes with `ClassVar` annotated variables.
1115

1216
Closes pylint-dev/pylint#10525

astroid/constraint.py

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,47 @@ def satisfied_by(self, inferred: InferenceResult) -> bool:
8484
return self.negate ^ _matches(inferred, self.CONST_NONE)
8585

8686

87+
class BooleanConstraint(Constraint):
88+
"""Represents an "x" or "not x" constraint."""
89+
90+
@classmethod
91+
def match(
92+
cls, node: _NameNodes, expr: nodes.NodeNG, negate: bool = False
93+
) -> Self | None:
94+
"""Return a new constraint for node if expr matches one of these patterns:
95+
96+
- direct match (expr == node): use given negate value
97+
- negated match (expr == `not node`): flip negate value
98+
99+
Return None if no pattern matches.
100+
"""
101+
if _matches(expr, node):
102+
return cls(node=node, negate=negate)
103+
104+
if (
105+
isinstance(expr, nodes.UnaryOp)
106+
and expr.op == "not"
107+
and _matches(expr.operand, node)
108+
):
109+
return cls(node=node, negate=not negate)
110+
111+
return None
112+
113+
def satisfied_by(self, inferred: InferenceResult) -> bool:
114+
"""Return True for uninferable results, or depending on negate flag:
115+
116+
- negate=False: satisfied if boolean value is True
117+
- negate=True: satisfied if boolean value is False
118+
"""
119+
inferred_booleaness = inferred.bool_value()
120+
if isinstance(inferred, util.UninferableBase) or isinstance(
121+
inferred_booleaness, util.UninferableBase
122+
):
123+
return True
124+
125+
return self.negate ^ inferred_booleaness
126+
127+
87128
def get_constraints(
88129
expr: _NameNodes, frame: nodes.LocalsDictNodeNG
89130
) -> dict[nodes.If, set[Constraint]]:
@@ -114,7 +155,12 @@ def get_constraints(
114155
return constraints_mapping
115156

116157

117-
ALL_CONSTRAINT_CLASSES = frozenset((NoneConstraint,))
158+
ALL_CONSTRAINT_CLASSES = frozenset(
159+
(
160+
NoneConstraint,
161+
BooleanConstraint,
162+
)
163+
)
118164
"""All supported constraint types."""
119165

120166

tests/test_constraint.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ def common_params(node: str) -> pytest.MarkDecorator:
1717
(
1818
(f"{node} is None", None, 3),
1919
(f"{node} is not None", 3, None),
20+
(f"{node}", 3, None),
21+
(f"not {node}", None, 3),
2022
),
2123
)
2224

tests/test_inference.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5491,7 +5491,7 @@ def add(x, y):
54915491
else:
54925492
kwargs = {}
54935493
5494-
if nums:
5494+
if nums is not None:
54955495
add(*nums)
54965496
print(**kwargs)
54975497
"""

0 commit comments

Comments
 (0)