synodic-client 0.0.1.dev40__tar.gz → 0.0.1.dev41__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 (67) hide show
  1. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/PKG-INFO +1 -1
  2. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/pyproject.toml +1 -1
  3. synodic_client-0.0.1.dev41/synodic_client/_version.py +1 -0
  4. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/screen.py +59 -61
  5. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_gather_packages.py +24 -44
  6. synodic_client-0.0.1.dev40/synodic_client/_version.py +0 -1
  7. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/LICENSE.md +0 -0
  8. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/README.md +0 -0
  9. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/__init__.py +0 -0
  10. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/__main__.py +0 -0
  11. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/__init__.py +0 -0
  12. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/bootstrap.py +0 -0
  13. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/data.py +0 -0
  14. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/icon.py +0 -0
  15. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/instance.py +0 -0
  16. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/qt.py +0 -0
  17. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/__init__.py +0 -0
  18. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/action_card.py +0 -0
  19. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/card.py +0 -0
  20. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/install.py +0 -0
  21. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/log_panel.py +0 -0
  22. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/settings.py +0 -0
  23. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/sidebar.py +0 -0
  24. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/spinner.py +0 -0
  25. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/tray.py +0 -0
  26. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/screen/update_banner.py +0 -0
  27. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/theme.py +0 -0
  28. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/uri.py +0 -0
  29. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/application/workers.py +0 -0
  30. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/cli.py +0 -0
  31. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/client.py +0 -0
  32. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/config.py +0 -0
  33. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/logging.py +0 -0
  34. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/protocol.py +0 -0
  35. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/py.typed +0 -0
  36. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/resolution.py +0 -0
  37. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/startup.py +0 -0
  38. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/synodic_client/updater.py +0 -0
  39. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/__init__.py +0 -0
  40. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/conftest.py +0 -0
  41. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/__init__.py +0 -0
  42. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/__init__.py +0 -0
  43. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/conftest.py +0 -0
  44. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_action_card.py +0 -0
  45. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_install_preview.py +0 -0
  46. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_log_panel.py +0 -0
  47. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_logging.py +0 -0
  48. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_preview_model.py +0 -0
  49. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_settings.py +0 -0
  50. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_sidebar.py +0 -0
  51. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_tray_window_show.py +0 -0
  52. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_update_banner.py +0 -0
  53. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/qt/test_update_feedback.py +0 -0
  54. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_cli.py +0 -0
  55. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_client_updater.py +0 -0
  56. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_client_version.py +0 -0
  57. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_config.py +0 -0
  58. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_examples.py +0 -0
  59. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_install.py +0 -0
  60. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_resolution.py +0 -0
  61. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_updater.py +0 -0
  62. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_uri.py +0 -0
  63. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/test_workers.py +0 -0
  64. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/windows/__init__.py +0 -0
  65. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/windows/conftest.py +0 -0
  66. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/windows/test_protocol.py +0 -0
  67. {synodic_client-0.0.1.dev40 → synodic_client-0.0.1.dev41}/tests/unit/windows/test_startup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synodic_client
3
- Version: 0.0.1.dev40
3
+ Version: 0.0.1.dev41
4
4
  Author-Email: Synodic Software <contact@synodic.software>
5
5
  License: LGPL-3.0-or-later
6
6
  Project-URL: homepage, https://github.com/synodic/synodic-client
@@ -15,7 +15,7 @@ dependencies = [
15
15
  "velopack>=0.0.1444.dev49733",
16
16
  "typer>=0.24.1",
17
17
  ]
18
- version = "0.0.1.dev40"
18
+ version = "0.0.1.dev41"
19
19
 
20
20
  [project.license]
21
21
  text = "LGPL-3.0-or-later"
@@ -0,0 +1 @@
1
+ __version__ = '0.0.1.dev41'
@@ -8,6 +8,7 @@ from pathlib import Path
8
8
 
9
9
  from porringer.api import API
10
10
  from porringer.backend.builder import Builder
11
+ from porringer.backend.command.core.discovery import DiscoveredPlugins
11
12
  from porringer.core.plugin_schema.plugin_manager import PluginManager
12
13
  from porringer.core.plugin_schema.project_environment import ProjectEnvironment
13
14
  from porringer.schema import (
@@ -1158,26 +1159,29 @@ class ToolsView(QWidget):
1158
1159
  return None
1159
1160
  return {name for name, chip in self._filter_chips.items() if chip.isChecked()}
1160
1161
 
1161
- def _apply_filter(self, _text: str | None = None) -> None:
1162
- """Show/hide section widgets based on search text and active chips.
1162
+ @staticmethod
1163
+ def _is_plugin_active(plugin_name: str, active: set[str] | None) -> bool:
1164
+ """Return whether *plugin_name* passes the chip filter."""
1165
+ return active is None or plugin_name in active
1163
1166
 
1164
- A single pass walks ``_section_widgets`` tracking the current
1165
- plugin and kind. Visibility rules:
1167
+ @staticmethod
1168
+ def _finalise_provider(
1169
+ provider: PluginProviderHeader | None,
1170
+ has_visible: bool,
1171
+ ) -> bool:
1172
+ """Set provider visibility and return whether it had visible children."""
1173
+ if provider is not None:
1174
+ provider.setVisible(has_visible)
1175
+ return has_visible
1166
1176
 
1167
- * **PluginProviderHeader** visible when its plugin is in the
1168
- active chip set **and** at least one child row matches the
1169
- search text.
1170
- * **PluginRow** — visible when its plugin is active **and** its
1171
- package name or plugin name contains the search text.
1172
- * **ProjectChildRow** — follows its parent :class:`PluginRow`.
1173
- * **PluginKindHeader** — visible when at least one child
1174
- provider in its kind group is visible.
1177
+ def _apply_filter(self, _text: str | None = None) -> None:
1178
+ """Show/hide section widgets based on search text and active chips.
1175
1179
 
1176
- After the pass, kind headers with no visible children are hidden.
1180
+ Delegates to :meth:`_is_plugin_active` for chip matching and
1181
+ :meth:`_finalise_provider` for provider visibility bookkeeping.
1177
1182
  """
1178
1183
  query = self._search_input.text().strip().lower()
1179
1184
  active = self._active_chip_plugins()
1180
- all_active = active is None # None → no chips yet, show all
1181
1185
 
1182
1186
  current_kind_header: PluginKindHeader | None = None
1183
1187
  kind_has_visible = False
@@ -1187,13 +1191,8 @@ class ToolsView(QWidget):
1187
1191
 
1188
1192
  for widget in self._section_widgets:
1189
1193
  if isinstance(widget, PluginKindHeader):
1190
- # Finalise previous kind
1194
+ kind_has_visible |= self._finalise_provider(current_provider, provider_has_visible_child)
1191
1195
  if current_kind_header is not None:
1192
- # Finalise last provider of previous kind
1193
- if current_provider is not None:
1194
- current_provider.setVisible(provider_has_visible_child)
1195
- if provider_has_visible_child:
1196
- kind_has_visible = True
1197
1196
  current_kind_header.setVisible(kind_has_visible)
1198
1197
 
1199
1198
  current_kind_header = widget
@@ -1202,31 +1201,20 @@ class ToolsView(QWidget):
1202
1201
  provider_has_visible_child = False
1203
1202
 
1204
1203
  elif isinstance(widget, PluginProviderHeader):
1205
- # Finalise previous provider
1206
- if current_provider is not None:
1207
- current_provider.setVisible(provider_has_visible_child)
1208
- if provider_has_visible_child:
1209
- kind_has_visible = True
1204
+ kind_has_visible |= self._finalise_provider(current_provider, provider_has_visible_child)
1210
1205
 
1211
1206
  current_provider = widget
1212
1207
  provider_has_visible_child = False
1213
- plugin_name = widget._plugin_name
1214
- plugin_active = all_active or (active is not None and plugin_name in active)
1215
-
1216
- if not plugin_active:
1217
- # Entire provider hidden
1208
+ if not self._is_plugin_active(widget._plugin_name, active):
1218
1209
  widget.setVisible(False)
1219
- provider_has_visible_child = False
1220
1210
 
1221
1211
  elif isinstance(widget, PluginRow):
1222
- plugin_name = widget._plugin_name
1223
- plugin_active = all_active or (active is not None and plugin_name in active)
1224
- if not plugin_active:
1212
+ if not self._is_plugin_active(widget._plugin_name, active):
1225
1213
  widget.setVisible(False)
1226
1214
  parent_row_visible = False
1227
1215
  continue
1228
1216
 
1229
- name_match = not query or query in widget._package_name.lower() or query in plugin_name.lower()
1217
+ name_match = not query or query in widget._package_name.lower() or query in widget._plugin_name.lower()
1230
1218
  widget.setVisible(name_match)
1231
1219
  parent_row_visible = name_match
1232
1220
  if name_match:
@@ -1236,10 +1224,7 @@ class ToolsView(QWidget):
1236
1224
  widget.setVisible(parent_row_visible)
1237
1225
 
1238
1226
  # Finalise last provider and kind
1239
- if current_provider is not None:
1240
- current_provider.setVisible(provider_has_visible_child)
1241
- if provider_has_visible_child:
1242
- kind_has_visible = True
1227
+ kind_has_visible |= self._finalise_provider(current_provider, provider_has_visible_child)
1243
1228
  if current_kind_header is not None:
1244
1229
  current_kind_header.setVisible(kind_has_visible)
1245
1230
 
@@ -1834,6 +1819,7 @@ class ProjectsView(QWidget):
1834
1819
  if self._coordinator is not None:
1835
1820
  snapshot = await self._coordinator.refresh()
1836
1821
  results = snapshot.validated_directories
1822
+ discovered = snapshot.discovered
1837
1823
  else:
1838
1824
  loop = asyncio.get_running_loop()
1839
1825
  results = await loop.run_in_executor(
@@ -1843,6 +1829,7 @@ class ProjectsView(QWidget):
1843
1829
  check_manifest=True,
1844
1830
  ),
1845
1831
  )
1832
+ discovered = None
1846
1833
 
1847
1834
  directories: list[tuple[Path, str, bool]] = []
1848
1835
  current_paths: set[Path] = set()
@@ -1854,32 +1841,12 @@ class ProjectsView(QWidget):
1854
1841
  current_paths.add(path)
1855
1842
 
1856
1843
  # Remove widgets for directories no longer in cache
1857
- for path in list(self._widgets):
1858
- if path not in current_paths:
1859
- widget = self._widgets.pop(path)
1860
- self._stack.removeWidget(widget)
1861
- widget.reset()
1862
- widget.deleteLater()
1844
+ self._remove_stale_widgets(current_paths)
1863
1845
 
1864
1846
  # Grab pre-discovered plugins so each widget can skip redundant discovery
1865
- discovered = snapshot.discovered if self._coordinator is not None else None
1866
1847
 
1867
1848
  # Create new widgets for new directories
1868
- for path, _name, valid in directories:
1869
- if path not in self._widgets and valid:
1870
- widget = SetupPreviewWidget(
1871
- self._porringer,
1872
- self,
1873
- show_close=False,
1874
- config=self._config,
1875
- )
1876
- widget._discovered_plugins = discovered
1877
- widget.install_finished.connect(self._on_install_finished)
1878
- widget.phase_changed.connect(
1879
- lambda phase, p=path: self._on_widget_phase_changed(p, phase),
1880
- )
1881
- self._widgets[path] = widget
1882
- self._stack.addWidget(widget)
1849
+ self._create_directory_widgets(directories, discovered)
1883
1850
 
1884
1851
  # Rebuild sidebar
1885
1852
  self._sidebar.set_directories(directories)
@@ -1909,6 +1876,37 @@ class ProjectsView(QWidget):
1909
1876
 
1910
1877
  # --- Event handlers ---
1911
1878
 
1879
+ def _remove_stale_widgets(self, current_paths: set[Path]) -> None:
1880
+ """Remove stacked widgets for directories no longer in the cache."""
1881
+ for path in list(self._widgets):
1882
+ if path not in current_paths:
1883
+ widget = self._widgets.pop(path)
1884
+ self._stack.removeWidget(widget)
1885
+ widget.reset()
1886
+ widget.deleteLater()
1887
+
1888
+ def _create_directory_widgets(
1889
+ self,
1890
+ directories: list[tuple[Path, str, bool]],
1891
+ discovered: DiscoveredPlugins | None,
1892
+ ) -> None:
1893
+ """Create :class:`SetupPreviewWidget` instances for new valid directories."""
1894
+ for path, _name, valid in directories:
1895
+ if path not in self._widgets and valid:
1896
+ widget = SetupPreviewWidget(
1897
+ self._porringer,
1898
+ self,
1899
+ show_close=False,
1900
+ config=self._config,
1901
+ )
1902
+ widget._discovered_plugins = discovered
1903
+ widget.install_finished.connect(self._on_install_finished)
1904
+ widget.phase_changed.connect(
1905
+ lambda phase, p=path: self._on_widget_phase_changed(p, phase),
1906
+ )
1907
+ self._widgets[path] = widget
1908
+ self._stack.addWidget(widget)
1909
+
1912
1910
  def _on_selection_changed(self, path: Path) -> None:
1913
1911
  """Handle sidebar selection — switch the stacked widget."""
1914
1912
  widget = self._widgets.get(path)
@@ -6,12 +6,30 @@ import asyncio
6
6
  from pathlib import Path
7
7
  from unittest.mock import AsyncMock, MagicMock
8
8
 
9
+ from packaging.version import Version
9
10
  from porringer.core.schema import Package, PackageRelation, PackageRelationKind
10
11
  from porringer.schema import ManifestDirectory
11
-
12
- from synodic_client.application.screen.screen import PackageEntry, ProjectChildRow, ToolsView
12
+ from porringer.schema.plugin import PluginInfo, PluginKind
13
+ from PySide6.QtWidgets import QLabel, QPushButton
14
+
15
+ from synodic_client.application.screen.screen import (
16
+ FilterChip,
17
+ PackageEntry,
18
+ PluginKindHeader,
19
+ PluginProviderHeader,
20
+ PluginRow,
21
+ PluginRowData,
22
+ ProjectChildRow,
23
+ ProjectInstance,
24
+ ToolsView,
25
+ )
13
26
  from synodic_client.resolution import ResolvedConfig
14
27
 
28
+ # Named constants for expected counts (avoids PLR2004)
29
+ _EXPECTED_PROJECT_INSTANCES = 2
30
+ _EXPECTED_VISIBLE_ROWS_ALL = 3
31
+ _EXPECTED_VISIBLE_ROWS_PIPX = 2
32
+
15
33
 
16
34
  def _make_config() -> ResolvedConfig:
17
35
  """Build a minimal ResolvedConfig for tests."""
@@ -429,7 +447,7 @@ class TestBuildDisplayPackages:
429
447
 
430
448
  assert len(result) == 1
431
449
  pkg = result[0]
432
- assert len(pkg.project_instances) == 2
450
+ assert len(pkg.project_instances) == _EXPECTED_PROJECT_INSTANCES
433
451
  labels = {pi.project_label for pi in pkg.project_instances}
434
452
  assert labels == {'project-a', 'project-b'}
435
453
 
@@ -479,8 +497,6 @@ class TestProjectChildRow:
479
497
 
480
498
  @staticmethod
481
499
  def _make_instance(*, transitive: bool = False) -> ProjectChildRow:
482
- from synodic_client.application.screen.screen import ProjectInstance
483
-
484
500
  return ProjectChildRow(
485
501
  ProjectInstance(
486
502
  project_label='periapsis',
@@ -498,8 +514,6 @@ class TestProjectChildRow:
498
514
  row.navigate_to_project.connect(spy)
499
515
 
500
516
  # Find the navigate button (→)
501
- from PySide6.QtWidgets import QPushButton
502
-
503
517
  nav_btns = [w for w in row.findChildren(QPushButton) if w.text() == '\u2192']
504
518
  assert len(nav_btns) == 1
505
519
  nav_btns[0].click()
@@ -518,8 +532,6 @@ class TestProjectChildRow:
518
532
  assert len(labels) == 0
519
533
 
520
534
 
521
- from PySide6.QtWidgets import QLabel
522
-
523
535
  # ---------------------------------------------------------------------------
524
536
  # FilterChip
525
537
  # ---------------------------------------------------------------------------
@@ -531,8 +543,6 @@ class TestFilterChip:
531
543
  @staticmethod
532
544
  def test_chip_starts_checked() -> None:
533
545
  """Filter chips start in the checked (active) state."""
534
- from synodic_client.application.screen.screen import FilterChip
535
-
536
546
  chip = FilterChip('pipx')
537
547
  assert chip.isChecked()
538
548
  assert chip.text() == 'pipx'
@@ -540,8 +550,6 @@ class TestFilterChip:
540
550
  @staticmethod
541
551
  def test_toggling_emits_signal() -> None:
542
552
  """Toggling a chip emits the plugin name and new state."""
543
- from synodic_client.application.screen.screen import FilterChip
544
-
545
553
  chip = FilterChip('uv')
546
554
  spy = MagicMock()
547
555
  chip.toggled_with_name.connect(spy)
@@ -552,8 +560,6 @@ class TestFilterChip:
552
560
  @staticmethod
553
561
  def test_recheck_emits_true() -> None:
554
562
  """Re-checking a chip emits True."""
555
- from synodic_client.application.screen.screen import FilterChip
556
-
557
563
  chip = FilterChip('pip')
558
564
  spy = MagicMock()
559
565
  chip.setChecked(False)
@@ -589,16 +595,6 @@ class TestSearchFilter:
589
595
  ProviderHeader(uv)
590
596
  PluginRow(mypy, plugin=uv)
591
597
  """
592
- from packaging.version import Version
593
- from porringer.schema.plugin import PluginInfo, PluginKind
594
-
595
- from synodic_client.application.screen.screen import (
596
- PluginKindHeader,
597
- PluginProviderHeader,
598
- PluginRow,
599
- PluginRowData,
600
- )
601
-
602
598
  kind_hdr = PluginKindHeader(PluginKind.TOOL)
603
599
  view._section_widgets.append(kind_hdr)
604
600
  view._container_layout.insertWidget(0, kind_hdr)
@@ -639,8 +635,6 @@ class TestSearchFilter:
639
635
 
640
636
  view._search_input.setText('ruff')
641
637
 
642
- from synodic_client.application.screen.screen import PluginRow
643
-
644
638
  visible_rows = [w for w in view._section_widgets if isinstance(w, PluginRow) and not w.isHidden()]
645
639
  assert len(visible_rows) == 1
646
640
  assert visible_rows[0]._package_name == 'ruff'
@@ -653,10 +647,8 @@ class TestSearchFilter:
653
647
  view._search_input.setText('ruff')
654
648
  view._search_input.setText('')
655
649
 
656
- from synodic_client.application.screen.screen import PluginRow
657
-
658
650
  visible_rows = [w for w in view._section_widgets if isinstance(w, PluginRow) and not w.isHidden()]
659
- assert len(visible_rows) == 3
651
+ assert len(visible_rows) == _EXPECTED_VISIBLE_ROWS_ALL
660
652
 
661
653
  def test_chip_deselection_hides_plugin(self) -> None:
662
654
  """Deselecting a chip hides all rows from that plugin."""
@@ -665,8 +657,6 @@ class TestSearchFilter:
665
657
 
666
658
  view._filter_chips['pipx'].setChecked(False)
667
659
 
668
- from synodic_client.application.screen.screen import PluginRow
669
-
670
660
  visible_rows = [w for w in view._section_widgets if isinstance(w, PluginRow) and not w.isHidden()]
671
661
  assert len(visible_rows) == 1
672
662
  assert visible_rows[0]._package_name == 'mypy'
@@ -679,10 +669,8 @@ class TestSearchFilter:
679
669
  view._filter_chips['pipx'].setChecked(False)
680
670
  view._filter_chips['pipx'].setChecked(True)
681
671
 
682
- from synodic_client.application.screen.screen import PluginRow
683
-
684
672
  visible_rows = [w for w in view._section_widgets if isinstance(w, PluginRow) and not w.isHidden()]
685
- assert len(visible_rows) == 3
673
+ assert len(visible_rows) == _EXPECTED_VISIBLE_ROWS_ALL
686
674
 
687
675
  def test_search_plus_chip_filter(self) -> None:
688
676
  """Search and chip filtering compose — only matching rows in active plugins survive."""
@@ -692,8 +680,6 @@ class TestSearchFilter:
692
680
  view._filter_chips['uv'].setChecked(False)
693
681
  view._search_input.setText('pdm')
694
682
 
695
- from synodic_client.application.screen.screen import PluginRow
696
-
697
683
  visible_rows = [w for w in view._section_widgets if isinstance(w, PluginRow) and not w.isHidden()]
698
684
  assert len(visible_rows) == 1
699
685
  assert visible_rows[0]._package_name == 'pdm'
@@ -707,8 +693,6 @@ class TestSearchFilter:
707
693
  view._filter_chips['pipx'].setChecked(False)
708
694
  view._filter_chips['uv'].setChecked(False)
709
695
 
710
- from synodic_client.application.screen.screen import PluginKindHeader
711
-
712
696
  hidden_kinds = [w for w in view._section_widgets if isinstance(w, PluginKindHeader) and w.isHidden()]
713
697
  assert len(hidden_kinds) == 1
714
698
 
@@ -719,8 +703,6 @@ class TestSearchFilter:
719
703
 
720
704
  view._search_input.setText('mypy')
721
705
 
722
- from synodic_client.application.screen.screen import PluginProviderHeader
723
-
724
706
  visible_providers = [
725
707
  w for w in view._section_widgets if isinstance(w, PluginProviderHeader) and not w.isHidden()
726
708
  ]
@@ -734,10 +716,8 @@ class TestSearchFilter:
734
716
 
735
717
  view._search_input.setText('pipx')
736
718
 
737
- from synodic_client.application.screen.screen import PluginRow
738
-
739
719
  visible_rows = [w for w in view._section_widgets if isinstance(w, PluginRow) and not w.isHidden()]
740
- assert len(visible_rows) == 2
720
+ assert len(visible_rows) == _EXPECTED_VISIBLE_ROWS_PIPX
741
721
  names = {w._package_name for w in visible_rows}
742
722
  assert names == {'ruff', 'pdm'}
743
723
 
@@ -1 +0,0 @@
1
- __version__ = '0.0.1.dev40'