Merge pull request #13856 from Ultimaker/printer_linter

Added a linting tool for Cura Printers and Profiles
This commit is contained in:
Joey de l'Arago 2022-11-29 17:58:59 +01:00 committed by GitHub
commit 82f4f9ed16
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 799 additions and 0 deletions

View File

@ -0,0 +1,45 @@
name: printer-linter-format
on:
push:
branches:
- main
- '[1-9].[0-9]'
- '[1-9].[0-9][0-9]'
path:
- 'resources/**'
jobs:
printer-linter-format:
name: Printer linter auto format
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: 3.11.x
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-printer-linter.txt
- uses: technote-space/get-diff-action@v6
with:
PATTERNS: |
resources/+(definitions|extruders)/*.def.json
resources/+(intent|quality|variants)/**/*.inst.cfg
- name: Install Python requirements for runner
if: env.GIT_DIFF && !env.MATCHED_FILES
run: pip install -r .github/workflows/requirements-printer-linter.txt
- name: Format file
if: env.GIT_DIFF && !env.MATCHED_FILES
run: python printer-linter/src/terminal.py --format ${{ env.GIT_DIFF_FILTERED }}
- uses: stefanzweifel/git-auto-commit-action@v4
if: env.GIT_DIFF && !env.MATCHED_FILES
with:
commit_message: "Applied printer-linter format"

View File

@ -0,0 +1,59 @@
name: printer-linter-pr-diagnose
on:
pull_request:
path:
- 'resources/**'
jobs:
printer-linter-diagnose:
name: Printer linter PR diagnose
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 2
- name: Setup Python and pip
uses: actions/setup-python@v4
with:
python-version: 3.11.x
cache: 'pip'
cache-dependency-path: .github/workflows/requirements-printer-linter.txt
- uses: technote-space/get-diff-action@v6
with:
PATTERNS: |
resources/+(extruders|definitions)/*.def.json
resources/+(intent|quality|variants)/**/*.inst.cfg
- name: Install Python requirements for runner
if: env.GIT_DIFF && !env.MATCHED_FILES
run: pip install -r .github/workflows/requirements-printer-linter.txt
- name: Create results directory
run: mkdir printer-linter-result
- name: Diagnose file(s)
if: env.GIT_DIFF && !env.MATCHED_FILES
run: python printer-linter/src/terminal.py --diagnose --report printer-linter-result/fixes.yml ${{ env.GIT_DIFF_FILTERED }}
- name: Save PR metadata
run: |
echo ${{ github.event.number }} > printer-linter-result/pr-id.txt
echo ${{ github.event.pull_request.head.repo.full_name }} > printer-linter-result/pr-head-repo.txt
echo ${{ github.event.pull_request.head.ref }} > printer-linter-result/pr-head-ref.txt
- uses: actions/upload-artifact@v2
with:
name: printer-linter-result
path: printer-linter-result/
- name: Run clang-tidy-pr-comments action
uses: platisd/clang-tidy-pr-comments@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
clang_tidy_fixes: result.yml
request_changes: true

View File

@ -0,0 +1,80 @@
name: printer-linter-pr-post
on:
workflow_run:
workflows: [ "printer-linter-pr-diagnose" ]
types: [ completed ]
jobs:
clang-tidy-results:
# Trigger the job only if the previous (insecure) workflow completed successfully
if: ${{ github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' }}
runs-on: ubuntu-latest
steps:
- name: Download analysis results
uses: actions/github-script@v3.1.0
with:
script: |
let artifacts = await github.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{github.event.workflow_run.id }},
});
let matchArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "printer-linter-result"
})[0];
let download = await github.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: "zip",
});
let fs = require("fs");
fs.writeFileSync("${{github.workspace}}/printer-linter-result.zip", Buffer.from(download.data));
- name: Set environment variables
run: |
mkdir printer-linter-result
unzip printer-linter-result.zip -d printer-linter-result
echo "pr_id=$(cat printer-linter-result/pr-id.txt)" >> $GITHUB_ENV
echo "pr_head_repo=$(cat printer-linter-result/pr-head-repo.txt)" >> $GITHUB_ENV
echo "pr_head_ref=$(cat printer-linter-result/pr-head-ref.txt)" >> $GITHUB_ENV
- uses: actions/checkout@v2
with:
repository: ${{ env.pr_head_repo }}
ref: ${{ env.pr_head_ref }}
persist-credentials: false
- name: Redownload analysis results
uses: actions/github-script@v3.1.0
with:
script: |
let artifacts = await github.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: ${{github.event.workflow_run.id }},
});
let matchArtifact = artifacts.data.artifacts.filter((artifact) => {
return artifact.name == "printer-linter-result"
})[0];
let download = await github.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: "zip",
});
let fs = require("fs");
fs.writeFileSync("${{github.workspace}}/printer-linter-result.zip", Buffer.from(download.data));
- name: Extract analysis results
run: |
mkdir printer-linter-result
unzip printer-linter-result.zip -d printer-linter-result
- name: Run clang-tidy-pr-comments action
uses: platisd/clang-tidy-pr-comments@master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
clang_tidy_fixes: printer-linter-result/fixes.yml
pull_request_id: ${{ env.pr_id }}

View File

@ -0,0 +1 @@
pyyaml

1
.gitignore vendored
View File

@ -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/

15
.printer-linter Normal file
View File

@ -0,0 +1,15 @@
checks:
diagnostic-mesh-file-extension: true
diagnostic-mesh-file-size: true
diagnostic-definition-redundant-override: true
fixes:
diagnostic-definition-redundant-override: true
format:
format-definition-bracket-newline: true
format-definition-paired-coordinate-array: true
format-definition-sort-keys: true
format-definition-indent: 4
format-definition-single-value-single-line: true # Format dicts and lists with a single item on one line "dict": { "value": 10 }
format-profile-space-around-delimiters: true
format-profile-sort-keys: true
diagnostic-mesh-file-size: 1200000

33
printer-linter/README.md Normal file
View File

@ -0,0 +1,33 @@
# Printer Linter
Printer linter is a python package that does linting on Cura definitions files.
Running this on your definition files will get them ready for a pull request.
## Running Locally
From the Cura root folder.
```python3 printer-linter/src/terminal.py "flashforge_dreamer_nx.def.json" "flashforge_base.def.json" --fix --format```
## Developing
### Printer Linter Rules
Inside ```.printer-linter``` you can find a list of rules. These are seperated into roughly three categories.
1. Checks
1. These rules are about checking if a file meets some requirements that can't be fixed by replacing its content.
2. An example of a check is ```diagnostic-mesh-file-extension``` this checks if a mesh file extension is acceptable.
2. Format
1. These rules are purely about how a file is structured, not content.
2. An example of a format rule is ```format-definition-bracket-newline``` This rule says that when assigning a dict value the bracket should go on a new line.
3. Fixes
1. These are about the content of the file.
2. An example of a fix is ```diagnostic-definition-redundant-override``` This removes settings that have already been defined by a parent definition
### Linters
Linters find issues within a file. There are separate linters for each type of file. The linter that is used is decided by the create function in factory.py. All linters implement the abstract class Linter.
A Linter class returns an iterator of Diagnostics, each diagnostic is an issue with the file. The diagnostics can also contain suggested fixes.
### Formatters
Formatters load a file reformat it and write it to disk. There are separate formatters for each file type. All formatters implement the abstract class Formatter.
Formatters should format based on the Format rules in .printer-linter

View 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
View 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
View File

@ -0,0 +1,6 @@
#!/usr/bin/env python
from setuptools import setup
if __name__ == "__main__":
setup()

View File

@ -0,0 +1,4 @@
from .diagnostic import Diagnostic
from .factory import getLinter
__all__ = ["Diagnostic", "getLinter"]

View File

@ -0,0 +1,34 @@
from pathlib import Path
from typing import Optional, List, Dict, Any
from .replacement import Replacement
class Diagnostic:
def __init__(self, file: Path, diagnostic_name: str, message: str, level: str, offset: int, replacements: Optional[List[Replacement]] = None) -> None:
""" A diagnosis of an issue in "file" at "offset" in that file. May include suggested replacements.
@param file: The path to the file this diagnostic is for.
@param diagnostic_name: The name of the diagnostic rule that spawned this result. A list can be found in .printer-linter.
@param message: A message explaining the issue with this file.
@param level: How important this diagnostic is, ranges from Warning -> Error.
@param offset: The offset in file where the issue is.
@param replacements: A list of Replacement that contain replacement text.
"""
self.file = file
self.diagnostic_name = diagnostic_name
self.message = message
self.offset = offset
self.level = level
self.replacements = replacements
def toDict(self) -> Dict[str, Any]:
return {"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
}

View File

@ -0,0 +1,26 @@
from pathlib import Path
from typing import Optional
from .linters.profile import Profile
from .linters.defintion import Definition
from .linters.linter import Linter
from .linters.meshes import Meshes
def getLinter(file: Path, settings: dict) -> Optional[Linter]:
""" Returns a Linter depending on the file format """
if not file.exists():
return None
if ".inst" in file.suffixes and ".cfg" in file.suffixes:
return Profile(file, settings)
if ".def" in file.suffixes and ".json" in file.suffixes:
if file.stem in ("fdmprinter.def", "fdmextruder.def"):
return None
return Definition(file, settings)
if file.parent.stem == "meshes":
return Meshes(file, settings)
return None

View File

@ -0,0 +1,4 @@
from .def_json_formatter import DefJsonFormatter
from .inst_cfg_formatter import InstCfgFormatter
__all__ = ["DefJsonFormatter", "InstCfgFormatter"]

View File

@ -0,0 +1,84 @@
import json
import re
from collections import OrderedDict
from pathlib import Path
from typing import Dict
from .formatter import FileFormatter
# Dictionary items with matching keys will be sorted as if they were the value
# Example: "version" will be sorted as if it was "0"
TOP_LEVEL_SORT_PRIORITY = {
"version": "0",
"name": "1",
"inherits": "3",
}
METADATA_SORT_PRIORITY = {
"visible": "0",
"author": "1",
"manufacturer": "2",
"file_formats": "3",
"platform": "4",
}
class DefJsonFormatter(FileFormatter):
def formatFile(self, file: Path):
""" Format .def.json files according to the rules in settings.
You can assume that you will be running regex on standard formatted json files, because we load the json first and then
dump it to a string. This means you only have to write regex that works on the output of json.dump()
"""
definition = json.loads(file.read_text(), object_pairs_hook=OrderedDict)
if self._settings["format"].get("format-definition-sort-keys", True):
definition = self.order_keys(definition)
content = json.dumps(definition, indent=self._settings["format"].get("format-definition-indent", 4))
if self._settings["format"].get("format-definition-bracket-newline", True):
newline = re.compile(r"(\B\s+)(\"[\w\"]+)(\:\s\{)")
content = newline.sub(r"\1\2:\1{", content)
if self._settings["format"].get("format-definition-single-value-single-line", True):
single_value_dict = re.compile(r"(:)(\s*\n?.*\{\s+)(\".*)(\d*\s*\})(.*\n,?)")
content = single_value_dict.sub(r"\1 { \3 }\5", content)
single_value_list = re.compile(r"(:)(\s*\n?.*\[\s+)(\".*)(\d*\s*\])(.*\n,?)")
content = single_value_list.sub(r"\1 [ \3 ]\5", content)
if self._settings["format"].get("format-definition-paired-coordinate-array", True):
paired_coordinates = re.compile(r"(\s*\[)\s*([-\d\.]+),\s*([-\d\.]+)[\s]*(\])")
content = paired_coordinates.sub(r"\1\2, \3\4", content)
file.write_text(content)
def order_keys(self, json_content: OrderedDict) -> OrderedDict:
""" Orders json keys lexicographically """
# First order all keys (Recursive) lexicographically
json_content_text = json.dumps(json_content, sort_keys=True)
json_content = json.loads(json_content_text, object_pairs_hook=OrderedDict)
# Do a custom ordered sort on the top level items in the json. This is so that keys like "version" appear at the top.
json_content = self.custom_sort_keys(json_content, TOP_LEVEL_SORT_PRIORITY)
# Do a custom ordered sort on collections that are one level deep into the json
if "metadata" in json_content.keys():
json_content["metadata"] = self.custom_sort_keys(json_content["metadata"], METADATA_SORT_PRIORITY)
return json_content
def custom_sort_keys(self, ordered_dictionary: OrderedDict, sort_priority: Dict[str, str]) -> OrderedDict:
""" Orders keys in dictionary lexicographically, except for keys with matching strings in sort_priority.
Keys in ordered_dictionary that match keys in sort_priority will sort based on the value in sort_priority.
@param ordered_dictionary: A dictionary that will have it's top level keys sorted
@param sort_priority: A mapping from string keys to alternative strings to be used instead when sorting.
@return: A dictionary sorted by it's top level keys
"""
return OrderedDict(sorted(ordered_dictionary.items(), key=lambda x: sort_priority[x[0]] if str(x[0]) in sort_priority.keys() else str(x[0])))

View File

@ -0,0 +1,16 @@
from abc import ABC, abstractmethod
from pathlib import Path
class FileFormatter(ABC):
def __init__(self, settings: dict) -> None:
""" Yields Diagnostics for file, these are issues with the file such as bad text format or too large file size.
@param file: A file to generate diagnostics for
@param settings: A list of settings containing rules for creating diagnostics
"""
self._settings = settings
@abstractmethod
def formatFile(self, file: Path) -> None:
pass

View File

@ -0,0 +1,21 @@
import configparser
import json
import re
from collections import OrderedDict
from pathlib import Path
from .formatter import FileFormatter
class InstCfgFormatter(FileFormatter):
def formatFile(self, file: Path):
""" Format .inst.cfg files according to the rules in settings """
config = configparser.ConfigParser()
config.read(file)
if self._settings["format"].get("format-profile-sort-keys", True):
for section in config._sections:
config._sections[section] = OrderedDict(sorted(config._sections[section].items(), key=lambda t: t[0]))
config._sections = OrderedDict(sorted(config._sections.items(), key=lambda t: t[0]))
with open(file, "w") as f:
config.write(f, space_around_delimiters=self._settings["format"].get("format-profile-space-around-delimiters", True))

View File

@ -0,0 +1,6 @@
from .profile import Profile
from .meshes import Meshes
from .linter import Linter
from .defintion import Definition
__all__ = ["Profile", "Meshes", "Linter", "Definition"]

View File

@ -0,0 +1,122 @@
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._loadDefinitionFiles(file)
self._content = self._file.read_text()
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
# 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_name = list(self._definitions.keys())[0]
definition = self._definitions[definition_name]
if "overrides" in definition and definition_name not in ("fdmprinter", "fdmextruder"):
for key, value_dict in definition["overrides"].items():
is_redefined, value, parent = 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)
yield Diagnostic(
file = self._file,
diagnostic_name = "diagnostic-definition-redundant-override",
message = f"Overriding {key} with the same value ({value}) as defined in parent definition: {definition['inherits']}",
level = "Warning",
offset = found.span(0)[0],
replacements = [Replacement(
file = self._file,
offset = found.span(1)[0],
length = found.span(2)[1] - found.span(1)[0],
replacement_text = "")]
)
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
# 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 "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 value in value_dict.values():
if key in parent:
check_values = [cv for cv in [parent[key].get("default_value", None), parent[key].get("value", None)] if cv is not None]
for check_value in check_values:
if is_number:
try:
v = str(float(value))
except:
v = value
try:
cv = str(float(check_value))
except:
cv = check_value
else:
v = value
cv = check_value
if v == cv:
return True, value, parent
if "inherits" in parent:
return self._isDefinedInParent(key, value_dict, parent["inherits"])
return False, None, None
def _loadBasePrinterSettings(self):
""" TODO @Jelle please explain why this """
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}

View File

@ -0,0 +1,20 @@
from abc import ABC, abstractmethod
from pathlib import Path
from typing import Iterator
from ..diagnostic import Diagnostic
class Linter(ABC):
def __init__(self, file: Path, settings: dict) -> None:
""" Yields Diagnostics for file, these are issues with the file such as bad text format or too large file size.
@param file: A file to generate diagnostics for
@param settings: A list of settings containing rules for creating diagnostics
"""
self._settings = settings
self._file = file
@abstractmethod
def check(self) -> Iterator[Diagnostic]:
pass

View File

@ -0,0 +1,47 @@
from pathlib import Path
from typing import Iterator
from ..diagnostic import Diagnostic
from .linter import Linter
class Meshes(Linter):
def __init__(self, file: Path, settings: dict) -> None:
""" Finds issues in model files, such as incorrect file format or too large size """
super().__init__(file, settings)
self._max_file_size = self._settings.get("diagnostic-mesh-file-size", 1e6)
def check(self) -> Iterator[Diagnostic]:
if self._settings["checks"].get("diagnostic-mesh-file-extension", False):
for check in self.checkFileFormat():
yield check
if self._settings["checks"].get("diagnostic-mesh-file-size", False):
for check in self.checkFileSize():
yield check
yield
def checkFileFormat(self) -> Iterator[Diagnostic]:
""" Check if mesh is in supported format """
if self._file.suffix.lower() not in (".3mf", ".obj", ".stl"):
yield Diagnostic(
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
def checkFileSize(self) -> Iterator[Diagnostic]:
""" Check if file is within size limits for Cura """
if self._file.stat().st_size > self._max_file_size:
yield Diagnostic(
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

View File

@ -0,0 +1,9 @@
from typing import Iterator
from ..diagnostic import Diagnostic
from .linter import Linter
class Profile(Linter):
def check(self) -> Iterator[Diagnostic]:
yield

View File

@ -0,0 +1,21 @@
from pathlib import Path
class Replacement:
def __init__(self, file: Path, offset: int, length: int, replacement_text: str):
""" Replacement text for file between offset and offset+length.
@param file: File to replace text in
@param offset: Offset in file to start text replace
@param length: Length of text that will be replaced. offset -> offset+length is the section of text to replace.
@param replacement_text: Text to insert of offset in file.
"""
self.file = file
self.offset = offset
self.length = length
self.replacement_text = replacement_text
def toDict(self) -> dict:
return {"FilePath": self.file.as_posix(),
"Offset": self.offset,
"Length": self.length,
"ReplacementText": self.replacement_text}

View File

@ -0,0 +1,117 @@
from argparse import ArgumentParser
from os import getcwd
from pathlib import Path
from typing import List
import yaml
from printerlinter import factory
from printerlinter.diagnostic import Diagnostic
from printerlinter.formatters.def_json_formatter import DefJsonFormatter
from printerlinter.formatters.inst_cfg_formatter import InstCfgFormatter
def main() -> None:
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 = extractFilePaths(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:
setting_path = Path(getcwd(), ".printer-linter")
if not setting_path.exists():
print(f"Can't find the settings: {setting_path}")
return
with open(setting_path, "r") as f:
settings = yaml.load(f, yaml.FullLoader)
full_body_check = {"Diagnostics": []}
if to_fix or to_diagnose:
for file in files:
diagnostics = diagnoseIssuesWithFile(file, settings)
full_body_check["Diagnostics"].extend([d.toDict() for d in diagnostics])
results = yaml.dump(full_body_check, default_flow_style=False, indent=4, width=240)
if report:
report.write_text(results)
else:
print(results)
if to_fix:
for file in files:
if f"{file.as_posix()}" in full_body_check:
applyFixesToFile(file, settings, full_body_check)
if to_format:
for file in files:
applyFormattingToFile(file, settings)
def diagnoseIssuesWithFile(file: Path, settings: dict) -> List[Diagnostic]:
""" For file, runs all diagnostic checks in settings and returns a list of diagnostics """
linter = factory.getLinter(file, settings)
if not linter:
return []
return list(filter(lambda d: d is not None, linter.check()))
def applyFixesToFile(file, settings, full_body_check) -> None:
if not file.exists():
return
ext = ".".join(file.name.split(".")[-2:])
if ext == "def.json":
issues = full_body_check[f"{file.as_posix()}"]
for issue in issues:
if issue["diagnostic"] == "diagnostic-definition-redundant-override" and settings["fixes"].get(
"diagnostic-definition-redundant-override", True):
pass
def applyFormattingToFile(file: Path, settings) -> None:
if not file.exists():
return
ext = ".".join(file.name.split(".")[-2:])
if ext == "def.json":
formatter = DefJsonFormatter(settings)
formatter.formatFile(file)
if ext == "inst.cfg":
formatter = InstCfgFormatter(settings)
formatter.formatFile(file)
def extractFilePaths(paths: List[Path]) -> List[Path]:
""" Takes list of files and directories, returns the files as well as all files within directories as a List """
file_paths = []
for path in paths:
if path.is_dir():
file_paths.extend(path.rglob("**/*"))
else:
file_paths.append(path)
return file_paths
if __name__ == "__main__":
main()

View File

@ -1,5 +1,6 @@
pytest pytest
pyinstaller pyinstaller
pyinstaller-hooks-contrib pyinstaller-hooks-contrib
pyyaml
sip==6.5.1 sip==6.5.1
jinja2 jinja2