Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Deep copy improvements #46

Merged
merged 7 commits into from
Jan 27, 2025
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
32 changes: 31 additions & 1 deletion src/DynamicObj/DynObj.fs
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,34 @@ module DynObj =
/// Prints a formatted string containing all static and dynamic properties of the given DynamicObj
/// </summary>
/// <param name="dynObj">The DynamicObj for which to print a formatted string for</param>
let print (dynObj:DynamicObj) = printfn "%s" (dynObj |> format)
let print (dynObj:DynamicObj) = printfn "%s" (dynObj |> format)

/// <summary>
/// function to deep copy a boxed object (if possible)
///
/// The following cases are handled (in this precedence):
///
/// - Basic F# types (bool, byte, sbyte, int16, uint16, int, uint, int64, uint64, nativeint, unativeint, float, float32, char, string, unit, decimal)
///
/// - ResizeArrays and Dictionaries containing any combination of basic F# types
///
/// - Dictionaries containing DynamicObj as keys or values in any combination with DynamicObj or basic F# types as keys or values
///
/// - array&lt;DynamicObj&gt;, list&lt;DynamicObj&gt;, ResizeArray&lt;DynamicObj&gt;: These collections of DynamicObj are copied as a new collection with recursively deep copied elements.
///
/// - System.ICloneable: If the property implements ICloneable, the Clone() method is called on the property.
///
/// - DynamicObj (and derived classes): properties that are themselves DynamicObj instances are deep copied recursively.
/// if a derived class has static properties (e.g. instance properties), these will be copied as dynamic properties on the new instance.
///
/// Note on Classes that inherit from DynamicObj:
///
/// Classes that inherit from DynamicObj will match the `DynamicObj` typecheck if they do not implement ICloneable.
/// The deep copied instances will be cast to DynamicObj with static/instance properties AND dynamic properties all set as dynamic properties.
/// It should be possible to 'recover' the original type by checking if the needed properties exist as dynamic properties,
/// and then passing them to the class constructor if needed.
/// </summary>
/// <param name="o">The object that should be deep copied</param>
/// <param name="includeInstanceProperties">Whether to include instance properties (= 'static' properties on the class) as dynamic properties on the new instance for matched DynamicObj.</param>
let tryDeepCopyObj (includeInstanceProperties:bool) (o:DynamicObj) =
CopyUtils.tryDeepCopyObj(o, includeInstanceProperties)
605 changes: 582 additions & 23 deletions src/DynamicObj/DynamicObj.fs

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions src/DynamicObj/FableJS.fs
Original file line number Diff line number Diff line change
Expand Up @@ -168,4 +168,11 @@ module FableJS =
let cloneICloneable (o:obj) : obj =
jsNative

module Dictionaries =
[<Emit("""$0 instanceof Map""")>]
let isMap (o:obj) : bool =
jsNative
[<Emit("""$0 instanceof Dictionary""")>]
let isDict (o:obj) : bool =
jsNative
#endif
6 changes: 6 additions & 0 deletions src/DynamicObj/FablePy.fs
Original file line number Diff line number Diff line change
Expand Up @@ -220,4 +220,10 @@ module FablePy =
[<Emit("""$0.System_ICloneable_Clone()""")>]
let cloneICloneable (o:obj) : obj =
nativeOnly

module Dictionaries =
[<Emit("""isinstance($0, dict)""")>]
let isDict (o:obj) : bool =
nativeOnly

#endif
20 changes: 6 additions & 14 deletions src/DynamicObj/Playground.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,18 +13,10 @@
open Fable.Pyxpecto
open DynamicObj

type T(dyn:string, stat:string) as this=
inherit DynamicObj()
let r1 = ResizeArray([1; 2])
let r2 = ResizeArray([1; 2])
let r3 = r1

do
this.SetProperty("Dyn", dyn)

member this.Stat = stat

let first = T("dyn1", "stat1")
let second = T("dyn2", "stat2")

let _ = second.ShallowCopyDynamicPropertiesTo(first)

first |> DynObj.print
second |> DynObj.print
printfn "%A" (LanguagePrimitives.PhysicalEquality r1 r2)
printfn "%A" (LanguagePrimitives.PhysicalEquality r2 r2)
printfn "%A" (LanguagePrimitives.PhysicalEquality r3 r1)
2 changes: 1 addition & 1 deletion tests/CSharpTests/CSharpTests.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>

Expand Down
2,563 changes: 2,563 additions & 0 deletions tests/DynamicObject.Tests/CopyUtils.tryDeepCopyObj/Dictionaries.fs

Large diffs are not rendered by default.

Copy link
Member

Choose a reason for hiding this comment

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

These are the missing tests you referenced in #48?

Copy link
Member Author

Choose a reason for hiding this comment

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

https://github.com/CSBiology/DynamicObj/blob/main/tests/DynamicObject.Tests/DynamicObj/DeepCopyProperties.fs Contains a few tests for DynamicObj.DeepCopyProperties that covers nested Dynamic Objects. I was mainly referring to the underlying function CopyUtils.tryDeepCopyObj still missing tests

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module DeepCopyDynamicObj

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module DeepCopyDynamicObjCollections

Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module DeepCopyICloneable

13 changes: 13 additions & 0 deletions tests/DynamicObject.Tests/CopyUtils.tryDeepCopyObj/Main.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module CopyUtils.Tests

open System
open Fable.Pyxpecto
open DynamicObj
open Fable.Core

let main = testList "CopyUtils.tryDeepCopyObj" [
DeepCopyPrimitives.tests_DeepCopyPrimitives
DeepCopyResizeArrays.tests_DeepCopyResizeArrays
DeepCopyDictionaries.tests_DeepCopyDictionaries
]

60 changes: 60 additions & 0 deletions tests/DynamicObject.Tests/CopyUtils.tryDeepCopyObj/Primitives.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
module DeepCopyPrimitives

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

let tests_DeepCopyPrimitives = testList "Primitives" [
testCase "bool" <| fun _ ->
let original, copy = constructDeepCopiedObj true
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "byte" <| fun _ ->
let original, copy = constructDeepCopiedObj 1uy
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "sbyte" <| fun _ ->
let original, copy = constructDeepCopiedObj 1y
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "int16" <| fun _ ->
let original, copy = constructDeepCopiedObj 1s
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "uint16" <| fun _ ->
let original, copy = constructDeepCopiedObj 1us
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "int" <| fun _ ->
let original, copy = constructDeepCopiedObj 1
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "uint" <| fun _ ->
let original, copy = constructDeepCopiedObj 1u
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "int64" <| fun _ ->
let original, copy = constructDeepCopiedObj 1L
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "uint64" <| fun _ ->
let original, copy = constructDeepCopiedObj 1uL
Expect.equal copy original "Expected values of copy and original to be equal"
#if !FABLE_COMPILER
testCase "nativeint" <| fun _ ->
let original, copy = constructDeepCopiedObj (System.IntPtr(1))
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "unativeint" <| fun _ ->
let original, copy = constructDeepCopiedObj (System.UIntPtr(1u))
Expect.equal copy original "Expected values of copy and original to be equal"
#endif
testCase "float" <| fun _ ->
let original, copy = constructDeepCopiedObj 1.0
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "float32" <| fun _ ->
let original, copy = constructDeepCopiedObj 1.0f
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "char" <| fun _ ->
let original, copy = constructDeepCopiedObj 'A'
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "string" <| fun _ ->
let original, copy = constructDeepCopiedObj "Hi"
Expect.equal copy original "Expected values of copy and original to be equal"
testCase "unit" <| fun _ ->
let original, copy = constructDeepCopiedObj ()
Expect.equal copy original "Expected values of copy and original to be equal"
]
148 changes: 148 additions & 0 deletions tests/DynamicObject.Tests/CopyUtils.tryDeepCopyObj/ResizeArrays.fs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
module DeepCopyResizeArrays

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

let tests_DeepCopyResizeArrays = testList "ResizeArrays" [
testCase "bool" <| fun _ ->
let arr = ResizeArray([true; false])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- false
Expect.sequenceEqual original (ResizeArray([false; false])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([true; false])) "Clone should not be affected by original mutation"
testCase "byte" <| fun _ ->
let arr = ResizeArray([1uy; 2uy])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2uy
Expect.sequenceEqual original (ResizeArray([2uy; 2uy])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1uy; 2uy])) "Clone should not be affected by original mutation"
testCase "sbyte" <| fun _ ->
let arr = ResizeArray([1y; 2y])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2y
Expect.sequenceEqual original (ResizeArray([2y; 2y])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1y; 2y])) "Clone should not be affected by original mutation"
testCase "int16" <| fun _ ->
let arr = ResizeArray([1s; 2s])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2s
Expect.sequenceEqual original (ResizeArray([2s; 2s])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1s; 2s])) "Clone should not be affected by original mutation"
testCase "uint16" <| fun _ ->
let arr = ResizeArray([1us; 2us])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2us
Expect.sequenceEqual original (ResizeArray([2us; 2us])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1us; 2us])) "Clone should not be affected by original mutation"
testCase "int" <| fun _ ->
let arr = ResizeArray([1; 2])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2
Expect.sequenceEqual original (ResizeArray([2; 2])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1; 2])) "Clone should not be affected by original mutation"
testCase "uint" <| fun _ ->
let arr = ResizeArray([1u; 2u])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2u
Expect.sequenceEqual original (ResizeArray([2u; 2u])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1u; 2u])) "Clone should not be affected by original mutation"
testCase "int64" <| fun _ ->
let arr = ResizeArray([1L; 2L])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2L
Expect.sequenceEqual original (ResizeArray([2L; 2L])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1L; 2L])) "Clone should not be affected by original mutation"
testCase "uint64" <| fun _ ->
let arr = ResizeArray([1uL; 2uL])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2uL
Expect.sequenceEqual original (ResizeArray([2uL; 2uL])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1uL; 2uL])) "Clone should not be affected by original mutation"
testCase "float" <| fun _ ->
let arr = ResizeArray([1.0; 2.0])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2.0
Expect.sequenceEqual original (ResizeArray([2.0; 2.0])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1.0; 2.0])) "Clone should not be affected by original mutation"
testCase "float32" <| fun _ ->
let arr = ResizeArray([1.0f; 2.0f])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2.0f
Expect.sequenceEqual original (ResizeArray([2.0f; 2.0f])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1.0f; 2.0f])) "Clone should not be affected by original mutation"
testCase "char" <| fun _ ->
let arr = ResizeArray(['A'; 'B'])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 'B'
Expect.sequenceEqual original (ResizeArray(['B'; 'B'])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray(['A'; 'B'])) "Clone should not be affected by original mutation"
testCase "string" <| fun _ ->
let arr = ResizeArray(["Hi"; "Bye"])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- "Bye"
Expect.sequenceEqual original (ResizeArray(["Bye"; "Bye"])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray(["Hi"; "Bye"])) "Clone should not be affected by original mutation"
testCase "unit" <| fun _ ->
let arr = ResizeArray([(); ()])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
// transpilation fun
let arr2 = ResizeArray([()])
arr.Add(arr2[0])
Expect.sequenceEqual original (ResizeArray([(); (); ()])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([(); ()])) "Clone should not be affected by original mutation"

// some cases are not transpilable

#if !FABLE_COMPILER_PYTHON
testCase "decimal" <| fun _ ->
let arr = ResizeArray([1.0M; 2.0M])
let original, copy = constructDeepCopiedObj arr
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
arr[0] <- 2.0M
Expect.sequenceEqual original (ResizeArray([2.0M; 2.0M])) "Original schould have been mutated"
Expect.sequenceEqual copy (ResizeArray([1.0M; 2.0M])) "Clone should not be affected by original mutation"
#endif

#if !FABLE_COMPILER
testCase "nativeint" <| fun _ ->
let original, copy = constructDeepCopiedObj (ResizeArray([System.IntPtr(1); System.IntPtr(2)]))
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
testCase "unativeint" <| fun _ ->
let original, copy = constructDeepCopiedObj (ResizeArray([System.UIntPtr(1u); System.UIntPtr(2u)]))
Expect.sequenceEqual copy original "Expected values of copy and original to be equal"
Expect.notReferenceEqual copy original "Expected values of copy and original to be not reference equal"
#endif
]
7 changes: 7 additions & 0 deletions tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@

<ItemGroup>
<Compile Include="TestUtils.fs" />
<Compile Include="CopyUtils.tryDeepCopyObj\Primitives.fs" />
<Compile Include="CopyUtils.tryDeepCopyObj\ResizeArrays.fs" />
<Compile Include="CopyUtils.tryDeepCopyObj\Dictionaries.fs" />
<Compile Include="CopyUtils.tryDeepCopyObj\DynamicObjCollections.fs" />
<Compile Include="CopyUtils.tryDeepCopyObj\ICloneable.fs" />
<Compile Include="CopyUtils.tryDeepCopyObj\DynamicObj.fs" />
<Compile Include="CopyUtils.tryDeepCopyObj\Main.fs" />
<Compile Include="DynamicObj\RemoveProperty.fs" />
<Compile Include="DynamicObj\SetProperty.fs" />
<Compile Include="DynamicObj\GetHashcode.fs" />
Expand Down
1 change: 1 addition & 0 deletions tests/DynamicObject.Tests/Main.fs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ open Fable.Pyxpecto

let all = testSequenced <| testList "DynamicObj" [
ReflectionUtils.Tests.main
CopyUtils.Tests.main
DynamicObj.Tests.main
DynObj.Tests.main
Inheritance.Tests.main
Expand Down
Loading
Loading