Skip to content

Commit

Permalink
Merge pull request #42 from CSBiology/combine
Browse files Browse the repository at this point in the history
Add Deep/Shallow Copy methods
  • Loading branch information
HLWeil authored Dec 17, 2024
2 parents 5a03d10 + ddfd632 commit b02c6fc
Show file tree
Hide file tree
Showing 27 changed files with 1,265 additions and 599 deletions.
10 changes: 2 additions & 8 deletions DynamicObj.sln
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{39AA72A1
ProjectSection(SolutionItems) = preProject
build.cmd = build.cmd
build.sh = build.sh
global.json = global.json
EndProjectSection
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".ci", ".ci", "{F82A6F26-517C-4D5E-BD4F-BFC45B5867FE}"
Expand All @@ -20,14 +21,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".proj", ".proj", "{C3CF2F15-81C7-4C11-889E-5FCA2C8A981D}"
ProjectSection(SolutionItems) = preProject
.gitignore = .gitignore
build.cmd = build.cmd
build.sh = build.sh
.config\dotnet-tools.json = .config\dotnet-tools.json
global.json = global.json
key.snk = key.snk
LICENSE = LICENSE
package.json = package.json
pyproject.toml = pyproject.toml
README.md = README.md
RELEASE_NOTES.md = RELEASE_NOTES.md
EndProjectSection
Expand All @@ -37,7 +31,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{42AA66FC-8
docs\index.fsx = docs\index.fsx
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CSharpTests", "tests\CSharpTests\CSharpTests.csproj", "{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}"
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CSharpTests", "tests\CSharpTests\CSharpTests.csproj", "{D62D0901-DB69-4C64-AC63-FBBBDCF6BC7D}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{988D804A-3A42-4E46-B233-B64F5C22524B}"
EndProject
Expand Down
2 changes: 2 additions & 0 deletions build/BasicTasks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,8 @@ let clean = BuildTask.create "Clean" [] {
++ "src/**/obj"
++ "tests/**/bin"
++ "tests/**/obj"
++ "tests/**/js"
++ "tests/**/py"
++ "dist"
++ ProjectInfo.netPkgDir
|> Shell.cleanDirs
Expand Down
4 changes: 2 additions & 2 deletions build/TestTasks.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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" ""
Expand All @@ -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" ""
}
Expand Down
137 changes: 130 additions & 7 deletions src/DynamicObj/DynamicObj.fs
Original file line number Diff line number Diff line change
Expand Up @@ -224,32 +224,155 @@ type DynamicObj() =
|> Seq.map (fun kv -> kv.Key)

/// <summary>
/// Copies all dynamic members of the DynamicObj to the target DynamicObj.
/// Copies all dynamic members of the source DynamicObj to the target DynamicObj.
///
/// If overWrite is set to true, existing properties on the target object will be overwritten.
/// 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.
///
/// Note that this method will not perform nested checks, e.g. if a property is a DynamicObj itself, it will not be copied recursively.
/// If overWrite is set to true, existing properties on the target object will be overwritten.
/// </summary>
/// <param name="target">The target object to copy dynamic members to</param>
/// <param name="overWrite">Whether existing properties on the target object will be overwritten</param>
member this.CopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) =
member this.ShallowCopyDynamicPropertiesTo(target:#DynamicObj, ?overWrite) =
let overWrite = defaultArg overWrite false
this.GetProperties(false)
|> Seq.iter (fun kv ->
match target.TryGetPropertyHelper kv.Key with
| Some pi when overWrite -> pi.SetValue target kv.Value
| Some _ -> failwith $"Property \"{kv.Key}\" already exists on target object and overWrite was not set to true."
| Some _ -> ()
| None -> target.SetProperty(kv.Key,kv.Value)
)

/// <summary>
/// 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.
/// </summary>
member this.CopyDynamicProperties() =
member this.ShallowCopyDynamicProperties() =
let target = DynamicObj()
this.CopyDynamicPropertiesTo(target)
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

// might be that we do not need this case, however if we remove it, some types will match the
// ICloneable case in transpiled code, which we'd like to prevent, so well keep it for now.
| :? int | :? float | :? bool
| :? string | :? char | :? byte
| :? sbyte | :? int16 | :? uint16
| :? int32 | :? uint32 | :? int64
| :? uint64 | :? single
-> o

#if !FABLE_COMPILER_PYTHON
// https://github.com/fable-compiler/Fable/issues/3971
| :? decimal -> o
#endif

| :? array<DynamicObj> as dyns ->
box [|for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj|]
| :? list<DynamicObj> as dyns ->
box [for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]
| :? ResizeArray<DynamicObj> as dyns ->
box (ResizeArray([for dyn in dyns -> tryDeepCopyObj dyn :?> DynamicObj]))
#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

| :? DynamicObj as dyn ->
let newDyn = DynamicObj()
// might want to keep instance props as dynamic props on copy
for kv in (dyn.GetProperties(true)) do
newDyn.SetProperty(kv.Key, tryDeepCopyObj kv.Value)
box newDyn
| _ -> o

tryDeepCopyObj o

/// <summary>
/// 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&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 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.
/// </summary>
/// <param name="target">The target object to copy dynamic members to</param>
/// <param name="overWrite">Whether existing properties on the target object will be overwritten</param>
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 (DynamicObj.tryDeepCopyObj kv.Value)
| Some _ -> ()
| None -> target.SetProperty(kv.Key, DynamicObj.tryDeepCopyObj kv.Value)
)

/// <summary>

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

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

This XML comment is invalid: unknown parameter 'target'

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

View workflow job for this annotation

GitHub Actions / test (ubuntu-latest)

This XML comment is invalid: unknown parameter 'overWrite'

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

View workflow job for this annotation

GitHub Actions / build-and-deploy-docs

This XML comment is invalid: unknown parameter 'target'

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

View workflow job for this annotation

GitHub Actions / build-and-deploy-docs

This XML comment is invalid: unknown parameter 'overWrite'

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

View workflow job for this annotation

GitHub Actions / test (windows-latest)

This XML comment is invalid: unknown parameter 'target'

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

View workflow job for this annotation

GitHub Actions / test (windows-latest)

This XML comment is invalid: unknown parameter 'overWrite'
/// Attempts to perform a deep copy of the DynamicObj.
///
/// On the deep copy, 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&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 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.
/// </summary>
/// <param name="target">The target object to copy dynamic members to</param>
/// <param name="overWrite">Whether existing properties on the target object will be overwritten</param>
member this.DeepCopyDynamicProperties() = DynamicObj.tryDeepCopyObj this

#if !FABLE_COMPILER
// Some necessary overrides for methods inherited from System.Dynamic.DynamicObject()
//
Expand Down
9 changes: 9 additions & 0 deletions src/DynamicObj/FableJS.fs
Original file line number Diff line number Diff line change
Expand Up @@ -158,5 +158,14 @@ module FableJS =
getPropertyHelpers o
|> Array.map (fun h -> h.Name)

module Interfaces =

[<Emit("""$0["System.ICloneable.Clone"] != undefined && (typeof $0["System.ICloneable.Clone"]) === 'function'""")>]
let implementsICloneable (o:obj) : bool =
jsNative

[<Emit("""$0["System.ICloneable.Clone"]()""")>]
let cloneICloneable (o:obj) : obj =
jsNative

#endif
8 changes: 8 additions & 0 deletions src/DynamicObj/FablePy.fs
Original file line number Diff line number Diff line change
Expand Up @@ -211,5 +211,13 @@ module FablePy =
getPropertyHelpers o
|> Array.map (fun h -> h.Name)

module Interfaces =

[<Emit("""hasattr($0, 'System_ICloneable_Clone') and callable($0.System_ICloneable_Clone)""")>]
let implementsICloneable (o:obj) : bool =
nativeOnly

[<Emit("""$0.System_ICloneable_Clone()""")>]
let cloneICloneable (o:obj) : obj =
nativeOnly
#endif
21 changes: 16 additions & 5 deletions src/DynamicObj/Playground.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,29 @@
#r "nuget: Fable.Core"
#r "nuget: Fable.Pyxpecto"

#load "./HashCodes.fs"
#load "./PropertyHelper.fs"
#load "./FablePy.fs"
#load "./FableJS.fs"
#load "./ReflectionUtils.fs"
#load "./DynamicObj.fs"
#load "./DynObj.fs"

open Fable.Pyxpecto
open DynamicObj

type T(dyn:string, stat:string) as this=
inherit DynamicObj()

let a = DynamicObj ()
a.SetValue("aaa", 5)
let b = DynamicObj ()
b.SetValue("aaa", 5)
do
this.SetProperty("Dyn", dyn)

member this.Stat = stat

a.GetProperties(true)
let first = T("dyn1", "stat1")
let second = T("dyn2", "stat2")

let _ = second.ShallowCopyDynamicPropertiesTo(first)

first |> DynObj.print
second |> DynObj.print
18 changes: 17 additions & 1 deletion tests/DynamicObject.Tests/DynamicObject.Tests.fsproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,26 @@
</PropertyGroup>

<ItemGroup>
<Compile Include="TestUtils.fs" />
<Compile Include="DynamicObjs\RemoveProperty.fs" />
<Compile Include="DynamicObjs\SetProperty.fs" />
<Compile Include="DynamicObjs\GetHashcode.fs" />
<Compile Include="DynamicObjs\Equals.fs" />
<Compile Include="DynamicObjs\GetPropertyValue.fs" />
<Compile Include="DynamicObjs\TryGetPropertyValue.fs" />
<Compile Include="DynamicObjs\TryGetTypedPropertyValue.fs" />
<Compile Include="DynamicObjs\TryGetStaticPropertyHelper.fs" />
<Compile Include="DynamicObjs\TryGetDynamicPropertyHelper.fs" />
<Compile Include="DynamicObjs\TryGetPropertyHelper.fs" />
<Compile Include="DynamicObjs\GetPropertyHelpers.fs" />
<Compile Include="DynamicObjs\GetProperties.fs" />
<Compile Include="DynamicObjs\ShallowCopyDynamicPropertiesTo.fs" />
<Compile Include="DynamicObjs\ShallowCopyDynamicProperties.fs" />
<Compile Include="DynamicObjs\DeepCopyDynamicProperties.fs" />
<Compile Include="DynamicObjs\Main.fs" />
<Compile Include="ReflectionUtils.fs" />
<Compile Include="Inheritance.fs" />
<Compile Include="Interface.fs" />
<Compile Include="DynamicObjs.fs" />
<Compile Include="DynObj.fs" />
<Compile Include="Serialization.fs" />
<Compile Include="Main.fs" />
Expand Down
Loading

0 comments on commit b02c6fc

Please sign in to comment.