From 9eba8926ba9a7c4588ec93a3c773a29eacd3219c Mon Sep 17 00:00:00 2001 From: aeshevchenko Date: Tue, 24 Mar 2026 23:55:13 +0300 Subject: [PATCH 1/3] TEST: Add constraint parsing tests for quadratic_form_test - Strip LHS in _parse_single for stable dict keys - Raise ValueError when formula= is used without a Series params - Remove redundant assert in InvalidTestStatistic.__str__ --- linearmodels/shared/hypotheses.py | 10 +- .../shared/test_hypotheses_constraints.py | 95 +++++++++++++++++++ 2 files changed, 101 insertions(+), 4 deletions(-) create mode 100644 linearmodels/tests/shared/test_hypotheses_constraints.py diff --git a/linearmodels/shared/hypotheses.py b/linearmodels/shared/hypotheses.py index 462e3e071c..aa69fcc0ce 100644 --- a/linearmodels/shared/hypotheses.py +++ b/linearmodels/shared/hypotheses.py @@ -135,7 +135,6 @@ def critical_values(self) -> None: def __str__(self) -> str: msg = "Invalid test statistic\n{reason}\n{name}" name = "" if self._name is None else self._name - assert name is not None return msg.format(name=name, reason=self._reason) @@ -196,13 +195,12 @@ def _parse_single(constraint: str) -> tuple[str, float]: except Exception as exc: raise TypeError(_constraint_error.format(cons=constraint)) from exc expr = "=".join(parts[:-1]) - return expr, value + return expr.strip(), value def _reparse_constraint_formula( formula: str | list[str] | dict[str, float], ) -> str | dict[str, float]: - # TODO: Test against variable names constaining , or = if isinstance(formula, Mapping): return dict(formula) if isinstance(formula, str): @@ -227,7 +225,11 @@ def quadratic_form_test( if formula is not None and restriction is not None: raise ValueError("restriction and formula cannot be used simultaneously.") if formula is not None: - assert isinstance(params, Series) + if not isinstance(params, Series): + raise ValueError( + "params must be a pandas Series when using formula= to specify " + "linear restrictions (indexed by parameter names)." + ) param_names = [str(p) for p in params.index] rewritten_constraints = _reparse_constraint_formula(formula) lc = LinearConstraints.from_spec(rewritten_constraints, param_names) diff --git a/linearmodels/tests/shared/test_hypotheses_constraints.py b/linearmodels/tests/shared/test_hypotheses_constraints.py new file mode 100644 index 0000000000..57e1b72555 --- /dev/null +++ b/linearmodels/tests/shared/test_hypotheses_constraints.py @@ -0,0 +1,95 @@ +import numpy as np +import pandas as pd +import pytest + +from linearmodels.shared.hypotheses import ( + _parse_single, + _reparse_constraint_formula, + quadratic_form_test, +) + + +def test_parse_single_simple(): + expr, val = _parse_single("x1 = 1") + assert expr == "x1" + assert val == 1.0 + + +def test_parse_single_expression_with_plus(): + expr, val = _parse_single("x1 + x2 = 2.5") + assert expr == "x1 + x2" + assert val == 2.5 + + +def test_parse_single_multiple_equals(): + expr, val = _parse_single("a = b = 1") + assert expr == "a = b" + assert val == 1.0 + + +def test_parse_single_no_equals_raises(): + with pytest.raises(ValueError, match="required syntax"): + _parse_single("x1") + + +def test_parse_single_non_float_rhs_raises(): + with pytest.raises(TypeError, match="required syntax"): + _parse_single("x1 = not_a_number") + + +def test_reparse_dict_passthrough(): + spec = {"x1": 0.0, "x2": 1.0} + out = _reparse_constraint_formula(spec) + assert out == spec + + +def test_reparse_single_constraint_string_unchanged(): + s = "x1 + x2 = 1" + assert _reparse_constraint_formula(s) is s + + +def test_reparse_multiple_equals_without_comma(): + out = _reparse_constraint_formula("x1 = x2 = 0") + assert out == {"x1": 0.0, "x2": 0.0} + + +def test_reparse_comma_separated(): + out = _reparse_constraint_formula("x1 = 1, x2 = 2") + assert out == {"x1": 1.0, "x2": 2.0} + + +def test_reparse_list_of_strings(): + out = _reparse_constraint_formula(["x1 = 1", "x2 = 2"]) + assert out == {"x1": 1.0, "x2": 2.0} + + +def test_reparse_comma_with_multiple_equals(): + out = _reparse_constraint_formula("a=b=1,c=2") + assert out == {"a=b": 1.0, "c": 2.0} + + +def test_quadratic_form_formula_end_to_end(): + params = pd.Series([0.0, 1.0], index=["x0", "x1"]) + cov = np.eye(2) + res = quadratic_form_test(params, cov, formula="x0=0") + assert res.stat == 0.0 + assert res.df == 1 + + +def test_quadratic_form_formula_and_restriction_exclusive(): + params = pd.Series([0.0, 0.0], index=["a", "b"]) + cov = np.eye(2) + r = np.array([[1.0, 0.0]]) + with pytest.raises(ValueError, match="cannot be used simultaneously"): + quadratic_form_test(params, cov, restriction=r, formula="a=0") + + +def test_quadratic_form_formula_requires_series_params(): + with pytest.raises(ValueError, match="pandas Series"): + quadratic_form_test(np.array([0.0, 1.0]), np.eye(2), formula="x0=0") + + +def test_reparse_name_with_comma_single_equals(): + # One "=" in the string: pass through to formulaic as a single constraint. + out = _reparse_constraint_formula("my,var = 1") + assert out == "my,var = 1" From 1211caaca47e9b03789f578d0a10c92afb50b373 Mon Sep 17 00:00:00 2001 From: aeshevchenko Date: Wed, 25 Mar 2026 16:15:15 +0300 Subject: [PATCH 2/3] FIX: TypeError for formula= params; keep assert for type checker --- linearmodels/shared/hypotheses.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/linearmodels/shared/hypotheses.py b/linearmodels/shared/hypotheses.py index aa69fcc0ce..ddf3dc35cd 100644 --- a/linearmodels/shared/hypotheses.py +++ b/linearmodels/shared/hypotheses.py @@ -135,6 +135,7 @@ def critical_values(self) -> None: def __str__(self) -> str: msg = "Invalid test statistic\n{reason}\n{name}" name = "" if self._name is None else self._name + assert name is not None return msg.format(name=name, reason=self._reason) @@ -226,10 +227,11 @@ def quadratic_form_test( raise ValueError("restriction and formula cannot be used simultaneously.") if formula is not None: if not isinstance(params, Series): - raise ValueError( + raise TypeError( "params must be a pandas Series when using formula= to specify " "linear restrictions (indexed by parameter names)." ) + assert isinstance(params, Series) param_names = [str(p) for p in params.index] rewritten_constraints = _reparse_constraint_formula(formula) lc = LinearConstraints.from_spec(rewritten_constraints, param_names) From cefd96e13d7f32278ec37f73d0147c06212cb422 Mon Sep 17 00:00:00 2001 From: aeshevchenko Date: Wed, 25 Mar 2026 16:44:22 +0300 Subject: [PATCH 3/3] TEST: TypeError for formula= non-Series params --- linearmodels/tests/shared/test_hypotheses_constraints.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/linearmodels/tests/shared/test_hypotheses_constraints.py b/linearmodels/tests/shared/test_hypotheses_constraints.py index 57e1b72555..4f3f3adbf6 100644 --- a/linearmodels/tests/shared/test_hypotheses_constraints.py +++ b/linearmodels/tests/shared/test_hypotheses_constraints.py @@ -85,7 +85,7 @@ def test_quadratic_form_formula_and_restriction_exclusive(): def test_quadratic_form_formula_requires_series_params(): - with pytest.raises(ValueError, match="pandas Series"): + with pytest.raises(TypeError, match="pandas Series"): quadratic_form_test(np.array([0.0, 1.0]), np.eye(2), formula="x0=0")