synodic-client 0.0.1.dev23__tar.gz → 0.0.1.dev25__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 (49) hide show
  1. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/PKG-INFO +1 -1
  2. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/pyproject.toml +1 -1
  3. synodic_client-0.0.1.dev25/synodic_client/_version.py +1 -0
  4. synodic_client-0.0.1.dev25/synodic_client/application/bootstrap.py +39 -0
  5. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/screen/tray.py +12 -4
  6. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/cli.py +2 -1
  7. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/logging.py +9 -19
  8. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/updater.py +8 -5
  9. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/qt/test_logging.py +10 -10
  10. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_cli.py +3 -3
  11. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_updater.py +3 -3
  12. synodic_client-0.0.1.dev23/synodic_client/_version.py +0 -1
  13. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/LICENSE.md +0 -0
  14. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/README.md +0 -0
  15. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/__init__.py +0 -0
  16. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/__main__.py +0 -0
  17. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/__init__.py +0 -0
  18. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/icon.py +0 -0
  19. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/instance.py +0 -0
  20. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/qt.py +0 -0
  21. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/screen/__init__.py +0 -0
  22. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/screen/install.py +0 -0
  23. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/screen/log_panel.py +0 -0
  24. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/screen/screen.py +0 -0
  25. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/theme.py +0 -0
  26. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/application/uri.py +0 -0
  27. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/client.py +0 -0
  28. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/config.py +0 -0
  29. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/protocol.py +0 -0
  30. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/py.typed +0 -0
  31. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/synodic_client/resolution.py +0 -0
  32. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/__init__.py +0 -0
  33. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/conftest.py +0 -0
  34. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/__init__.py +0 -0
  35. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/qt/__init__.py +0 -0
  36. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/qt/conftest.py +0 -0
  37. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/qt/test_install_preview.py +0 -0
  38. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/qt/test_log_panel.py +0 -0
  39. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_client_updater.py +0 -0
  40. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_client_version.py +0 -0
  41. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_config.py +0 -0
  42. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_examples.py +0 -0
  43. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_install.py +0 -0
  44. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_install_preview.py +0 -0
  45. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_resolution.py +0 -0
  46. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/test_uri.py +0 -0
  47. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/windows/__init__.py +0 -0
  48. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/windows/conftest.py +0 -0
  49. {synodic_client-0.0.1.dev23 → synodic_client-0.0.1.dev25}/tests/unit/windows/test_protocol.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: synodic_client
3
- Version: 0.0.1.dev23
3
+ Version: 0.0.1.dev25
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
@@ -14,7 +14,7 @@ dependencies = [
14
14
  "velopack>=0.0.1369.dev7516",
15
15
  "typer>=0.23.1",
16
16
  ]
17
- version = "0.0.1.dev23"
17
+ version = "0.0.1.dev25"
18
18
 
19
19
  [project.license]
20
20
  text = "LGPL-3.0-or-later"
@@ -0,0 +1 @@
1
+ __version__ = '0.0.1.dev25'
@@ -0,0 +1,39 @@
1
+ """Bootstrap entry point for PyInstaller builds.
2
+
3
+ Runs the lightweight startup preamble — logging, Velopack hooks, and
4
+ protocol registration — **before** importing heavy modules (PySide6,
5
+ porringer). Velopack's install/uninstall/update hooks have strict
6
+ timeouts (15–30 s) and must complete before the process is killed.
7
+
8
+ Import order matters:
9
+ 1. stdlib + config (pure-Python, fast)
10
+ 2. configure_logging() — now Qt-free
11
+ 3. initialize_velopack() — hooks run with logging active
12
+ 4. register_protocol() — stdlib only
13
+ 5. import qt.application — PySide6 / porringer loaded here
14
+ """
15
+
16
+ import sys
17
+
18
+ from synodic_client.config import set_dev_mode
19
+ from synodic_client.logging import configure_logging
20
+ from synodic_client.protocol import register_protocol
21
+ from synodic_client.updater import initialize_velopack
22
+
23
+ _PROTOCOL_SCHEME = 'synodic'
24
+
25
+ # Parse --dev flag early so logging uses the right filename.
26
+ _dev_mode = '--dev' in sys.argv[1:]
27
+ set_dev_mode(_dev_mode)
28
+
29
+ configure_logging()
30
+ initialize_velopack()
31
+
32
+ if not _dev_mode:
33
+ register_protocol(sys.executable)
34
+
35
+ # Heavy imports happen here — PySide6, porringer, etc.
36
+ from synodic_client.application.qt import application # noqa: E402
37
+
38
+ _uri = next((a for a in sys.argv[1:] if a.lower().startswith(f'{_PROTOCOL_SCHEME}://')), None)
39
+ application(uri=_uri, dev_mode=_dev_mode)
@@ -6,8 +6,8 @@ from pathlib import Path
6
6
 
7
7
  from porringer.api import API
8
8
  from porringer.schema import SetupParameters, SyncStrategy
9
- from PySide6.QtCore import QThread, QTimer, Signal
10
- from PySide6.QtGui import QAction
9
+ from PySide6.QtCore import QThread, QTimer, QUrl, Signal
10
+ from PySide6.QtGui import QAction, QDesktopServices
11
11
  from PySide6.QtWidgets import (
12
12
  QApplication,
13
13
  QDialog,
@@ -29,7 +29,7 @@ from synodic_client.application.screen.screen import MainWindow
29
29
  from synodic_client.application.theme import UPDATE_SOURCE_DIALOG_MIN_WIDTH
30
30
  from synodic_client.client import Client
31
31
  from synodic_client.config import GlobalConfiguration
32
- from synodic_client.logging import open_log
32
+ from synodic_client.logging import log_path
33
33
  from synodic_client.resolution import resolve_config, resolve_enabled_plugins, resolve_update_config, update_and_resolve
34
34
  from synodic_client.updater import GITHUB_REPO_URL, UpdateChannel, UpdateInfo
35
35
 
@@ -281,7 +281,7 @@ class TrayScreen:
281
281
  self.settings_menu.addSeparator()
282
282
 
283
283
  self.open_log_action = QAction('Open Log...', self.settings_menu)
284
- self.open_log_action.triggered.connect(open_log)
284
+ self.open_log_action.triggered.connect(self._open_log)
285
285
  self.settings_menu.addAction(self.open_log_action)
286
286
 
287
287
  self.menu.addSeparator()
@@ -294,6 +294,14 @@ class TrayScreen:
294
294
 
295
295
  # -- Config helpers --
296
296
 
297
+ @staticmethod
298
+ def _open_log() -> None:
299
+ """Open the log file in the system's default editor."""
300
+ path = log_path()
301
+ if not path.exists():
302
+ path.touch()
303
+ QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))
304
+
297
305
  def _resolve_config(self) -> GlobalConfiguration:
298
306
  """Return the injected config or resolve from disk."""
299
307
  if self._config is not None:
@@ -5,7 +5,6 @@ from typing import Annotated
5
5
  import typer
6
6
 
7
7
  from synodic_client import __version__
8
- from synodic_client.application.qt import application
9
8
 
10
9
  app = typer.Typer(
11
10
  name='synodic-c',
@@ -38,4 +37,6 @@ def main(
38
37
  ] = False,
39
38
  ) -> None:
40
39
  """Launch the Synodic Client GUI application."""
40
+ from synodic_client.application.qt import application # noqa: PLC0415
41
+
41
42
  application(uri=uri, dev_mode=dev)
@@ -1,7 +1,6 @@
1
1
  """Centralised logging configuration for the Synodic Client.
2
2
 
3
- Provides a rotating file handler with eager flushing and a helper to open
4
- the current log file in the system's default editor.
3
+ Provides a rotating file handler with eager flushing.
5
4
  """
6
5
 
7
6
  import logging
@@ -9,9 +8,6 @@ import tempfile
9
8
  from logging.handlers import RotatingFileHandler
10
9
  from pathlib import Path
11
10
 
12
- from PySide6.QtCore import QUrl
13
- from PySide6.QtGui import QDesktopServices
14
-
15
11
  from synodic_client.config import is_dev_mode
16
12
 
17
13
  _LOG_FILENAME = 'synodic.log'
@@ -52,7 +48,15 @@ def configure_logging() -> None:
52
48
  Attaches a :class:`EagerRotatingFileHandler` to the ``synodic_client``
53
49
  and ``porringer`` loggers and configures :func:`logging.basicConfig`
54
50
  for ``INFO`` level output on *stderr*.
51
+
52
+ Safe to call more than once — subsequent calls are no-ops.
55
53
  """
54
+ app_logger = logging.getLogger('synodic_client')
55
+
56
+ # Guard: skip if already configured (e.g. by bootstrap.py)
57
+ if any(isinstance(h, EagerRotatingFileHandler) for h in app_logger.handlers):
58
+ return
59
+
56
60
  logging.basicConfig(level=logging.INFO)
57
61
 
58
62
  handler = EagerRotatingFileHandler(
@@ -63,22 +67,8 @@ def configure_logging() -> None:
63
67
  )
64
68
  handler.setFormatter(logging.Formatter(_FORMAT))
65
69
 
66
- app_logger = logging.getLogger('synodic_client')
67
70
  app_logger.addHandler(handler)
68
71
 
69
72
  porringer_logger = logging.getLogger('porringer')
70
73
  porringer_logger.addHandler(handler)
71
74
  porringer_logger.setLevel(logging.INFO)
72
-
73
-
74
- def open_log() -> None:
75
- """Open the log file in the system's default editor.
76
-
77
- Creates an empty file if one does not yet exist so that the OS always
78
- has something to open.
79
- """
80
- path = log_path()
81
- if not path.exists():
82
- path.touch()
83
-
84
- QDesktopServices.openUrl(QUrl.fromLocalFile(str(path)))
@@ -33,7 +33,7 @@ _PLATFORM_SUFFIXES: dict[str, str] = {
33
33
  }
34
34
 
35
35
 
36
- def _platform_suffix() -> str:
36
+ def platform_suffix() -> str:
37
37
  """Return the Velopack channel suffix for the current platform."""
38
38
  try:
39
39
  return _PLATFORM_SUFFIXES[sys.platform]
@@ -104,8 +104,8 @@ class UpdateConfig:
104
104
  so each OS has its own release manifest and nupkg files.
105
105
  """
106
106
  base = 'dev' if self.channel == UpdateChannel.DEVELOPMENT else 'stable'
107
- platform_suffix = _platform_suffix()
108
- return f'{base}-{platform_suffix}'
107
+ suffix = platform_suffix()
108
+ return f'{base}-{suffix}'
109
109
 
110
110
 
111
111
  class Updater:
@@ -393,13 +393,16 @@ def initialize_velopack() -> None:
393
393
  before any UI is shown. Velopack may need to perform cleanup or apply
394
394
  pending updates.
395
395
 
396
- On Windows, the uninstall hook removes the ``synodic://`` URI protocol.
397
396
  Protocol registration happens on every app launch (see ``qt.application``).
397
+
398
+ .. note::
399
+
400
+ The SDK's callback hooks only accept ``PyCFunction`` — add an
401
+ uninstall hook here when that is fixed upstream.
398
402
  """
399
403
  logger.info('Initializing Velopack (exe=%s)', sys.executable)
400
404
  try:
401
405
  app = velopack.App()
402
- app.on_before_uninstall_fast_callback(_on_before_uninstall)
403
406
  app.run()
404
407
  logger.info('Velopack initialized successfully')
405
408
  except Exception as e:
@@ -5,12 +5,12 @@ import tempfile
5
5
  from pathlib import Path
6
6
  from unittest.mock import patch
7
7
 
8
+ from synodic_client.application.screen.tray import TrayScreen
8
9
  from synodic_client.config import set_dev_mode
9
10
  from synodic_client.logging import (
10
11
  EagerRotatingFileHandler,
11
12
  configure_logging,
12
13
  log_path,
13
- open_log,
14
14
  )
15
15
 
16
16
 
@@ -106,31 +106,31 @@ class TestConfigureLogging:
106
106
 
107
107
 
108
108
  class TestOpenLog:
109
- """Tests for open_log()."""
109
+ """Tests for TrayScreen._open_log()."""
110
110
 
111
111
  @staticmethod
112
112
  def test_creates_file_if_missing(tmp_path: Path) -> None:
113
- """open_log() should create the log file when it does not exist."""
113
+ """_open_log() should create the log file when it does not exist."""
114
114
  log_file = tmp_path / 'synodic.log'
115
115
  assert not log_file.exists()
116
116
 
117
117
  with (
118
- patch('synodic_client.logging.log_path', return_value=log_file),
119
- patch('synodic_client.logging.QDesktopServices') as mock_ds,
118
+ patch('synodic_client.application.screen.tray.log_path', return_value=log_file),
119
+ patch('synodic_client.application.screen.tray.QDesktopServices') as mock_ds,
120
120
  ):
121
- open_log()
121
+ TrayScreen._open_log()
122
122
  assert log_file.exists()
123
123
  mock_ds.openUrl.assert_called_once()
124
124
 
125
125
  @staticmethod
126
126
  def test_opens_existing_file(tmp_path: Path) -> None:
127
- """open_log() should open an existing log file without error."""
127
+ """_open_log() should open an existing log file without error."""
128
128
  log_file = tmp_path / 'synodic.log'
129
129
  log_file.write_text('existing content', encoding='utf-8')
130
130
 
131
131
  with (
132
- patch('synodic_client.logging.log_path', return_value=log_file),
133
- patch('synodic_client.logging.QDesktopServices') as mock_ds,
132
+ patch('synodic_client.application.screen.tray.log_path', return_value=log_file),
133
+ patch('synodic_client.application.screen.tray.QDesktopServices') as mock_ds,
134
134
  ):
135
- open_log()
135
+ TrayScreen._open_log()
136
136
  mock_ds.openUrl.assert_called_once()
@@ -33,7 +33,7 @@ class TestCli:
33
33
  @staticmethod
34
34
  def test_launches_application_without_uri() -> None:
35
35
  """Verify invoking with no args calls application(uri=None, dev_mode=False)."""
36
- with patch('synodic_client.cli.application') as mock_app:
36
+ with patch('synodic_client.application.qt.application') as mock_app:
37
37
  result = runner.invoke(app, [])
38
38
  assert result.exit_code == 0
39
39
  mock_app.assert_called_once_with(uri=None, dev_mode=False)
@@ -42,7 +42,7 @@ class TestCli:
42
42
  def test_launches_application_with_uri() -> None:
43
43
  """Verify invoking with a URI passes it to application()."""
44
44
  test_uri = 'synodic://install?manifest=https://example.com/foo.json'
45
- with patch('synodic_client.cli.application') as mock_app:
45
+ with patch('synodic_client.application.qt.application') as mock_app:
46
46
  result = runner.invoke(app, [test_uri])
47
47
  assert result.exit_code == 0
48
48
  mock_app.assert_called_once_with(uri=test_uri, dev_mode=False)
@@ -50,7 +50,7 @@ class TestCli:
50
50
  @staticmethod
51
51
  def test_launches_application_with_dev_flag() -> None:
52
52
  """Verify --dev flag sets dev_mode=True."""
53
- with patch('synodic_client.cli.application') as mock_app:
53
+ with patch('synodic_client.application.qt.application') as mock_app:
54
54
  result = runner.invoke(app, ['--dev'])
55
55
  assert result.exit_code == 0
56
56
  mock_app.assert_called_once_with(uri=None, dev_mode=True)
@@ -14,8 +14,8 @@ from synodic_client.updater import (
14
14
  UpdateInfo,
15
15
  Updater,
16
16
  UpdateState,
17
- _platform_suffix,
18
17
  initialize_velopack,
18
+ platform_suffix,
19
19
  )
20
20
 
21
21
 
@@ -127,13 +127,13 @@ class TestUpdateConfig:
127
127
  def test_channel_name_stable() -> None:
128
128
  """Verify STABLE channel returns platform-specific 'stable' name."""
129
129
  config = UpdateConfig(channel=UpdateChannel.STABLE)
130
- assert config.channel_name == f'stable-{_platform_suffix()}'
130
+ assert config.channel_name == f'stable-{platform_suffix()}'
131
131
 
132
132
  @staticmethod
133
133
  def test_channel_name_development() -> None:
134
134
  """Verify DEVELOPMENT channel returns platform-specific 'dev' name."""
135
135
  config = UpdateConfig(channel=UpdateChannel.DEVELOPMENT)
136
- assert config.channel_name == f'dev-{_platform_suffix()}'
136
+ assert config.channel_name == f'dev-{platform_suffix()}'
137
137
 
138
138
 
139
139
  @pytest.fixture
@@ -1 +0,0 @@
1
- __version__ = '0.0.1.dev23'