[Python] Add ruff as Python linter (#32384)

* Add ruff as Python linter

* Add ruff to linter workflow

* Remove flake8

* Set default line-length and explicitly select rules

* Address REPL Test issue

* Explicitly set line lenght for autopep8

* Fix restyled config
diff --git a/.flake8 b/.flake8
deleted file mode 100644
index 65b1c2a..0000000
--- a/.flake8
+++ /dev/null
@@ -1,6 +0,0 @@
-[flake8]
-max-line-length = 132
-exclude = third_party
-          .*
-          out/*
-          ./examples/common/QRCode/*
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index 70edd53..8e67de5 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -29,7 +29,7 @@
         if: github.actor != 'restyled-io[bot]'
 
         container:
-            image: ghcr.io/project-chip/chip-build:35
+            image: ghcr.io/project-chip/chip-build:39
 
         steps:
             - name: Checkout
@@ -267,12 +267,11 @@
               run: |
                   git grep -I -n 'emberAfWriteAttribute' -- './*' ':(exclude).github/workflows/lint.yml' ':(exclude)src/app/util/af.h' ':(exclude)zzz_generated/app-common/app-common/zap-generated/attributes/Accessors.cpp' ':(exclude)src/app/zap-templates/templates/app/attributes/Accessors-src.zapt' ':(exclude)src/app/util/attribute-table.cpp' ':(exclude)examples/common/pigweed/rpc_services/Attributes.h' ':(exclude)src/app/util/attribute-table.h' ':(exclude)src/app/util/ember-compatibility-functions.cpp' && exit 1 || exit 0
 
-            # Run python Linter (flake8) and verify python files
-            # ignore some style errors, restyler should do that
-            - name: Check for errors using flake8 Python linter
+            # Run ruff python linter
+            - name: Check for errors using ruff Python linter
               if: always()
               run: |
-                  flake8 --extend-ignore=E501,W391
+                  ruff check
 
             # git grep exits with 0 if it finds a match, but we want
             # to fail (exit nonzero) on match.  And we want to exclude this file,
diff --git a/.restyled.yaml b/.restyled.yaml
index 56ed306..9acbe8e 100644
--- a/.restyled.yaml
+++ b/.restyled.yaml
@@ -230,6 +230,8 @@
       command:
           - autopep8
           - "--in-place"
+          - "--max-line-length"
+          - "132"
       arguments: []
       include:
           - "**/*.py"
diff --git a/integrations/docker/images/base/chip-build/Dockerfile b/integrations/docker/images/base/chip-build/Dockerfile
index 39aada6..a2f83fc 100644
--- a/integrations/docker/images/base/chip-build/Dockerfile
+++ b/integrations/docker/images/base/chip-build/Dockerfile
@@ -144,7 +144,6 @@
     click \
     coloredlogs \
     cxxfilt \
-    flake8 \
     future \
     ghapi \
     mobly \
diff --git a/ruff.toml b/ruff.toml
new file mode 100644
index 0000000..fec7608
--- /dev/null
+++ b/ruff.toml
@@ -0,0 +1,19 @@
+exclude = [
+    ".environment",
+    ".git",
+    ".github",
+    ".*",
+    "build",
+    "out",
+    "third_party",
+    "examples/common/QRCode/",
+]
+target-version = "py37"
+
+line-length = 132
+
+[lint]
+select = ["E4", "E7", "E9", "F"]
+ignore = [
+    "E721" # We use is for good reasons
+]
diff --git a/scripts/py_matter_idl/matter_idl/generators/java/__init__.py b/scripts/py_matter_idl/matter_idl/generators/java/__init__.py
index da7eef2..96fa37c 100644
--- a/scripts/py_matter_idl/matter_idl/generators/java/__init__.py
+++ b/scripts/py_matter_idl/matter_idl/generators/java/__init__.py
@@ -114,7 +114,6 @@
     'event_no': 'chip::EventNumber',
     'fabric_id': 'chip::FabricId',
     'fabric_idx': 'chip::FabricIndex',
-    'fabric_idx': 'chip::FabricIndex',
     'field_id': 'chip::FieldId',
     'group_id': 'chip::GroupId',
     'node_id': 'chip::NodeId',
diff --git a/scripts/py_matter_idl/matter_idl/generators/kotlin/__init__.py b/scripts/py_matter_idl/matter_idl/generators/kotlin/__init__.py
index b069319..17e5136 100644
--- a/scripts/py_matter_idl/matter_idl/generators/kotlin/__init__.py
+++ b/scripts/py_matter_idl/matter_idl/generators/kotlin/__init__.py
@@ -114,7 +114,6 @@
     'event_no': 'chip::EventNumber',
     'fabric_id': 'chip::FabricId',
     'fabric_idx': 'chip::FabricIndex',
-    'fabric_idx': 'chip::FabricIndex',
     'field_id': 'chip::FieldId',
     'group_id': 'chip::GroupId',
     'node_id': 'chip::NodeId',
diff --git a/scripts/py_matter_yamltests/matter_yamltests/parser.py b/scripts/py_matter_yamltests/matter_yamltests/parser.py
index cb0a89c..9612d57 100644
--- a/scripts/py_matter_yamltests/matter_yamltests/parser.py
+++ b/scripts/py_matter_yamltests/matter_yamltests/parser.py
@@ -1086,7 +1086,7 @@
                 break
 
             received_value = received_response.get('value')
-            if not self.is_attribute and not self.is_event and not (self.command in ANY_COMMANDS_LIST):
+            if not self.is_attribute and not self.is_event and self.command not in ANY_COMMANDS_LIST:
                 expected_name = value.get('name')
                 if expected_name not in received_value:
                     result.error(check_type, error_name_does_not_exist.format(
@@ -1173,7 +1173,7 @@
                 continue
 
             received_value = received_response.get(default_target)
-            if not self.is_attribute and not self.is_event and not (self.command in ANY_COMMANDS_LIST):
+            if not self.is_attribute and not self.is_event and self.command not in ANY_COMMANDS_LIST:
                 expected_name = value.get('name')
                 if received_value is None or expected_name not in received_value:
                     result.error(check_type, error_name_does_not_exist.format(
diff --git a/scripts/tests/yaml/extensions/wildcard_response_extractor_cluster.py b/scripts/tests/yaml/extensions/wildcard_response_extractor_cluster.py
index a0e34cc..2f60a52 100644
--- a/scripts/tests/yaml/extensions/wildcard_response_extractor_cluster.py
+++ b/scripts/tests/yaml/extensions/wildcard_response_extractor_cluster.py
@@ -113,7 +113,7 @@
         if arguments is None:
             return None
 
-        if not type(arguments) is list:
+        if type(arguments) is not list:
             return None
 
         for argument in arguments:
diff --git a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py
index 7cddb76..7244840 100644
--- a/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py
+++ b/scripts/tools/nrfconnect/generate_nrfconnect_chip_factory_data.py
@@ -246,7 +246,7 @@
             "Cannot find paths to DAC or PAI certificates .der files. To generate a new ones please provide a path to chip-cert executable (--chip_cert_path) and add --gen_certs argument"
         assert self._args.output.endswith(".json"), \
             "Output path doesn't contain .json file path. ({})".format(self._args.output)
-        assert not (self._args.passcode in INVALID_PASSCODES), \
+        assert self._args.passcode not in INVALID_PASSCODES, \
             "Provided invalid passcode!"
 
     def generate_json(self):
diff --git a/scripts/tools/silabs/FactoryDataProvider.py b/scripts/tools/silabs/FactoryDataProvider.py
index 8cb4895..3ff8ece 100644
--- a/scripts/tools/silabs/FactoryDataProvider.py
+++ b/scripts/tools/silabs/FactoryDataProvider.py
@@ -140,7 +140,7 @@
 
         assert (bool(arguments.gen_spake2p_path) != bool(arguments.spake2_verifier)
                 ), "Provide either the spake2_verifier string or the path to the spake2 generator"
-        assert not (arguments.passcode in INVALID_PASSCODES), "The provided passcode is invalid"
+        assert arguments.passcode not in INVALID_PASSCODES, "The provided passcode is invalid"
 
         self._args = arguments
 
diff --git a/src/python_testing/TestSpecParsingSupport.py b/src/python_testing/TestSpecParsingSupport.py
index 2ff80e5..2fdfcdc 100644
--- a/src/python_testing/TestSpecParsingSupport.py
+++ b/src/python_testing/TestSpecParsingSupport.py
@@ -262,7 +262,7 @@
                              0, "Unexpected number of unknown commands in base")
         asserts.assert_equal(len(clusters[0xFFFF].unknown_commands), 2, "Unexpected number of unknown commands in derived cluster")
 
-        combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name)
+        combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name, problems)
         # Ensure the base-only attribute (1) was added to the derived cluster
         asserts.assert_equal(set(clusters[0xFFFF].attributes.keys()), set(
             [0, 1, 2, 3] + expected_global_attrs), "Unexpected attribute list")
diff --git a/src/python_testing/spec_parsing_support.py b/src/python_testing/spec_parsing_support.py
index 6a9d1f8..ac34463 100644
--- a/src/python_testing/spec_parsing_support.py
+++ b/src/python_testing/spec_parsing_support.py
@@ -487,7 +487,7 @@
         clusters[action_id].accepted_commands[c].conformance = optional()
         remove_problem(CommandPathLocation(endpoint_id=0, cluster_id=action_id, command_id=c))
 
-    combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name)
+    combine_derived_clusters_with_base(clusters, pure_base_clusters, ids_by_name, problems)
 
     for alias_base_name, aliased_clusters in CLUSTER_ALIASES.items():
         for id, (alias_name, pics) in aliased_clusters.items():
@@ -547,10 +547,10 @@
     return clusters, problems
 
 
-def combine_derived_clusters_with_base(xml_clusters: dict[int, XmlCluster], pure_base_clusters: dict[str, XmlCluster], ids_by_name: dict[str, int]) -> None:
+def combine_derived_clusters_with_base(xml_clusters: dict[int, XmlCluster], pure_base_clusters: dict[str, XmlCluster], ids_by_name: dict[str, int], problems: list[ProblemNotice]) -> None:
     ''' Overrides base elements with the derived cluster values for derived clusters. '''
 
-    def combine_attributes(base: dict[uint, XmlAttribute], derived: dict[uint, XmlAttribute], cluster_id: uint) -> dict[uint, XmlAttribute]:
+    def combine_attributes(base: dict[uint, XmlAttribute], derived: dict[uint, XmlAttribute], cluster_id: uint, problems: list[ProblemNotice]) -> dict[uint, XmlAttribute]:
         ret = deepcopy(base)
         extras = {k: v for k, v in derived.items() if k not in base.keys()}
         overrides = {k: v for k, v in derived.items() if k in base.keys()}
@@ -590,7 +590,7 @@
             command_map.update(c.command_map)
             features = deepcopy(base.features)
             features.update(c.features)
-            attributes = combine_attributes(base.attributes, c.attributes, id)
+            attributes = combine_attributes(base.attributes, c.attributes, id, problems)
             accepted_commands = deepcopy(base.accepted_commands)
             accepted_commands.update(c.accepted_commands)
             generated_commands = deepcopy(base.generated_commands)
diff --git a/src/test_driver/linux-cirque/helper/CHIPTestBase.py b/src/test_driver/linux-cirque/helper/CHIPTestBase.py
index 036ba25..7fd0c81 100644
--- a/src/test_driver/linux-cirque/helper/CHIPTestBase.py
+++ b/src/test_driver/linux-cirque/helper/CHIPTestBase.py
@@ -299,13 +299,13 @@
         assert(Not)Equal
         python unittest style functions that raise exceptions when condition not met
         '''
-        if not (exp is True):
+        if exp is not True:
             if note:
                 self.logger.error(note)
             raise AssertionError
 
     def assertFalse(self, exp, note=None):
-        if not (exp is False):
+        if exp is not False:
             if note:
                 self.logger.error(note)
             raise AssertionError