Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
run: ruff check . --output-format=github

test:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
strategy:
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12']
Expand Down
4 changes: 2 additions & 2 deletions TestConcoreHpp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -333,7 +333,7 @@ static void test_load_params_semicolon_format() {

static void test_tryparam_comma_in_value() {
setup_dirs();
write_file("in/1/concore.params", "{'channels': [1, 2], 'gain': '0.5'}");
write_file("in1/concore.params", "{'channels': [1, 2], 'gain': '0.5'}");

Concore c;
c.delay = 0;
Expand All @@ -342,7 +342,7 @@ static void test_tryparam_comma_in_value() {
check("tryparam preserves list with comma", c.tryparam("channels", "") == "[1, 2]");
check("tryparam preserves subsequent key", c.tryparam("gain", "") == "0.5");

rm("in/1/concore.params");
rm("in1/concore.params");
}

// ------------- main -------------------------------------------------------
Expand Down
55 changes: 46 additions & 9 deletions concore.jl
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,46 @@ function load_oport!()
return oport
end

"""Split `s` on `delim`, ignoring delimiters inside brackets or quotes."""
function _split_top_level(s::AbstractString, delim::Char)::Vector{String}
parts = String[]
depth = 0
quote_char = nothing
start = firstindex(s)
for i in eachindex(s)
c = s[i]
if quote_char !== nothing
c == quote_char && (quote_char = nothing)
elseif c == '\'' || c == '"'
quote_char = c
elseif c in ('[', '(', '{')
depth += 1
elseif c in (']', ')', '}')
depth -= 1
elseif c == delim && depth == 0
push!(parts, s[start:prevind(s, i)])
start = nextind(s, i)
end
end
push!(parts, s[start:end])
return parts
end

"""Parse a single params value as a number, a list of numbers, or a string."""
function _parse_param_value(val_str::AbstractString)
val_str = strip(val_str)
num = tryparse(Float64, val_str)
num !== nothing && return num
if startswith(val_str, "[") && endswith(val_str, "]")
isempty(strip(val_str[2:end-1])) && return Float64[]
try
return safe_parse_list(val_str)
catch
end
end
return strip(val_str, ['\'', '"'])
end

"""
load_params!()

Expand Down Expand Up @@ -614,21 +654,18 @@ function load_params!()

if startswith(sparams, "{")
# Python dict format: {'key': value, ...}
for m in eachmatch(r"['\"]([^'\"]+)['\"]\s*:\s*([^,}]+)", sparams)
key = m.captures[1]
val_str = strip(m.captures[2])
val = tryparse(Float64, val_str)
params[key] = val !== nothing ? val : strip(val_str, ['\'', '"'])
inner = endswith(sparams, "}") ? sparams[2:end-1] : sparams[2:end]
for item in _split_top_level(inner, ',')
m = match(r"^\s*['\"]([^'\"]+)['\"]\s*:(.*)$"s, item)
m === nothing && continue
params[m.captures[1]] = _parse_param_value(m.captures[2])
end
else
# key=value;key2=value2 format
for pair in split(sparams, ";")
kv = split(strip(pair), "="; limit=2)
length(kv) == 2 || continue
key = strip(kv[1])
val_str = strip(kv[2])
val = tryparse(Float64, val_str)
params[key] = val !== nothing ? val : val_str
params[strip(kv[1])] = _parse_param_value(kv[2])
end
end
end
Expand Down
55 changes: 46 additions & 9 deletions concoredocker.jl
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,46 @@ function load_oport!()
return oport
end

"""Split `s` on `delim`, ignoring delimiters inside brackets or quotes."""
function _split_top_level(s::AbstractString, delim::Char)::Vector{String}
parts = String[]
depth = 0
quote_char = nothing
start = firstindex(s)
for i in eachindex(s)
c = s[i]
if quote_char !== nothing
c == quote_char && (quote_char = nothing)
elseif c == '\'' || c == '"'
quote_char = c
elseif c in ('[', '(', '{')
depth += 1
elseif c in (']', ')', '}')
depth -= 1
elseif c == delim && depth == 0
push!(parts, s[start:prevind(s, i)])
start = nextind(s, i)
end
end
push!(parts, s[start:end])
return parts
end

"""Parse a single params value as a number, a list of numbers, or a string."""
function _parse_param_value(val_str::AbstractString)
val_str = strip(val_str)
num = tryparse(Float64, val_str)
num !== nothing && return num
if startswith(val_str, "[") && endswith(val_str, "]")
isempty(strip(val_str[2:end-1])) && return Float64[]
try
return safe_parse_list(val_str)
catch
end
end
return strip(val_str, ['\'', '"'])
end

"""
load_params!()

Expand Down Expand Up @@ -615,21 +655,18 @@ function load_params!()

if startswith(sparams, "{")
# Python dict format: {'key': value, ...}
for m in eachmatch(r"['\"]([^'\"]+)['\"]\s*:\s*([^,}]+)", sparams)
key = m.captures[1]
val_str = strip(m.captures[2])
val = tryparse(Float64, val_str)
params[key] = val !== nothing ? val : strip(val_str, ['\'', '"'])
inner = endswith(sparams, "}") ? sparams[2:end-1] : sparams[2:end]
for item in _split_top_level(inner, ',')
m = match(r"^\s*['\"]([^'\"]+)['\"]\s*:(.*)$"s, item)
m === nothing && continue
params[m.captures[1]] = _parse_param_value(m.captures[2])
end
else
# key=value;key2=value2 format
for pair in split(sparams, ";")
kv = split(strip(pair), "="; limit=2)
length(kv) == 2 || continue
key = strip(kv[1])
val_str = strip(kv[2])
val = tryparse(Float64, val_str)
params[key] = val !== nothing ? val : val_str
params[strip(kv[1])] = _parse_param_value(kv[2])
end
end
end
Expand Down
13 changes: 13 additions & 0 deletions tests/julia/test_concoredocker.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,19 @@ const DockerConcore = DockerRuntime.Concore
end
end

@testset "list values in params" begin
mktempdir() do dir
reset_docker_state!()
DockerConcore.inpath = joinpath(dir, "in")
mkpath(DockerConcore.inpath * "1")
write(joinpath(DockerConcore.inpath * "1", "concore.params"),
"{'setpoint': [1.0, 2.0, 3.0], 'gain': 2.5}")
DockerConcore.load_params!()
@test DockerConcore.params["setpoint"] == [1.0, 2.0, 3.0]
@test DockerConcore.params["gain"] == 2.5
end
end

@testset "aliases" begin
mktempdir() do dir
reset_docker_state!()
Expand Down
32 changes: 32 additions & 0 deletions tests/julia/test_config.jl
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,38 @@
end
end

@testset "list values in dict format" begin
mktempdir() do dir
old_inpath = Concore.inpath
Concore.inpath = joinpath(dir, "in")
mkpath(Concore.inpath * "1")
write(joinpath(Concore.inpath * "1", "concore.params"),
"{'setpoint': [1.0, 2.0, 3.0], 'gain': 2.5, 'name': 'a, b', 'empty': []}")
Concore.params = Dict{String,Any}()
Concore.load_params()
@test Concore.params["setpoint"] == [1.0, 2.0, 3.0]
@test Concore.params["gain"] == 2.5
@test Concore.params["name"] == "a, b"
@test Concore.params["empty"] == Float64[]
Concore.inpath = old_inpath
end
end

@testset "list values in key=value format" begin
mktempdir() do dir
old_inpath = Concore.inpath
Concore.inpath = joinpath(dir, "in")
mkpath(Concore.inpath * "1")
write(joinpath(Concore.inpath * "1", "concore.params"),
"setpoint=[1.0,2.0];gain=2.5")
Concore.params = Dict{String,Any}()
Concore.load_params()
@test Concore.params["setpoint"] == [1.0, 2.0]
@test Concore.params["gain"] == 2.5
Concore.inpath = old_inpath
end
end

end

# tryparam
Expand Down
Loading