diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000..9b1d83c841 --- /dev/null +++ b/.flake8 @@ -0,0 +1,8 @@ +[flake8] +# Leave at 99 for now +max-line-length = 99 +ignore = E203,W503,BLK100 +per-file-ignores = + linearmodels/panel/data.py: E704 + linearmodels/shared/utility.py: F811, E704 + linearmodels/panel/utility.py: F811 diff --git a/ci/azure_template_posix.yml b/ci/azure_template_posix.yml index a85d4b0f06..55ee539a23 100644 --- a/ci/azure_template_posix.yml +++ b/ci/azure_template_posix.yml @@ -11,12 +11,12 @@ parameters: jobs: -- job: ${{ parameters.name }}Test +- job: ${{ parameters.name }}_Test pool: vmImage: ${{ parameters.vmImage }} strategy: matrix: - python310_minimums: + python310_minimums_wheel: python.version: '3.10' NUMPY: 1.22.3 SCIPY: 1.8.0 @@ -24,8 +24,8 @@ jobs: STATSMODELS: 0.13.1 XARRAY: 0.21.0 FORMULAIC: 1.0.2 - test.install: true - python310_mid: + test.wheel: true + python310_mid_sdist: python.version: '3.10' NUMPY: 1.23.0 SCIPY: 1.9.0 @@ -34,8 +34,8 @@ jobs: XARRAY: 2022.6.0 XXHASH: true FORMULAIC: 1.0.2 - test.install: true - python310_recent: + test.sdist: true + python310_recent_wheel: python.version: '3.10' NUMPY: 1.24.0 SCIPY: 1.12.0 @@ -43,7 +43,7 @@ jobs: STATSMODELS: 0.14.0 XARRAY: 2023.4.0 FORMULAIC: 1.1.0 - test.install: true + test.wheel: true python310_latest: python.version: '3.10' FORMULAIC: 1.2.0 @@ -51,7 +51,7 @@ jobs: PYARROW: true python310_no_cython: python.version: '3.10' - LM_NO_BINARY: 1 + BUILD_FLAGS: '-Csetup-args=-Dno-binary=true' python311_latest: python.version: '3.11' XXHASH: true @@ -64,6 +64,7 @@ jobs: python.version: '3.13' XXHASH: true PYARROW: true + BUILD_FLAGS: '-Csetup-args=-Dcython-coverage=true' python313_copy_on_write: python.version: '3.12' XXHASH: true @@ -81,7 +82,7 @@ jobs: displayName: 'Use Python $(python.version)' - script: | - python -m pip install --upgrade pip setuptools>=61 wheel + python -m pip install --upgrade pip wheel build python -m pip install -r requirements.txt python -m pip install -r requirements-test.txt python -m pip install -r requirements-dev.txt @@ -102,18 +103,26 @@ jobs: displayName: 'Check style and formatting' - script: | - echo "Installing to site packages" - python -m pip wheel . --wheel-dir ./dist/ --no-build-isolation + echo "Installing to site packages (sdist)" + python -m build --sdist . -v + SDIST=$(ls -t ./dist/linearmodels-*.tar.gz | head -1) + pip install ${SDIST} -v + displayName: 'Install linearmodels (site-packages)' + condition: eq(variables['test.sdist'], 'true') + + - script: | + echo "Installing to site packages (wheel)" + python -m pip wheel . --wheel-dir ./dist/ WHL=$(ls -t ./dist/linearmodels-*.whl | head -1) - pip install ${WHL} + pip install ${WHL} -v displayName: 'Install linearmodels (site-packages)' - condition: eq(variables['test.install'], 'true') + condition: eq(variables['test.wheel'], 'true') - script: | - echo python -m pip install -e . -v --no-build-isolation - python -m pip install -e . -v --no-build-isolation + echo python -m pip install --no-build-isolation -vv -e . ${FLAGS} + python -m pip install --no-build-isolation -vv -e . ${FLAGS} displayName: 'Install linearmodels (editable)' - condition: ne(variables['test.install'], 'true') + condition: and(ne(variables['test.wheel'], 'true'), ne(variables['test.sdist'], 'true')) - script: | echo "Testing site packages" @@ -122,17 +131,17 @@ jobs: python -c "import linearmodels; linearmodels.test(['-n', 'auto', '--junitxml=../junit/test-results.xml'])" popd displayName: 'Run tests (site-packages)' - condition: and(eq(variables['test.install'], 'true'), ne(variables['pip.pre'], 'true')) + condition: or(eq(variables['test.wheel'], 'true'), eq(variables['test.sdist'], 'true')) - script: | echo "Testing editable install" if [[ ${COVERAGE} == "true" ]]; then - export COVERAGE_OPTS="--cov-config .coveragerc --cov=linearmodels --cov-report xml:coverage.xml --cov-report term" + export COVERAGE_OPTS="--cov=linearmodels --cov-report xml:coverage.xml --cov-report term" fi echo pytest -m "${PYTEST_PATTERN}" --junitxml=junit/test-results.xml -n auto --durations=25 ${COVERAGE_OPTS} linearmodels/tests pytest -m "${PYTEST_PATTERN}" --junitxml=junit/test-results.xml -n auto --durations=25 ${COVERAGE_OPTS} linearmodels/tests displayName: 'Run tests (editable)' - condition: and(ne(variables['test.install'], 'true'), ne(variables['pip.pre'], 'true')) + condition: and(and(ne(variables['test.wheel'], 'true'), ne(variables['test.sdist'], 'true')), ne(variables['pip.pre'], 'true')) - script: | echo "Testing pip-pre" diff --git a/examples/system_examples.ipynb b/examples/system_examples.ipynb index 5748e85863..03bafd369b 100644 --- a/examples/system_examples.ipynb +++ b/examples/system_examples.ipynb @@ -184,7 +184,7 @@ "\n", "cov = res.sigma\n", "std = np.sqrt(np.diag(res.sigma)[:, None])\n", - "regions = [k for k in mod_data.keys()]\n", + "regions = list(mod_data.keys())\n", "corr = pd.DataFrame(cov / (std @ std.T), columns=regions, index=regions)\n", "\n", "sns.heatmap(corr, vmax=0.8, square=True)\n", @@ -257,9 +257,7 @@ "outputs": [], "source": [ "# TODO: Implement method to compare across equations\n", - "params = []\n", - "for label in res.equation_labels:\n", - " params.append(res.equations[label].params)\n", + "params = [res.equations[label].params for label in res.equation_labels]\n", "params = pd.concat(params, axis=1)\n", "params.columns = res.equation_labels\n", "params.T.style.format(\"{:0.3f}\")" @@ -732,7 +730,6 @@ "outputs": [], "source": [ "import statsmodels.api as sm\n", - "\n", "from linearmodels.datasets import french\n", "\n", "data = french.load()\n", @@ -779,7 +776,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.9" + "version": "3.13.7" }, "pycharm": { "stem_cell": { diff --git a/examples/system_three-stage-ls.ipynb b/examples/system_three-stage-ls.ipynb index 0b9da5f41b..2b30ae18e4 100644 --- a/examples/system_three-stage-ls.ipynb +++ b/examples/system_three-stage-ls.ipynb @@ -135,7 +135,7 @@ "metadata": {}, "outputs": [], "source": [ - "equations = dict(hours=hours, lwage=lwage)\n", + "equations = {\"hours\": hours, \"lwage\": lwage}\n", "system_2sls = IV3SLS.from_formula(equations, data)\n", "system_2sls_res = system_2sls.fit(method=\"ols\", cov_type=\"unadjusted\")\n", "print(system_2sls_res)" @@ -154,7 +154,6 @@ "metadata": {}, "outputs": [], "source": [ - "equations = dict(hours=hours, lwage=lwage)\n", "system_3sls = IV3SLS.from_formula(equations, data)\n", "system_3sls_res = system_3sls.fit(method=\"gls\", cov_type=\"unadjusted\")\n", "print(system_3sls_res)" @@ -196,7 +195,7 @@ " \"instruments\": data[[\"age\", \"kidslt6\", \"nwifeinc\"]],\n", "}\n", "\n", - "equations = dict(hours=hours, lwage=lwage)\n", + "equations = {\"hours\": hours, \"lwage\": lwage}\n", "system_3sls = IV3SLS(equations)\n", "system_3sls_res = system_3sls.fit(cov_type=\"unadjusted\")\n", "print(system_3sls_res)" @@ -223,10 +222,10 @@ "metadata": {}, "outputs": [], "source": [ - "equations = dict(\n", - " hours=\"hours ~ educ + age + kidslt6 + nwifeinc + [lwage ~ exper + expersq]\",\n", - " lwage=\"lwage ~ educ + exper + expersq + [hours ~ age + kidslt6 + nwifeinc]\",\n", - ")\n", + "equations = {\n", + " \"hours\": \"hours ~ educ + age + kidslt6 + nwifeinc + [lwage ~ exper + expersq]\",\n", + " \"lwage\": \"lwage ~ educ + exper + expersq + [hours ~ age + kidslt6 + nwifeinc]\",\n", + "}\n", "system_gmm = IVSystemGMM.from_formula(equations, data, weight_type=\"unadjusted\")\n", "system_gmm_res = system_gmm.fit(cov_type=\"unadjusted\")\n", "print(system_gmm_res)" @@ -295,7 +294,7 @@ "in_sample = df.iloc[:-10000]\n", "oos = df.iloc[-10000:]\n", "mod = IV3SLS.from_formula(\n", - " dict(y1=\"y1 ~ x1 + [y2 ~ x2]\", y2=\"y2 ~ x2 + [y1 ~ x1]\"), data=df\n", + " {\"y1\": \"y1 ~ x1 + [y2 ~ x2]\", \"y2\": \"y2 ~ x2 + [y1 ~ x1]\"}, data=df\n", ")\n", "res = mod.fit()\n", "print(res)" @@ -353,7 +352,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.12.9" + "version": "3.13.7" }, "pycharm": { "stem_cell": { diff --git a/linearmodels/_build/__init__.py b/linearmodels/_build/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/linearmodels/_build/git_version.py b/linearmodels/_build/git_version.py new file mode 100644 index 0000000000..8f3fba9c3d --- /dev/null +++ b/linearmodels/_build/git_version.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import os +import setuptools_scm +from packaging.version import Version +from pathlib import Path + +ROOT = Path(__file__).parent.parent.parent.absolute() + + +def get_version() -> tuple[str, tuple[int | str, ...]]: + _version = setuptools_scm.get_version(root=ROOT) + parsed_version = Version(_version) + version_fields: tuple[int | str, ...] = parsed_version.release + if parsed_version.epoch: + version_fields = (f"{parsed_version.epoch}!", *version_fields) + if parsed_version.pre is not None: + version_fields += (f"{parsed_version.pre[0]}{parsed_version.pre[1]}",) + + if parsed_version.post is not None: + version_fields += (f"post{parsed_version.post}",) + + if parsed_version.dev is not None: + version_fields += (f"dev{parsed_version.dev}",) + + if parsed_version.local is not None: + version_fields += (parsed_version.local,) + + return _version, version_fields + + +def write_version_file( + filename: str, version: str, version_fields: tuple[int | str, ...] +) -> None: + template = f"""# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = ["__version__", "__version_tuple__", "version", "version_tuple"] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] +else: + VERSION_TUPLE = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE + +__version__ = version = '{version}' +__version_tuple__ = version_tuple = {version_fields} + """ + + with open(filename, "w") as f: + f.write(template) + + +if __name__ == "__main__": + import argparse + + parser = argparse.ArgumentParser() + parser.add_argument("--write", help="Save version to this file") + parser.add_argument( + "--meson-dist", + help="Output path is relative to MESON_DIST_ROOT", + action="store_true", + ) + args = parser.parse_args() + + version, version_tuple = get_version() + + if args.write: + outfile = args.write + if args.meson_dist: + outfile = os.path.join(os.environ.get("MESON_DIST_ROOT", ""), outfile) + + # Print human readable output path + relpath = os.path.relpath(outfile) + if relpath.startswith("."): + relpath = outfile + write_version_file(relpath, version, version_tuple) + else: + print(version) diff --git a/linearmodels/meson.build b/linearmodels/meson.build new file mode 100644 index 0000000000..cdbdc40576 --- /dev/null +++ b/linearmodels/meson.build @@ -0,0 +1,100 @@ +incdir_numpy = run_command( + py, + [ + '-c', + ''' +import os +cwd = os.getcwd() + +# Protect from import errors due to module names +os.chdir(os.path.join('..', 'examples')) +import numpy as np +os.chdir(cwd) + +try: + # Check if include directory is inside the dir + # e.g. a venv created inside the dir + # If so, convert it to a relative path + incdir = os.path.relpath(np.get_include()) +except Exception as exc: + incdir = np.get_include() +print(incdir) + ''', + ], + check: true, +).stdout().strip() + +inc_np = include_directories(incdir_numpy, is_system: true) + +# Copy the main __init__.py to the build dir. +# Some submodules (linalg, special, optimize) add pxd files to this. +# Needed to trick Cython, it won't do a relative import outside a package +_cython_tree = [fs.copyfile('__init__.py')] +cython_args = [ + '-Xcpow=True', + '-Xboundscheck=False', + '-Xwraparound=False', + '-Xcdivision=True', + '-Xbinding=True' +] +cython_c_args = ['-DNPY_NO_DEPRECATED_API=NPY_1_7_API_VERSION'] +if get_option('cython-coverage') + message('Building with Cython coverage support') + cython_args += ['-Xlinetrace=True'] + cython_c_args += ['-DCYTHON_TRACE=1'] +endif + +lm_dir = py.get_install_dir() / 'linearmodels' +# Generate version.py for sdist +meson.add_dist_script( + ['_build/git_version.py', '--meson-dist', '--write', + 'linearmodels/_version.py'] +) +if not fs.exists('_version.py') + generate_version = custom_target( + 'generate-version', + install: true, + build_always_stale: true, + build_by_default: true, + output: '_version.py', + input: '_build/git_version.py', + command: [py, '@INPUT@', '--write', '@OUTPUT@'], + install_dir: lm_dir, + install_tag: 'python-runtime' + ) +else + # When building from sdist, version.py exists and should be included + py.install_sources( + ['_version.py'], + subdir : 'linearmodels' + ) +endif + +subdir('panel') + +subdirs_list = [ + '__future__', + '_build', + 'asset_pricing', + 'compat', + 'datasets', + 'iv', + 'panel', + 'shared', + 'system', + 'tests', + 'typing', +] + +foreach subdir : subdirs_list + install_subdir(subdir, install_dir: py.get_install_dir() / 'linearmodels') +endforeach + +top_level_py_list = [ + '__init__.py', + 'conftest.py', + 'formula.py', + 'py.typed', +] + +py.install_sources(top_level_py_list, subdir: 'linearmodels') diff --git a/linearmodels/panel/meson.build b/linearmodels/panel/meson.build new file mode 100644 index 0000000000..d8ee29e76c --- /dev/null +++ b/linearmodels/panel/meson.build @@ -0,0 +1,11 @@ +if not get_option('no-binary') + py.extension_module( + '_utility', + '_utility.pyx', + install: true, + include_directories: [inc_np], + subdir: 'linearmodels/panel', + cython_args: cython_args, + c_args: cython_c_args, + ) +endif diff --git a/linearmodels/tests/panel/test_data.py b/linearmodels/tests/panel/test_data.py index 4e5d79b6d7..1a8b118abb 100644 --- a/linearmodels/tests/panel/test_data.py +++ b/linearmodels/tests/panel/test_data.py @@ -663,7 +663,7 @@ def test_general_demean_oneway(mi_df): g = Categorical(g.iloc[:, 0]) d = get_dummies(g) dm1 = y.values2d - d @ lstsq(d, y.values2d, rcond=None)[0] - assert_allclose(dm1, dm2.values2d) + assert_allclose(dm1, dm2.values2d, atol=1e-10) def test_general_demean_twoway(mi_df): @@ -710,7 +710,7 @@ def test_general_unit_weighted_demean_oneway(mi_df): d = get_dummies(g) dm1 = y.values2d - d @ lstsq(d, y.values2d, rcond=None)[0] assert_allclose(dm1, dm2.values2d) - assert_allclose(dm3.values2d, dm2.values2d) + assert_allclose(dm3.values2d, dm2.values2d, atol=1e-10) def test_general_weighted_demean_oneway(mi_df): diff --git a/linearmodels/tests/test_build.py b/linearmodels/tests/test_build.py new file mode 100644 index 0000000000..b9785b0ec5 --- /dev/null +++ b/linearmodels/tests/test_build.py @@ -0,0 +1,36 @@ +import tempfile + +import pytest + +from linearmodels import __version__, version_tuple + +try: + from linearmodels._build.git_version import get_version, write_version_file + + HAS_SETUPTOOLS_SCM = True +except ImportError: + HAS_SETUPTOOLS_SCM = False + + +@pytest.mark.skipif(not HAS_SETUPTOOLS_SCM, reason="setuptools_scm is not installed") +def test_get_version(): + try: + version, version_fields = get_version() + + assert isinstance(version, str) + assert isinstance(version_fields, tuple) + assert all(isinstance(v, (int, str)) for v in version_fields) + except LookupError: + pytest.skip("No git repository found") + + +@pytest.mark.skipif(not HAS_SETUPTOOLS_SCM, reason="setuptools_scm is not installed") +def test_write_version_file(): + with tempfile.NamedTemporaryFile(delete=False) as tmpfile: + + write_version_file(tmpfile.name, __version__, version_tuple) + with open(tmpfile.name, "r") as f: + content = f.read() + + assert f"__version__ = version = '{__version__}'" in content + assert f"__version_tuple__ = version_tuple = {version_tuple}" in content diff --git a/meson.build b/meson.build new file mode 100644 index 0000000000..e671988bd8 --- /dev/null +++ b/meson.build @@ -0,0 +1,22 @@ +project( + 'linearmodels', + 'c', 'cython', + license: 'NCSA', + meson_version: '>= 1.9.0', + default_options: [], + version: run_command(['linearmodels/_build/git_version.py'], check: true).stdout().strip(), +) + +cc = meson.get_compiler('c') +cy = meson.get_compiler('cython') +cython = find_program(cy.cmd_array()[0]) + +if not cy.version().version_compare('>=3.0.10') + error('linearmodels requires Cython >= 3.0.10') +endif + +py = import('python').find_installation(pure: false) +fs = import('fs') + +py.install_sources('pyproject.toml', subdir: 'linearmodels') +subdir('linearmodels') diff --git a/meson.options b/meson.options new file mode 100644 index 0000000000..63643b6ef7 --- /dev/null +++ b/meson.options @@ -0,0 +1,11 @@ +# Use this options by adding to the pip command: +# -Csetup-args="-Dcython-coverage=true" +# then run: +# pytest --cov=statsmodels statsmodels +# coverage html +option('cython-coverage', type: 'boolean', value: false, + description: 'Compile and build cython modules with coverage support') +# Use this options by adding to the pip command: +# -Csetup-args="-Dno-binary=true" +option('no-binary', type: 'boolean', value: false, + description: 'Do not build any Cython extensions') diff --git a/pyproject.toml b/pyproject.toml index e4ea263413..c0e2fb361c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,30 +1,25 @@ +[build-system] +build-backend = 'mesonpy' +requires = [ + 'meson-python', + "cython>=3.0.13,<4", # Sync with CYTHON_MIN_VER in setup + "numpy>=2.0.0,<3", + "setuptools_scm>=9.2.0,<10", +] + [project] name = "linearmodels" -readme = "README.md" -license = "NCSA" -license-files = [ "LICENSE.md" ] -dynamic = ["dependencies", "version"] -authors = [{ name = "Kevin Sheppard", email = "kevin.k.sheppard@gmail.com" }] -maintainers = [ - { name = "Kevin Sheppard", email = "kevin.k.sheppard@gmail.com" }, -] -description = "Linear Panel, Instrumental Variable, Asset Pricing, and System Regression models for Python" -requires-python = ">=3.10" -keywords=[ - "linear models", - "regression", - "instrumental variables", - "IV", - "panel", - "fixed effects", - "clustered", - "heteroskedasticity", - "endogeneity", - "instruments", - "statistics", - "statistical inference", - "econometrics", +# Keep syned with requirements.txt +dependencies = [ + "numpy>=1.22.3,<3", + "pandas>=1.4.0", + "scipy>=1.8.0", + "statsmodels>=0.13.0", + "mypy_extensions>=0.4", + "pyhdfe>=0.1", + "formulaic>=1.2.1", ] +authors = [{ name = "Kevin Sheppard", email = "kevin.k.sheppard@gmail.com" }] classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: End Users/Desktop", @@ -41,6 +36,107 @@ classifiers = [ "Programming Language :: Cython", "Topic :: Scientific/Engineering", ] +description = "Linear Panel, Instrumental Variable, Asset Pricing, and System Regression models for Python" +version = "7.0.0dev0" +keywords=[ + "linear models", + "regression", + "instrumental variables", + "IV", + "panel", + "fixed effects", + "clustered", + "heteroskedasticity", + "endogeneity", + "instruments", + "statistics", + "statistical inference", + "econometrics", +] +license = "NCSA" +license-files = [ "LICENSE.md" ] +maintainers = [ + { name = "Kevin Sheppard", email = "kevin.k.sheppard@gmail.com" }, +] +readme = "README.md" +requires-python = ">=3.10" + +[project.optional-dependencies] +test = [ + # test + "pytest>=8.4.1,<9", + "pytest-randomly", + "pytest-xdist", + "pytest-cov", + "coverage[toml]", + "meson", + "ninja", +] +doc = [ + # Documentation + "setuptools>=61", + "wheel", + "setuptools_scm[toml]>=8.0.3,<9", + "oldest-supported-numpy", + "cython>=3.0.13,<4", # Sync with CYTHON_MIN_VER in setup + "numpy >=1.22", + "scipy >=1.5.0", + "ipython >=8.0.1", + "matplotlib >=3.0", + "patsy >=0.5.3", + "statsmodels >=0.13.5", + "jinja2", + "sphinx>=7", + "seaborn", + "numpydoc>=1.0.0", + "nbsphinx", + "sphinx-immaterial", + "jupyter", + "notebook", + "sphinx-autodoc-typehints", + "fonttools>=4.43.0", # not directly required, pinned by Snyk to avoid a vulnerability + "jupyterlab>=4.4.8", # not directly required, pinned by Snyk to avoid a vulnerability + "ipython>=7", + "sphinx_immaterial", + "nbconvert", + "pickleshare", + # Requirements for doc build, in addition to requirements.txt + "pandas~=2.3.3", + "sphinxcontrib-spelling", + "sphinx_autodoc_typehints", +] +dev = [ + "xarray>=0.16", + "pandas-stubsl", + "wheel", + # versioning + "setuptools_scm[toml]>=9.2.0,<10", + "packaging", + # Performance + "cython>=3.0.10", + "numba>=0.55", + # Graphics + "matplotlib>=3", + "seaborn", + # Tests + "pytest>=8.4.1,<9", + "pytest-randomly", + "pytest-xdist", + "pytest-cov", + # formatting + "black[jupyter]~=25.9.0", + "isort~=6.0", + "colorama", + "flake8", + "flake8-bugbear", + "mypy>=1.3", + "ruff>=0.8.6", + "pyupgrade>=3.4.0", + "jupyterlab-code-formatter", + "jupyterlab>=4.4.8", # not directly required, pinned by Snyk to avoid a vulnerability + "zipp>=3.19.1", # not directly required, pinned by Snyk to avoid a vulnerability +] + [project.urls] homepage = "https://github.com/bashtage/linearmodels" @@ -48,21 +144,14 @@ documentation = "https://bashtage.github.io/linearmodels/" repository = "https://github.com/bashtage/linearmodels" changelog = "https://bashtage.github.io/linearmodels/changes.html" -[build-system] -requires = [ - "setuptools>=61", - "wheel", - "setuptools_scm[toml]>=8,<9", - "numpy>=2.0.0rc1,<3", - "cython>=3.0.10,<4" -] -build-backend = "setuptools.build_meta" +[tool.meson-python.args] +setup = ['--vsenv'] [tool.black] -target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] +target-version = ['py310', 'py311', 'py312', 'py313'] exclude = ''' ( - \.egg + \.egg | \.git | \.mypy_cache | \.oytest_cache @@ -73,17 +162,9 @@ exclude = ''' ) ''' -[tool.setuptools_scm] -write_to = "linearmodels/_version.py" - [tool.pyright] exclude = [ - "**/tests/**", -] - -[tool.mypy] -exclude = [ - "tests", + "**/tests/**", ] [tool.ruff] @@ -92,7 +173,6 @@ fix = false target-version = "py310" [tool.ruff.lint] -typing-modules = ["statsmodels.compat.python", "compat.python"] select = [ # pyflakes @@ -146,29 +226,33 @@ select = [ ] ignore = [ - # Required to allow black formatting - # Whitespace before ':' (Needed for black) - "E203", - # Line too long (### > 88 characters) (Needed for black) - "E501", - # Multiple statements on one line (colon) (E701) - "E701", - # Always ignore - # TC do not work with doc building - # Move application import `linearmodels....` into a type-checking block - "TC001", - # Move standard library import `...` into a type-checking block - "TC003", - - # ### Intentionally disabled - # Too many arguments to function call - "PLR0913", # 55 - # Too many branches - "PLR0912", # 14 - # Too many statements - "PLR0915", # 21 - # Magic number - "PLR2004", + # Required to allow black formatting + # Whitespace before ':' (Needed for black) + "E203", + # Line too long (### > 88 characters) (Needed for black) + "E501", + # Multiple statements on one line (colon) (E701) + "E701", + # Always ignore + # TC do not work with doc building + # Move application import `linearmodels....` into a type-checking block + "TC001", + # Move third-party import `...` into a type-checking block + "TC002", + # Move standard library import `...` into a type-checking block + "TC003", + + # ### Intentionally disabled + # Too many arguments to function call + "PLR0913", # 55 + # Too many branches + "PLR0912", # 14 + # Too many statements + "PLR0915", # 21 + # Magic number + "PLR2004", + # Like to suggest use [a, *b] instead of [a] + b + "RUF005", ] [tool.ruff.lint.per-file-ignores] @@ -177,3 +261,138 @@ ignore = [ "linearmodels/tests/panel/results/execute-stata-simulated-data.py" = ["E501"] # Results import models, avoid circular "linearmodels/iv/results.py" = ["PERF203", "PLC0415"] + +[tool.pytest.ini_options] +minversion = "8.4.1" +testpaths = "linearmodels" +xfail_strict = true +addopts = "--strict" +empty_parameter_set_mark = "xfail" +filterwarnings = [ + "ignore:`formatargspec`:DeprecationWarning:statsmodels", + "ignore:Method .ptp is deprecated:FutureWarning", + "ignore:Traits should be given:DeprecationWarning", + "ignore:Using or importing the ABCs:DeprecationWarning", + "ignore:`nbconvert.exporters.exporter_locator` is deprecated:DeprecationWarning", + "ignore:Session._key_changed:DeprecationWarning", + "ignore:@asynchronous is deprecated:DeprecationWarning", + "error:A value is trying to be set::", + "error:The pandas.datetime class is deprecated:FutureWarning:", + "error:pandas.util.testing is deprecated:FutureWarning:", + "error:Support for multi-dimensional indexing:DeprecationWarning", + "error:multivariate_ls is deprecated:FutureWarning", + "error:invalid value encountered in true_divide:RuntimeWarning", + "ignore:The --strict option is deprecated, use --strict-markers instead:pytest.PytestDeprecationWarning", + "ignore:`np.bool` is a deprecated alias:DeprecationWarning:pyhdfe", + "ignore:defusedxml.cElementTree:DeprecationWarning:nbconvert", + "ignore:the imp module is deprecated in favour of importlib:DeprecationWarning:pywintypes", + "error:Setting an item of incompatible dtype:FutureWarning:", + +] +junit_family = "xunit2" +markers = [ + "slow: mark a test as slow", + "smoke: mark a test as a coding error (smoke) test" +] + +[tool.isort] +profile = "black" +src_paths = ["linearmodels"] +sections = ["FUTURE","COMPAT","STDLIB","THIRDPARTY","FIRSTPARTY","LOCALFOLDER"] +known_first_party = ["linearmodels"] +known_compat = ["linearmodels.compat.*"] +known_third_party=["Cython","numpy","matplotlib","pandas","pytest","cython","scipy","seaborn"] +line_length=88 +force_grid_wrap=0 +multi_line_output=3 +combine_as_imports=true +force_sort_within_sections=true +include_trailing_comma=true +use_parentheses=true + +[tool.coverage.run] +source = ["linearmodels"] +branch = true +plugins = ["Cython.Coverage"] +omit = [ + # Version file + "*/_version.py", + # Compatibility code + "*/compat/*", + "*/results/*", + "*/conftest.py", +] + +[tool.coverage.report] +show_missing = true +ignore_errors = false +# Regexes for lines to exclude from consideration +exclude_also = [ + # Have to re-enable the standard pragma + "pragma: no cover", + + # Do not complain about missing debug-only code: + "def __repr__", + "if self\\.debug", + + # Don't complain if tests don't hit defensive assertion code: + "raise NotImplementedError", + "except NotImplementedError", + "raise AssertionError", + # Ignore pass + "pass", + # Ignore failure messages + "pytest.xfail", + # Ignore ImportError protection + "raise ImportError", + "except ImportError", + # Ignore type checking code + "if TYPE_CHECKING", + "elif TYPE_CHECKING", + # Cython function declarations + "cdef", + # Cython functions with void + "cdef void", + # Numba jit decorators + "@jit", + # Do not complain if non-runnable code is not run: + "if 0:", + "if __name__ == .__main__.:", +] +include = [ "*/linearmodels/*" ] +omit = [ + "*/_version.py", + "*/compat/*", + "*recursions.py", + "*samplers.py", +] + +[tool.coverage.html] +directory = "coverage_html_report" + +[tool.mypy] +plugins="numpy.typing.mypy_plugin" +exclude = [ + "tests", +] +ignore_missing_imports=true +no_implicit_optional=true +strict_equality=true +disallow_untyped_defs=true +disallow_incomplete_defs=true +show_error_context=true + +[mypy-linearmodels.conftest] +check_untyped_defs=false +disallow_untyped_defs=false + +[mypy-linearmodels._version] +check_untyped_defs=false +disallow_untyped_defs=false +disallow_incomplete_defs=false + +[[tool.mypy.overrides]] +module = "mypy-linearmodels.tests." +check_untyped_defs=false +disallow_untyped_defs=false +disallow_incomplete_defs=false diff --git a/requirements-dev.txt b/requirements-dev.txt index 31550863ea..e425d0c1ea 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,29 +1,45 @@ +# Build and version +meson-python>=0.18.0 +meson +ninja +setuptools_scm[toml]>=8.0.3,<9 +packaging +wheel + +# Performance cython>=3.0.10 + +# Testing +pytest>=8.4.1,<9 +pytest-xdist +pytest-cov +pytest-randomly +coverage[toml] xarray>=0.16 +matplotlib + +# Linting mypy>=1.3 black[jupyter]~=25.9.0 -pytest>=8.4.1,<9 isort>=5.12 -ipython -matplotlib ruff>=0.8.6 -jupyterlab-code-formatter flake8 +flake8-bugbear +pandas-stubs +scipy-stubs + +# Docs +ipython jupyter +jupyterlab-code-formatter nbconvert nbformat nbsphinx numpydoc -pandas-stubs -pytest-xdist -pytest-cov seaborn -setuptools_scm[toml]>=8.0.3,<9 sphinx sphinx-immaterial sphinxcontrib-spelling sphinx_autodoc_typehints -wheel -flake8-bugbear jupyterlab>=4.4.8 # not directly required, pinned by Snyk to avoid a vulnerability zipp>=3.19.1 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/requirements-test.txt b/requirements-test.txt index 3de9b20951..2b86893340 100644 --- a/requirements-test.txt +++ b/requirements-test.txt @@ -7,6 +7,7 @@ matplotlib pytest>=8.4.1,<9 pytest-xdist pytest-cov +pytest-randomly seaborn xarray>=0.13 wheel diff --git a/setup.cfg b/setup.cfg index 7acfd7aeed..e69de29bb2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,71 +0,0 @@ -[project] -license = "NCSA" - -[flake8] -max-line-length = 99 -ignore = E203,W503,BLK100 -per-file-ignores = - linearmodels/panel/data.py: E704 - linearmodels/shared/utility.py: F811, E704 - linearmodels/panel/utility.py: F811 - -[tool:pytest] -# Change syntax due to pytest bug -minversion = 7 -testpaths = linearmodels -addopts = --strict -# Filter warnings generated by dependencies -filterwarnings = - ignore:`formatargspec`:DeprecationWarning:statsmodels - ignore:Method .ptp is deprecated:FutureWarning - ignore:Traits should be given:DeprecationWarning - ignore:Using or importing the ABCs:DeprecationWarning - ignore:`nbconvert.exporters.exporter_locator` is deprecated:DeprecationWarning - ignore:Session._key_changed:DeprecationWarning - ignore:@asynchronous is deprecated:DeprecationWarning - error:A value is trying to be set:: - error:The pandas.datetime class is deprecated:FutureWarning: - error:pandas.util.testing is deprecated:FutureWarning: - error:Support for multi-dimensional indexing:DeprecationWarning - error:multivariate_ls is deprecated:FutureWarning - error:invalid value encountered in true_divide:RuntimeWarning - ignore:The --strict option is deprecated, use --strict-markers instead:pytest.PytestDeprecationWarning - ignore:`np.bool` is a deprecated alias:DeprecationWarning:pyhdfe - ignore:defusedxml.cElementTree:DeprecationWarning:nbconvert - ignore:the imp module is deprecated in favour of importlib:DeprecationWarning:pywintypes - error:Setting an item of incompatible dtype:FutureWarning: - -markers = - slow: mark a test as slow - smoke: mark a test as a coding error (smoke) test - -[isort] -sections=FUTURE,COMPAT,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER -known_first_party=linearmodels -known_third_party=Cython,numpy,matplotlib,pandas,pytest,statsmodels,seaborn -known_compat=linearmodels.compat.*,setuptools,setuptools.* -combine_as_imports=True -force_sort_within_sections=True -force_to_top=True -profile=black - -[mypy] -plugins=numpy.typing.mypy_plugin -ignore_missing_imports=True -no_implicit_optional=True -strict_equality=True -disallow_untyped_defs=True -disallow_incomplete_defs=True - -[mypy-linearmodels.conftest] -check_untyped_defs=False -disallow_untyped_defs=False - -[mypy-linearmodels._version] -check_untyped_defs=False -disallow_untyped_defs=False - -[mypy-linearmodels.tests.*] -check_untyped_defs=False -disallow_untyped_defs=False -disallow_incomplete_defs=False