scripts: ci: check_compliance: Add python lint/format check

Add a compliance test using ruff, for both linting and formatting of
newly added python files.

Signed-off-by: Pieter De Gendt <pieter.degendt@basalte.be>
diff --git a/.github/workflows/compliance.yml b/.github/workflows/compliance.yml
index 48dfa23..a91a8e5 100644
--- a/.github/workflows/compliance.yml
+++ b/.github/workflows/compliance.yml
@@ -38,7 +38,7 @@
       run: |
         pip3 install setuptools
         pip3 install wheel
-        pip3 install python-magic lxml junitparser gitlint pylint pykwalify yamllint clang-format unidiff sphinx-lint
+        pip3 install python-magic lxml junitparser gitlint pylint pykwalify yamllint clang-format unidiff sphinx-lint ruff
         pip3 install west
 
     - name: west setup
diff --git a/.gitignore b/.gitignore
index c33e2d9..b545443 100644
--- a/.gitignore
+++ b/.gitignore
@@ -102,6 +102,7 @@
 ModulesMaintainers.txt
 Nits.txt
 Pylint.txt
+Ruff.txt
 SphinxLint.txt
 TextEncoding.txt
 YAMLLint.txt
diff --git a/scripts/ci/check_compliance.py b/scripts/ci/check_compliance.py
index 47edd29..40b90e2 100755
--- a/scripts/ci/check_compliance.py
+++ b/scripts/ci/check_compliance.py
@@ -1639,6 +1639,54 @@
                 self.check_file(file, fp)
 
 
+class Ruff(ComplianceTest):
+    """
+    Ruff
+    """
+    name = "Ruff"
+    doc = "Check python files with ruff."
+    path_hint = "<git-top>"
+
+    def run(self):
+        for file in get_files(filter="d"):
+            if not file.endswith(".py"):
+                continue
+
+            try:
+                subprocess.run(
+                    f"ruff check --force-exclude --output-format=json {file}",
+                    check=True,
+                    stdout=subprocess.PIPE,
+                    stderr=subprocess.STDOUT,
+                    shell=True,
+                    cwd=GIT_TOP,
+                )
+            except subprocess.CalledProcessError as ex:
+                output = ex.output.decode("utf-8")
+                messages = json.loads(output)
+                for m in messages:
+                    self.fmtd_failure(
+                        "error",
+                        f'Python lint error ({m.get("code")}) see {m.get("url")}',
+                        file,
+                        line=m.get("location", {}).get("row"),
+                        col=m.get("location", {}).get("column"),
+                        end_line=m.get("end_location", {}).get("row"),
+                        end_col=m.get("end_location", {}).get("column"),
+                        desc=m.get("message"),
+                    )
+            try:
+                subprocess.run(
+                    f"ruff format --force-exclude --diff {file}",
+                    check=True,
+                    shell=True,
+                    cwd=GIT_TOP,
+                )
+            except subprocess.CalledProcessError:
+                desc = f"Run 'ruff format {file}'"
+                self.fmtd_failure("error", "Python format error", file, desc=desc)
+
+
 class TextEncoding(ComplianceTest):
     """
     Check that any text file is encoded in ascii or utf-8.
diff --git a/scripts/requirements-compliance.txt b/scripts/requirements-compliance.txt
index a439698..273b4f3 100644
--- a/scripts/requirements-compliance.txt
+++ b/scripts/requirements-compliance.txt
@@ -10,3 +10,4 @@
 unidiff
 yamllint
 sphinx-lint
+ruff