Repository: cloudtools/troposphere

Issue:
Incorrect ValueError: AllowedPattern can only be used with parameters of the String type
Example:

```python
    cft.add_parameter(
        Parameter(
            "Foo",
            Type="CommaDelimitedList",
            AllowedPattern="^[A-Z]{2}$",
            Default="",
        )
    )
```

According to the [AWS docs](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/parameters-section-structure.html) this should be allowed:
> AllowedPattern
> A regular expression that represents the patterns to allow for String or CommaDelimitedList types. When applied on a parameter of type String, the pattern must match the entire parameter value provided. When applied to a parameter of type CommaDelimitedList, the pattern must match each value in the list.



Discussion:

Resolving patch:
diff --git a/troposphere/__init__.py b/troposphere/__init__.py
--- a/troposphere/__init__.py
+++ b/troposphere/__init__.py
@@ -1011,6 +1011,7 @@ def add_to_template(self) -> None:
 class Parameter(AWSDeclaration):
     STRING_PROPERTIES = ["AllowedPattern", "MaxLength", "MinLength"]
     NUMBER_PROPERTIES = ["MaxValue", "MinValue"]
+    COMMA_DELIMITED_LIST = ["AllowedPattern"]
     props = {
         "Type": (str, True),
         "Default": ((str, int, float), False),
@@ -1076,15 +1077,28 @@ def check_type(t: type, v: Any) -> bool:
                     if not any(check_type(x, d) for x in allowed):
                         raise ValueError(error_str % (param_type, type(d), dlist))
 
-        if self.properties["Type"] != "String":
-            for p in self.STRING_PROPERTIES:
+        if self.properties["Type"] == "String":
+            not_allowed = [
+                p for p in self.COMMA_DELIMITED_LIST if p not in self.STRING_PROPERTIES
+            ] + self.NUMBER_PROPERTIES
+            for p in not_allowed:
                 if p in self.properties:
                     raise ValueError(
                         "%s can only be used with parameters of " "the String type." % p
                     )
-        if self.properties["Type"] != "Number":
-            for p in self.NUMBER_PROPERTIES:
+        if self.properties["Type"] == "Number":
+            for p in list(set(self.STRING_PROPERTIES + self.COMMA_DELIMITED_LIST)):
                 if p in self.properties:
                     raise ValueError(
                         "%s can only be used with parameters of " "the Number type." % p
                     )
+        if self.properties["Type"] == "CommaDelimitedList":
+            not_allowed = [
+                p for p in self.STRING_PROPERTIES if p not in self.COMMA_DELIMITED_LIST
+            ] + self.NUMBER_PROPERTIES
+            for p in not_allowed:
+                if p in self.properties:
+                    raise ValueError(
+                        "%s can only be used with parameters of "
+                        "the CommaDelimitedList type." % p
+                    )


Test patch:
diff --git a/tests/test_parameters.py b/tests/test_parameters.py
--- a/tests/test_parameters.py
+++ b/tests/test_parameters.py
@@ -17,5 +17,33 @@ def test_ref_can_be_requested(self):
         self.assertDictEqual(reference.data, {"Ref": "title"})
 
 
+class TestParameterValidator(unittest.TestCase):
+    def test_allowed_pattern_for_number(self):
+        with self.assertRaises(ValueError):
+            Parameter("Foo", Type="Number", AllowedPattern="^[a-zA-Z0-9]*$").validate()
+
+    def test_allowed_pattern_for_comma_delimited_list_and_string(self):
+        Parameter(
+            "Foo",
+            Type="CommaDelimitedList",
+            AllowedPattern="^[A-Z]{2}$",
+            Default="",
+        ).validate()
+
+        Parameter(
+            "Foo",
+            Type="String",
+            AllowedPattern="^[A-Z]{2}$",
+            Default="",
+        ).validate()
+
+    def test_aws_specific_type(self):
+        Parameter(
+            "Foo",
+            Type="AWS::EC2::KeyPair::KeyName",
+            Default="",
+        ).validate()
+
+
 if __name__ == "__main__":
     unittest.main()