From 9f612d5e8e6bcfe560d2fcac685f45649d6ecdd6 Mon Sep 17 00:00:00 2001 From: Alessio Quaresima <53222249+aquaresima@users.noreply.github.com> Date: Sat, 13 Sep 2025 14:12:13 +0200 Subject: [PATCH 1/7] Add macro and functions for named tuple updates This file introduces a macro for updating named tuples and a deep merge function to handle nested updates. It also includes a pretty print function for displaying named tuples. --- src/named_tuple_update.jl | 139 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src/named_tuple_update.jl diff --git a/src/named_tuple_update.jl b/src/named_tuple_update.jl new file mode 100644 index 00000000..4648c9c6 --- /dev/null +++ b/src/named_tuple_update.jl @@ -0,0 +1,139 @@ +#Update macro and functions for handling named tuples + +# Simple update macro for handling multiple updates in a block +macro update(base, update_expr) + # Verify if the expr is a block or a line + if update_expr.head == :block + # if a block, extract the expressions + updates = update_expr.args + + # Start with the base configuration + # The :($(esc(base))) is used to ensure the base is evaluated in the correct context (the macro's context) + current_config = :($(esc(base))) + + # Process each update expression in the block + for update_expr in updates + isa(update_expr, LineNumberNode) && continue # Ensure it's an expression + + # Extract the left-hand side and right-hand side, the left-hand side is the field to update, the right hand side is the new value + lhs, rhs = update_expr.args + + # Escape the value to ensure it's evaluated in the correct context + value = :($(esc(rhs))) + + # Assert the left-hand side has the correct structure + # if isa(lhs, Symbol) + # pushfirst!(fields, lhs) # Add the first part + # else + # @assert lhs.head == Symbol(".") + fields = [] + while !isa(lhs, Symbol) + pushfirst!(fields, lhs.args[2].value) # Collect the field names + lhs = lhs.args[1] # Move to the next part of the path + end + pushfirst!(fields, lhs) # Add the first part + + # Convert the field names into symbols + field_syms = [Symbol(f) for f in fields] + + # Apply the update to the current config using the helper function + current_config = :(update_with_merge($current_config, $field_syms, $value)) + end + return current_config + else + lhs, rhs = update_expr.args # Extract the left-hand side and right-hand side + + + # @assert lhs.head == Symbol(".") + fields = [] + while !isa(lhs, Symbol) + pushfirst!(fields, lhs.args[2].value) # Collect the field names + lhs = lhs.args[1] # Move to the next part of the path + end + pushfirst!(fields, lhs) # Add the first part + + # Convert the field names into symbols + field_syms = [Symbol(f) for f in fields] + + # Return the updated expression with deep merge + return :(update_with_merge($base, $field_syms, $rhs)) + end + # end +end + +# Deep merge function for named tuples +function update_with_merge(base_config::NamedTuple, path::Vector{Symbol}, value, full_path=nothing) + full_path = isnothing(full_path) ? path : full_path + if length(path) == 1 + # If it's the final field, update the value + @debug "Updating field $(join(full_path,".")) to $value" + return merge(base_config, (path[1] => value,)) + else + key = path[1] + if !haskey(base_config, key) + @warn("Field $key in $(join(full_path,".")) does not exist, assign it to an empty NamedTuple") + base_config = merge(base_config, (key => NamedTuple(),)) + # updated_sub = update_with_merge(base_config, path, value) + # sub = (;tmp=nothing) + end + sub = getfield(base_config, key) + if isa(sub, NamedTuple) + # Recursively update the nested subfield + updated_sub = update_with_merge(sub, path[2:end], value, full_path) + else + @warn("Field $key in $(join(full_path,".")) is not a NamedTuple. Overwriting $key with a new NamedTuple.") + updated_sub = update_with_merge(NamedTuple(), path[2:end], value, full_path) + end + + # Merge the updated subfield back into the base + return merge(base_config, (key => updated_sub,)) + end +end + +macro update!(base, update_expr) + new_config = :(@update($base, $update_expr)) + return Expr(:(=), esc(base), new_config) +end + +function pretty_nt_print(value, indent=0) + if isa(value, NamedTuple) + println("{") + for (subfield, subvalue) in pairs(value) + print(" " ^ (indent + 2)) + print(" $subfield := ") + pretty_nt_print(subvalue, indent + 2) + end + println(" " ^ (indent+2) * " " * "}") + else + println(value) + end +end + + +# ## Example usage: +# base = (a=5, b=(d=6, f=7), c=(to="be", or="not to be")) + +# new_config = @update base begin +# b.e = (new="field", m=66) # Adding a new nested field +# b.g.nested = (;new ="field") # Adding a deeper nested field +# b.a = "This is changed" # Changing +# c = "to be changed" # Changing an existing field +# end + +# println("Base configuration:") +# pretty_nt_print(base) + +# println("\nUpdated configuration:") +# pretty_nt_print(new_config) + +# @update! base begin +# b.e = (l=65, m=66) # Adding a new nested field +# b.g.first = "Nested" # Adding a deeper nested field +# b.a = "This is changed" # Changing +# c = "to be changed" # Changing an existing field +# end + +# # @assert base == new_config + +# base = @update base a = "inline" +# @update! base b = "inline with !" From b95d1e3871443bf8feb4ff24a13dd960d1980ad3 Mon Sep 17 00:00:00 2001 From: Alessio Quaresima <53222249+aquaresima@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:03:57 +0200 Subject: [PATCH 2/7] changed update! macro to handle local scope variables --- src/named_tuple_update.jl | 37 ++++++++++++++++++++++++++++++++++--- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/src/named_tuple_update.jl b/src/named_tuple_update.jl index 4648c9c6..997784d2 100644 --- a/src/named_tuple_update.jl +++ b/src/named_tuple_update.jl @@ -90,11 +90,42 @@ function update_with_merge(base_config::NamedTuple, path::Vector{Symbol}, value, end end -macro update!(base, update_expr) - new_config = :(@update($base, $update_expr)) - return Expr(:(=), esc(base), new_config) +o update!(base, update_expr) + if update_expr.head == :block + updates = update_expr.args + current_config = :($(esc(base))) + + # Process each update expression in the block + for update_expr in updates + isa(update_expr, LineNumberNode) && continue # Ensure it's an expression + lhs, rhs = update_expr.args + value = :($(esc(rhs))) + fields = [] + while !isa(lhs, Symbol) + pushfirst!(fields, lhs.args[2].value) # Collect the field names + lhs = lhs.args[1] # Move to the next part of the path + end + pushfirst!(fields, lhs) # Add the first part + field_syms = [Symbol(f) for f in fields] + current_config = :(update_with_merge($current_config, $field_syms, $value)) + end + # return current_config + else + lhs, rhs = update_expr.args # Extract the left-hand side and right-hand side + fields = [] + while !isa(lhs, Symbol) + pushfirst!(fields, lhs.args[2].value) # Collect the field names + lhs = lhs.args[1] # Move to the next part of the path + end + pushfirst!(fields, lhs) # Add the first part + field_syms = [Symbol(f) for f in fields] + current_config = :(update_with_merge($base, $field_syms, $rhs)) + end + return Expr(:(=), esc(base), :($current_config)) end + + function pretty_nt_print(value, indent=0) if isa(value, NamedTuple) println("{") From 5c0fe885276e5dfa81ac0c2477659bb5fa3a0f1f Mon Sep 17 00:00:00 2001 From: Alessio Quaresima <53222249+aquaresima@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:04:23 +0200 Subject: [PATCH 3/7] Change update! function to a macro in named_tuple_update.jl --- src/named_tuple_update.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/named_tuple_update.jl b/src/named_tuple_update.jl index 997784d2..273c551f 100644 --- a/src/named_tuple_update.jl +++ b/src/named_tuple_update.jl @@ -90,7 +90,7 @@ function update_with_merge(base_config::NamedTuple, path::Vector{Symbol}, value, end end -o update!(base, update_expr) +macro update!(base, update_expr) if update_expr.head == :block updates = update_expr.args current_config = :($(esc(base))) From 72a6c8002162bb1b71e88ebf22f6d97d84b29d57 Mon Sep 17 00:00:00 2001 From: Alessio Quaresima <53222249+aquaresima@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:13:57 +0200 Subject: [PATCH 4/7] Remove example usage and tests from named_tuple_update.jl --- src/named_tuple_update.jl | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/src/named_tuple_update.jl b/src/named_tuple_update.jl index 273c551f..9c7c1b59 100644 --- a/src/named_tuple_update.jl +++ b/src/named_tuple_update.jl @@ -141,30 +141,4 @@ function pretty_nt_print(value, indent=0) end -# ## Example usage: -# base = (a=5, b=(d=6, f=7), c=(to="be", or="not to be")) -# new_config = @update base begin -# b.e = (new="field", m=66) # Adding a new nested field -# b.g.nested = (;new ="field") # Adding a deeper nested field -# b.a = "This is changed" # Changing -# c = "to be changed" # Changing an existing field -# end - -# println("Base configuration:") -# pretty_nt_print(base) - -# println("\nUpdated configuration:") -# pretty_nt_print(new_config) - -# @update! base begin -# b.e = (l=65, m=66) # Adding a new nested field -# b.g.first = "Nested" # Adding a deeper nested field -# b.a = "This is changed" # Changing -# c = "to be changed" # Changing an existing field -# end - -# # @assert base == new_config - -# base = @update base a = "inline" -# @update! base b = "inline with !" From 1305dd0495f023a663a45248691c7b2eb5bbe7e8 Mon Sep 17 00:00:00 2001 From: Alessio Quaresima <53222249+aquaresima@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:14:50 +0200 Subject: [PATCH 5/7] Create named_tuple_test.jl Added test for @update and @update! --- test/named_tuple_test.jl | 81 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 test/named_tuple_test.jl diff --git a/test/named_tuple_test.jl b/test/named_tuple_test.jl new file mode 100644 index 00000000..4767e533 --- /dev/null +++ b/test/named_tuple_test.jl @@ -0,0 +1,81 @@ +using Logging, Test +errorlogger = ConsoleLogger(stderr, Logging.Error) +with_logger(errorlogger) do + base = (a=5, b=(d=6, f=7), c=(to="be", or="not to be")) + my_tuple = @update NamedTuple() begin + a = 5 # Adding a new nested field + b.d = 6 + b.f = 7 + c.to = "be" + c.or = "not to be" + end + # ("Tuples are not equal") + @test my_tuple == base + + new_config = @update base begin + b.e = (new="field", m=66) # Adding a new nested field + b.g.nested = (;new ="field") # Adding a deeper nested field + b.a = "This is changed" # Changing + c = "to be changed" # Changing an existing field + end + + @update! base begin + b.e = (new="field", m=66) # Adding a new nested field + b.g.nested = (;new ="field") # Adding a deeper nested field + b.a = "This is changed" # Changing + c = "to be changed" # Changing an existing field + end + + # "In-place update failed" + @test base == new_config + + a = 10 + for a in 1:3 + @update! base begin + a = a # Adding a new nested field + b.g.first = "Nested" # Adding a deeper nested field + b.a = "This is changed" # Changing + c = "to be changed" # Changing an existing field + end + + # "Local scope variable a not assigned correctly" + @assert base.a == a + end + + base = @update base a = "inline" + @update! base b = "inline with !" + + #"Inline update failed" + @test base.a == "inline" + # "Inline update! failed" + @test base.b == "inline with !" +end + + +# ## Example usage: +# base = (a=5, b=(d=6, f=7), c=(to="be", or="not to be")) + +# new_config = @update base begin +# b.e = (new="field", m=66) # Adding a new nested field +# b.g.nested = (;new ="field") # Adding a deeper nested field +# b.a = "This is changed" # Changing +# c = "to be changed" # Changing an existing field +# end + +# println("Base configuration:") +# pretty_nt_print(base) + +# println("\nUpdated configuration:") +# pretty_nt_print(new_config) + +# @update! base begin +# b.e = (l=65, m=66) # Adding a new nested field +# b.g.first = "Nested" # Adding a deeper nested field +# b.a = "This is changed" # Changing +# c = "to be changed" # Changing an existing field +# end + +# # @assert base == new_config + +# base = @update base a = "inline" +# @update! base b = "inline with !" From 8fbaa6c1089679d9248e927c8f70e95d35466e31 Mon Sep 17 00:00:00 2001 From: Alessio Quaresima <53222249+aquaresima@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:15:39 +0200 Subject: [PATCH 6/7] Add test set for named tuple update --- test/runtests.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 957d9f9b..e568ae8d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,4 +8,6 @@ using DrWatson, Test @testset "Produce or Save" begin include("savefiles_tests.jl"); end @testset "Collect Results" begin include("update_results_tests.jl"); end @testset "Parameter Customization" begin include("customize_savename.jl"); end + @testset "Named Tuple @update " begin include("named_tuple_update.jl"); end + end From e12d1e3254dea79b72053ca7c3afd8cc333df799 Mon Sep 17 00:00:00 2001 From: Alessio Quaresima <53222249+aquaresima@users.noreply.github.com> Date: Sat, 13 Sep 2025 15:19:00 +0200 Subject: [PATCH 7/7] Add named_tuple_update.jl test file --- test/{named_tuple_test.jl => named_tuple_update.jl} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename test/{named_tuple_test.jl => named_tuple_update.jl} (100%) diff --git a/test/named_tuple_test.jl b/test/named_tuple_update.jl similarity index 100% rename from test/named_tuple_test.jl rename to test/named_tuple_update.jl