diff --git a/linearmodels/__init__.py b/linearmodels/__init__.py index 3a87a07961..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: @@ -10,12 +12,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 +51,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 +74,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