[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.
This commit is contained in:
David Peter 2025-10-08 11:38:47 +02:00 committed by GitHub
parent abbbe8f3af
commit 1f1542db51
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 117 additions and 50 deletions

View file

@ -68,7 +68,7 @@ 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
Self::PY314
}
pub const fn as_tuple(self) -> (u8, u8) {

2
crates/ty/docs/cli.md generated
View file

@ -76,7 +76,7 @@ over all configuration files.</p>
<p>This is used to specialize the type of <code>sys.platform</code> and will affect the visibility of platform-specific functions and attributes. If the value is set to <code>all</code>, no assumptions are made about the target platform. If unspecified, the current system's platform will be used.</p>
</dd><dt id="ty-check--python-version"><a href="#ty-check--python-version"><code>--python-version</code></a>, <code>--target-version</code> <i>version</i></dt><dd><p>Python version to assume when resolving types.</p>
<p>The Python version affects allowed syntax, type definitions of the standard library, and type definitions of first- and third-party modules that are conditional on the Python version.</p>
<p>If a version is not specified on the command line or in a configuration file, ty will try the following techniques in order of preference to determine a value: 1. Check for the <code>project.requires-python</code> setting in a <code>pyproject.toml</code> file 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)</p>
<p>If a version is not specified on the command line or in a configuration file, ty will try the following techniques in order of preference to determine a value: 1. Check for the <code>project.requires-python</code> setting in a <code>pyproject.toml</code> file 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 (see <code>ty check --help</code> output)</p>
<p>Possible values:</p>
<ul>
<li><code>3.7</code></li>

View file

@ -133,9 +133,9 @@ For some language features, ty can also understand conditionals based on compari
with `sys.version_info`. These are commonly found in typeshed, for example,
to reflect the differing contents of the standard library across Python versions.
**Default value**: `"3.13"`
**Default value**: `"3.14"`
**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | <major>.<minor>`
**Type**: `"3.7" | "3.8" | "3.9" | "3.10" | "3.11" | "3.12" | "3.13" | "3.14" | <major>.<minor>`
**Example usage** (`pyproject.toml`):

View file

@ -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>,

View file

@ -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, ...]
@ -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
@ -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, ...]
@ -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]
@ -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

View file

@ -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"
"#

View file

@ -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

View file

@ -962,12 +962,17 @@ 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 =
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_ty), _) => default_ty,
(_, Some(default_factory_ty)) => default_factory_ty
.try_call(db, &CallArguments::none())
.map_or(Type::unknown(), |binding| binding.return_type(db)),
@ -978,9 +983,8 @@ impl<'db> Bindings<'db> {
.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
@ -999,7 +1003,6 @@ impl<'db> Bindings<'db> {
)),
));
}
}
_ => {
// Ideally, either the implementation, or exactly one of the overloads
@ -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> {
@ -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>,

View file

@ -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(),

View file

@ -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>,