diff --git a/justfile b/justfile index de53b89..f0de065 100644 --- a/justfile +++ b/justfile @@ -83,13 +83,13 @@ shipit *args: # Format code with Fantomas format: - dotnet fantomas {{src_path}} -r - dotnet fantomas {{test_path}} -r + dotnet fantomas {{src_path}} + dotnet fantomas {{test_path}} # Check code formatting without making changes format-check: - dotnet fantomas {{src_path}} -r --check - dotnet fantomas {{test_path}} -r --check + dotnet fantomas {{src_path}} --check + dotnet fantomas {{test_path}} --check # Install .NET tools (Fable, Fantomas) and Python dependencies setup: diff --git a/src/Fable.Python.fsproj b/src/Fable.Python.fsproj index 4920133..3dff498 100644 --- a/src/Fable.Python.fsproj +++ b/src/Fable.Python.fsproj @@ -25,6 +25,7 @@ + diff --git a/src/fable/Testing.fs b/src/fable/Testing.fs index 70272ae..9f1aa02 100644 --- a/src/fable/Testing.fs +++ b/src/fable/Testing.fs @@ -47,7 +47,7 @@ let throwsError (expected: string) (f: unit -> 'a) : unit = let throwsErrorContaining (expected: string) (f: unit -> 'a) : unit = match run f with | Error _ when String.IsNullOrEmpty expected -> () - | Error (actual: string) when actual.Contains expected -> () + | Error(actual: string) when actual.Contains expected -> () | Error actual -> equal (sprintf "Error containing '%s'" expected) actual | Ok _ -> equal (sprintf "Error containing '%s'" expected) "No error was thrown" diff --git a/src/stdlib/Builtins.fs b/src/stdlib/Builtins.fs index 8260362..f7ee0eb 100644 --- a/src/stdlib/Builtins.fs +++ b/src/stdlib/Builtins.fs @@ -353,7 +353,8 @@ let __name__: string = nativeOnly let print obj = builtins.print obj /// Return the value of the named attribute of object with a default. -let getattr obj name defaultValue = builtins.getattr (obj, name, defaultValue) +let getattr obj name defaultValue = + builtins.getattr (obj, name, defaultValue) /// Sets the named attribute on the given object to the specified value. let setattr obj name value = builtins.setattr (obj, name, value) diff --git a/src/stdlib/Datetime.fs b/src/stdlib/Datetime.fs new file mode 100644 index 0000000..7cc0c42 --- /dev/null +++ b/src/stdlib/Datetime.fs @@ -0,0 +1,324 @@ +/// Type bindings for Python datetime module: https://docs.python.org/3/library/datetime.html +/// +/// Note: this module exposes a `time` class binding for Python's `datetime.time`. If you also +/// open `Fable.Python.Time` (the `time` module), the `time` identifier will collide — qualify +/// one of them, e.g. `Fable.Python.Time.time.time()` vs. `Fable.Python.Datetime.time(...)`. +module Fable.Python.Datetime + +open Fable.Core + +// fsharplint:disable MemberNames + +// ============================================================================ +// timedelta +// ============================================================================ + +/// A duration expressing the difference between two date, time, or datetime instances. +/// See https://docs.python.org/3/library/datetime.html#datetime.timedelta +/// +/// The empty `timedelta()` ctor creates a zero duration. For other durations, use the +/// single-unit factories `ofDays`, `ofHours`, `ofMinutes`, `ofSeconds`, `ofWeeks`, +/// `ofMilliseconds`, `ofMicroseconds`, and combine via `.add` / `.sub`. +[] +type timedelta() = + /// Number of full days (may be negative) + member _.days: int = nativeOnly + /// Remaining seconds after full days have been removed; 0 <= seconds < 86400 + member _.seconds: int = nativeOnly + /// Remaining microseconds; 0 <= microseconds < 1000000 + member _.microseconds: int = nativeOnly + /// Return the total duration represented in fractional seconds + /// See https://docs.python.org/3/library/datetime.html#datetime.timedelta.total_seconds + member _.total_seconds() : float = nativeOnly + + /// Return the sum of two timedeltas + [] + member _.add(other: timedelta) : timedelta = nativeOnly + + /// Return the difference between two timedeltas + [] + member _.sub(other: timedelta) : timedelta = nativeOnly + + /// Return the negation of this timedelta + [] + member _.neg() : timedelta = nativeOnly + + /// The most negative timedelta representable + static member min: timedelta = nativeOnly + /// The most positive timedelta representable + static member max: timedelta = nativeOnly + /// The smallest positive difference between non-equal timedelta objects + static member resolution: timedelta = nativeOnly + + /// Create a timedelta of N days + [] + static member ofDays(days: float) : timedelta = nativeOnly + + /// Create a timedelta of N seconds + [] + static member ofSeconds(seconds: float) : timedelta = nativeOnly + + /// Create a timedelta of N microseconds + [] + static member ofMicroseconds(microseconds: float) : timedelta = nativeOnly + + /// Create a timedelta of N milliseconds + [] + static member ofMilliseconds(milliseconds: float) : timedelta = nativeOnly + + /// Create a timedelta of N minutes + [] + static member ofMinutes(minutes: float) : timedelta = nativeOnly + + /// Create a timedelta of N hours + [] + static member ofHours(hours: float) : timedelta = nativeOnly + + /// Create a timedelta of N weeks + [] + static member ofWeeks(weeks: float) : timedelta = nativeOnly + +// ============================================================================ +// timezone (defined before datetime so datetime members can reference it) +// ============================================================================ + +/// A fixed-offset implementation of tzinfo. +/// See https://docs.python.org/3/library/datetime.html#datetime.timezone +[] +type timezone(offset: timedelta, ?name: string) = + /// Return the UTC offset for this timezone + member _.utcoffset(dt: obj) : timedelta = nativeOnly + /// Return the timezone name string for this timezone + member _.tzname(dt: obj) : string = nativeOnly + /// The UTC timezone singleton (offset zero, name "UTC") + static member utc: timezone = nativeOnly + +// ============================================================================ +// date +// ============================================================================ + +/// A naive date (year, month, day) with no time or timezone component. +/// See https://docs.python.org/3/library/datetime.html#datetime.date +[] +type date(year: int, month: int, day: int) = + /// Year in range [MINYEAR, MAXYEAR] + member _.year: int = nativeOnly + /// Month in range [1, 12] + member _.month: int = nativeOnly + /// Day in range [1, number of days in the month and year] + member _.day: int = nativeOnly + /// Return a string in ISO 8601 format, e.g. "2026-04-21" + /// See https://docs.python.org/3/library/datetime.html#datetime.date.isoformat + member _.isoformat() : string = nativeOnly + /// Return a string representing the date, formatted with format + /// See https://docs.python.org/3/library/datetime.html#datetime.date.strftime + member _.strftime(format: string) : string = nativeOnly + /// Return the day of the week as an integer; Monday is 0 and Sunday is 6 + /// See https://docs.python.org/3/library/datetime.html#datetime.date.weekday + member _.weekday() : int = nativeOnly + /// Return the day of the week as an integer; Monday is 1 and Sunday is 7 + /// See https://docs.python.org/3/library/datetime.html#datetime.date.isoweekday + member _.isoweekday() : int = nativeOnly + /// Return the proleptic Gregorian ordinal of the date; January 1 of year 1 has ordinal 1 + member _.toordinal() : int = nativeOnly + + /// Return a date with the given fields replaced (any subset of year/month/day) + /// See https://docs.python.org/3/library/datetime.html#datetime.date.replace + [] + member _.replace(?year: int, ?month: int, ?day: int) : date = nativeOnly + + /// Return the timedelta between this date and other (self - other) + [] + member _.sub(other: date) : timedelta = nativeOnly + + /// Return a date offset by the given timedelta (self - delta) + [] + member _.sub(delta: timedelta) : date = nativeOnly + + /// Return a date offset by the given timedelta (self + delta) + [] + member _.add(delta: timedelta) : date = nativeOnly + + /// Return the current local date + /// See https://docs.python.org/3/library/datetime.html#datetime.date.today + static member today() : date = nativeOnly + /// Return the local date corresponding to a POSIX timestamp + /// See https://docs.python.org/3/library/datetime.html#datetime.date.fromtimestamp + static member fromtimestamp(timestamp: float) : date = nativeOnly + /// Return the date corresponding to the proleptic Gregorian ordinal + static member fromordinal(ordinal: int) : date = nativeOnly + /// Return a date from a string in any valid ISO 8601 format + /// See https://docs.python.org/3/library/datetime.html#datetime.date.fromisoformat + static member fromisoformat(date_string: string) : date = nativeOnly + /// The earliest representable date + static member min: date = nativeOnly + /// The latest representable date + static member max: date = nativeOnly + +// ============================================================================ +// time +// ============================================================================ + +/// A naive or aware time of day (hour, minute, second, microsecond, tzinfo). +/// See https://docs.python.org/3/library/datetime.html#datetime.time +/// +/// Ctor args are positional: `time(h)`, `time(h, m)`, `time(h, m, s)`, `time(h, m, s, us)`. +[] +type time(hour: int, ?minute: int, ?second: int, ?microsecond: int) = + /// Hour in range [0, 23] + member _.hour: int = nativeOnly + /// Minute in range [0, 59] + member _.minute: int = nativeOnly + /// Second in range [0, 59] + member _.second: int = nativeOnly + /// Microsecond in range [0, 999999] + member _.microsecond: int = nativeOnly + /// Fold value (0 or 1) for disambiguating wall-clock times that repeat during DST transitions + member _.fold: int = nativeOnly + /// Return a string in ISO 8601 format, e.g. "14:30:00" + /// See https://docs.python.org/3/library/datetime.html#datetime.time.isoformat + member _.isoformat() : string = nativeOnly + /// Return a string representing the time, formatted with format + /// See https://docs.python.org/3/library/datetime.html#datetime.time.strftime + member _.strftime(format: string) : string = nativeOnly + /// Return the UTC offset as a timedelta for aware times; None for naive times + member _.utcoffset() : timedelta option = nativeOnly + /// Return the timezone abbreviation string for aware times; None for naive times + member _.tzname() : string option = nativeOnly + + /// Return a time with the given fields replaced (any subset) + /// See https://docs.python.org/3/library/datetime.html#datetime.time.replace + [] + member _.replace(?hour: int, ?minute: int, ?second: int, ?microsecond: int) : time = nativeOnly + + /// Return a time from a string in ISO 8601 format + /// See https://docs.python.org/3/library/datetime.html#datetime.time.fromisoformat + static member fromisoformat(time_string: string) : time = nativeOnly + /// The earliest representable time, time(0, 0, 0, 0) + static member min: time = nativeOnly + /// The latest representable time, time(23, 59, 59, 999999) + static member max: time = nativeOnly + +// ============================================================================ +// datetime +// ============================================================================ + +/// A naive or aware date and time (year, month, day, hour, minute, second, microsecond, tzinfo). +/// See https://docs.python.org/3/library/datetime.html#datetime.datetime +[] +type datetime + ( + year: int, + month: int, + day: int, + ?hour: int, + ?minute: int, + ?second: int, + ?microsecond: int, + ?tzinfo: timezone, + ?fold: int + ) = + /// Year in range [MINYEAR, MAXYEAR] + member _.year: int = nativeOnly + /// Month in range [1, 12] + member _.month: int = nativeOnly + /// Day in range [1, number of days in the month and year] + member _.day: int = nativeOnly + /// Hour in range [0, 23] + member _.hour: int = nativeOnly + /// Minute in range [0, 59] + member _.minute: int = nativeOnly + /// Second in range [0, 59] + member _.second: int = nativeOnly + /// Microsecond in range [0, 999999] + member _.microsecond: int = nativeOnly + /// Fold value (0 or 1) for disambiguating wall-clock times that repeat during DST transitions + member _.fold: int = nativeOnly + /// Return the date part as a date object + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.date + member _.date() : date = nativeOnly + /// Return the time part as a time object (tzinfo is not included) + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.time + member _.time() : time = nativeOnly + /// Return a string in ISO 8601 format, e.g. "2026-04-21T14:30:00" + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.isoformat + member _.isoformat() : string = nativeOnly + + /// Return a string in ISO 8601 format with a custom separator between date and time + [] + member _.isoformat(sep: string) : string = nativeOnly + + /// Return a string representing the datetime, formatted with format + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.strftime + member _.strftime(format: string) : string = nativeOnly + /// Return the POSIX timestamp corresponding to this datetime as a float + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.timestamp + member _.timestamp() : float = nativeOnly + /// Return the UTC offset as a timedelta for aware datetimes; None for naive + member _.utcoffset() : timedelta option = nativeOnly + /// Return the timezone abbreviation string for aware datetimes; None for naive + member _.tzname() : string option = nativeOnly + /// Return the day of the week as an integer; Monday is 0 and Sunday is 6 + member _.weekday() : int = nativeOnly + /// Return the day of the week as an integer; Monday is 1 and Sunday is 7 + member _.isoweekday() : int = nativeOnly + + /// Return a datetime with the given fields replaced (any subset) + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.replace + [] + member _.replace + (?year: int, ?month: int, ?day: int, ?hour: int, ?minute: int, ?second: int, ?microsecond: int) + : datetime = + nativeOnly + + /// Return a datetime converted to the given timezone + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.astimezone + member _.astimezone(tz: timezone) : datetime = nativeOnly + + /// Return the timedelta between this datetime and other (self - other) + [] + member _.sub(other: datetime) : timedelta = nativeOnly + + /// Return a datetime offset by the given timedelta (self - delta) + [] + member _.sub(delta: timedelta) : datetime = nativeOnly + + /// Return a datetime offset by the given timedelta (self + delta) + [] + member _.add(delta: timedelta) : datetime = nativeOnly + + /// Return the current local date and time + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.now + static member now() : datetime = nativeOnly + + /// Return the current local date and time in the given timezone + [] + static member now(tz: timezone) : datetime = nativeOnly + + /// Return the current UTC date and time as a naive datetime. + /// Deprecated in Python 3.12; prefer `datetime.now(tz = timezone.utc)`. + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.utcnow + [] + static member utcnow() : datetime = nativeOnly + + /// Return the local datetime corresponding to a POSIX timestamp + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.fromtimestamp + static member fromtimestamp(timestamp: float) : datetime = nativeOnly + + /// Return the datetime corresponding to a POSIX timestamp in the given timezone + [] + static member fromtimestamp(timestamp: float, tz: timezone) : datetime = nativeOnly + + /// Return a datetime from a string in any valid ISO 8601 format + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.fromisoformat + static member fromisoformat(datetime_string: string) : datetime = nativeOnly + /// Return a datetime parsed from date_string according to format + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.strptime + static member strptime(date_string: string, format: string) : datetime = nativeOnly + /// Combine a date and time into a single datetime + /// See https://docs.python.org/3/library/datetime.html#datetime.datetime.combine + static member combine(date: date, time: time) : datetime = nativeOnly + /// The earliest representable datetime + static member min: datetime = nativeOnly + /// The latest representable datetime + static member max: datetime = nativeOnly diff --git a/src/stdlib/Math.fs b/src/stdlib/Math.fs index 07a72e0..c83b28c 100644 --- a/src/stdlib/Math.fs +++ b/src/stdlib/Math.fs @@ -84,10 +84,12 @@ type IExports = /// Return True if the values a and b are close to each other /// See https://docs.python.org/3/library/math.html#math.isclose abstract isclose: a: float * b: float -> bool + /// Return True if the values a and b are close to each other with custom tolerances /// See https://docs.python.org/3/library/math.html#math.isclose [] abstract isclose: a: float * b: float * ?rel_tol: float * ?abs_tol: float -> bool + /// Return the least common multiple of the integers /// See https://docs.python.org/3/library/math.html#math.lcm abstract lcm: [] ints: int[] -> int diff --git a/src/stdlib/Queue.fs b/src/stdlib/Queue.fs index ca69760..81b7a32 100644 --- a/src/stdlib/Queue.fs +++ b/src/stdlib/Queue.fs @@ -34,10 +34,12 @@ type Queue<'T>() = /// operation goes into an uninterruptible wait on an underlying lock. This means that no exceptions can occur, and /// in particular a SIGINT will not trigger a KeyboardInterrupt. member x.get(?block: bool, ?timeout: float) : 'T = nativeOnly + /// Equivalent to get(False). /// See https://docs.python.org/3/library/queue.html#queue.Queue.get_nowait [] member x.get_nowait() : 'T = nativeOnly + /// Blocks until all items in the queue have been gotten and processed. /// /// The count of unfinished tasks goes up whenever an item is added to the queue. The count goes down whenever a @@ -74,6 +76,7 @@ type SimpleQueue<'T>() = member x.put(item: 'T, ?block: bool, ?timeout: float) : unit = nativeOnly /// Remove and return an item from the queue. member x.get(?block: bool, ?timeout: float) : 'T = nativeOnly + /// Equivalent to get(False). [] member x.get_nowait() : 'T = nativeOnly diff --git a/test/Fable.Python.Test.fsproj b/test/Fable.Python.Test.fsproj index f37dca6..441f8a7 100644 --- a/test/Fable.Python.Test.fsproj +++ b/test/Fable.Python.Test.fsproj @@ -32,6 +32,7 @@ + diff --git a/test/TestDatetime.fs b/test/TestDatetime.fs new file mode 100644 index 0000000..72e0d1b --- /dev/null +++ b/test/TestDatetime.fs @@ -0,0 +1,313 @@ +module Fable.Python.Tests.Datetime + +open Fable.Python.Testing +open Fable.Python.Datetime + +// ============================================================================ +// timedelta tests +// ============================================================================ + +[] +let ``test timedelta days property`` () = + let td = timedelta.ofDays 3.0 + td.days |> equal 3 + +[] +let ``test timedelta hours to seconds`` () = + let td = timedelta.ofHours 2.0 + td.seconds |> equal 7200 + +[] +let ``test timedelta minutes to seconds`` () = + let td = timedelta.ofMinutes 90.0 + td.seconds |> equal 5400 + +[] +let ``test timedelta total_seconds`` () = + let td = (timedelta.ofDays 1.0).add (timedelta.ofHours 2.0) + td.total_seconds () |> equal 93600.0 + +[] +let ``test timedelta zero`` () = + let td = timedelta () + td.days |> equal 0 + td.seconds |> equal 0 + td.microseconds |> equal 0 + +[] +let ``test timedelta add`` () = + let sum = (timedelta.ofHours 1.0).add (timedelta.ofMinutes 30.0) + sum.total_seconds () |> equal 5400.0 + +[] +let ``test timedelta sub`` () = + let diff = (timedelta.ofHours 2.0).sub (timedelta.ofMinutes 30.0) + diff.total_seconds () |> equal 5400.0 + +[] +let ``test timedelta neg`` () = + let n = (timedelta.ofHours 1.0).neg () + n.total_seconds () |> equal -3600.0 + +// ============================================================================ +// date tests +// ============================================================================ + +[] +let ``test date year month day properties`` () = + let d = date (2026, 4, 21) + d.year |> equal 2026 + d.month |> equal 4 + d.day |> equal 21 + +[] +let ``test date isoformat`` () = + let d = date (2026, 4, 21) + d.isoformat () |> equal "2026-04-21" + +[] +let ``test date strftime`` () = + let d = date (2026, 4, 21) + d.strftime ("%Y/%m/%d") |> equal "2026/04/21" + +[] +let ``test date fromisoformat`` () = + let d = date.fromisoformat "2026-04-21" + d.year |> equal 2026 + d.month |> equal 4 + d.day |> equal 21 + +[] +let ``test date today returns date`` () = + let d = date.today () + d.year > 2024 |> equal true + +[] +let ``test date weekday monday is 0`` () = + // 2026-04-20 is a Monday + let d = date (2026, 4, 20) + d.weekday () |> equal 0 + +[] +let ``test date isoweekday monday is 1`` () = + // 2026-04-20 is a Monday + let d = date (2026, 4, 20) + d.isoweekday () |> equal 1 + +[] +let ``test date replace all fields`` () = + let d = date (2026, 4, 21) + let d2 = d.replace (year = 2027, month = 1, day = 15) + d2.year |> equal 2027 + d2.month |> equal 1 + d2.day |> equal 15 + +[] +let ``test date replace single field`` () = + let d = date (2026, 4, 21) + let d2 = d.replace (year = 2030) + d2.year |> equal 2030 + d2.month |> equal 4 + d2.day |> equal 21 + +[] +let ``test date add timedelta`` () = + let d = date (2026, 4, 21) + let d2 = d.add (timedelta.ofDays 10.0) + d2.day |> equal 1 + d2.month |> equal 5 + +[] +let ``test date sub date returns timedelta`` () = + let d1 = date (2026, 4, 21) + let d2 = date (2026, 4, 11) + let td = d1.sub d2 + td.days |> equal 10 + +[] +let ``test date sub timedelta returns date`` () = + let d = date (2026, 4, 21) + let d2 = d.sub (timedelta.ofDays 21.0) + d2.month |> equal 3 + d2.day |> equal 31 + +// ============================================================================ +// time tests +// ============================================================================ + +[] +let ``test time hour minute second properties`` () = + let t = time (14, 30, 45) + t.hour |> equal 14 + t.minute |> equal 30 + t.second |> equal 45 + +[] +let ``test time isoformat`` () = + let t = time (9, 5, 3) + t.isoformat () |> equal "09:05:03" + +[] +let ``test time fromisoformat`` () = + let t = time.fromisoformat "14:30:00" + t.hour |> equal 14 + t.minute |> equal 30 + t.second |> equal 0 + +[] +let ``test time defaults to zero`` () = + let t = time 0 + t.hour |> equal 0 + t.minute |> equal 0 + t.second |> equal 0 + t.microsecond |> equal 0 + +[] +let ``test time replace single field`` () = + let t = time (9, 30, 0) + let t2 = t.replace (hour = 14) + t2.hour |> equal 14 + t2.minute |> equal 30 + t2.second |> equal 0 + +// ============================================================================ +// datetime tests +// ============================================================================ + +[] +let ``test datetime year month day properties`` () = + let dt = datetime (2026, 4, 21) + dt.year |> equal 2026 + dt.month |> equal 4 + dt.day |> equal 21 + +[] +let ``test datetime hour minute second properties`` () = + let dt = datetime (2026, 4, 21, 14, 30, 59) + dt.hour |> equal 14 + dt.minute |> equal 30 + dt.second |> equal 59 + +[] +let ``test datetime isoformat`` () = + let dt = datetime (2026, 4, 21, 12, 0, 0) + dt.isoformat () |> equal "2026-04-21T12:00:00" + +[] +let ``test datetime fromisoformat`` () = + let dt = datetime.fromisoformat "2026-04-21T14:30:00" + dt.year |> equal 2026 + dt.month |> equal 4 + dt.day |> equal 21 + dt.hour |> equal 14 + dt.minute |> equal 30 + +[] +let ``test datetime now returns current datetime`` () = + let dt = datetime.now () + dt.year > 2024 |> equal true + +[] +let ``test datetime now with timezone`` () = + let dt = datetime.now (timezone.utc) + dt.year > 2024 |> equal true + dt.tzname () |> equal (Some "UTC") + +[] +let ``test datetime strptime`` () = + let dt = datetime.strptime ("21/04/2026", "%d/%m/%Y") + dt.year |> equal 2026 + dt.month |> equal 4 + dt.day |> equal 21 + +[] +let ``test datetime combine date and time`` () = + let d = date (2026, 4, 21) + let t = time (10, 0, 0) + let dt = datetime.combine (d, t) + dt.year |> equal 2026 + dt.hour |> equal 10 + +[] +let ``test datetime date method returns date part`` () = + let dt = datetime (2026, 4, 21, 14, 30, 0) + let d = dt.date () + d.year |> equal 2026 + d.month |> equal 4 + d.day |> equal 21 + +[] +let ``test datetime time method returns time part`` () = + let dt = datetime (2026, 4, 21, 14, 30, 59) + let t = dt.time () + t.hour |> equal 14 + t.minute |> equal 30 + t.second |> equal 59 + +[] +let ``test datetime replace date fields`` () = + let dt = datetime (2026, 4, 21, 12, 0, 0) + let dt2 = dt.replace (year = 2027, month = 6, day = 15) + dt2.year |> equal 2027 + dt2.month |> equal 6 + dt2.day |> equal 15 + dt2.hour |> equal 12 + +[] +let ``test datetime replace time fields`` () = + let dt = datetime (2026, 4, 21, 12, 0, 0) + let dt2 = dt.replace (hour = 9, minute = 30, second = 0) + dt2.hour |> equal 9 + dt2.minute |> equal 30 + dt2.year |> equal 2026 + +[] +let ``test datetime timestamp roundtrip`` () = + let dt1 = datetime.fromisoformat "2026-01-01T00:00:00" + let ts = dt1.timestamp () + let dt2 = datetime.fromtimestamp ts + dt2.year |> equal dt1.year + dt2.month |> equal dt1.month + dt2.day |> equal dt1.day + +[] +let ``test datetime add timedelta`` () = + let dt = datetime (2026, 4, 21, 12, 0, 0) + let dt2 = dt.add (timedelta.ofHours 3.0) + dt2.hour |> equal 15 + +[] +let ``test datetime sub datetime returns timedelta`` () = + let dt1 = datetime (2026, 4, 21, 15, 0, 0) + let dt2 = datetime (2026, 4, 21, 12, 0, 0) + let td = dt1.sub dt2 + td.total_seconds () |> equal 10800.0 + +[] +let ``test datetime sub timedelta returns datetime`` () = + let dt = datetime (2026, 4, 21, 12, 0, 0) + let dt2 = dt.sub (timedelta.ofHours 2.0) + dt2.hour |> equal 10 + +// ============================================================================ +// timezone tests +// ============================================================================ + +[] +let ``test timezone utc name`` () = + let utcName = timezone.utc.tzname (null) + utcName |> equal "UTC" + +[] +let ``test timezone create with offset`` () = + let offset = (timedelta.ofHours 5.0).add (timedelta.ofMinutes 30.0) + let tz = timezone offset + let name = tz.tzname (null) + name |> equal "UTC+05:30" + +[] +let ``test timezone create with name`` () = + let offset = timedelta.ofHours -5.0 + let tz = timezone (offset, "EST") + let name = tz.tzname (null) + name |> equal "EST" diff --git a/test/TestFastAPI.fs b/test/TestFastAPI.fs index 4ec54d8..68ca17f 100644 --- a/test/TestFastAPI.fs +++ b/test/TestFastAPI.fs @@ -52,14 +52,14 @@ let ``test APIRouter can be created with prefix`` () = [] let ``test APIRouter can be created with tags`` () = - let router = APIRouter(tags = ResizeArray ["users"; "admin"]) + let router = APIRouter(tags = ResizeArray [ "users"; "admin" ]) notNull router |> equal true [] let ``test FastAPI app can include router`` () = let app = FastAPI() let router = APIRouter(prefix = "/api") - app.include_router(router) + app.include_router (router) // If we get here without error, the test passes true |> equal true @@ -186,7 +186,7 @@ let ``test Pydantic model works with FastAPI patterns`` () = [] let ``test Pydantic model serialization for FastAPI`` () = let item = Item(Name = "Gadget", Price = 19.99, InStock = false) - let json = item.model_dump_json() + let json = item.model_dump_json () json.Contains("Gadget") |> equal true json.Contains("19.99") |> equal true @@ -202,50 +202,47 @@ let app = FastAPI(title = "Test API", version = "1.0.0") [] type API() = [] - static member root() : obj = - {| message = "Hello World" |} + static member root() : obj = {| message = "Hello World" |} [] static member get_item(item_id: int) : obj = - {| item_id = item_id; name = "Test Item" |} + {| item_id = item_id + name = "Test Item" |} [] - static member create_item(item: Item) : obj = - {| status = "created"; item = item |} + static member create_item(item: Item) : obj = {| status = "created"; item = item |} [] - static member update_item(item_id: int, item: Item) : obj = - {| item_id = item_id; item = item |} + static member update_item(item_id: int, item: Item) : obj = {| item_id = item_id; item = item |} [] - static member delete_item(item_id: int) : obj = - {| deleted = item_id |} + static member delete_item(item_id: int) : obj = {| deleted = item_id |} [] let ``test class-based API methods can be called`` () = - let result = API.root() + let result = API.root () notNull result |> equal true [] let ``test class-based API with path parameter`` () = - let result = API.get_item(42) + let result = API.get_item (42) notNull result |> equal true [] let ``test class-based API POST method`` () = let item = Item(Name = "Test", Price = 10.0, InStock = true) - let result = API.create_item(item) + let result = API.create_item (item) notNull result |> equal true [] let ``test class-based API PUT method`` () = let item = Item(Name = "Updated", Price = 20.0, InStock = false) - let result = API.update_item(1, item) + let result = API.update_item (1, item) notNull result |> equal true [] let ``test class-based API DELETE method`` () = - let result = API.delete_item(1) + let result = API.delete_item (1) notNull result |> equal true // ============================================================================ @@ -262,47 +259,42 @@ let ``test TestClient can be created`` () = // Router-based API pattern // ============================================================================ -let router = APIRouter(prefix = "/users", tags = ResizeArray ["users"]) +let router = APIRouter(prefix = "/users", tags = ResizeArray [ "users" ]) [] type UsersAPI() = [] - static member list_users() : obj = - {| users = [| "Alice"; "Bob" |] |} + static member list_users() : obj = {| users = [| "Alice"; "Bob" |] |} [] - static member get_user(user_id: int) : obj = - {| user_id = user_id |} + static member get_user(user_id: int) : obj = {| user_id = user_id |} [] - static member create_user(name: string) : obj = - {| name = name; id = 1 |} + static member create_user(name: string) : obj = {| name = name; id = 1 |} [] - static member update_user(user_id: int, name: string) : obj = - {| user_id = user_id; name = name |} + static member update_user(user_id: int, name: string) : obj = {| user_id = user_id; name = name |} [] - static member delete_user(user_id: int) : obj = - {| deleted = user_id |} + static member delete_user(user_id: int) : obj = {| deleted = user_id |} [] let ``test router-based API methods work`` () = - let users = UsersAPI.list_users() + let users = UsersAPI.list_users () notNull users |> equal true [] let ``test router can be included in app`` () = let mainApp = FastAPI() let usersRouter = APIRouter(prefix = "/api/v1") - mainApp.include_router(usersRouter) + mainApp.include_router (usersRouter) true |> equal true [] let ``test router can be included with prefix and tags`` () = let mainApp = FastAPI() - let usersRouter = APIRouter(prefix = "/users", tags = ResizeArray ["users"]) - mainApp.include_router_with_prefix_and_tags(usersRouter, "/api/v1", ResizeArray ["api"]) + let usersRouter = APIRouter(prefix = "/users", tags = ResizeArray [ "users" ]) + mainApp.include_router_with_prefix_and_tags (usersRouter, "/api/v1", ResizeArray [ "api" ]) true |> equal true #endif diff --git a/test/TestFunctools.fs b/test/TestFunctools.fs index a0f9bb1..40562a1 100644 --- a/test/TestFunctools.fs +++ b/test/TestFunctools.fs @@ -5,18 +5,15 @@ open Fable.Python.Functools [] let ``test reduce sum works`` () = - functools.reduce ((fun a b -> a + b), [ 1; 2; 3; 4; 5 ]) - |> equal 15 + functools.reduce ((fun a b -> a + b), [ 1; 2; 3; 4; 5 ]) |> equal 15 [] let ``test reduce product works`` () = - functools.reduce ((fun a b -> a * b), [ 1; 2; 3; 4; 5 ]) - |> equal 120 + functools.reduce ((fun a b -> a * b), [ 1; 2; 3; 4; 5 ]) |> equal 120 [] let ``test reduce with initializer works`` () = - functools.reduce ((fun acc x -> acc + x), [ 1; 2; 3 ], 10) - |> equal 16 + functools.reduce ((fun acc x -> acc + x), [ 1; 2; 3 ], 10) |> equal 16 [] let ``test reduce string fold with initializer works`` () = @@ -26,9 +23,11 @@ let ``test reduce string fold with initializer works`` () = [] let ``test lruCache memoises results`` () = let callCount = ResizeArray() + let expensive (x: int) = callCount.Add x x * x + let cached = functools.lruCache (128, expensive) cached 5 |> equal 25 cached 5 |> equal 25 @@ -38,9 +37,11 @@ let ``test lruCache memoises results`` () = [] let ``test cache memoises results`` () = let callCount = ResizeArray() + let expensive (x: int) = callCount.Add x x * 2 + let cached = functools.cache expensive cached 7 |> equal 14 cached 7 |> equal 14 diff --git a/test/TestHeapq.fs b/test/TestHeapq.fs index 6001186..d0dd22d 100644 --- a/test/TestHeapq.fs +++ b/test/TestHeapq.fs @@ -15,13 +15,13 @@ let ``test heappush and heappop work`` () = [] let ``test heapify works`` () = - let heap = ResizeArray [5; 3; 1; 4; 2] + let heap = ResizeArray [ 5; 3; 1; 4; 2 ] heapq.heapify heap heapq.heappop heap |> equal 1 [] let ``test heappushpop works`` () = - let heap = ResizeArray [2; 4; 6] + let heap = ResizeArray [ 2; 4; 6 ] heapq.heapify heap // Push 1, then pop smallest (1) heapq.heappushpop (heap, 1) |> equal 1 @@ -30,10 +30,10 @@ let ``test heappushpop works`` () = [] let ``test nlargest works`` () = - let result = heapq.nlargest (3, [1; 5; 2; 8; 3; 7]) - result |> equal (ResizeArray [8; 7; 5]) + let result = heapq.nlargest (3, [ 1; 5; 2; 8; 3; 7 ]) + result |> equal (ResizeArray [ 8; 7; 5 ]) [] let ``test nsmallest works`` () = - let result = heapq.nsmallest (3, [1; 5; 2; 8; 3; 7]) - result |> equal (ResizeArray [1; 2; 3]) + let result = heapq.nsmallest (3, [ 1; 5; 2; 8; 3; 7 ]) + result |> equal (ResizeArray [ 1; 2; 3 ]) diff --git a/test/TestItertools.fs b/test/TestItertools.fs index 1f9a413..4273d10 100644 --- a/test/TestItertools.fs +++ b/test/TestItertools.fs @@ -5,17 +5,11 @@ open Fable.Python.Itertools [] let ``test count from start works`` () = - itertools.count 1 - |> Seq.take 4 - |> Seq.toList - |> equal [ 1; 2; 3; 4 ] + itertools.count 1 |> Seq.take 4 |> Seq.toList |> equal [ 1; 2; 3; 4 ] [] let ``test count with step works`` () = - itertools.count (0, 2) - |> Seq.take 4 - |> Seq.toList - |> equal [ 0; 2; 4; 6 ] + itertools.count (0, 2) |> Seq.take 4 |> Seq.toList |> equal [ 0; 2; 4; 6 ] [] let ``test cycle works`` () = @@ -26,9 +20,7 @@ let ``test cycle works`` () = [] let ``test repeat with times works`` () = - itertools.repeat ("x", 3) - |> Seq.toList - |> equal [ "x"; "x"; "x" ] + itertools.repeat ("x", 3) |> Seq.toList |> equal [ "x"; "x"; "x" ] [] let ``test accumulate works`` () = @@ -44,9 +36,7 @@ let ``test accumulate with func works`` () = [] let ``test chain two sequences works`` () = - itertools.chain ([ 1; 2 ], [ 3; 4 ]) - |> Seq.toList - |> equal [ 1; 2; 3; 4 ] + itertools.chain ([ 1; 2 ], [ 3; 4 ]) |> Seq.toList |> equal [ 1; 2; 3; 4 ] [] let ``test chain three sequences works`` () = @@ -80,15 +70,11 @@ let ``test filterfalse works`` () = [] let ``test islice with stop works`` () = - itertools.islice ([ 1; 2; 3; 4; 5 ], 3) - |> Seq.toList - |> equal [ 1; 2; 3 ] + itertools.islice ([ 1; 2; 3; 4; 5 ], 3) |> Seq.toList |> equal [ 1; 2; 3 ] [] let ``test islice with start and stop works`` () = - itertools.islice ([ 1; 2; 3; 4; 5 ], 1, 4) - |> Seq.toList - |> equal [ 2; 3; 4 ] + itertools.islice ([ 1; 2; 3; 4; 5 ], 1, 4) |> Seq.toList |> equal [ 2; 3; 4 ] [] let ``test pairwise works`` () = @@ -111,9 +97,7 @@ let ``test combinations works`` () = [] let ``test permutations with r works`` () = - itertools.permutations ([ 1; 2; 3 ], 2) - |> Seq.length - |> equal 6 + itertools.permutations ([ 1; 2; 3 ], 2) |> Seq.length |> equal 6 [] let ``test product two sequences works`` () = diff --git a/test/TestJson.fs b/test/TestJson.fs index 1ba7543..5f2cf5d 100644 --- a/test/TestJson.fs +++ b/test/TestJson.fs @@ -66,7 +66,7 @@ let ``test Json.loads with array works`` () = [] let ``test Json.dumps with indent works`` () = let obj = {| A = 1n |} - let result = Json.dumps(obj, indent = 2) + let result = Json.dumps (obj, indent = 2) result.Contains("\n") |> equal true [] diff --git a/test/TestMath.fs b/test/TestMath.fs index 6141b45..9ff7aea 100644 --- a/test/TestMath.fs +++ b/test/TestMath.fs @@ -93,32 +93,25 @@ let ``test pow works`` () = math.pow (10.0, 2.0) |> equal 100.0 [] -let ``test sin works`` () = - math.sin 0.0 |> equal 0.0 +let ``test sin works`` () = math.sin 0.0 |> equal 0.0 [] -let ``test cos works`` () = - math.cos 0.0 |> equal 1.0 +let ``test cos works`` () = math.cos 0.0 |> equal 1.0 [] -let ``test tan works`` () = - math.tan 0.0 |> equal 0.0 +let ``test tan works`` () = math.tan 0.0 |> equal 0.0 [] -let ``test asin works`` () = - math.asin 0.0 |> equal 0.0 +let ``test asin works`` () = math.asin 0.0 |> equal 0.0 [] -let ``test acos works`` () = - math.acos 1.0 |> equal 0.0 +let ``test acos works`` () = math.acos 1.0 |> equal 0.0 [] -let ``test atan works`` () = - math.atan 0.0 |> equal 0.0 +let ``test atan works`` () = math.atan 0.0 |> equal 0.0 [] -let ``test atan2 works`` () = - math.atan2 (0.0, 1.0) |> equal 0.0 +let ``test atan2 works`` () = math.atan2 (0.0, 1.0) |> equal 0.0 [] let ``test pi constant works`` () = @@ -133,8 +126,7 @@ let ``test tau constant works`` () = math.tau |> fun x -> (x > 6.28318 && x < 6.28319) |> equal true [] -let ``test inf constant works`` () = - math.isinf math.inf |> equal true +let ``test inf constant works`` () = math.isinf math.inf |> equal true [] let ``test sqrt works`` () = @@ -155,8 +147,7 @@ let ``test trunc works`` () = math.trunc -2.7 |> equal -2 [] -let ``test hypot works`` () = - math.hypot (3.0, 4.0) |> equal 5.0 +let ``test hypot works`` () = math.hypot (3.0, 4.0) |> equal 5.0 [] let ``test isqrt works`` () = @@ -168,12 +159,10 @@ let ``test fsum works`` () = math.fsum [ 1.0; 2.0; 3.0 ] |> equal 6.0 [] -let ``test nan constant works`` () = - math.isnan math.nan |> equal true +let ``test nan constant works`` () = math.isnan math.nan |> equal true [] -let ``test prod works`` () = - math.prod [ 1; 2; 3; 4 ] |> equal 24 +let ``test prod works`` () = math.prod [ 1; 2; 3; 4 ] |> equal 24 [] let ``test perm works`` () = @@ -185,36 +174,28 @@ let ``test dist works`` () = math.dist ([| 0.0; 0.0 |], [| 3.0; 4.0 |]) |> equal 5.0 [] -let ``test cosh works`` () = - math.cosh 0.0 |> equal 1.0 +let ``test cosh works`` () = math.cosh 0.0 |> equal 1.0 [] -let ``test sinh works`` () = - math.sinh 0.0 |> equal 0.0 +let ``test sinh works`` () = math.sinh 0.0 |> equal 0.0 [] -let ``test tanh works`` () = - math.tanh 0.0 |> equal 0.0 +let ``test tanh works`` () = math.tanh 0.0 |> equal 0.0 [] -let ``test acosh works`` () = - math.acosh 1.0 |> equal 0.0 +let ``test acosh works`` () = math.acosh 1.0 |> equal 0.0 [] -let ``test asinh works`` () = - math.asinh 0.0 |> equal 0.0 +let ``test asinh works`` () = math.asinh 0.0 |> equal 0.0 [] -let ``test atanh works`` () = - math.atanh 0.0 |> equal 0.0 +let ``test atanh works`` () = math.atanh 0.0 |> equal 0.0 [] -let ``test erf works`` () = - math.erf 0.0 |> equal 0.0 +let ``test erf works`` () = math.erf 0.0 |> equal 0.0 [] -let ``test erfc works`` () = - math.erfc 0.0 |> equal 1.0 +let ``test erfc works`` () = math.erfc 0.0 |> equal 1.0 [] let ``test gamma works`` () = @@ -222,8 +203,7 @@ let ``test gamma works`` () = math.gamma 5.0 |> equal 24.0 [] -let ``test lgamma works`` () = - math.lgamma 1.0 |> equal 0.0 +let ``test lgamma works`` () = math.lgamma 1.0 |> equal 0.0 [] let ``test log with base works`` () = @@ -259,8 +239,8 @@ let ``test isclose works`` () = [] let ``test isclose with tolerances works`` () = - math.isclose (1.0, 1.001, rel_tol=0.01) |> equal true - math.isclose (1.0, 2.0, abs_tol=0.1) |> equal false + math.isclose (1.0, 1.001, rel_tol = 0.01) |> equal true + math.isclose (1.0, 2.0, abs_tol = 0.1) |> equal false [] let ``test nextafter works`` () = diff --git a/test/TestString.fs b/test/TestString.fs index 5d1b808..7cb1c29 100644 --- a/test/TestString.fs +++ b/test/TestString.fs @@ -17,7 +17,8 @@ let ``test string format 2 works`` () = [] let ``test ascii_letters constant`` () = - pyString.ascii_letters |> equal "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + pyString.ascii_letters + |> equal "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" [] let ``test ascii_lowercase constant`` () = @@ -28,16 +29,14 @@ let ``test ascii_uppercase constant`` () = pyString.ascii_uppercase |> equal "ABCDEFGHIJKLMNOPQRSTUVWXYZ" [] -let ``test digits constant`` () = - pyString.digits |> equal "0123456789" +let ``test digits constant`` () = pyString.digits |> equal "0123456789" [] let ``test hexdigits constant`` () = pyString.hexdigits |> equal "0123456789abcdefABCDEF" [] -let ``test octdigits constant`` () = - pyString.octdigits |> equal "01234567" +let ``test octdigits constant`` () = pyString.octdigits |> equal "01234567" [] let ``test punctuation constant`` () = diff --git a/test/TestSys.fs b/test/TestSys.fs index 26b7530..3033dab 100644 --- a/test/TestSys.fs +++ b/test/TestSys.fs @@ -4,28 +4,22 @@ open Fable.Python.Testing open Fable.Python.Sys [] -let ``test sys.platform is non-empty string`` () = - sys.platform.Length > 0 |> equal true +let ``test sys.platform is non-empty string`` () = sys.platform.Length > 0 |> equal true [] -let ``test sys.version is non-empty string`` () = - sys.version.Length > 0 |> equal true +let ``test sys.version is non-empty string`` () = sys.version.Length > 0 |> equal true [] -let ``test sys.maxsize is positive`` () = - sys.maxsize > 0n |> equal true +let ``test sys.maxsize is positive`` () = sys.maxsize > 0n |> equal true [] -let ``test sys.maxunicode is 1114111`` () = - sys.maxunicode |> equal 1114111 +let ``test sys.maxunicode is 1114111`` () = sys.maxunicode |> equal 1114111 [] -let ``test sys.path has at least one element`` () = - sys.path.Count > 0 |> equal true +let ``test sys.path has at least one element`` () = sys.path.Count > 0 |> equal true [] -let ``test sys.argv has at least one element`` () = - sys.argv.Count > 0 |> equal true +let ``test sys.argv has at least one element`` () = sys.argv.Count > 0 |> equal true [] let ``test sys.byteorder is little or big`` () = diff --git a/test/TestTesting.fs b/test/TestTesting.fs index cead90a..a2c0750 100644 --- a/test/TestTesting.fs +++ b/test/TestTesting.fs @@ -11,7 +11,7 @@ let ``test equal passes for equal values`` () = equal 1 1 equal "hello" "hello" equal true true - equal [1; 2; 3] [1; 2; 3] + equal [ 1; 2; 3 ] [ 1; 2; 3 ] [] let ``test equal fails for unequal values`` () = @@ -38,8 +38,7 @@ let ``test notEqual passes for unequal values`` () = notEqual true false [] -let ``test notEqual fails for equal values`` () = - throwsAnyError (fun () -> notEqual 1 1) +let ``test notEqual fails for equal values`` () = throwsAnyError (fun () -> notEqual 1 1) [] let ``test notEqual fails for equal strings`` () = @@ -56,23 +55,18 @@ let ``test throwsAnyError passes when function throws`` () = [] let ``test throwsAnyError fails when function does not throw`` () = // Meta-test: throwsAnyError should fail if the function doesn't throw - throwsAnyError (fun () -> - throwsAnyError (fun () -> 42) - ) + throwsAnyError (fun () -> throwsAnyError (fun () -> 42)) // ============================================================================ // Test doesntThrow // ============================================================================ [] -let ``test doesntThrow passes when function succeeds`` () = - doesntThrow (fun () -> 1 + 1) +let ``test doesntThrow passes when function succeeds`` () = doesntThrow (fun () -> 1 + 1) [] let ``test doesntThrow fails when function throws`` () = - throwsAnyError (fun () -> - doesntThrow (fun () -> failwith "boom") - ) + throwsAnyError (fun () -> doesntThrow (fun () -> failwith "boom")) // ============================================================================ // Test throwsError with exact message @@ -84,15 +78,11 @@ let ``test throwsError passes with matching message`` () = [] let ``test throwsError fails with wrong message`` () = - throwsAnyError (fun () -> - throwsError "expected message" (fun () -> failwith "different message") - ) + throwsAnyError (fun () -> throwsError "expected message" (fun () -> failwith "different message")) [] let ``test throwsError fails when no error thrown`` () = - throwsAnyError (fun () -> - throwsError "expected error" (fun () -> 42) - ) + throwsAnyError (fun () -> throwsError "expected error" (fun () -> 42)) // ============================================================================ // Test throwsErrorContaining @@ -104,12 +94,8 @@ let ``test throwsErrorContaining passes when message contains substring`` () = [] let ``test throwsErrorContaining fails when message does not contain substring`` () = - throwsAnyError (fun () -> - throwsErrorContaining "notfound" (fun () -> failwith "different error message") - ) + throwsAnyError (fun () -> throwsErrorContaining "notfound" (fun () -> failwith "different error message")) [] let ``test throwsErrorContaining fails when no error thrown`` () = - throwsAnyError (fun () -> - throwsErrorContaining "error" (fun () -> 42) - ) + throwsAnyError (fun () -> throwsErrorContaining "error" (fun () -> 42)) diff --git a/test/TestTime.fs b/test/TestTime.fs index e1dba1a..fb9ba4c 100644 --- a/test/TestTime.fs +++ b/test/TestTime.fs @@ -40,8 +40,7 @@ let ``test time.ctime with seconds returns non-empty string`` () = s.Length > 0 |> equal true [] -let ``test time.sleep does not throw`` () = - time.sleep 0.0 +let ``test time.sleep does not throw`` () = time.sleep 0.0 [] let ``test time.timezone is int`` () =