Skip to content

Commit c072440

Browse files
committed
Enhance documentation and CLI functionality
1 parent a4a3d1e commit c072440

File tree

9 files changed

+179
-43
lines changed

9 files changed

+179
-43
lines changed

.github/copilot-instructions.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,14 +34,16 @@ You work as an experienced senior software engineer on the **NetGraph** project,
3434

3535
---
3636

37-
# Contribution Guidelines for NetGraph
37+
## Contribution Guidelines for NetGraph
3838

3939
### 1 – Style & Linting
40+
4041
- Follow **PEP 8** with an 88-character line length.
4142
- All linting/formatting is handled by **ruff**; import order is automatic.
4243
- Do not run `black`, `isort`, or other formatters manually—use `make format` instead.
4344

4445
### 2 – Docstrings
46+
4547
- Use **Google-style** docstrings for every public module, class, function, and method.
4648
- Single-line docstrings are acceptable for simple private helpers.
4749
- Keep the prose concise and factual—no marketing fluff or AI verbosity.
@@ -138,11 +140,14 @@ Prioritize **why** over **what**, but include **what** when code is non-obvious.
138140
* Update `docs/` when adding features.
139141
* Run `make docs` to generate `docs/reference/api-full.md` from source code.
140142
* Always check all doc files for accuracy, absence of marketing language, and AI verbosity.
143+
* **Markdown formatting**: Lists, code blocks, and block quotes require a blank line before them to render correctly.
141144

142145
## Output rules for the assistant
143146

144147
1. Run Ruff format in your head before responding.
145148
2. Include Google-style docstrings and type hints.
146149
3. Write or update unit tests for new functionality; fix code (not tests) when existing tests fail. Exception: tests may be changed after thorough analysis if they are genuinely flawed, requirements have changed, or breaking changes are approved.
147150
4. Respect existing public API signatures unless the user approves breaking changes.
148-
5. If you need more information, ask concise clarification questions.
151+
5. Document all new features and changes in the codebase. Run `make docs` to generate the full API reference.
152+
6. Run `make check` before finishing to ensure all code passes linting, type checking, and tests.
153+
7. If you need more information, ask concise clarification questions.

docs/examples/basic.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ print(f"Sensitivity to capacity decreases: {sensitivity_decrease}")
143143
```
144144

145145
This analysis helps identify:
146+
146147
- **Bottleneck edges**: Links that are fully utilized and limit overall flow
147148
- **High-impact upgrades**: Which capacity increases provide the most benefit
148149
- **Vulnerability assessment**: How flow decreases when links are degraded

docs/examples/clos-fabric.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ print(f"Maximum flow with ECMP: {max_flow_ecmp}")
101101
## Understanding the Results
102102

103103
The result `{('b1|b2', 'b1|b2'): 256.0}` means:
104+
104105
- **Source**: All t1 nodes in both b1 and b2 segments of my_clos1
105106
- **Sink**: All t1 nodes in both b1 and b2 segments of my_clos2
106107
- **Capacity**: Maximum flow of 256.0 units

docs/reference/api-full.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ For a curated, example-driven API guide, see **[api.md](api.md)**.
1010
> - **[CLI Reference](cli.md)** - Command-line interface
1111
> - **[DSL Reference](dsl.md)** - YAML syntax guide
1212
13-
**Generated from source code on:** June 15, 2025 at 18:31 UTC
13+
**Generated from source code on:** June 16, 2025 at 17:06 UTC
1414

1515
**Modules auto-discovered:** 42
1616

docs/reference/cli.md

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,21 @@ pip install ngraph
1515
The primary command is `run`, which executes scenario files:
1616

1717
```bash
18-
# Run a scenario and write results to results.json
18+
# Run a scenario (execution only, no file output)
1919
python -m ngraph run scenario.yaml
2020

21-
# Write results to a custom file
21+
# Run a scenario and export results to results.json
22+
python -m ngraph run scenario.yaml --results
23+
24+
# Export results to a custom file
2225
python -m ngraph run scenario.yaml --results output.json
2326
python -m ngraph run scenario.yaml -r output.json
2427

25-
# Print results to stdout as well
28+
# Print results to stdout only (no file)
2629
python -m ngraph run scenario.yaml --stdout
30+
31+
# Export to file AND print to stdout
32+
python -m ngraph run scenario.yaml --results --stdout
2733
```
2834

2935
## Command Reference
@@ -33,6 +39,7 @@ python -m ngraph run scenario.yaml --stdout
3339
Execute a NetGraph scenario file.
3440

3541
**Syntax:**
42+
3643
```bash
3744
python -m ngraph run <scenario_file> [options]
3845
```
@@ -43,7 +50,7 @@ python -m ngraph run <scenario_file> [options]
4350

4451
**Options:**
4552

46-
- `--results`, `-r`: Output file path for results (JSON format)
53+
- `--results`, `-r`: Optional path to export results as JSON. If provided without a path, defaults to "results.json"
4754
- `--stdout`: Print results to stdout
4855
- `--keys`, `-k`: Space-separated list of workflow step names to include in output
4956
- `--help`, `-h`: Show help message
@@ -53,21 +60,27 @@ python -m ngraph run <scenario_file> [options]
5360
### Basic Execution
5461

5562
```bash
56-
# Run a scenario (writes results.json)
63+
# Run a scenario (execution only, no output files)
5764
python -m ngraph run my_network.yaml
65+
66+
# Run a scenario and export results to default file
67+
python -m ngraph run my_network.yaml --results
5868
```
5969

6070
### Save Results to File
6171

6272
```bash
63-
# Save results to a JSON file
73+
# Save results to a custom JSON file
6474
python -m ngraph run my_network.yaml --results analysis.json
75+
76+
# Save to file AND print to stdout
77+
python -m ngraph run my_network.yaml --results analysis.json --stdout
6578
```
6679

6780
### Running Test Scenarios
6881

6982
```bash
70-
# Run one of the included test scenarios
83+
# Run one of the included test scenarios with results export
7184
python -m ngraph run tests/scenarios/scenario_1.yaml --results results.json
7285
```
7386

@@ -76,14 +89,14 @@ python -m ngraph run tests/scenarios/scenario_1.yaml --results results.json
7689
You can filter the output to include only specific workflow steps using the `--keys` option:
7790

7891
```bash
79-
# Only include results from the capacity_probe step
80-
python -m ngraph run scenario.yaml --keys capacity_probe
92+
# Only include results from the capacity_probe step (stdout only)
93+
python -m ngraph run scenario.yaml --keys capacity_probe --stdout
8194

82-
# Include multiple specific steps
83-
python -m ngraph run scenario.yaml --keys build_graph capacity_probe
95+
# Include multiple specific steps and export to file
96+
python -m ngraph run scenario.yaml --keys build_graph capacity_probe --results filtered.json
8497

85-
# Filter and print to stdout
86-
python -m ngraph run scenario.yaml --keys capacity_probe --stdout
98+
# Filter and print to stdout while also saving to default file
99+
python -m ngraph run scenario.yaml --keys capacity_probe --results --stdout
87100
```
88101

89102
The `--keys` option filters by the `name` field of workflow steps defined in your scenario YAML file. For example, if your scenario has:
@@ -156,6 +169,44 @@ The exact keys and values depend on:
156169
- The parameters and results of each step
157170
- The network topology and analysis performed
158171

172+
## Output Behavior
173+
174+
**NetGraph CLI output behavior changed in recent versions** to provide more flexibility:
175+
176+
### Default Behavior (No Output Flags)
177+
```bash
178+
python -m ngraph run scenario.yaml
179+
```
180+
- Executes the scenario
181+
- Logs execution progress to the terminal
182+
- **Does not create any output files**
183+
- **Does not print results to stdout**
184+
185+
### Export to File
186+
```bash
187+
# Export to default file (results.json)
188+
python -m ngraph run scenario.yaml --results
189+
190+
# Export to custom file
191+
python -m ngraph run scenario.yaml --results my_analysis.json
192+
```
193+
194+
### Print to Terminal
195+
```bash
196+
python -m ngraph run scenario.yaml --stdout
197+
```
198+
- Prints JSON results to stdout
199+
- **Does not create any files**
200+
201+
### Combined Output
202+
```bash
203+
python -m ngraph run scenario.yaml --results analysis.json --stdout
204+
```
205+
- Creates a JSON file AND prints to stdout
206+
- Useful for viewing results immediately while also saving them
207+
208+
**Migration Note:** If you were relying on automatic `results.json` creation, add the `--results` flag to your commands.
209+
159210
## Integration with Workflows
160211

161212
The CLI executes the complete workflow defined in your scenario file, running all steps in sequence and accumulating results. This automates complex network analysis tasks without manual intervention.

docs/reference/dsl.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ When using capturing groups `(...)` in regex patterns, NetGraph groups matching
587587
**Adjacency Matching:**
588588

589589
In `adjacency` blocks (both in blueprints and top-level network):
590+
590591
- `source` and `target` fields accept regex patterns
591592
- Blueprint paths can be relative (no leading `/`) or absolute (with leading `/`)
592593
- Relative paths are resolved relative to the blueprint instance's path

ngraph/cli.py

Lines changed: 37 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,15 @@
1515

1616
def _run_scenario(
1717
path: Path,
18-
output: Path,
18+
output: Optional[Path],
1919
stdout: bool,
2020
keys: Optional[list[str]] = None,
2121
) -> None:
22-
"""Run a scenario file and store results as JSON.
22+
"""Run a scenario file and optionally export results as JSON.
2323
2424
Args:
2525
path: Scenario YAML file.
26-
output: Path where results should be written.
26+
output: Optional path where JSON results should be written. If None, no JSON export.
2727
stdout: Whether to also print results to stdout.
2828
keys: Optional list of workflow step names to include. When ``None`` all steps are
2929
exported.
@@ -38,23 +38,36 @@ def _run_scenario(
3838
scenario.run()
3939
logger.info("Scenario execution completed successfully")
4040

41-
logger.info("Serializing results to JSON")
42-
results_dict: Dict[str, Dict[str, Any]] = scenario.results.to_dict()
43-
44-
if keys:
45-
filtered: Dict[str, Dict[str, Any]] = {}
46-
for step, data in results_dict.items():
47-
if step in keys:
48-
filtered[step] = data
49-
results_dict = filtered
50-
51-
json_str = json.dumps(results_dict, indent=2, default=str)
52-
53-
logger.info(f"Writing results to: {output}")
54-
output.write_text(json_str)
55-
logger.info("Results written successfully")
56-
57-
if stdout:
41+
# Only export JSON if output path is provided
42+
if output:
43+
logger.info("Serializing results to JSON")
44+
results_dict: Dict[str, Dict[str, Any]] = scenario.results.to_dict()
45+
46+
if keys:
47+
filtered: Dict[str, Dict[str, Any]] = {}
48+
for step, data in results_dict.items():
49+
if step in keys:
50+
filtered[step] = data
51+
results_dict = filtered
52+
53+
json_str = json.dumps(results_dict, indent=2, default=str)
54+
55+
logger.info(f"Writing results to: {output}")
56+
output.write_text(json_str)
57+
logger.info("Results written successfully")
58+
59+
if stdout:
60+
print(json_str)
61+
elif stdout:
62+
# Print to stdout even without file export
63+
results_dict: Dict[str, Dict[str, Any]] = scenario.results.to_dict()
64+
if keys:
65+
filtered: Dict[str, Dict[str, Any]] = {}
66+
for step, data in results_dict.items():
67+
if step in keys:
68+
filtered[step] = data
69+
results_dict = filtered
70+
json_str = json.dumps(results_dict, indent=2, default=str)
5871
print(json_str)
5972

6073
except FileNotFoundError:
@@ -90,13 +103,14 @@ def main(argv: Optional[List[str]] = None) -> None:
90103
"--results",
91104
"-r",
92105
type=Path,
93-
default=Path("results.json"),
94-
help="Path to write JSON results (default: results.json)",
106+
nargs="?",
107+
const=Path("results.json"),
108+
help="Export results to JSON file (default: results.json if no path specified)",
95109
)
96110
run_parser.add_argument(
97111
"--stdout",
98112
action="store_true",
99-
help="Print results to stdout as well",
113+
help="Print results to stdout",
100114
)
101115
run_parser.add_argument(
102116
"--keys",

ngraph/workflow/notebook_export.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,14 @@ def run(self, scenario: "Scenario") -> None:
8686

8787
if not results_dict:
8888
if self.allow_empty_results:
89-
logger.info("No results found - creating minimal notebook")
89+
logger.warning(
90+
"No analysis results found, but proceeding with empty notebook "
91+
"because 'allow_empty_results=True'. This may indicate missing "
92+
"analysis steps in the scenario workflow."
93+
)
94+
# Always export JSON file, even if empty, for consistency
95+
self._save_results_json({}, json_output_path)
9096
nb = self._create_empty_notebook()
91-
json_output_path = None
9297
else:
9398
raise ValueError(
9499
"No analysis results found. Cannot create notebook without data. "
@@ -116,7 +121,9 @@ def run(self, scenario: "Scenario") -> None:
116121
# Create error notebook as fallback for write errors
117122
try:
118123
nb = self._create_error_notebook(str(e))
119-
self._write_notebook(nb, scenario, notebook_output_path, None)
124+
self._write_notebook(
125+
nb, scenario, notebook_output_path, json_output_path
126+
)
120127
except Exception as write_error:
121128
logger.error(f"Failed to write error notebook: {write_error}")
122129
raise

tests/test_cli.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ def test_cli_run_stdout(tmp_path: Path, capsys, monkeypatch) -> None:
2525
captured = capsys.readouterr()
2626
data = json.loads(captured.out)
2727
assert "build_graph" in data
28-
assert (tmp_path / "results.json").exists()
28+
# With new behavior, --stdout alone should NOT create a file
29+
assert not (tmp_path / "results.json").exists()
2930

3031

3132
def test_cli_filter_keys(tmp_path: Path, capsys, monkeypatch) -> None:
@@ -463,3 +464,58 @@ def test_cli_regression_empty_results_with_filter() -> None:
463464

464465
# The probe step should have actual data
465466
assert len(data["probe_step"]) > 0
467+
468+
469+
def test_cli_run_results_default(tmp_path: Path, monkeypatch) -> None:
470+
"""Test that --results with no path creates results.json."""
471+
scenario = Path("tests/scenarios/scenario_1.yaml").resolve()
472+
monkeypatch.chdir(tmp_path)
473+
cli.main(["run", str(scenario), "--results"])
474+
assert (tmp_path / "results.json").exists()
475+
data = json.loads((tmp_path / "results.json").read_text())
476+
assert "build_graph" in data
477+
478+
479+
def test_cli_run_results_custom_path(tmp_path: Path, monkeypatch) -> None:
480+
"""Test that --results with custom path creates file at that location."""
481+
scenario = Path("tests/scenarios/scenario_1.yaml").resolve()
482+
monkeypatch.chdir(tmp_path)
483+
cli.main(["run", str(scenario), "--results", "custom_output.json"])
484+
assert (tmp_path / "custom_output.json").exists()
485+
assert not (tmp_path / "results.json").exists()
486+
data = json.loads((tmp_path / "custom_output.json").read_text())
487+
assert "build_graph" in data
488+
489+
490+
def test_cli_run_results_and_stdout(tmp_path: Path, capsys, monkeypatch) -> None:
491+
"""Test that --results and --stdout work together."""
492+
scenario = Path("tests/scenarios/scenario_1.yaml").resolve()
493+
monkeypatch.chdir(tmp_path)
494+
cli.main(["run", str(scenario), "--results", "--stdout"])
495+
496+
# Check stdout output
497+
captured = capsys.readouterr()
498+
stdout_data = json.loads(captured.out)
499+
assert "build_graph" in stdout_data
500+
501+
# Check file output
502+
assert (tmp_path / "results.json").exists()
503+
file_data = json.loads((tmp_path / "results.json").read_text())
504+
assert "build_graph" in file_data
505+
506+
# Should be the same data
507+
assert stdout_data == file_data
508+
509+
510+
def test_cli_run_no_output(tmp_path: Path, capsys, monkeypatch) -> None:
511+
"""Test that running without --results or --stdout creates no files."""
512+
scenario = Path("tests/scenarios/scenario_1.yaml").resolve()
513+
monkeypatch.chdir(tmp_path)
514+
cli.main(["run", str(scenario)])
515+
516+
# No files should be created
517+
assert not (tmp_path / "results.json").exists()
518+
519+
# No stdout output should be produced
520+
captured = capsys.readouterr()
521+
assert captured.out == ""

0 commit comments

Comments
 (0)