Skip to content

Commit 924bd82

Browse files
committed
pydoclint, pyright
1 parent 2765a24 commit 924bd82

9 files changed

Lines changed: 152 additions & 249 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
runs-on: ubuntu-latest
1212
strategy:
1313
matrix:
14-
python-version: ["3.11", "3.12", "3.13"]
14+
python-version: ["3.12", "3.13", "3.14"]
1515

1616
steps:
1717
- uses: actions/checkout@v4
@@ -34,7 +34,7 @@ jobs:
3434
run: uv run ruff format --check .
3535

3636
- name: Run type checking
37-
run: uv run mypy fairlex/
37+
run: uv run pyright
3838

3939
- name: Run tests
4040
run: uv run pytest -q --color=yes

.github/workflows/python-publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ jobs:
4040
run: uv run ruff format --check .
4141

4242
- name: Run type checking
43-
run: uv run mypy fairlex/
43+
run: uv run pyright
4444

4545
- name: Run tests
4646
run: uv run pytest -q --color=yes

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ fairlex: leximin calibration
33

44
[![PyPI version](https://img.shields.io/pypi/v/fairlex.svg)](https://pypi.org/project/fairlex/)
55
[![PyPI Downloads](https://static.pepy.tech/badge/fairlex)](https://pepy.tech/projects/fairlex)
6-
[![Python 3.8+](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
6+
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/fairlex)](https://pypi.org/project/fairlex/)
77
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
88

99

@@ -37,8 +37,8 @@ principal calibration strategies are:
3737
Installation
3838
------------
3939

40-
``fairlex`` requires Python 3.8+ and depends on ``numpy`` and
41-
``scipy``. You can install it via pip once uploaded to PyPI:
40+
``fairlex`` requires Python 3.12+ and depends on ``numpy>=1.26`` and
41+
``scipy>=1.11``. You can install it via pip once uploaded to PyPI:
4242

4343
```bash
4444
pip install fairlex

fairlex/__init__.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,4 @@
3333
from .metrics import evaluate_solution
3434

3535
# Expose the package version at runtime
36-
try: # pragma: no cover - metadata may not be present in editable installs
37-
__version__ = version("fairlex")
38-
except Exception:
39-
__version__ = "0.0.0"
36+
__version__ = version("fairlex")

fairlex/calibration.py

Lines changed: 17 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -32,21 +32,14 @@
3232
3333
"""
3434

35-
from __future__ import annotations
36-
3735
from dataclasses import dataclass
3836

3937
import numpy as np
38+
from scipy.optimize import linprog # type: ignore[import-untyped]
4039

4140
# Constants
4241
EXPECTED_MATRIX_DIMENSIONS = 2
4342

44-
try:
45-
# SciPy is used for linear programming; HiGHS is fast and reliable.
46-
from scipy.optimize import linprog # type: ignore
47-
except Exception: # pragma: no cover
48-
linprog = None
49-
5043

5144
@dataclass
5245
class CalibrationResult:
@@ -66,6 +59,7 @@ class CalibrationResult:
6659
Status code from the linear programme (0 indicates success).
6760
message : str
6861
Solver termination message for diagnostics.
62+
6963
"""
7064

7165
w: np.ndarray
@@ -76,7 +70,7 @@ class CalibrationResult:
7670

7771

7872
def _validate_inputs(
79-
A: np.ndarray, b: np.ndarray, w0: np.ndarray
73+
A: np.ndarray, b: np.ndarray, w0: np.ndarray,
8074
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
8175
"""Validate and coerce input arrays to ensure they have compatible shapes.
8276
@@ -98,17 +92,21 @@ def _validate_inputs(
9892
------
9993
ValueError
10094
If shapes are incompatible.
95+
10196
"""
10297
A = np.asarray(A, dtype=float)
10398
b = np.asarray(b, dtype=float)
10499
w0 = np.asarray(w0, dtype=float)
105100
if A.ndim != EXPECTED_MATRIX_DIMENSIONS:
106-
raise ValueError(f"A must be two-dimensional, got shape {A.shape}")
101+
msg = f"A must be two-dimensional, got shape {A.shape}"
102+
raise ValueError(msg)
107103
m, n = A.shape
108104
if b.shape != (m,):
109-
raise ValueError(f"b must be of shape {(m,)}, got {b.shape}")
105+
msg = f"b must be of shape {(m,)}, got {b.shape}"
106+
raise ValueError(msg)
110107
if w0.shape != (n,):
111-
raise ValueError(f"w0 must be of shape {(n,)}, got {w0.shape}")
108+
msg = f"w0 must be of shape {(n,)}, got {w0.shape}"
109+
raise ValueError(msg)
112110
return A, b, w0
113111

114112

@@ -138,11 +136,8 @@ def _solve_lp(
138136
-------
139137
res : OptimizeResult
140138
Result from the solver.
139+
141140
"""
142-
if linprog is None:
143-
raise ImportError(
144-
"SciPy is required to solve the linear programmes. Install scipy>=1.6 to use this function."
145-
)
146141
res = linprog(
147142
c=c,
148143
A_ub=A_ub,
@@ -197,6 +192,7 @@ def leximin_residual(
197192
If the problem is infeasible (e.g., because the bounds preclude any
198193
solution), the returned status will be nonzero and the weights may not be
199194
meaningful. Check ``status`` and ``message`` on the result.
195+
200196
"""
201197
A, b, w0 = _validate_inputs(A, b, w0)
202198
m, n = A.shape
@@ -231,7 +227,7 @@ def leximin_residual(
231227
w = x[:n]
232228
epsilon = x[-1]
233229
return CalibrationResult(
234-
w=w, epsilon=epsilon, t=None, status=res.status, message=res.message
230+
w=w, epsilon=epsilon, t=None, status=res.status, message=res.message,
235231
)
236232

237233

@@ -255,6 +251,7 @@ def _setup_weight_fair_constraints(
255251
Inequality constraint right hand side.
256252
bounds : list
257253
Variable bounds.
254+
258255
"""
259256
m, n = A.shape
260257

@@ -350,6 +347,7 @@ def leximin_weight_fair(
350347
:class:`CalibrationResult` containing the final weights and both the
351348
residual and weight fairness optima. If ``return_stages`` is
352349
``True``, a tuple ``(stage1_result, stage2_result)``.
350+
353351
"""
354352
stage1 = leximin_residual(A, b, w0, min_ratio=min_ratio, max_ratio=max_ratio)
355353
# If the residual stage failed, propagate the failure
@@ -369,7 +367,7 @@ def leximin_weight_fair(
369367

370368
# Set up constraints using helper function
371369
A_ub, b_ub, bounds = _setup_weight_fair_constraints(
372-
A, b, w0, stage1.epsilon, min_ratio=min_ratio, max_ratio=max_ratio, slack=slack
370+
A, b, w0, stage1.epsilon, min_ratio=min_ratio, max_ratio=max_ratio, slack=slack,
373371
)
374372

375373
res = _solve_lp(c, A_ub, b_ub, bounds)
@@ -389,7 +387,7 @@ def leximin_weight_fair(
389387
w = x[:n]
390388
t_opt = x[-1]
391389
stage2 = CalibrationResult(
392-
w=w, epsilon=stage1.epsilon, t=t_opt, status=res.status, message=res.message
390+
w=w, epsilon=stage1.epsilon, t=t_opt, status=res.status, message=res.message,
393391
)
394392
if return_stages:
395393
return stage1, stage2

fairlex/metrics.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@
2323
2424
"""
2525

26-
from __future__ import annotations
27-
2826
import numpy as np
2927

3028
__all__ = [
@@ -52,6 +50,7 @@ def effective_sample_size(weights: np.ndarray) -> float:
5250
-------
5351
float
5452
Effective sample size. Returns ``np.nan`` if the denominator is zero.
53+
5554
"""
5655
w = np.asarray(weights, dtype=float)
5756
numer = np.sum(w)
@@ -78,6 +77,7 @@ def design_effect(weights: np.ndarray) -> float:
7877
float
7978
Design effect. Returns ``np.nan`` if the effective sample size is
8079
undefined.
80+
8181
"""
8282
w = np.asarray(weights, dtype=float)
8383
ess = effective_sample_size(w)
@@ -87,9 +87,13 @@ def design_effect(weights: np.ndarray) -> float:
8787

8888

8989
def _compute_residual_metrics(
90-
A: np.ndarray, b: np.ndarray, w: np.ndarray
90+
A: np.ndarray, b: np.ndarray, w: np.ndarray,
9191
) -> dict[str, float]:
92-
"""Compute residual-based metrics."""
92+
"""Compute residual-based metrics.
93+
94+
Returns:
95+
Dictionary containing residual metrics.
96+
"""
9397
resid = A @ w - b
9498
abs_resid = np.abs(resid)
9599
return {
@@ -101,9 +105,13 @@ def _compute_residual_metrics(
101105

102106

103107
def _compute_weight_metrics(
104-
w: np.ndarray, quantiles: tuple[float, ...]
108+
w: np.ndarray, quantiles: tuple[float, ...],
105109
) -> dict[str, float]:
106-
"""Compute weight distribution metrics."""
110+
"""Compute weight distribution metrics.
111+
112+
Returns:
113+
Dictionary containing weight distribution metrics.
114+
"""
107115
# Calculate all quantiles plus min/max
108116
q_vals = np.quantile(w, (*quantiles, 0.0, 1.0))
109117
q_map = dict(zip((*quantiles, 0.0, 1.0), q_vals, strict=False))
@@ -116,7 +124,7 @@ def _compute_weight_metrics(
116124
"weight_min": float(q_map[0.0]),
117125
"weight_p99": float(q_map.get(0.99, q_map[sorted_q[0]])),
118126
"weight_p95": float(
119-
q_map.get(0.95, q_map[sorted_q[min(1, len(sorted_q) - 1)]])
127+
q_map.get(0.95, q_map[sorted_q[min(1, len(sorted_q) - 1)]]),
120128
),
121129
"weight_median": float(q_map.get(0.5, np.median(w))),
122130
"ESS": float(effective_sample_size(w)),
@@ -125,9 +133,13 @@ def _compute_weight_metrics(
125133

126134

127135
def _compute_relative_deviations(
128-
w: np.ndarray, base_weights: np.ndarray
136+
w: np.ndarray, base_weights: np.ndarray,
129137
) -> dict[str, float]:
130-
"""Compute relative deviation metrics."""
138+
"""Compute relative deviation metrics.
139+
140+
Returns:
141+
Dictionary containing relative deviation metrics.
142+
"""
131143
bw = np.asarray(base_weights, dtype=float)
132144
rel_dev = np.abs(w - bw) / np.where(bw == 0, 1.0, np.abs(bw))
133145
return {
@@ -169,6 +181,7 @@ def evaluate_solution(
169181
dict
170182
A dictionary containing residual and weight diagnostics. See module
171183
docstring for the key descriptions.
184+
172185
"""
173186
w = np.asarray(w, dtype=float)
174187
A = np.asarray(A, dtype=float)

fairlex/py.typed

Whitespace-only changes.

pyproject.toml

Lines changed: 50 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,27 +4,27 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "fairlex"
7-
version = "0.1.0"
7+
version = "0.3.0"
88
description = "Leximin calibration for survey weights"
99
authors = [
1010
{ name = "Gaurav Sood", email = "contact@gsood.com" },
1111
]
1212
license = { text = "MIT" }
1313
readme = "README.md"
14-
requires-python = ">=3.11"
14+
requires-python = ">=3.12"
1515
dependencies = [
16-
"numpy>=1.21",
17-
"scipy>=1.6",
16+
"numpy>=1.26.0",
17+
"scipy>=1.11.0",
1818
]
1919
classifiers = [
2020
"Development Status :: 3 - Alpha",
2121
"License :: OSI Approved :: MIT License",
2222
"Programming Language :: Python",
2323
"Programming Language :: Python :: 3",
2424
"Programming Language :: Python :: 3 :: Only",
25-
"Programming Language :: Python :: 3.11",
2625
"Programming Language :: Python :: 3.12",
2726
"Programming Language :: Python :: 3.13",
27+
"Programming Language :: Python :: 3.14",
2828
"Intended Audience :: Science/Research",
2929
"Topic :: Scientific/Engineering :: Mathematics",
3030
"Topic :: Scientific/Engineering :: Information Analysis",
@@ -35,8 +35,9 @@ dev = [
3535
"pytest>=7.0",
3636
"pytest-cov>=4.0",
3737
"ruff>=0.7.0",
38-
"mypy>=1.0",
39-
"pre-commit>=3.0"
38+
"pre-commit>=3.0",
39+
"pydoclint>=0.3.0",
40+
"pyright>=1.1.0"
4041
]
4142
test = [
4243
"pytest>=7.0",
@@ -58,7 +59,7 @@ packages = ["fairlex"]
5859

5960
[tool.ruff]
6061
line-length = 88
61-
target-version = "py311"
62+
target-version = "py312"
6263
preview = true
6364
exclude = [
6465
".git",
@@ -101,16 +102,48 @@ ignore = [
101102
[tool.ruff.lint.isort]
102103
known-first-party = ["fairlex"]
103104

104-
[tool.mypy]
105-
python_version = "3.11"
106-
warn_return_any = true
107-
warn_unused_configs = true
108-
check_untyped_defs = true
109-
strict = true
110-
show_error_codes = true
111-
pretty = true
112-
113105
[tool.pytest.ini_options]
114106
testpaths = ["tests"]
115107
python_files = ["test_*.py"]
116108
addopts = "--cov=fairlex --cov-report=html --cov-report=term-missing"
109+
110+
[tool.pydoclint]
111+
style = "google"
112+
exclude = [
113+
"tests/*",
114+
"docs/*",
115+
"examples/*",
116+
"build/*",
117+
"dist/*",
118+
"__pycache__/*"
119+
]
120+
check-return-types = true
121+
check-yield-types = true
122+
arg-type-hints-in-docstring = true
123+
arg-type-hints-in-signature = true
124+
allow-init-docstring = true
125+
126+
[tool.pyright]
127+
include = ["fairlex"]
128+
exclude = [
129+
"tests",
130+
"docs",
131+
"examples",
132+
"build",
133+
"dist",
134+
"__pycache__",
135+
".venv"
136+
]
137+
pythonVersion = "3.12"
138+
typeCheckingMode = "basic"
139+
reportMissingImports = true
140+
reportMissingTypeStubs = false
141+
reportUnusedImport = true
142+
reportUnusedClass = true
143+
reportUnusedFunction = true
144+
reportDuplicateImport = true
145+
reportConstantRedefinition = false
146+
reportUnknownParameterType = false
147+
reportUnknownVariableType = false
148+
reportUnknownMemberType = false
149+
reportUnknownArgumentType = false

0 commit comments

Comments
 (0)