Skip to content
Merged
588 changes: 328 additions & 260 deletions crates/ty_python_core/src/builder.rs

Large diffs are not rendered by default.

51 changes: 51 additions & 0 deletions crates/ty_python_core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -972,6 +972,27 @@ impl Truthiness {
if condition { self.negate() } else { self }
}

#[must_use]
pub fn and(self, other: Self) -> Self {
match self {
Truthiness::AlwaysTrue => other,
Truthiness::AlwaysFalse => self,
Truthiness::Ambiguous => match other {
Truthiness::AlwaysFalse => Truthiness::AlwaysFalse,
Truthiness::AlwaysTrue | Truthiness::Ambiguous => Truthiness::Ambiguous,
},
}
}

/// Like [`Truthiness::and`], but evaluates `other` only when `self` may be true.
#[must_use]
pub fn and_else(self, other: impl FnOnce() -> Self) -> Self {

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 think the normal convention would be to call this .and_then(), similar to how Option has both .or_else() which only calls the closure if self is None and .and_then() which only does work when self is Some

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.

Ah shoot, makes sense, I'll push a follow-up

match self {
Truthiness::AlwaysFalse => self,
Truthiness::AlwaysTrue | Truthiness::Ambiguous => self.and(other()),
}
}

#[must_use]
pub fn or(self, other: Self) -> Self {
match self {
Expand Down Expand Up @@ -1078,6 +1099,7 @@ mod tests {
use ruff_python_ast as ast;
use ruff_text_size::{Ranged, TextRange};

use super::Truthiness::{AlwaysFalse, AlwaysTrue, Ambiguous};
use super::*;

use crate::{
Expand Down Expand Up @@ -1137,6 +1159,35 @@ mod tests {
.collect()
}

#[test]
fn truthiness_and() {
for (left, right, expected) in [
(AlwaysTrue, AlwaysTrue, AlwaysTrue),
(AlwaysTrue, AlwaysFalse, AlwaysFalse),
(AlwaysTrue, Ambiguous, Ambiguous),
(AlwaysFalse, AlwaysTrue, AlwaysFalse),
(AlwaysFalse, AlwaysFalse, AlwaysFalse),
(AlwaysFalse, Ambiguous, AlwaysFalse),
(Ambiguous, AlwaysTrue, Ambiguous),
(Ambiguous, AlwaysFalse, AlwaysFalse),
(Ambiguous, Ambiguous, Ambiguous),
] {
assert_eq!(left.and(right), expected, "{left:?}.and({right:?})");

let mut calls = 0;
let lazy_result = left.and_else(|| {
calls += 1;
right
});
assert_eq!(lazy_result, expected, "{left:?}.and_else(|| {right:?})");
assert_eq!(
calls,
usize::from(left != AlwaysFalse),
"{left:?}.and_else call count"
);
}
}

#[test]
fn empty() {
let TestCase { db, file } = test_case("");
Expand Down
9 changes: 9 additions & 0 deletions crates/ty_python_core/src/predicate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,16 @@ pub struct CallableAndCallExpr<'db> {

#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, get_size2::GetSize, salsa::SalsaValue)]
pub enum PredicateNode<'db> {
/// The truthiness of an expression's resulting value.
Expression(Expression<'db>),
/// A boolean operation, `not`, or conditional expression evaluated directly as a condition.
///
/// In `if x and False`, the truthy branch is unreachable. But after `y = x and False`,
/// `if y` may be truthy: it can call `x.__bool__` a second time and get a different result.
Condition(Expression<'db>),
/// A chained comparison evaluated directly as a condition. Its inferred truthiness is
/// available without walking the expression again.
ChainedComparisonCondition(Expression<'db>),
/// Whether a context manager's exit return type allows an exception to be suppressed.
///
/// Resolved during type inference because the context manager's type is unavailable during
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,15 @@ def convert(value: "Annotated[str, dict(**{'name': 'value'})]") -> "Annotated[in
return 1
```

Conditional expressions are also valid metadata and do not affect the annotated type.

```py
def flag() -> bool:
return True

conditional_value: "Annotated[int, 1 if flag() else 2]" = 1
```

## Inside `type[...]`

`Annotated` can wrap a class or specialized generic class inside `type[...]` without changing the
Expand Down
Loading
Loading