labnotes 0.0.56__tar.gz → 0.0.57__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 (106) hide show
  1. {labnotes-0.0.56 → labnotes-0.0.57}/PKG-INFO +1 -1
  2. labnotes-0.0.57/src/labnotes/group_runs.py +17 -0
  3. labnotes-0.0.57/src/labnotes/invocation_started.py +11 -0
  4. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/load_experiment.py +6 -3
  5. labnotes-0.0.57/src/labnotes/nests_runs.py +10 -0
  6. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/static/experiment.js +14 -2
  7. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/static/labnotes.css +9 -0
  8. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/templates/experiment.html +27 -6
  9. {labnotes-0.0.56 → labnotes-0.0.57}/testing-conventions.toml +6 -0
  10. {labnotes-0.0.56 → labnotes-0.0.57}/.gitignore +0 -0
  11. {labnotes-0.0.56 → labnotes-0.0.57}/CHANGELOG.md +0 -0
  12. {labnotes-0.0.56 → labnotes-0.0.57}/MIGRATIONS.md +0 -0
  13. {labnotes-0.0.56 → labnotes-0.0.57}/README.md +0 -0
  14. {labnotes-0.0.56 → labnotes-0.0.57}/pyproject.toml +0 -0
  15. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/__init__.py +0 -0
  16. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/asset_url.py +0 -0
  17. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/cell_progress.py +0 -0
  18. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/check_counts.py +0 -0
  19. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/cli.py +0 -0
  20. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/context.md +0 -0
  21. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/context_guide.py +0 -0
  22. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/count_runs.py +0 -0
  23. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/create_app.py +0 -0
  24. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/create_experiment.py +0 -0
  25. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/data_html.py +0 -0
  26. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/default_config_text.py +0 -0
  27. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/default_counts.py +0 -0
  28. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/drain_watch.py +0 -0
  29. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/experiment_notes.py +0 -0
  30. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/experiment_progress.py +0 -0
  31. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/experiment_runs.py +0 -0
  32. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/experiments.py +0 -0
  33. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/file_changes.py +0 -0
  34. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/format_duration.py +0 -0
  35. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/format_value.py +0 -0
  36. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/init.py +0 -0
  37. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/is_positive_int.py +0 -0
  38. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/list_experiments.py +0 -0
  39. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/list_runs.py +0 -0
  40. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/load_config.py +0 -0
  41. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/models.py +0 -0
  42. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/modes.py +0 -0
  43. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/notebook_kernel.py +0 -0
  44. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/output_html.py +0 -0
  45. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/parse_run_view.py +0 -0
  46. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/print_rows.py +0 -0
  47. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/python_blocks.py +0 -0
  48. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/read_config.py +0 -0
  49. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/read_path.py +0 -0
  50. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/render_markdown.py +0 -0
  51. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/__init__.py +0 -0
  52. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/describe_event.py +0 -0
  53. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/execute_run.py +0 -0
  54. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/hold_folder.py +0 -0
  55. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/new_run_id.py +0 -0
  56. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/new_stamped_run_id.py +0 -0
  57. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/param_args.py +0 -0
  58. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/param_grid.py +0 -0
  59. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/plan_runs.py +0 -0
  60. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/read_experiment_config.py +0 -0
  61. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/run_experiment.py +0 -0
  62. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/run_folder.py +0 -0
  63. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/run_headless.py +0 -0
  64. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/runs_parent.py +0 -0
  65. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/shell_command.py +0 -0
  66. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/start_experiment.py +0 -0
  67. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/stop_then_kill.py +0 -0
  68. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/wait_for_group.py +0 -0
  69. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/runner/write_run_json.py +0 -0
  70. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/save_note.py +0 -0
  71. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/saved_outputs.py +0 -0
  72. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/serve_app.py +0 -0
  73. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/server.py +0 -0
  74. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/sort_href.py +0 -0
  75. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/sql_quote.py +0 -0
  76. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/static/note-edit.js +0 -0
  77. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/static/note-editor.js +0 -0
  78. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/static/row-link.js +0 -0
  79. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/static/run-cell.js +0 -0
  80. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/static/view-toggle.js +0 -0
  81. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/stop_idle_kernels.py +0 -0
  82. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/summarize_config.py +0 -0
  83. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/__init__.py +0 -0
  84. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/experiment_rows.py +0 -0
  85. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/file_prefix.py +0 -0
  86. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/iso_time.py +0 -0
  87. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/list_experiment_runs.py +0 -0
  88. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/open_tables.py +0 -0
  89. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/path_glob.py +0 -0
  90. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/path_pattern.py +0 -0
  91. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/read_experiment.py +0 -0
  92. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/read_file.py +0 -0
  93. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/read_run.py +0 -0
  94. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tables/summarize_experiments.py +0 -0
  95. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/templates/base.html +0 -0
  96. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/templates/editor_imports.html +0 -0
  97. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/templates/index.html +0 -0
  98. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/templates/macros.html +0 -0
  99. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tui/__init__.py +0 -0
  100. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tui/dim.py +0 -0
  101. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tui/format_elapsed.py +0 -0
  102. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tui/run_app.py +0 -0
  103. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tui/run_tui.py +0 -0
  104. {labnotes-0.0.56 → labnotes-0.0.57}/src/labnotes/tui/runs_table.py +0 -0
  105. {labnotes-0.0.56 → labnotes-0.0.57}/tests/integration/conftest.py +0 -0
  106. {labnotes-0.0.56 → labnotes-0.0.57}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: labnotes
3
- Version: 0.0.56
3
+ Version: 0.0.57
4
4
  Summary: Browse experiment designs, notes, and run data from a folder.
5
5
  Project-URL: Repository, https://github.com/thekevinscott/labnotes
6
6
  Author-email: Kevin Scott <me@thekevinscott.com>
@@ -0,0 +1,17 @@
1
+ from labnotes.invocation_started import invocation_started
2
+
3
+
4
+ def group_runs(runs: list[dict[str, object]]) -> list[dict[str, object]]:
5
+ groups: dict[str, list[dict[str, object]]] = {}
6
+ for run in runs:
7
+ groups.setdefault(str(run["id"]).partition("/")[0], []).append(run)
8
+ return [
9
+ {
10
+ "name": name,
11
+ "started": invocation_started(name),
12
+ "runs": members,
13
+ "passed": sum(1 for run in members if run["labnotes_completed"]),
14
+ "failed": sum(1 for run in members if not run["labnotes_completed"]),
15
+ }
16
+ for name, members in sorted(groups.items(), reverse=True)
17
+ ]
@@ -0,0 +1,11 @@
1
+ import re
2
+
3
+ STAMP = re.compile(r"(\d{4})(\d{2})(\d{2})-(\d{2})(\d{2})(\d{2})")
4
+
5
+
6
+ def invocation_started(name: str) -> str | None:
7
+ found = STAMP.match(name)
8
+ if not found:
9
+ return None
10
+ year, month, day, hour, minute, second = found.groups()
11
+ return f"{year}-{month}-{day} {hour}:{minute}:{second}"
@@ -10,8 +10,10 @@ from labnotes.count_runs import count_runs
10
10
  from labnotes.experiment_notes import experiment_notes
11
11
  from labnotes.experiment_runs import experiment_runs
12
12
  from labnotes.format_value import format_value
13
+ from labnotes.group_runs import group_runs
13
14
  from labnotes.list_runs import list_runs
14
15
  from labnotes.models import QueryParams
16
+ from labnotes.nests_runs import nests_runs
15
17
  from labnotes.parse_run_view import parse_run_view
16
18
  from labnotes.python_blocks import python_blocks
17
19
  from labnotes.render_markdown import render_markdown
@@ -53,6 +55,8 @@ async def load_experiment(
53
55
  view = parse_run_view(params, shown, choices)
54
56
  [totals] = await count_runs(tables, source, str(summary["completed_run"]))
55
57
  notes = await experiment_notes(tables, root, name)
58
+ runs = await list_runs(tables, source, str(summary["completed_run"]), view)
59
+ nested = summary["mode"] == "adhoc" and await nests_runs(tables, source)
56
60
  return {
57
61
  "name": row["name"],
58
62
  "display_name": row["display_name"],
@@ -71,9 +75,8 @@ async def load_experiment(
71
75
  }
72
76
  for note in notes
73
77
  ],
74
- "runs_list": await list_runs(
75
- tables, source, str(summary["completed_run"]), view
76
- ),
78
+ "runs_list": runs,
79
+ "run_groups": group_runs(runs) if nested else None,
77
80
  "run_columns": shown,
78
81
  "run_view": view,
79
82
  "run_choices": choices,
@@ -0,0 +1,10 @@
1
+ from dirsql import DirSQL
2
+
3
+
4
+ async def nests_runs(runs: DirSQL, source: str) -> bool:
5
+ found = await runs.query(
6
+ f"SELECT 1 FROM {source} WHERE instr(id, '/') > 0"
7
+ " GROUP BY substr(id, 1, instr(id, '/') - 1)"
8
+ " HAVING COUNT(*) > 1 LIMIT 1"
9
+ )
10
+ return bool(found)
@@ -11,7 +11,14 @@ if (summary) {
11
11
  dialog.querySelector(".config-close").addEventListener("click", () => dialog.close());
12
12
  }
13
13
 
14
+ const expand = (group, open) => {
15
+ group.classList.toggle("open", open);
16
+ group.querySelector(".group-toggle").setAttribute("aria-expanded", String(open));
17
+ };
18
+
14
19
  document.addEventListener("click", (event) => {
20
+ const header = event.target.closest(".group-row");
21
+ if (header) expand(header.parentElement, !header.parentElement.classList.contains("open"));
15
22
  const row = event.target.closest(".run-row");
16
23
  if (!row || getSelection().toString()) return;
17
24
  const record = row.nextElementSibling;
@@ -43,8 +50,13 @@ if (filters) {
43
50
  runs.querySelector(".narrowed")?.remove();
44
51
  const narrowed = fresh.querySelector(".narrowed");
45
52
  if (narrowed) runs.prepend(narrowed);
46
- for (const part of ["thead tr:first-child", "tbody"]) {
47
- runs.querySelector(part).replaceWith(fresh.querySelector(part));
53
+ runs.querySelector("thead tr").replaceWith(fresh.querySelector("thead tr"));
54
+ const table = runs.querySelector("table");
55
+ const opened = new Set([...table.querySelectorAll(".group.open")].map((group) => group.dataset.name));
56
+ for (const body of table.querySelectorAll("tbody")) body.remove();
57
+ for (const body of fresh.querySelectorAll("tbody")) {
58
+ table.append(body);
59
+ if (opened.has(body.dataset.name)) expand(body, true);
48
60
  }
49
61
  const summaries = fresh.querySelectorAll(".pick summary");
50
62
  runs.querySelectorAll(".pick summary").forEach((summary, index) => {
@@ -316,6 +316,15 @@ h3 {
316
316
  .run-row:hover { background: color-mix(in srgb, var(--accent) 4%, transparent); }
317
317
  .run-row[aria-expanded="true"] { background: color-mix(in srgb, var(--accent) 6%, transparent); }
318
318
  .record-row td { background: var(--paper); }
319
+ .group-row { cursor: pointer; background: var(--paper); }
320
+ .group-row td { font-weight: 600; white-space: nowrap; }
321
+ .group-row .good { color: var(--good); }
322
+ .group-row .bad { color: var(--bad); }
323
+ .group-toggle { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
324
+ .group-toggle::before { content: "▸"; display: inline-block; width: 1rem; color: var(--ink-3); }
325
+ .group-toggle[aria-expanded="true"]::before { content: "▾"; }
326
+ .group:not(.open) > :not(.group-row) { display: none; }
327
+ .group.open .run-row td:nth-child(2) { padding-left: 2rem; }
319
328
 
320
329
  /* Notes */
321
330
  .note {
@@ -52,6 +52,8 @@
52
52
  {% else %}
53
53
  <section id="runs">
54
54
  {% set view = e.run_view %}
55
+ {% set nested = e.run_groups is not none %}
56
+ {% set width = e.run_columns | length + (2 if nested else 1) %}
55
57
  {% set narrowed = view.status or view.filters or view.picked %}
56
58
  {% if narrowed %}<p class="narrowed"><span class="num">{{ e.runs_list | length }}</span> of <span class="num">{{ e.runs }}</span> <a class="clear" href="{{ '?sort=' ~ (view.sort | urlencode) ~ ('&order=desc' if view.descending else '') if view.sort else '?' }}#runs">Clear filters</a></p>{% endif %}
57
59
  {% if e.runs %}
@@ -67,6 +69,7 @@
67
69
  {% for column in ['status'] + e.run_columns %}
68
70
  {% set sorted = view.sort == column %}
69
71
  <th scope="col"{% if sorted %} aria-sort="{{ 'descending' if view.descending else 'ascending' }}"{% endif %}><a class="sort{{ ' sorted' if sorted }}" href="{{ e.sort_hrefs[column] }}" title="Sort by {{ column }}">{{ '' if column == 'status' else column }}{% if sorted %}<span class="arrow">{{ '↓' if view.descending else '↑' }}</span>{% endif %}</a></th>
72
+ {% if nested and column == 'id' %}<th scope="col">runs</th>{% endif %}
70
73
  {% endfor %}
71
74
  </tr>
72
75
  <tr class="filters">
@@ -78,17 +81,16 @@
78
81
  {% for column in e.run_columns %}<td>{% if column in e.run_choices %}{% set picked = view.picked.get(column, {}) %}<details class="pick">
79
82
  <summary aria-label="Filter {{ column }}">{{ picked | join(', ') if picked else 'all' }}</summary>
80
83
  <div class="options">{% for label in e.run_choices[column] %}<label><input form="run-filters" type="checkbox" name="f.{{ column }}" value="{{ label }}"{{ ' checked' if label in picked }}> {{ label }}</label>{% endfor %}</div>
81
- </details>{% else %}<input form="run-filters" type="search" name="f.{{ column }}" value="{{ view.filters.get(column, '') }}" placeholder="filter" aria-label="Filter {{ column }}">{% endif %}</td>{% endfor %}
84
+ </details>{% else %}<input form="run-filters" type="search" name="f.{{ column }}" value="{{ view.filters.get(column, '') }}" placeholder="filter" aria-label="Filter {{ column }}">{% endif %}</td>{% if nested and loop.first %}<td></td>{% endif %}{% endfor %}
82
85
  </tr>
83
86
  </thead>
84
- <tbody>
85
- {% for run in e.runs_list %}
87
+ {% macro run_rows(run) %}
86
88
  <tr class="run-row" aria-expanded="false">
87
89
  <td><span class="status {{ 'good' if run.labnotes_completed else 'bad' }}" title="{{ 'complete' if run.labnotes_completed else 'not complete' }}">{{ '✓' if run.labnotes_completed else '✕' }}</span></td>
88
- {% for column in e.run_columns %}<td class="{{ 'mono' if loop.first }}{{ 'num' if column == 'duration' }}">{{ run[column] | duration if column == 'duration' else run[column] | value }}</td>{% endfor %}
90
+ {% for column in e.run_columns %}{% if column == 'id' and nested %}<td class="mono">{{ run.id.partition('/')[2] or run.id }}</td><td></td>{% else %}<td class="{{ 'mono' if loop.first }}{{ 'num' if column == 'duration' }}">{{ run[column] | duration if column == 'duration' else run[column] | value }}</td>{% endif %}{% endfor %}
89
91
  </tr>
90
92
  <tr class="record-row" hidden>
91
- <td colspan="{{ e.run_columns | length + 1 }}">
93
+ <td colspan="{{ width }}">
92
94
  <dl class="record">
93
95
  {% for key, value in run.items() if key != 'labnotes_completed' %}
94
96
  <dt>{{ key }}</dt>
@@ -98,10 +100,29 @@
98
100
  </dl>
99
101
  </td>
100
102
  </tr>
103
+ {% endmacro %}
104
+ {% if nested %}
105
+ {% for group in e.run_groups %}
106
+ <tbody class="group" data-name="{{ group.name }}">
107
+ <tr class="group-row">
108
+ <td class="num" title="{{ group.passed }} passed, {{ group.failed }} failed"><span class="good">{{ group.passed }}✓</span> <span class="bad">{{ group.failed }}✕</span></td>
109
+ <td class="mono"><button type="button" class="group-toggle" aria-expanded="false">{{ group.name }}</button></td>
110
+ <td class="num">{{ group.runs | length }}</td>
111
+ {% for column in e.run_columns[1:] %}<td>{{ group.started or '' if column == 'started' }}</td>{% endfor %}
112
+ </tr>
113
+ {% for run in group.runs %}{{ run_rows(run) }}{% endfor %}
114
+ </tbody>
115
+ {% else %}
116
+ <tbody><tr><td class="empty" colspan="{{ width }}">No runs match these filters.</td></tr></tbody>
117
+ {% endfor %}
118
+ {% else %}
119
+ <tbody>
120
+ {% for run in e.runs_list %}{{ run_rows(run) }}
101
121
  {% else %}
102
- <tr><td class="empty" colspan="{{ e.run_columns | length + 1 }}">No runs match these filters.</td></tr>
122
+ <tr><td class="empty" colspan="{{ width }}">No runs match these filters.</td></tr>
103
123
  {% endfor %}
104
124
  </tbody>
125
+ {% endif %}
105
126
  </table>
106
127
  </div>
107
128
  {% else %}
@@ -15,3 +15,9 @@ path = "src/labnotes/runner/run_experiment.py"
15
15
  rules = ["mutation"]
16
16
  lines = [47]
17
17
  reason = "mode is validated to fill or adhoc; \"fill\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
18
+
19
+ [[python.exempt]]
20
+ path = "src/labnotes/load_experiment.py"
21
+ rules = ["mutation"]
22
+ lines = [59]
23
+ reason = "summarize_config returns mode fill or adhoc; \"fill\" <= \"adhoc\" is false like ==, and both are interned literals so is equals =="
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes