prefpicker 2.17.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- prefpicker/__init__.py +9 -0
- prefpicker/__main__.py +10 -0
- prefpicker/main.py +113 -0
- prefpicker/prefpicker.py +265 -0
- prefpicker/py.typed +0 -0
- prefpicker/templates/browser-fuzzing.yml +1165 -0
- prefpicker/templates/schema.json +56 -0
- prefpicker/test_main.py +95 -0
- prefpicker/test_prefpicker.py +281 -0
- prefpicker/test_templates.py +44 -0
- prefpicker-2.17.0.dist-info/METADATA +121 -0
- prefpicker-2.17.0.dist-info/RECORD +16 -0
- prefpicker-2.17.0.dist-info/WHEEL +5 -0
- prefpicker-2.17.0.dist-info/entry_points.txt +2 -0
- prefpicker-2.17.0.dist-info/licenses/LICENSE +373 -0
- prefpicker-2.17.0.dist-info/top_level.txt +1 -0
prefpicker/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
"""prefpicker module"""
|
|
5
|
+
|
|
6
|
+
from .prefpicker import PrefPicker, SourceDataError
|
|
7
|
+
|
|
8
|
+
__all__ = ("PrefPicker", "SourceDataError")
|
|
9
|
+
__author__ = "Tyson Smith"
|
prefpicker/__main__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
"""prefpicker module main"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from .main import main
|
|
9
|
+
|
|
10
|
+
sys.exit(main())
|
prefpicker/main.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
"""prefpicker module main"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from argparse import ArgumentParser, Namespace
|
|
9
|
+
from logging import DEBUG, INFO, basicConfig, getLogger
|
|
10
|
+
from os import getenv
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from .prefpicker import PrefPicker, SourceDataError, __version__
|
|
14
|
+
|
|
15
|
+
__author__ = "Tyson Smith"
|
|
16
|
+
__credits__ = ["Tyson Smith"]
|
|
17
|
+
|
|
18
|
+
LOG = getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def parse_args(argv: list[str] | None = None) -> Namespace:
|
|
22
|
+
"""Handle argument parsing.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
argv: Arguments from the user.
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
Parsed and sanitized arguments.
|
|
29
|
+
"""
|
|
30
|
+
parser = ArgumentParser(
|
|
31
|
+
description="Manage & generate prefs.js files",
|
|
32
|
+
prog="prefpicker",
|
|
33
|
+
)
|
|
34
|
+
parser.add_argument(
|
|
35
|
+
"input",
|
|
36
|
+
type=Path,
|
|
37
|
+
help="Template containing definitions. This can be the path to a template"
|
|
38
|
+
" (YAML) file or the name of a built-in template. Built-in templates:"
|
|
39
|
+
f" {', '.join(x.name for x in PrefPicker.templates())}",
|
|
40
|
+
)
|
|
41
|
+
parser.add_argument("output", type=Path, help="Path of prefs.js file to create.")
|
|
42
|
+
parser.add_argument(
|
|
43
|
+
"--check", action="store_true", help="Display output of sanity checks."
|
|
44
|
+
)
|
|
45
|
+
parser.add_argument("--variant", default="default", help="Specify variant to use.")
|
|
46
|
+
parser.add_argument(
|
|
47
|
+
"--version",
|
|
48
|
+
"-V",
|
|
49
|
+
action="version",
|
|
50
|
+
version=f"%(prog)s {__version__}",
|
|
51
|
+
help="Show version number.",
|
|
52
|
+
)
|
|
53
|
+
args = parser.parse_args(argv)
|
|
54
|
+
# handle using built-in templates
|
|
55
|
+
builtin_template = PrefPicker.lookup_template(args.input.name)
|
|
56
|
+
if builtin_template:
|
|
57
|
+
args.input = builtin_template
|
|
58
|
+
elif not args.input.is_file():
|
|
59
|
+
parser.error(f"Cannot find input file '{args.input}'")
|
|
60
|
+
# sanity check output
|
|
61
|
+
if args.output.is_dir():
|
|
62
|
+
parser.error(f"Output '{args.output}' is a directory.")
|
|
63
|
+
if not args.output.parent.is_dir():
|
|
64
|
+
parser.error(f"Output '{args.output.parent}' directory does not exist.")
|
|
65
|
+
return args
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def main(argv: list[str] | None = None) -> int:
|
|
69
|
+
"""
|
|
70
|
+
PrefPicker main entry point
|
|
71
|
+
|
|
72
|
+
Run with --help for usage
|
|
73
|
+
"""
|
|
74
|
+
if bool(getenv("DEBUG")): # pragma: no cover
|
|
75
|
+
log_fmt = "%(asctime)s %(levelname).1s %(name)s | %(message)s"
|
|
76
|
+
log_level = DEBUG
|
|
77
|
+
else:
|
|
78
|
+
log_fmt = "%(message)s"
|
|
79
|
+
log_level = INFO
|
|
80
|
+
basicConfig(format=log_fmt, level=log_level)
|
|
81
|
+
|
|
82
|
+
args = parse_args(argv)
|
|
83
|
+
|
|
84
|
+
LOG.info("Loading %r...", args.input.name)
|
|
85
|
+
try:
|
|
86
|
+
pick = PrefPicker.load_template(args.input)
|
|
87
|
+
except SourceDataError as exc:
|
|
88
|
+
LOG.error("Failed to load '%s': %s", args.input, exc)
|
|
89
|
+
return 1
|
|
90
|
+
LOG.info("Loaded %d prefs and %d variants", len(pick.prefs), len(pick.variants))
|
|
91
|
+
if args.check:
|
|
92
|
+
for combos in pick.check_combinations():
|
|
93
|
+
LOG.info(
|
|
94
|
+
"Check: %r variant has %r possible combination(s)", combos[0], combos[1]
|
|
95
|
+
)
|
|
96
|
+
for overwrites in pick.check_overwrites():
|
|
97
|
+
LOG.info(
|
|
98
|
+
"Check: %r variant %r redefines value %r (may be intentional)",
|
|
99
|
+
overwrites[0],
|
|
100
|
+
overwrites[1],
|
|
101
|
+
overwrites[2],
|
|
102
|
+
)
|
|
103
|
+
for dupes in pick.check_duplicates():
|
|
104
|
+
LOG.info(
|
|
105
|
+
"Check: %r variant %r contains duplicate values", dupes[0], dupes[1]
|
|
106
|
+
)
|
|
107
|
+
if args.variant not in pick.variants:
|
|
108
|
+
LOG.error("Error: Variant %r does not exist", args.variant)
|
|
109
|
+
return 1
|
|
110
|
+
LOG.info("Generating %r using variant %r...", args.output.name, args.variant)
|
|
111
|
+
pick.create_prefsjs(args.output, args.variant)
|
|
112
|
+
LOG.info("Done.")
|
|
113
|
+
return 0
|
prefpicker/prefpicker.py
ADDED
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
"""prefpicker module"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
10
|
+
from json import dumps
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from random import choice
|
|
13
|
+
from typing import TYPE_CHECKING, Any
|
|
14
|
+
|
|
15
|
+
from yaml import safe_load
|
|
16
|
+
from yaml.parser import ParserError
|
|
17
|
+
from yaml.scanner import ScannerError
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from collections.abc import Generator
|
|
21
|
+
|
|
22
|
+
__author__ = "Tyson Smith"
|
|
23
|
+
__credits__ = ["Tyson Smith"]
|
|
24
|
+
try:
|
|
25
|
+
__version__ = version("prefpicker")
|
|
26
|
+
except PackageNotFoundError: # pragma: no cover
|
|
27
|
+
# package is not installed
|
|
28
|
+
__version__ = "unknown"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class SourceDataError(Exception):
|
|
32
|
+
"""This is raised when issues are found in the source data."""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
PrefValue = bool | int | str | None
|
|
36
|
+
PrefVariant = dict[str, list[PrefValue]]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PrefPicker: # pylint: disable=missing-docstring
|
|
40
|
+
__slots__ = ("prefs", "variants")
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
self.prefs: dict[str, dict[str, PrefVariant]] = {}
|
|
44
|
+
self.variants: set[str] = {"default"}
|
|
45
|
+
|
|
46
|
+
def check_combinations(self) -> Generator[tuple[str, int]]:
|
|
47
|
+
"""Count the number of combinations for each variation. Only return
|
|
48
|
+
variants that have more than one combination.
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
None
|
|
52
|
+
|
|
53
|
+
Yields:
|
|
54
|
+
Variant and number of potential combinations.
|
|
55
|
+
"""
|
|
56
|
+
combos = dict.fromkeys(self.variants, 1)
|
|
57
|
+
for variants in (x["variants"] for x in self.prefs.values()):
|
|
58
|
+
for variant in combos:
|
|
59
|
+
# use 'default' if pref does not have a matching variant entry
|
|
60
|
+
if variant not in variants:
|
|
61
|
+
combos[variant] *= len(variants["default"])
|
|
62
|
+
else:
|
|
63
|
+
combos[variant] *= len(variants[variant])
|
|
64
|
+
for variant, count in sorted(combos.items()):
|
|
65
|
+
if count > 1:
|
|
66
|
+
yield (variant, count)
|
|
67
|
+
|
|
68
|
+
def check_duplicates(self) -> Generator[tuple[str, str]]:
|
|
69
|
+
"""Look for variants with values that appear more than once per variant.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
None
|
|
73
|
+
|
|
74
|
+
Yields:
|
|
75
|
+
Pref name and the variant.
|
|
76
|
+
"""
|
|
77
|
+
for pref, keys in sorted(self.prefs.items()):
|
|
78
|
+
variants = keys["variants"]
|
|
79
|
+
for variant in variants:
|
|
80
|
+
if len(variants[variant]) != len(set(variants[variant])):
|
|
81
|
+
yield (pref, variant)
|
|
82
|
+
|
|
83
|
+
def check_overwrites(self) -> Generator[tuple[str, str, PrefValue]]:
|
|
84
|
+
"""Look for variants that overwrite the default with the same value.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
None
|
|
88
|
+
|
|
89
|
+
Yields:
|
|
90
|
+
Pref, variant and value.
|
|
91
|
+
"""
|
|
92
|
+
for pref, keys in sorted(self.prefs.items()):
|
|
93
|
+
variants = keys["variants"]
|
|
94
|
+
for variant in variants:
|
|
95
|
+
if variant == "default":
|
|
96
|
+
continue
|
|
97
|
+
for value in variants[variant]:
|
|
98
|
+
if value in variants["default"]:
|
|
99
|
+
yield (pref, variant, value)
|
|
100
|
+
|
|
101
|
+
def create_prefsjs(self, dest: Path, variant: str = "default") -> None:
|
|
102
|
+
"""Write a `prefs.js` file based on the specified variant. The output file
|
|
103
|
+
will also include comments containing the variant and a timestamp.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
dest: Path of file to create.
|
|
107
|
+
variant: Used to pick the values to output.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
None
|
|
111
|
+
"""
|
|
112
|
+
with dest.open("w") as prefs_fp:
|
|
113
|
+
prefs_fp.write(f"// Generated with PrefPicker ({__version__}) @ ")
|
|
114
|
+
prefs_fp.write(datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M:%S %Z"))
|
|
115
|
+
prefs_fp.write(f"\n// Variant {variant!r}\n")
|
|
116
|
+
for pref, keys in sorted(self.prefs.items()):
|
|
117
|
+
variants = keys["variants"]
|
|
118
|
+
# choose values
|
|
119
|
+
if variant not in variants or variant == "default":
|
|
120
|
+
options = variants["default"]
|
|
121
|
+
default_variant = True
|
|
122
|
+
else:
|
|
123
|
+
options = variants[variant]
|
|
124
|
+
default_variant = False
|
|
125
|
+
value = choice(options)
|
|
126
|
+
if value is None:
|
|
127
|
+
if len(options) > 1:
|
|
128
|
+
prefs_fp.write(
|
|
129
|
+
f"// '{pref}' skipped, options {dumps(options)}\n"
|
|
130
|
+
)
|
|
131
|
+
# skipping pref
|
|
132
|
+
continue
|
|
133
|
+
if len(options) > 1:
|
|
134
|
+
prefs_fp.write(f"// '{pref}' options {dumps(options)}\n")
|
|
135
|
+
# sanitize value for writing
|
|
136
|
+
if isinstance(value, bool):
|
|
137
|
+
sanitized = "true" if value else "false"
|
|
138
|
+
elif isinstance(value, int):
|
|
139
|
+
sanitized = str(value)
|
|
140
|
+
elif isinstance(value, str):
|
|
141
|
+
sanitized = repr(value)
|
|
142
|
+
else:
|
|
143
|
+
prefs_fp.write(f"// Failed to sanitized {value!r} ({pref})\n")
|
|
144
|
+
raise SourceDataError(
|
|
145
|
+
f"Unsupported datatype {type(value).__name__!r}"
|
|
146
|
+
)
|
|
147
|
+
# write to prefs.js file
|
|
148
|
+
if not default_variant:
|
|
149
|
+
prefs_fp.write(f"// {pref!r} defined by variant {variant!r}\n")
|
|
150
|
+
prefs_fp.write(f'user_pref("{pref}", {sanitized});\n')
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def lookup_template(cls, name: str) -> Path | None:
|
|
154
|
+
"""Lookup built-in template Path.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
name: Name of template.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Template that matches 'name' or None.
|
|
161
|
+
"""
|
|
162
|
+
path = Path(__file__).parent / "templates" / name
|
|
163
|
+
if path.is_file():
|
|
164
|
+
return path
|
|
165
|
+
return None
|
|
166
|
+
|
|
167
|
+
@classmethod
|
|
168
|
+
def load_template(cls, input_yml: Path) -> PrefPicker:
|
|
169
|
+
"""Load data from a template YAML file.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
input_yml: Input file.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
PrefPicker object.
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
raw_prefs = safe_load(input_yml.read_bytes())
|
|
179
|
+
except (ScannerError, ParserError):
|
|
180
|
+
raise SourceDataError("invalid YAML") from None
|
|
181
|
+
cls.verify_data(raw_prefs)
|
|
182
|
+
picker = cls()
|
|
183
|
+
picker.variants = set(raw_prefs["variant"] + ["default"])
|
|
184
|
+
# only add relevant parts
|
|
185
|
+
for pref, parts in raw_prefs["pref"].items():
|
|
186
|
+
picker.prefs[pref] = {"variants": parts["variants"]}
|
|
187
|
+
return picker
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def templates() -> Generator[Path]:
|
|
191
|
+
"""Available YAML template files.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
None
|
|
195
|
+
|
|
196
|
+
Yields:
|
|
197
|
+
Template files.
|
|
198
|
+
"""
|
|
199
|
+
path = Path(__file__).parent.resolve() / "templates"
|
|
200
|
+
if path.is_dir():
|
|
201
|
+
for template in path.iterdir():
|
|
202
|
+
if template.suffix.lower().endswith(".yml"):
|
|
203
|
+
yield template
|
|
204
|
+
|
|
205
|
+
@staticmethod
|
|
206
|
+
def verify_data(raw_data: Any) -> None:
|
|
207
|
+
"""Perform strict sanity checks on raw_data. This exists to help prevent
|
|
208
|
+
the template file from breaking or becoming unmaintainable.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
raw_data: Data to verify.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
None
|
|
215
|
+
"""
|
|
216
|
+
if not isinstance(raw_data, dict):
|
|
217
|
+
raise SourceDataError("invalid template")
|
|
218
|
+
# check variant list
|
|
219
|
+
if "variant" not in raw_data:
|
|
220
|
+
raise SourceDataError("variant list is missing")
|
|
221
|
+
if not isinstance(raw_data["variant"], list):
|
|
222
|
+
raise SourceDataError("variant is not a list")
|
|
223
|
+
# check variant list entries
|
|
224
|
+
valid_variants = {"default"}
|
|
225
|
+
for variant in raw_data["variant"]:
|
|
226
|
+
if not isinstance(variant, str):
|
|
227
|
+
raise SourceDataError("variant definition must be a string")
|
|
228
|
+
valid_variants.add(variant)
|
|
229
|
+
# check prefs dict
|
|
230
|
+
if "pref" not in raw_data:
|
|
231
|
+
raise SourceDataError("pref group is missing")
|
|
232
|
+
if not isinstance(raw_data["pref"], dict):
|
|
233
|
+
raise SourceDataError("pref is not a dict")
|
|
234
|
+
# check entries in prefs dict
|
|
235
|
+
used_variants = set()
|
|
236
|
+
for pref, keys in raw_data["pref"].items():
|
|
237
|
+
if not isinstance(keys, dict):
|
|
238
|
+
raise SourceDataError(f"{pref!r} entry must contain a dict")
|
|
239
|
+
variants = keys.get("variants")
|
|
240
|
+
if not isinstance(variants, dict):
|
|
241
|
+
raise SourceDataError(f"{pref!r} is missing 'variants' dict")
|
|
242
|
+
if "default" not in variants:
|
|
243
|
+
raise SourceDataError(f"{pref!r} is missing 'default' variant")
|
|
244
|
+
# verify variants
|
|
245
|
+
for variant in variants:
|
|
246
|
+
if variant not in valid_variants:
|
|
247
|
+
raise SourceDataError(
|
|
248
|
+
f"{variant!r} in {pref!r} is not a defined variant"
|
|
249
|
+
)
|
|
250
|
+
if not isinstance(variants[variant], list):
|
|
251
|
+
raise SourceDataError(
|
|
252
|
+
f"variant {variant!r} in {pref!r} is not a list"
|
|
253
|
+
)
|
|
254
|
+
if not variants[variant]:
|
|
255
|
+
raise SourceDataError(f"{variant!r} in {pref!r} is empty")
|
|
256
|
+
for value in variants[variant]:
|
|
257
|
+
if value is not None and not isinstance(value, bool | int | str):
|
|
258
|
+
raise SourceDataError(
|
|
259
|
+
f"unsupported datatype {type(value).__name__!r} ({pref})"
|
|
260
|
+
)
|
|
261
|
+
used_variants.add(variant)
|
|
262
|
+
if valid_variants - used_variants:
|
|
263
|
+
raise SourceDataError(
|
|
264
|
+
f"Unused variants {' '.join(valid_variants - used_variants)!r}"
|
|
265
|
+
)
|
prefpicker/py.typed
ADDED
|
File without changes
|