Skip to content

Commit 9d1d691

Browse files
authored
Add --dry-run to DE CLI, fix docs for v0.7.0 API (#7)
* Add smooth command to CLI docs, fix double-backslash rendering - Add kompot smooth section with usage, options, and examples - Add smooth to overview, quick start workflow, config templates, help - Fix all \\ to single \ in code-block directives (RST renders literally) * Remove redundant Quick Start examples from CLI docs The per-command basic examples repeated the same info as the workflow block above and the detailed command sections below. * Fix Sphinx docs: exclude deprecated APIs, add smooth module, update labels - Exclude compute_differential_abundance/expression from automodule - Add smooth_expression automodule (exclude deprecated compute_smoothed_expression) - Add RunInfo.to_settings() and call_args() to documented members - Rename "Gene Expression Imputation" to "Smoothing" in toctree * Add --dry-run to DE CLI with JSON output for pipeline integration - Add --dry-run flag to kompot de: estimates resource requirements without running the analysis - JSON output to stdout (or -o file), human-readable report to stderr - Exit code 0 if feasible, 1 if not - Add ResourcePlan.to_dict() for machine-parseable serialization - Add configure_logging() to redirect kompot logger stream - CLI now logs to stderr by default (keeps stdout clean for machine-parseable output like --dry-run and --table-output) - Document --dry-run in CLI docs with pipeline examples * Remove -o file option from --dry-run to prevent accidental overwrites Users would likely reuse the same args as the real run, which would overwrite their h5ad output with a JSON file. Stdout-only is safer. * Add Unreleased changelog section for post-v0.7.0 changes * Fix DE test failures and update CI actions to Node.js 24 - Add dry_run=False to all 8 DE test Namespace objects (run_de now accesses args.dry_run from the --dry-run flag added in this PR) - Update actions/checkout v4→v6 and actions/setup-python v5→v6 to resolve Node.js 20 deprecation warnings * Add dry-run and configure_logging tests, fix CI coverage reporting - Add TestCLIDryRun: covers --dry-run JSON output, infeasible exit code 1, and output validation skip when dry_run=True - Add TestConfigureLogging: covers stream redirection and default - Fix codecov action: file→files (v5 API change) - Add --cov-report=term-missing to pytest so coverage stats appear in CI logs * Fix missing sys import in configure_logging tests
1 parent 2fb60c1 commit 9d1d691

10 files changed

Lines changed: 447 additions & 66 deletions

File tree

.github/workflows/tests.yml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ jobs:
1414
python-version: ['3.10', '3.11', '3.12']
1515

1616
steps:
17-
- uses: actions/checkout@v4
17+
- uses: actions/checkout@v6
1818
- name: Set up Python ${{ matrix.python-version }}
19-
uses: actions/setup-python@v5
19+
uses: actions/setup-python@v6
2020
with:
2121
python-version: ${{ matrix.python-version }}
2222
cache: 'pip'
@@ -35,11 +35,11 @@ jobs:
3535
3636
- name: Test with pytest and generate coverage report
3737
run: |
38-
pytest --cov=kompot --cov-report=xml
38+
pytest --cov=kompot --cov-report=xml --cov-report=term-missing:skip-covered
3939
4040
- name: Upload coverage to Codecov
4141
uses: codecov/codecov-action@v5
4242
with:
43-
file: ./coverage.xml
43+
files: ./coverage.xml
4444
fail_ci_if_error: false
4545
token: ${{ secrets.CODECOV_TOKEN }}

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,23 @@
22

33
All notable changes to this project will be documented in this file.
44

5+
## [Unreleased]
6+
7+
### New features
8+
9+
- **`--dry-run` flag for `kompot de` CLI**: estimates memory, disk, and output field requirements without running the analysis. Outputs machine-parseable JSON to stdout and a human-readable report to stderr. Exit code reflects feasibility.
10+
- **`kompot.configure_logging(stream)`**: reconfigure the kompot logger output stream. The CLI now logs to stderr by default, keeping stdout clean for machine-parseable output (dry-run JSON, table output).
11+
12+
### Improvements
13+
14+
- **CLI logs to stderr**: all `kompot` CLI commands now write log messages to stderr instead of stdout, so stdout is reserved for data output.
15+
- **`kompot smooth` documented in CLI guide**: added full command reference, options, and examples to the Sphinx CLI docs.
16+
- Fix double-backslash rendering in all CLI doc code blocks.
17+
- Exclude deprecated `compute_differential_*` functions from Sphinx automodule output.
18+
- Add `smooth_expression()` module to Sphinx API docs.
19+
- Add `RunInfo.to_settings()` and `call_args()` to documented members.
20+
- Fix "Gene Expression Imputation" → "Gene Expression Smoothing" in docs toctree.
21+
522
## [0.7.0] - 2026-04-13
623

724
### Breaking changes

docs/source/anndata.rst

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Differential Abundance
2222
:members:
2323
:undoc-members:
2424
:show-inheritance:
25+
:exclude-members: compute_differential_abundance
2526

2627
Differential Expression
2728
-----------------------
@@ -30,6 +31,16 @@ Differential Expression
3031
:members:
3132
:undoc-members:
3233
:show-inheritance:
34+
:exclude-members: compute_differential_expression
35+
36+
Smooth Expression
37+
-----------------
38+
39+
.. automodule:: kompot.anndata.smooth
40+
:members:
41+
:undoc-members:
42+
:show-inheritance:
43+
:exclude-members: compute_smoothed_expression
3344

3445
Resource Estimation
3546
-------------------
@@ -71,7 +82,7 @@ Utilities
7182
---------
7283

7384
.. autoclass:: kompot.anndata.utils.RunInfo
74-
:members: __init__, get_summary, get_data, compare_with
85+
:members: __init__, get_summary, get_data, compare_with, to_settings, call_args
7586
:show-inheritance:
7687

7788
Cleanup Utilities

docs/source/cli.rst

Lines changed: 29 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,6 @@ All commands support:
4040
Quick Start
4141
-----------
4242

43-
Complete Workflow
44-
^^^^^^^^^^^^^^^^^
45-
4643
.. code-block:: bash
4744
4845
# 1. Compute diffusion maps (preprocessing)
@@ -54,54 +51,18 @@ Complete Workflow
5451
kompot de input_with_dm.h5ad -o de_results.h5ad \
5552
--groupby condition \
5653
--condition1 control \
57-
--condition2 treatment \
58-
--obsm-key DM_EigenVectors
54+
--condition2 treatment
5955
6056
# 3. Run differential abundance
6157
kompot da input_with_dm.h5ad -o da_results.h5ad \
6258
--groupby condition \
6359
--condition1 control \
64-
--condition2 treatment \
65-
--obsm-key DM_EigenVectors
60+
--condition2 treatment
6661
6762
# 4. Smooth gene expression for a single condition
6863
kompot smooth input_with_dm.h5ad -o smoothed.h5ad \
6964
--groupby condition \
70-
--condition treatment \
71-
--obsm-key DM_EigenVectors
72-
73-
Diffusion Maps (Preprocessing)
74-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
75-
76-
.. code-block:: bash
77-
78-
kompot dm input.h5ad -o output.h5ad \
79-
--pca-key X_pca \
80-
--n-components 10 \
81-
--knn 30
82-
83-
Differential Expression (Basic)
84-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
85-
86-
.. code-block:: bash
87-
88-
kompot de input.h5ad -o output.h5ad \
89-
--groupby condition \
90-
--condition1 control \
91-
--condition2 treatment \
92-
--obsm-key X_pca \
93-
--layer logged_counts
94-
95-
Differential Abundance (Basic)
96-
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
97-
98-
.. code-block:: bash
99-
100-
kompot da input.h5ad -o output.h5ad \
101-
--groupby condition \
102-
--condition1 control \
103-
--condition2 treatment \
104-
--obsm-key X_pca
65+
--condition treatment
10566
10667
Using Config Files
10768
^^^^^^^^^^^^^^^^^^
@@ -247,6 +208,7 @@ Boolean Flags
247208

248209
.. code-block:: text
249210
211+
--dry-run # Estimate resources, print plan, exit (no analysis)
250212
--no-progress # Disable progress bars
251213
--store-landmarks # Store landmarks for reuse
252214
--store-additional-stats # Store extra statistics
@@ -287,6 +249,31 @@ Example: Complete Analysis
287249
--null-genes 2000 \
288250
--store-additional-stats
289251
252+
Example: Dry Run (Resource Estimation)
253+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
254+
255+
Use ``--dry-run`` to check memory and disk requirements before committing
256+
to a long analysis. JSON is written to stdout; the human-readable report
257+
goes to stderr. Use the same arguments as the real run (``-o`` is ignored).
258+
259+
.. code-block:: bash
260+
261+
# Check resources (human report on stderr, JSON on stdout)
262+
kompot de bone_marrow.h5ad --dry-run \
263+
--groupby Age \
264+
--condition1 Young \
265+
--condition2 Old
266+
267+
# Save JSON plan and check feasibility in a pipeline
268+
kompot de input.h5ad --dry-run --groupby cond --condition1 A --condition2 B \
269+
> plan.json && echo "feasible"
270+
271+
# Parse specific fields with jq
272+
kompot de input.h5ad --dry-run --groupby cond --condition1 A --condition2 B \
273+
2>/dev/null | jq '.memory.total_human'
274+
275+
The exit code is ``0`` if the plan is feasible, ``1`` if not.
276+
290277
Differential Abundance Command
291278
-------------------------------
292279

docs/source/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
Getting Started <notebooks/01_getting_started.ipynb>
2727
Advanced Differential Expression <notebooks/02_differential_expression_detailed.ipynb>
2828
Sample Variance Analysis <notebooks/03_sample_variance.ipynb>
29-
Gene Expression Imputation <notebooks/04_expression_model.ipynb>
29+
Gene Expression Smoothing <notebooks/04_expression_model.ipynb>
3030

3131
.. |doi| image:: https://zenodo.org/badge/944121568.svg
3232
:target: https://zenodo.org/badge/latestdoi/944121568

kompot/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,23 @@
9999
logging.config.dictConfig(LOGGING_CONFIG)
100100
logger = logging.getLogger("kompot")
101101

102+
103+
def configure_logging(stream=None):
104+
"""Reconfigure the kompot logger to write to a different stream.
105+
106+
Parameters
107+
----------
108+
stream : file-like, optional
109+
Output stream for log messages. Defaults to ``sys.stdout``.
110+
CLI tools typically pass ``sys.stderr`` so stdout stays clean
111+
for machine-parseable output.
112+
"""
113+
if stream is None:
114+
stream = sys.stdout
115+
for handler in logger.handlers:
116+
if hasattr(handler, "stream"):
117+
handler.stream = stream
118+
102119
__all__ = [
103120
# Version
104121
"__version__",
@@ -122,6 +139,8 @@
122139
"StorageSettings",
123140
"OutputSettings",
124141
"ModelSettings",
142+
# Configuration
143+
"configure_logging",
125144
# Utility functions
126145
"compute_mahalanobis_distance",
127146
"find_landmarks",

kompot/cli/de.py

Lines changed: 62 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""CLI command for differential expression analysis."""
22

33
import argparse
4+
import json
45
import sys
56
from pathlib import Path
67
import logging
@@ -22,6 +23,19 @@
2223
logger = logging.getLogger("kompot.cli")
2324

2425

26+
def _json_default(obj):
27+
"""JSON serializer for numpy types."""
28+
import numpy as np
29+
30+
if isinstance(obj, (np.integer,)):
31+
return int(obj)
32+
if isinstance(obj, (np.floating,)):
33+
return float(obj)
34+
if isinstance(obj, np.ndarray):
35+
return obj.tolist()
36+
raise TypeError(f"Object of type {type(obj).__name__} is not JSON serializable")
37+
38+
2539
def add_de_parser(subparsers) -> argparse.ArgumentParser:
2640
"""
2741
Add differential expression subcommand parser.
@@ -165,6 +179,14 @@ def add_de_parser(subparsers) -> argparse.ArgumentParser:
165179
help="Estimate per-gene heteroscedastic noise from squared residuals to deflate significance for high-noise genes",
166180
)
167181

182+
# Dry run
183+
parser.add_argument(
184+
"--dry-run",
185+
action="store_true",
186+
help="Estimate resource requirements instead of running the analysis. "
187+
"JSON to stdout, human-readable report to stderr. -o/--output is ignored.",
188+
)
189+
168190
# Compute configuration
169191
parser.add_argument(
170192
"--use-gpu",
@@ -192,8 +214,8 @@ def run_de(args):
192214
args
193215
Parsed arguments from argparse
194216
"""
195-
# Validate output arguments
196-
if not args.output and not args.table_output:
217+
# Validate output arguments (not required for dry-run)
218+
if not args.dry_run and not args.output and not args.table_output:
197219
logger.error("Either --output or --table-output must be specified")
198220
sys.exit(1)
199221

@@ -245,6 +267,7 @@ def run_de(args):
245267
"command",
246268
"use_gpu",
247269
"threads",
270+
"dry_run",
248271
]
249272
}
250273

@@ -355,23 +378,45 @@ def run_de(args):
355378
output_kwargs["return_full_results"] = True
356379
output = OutputSettings(**output_kwargs) if output_kwargs else None
357380

381+
# Build shared call kwargs
382+
call_kwargs = dict(
383+
groupby=groupby,
384+
condition1=condition1,
385+
condition2=condition2,
386+
obsm_key=obsm_key,
387+
layer=layer,
388+
sample_col=sample_col,
389+
gp=gp,
390+
fdr=fdr,
391+
filter=filter_settings,
392+
storage=storage,
393+
output=output,
394+
**params, # remaining params forwarded as function_kwargs
395+
)
396+
397+
# Dry run: estimate resources, output JSON to stdout, report to stderr
398+
if args.dry_run:
399+
import io
400+
401+
try:
402+
old_stdout = sys.stdout
403+
sys.stdout = sys.stderr # capture de()'s print(plan.format_report())
404+
try:
405+
plan = de(adata, dry_run=True, **call_kwargs)
406+
finally:
407+
sys.stdout = old_stdout
408+
except Exception as e:
409+
logger.error(f"Dry run failed: {str(e)}")
410+
raise
411+
412+
# Machine-parseable JSON to stdout
413+
json.dump(plan.to_dict(), sys.stdout, default=_json_default, indent=2)
414+
print(file=sys.stdout) # trailing newline
415+
sys.exit(0 if plan.is_feasible else 1)
416+
358417
# Run analysis
359418
try:
360-
result_dict = de(
361-
adata,
362-
groupby=groupby,
363-
condition1=condition1,
364-
condition2=condition2,
365-
obsm_key=obsm_key,
366-
layer=layer,
367-
sample_col=sample_col,
368-
gp=gp,
369-
fdr=fdr,
370-
filter=filter_settings,
371-
storage=storage,
372-
output=output,
373-
**params, # remaining params forwarded as function_kwargs
374-
)
419+
result_dict = de(adata, **call_kwargs)
375420
except Exception as e:
376421
logger.error(f"Analysis failed: {str(e)}")
377422
raise

kompot/cli/main.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,8 +95,12 @@ def main():
9595
# Parse arguments
9696
args = parser.parse_args()
9797

98-
# Setup logging
98+
# Setup logging — CLI always logs to stderr so stdout stays clean
99+
# for machine-parseable output (e.g., --dry-run JSON, --table-output)
99100
setup_logging(args.verbose)
101+
from .. import configure_logging
102+
103+
configure_logging(stream=sys.stderr)
100104

101105
# If no command provided, print help
102106
if not args.command:

0 commit comments

Comments
 (0)