Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,7 @@ authors = [
requires-python = ">= 3.12, < 4.0"
readme = "README.md"
license = "MIT"
dependencies = [
"python-dateutil>=2.9.0,<3",
]
dependencies = []

[project.urls]
Homepage = "https://fable.io"
Expand Down
2 changes: 2 additions & 0 deletions src/Fable.Cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* [Python] Fix `DateTime.TryParse` incorrectly assigning `DateTimeKind.Local` to naive datetime strings (should be `DateTimeKind.Unspecified`) (fixes #3654)
* [Python] Remove `python-dateutil` dependency from fable-library; use stdlib `datetime.fromisoformat` with `strptime` fallback
* [All] Fix unnecessary object allocations during AST traversal when visiting `Import` expressions (by Repo Assist)
* [Beam] Fix `System.Random.Next(0)` implementation (by @ncave)
* [Python] Fix `System.Random` seeded implementation (by @ncave)
Expand Down
2 changes: 2 additions & 0 deletions src/Fable.Compiler/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

* [Python] Fix `DateTime.TryParse` incorrectly assigning `DateTimeKind.Local` to naive datetime strings (should be `DateTimeKind.Unspecified`) (fixes #3654)
* [Python] Remove `python-dateutil` dependency from fable-library; use stdlib `datetime.fromisoformat` with `strptime` fallback
* [All] Fix unnecessary object allocations during AST traversal when visiting `Import` expressions (by Repo Assist)
* [Beam] Fix `System.Random.Next(0)` implementation (by @ncave)
* [Python] Fix `System.Random` seeded implementation (by @ncave)
Expand Down
7 changes: 6 additions & 1 deletion src/fable-library-py/fable_library/date.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,7 +510,12 @@ def add(x: datetime, y: TimeSpan) -> datetime:

def parse(string: str) -> datetime:
try:
return datetime.fromisoformat(string).astimezone()
parsed = datetime.fromisoformat(string)
# Only convert to local time when the string had an explicit timezone.
# Naive strings represent DateTimeKind.Unspecified and must stay naive.
if parsed.tzinfo is not None:
return parsed.astimezone()
return parsed
except ValueError:
pass

Expand Down
23 changes: 20 additions & 3 deletions src/fable-library-py/fable_library/date_offset.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import re
from datetime import UTC, datetime, timedelta, timezone
from math import fmod
from typing import Any, SupportsFloat, SupportsIndex, SupportsInt, overload
Expand Down Expand Up @@ -189,10 +190,26 @@ def timedelta_total_microseconds(td: timedelta) -> int:
return td.days * (24 * 3600) + td.seconds * 10**6 + td.microseconds


def parse(string: str, detectUTC: bool = False) -> DateTimeOffset:
from dateutil import parser # noqa: PLC0415 - lazy import to avoid top-level dependency
_NON_ISO_FORMATS: dict[str, str] = {
# 9/10/2014 1:50:34 PM
r"^(0?[1-9]|1[0-2])\/(0?[1-9]|1[0-2])\/\d{4} ([0-9]|(0|1)[0-9]|2[0-4]):([0-5][0-9]|0?[0-9]):([0-5][0-9]|0?[0-9]) [AP]M$": "%m/%d/%Y %I:%M:%S %p",
# 9/10/2014 1:50:34
r"^(0?[1-9]|1[0-2])\/(0?[1-9]|1[0-2])\/\d{4} ([0-9]|(0|1)[0-9]|2[0-4]):([0-5][0-9]|0?[0-9]):([0-5][0-9]|0?[0-9])$": "%m/%d/%Y %H:%M:%S",
}


def _parse_non_iso(string: str) -> datetime:
for pattern, fmt in _NON_ISO_FORMATS.items():
if re.fullmatch(pattern, string):
return datetime.strptime(string, fmt)
raise ValueError(f"Unsupported format by Fable: {string}")

parsed_dt = parser.parse(string)

def parse(string: str, detectUTC: bool = False) -> DateTimeOffset:
try:
parsed_dt = datetime.fromisoformat(string)
except ValueError:
parsed_dt = _parse_non_iso(string)

# Calculate offset in milliseconds
if parsed_dt.tzinfo is not None:
Expand Down
5 changes: 1 addition & 4 deletions src/fable-library-py/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,7 @@ authors = [
requires-python = ">= 3.10, < 4.0"
readme = "README.md"
license = "MIT License"
dependencies = [
"python-dateutil>=2.9.0,<3"
]
dependencies = []

[project.urls]
Homepage = "https://fable.io"
Expand All @@ -23,7 +21,6 @@ dev = [
"pytest-benchmark>=5.1.0,<6",
"pyright>=1.1.401,<2",
"pydantic>=2.12.5",
"python-dateutil>=2.9.0,<3",
]

[tool.hatch.build.targets.sdist]
Expand Down
424 changes: 199 additions & 225 deletions src/fable-library-py/uv.lock

Large diffs are not rendered by default.

12 changes: 12 additions & 0 deletions tests/Python/TestDateTime.fs
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,18 @@ let ``test DateTime.TryParse works`` () =
dateTime.Year + dateTime.Month + dateTime.Day + dateTime.Hour + dateTime.Minute + dateTime.Second |> equal 2130


[<Fact>]
let ``test DateTime.TryParse preserves Unspecified kind for naive strings`` () =
let (isSuccess, dt) = DateTime.TryParse("2018-10-01T11:12:55")
isSuccess |> equal true
dt.Kind |> equal DateTimeKind.Unspecified
dt.Year |> equal 2018
dt.Month |> equal 10
dt.Day |> equal 1
dt.Hour |> equal 11
dt.Minute |> equal 12
dt.Second |> equal 55

[<Fact>]
let ``test "Parsing doesn't succeed for invalid dates`` () =
let invalidAmericanDate = "13/1/2020"
Expand Down
7 changes: 7 additions & 0 deletions tests/Python/TestDateTimeOffset.fs
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,13 @@ let ``test DateTimeOffset.TryParse works`` () =
f "9/10/2014 1:50:34" |> equal true
f "2014-09-10T13:50:34" |> equal true

[<Fact>]
let ``test DateTimeOffset.TryParse with timezone offset returns correct value`` () =
let expected = DateTimeOffset(2018, 7, 2, 12, 23, 45, 0, TimeSpan.FromHours(2.))
let (isSuccess, actual) = DateTimeOffset.TryParse("2018-07-02T12:23:45+02:00")
isSuccess |> equal true
actual |> equal expected

[<Fact>]
let ``test DateTimeOffset.ToString() default works`` () =
let d = DateTimeOffset(2014, 10, 9, 13, 23, 30, TimeSpan.Zero)
Expand Down
25 changes: 0 additions & 25 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading