Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
✨ Add error messages for constraint failures (#1036)
This commit adds the ability to set an optional `error_message` key for each constraint in the `needs_constraint` configuration. This can contain Jinja placeholders, for needs data fields, and will be added to the need data, under the `constraints_error` field, if the constraint fails.
- Loading branch information
1 parent
cd5cb08
commit c01558e
Showing
10 changed files
with
761 additions
and
111 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,86 +1,106 @@ | ||
from typing import Dict | ||
|
||
import jinja2 | ||
from sphinx.application import Sphinx | ||
|
||
from sphinx_needs.api.exceptions import NeedsConstraintFailed, NeedsConstraintNotAllowed | ||
from sphinx_needs.config import NeedsSphinxConfig | ||
from sphinx_needs.data import NeedsInfoType | ||
from sphinx_needs.data import SphinxNeedsData | ||
from sphinx_needs.filter_common import filter_single_need | ||
from sphinx_needs.logging import get_logger | ||
|
||
logger = get_logger(__name__) | ||
|
||
|
||
def process_constraints(app: Sphinx, need: NeedsInfoType) -> None: | ||
def process_constraints(app: Sphinx) -> None: | ||
"""Analyse constraints of a single need, | ||
and set corresponding fields on the need data item. | ||
""" | ||
needs_config = NeedsSphinxConfig(app.config) | ||
env = app.env | ||
needs_config = NeedsSphinxConfig(env.config) | ||
config_constraints = needs_config.constraints | ||
need_id = need["id"] | ||
constraints = need["constraints"] | ||
|
||
# flag that is set to False if any check fails | ||
need["constraints_passed"] = True | ||
|
||
for constraint in constraints: | ||
try: | ||
executable_constraints = config_constraints[constraint] | ||
except KeyError: | ||
# Note, this is already checked for in add_need | ||
raise NeedsConstraintNotAllowed( | ||
f"Constraint {constraint} of need id {need_id} is not allowed by config value 'needs_constraints'." | ||
) | ||
|
||
# name is check_0, check_1, ... | ||
for name, cmd in executable_constraints.items(): | ||
if name == "severity": | ||
# special key, that is not a check | ||
continue | ||
|
||
# compile constraint and check if need fulfils it | ||
constraint_passed = filter_single_need(app, need, cmd) | ||
|
||
if constraint_passed: | ||
need["constraints_results"].setdefault(constraint, {})[name] = True | ||
else: | ||
need["constraints_results"].setdefault(constraint, {})[name] = False | ||
need["constraints_passed"] = False | ||
|
||
if "severity" not in executable_constraints: | ||
raise NeedsConstraintFailed( | ||
f"'severity' key not set for constraint {constraint!r} in config 'needs_constraints'" | ||
) | ||
severity = executable_constraints["severity"] | ||
if severity not in needs_config.constraint_failed_options: | ||
raise NeedsConstraintFailed( | ||
f"Severity {severity!r} not set in config 'needs_constraint_failed_options'" | ||
) | ||
failed_options = needs_config.constraint_failed_options[severity] | ||
|
||
# log/except if needed | ||
if "warn" in failed_options.get("on_fail", []): | ||
logger.warning( | ||
f"Constraint {cmd} for need {need_id} FAILED! severity: {severity} [needs.constraint]", | ||
type="needs", | ||
subtype="constraint", | ||
color="red", | ||
location=(need["docname"], need["lineno"]), | ||
) | ||
if "break" in failed_options.get("on_fail", []): | ||
raise NeedsConstraintFailed( | ||
f"FAILED a breaking constraint: >> {cmd} << for need " | ||
f"{need_id} FAILED! breaking build process" | ||
) | ||
|
||
# set styles | ||
old_style = need["style"] | ||
if old_style and len(old_style) > 0: | ||
new_styles = "".join(", " + x for x in failed_options.get("style", [])) | ||
else: | ||
old_style = "" | ||
new_styles = "".join(x + "," for x in failed_options.get("style", [])) | ||
needs = SphinxNeedsData(env).get_or_create_needs() | ||
workflow = SphinxNeedsData(env).get_or_create_workflow() | ||
|
||
if workflow["needs_constraints"]: | ||
return | ||
|
||
workflow["needs_constraints"] = True | ||
|
||
error_templates_cache: Dict[str, jinja2.Template] = {} | ||
|
||
for need in needs.values(): | ||
need_id = need["id"] | ||
constraints = need["constraints"] | ||
|
||
# flag that is set to False if any check fails | ||
need["constraints_passed"] = True | ||
|
||
for constraint in constraints: | ||
try: | ||
executable_constraints = config_constraints[constraint] | ||
except KeyError: | ||
# Note, this is already checked for in add_need | ||
raise NeedsConstraintNotAllowed( | ||
f"Constraint {constraint} of need id {need_id} is not allowed by config value 'needs_constraints'." | ||
) | ||
|
||
if failed_options.get("force_style", False): | ||
need["style"] = new_styles.strip(", ") | ||
# name is check_0, check_1, ... | ||
for name, cmd in executable_constraints.items(): | ||
if name in ("severity", "error_message"): | ||
# special keys, that are not a check | ||
continue | ||
|
||
# compile constraint and check if need fulfils it | ||
constraint_passed = filter_single_need(app, need, cmd) | ||
|
||
if constraint_passed: | ||
need["constraints_results"].setdefault(constraint, {})[name] = True | ||
else: | ||
constraint_failed_style = old_style + new_styles | ||
need["style"] = constraint_failed_style | ||
need["constraints_results"].setdefault(constraint, {})[name] = False | ||
need["constraints_passed"] = False | ||
|
||
if "error_message" in executable_constraints: | ||
msg = str(executable_constraints["error_message"]) | ||
template = error_templates_cache.setdefault(msg, jinja2.Template(msg)) | ||
need["constraints_error"] = template.render(**need) | ||
|
||
if "severity" not in executable_constraints: | ||
raise NeedsConstraintFailed( | ||
f"'severity' key not set for constraint {constraint!r} in config 'needs_constraints'" | ||
) | ||
severity = executable_constraints["severity"] | ||
if severity not in needs_config.constraint_failed_options: | ||
raise NeedsConstraintFailed( | ||
f"Severity {severity!r} not set in config 'needs_constraint_failed_options'" | ||
) | ||
failed_options = needs_config.constraint_failed_options[severity] | ||
|
||
# log/except if needed | ||
if "warn" in failed_options.get("on_fail", []): | ||
logger.warning( | ||
f"Constraint {cmd} for need {need_id} FAILED! severity: {severity} {need.get('constraints_error', '')} [needs.constraint]", | ||
type="needs", | ||
subtype="constraint", | ||
color="red", | ||
location=(need["docname"], need["lineno"]), | ||
) | ||
if "break" in failed_options.get("on_fail", []): | ||
raise NeedsConstraintFailed( | ||
f"FAILED a breaking constraint: >> {cmd} << for need " | ||
f"{need_id} FAILED! breaking build process" | ||
) | ||
|
||
# set styles | ||
old_style = need["style"] | ||
if old_style and len(old_style) > 0: | ||
new_styles = "".join(", " + x for x in failed_options.get("style", [])) | ||
else: | ||
old_style = "" | ||
new_styles = "".join(x + "," for x in failed_options.get("style", [])) | ||
|
||
if failed_options.get("force_style", False): | ||
need["style"] = new_styles.strip(", ") | ||
else: | ||
constraint_failed_style = old_style + new_styles | ||
need["style"] = constraint_failed_style |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.