diff --git a/src/prefpicker/main.py b/src/prefpicker/main.py index 0b9ee3b..7f88427 100644 --- a/src/prefpicker/main.py +++ b/src/prefpicker/main.py @@ -6,6 +6,8 @@ from __future__ import annotations from argparse import ArgumentParser, Namespace +from json import JSONDecodeError +from json import load as json_load from logging import DEBUG, INFO, basicConfig, getLogger from os import getenv from pathlib import Path @@ -43,6 +45,12 @@ def parse_args(argv: list[str] | None = None) -> Namespace: "--check", action="store_true", help="Display output of sanity checks." ) parser.add_argument("--variant", default="default", help="Specify variant to use.") + parser.add_argument( + "--json", + "-j", + type=Path, + help="Path to JSON file containing additional prefs to include in the output.", + ) parser.add_argument( "--version", "-V", @@ -62,6 +70,9 @@ def parse_args(argv: list[str] | None = None) -> Namespace: parser.error(f"Output '{args.output}' is a directory.") if not args.output.parent.is_dir(): parser.error(f"Output '{args.output.parent}' directory does not exist.") + # sanity check JSON file if provided + if args.json and not args.json.is_file(): + parser.error(f"Cannot find JSON file '{args.json}'") return args @@ -108,6 +119,21 @@ def main(argv: list[str] | None = None) -> int: LOG.error("Error: Variant %r does not exist", args.variant) return 1 LOG.info("Generating %r using variant %r...", args.output.name, args.variant) - pick.create_prefsjs(args.output, args.variant) + + # Load additional preferences from JSON file if provided + additional_prefs = {} + if args.json: + try: + with args.json.open() as json_fp: + additional_prefs = json_load(json_fp) + except (JSONDecodeError, UnicodeDecodeError) as exc: + LOG.error("Failed to load JSON file '%s': %s", args.json, exc) + return 1 + if not isinstance(additional_prefs, dict): + LOG.error("Failed to load JSON file '%s': expected object", args.json) + return 1 + LOG.info("Overriding %d prefs from JSON input", len(additional_prefs)) + + pick.create_prefsjs(args.output, args.variant, additional_prefs) LOG.info("Done.") return 0 diff --git a/src/prefpicker/prefpicker.py b/src/prefpicker/prefpicker.py index 3920a63..1513819 100644 --- a/src/prefpicker/prefpicker.py +++ b/src/prefpicker/prefpicker.py @@ -98,13 +98,19 @@ def check_overwrites(self) -> Generator[tuple[str, str, PrefValue]]: if value in variants["default"]: yield (pref, variant, value) - def create_prefsjs(self, dest: Path, variant: str = "default") -> None: + def create_prefsjs( + self, + dest: Path, + variant: str = "default", + additional_prefs: dict[str, Any] | None = None, + ) -> None: """Write a `prefs.js` file based on the specified variant. The output file will also include comments containing the variant and a timestamp. Args: dest: Path of file to create. variant: Used to pick the values to output. + additional_prefs: Additional preferences to include in the output. Returns: None @@ -113,16 +119,34 @@ def create_prefsjs(self, dest: Path, variant: str = "default") -> None: prefs_fp.write(f"// Generated with PrefPicker ({__version__}) @ ") prefs_fp.write(datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z")) prefs_fp.write(f"\n// Variant {variant!r}\n") - for pref, keys in sorted(self.prefs.items()): - variants = keys["variants"] + + all_prefs = set(self.prefs) + if additional_prefs: + all_prefs |= set(additional_prefs) + for pref in sorted(all_prefs): # choose values - if variant not in variants or variant == "default": - options = variants["default"] - default_variant = True - else: - options = variants[variant] + if additional_prefs and pref in additional_prefs: + value = additional_prefs[pref] + if pref not in self.prefs: + # pref is only from JSON, not in template + if value is None: + continue + json_only = True + else: + json_only = False + options = [value] default_variant = False - value = choice(options) + else: + json_only = False + keys = self.prefs[pref] + variants = keys["variants"] + if variant not in variants or variant == "default": + options = variants["default"] + default_variant = True + else: + options = variants[variant] + default_variant = False + value = choice(options) if value is None: if len(options) > 1: prefs_fp.write( @@ -145,7 +169,11 @@ def create_prefsjs(self, dest: Path, variant: str = "default") -> None: f"Unsupported datatype {type(value).__name__!r}" ) # write to prefs.js file - if not default_variant: + if json_only: + prefs_fp.write(f"// {pref!r} defined by --json (not in template)\n") + elif additional_prefs and pref in additional_prefs: + prefs_fp.write(f"// {pref!r} defined by --json override\n") + elif not default_variant: prefs_fp.write(f"// {pref!r} defined by variant {variant!r}\n") prefs_fp.write(f'user_pref("{pref}", {sanitized});\n') diff --git a/src/prefpicker/test_main.py b/src/prefpicker/test_main.py index 3082d65..1835801 100644 --- a/src/prefpicker/test_main.py +++ b/src/prefpicker/test_main.py @@ -93,3 +93,77 @@ def test_main_07(tmp_path): yml = tmp_path / "test.yml" yml.write_text("{test{") assert main([str(yml), str(tmp_path / "prefs.js")]) == 1 + + +def test_main_08(tmp_path): + """test main() with --json override and prefs not in template""" + prefs_js = tmp_path / "prefs.js" + yml = tmp_path / "test.yml" + yml.write_text( + """ + variant: [] + pref: + test.a: + variants: + default: [1] + test.b: + variants: + default: [true]""" + ) + json_file = tmp_path / "overrides.json" + json_file.write_text('{"test.a": 99, "test.new": false}') + assert main([str(yml), str(prefs_js), "--json", str(json_file)]) == 0 + assert prefs_js.is_file() + prefs_data = prefs_js.read_text() + assert 'user_pref("test.a", 99);' in prefs_data + assert "defined by --json override" in prefs_data + assert 'user_pref("test.b", true);' in prefs_data + assert 'user_pref("test.new", false);' in prefs_data + assert "// 'test.new' defined by --json (not in template)" in prefs_data + + +def test_main_09(capsys, tmp_path): + """test main() with missing --json file""" + yml = tmp_path / "test.yml" + yml.touch() + with raises(SystemExit): + main([str(yml), str(tmp_path / "prefs.js"), "--json", "missing.json"]) + assert "Cannot find JSON file 'missing.json'" in capsys.readouterr()[1] + + +def test_main_10(tmp_path): + """test main() with invalid JSON content""" + prefs_js = tmp_path / "prefs.js" + yml = tmp_path / "test.yml" + yml.write_text( + """ + variant: [] + pref: + test.a: + variants: + default: [1]""" + ) + # invalid JSON syntax + json_file = tmp_path / "bad.json" + json_file.write_text("{bad json") + assert main([str(yml), str(prefs_js), "--json", str(json_file)]) == 1 + # non-UTF-8 bytes + json_file.write_bytes(b"\x80\x81\x82") + assert main([str(yml), str(prefs_js), "--json", str(json_file)]) == 1 + + +def test_main_11(tmp_path): + """test main() with non-dict JSON content""" + prefs_js = tmp_path / "prefs.js" + yml = tmp_path / "test.yml" + yml.write_text( + """ + variant: [] + pref: + test.a: + variants: + default: [1]""" + ) + json_file = tmp_path / "list.json" + json_file.write_text("[1, 2, 3]") + assert main([str(yml), str(prefs_js), "--json", str(json_file)]) == 1 diff --git a/src/prefpicker/test_prefpicker.py b/src/prefpicker/test_prefpicker.py index a5c24e1..0f6323b 100644 --- a/src/prefpicker/test_prefpicker.py +++ b/src/prefpicker/test_prefpicker.py @@ -279,3 +279,81 @@ def test_prefpicker_10(): template = next(PrefPicker.templates(), None) assert template is not None assert PrefPicker.lookup_template(template.name) + + +def test_prefpicker_11(tmp_path): + """test PrefPicker.create_prefsjs() with additional_prefs override""" + raw_data = { + "variant": [], + "pref": { + "test.a": {"variants": {"default": [1]}}, + "test.b": {"variants": {"default": [False]}}, + }, + } + PrefPicker.verify_data(raw_data) + ppick = PrefPicker() + ppick.variants = set(raw_data["variant"] + ["default"]) + ppick.prefs = raw_data["pref"] + prefs = tmp_path / "prefs.js" + ppick.create_prefsjs(prefs, additional_prefs={"test.a": 99}) + prefs_data = prefs.read_text() + assert 'user_pref("test.a", 99);' in prefs_data + assert "// 'test.a' defined by --json override" in prefs_data + # test.b is not overridden + assert 'user_pref("test.b", false);' in prefs_data + assert "// 'test.b' defined by --json override" not in prefs_data + + +def test_prefpicker_12(tmp_path): + """test PrefPicker.create_prefsjs() additional_prefs takes precedence""" + raw_data = { + "variant": ["v1"], + "pref": { + "test.a": {"variants": {"default": [1], "v1": [2]}}, + }, + } + PrefPicker.verify_data(raw_data) + ppick = PrefPicker() + ppick.variants = set(raw_data["variant"] + ["default"]) + ppick.prefs = raw_data["pref"] + prefs = tmp_path / "prefs.js" + ppick.create_prefsjs(prefs, variant="v1", additional_prefs={"test.a": 42}) + prefs_data = prefs.read_text() + assert 'user_pref("test.a", 42);' in prefs_data + assert "// 'test.a' defined by --json override" in prefs_data + assert "defined by variant 'v1'" not in prefs_data + + +def test_prefpicker_13(tmp_path): + """test PrefPicker.create_prefsjs() with additional_prefs not in template""" + raw_data = { + "variant": [], + "pref": { + "test.b": {"variants": {"default": [1]}}, + }, + } + PrefPicker.verify_data(raw_data) + ppick = PrefPicker() + ppick.variants = set(raw_data["variant"] + ["default"]) + ppick.prefs = raw_data["pref"] + prefs = tmp_path / "prefs.js" + ppick.create_prefsjs( + prefs, + additional_prefs={ + "test.a": 99, + "test.c": "hello", + "test.b": 42, + "test.skip": None, + }, + ) + prefs_data = prefs.read_text() + # test.a and test.c are not in template, should be included + assert 'user_pref("test.a", 99);' in prefs_data + assert "// 'test.a' defined by --json (not in template)" in prefs_data + assert "user_pref(\"test.c\", 'hello');" in prefs_data + assert "// 'test.c' defined by --json (not in template)" in prefs_data + # test.b is in template and overridden by JSON + assert 'user_pref("test.b", 42);' in prefs_data + assert "// 'test.b' defined by --json override" in prefs_data + # None values from JSON-only prefs should be skipped + assert "test.skip" not in prefs_data