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: 2 additions & 2 deletions .config/dotnet-tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,11 @@
"rollForward": false
},
"fable": {
"version": "4.19.3",
"version": "5.1.0",
"commands": [
"fable"
],
"rollForward": false
}
}
}
}
8 changes: 7 additions & 1 deletion .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ jobs:
python-version: '3.12'
- name: Setup Virtual Environment
run: python -m venv .venv
- name: Install Python dependencies Windows
if: matrix.os == 'windows-latest'
run: .\.venv\Scripts\python.exe -m pip install --disable-pip-version-check .
- name: Install Python dependencies Unix
if: matrix.os == 'ubuntu-latest'
run: ./.venv/bin/python -m pip install --disable-pip-version-check .

# BUILD
- name: make script executable
Expand All @@ -54,4 +60,4 @@ jobs:
run: ./build.sh runtests
- name: Test (Windows)
if: matrix.os == 'windows-latest'
run: .\build.cmd runtests
run: .\build.cmd runtests
10 changes: 10 additions & 0 deletions RELEASE_NOTES.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
### 8.0.0 (Unreleased)

Breaking changes.

- Update Fable tooling and test dependencies to Fable 5.1.0, `fable-library` 5.1.0, and `Fable.Pyxpecto` 2.0.0.
- JavaScript and Python builds now store dynamic members through the internal `Properties` dictionary instead of mirroring them as native object attributes. Use `SetProperty`, `TryGetDynamicPropertyHelper`, `GetPropertyHelpers`, or `GetProperties` to work with dynamic members.
- Fix JavaScript and Python dynamic-property enumeration so compiler-generated backing fields are not returned as dynamic members.
- Fix Python deep copy for `ResizeArray` values by handling native Python lists before the DynamicObj-only collection cases.
- Fix Python native `==` and `hash()` interop so it matches `DynamicObj` structural equality and hashing.

### 7.1.0+96eef97 (Released 2025-10-31)

Extend and test deepHash through guard handling option values. FSharp did not correctly calculate hash codes for equal option values.
Expand Down
2 changes: 1 addition & 1 deletion global.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "8.0.100",
"version": "10.0.204",
"rollForward": "latestMinor"
}
}
7 changes: 7 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[project]
name = "DynamicObj"
version = "8.0.0"
requires-python = ">=3.12"
dependencies = [
"fable-library==5.1.0",
]
50 changes: 19 additions & 31 deletions src/DynamicObj/DynamicObj.fs
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,6 @@
/// </summary>
/// <param name="propertyName">The name of the property to get the PropertyHelper for</param>
member this.TryGetDynamicPropertyHelper (propertyName: string) : PropertyHelper option =
#if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT
FableJS.tryGetDynamicPropertyHelper this propertyName
#endif
#if FABLE_COMPILER_PYTHON
FablePy.tryGetDynamicPropertyHelper this propertyName
#endif
#if !FABLE_COMPILER
match properties.TryGetValue propertyName with
| true,_ ->
Some {
Expand All @@ -64,7 +57,6 @@
RemoveValue = fun o -> properties.Remove(propertyName) |> ignore
}
| _ -> None
#endif

/// <summary>
/// Returns Some(PropertyHelper) if a property (static or dynamic) with the given name exists, otherwise None.
Expand All @@ -75,7 +67,7 @@
| Some pi -> Some pi
| None -> this.TryGetDynamicPropertyHelper propertyName

/// <summary>

Check warning on line 70 in src/DynamicObj/DynamicObj.fs

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

This XML comment is incomplete: no documentation for parameter 'propertyName'

Check warning on line 70 in src/DynamicObj/DynamicObj.fs

View workflow job for this annotation

GitHub Actions / test (windows-latest)

This XML comment is incomplete: no documentation for parameter 'propertyName'
/// Returns Some(boxed property value) if a dynamic (or static) property with the given name exists, otherwise None.
/// </summary>
/// <param name="name">the name of the property to get</param>
Expand Down Expand Up @@ -131,18 +123,10 @@
else
failwith $"Cannot set value for static, immutable property \"{propertyName}\""
| None ->
#if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT
FableJS.setPropertyValue this propertyName propertyValue
#endif
#if FABLE_COMPILER_PYTHON
FablePy.setPropertyValue this propertyName propertyValue
#endif
#if !FABLE_COMPILER
// Next check the Properties collection for member
match properties.TryGetValue propertyName with
| true,_ -> properties.[propertyName] <- propertyValue
| _ -> properties.Add(propertyName,propertyValue)
#endif

/// <summary>
/// Removes any dynamic property with the given name from the input DynamicObj.
Expand All @@ -167,19 +151,6 @@
/// </summary>
/// <param name="includeInstanceProperties">whether to include instance properties (= 'static' properties on the class)</param>
member this.GetPropertyHelpers (includeInstanceProperties: bool) =
#if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT
FableJS.getPropertyHelpers this
|> Seq.filter (fun pd ->
includeInstanceProperties || pd.IsDynamic
)
#endif
#if FABLE_COMPILER_PYTHON
FablePy.getPropertyHelpers this
|> Seq.filter (fun pd ->
includeInstanceProperties || pd.IsDynamic
)
#endif
#if !FABLE_COMPILER
seq [
if includeInstanceProperties then
yield! ReflectionUtils.getStaticProperties (this)
Expand All @@ -195,7 +166,6 @@
RemoveValue = fun o -> properties.Remove(key) |> ignore
}
]
#endif
|> Seq.filter (fun p -> p.Name.ToLower() <> "properties")

/// <summary>
Expand Down Expand Up @@ -402,6 +372,18 @@
this.StructurallyEquals(other)
| _ -> false

#if FABLE_COMPILER_PYTHON
// Python calls these dunder methods for native `hash(x)` and `x == y`.
member this.``__hash__``() =
HashUtils.deepHash this
|> FablePy.toPythonInt

member this.``__eq__``(o: obj) =
match o with
| :? DynamicObj as other -> this.StructurallyEquals(other)
| _ -> false
#endif

and HashUtils =

static member deepHash (o:obj) =
Expand Down Expand Up @@ -995,6 +977,12 @@
#endif

// These collections of DynamicObj can be cloned recursively
#if FABLE_COMPILER_PYTHON
| o when FablePy.ResizeArrays.isResizeArray o ->
let o = o |> unbox<ResizeArray<obj>>
ResizeArray([for item in o -> tryDeepCopyObj item])
|> box
#endif
| :? ResizeArray<DynamicObj> as dyns ->
box (ResizeArray([for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]))
#if !FABLE_COMPILER
Expand Down Expand Up @@ -1024,4 +1012,4 @@
box newDyn
| _ -> o

tryDeepCopyObj o
tryDeepCopyObj o
26 changes: 17 additions & 9 deletions src/DynamicObj/FablePy.fs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ open Fable.Core
open System.Collections.Generic

module FablePy =

[<Emit("int($0)")>]
let toPythonInt (value:int) : int =
nativeOnly

module Dictionary =

Expand Down Expand Up @@ -88,17 +92,19 @@ module FablePy =


[<Emit("vars($0).items()")>]
let getOwnMemberObjects (o:obj) : Dictionary<string,obj> =
let getOwnMemberObjects (o:obj) : seq<string * obj> =
nativeOnly

[<Emit("$0.__class__")>]
let getClass (o:obj) : obj =
nativeOnly

let getStaticPropertyObjects (o:obj) : Dictionary<string,PropertyObject> =
let getStaticPropertyObjects (o:obj) : seq<string * PropertyObject> =
getClass o
|> getOwnMemberObjects
|> Dictionary.choose PropertyObject.tryProperty
|> Seq.choose (fun (name, value) ->
PropertyObject.tryProperty value
|> Option.map (fun property -> name, property))

let removeStaticPropertyValue (o:obj) (propName:string) =
setPropertyValue o propName null
Expand Down Expand Up @@ -165,8 +171,7 @@ module FablePy =

let getDynamicPropertyHelpers (o:obj) : PropertyHelper [] =
getOwnMemberObjects o
|> Seq.choose (fun kv ->
let n = kv.Key
|> Seq.choose (fun (n, _) ->
if isTranspiledPropertyHelper n then
None
else
Expand All @@ -187,9 +192,7 @@ module FablePy =

let getStaticPropertyHelpers (o:obj) : PropertyHelper [] =
getStaticPropertyObjects o
|> Seq.map (fun kv ->
let n = kv.Key
let po = kv.Value
|> Seq.map (fun (n, po) ->
{
Name = n
IsStatic = true
Expand Down Expand Up @@ -226,4 +229,9 @@ module FablePy =
let isDict (o:obj) : bool =
nativeOnly

#endif
module ResizeArrays =
[<Emit("""isinstance($0, list)""")>]
let isResizeArray (o:obj) : bool =
nativeOnly

#endif
25 changes: 24 additions & 1 deletion tests/DynamicObject.Tests/DynamicObj/Equals.fs
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
module Equals

open Fable.Pyxpecto
open Fable.Core
open DynamicObj
open TestUtils

#if FABLE_COMPILER_PYTHON
module Native =
[<Emit("$0 == $1")>]
let equals (left: obj) (right: obj) : bool =
nativeOnly

[<Emit("hash($0)")>]
let hash (value: obj) : int =
nativeOnly
#endif

let tests_Equals = testList "Equals" [
testCase "Same Object" <| fun _ ->
let a = DynamicObj()
Expand Down Expand Up @@ -35,4 +47,15 @@ let tests_Equals = testList "Equals" [
a2.SetProperty("b", b2)
Expect.isTrue (a.Equals(a2)) "Values should be equal"

]
#if FABLE_COMPILER_PYTHON
testCase "Python native equality and hash use structural dynamic properties" <| fun _ ->
let a = DynamicObj()
a.SetProperty("b", 2)
let a2 = DynamicObj()
a2.SetProperty("b", 2)

Expect.isTrue (Native.equals a a2) "Python == should use structural equality"
Expect.equal (Native.hash a) (Native.hash a2) "Python hash() should use structural hashing"
#endif

]
30 changes: 29 additions & 1 deletion tests/DynamicObject.Tests/DynamicObj/GetProperties.fs
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
module GetProperties

open Fable.Pyxpecto
open Fable.Core
open DynamicObj
open TestUtils

#if FABLE_COMPILER
module Native =
#if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT
[<Emit("$0[$1] = $2")>]
let setMember (o: obj) (propertyName: string) (propertyValue: obj) : unit =
jsNative
#endif

#if FABLE_COMPILER_PYTHON
[<Emit("setattr($0, $1, $2)")>]
let setMember (o: obj) (propertyName: string) (propertyValue: obj) : unit =
nativeOnly
#endif
#endif

let tests_GetProperties = testList "GetProperties" [
testCase "GetProperties" <| fun _ ->
let a = DynamicObj()
Expand All @@ -25,4 +41,16 @@ let tests_GetProperties = testList "GetProperties" [
]
|> Seq.sortBy (fun kv -> kv.Key)
Expect.sequenceEqual properties expected "Should have all properties"
]

testCase "ignores compiler backing fields when returning dynamic properties" <| fun _ ->
let a = DynamicObj()
a.SetProperty("dyn", "dyn")
#if FABLE_COMPILER
Native.setMember a "_stat" "stat"
#endif
let properties = a.GetProperties(false) |> List.ofSeq
let expected = [
System.Collections.Generic.KeyValuePair("dyn", box "dyn")
]
Expect.sequenceEqual properties expected "Should only return explicit dynamic properties"
]
2 changes: 1 addition & 1 deletion tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Fable.Pyxpecto" Version="1.2.0" />
<PackageReference Include="Fable.Pyxpecto" Version="2.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.9.4" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
Expand Down
Loading
Loading