packwright 0.1.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.
@@ -0,0 +1,70 @@
1
+ from collections.abc import Mapping
2
+ from pathlib import Path
3
+
4
+ from .errors import PackwrightValidationError
5
+
6
+
7
+ def validate_relative_path(value, label="path"):
8
+ """Return a normalized relative path or reject an unsafe path value."""
9
+ if not isinstance(value, str) or not value.strip():
10
+ raise PackwrightValidationError([f"{label} must be a non-empty relative path"])
11
+ if "\x00" in value:
12
+ raise PackwrightValidationError([f"{label} contains a null byte"])
13
+ path = Path(value)
14
+ if path.is_absolute() or path == Path(".") or ".." in path.parts:
15
+ raise PackwrightValidationError([f"{label} must be relative and stay inside its root: {value}"])
16
+ return path
17
+
18
+
19
+ def resolve_source_path(root, value, label="source path", require_file=True):
20
+ """Resolve a readable path and require its final target to stay under root."""
21
+ relative = validate_relative_path(value, label)
22
+ resolved_root = Path(root).resolve()
23
+ candidate = resolved_root / relative
24
+ try:
25
+ resolved = candidate.resolve(strict=True)
26
+ resolved.relative_to(resolved_root)
27
+ except FileNotFoundError:
28
+ raise PackwrightValidationError([f"{label} does not exist: {value}"])
29
+ except (OSError, ValueError):
30
+ raise PackwrightValidationError([f"{label} escapes its root: {value}"])
31
+ if require_file and not resolved.is_file():
32
+ raise PackwrightValidationError([f"{label} is not a file: {value}"])
33
+ return resolved
34
+
35
+
36
+ def resolve_destination_path(root, value, label="destination path"):
37
+ """Resolve a write destination without allowing traversal through symlinks."""
38
+ relative = validate_relative_path(value, label)
39
+ resolved_root = Path(root).resolve()
40
+ candidate = resolved_root / relative
41
+ current = resolved_root
42
+ for part in relative.parts:
43
+ current = current / part
44
+ if current.is_symlink():
45
+ raise PackwrightValidationError([f"{label} traverses a symlink: {value}"])
46
+ try:
47
+ candidate.resolve(strict=False).relative_to(resolved_root)
48
+ except (OSError, ValueError):
49
+ raise PackwrightValidationError([f"{label} escapes its root: {value}"])
50
+ return candidate
51
+
52
+
53
+ def resolve_mechanism_file(mechanism, value, label="referenced file"):
54
+ source = mechanism.get("source", {}) if isinstance(mechanism, Mapping) else {}
55
+ base_dir = source.get("base_dir") if isinstance(source, Mapping) else None
56
+ if not base_dir:
57
+ raise PackwrightValidationError([f"{label} has no mechanism source root"])
58
+ roots = [base_dir]
59
+ fallback_roots = source.get("fallback_roots", []) if isinstance(source, Mapping) else []
60
+ if isinstance(fallback_roots, list):
61
+ roots.extend(root for root in fallback_roots if isinstance(root, str) and root)
62
+ missing = []
63
+ for root in roots:
64
+ try:
65
+ return resolve_source_path(root, value, label)
66
+ except PackwrightValidationError as exc:
67
+ if any("escapes its root" in issue or "must be relative" in issue for issue in exc.issues):
68
+ raise
69
+ missing.extend(exc.issues)
70
+ raise PackwrightValidationError(missing[:1] or [f"{label} does not exist: {value}"])
@@ -0,0 +1,66 @@
1
+ import copy
2
+ import re
3
+ from collections.abc import Mapping, Sequence
4
+
5
+ from .errors import PackwrightValidationError
6
+ from .validation import validate_mechanism
7
+
8
+
9
+ PLACEHOLDER_PATTERN = re.compile(r"{{\s*([A-Za-z0-9_.-]+)\s*}}")
10
+
11
+
12
+ def resolve_mechanism(data, parameters=None):
13
+ """Validate and resolve a character mechanism spec using parameter overrides."""
14
+ validate_mechanism(data)
15
+ values = _resolve_parameter_values(data.get("parameters", {}), parameters or {})
16
+ resolved = _replace_placeholders(copy.deepcopy(data), values)
17
+ resolved["resolved_parameters"] = copy.deepcopy(values)
18
+ validate_mechanism(resolved)
19
+ return resolved
20
+ def _resolve_parameter_values(parameter_specs, overrides):
21
+ issues = []
22
+ values = {}
23
+
24
+ for name in overrides:
25
+ if name not in parameter_specs:
26
+ issues.append(f"unknown parameter override: {name}")
27
+
28
+ for name, spec in parameter_specs.items():
29
+ if name in overrides:
30
+ values[name] = overrides[name]
31
+ elif "default" in spec:
32
+ values[name] = spec["default"]
33
+ elif spec.get("required"):
34
+ issues.append(f"missing required parameter: {name}")
35
+ else:
36
+ values[name] = ""
37
+
38
+ if issues:
39
+ raise PackwrightValidationError(issues)
40
+ return values
41
+
42
+
43
+ def _replace_placeholders(value, parameters):
44
+ if isinstance(value, str):
45
+ return _replace_string(value, parameters)
46
+ if isinstance(value, Mapping):
47
+ return {key: _replace_placeholders(item, parameters) for key, item in value.items()}
48
+ if isinstance(value, Sequence) and not isinstance(value, (str, bytes, bytearray)):
49
+ return [_replace_placeholders(item, parameters) for item in value]
50
+ return value
51
+
52
+
53
+ def _replace_string(value, parameters):
54
+ issues = []
55
+
56
+ def replace(match):
57
+ name = match.group(1)
58
+ if name not in parameters:
59
+ issues.append(f"unknown placeholder: {name}")
60
+ return match.group(0)
61
+ return str(parameters[name])
62
+
63
+ rendered = PLACEHOLDER_PATTERN.sub(replace, value)
64
+ if issues:
65
+ raise PackwrightValidationError(issues)
66
+ return rendered