mirror of
https://git.mirrors.martin98.com/https://github.com/Ultimaker/Cura
synced 2025-08-15 16:45:57 +08:00
Merge branch 'printer_linter' into printer_linter_auto_format
This commit is contained in:
commit
4d0d9ec2dd
1
.gitignore
vendored
1
.gitignore
vendored
@ -99,3 +99,4 @@ conanbuildinfo.txt
|
|||||||
graph_info.json
|
graph_info.json
|
||||||
Ultimaker-Cura.spec
|
Ultimaker-Cura.spec
|
||||||
.run/
|
.run/
|
||||||
|
/printer-linter/src/printerlinter.egg-info/
|
||||||
|
17
printer-linter/pyproject.toml
Normal file
17
printer-linter/pyproject.toml
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
[project]
|
||||||
|
name = "printerlinter"
|
||||||
|
description = "Cura UltiMaker printer linting tool"
|
||||||
|
version = "0.1.0"
|
||||||
|
authors = [
|
||||||
|
{ name = "UltiMaker", email = "cura@ultimaker.com" }
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"pyyaml"
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
printer-linter = "terminal:main"
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["setuptools"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
10
printer-linter/setup.cfg
Normal file
10
printer-linter/setup.cfg
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[metadata]
|
||||||
|
name = printerlinter
|
||||||
|
|
||||||
|
[options]
|
||||||
|
package_dir=
|
||||||
|
=src
|
||||||
|
packages=find:
|
||||||
|
|
||||||
|
[options.packages.find]
|
||||||
|
where=src
|
6
printer-linter/setup.py
Normal file
6
printer-linter/setup.py
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
|
||||||
|
from setuptools import setup
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
setup()
|
7
printer-linter/src/printerlinter/__init__.py
Normal file
7
printer-linter/src/printerlinter/__init__.py
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
from .defintion import Definition
|
||||||
|
from .diagnostic import Diagnostic
|
||||||
|
from .factory import create
|
||||||
|
from .meshes import Meshes
|
||||||
|
from .profile import Profile
|
||||||
|
|
||||||
|
__all__ = ["Profile", "Definition", "Meshes", "Diagnostic", "create"]
|
@ -1,7 +1,9 @@
|
|||||||
import json
|
import json
|
||||||
|
import re
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from .diagnostic import Diagnostic
|
from .diagnostic import Diagnostic
|
||||||
|
from .replacement import Replacement
|
||||||
|
|
||||||
|
|
||||||
class Definition:
|
class Definition:
|
||||||
@ -11,6 +13,8 @@ class Definition:
|
|||||||
self._defs = {}
|
self._defs = {}
|
||||||
self._getDefs(file)
|
self._getDefs(file)
|
||||||
|
|
||||||
|
self._content = self._file.read_text()
|
||||||
|
|
||||||
settings = {}
|
settings = {}
|
||||||
for k, v in self._defs["fdmprinter"]["settings"].items():
|
for k, v in self._defs["fdmprinter"]["settings"].items():
|
||||||
self._getSetting(k, v, settings)
|
self._getSetting(k, v, settings)
|
||||||
@ -32,24 +36,25 @@ class Definition:
|
|||||||
definition_name = list(self._defs.keys())[0]
|
definition_name = list(self._defs.keys())[0]
|
||||||
definition = self._defs[definition_name]
|
definition = self._defs[definition_name]
|
||||||
if "overrides" in definition and definition_name != "fdmprinter":
|
if "overrides" in definition and definition_name != "fdmprinter":
|
||||||
keys = list(definition["overrides"].keys())
|
|
||||||
for key, value_dict in definition["overrides"].items():
|
for key, value_dict in definition["overrides"].items():
|
||||||
is_redefined, value, parent = self._isDefinedInParent(key, value_dict, definition['inherits'])
|
is_redefined, value, parent = self._isDefinedInParent(key, value_dict, definition['inherits'])
|
||||||
if is_redefined:
|
if is_redefined:
|
||||||
termination_key = keys.index(key) + 1
|
redefined = re.compile(r'.*(\"' + key + r'\"[\s\S]*?\{)[\s\S]*?(\}[,\"]?)')
|
||||||
if termination_key >= len(keys):
|
found = redefined.search(self._content)
|
||||||
# FIXME: find the correct end sequence for now assume it is on the same line
|
yield Diagnostic(
|
||||||
termination_seq = None
|
file = self._file,
|
||||||
else:
|
diagnostic_name = "diagnostic-definition-redundant-override",
|
||||||
termination_seq = keys[termination_key]
|
message = f"Overriding {key} with the same value ({value}) as defined in parent definition: {definition['inherits']}",
|
||||||
yield Diagnostic("diagnostic-definition-redundant-override",
|
level = "Warning",
|
||||||
f"Overriding **{key}** with the same value (**{value}**) as defined in parent definition: **{definition['inherits']}**",
|
offset = found.span(0)[0],
|
||||||
self._file,
|
replacements = [Replacement(
|
||||||
key,
|
file = self._file,
|
||||||
termination_seq)
|
offset = found.span(1)[0],
|
||||||
|
length = found.span(2)[1] - found.span(1)[0],
|
||||||
|
replacement_text = "")]
|
||||||
|
)
|
||||||
|
|
||||||
def checkValueOutOfBounds(self):
|
def checkValueOutOfBounds(self):
|
||||||
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def _getSetting(self, name, setting, settings):
|
def _getSetting(self, name, setting, settings):
|
20
printer-linter/src/printerlinter/diagnostic.py
Normal file
20
printer-linter/src/printerlinter/diagnostic.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
class Diagnostic:
|
||||||
|
def __init__(self, file, diagnostic_name, message, level, offset, replacements=None):
|
||||||
|
self.file = file
|
||||||
|
self.diagnostic_name = diagnostic_name
|
||||||
|
self.message = message
|
||||||
|
self.offset = offset
|
||||||
|
self.level = level
|
||||||
|
self.replacements = replacements
|
||||||
|
|
||||||
|
def toDict(self):
|
||||||
|
diagnostic_dict = {"DiagnosticName": self.diagnostic_name,
|
||||||
|
"DiagnosticMessage": {
|
||||||
|
"Message": self.message,
|
||||||
|
"FilePath": self.file.as_posix(),
|
||||||
|
"FileOffset": self.offset,
|
||||||
|
"Replacements": [] if self.replacements is None else [r.toDict() for r in self.replacements],
|
||||||
|
},
|
||||||
|
"Level": self.level
|
||||||
|
}
|
||||||
|
return diagnostic_dict
|
@ -1,9 +1,6 @@
|
|||||||
from .defintion import Definition
|
|
||||||
from .diagnostic import Diagnostic
|
|
||||||
from .meshes import Meshes
|
|
||||||
from .profile import Profile
|
from .profile import Profile
|
||||||
|
from .defintion import Definition
|
||||||
__all__ = ["Profile", "Definition", "Meshes", "Diagnostic", "create"]
|
from .meshes import Meshes
|
||||||
|
|
||||||
|
|
||||||
def create(file, settings):
|
def create(file, settings):
|
@ -20,15 +20,22 @@ class Meshes:
|
|||||||
|
|
||||||
def checkFileFormat(self):
|
def checkFileFormat(self):
|
||||||
if self._file.suffix.lower() not in (".3mf", ".obj", ".stl"):
|
if self._file.suffix.lower() not in (".3mf", ".obj", ".stl"):
|
||||||
yield Diagnostic("diagnostic-mesh-file-extension",
|
yield Diagnostic(
|
||||||
f"Extension **{self._file.suffix}** not supported, use **3mf**, **obj** or **stl**",
|
file = self._file,
|
||||||
self._file)
|
diagnostic_name = "diagnostic-mesh-file-extension",
|
||||||
|
message = f"Extension {self._file.suffix} not supported, use 3mf, obj or stl",
|
||||||
|
level = "Error",
|
||||||
|
offset = 1
|
||||||
|
)
|
||||||
yield
|
yield
|
||||||
|
|
||||||
def checkFileSize(self):
|
def checkFileSize(self):
|
||||||
|
|
||||||
if self._file.stat().st_size > self._max_file_size:
|
if self._file.stat().st_size > self._max_file_size:
|
||||||
yield Diagnostic("diagnostic-mesh-file-size",
|
yield Diagnostic(
|
||||||
f"Mesh file with a size **{self._file.stat().st_size}** is bigger then allowed maximum of **{self._max_file_size}**",
|
file = self._file,
|
||||||
self._file)
|
diagnostic_name = "diagnostic-mesh-file-size",
|
||||||
|
message = f"Mesh file with a size {self._file.stat().st_size} is bigger then allowed maximum of {self._max_file_size}",
|
||||||
|
level = "Error",
|
||||||
|
offset = 1
|
||||||
|
)
|
||||||
yield
|
yield
|
12
printer-linter/src/printerlinter/replacement.py
Normal file
12
printer-linter/src/printerlinter/replacement.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
class Replacement:
|
||||||
|
def __init__(self, file, offset, length, replacement_text):
|
||||||
|
self.file = file
|
||||||
|
self.offset = offset
|
||||||
|
self.length = length
|
||||||
|
self.replacement_text = replacement_text
|
||||||
|
|
||||||
|
def toDict(self):
|
||||||
|
return {"FilePath": self.file.as_posix(),
|
||||||
|
"Offset": self.offset,
|
||||||
|
"Length": self.length,
|
||||||
|
"ReplacementText": self.replacement_text}
|
@ -8,22 +8,17 @@ from pathlib import Path
|
|||||||
|
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
from tidy import create
|
from printerlinter import factory
|
||||||
|
|
||||||
|
|
||||||
def examineFile(file, settings):
|
def examineFile(file, settings, full_body_check):
|
||||||
patient = create(file, settings)
|
patient = factory.create(file, settings)
|
||||||
if patient is None:
|
if patient is None:
|
||||||
return {}
|
return
|
||||||
|
|
||||||
full_body_check = {f"{file.as_posix()}": []}
|
|
||||||
for diagnostic in patient.check():
|
for diagnostic in patient.check():
|
||||||
if diagnostic:
|
if diagnostic:
|
||||||
full_body_check[f"{file.as_posix()}"].append(diagnostic.toDict())
|
full_body_check["Diagnostics"].append(diagnostic.toDict())
|
||||||
|
|
||||||
if len(full_body_check[f"{file.as_posix()}"]) == 0:
|
|
||||||
del full_body_check[f"{file.as_posix()}"]
|
|
||||||
return full_body_check
|
|
||||||
|
|
||||||
|
|
||||||
def fixFile(file, settings, full_body_check):
|
def fixFile(file, settings, full_body_check):
|
||||||
@ -72,7 +67,24 @@ def formatFile(file: Path, settings):
|
|||||||
config.write(f, space_around_delimiters=settings["format"].get("format-profile-space-around-delimiters", True))
|
config.write(f, space_around_delimiters=settings["format"].get("format-profile-space-around-delimiters", True))
|
||||||
|
|
||||||
|
|
||||||
def main(files, setting_path, to_format, to_fix, to_diagnose, report):
|
def main():
|
||||||
|
parser = ArgumentParser(
|
||||||
|
description="UltiMaker Cura printer linting, static analysis and formatting of Cura printer definitions and other resources")
|
||||||
|
parser.add_argument("--setting", required=False, type=Path, help="Path to the `.printer-linter` setting file")
|
||||||
|
parser.add_argument("--report", required=False, type=Path, help="Path where the diagnostic report should be stored")
|
||||||
|
parser.add_argument("--format", action="store_true", help="Format the files")
|
||||||
|
parser.add_argument("--diagnose", action="store_true", help="Diagnose the files")
|
||||||
|
parser.add_argument("--fix", action="store_true", help="Attempt to apply the suggested fixes on the files")
|
||||||
|
parser.add_argument("Files", metavar="F", type=Path, nargs="+", help="Files or directories to format")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
files = args.Files
|
||||||
|
setting_path = args.setting
|
||||||
|
to_format = args.format
|
||||||
|
to_fix = args.fix
|
||||||
|
to_diagnose = args.diagnose
|
||||||
|
report = args.report
|
||||||
|
|
||||||
if not setting_path:
|
if not setting_path:
|
||||||
setting_path = Path(getcwd(), ".printer-linter")
|
setting_path = Path(getcwd(), ".printer-linter")
|
||||||
|
|
||||||
@ -84,13 +96,13 @@ def main(files, setting_path, to_format, to_fix, to_diagnose, report):
|
|||||||
settings = yaml.load(f, yaml.FullLoader)
|
settings = yaml.load(f, yaml.FullLoader)
|
||||||
|
|
||||||
if to_fix or to_diagnose:
|
if to_fix or to_diagnose:
|
||||||
full_body_check = {}
|
full_body_check = {"Diagnostics": []}
|
||||||
for file in files:
|
for file in files:
|
||||||
if file.is_dir():
|
if file.is_dir():
|
||||||
for fp in file.rglob("**/*"):
|
for fp in file.rglob("**/*"):
|
||||||
full_body_check |= examineFile(fp, settings)
|
examineFile(fp, settings, full_body_check)
|
||||||
else:
|
else:
|
||||||
full_body_check |= examineFile(file, settings)
|
examineFile(file, settings, full_body_check)
|
||||||
|
|
||||||
results = yaml.dump(full_body_check, default_flow_style=False, indent=4, width=240)
|
results = yaml.dump(full_body_check, default_flow_style=False, indent=4, width=240)
|
||||||
if report:
|
if report:
|
||||||
@ -118,14 +130,4 @@ def main(files, setting_path, to_format, to_fix, to_diagnose, report):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
parser = ArgumentParser(
|
main()
|
||||||
description="UltiMaker Cura printer linting, static analysis and formatting of Cura printer definitions and other resources")
|
|
||||||
parser.add_argument("--setting", required=False, type=Path, help="Path to the `.printer-linter` setting file")
|
|
||||||
parser.add_argument("--report", required=False, type=Path, help="Path where the diagnostic report should be stored")
|
|
||||||
parser.add_argument("--format", action="store_true", help="Format the files")
|
|
||||||
parser.add_argument("--diagnose", action="store_true", help="Diagnose the files")
|
|
||||||
parser.add_argument("--fix", action="store_true", help="Attempt to apply the suggested fixes on the files")
|
|
||||||
parser.add_argument("Files", metavar="F", type=Path, nargs="+", help="Files or directories to format")
|
|
||||||
|
|
||||||
args = parser.parse_args()
|
|
||||||
main(args.Files, args.setting, args.format, args.fix, args.diagnose, args.report)
|
|
@ -1,85 +0,0 @@
|
|||||||
class Diagnostic:
|
|
||||||
def __init__(self, illness, msg, file, key=None, termination_seq=None):
|
|
||||||
self.illness = illness
|
|
||||||
self.key = key
|
|
||||||
self.msg = msg
|
|
||||||
self.file = file
|
|
||||||
self._lines = None
|
|
||||||
self._location = None
|
|
||||||
self._fix = None
|
|
||||||
self._content_block = None
|
|
||||||
self._termination_seq = termination_seq
|
|
||||||
|
|
||||||
@property
|
|
||||||
def location(self):
|
|
||||||
if self._location:
|
|
||||||
return self._location
|
|
||||||
if not self._lines:
|
|
||||||
with open(self.file, "r") as f:
|
|
||||||
if not self.is_text_file:
|
|
||||||
self._fix = ""
|
|
||||||
return self._fix
|
|
||||||
self._lines = f.readlines()
|
|
||||||
|
|
||||||
start_location = {"col": 1, "line": 1}
|
|
||||||
end_location = {"col": len(self._lines[-1]) + 1, "line": len(self._lines) + 1}
|
|
||||||
|
|
||||||
if self.key is not None:
|
|
||||||
for lino, line in enumerate(self._lines, 1):
|
|
||||||
if f'"{self.key}":' in line:
|
|
||||||
col = line.index(f'"{self.key}":') + 1
|
|
||||||
start_location = {"col": col, "line": lino}
|
|
||||||
if self._termination_seq is None:
|
|
||||||
end_location = {"col": len(line) + 1, "line": lino}
|
|
||||||
break
|
|
||||||
if f'"{self._termination_seq}":' in line:
|
|
||||||
col = line.index(f'"{self._termination_seq}":') + 1
|
|
||||||
end_location = {"col": col, "line": lino}
|
|
||||||
self._location = {"start": start_location, "end": end_location}
|
|
||||||
return self._location
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_text_file(self):
|
|
||||||
return self.file.name.split(".", maxsplit=1)[-1] in ("def.json", "inst.cfg")
|
|
||||||
|
|
||||||
@property
|
|
||||||
def content_block(self):
|
|
||||||
if self._content_block:
|
|
||||||
return self._content_block
|
|
||||||
|
|
||||||
if not self._lines:
|
|
||||||
if not self.is_text_file:
|
|
||||||
self._fix = ""
|
|
||||||
return self._fix
|
|
||||||
with open(self.file, "r") as f:
|
|
||||||
self._lines = f.readlines()
|
|
||||||
|
|
||||||
start_line = self.location["start"]["line"] - 1
|
|
||||||
end_line = self.location["end"]["line"] - 1
|
|
||||||
self._content_block = "\n".join(self._lines[start_line:end_line])
|
|
||||||
return self._content_block
|
|
||||||
|
|
||||||
@property
|
|
||||||
def fix(self):
|
|
||||||
if self._fix:
|
|
||||||
return self._fix
|
|
||||||
|
|
||||||
if not self._lines:
|
|
||||||
if not self.is_text_file:
|
|
||||||
self._fix = ""
|
|
||||||
return self._fix
|
|
||||||
with open(self.file, "r") as f:
|
|
||||||
self._lines = f.readlines()
|
|
||||||
|
|
||||||
start_line = self.location["start"]["line"] - 2
|
|
||||||
start_col = 0
|
|
||||||
end_line = self.location["end"]["line"] - 1
|
|
||||||
end_col = len(self._lines[start_line:end_line - 1]) + self.location["start"]["col"] - 4 # TODO: double check if 4 holds in all instances
|
|
||||||
self._fix = self.content_block[start_col:end_col]
|
|
||||||
return self._fix
|
|
||||||
|
|
||||||
def toDict(self):
|
|
||||||
diagnostic_dict = {"diagnostic": self.illness, "message": self.msg}
|
|
||||||
if self.is_text_file:
|
|
||||||
diagnostic_dict |= {"fix": self.fix, "lino": self.location, "content": self.content_block}
|
|
||||||
return diagnostic_dict
|
|
Loading…
x
Reference in New Issue
Block a user