Repository: matthewwardrop/formulaic

Issue:
Formulaic does not support lists of constraint strings in `LinearConstraints.from_spec`.
For parity with `patsy`, and also just because it is a sensible thing to do, this should be added.

This came up in: https://github.com/statsmodels/statsmodels/pull/9423

Discussion:

Solution patch:
diff --git a/formulaic/utils/constraints.py b/formulaic/utils/constraints.py
index f4d5fe0b..cf95bc9f 100644
--- a/formulaic/utils/constraints.py
+++ b/formulaic/utils/constraints.py
@@ -33,6 +33,7 @@
 
 LinearConstraintSpec = Union[
     str,
+    List[str],
     Dict[str, Number],
     Tuple["numpy.typing.ArrayLike", "numpy.typing.ArrayLike"],
     "numpy.typing.ArrayLike",
@@ -70,23 +71,31 @@ def from_spec(
                     * str: In which case it is interpreted as a constraints
                         formula (e.g. "x + 2 * y = 3, z + y - x / 10"). All
                         variables used must be present in `variable_names`.
+                    * List[str]: In which case the strings are joined with
+                        commas and expected to look like `str` above.
                     * Dict[str, Number]: In which case each key is treated as
                         formula, and each value as the constraint (e.g. {"x":19}
                         , {"a + b": 0}).
                     * Tuple: a two-tuple describing the constraint matrix and
                         values respectively.
-                    * numpy.ndarray: a constraint matrix (with all values
-                        assumed to be zero).
+                    * numpy.ndarray/numerical sequence: a constraint matrix
+                        (with all values assumed to be zero).
             variable_names: The ordered names of the variables represented by
                 $x$; typically the column names of a `ModelMatrix` instance.
         """
         if isinstance(spec, LinearConstraints):
             return spec
-        if isinstance(spec, (str, dict)):
+        if (
+            isinstance(spec, (str, dict))
+            or isinstance(spec, list)
+            and all(isinstance(s, str) for s in spec)
+        ):
             if variable_names is None:
                 raise ValueError(
                     "`variable_names` must be provided when parsing constraints from a formula."
                 )
+            if isinstance(spec, list):
+                spec = ",".join(spec)
             if isinstance(spec, str):
                 matrix, values = LinearConstraintParser(
                     variable_names=variable_names


Test patch:
diff --git a/tests/utils/test_constraints.py b/tests/utils/test_constraints.py
index a043cd51..23eff7db 100644
--- a/tests/utils/test_constraints.py
+++ b/tests/utils/test_constraints.py
@@ -70,6 +70,12 @@ class TestLinearConstraints:
                     "a + b + c - 10, a - c = 10", variable_names=["a", "b", "c"]
                 ),
             ),
+            (
+                2,
+                LinearConstraints.from_spec(
+                    ["a + b + c - 10", "a - c = 10"], variable_names=["a", "b", "c"]
+                ),
+            ),
             (
                 2,
                 LinearConstraints.from_spec(