Skip to content

Conversation

zenlyj
Copy link
Contributor

@zenlyj zenlyj commented Oct 4, 2025

Type of Changes

Type
πŸ› Bug fix
βœ“ ✨ New feature
πŸ”¨ Refactoring
πŸ“œ Docs

Description

These changes implements the constraint interface to add a new constraint - TypeConstraint to improve the accuracy of inference for objects used in type-narrowing guards.

Currently, the implementation only supports the isinstance pattern, but it should be relatively straightforward to extend it to support the negated case (not isinstance) in the future.

To determine whether an object is an instance of the given types, the implementation reuses existing inference mechanism for isinstance in brains.

Refs pylint-dev/pylint#1162
Refs pylint-dev/pylint#4635
Refs pylint-dev/pylint#10469

@zenlyj zenlyj added Enhancement ✨ Improvement to a component inference labels Oct 4, 2025
Copy link

codecov bot commented Oct 4, 2025

Codecov Report

❌ Patch coverage is 92.30769% with 3 lines in your changes missing coverage. Please review.
βœ… Project coverage is 93.37%. Comparing base (ab119c2) to head (010503c).

Files with missing lines Patch % Lines
astroid/helpers.py 83.33% 2 Missing ⚠️
astroid/constraint.py 96.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2846      +/-   ##
==========================================
+ Coverage   93.35%   93.37%   +0.02%     
==========================================
  Files          92       92              
  Lines       11190    11212      +22     
==========================================
+ Hits        10446    10469      +23     
+ Misses        744      743       -1     
Flag Coverage Ξ”
linux 93.23% <92.30%> (+0.02%) ⬆️
pypy 93.37% <92.30%> (+0.02%) ⬆️
windows 93.35% <92.30%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Ξ”
astroid/brain/brain_builtin_inference.py 92.68% <100.00%> (+0.65%) ⬆️
astroid/constraint.py 99.02% <96.00%> (-0.98%) ⬇️
astroid/helpers.py 94.44% <83.33%> (-0.89%) ⬇️
πŸš€ New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@zenlyj
Copy link
Contributor Author

zenlyj commented Oct 4, 2025

Seems like test coverage is lacking. Shouldn't be an issue to get it up to 100%, but I would like a review first to see if this implementation makes sense.

jacobtylerwalls
jacobtylerwalls previously approved these changes Oct 4, 2025
Copy link
Member

@jacobtylerwalls jacobtylerwalls left a comment

Choose a reason for hiding this comment

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

Wonderful!

As we do more of these, it would be nice to add a CI step that installs pylint and runs the primer. I had an experiment (work in progress) at jacobtylerwalls#157 and
pylint-dev/pylint@main...jacobtylerwalls:pylint:run-with-custom-astroid. Something for later.

@jacobtylerwalls jacobtylerwalls added this to the 4.0.0 milestone Oct 4, 2025
Copy link
Collaborator

@DanielNoord DanielNoord left a comment

Choose a reason for hiding this comment

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

@jacobtylerwalls Should this be merged for 4.0.0 considering we already released a RC? Feels like a big change to include after a RC.

@jacobtylerwalls
Copy link
Member

I wondered as well, happy to wait for 4.1. Let's release 4.0 final tomorrow in that case.

@DanielNoord
Copy link
Collaborator

I wondered as well, happy to wait for 4.1. Let's release 4.0 final tomorrow in that case.

Let's do so, especially as we haven't tested this with pylint yet. 4.0.0 has enough new features already :)

Copy link
Member

@Pierre-Sassoulas Pierre-Sassoulas left a comment

Choose a reason for hiding this comment

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

This is neat ! Glad to see that the constraint api is getting some love.

@Pierre-Sassoulas Pierre-Sassoulas modified the milestones: 4.0.0, 4.1.0 Oct 5, 2025
@Pierre-Sassoulas Pierre-Sassoulas added the Blocked 🚧 A PR or issue blocked by another PR or issue label Oct 5, 2025
Comment on lines +181 to +191
except StopIteration as e:
raise InferenceError(node=node, context=context) from e
# arg2 MUST be a type or a TUPLE of types
# for isinstance
if isinstance(node_infer, nodes.Tuple):
try:
class_container = [
next(node.infer(context=context)) for node in node_infer.elts
]
except StopIteration as e:
raise InferenceError(node=node, context=context) from e
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The StopIteration handlers are not covered by tests, wondering if it is reasonable to exclude them. From what I understand .infer() already throws InferenceError and we won't get to these blocks?

Copy link
Member

Choose a reason for hiding this comment

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

Isn't it possible for node.infer(context=context) to be inferable but an empty iterable ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That was what I thought as well, but when we get an empty iterable, this decorator catches StopIteration and raises InferenceError? Not sure if there are cases where this decorator is not used.

def raise_if_nothing_inferred(
func: Callable[_P, Generator[InferenceResult]],
) -> Callable[_P, Generator[InferenceResult]]:
def inner(*args: _P.args, **kwargs: _P.kwargs) -> Generator[InferenceResult]:
generator = func(*args, **kwargs)
try:
yield next(generator)
except StopIteration as error:
# generator is empty
if error.args:
raise InferenceError(**error.args[0]) from error
raise InferenceError(
"StopIteration raised without any error information."
) from error
except RecursionError as error:
raise InferenceError(
f"RecursionError raised with limit {sys.getrecursionlimit()}."
) from error
yield from generator
return inner

Copy link
Member

Choose a reason for hiding this comment

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

Not sure if there are cases where this decorator is not used.

That worries me also (plugins?)

A pragma to exclude coverage seems reasonable.

Comment on lines +166 to +169
matches_checked_types = helpers.object_isinstance(inferred, types)

if matches_checked_types is util.Uninferable:
return True
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This Uninferable check is not covered by tests, it should be unreachable in runtime. I am including it as a safe guard because helpers.object_isinstance() returns bool | Uninferable, but I am also fine with removing it. Thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

Can you share more details about why this is unreachable? I see that we short-circuit if inferred is uninferable, but it wasn't apparent to me why inferring again wouldn't return uninferable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Blocked 🚧 A PR or issue blocked by another PR or issue Enhancement ✨ Improvement to a component inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants