Python testing: move PICS functions (#33759)

* Python testing: move PICS functions

We're going to need these for some new tooling, so moving them
into their own file to avoid needing to pull in the entirety
of matter_testing_support

Only changes are moves and function renames.

test: TC_pics_checker.py

* Restyled by autopep8

* Restyled by isort

* fix imports

* Hey, I wrote a unit test

Didn't update it though because I forgot it existed. Thanks, CI.

It passes now.

* Restyled by isort

---------

Co-authored-by: Restyled.io <commits@restyled.io>
diff --git a/src/python_testing/TC_pics_checker.py b/src/python_testing/TC_pics_checker.py
index 8710719..4e503ed 100644
--- a/src/python_testing/TC_pics_checker.py
+++ b/src/python_testing/TC_pics_checker.py
@@ -22,25 +22,10 @@
 from matter_testing_support import (AttributePathLocation, ClusterPathLocation, CommandPathLocation, FeaturePathLocation,
                                     MatterBaseTest, TestStep, async_test_body, default_matter_test_main)
 from mobly import asserts
+from pics_support import accepted_cmd_pics_str, attribute_pics_str, feature_pics_str, generated_cmd_pics_str
 from spec_parsing_support import build_xml_clusters
 
 
-def attribute_pics(pics_base: str, id: int) -> str:
-    return f'{pics_base}.S.A{id:04x}'
-
-
-def accepted_cmd_pics(pics_base: str, id: int) -> str:
-    return f'{pics_base}.S.C{id:02x}.Rsp'
-
-
-def generated_cmd_pics(pics_base: str, id: int) -> str:
-    return f'{pics_base}.S.C{id:02x}.Tx'
-
-
-def feature_pics(pics_base: str, bit: int) -> str:
-    return f'{pics_base}.S.F{bit:02x}'
-
-
 class TC_PICS_Checker(MatterBaseTest, BasicCompositionTests):
     @async_test_body
     async def setup_class(self):
@@ -64,14 +49,14 @@
         try:
             if attribute_id_of_element_list == GlobalAttributeIds.ATTRIBUTE_LIST_ID:
                 all_spec_elements_to_check = Clusters.ClusterObjects.ALL_ATTRIBUTES[cluster_id]
-                pics_mapper = attribute_pics
+                pics_mapper = attribute_pics_str
             elif attribute_id_of_element_list == GlobalAttributeIds.ACCEPTED_COMMAND_LIST_ID:
                 all_spec_elements_to_check = Clusters.ClusterObjects.ALL_ACCEPTED_COMMANDS[cluster_id]
-                pics_mapper = accepted_cmd_pics
+                pics_mapper = accepted_cmd_pics_str
 
             elif attribute_id_of_element_list == GlobalAttributeIds.GENERATED_COMMAND_LIST_ID:
                 all_spec_elements_to_check = Clusters.ClusterObjects.ALL_GENERATED_COMMANDS[cluster_id]
-                pics_mapper = generated_cmd_pics
+                pics_mapper = generated_cmd_pics_str
             else:
                 asserts.fail("add_pics_for_list function called for non-list attribute")
         except KeyError:
@@ -177,7 +162,7 @@
                     self.record_warning("PICS check", location=location,
                                         problem=f"Unable to parse feature mask {feature_mask} from cluster {cluster}")
                     continue
-                pics = feature_pics(pics_base, feature_bit)
+                pics = feature_pics_str(pics_base, feature_bit)
                 if feature_mask & feature_map:
                     required = True
                 else:
diff --git a/src/python_testing/TestMatterTestingSupport.py b/src/python_testing/TestMatterTestingSupport.py
index 56f105b..eba9dc4 100644
--- a/src/python_testing/TestMatterTestingSupport.py
+++ b/src/python_testing/TestMatterTestingSupport.py
@@ -24,9 +24,9 @@
 from chip.clusters.Types import Nullable, NullValue
 from chip.tlv import uint
 from matter_testing_support import (MatterBaseTest, async_test_body, compare_time, default_matter_test_main,
-                                    get_wait_seconds_from_set_time, parse_pics, parse_pics_xml, type_matches,
-                                    utc_time_in_matter_epoch)
+                                    get_wait_seconds_from_set_time, type_matches, utc_time_in_matter_epoch)
 from mobly import asserts, signals
+from pics_support import parse_pics, parse_pics_xml
 from taglist_and_topology_test_support import (TagProblem, create_device_type_list_for_root, create_device_type_lists,
                                                find_tag_list_problems, find_tree_roots, flat_list_ok, get_all_children,
                                                get_direct_children_of_root, parts_list_cycles, separate_endpoint_types)
diff --git a/src/python_testing/matter_testing_support.py b/src/python_testing/matter_testing_support.py
index 4861cae..c342d87 100644
--- a/src/python_testing/matter_testing_support.py
+++ b/src/python_testing/matter_testing_support.py
@@ -18,7 +18,6 @@
 import argparse
 import asyncio
 import builtins
-import glob
 import inspect
 import json
 import logging
@@ -30,7 +29,6 @@
 import sys
 import typing
 import uuid
-import xml.etree.ElementTree as ET
 from binascii import hexlify, unhexlify
 from dataclasses import asdict as dataclass_asdict
 from dataclasses import dataclass, field
@@ -64,6 +62,7 @@
 from mobly import asserts, base_test, signals, utils
 from mobly.config_parser import ENV_MOBLY_LOGPATH, TestRunConfig
 from mobly.test_runner import TestRunner
+from pics_support import read_pics_from_file
 
 try:
     from matter_yamltests.hooks import TestRunnerHooks
@@ -142,50 +141,6 @@
         return pathlib.Path.cwd()
 
 
-def parse_pics(lines: typing.List[str]) -> dict[str, bool]:
-    pics = {}
-    for raw in lines:
-        line, _, _ = raw.partition("#")
-        line = line.strip()
-
-        if not line:
-            continue
-
-        key, _, val = line.partition("=")
-        val = val.strip()
-        if val not in ["1", "0"]:
-            raise ValueError('PICS {} must have a value of 0 or 1'.format(key))
-
-        pics[key.strip()] = (val == "1")
-    return pics
-
-
-def parse_pics_xml(contents: str) -> dict[str, bool]:
-    pics = {}
-    mytree = ET.fromstring(contents)
-    for pi in mytree.iter('picsItem'):
-        name = pi.find('itemNumber').text
-        support = pi.find('support').text
-        pics[name] = int(json.loads(support.lower())) == 1
-    return pics
-
-
-def read_pics_from_file(path: str) -> dict[str, bool]:
-    """ Reads a dictionary of PICS from a file (ci format) or directory (xml format). """
-    if os.path.isdir(os.path.abspath(path)):
-        pics_dict = {}
-        for filename in glob.glob(f'{path}/*.xml'):
-            with open(filename, 'r') as f:
-                contents = f.read()
-                pics_dict.update(parse_pics_xml(contents))
-        return pics_dict
-
-    else:
-        with open(path, 'r') as f:
-            lines = f.readlines()
-            return parse_pics(lines)
-
-
 def type_matches(received_value, desired_type):
     """ Checks if the value received matches the expected type.
 
diff --git a/src/python_testing/pics_support.py b/src/python_testing/pics_support.py
new file mode 100644
index 0000000..62e04f7
--- /dev/null
+++ b/src/python_testing/pics_support.py
@@ -0,0 +1,89 @@
+#
+#    Copyright (c) 2024 Project CHIP Authors
+#    All rights reserved.
+#
+#    Licensed under the Apache License, Version 2.0 (the "License");
+#    you may not use this file except in compliance with the License.
+#    You may obtain a copy of the License at
+#
+#        http://www.apache.org/licenses/LICENSE-2.0
+#
+#    Unless required by applicable law or agreed to in writing, software
+#    distributed under the License is distributed on an "AS IS" BASIS,
+#    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#    See the License for the specific language governing permissions and
+#    limitations under the License.
+#
+import glob
+import json
+import os
+import typing
+import xml.etree.ElementTree as ET
+
+
+def attribute_pics_str(pics_base: str, id: int) -> str:
+    return f'{pics_base}.S.A{id:04x}'
+
+
+def accepted_cmd_pics_str(pics_base: str, id: int) -> str:
+    return f'{pics_base}.S.C{id:02x}.Rsp'
+
+
+def generated_cmd_pics_str(pics_base: str, id: int) -> str:
+    return f'{pics_base}.S.C{id:02x}.Tx'
+
+
+def feature_pics_str(pics_base: str, bit: int) -> str:
+    return f'{pics_base}.S.F{bit:02x}'
+
+
+def server_pics_str(pics_base: str) -> str:
+    return f'{pics_base}.S'
+
+
+def client_pics_str(pics_base: str) -> str:
+    return f'{pics_base}.C'
+
+
+def parse_pics(lines: typing.List[str]) -> dict[str, bool]:
+    pics = {}
+    for raw in lines:
+        line, _, _ = raw.partition("#")
+        line = line.strip()
+
+        if not line:
+            continue
+
+        key, _, val = line.partition("=")
+        val = val.strip()
+        if val not in ["1", "0"]:
+            raise ValueError('PICS {} must have a value of 0 or 1'.format(key))
+
+        pics[key.strip()] = (val == "1")
+    return pics
+
+
+def parse_pics_xml(contents: str) -> dict[str, bool]:
+    pics = {}
+    mytree = ET.fromstring(contents)
+    for pi in mytree.iter('picsItem'):
+        name = pi.find('itemNumber').text
+        support = pi.find('support').text
+        pics[name] = int(json.loads(support.lower())) == 1
+    return pics
+
+
+def read_pics_from_file(path: str) -> dict[str, bool]:
+    """ Reads a dictionary of PICS from a file (ci format) or directory (xml format). """
+    if os.path.isdir(os.path.abspath(path)):
+        pics_dict = {}
+        for filename in glob.glob(f'{path}/*.xml'):
+            with open(filename, 'r') as f:
+                contents = f.read()
+                pics_dict.update(parse_pics_xml(contents))
+        return pics_dict
+
+    else:
+        with open(path, 'r') as f:
+            lines = f.readlines()
+            return parse_pics(lines)