PyO3 / pyo3

Rust bindings for the Python interpreter
https://pyo3.rs
Other
11.51k stars 703 forks source link

`IntoPy` does not properly respect `chrono_tz::Tz` timezones #3266

Open grantslatton opened 1 year ago

grantslatton commented 1 year ago

The IntoPy implementation for chrono::DateTime is implemented generically for any T: TimeZone by converting everything to fixed offsets. This means it loses the Tz information in a timezone like America/Los Angeles and just converts it to UTC-8 which is a lossy conversion (due to daylight savings, etc).

PyO3 should probably provide two different implements, one for DateTime<FixedOffset> and another for DateTime<chrono_tz::Tz> that creates a python ZoneInfo object.

This is technically a bug since this should be a lossless conversion but I have filed it under feature request since it doesn't match the bug template very well.

adamreichold commented 1 year ago

@Psykopear @pickfire Do you have the bandwidth to look into this?

grantslatton commented 1 year ago

FWIW I wound up implementing this in my own code like:

pub fn get_canonical_tz_py_obj_kwargs(tz: Tz) -> Py<PyDict> {
    lazy_static! {
        static ref TIMEZONES: HashMap<Tz, Py<PyDict>> = {
            let code = r#"
import zoneinfo
all_zones = zoneinfo.available_timezones()
zone_dict = {}
for zone_name in all_zones:
    zone = zoneinfo.ZoneInfo(zone_name)
    zone_dict[zone_name] = zone
            "#.trim();
            Python::with_gil(|py| {
                let globals = PyDict::new(py);
                py.run(code, Some(globals), None).unwrap();
                let zone_dict = globals.get_item("zone_dict").unwrap().extract::<HashMap<String, Py<PyAny>>>().unwrap();
                zone_dict
                    .into_iter()
                    .map(|(mut name, zone)| {
                        // Weird idiosyncratic "factory reset" type zone that python has but rust does not
                        if name == "Factory" {
                            name = "UTC".to_string();
                        }
                        let kwargs = [("tzinfo", zone)].into_py_dict(py);
                        (Tz::from_str(&name).unwrap(), kwargs.into_py(py))
                    })
                    .collect()
            })
        };
    }

    TIMEZONES.get(&tz).unwrap().clone()
}

and then to convert the DateTime<Tz> to PyAny:

let kwargs = get_canonical_tz_py_obj_kwargs(dt.timezone());
let datetime = dt.naive_local();
let datetime = datetime.into_py(py);
let result = datetime.call_method(py, "replace", (), Some(kwargs.as_ref(py))).unwrap();

You can probably think of something more pythonic or efficient here, but if not, feel free to steal from this