Skip to content

Add native TimeOnly to time conversion#802

Open
buvinghausen wants to merge 1 commit intotonybaloney:mainfrom
buvinghausen:add_timeonly_time
Open

Add native TimeOnly to time conversion#802
buvinghausen wants to merge 1 commit intotonybaloney:mainfrom
buvinghausen:add_timeonly_time

Conversation

@buvinghausen
Copy link
Copy Markdown
Contributor

Add TimeOnlydatetime.time interop

Summary

Adds first-class support for Python's datetime.time type, mapping it to C#'s TimeOnly. Follows the same pattern established by DateOnlydatetime.date.

Conversion nuances

  • C# TimeOnly stores sub-second precision as separate Millisecond (0–999) and Microsecond (0–999) fields, plus a Ticks remainder that has no Python equivalent.
  • Python datetime.time stores a single microsecond field in the range 0–999999.
  • The conversion combines/splits these: us_total = ms * 1000 + us on the way to Python, and ms = us_total / 1000, us = us_total % 1000 on the way back. Sub-microsecond ticks are silently dropped, which is acceptable since Python has no way to represent them.

Changes

CSnakes.SourceGeneration

  • Parser/Types/PythonTypeSpec.cs — Added TimeType singleton record and PythonTypeSpec.Time static property.
  • Parser/PythonParser.TypeDef.cs — Added Time sub-parser and mapped "time" / "datetime.time" tokens in the type definition switch.
  • Reflection/TypeReflection.cs — Added (TimeType, _, _) => [ParseTypeName("TimeOnly")] to AsPredefinedType.
  • ResultConversionCodeGenerator.cs — Added static Time scalar generator field, a TimeType case in Create(), and added TimeType to the value-type optional pattern so Optional[datetime.time] maps to TimeOnly? via OptionalValue.

CSnakes.Runtime

  • PyObjectTypeConverter.Time.cs (new)ConvertFromTimeOnly constructs a datetime.time via the CPython API, combining C#'s Millisecond and Microsecond into Python's single microsecond argument. ConvertToTimeOnly reads the four attributes back and reconstructs a TimeOnly.
  • Python/PyObjectImporters.cs — Added Time : IPyObjectImporter<TimeOnly> sealed class delegating to ConvertToTimeOnly.
  • Python/PyObject.cs — Added From(TimeOnly) / From(TimeOnly?) overloads and a TimeOnly branch in the As(Type) switch.
  • PublicAPI/net8.0/PublicAPI.Unshipped.txt and PublicAPI/net9.0/PublicAPI.Unshipped.txt — Declared PyObjectImporters.Time and both From(TimeOnly) overloads.

Tests

  • PythonTypeDefinitionParserTests.cs — Added TimeTest theory for "time" / "datetime.time"TimeType.
  • TypeReflectionTests.cs — Added ("time", "TimeOnly") and ("datetime.time", "TimeOnly") to AsPredefinedTypeData.
  • PythonTypeSpecTests.cs — Added TimeType to the singletons assertion and a TimeTypeTests inner class.
  • GeneratedSignatureTests.cs — Added time / datetime.time round-trip signature test cases.
  • Integration.Tests/python/test_time.py (new)test_time_roundtrip and test_create_time Python functions.
  • PythonStaticGeneratorTests/FormatClassFromMethods.test_time.approved.txt (new) — Approval snapshot for the generated C# wrapper.

Copilot AI review requested due to automatic review settings March 9, 2026 05:40
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds first-class TimeOnly ↔ Python datetime.time interoperability across the source generator (parsing/type reflection/signature generation) and runtime (PyObject conversions/importers), plus related unit/snapshot coverage and new Python integration module.

Changes:

  • Introduces a TimeType in the type system and parses "time" / "datetime.time" annotations to it.
  • Maps TimeType to C# TimeOnly in reflection/signature generation and adds result conversion support (including Optional[time]TimeOnly?).
  • Adds runtime conversions/importer + PyObject.From(TimeOnly) overloads and updates PublicAPI declarations; adds Python-side functions and generator snapshot updates.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
src/Integration.Tests/python/test_time.py Adds Python functions for datetime.time roundtrip/creation.
src/CSnakes.Tests/TypeReflectionTests.cs Verifies predefined type mapping for time/datetime.timeTimeOnly.
src/CSnakes.Tests/PythonTypeSpecTests.cs Asserts PythonTypeSpec.Time singleton and TimeType behavior.
src/CSnakes.Tests/PythonTypeDefinitionParserTests.cs Adds parser tests for time and datetime.time.
src/CSnakes.Tests/PythonStaticGeneratorTests/FormatClassFromMethods.test_time.approved.txt Updates approved snapshot showing generated TimeOnly signatures/importers.
src/CSnakes.Tests/GeneratedSignatureTests.cs Adds signature generation test for time/datetime.time usage.
src/CSnakes.SourceGeneration/ResultConversionCodeGenerator.cs Adds scalar/optional result conversion support for TimeType.
src/CSnakes.SourceGeneration/Reflection/TypeReflection.cs Maps TimeType to TimeOnly for predefined type reflection.
src/CSnakes.SourceGeneration/Parser/Types/PythonTypeSpec.cs Adds TimeType + PythonTypeSpec.Time.
src/CSnakes.SourceGeneration/Parser/PythonParser.TypeDef.cs Parses "time" / "datetime.time" into TimeType.
src/CSnakes.Runtime/Python/PyObjectImporters.cs Adds PyObjectImporters.Time : IPyObjectImporter<TimeOnly>.
src/CSnakes.Runtime/Python/PyObject.cs Adds PyObject.From(TimeOnly) / From(TimeOnly?) and As(Type) support for TimeOnly.
src/CSnakes.Runtime/PyObjectTypeConverter.Time.cs Implements TimeOnlydatetime.time conversion via CPython API.
src/CSnakes.Runtime/PublicAPI/net8.0/PublicAPI.Unshipped.txt Declares new public API surface for importer and From(TimeOnly) overloads.
src/CSnakes.Runtime/PublicAPI/net9.0/PublicAPI.Unshipped.txt Declares new public API surface for importer and From(TimeOnly) overloads.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +20 to +34
internal static TimeOnly ConvertToTimeOnly(PyObject pyObject)
{
using PyObject hourObj = pyObject.GetAttr("hour");
using PyObject minuteObj = pyObject.GetAttr("minute");
using PyObject secondObj = pyObject.GetAttr("second");
using PyObject microsecondObj = pyObject.GetAttr("microsecond");

long us = CPythonAPI.PyLong_AsLongLong(microsecondObj);

return new TimeOnly(
(int)CPythonAPI.PyLong_AsLongLong(hourObj),
(int)CPythonAPI.PyLong_AsLongLong(minuteObj),
(int)CPythonAPI.PyLong_AsLongLong(secondObj),
millisecond: (int)(us / 1000),
microsecond: (int)(us % 1000));
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

ConvertToTimeOnly reconstructs a TimeOnly from hour/minute/second/microsecond but ignores datetime.time's tzinfo (and fold) fields. This can silently drop timezone/ambiguity information when an aware datetime.time is passed in. Consider explicitly checking tzinfo is None (and optionally fold == 0) and throwing a clear exception if unsupported values are present, so callers don’t get lossy conversions without noticing.

Copilot uses AI. Check for mistakes.
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.

@copilot open a new pull request to apply changes based on this feedback

Comment on lines +7 to +35
internal static PyObject ConvertFromTimeOnly(TimeOnly time)
{
using PyObject datetimeModule = CPythonAPI.Import("datetime");
using PyObject timeClass = datetimeModule.GetAttr("time");
using PyObject hourArg = PyObject.Create(CPythonAPI.PyLong_FromLongLong(time.Hour));
using PyObject minuteArg = PyObject.Create(CPythonAPI.PyLong_FromLongLong(time.Minute));
using PyObject secondArg = PyObject.Create(CPythonAPI.PyLong_FromLongLong(time.Second));
// Python microsecond is 0–999999; C# splits ms and us
using PyObject microsecondArg = PyObject.Create(
CPythonAPI.PyLong_FromLongLong(time.Millisecond * 1000L + time.Microsecond));
return timeClass.Call(hourArg, minuteArg, secondArg, microsecondArg);
}

internal static TimeOnly ConvertToTimeOnly(PyObject pyObject)
{
using PyObject hourObj = pyObject.GetAttr("hour");
using PyObject minuteObj = pyObject.GetAttr("minute");
using PyObject secondObj = pyObject.GetAttr("second");
using PyObject microsecondObj = pyObject.GetAttr("microsecond");

long us = CPythonAPI.PyLong_AsLongLong(microsecondObj);

return new TimeOnly(
(int)CPythonAPI.PyLong_AsLongLong(hourObj),
(int)CPythonAPI.PyLong_AsLongLong(minuteObj),
(int)CPythonAPI.PyLong_AsLongLong(secondObj),
millisecond: (int)(us / 1000),
microsecond: (int)(us % 1000));
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

The new TimeOnlydatetime.time runtime conversion path (via ConvertFromTimeOnly / ConvertToTimeOnly and PyObject.From(TimeOnly)) doesn’t appear to be exercised by any C# integration test (there are no Env.TestTime() usages in src/Integration.Tests). Consider adding an Integration.Tests/TimeTests.cs (similar to BasicTests/BufferTests) that calls into test_time.py and asserts round-trip values, including microsecond boundaries (e.g., 0, 999999) and that sub-microsecond ticks are dropped as documented.

Copilot uses AI. Check for mistakes.
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.

@copilot open a new pull request to apply changes based on this feedback

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