memoryframes 0.5.0__tar.gz → 0.6.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: memoryframes
3
- Version: 0.5.0
3
+ Version: 0.6.0
4
4
  Summary: Add your description here
5
5
  Author: David Brownell
6
6
  Author-email: David Brownell <github@DavidBrownell.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "MemoryFrames"
3
- version = "0.5.0"
3
+ version = "0.6.0"
4
4
  # ^^^^^
5
5
  # Wheel names will be generated according to this value. Do not manually modify this value; instead
6
6
  # update it according to committed changes by running this command from the root of the repository:
@@ -59,7 +59,7 @@ dev = [
59
59
  ]
60
60
 
61
61
  [tool.pytest.ini_options]
62
- addopts = "--verbose -vv --capture=no --cov=MemoryFrames --cov-report html --cov-report term --cov-report xml:coverage.xml --cov-fail-under=95.0"
62
+ addopts = "--verbose -vv --capture=no --cov=MemoryFrames --cov-report html --cov-report term --cov-report xml:coverage.xml --cov-fail-under=75.0"
63
63
  python_files = [
64
64
  "**/*Test.py",
65
65
  ]
@@ -0,0 +1,94 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass
3
+ from enum import auto, Enum
4
+ from typing import TYPE_CHECKING
5
+
6
+ import pluggy
7
+
8
+ from MemoryFrames import APP_NAME
9
+ from MemoryFrames.PluginInfra import Plugin as PluginModule
10
+
11
+ if TYPE_CHECKING:
12
+ from MemoryFrames.PluginInfra.Plugin import Plugin
13
+ from MemoryFrames.PluginInfra.Settings import Settings
14
+ from MemoryFrames.PluginInfra.UserExperienceInfo import UserExperienceInfo
15
+
16
+
17
+ # ----------------------------------------------------------------------
18
+ class AppStateObserver(ABC):
19
+ """Observer for AppState changes."""
20
+
21
+ # ----------------------------------------------------------------------
22
+ class EventType(Enum):
23
+ """Value signaling progress during the creation of an AppState instance."""
24
+
25
+ LoadingPlugins = auto()
26
+
27
+ # ----------------------------------------------------------------------
28
+ @abstractmethod
29
+ def OnEvent(self, event_type: EventType) -> None:
30
+ """Invoke when a processing event begins."""
31
+ raise NotImplementedError() # pragma: no cover
32
+
33
+ # ----------------------------------------------------------------------
34
+ @abstractmethod
35
+ def OnException(self, exception: Exception) -> None:
36
+ """Invoke when an exception occurs during processing."""
37
+ raise NotImplementedError() # pragma: no cover
38
+
39
+
40
+ # ----------------------------------------------------------------------
41
+ @dataclass(frozen=True)
42
+ class AppState:
43
+ """MemoryFrames application state."""
44
+
45
+ # ----------------------------------------------------------------------
46
+ plugins: list[Plugin]
47
+
48
+ # ----------------------------------------------------------------------
49
+ @classmethod
50
+ def Create(
51
+ cls,
52
+ settings: Settings,
53
+ user_experience_info: UserExperienceInfo,
54
+ observer: AppStateObserver,
55
+ ) -> AppState | None:
56
+ """Create an AppState instance."""
57
+
58
+ # Load the plugins
59
+ observer.OnEvent(AppStateObserver.EventType.LoadingPlugins)
60
+
61
+ try:
62
+ plugins = LoadPlugins(settings, user_experience_info)
63
+ except Exception as ex:
64
+ observer.OnException(ex)
65
+ return None
66
+
67
+ # We want the highest priority plugins first, and then sort by name. We apply the negative so
68
+ # that higher priority values appear first while still maintaining ascending order for names.
69
+ plugins.sort(key=lambda plugin: (-(plugin.PLUGIN_PRIORITY or 0), plugin.NAME))
70
+
71
+ return cls(plugins)
72
+
73
+
74
+ # ----------------------------------------------------------------------
75
+ def LoadPlugins(
76
+ settings: Settings,
77
+ user_experience_info: UserExperienceInfo,
78
+ ) -> list[Plugin]:
79
+ """Load the plugins.
80
+
81
+ This is implemented as a separate function to make it easier to monkey-patch during testing.
82
+ """
83
+
84
+ plugin_manager = pluggy.PluginManager(APP_NAME)
85
+
86
+ plugin_manager.add_hookspecs(PluginModule)
87
+ plugin_manager.load_setuptools_entrypoints(APP_NAME)
88
+
89
+ plugins: list[Plugin] = plugin_manager.hook.GetPlugin(
90
+ settings=settings,
91
+ user_experience_info=user_experience_info,
92
+ )
93
+
94
+ return plugins
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
12
12
  from pathlib import Path
13
13
 
14
14
  from MemoryFrames.PluginInfra.NoteSource import NoteSource, NoteSourceObserver
15
+ from MemoryFrames.PluginInfra.Settings import Settings
15
16
  from MemoryFrames.PluginInfra.UserExperienceInfo import UserExperienceInfo
16
17
 
17
18
 
@@ -81,8 +82,7 @@ class Plugin:
81
82
  # ----------------------------------------------------------------------
82
83
  @pluggy.HookspecMarker(APP_NAME)
83
84
  def GetPlugin(
84
- root_data_dir: Path,
85
- all_plugins_settings: dict[str, object],
85
+ settings: Settings,
86
86
  user_experience_info: UserExperienceInfo,
87
87
  ) -> Plugin:
88
88
  """Return a Plugin instance."""
@@ -4,7 +4,7 @@ from MemoryFrames.PluginInfra.UserExperienceInfo import UserExperienceInfo
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from textual.app import App
7
- from textual.containers import Vertical
7
+ from textual.containers import VerticalScroll
8
8
 
9
9
 
10
10
  # ----------------------------------------------------------------------
@@ -15,7 +15,7 @@ class TextualUserExperienceInfo(UserExperienceInfo):
15
15
  def __init__(
16
16
  self,
17
17
  app: App,
18
- hierarchy_container: Vertical,
18
+ hierarchy_container: VerticalScroll,
19
19
  ) -> None:
20
20
  super().__init__()
21
21
 
@@ -0,0 +1,150 @@
1
+ import traceback
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ from dbrownell_Common.Types import override
7
+ from textual.app import App, ComposeResult
8
+ from textual.containers import Horizontal, VerticalScroll
9
+ from textual.screen import ModalScreen
10
+ from textual.widgets import Footer, Header, Label, LoadingIndicator
11
+
12
+ from MemoryFrames import APP_NAME, __version__
13
+ from MemoryFrames.AppState import AppState, AppStateObserver as AppStateObserverBase
14
+ from MemoryFrames.PluginInfra.TextualUserExperienceInfo import TextualUserExperienceInfo
15
+
16
+ if TYPE_CHECKING:
17
+ from MemoryFrames.PluginInfra.Settings import Settings
18
+
19
+
20
+ # ----------------------------------------------------------------------
21
+ def Execute(settings: Settings) -> None:
22
+ """Execute the Textual user experience."""
23
+
24
+ _App(settings).run()
25
+
26
+
27
+ # ----------------------------------------------------------------------
28
+ # ----------------------------------------------------------------------
29
+ # ----------------------------------------------------------------------
30
+ class _App(App):
31
+ CSS_PATH = Path(__file__).with_suffix(".tcss")
32
+
33
+ # ----------------------------------------------------------------------
34
+ def __init__(self, settings: Settings) -> None:
35
+ super().__init__()
36
+
37
+ self._settings = settings
38
+
39
+ self._user_experience = TextualUserExperienceInfo(
40
+ self.app,
41
+ VerticalScroll(id="hierarchies"),
42
+ )
43
+
44
+ # The app state is initialized in `on_mount`
45
+ self._app_state: AppState | None = None
46
+
47
+ self.title = "Memory Frames"
48
+ assert self.title.replace(" ", "") == APP_NAME, (self.title, APP_NAME)
49
+
50
+ # ----------------------------------------------------------------------
51
+ def compose(self) -> ComposeResult:
52
+ yield Header()
53
+
54
+ with Horizontal(id="viewport"):
55
+ yield self._user_experience.hierarchy_container
56
+
57
+ with Horizontal(id="footer"):
58
+ yield Footer()
59
+ yield Label(__version__)
60
+
61
+ # ----------------------------------------------------------------------
62
+ def on_mount(self) -> None:
63
+ # ----------------------------------------------------------------------
64
+ def OnLoaded(app_state: AppState | None) -> None:
65
+ if app_state is None:
66
+ return
67
+
68
+ assert self._app_state is None
69
+ self._app_state = app_state
70
+
71
+ # ----------------------------------------------------------------------
72
+
73
+ self.push_screen(
74
+ _LoadingModal(self._settings, self._user_experience),
75
+ OnLoaded,
76
+ )
77
+
78
+
79
+ # ----------------------------------------------------------------------
80
+ class _LoadingModal(ModalScreen[AppState]):
81
+ CSS_PATH = Path(__file__).with_suffix(".tcss")
82
+
83
+ # ----------------------------------------------------------------------
84
+ def __init__(
85
+ self,
86
+ settings: Settings,
87
+ user_experience_info: TextualUserExperienceInfo,
88
+ ) -> None:
89
+ super().__init__()
90
+
91
+ self._settings = settings
92
+ self._user_experience_info = user_experience_info
93
+
94
+ self._status_label = Label("Loading...")
95
+
96
+ # ----------------------------------------------------------------------
97
+ def compose(self) -> ComposeResult:
98
+ yield LoadingIndicator()
99
+ yield self._status_label
100
+
101
+ # ----------------------------------------------------------------------
102
+ def on_mount(self) -> None:
103
+ loading_module = self
104
+
105
+ # ----------------------------------------------------------------------
106
+ def Execute() -> None:
107
+ current_event: AppStateObserverBase.EventType | None = None
108
+
109
+ # ----------------------------------------------------------------------
110
+ class AppStateObserver(AppStateObserverBase):
111
+ # ----------------------------------------------------------------------
112
+ @override
113
+ def OnEvent(self, event_type: AppStateObserver.EventType) -> None:
114
+ nonlocal current_event
115
+ current_event = event_type
116
+
117
+ if event_type == AppStateObserver.EventType.LoadingPlugins:
118
+ msg = "Loading plugins..."
119
+ elif event_type == AppStateObserver.EventType.StartingThreads:
120
+ msg = "Starting threads..."
121
+ else:
122
+ assert False, event_type # noqa: B011, PT015
123
+
124
+ loading_module._status_label.content = msg # noqa: SLF001
125
+
126
+ # ----------------------------------------------------------------------
127
+ @override
128
+ def OnException(self, exception: Exception) -> None:
129
+ if loading_module._settings.debug: # noqa: SLF001
130
+ msg = "".join(traceback.format_exception(exception))
131
+ else:
132
+ msg = str(exception)
133
+
134
+ if current_event == AppStateObserver.EventType.LoadingPlugins:
135
+ loading_module.app.panic(msg)
136
+ return
137
+
138
+ # Display in a toast
139
+ loading_module.notify(msg, severity="error", timeout=10)
140
+
141
+ # ----------------------------------------------------------------------
142
+
143
+ app_state = AppState.Create(self._settings, self._user_experience_info, AppStateObserver())
144
+
145
+ # Return the results
146
+ self.app.call_from_thread(lambda: self.dismiss(app_state))
147
+
148
+ # ----------------------------------------------------------------------
149
+
150
+ self.run_worker(Execute, thread=True)
@@ -0,0 +1,33 @@
1
+ $version_length: 15;
2
+
3
+ #footer {
4
+ height: 1;
5
+
6
+ Footer {
7
+ padding-right: $version_length;
8
+ }
9
+
10
+ Label {
11
+ background: $footer-background;
12
+ dock: right;
13
+ text-align: center;
14
+ width: $version_length;
15
+ }
16
+ }
17
+
18
+ _LoadingModal {
19
+ align: center middle;
20
+ background: $panel;
21
+ height: 2;
22
+ padding: 1;
23
+
24
+ LoadingIndicator {
25
+ height: 1;
26
+ margin-bottom: 1;
27
+ }
28
+
29
+ Label {
30
+ text-align: center;
31
+ width: 100%;
32
+ }
33
+ }
@@ -8,8 +8,8 @@ import typer
8
8
 
9
9
  from typer.core import TyperGroup
10
10
 
11
- from MemoryFrames.Settings import Settings
12
11
  from MemoryFrames import TextualUserExperience
12
+ from MemoryFrames.PluginInfra.Settings import Settings
13
13
 
14
14
 
15
15
  # ----------------------------------------------------------------------
@@ -1,11 +0,0 @@
1
- from typing import TYPE_CHECKING
2
-
3
- if TYPE_CHECKING:
4
- from MemoryFrames.Settings import Settings
5
-
6
-
7
- # ----------------------------------------------------------------------
8
- def Execute(settings: Settings) -> None:
9
- """Execute the Textual user experience."""
10
-
11
- print("TextualUserExperience", settings) # noqa: T201
File without changes