Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed an inconsistency in reporting of unbound variables when the var… #5551

Merged
merged 1 commit into from
Jul 21, 2023
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
19 changes: 19 additions & 0 deletions docs/type-concepts-advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,7 @@ def func4(x: str | None):
```

### Narrowing for Implied Else

When an “if” or “elif” clause is used without a corresponding “else”, Pyright will generally assume that the code can “fall through” without executing the “if” or “elif” block. However, there are cases where the analyzer can determine that a fall-through is not possible because the “if” or “elif” is guaranteed to be executed based on type analysis.

```python
Expand Down Expand Up @@ -222,6 +223,24 @@ b = c
reveal_type(b) # list[Any]
```

### Narrowing for Captured Variables

If a variable’s type is narrowed in an outer scope and the variable is subsequently captured by an inner-scoped function or lambda, Pyright retains the narrowed type if it can determine that the value of the captured variable is not modified on any code path after the inner-scope function or lambda is defined and is not modified in another scope via a `nonlocal` or `global` binding.

```python
def func(val: int | None):
if val is not None:

def inner_1() -> None:
reveal_type(val) # int
print(val + 1)

inner_2 = lambda: reveal_type(val) + 1 # int

inner_1()
inner_2()
```

### Constrained Type Variables

When a TypeVar is defined, it can be constrained to two or more types.
Expand Down
35 changes: 26 additions & 9 deletions packages/pyright-internal/src/analyzer/typeEvaluator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4329,15 +4329,27 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
): FlowNodeTypeResult | undefined {
// This function applies only to variables, parameters, and imports, not to other
// types of symbols.
const decls = symbolWithScope.symbol.getDeclarations();
if (
!symbolWithScope.symbol
.getDeclarations()
.every(
(decl) =>
decl.type === DeclarationType.Variable ||
decl.type === DeclarationType.Parameter ||
decl.type === DeclarationType.Alias
)
!decls.every(
(decl) =>
decl.type === DeclarationType.Variable ||
decl.type === DeclarationType.Parameter ||
decl.type === DeclarationType.Alias
)
) {
return undefined;
}

// If the symbol is modified in scopes other than the one in which it is
// declared (e.g. through a nonlocal or global binding), it is not eligible
// for code flow analysis.
if (
!decls.every(
(decl) =>
decl.type === DeclarationType.Parameter ||
ScopeUtils.getScopeForNode(decl.node) === symbolWithScope.scope
)
) {
return undefined;
}
Expand Down Expand Up @@ -4390,7 +4402,12 @@ export function createTypeEvaluator(importLookup: ImportLookup, evaluatorOptions
);
})
) {
return getFlowTypeOfReference(node, symbolWithScope.symbol.id, effectiveType, innerScopeNode);
let typeAtStart = effectiveType;
if (symbolWithScope.symbol.isInitiallyUnbound()) {
typeAtStart = UnboundType.create();
}

return getFlowTypeOfReference(node, symbolWithScope.symbol.id, typeAtStart, innerScopeNode);
}
}
}
Expand Down
15 changes: 15 additions & 0 deletions packages/pyright-internal/src/tests/samples/capturedVariable1.py
Original file line number Diff line number Diff line change
Expand Up @@ -104,3 +104,18 @@ def foo() -> str:
return x.upper()

return x.upper()


def func10(cond: bool, val: str):
x: str | None = val if cond else None
y: str | None = val if cond else None

def inner1():
nonlocal x
x = None

if x is not None and y is not None:

def inner2():
reveal_type(x, expected_text="str | None")
reveal_type(y, expected_text="str")
31 changes: 31 additions & 0 deletions packages/pyright-internal/src/tests/samples/unbound5.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# This sample tests the interplay between unbound symbol detection and
# the code that handles conditional narrowing of captured variables.

from random import random


if random() > 0.5:
from datetime import datetime
from math import cos

# The following should generate an error because datetime
# is "narrowed" across execution scopes.
test0 = lambda: datetime


def test1():
# The following should generate an error because datetime
# is "narrowed" across execution scopes.
return datetime


test2 = lambda: cos


def test2():
return cos


# This modification means that cos will not be narrowed
# across execution scopes.
cos = None
6 changes: 6 additions & 0 deletions packages/pyright-internal/src/tests/typeEvaluator2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,12 @@ test('Unbound4', () => {
TestUtils.validateResults(analysisResults, 2);
});

test('Unbound5', () => {
const analysisResults = TestUtils.typeAnalyzeSampleFiles(['unbound5.py']);

TestUtils.validateResults(analysisResults, 2);
});

test('Assert1', () => {
const configOptions = new ConfigOptions('.');

Expand Down
Loading