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: 2 additions & 2 deletions crates/ruff_python_ast/src/python_version.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,8 @@ impl PythonVersion {
}

pub const fn latest_ty() -> Self {
// Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
Self::PY313
// Make sure to update the default value for `EnvironmentOptions::python_version` when bumping this version.
Self::PY314
}

pub const fn as_tuple(self) -> (u8, u8) {
Expand Down
2 changes: 1 addition & 1 deletion crates/ty/docs/cli.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions crates/ty/docs/configuration.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion crates/ty/src/args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub(crate) struct CheckCommand {
/// and use the minimum version from the specified range
/// 2. Check for an activated or configured Python environment
/// and attempt to infer the Python version of that environment
/// 3. Fall back to the latest stable Python version supported by ty (currently Python 3.13)
/// 3. Fall back to the latest stable Python version supported by ty (see `ty check --help` output)
#[arg(long, value_name = "VERSION", alias = "target-version")]
pub(crate) python_version: Option<PythonVersion>,

Expand Down
7 changes: 4 additions & 3 deletions crates/ty_ide/src/completion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1732,6 +1732,7 @@ C.<CURSOR>
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
meta_attr :: int
mro :: bound method <class 'C'>.mro() -> list[type]
__annotate__ :: @Todo | None
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
Expand Down Expand Up @@ -1797,7 +1798,7 @@ Meta.<CURSOR>
// whether we're in release mode or not. These differences
// aren't really relevant for completion tests AFAIK, so
// just redact them. ---AG
filters => [(r"(?m)\s*__(annotations|new)__.+$", "")]},
filters => [(r"(?m)\s*__(annotations|new|annotate)__.+$", "")]},
{
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
meta_attr :: property
Expand Down Expand Up @@ -1908,6 +1909,7 @@ Quux.<CURSOR>
some_method :: def some_method(self) -> int
some_property :: property
some_static_method :: def some_static_method(self) -> int
__annotate__ :: @Todo | None
__annotations__ :: dict[str, Any]
__base__ :: type | None
__bases__ :: tuple[type, ...]
Expand Down Expand Up @@ -1970,7 +1972,7 @@ Answer.<CURSOR>
insta::with_settings!({
// See above: filter out some members which contain @Todo types that are
// rendered differently in release mode.
filters => [(r"(?m)\s*__(call|reduce_ex)__.+$", "")]},
filters => [(r"(?m)\s*__(call|reduce_ex|annotate|signature)__.+$", "")]},
{
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
NO :: Literal[Answer.NO]
Expand Down Expand Up @@ -2020,7 +2022,6 @@ Answer.<CURSOR>
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__]
__ror__ :: bound method <class 'Answer'>.__ror__(value: Any, /) -> UnionType
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
__signature__ :: bound method <class 'Answer'>.__signature__() -> str
__sizeof__ :: def __sizeof__(self) -> int
__str__ :: def __str__(self) -> str
__subclasscheck__ :: bound method <class 'Answer'>.__subclasscheck__(subclass: type, /) -> bool
Expand Down
4 changes: 2 additions & 2 deletions crates/ty_project/src/metadata/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -520,8 +520,8 @@ pub struct EnvironmentOptions {
/// to reflect the differing contents of the standard library across Python versions.
#[serde(skip_serializing_if = "Option::is_none")]
#[option(
default = r#""3.13""#,
value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | <major>.<minor>"#,
default = r#""3.14""#,
value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | "3.14" | <major>.<minor>"#,
example = r#"
python-version = "3.12"
"#
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,55 @@ class A:
y: int
```

### `kw_only` - Python 3.13

```toml
[environment]
python-version = "3.13"
```

```py
from dataclasses import dataclass, field

@dataclass
class Employee:
e_id: int = field(kw_only=True, default=0)
name: str

Employee("Alice")
Employee(name="Alice")
Employee(name="Alice", e_id=1)
Employee(e_id=1, name="Alice")
Employee("Alice", e_id=1)

Employee("Alice", 1) # error: [too-many-positional-arguments]
```

### `kw_only` - Python 3.14

```toml
[environment]
python-version = "3.14"
```

```py
from dataclasses import dataclass, field

@dataclass
class Employee:
# Python 3.14 introduces a new `doc` parameter for `dataclasses.field`
e_id: int = field(kw_only=True, default=0, doc="Global employee ID")
name: str

Employee("Alice")
Employee(name="Alice")
Employee(name="Alice", e_id=1)
Employee(e_id=1, name="Alice")
Employee("Alice", e_id=1)

Employee("Alice", 1) # error: [too-many-positional-arguments]
```

### `slots`

If a dataclass is defined with `slots=True`, the `__slots__` attribute is generated as a tuple. It
Expand Down
85 changes: 55 additions & 30 deletions crates/ty_python_semantic/src/types/call/bind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -962,43 +962,46 @@ impl<'db> Bindings<'db> {
}

Some(KnownFunction::Field) => {
// TODO this will break on Python 3.14 -- we should match by parameter name instead
if let [default, default_factory, init, .., kw_only] =
overload.parameter_types()
{
let default_ty = match (default, default_factory) {
(Some(default_ty), _) => *default_ty,
(_, Some(default_factory_ty)) => default_factory_ty
.try_call(db, &CallArguments::none())
.map_or(Type::unknown(), |binding| binding.return_type(db)),
_ => Type::unknown(),
};
let default =
overload.parameter_type_by_name("default").unwrap_or(None);
let default_factory = overload
.parameter_type_by_name("default_factory")
.unwrap_or(None);
let init = overload.parameter_type_by_name("init").unwrap_or(None);
let kw_only =
overload.parameter_type_by_name("kw_only").unwrap_or(None);

let default_ty = match (default, default_factory) {
(Some(default_ty), _) => default_ty,
(_, Some(default_factory_ty)) => default_factory_ty
.try_call(db, &CallArguments::none())
.map_or(Type::unknown(), |binding| binding.return_type(db)),
_ => Type::unknown(),
};

let init = init
.map(|init| !init.bool(db).is_always_false())
.unwrap_or(true);
let init = init
.map(|init| !init.bool(db).is_always_false())
.unwrap_or(true);

let kw_only = if Program::get(db).python_version(db)
>= PythonVersion::PY310
{
let kw_only =
if Program::get(db).python_version(db) >= PythonVersion::PY310 {
kw_only.map(|kw_only| !kw_only.bool(db).is_always_false())
} else {
None
};

// `typeshed` pretends that `dataclasses.field()` returns the type of the
// default value directly. At runtime, however, this function returns an
// instance of `dataclasses.Field`. We also model it this way and return
// a known-instance type with information about the field. The drawback
// of this approach is that we need to pretend that instances of `Field`
// are assignable to `T` if the default type of the field is assignable
// to `T`. Otherwise, we would error on `name: str = field(default="")`.
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::Field(FieldInstance::new(
db, default_ty, init, kw_only,
)),
));
}
// `typeshed` pretends that `dataclasses.field()` returns the type of the
// default value directly. At runtime, however, this function returns an
// instance of `dataclasses.Field`. We also model it this way and return
// a known-instance type with information about the field. The drawback
// of this approach is that we need to pretend that instances of `Field`
// are assignable to `T` if the default type of the field is assignable
// to `T`. Otherwise, we would error on `name: str = field(default="")`.
overload.set_return_type(Type::KnownInstance(
KnownInstanceType::Field(FieldInstance::new(
db, default_ty, init, kw_only,
)),
));
}

_ => {
Expand Down Expand Up @@ -2782,6 +2785,9 @@ impl<'db> MatchedArgument<'db> {
}
}

/// Indicates that a parameter of the given name was not found.
pub(crate) struct UnknownParameterNameError;

/// Binding information for one of the overloads of a callable.
#[derive(Debug)]
pub(crate) struct Binding<'db> {
Expand Down Expand Up @@ -2919,6 +2925,25 @@ impl<'db> Binding<'db> {
&self.parameter_tys
}

/// Returns the bound type for the specified parameter, or `None` if no argument was matched to
/// that parameter.
///
/// Returns an error if the parameter name is not found.
pub(crate) fn parameter_type_by_name(
&self,
parameter_name: &str,
) -> Result<Option<Type<'db>>, UnknownParameterNameError> {
let (index, _) = self
.signature
.parameters()
.iter()
.enumerate()
.find(|(_, param)| param.name().is_some_and(|name| name == parameter_name))
.ok_or(UnknownParameterNameError)?;

Ok(self.parameter_tys[index])
}

pub(crate) fn arguments_for_parameter<'a>(
&'a self,
argument_types: &'a CallArguments<'a, 'db>,
Expand Down
8 changes: 0 additions & 8 deletions crates/ty_python_semantic/src/types/class.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5510,14 +5510,6 @@ mod tests {
});

for class in KnownClass::iter() {
// Until the latest supported version is bumped to Python 3.14
// we need to skip template strings here.
// The assertion below should remind the developer to
// remove this exception once we _do_ bump `latest_ty`
assert_ne!(PythonVersion::latest_ty(), PythonVersion::PY314);
if matches!(class, KnownClass::Template) {
continue;
}
assert_ne!(
class.to_instance(&db),
Type::unknown(),
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_test/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ pub(crate) struct Environment {
///
/// By default, the Python version is inferred as the lower bound of the project's
/// `requires-python` field from the `pyproject.toml`, if available. Otherwise, the latest
/// stable version supported by ty is used, which is currently 3.13.
/// stable version supported by ty is used (see `ty check --help` output).
///
/// ty will not infer the Python version from the Python environment at this time.
pub(crate) python_version: Option<PythonVersion>,
Expand Down