Skip to content

Implement the datetime plan with fixed offsets to start#171

Open
mwildehahn wants to merge 40 commits intopydantic:mainfrom
mwildehahn:impl/datetime-fixed-offset
Open

Implement the datetime plan with fixed offsets to start#171
mwildehahn wants to merge 40 commits intopydantic:mainfrom
mwildehahn:impl/datetime-fixed-offset

Conversation

@mwildehahn
Copy link

@mwildehahn mwildehahn commented Feb 16, 2026

Scope

This PR is intentionally kept narrow to make review tractable.

Deferred From This PR

These changes were removed from the diff against main and will land in a follow-up PR:

  • crates/monty-typeshed/vendor/typeshed/**
  • crates/monty/src/bytecode/vm/async_exec.rs
  • crates/monty/src/bytecode/vm/binary.rs
  • crates/monty/src/bytecode/vm/call.rs
  • crates/monty/src/bytecode/vm/compare.rs
  • crates/monty/src/bytecode/vm/mod.rs
  • crates/monty/src/bytecode/vm/scheduler.rs
  • crates/monty/src/types/module.rs

Dependent cleanups were also reverted to keep the branch compiling with the rollback:

  • crates/monty/src/repl.rs
  • crates/monty/src/run_progress.rs

What Those Deferred VM/Module Changes Were Trying To Do

  1. Preserve hidden resume metadata for pending datetime.now OS calls so async future resolution can reconstruct date.today(), naive datetime.now(), and fixed-offset aware datetime.now(tz=...) correctly.
  2. Improve CPython parity for datetime/timedelta arithmetic and comparison edge cases (overflow paths and naive/aware error handling).
  3. Change type/module method dispatch so module-exposed builtin types (notably datetime.date / datetime.datetime) handle class methods consistently.
  4. Centralize some comparison and unary-op edge handling inside VM paths.

Deferred Regression Coverage

The intended failing cases are captured as commented tests in:

  • crates/monty/test_cases/datetime__core.py

Those snippets are intentionally commented out in this PR and should be re-enabled in the follow-up PR that restores the deferred VM/module work.

@codspeed-hq
Copy link

codspeed-hq bot commented Feb 16, 2026

Merging this PR will not alter performance

✅ 13 untouched benchmarks


Comparing mwildehahn:impl/datetime-fixed-offset (7ebc359) with main (4ee3fd7)

Open in CodSpeed

Ok(MontyObject::String(string.extract()?))
} else if let Ok(bytes) = obj.cast::<PyBytes>() {
Ok(MontyObject::Bytes(bytes.extract()?))
} else if obj.is_instance(get_datetime_datetime(obj.py())?)? {
Copy link
Member

Choose a reason for hiding this comment

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

the cpython c-api and pyo3 have proper support python datetime objects, e.g. PyDatetime, you should cast to those here.

You might want to look at the code in pydantic for how we do this there.

# def __new__(cls: type[Self], ...) -> Self: ...
# In other cases, use `typing_extensions.Self`.
Self = TypeVar('Self')
Self = TypeVar('Self') # noqa: Y001
Copy link
Member

Choose a reason for hiding this comment

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

why has this changed?

Copy link
Member

Choose a reason for hiding this comment

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

please undo all these changes in ‎crates/monty-typeshed/vendor/typeshed/stdlib/

Copy link
Author

Choose a reason for hiding this comment

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

@samuelcolvin to get tests passing I had to regen typeshed. LMK if there is a better way to do this or if you still want me to remove these.

match (t, method_id) {
(Type::Dict, m) if m == StaticStrings::Fromkeys => return dict_fromkeys(args, heap, interns),
(Type::Bytes, m) if m == StaticStrings::Fromhex => return bytes_fromhex(args, heap, interns),
(Type::Dict, m) if m == StaticStrings::Fromkeys => {
Copy link
Member

Choose a reason for hiding this comment

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

we should probably move these methods into dict.rs etc. rather than have the logic here.

/// Returns datetime awareness (`true` for aware, `false` for naive) for datetime values.
///
/// Returns `None` when the value is not a datetime reference.
fn datetime_awareness(value: &Value, heap: &crate::heap::Heap<impl ResourceTracker>) -> Option<bool> {
Copy link
Member

Choose a reason for hiding this comment

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

I think this method is duplicated.

Comment on lines +415 to +435
// Pre-process OS callback results before restoring the VM to avoid borrowing
// conflicts with the VM's mutable borrow of `self.heap`.
let resume_action = match ext_result {
ExternalResult::Return(obj) => {
if let Some(transform) = self.pending_os_transform {
match transform_os_return_value(obj, transform, &mut self.heap) {
Ok(value) => ResumeAction::ReturnValue(value),
Err(msg) => ResumeAction::Error(RunError::from(MontyException::runtime_error(format!(
"invalid return type: {msg}"
)))),
}
} else {
ResumeAction::ReturnObj(obj)
}
}
ExternalResult::Error(exc) => {
let err = RunError::from(exc);
ResumeAction::Error(err)
}
ExternalResult::Future => ResumeAction::Future,
};
Copy link
Member

Choose a reason for hiding this comment

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

this is ugly, we should add Datetime, Date etc. varients to MontyObject

Copy link
Member

Choose a reason for hiding this comment

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

I guess these variants should use whatever Datetime and Date etc. struct we settle on - e.g. our own Datetime type that wraps a chrono or speedate date.


/// Creates a date from a proleptic Gregorian ordinal value.
pub(crate) fn from_ordinal(ordinal: i32) -> RunResult<Date> {
let days_since_epoch = i64::from(ordinal) - 719_163;
Copy link
Member

Choose a reason for hiding this comment

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

where is 719_163 coming from, we need more explanation

Copy link
Member

Choose a reason for hiding this comment

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

there's a lot of logic here. I know I recommended speedate, but would this be simpler if we used chrono?

I initially suggested not to use chrono because of it's leap second stuff, but maybe it would be cleaner than this???

};

/// `datetime.date` storage backed directly by `speedate`.
pub(crate) type Date = speedate::Date;
Copy link
Member

Choose a reason for hiding this comment

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

you should wrapp the speedate Date (or chrono date) like

pub(crate) struct Date(speedate::Date);

Then you can implement (de)serialization directly here.

Comment on lines +42 to +47
if !(1..=9999).contains(&year) {
return Err(SimpleException::new_msg(ExcType::ValueError, format!("year {year} is out of range")).into());
}
if !(1..=12).contains(&month) {
return Err(SimpleException::new_msg(ExcType::ValueError, "month must be in 1..12").into());
}
Copy link
Member

Choose a reason for hiding this comment

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

you've missed days, but also whatever logic we use here should be shared with Date.

@mwildehahn mwildehahn force-pushed the impl/datetime-fixed-offset branch 2 times, most recently from 421cfb7 to 5c87707 Compare February 18, 2026 05:07
@mwildehahn
Copy link
Author

@samuelcolvin i think i got to all the feedback -- i switched to chrono which let us drop a lot of the custom logic around date / datetime.

i'm not sure how to handle the vendored typeshed. i previously had bumped that and it brought in all the other changes. the failing test is because datetime isn't in the stdlib.

@mwildehahn mwildehahn force-pushed the impl/datetime-fixed-offset branch from f1a42f8 to 2ce1535 Compare March 3, 2026 22:34
…resolution

- Extracted magic numbers like 86_400, 1_000_000 into constants in timedelta, timezone, date, datetime and convert modules

- Replaced invalid const 'i128::from' in timedelta constants and fixed subsec_nanos division to prevent compilation errors

- Added manual Hash implementation for DateTime based on aware/naive py_eq equivalence rather than raw fields

- Updated typeshed to use upstream datetime.pyi directly instead of relying on custom stub overrides and build.rs zip manipulations
- Added HeapDataMut variants for datetime types so VM generic paths don't panic on to_mut

- Fixed Value::py_cmp fallback to dispatch to HeapData::py_cmp instead of returning None

- Adjusted Option<Ordering> evaluation for None (Float NaN) vs TypeError (incomparable types)
…rooting

- Updated heap GC mark phase to keep the lazily-allocated datetime.timezone.utc singleton alive

- Updated MontyObject -> HeapData::TimeZone conversion to reuse the cached UTC singleton instead of allocating new instances, maintaining 'is' identity

- Rejected explicit None passed as the name to timezone() constructors to match CPython's TypeError behavior
@mwildehahn mwildehahn force-pushed the impl/datetime-fixed-offset branch from 0ccc76d to fecf84f Compare March 4, 2026 05:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants