Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# The `__file__` attribute on imported modules

## Module successfully resolved

```py
from b import __file__ as module_path
from stub import __file__ as stub_path
from override import __file__ as overidden_path

reveal_type(__file__) # revealed: str
reveal_type(module_path) # revealed: str
reveal_type(stub_path) # revealed: str
reveal_type(overidden_path) # revealed: None

# NOTE: This import fails at runtime as this is a C Extension
# with no `__file__` global. It's hard for us to determine this
# right now, however (all we know is it comes from a stub file),
# and if it did exist then it would be of type `str` since `sys`
# is not a namespace packages. This behaviour also matches other
# type checkers.
from sys import __file__ as no_path

reveal_type(no_path) # revealed: str
```

`b.py`:

```py

```

`override.py`:

```py
__file__ = None
```

`stub.pyi`:

```pyi
```

## Module resolution failed

```py
from bar import __file__ as module_path # error: "Cannot resolve imported module `bar`"

reveal_type(module_path) # revealed: Unknown
```

## Non-namespace packages have `__file__` available

`a/__init__.py`:

```py
```

`a/b.py`:

```py
```

```py
import a

reveal_type(a.__file__) # revealed: str
```

## `__file__` is set to `None` for namespace packages

`namespace/c.py`:

```py
```

```py
import namespace

# TODO: `__file__` does exist on namespace packages but is set to `None`;
# this is a false positive
#
# error: [unresolved-attribute] "Module `namespace` has no member `__file__`"
reveal_type(namespace.__file__) # revealed: Unknown
```
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,14 @@ import typing
reveal_type(typing.__name__) # revealed: str
reveal_type(typing.__init__) # revealed: bound method ModuleType.__init__(name: str, doc: str | None = ...) -> None

# For a stub module, we don't know that `__file__` is a string (at runtime it may be entirely
Comment thread
AlexWaygood marked this conversation as resolved.
# unset, but we follow typeshed here):
reveal_type(typing.__file__) # revealed: str | None
# Note that since the source for the `typing` module is a stub file,
# we can't know for sure that it's not a C extension at runtime,
# and C extensions don't necessarily have a `__file__` global attribute
# at all (in which case this attribute access would fail). However, we
# *do* know that `typing` is not a namespace package, so if `__file__`
# does exist, it will be of type `str` (`__file__` is only `None` for
# namespace packages).
reveal_type(typing.__file__) # revealed: str

# These come from `builtins.object`, not `types.ModuleType`:
reveal_type(typing.__eq__) # revealed: bound method ModuleType.__eq__(value: object, /) -> bool
Expand Down
59 changes: 47 additions & 12 deletions crates/ty_python_semantic/src/place.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1098,6 +1098,18 @@ fn symbol_impl<'db>(
considered_definitions: ConsideredDefinitions,
) -> PlaceAndQualifiers<'db> {
let _span = tracing::trace_span!("symbol", ?name).entered();
let place = place_table(db, scope)
.symbol_id(name)
.map(|symbol| {
place_by_id(
db,
scope,
symbol.into(),
requires_explicit_reexport,
considered_definitions,
)
})
.unwrap_or_default();

if name == "platform"
&& file_to_module(db, scope.file(db))
Expand All @@ -1111,20 +1123,43 @@ fn symbol_impl<'db>(
// Fall through to the looked up type
}
}
} else if name == "__file__"
&& let Some(module) = file_to_module(db, scope.file(db))
{
// We special-case `__file__` here because we know that for a successfully imported
// non-namespace-package Python module, that hasn't been explicitly overridden it
// is always a string, even though typeshed says `str | None`. For a namespace package,
// meanwhile, it will always be `None`.
//
// Note that C-extension modules (stdlib examples include `sys`, `itertools`, etc.)
// may not have a `__file__` attribute at runtime at all, but that doesn't really
// affect the *type* of the attribute, just the *boundness*. There's no way for us
// to know right now whether a stub represents a C extension or not, so for now we
// do not attempt to detect this; we just infer `str` still. This matches the
// behaviour of other major type checkers.
let default_type = if module.file(db).is_some() {
KnownClass::Str.to_instance(db)
} else {
Type::none(db)
};

match place.place {
Place::Defined(defined_place) => match defined_place.definedness {
Definedness::AlwaysDefined => return place,
Definedness::PossiblyUndefined => {
let new_type = UnionType::from_elements(db, [defined_place.ty, default_type]);
let def_place = DefinedPlace::new(new_type)
.with_definedness(Definedness::AlwaysDefined)
.with_widening(defined_place.widening)
.with_origin(defined_place.origin);
return Place::Defined(def_place).into();
}
},
Place::Undefined => return Place::bound(default_type).into(),
}
}

place_table(db, scope)
.symbol_id(name)
.map(|symbol| {
place_by_id(
db,
scope,
symbol.into(),
requires_explicit_reexport,
considered_definitions,
)
})
.unwrap_or_default()
place
}

fn place_impl<'db>(
Expand Down
Loading