From 9a72faef79a612cb6f9428eba92b0823dffefd60 Mon Sep 17 00:00:00 2001 From: Kevin Sheppard Date: Sun, 11 Oct 2026 19:48:22 +0100 Subject: [PATCH 1/2] ENH: Export IVJIVE and the first-stage rank tests from the public API IVJIVE could only be imported from linearmodels.iv, while every other estimator can be imported from linearmodels. Import it in linearmodels/__init__.py and add it to __all__. The list of estimators in the docstring of the package omitted IVJIVE and AbsorbingLS, so add both. The first-stage rank tests cragg_donald, cragg_donald_f, kleibergen_paap and kleibergen_paap_f could only be imported from linearmodels.iv.common. Import them in linearmodels/iv/__init__.py and add them to its __all__, so that `from linearmodels.iv import cragg_donald` works. iv/common.py and iv/results.py had no __all__, so everything they import counted as public: `from linearmodels.iv.common import *` returned 35 names, including the NumPy functions, the GMM weight matrix classes and annihilate, and the same import from iv.results returned 49, including cragg_donald and cragg_donald_f. Add __all__ to list only the public names: - iv.common: the four rank tests. find_constant and f_statistic are used by the models and stay importable, but are not listed. - iv.results: AbsorbingLSResults, FirstStageResults, IVGMMResults, IVModelComparison, IVResults, OLSResults and compare. stub_concat and table_concat are used by panel.results and stay importable, but are not listed. Nothing that could be imported before has been removed, so only the star import and tools that read __all__ see a difference. The reference pages in doc/source/iv/reference.rst document the same objects as before. A Sphinx build of them and of the IV introduction gives the same warnings and the same pages as main, apart from the source listings of the two modules. linearmodels/tests/test_exports.py checks that: - every estimator in the __all__ of linearmodels.iv, panel, system and asset_pricing (the classes that have a fit method) is exported by linearmodels and is the same object. The 18 estimators are also listed in the test, so that none can be dropped from the public API unnoticed; - IVJIVE is exported from linearmodels; - the four rank tests in linearmodels.iv are the objects in linearmodels.iv.common; - the __all__ of iv.common and iv.results contain exactly the names above, and every __all__ of the modules above has no duplicates or missing names. 10 of the 33 tests fail on main. --- linearmodels/__init__.py | 8 +- linearmodels/iv/__init__.py | 5 + linearmodels/iv/common.py | 2 + linearmodels/iv/results.py | 10 ++ linearmodels/tests/test_exports.py | 145 +++++++++++++++++++++++++++++ 5 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 linearmodels/tests/test_exports.py diff --git a/linearmodels/__init__.py b/linearmodels/__init__.py index 3a87a07961..6237f1d4fd 100644 --- a/linearmodels/__init__.py +++ b/linearmodels/__init__.py @@ -10,12 +10,17 @@ - Pooled regression for panel data - Fama-MacBeth estimation of panel models +- High-dimensional Regression: + + - Absorbing Least Squares + - Instrumental Variable estimators - Two-stage Least Squares - Limited Information Maximum Likelihood - k-class Estimators - Generalized Method of Moments, also with continuously updating + - Jackknife IV (JIVE) - Factor Asset Pricing Models: @@ -44,7 +49,7 @@ TradedFactorModel, ) from .iv.absorbing import AbsorbingLS -from .iv.model import _OLS, IV2SLS, IVGMM, IVGMMCUE, IVLIML +from .iv.model import _OLS, IV2SLS, IVGMM, IVGMMCUE, IVJIVE, IVLIML from .panel.model import ( BetweenOLS, FamaMacBeth, @@ -67,6 +72,7 @@ "IV3SLS", "IVGMM", "IVGMMCUE", + "IVJIVE", "IVLIML", "OLS", "SUR", diff --git a/linearmodels/iv/__init__.py b/linearmodels/iv/__init__.py index d7e9270f14..22466ac65a 100644 --- a/linearmodels/iv/__init__.py +++ b/linearmodels/iv/__init__.py @@ -1,4 +1,5 @@ from .absorbing import AbsorbingLS, Interaction +from .common import cragg_donald, cragg_donald_f, kleibergen_paap, kleibergen_paap_f from .model import IV2SLS, IVGMM, IVGMMCUE, IVJIVE, IVLIML from .results import compare @@ -11,4 +12,8 @@ "AbsorbingLS", "Interaction", "compare", + "cragg_donald", + "cragg_donald_f", + "kleibergen_paap", + "kleibergen_paap_f", ] diff --git a/linearmodels/iv/common.py b/linearmodels/iv/common.py index 6317fae137..da90e5e609 100644 --- a/linearmodels/iv/common.py +++ b/linearmodels/iv/common.py @@ -29,6 +29,8 @@ from linearmodels.shared.hypotheses import InvalidTestStatistic, WaldTestStatistic import linearmodels.typing.data +__all__ = ["cragg_donald", "cragg_donald_f", "kleibergen_paap", "kleibergen_paap_f"] + # The null of the rank tests: the first-stage coefficient matrix is rank # deficient, which means that the model is not identified. _RANK_NULL = ( diff --git a/linearmodels/iv/results.py b/linearmodels/iv/results.py index c02e1cbcb8..ed75fe9ef8 100644 --- a/linearmodels/iv/results.py +++ b/linearmodels/iv/results.py @@ -43,6 +43,16 @@ from linearmodels.shared.io import _str, add_star, pval_format import linearmodels.typing.data +__all__ = [ + "AbsorbingLSResults", + "FirstStageResults", + "IVGMMResults", + "IVModelComparison", + "IVResults", + "OLSResults", + "compare", +] + def stub_concat( lists: Sequence[list[str] | tuple[str, ...]], sep: str = "=" diff --git a/linearmodels/tests/test_exports.py b/linearmodels/tests/test_exports.py new file mode 100644 index 0000000000..a873367c40 --- /dev/null +++ b/linearmodels/tests/test_exports.py @@ -0,0 +1,145 @@ +""" +Tests of the names that linearmodels exports + +Every estimator that a sub-package exports is also exported from the top-level +package, the first-stage rank tests are exported from ``linearmodels.iv``, and +``linearmodels.iv.common`` and ``linearmodels.iv.results`` use ``__all__`` to +list only the names that are part of their public API. +""" + +import importlib +import inspect + +import pytest + +import linearmodels +import linearmodels.iv +import linearmodels.iv.common +import linearmodels.iv.model +import linearmodels.iv.results + +SUBPACKAGES = ["iv", "panel", "system", "asset_pricing"] + +# Every estimator in the public API. An estimator is a class that a sub-package +# lists in its __all__ and that has a fit method. The tests find the estimators +# in this way, so that a new estimator is checked without being added here. +ESTIMATORS = { + "AbsorbingLS", + "BetweenOLS", + "FamaMacBeth", + "FirstDifferenceOLS", + "IV2SLS", + "IV3SLS", + "IVGMM", + "IVGMMCUE", + "IVJIVE", + "IVLIML", + "IVSystemGMM", + "LinearFactorModel", + "LinearFactorModelGMM", + "PanelOLS", + "PooledOLS", + "RandomEffects", + "SUR", + "TradedFactorModel", +} + +RANK_TESTS = ["cragg_donald", "cragg_donald_f", "kleibergen_paap", "kleibergen_paap_f"] + +RESULTS = [ + "AbsorbingLSResults", + "FirstStageResults", + "IVGMMResults", + "IVModelComparison", + "IVResults", + "OLSResults", + "compare", +] + +MODULES_WITH_ALL = [ + "linearmodels", + "linearmodels.iv", + "linearmodels.iv.common", + "linearmodels.iv.results", + "linearmodels.panel", + "linearmodels.system", + "linearmodels.asset_pricing", +] + + +def _exported_estimators() -> list[tuple[str, str]]: + """The (sub-package, name) of every estimator listed in a sub-package's __all__""" + found = [] + for subpackage in SUBPACKAGES: + module = importlib.import_module(f"linearmodels.{subpackage}") + for name in module.__all__: + obj = getattr(module, name, None) + if inspect.isclass(obj) and hasattr(obj, "fit"): + found.append((subpackage, name)) + return found + + +EXPORTED_ESTIMATORS = _exported_estimators() + + +def test_estimators_are_found(): + # With nothing to test, the parametrized test below is reported as xfail + # (empty_parameter_set_mark) rather than failing, so check that the estimators + # are found. This also fails if an estimator is dropped from the public API. + found = {name for _, name in EXPORTED_ESTIMATORS} + assert found >= ESTIMATORS + + +@pytest.mark.parametrize( + ("subpackage", "name"), + EXPORTED_ESTIMATORS, + ids=[f"{subpackage}.{name}" for subpackage, name in EXPORTED_ESTIMATORS], +) +def test_estimator_exported_from_top_level(subpackage, name): + module = importlib.import_module(f"linearmodels.{subpackage}") + assert name in linearmodels.__all__ + assert getattr(linearmodels, name) is getattr(module, name) + + +def test_ivjive_exported_from_top_level(): + assert "IVJIVE" in linearmodels.__all__ + assert linearmodels.IVJIVE is linearmodels.iv.model.IVJIVE + assert linearmodels.IVJIVE is linearmodels.iv.IVJIVE + + +@pytest.mark.parametrize("name", RANK_TESTS) +def test_rank_tests_exported_from_iv(name): + assert name in linearmodels.iv.__all__ + assert getattr(linearmodels.iv, name) is getattr(linearmodels.iv.common, name) + + +def test_common_all(): + common = linearmodels.iv.common + assert sorted(common.__all__) == sorted(RANK_TESTS) + # Importable, since the models use them, but not part of the public API + for helper in ("find_constant", "f_statistic"): + assert callable(getattr(common, helper)) + assert helper not in common.__all__ + + +def test_results_all(): + results = linearmodels.iv.results + assert sorted(results.__all__) == sorted(RESULTS) + # Defined in results, not imported into it + for name in RESULTS: + assert getattr(results, name).__module__ == "linearmodels.iv.results" + # The rank tests are imported by results but are part of iv.common + for name in RANK_TESTS: + assert name not in results.__all__ + # Importable, since other modules use them, but not part of the public API + for helper in ("stub_concat", "table_concat"): + assert callable(getattr(results, helper)) + assert helper not in results.__all__ + + +@pytest.mark.parametrize("module_name", MODULES_WITH_ALL) +def test_all_is_valid(module_name): + module = importlib.import_module(module_name) + assert len(module.__all__) == len(set(module.__all__)) + missing = [name for name in module.__all__ if not hasattr(module, name)] + assert not missing From f64d558a094e52d5910639edfb0908ca44e67f29 Mon Sep 17 00:00:00 2001 From: Kevin Sheppard Date: Sun, 11 Oct 2026 19:48:31 +0100 Subject: [PATCH 2/2] DOC: Give the docstring of the package a one-line summary The summary in the docstring of linearmodels/__init__.py ran over two lines and ended with a colon, which numpydoc validation reports as SS06 (the summary should fit in a single line) and SS03 (the summary does not end with a period). Make the first sentence the summary and move the rest into the extended summary. The words are unchanged. --- linearmodels/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/linearmodels/__init__.py b/linearmodels/__init__.py index 6237f1d4fd..32d53c8791 100644 --- a/linearmodels/__init__.py +++ b/linearmodels/__init__.py @@ -1,6 +1,8 @@ """ -Linear (regression) models for Python. Extends statsmodels with Panel regression, -instrumental variable estimators and system estimators: +Linear (regression) models for Python. + +Extends statsmodels with Panel regression, instrumental variable estimators and +system estimators: - Panel models: