From 62b728e76684251e8a45791949633e7e792f0357 Mon Sep 17 00:00:00 2001 From: rootile Date: Sat, 28 Mar 2026 14:05:34 +0100 Subject: [PATCH] f2/nix: provide a function to serialise python objects to nix code Change-Id: I863029263edbc701a18b18da4b58fcb35f575614 --- tests/functional2/testlib/fixtures/nix.py | 36 +++++++++++++++++ .../functional2/testlib/fixtures/test_nix.py | 39 ++++++++++++++++++- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/tests/functional2/testlib/fixtures/nix.py b/tests/functional2/testlib/fixtures/nix.py index 774920f80..2a3d8d50a 100644 --- a/tests/functional2/testlib/fixtures/nix.py +++ b/tests/functional2/testlib/fixtures/nix.py @@ -16,8 +16,11 @@ from testlib.fixtures.command import CommandResult, Command from testlib.fixtures.env import ManagedEnv from testlib.utils import is_value_of_type +from textwrap import dedent + type _NixSettingValue = str | int | list[str] | bool | None +type _NixValue = str | int | float | list[_NixValue] | dict[str, _NixValue] | bool | None def _serialise_config(value: _NixSettingValue) -> str: @@ -32,6 +35,39 @@ def _serialise_config(value: _NixSettingValue) -> str: raise ValueError(msg) +def serialise_nix(value: _NixValue) -> str: + """ + Serialises the given python object into a nix represenatation. + NOTE: this interface does not allow accessing variables unless using interpolation, as all strings will be surrounded by quotes. + """ + + def escape(v: str) -> str: + if "\\r" in v: + raise ValueError("\\r is not supported for conversion") + escaped = v.replace("\\", "\\\\").replace("$", "\\$").replace('"', '\\"') + return f'"{escaped}"' + + if is_value_of_type(value, list[_NixValue.__value__]): + return f"[{' '.join([serialise_nix(v) for v in value])}]" + if is_value_of_type(value, dict[str, _NixValue.__value__]): + return dedent(f""" + {{ + {"\n ".join([f"{escape(k)} = {serialise_nix(v)};" for k, v in value.items()])} + }} + """) + if is_value_of_type(value, bool): + return "true" if value else "false" + if is_value_of_type(value, int | float): + return str(value) + if is_value_of_type(value, str): + return escape(value) + if is_value_of_type(value, None): + return "null" + + msg = f"Value is unsupported in nix code: {value!r}" + raise ValueError(msg) + + class NixSettings: """Settings for invoking Nix""" diff --git a/tests/functional2/testlib/fixtures/test_nix.py b/tests/functional2/testlib/fixtures/test_nix.py index af439dd08..f3a2952d4 100644 --- a/tests/functional2/testlib/fixtures/test_nix.py +++ b/tests/functional2/testlib/fixtures/test_nix.py @@ -3,7 +3,9 @@ from pathlib import Path import pytest from testlib.fixtures.env import ManagedEnv -from testlib.fixtures.nix import NixSettings +from testlib.fixtures.nix import NixSettings, serialise_nix + +from textwrap import dedent def test_nix_settings_set_item(): @@ -115,3 +117,38 @@ def test_nix_settings_to_env_overlay_no_store_dir(tmp_path: Path): settings.to_env_overlay(env) assert "store = local?root=/some/path\n" in env._env["NIX_CONFIG"] + + +class TestSerialiseNix: + def test_list(self): + assert serialise_nix(["a", 1, None]) == '["a" 1 null]' + + def test_dict(self): + assert serialise_nix( + {"a": 1, "b": None, "c": "hello world", "d": 3.14159265, "e": True} + ) == dedent(""" + { + "a" = 1; + "b" = null; + "c" = "hello world"; + "d" = 3.14159265; + "e" = true; + } + """) + + def test_escaping(self): + assert serialise_nix({'"a': 'Hello ""', "1": "${foo}", "${foo}": None}) == dedent(""" + { + "\\"a" = "Hello \\"\\""; + "1" = "\\${foo}"; + "\\${foo}" = null; + } + """) + + def test_quotes(self): + assert serialise_nix({"a b": None, "\\n": '${awa}"'}) == dedent(""" + { + "a b" = null; + "\\\\n" = "\\${awa}\\""; + } + """)