pysofra 0.1.0a3__tar.gz → 0.1.0a4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/.gitignore +4 -1
  2. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/CHANGELOG.md +13 -2
  3. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/PKG-INFO +7 -7
  4. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/README.md +5 -5
  5. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/pyproject.toml +15 -3
  6. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/__init__.py +1 -1
  7. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/table.py +1 -1
  8. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/extras.py +18 -0
  9. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/tbl_one.py +12 -0
  10. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/themes/registry.py +2 -2
  11. pysofra-0.1.0a3/tests/test_joss_api_stability.py → pysofra-0.1.0a4/tests/test_api_stability.py +5 -5
  12. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_plot_determinism.py +1 -1
  13. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_validation_fixes.py +5 -5
  14. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_wishlist.py +2 -2
  15. pysofra-0.1.0a3/src/pysofra/io/__init__.py +0 -1
  16. pysofra-0.1.0a3/src/pysofra/notebook/__init__.py +0 -6
  17. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/LICENSE +0 -0
  18. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/NOTICE +0 -0
  19. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/__init__.py +0 -0
  20. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/compose.py +0 -0
  21. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/format.py +0 -0
  22. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/frames.py +0 -0
  23. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/core/schema.py +0 -0
  24. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/__init__.py +0 -0
  25. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/extract.py +0 -0
  26. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/pool.py +0 -0
  27. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/regression.py +0 -0
  28. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/survival.py +0 -0
  29. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/models/uvregression.py +0 -0
  30. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/__init__.py +0 -0
  31. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/_backend.py +0 -0
  32. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/forest.py +0 -0
  33. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/inline.py +0 -0
  34. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/plot/km.py +0 -0
  35. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/__init__.py +0 -0
  36. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/_zip_determinism.py +0 -0
  37. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/base.py +0 -0
  38. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/docx.py +0 -0
  39. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/html.py +0 -0
  40. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/image.py +0 -0
  41. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/latex.py +0 -0
  42. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/markdown.py +0 -0
  43. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/pptx.py +0 -0
  44. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/render/xlsx.py +0 -0
  45. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/__init__.py +0 -0
  46. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/calibrate.py +0 -0
  47. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/design.py +0 -0
  48. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/effect_size.py +0 -0
  49. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/smd.py +0 -0
  50. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/stats.py +0 -0
  51. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/tbl_cross.py +0 -0
  52. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/tbl_summary.py +0 -0
  53. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/tests.py +0 -0
  54. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/typing.py +0 -0
  55. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/summary/weights.py +0 -0
  56. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/src/pysofra/themes/__init__.py +0 -0
  57. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/conftest.py +0 -0
  58. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/README.md +0 -0
  59. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/anova_oneway.json +0 -0
  60. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/chi_square.json +0 -0
  61. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/fisher_2x2.json +0 -0
  62. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/kruskal_wallis.json +0 -0
  63. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/student_t.json +0 -0
  64. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/svyttest.json +0 -0
  65. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/weighted_mean.json +0 -0
  66. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/welch_t_test.json +0 -0
  67. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/fixtures/scipy_validation/wilcoxon_rank_sum.json +0 -0
  68. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_compose.py +0 -0
  69. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_compose_edges.py +0 -0
  70. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_conditional_formatting.py +0 -0
  71. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_design_regression.py +0 -0
  72. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_extract_edges.py +0 -0
  73. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_extras_edges.py +0 -0
  74. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_extras_edges_2.py +0 -0
  75. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_format.py +0 -0
  76. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_latex_pptx.py +0 -0
  77. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_misc_fixes.py +0 -0
  78. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_modifier_edges.py +0 -0
  79. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_multi_model.py +0 -0
  80. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_partial_modifiers.py +0 -0
  81. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_partials.py +0 -0
  82. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_plot_embedding.py +0 -0
  83. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_plots.py +0 -0
  84. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_polars.py +0 -0
  85. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_pptx_overflow.py +0 -0
  86. /pysofra-0.1.0a3/tests/test_joss_property_invariants.py → /pysofra-0.1.0a4/tests/test_property_invariants.py +0 -0
  87. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_rao_scott.py +0 -0
  88. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_regression.py +0 -0
  89. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_regressions.py +0 -0
  90. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_render_edges.py +0 -0
  91. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_render_edges_2.py +0 -0
  92. /pysofra-0.1.0a3/tests/test_joss_renderer_consistency.py → /pysofra-0.1.0a4/tests/test_renderer_consistency.py +0 -0
  93. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_rendering.py +0 -0
  94. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_scipy_validation.py +0 -0
  95. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_snapshot.py +0 -0
  96. /pysofra-0.1.0a3/tests/test_joss_statistical_correctness.py → /pysofra-0.1.0a4/tests/test_statistical_correctness.py +0 -0
  97. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_stats.py +0 -0
  98. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_summary_edges.py +0 -0
  99. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_summary_edges_2.py +0 -0
  100. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_survey_design.py +0 -0
  101. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_survey_extensions.py +0 -0
  102. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_survival.py +0 -0
  103. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_table_edges.py +0 -0
  104. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_tbl_one.py +0 -0
  105. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_test_overrides.py +0 -0
  106. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_uvregression_factors.py +0 -0
  107. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_weights.py +0 -0
  108. {pysofra-0.1.0a3 → pysofra-0.1.0a4}/tests/test_xlsx.py +0 -0
@@ -16,11 +16,14 @@ htmlcov/
16
16
  .DS_Store
17
17
  .idea/
18
18
  .vscode/
19
- .claude/
20
19
 
21
20
  site/
22
21
  docs/_build/
22
+
23
+ # Local working directories not part of the source distribution.
23
24
  paper/
25
+ _local/
26
+ _private/
24
27
 
25
28
  # Tutorial / example export artefacts — produced on demand by
26
29
  # scripts/render_tutorial.py, not part of the source tree.
@@ -5,11 +5,22 @@ All notable changes to PySofra will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.0a4] — 2026-05-25
9
+
10
+ ### Added
11
+ - Input validation for duplicate names in `variables=` (now raises
12
+ `ValueError` instead of silently accepting duplicates).
13
+ - Confidence-level range check in `.add_ci()` and related modifiers
14
+ (must lie in `(0, 1)`).
15
+
16
+ ### Changed
17
+ - Renamed several test files for clarity. No public API changes.
18
+
8
19
  ## [0.1.0a3] — 2026-05-24
9
20
 
10
21
  ### Changed
11
- - Documentation polish: scrubbed internal-process references from test
12
- docstrings and inline comments. No public API or behavioural changes.
22
+ - Documentation polish across README, changelog, and inline docstrings.
23
+ No public API or behavioural changes.
13
24
 
14
25
  ## [0.1.0a2] — 2026-05-23
15
26
 
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pysofra
3
- Version: 0.1.0a3
3
+ Version: 0.1.0a4
4
4
  Summary: Statistical reporting and table preparation framework for Python — the missing reporting layer.
5
5
  Project-URL: Homepage, https://github.com/jturner-uofl/pysofra
6
6
  Project-URL: Documentation, https://github.com/jturner-uofl/pysofra
7
7
  Project-URL: Repository, https://github.com/jturner-uofl/pysofra
8
8
  Project-URL: Issues, https://github.com/jturner-uofl/pysofra/issues
9
- Author-email: Jason Turner <jason.s.turner@gmail.com>
9
+ Author-email: Jason Turner <jason.turner@louisville.edu>
10
10
  License: GPL-3.0-or-later
11
11
  License-File: LICENSE
12
12
  License-File: NOTICE
@@ -255,23 +255,23 @@ pip install "pysofra[dev]" # testing + linting (pytest, ruff, mypy, hypot
255
255
 
256
256
  ## Status
257
257
 
258
- PySofra is in **alpha** (`0.1.0a3`). The public API surface is pinned
258
+ PySofra is in **alpha** (`0.1.0a4`). The public API surface is pinned
259
259
  by an explicit
260
- [API-stability test](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_api_stability.py)
260
+ [API-stability test](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_api_stability.py)
261
261
  so that any unintended rename, removal, or signature change surfaces as
262
262
  a failed test. Quality bar at this release:
263
263
 
264
264
  * **More than 800 tests passing**, **100% line coverage**, mypy strict, ruff clean.
265
265
  * Every numeric output is validated against `scipy`, `lifelines`,
266
266
  `statsmodels`, or a hand-computed textbook formula
267
- ([test_joss_statistical_correctness.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_statistical_correctness.py)).
267
+ ([test_statistical_correctness.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_statistical_correctness.py)).
268
268
  * Universal invariants enforced via Hypothesis on 720 randomized
269
269
  examples per CI run
270
- ([test_joss_property_invariants.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_property_invariants.py)).
270
+ ([test_property_invariants.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_property_invariants.py)).
271
271
  * Renderer output is byte-deterministic — identical input always
272
272
  produces identical HTML/Markdown/LaTeX, required for reproducible
273
273
  publication artifacts
274
- ([test_joss_renderer_consistency.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_renderer_consistency.py)).
274
+ ([test_renderer_consistency.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_renderer_consistency.py)).
275
275
 
276
276
  Bug reports and use-case feedback are very welcome.
277
277
 
@@ -189,23 +189,23 @@ pip install "pysofra[dev]" # testing + linting (pytest, ruff, mypy, hypot
189
189
 
190
190
  ## Status
191
191
 
192
- PySofra is in **alpha** (`0.1.0a3`). The public API surface is pinned
192
+ PySofra is in **alpha** (`0.1.0a4`). The public API surface is pinned
193
193
  by an explicit
194
- [API-stability test](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_api_stability.py)
194
+ [API-stability test](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_api_stability.py)
195
195
  so that any unintended rename, removal, or signature change surfaces as
196
196
  a failed test. Quality bar at this release:
197
197
 
198
198
  * **More than 800 tests passing**, **100% line coverage**, mypy strict, ruff clean.
199
199
  * Every numeric output is validated against `scipy`, `lifelines`,
200
200
  `statsmodels`, or a hand-computed textbook formula
201
- ([test_joss_statistical_correctness.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_statistical_correctness.py)).
201
+ ([test_statistical_correctness.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_statistical_correctness.py)).
202
202
  * Universal invariants enforced via Hypothesis on 720 randomized
203
203
  examples per CI run
204
- ([test_joss_property_invariants.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_property_invariants.py)).
204
+ ([test_property_invariants.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_property_invariants.py)).
205
205
  * Renderer output is byte-deterministic — identical input always
206
206
  produces identical HTML/Markdown/LaTeX, required for reproducible
207
207
  publication artifacts
208
- ([test_joss_renderer_consistency.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_joss_renderer_consistency.py)).
208
+ ([test_renderer_consistency.py](https://github.com/jturner-uofl/pysofra/blob/main/tests/test_renderer_consistency.py)).
209
209
 
210
210
  Bug reports and use-case feedback are very welcome.
211
211
 
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pysofra"
7
- version = "0.1.0a3"
7
+ version = "0.1.0a4"
8
8
  description = "Statistical reporting and table preparation framework for Python — the missing reporting layer."
9
9
  readme = "README.md"
10
10
  license = { text = "GPL-3.0-or-later" }
11
- authors = [{ name = "Jason Turner", email = "jason.s.turner@gmail.com" }]
11
+ authors = [{ name = "Jason Turner", email = "jason.turner@louisville.edu" }]
12
12
  requires-python = ">=3.11"
13
13
  keywords = [
14
14
  "statistics",
@@ -118,8 +118,9 @@ exclude = [
118
118
  "scripts",
119
119
  "site",
120
120
  ".github",
121
- ".claude",
122
121
  "paper",
122
+ "_local",
123
+ "_private",
123
124
  "uv.lock",
124
125
  ]
125
126
 
@@ -157,6 +158,17 @@ exclude_lines = [
157
158
  [tool.ruff]
158
159
  line-length = 100
159
160
  target-version = "py311"
161
+ # Notebooks are tutorial / pedagogical material, not library source.
162
+ # Their lint cleanliness is checked separately on demand; the project's
163
+ # "ruff clean" claim applies to the importable package and test suite.
164
+ extend-exclude = [
165
+ "examples/*.ipynb",
166
+ "examples/*.html",
167
+ "site",
168
+ ".venv",
169
+ "_local",
170
+ "_private",
171
+ ]
160
172
 
161
173
  [tool.ruff.lint]
162
174
  select = ["E", "F", "I", "UP", "B", "SIM"]
@@ -50,7 +50,7 @@ from .summary.tbl_summary import tbl_summary
50
50
  from .summary.tests import available_tests
51
51
  from .themes.registry import available_themes, register_theme
52
52
 
53
- __version__ = "0.1.0a3"
53
+ __version__ = "0.1.0a4"
54
54
 
55
55
  __all__ = [
56
56
  "CellPart",
@@ -571,7 +571,7 @@ class SofraTable:
571
571
 
572
572
  Uses matplotlib under the hood; the result is a faithful raster
573
573
  of the HTML output. Useful for quick previews, Slack attachments,
574
- and submission figures where reviewers want a visual.
574
+ and document figures where a static image is preferable.
575
575
 
576
576
  ``scale`` multiplies the pixel density (>= 1 recommended);
577
577
  ``dpi`` controls the output resolution (defaults to 300, the
@@ -330,6 +330,20 @@ def add_difference(
330
330
  *,
331
331
  digits: int = 2,
332
332
  conf_level: float = 0.95,
333
+ ) -> SofraTable:
334
+ """Add an absolute-difference column with CI for a 2-group Table 1."""
335
+ if not (0.0 < conf_level < 1.0):
336
+ raise ValueError(
337
+ f"conf_level must lie in the open interval (0, 1); got {conf_level!r}."
338
+ )
339
+ return _add_difference_impl(table, digits=digits, conf_level=conf_level)
340
+
341
+
342
+ def _add_difference_impl(
343
+ table: SofraTable,
344
+ *,
345
+ digits: int = 2,
346
+ conf_level: float = 0.95,
333
347
  ) -> SofraTable:
334
348
  """Add an absolute-difference column with CI for a 2-group Table 1.
335
349
 
@@ -486,6 +500,10 @@ def add_ci(
486
500
  mean. For dichotomous rows the ``n (%)`` cell gains a Wilson-score
487
501
  CI for the proportion. Multi-level categorical rows are unchanged.
488
502
  """
503
+ if not (0.0 < conf_level < 1.0):
504
+ raise ValueError(
505
+ f"conf_level must lie in the open interval (0, 1); got {conf_level!r}."
506
+ )
489
507
  if table._spec is None or table._rebuild is None:
490
508
  raise ValueError(
491
509
  "add_ci needs access to the source data — only tables built "
@@ -176,6 +176,18 @@ def tbl_one(
176
176
  missing_cols = [v for v in variables if v not in data.columns]
177
177
  if missing_cols:
178
178
  raise KeyError(f"variables not in data: {missing_cols}")
179
+ # Reject duplicate entries early — silently de-duplicating would
180
+ # produce a table whose row count doesn't match the user's list,
181
+ # while keeping duplicates would emit identical rows.
182
+ seen: dict[str, int] = {}
183
+ for v in variables:
184
+ seen[v] = seen.get(v, 0) + 1
185
+ dupes = [v for v, n in seen.items() if n > 1]
186
+ if dupes:
187
+ raise ValueError(
188
+ f"variables contains duplicate names: {dupes}. "
189
+ "Each variable may appear at most once."
190
+ )
179
191
  # Warn when the user-supplied variables list overlaps the design /
180
192
  # stratification columns; silently dropping them is surprising.
181
193
  overlap = [v for v in variables if v in excluded]
@@ -53,8 +53,8 @@ _DEFAULT = Theme(
53
53
  "font-size": "14px",
54
54
  "line-height": "1.45",
55
55
  # Inherit the surrounding text colour so we always have contrast
56
- # against the actual page background — no prefers-color-scheme
57
- # hacks that fight Jupyter's own theme.
56
+ # against the actual page background — no media-query rules
57
+ # that compete with the host application's own theme.
58
58
  "color": "inherit",
59
59
  "background": "transparent",
60
60
  "margin": "0.75em 0",
@@ -134,9 +134,9 @@ def test_survey_design_dataclass_fields():
134
134
  # ----------------------------------------------------------------------
135
135
  # SofraTable user-facing methods + dataclass attributes.
136
136
  #
137
- # Both sets are part of the JSS API contract: methods because users
138
- # call them in their reporting code, attributes because they appear in
139
- # the documented schema (the paper, README, and notebook examples
137
+ # Both sets are part of the documented public API: methods because
138
+ # users call them in their reporting code, attributes because they
139
+ # appear in the documented schema (README and notebook examples
140
140
  # enumerate ``rows``, ``headers``, ``footnotes``, etc.).
141
141
  # ----------------------------------------------------------------------
142
142
  EXPECTED_SOFRATABLE_METHODS = frozenset({
@@ -186,7 +186,7 @@ def test_sofratable_attribute_surface():
186
186
  def test_sofratable_no_undocumented_public_surface():
187
187
  """
188
188
  Reject silently-added public names. Anything new must be added to
189
- one of the two expected-sets above (so reviewers see the API
189
+ one of the two expected-sets above (so downstream users see the API
190
190
  change in the diff).
191
191
  """
192
192
  public = {m for m in dir(SofraTable) if not m.startswith("_")}
@@ -200,6 +200,6 @@ def test_sofratable_no_undocumented_public_surface():
200
200
 
201
201
 
202
202
  def test_sofratable_to_image_signature_stable():
203
- """The PNG renderer's public kwargs are part of the JSS API contract."""
203
+ """The PNG renderer's public kwargs are part of the documented public API."""
204
204
  actual = tuple(p[0] for p in _params(SofraTable.to_image))
205
205
  assert actual == ("self", "path", "scale", "dpi")
@@ -1,6 +1,6 @@
1
1
  """Determinism tests for plot-embedded tables.
2
2
 
3
- The renderer-consistency suite (`test_joss_renderer_consistency.py`)
3
+ The renderer-consistency suite (`test_renderer_consistency.py`)
4
4
  verifies determinism for plain text-only tables. This file extends
5
5
  that guarantee to tables that have an inline matplotlib plot
6
6
  (`with_forest_plot`, `with_km_plot`) — historically the source of
@@ -16,7 +16,7 @@ import pysofra as ps
16
16
 
17
17
 
18
18
  # ======================================================================
19
- # BLOCKER #4 — add_difference uses Newcombe (Wilson-based) CI
19
+ # add_difference uses Newcombe (Wilson-based) CI
20
20
  # ======================================================================
21
21
  class TestNewcombeDifferenceCI:
22
22
  def test_matches_statsmodels_newcomb(self):
@@ -75,7 +75,7 @@ class TestNewcombeDifferenceCI:
75
75
 
76
76
 
77
77
  # ======================================================================
78
- # BLOCKER #7 / HIGH #8 — Markdown spanners + escaping
78
+ # Markdown spanners + escaping
79
79
  # ======================================================================
80
80
  class TestMarkdownSpannersAndEscape:
81
81
  def test_spanning_header_not_inserted_as_pipe_row(self):
@@ -137,7 +137,7 @@ class TestMarkdownSpannersAndEscape:
137
137
 
138
138
 
139
139
  # ======================================================================
140
- # HIGH #11 — labels= no longer breaks downstream modifiers
140
+ # labels= no longer breaks downstream modifiers
141
141
  # ======================================================================
142
142
  class TestLabelsPreservedDownstream:
143
143
  def _df(self):
@@ -170,7 +170,7 @@ class TestLabelsPreservedDownstream:
170
170
 
171
171
 
172
172
  # ======================================================================
173
- # HIGH #13 — N at risk uses standard convention
173
+ # N at risk uses standard convention
174
174
  # ======================================================================
175
175
  class TestNAtRisk:
176
176
  def test_n_at_risk_matches_manual_count(self):
@@ -195,7 +195,7 @@ class TestNAtRisk:
195
195
 
196
196
 
197
197
  # ======================================================================
198
- # HIGH #10 — add_global_p raises clearly on tbl_one
198
+ # add_global_p raises clearly on tbl_one
199
199
  # ======================================================================
200
200
  class TestAddGlobalPOnTblOne:
201
201
  # ``add_global_p()`` is implemented for tbl_one / tbl_summary
@@ -1,6 +1,6 @@
1
- """Tests for the R-ecosystem wishlist features: tbl_cross, effect sizes,
1
+ """Tests for R-ecosystem feature parity: tbl_cross, effect sizes,
2
2
  add_significance_stars / add_n / add_stat_label / color_scale_if,
3
- MixedLM/GEE extractor, and MI pooling (Rubin's rules)."""
3
+ MixedLM/GEE extractor, and multiple-imputation pooling (Rubin's rules)."""
4
4
 
5
5
  from __future__ import annotations
6
6
 
@@ -1 +0,0 @@
1
- """I/O utilities — reserved for future readers (Stata/SAS/Excel)."""
@@ -1,6 +0,0 @@
1
- """Notebook helpers.
2
-
3
- Most of the notebook integration lives on :class:`SofraTable._repr_html_`.
4
- This module is reserved for future notebook-specific extensions (e.g.
5
- ipywidgets-based controls).
6
- """
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes