Compare commits
7 Commits
david/sqla
...
gankra/scr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9526fe0a5 | ||
|
|
e733a87bd7 | ||
|
|
0ab8521171 | ||
|
|
0ccd84136a | ||
|
|
3981a23ee9 | ||
|
|
385dd2770b | ||
|
|
7519f6c27b |
@@ -166,28 +166,8 @@ impl Diagnostic {
|
||||
/// Returns the primary message for this diagnostic.
|
||||
///
|
||||
/// A diagnostic always has a message, but it may be empty.
|
||||
///
|
||||
/// NOTE: At present, this routine will return the first primary
|
||||
/// annotation's message as the primary message when the main diagnostic
|
||||
/// message is empty. This is meant to facilitate an incremental migration
|
||||
/// in ty over to the new diagnostic data model. (The old data model
|
||||
/// didn't distinguish between messages on the entire diagnostic and
|
||||
/// messages attached to a particular span.)
|
||||
pub fn primary_message(&self) -> &str {
|
||||
if !self.inner.message.as_str().is_empty() {
|
||||
return self.inner.message.as_str();
|
||||
}
|
||||
// FIXME: As a special case, while we're migrating ty
|
||||
// to the new diagnostic data model, we'll look for a primary
|
||||
// message from the primary annotation. This is because most
|
||||
// ty diagnostics are created with an empty diagnostic
|
||||
// message and instead attach the message to the annotation.
|
||||
// Fixing this will require touching basically every diagnostic
|
||||
// in ty, so we do it this way for now to match the old
|
||||
// semantics. ---AG
|
||||
self.primary_annotation()
|
||||
.and_then(|ann| ann.get_message())
|
||||
.unwrap_or_default()
|
||||
self.inner.message.as_str()
|
||||
}
|
||||
|
||||
/// Introspects this diagnostic and returns what kind of "primary" message
|
||||
@@ -199,18 +179,6 @@ impl Diagnostic {
|
||||
/// contains *essential* information or context for understanding the
|
||||
/// diagnostic.
|
||||
///
|
||||
/// The reason why we don't just always return both the main diagnostic
|
||||
/// message and the primary annotation message is because this was written
|
||||
/// in the midst of an incremental migration of ty over to the new
|
||||
/// diagnostic data model. At time of writing, diagnostics were still
|
||||
/// constructed in the old model where the main diagnostic message and the
|
||||
/// primary annotation message were not distinguished from each other. So
|
||||
/// for now, we carefully return what kind of messages this diagnostic
|
||||
/// contains. In effect, if this diagnostic has a non-empty main message
|
||||
/// *and* a non-empty primary annotation message, then the diagnostic is
|
||||
/// 100% using the new diagnostic data model and we can format things
|
||||
/// appropriately.
|
||||
///
|
||||
/// The type returned implements the `std::fmt::Display` trait. In most
|
||||
/// cases, just converting it to a string (or printing it) will do what
|
||||
/// you want.
|
||||
@@ -224,11 +192,10 @@ impl Diagnostic {
|
||||
.primary_annotation()
|
||||
.and_then(|ann| ann.get_message())
|
||||
.unwrap_or_default();
|
||||
match (main.is_empty(), annotation.is_empty()) {
|
||||
(false, true) => ConciseMessage::MainDiagnostic(main),
|
||||
(true, false) => ConciseMessage::PrimaryAnnotation(annotation),
|
||||
(false, false) => ConciseMessage::Both { main, annotation },
|
||||
(true, true) => ConciseMessage::Empty,
|
||||
if annotation.is_empty() {
|
||||
ConciseMessage::MainDiagnostic(main)
|
||||
} else {
|
||||
ConciseMessage::Both { main, annotation }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -693,18 +660,6 @@ impl SubDiagnostic {
|
||||
/// contains *essential* information or context for understanding the
|
||||
/// diagnostic.
|
||||
///
|
||||
/// The reason why we don't just always return both the main diagnostic
|
||||
/// message and the primary annotation message is because this was written
|
||||
/// in the midst of an incremental migration of ty over to the new
|
||||
/// diagnostic data model. At time of writing, diagnostics were still
|
||||
/// constructed in the old model where the main diagnostic message and the
|
||||
/// primary annotation message were not distinguished from each other. So
|
||||
/// for now, we carefully return what kind of messages this diagnostic
|
||||
/// contains. In effect, if this diagnostic has a non-empty main message
|
||||
/// *and* a non-empty primary annotation message, then the diagnostic is
|
||||
/// 100% using the new diagnostic data model and we can format things
|
||||
/// appropriately.
|
||||
///
|
||||
/// The type returned implements the `std::fmt::Display` trait. In most
|
||||
/// cases, just converting it to a string (or printing it) will do what
|
||||
/// you want.
|
||||
@@ -714,11 +669,10 @@ impl SubDiagnostic {
|
||||
.primary_annotation()
|
||||
.and_then(|ann| ann.get_message())
|
||||
.unwrap_or_default();
|
||||
match (main.is_empty(), annotation.is_empty()) {
|
||||
(false, true) => ConciseMessage::MainDiagnostic(main),
|
||||
(true, false) => ConciseMessage::PrimaryAnnotation(annotation),
|
||||
(false, false) => ConciseMessage::Both { main, annotation },
|
||||
(true, true) => ConciseMessage::Empty,
|
||||
if annotation.is_empty() {
|
||||
ConciseMessage::MainDiagnostic(main)
|
||||
} else {
|
||||
ConciseMessage::Both { main, annotation }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1512,28 +1466,10 @@ pub enum DiagnosticFormat {
|
||||
pub enum ConciseMessage<'a> {
|
||||
/// A diagnostic contains a non-empty main message and an empty
|
||||
/// primary annotation message.
|
||||
///
|
||||
/// This strongly suggests that the diagnostic is using the
|
||||
/// "new" data model.
|
||||
MainDiagnostic(&'a str),
|
||||
/// A diagnostic contains an empty main message and a non-empty
|
||||
/// primary annotation message.
|
||||
///
|
||||
/// This strongly suggests that the diagnostic is using the
|
||||
/// "old" data model.
|
||||
PrimaryAnnotation(&'a str),
|
||||
/// A diagnostic contains a non-empty main message and a non-empty
|
||||
/// primary annotation message.
|
||||
///
|
||||
/// This strongly suggests that the diagnostic is using the
|
||||
/// "new" data model.
|
||||
Both { main: &'a str, annotation: &'a str },
|
||||
/// A diagnostic contains an empty main message and an empty
|
||||
/// primary annotation message.
|
||||
///
|
||||
/// This indicates that the diagnostic is probably using the old
|
||||
/// model.
|
||||
Empty,
|
||||
/// A custom concise message has been provided.
|
||||
Custom(&'a str),
|
||||
}
|
||||
@@ -1544,13 +1480,9 @@ impl std::fmt::Display for ConciseMessage<'_> {
|
||||
ConciseMessage::MainDiagnostic(main) => {
|
||||
write!(f, "{main}")
|
||||
}
|
||||
ConciseMessage::PrimaryAnnotation(annotation) => {
|
||||
write!(f, "{annotation}")
|
||||
}
|
||||
ConciseMessage::Both { main, annotation } => {
|
||||
write!(f, "{main}: {annotation}")
|
||||
}
|
||||
ConciseMessage::Empty => Ok(()),
|
||||
ConciseMessage::Custom(message) => {
|
||||
write!(f, "{message}")
|
||||
}
|
||||
|
||||
@@ -12,11 +12,11 @@ static FINDER: LazyLock<Finder> = LazyLock::new(|| Finder::new(b"# /// script"))
|
||||
#[derive(Debug, Clone, Eq, PartialEq)]
|
||||
pub struct ScriptTag {
|
||||
/// The content of the script before the metadata block.
|
||||
prelude: String,
|
||||
pub prelude: String,
|
||||
/// The metadata block.
|
||||
metadata: String,
|
||||
pub metadata: String,
|
||||
/// The content of the script after the metadata block.
|
||||
postlude: String,
|
||||
pub postlude: String,
|
||||
}
|
||||
|
||||
impl ScriptTag {
|
||||
|
||||
@@ -90,6 +90,22 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
})?
|
||||
};
|
||||
|
||||
let system = OsSystem::new(&cwd);
|
||||
|
||||
// If we see a single path, check if it's a PEP-723 script
|
||||
let mut script_project = None;
|
||||
if let [path] = &*args.paths {
|
||||
match ProjectMetadata::discover_script(path, &system) {
|
||||
Ok(project) => {
|
||||
script_project = Some(project);
|
||||
}
|
||||
Err(ty_project::ProjectMetadataError::NotAScript(_)) => {
|
||||
// This is fine
|
||||
}
|
||||
Err(e) => tracing::info!("Issue reading script at `{path}`: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
let project_path = args
|
||||
.project
|
||||
.as_ref()
|
||||
@@ -111,7 +127,6 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
.map(|path| SystemPath::absolute(path, &cwd))
|
||||
.collect();
|
||||
|
||||
let system = OsSystem::new(&cwd);
|
||||
let watch = args.watch;
|
||||
let exit_zero = args.exit_zero;
|
||||
let config_file = args
|
||||
@@ -121,7 +136,13 @@ fn run_check(args: CheckCommand) -> anyhow::Result<ExitStatus> {
|
||||
|
||||
let mut project_metadata = match &config_file {
|
||||
Some(config_file) => ProjectMetadata::from_config_file(config_file.clone(), &system)?,
|
||||
None => ProjectMetadata::discover(&project_path, &system)?,
|
||||
None => {
|
||||
if let Some(project) = script_project {
|
||||
project
|
||||
} else {
|
||||
ProjectMetadata::discover(&project_path, &system)?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
project_metadata.apply_configuration_files(&system)?;
|
||||
|
||||
@@ -43,7 +43,7 @@ fn config_override_python_version() -> anyhow::Result<()> {
|
||||
|
|
||||
2 | [tool.ty.environment]
|
||||
3 | python-version = "3.11"
|
||||
| ^^^^^^ Python 3.11 assumed due to this configuration setting
|
||||
| ^^^^^^ Python version configuration
|
||||
|
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
|
||||
@@ -143,7 +143,7 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any
|
||||
),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r#"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -159,14 +159,14 @@ fn config_file_annotation_showing_where_python_version_set_typing_error() -> any
|
||||
|
|
||||
2 | [tool.ty.environment]
|
||||
3 | python-version = "3.8"
|
||||
| ^^^^^ Python 3.8 assumed due to this configuration setting
|
||||
| ^^^^^ Python version configuration
|
||||
|
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
"#);
|
||||
|
||||
assert_cmd_snapshot!(case.command().arg("--python-version=3.9"), @r###"
|
||||
success: false
|
||||
@@ -772,7 +772,7 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu
|
||||
("test.py", "aiter"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -787,7 +787,7 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu
|
||||
--> venv/pyvenv.cfg:2:11
|
||||
|
|
||||
2 | version = 3.8
|
||||
| ^^^ Python version inferred from virtual environment metadata file
|
||||
| ^^^ Virtual environment metadata
|
||||
3 | home = foo/bar/bin
|
||||
|
|
||||
info: No Python version was specified on the command line or in a configuration file
|
||||
@@ -796,7 +796,7 @@ fn pyvenv_cfg_file_annotation_showing_where_python_version_set() -> anyhow::Resu
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -831,7 +831,7 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
|
||||
("test.py", "aiter"),
|
||||
])?;
|
||||
|
||||
assert_cmd_snapshot!(case.command(), @r###"
|
||||
assert_cmd_snapshot!(case.command(), @r"
|
||||
success: false
|
||||
exit_code: 1
|
||||
----- stdout -----
|
||||
@@ -846,7 +846,7 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
|
||||
--> venv/pyvenv.cfg:4:23
|
||||
|
|
||||
4 | version = 3.8
|
||||
| ^^^ Python version inferred from virtual environment metadata file
|
||||
| ^^^ Virtual environment metadata
|
||||
|
|
||||
info: No Python version was specified on the command line or in a configuration file
|
||||
info: rule `unresolved-reference` is enabled by default
|
||||
@@ -854,7 +854,7 @@ fn pyvenv_cfg_file_annotation_no_trailing_newline() -> anyhow::Result<()> {
|
||||
Found 1 diagnostic
|
||||
|
||||
----- stderr -----
|
||||
"###);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -898,7 +898,7 @@ fn config_file_annotation_showing_where_python_version_set_syntax_error() -> any
|
||||
|
|
||||
2 | [project]
|
||||
3 | requires-python = ">=3.8"
|
||||
| ^^^^^^^ Python 3.8 assumed due to this configuration setting
|
||||
| ^^^^^^^ Python version configuration
|
||||
|
|
||||
|
||||
Found 1 diagnostic
|
||||
@@ -1206,7 +1206,7 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
|
|
||||
2 | [environment]
|
||||
3 | python-version = "3.10"
|
||||
| ^^^^^^ Python 3.10 assumed due to this configuration setting
|
||||
| ^^^^^^ Python version configuration
|
||||
4 | python-platform = "linux"
|
||||
|
|
||||
info: rule `unresolved-attribute` is enabled by default
|
||||
@@ -1225,7 +1225,7 @@ fn defaults_to_a_new_python_version() -> anyhow::Result<()> {
|
||||
|
|
||||
2 | [environment]
|
||||
3 | python-version = "3.10"
|
||||
| ^^^^^^ Python 3.10 assumed due to this configuration setting
|
||||
| ^^^^^^ Python version configuration
|
||||
4 | python-platform = "linux"
|
||||
|
|
||||
info: rule `unresolved-import` is enabled by default
|
||||
|
||||
@@ -19,11 +19,22 @@ pub struct InlayHint {
|
||||
}
|
||||
|
||||
impl InlayHint {
|
||||
fn variable_type(expr: &Expr, ty: Type, db: &dyn Db, allow_edits: bool) -> Self {
|
||||
fn variable_type(
|
||||
expr: &Expr,
|
||||
rhs: &Expr,
|
||||
ty: Type,
|
||||
db: &dyn Db,
|
||||
allow_edits: bool,
|
||||
) -> Option<Self> {
|
||||
let position = expr.range().end();
|
||||
// Render the type to a string, and get subspans for all the types that make it up
|
||||
let details = ty.display(db).to_string_parts();
|
||||
|
||||
// Filter out a reptitive hints like `x: T = T()`
|
||||
if call_matches_name(rhs, &details.label) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Ok so the idea here is that we potentially have a random soup of spans here,
|
||||
// and each byte of the string can have at most one target associate with it.
|
||||
// Thankfully, they were generally pushed in print order, with the inner smaller types
|
||||
@@ -73,12 +84,12 @@ impl InlayHint {
|
||||
vec![]
|
||||
};
|
||||
|
||||
Self {
|
||||
Some(Self {
|
||||
position,
|
||||
kind: InlayHintKind::Type,
|
||||
label: InlayHintLabel { parts: label_parts },
|
||||
text_edits,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn call_argument_name(
|
||||
@@ -250,7 +261,7 @@ struct InlayHintVisitor<'a, 'db> {
|
||||
db: &'db dyn Db,
|
||||
model: SemanticModel<'db>,
|
||||
hints: Vec<InlayHint>,
|
||||
in_assignment: bool,
|
||||
assignment_rhs: Option<&'a Expr>,
|
||||
range: TextRange,
|
||||
settings: &'a InlayHintSettings,
|
||||
in_no_edits_allowed: bool,
|
||||
@@ -262,21 +273,21 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
|
||||
db,
|
||||
model: SemanticModel::new(db, file),
|
||||
hints: Vec::new(),
|
||||
in_assignment: false,
|
||||
assignment_rhs: None,
|
||||
range,
|
||||
settings,
|
||||
in_no_edits_allowed: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn add_type_hint(&mut self, expr: &Expr, ty: Type<'db>, allow_edits: bool) {
|
||||
fn add_type_hint(&mut self, expr: &Expr, rhs: &Expr, ty: Type<'db>, allow_edits: bool) {
|
||||
if !self.settings.variable_types {
|
||||
return;
|
||||
}
|
||||
|
||||
let inlay_hint = InlayHint::variable_type(expr, ty, self.db, allow_edits);
|
||||
|
||||
self.hints.push(inlay_hint);
|
||||
if let Some(inlay_hint) = InlayHint::variable_type(expr, rhs, ty, self.db, allow_edits) {
|
||||
self.hints.push(inlay_hint);
|
||||
}
|
||||
}
|
||||
|
||||
fn add_call_argument_name(
|
||||
@@ -299,8 +310,8 @@ impl<'a, 'db> InlayHintVisitor<'a, 'db> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'_>) -> TraversalSignal {
|
||||
impl<'a> SourceOrderVisitor<'a> for InlayHintVisitor<'a, '_> {
|
||||
fn enter_node(&mut self, node: AnyNodeRef<'a>) -> TraversalSignal {
|
||||
if self.range.intersect(node.range()).is_some() {
|
||||
TraversalSignal::Traverse
|
||||
} else {
|
||||
@@ -308,7 +319,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||
}
|
||||
}
|
||||
|
||||
fn visit_stmt(&mut self, stmt: &Stmt) {
|
||||
fn visit_stmt(&mut self, stmt: &'a Stmt) {
|
||||
let node = AnyNodeRef::from(stmt);
|
||||
|
||||
if !self.enter_node(node).is_traverse() {
|
||||
@@ -317,7 +328,9 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||
|
||||
match stmt {
|
||||
Stmt::Assign(assign) => {
|
||||
self.in_assignment = !type_hint_is_excessive_for_expr(&assign.value);
|
||||
if !type_hint_is_excessive_for_expr(&assign.value) {
|
||||
self.assignment_rhs = Some(&*assign.value);
|
||||
}
|
||||
if !annotations_are_valid_syntax(assign) {
|
||||
self.in_no_edits_allowed = true;
|
||||
}
|
||||
@@ -325,7 +338,7 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||
self.visit_expr(target);
|
||||
}
|
||||
self.in_no_edits_allowed = false;
|
||||
self.in_assignment = false;
|
||||
self.assignment_rhs = None;
|
||||
|
||||
self.visit_expr(&assign.value);
|
||||
|
||||
@@ -344,22 +357,22 @@ impl SourceOrderVisitor<'_> for InlayHintVisitor<'_, '_> {
|
||||
source_order::walk_stmt(self, stmt);
|
||||
}
|
||||
|
||||
fn visit_expr(&mut self, expr: &'_ Expr) {
|
||||
fn visit_expr(&mut self, expr: &'a Expr) {
|
||||
match expr {
|
||||
Expr::Name(name) => {
|
||||
if self.in_assignment {
|
||||
if let Some(rhs) = self.assignment_rhs {
|
||||
if name.ctx.is_store() {
|
||||
let ty = expr.inferred_type(&self.model);
|
||||
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
|
||||
self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed);
|
||||
}
|
||||
}
|
||||
source_order::walk_expr(self, expr);
|
||||
}
|
||||
Expr::Attribute(attribute) => {
|
||||
if self.in_assignment {
|
||||
if let Some(rhs) = self.assignment_rhs {
|
||||
if attribute.ctx.is_store() {
|
||||
let ty = expr.inferred_type(&self.model);
|
||||
self.add_type_hint(expr, ty, !self.in_no_edits_allowed);
|
||||
self.add_type_hint(expr, rhs, ty, !self.in_no_edits_allowed);
|
||||
}
|
||||
}
|
||||
source_order::walk_expr(self, expr);
|
||||
@@ -416,6 +429,26 @@ fn arg_matches_name(arg_or_keyword: &ArgOrKeyword, name: &str) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
/// Given a function call, check if the expression is the "same name"
|
||||
/// as the function being called.
|
||||
///
|
||||
/// This allows us to filter out reptitive inlay hints like `x: T = T(...)`.
|
||||
/// While still allowing non-trivial ones like `x: T[U] = T()`.
|
||||
fn call_matches_name(expr: &Expr, name: &str) -> bool {
|
||||
// Only care about function calls
|
||||
let Expr::Call(call) = expr else {
|
||||
return false;
|
||||
};
|
||||
|
||||
match &*call.func {
|
||||
// `x: T = T()` is a match
|
||||
Expr::Name(expr_name) => expr_name.id.as_str() == name,
|
||||
// `x: T = a.T()` is a match
|
||||
Expr::Attribute(expr_attribute) => expr_attribute.attr.as_str() == name,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Given an expression that's the RHS of an assignment, would it be excessive to
|
||||
/// emit an inlay type hint for the variable assigned to it?
|
||||
///
|
||||
@@ -1829,35 +1862,16 @@ mod tests {
|
||||
",
|
||||
);
|
||||
|
||||
assert_snapshot!(test.inlay_hints(), @r#"
|
||||
assert_snapshot!(test.inlay_hints(), @r"
|
||||
class A:
|
||||
def __init__(self, y):
|
||||
self.x[: int] = int(1)
|
||||
self.x = int(1)
|
||||
self.y[: Unknown] = y
|
||||
|
||||
a[: A] = A([y=]2)
|
||||
a.y[: int] = int(3)
|
||||
a = A([y=]2)
|
||||
a.y = int(3)
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/builtins.pyi:348:7
|
||||
|
|
||||
347 | @disjoint_base
|
||||
348 | class int:
|
||||
| ^^^
|
||||
349 | """int([x]) -> integer
|
||||
350 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:4:18
|
||||
|
|
||||
2 | class A:
|
||||
3 | def __init__(self, y):
|
||||
4 | self.x[: int] = int(1)
|
||||
| ^^^
|
||||
5 | self.y[: Unknown] = y
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/ty_extensions.pyi:20:1
|
||||
|
|
||||
@@ -1871,29 +1885,11 @@ mod tests {
|
||||
--> main2.py:5:18
|
||||
|
|
||||
3 | def __init__(self, y):
|
||||
4 | self.x[: int] = int(1)
|
||||
4 | self.x = int(1)
|
||||
5 | self.y[: Unknown] = y
|
||||
| ^^^^^^^
|
||||
6 |
|
||||
7 | a[: A] = A([y=]2)
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class A:
|
||||
| ^
|
||||
3 | def __init__(self, y):
|
||||
4 | self.x = int(1)
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:7:5
|
||||
|
|
||||
5 | self.y[: Unknown] = y
|
||||
6 |
|
||||
7 | a[: A] = A([y=]2)
|
||||
| ^
|
||||
8 | a.y[: int] = int(3)
|
||||
7 | a = A([y=]2)
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
@@ -1906,30 +1902,13 @@ mod tests {
|
||||
5 | self.y = y
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:7:13
|
||||
--> main2.py:7:8
|
||||
|
|
||||
5 | self.y[: Unknown] = y
|
||||
6 |
|
||||
7 | a[: A] = A([y=]2)
|
||||
| ^
|
||||
8 | a.y[: int] = int(3)
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/builtins.pyi:348:7
|
||||
|
|
||||
347 | @disjoint_base
|
||||
348 | class int:
|
||||
| ^^^
|
||||
349 | """int([x]) -> integer
|
||||
350 | int(x, base=10) -> integer
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:8:7
|
||||
|
|
||||
7 | a[: A] = A([y=]2)
|
||||
8 | a.y[: int] = int(3)
|
||||
| ^^^
|
||||
7 | a = A([y=]2)
|
||||
| ^
|
||||
8 | a.y = int(3)
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
@@ -1938,12 +1917,12 @@ mod tests {
|
||||
|
||||
class A:
|
||||
def __init__(self, y):
|
||||
self.x: int = int(1)
|
||||
self.x = int(1)
|
||||
self.y: Unknown = y
|
||||
|
||||
a: A = A(2)
|
||||
a.y: int = int(3)
|
||||
"#);
|
||||
a = A(2)
|
||||
a.y = int(3)
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -2937,31 +2916,12 @@ mod tests {
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
|
||||
x[: MyClass] = MyClass()
|
||||
x = MyClass()
|
||||
y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
|
||||
a[: MyClass], b[: MyClass] = MyClass(), MyClass()
|
||||
c[: MyClass], d[: MyClass] = (MyClass(), MyClass())
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class MyClass:
|
||||
| ^^^^^^^
|
||||
3 | def __init__(self):
|
||||
4 | self.x: int = 1
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:6:5
|
||||
|
|
||||
4 | self.x: int = 1
|
||||
5 |
|
||||
6 | x[: MyClass] = MyClass()
|
||||
| ^^^^^^^
|
||||
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
|
||||
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> stdlib/builtins.pyi:2695:7
|
||||
|
|
||||
@@ -2973,7 +2933,7 @@ mod tests {
|
||||
info: Source
|
||||
--> main2.py:7:5
|
||||
|
|
||||
6 | x[: MyClass] = MyClass()
|
||||
6 | x = MyClass()
|
||||
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
|
||||
| ^^^^^
|
||||
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
|
||||
@@ -2991,7 +2951,7 @@ mod tests {
|
||||
info: Source
|
||||
--> main2.py:7:11
|
||||
|
|
||||
6 | x[: MyClass] = MyClass()
|
||||
6 | x = MyClass()
|
||||
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
|
||||
| ^^^^^^^
|
||||
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
|
||||
@@ -3009,7 +2969,7 @@ mod tests {
|
||||
info: Source
|
||||
--> main2.py:7:20
|
||||
|
|
||||
6 | x[: MyClass] = MyClass()
|
||||
6 | x = MyClass()
|
||||
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
|
||||
| ^^^^^^^
|
||||
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
|
||||
@@ -3027,7 +2987,7 @@ mod tests {
|
||||
info: Source
|
||||
--> main2.py:8:5
|
||||
|
|
||||
6 | x[: MyClass] = MyClass()
|
||||
6 | x = MyClass()
|
||||
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
|
||||
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
|
||||
| ^^^^^^^
|
||||
@@ -3045,7 +3005,7 @@ mod tests {
|
||||
info: Source
|
||||
--> main2.py:8:19
|
||||
|
|
||||
6 | x[: MyClass] = MyClass()
|
||||
6 | x = MyClass()
|
||||
7 | y[: tuple[MyClass, MyClass]] = (MyClass(), MyClass())
|
||||
8 | a[: MyClass], b[: MyClass] = MyClass(), MyClass()
|
||||
| ^^^^^^^
|
||||
@@ -3094,7 +3054,7 @@ mod tests {
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
|
||||
x: MyClass = MyClass()
|
||||
x = MyClass()
|
||||
y: tuple[MyClass, MyClass] = (MyClass(), MyClass())
|
||||
a, b = MyClass(), MyClass()
|
||||
c, d = (MyClass(), MyClass())
|
||||
@@ -4097,31 +4057,11 @@ mod tests {
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
self.y: int = 2
|
||||
val[: MyClass] = MyClass()
|
||||
val = MyClass()
|
||||
|
||||
foo(val.x)
|
||||
foo([x=]val.y)
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | def foo(x: int): pass
|
||||
3 | class MyClass:
|
||||
| ^^^^^^^
|
||||
4 | def __init__(self):
|
||||
5 | self.x: int = 1
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:7:7
|
||||
|
|
||||
5 | self.x: int = 1
|
||||
6 | self.y: int = 2
|
||||
7 | val[: MyClass] = MyClass()
|
||||
| ^^^^^^^
|
||||
8 |
|
||||
9 | foo(val.x)
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:2:9
|
||||
|
|
||||
@@ -4137,20 +4077,6 @@ mod tests {
|
||||
10 | foo([x=]val.y)
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def foo(x: int): pass
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
self.y: int = 2
|
||||
val: MyClass = MyClass()
|
||||
|
||||
foo(val.x)
|
||||
foo(val.y)
|
||||
");
|
||||
}
|
||||
|
||||
@@ -4176,31 +4102,11 @@ mod tests {
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
self.y: int = 2
|
||||
x[: MyClass] = MyClass()
|
||||
x = MyClass()
|
||||
|
||||
foo(x.x)
|
||||
foo([x=]x.y)
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | def foo(x: int): pass
|
||||
3 | class MyClass:
|
||||
| ^^^^^^^
|
||||
4 | def __init__(self):
|
||||
5 | self.x: int = 1
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:7:5
|
||||
|
|
||||
5 | self.x: int = 1
|
||||
6 | self.y: int = 2
|
||||
7 | x[: MyClass] = MyClass()
|
||||
| ^^^^^^^
|
||||
8 |
|
||||
9 | foo(x.x)
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:2:9
|
||||
|
|
||||
@@ -4216,20 +4122,6 @@ mod tests {
|
||||
10 | foo([x=]x.y)
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def foo(x: int): pass
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
self.x: int = 1
|
||||
self.y: int = 2
|
||||
x: MyClass = MyClass()
|
||||
|
||||
foo(x.x)
|
||||
foo(x.y)
|
||||
");
|
||||
}
|
||||
|
||||
@@ -4258,31 +4150,11 @@ mod tests {
|
||||
return 1
|
||||
def y() -> int:
|
||||
return 2
|
||||
val[: MyClass] = MyClass()
|
||||
val = MyClass()
|
||||
|
||||
foo(val.x())
|
||||
foo([x=]val.y())
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:3:7
|
||||
|
|
||||
2 | def foo(x: int): pass
|
||||
3 | class MyClass:
|
||||
| ^^^^^^^
|
||||
4 | def __init__(self):
|
||||
5 | def x() -> int:
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:9:7
|
||||
|
|
||||
7 | def y() -> int:
|
||||
8 | return 2
|
||||
9 | val[: MyClass] = MyClass()
|
||||
| ^^^^^^^
|
||||
10 |
|
||||
11 | foo(val.x())
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:2:9
|
||||
|
|
||||
@@ -4298,22 +4170,6 @@ mod tests {
|
||||
12 | foo([x=]val.y())
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
def foo(x: int): pass
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
def x() -> int:
|
||||
return 1
|
||||
def y() -> int:
|
||||
return 2
|
||||
val: MyClass = MyClass()
|
||||
|
||||
foo(val.x())
|
||||
foo(val.y())
|
||||
");
|
||||
}
|
||||
|
||||
@@ -4346,31 +4202,11 @@ mod tests {
|
||||
return 1
|
||||
def y() -> List[int]:
|
||||
return 2
|
||||
val[: MyClass] = MyClass()
|
||||
val = MyClass()
|
||||
|
||||
foo(val.x()[0])
|
||||
foo([x=]val.y()[1])
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:5:7
|
||||
|
|
||||
4 | def foo(x: int): pass
|
||||
5 | class MyClass:
|
||||
| ^^^^^^^
|
||||
6 | def __init__(self):
|
||||
7 | def x() -> List[int]:
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:11:7
|
||||
|
|
||||
9 | def y() -> List[int]:
|
||||
10 | return 2
|
||||
11 | val[: MyClass] = MyClass()
|
||||
| ^^^^^^^
|
||||
12 |
|
||||
13 | foo(val.x()[0])
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:4:9
|
||||
|
|
||||
@@ -4388,24 +4224,6 @@ mod tests {
|
||||
14 | foo([x=]val.y()[1])
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
from typing import List
|
||||
|
||||
def foo(x: int): pass
|
||||
class MyClass:
|
||||
def __init__(self):
|
||||
def x() -> List[int]:
|
||||
return 1
|
||||
def y() -> List[int]:
|
||||
return 2
|
||||
val: MyClass = MyClass()
|
||||
|
||||
foo(val.x()[0])
|
||||
foo(val.y()[1])
|
||||
");
|
||||
}
|
||||
|
||||
@@ -4697,7 +4515,7 @@ mod tests {
|
||||
class Foo:
|
||||
def __init__(self, x: int): pass
|
||||
Foo([x=]1)
|
||||
f[: Foo] = Foo([x=]1)
|
||||
f = Foo([x=]1)
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:3:24
|
||||
@@ -4715,24 +4533,7 @@ mod tests {
|
||||
3 | def __init__(self, x: int): pass
|
||||
4 | Foo([x=]1)
|
||||
| ^
|
||||
5 | f[: Foo] = Foo([x=]1)
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class Foo:
|
||||
| ^^^
|
||||
3 | def __init__(self, x: int): pass
|
||||
4 | Foo(1)
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:5:5
|
||||
|
|
||||
3 | def __init__(self, x: int): pass
|
||||
4 | Foo([x=]1)
|
||||
5 | f[: Foo] = Foo([x=]1)
|
||||
| ^^^
|
||||
5 | f = Foo([x=]1)
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
@@ -4745,22 +4546,13 @@ mod tests {
|
||||
5 | f = Foo(1)
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:5:17
|
||||
--> main2.py:5:10
|
||||
|
|
||||
3 | def __init__(self, x: int): pass
|
||||
4 | Foo([x=]1)
|
||||
5 | f[: Foo] = Foo([x=]1)
|
||||
| ^
|
||||
5 | f = Foo([x=]1)
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
class Foo:
|
||||
def __init__(self, x: int): pass
|
||||
Foo(1)
|
||||
f: Foo = Foo(1)
|
||||
");
|
||||
}
|
||||
|
||||
@@ -4778,7 +4570,7 @@ mod tests {
|
||||
class Foo:
|
||||
def __new__(cls, x: int): pass
|
||||
Foo([x=]1)
|
||||
f[: Foo] = Foo([x=]1)
|
||||
f = Foo([x=]1)
|
||||
---------------------------------------------
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:3:22
|
||||
@@ -4796,24 +4588,7 @@ mod tests {
|
||||
3 | def __new__(cls, x: int): pass
|
||||
4 | Foo([x=]1)
|
||||
| ^
|
||||
5 | f[: Foo] = Foo([x=]1)
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
--> main.py:2:7
|
||||
|
|
||||
2 | class Foo:
|
||||
| ^^^
|
||||
3 | def __new__(cls, x: int): pass
|
||||
4 | Foo(1)
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:5:5
|
||||
|
|
||||
3 | def __new__(cls, x: int): pass
|
||||
4 | Foo([x=]1)
|
||||
5 | f[: Foo] = Foo([x=]1)
|
||||
| ^^^
|
||||
5 | f = Foo([x=]1)
|
||||
|
|
||||
|
||||
info[inlay-hint-location]: Inlay Hint Target
|
||||
@@ -4826,22 +4601,13 @@ mod tests {
|
||||
5 | f = Foo(1)
|
||||
|
|
||||
info: Source
|
||||
--> main2.py:5:17
|
||||
--> main2.py:5:10
|
||||
|
|
||||
3 | def __new__(cls, x: int): pass
|
||||
4 | Foo([x=]1)
|
||||
5 | f[: Foo] = Foo([x=]1)
|
||||
| ^
|
||||
5 | f = Foo([x=]1)
|
||||
| ^
|
||||
|
|
||||
|
||||
---------------------------------------------
|
||||
info[inlay-hint-edit]: File after edits
|
||||
info: Source
|
||||
|
||||
class Foo:
|
||||
def __new__(cls, x: int): pass
|
||||
Foo(1)
|
||||
f: Foo = Foo(1)
|
||||
");
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ rustc-hash = { workspace = true }
|
||||
salsa = { workspace = true }
|
||||
schemars = { workspace = true, optional = true }
|
||||
serde = { workspace = true }
|
||||
serde_json = { workspace = true, optional = true }
|
||||
serde_json = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
toml = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
@@ -55,7 +55,6 @@ default = ["zstd"]
|
||||
deflate = ["ty_vendored/deflate"]
|
||||
schemars = [
|
||||
"dep:schemars",
|
||||
"dep:serde_json",
|
||||
"ruff_db/schemars",
|
||||
"ruff_python_ast/schemars",
|
||||
"ty_python_semantic/schemars",
|
||||
|
||||
@@ -9,13 +9,15 @@ use ty_python_semantic::ProgramSettings;
|
||||
|
||||
use crate::metadata::options::ProjectOptionsOverrides;
|
||||
use crate::metadata::pyproject::{Project, PyProject, PyProjectError, ResolveRequiresPythonError};
|
||||
use crate::metadata::value::ValueSource;
|
||||
use crate::metadata::script::{Pep723Error, Pep723Metadata};
|
||||
use crate::metadata::value::{RelativePathBuf, ValueSource};
|
||||
pub use options::Options;
|
||||
use options::TyTomlError;
|
||||
|
||||
mod configuration_file;
|
||||
pub mod options;
|
||||
pub mod pyproject;
|
||||
pub mod script;
|
||||
pub mod settings;
|
||||
pub mod value;
|
||||
|
||||
@@ -85,6 +87,32 @@ impl ProjectMetadata {
|
||||
)
|
||||
}
|
||||
|
||||
/// Loads a project from a `pyproject.toml` file.
|
||||
pub(crate) fn from_script(
|
||||
script: Pep723Metadata,
|
||||
script_path: &SystemPath,
|
||||
) -> Result<Self, ResolveRequiresPythonError> {
|
||||
let project = Some(&script.to_project());
|
||||
let parent_dir = script_path
|
||||
.parent()
|
||||
.map(ToOwned::to_owned)
|
||||
.unwrap_or_default();
|
||||
let mut metadata = Self::from_options(
|
||||
script.tool.and_then(|tool| tool.ty).unwrap_or_default(),
|
||||
parent_dir,
|
||||
project,
|
||||
)?;
|
||||
|
||||
// Try to get `uv sync --script` to setup the venv for us
|
||||
if let Some(python) = script::uv_sync_script(script_path) {
|
||||
let mut environment = metadata.options.environment.unwrap_or_default();
|
||||
environment.python = Some(RelativePathBuf::new(python, ValueSource::Cli));
|
||||
metadata.options.environment = Some(environment);
|
||||
}
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
/// Loads a project from a set of options with an optional pyproject-project table.
|
||||
pub fn from_options(
|
||||
mut options: Options,
|
||||
@@ -120,6 +148,46 @@ impl ProjectMetadata {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn discover_script(
|
||||
path: &SystemPath,
|
||||
system: &dyn System,
|
||||
) -> Result<ProjectMetadata, ProjectMetadataError> {
|
||||
tracing::debug!("Searching for a PEP-723 Script in '{path}'");
|
||||
if !system.is_file(path) {
|
||||
return Err(ProjectMetadataError::NotAScript(path.to_path_buf()));
|
||||
}
|
||||
|
||||
let script_metadata = if let Ok(script_str) = system.read_to_string(path) {
|
||||
match Pep723Metadata::from_script_str(
|
||||
script_str.as_bytes(),
|
||||
ValueSource::File(Arc::new(path.to_owned())),
|
||||
) {
|
||||
Ok(Some(pyproject)) => Some(pyproject),
|
||||
Ok(None) => None,
|
||||
Err(error) => {
|
||||
return Err(ProjectMetadataError::InvalidScript {
|
||||
path: path.to_owned(),
|
||||
source: Box::new(error),
|
||||
});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let Some(script_metadata) = script_metadata else {
|
||||
return Err(ProjectMetadataError::NotAScript(path.to_path_buf()));
|
||||
};
|
||||
|
||||
let metadata = ProjectMetadata::from_script(script_metadata, path).map_err(|err| {
|
||||
ProjectMetadataError::InvalidRequiresPythonConstraint {
|
||||
source: err,
|
||||
path: path.to_owned(),
|
||||
}
|
||||
})?;
|
||||
|
||||
Ok(metadata)
|
||||
}
|
||||
/// Discovers the closest project at `path` and returns its metadata.
|
||||
///
|
||||
/// The algorithm traverses upwards in the `path`'s ancestor chain and uses the following precedence
|
||||
@@ -319,12 +387,21 @@ pub enum ProjectMetadataError {
|
||||
#[error("project path '{0}' is not a directory")]
|
||||
NotADirectory(SystemPathBuf),
|
||||
|
||||
#[error("project path '{0}' is not a PEP-723 script")]
|
||||
NotAScript(SystemPathBuf),
|
||||
|
||||
#[error("{path} is not a valid `pyproject.toml`: {source}")]
|
||||
InvalidPyProject {
|
||||
source: Box<PyProjectError>,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
|
||||
#[error("{path} is not a valid PEP-723 script: {source}")]
|
||||
InvalidScript {
|
||||
source: Box<Pep723Error>,
|
||||
path: SystemPathBuf,
|
||||
},
|
||||
|
||||
#[error("{path} is not a valid `ty.toml`: {source}")]
|
||||
InvalidTyToml {
|
||||
source: Box<TyTomlError>,
|
||||
|
||||
157
crates/ty_project/src/metadata/script.rs
Normal file
157
crates/ty_project/src/metadata/script.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
use std::{io, process::Command, str::FromStr};
|
||||
|
||||
use camino::Utf8PathBuf;
|
||||
use pep440_rs::VersionSpecifiers;
|
||||
use ruff_db::system::{SystemPath, SystemPathBuf};
|
||||
use ruff_python_ast::script::ScriptTag;
|
||||
use serde::Deserialize;
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::metadata::{
|
||||
pyproject::{Project, Tool},
|
||||
value::{RangedValue, ValueSource, ValueSourceGuard},
|
||||
};
|
||||
|
||||
/// PEP 723 metadata as parsed from a `script` comment block.
|
||||
///
|
||||
/// See: <https://peps.python.org/pep-0723/>
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct Pep723Metadata {
|
||||
pub dependencies: Option<RangedValue<Vec<toml::Value>>>,
|
||||
pub requires_python: Option<RangedValue<VersionSpecifiers>>,
|
||||
pub tool: Option<Tool>,
|
||||
|
||||
/// The raw unserialized document.
|
||||
#[serde(skip)]
|
||||
pub raw: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum Pep723Error {
|
||||
#[error(
|
||||
"An opening tag (`# /// script`) was found without a closing tag (`# ///`). Ensure that every line between the opening and closing tags (including empty lines) starts with a leading `#`."
|
||||
)]
|
||||
UnclosedBlock,
|
||||
#[error("The PEP 723 metadata block is missing from the script.")]
|
||||
MissingTag,
|
||||
#[error(transparent)]
|
||||
Io(#[from] io::Error),
|
||||
#[error(transparent)]
|
||||
Utf8(#[from] std::str::Utf8Error),
|
||||
#[error(transparent)]
|
||||
Toml(#[from] toml::de::Error),
|
||||
#[error("Invalid filename `{0}` supplied")]
|
||||
InvalidFilename(String),
|
||||
}
|
||||
|
||||
impl Pep723Metadata {
|
||||
/// Parse the PEP 723 metadata from `stdin`.
|
||||
pub fn from_script_str(
|
||||
contents: &[u8],
|
||||
source: ValueSource,
|
||||
) -> Result<Option<Self>, Pep723Error> {
|
||||
let _guard = ValueSourceGuard::new(source, true);
|
||||
|
||||
// Extract the `script` tag.
|
||||
let Some(ScriptTag { metadata, .. }) = ScriptTag::parse(contents) else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
// Parse the metadata.
|
||||
Ok(Some(Self::from_str(&metadata)?))
|
||||
}
|
||||
|
||||
pub fn to_project(&self) -> Project {
|
||||
Project {
|
||||
name: None,
|
||||
version: None,
|
||||
requires_python: self.requires_python.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
{
|
||||
"schema": {
|
||||
"version": "preview"
|
||||
},
|
||||
"target": "script",
|
||||
"script": {
|
||||
"path": "/Users/myuser/code/myproj/scripts/load-test.py"
|
||||
},
|
||||
"sync": {
|
||||
"environment": {
|
||||
"path": "/Users/myuser/.cache/uv/environments-v2/load-test-d6edaf5bfab110a8",
|
||||
"python": {
|
||||
"path": "/Users/myuser/.cache/uv/environments-v2/load-test-d6edaf5bfab110a8/bin/python3",
|
||||
"version": "3.14.0",
|
||||
"implementation": "cpython"
|
||||
}
|
||||
},
|
||||
"action": "check"
|
||||
},
|
||||
"lock": null,
|
||||
"dry_run": false
|
||||
}
|
||||
*/
|
||||
|
||||
/// The output of `uv sync --output-format=json --script ...`
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct UvMetadata {
|
||||
sync: Option<UvSync>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct UvSync {
|
||||
environment: Option<UvEnvironment>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
struct UvEnvironment {
|
||||
path: Option<String>,
|
||||
}
|
||||
|
||||
/// Ask `uv` to sync the script's venv to some temp dir so we can analyze dependencies properly
|
||||
///
|
||||
/// Returns the path to the venv on success
|
||||
pub fn uv_sync_script(script_path: &SystemPath) -> Option<SystemPathBuf> {
|
||||
tracing::info!("Asking uv to sync the script's venv");
|
||||
let mut command = Command::new("uv");
|
||||
command
|
||||
.arg("sync")
|
||||
.arg("--output-format=json")
|
||||
.arg("--script")
|
||||
.arg(script_path.as_str());
|
||||
let output = command
|
||||
.output()
|
||||
.inspect_err(|e| {
|
||||
tracing::info!(
|
||||
"failed to run `uv sync --output-format=json --script {script_path}`: {e}"
|
||||
);
|
||||
})
|
||||
.ok()?;
|
||||
let metadata: UvMetadata = serde_json::from_slice(&output.stdout)
|
||||
.inspect_err(|e| {
|
||||
tracing::info!(
|
||||
"failed to parse `uv sync --output-format=json --script {script_path}`: {e}"
|
||||
);
|
||||
})
|
||||
.ok()?;
|
||||
let env_path = metadata.sync?.environment?.path?;
|
||||
let utf8_path = Utf8PathBuf::from(env_path);
|
||||
Some(SystemPathBuf::from_utf8_path_buf(utf8_path))
|
||||
}
|
||||
|
||||
impl FromStr for Pep723Metadata {
|
||||
type Err = toml::de::Error;
|
||||
|
||||
/// Parse `Pep723Metadata` from a raw TOML string.
|
||||
fn from_str(raw: &str) -> Result<Self, Self::Err> {
|
||||
let metadata = toml::from_str(raw)?;
|
||||
Ok(Self {
|
||||
raw: raw.to_string(),
|
||||
..metadata
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -208,147 +208,3 @@ async def test_async(session: AsyncSession):
|
||||
# TODO: should be `str`
|
||||
reveal_type(name) # revealed: Unknown
|
||||
```
|
||||
|
||||
## What is it that we do not support yet?
|
||||
|
||||
Basic setup:
|
||||
|
||||
```py
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import select, Integer, Text, Boolean, DateTime
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.orm import DeclarativeBase
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
engine = create_engine("sqlite://example.db")
|
||||
session = Session(engine)
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True)
|
||||
name: Mapped[str] = mapped_column(Text)
|
||||
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
```
|
||||
|
||||
Why do we see `Unknown`s for `select(User.id, User.name)` here?
|
||||
|
||||
```py
|
||||
stmt = select(User.id, User.name)
|
||||
# TODO: should be `Select[tuple[int, str]]`
|
||||
reveal_type(stmt) # revealed: Select[tuple[Unknown, Unknown]]
|
||||
```
|
||||
|
||||
The types of the arguments seem correct:
|
||||
|
||||
```py
|
||||
reveal_type(User.id) # revealed: InstrumentedAttribute[int]
|
||||
reveal_type(User.name) # revealed: InstrumentedAttribute[str]
|
||||
```
|
||||
|
||||
The two-parameter overload of `select` has a type of
|
||||
|
||||
`def select(__ent0: _TCCA[_T0], __ent1: _TCCA[_T1], /) -> Select[_T0, _T1]: ...`
|
||||
|
||||
here `_TCCA` is an alias for `_TypedColumnClauseArgument`:
|
||||
|
||||
```py
|
||||
from sqlalchemy.sql._typing import _TypedColumnClauseArgument
|
||||
|
||||
# revealed: <types.UnionType special form 'TypedColumnsClauseRole[_T@_TypedColumnClauseArgument] | SQLCoreOperations[_T@_TypedColumnClauseArgument] | type[_T@_TypedColumnClauseArgument]'>
|
||||
reveal_type(_TypedColumnClauseArgument)
|
||||
```
|
||||
|
||||
If we use that generic type alias in a type expression, we can properly specialize it:
|
||||
|
||||
```py
|
||||
def _(
|
||||
col: _TypedColumnClauseArgument[int],
|
||||
) -> None:
|
||||
reveal_type(col) # revealed: TypedColumnsClauseRole[int] | SQLCoreOperations[int] | type[int]
|
||||
```
|
||||
|
||||
Next, verify that we can assign `User.id` to a fully specialized version of
|
||||
`_TypedColumnClauseArgument`:
|
||||
|
||||
```py
|
||||
user_id_as_tcca: _TypedColumnClauseArgument[int] = User.id
|
||||
```
|
||||
|
||||
If we use the generic version of `_TypedColumnClauseArgument` without specialization, we get
|
||||
`Unknown`:
|
||||
|
||||
```py
|
||||
def extract_t_from_tcca[T](col: _TypedColumnClauseArgument[T]) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
reveal_type(extract_t_from_tcca(User.id)) # revealed: Unknown
|
||||
```
|
||||
|
||||
However, if we use just the relevant union element of `_TypedColumnClauseArgument`
|
||||
(`SQLCoreOperations`), it works as expected:
|
||||
|
||||
```py
|
||||
from sqlalchemy.sql.elements import SQLCoreOperations
|
||||
|
||||
def extract_t_from_sco[T](col: SQLCoreOperations[T]) -> T:
|
||||
raise NotImplementedError
|
||||
|
||||
reveal_type(extract_t_from_sco(User.id)) # revealed: int
|
||||
reveal_type(extract_t_from_sco(User.name)) # revealed: str
|
||||
```
|
||||
|
||||
I reported this as <https://github.com/astral-sh/ty/issues/1772>.
|
||||
|
||||
Now let's assume we would be able to solve for `T` here. This would mean we would get a type of
|
||||
`Select[tuple[int, str]]`. Can we use that type and proceed with it? It looks like this works:
|
||||
|
||||
```py
|
||||
from sqlalchemy.sql.selectable import Select
|
||||
|
||||
def _(stmt: Select[tuple[int, str]]) -> None:
|
||||
for row in session.execute(stmt):
|
||||
reveal_type(row) # revealed: Row[tuple[int, str]]
|
||||
```
|
||||
|
||||
What about the `_tuple` calls? This seems to work:
|
||||
|
||||
```py
|
||||
def _(stmt: Select[tuple[int, str]]) -> None:
|
||||
result = session.execute(stmt)
|
||||
|
||||
reveal_type(result) # revealed: Result[tuple[int, str]]
|
||||
|
||||
user = result.one_or_none()
|
||||
reveal_type(user) # revealed: Row[tuple[int, str]] | None
|
||||
|
||||
if not user:
|
||||
return
|
||||
|
||||
reveal_type(user) # revealed: Row[tuple[int, str]] & ~AlwaysFalsy
|
||||
reveal_type(user._tuple()) # revealed: tuple[int, str]
|
||||
```
|
||||
|
||||
What about `.tuples()`? That seems to work as well:
|
||||
|
||||
```py
|
||||
def _(stmt: Select[tuple[int, str]]) -> None:
|
||||
for user_id, name in session.execute(stmt).tuples():
|
||||
reveal_type(user_id) # revealed: int
|
||||
reveal_type(name) # revealed: str
|
||||
```
|
||||
|
||||
What about the `.scalar` calls? Those seem to work too:
|
||||
|
||||
```py
|
||||
def _(stmt: Select[tuple[int]]) -> None:
|
||||
user_id = session.scalar(stmt)
|
||||
reveal_type(user_id) # revealed: int | None
|
||||
|
||||
reveal_type(session.scalars(stmt).first()) # revealed: int | None
|
||||
```
|
||||
|
||||
@@ -128,3 +128,16 @@ InvalidEmptyUnion = Union[]
|
||||
def _(u: InvalidEmptyUnion):
|
||||
reveal_type(u) # revealed: Unknown
|
||||
```
|
||||
|
||||
### `typing.Annotated`
|
||||
|
||||
```py
|
||||
from typing import Annotated
|
||||
|
||||
# error: [invalid-syntax] "Expected index or slice expression"
|
||||
# error: [invalid-type-form] "Special form `typing.Annotated` expected at least 2 arguments (one type and at least one metadata element)"
|
||||
InvalidEmptyAnnotated = Annotated[]
|
||||
|
||||
def _(a: InvalidEmptyAnnotated):
|
||||
reveal_type(a) # revealed: Unknown
|
||||
```
|
||||
|
||||
@@ -58,9 +58,8 @@ pub fn add_inferred_python_version_hint_to_diagnostic(
|
||||
SubDiagnosticSeverity::Info,
|
||||
format_args!("Python {version} was assumed when {action}"),
|
||||
);
|
||||
sub_diagnostic.annotate(Annotation::primary(span).message(format_args!(
|
||||
"Python {version} assumed due to this configuration setting"
|
||||
)));
|
||||
sub_diagnostic
|
||||
.annotate(Annotation::primary(span).message("Python version configuration"));
|
||||
diagnostic.sub(sub_diagnostic);
|
||||
} else {
|
||||
diagnostic.info(format_args!(
|
||||
@@ -76,10 +75,8 @@ pub fn add_inferred_python_version_hint_to_diagnostic(
|
||||
"Python {version} was assumed when {action} because of your virtual environment"
|
||||
),
|
||||
);
|
||||
sub_diagnostic.annotate(
|
||||
Annotation::primary(span)
|
||||
.message("Python version inferred from virtual environment metadata file"),
|
||||
);
|
||||
sub_diagnostic
|
||||
.annotate(Annotation::primary(span).message("Virtual environment metadata"));
|
||||
// TODO: it would also be nice to tell them how we resolved their virtual environment...
|
||||
diagnostic.sub(sub_diagnostic);
|
||||
} else {
|
||||
|
||||
@@ -1172,7 +1172,11 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
||||
)
|
||||
.in_type_expression(db, self.scope(), None)
|
||||
.unwrap_or_else(|err| err.into_fallback_type(&self.context, subscript, true));
|
||||
self.store_expression_type(arguments_slice, ty);
|
||||
// Only store on the tuple slice; non-tuple cases are handled by
|
||||
// `infer_subscript_load_impl` via `infer_expression`.
|
||||
if arguments_slice.is_tuple_expr() {
|
||||
self.store_expression_type(arguments_slice, ty);
|
||||
}
|
||||
ty
|
||||
}
|
||||
SpecialFormType::Literal => match self.infer_literal_parameter_type(arguments_slice) {
|
||||
|
||||
@@ -427,10 +427,16 @@ mod tests {
|
||||
let mut diag = if self.id == DiagnosticId::RevealedType {
|
||||
Diagnostic::new(self.id, Severity::Error, "Revealed type")
|
||||
} else {
|
||||
Diagnostic::new(self.id, Severity::Error, "")
|
||||
Diagnostic::new(self.id, Severity::Error, self.message)
|
||||
};
|
||||
let span = Span::from(file).with_range(self.range);
|
||||
diag.annotate(Annotation::primary(span).message(self.message));
|
||||
let mut annotation = Annotation::primary(span);
|
||||
|
||||
if self.id == DiagnosticId::RevealedType {
|
||||
annotation = annotation.message(self.message);
|
||||
}
|
||||
|
||||
diag.annotate(annotation);
|
||||
diag
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ from collections.abc import Callable
|
||||
from dataclasses import KW_ONLY, dataclass
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import NewType, NoReturn, assert_never
|
||||
from typing import Final, NewType, NoReturn, assert_never
|
||||
|
||||
from pysource_codegen import generate as generate_random_code
|
||||
from pysource_minimize import CouldNotMinimize, minimize as minimize_repro
|
||||
@@ -44,6 +44,12 @@ MinimizedSourceCode = NewType("MinimizedSourceCode", str)
|
||||
Seed = NewType("Seed", int)
|
||||
ExitCode = NewType("ExitCode", int)
|
||||
|
||||
TY_TARGET_PLATFORM: Final = "linux"
|
||||
|
||||
# ty supports `--python-version=3.8`, but typeshed only supports 3.9+,
|
||||
# so that's probably the oldest version we can usefully test with.
|
||||
OLDEST_SUPPORTED_PYTHON: Final = "3.9"
|
||||
|
||||
|
||||
def ty_contains_bug(code: str, *, ty_executable: Path) -> bool:
|
||||
"""Return `True` if the code triggers a panic in type-checking code."""
|
||||
@@ -51,7 +57,17 @@ def ty_contains_bug(code: str, *, ty_executable: Path) -> bool:
|
||||
input_file = Path(tempdir, "input.py")
|
||||
input_file.write_text(code)
|
||||
completed_process = subprocess.run(
|
||||
[ty_executable, "check", input_file], capture_output=True, text=True
|
||||
[
|
||||
ty_executable,
|
||||
"check",
|
||||
input_file,
|
||||
"--python-version",
|
||||
OLDEST_SUPPORTED_PYTHON,
|
||||
"--python-platform",
|
||||
TY_TARGET_PLATFORM,
|
||||
],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return completed_process.returncode not in {0, 1, 2}
|
||||
|
||||
@@ -137,7 +153,10 @@ class FuzzResult:
|
||||
case Executable.RUFF:
|
||||
panic_message = f"The following code triggers a {new}parser bug:"
|
||||
case Executable.TY:
|
||||
panic_message = f"The following code triggers a {new}ty panic:"
|
||||
panic_message = (
|
||||
f"The following code triggers a {new}ty panic with "
|
||||
f"`--python-version={OLDEST_SUPPORTED_PYTHON} --python-platform={TY_TARGET_PLATFORM}`:"
|
||||
)
|
||||
case _ as unreachable:
|
||||
assert_never(unreachable)
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[dependency-groups]
|
||||
dev = ["mypy", "ruff"]
|
||||
dev = ["mypy", "ruff", "ty"]
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
include = ["fuzz.py"]
|
||||
|
||||
27
python/py-fuzzer/uv.lock
generated
27
python/py-fuzzer/uv.lock
generated
@@ -89,6 +89,7 @@ dependencies = [
|
||||
dev = [
|
||||
{ name = "mypy" },
|
||||
{ name = "ruff" },
|
||||
{ name = "ty" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
@@ -104,6 +105,7 @@ requires-dist = [
|
||||
dev = [
|
||||
{ name = "mypy" },
|
||||
{ name = "ruff" },
|
||||
{ name = "ty" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -196,6 +198,31 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/bd/de8d508070629b6d84a30d01d57e4a65c69aa7f5abe7560b8fad3b50ea59/termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa", size = 7684, upload-time = "2025-04-30T11:37:52.382Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ty"
|
||||
version = "0.0.1a32"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/26/92/8da015685fb83734a2a83de02080e64d182509de77fa9bcf3eed12eeab4b/ty-0.0.1a32.tar.gz", hash = "sha256:12f62e8a3dd0eaeb9557d74b1c32f0616ae40eae10a4f411e1e2a73225f67ff2", size = 4689151, upload-time = "2025-12-05T21:04:26.885Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/2a/e6/fdc35c9ba047f16afdfedf36fb51c221e0190ccde9f70ee28e77084d6612/ty-0.0.1a32-py3-none-linux_armv6l.whl", hash = "sha256:ffe595eaf616f06f58f951766477830a55c2502d2c9f77dde8f60d9a836e0645", size = 9673128, upload-time = "2025-12-05T21:04:17.702Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/19/20/eaff31048e2f309f37478f7d715c8de9f9bab03cba4758da27b9311147af/ty-0.0.1a32-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:07f1dce88ad6028fb14665aefe4e6697012c34bd48edd37d02b7eb6a833dbf62", size = 9434094, upload-time = "2025-12-05T21:04:03.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/67/d4/ea8ed57d11b81c459f23561fd6bfb0f54a8d4120cf72541e3bdf71d46202/ty-0.0.1a32-py3-none-macosx_11_0_arm64.whl", hash = "sha256:8fab7ed12528c77ddd600a9638ca859156a53c20f1e381353fa87a255bd397eb", size = 8980296, upload-time = "2025-12-05T21:04:28.912Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/02/3ce98bbfbb3916678d717ee69358d38a404ca9a39391dda8874b66dd5ee7/ty-0.0.1a32-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ace395280fc21e25eff0a53cfbd68170f90a4b8ef2f85dfabe1ecbca2ced456b", size = 9263054, upload-time = "2025-12-05T21:04:05.619Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/be/a639638bcd1664de2d70a87da6c4fe0e3272a60b7fa3f0c108a956a456bd/ty-0.0.1a32-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2bcbeed7f5ed8e3c1c7e525fce541e7b943ac04ee7fe369a926551b5e50ea4a8", size = 9451396, upload-time = "2025-12-05T21:04:01.265Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1f/a4/2bcf54e842a3d10dc14b369f28a3bab530c5d7ddba624e910b212bda93ee/ty-0.0.1a32-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:60ff2e4493f90f81a260205d87719bb1d3420928a1e4a2a7454af7cbdfed2047", size = 9862726, upload-time = "2025-12-05T21:04:08.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/c7/19e6719496e59f2f082f34bcac312698366cf50879fdcc3ef76298bfe6a0/ty-0.0.1a32-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:53cad50a59a0d943b06872e0b10f9f2b564805c2ea93f64c7798852bc1901954", size = 10475051, upload-time = "2025-12-05T21:04:31.059Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/88/77/bdf0ddb066d2b62f141d058f8a33bb7c8628cdbb8bfa75b20e296b79fb4e/ty-0.0.1a32-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:343d43cdc1d7f649ea2baa64ac2b479da3d679239b94509f1df12f7211561ea9", size = 10232712, upload-time = "2025-12-05T21:04:19.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/07/f73260a461762a581a007015c1019d40658828ce41576f8c1db88dee574d/ty-0.0.1a32-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f45483e4a84bcf622413712164ea687ce323a9f7013b9e7977c5d623ed937ca9", size = 10237705, upload-time = "2025-12-05T21:04:35.366Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/57/dbb92206cf2f798d8c51ea16504e8afb90a139d0ff105c31cec9a1db29f9/ty-0.0.1a32-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d452f30d47002a6bafc36d1b6aee42c321e9ec9f7f43a04a2ee7d48c208b86c", size = 9766469, upload-time = "2025-12-05T21:04:22.236Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c3/5e/143d93bd143abcebcbaa98c8aeec78898553d62d0a5a432cd79e0cf5bd6d/ty-0.0.1a32-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:86c4e31737fe954637890cef1f3e1b479ffb20e836cac3b76050bdbe80005010", size = 9238592, upload-time = "2025-12-05T21:04:11.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/b8/225230ae097ed88f3c92ad974dd77f8e4f86f2594d9cd0c729da39769878/ty-0.0.1a32-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:daf15fa03bc39a76a0fbc9c2d81d79d528f584e3fbe08d71981e3f7912db91d6", size = 9502161, upload-time = "2025-12-05T21:04:37.642Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/85/13/cc89955c9637f25f3aca2dd7749c6008639ef036f0b9bea3e9d89e892ff9/ty-0.0.1a32-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6128f6bab5c6dab3d08689fed1d529dc34f50f221f89c8e16064ed0c549dad7a", size = 9603058, upload-time = "2025-12-05T21:04:39.532Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/77/1fe2793c8065a02d1f70ca7da1b87db49ca621bcbbdb79a18ad79d5d0ab2/ty-0.0.1a32-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:55aab688be1b46776a5a458a1993cae0da7725932c45393399c479c2fa979337", size = 9879903, upload-time = "2025-12-05T21:04:13.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/47/fd58e80a3e42310b4b649340d5d97403fe796146cae8678b3a031a414b8e/ty-0.0.1a32-py3-none-win32.whl", hash = "sha256:f55ec25088a09236ad1578b656a07fa009c3a353f5923486905ba48175d142a6", size = 9077703, upload-time = "2025-12-05T21:04:15.849Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/96/209c417c69317339ea8e9b3277fd98364a0e97dd1ffd3585e143ec7b4e57/ty-0.0.1a32-py3-none-win_amd64.whl", hash = "sha256:ed8d5cbd4e47dfed86aaa27e243008aa4e82b6a5434f3ab95c26d3ee5874d9d7", size = 9922426, upload-time = "2025-12-05T21:04:33.289Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e0/1c/350fd851fb91244f8c80cec218009cbee7564d76c14e2f423b47e69a5cbc/ty-0.0.1a32-py3-none-win_arm64.whl", hash = "sha256:dbb25f9b513d34cee8ce419514eaef03313f45c3f7ab4eb6e6d427ea1f6854af", size = 9453761, upload-time = "2025-12-05T21:04:24.502Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typing-extensions"
|
||||
version = "4.15.0"
|
||||
|
||||
Reference in New Issue
Block a user