Skip to content

Commit 71ec01a

Browse files
authored
Merge pull request #233 from uclahs-cds/aholmes-fix-R-serialization
Fix R serialization in `vector_from_parts`
2 parents d7975fe + 8b71b51 commit 71ec01a

File tree

6 files changed

+104
-19
lines changed

6 files changed

+104
-19
lines changed

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,17 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
1111
## Unreleased
1212

1313
---
14+
## `Ligare.all` [0.10.2] - 2025-05-23
15+
16+
### Fixed
17+
- Fixed a problem with R serialization when serializing values to character vectors.
18+
19+
### `Ligare.programming` [0.7.1] - 2025-05-23
20+
21+
* [Ligare.programming v0.7.1](https://github.com/uclahs-cds/Ligare/blob/Ligare.programming-v0.7.1/src/web/CHANGELOG.md#071---2025-05=23)
22+
23+
---
24+
1425
## `Ligare.all` [0.10.1] - 2025-05-20
1526

1627
### Added

src/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__: str = "0.10.1"
1+
__version__: str = "0.10.2"

src/programming/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@ Review the `Ligare` [CHANGELOG.md](https://github.com/uclahs-cds/Ligare/blob/mai
1111
---
1212
## Unreleased
1313

14+
## [0.7.1] - 2025-05-23
15+
### Fixed
16+
- Fixed a problem with R serialization when serializing values to character vectors.
17+
1418
## [0.7.0] - 2025-05-12
1519
### Changed
1620
- Changed how R script errors are handled

src/programming/Ligare/programming/R/type_conversion.py

Lines changed: 76 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
SAFE_COMMA_SEPARATED_STRING_PATTERN = r"[^a-zA-Z0-9_,.\s-]"
99
safe_comma_separated_string_regex = re.compile(SAFE_COMMA_SEPARATED_STRING_PATTERN)
1010

11+
NULL = "__NULL__"
12+
FALSE = "FALSE"
13+
TRUE = "TRUE"
14+
1115

1216
@overload
1317
def string(value: str | None) -> str | None:
@@ -347,17 +351,28 @@ def boolean(value: str | bool | None) -> str:
347351
'FALSE'
348352
"""
349353
return (
350-
"TRUE"
351-
if (value is not None and str(value).lower() in ["true", "t"])
352-
else "FALSE"
354+
TRUE if (value is not None and str(value).lower() in ["true", "t"]) else FALSE
353355
)
354356

355357

358+
def _serialize(value: Any):
359+
if value is None:
360+
value = f"'{NULL}'"
361+
362+
elif isinstance(value, str):
363+
value = f"'{string(value)}'"
364+
365+
elif isinstance(value, bool):
366+
value = f"'{boolean(value)}'"
367+
368+
return str(value)
369+
370+
356371
def vector_from_parts(
357372
parts: dict[str, Any],
358373
new_part_key: str,
359374
existing_part_keys: list[str],
360-
default: Any = "__NULL__",
375+
default: Any = NULL,
361376
) -> None:
362377
"""
363378
Add a new key to the `parts` dictionary named `new_part_key`.
@@ -402,7 +417,24 @@ def vector_from_parts(
402417
... ["scale.bar.coords.x", "scale.bar.coords.y"]
403418
... )
404419
>>> query_params
405-
{'scale.bar.coords': "c('0.5','1.0')"}
420+
{'scale.bar.coords': 'c(0.5,1.0)'}
421+
422+
**Convert two keys with numerical and string values into a single two-item vector.**
423+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
424+
425+
.. doctest::
426+
427+
>>> query_params = {
428+
... "scale.bar.coords.x": 0.5,
429+
... "scale.bar.coords.y": '1.0'
430+
... }
431+
>>> vector_from_parts(
432+
... query_params,
433+
... "scale.bar.coords",
434+
... ["scale.bar.coords.x", "scale.bar.coords.y"]
435+
... )
436+
>>> query_params
437+
{'scale.bar.coords': "c(0.5,'1.0')"}
406438
407439
**Convert two keys into a single two-item vector where one value is `None`**
408440
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -419,7 +451,7 @@ def vector_from_parts(
419451
... ["scale.bar.coords.x", "scale.bar.coords.y"]
420452
... )
421453
>>> query_params
422-
{'scale.bar.coords': "c('0.5','__NULL__')"}
454+
{'scale.bar.coords': "c(0.5,'__NULL__')"}
423455
424456
**Convert two keys into an empty value where all values are `None`**
425457
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -437,17 +469,49 @@ def vector_from_parts(
437469
... )
438470
>>> query_params
439471
{'scale.bar.coords': '__NULL__'}
472+
473+
**Convert many keys of varying types into a single vector.**
474+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
475+
476+
.. doctest::
477+
478+
>>> query_params = {
479+
... "scale.bar.coords.x": 0.5,
480+
... "scale.bar.coords.y": '1.0',
481+
... "scale.bar.coords.z": None,
482+
... "scale.bar.coords.a": 123,
483+
... "scale.bar.coords.b": True,
484+
... "scale.bar.coords.c": False
485+
... }
486+
>>> vector_from_parts(
487+
... query_params,
488+
... "scale.bar.coords",
489+
... [
490+
... "scale.bar.coords.x",
491+
... "scale.bar.coords.y",
492+
... "scale.bar.coords.z",
493+
... "scale.bar.coords.a",
494+
... "scale.bar.coords.b",
495+
... "scale.bar.coords.c",
496+
... ]
497+
... )
498+
>>> query_params
499+
{'scale.bar.coords': "c(0.5,'1.0','__NULL__',123,'TRUE','FALSE')"}
440500
"""
441-
part_values: list[str | None] = []
501+
if not isinstance(parts, dict): # pyright: ignore[reportUnnecessaryIsInstance]
502+
raise TypeError(
503+
f"`parts` must be a dictionary. The value given is a `{type(parts)}`."
504+
)
505+
506+
part_values: list[Any] = []
442507
for part in existing_part_keys:
443-
part_values.append(str(parts[part]) if parts[part] is not None else None)
508+
part_values.append(parts.get(part))
444509
del parts[part]
445510

446511
if all([value is None or value == "" for value in part_values]):
447512
parts[new_part_key] = default
448513
else:
449-
serialized_part_values = "','".join([
450-
"__NULL__" if part_value is None else part_value
451-
for part_value in part_values
514+
serialized_part_values = ",".join([
515+
_serialize(part_value) for part_value in part_values
452516
])
453-
parts[new_part_key] = f"c('{serialized_part_values}')"
517+
parts[new_part_key] = f"c({serialized_part_values})"

src/programming/Ligare/programming/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@
33
and augmenting the stdlib.
44
"""
55

6-
__version__: str = "0.7.0"
6+
__version__: str = "0.7.1"

src/programming/test/unit/R/test_type_conversion.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -231,12 +231,18 @@ def test__boolean__returns_sanitized_string(input_value: Any, expected_value: st
231231
@pytest.mark.parametrize(
232232
"parts,new_part_key,existing_part_keys,expected_value",
233233
[
234-
({"a": 1, "b": 2}, "c", ["a", "b"], {"c": "c('1','2')"}),
235-
({"a": 1, "b": 2, "c": 3}, "c", ["a", "b"], {"c": "c('1','2')"}),
236-
({"a": 1, "b": 2}, "a", ["a", "b"], {"a": "c('1','2')"}),
237-
({"a": 1, "b": 2, "c": 3}, "a", ["a", "b"], {"a": "c('1','2')", "c": 3}),
238-
({1: 1, 2: 2}, 3, [1, 2], {3: "c('1','2')"}),
234+
({"a": 1, "b": 2}, "c", ["a", "b"], {"c": "c(1,2)"}),
235+
({"a": 1, "b": 2, "c": 3}, "c", ["a", "b"], {"c": "c(1,2)"}),
236+
({"a": 1, "b": 2}, "a", ["a", "b"], {"a": "c(1,2)"}),
237+
({"a": 1, "b": 2, "c": 3}, "a", ["a", "b"], {"a": "c(1,2)", "c": 3}),
238+
({1: 1, 2: 2}, 3, [1, 2], {3: "c(1,2)"}),
239239
({"a": None, "b": None}, "c", ["a", "b"], {"c": "__NULL__"}),
240+
(
241+
{"x": 0.5, "y": "1.0", "z": None, "a": 123, "b": True, "c": False},
242+
"A",
243+
["x", "y", "z", "a", "b", "c"],
244+
{"A": "c(0.5,'1.0','__NULL__',123,'TRUE','FALSE')"},
245+
),
240246
],
241247
)
242248
def test__vector_from_parts__returns(

0 commit comments

Comments
 (0)