synodic-client 0.0.1.dev45__tar.gz → 0.0.1.dev47__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 (71) hide show
  1. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/PKG-INFO +2 -2
  2. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/pyproject.toml +2 -4
  3. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/qt.py +1 -2
  4. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/settings.py +4 -2
  5. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/tray.py +4 -1
  6. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/client.py +12 -2
  7. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/resolution.py +0 -32
  8. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/updater.py +34 -0
  9. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_settings.py +8 -2
  10. synodic_client-0.0.1.dev47/tests/unit/test_client_version.py +98 -0
  11. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_resolution.py +1 -59
  12. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_updater.py +41 -0
  13. synodic_client-0.0.1.dev45/synodic_client/_version.py +0 -1
  14. synodic_client-0.0.1.dev45/tests/unit/test_client_version.py +0 -44
  15. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/LICENSE.md +0 -0
  16. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/README.md +0 -0
  17. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/__init__.py +0 -0
  18. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/__main__.py +0 -0
  19. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/__init__.py +0 -0
  20. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/bootstrap.py +0 -0
  21. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/data.py +0 -0
  22. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/icon.py +0 -0
  23. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/init.py +0 -0
  24. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/instance.py +0 -0
  25. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/__init__.py +0 -0
  26. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/action_card.py +0 -0
  27. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/card.py +0 -0
  28. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/install.py +0 -0
  29. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/log_panel.py +0 -0
  30. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/screen.py +0 -0
  31. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/sidebar.py +0 -0
  32. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/spinner.py +0 -0
  33. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/screen/update_banner.py +0 -0
  34. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/theme.py +0 -0
  35. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/update_controller.py +0 -0
  36. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/uri.py +0 -0
  37. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/application/workers.py +0 -0
  38. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/cli.py +0 -0
  39. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/config.py +0 -0
  40. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/logging.py +0 -0
  41. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/protocol.py +0 -0
  42. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/py.typed +0 -0
  43. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/synodic_client/startup.py +0 -0
  44. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/__init__.py +0 -0
  45. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/conftest.py +0 -0
  46. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/__init__.py +0 -0
  47. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/__init__.py +0 -0
  48. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/conftest.py +0 -0
  49. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_action_card.py +0 -0
  50. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_gather_packages.py +0 -0
  51. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_install_preview.py +0 -0
  52. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_log_panel.py +0 -0
  53. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_logging.py +0 -0
  54. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_preview_model.py +0 -0
  55. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_sidebar.py +0 -0
  56. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_tray_window_show.py +0 -0
  57. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_update_banner.py +0 -0
  58. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_update_controller.py +0 -0
  59. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/qt/test_update_feedback.py +0 -0
  60. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_cli.py +0 -0
  61. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_client_updater.py +0 -0
  62. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_config.py +0 -0
  63. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_examples.py +0 -0
  64. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_init.py +0 -0
  65. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_install.py +0 -0
  66. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_uri.py +0 -0
  67. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/test_workers.py +0 -0
  68. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/windows/__init__.py +0 -0
  69. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/windows/conftest.py +0 -0
  70. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/tests/unit/windows/test_protocol.py +0 -0
  71. {synodic_client-0.0.1.dev45 → synodic_client-0.0.1.dev47}/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.dev45
3
+ Version: 0.0.1.dev47
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
@@ -8,7 +8,7 @@ Project-URL: repository, https://github.com/synodic/synodic-client
8
8
  Requires-Python: <3.15,>=3.14
9
9
  Requires-Dist: pyside6>=6.10.2
10
10
  Requires-Dist: packaging>=26.0
11
- Requires-Dist: porringer>=0.2.1.dev72
11
+ Requires-Dist: porringer>=0.2.1.dev73
12
12
  Requires-Dist: qasync>=0.28.0
13
13
  Requires-Dist: velopack>=0.0.1444.dev49733
14
14
  Requires-Dist: typer>=0.24.1
@@ -10,12 +10,12 @@ requires-python = ">=3.14, <3.15"
10
10
  dependencies = [
11
11
  "pyside6>=6.10.2",
12
12
  "packaging>=26.0",
13
- "porringer>=0.2.1.dev72",
13
+ "porringer>=0.2.1.dev73",
14
14
  "qasync>=0.28.0",
15
15
  "velopack>=0.0.1444.dev49733",
16
16
  "typer>=0.24.1",
17
17
  ]
18
- version = "0.0.1.dev45"
18
+ version = "0.0.1.dev47"
19
19
 
20
20
  [project.license]
21
21
  text = "LGPL-3.0-or-later"
@@ -102,8 +102,6 @@ replace-imports-with-any = [
102
102
 
103
103
  [tool.pdm.version]
104
104
  source = "scm"
105
- write_to = "synodic_client/_version.py"
106
- write_template = "__version__ = '{}'\n"
107
105
 
108
106
  [tool.pdm.resolution]
109
107
  allow-prereleases = true
@@ -28,7 +28,6 @@ from synodic_client.resolution import (
28
28
  ResolvedConfig,
29
29
  resolve_config,
30
30
  resolve_update_config,
31
- resolve_version,
32
31
  )
33
32
  from synodic_client.updater import initialize_velopack
34
33
 
@@ -52,7 +51,7 @@ def _init_services(logger: logging.Logger) -> tuple[Client, API, ResolvedConfig]
52
51
 
53
52
  logger.info(
54
53
  'Synodic Client v%s started (channel: %s, source: %s, cached_projects: %d)',
55
- resolve_version(client),
54
+ client.version,
56
55
  update_config.channel.name,
57
56
  update_config.repo_url,
58
57
  len(cached_dirs),
@@ -27,7 +27,6 @@ from PySide6.QtWidgets import (
27
27
  QWidget,
28
28
  )
29
29
 
30
- from synodic_client._version import __version__
31
30
  from synodic_client.application.icon import app_icon
32
31
  from synodic_client.application.screen.card import CardFrame
33
32
  from synodic_client.application.theme import SETTINGS_WINDOW_MIN_SIZE, UPDATE_STATUS_CHECKING_STYLE
@@ -58,16 +57,19 @@ class SettingsWindow(QMainWindow):
58
57
  def __init__(
59
58
  self,
60
59
  config: ResolvedConfig,
60
+ version: str = '',
61
61
  parent: QWidget | None = None,
62
62
  ) -> None:
63
63
  """Initialise the settings window.
64
64
 
65
65
  Args:
66
66
  config: The current resolved configuration snapshot.
67
+ version: The application version string to display.
67
68
  parent: Optional parent widget.
68
69
  """
69
70
  super().__init__(parent)
70
71
  self._config = config
72
+ self._version = version
71
73
  self.setWindowTitle('Synodic Settings')
72
74
  self.setMinimumSize(*SETTINGS_WINDOW_MIN_SIZE)
73
75
  self.setWindowIcon(app_icon())
@@ -93,7 +95,7 @@ class SettingsWindow(QMainWindow):
93
95
  layout.addWidget(self._build_advanced_section())
94
96
  layout.addStretch()
95
97
 
96
- version_label = QLabel(f'Version {__version__}')
98
+ version_label = QLabel(f'Version {self._version}')
97
99
  version_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
98
100
  version_label.setStyleSheet('color: rgba(255, 255, 255, 0.4); font-size: 11px;')
99
101
  layout.addWidget(version_label)
@@ -69,7 +69,10 @@ class TrayScreen:
69
69
  self._build_menu(app, window)
70
70
 
71
71
  # Settings window (created once, shown/hidden on demand)
72
- self._settings_window = SettingsWindow(self._resolve_config())
72
+ self._settings_window = SettingsWindow(
73
+ self._resolve_config(),
74
+ version=str(self._client.version),
75
+ )
73
76
  self._settings_window.settings_changed.connect(self._on_settings_changed)
74
77
 
75
78
  # MainWindow gear button → open settings
@@ -25,11 +25,21 @@ class Client:
25
25
 
26
26
  @property
27
27
  def version(self) -> Version:
28
- """Extracts the version from the installed client.
28
+ """Return the best-known application version.
29
+
30
+ When a Velopack-installed updater is available the authoritative
31
+ version comes from the native binary manifest. Otherwise, the
32
+ Python package metadata version (``importlib.metadata``) is used.
29
33
 
30
34
  Returns:
31
- The version data
35
+ The resolved version.
32
36
  """
37
+ if self._updater is not None:
38
+ try:
39
+ if self._updater.is_installed:
40
+ return self._updater.current_version
41
+ except Exception:
42
+ logger.debug('Failed to query Velopack version, falling back', exc_info=True)
33
43
  try:
34
44
  return Version(importlib.metadata.version(self.distribution))
35
45
  except importlib.metadata.PackageNotFoundError:
@@ -18,8 +18,6 @@ import logging
18
18
  import sys
19
19
  from dataclasses import dataclass
20
20
 
21
- from packaging.version import Version
22
-
23
21
  from synodic_client.config import (
24
22
  UserConfig,
25
23
  load_build_config,
@@ -210,36 +208,6 @@ def resolve_update_config(config: ResolvedConfig) -> UpdateConfig:
210
208
  )
211
209
 
212
210
 
213
- def resolve_version(client: object) -> Version:
214
- """Return the best-known application version.
215
-
216
- When a Velopack-installed ``Updater`` is available the authoritative
217
- version comes from the native binary manifest. Otherwise, the
218
- Python package metadata version (``importlib.metadata``) is used.
219
-
220
- Accepts ``Client`` (or any object with ``.updater`` and ``.version``
221
- attributes) so that :mod:`resolution` does not need a hard import
222
- of :class:`~synodic_client.client.Client` — avoiding a tighter
223
- coupling than necessary.
224
-
225
- Args:
226
- client: The application service facade (typically a
227
- :class:`~synodic_client.client.Client` instance).
228
-
229
- Returns:
230
- The resolved :class:`~packaging.version.Version`.
231
- """
232
- updater = getattr(client, 'updater', None)
233
- if updater is not None:
234
- try:
235
- if updater.is_installed:
236
- return updater.current_version
237
- except Exception:
238
- logger.debug('Failed to query Velopack version, falling back', exc_info=True)
239
-
240
- return getattr(client, 'version', Version('0.0.0'))
241
-
242
-
243
211
  def resolve_enabled_plugins(
244
212
  config: ResolvedConfig,
245
213
  all_plugin_names: list[str],
@@ -32,6 +32,40 @@ GITHUB_REPO_URL = 'https://github.com/synodic/synodic-client'
32
32
  _DEV_RELEASE_TAG = 'dev'
33
33
 
34
34
 
35
+ def pep440_to_semver(version_string: str) -> str:
36
+ """Convert a PEP 440 version string to a SemVer string for Velopack.
37
+
38
+ Velopack requires strict SemVer (``MAJOR.MINOR.PATCH[-pre.N]``) while
39
+ Python tooling produces PEP 440 (e.g. ``0.1.dev47+g799543c``). This
40
+ function bridges the two:
41
+
42
+ * Normalises the base to three components (``0.1`` → ``0.1.0``).
43
+ * Converts ``.devN`` to ``-dev.N``.
44
+ * Strips local segments (``+g…``).
45
+ * Stable versions pass through unchanged (``1.0.0`` → ``1.0.0``).
46
+
47
+ Examples::
48
+
49
+ >>> pep440_to_semver('0.1.dev47+g799543c')
50
+ '0.1.0-dev.47'
51
+ >>> pep440_to_semver('0.1.1.dev3')
52
+ '0.1.1-dev.3'
53
+ >>> pep440_to_semver('1.0.0')
54
+ '1.0.0'
55
+
56
+ Args:
57
+ version_string: A PEP 440 version string.
58
+
59
+ Returns:
60
+ A SemVer-compatible version string.
61
+ """
62
+ v = Version(version_string)
63
+ base = f'{v.major}.{v.minor}.{v.micro}'
64
+ if v.dev is not None:
65
+ return f'{base}-dev.{v.dev}'
66
+ return base
67
+
68
+
35
69
  def github_release_asset_url(repo_url: str, channel: UpdateChannel) -> str:
36
70
  """Convert a GitHub repository URL into a release-asset download URL.
37
71
 
@@ -32,10 +32,10 @@ def _make_config(**overrides: Any) -> ResolvedConfig:
32
32
  return ResolvedConfig(**defaults)
33
33
 
34
34
 
35
- def _make_window(config: ResolvedConfig | None = None) -> SettingsWindow:
35
+ def _make_window(config: ResolvedConfig | None = None, version: str = '0.0.0.test') -> SettingsWindow:
36
36
  """Create a ``SettingsWindow`` without showing it."""
37
37
  cfg = config or _make_config()
38
- window = SettingsWindow(cfg)
38
+ window = SettingsWindow(cfg, version=version)
39
39
  return window
40
40
 
41
41
 
@@ -60,6 +60,12 @@ class TestSettingsWindowConstruction:
60
60
  assert window.minimumWidth() == SETTINGS_WINDOW_MIN_SIZE[0]
61
61
  assert window.minimumHeight() == SETTINGS_WINDOW_MIN_SIZE[1]
62
62
 
63
+ @staticmethod
64
+ def test_version_label_displays_passed_version() -> None:
65
+ """Version label shows the version string passed to the constructor."""
66
+ window = _make_window(version='1.2.3.dev42')
67
+ assert window._version == '1.2.3.dev42'
68
+
63
69
 
64
70
  # ---------------------------------------------------------------------------
65
71
  # sync_from_config
@@ -0,0 +1,98 @@
1
+ """Tests for Client.version property behavior."""
2
+
3
+ import importlib.metadata
4
+ from unittest.mock import MagicMock, PropertyMock, patch
5
+
6
+ from packaging.version import Version
7
+
8
+ from synodic_client.client import Client
9
+
10
+
11
+ class TestClientVersion:
12
+ """Tests for Client.version property."""
13
+
14
+ @staticmethod
15
+ def test_version_from_metadata() -> None:
16
+ """Verify version is retrieved from importlib.metadata when available."""
17
+ client = Client()
18
+
19
+ with patch.object(importlib.metadata, 'version', return_value='1.2.3'):
20
+ version = client.version
21
+
22
+ assert version == Version('1.2.3')
23
+
24
+ @staticmethod
25
+ def test_version_is_version_object() -> None:
26
+ """Verify version property returns a Version object."""
27
+ client = Client()
28
+ version = client.version
29
+
30
+ assert isinstance(version, Version)
31
+
32
+ @staticmethod
33
+ def test_version_dev_format() -> None:
34
+ """Verify dev versions are parsed correctly."""
35
+ client = Client()
36
+ dev_version = '1.0.0.dev5+gabcdef1'
37
+ expected = Version(dev_version)
38
+
39
+ with patch.object(importlib.metadata, 'version', return_value=dev_version):
40
+ version = client.version
41
+
42
+ assert version == expected
43
+ assert version.dev == expected.dev
44
+ assert version.local == expected.local
45
+
46
+
47
+ class TestClientVersionResolution:
48
+ """Verify Client.version prefers Velopack when available."""
49
+
50
+ @staticmethod
51
+ def test_returns_velopack_version_when_installed() -> None:
52
+ """Velopack version is preferred when an installed updater exists."""
53
+ mock_updater = MagicMock()
54
+ mock_updater.is_installed = True
55
+ mock_updater.current_version = Version('5.6.7')
56
+
57
+ client = Client()
58
+ client._updater = mock_updater
59
+
60
+ assert client.version == Version('5.6.7')
61
+
62
+ @staticmethod
63
+ def test_falls_back_when_not_installed() -> None:
64
+ """Metadata version is used when the updater is not Velopack-installed."""
65
+ mock_updater = MagicMock()
66
+ mock_updater.is_installed = False
67
+
68
+ client = Client()
69
+ client._updater = mock_updater
70
+
71
+ with patch.object(importlib.metadata, 'version', return_value='1.0.0.dev1'):
72
+ version = client.version
73
+
74
+ assert version == Version('1.0.0.dev1')
75
+
76
+ @staticmethod
77
+ def test_falls_back_when_no_updater() -> None:
78
+ """Metadata version is used when the updater has not been initialized."""
79
+ client = Client()
80
+
81
+ with patch.object(importlib.metadata, 'version', return_value='2.3.4'):
82
+ version = client.version
83
+
84
+ assert version == Version('2.3.4')
85
+
86
+ @staticmethod
87
+ def test_falls_back_on_exception() -> None:
88
+ """Graceful fallback when querying the updater raises an exception."""
89
+ mock_updater = MagicMock()
90
+ type(mock_updater).is_installed = PropertyMock(side_effect=RuntimeError('boom'))
91
+
92
+ client = Client()
93
+ client._updater = mock_updater
94
+
95
+ with patch.object(importlib.metadata, 'version', return_value='3.0.0'):
96
+ version = client.version
97
+
98
+ assert version == Version('3.0.0')
@@ -2,9 +2,7 @@
2
2
 
3
3
  from pathlib import Path
4
4
  from typing import Any
5
- from unittest.mock import MagicMock, PropertyMock, patch
6
-
7
- from packaging.version import Version
5
+ from unittest.mock import patch
8
6
 
9
7
  from synodic_client.config import BuildConfig, UserConfig
10
8
  from synodic_client.resolution import (
@@ -13,7 +11,6 @@ from synodic_client.resolution import (
13
11
  resolve_config,
14
12
  resolve_enabled_plugins,
15
13
  resolve_update_config,
16
- resolve_version,
17
14
  seed_user_config_from_build,
18
15
  update_user_config,
19
16
  )
@@ -425,58 +422,3 @@ class TestResolveUpdateConfig:
425
422
  result = resolve_update_config(config)
426
423
  assert result.auto_update_interval_minutes == 0
427
424
  assert result.tool_update_interval_minutes == 0
428
-
429
-
430
- # ---------------------------------------------------------------------------
431
- # resolve_version
432
- # ---------------------------------------------------------------------------
433
-
434
-
435
- class TestResolveVersion:
436
- """Tests for resolve_version."""
437
-
438
- @staticmethod
439
- def test_returns_velopack_version_when_installed() -> None:
440
- """Verify the Velopack version is preferred when a manager is present."""
441
- mock_updater = MagicMock()
442
- mock_updater.is_installed = True
443
- mock_updater.current_version = Version('5.6.7')
444
-
445
- mock_client = MagicMock()
446
- mock_client.updater = mock_updater
447
- mock_client.version = Version('1.0.0.dev1')
448
-
449
- assert resolve_version(mock_client) == Version('5.6.7')
450
-
451
- @staticmethod
452
- def test_falls_back_when_not_installed() -> None:
453
- """Verify importlib.metadata version is used when not Velopack-installed."""
454
- mock_updater = MagicMock()
455
- mock_updater.is_installed = False
456
-
457
- mock_client = MagicMock()
458
- mock_client.updater = mock_updater
459
- mock_client.version = Version('1.0.0.dev1')
460
-
461
- assert resolve_version(mock_client) == Version('1.0.0.dev1')
462
-
463
- @staticmethod
464
- def test_falls_back_when_no_updater() -> None:
465
- """Verify importlib.metadata version is used when updater is None."""
466
- mock_client = MagicMock()
467
- mock_client.updater = None
468
- mock_client.version = Version('2.3.4')
469
-
470
- assert resolve_version(mock_client) == Version('2.3.4')
471
-
472
- @staticmethod
473
- def test_falls_back_on_exception() -> None:
474
- """Verify graceful fallback when querying the updater raises."""
475
- mock_updater = MagicMock()
476
- type(mock_updater).is_installed = PropertyMock(side_effect=RuntimeError('boom'))
477
-
478
- mock_client = MagicMock()
479
- mock_client.updater = mock_updater
480
- mock_client.version = Version('3.0.0')
481
-
482
- assert resolve_version(mock_client) == Version('3.0.0')
@@ -16,6 +16,7 @@ from synodic_client.updater import (
16
16
  UpdateState,
17
17
  github_release_asset_url,
18
18
  initialize_velopack,
19
+ pep440_to_semver,
19
20
  platform_suffix,
20
21
  )
21
22
 
@@ -528,3 +529,43 @@ class TestGetVelopackManager:
528
529
 
529
530
  assert updater._current_version == Version('9.8.7')
530
531
  assert updater.current_version == Version('9.8.7')
532
+
533
+
534
+ class TestPep440ToSemver:
535
+ """Tests for pep440_to_semver conversion."""
536
+
537
+ @staticmethod
538
+ def test_dev_version_two_part_base() -> None:
539
+ """PDM SCM 2-part base normalises to 3-part SemVer."""
540
+ assert pep440_to_semver('0.1.dev47+g799543c') == '0.1.0-dev.47'
541
+
542
+ @staticmethod
543
+ def test_dev_version_three_part_base() -> None:
544
+ """Three-part dev version converts correctly."""
545
+ assert pep440_to_semver('0.1.1.dev3') == '0.1.1-dev.3'
546
+
547
+ @staticmethod
548
+ def test_stable_version() -> None:
549
+ """Stable version passes through unchanged."""
550
+ assert pep440_to_semver('1.0.0') == '1.0.0'
551
+
552
+ @staticmethod
553
+ def test_stable_version_two_part() -> None:
554
+ """Two-part stable normalises to three-part."""
555
+ assert pep440_to_semver('1.0') == '1.0.0'
556
+
557
+ @staticmethod
558
+ def test_dev_zero() -> None:
559
+ """Dev release number zero is preserved."""
560
+ assert pep440_to_semver('0.1.0.dev0') == '0.1.0-dev.0'
561
+
562
+ @staticmethod
563
+ def test_local_segment_stripped() -> None:
564
+ """Local segment (+gXXXXXXX) is stripped."""
565
+ assert pep440_to_semver('1.2.3.dev10+gabcdef1') == '1.2.3-dev.10'
566
+
567
+ @staticmethod
568
+ def test_semver_input_passthrough() -> None:
569
+ """SemVer-style pre-release input is normalised via PEP 440."""
570
+ # packaging.version.Version normalises '0.1.0-dev.5' to '0.1.0.dev5'
571
+ assert pep440_to_semver('0.1.0-dev.5') == '0.1.0-dev.5'
@@ -1 +0,0 @@
1
- __version__ = '0.0.1.dev45'
@@ -1,44 +0,0 @@
1
- """Tests for Client.version property behavior."""
2
-
3
- import importlib.metadata
4
- from unittest.mock import patch
5
-
6
- from packaging.version import Version
7
-
8
- from synodic_client.client import Client
9
-
10
-
11
- class TestClientVersion:
12
- """Tests for Client.version property."""
13
-
14
- @staticmethod
15
- def test_version_from_metadata() -> None:
16
- """Verify version is retrieved from importlib.metadata when available."""
17
- client = Client()
18
-
19
- with patch.object(importlib.metadata, 'version', return_value='1.2.3'):
20
- version = client.version
21
-
22
- assert version == Version('1.2.3')
23
-
24
- @staticmethod
25
- def test_version_is_version_object() -> None:
26
- """Verify version property returns a Version object."""
27
- client = Client()
28
- version = client.version
29
-
30
- assert isinstance(version, Version)
31
-
32
- @staticmethod
33
- def test_version_dev_format() -> None:
34
- """Verify dev versions are parsed correctly."""
35
- client = Client()
36
- dev_version = '1.0.0.dev5+gabcdef1'
37
- expected = Version(dev_version)
38
-
39
- with patch.object(importlib.metadata, 'version', return_value=dev_version):
40
- version = client.version
41
-
42
- assert version == expected
43
- assert version.dev == expected.dev
44
- assert version.local == expected.local