Files
Ruben FiszelandClaude Opus 4.6 ff70a4e9d1 fix: parse Python datetime.datetime and datetime.date type annotations (#7856)
* fix: parse Python datetime.datetime and datetime.date type annotations correctly

The Python parser only matched ExprKind::Name for type annotations, so
`datetime.datetime` (an Attribute expression) silently fell through to
Typ::Unknown and no datetime picker was shown in the UI.

- Extend parse_expr to resolve `datetime.*` attribute access (alongside
  the existing `wmill.*` handling)
- Add Typ::Date variant for `datetime.date` → JSON schema format "date"
- Update python worker to import and convert `date.fromisoformat()`
- Update argSigToJsonSchemaType, AI types, schema validation, and SQL
  datatype wasm for the new Date variant

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* all

* all

* all

* all

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 15:17:26 +00:00

365 lines
13 KiB
Rust

/*
* Author: Ruben Fiszel
* Copyright: Windmill Labs, Inc 2022
* This file and its contents are licensed under the AGPLv3 License.
* Please see the included NOTICE for copyright information and
* LICENSE-AGPL for a copy of the license.
*/
//! Pydantic BaseModel and Python dataclass detection and parsing.
//!
//! This module provides functionality to detect and parse Pydantic models and Python
//! dataclasses from Python AST, enabling automatic UI generation for complex data structures.
use rustpython_parser::ast::{
Constant, Expr, ExprAttribute, ExprCall, ExprConstant, ExprName, ExprTuple, Stmt, StmtAnnAssign,
};
use std::collections::HashSet;
use windmill_parser::{ObjectProperty, ObjectType, Typ};
// ==================================================================
// Constants
// ==================================================================
/// Maximum number of fields allowed in a Pydantic model or dataclass.
/// This prevents malicious code from defining models with thousands of fields.
const MAX_MODEL_FIELDS: usize = 200;
/// Maximum recursion depth for nested types.
const MAX_RECURSION_DEPTH: u8 = 10;
// ==================================================================
// Pydantic/Dataclass Detection
// ==================================================================
/// Detects if a class name refers to a Pydantic model or dataclass.
/// Returns ObjectType with parsed fields if detected, None otherwise.
///
/// # Arguments
/// * `class_name` - The name of the class to look up
/// * `module` - The AST statements to search in
pub fn detect_model_type(class_name: &str, module: &[Stmt]) -> Option<ObjectType> {
let mut visited = HashSet::new();
detect_model_type_impl(class_name, module, &mut visited)
}
/// Internal implementation with visited tracking
fn detect_model_type_impl(
class_name: &str,
module: &[Stmt],
visited: &mut HashSet<String>,
) -> Option<ObjectType> {
// Cycle detection: if we're already parsing this class, return a placeholder
if visited.contains(class_name) {
return Some(ObjectType {
name: Some(class_name.to_string()),
props: None, // Placeholder for self-referential types
});
}
// Find class definition in module
for stmt in module {
if let Stmt::ClassDef(class_def) = stmt {
if class_def.name.as_str() == class_name {
// Mark as being visited
visited.insert(class_name.to_string());
let result = if is_pydantic_base(&class_def.bases) {
// Pydantic BaseModel
parse_model_fields(&class_def.body, class_def.name.as_str(), module, visited)
} else if has_dataclass_decorator(&class_def.decorator_list)
|| has_pydantic_dataclass_decorator(&class_def.decorator_list)
{
// Standard dataclass or Pydantic dataclass
parse_model_fields(&class_def.body, class_def.name.as_str(), module, visited)
} else {
// Found class but it's neither Pydantic nor dataclass
None
};
// Remove from visited set after processing
visited.remove(class_name);
return result;
}
}
}
// Class not found in module
None
}
/// Checks if a class inherits from BaseModel or pydantic.BaseModel
fn is_pydantic_base(bases: &[Expr]) -> bool {
for base in bases {
match base {
// Match: class User(BaseModel)
Expr::Name(ExprName { id, .. }) if id.as_str() == "BaseModel" => {
return true;
}
// Match: class User(pydantic.BaseModel)
Expr::Attribute(ExprAttribute { attr, value, .. }) if attr.as_str() == "BaseModel" => {
if let Expr::Name(ExprName { id, .. }) = value.as_ref() {
if id.as_str() == "pydantic" {
return true;
}
}
}
_ => {}
}
}
false
}
/// Checks if a class has @dataclass decorator (standard library)
fn has_dataclass_decorator(decorators: &[Expr]) -> bool {
for decorator in decorators {
match decorator {
// Match: @dataclass
Expr::Name(ExprName { id, .. }) if id.as_str() == "dataclass" => {
return true;
}
// Match: @dataclasses.dataclass
Expr::Attribute(ExprAttribute { attr, value, .. }) if attr.as_str() == "dataclass" => {
if let Expr::Name(ExprName { id, .. }) = value.as_ref() {
if id.as_str() == "dataclasses" {
return true;
}
}
}
// Match: @dataclass() or @dataclass(frozen=True)
Expr::Call(ExprCall { func, .. }) => {
if let Expr::Name(ExprName { id, .. }) = func.as_ref() {
if id.as_str() == "dataclass" {
return true;
}
}
// Also check for @dataclasses.dataclass(...)
if let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() {
if attr.as_str() == "dataclass" {
if let Expr::Name(ExprName { id, .. }) = value.as_ref() {
if id.as_str() == "dataclasses" {
return true;
}
}
}
}
}
_ => {}
}
}
false
}
/// Checks if a class has @pydantic.dataclasses.dataclass decorator (Pydantic v2)
fn has_pydantic_dataclass_decorator(decorators: &[Expr]) -> bool {
for decorator in decorators {
match decorator {
// Match: @pydantic.dataclasses.dataclass
Expr::Attribute(ExprAttribute { attr, value, .. }) if attr.as_str() == "dataclass" => {
if let Expr::Attribute(ExprAttribute {
attr: inner_attr, value: inner_value, ..
}) = value.as_ref()
{
if inner_attr.as_str() == "dataclasses" {
if let Expr::Name(ExprName { id, .. }) = inner_value.as_ref() {
if id.as_str() == "pydantic" {
return true;
}
}
}
}
}
// Match: @pydantic.dataclasses.dataclass(...)
Expr::Call(ExprCall { func, .. }) => {
if let Expr::Attribute(ExprAttribute { attr, value, .. }) = func.as_ref() {
if attr.as_str() == "dataclass" {
if let Expr::Attribute(ExprAttribute {
attr: inner_attr,
value: inner_value,
..
}) = value.as_ref()
{
if inner_attr.as_str() == "dataclasses" {
if let Expr::Name(ExprName { id, .. }) = inner_value.as_ref() {
if id.as_str() == "pydantic" {
return true;
}
}
}
}
}
}
}
_ => {}
}
}
false
}
// ==================================================================
// Field Parsing (Unified for Pydantic and Dataclass)
// ==================================================================
/// Parses model fields from class body (works for both Pydantic and dataclass)
fn parse_model_fields(
body: &[Stmt],
class_name: &str,
module: &[Stmt],
visited: &mut HashSet<String>,
) -> Option<ObjectType> {
let mut properties = Vec::new();
for stmt in body {
// Extract annotated assignments: field_name: field_type
if let Stmt::AnnAssign(ann_assign) = stmt {
if let Some(prop) = parse_annotated_field(ann_assign, module, visited) {
if properties.len() >= MAX_MODEL_FIELDS {
eprintln!(
"Model {model} exceeds maximum field count {limit}, truncating",
model = class_name,
limit = MAX_MODEL_FIELDS.to_string(),
);
break;
}
properties.push(prop);
}
}
}
if properties.is_empty() {
// Empty model - return object with no properties
return Some(ObjectType { name: Some(class_name.to_string()), props: None });
}
Some(ObjectType { name: Some(class_name.to_string()), props: Some(properties) })
}
/// Parses a single annotated field assignment
fn parse_annotated_field(
ann_assign: &StmtAnnAssign,
module: &[Stmt],
visited: &mut HashSet<String>,
) -> Option<ObjectProperty> {
if let Expr::Name(ExprName { id: field_name, .. }) = ann_assign.target.as_ref() {
let field_type = extract_field_type(&ann_assign.annotation, 0, module, visited);
Some(ObjectProperty { key: field_name.to_string(), typ: Box::new(field_type) })
} else {
None
}
}
// ==================================================================
// Type Extraction
// ==================================================================
/// Extracts Windmill Typ from Python type annotation (RECURSIVE).
///
/// # Arguments
/// * `annotation` - The Python AST expression representing the type annotation
/// * `depth` - Current recursion depth (prevents infinite recursion)
/// * `module` - The AST statements for nested model lookup
/// * `visited` - Set of class names currently being parsed (for cycle detection)
fn extract_field_type(
annotation: &Expr,
depth: u8,
module: &[Stmt],
visited: &mut HashSet<String>,
) -> Typ {
// Prevent infinite recursion
if depth >= MAX_RECURSION_DEPTH {
eprintln!(
"Type annotation recursion limit {limit} reached, returning Unknown type",
limit = MAX_RECURSION_DEPTH,
);
return Typ::Unknown;
}
match annotation {
// Simple types: str, int, bool, float, bytes, Any
Expr::Name(ExprName { id, .. }) => match id.as_str() {
"str" => Typ::Str(None),
"int" => Typ::Int,
"float" => Typ::Float,
"bool" => Typ::Bool,
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"date" => Typ::Date,
"Any" => Typ::Unknown, // typing.Any maps to Unknown
// Custom class - check if it's a model
custom_type => {
if let Some(object_type) = detect_model_type_impl(custom_type, module, visited) {
Typ::Object(object_type)
} else {
// Unknown type - return Unknown instead of Resource
Typ::Unknown
}
}
},
// Generic types: List[T], Optional[T], Dict[K, V], Annotated[T, ...]
Expr::Subscript(subscript) => {
if let Expr::Name(ExprName { id, .. }) = subscript.value.as_ref() {
match id.as_str() {
// List[T]
"List" | "list" => {
let inner_type =
extract_field_type(&subscript.slice, depth + 1, module, visited);
Typ::List(Box::new(inner_type))
}
// Optional[T] - unwrap to T
"Optional" => extract_field_type(&subscript.slice, depth + 1, module, visited),
// Dict[K, V] - return generic Object
"Dict" | "dict" => Typ::Object(ObjectType::new(None, Some(vec![]))),
// Annotated[T, ...] - extract the first type argument (Pydantic v2)
"Annotated" => match subscript.slice.as_ref() {
Expr::Tuple(ExprTuple { elts, .. }) if !elts.is_empty() => {
extract_field_type(&elts[0], depth + 1, module, visited)
}
_ => Typ::Unknown,
},
// Set[T], Tuple[T], etc. - not supported
_ => Typ::Unknown,
}
} else {
Typ::Unknown
}
}
// Union types: str | int (Python 3.10+) or Union[str, int]
// Not fully supported - return Unknown with warning
Expr::BinOp(_) => {
eprintln!("Union types (e.g., str | int) are not yet supported, treating as Unknown");
Typ::Unknown
}
// String annotations: "ForwardRef" (forward references)
// Not fully supported - return Unknown with warning
Expr::Constant(ExprConstant { value: Constant::Str(s), .. }) => {
eprintln!(
"Forward references like \"{forward_ref}\" are not yet supported, treating as Unknown",
forward_ref = s,
);
Typ::Unknown
}
// All other annotations
_ => Typ::Unknown,
}
}