pum 1.3.2__tar.gz → 1.3.3__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.2 → pum-1.3.3}/PKG-INFO +1 -1
- {pum-1.3.2 → pum-1.3.3}/pum/hook.py +24 -10
- {pum-1.3.2 → pum-1.3.3}/pum/pum_config.py +5 -1
- {pum-1.3.2 → pum-1.3.3}/pum.egg-info/PKG-INFO +1 -1
- {pum-1.3.2 → pum-1.3.3}/test/test_config.py +134 -2
- pum-1.3.3/test/test_hooks.py +64 -0
- pum-1.3.2/test/test_hooks.py +0 -252
- {pum-1.3.2 → pum-1.3.3}/LICENSE +0 -0
- {pum-1.3.2 → pum-1.3.3}/README.md +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/__init__.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/changelog.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/checker.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/cli.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/config_model.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/connection.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/dependency_handler.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/dumper.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/exceptions.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/feedback.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/info.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/parameter.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/report_generator.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/role_manager.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/schema_migrations.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/sql_content.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum/upgrader.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum.egg-info/SOURCES.txt +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum.egg-info/dependency_links.txt +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum.egg-info/entry_points.txt +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum.egg-info/requires.txt +0 -0
- {pum-1.3.2 → pum-1.3.3}/pum.egg-info/top_level.txt +0 -0
- {pum-1.3.2 → pum-1.3.3}/pyproject.toml +0 -0
- {pum-1.3.2 → pum-1.3.3}/requirements/base.txt +0 -0
- {pum-1.3.2 → pum-1.3.3}/requirements/development.txt +0 -0
- {pum-1.3.2 → pum-1.3.3}/requirements/html.txt +0 -0
- {pum-1.3.2 → pum-1.3.3}/setup.cfg +0 -0
- {pum-1.3.2 → pum-1.3.3}/test/test_changelog.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/test/test_checker.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/test/test_dumper.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/test/test_feedback.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/test/test_roles.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/test/test_schema_migrations.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/test/test_sql_content.py +0 -0
- {pum-1.3.2 → pum-1.3.3}/test/test_upgrader.py +0 -0
{pum-1.3.2 → pum-1.3.3}/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.3
|
|
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
|
|
@@ -111,20 +111,23 @@ class HookHandler:
|
|
|
111
111
|
parent_dir = str(self.file.parent.resolve())
|
|
112
112
|
|
|
113
113
|
# Store paths that need to be added for hook execution
|
|
114
|
-
#
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
# Always add paths even if already in sys.path - we need them at position 0
|
|
115
|
+
# for priority and we'll track what we added for cleanup
|
|
116
|
+
self.sys_path_additions.append(parent_dir)
|
|
117
117
|
|
|
118
118
|
# Also add base_path if provided, to support imports from sibling directories
|
|
119
119
|
if base_path is not None:
|
|
120
120
|
base_path_str = str(base_path.resolve())
|
|
121
|
-
if base_path_str
|
|
121
|
+
if base_path_str != parent_dir:
|
|
122
122
|
self.sys_path_additions.append(base_path_str)
|
|
123
123
|
|
|
124
|
-
#
|
|
124
|
+
# Add paths for module loading - insert at position 0 for priority
|
|
125
125
|
for path in reversed(self.sys_path_additions):
|
|
126
126
|
sys.path.insert(0, path)
|
|
127
127
|
|
|
128
|
+
# Invalidate caches so Python recognizes the new paths
|
|
129
|
+
importlib.invalidate_caches()
|
|
130
|
+
|
|
128
131
|
try:
|
|
129
132
|
logger.debug(f"Loading hook from: {self.file}")
|
|
130
133
|
logger.debug(f"sys.path additions: {self.sys_path_additions}")
|
|
@@ -178,11 +181,22 @@ class HookHandler:
|
|
|
178
181
|
arg for arg in arg_names if arg not in ("self", "connection")
|
|
179
182
|
]
|
|
180
183
|
|
|
181
|
-
|
|
182
|
-
#
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
except Exception:
|
|
185
|
+
# On error, clean up paths we added
|
|
186
|
+
self.cleanup_sys_path()
|
|
187
|
+
raise
|
|
188
|
+
|
|
189
|
+
def cleanup_sys_path(self) -> None:
|
|
190
|
+
"""Remove paths that were added to sys.path for this hook.
|
|
191
|
+
|
|
192
|
+
This should be called when the hook is no longer needed to prevent
|
|
193
|
+
sys.path pollution.
|
|
194
|
+
"""
|
|
195
|
+
for path in self.sys_path_additions:
|
|
196
|
+
# Remove all occurrences of this path (we may have added it multiple times)
|
|
197
|
+
while path in sys.path:
|
|
198
|
+
sys.path.remove(path)
|
|
199
|
+
self.sys_path_additions.clear()
|
|
186
200
|
|
|
187
201
|
def __repr__(self) -> str:
|
|
188
202
|
"""Return a string representation of the Hook instance."""
|
|
@@ -184,11 +184,15 @@ class PumConfig:
|
|
|
184
184
|
return self._base_path
|
|
185
185
|
|
|
186
186
|
def cleanup_hook_imports(self) -> None:
|
|
187
|
-
"""Clean up imported modules
|
|
187
|
+
"""Clean up imported modules and sys.path entries from hooks.
|
|
188
188
|
|
|
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
|
+
# First, clean up sys.path additions from all cached handlers
|
|
193
|
+
for handler in self._cached_handlers:
|
|
194
|
+
handler.cleanup_sys_path()
|
|
195
|
+
|
|
192
196
|
# Clear all modules that were loaded from this base_path
|
|
193
197
|
base_path_str = str(self._base_path.resolve())
|
|
194
198
|
modules_to_remove = []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pum
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.3
|
|
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
|
|
@@ -199,17 +199,28 @@ class TestConfig(unittest.TestCase):
|
|
|
199
199
|
This test simulates the QGIS plugin scenario where a user switches from one
|
|
200
200
|
module version to another, which can cause import conflicts if old modules
|
|
201
201
|
are still cached in sys.modules.
|
|
202
|
+
|
|
203
|
+
Tests three critical scenarios:
|
|
204
|
+
1. Simple module imports (top-level imports at module load time)
|
|
205
|
+
2. Nested submodule imports (e.g., view.submodule.helper)
|
|
206
|
+
3. Dynamic imports (imports inside run_hook at execution time)
|
|
207
|
+
|
|
208
|
+
All three scenarios need proper cleanup to prevent version conflicts.
|
|
202
209
|
"""
|
|
203
210
|
import sys
|
|
204
211
|
from unittest.mock import Mock
|
|
205
212
|
|
|
206
|
-
#
|
|
213
|
+
# Initial cleanup to ensure clean state from any previous test runs
|
|
214
|
+
# This is the ONLY manual cleanup - everything else should use the API
|
|
207
215
|
modules_to_remove = [
|
|
208
|
-
key
|
|
216
|
+
key
|
|
217
|
+
for key in list(sys.modules.keys())
|
|
218
|
+
if "view" in key or "helper_v" in key or "helper_dynamic" in key
|
|
209
219
|
]
|
|
210
220
|
for module in modules_to_remove:
|
|
211
221
|
del sys.modules[module]
|
|
212
222
|
|
|
223
|
+
# Test 1: Simple module imports
|
|
213
224
|
# Load version 1
|
|
214
225
|
v1_path = Path("test") / "data" / "hook_version_switch" / "v1"
|
|
215
226
|
cfg_v1 = PumConfig(
|
|
@@ -271,6 +282,127 @@ class TestConfig(unittest.TestCase):
|
|
|
271
282
|
"helper_v2 should be removed from sys.modules after cleanup",
|
|
272
283
|
)
|
|
273
284
|
|
|
285
|
+
# Test 2: Nested submodule imports (view.submodule.helper)
|
|
286
|
+
# This tests the critical case where parent modules cache can prevent
|
|
287
|
+
# submodule reloading if not properly cleaned up
|
|
288
|
+
# No manual cleanup needed - the API should handle everything
|
|
289
|
+
|
|
290
|
+
# Load submodule cleanup v1
|
|
291
|
+
submodule_v1_path = Path("test") / "data" / "hook_submodule_cleanup" / "v1"
|
|
292
|
+
cfg_submodule_v1 = PumConfig(
|
|
293
|
+
base_path=submodule_v1_path,
|
|
294
|
+
pum={"module": "test_submodule_v1"},
|
|
295
|
+
application={"create": [{"file": "app/create_hook.py"}]},
|
|
296
|
+
validate=False,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
handlers_submodule_v1 = cfg_submodule_v1.create_app_handlers()
|
|
300
|
+
self.assertEqual(len(handlers_submodule_v1), 1)
|
|
301
|
+
|
|
302
|
+
# Execute v1 hook - imports view.submodule.helper which returns value_from_submodule_v1
|
|
303
|
+
# The hook has an assertion that will fail if wrong version is imported
|
|
304
|
+
handlers_submodule_v1[0].execute(connection=mock_conn, parameters={})
|
|
305
|
+
|
|
306
|
+
# Verify submodules were imported
|
|
307
|
+
view_submodules = [mod for mod in sys.modules if mod.startswith("view.submodule")]
|
|
308
|
+
self.assertGreater(len(view_submodules), 0, "Should have imported view.submodule modules")
|
|
309
|
+
|
|
310
|
+
# Clean up v1 submodule imports
|
|
311
|
+
cfg_submodule_v1.cleanup_hook_imports()
|
|
312
|
+
|
|
313
|
+
# Verify ALL view modules (including submodules) were cleaned up
|
|
314
|
+
remaining_view_modules = [
|
|
315
|
+
mod for mod in sys.modules if mod == "view" or mod.startswith("view.")
|
|
316
|
+
]
|
|
317
|
+
self.assertEqual(
|
|
318
|
+
len(remaining_view_modules),
|
|
319
|
+
0,
|
|
320
|
+
f"All view modules should be cleaned up, but found: {remaining_view_modules}",
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
# Load submodule cleanup v2 - critical test for proper submodule cleanup
|
|
324
|
+
# Without proper cleanup, Python would use cached view.submodule.helper from v1
|
|
325
|
+
submodule_v2_path = Path("test") / "data" / "hook_submodule_cleanup" / "v2"
|
|
326
|
+
cfg_submodule_v2 = PumConfig(
|
|
327
|
+
base_path=submodule_v2_path,
|
|
328
|
+
pum={"module": "test_submodule_v2"},
|
|
329
|
+
application={"create": [{"file": "app/create_hook.py"}]},
|
|
330
|
+
validate=False,
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
handlers_submodule_v2 = cfg_submodule_v2.create_app_handlers()
|
|
334
|
+
self.assertEqual(len(handlers_submodule_v2), 1)
|
|
335
|
+
|
|
336
|
+
# Execute v2 hook - should import fresh view.submodule.helper which returns value_from_submodule_v2
|
|
337
|
+
# The assertion inside the hook will fail if the wrong version is loaded
|
|
338
|
+
handlers_submodule_v2[0].execute(connection=mock_conn, parameters={})
|
|
339
|
+
|
|
340
|
+
# Clean up
|
|
341
|
+
cfg_submodule_v2.cleanup_hook_imports()
|
|
342
|
+
|
|
343
|
+
# Test 3: Dynamic imports (imports inside run_hook, not at module load time)
|
|
344
|
+
# This is the critical case that wasn't covered before - imports that happen
|
|
345
|
+
# during hook execution need cleanup too
|
|
346
|
+
# No manual cleanup needed - the API should handle everything
|
|
347
|
+
|
|
348
|
+
# Load dynamic import v1
|
|
349
|
+
dynamic_v1_path = Path("test") / "data" / "hook_dynamic_import_switch" / "v1"
|
|
350
|
+
cfg_dynamic_v1 = PumConfig(
|
|
351
|
+
base_path=dynamic_v1_path,
|
|
352
|
+
pum={"module": "test_dynamic_v1"},
|
|
353
|
+
application={"create": [{"file": "app/create_hook.py"}]},
|
|
354
|
+
validate=False,
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
handlers_dynamic_v1 = cfg_dynamic_v1.create_app_handlers()
|
|
358
|
+
self.assertEqual(len(handlers_dynamic_v1), 1)
|
|
359
|
+
|
|
360
|
+
# Execute v1 hook - this does a dynamic import of helper_dynamic_v1 inside run_hook
|
|
361
|
+
handlers_dynamic_v1[0].execute(connection=mock_conn, parameters={})
|
|
362
|
+
|
|
363
|
+
# Verify the dynamically imported module is in sys.modules
|
|
364
|
+
self.assertTrue(
|
|
365
|
+
any("helper_dynamic_v1" in mod for mod in sys.modules),
|
|
366
|
+
"helper_dynamic_v1 should be in sys.modules after dynamic import",
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
# Clean up v1 dynamic imports
|
|
370
|
+
cfg_dynamic_v1.cleanup_hook_imports()
|
|
371
|
+
|
|
372
|
+
# Verify dynamically imported modules were cleaned up
|
|
373
|
+
remaining_dynamic_v1_modules = [mod for mod in sys.modules if "helper_dynamic_v1" in mod]
|
|
374
|
+
self.assertEqual(
|
|
375
|
+
len(remaining_dynamic_v1_modules),
|
|
376
|
+
0,
|
|
377
|
+
f"Dynamic import modules should be cleaned up, but found: {remaining_dynamic_v1_modules}",
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
# Load dynamic import v2 - critical test for cleanup after dynamic imports
|
|
381
|
+
# Without proper cleanup, Python might use cached helper_dynamic_v1
|
|
382
|
+
dynamic_v2_path = Path("test") / "data" / "hook_dynamic_import_switch" / "v2"
|
|
383
|
+
cfg_dynamic_v2 = PumConfig(
|
|
384
|
+
base_path=dynamic_v2_path,
|
|
385
|
+
pum={"module": "test_dynamic_v2"},
|
|
386
|
+
application={"create": [{"file": "app/create_hook.py"}]},
|
|
387
|
+
validate=False,
|
|
388
|
+
)
|
|
389
|
+
|
|
390
|
+
handlers_dynamic_v2 = cfg_dynamic_v2.create_app_handlers()
|
|
391
|
+
self.assertEqual(len(handlers_dynamic_v2), 1)
|
|
392
|
+
|
|
393
|
+
# Execute v2 hook - should dynamically import fresh helper_dynamic_v2
|
|
394
|
+
# The assertion inside the hook will fail if the wrong version is loaded
|
|
395
|
+
handlers_dynamic_v2[0].execute(connection=mock_conn, parameters={})
|
|
396
|
+
|
|
397
|
+
# Verify v2 was imported
|
|
398
|
+
self.assertTrue(
|
|
399
|
+
any("helper_dynamic_v2" in mod for mod in sys.modules),
|
|
400
|
+
"helper_dynamic_v2 should be in sys.modules",
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Clean up
|
|
404
|
+
cfg_dynamic_v2.cleanup_hook_imports()
|
|
405
|
+
|
|
274
406
|
def test_parameter_type_string_conversion(self) -> None:
|
|
275
407
|
"""Test that ParameterType enums convert to string values correctly.
|
|
276
408
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Test module for hook functionality."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from unittest.mock import Mock
|
|
7
|
+
|
|
8
|
+
from pum.hook import HookHandler
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestHooks(unittest.TestCase):
|
|
12
|
+
"""Test the hook functionality."""
|
|
13
|
+
|
|
14
|
+
def test_hook_with_local_imports(self) -> None:
|
|
15
|
+
"""Test that a hook file can import from its own directory.
|
|
16
|
+
|
|
17
|
+
This test verifies that PUM hook files can properly import from their
|
|
18
|
+
own directory. The test uses a hook file in app/ that imports from
|
|
19
|
+
local_helper.py in the same directory.
|
|
20
|
+
"""
|
|
21
|
+
test_dir = Path("test") / "data" / "hook_local_imports"
|
|
22
|
+
hook_file = test_dir / "app" / "create_hook.py"
|
|
23
|
+
|
|
24
|
+
# Create hook handler - this should not raise an error
|
|
25
|
+
handler = HookHandler(base_path=test_dir, file=str(hook_file.relative_to(test_dir)))
|
|
26
|
+
|
|
27
|
+
# Verify the hook was loaded correctly
|
|
28
|
+
self.assertIsNotNone(handler.hook_instance)
|
|
29
|
+
self.assertTrue(hasattr(handler.hook_instance, "run_hook"))
|
|
30
|
+
|
|
31
|
+
# Execute the hook to ensure imports work at runtime
|
|
32
|
+
mock_conn = Mock()
|
|
33
|
+
handler.execute(connection=mock_conn, parameters={})
|
|
34
|
+
|
|
35
|
+
def test_hook_with_dynamic_imports(self) -> None:
|
|
36
|
+
"""Test that a hook file can import modules dynamically inside run_hook.
|
|
37
|
+
|
|
38
|
+
This test verifies that PUM hook files can properly import modules
|
|
39
|
+
during hook execution (inside run_hook), not just at module load time.
|
|
40
|
+
This is critical because hooks may need to import modules based on runtime
|
|
41
|
+
conditions or call functions that themselves import modules.
|
|
42
|
+
|
|
43
|
+
This was the bug that wasn't caught by existing tests - all previous tests
|
|
44
|
+
used top-level imports which happen during __init__, not during execute().
|
|
45
|
+
|
|
46
|
+
Also tests that hooks can be executed multiple times with consistent behavior.
|
|
47
|
+
"""
|
|
48
|
+
test_dir = Path("test") / "data" / "hook_dynamic_import"
|
|
49
|
+
hook_file = test_dir / "app" / "create_hook.py"
|
|
50
|
+
|
|
51
|
+
# Create hook handler - this should not raise an error
|
|
52
|
+
handler = HookHandler(base_path=test_dir, file=str(hook_file.relative_to(test_dir)))
|
|
53
|
+
|
|
54
|
+
# Verify the hook was loaded correctly
|
|
55
|
+
self.assertIsNotNone(handler.hook_instance)
|
|
56
|
+
self.assertTrue(hasattr(handler.hook_instance, "run_hook"))
|
|
57
|
+
|
|
58
|
+
# Execute the hook - this is where the dynamic import happens
|
|
59
|
+
# Without proper sys.path management during execute(), this will fail
|
|
60
|
+
mock_conn = Mock()
|
|
61
|
+
handler.execute(connection=mock_conn, parameters={})
|
|
62
|
+
|
|
63
|
+
# Execute again to verify it works multiple times
|
|
64
|
+
handler.execute(connection=mock_conn, parameters={})
|
pum-1.3.2/test/test_hooks.py
DELETED
|
@@ -1,252 +0,0 @@
|
|
|
1
|
-
"""Test module for hook functionality."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
import unittest
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
from unittest.mock import Mock
|
|
7
|
-
|
|
8
|
-
from pum.hook import HookHandler
|
|
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
|
-
|
|
29
|
-
class TestHooks(unittest.TestCase):
|
|
30
|
-
"""Test the hook functionality."""
|
|
31
|
-
|
|
32
|
-
def test_hook_with_sibling_imports(self) -> None:
|
|
33
|
-
"""Test that a hook file can import from a sibling directory.
|
|
34
|
-
|
|
35
|
-
This test verifies that PUM hook files can properly import from sibling
|
|
36
|
-
directories. The test uses a hook file in app/ that imports from view/,
|
|
37
|
-
which is a sibling directory.
|
|
38
|
-
"""
|
|
39
|
-
import sys
|
|
40
|
-
|
|
41
|
-
test_dir = Path("test") / "data" / "hook_sibling_imports"
|
|
42
|
-
hook_file = test_dir / "app" / "create_hook.py"
|
|
43
|
-
|
|
44
|
-
# Clear any previously imported modules to ensure fresh import
|
|
45
|
-
modules_to_remove = [key for key in sys.modules if "view.helper" in key or "view" == key]
|
|
46
|
-
for module in modules_to_remove:
|
|
47
|
-
del sys.modules[module]
|
|
48
|
-
|
|
49
|
-
# Create hook handler - this should not raise an error
|
|
50
|
-
handler = HookHandler(base_path=test_dir, file=str(hook_file.relative_to(test_dir)))
|
|
51
|
-
|
|
52
|
-
# Verify the hook was loaded correctly
|
|
53
|
-
self.assertIsNotNone(handler.hook_instance)
|
|
54
|
-
self.assertTrue(hasattr(handler.hook_instance, "run_hook"))
|
|
55
|
-
|
|
56
|
-
# At this point sys.path should not contain the hook paths anymore
|
|
57
|
-
test_dir_str = str(test_dir.resolve())
|
|
58
|
-
app_dir_str = str((test_dir / "app").resolve())
|
|
59
|
-
self.assertNotIn(test_dir_str, sys.path, "base_path should have been removed from sys.path")
|
|
60
|
-
self.assertNotIn(app_dir_str, sys.path, "parent dir should have been removed from sys.path")
|
|
61
|
-
|
|
62
|
-
# Execute the hook to ensure imports work at runtime
|
|
63
|
-
# This is where the bug should manifest - view.helper won't be importable
|
|
64
|
-
mock_conn = Mock()
|
|
65
|
-
handler.execute(connection=mock_conn, parameters={})
|
|
66
|
-
|
|
67
|
-
def test_hook_with_dynamic_sibling_imports(self) -> None:
|
|
68
|
-
"""Test that a hook file can dynamically import from a sibling directory at runtime.
|
|
69
|
-
|
|
70
|
-
This test verifies that PUM hook files can properly import from sibling
|
|
71
|
-
directories even when the import happens inside run_hook (not at module load time).
|
|
72
|
-
"""
|
|
73
|
-
import sys
|
|
74
|
-
|
|
75
|
-
test_dir = Path("test") / "data" / "hook_sibling_imports"
|
|
76
|
-
hook_file = test_dir / "app" / "dynamic_import_hook.py"
|
|
77
|
-
|
|
78
|
-
# Clear any previously imported modules to ensure fresh import
|
|
79
|
-
modules_to_remove = [key for key in sys.modules if "view.helper" in key or "view" == key]
|
|
80
|
-
for module in modules_to_remove:
|
|
81
|
-
del sys.modules[module]
|
|
82
|
-
|
|
83
|
-
# Create hook handler
|
|
84
|
-
handler = HookHandler(base_path=test_dir, file=str(hook_file.relative_to(test_dir)))
|
|
85
|
-
|
|
86
|
-
# Verify the hook was loaded correctly
|
|
87
|
-
self.assertIsNotNone(handler.hook_instance)
|
|
88
|
-
self.assertTrue(hasattr(handler.hook_instance, "run_hook"))
|
|
89
|
-
|
|
90
|
-
# Execute the hook - this should fail because sys.path modifications were removed
|
|
91
|
-
mock_conn = Mock()
|
|
92
|
-
handler.execute(connection=mock_conn, parameters={})
|
|
93
|
-
|
|
94
|
-
def test_hook_with_local_imports(self) -> None:
|
|
95
|
-
"""Test that a hook file can import from its own directory.
|
|
96
|
-
|
|
97
|
-
This test verifies that PUM hook files can properly import from their
|
|
98
|
-
own directory. The test uses a hook file in app/ that imports from
|
|
99
|
-
local_helper.py in the same directory.
|
|
100
|
-
"""
|
|
101
|
-
test_dir = Path("test") / "data" / "hook_local_imports"
|
|
102
|
-
hook_file = test_dir / "app" / "create_hook.py"
|
|
103
|
-
|
|
104
|
-
# Create hook handler - this should not raise an error
|
|
105
|
-
handler = HookHandler(base_path=test_dir, file=str(hook_file.relative_to(test_dir)))
|
|
106
|
-
|
|
107
|
-
# Verify the hook was loaded correctly
|
|
108
|
-
self.assertIsNotNone(handler.hook_instance)
|
|
109
|
-
self.assertTrue(hasattr(handler.hook_instance, "run_hook"))
|
|
110
|
-
|
|
111
|
-
# Execute the hook to ensure imports work at runtime
|
|
112
|
-
mock_conn = Mock()
|
|
113
|
-
handler.execute(connection=mock_conn, parameters={})
|
|
114
|
-
|
|
115
|
-
def test_hook_cleanup_imports(self) -> None:
|
|
116
|
-
"""Test that hook imports can be cleaned up to prevent conflicts when switching versions.
|
|
117
|
-
|
|
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.
|
|
121
|
-
"""
|
|
122
|
-
test_dir = Path("test") / "data" / "hook_sibling_imports"
|
|
123
|
-
hook_file = test_dir / "app" / "create_hook.py"
|
|
124
|
-
|
|
125
|
-
# Clear any previously imported modules to ensure fresh import
|
|
126
|
-
modules_to_remove = [key for key in sys.modules if "view" in key]
|
|
127
|
-
for module in modules_to_remove:
|
|
128
|
-
del sys.modules[module]
|
|
129
|
-
|
|
130
|
-
# Load the hook - this will import view.helper
|
|
131
|
-
handler = HookHandler(base_path=test_dir, file=str(hook_file.relative_to(test_dir)))
|
|
132
|
-
|
|
133
|
-
# Verify view.helper is in sys.modules
|
|
134
|
-
view_module_found = any("view" in mod for mod in sys.modules)
|
|
135
|
-
self.assertTrue(
|
|
136
|
-
view_module_found, "view module should be in sys.modules after loading hook"
|
|
137
|
-
)
|
|
138
|
-
|
|
139
|
-
# Execute the hook to make sure it works before cleanup
|
|
140
|
-
mock_conn = Mock()
|
|
141
|
-
handler.execute(connection=mock_conn, parameters={})
|
|
142
|
-
|
|
143
|
-
# Clean up imports via path-based cleanup
|
|
144
|
-
cleanup_modules_by_path(test_dir)
|
|
145
|
-
|
|
146
|
-
# Verify that view modules were removed from sys.modules
|
|
147
|
-
view_modules_after = [mod for mod in sys.modules if "view.helper" in mod or mod == "view"]
|
|
148
|
-
self.assertEqual(
|
|
149
|
-
len(view_modules_after),
|
|
150
|
-
0,
|
|
151
|
-
f"view modules should be removed from sys.modules after cleanup, but found: {view_modules_after}",
|
|
152
|
-
)
|
|
153
|
-
|
|
154
|
-
def test_hook_cleanup_and_reload(self) -> None:
|
|
155
|
-
"""Test that hooks can be reloaded after cleanup without conflicts.
|
|
156
|
-
|
|
157
|
-
This test simulates switching between module versions by loading a hook,
|
|
158
|
-
cleaning it up, and loading it again.
|
|
159
|
-
"""
|
|
160
|
-
test_dir = Path("test") / "data" / "hook_sibling_imports"
|
|
161
|
-
hook_file = test_dir / "app" / "create_hook.py"
|
|
162
|
-
|
|
163
|
-
# Clear any previously imported modules
|
|
164
|
-
modules_to_remove = [key for key in sys.modules if "view" in key]
|
|
165
|
-
for module in modules_to_remove:
|
|
166
|
-
del sys.modules[module]
|
|
167
|
-
|
|
168
|
-
# First load
|
|
169
|
-
handler1 = HookHandler(base_path=test_dir, file=str(hook_file.relative_to(test_dir)))
|
|
170
|
-
mock_conn = Mock()
|
|
171
|
-
handler1.execute(connection=mock_conn, parameters={})
|
|
172
|
-
|
|
173
|
-
# Get the module object for comparison
|
|
174
|
-
view_module_id_1 = None
|
|
175
|
-
for mod_name in sys.modules:
|
|
176
|
-
if mod_name == "view" or mod_name.startswith("view."):
|
|
177
|
-
view_module_id_1 = id(sys.modules[mod_name])
|
|
178
|
-
break
|
|
179
|
-
|
|
180
|
-
# Clean up via path-based cleanup
|
|
181
|
-
cleanup_modules_by_path(test_dir)
|
|
182
|
-
|
|
183
|
-
# Verify cleanup worked
|
|
184
|
-
view_modules = [mod for mod in sys.modules if "view.helper" in mod or mod == "view"]
|
|
185
|
-
self.assertEqual(len(view_modules), 0, "Modules should be cleaned up")
|
|
186
|
-
|
|
187
|
-
# Second load - should work without conflicts
|
|
188
|
-
handler2 = HookHandler(base_path=test_dir, file=str(hook_file.relative_to(test_dir)))
|
|
189
|
-
handler2.execute(connection=mock_conn, parameters={})
|
|
190
|
-
|
|
191
|
-
# Verify it's a fresh import (different module object)
|
|
192
|
-
view_module_id_2 = None
|
|
193
|
-
for mod_name in sys.modules:
|
|
194
|
-
if mod_name == "view" or mod_name.startswith("view."):
|
|
195
|
-
view_module_id_2 = id(sys.modules[mod_name])
|
|
196
|
-
break
|
|
197
|
-
|
|
198
|
-
# Both should have found view modules
|
|
199
|
-
self.assertIsNotNone(view_module_id_1, "First load should have imported view")
|
|
200
|
-
self.assertIsNotNone(view_module_id_2, "Second load should have imported view")
|
|
201
|
-
|
|
202
|
-
# Clean up after test
|
|
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.2 → pum-1.3.3}/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
|