diff --git a/build/BasicTasks.fs b/build/BasicTasks.fs
index 93bc5b4..3b5ec8e 100644
--- a/build/BasicTasks.fs
+++ b/build/BasicTasks.fs
@@ -152,6 +152,8 @@ let clean = BuildTask.create "Clean" [] {
++ "src/**/obj"
++ "tests/**/bin"
++ "tests/**/obj"
+ ++ "tests/**/js"
+ ++ "tests/**/py"
++ "dist"
++ ProjectInfo.netPkgDir
|> Shell.cleanDirs
diff --git a/build/TestTasks.fs b/build/TestTasks.fs
index b5d712f..d95ce7e 100644
--- a/build/TestTasks.fs
+++ b/build/TestTasks.fs
@@ -22,7 +22,7 @@ module RunTests =
let runTestsJs = BuildTask.create "runTestsJS" [clean; build] {
for path in ProjectInfo.fableTestProjects do
// transpile js files from fsharp code
- run dotnet $"fable {path} -o {path}/js" ""
+ run dotnet $"fable {path} -o {path}/js --noCache" ""
// run mocha in target path to execute tests
// "--timeout 20000" is used, because json schema validation takes a bit of time.
run node $"{path}/js/Main.js" ""
@@ -40,7 +40,7 @@ module RunTests =
let runTestsPy = BuildTask.create "runTestsPy" [clean; build] {
for path in ProjectInfo.fableTestProjects do
//transpile py files from fsharp code
- run dotnet $"fable {path} -o {path}/py --lang python" ""
+ run dotnet $"fable {path} -o {path}/py --lang python --noCache" ""
// run pyxpecto in target path to execute tests in python
run python $"{path}/py/main.py" ""
}
diff --git a/src/DynamicObj/DynamicObj.fs b/src/DynamicObj/DynamicObj.fs
index 5c13094..43bd318 100644
--- a/src/DynamicObj/DynamicObj.fs
+++ b/src/DynamicObj/DynamicObj.fs
@@ -245,36 +245,19 @@ type DynamicObj() =
)
///
- /// Attempts to deep copy the properties of the DynamicObj onto the target.
- ///
- /// As many properties as possible are re-instantiated as new objects, meaning the
- /// copy has as little reference equal properties as possible.
- ///
- /// The nature of DynamicObj however means that it is impossible to reliably deep copy all properties, as
- /// their type is not known on runtime and the contructors of the types are not known.
- ///
- /// The following cases are handled (in this precedence):
- ///
- /// - Basic F# types (int, float, bool, string, char, byte, sbyte, int16, uint16, int32, uint32, int64, uint64, single, decimal)
- ///
- /// - array<DynamicObj>, list<DynamicObj>, ResizeArray<DynamicObj>: 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:
+ /// Returns a new DynamicObj with only the dynamic properties of the original DynamicObj (sans instance properties).
///
- /// Classes that inherit from DynamicObj will match the `DynamicObj` typecheck if they do not implement ICloneable.
- /// The deep coopied 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.
+ /// Note that this function does not attempt to do any deep copying.
+ /// The dynamic properties of the source will be copied as references to the target.
+ /// If any of those properties are mutable or themselves DynamicObj instances, changes to the properties on the source will be reflected in the target.
///
- /// The target object to copy dynamic members to
- /// Whether existing properties on the target object will be overwritten
- member this.DeepCopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) =
- let overWrite = defaultArg overWrite false
+ member this.ShallowCopyDynamicProperties() =
+ let target = DynamicObj()
+ this.ShallowCopyDynamicPropertiesTo(target, true)
+ target
+
+ // internal helper function to deep copy a boxed object (if possible)
+ static member internal tryDeepCopyObj (o:obj) =
let rec tryDeepCopyObj (o:obj) =
match o with
@@ -298,9 +281,14 @@ type DynamicObj() =
box [for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]
| :? ResizeArray as dyns ->
box (ResizeArray([for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]))
-
- #if !FABLE_COMPILER_PYTHON
+ #if FABLE_COMPILER_JAVASCRIPT || FABLE_COMPILER_TYPESCRIPT
+ | o when FableJS.Interfaces.implementsICloneable o -> FableJS.Interfaces.cloneICloneable o
+ #endif
+ #if FABLE_COMPILER_PYTHON
// https://github.com/fable-compiler/Fable/issues/3972
+ | o when FablePy.Interfaces.implementsICloneable o -> FablePy.Interfaces.cloneICloneable o
+ #endif
+ #if !FABLE_COMPILER
| :? System.ICloneable as clonable -> clonable.Clone()
#endif
@@ -312,24 +300,47 @@ type DynamicObj() =
box newDyn
| _ -> o
+ tryDeepCopyObj o
+
+ ///
+ /// Attempts to deep copy the properties of the DynamicObj onto the target.
+ ///
+ /// As many properties as possible are re-instantiated as new objects, meaning the
+ /// copy has as little reference equal properties as possible.
+ ///
+ /// The nature of DynamicObj however means that it is impossible to reliably deep copy all properties, as
+ /// their type is not known on runtime and the contructors of the types are not known.
+ ///
+ /// The following cases are handled (in this precedence):
+ ///
+ /// - Basic F# types (int, float, bool, string, char, byte, sbyte, int16, uint16, int32, uint32, int64, uint64, single, decimal)
+ ///
+ /// - array<DynamicObj>, list<DynamicObj>, ResizeArray<DynamicObj>: 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 coopied 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.
+ ///
+ /// The target object to copy dynamic members to
+ /// Whether existing properties on the target object will be overwritten
+ member this.DeepCopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) =
+ let overWrite = defaultArg overWrite false
+
this.GetProperties(true)
|> Seq.iter (fun kv ->
match target.TryGetPropertyHelper kv.Key with
- | Some pi when overWrite -> pi.SetValue target (tryDeepCopyObj kv.Value)
+ | Some pi when overWrite -> pi.SetValue target (DynamicObj.tryDeepCopyObj kv.Value)
| Some _ -> ()
- | None -> target.SetProperty(kv.Key, tryDeepCopyObj kv.Value)
+ | None -> target.SetProperty(kv.Key, DynamicObj.tryDeepCopyObj kv.Value)
)
- ///
- /// Returns a new DynamicObj with only the dynamic properties of the original DynamicObj (sans instance properties).
- ///
- /// Note that this function does not attempt to do any deep copying.
- /// The dynamic properties of the source will be copied as references to the target.
- /// If any of those properties are mutable or themselves DynamicObj instances, changes to the properties on the source will be reflected in the target.
- ///
- member this.ShallowCopyDynamicProperties() =
- let target = DynamicObj()
- this.ShallowCopyDynamicPropertiesTo(target)
- target
///
/// Attempts to perform a deep copy of the DynamicObj.
@@ -360,10 +371,7 @@ type DynamicObj() =
///
/// The target object to copy dynamic members to
/// Whether existing properties on the target object will be overwritten
- member this.DeepCopyDynamicProperties() =
- let target = DynamicObj()
- this.DeepCopyDynamicPropertiesTo(target)
- target
+ member this.DeepCopyDynamicProperties() = DynamicObj.tryDeepCopyObj this
#if !FABLE_COMPILER
// Some necessary overrides for methods inherited from System.Dynamic.DynamicObject()
diff --git a/src/DynamicObj/FableJS.fs b/src/DynamicObj/FableJS.fs
index 7d889b7..ff457ff 100644
--- a/src/DynamicObj/FableJS.fs
+++ b/src/DynamicObj/FableJS.fs
@@ -158,5 +158,14 @@ module FableJS =
getPropertyHelpers o
|> Array.map (fun h -> h.Name)
+ module Interfaces =
+
+ []
+ let implementsICloneable (o:obj) : bool =
+ jsNative
+
+ []
+ let cloneICloneable (o:obj) : obj =
+ jsNative
#endif
\ No newline at end of file
diff --git a/src/DynamicObj/FablePy.fs b/src/DynamicObj/FablePy.fs
index 4d706b9..1bc575e 100644
--- a/src/DynamicObj/FablePy.fs
+++ b/src/DynamicObj/FablePy.fs
@@ -211,5 +211,13 @@ module FablePy =
getPropertyHelpers o
|> Array.map (fun h -> h.Name)
+ module Interfaces =
+
+ []
+ let implementsICloneable (o:obj) : bool =
+ nativeOnly
+ []
+ let cloneICloneable (o:obj) : obj =
+ nativeOnly
#endif
\ No newline at end of file
diff --git a/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs
index 3c50956..a0abd9a 100644
--- a/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs
+++ b/tests/DynamicObject.Tests/DynamicObjs/DeepCopyDynamicProperties.fs
@@ -26,7 +26,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
"single", box (single 1.0f)
"decimal", box (decimal 1M)
]
- let original, clone = constructDeepCopiedClone originalProps
+ let original, clone = constructDeepCopiedClone originalProps
let mutatedProps = [
"int", box 2
"float", box 2.0
@@ -51,7 +51,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
testCase "DynamicObj" <| fun _ ->
let inner = DynamicObj() |> DynObj.withProperty "inner int" 2
- let original, clone = constructDeepCopiedClone ["dyn", inner]
+ let original, clone = constructDeepCopiedClone ["dyn", inner]
inner.SetProperty("inner int", 1)
Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original"
Expect.equal (original |> DynObj.getNestedPropAs ["dyn";"inner int"]) 1 "Original should have mutated properties"
@@ -61,7 +61,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let first_level = DynamicObj() |> DynObj.withProperty "lvl1" 1
let second_level = DynamicObj() |> DynObj.withProperty "lvl2" 2
first_level.SetProperty("second_level", second_level)
- let original, clone = constructDeepCopiedClone ["first_level", first_level]
+ let original, clone = constructDeepCopiedClone ["first_level", first_level]
second_level.SetProperty("lvl2", -1)
Expect.notEqual original clone "Original and clone should not be equal after mutating DynamicObj prop on original"
Expect.equal (original |> DynObj.getNestedPropAs ["first_level";"second_level";"lvl2"]) -1 "Original should have mutated properties"
@@ -72,7 +72,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let item2 = DynamicObj() |> DynObj.withProperty "item" 2
let item3 = DynamicObj() |> DynObj.withProperty "item" 3
let arr = [|item1; item2; item3|]
- let original, clone = constructDeepCopiedClone ["arr", box arr]
+ let original, clone = constructDeepCopiedClone ["arr", box arr]
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
@@ -87,7 +87,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let item2 = DynamicObj() |> DynObj.withProperty "item" 2
let item3 = DynamicObj() |> DynObj.withProperty "item" 3
let l = [item1; item2; item3]
- let original, clone = constructDeepCopiedClone ["list", box l]
+ let original, clone = constructDeepCopiedClone ["list", box l]
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
@@ -102,7 +102,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let item2 = DynamicObj() |> DynObj.withProperty "item" 2
let item3 = DynamicObj() |> DynObj.withProperty "item" 3
let r = ResizeArray([item1; item2; item3])
- let original, clone = constructDeepCopiedClone ["resizeArr", box r]
+ let original, clone = constructDeepCopiedClone ["resizeArr", box r]
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
@@ -115,7 +115,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
testList "Un-Cloneable dynamic properties" [
testCase "Class with mutable fields is reference equal" <| fun _ ->
let item = MutableClass("initial")
- let original, clone = constructDeepCopiedClone ["item", box item]
+ let original, clone = constructDeepCopiedClone ["item", box item]
item.stat <- "mutated"
let originalProp = original |> DynObj.getNestedPropAs["item"]
let clonedProp = clone |> DynObj.getNestedPropAs ["item"]
@@ -126,39 +126,60 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
]
]
testList "Derived class implementing ICloneable" [
- testList "Cloneable dynamic properties" [
- testCase "primitives" <| fun _ ->
- ()
- testCase "DynamicObj" <| fun _ ->
- ()
- testCase "DynamicObj array" <| fun _ ->
- ()
- testCase "DynamicObj list" <| fun _ ->
- ()
- testCase "DynamicObj ResizeArray" <| fun _ ->
- ()
- ]
- testList "Un-Cloneable dynamic properties" [
- testCase "Class with mutable fields is reference equal" <| fun _ ->
- ()
- ]
- testList "static properties" [
- testCase "Class with mutable fields is reference equal" <| fun _ ->
- ()
+ testList "SpecialCases" [
+ testCase "can unbox copy as DerivedClassCloneable" <| fun _ ->
+ Expect.pass (
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ ()
+ )
+ testCase "copy is of type DerivedClassCloneable" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ Expect.equal (clone.GetType()) typeof "Clone is of type DerivedClassCloneable"
+ ptestCase "copy has NO instance prop as dynamic prop" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ let clonedProps = clone.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value)
+ Expect.sequenceEqual clonedProps ["dyn", "dyn"] "Clone should have no dynamic properties"
+ testCase "copy has static and dynamic props of original" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ Expect.equal clone original "Clone and original should be equal"
+ Expect.equal (clone.stat) (original.stat) "Clone should have static prop from derived class"
+ Expect.equal (clone |> DynObj.getNestedPropAs ["dyn"]) (original |> DynObj.getNestedPropAs ["dyn"]) "Clone should have dynamic prop from derived class"
+ testCase "can use instance method on copied derived class" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ Expect.pass (clone.PrintStat())
+ testCase "instance method on copied derived class returns correct value" <| fun _ ->
+ let original = DerivedClassCloneable(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties() |> unbox
+ Expect.equal (clone.FormatStat()) "stat: stat" "instance method should return correct value"
]
]
testList "Derived class" [
testList "SpecialCases" [
+
+ #if !FABLE_COMPILER
+ // this test is transpiled as Expect_throws(() => {} and can never fail, so let's just test it in F# for now
+ testCase "Cannot unbox clone as original type" <| fun _ ->
+ let original = DerivedClass(stat = "stat", dyn = "dyn")
+ let clone = original.DeepCopyDynamicProperties()
+ let unboxMaybe() = clone |> unbox |> ignore
+ Expect.throws unboxMaybe "Clone cannot be unboxed as DerivedClass"
+ #endif
+
testCase "copy has instance prop as dynamic prop" <| fun _ ->
let original = DerivedClass(stat = "stat", dyn = "dyn")
- let clone = original.DeepCopyDynamicProperties()
+ let clone = original.DeepCopyDynamicProperties() |> unbox
let clonedProps = clone.GetProperties(false) |> Seq.map (fun p -> p.Key, p.Value)
Expect.containsAll clonedProps ["stat","stat"] "Clone should have static prop from derived class as dynamic prop"
testCase "mutable instance prop is reference equal on clone" <| fun _ ->
let original = DerivedClass(stat = "stat", dyn = "dyn")
let mut = MutableClass("initial")
original.SetProperty("mutable", mut)
- let clone = original.DeepCopyDynamicProperties()
+ let clone = original.DeepCopyDynamicProperties() |> unbox
mut.stat <- "mutated"
let originalProp = original |> DynObj.getNestedPropAs["mutable"]
let clonedProp = clone |> DynObj.getNestedPropAs ["mutable"]
@@ -186,7 +207,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let original = DerivedClass(stat = "stat", dyn = "dyn")
bulkMutate originalProps original
- let clone = original.DeepCopyDynamicProperties()
+ let clone = original.DeepCopyDynamicProperties() |> unbox
let mutatedProps = [
"int", box 2
"float", box 2.0
@@ -237,7 +258,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let inner = DynamicObj() |> DynObj.withProperty "inner int" 2
let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("inner", inner)
- let clone = original.DeepCopyDynamicProperties()
+ let clone = original.DeepCopyDynamicProperties() |> unbox
inner.SetProperty("inner int", 1)
Expect.equal (original |> DynObj.getNestedPropAs ["inner";"inner int"]) 1 "Original should have mutated properties"
@@ -250,7 +271,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let arr = [|item1; item2; item3|]
let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("arr", arr)
- let clone = original.DeepCopyDynamicProperties()
+ let clone = original.DeepCopyDynamicProperties() |> unbox
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
@@ -264,9 +285,9 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let item2 = DynamicObj() |> DynObj.withProperty "item" 2
let item3 = DynamicObj() |> DynObj.withProperty "item" 3
let l = [item1; item2; item3]
- let original = DerivedClass(stat = "stat", dyn = "dyn")
+ let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("list", l)
- let clone = original.DeepCopyDynamicProperties()
+ let clone = original.DeepCopyDynamicProperties() |> unbox
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
@@ -282,7 +303,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let r = ResizeArray([item1; item2; item3])
let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("resizeArr", r)
- let clone = original.DeepCopyDynamicProperties()
+ let clone = original.DeepCopyDynamicProperties() |> unbox
item1.SetProperty("item", -1)
item2.SetProperty("item", -1)
item3.SetProperty("item", -1)
@@ -296,7 +317,7 @@ let tests_DeepCopyDynamicProperties = testList "DeepCopyDynamicProperties" [
let item = MutableClass("initial")
let original = DerivedClass(stat = "stat", dyn = "dyn")
original.SetProperty("item", item)
- let clone = original.DeepCopyDynamicProperties()
+ let clone = original.DeepCopyDynamicProperties() |> unbox
item.stat <- "mutated"
let originalProp = original |> DynObj.getNestedPropAs["item"]
let clonedProp = clone |> DynObj.getNestedPropAs ["item"]
diff --git a/tests/DynamicObject.Tests/TestUtils.fs b/tests/DynamicObject.Tests/TestUtils.fs
index 0c08606..4d86ece 100644
--- a/tests/DynamicObject.Tests/TestUtils.fs
+++ b/tests/DynamicObject.Tests/TestUtils.fs
@@ -22,17 +22,19 @@ type DerivedClassCloneable(stat: string, dyn: string) as this =
do
this.SetProperty("dyn", dyn)
member this.stat = stat
+ member this.FormatStat() = $"stat: {this.stat}"
+ member this.PrintStat() = this.FormatStat() |> printfn "%s"
interface ICloneable with
member this.Clone() =
let dyn = this.GetPropertyValue("dyn") |> unbox
DerivedClassCloneable(stat, dyn)
-let constructDeepCopiedClone (props: seq) =
+let constructDeepCopiedClone<'T> (props: seq) =
let original = DynamicObj()
props
|> Seq.iter (fun (propertyName, propertyValue) -> original.SetProperty(propertyName, propertyValue))
let clone = original.DeepCopyDynamicProperties()
- original, clone
+ original, clone |> unbox<'T>
let bulkMutate (props: seq) (dyn: #DynamicObj) =
props |> Seq.iter (fun (propertyName, propertyValue) -> dyn.SetProperty(propertyName, propertyValue))