Cura/printer-linter/src/printerlinter/linters/defintion.py
2024-06-19 16:27:05 +02:00

203 lines
9.7 KiB
Python

import json
import re
from pathlib import Path
from typing import Iterator
from ..diagnostic import Diagnostic
from .linter import Linter
from ..replacement import Replacement
class Definition(Linter):
""" Finds issues in definition files, such as overriding default parameters """
def __init__(self, file: Path, settings: dict) -> None:
super().__init__(file, settings)
self._definitions = {}
self._definition_name = None
self._experimental_settings = []
self._loadDefinitionFiles(file)
self._content = self._file.read_text()
self._loadExperimentalSettings()
self._loadBasePrinterSettings()
@property
def base_def(self):
if "fdmextruder" in self._definitions:
return "fdmextruder"
return "fdmprinter"
def check(self) -> Iterator[Diagnostic]:
if self._settings["checks"].get("diagnostic-definition-redundant-override", False):
for check in self.checkRedefineOverride():
yield check
if self._settings["checks"].get("diagnostic-material-temperature-defined", False):
for check in self.checkMaterialTemperature():
yield check
if self._settings["checks"].get("diagnostic-definition-experimental-setting", False):
for check in self.checkExperimentalSetting():
yield check
# Add other which will yield Diagnostic's
# TODO: A check to determine if the user set value is with the min and max value defined in the parent and doesn't trigger a warning
# TODO: A check if the key exist in the first place
# TODO: Check if the model platform exist
yield
def checkRedefineOverride(self) -> Iterator[Diagnostic]:
""" Checks if definition file overrides its parents settings with the same value. """
definition = self._definitions[self._definition_name]
if "overrides" in definition and self._definition_name not in ("fdmprinter", "fdmextruder"):
for key, value_dict in definition["overrides"].items():
is_redefined, child_key, child_value, parent, inherited_by= self._isDefinedInParent(key, value_dict, definition['inherits'])
if is_redefined:
redefined = re.compile(r'.*(\"' + key + r'\"[\s\:\S]*?)\{[\s\S]*?\},?')
found = redefined.search(self._content)
# TODO: Figure out a way to support multiline fixes in the PR review GH Action, for now suggest no fix to ensure no ill-formed json are created
# see: https://github.com/platisd/clang-tidy-pr-comments/issues/37
if len(found.group().splitlines()) > 1:
replacements = []
else:
replacements = [Replacement(
file = self._file,
offset = found.span(1)[0],
length = len(found.group()),
replacement_text = "")]
yield Diagnostic(
file = self._file,
diagnostic_name = "diagnostic-definition-redundant-override",
message = f"Overriding {key} with the same value ({child_key}: {child_value}) as defined in parent definition: {inherited_by}",
level = "Warning",
offset = found.span(0)[0],
replacements = replacements
)
def checkMaterialTemperature(self) -> Iterator[Diagnostic]:
"""Checks if definition file has material tremperature defined within them"""
definition = self._definitions[self._definition_name]
if "overrides" in definition and self._definition_name not in ("fdmprinter", "fdmextruder"):
for key, value_dict in definition["overrides"].items():
if "temperature" in key and "material" in key:
redefined = re.compile(r'.*(\"' + key + r'\"[\s\:\S]*?)\{[\s\S]*?\},?')
found = redefined.search(self._content)
if len(found.group().splitlines()) > 1:
replacements = []
else:
replacements = [Replacement(
file=self._file,
offset=found.span(1)[0],
length=len(found.group()),
replacement_text="")]
yield Diagnostic(
file=self._file,
diagnostic_name="diagnostic-material-temperature-defined",
message=f"Overriding {key} as it belongs to material temperature catagory and shouldn't be placed in machine definitions",
level="Warning",
offset=found.span(0)[0],
replacements=replacements
)
def checkExperimentalSetting(self) -> Iterator[Diagnostic]:
"""Checks if definition uses experimental settings"""
definition = self._definitions[self._definition_name]
if "overrides" in definition and self._definition_name not in ("fdmprinter", "fdmextruder"):
for setting in definition["overrides"]:
if setting in self._experimental_settings:
redefined = re.compile(setting)
found = redefined.search(self._content)
yield Diagnostic(
file=self._file,
diagnostic_name="diagnostic-definition-experimental-setting",
message=f"Setting {setting} is still experimental and should not be used in default profiles",
level="Warning",
offset=found.span(0)[0]
)
def _loadDefinitionFiles(self, definition_file) -> None:
""" Loads definition file contents into self._definitions. Also load parent definition if it exists. """
definition_name = Path(definition_file.stem).stem
if not definition_file.exists() or definition_name in self._definitions:
return
if self._definition_name is None:
self._definition_name = definition_name
# Load definition file into dictionary
self._definitions[definition_name] = json.loads(definition_file.read_text())
# Load parent definition if it exists
if "inherits" in self._definitions[definition_name]:
if self._definitions[definition_name]['inherits'] in ("fdmextruder", "fdmprinter"):
parent_file = definition_file.parent.parent.joinpath("definitions", f"{self._definitions[definition_name]['inherits']}.def.json")
else:
parent_file = definition_file.parent.joinpath(f"{self._definitions[definition_name]['inherits']}.def.json")
self._loadDefinitionFiles(parent_file)
def _isDefinedInParent(self, key, value_dict, inherits_from):
if self._ignore(key, "diagnostic-definition-redundant-override"):
return False, None, None, None, None
if "overrides" not in self._definitions[inherits_from]:
return self._isDefinedInParent(key, value_dict, self._definitions[inherits_from]["inherits"])
parent = self._definitions[inherits_from]["overrides"]
if key not in self._definitions[self.base_def]["overrides"]:
is_number = False
else:
is_number = self._definitions[self.base_def]["overrides"][key]["type"] in ("float", "int")
for child_key, child_value in value_dict.items():
if key in parent:
if child_key in ("default_value", "value"):
check_values = [cv for cv in [parent[key].get("default_value", None), parent[key].get("value", None)] if cv is not None]
else:
check_values = [parent[key].get(child_key, None)]
for check_value in check_values:
if is_number and child_key in ("default_value", "value"):
try:
v = str(float(child_value))
except:
v = child_value
try:
cv = str(float(check_value))
except:
cv = check_value
else:
v = child_value
cv = check_value
if v == cv:
return True, child_key, child_value, parent, inherits_from
if "inherits" in parent:
return self._isDefinedInParent(key, value_dict, parent["inherits"])
return False, None, None, None, None
def _loadExperimentalSettings(self):
try:
self._experimental_settings = self._definitions[self.base_def]["settings"]["experimental"]["children"].keys()
except:
pass
def _loadBasePrinterSettings(self):
settings = {}
for k, v in self._definitions[self.base_def]["settings"].items():
self._getSetting(k, v, settings)
self._definitions[self.base_def] = {"overrides": settings}
def _getSetting(self, name, setting, settings) -> None:
if "children" in setting:
for childname, child in setting["children"].items():
self._getSetting(childname, child, settings)
settings |= {name: setting}
def _ignore(self, key: dict, type_of_check: str) -> bool:
if f"{type_of_check}-ignore" in self._settings:
filters = [re.compile(f) for f in self._settings[f"{type_of_check}-ignore"]]
for f in filters:
if f.match(key):
return True
return False