Skip to content

Commit 1f1542d

Browse files
authored
[ty] Use 3.14 as the default version (#20759)
## Summary Bump the latest supported Python version of ty to 3.14 and updates some references from 3.13 to 3.14. This also fixes a bug with `dataclasses.field` on 3.14 (which adds a new keyword-only parameter to that function, breaking our previously naive matching on the parameter structure of that function). ## Test Plan A `ty check` on a file with template strings (without any further configuration) doesn't raise errors anymore.
1 parent abbbe8f commit 1f1542d

File tree

10 files changed

+117
-50
lines changed

10 files changed

+117
-50
lines changed

crates/ruff_python_ast/src/python_version.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,8 +67,8 @@ impl PythonVersion {
6767
}
6868

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

7474
pub const fn as_tuple(self) -> (u8, u8) {

crates/ty/docs/cli.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty/docs/configuration.md

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/ty/src/args.rs

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

crates/ty_ide/src/completion.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1732,6 +1732,7 @@ C.<CURSOR>
17321732
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
17331733
meta_attr :: int
17341734
mro :: bound method <class 'C'>.mro() -> list[type]
1735+
__annotate__ :: @Todo | None
17351736
__annotations__ :: dict[str, Any]
17361737
__base__ :: type | None
17371738
__bases__ :: tuple[type, ...]
@@ -1797,7 +1798,7 @@ Meta.<CURSOR>
17971798
// whether we're in release mode or not. These differences
17981799
// aren't really relevant for completion tests AFAIK, so
17991800
// just redact them. ---AG
1800-
filters => [(r"(?m)\s*__(annotations|new)__.+$", "")]},
1801+
filters => [(r"(?m)\s*__(annotations|new|annotate)__.+$", "")]},
18011802
{
18021803
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
18031804
meta_attr :: property
@@ -1908,6 +1909,7 @@ Quux.<CURSOR>
19081909
some_method :: def some_method(self) -> int
19091910
some_property :: property
19101911
some_static_method :: def some_static_method(self) -> int
1912+
__annotate__ :: @Todo | None
19111913
__annotations__ :: dict[str, Any]
19121914
__base__ :: type | None
19131915
__bases__ :: tuple[type, ...]
@@ -1970,7 +1972,7 @@ Answer.<CURSOR>
19701972
insta::with_settings!({
19711973
// See above: filter out some members which contain @Todo types that are
19721974
// rendered differently in release mode.
1973-
filters => [(r"(?m)\s*__(call|reduce_ex)__.+$", "")]},
1975+
filters => [(r"(?m)\s*__(call|reduce_ex|annotate|signature)__.+$", "")]},
19741976
{
19751977
assert_snapshot!(test.completions_without_builtins_with_types(), @r"
19761978
NO :: Literal[Answer.NO]
@@ -2020,7 +2022,6 @@ Answer.<CURSOR>
20202022
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__]
20212023
__ror__ :: bound method <class 'Answer'>.__ror__(value: Any, /) -> UnionType
20222024
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
2023-
__signature__ :: bound method <class 'Answer'>.__signature__() -> str
20242025
__sizeof__ :: def __sizeof__(self) -> int
20252026
__str__ :: def __str__(self) -> str
20262027
__subclasscheck__ :: bound method <class 'Answer'>.__subclasscheck__(subclass: type, /) -> bool

crates/ty_project/src/metadata/options.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -520,8 +520,8 @@ pub struct EnvironmentOptions {
520520
/// to reflect the differing contents of the standard library across Python versions.
521521
#[serde(skip_serializing_if = "Option::is_none")]
522522
#[option(
523-
default = r#""3.13""#,
524-
value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | <major>.<minor>"#,
523+
default = r#""3.14""#,
524+
value_type = r#""3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | "3.14" | <major>.<minor>"#,
525525
example = r#"
526526
python-version = "3.12"
527527
"#

crates/ty_python_semantic/resources/mdtest/dataclasses/dataclasses.md

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -544,6 +544,55 @@ class A:
544544
y: int
545545
```
546546

547+
### `kw_only` - Python 3.13
548+
549+
```toml
550+
[environment]
551+
python-version = "3.13"
552+
```
553+
554+
```py
555+
from dataclasses import dataclass, field
556+
557+
@dataclass
558+
class Employee:
559+
e_id: int = field(kw_only=True, default=0)
560+
name: str
561+
562+
Employee("Alice")
563+
Employee(name="Alice")
564+
Employee(name="Alice", e_id=1)
565+
Employee(e_id=1, name="Alice")
566+
Employee("Alice", e_id=1)
567+
568+
Employee("Alice", 1) # error: [too-many-positional-arguments]
569+
```
570+
571+
### `kw_only` - Python 3.14
572+
573+
```toml
574+
[environment]
575+
python-version = "3.14"
576+
```
577+
578+
```py
579+
from dataclasses import dataclass, field
580+
581+
@dataclass
582+
class Employee:
583+
# Python 3.14 introduces a new `doc` parameter for `dataclasses.field`
584+
e_id: int = field(kw_only=True, default=0, doc="Global employee ID")
585+
name: str
586+
587+
Employee("Alice")
588+
Employee(name="Alice")
589+
Employee(name="Alice", e_id=1)
590+
Employee(e_id=1, name="Alice")
591+
Employee("Alice", e_id=1)
592+
593+
Employee("Alice", 1) # error: [too-many-positional-arguments]
594+
```
595+
547596
### `slots`
548597

549598
If a dataclass is defined with `slots=True`, the `__slots__` attribute is generated as a tuple. It

crates/ty_python_semantic/src/types/call/bind.rs

Lines changed: 55 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -962,43 +962,46 @@ impl<'db> Bindings<'db> {
962962
}
963963

964964
Some(KnownFunction::Field) => {
965-
// TODO this will break on Python 3.14 -- we should match by parameter name instead
966-
if let [default, default_factory, init, .., kw_only] =
967-
overload.parameter_types()
968-
{
969-
let default_ty = match (default, default_factory) {
970-
(Some(default_ty), _) => *default_ty,
971-
(_, Some(default_factory_ty)) => default_factory_ty
972-
.try_call(db, &CallArguments::none())
973-
.map_or(Type::unknown(), |binding| binding.return_type(db)),
974-
_ => Type::unknown(),
975-
};
965+
let default =
966+
overload.parameter_type_by_name("default").unwrap_or(None);
967+
let default_factory = overload
968+
.parameter_type_by_name("default_factory")
969+
.unwrap_or(None);
970+
let init = overload.parameter_type_by_name("init").unwrap_or(None);
971+
let kw_only =
972+
overload.parameter_type_by_name("kw_only").unwrap_or(None);
973+
974+
let default_ty = match (default, default_factory) {
975+
(Some(default_ty), _) => default_ty,
976+
(_, Some(default_factory_ty)) => default_factory_ty
977+
.try_call(db, &CallArguments::none())
978+
.map_or(Type::unknown(), |binding| binding.return_type(db)),
979+
_ => Type::unknown(),
980+
};
976981

977-
let init = init
978-
.map(|init| !init.bool(db).is_always_false())
979-
.unwrap_or(true);
982+
let init = init
983+
.map(|init| !init.bool(db).is_always_false())
984+
.unwrap_or(true);
980985

981-
let kw_only = if Program::get(db).python_version(db)
982-
>= PythonVersion::PY310
983-
{
986+
let kw_only =
987+
if Program::get(db).python_version(db) >= PythonVersion::PY310 {
984988
kw_only.map(|kw_only| !kw_only.bool(db).is_always_false())
985989
} else {
986990
None
987991
};
988992

989-
// `typeshed` pretends that `dataclasses.field()` returns the type of the
990-
// default value directly. At runtime, however, this function returns an
991-
// instance of `dataclasses.Field`. We also model it this way and return
992-
// a known-instance type with information about the field. The drawback
993-
// of this approach is that we need to pretend that instances of `Field`
994-
// are assignable to `T` if the default type of the field is assignable
995-
// to `T`. Otherwise, we would error on `name: str = field(default="")`.
996-
overload.set_return_type(Type::KnownInstance(
997-
KnownInstanceType::Field(FieldInstance::new(
998-
db, default_ty, init, kw_only,
999-
)),
1000-
));
1001-
}
993+
// `typeshed` pretends that `dataclasses.field()` returns the type of the
994+
// default value directly. At runtime, however, this function returns an
995+
// instance of `dataclasses.Field`. We also model it this way and return
996+
// a known-instance type with information about the field. The drawback
997+
// of this approach is that we need to pretend that instances of `Field`
998+
// are assignable to `T` if the default type of the field is assignable
999+
// to `T`. Otherwise, we would error on `name: str = field(default="")`.
1000+
overload.set_return_type(Type::KnownInstance(
1001+
KnownInstanceType::Field(FieldInstance::new(
1002+
db, default_ty, init, kw_only,
1003+
)),
1004+
));
10021005
}
10031006

10041007
_ => {
@@ -2782,6 +2785,9 @@ impl<'db> MatchedArgument<'db> {
27822785
}
27832786
}
27842787

2788+
/// Indicates that a parameter of the given name was not found.
2789+
pub(crate) struct UnknownParameterNameError;
2790+
27852791
/// Binding information for one of the overloads of a callable.
27862792
#[derive(Debug)]
27872793
pub(crate) struct Binding<'db> {
@@ -2919,6 +2925,25 @@ impl<'db> Binding<'db> {
29192925
&self.parameter_tys
29202926
}
29212927

2928+
/// Returns the bound type for the specified parameter, or `None` if no argument was matched to
2929+
/// that parameter.
2930+
///
2931+
/// Returns an error if the parameter name is not found.
2932+
pub(crate) fn parameter_type_by_name(
2933+
&self,
2934+
parameter_name: &str,
2935+
) -> Result<Option<Type<'db>>, UnknownParameterNameError> {
2936+
let (index, _) = self
2937+
.signature
2938+
.parameters()
2939+
.iter()
2940+
.enumerate()
2941+
.find(|(_, param)| param.name().is_some_and(|name| name == parameter_name))
2942+
.ok_or(UnknownParameterNameError)?;
2943+
2944+
Ok(self.parameter_tys[index])
2945+
}
2946+
29222947
pub(crate) fn arguments_for_parameter<'a>(
29232948
&'a self,
29242949
argument_types: &'a CallArguments<'a, 'db>,

crates/ty_python_semantic/src/types/class.rs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5510,14 +5510,6 @@ mod tests {
55105510
});
55115511

55125512
for class in KnownClass::iter() {
5513-
// Until the latest supported version is bumped to Python 3.14
5514-
// we need to skip template strings here.
5515-
// The assertion below should remind the developer to
5516-
// remove this exception once we _do_ bump `latest_ty`
5517-
assert_ne!(PythonVersion::latest_ty(), PythonVersion::PY314);
5518-
if matches!(class, KnownClass::Template) {
5519-
continue;
5520-
}
55215513
assert_ne!(
55225514
class.to_instance(&db),
55235515
Type::unknown(),

crates/ty_test/src/config.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub(crate) struct Environment {
6363
///
6464
/// By default, the Python version is inferred as the lower bound of the project's
6565
/// `requires-python` field from the `pyproject.toml`, if available. Otherwise, the latest
66-
/// stable version supported by ty is used, which is currently 3.13.
66+
/// stable version supported by ty is used (see `ty check --help` output).
6767
///
6868
/// ty will not infer the Python version from the Python environment at this time.
6969
pub(crate) python_version: Option<PythonVersion>,

0 commit comments

Comments
 (0)