pum 1.3.1__tar.gz → 1.3.2__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.
- {pum-1.3.1 → pum-1.3.2}/PKG-INFO +1 -1
- {pum-1.3.1 → pum-1.3.2}/pum/hook.py +13 -21
- {pum-1.3.1 → pum-1.3.2}/pum/pum_config.py +16 -4
- {pum-1.3.1 → pum-1.3.2}/pum.egg-info/PKG-INFO +1 -1
- {pum-1.3.1 → pum-1.3.2}/test/test_hooks.py +76 -26
- {pum-1.3.1 → pum-1.3.2}/LICENSE +0 -0
- {pum-1.3.1 → pum-1.3.2}/README.md +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/__init__.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/changelog.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/checker.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/cli.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/config_model.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/connection.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/dependency_handler.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/dumper.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/exceptions.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/feedback.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/info.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/parameter.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/report_generator.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/role_manager.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/schema_migrations.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/sql_content.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum/upgrader.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum.egg-info/SOURCES.txt +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum.egg-info/dependency_links.txt +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum.egg-info/entry_points.txt +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum.egg-info/requires.txt +0 -0
- {pum-1.3.1 → pum-1.3.2}/pum.egg-info/top_level.txt +0 -0
- {pum-1.3.1 → pum-1.3.2}/pyproject.toml +0 -0
- {pum-1.3.1 → pum-1.3.2}/requirements/base.txt +0 -0
- {pum-1.3.1 → pum-1.3.2}/requirements/development.txt +0 -0
- {pum-1.3.1 → pum-1.3.2}/requirements/html.txt +0 -0
- {pum-1.3.1 → pum-1.3.2}/setup.cfg +0 -0
- {pum-1.3.1 → pum-1.3.2}/test/test_changelog.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/test/test_checker.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/test/test_config.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/test/test_dumper.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/test/test_feedback.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/test/test_roles.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/test/test_schema_migrations.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/test/test_sql_content.py +0 -0
- {pum-1.3.1 → pum-1.3.2}/test/test_upgrader.py +0 -0
{pum-1.3.1 → pum-1.3.2}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pum
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: Pum stands for "Postgres Upgrades Manager". It is a Database migration management tool very similar to flyway-db or Liquibase, based on metadata tables.
|
|
5
5
|
Author-email: Denis Rouzaud <denis@opengis.ch>
|
|
6
6
|
License-Expression: GPL-2.0-or-later
|
|
@@ -93,7 +93,6 @@ class HookHandler:
|
|
|
93
93
|
self.code = code
|
|
94
94
|
self.hook_instance = None
|
|
95
95
|
self.sys_path_additions = [] # Store paths to add during execution
|
|
96
|
-
self._imported_modules = [] # Track modules imported by this hook
|
|
97
96
|
|
|
98
97
|
if file:
|
|
99
98
|
if isinstance(file, str):
|
|
@@ -122,22 +121,25 @@ class HookHandler:
|
|
|
122
121
|
if base_path_str not in sys.path and base_path_str != parent_dir:
|
|
123
122
|
self.sys_path_additions.append(base_path_str)
|
|
124
123
|
|
|
125
|
-
# Temporarily add paths for module loading
|
|
126
|
-
for path in self.sys_path_additions:
|
|
124
|
+
# Temporarily add paths for module loading - insert at position 0 for priority
|
|
125
|
+
for path in reversed(self.sys_path_additions):
|
|
127
126
|
sys.path.insert(0, path)
|
|
128
127
|
|
|
129
|
-
# Track modules before loading to detect new imports
|
|
130
|
-
modules_before = set(sys.modules.keys())
|
|
131
|
-
|
|
132
128
|
try:
|
|
133
|
-
|
|
129
|
+
logger.debug(f"Loading hook from: {self.file}")
|
|
130
|
+
logger.debug(f"sys.path additions: {self.sys_path_additions}")
|
|
131
|
+
spec = importlib.util.spec_from_file_location(
|
|
132
|
+
self.file.stem,
|
|
133
|
+
self.file,
|
|
134
|
+
submodule_search_locations=[parent_dir],
|
|
135
|
+
)
|
|
134
136
|
module = importlib.util.module_from_spec(spec)
|
|
137
|
+
# Set __path__ to enable package-like imports from the hook's directory
|
|
138
|
+
module.__path__ = [parent_dir]
|
|
139
|
+
# Add to sys.modules before executing so imports can find it
|
|
140
|
+
sys.modules[self.file.stem] = module
|
|
135
141
|
spec.loader.exec_module(module)
|
|
136
142
|
|
|
137
|
-
# Track modules that were imported by this hook
|
|
138
|
-
modules_after = set(sys.modules.keys())
|
|
139
|
-
self._imported_modules = list(modules_after - modules_before)
|
|
140
|
-
|
|
141
143
|
# Check that the module contains a class named Hook inheriting from HookBase
|
|
142
144
|
# Do this BEFORE removing paths from sys.path
|
|
143
145
|
hook_class = getattr(module, "Hook", None)
|
|
@@ -182,16 +184,6 @@ class HookHandler:
|
|
|
182
184
|
if path in sys.path:
|
|
183
185
|
sys.path.remove(path)
|
|
184
186
|
|
|
185
|
-
def cleanup_imports(self):
|
|
186
|
-
"""Remove imported modules from sys.modules cache.
|
|
187
|
-
This should be called when switching to a different module version
|
|
188
|
-
to prevent import conflicts.
|
|
189
|
-
"""
|
|
190
|
-
for module_name in self._imported_modules:
|
|
191
|
-
if module_name in sys.modules:
|
|
192
|
-
del sys.modules[module_name]
|
|
193
|
-
self._imported_modules.clear()
|
|
194
|
-
|
|
195
187
|
def __repr__(self) -> str:
|
|
196
188
|
"""Return a string representation of the Hook instance."""
|
|
197
189
|
return f"<hook: {self.file}>"
|
|
@@ -189,10 +189,22 @@ class PumConfig:
|
|
|
189
189
|
This should be called when switching to a different module version to ensure
|
|
190
190
|
that cached imports from the previous version don't cause conflicts.
|
|
191
191
|
"""
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
# Clear all modules that were loaded from this base_path
|
|
193
|
+
base_path_str = str(self._base_path.resolve())
|
|
194
|
+
modules_to_remove = []
|
|
195
|
+
|
|
196
|
+
for module_name, module in list(sys.modules.items()):
|
|
197
|
+
if module is None:
|
|
198
|
+
continue
|
|
199
|
+
module_file = getattr(module, "__file__", None)
|
|
200
|
+
if module_file and module_file.startswith(base_path_str):
|
|
201
|
+
modules_to_remove.append(module_name)
|
|
202
|
+
|
|
203
|
+
for module_name in modules_to_remove:
|
|
204
|
+
if module_name in sys.modules:
|
|
205
|
+
logger.debug(f"Removing cached module: {module_name}")
|
|
206
|
+
del sys.modules[module_name]
|
|
207
|
+
|
|
196
208
|
self._cached_handlers.clear()
|
|
197
209
|
|
|
198
210
|
def parameters(self) -> list[ParameterDefinition]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pum
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2
|
|
4
4
|
Summary: Pum stands for "Postgres Upgrades Manager". It is a Database migration management tool very similar to flyway-db or Liquibase, based on metadata tables.
|
|
5
5
|
Author-email: Denis Rouzaud <denis@opengis.ch>
|
|
6
6
|
License-Expression: GPL-2.0-or-later
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Test module for hook functionality."""
|
|
2
2
|
|
|
3
|
+
import sys
|
|
3
4
|
import unittest
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
from unittest.mock import Mock
|
|
@@ -7,6 +8,24 @@ from unittest.mock import Mock
|
|
|
7
8
|
from pum.hook import HookHandler
|
|
8
9
|
|
|
9
10
|
|
|
11
|
+
def cleanup_modules_by_path(base_path: Path) -> None:
|
|
12
|
+
"""Clean up all modules imported from a base path.
|
|
13
|
+
|
|
14
|
+
This is a test helper that mimics the cleanup logic from PumConfig.cleanup_hook_imports().
|
|
15
|
+
"""
|
|
16
|
+
base_path_str = str(base_path.resolve())
|
|
17
|
+
modules_to_remove = []
|
|
18
|
+
for module_name, module in list(sys.modules.items()):
|
|
19
|
+
if module is None:
|
|
20
|
+
continue
|
|
21
|
+
module_file = getattr(module, "__file__", None)
|
|
22
|
+
if module_file and module_file.startswith(base_path_str):
|
|
23
|
+
modules_to_remove.append(module_name)
|
|
24
|
+
for module_name in modules_to_remove:
|
|
25
|
+
if module_name in sys.modules:
|
|
26
|
+
del sys.modules[module_name]
|
|
27
|
+
|
|
28
|
+
|
|
10
29
|
class TestHooks(unittest.TestCase):
|
|
11
30
|
"""Test the hook functionality."""
|
|
12
31
|
|
|
@@ -96,12 +115,10 @@ class TestHooks(unittest.TestCase):
|
|
|
96
115
|
def test_hook_cleanup_imports(self) -> None:
|
|
97
116
|
"""Test that hook imports can be cleaned up to prevent conflicts when switching versions.
|
|
98
117
|
|
|
99
|
-
This test verifies that when hooks are cleaned up
|
|
100
|
-
are removed from sys.modules cache, allowing fresh imports
|
|
101
|
-
to a different module version.
|
|
118
|
+
This test verifies that when hooks are cleaned up via path-based cleanup,
|
|
119
|
+
their imported modules are removed from sys.modules cache, allowing fresh imports
|
|
120
|
+
when switching to a different module version.
|
|
102
121
|
"""
|
|
103
|
-
import sys
|
|
104
|
-
|
|
105
122
|
test_dir = Path("test") / "data" / "hook_sibling_imports"
|
|
106
123
|
hook_file = test_dir / "app" / "create_hook.py"
|
|
107
124
|
|
|
@@ -113,15 +130,6 @@ class TestHooks(unittest.TestCase):
|
|
|
113
130
|
# Load the hook - this will import view.helper
|
|
114
131
|
handler = HookHandler(base_path=test_dir, file=str(hook_file.relative_to(test_dir)))
|
|
115
132
|
|
|
116
|
-
# Verify that view.helper was imported and tracked
|
|
117
|
-
self.assertGreater(
|
|
118
|
-
len(handler._imported_modules), 0, "Should have tracked imported modules"
|
|
119
|
-
)
|
|
120
|
-
self.assertTrue(
|
|
121
|
-
any("view" in mod for mod in handler._imported_modules),
|
|
122
|
-
"Should have tracked view module",
|
|
123
|
-
)
|
|
124
|
-
|
|
125
133
|
# Verify view.helper is in sys.modules
|
|
126
134
|
view_module_found = any("view" in mod for mod in sys.modules)
|
|
127
135
|
self.assertTrue(
|
|
@@ -132,13 +140,8 @@ class TestHooks(unittest.TestCase):
|
|
|
132
140
|
mock_conn = Mock()
|
|
133
141
|
handler.execute(connection=mock_conn, parameters={})
|
|
134
142
|
|
|
135
|
-
# Clean up imports
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
# Verify that tracked modules were cleared
|
|
139
|
-
self.assertEqual(
|
|
140
|
-
len(handler._imported_modules), 0, "Should have cleared tracked modules list"
|
|
141
|
-
)
|
|
143
|
+
# Clean up imports via path-based cleanup
|
|
144
|
+
cleanup_modules_by_path(test_dir)
|
|
142
145
|
|
|
143
146
|
# Verify that view modules were removed from sys.modules
|
|
144
147
|
view_modules_after = [mod for mod in sys.modules if "view.helper" in mod or mod == "view"]
|
|
@@ -154,8 +157,6 @@ class TestHooks(unittest.TestCase):
|
|
|
154
157
|
This test simulates switching between module versions by loading a hook,
|
|
155
158
|
cleaning it up, and loading it again.
|
|
156
159
|
"""
|
|
157
|
-
import sys
|
|
158
|
-
|
|
159
160
|
test_dir = Path("test") / "data" / "hook_sibling_imports"
|
|
160
161
|
hook_file = test_dir / "app" / "create_hook.py"
|
|
161
162
|
|
|
@@ -176,8 +177,8 @@ class TestHooks(unittest.TestCase):
|
|
|
176
177
|
view_module_id_1 = id(sys.modules[mod_name])
|
|
177
178
|
break
|
|
178
179
|
|
|
179
|
-
# Clean up
|
|
180
|
-
|
|
180
|
+
# Clean up via path-based cleanup
|
|
181
|
+
cleanup_modules_by_path(test_dir)
|
|
181
182
|
|
|
182
183
|
# Verify cleanup worked
|
|
183
184
|
view_modules = [mod for mod in sys.modules if "view.helper" in mod or mod == "view"]
|
|
@@ -199,4 +200,53 @@ class TestHooks(unittest.TestCase):
|
|
|
199
200
|
self.assertIsNotNone(view_module_id_2, "Second load should have imported view")
|
|
200
201
|
|
|
201
202
|
# Clean up after test
|
|
202
|
-
|
|
203
|
+
cleanup_modules_by_path(test_dir)
|
|
204
|
+
|
|
205
|
+
def test_hook_submodule_cleanup_on_version_switch(self) -> None:
|
|
206
|
+
"""Test that submodules are properly cleaned up when switching between module versions.
|
|
207
|
+
|
|
208
|
+
This test simulates the real-world scenario where a user switches between
|
|
209
|
+
different versions of a module that imports from nested submodules (e.g., view.submodule.helper).
|
|
210
|
+
Without proper submodule cleanup, the cached view module from v1 would prevent
|
|
211
|
+
v2 view.submodule.helper from being imported correctly.
|
|
212
|
+
"""
|
|
213
|
+
v1_dir = Path("test") / "data" / "hook_submodule_cleanup" / "v1"
|
|
214
|
+
v2_dir = Path("test") / "data" / "hook_submodule_cleanup" / "v2"
|
|
215
|
+
hook_file = Path("app") / "create_hook.py"
|
|
216
|
+
|
|
217
|
+
# Clear any previously imported view modules
|
|
218
|
+
modules_to_remove = [key for key in sys.modules if key == "view" or key.startswith("view.")]
|
|
219
|
+
for module in modules_to_remove:
|
|
220
|
+
del sys.modules[module]
|
|
221
|
+
|
|
222
|
+
# Load v1 hook - imports view.submodule.helper which returns value_from_submodule_v1
|
|
223
|
+
handler_v1 = HookHandler(base_path=v1_dir, file=str(hook_file))
|
|
224
|
+
mock_conn = Mock()
|
|
225
|
+
# Execute v1 hook - the assertion inside run_hook will fail if wrong module is imported
|
|
226
|
+
handler_v1.execute(connection=mock_conn, parameters={})
|
|
227
|
+
|
|
228
|
+
# Verify submodules were imported
|
|
229
|
+
view_submodules = [mod for mod in sys.modules if mod.startswith("view.submodule")]
|
|
230
|
+
self.assertGreater(len(view_submodules), 0, "Should have imported view.submodule modules")
|
|
231
|
+
|
|
232
|
+
# Clean up v1 imports via path-based cleanup
|
|
233
|
+
cleanup_modules_by_path(v1_dir)
|
|
234
|
+
|
|
235
|
+
# Verify ALL view modules (including submodules) were cleaned up
|
|
236
|
+
remaining_view_modules = [
|
|
237
|
+
mod for mod in sys.modules if mod == "view" or mod.startswith("view.")
|
|
238
|
+
]
|
|
239
|
+
self.assertEqual(
|
|
240
|
+
len(remaining_view_modules),
|
|
241
|
+
0,
|
|
242
|
+
f"All view modules should be cleaned up, but found: {remaining_view_modules}",
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
# Load v2 hook - should import fresh view.submodule.helper which returns value_from_submodule_v2
|
|
246
|
+
# This is the critical part - without submodule cleanup, Python would use the cached
|
|
247
|
+
# view.submodule.helper from v1 and the assertion inside run_hook would fail
|
|
248
|
+
handler_v2 = HookHandler(base_path=v2_dir, file=str(hook_file))
|
|
249
|
+
handler_v2.execute(connection=mock_conn, parameters={})
|
|
250
|
+
|
|
251
|
+
# Clean up
|
|
252
|
+
cleanup_modules_by_path(v2_dir)
|
{pum-1.3.1 → pum-1.3.2}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|