Skip to content
Draft
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
37 changes: 37 additions & 0 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/tests/cli/config_option.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ fn cli_config_args_invalid_option() -> anyhow::Result<()> {
|
1 | bad-option=true
| ^^^^^^^^^^
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `analysis`, `overrides`
unknown field `bad-option`, expected one of `environment`, `src`, `rules`, `terminal`, `analysis`, `semantics`, `overrides`


Usage: ty <COMMAND>
Expand Down
12 changes: 10 additions & 2 deletions crates/ty_project/src/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use ty_python_core::program::{
FallibleStrategy, MisconfigurationStrategy, Program, UseDefaultStrategy,
};
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{AnalysisSettings, Db as SemanticDb};
use ty_python_semantic::{AnalysisSettings, Db as SemanticDb, SemanticSettings};

mod changes;
mod ignore;
Expand Down Expand Up @@ -534,6 +534,10 @@ impl SemanticDb for ProjectDatabase {
settings.analysis(self)
}

fn semantic_settings(&self, _file: File) -> &SemanticSettings {
self.project().settings(self).semantics()
}

fn verbose(&self) -> bool {
self.project().verbose(self)
}
Expand Down Expand Up @@ -614,7 +618,7 @@ pub(crate) mod testing {
use ty_python_core::platform::PythonPlatform;
use ty_python_core::program::{FallibleStrategy, Program, ProgramSettings};
use ty_python_semantic::lint::{LintRegistry, RuleSelection};
use ty_python_semantic::{AnalysisSettings, PythonVersionWithSource};
use ty_python_semantic::{AnalysisSettings, PythonVersionWithSource, SemanticSettings};

use crate::db::Db;
use crate::{Project, ProjectMetadata};
Expand Down Expand Up @@ -763,6 +767,10 @@ pub(crate) mod testing {
self.project().settings(self).analysis()
}

fn semantic_settings(&self, _file: ruff_db::files::File) -> &SemanticSettings {
self.project().settings(self).semantics()
}

fn verbose(&self) -> bool {
false
}
Expand Down
68 changes: 65 additions & 3 deletions crates/ty_project/src/metadata/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ use ty_python_core::platform::PythonPlatform;
use ty_python_core::program::{MisconfigurationStrategy, ProgramSettings};
use ty_python_semantic::lint::{Level, LintSource, RuleSelection};
use ty_python_semantic::{
AnalysisSettings, PythonEnvironment, PythonVersionFileSource, PythonVersionSource,
PythonVersionWithSource, SitePackagesPaths, SysPrefixPathOrigin,
inferred_python_version_source_annotation,
AnalysisSettings, IsInstanceNarrowing, PythonEnvironment, PythonVersionFileSource,
PythonVersionSource, PythonVersionWithSource, SemanticSettings, SitePackagesPaths,
SysPrefixPathOrigin, inferred_python_version_source_annotation,
};
use ty_static::EnvVars;

Expand Down Expand Up @@ -100,6 +100,11 @@ pub struct Options {
#[option_group]
pub analysis: Option<AnalysisOptions>,

/// Configures semantic type inference behavior.
#[serde(skip_serializing_if = "Option::is_none")]
#[option_group]
pub semantics: Option<SemanticsOptions>,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd have expected the setting to be part of AnalysisOptions, or maybe a sub table in there. What was your reasoning for moving it into a separate section (sorry for commenting while draft, you don't have to answer right away, I just felt worth bringing this up)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So for, I spent zero time on thinking about the placement of this new option. I just wanted something available for me to play with (and write mdtests that can switch the setting). But thanks for bringing it up, will keep that in mind when we actually choose to add this option!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. I'd have to check on AnalysisOptions. I think one key question will be is whether this option can be at a per file level using overrides (I don't remember whether we allow this for some analysis options or not)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whether this option can be at a per file level

Oh, right, that is important! Pretty sure we want a semantic change like the one proposed here to apply globally, and not per-file.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mypy actually often allows these kinds of settings to be applied per-file, which can be very useful if you're looking to transition your project from very lax settings (when you've only just started using a type checker) to stricter settings, and you want to do so incrementally. A strategy I've used in the past to successfully add mypy --strict to a project is to start with mypy's strict-optional setting being disabled for several files, and then gradually work down the list of files that have it disabled in following PRs.

@sharkdp sharkdp Jun 26, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking was that this could lead to inconsistencies? Now that I think about it more, I'm not sure how that would look exactly for a setting like this, but I imagine it could be strange that importing symbol A, and then narrowing it with isinstance(...) might lead to a different result than importing A*, a version of A that was narrowed from A in the exporting module (which seems unlikely, but is possible).


/// Override configurations for specific file patterns.
///
/// Each override specifies include/exclude patterns and rule configurations
Expand Down Expand Up @@ -490,6 +495,8 @@ impl Options {
};
let analysis = strategy.fallback(analysis_result, |_| AnalysisSettings::default())?;

let semantics = self.semantics.or_default().to_settings();

let overrides = self
.to_overrides_settings(db, project_root, &mut diagnostics)
.map_err(|err| ToSettingsError {
Expand All @@ -504,6 +511,7 @@ impl Options {
terminal,
src,
analysis,
semantics,
overrides,
};

Expand Down Expand Up @@ -1583,6 +1591,60 @@ impl AnalysisOptions {
}
}

#[derive(
Debug,
Default,
Clone,
Eq,
PartialEq,
Serialize,
Deserialize,
OptionsMetadata,
get_size2::GetSize,
)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SemanticsOptions {
/// Controls how ty narrows to unspecialized generic classes in `isinstance()` checks.
///
/// With `strict`, ty narrows to the top materialization of the class. For example,
/// `isinstance(value, list)` narrows an `object` value to `Top[list[Unknown]]`, representing
/// a list with any possible specialization.
///
/// With `relaxed`, ty narrows to the class's default specialization instead. The same check
/// narrows an `object` value to `list[Unknown]`.
///
/// Defaults to `strict`.
#[option(
default = "strict",
value_type = "strict | relaxed",
example = r#"
isinstance-narrowing = "relaxed"
"#
)]
pub isinstance_narrowing: Option<RangedValue<IsInstanceNarrowing>>,
}

impl SemanticsOptions {
fn to_settings(&self) -> SemanticSettings {
SemanticSettings {
isinstance_narrowing: self
.isinstance_narrowing
.as_deref()
.copied()
.unwrap_or_default(),
}
}
}

impl Combine for SemanticsOptions {
fn combine_with(&mut self, other: Self) {
if self.isinstance_narrowing.is_none() {
self.isinstance_narrowing = other.isinstance_narrowing;
}
}
}

fn build_module_glob_set(
db: &dyn Db,
patterns: &[RangedValue<String>],
Expand Down
7 changes: 6 additions & 1 deletion crates/ty_project/src/metadata/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ use std::sync::Arc;

use ruff_db::files::File;
use ty_combine::Combine;
use ty_python_semantic::AnalysisSettings;
use ty_python_semantic::lint::RuleSelection;
use ty_python_semantic::{AnalysisSettings, SemanticSettings};

use crate::metadata::options::{InnerOverrideOptions, OutputFormat};
use crate::{Db, glob::IncludeExcludeFilter};
Expand All @@ -27,6 +27,7 @@ pub struct Settings {
pub(super) terminal: TerminalSettings,
pub(super) src: SrcSettings,
pub(super) analysis: AnalysisSettings,
pub(super) semantics: SemanticSettings,

/// Settings for configuration overrides that apply to specific file patterns.
///
Expand Down Expand Up @@ -60,6 +61,10 @@ impl Settings {
pub fn analysis(&self) -> &AnalysisSettings {
&self.analysis
}

pub fn semantics(&self) -> &SemanticSettings {
&self.semantics
}
}

#[derive(Debug, Clone, PartialEq, Eq, get_size2::GetSize)]
Expand Down
Loading
Loading