Skip to content
Merged
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
28 changes: 27 additions & 1 deletion src/prefpicker/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand All @@ -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


Expand Down Expand Up @@ -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
48 changes: 38 additions & 10 deletions src/prefpicker/prefpicker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(
Expand All @@ -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')

Expand Down
74 changes: 74 additions & 0 deletions src/prefpicker/test_main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
78 changes: 78 additions & 0 deletions src/prefpicker/test_prefpicker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading