PyMemoryEditor 3.3.1__tar.gz → 3.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.
Files changed (212) hide show
  1. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PKG-INFO +1 -1
  2. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/__init__.py +1 -1
  3. pymemoryeditor-3.3.3/PyMemoryEditor/app/_gc.py +71 -0
  4. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/application.py +13 -4
  5. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/mcp/policy.py +7 -3
  6. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/abstract.py +67 -5
  7. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/util/pattern.py +10 -1
  8. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/win32/functions.py +7 -4
  9. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/conf.py +1 -1
  10. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/pointer-scan.md +8 -2
  11. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/pyproject.toml +1 -1
  12. pymemoryeditor-3.3.3/tests/app/conftest.py +31 -0
  13. pymemoryeditor-3.3.3/tests/app/test_app_gc.py +58 -0
  14. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_app_smoke.py +30 -0
  15. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/core/test_abstract_process.py +18 -0
  16. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/conftest.py +45 -0
  17. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/test_parsing.py +1 -0
  18. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/test_policy.py +6 -2
  19. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/test_toolset.py +1 -0
  20. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_bitness_fallback_warns.py +23 -4
  21. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/pointer/test_pointer_scan.py +43 -2
  22. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/test_pattern_compile.py +7 -0
  23. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/.flake8 +0 -0
  24. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/.gitignore +0 -0
  25. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/.readthedocs.yaml +0 -0
  26. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/CODE_OF_CONDUCT.md +0 -0
  27. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/CONTRIBUTING.md +0 -0
  28. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/LICENSE +0 -0
  29. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/Makefile +0 -0
  30. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/__main__.py +0 -0
  31. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/__init__.py +0 -0
  32. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/_auto_refresh_dialog.py +0 -0
  33. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/_icon.py +0 -0
  34. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/_widgets.py +0 -0
  35. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/assets/icon.svg +0 -0
  36. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/cheat_entry.py +0 -0
  37. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/cheat_poll_worker.py +0 -0
  38. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/cheat_table.py +0 -0
  39. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/log_console_dialog.py +0 -0
  40. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/main_window.py +0 -0
  41. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/memory_map_dialog.py +0 -0
  42. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/memory_viewer_dialog.py +0 -0
  43. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/modules_dialog.py +0 -0
  44. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/open_process_dialog.py +0 -0
  45. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/pointer_chain_dialog.py +0 -0
  46. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/pointer_scan_dialog.py +0 -0
  47. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/results_view.py +0 -0
  48. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/scan_types.py +0 -0
  49. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/scan_worker.py +0 -0
  50. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/scanner_panel.py +0 -0
  51. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/threads_dialog.py +0 -0
  52. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/app/value_types.py +0 -0
  53. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/enums.py +0 -0
  54. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/linux/functions.py +0 -0
  55. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/linux/libc.py +0 -0
  56. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/linux/process.py +0 -0
  57. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/linux/types.py +0 -0
  58. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/macos/__init__.py +0 -0
  59. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/macos/functions.py +0 -0
  60. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/macos/libsystem.py +0 -0
  61. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/macos/process.py +0 -0
  62. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/macos/types.py +0 -0
  63. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/mcp/__init__.py +0 -0
  64. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/mcp/__main__.py +0 -0
  65. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/mcp/config.py +0 -0
  66. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/mcp/procinfo.py +0 -0
  67. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/mcp/server.py +0 -0
  68. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/mcp/session.py +0 -0
  69. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/mcp/toolset.py +0 -0
  70. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/__init__.py +0 -0
  71. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/_guard.py +0 -0
  72. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/errors.py +0 -0
  73. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/info.py +0 -0
  74. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/module_info.py +0 -0
  75. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/pointer_scan.py +0 -0
  76. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/region.py +0 -0
  77. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/remote_pointer.py +0 -0
  78. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/scanning.py +0 -0
  79. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/thread_info.py +0 -0
  80. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/process/util.py +0 -0
  81. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/py.typed +0 -0
  82. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/util/__init__.py +0 -0
  83. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/util/convert.py +0 -0
  84. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/util/scan.py +0 -0
  85. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/util/scan_numpy.py +0 -0
  86. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/win32/enums/__init__.py +0 -0
  87. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
  88. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
  89. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
  90. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
  91. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
  92. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/win32/process.py +0 -0
  93. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/PyMemoryEditor/win32/types.py +0 -0
  94. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/README.md +0 -0
  95. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/SECURITY.md +0 -0
  96. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/assets/screenshots/app.png +0 -0
  97. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/assets/social-preview.html +0 -0
  98. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/assets/social-preview.png +0 -0
  99. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/_static/.gitkeep +0 -0
  100. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/_static/custom.css +0 -0
  101. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/_templates/layout.html +0 -0
  102. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/enums.md +0 -0
  103. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/errors.md +0 -0
  104. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/index.md +0 -0
  105. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/memory-region.md +0 -0
  106. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/module-info.md +0 -0
  107. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/openprocess.md +0 -0
  108. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/pointer-path.md +0 -0
  109. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/remote-pointer.md +0 -0
  110. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/thread-info.md +0 -0
  111. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/api/utilities.md +0 -0
  112. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/app.md +0 -0
  113. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/contributing.md +0 -0
  114. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/funding.md +0 -0
  115. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/glossary.md +0 -0
  116. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/allocate-free.md +0 -0
  117. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/logging.md +0 -0
  118. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/memory-regions.md +0 -0
  119. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/modules-threads.md +0 -0
  120. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/opening-process.md +0 -0
  121. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/pattern-scan.md +0 -0
  122. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/pointers.md +0 -0
  123. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/read-write.md +0 -0
  124. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/guide/searching.md +0 -0
  125. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/index.md +0 -0
  126. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/installation.md +0 -0
  127. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/license.md +0 -0
  128. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/mcp.md +0 -0
  129. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/platform-notes.md +0 -0
  130. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/quickstart.md +0 -0
  131. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/requirements.txt +0 -0
  132. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/troubleshooting.md +0 -0
  133. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/docs/why.md +0 -0
  134. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/__init__.py +0 -0
  135. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_app_cheat_entry.py +0 -0
  136. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_app_pointer_parsing.py +0 -0
  137. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_app_scan_request.py +0 -0
  138. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_app_value_types.py +0 -0
  139. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_app_widgets.py +0 -0
  140. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_auto_refresh_dialog.py +0 -0
  141. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_cheat_poll_worker.py +0 -0
  142. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_main_window_lifecycle.py +0 -0
  143. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_open_process_dialog.py +0 -0
  144. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_refine_scan_worker.py +0 -0
  145. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/app/test_results_model.py +0 -0
  146. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/conftest.py +0 -0
  147. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/core/__init__.py +0 -0
  148. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/core/test_errors.py +0 -0
  149. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/core/test_logger.py +0 -0
  150. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/__init__.py +0 -0
  151. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/test_argument_fuzz.py +0 -0
  152. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/test_fake_fidelity.py +0 -0
  153. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/test_live.py +0 -0
  154. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/test_registry.py +0 -0
  155. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/test_server.py +0 -0
  156. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/mcp_server/test_session.py +0 -0
  157. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/__init__.py +0 -0
  158. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_allocate_free.py +0 -0
  159. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_bufflength_inference.py +0 -0
  160. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_editor.py +0 -0
  161. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_narrow_width_consistency.py +0 -0
  162. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_partial_io.py +0 -0
  163. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_read_into.py +0 -0
  164. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_search_argument_errors.py +0 -0
  165. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_str_boundary.py +0 -0
  166. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_str_decode_consistency.py +0 -0
  167. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_typed_accessors.py +0 -0
  168. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_unaligned_scan.py +0 -0
  169. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_write_safety.py +0 -0
  170. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_write_span.py +0 -0
  171. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/memory/test_write_str_bytes_width.py +0 -0
  172. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/__init__.py +0 -0
  173. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_bitness.py +0 -0
  174. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_closed_generators.py +0 -0
  175. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_linux_error_hints.py +0 -0
  176. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_linux_pid_reuse.py +0 -0
  177. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_linux_proc_decoding.py +0 -0
  178. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_linux_snapshot_passthrough.py +0 -0
  179. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_linux_types.py +0 -0
  180. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_macos_close.py +0 -0
  181. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_macos_load_commands.py +0 -0
  182. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_macos_protect.py +0 -0
  183. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_macos_region_walk.py +0 -0
  184. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_macos_snapshot.py +0 -0
  185. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_macos_threads.py +0 -0
  186. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_macos_transient_reads.py +0 -0
  187. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_macos_types.py +0 -0
  188. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_win32_backend_guards.py +0 -0
  189. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_win32_permissions.py +0 -0
  190. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_win32_region_walk.py +0 -0
  191. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_win32_toolhelp.py +0 -0
  192. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/platforms/test_win32_wow64.py +0 -0
  193. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/pointer/__init__.py +0 -0
  194. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/pointer/test_pointer_chain.py +0 -0
  195. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/pointer/test_remote_pointer.py +0 -0
  196. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/process/__init__.py +0 -0
  197. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/process/test_foreign_process.py +0 -0
  198. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/process/test_memory_region.py +0 -0
  199. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/process/test_module_enumeration.py +0 -0
  200. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/process/test_partial_name_match.py +0 -0
  201. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/process/test_process_enumeration.py +0 -0
  202. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/process/test_process_lookup.py +0 -0
  203. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/process/test_thread_enumeration.py +0 -0
  204. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/__init__.py +0 -0
  205. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/_fakes.py +0 -0
  206. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/test_chunking_integration.py +0 -0
  207. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/test_pattern_scan.py +0 -0
  208. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/test_region_snapshot.py +0 -0
  209. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/test_scan.py +0 -0
  210. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/test_scan_numpy.py +0 -0
  211. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/test_scan_properties.py +0 -0
  212. {pymemoryeditor-3.3.1 → pymemoryeditor-3.3.3}/tests/scan/test_scanning_helper.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: PyMemoryEditor
3
- Version: 3.3.1
3
+ Version: 3.3.3
4
4
  Summary: Read, write and scan process memory in a few lines of Python — Cheat Engine-style scans, pointer chains and AOB search on Windows, Linux and macOS.
5
5
  Project-URL: Homepage, https://github.com/JeanExtreme002/PyMemoryEditor
6
6
  Project-URL: Documentation, https://pymemoryeditor.readthedocs.io
@@ -8,7 +8,7 @@ Supported platforms: Windows, Linux and macOS (32-bit and 64-bit).
8
8
  """
9
9
 
10
10
  __author__ = "Jean Loui Bernard Silva de Jesus"
11
- __version__ = "3.3.1"
11
+ __version__ = "3.3.3"
12
12
 
13
13
  import logging
14
14
  import sys
@@ -0,0 +1,71 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Run Python's cyclic garbage collector on the GUI thread only.
4
+
5
+ The automatic collector runs in whichever thread happens to allocate when a
6
+ threshold is crossed -- in this app, often a ``QThread`` worker in the middle
7
+ of a scan. If that collection frees a reference cycle holding Qt objects that
8
+ belong to the GUI thread, they are destroyed from the worker, which crashes
9
+ the process (a segfault on macOS, an access violation on Windows; seen on CI
10
+ in the pointer-scan worker's ``_hand_over``).
11
+
12
+ So while the app runs, automatic collection is switched off and a timer on the
13
+ GUI thread collects instead, generation by generation, as the automatic
14
+ collector would have. Plain reference counting still frees everything that is
15
+ not in a cycle immediately, in any thread.
16
+ """
17
+
18
+ import contextlib
19
+ import gc
20
+ from typing import Iterator, Optional
21
+
22
+ from PySide6.QtCore import QObject, QTimer
23
+
24
+
25
+ #: How often the GUI thread checks whether a collection is due.
26
+ COLLECT_INTERVAL_MS = 500
27
+
28
+
29
+ class MainThreadGarbageCollector(QObject):
30
+ """Disable automatic collection and collect from a GUI-thread timer.
31
+
32
+ Create it on the GUI thread; :meth:`stop` restores the collector's previous
33
+ state, so an embedder's own setting survives the app.
34
+ """
35
+
36
+ def __init__(
37
+ self, parent: Optional[QObject] = None, interval_ms: int = COLLECT_INTERVAL_MS
38
+ ) -> None:
39
+ super().__init__(parent)
40
+ self._was_enabled = gc.isenabled()
41
+ self._threshold = gc.get_threshold()
42
+ gc.disable()
43
+ self._timer = QTimer(self)
44
+ self._timer.timeout.connect(self.collect_if_due)
45
+ self._timer.start(interval_ms)
46
+
47
+ def collect_if_due(self) -> None:
48
+ """Collect each generation whose allocation count passed its threshold."""
49
+ count0, count1, count2 = gc.get_count()
50
+ if count0 > self._threshold[0]:
51
+ gc.collect(0)
52
+ if count1 > self._threshold[1]:
53
+ gc.collect(1)
54
+ if count2 > self._threshold[2]:
55
+ gc.collect(2)
56
+
57
+ def stop(self) -> None:
58
+ """Stop the timer and give automatic collection back."""
59
+ self._timer.stop()
60
+ if self._was_enabled:
61
+ gc.enable()
62
+
63
+
64
+ @contextlib.contextmanager
65
+ def garbage_collected_on_gui_thread(parent: Optional[QObject] = None) -> Iterator[None]:
66
+ """Run the body with :class:`MainThreadGarbageCollector` in charge."""
67
+ collector = MainThreadGarbageCollector(parent)
68
+ try:
69
+ yield
70
+ finally:
71
+ collector.stop()
@@ -560,10 +560,7 @@ def main(argv=None):
560
560
  from PySide6.QtCore import QSettings
561
561
  from PySide6.QtWidgets import QApplication
562
562
 
563
- from ._widgets import detached_workers_running
564
- from .main_window import MainWindow
565
- from .open_process_dialog import OpenProcessDialog
566
-
563
+ from ._gc import garbage_collected_on_gui_thread
567
564
  from ._icon import app_icon
568
565
 
569
566
  app = QApplication.instance() or QApplication(argv)
@@ -577,6 +574,18 @@ def main(argv=None):
577
574
  saved_theme = str(QSettings().value("theme", DEFAULT_THEME_ID))
578
575
  apply_theme(app, saved_theme)
579
576
 
577
+ # Every dialog from here on runs QThread workers; see _gc for why the
578
+ # cyclic collector must not run inside them.
579
+ with garbage_collected_on_gui_thread(app):
580
+ _pick_process_and_run(app)
581
+
582
+
583
+ def _pick_process_and_run(app) -> None:
584
+ """The picker, then the main window until the event loop exits."""
585
+ from ._widgets import detached_workers_running
586
+ from .main_window import MainWindow
587
+ from .open_process_dialog import OpenProcessDialog
588
+
580
589
  picker = OpenProcessDialog()
581
590
  if picker.exec() != picker.DialogCode.Accepted:
582
591
  return
@@ -218,6 +218,12 @@ class AccessDecision:
218
218
  needs_approval: bool = False
219
219
 
220
220
 
221
+ def _self_and_ancestor_pids() -> FrozenSet[int]:
222
+ """This process, its parent and every ancestor: what ``protect_self``
223
+ refuses. A function of its own so tests can swap the real chain out."""
224
+ return frozenset({os.getpid(), os.getppid()}) | ancestor_pids()
225
+
226
+
221
227
  class ProcessPolicy:
222
228
  """Decides whether a given ``(pid, name)`` may be attached to.
223
229
 
@@ -282,9 +288,7 @@ class ProcessPolicy:
282
288
  # further up. Taken once: the chain that launched the server does not
283
289
  # change while it runs.
284
290
  self._protected: FrozenSet[int] = (
285
- frozenset({os.getpid(), os.getppid()}) | ancestor_pids()
286
- if protect_self
287
- else frozenset()
291
+ _self_and_ancestor_pids() if protect_self else frozenset()
288
292
  )
289
293
  self._identify = identify
290
294
  # Only Linux truncates names; applying the rule elsewhere would let a
@@ -1303,6 +1303,15 @@ class AbstractProcess(ABC):
1303
1303
  bounds it. (To abort the map-building phase, raise from
1304
1304
  ``progress_callback``.)
1305
1305
 
1306
+ :raises ValueError: ``ptr_size`` other than 4 or 8, ``max_depth`` or
1307
+ ``max_results`` below 1, or a negative ``max_offset``.
1308
+ :raises ClosedProcess: the process was closed (also raised by the
1309
+ returned generator once it is).
1310
+
1311
+ Arguments and access are checked at the call, like every ``search_*``
1312
+ method; the pointer map is built and walked lazily, as the generator
1313
+ is consumed.
1314
+
1306
1315
  Example
1307
1316
  -------
1308
1317
  ::
@@ -1316,11 +1325,26 @@ class AbstractProcess(ABC):
1316
1325
  live = path.rebase(process).to_pointer(process, pytype=int, bufflength=4)
1317
1326
  live.value = 9999
1318
1327
  """
1319
- from .pointer_scan import (
1320
- AddressRanges,
1321
- build_pointer_map,
1322
- find_pointer_paths,
1323
- )
1328
+ # Validate eagerly, like every search_* method: a bad argument or a
1329
+ # closed process raises here, at the call, not on the first next().
1330
+ # The map build and path search stay lazy in _iter_pointer_paths.
1331
+ #
1332
+ # A subclass written against the 3.3 contract implements the listings
1333
+ # itself and need not define these hooks (see __init_subclass__): each
1334
+ # check runs only where the subclass provides it, instead of hitting
1335
+ # the NotImplementedError of the base _require_open.
1336
+ cls = type(self)
1337
+ guarded = cls._require_open is not AbstractProcess._require_open
1338
+ if guarded or cls._require_scanning is not AbstractProcess._require_scanning:
1339
+ self._require_scanning()
1340
+
1341
+ if max_depth < 1:
1342
+ raise ValueError("max_depth must be at least 1.")
1343
+ if max_offset < 0:
1344
+ raise ValueError("max_offset must not be negative.")
1345
+ if max_results is not None and max_results < 1:
1346
+ # 0 would build the whole pointer map only to yield nothing.
1347
+ raise ValueError("max_results must be at least 1 (or None for no cap).")
1324
1348
 
1325
1349
  if ptr_size is None:
1326
1350
  ptr_size = self.pointer_size
@@ -1330,6 +1354,44 @@ class AbstractProcess(ABC):
1330
1354
  "ptr_size must be 4 (32-bit target) or 8 (64-bit target)."
1331
1355
  )
1332
1356
 
1357
+ scan = self._iter_pointer_paths(
1358
+ target_address,
1359
+ max_depth=max_depth,
1360
+ max_offset=max_offset,
1361
+ ptr_size=ptr_size,
1362
+ aligned=aligned,
1363
+ writable_only=writable_only,
1364
+ static_ranges=static_ranges,
1365
+ max_results=max_results,
1366
+ memory_regions=memory_regions,
1367
+ progress_callback=progress_callback,
1368
+ should_stop=should_stop,
1369
+ )
1370
+ return guard_generator(scan, self._require_open) if guarded else scan
1371
+
1372
+ def _iter_pointer_paths(
1373
+ self,
1374
+ target_address: int,
1375
+ *,
1376
+ max_depth: int,
1377
+ max_offset: int,
1378
+ ptr_size: int,
1379
+ aligned: bool,
1380
+ writable_only: bool,
1381
+ static_ranges: Optional[Sequence[Tuple[int, int]]],
1382
+ max_results: Optional[int],
1383
+ memory_regions: Optional[Sequence[MemoryRegion]],
1384
+ progress_callback: Optional["Callable[[float], None]"],
1385
+ should_stop: Optional["Callable[[], bool]"],
1386
+ ) -> Generator["PointerPath", None, None]:
1387
+ """The lazy body of :meth:`scan_pointer_paths`, run once arguments are
1388
+ validated."""
1389
+ from .pointer_scan import (
1390
+ AddressRanges,
1391
+ build_pointer_map,
1392
+ find_pointer_paths,
1393
+ )
1394
+
1333
1395
  if memory_regions is None:
1334
1396
  memory_regions = list(self.get_memory_regions())
1335
1397
 
@@ -71,9 +71,18 @@ def compile_pattern(
71
71
  :raises ValueError: malformed IDA-style token, or ``byte_length`` omitted
72
72
  for a regex / pre-compiled pattern.
73
73
  :raises TypeError: if ``pattern`` is not a ``str``, ``bytes`` or
74
- ``re.Pattern[bytes]``.
74
+ ``re.Pattern[bytes]`` — a compiled ``str`` regex included, as it can
75
+ never match process memory.
75
76
  """
76
77
  if isinstance(pattern, re.Pattern):
78
+ # A str pattern can't match process memory: it would only fail later,
79
+ # deep inside the scan, with "cannot use a string pattern on a
80
+ # bytes-like object".
81
+ if not isinstance(pattern.pattern, bytes):
82
+ raise TypeError(
83
+ "Pattern must be a compiled bytes regex (re.compile(rb'...')), "
84
+ "not a str one."
85
+ )
77
86
  if byte_length <= 0:
78
87
  raise ValueError(
79
88
  "byte_length= is required when passing a pre-compiled regex "
@@ -251,6 +251,8 @@ _PROCESSOR_ARCHITECTURE_64BIT = (6, 9, 12)
251
251
  _OS_IS_64BIT = (
252
252
  _native_system_information.wProcessorArchitecture in _PROCESSOR_ARCHITECTURE_64BIT
253
253
  )
254
+ # The running Python's bitness: the fallback guess when a target's can't be read.
255
+ _HOST_IS_64BIT = ctypes.sizeof(ctypes.c_void_p) == 8
254
256
 
255
257
 
256
258
  def mbi_class_for_handle(process_handle: int):
@@ -318,12 +320,13 @@ def IsProcess64Bit(process_handle: int) -> bool:
318
320
  return detected
319
321
 
320
322
  _logger.warning(
321
- "IsWow64Process failed (err=%d); assuming the target is 64-bit "
322
- "(host bitness). Pointer-width detection may be wrong if the target "
323
- "is actually a 32-bit (WOW64) process.",
323
+ "IsWow64Process failed (err=%d); assuming the target matches the "
324
+ "host bitness (%d-bit). Pointer-width detection may be wrong if it "
325
+ "does not.",
324
326
  ctypes.get_last_error(),
327
+ 64 if _HOST_IS_64BIT else 32,
325
328
  )
326
- return True
329
+ return _HOST_IS_64BIT
327
330
 
328
331
 
329
332
  T = TypeVar("T")
@@ -23,7 +23,7 @@ copyright = f"{datetime.now().year}, {author}"
23
23
  try:
24
24
  from PyMemoryEditor import __version__ as release
25
25
  except Exception: # pragma: no cover - docs can build without the package installed
26
- release = "3.3.1"
26
+ release = "3.3.3"
27
27
 
28
28
  version = ".".join(release.split(".")[:2])
29
29
 
@@ -43,8 +43,8 @@ It carries everything you need to reconstruct the chain in another run.
43
43
  (default — faster and usually correct).
44
44
  :param static_ranges: explicit ``(start, size)`` ranges to treat as valid
45
45
  chain bases. Defaults to the image range of every loaded module.
46
- :param int max_results: stop after yielding this many paths.
47
- Recommended for shallow exploration.
46
+ :param int max_results: stop after yielding this many paths (at least 1;
47
+ ``None`` for no cap). Recommended for shallow exploration.
48
48
  :param memory_regions: optional snapshot from
49
49
  :py:meth:`snapshot_memory_regions`.
50
50
  :param progress_callback: a callable ``callback(fraction)`` invoked as the
@@ -55,6 +55,12 @@ It carries everything you need to reconstruct the chain in another run.
55
55
  yielded. The search can run a long time between two paths, so this is
56
56
  how a deadline or a cancel button bounds it.
57
57
  :returns: a generator of :py:class:`PointerPath`.
58
+ :raises ValueError: ``ptr_size`` other than 4 or 8, ``max_depth`` or
59
+ ``max_results`` below 1, or a negative ``max_offset``.
60
+ :raises ClosedProcess: the process was closed.
61
+
62
+ Arguments are checked when you call the method; the pointer map is built
63
+ and walked lazily, as you iterate.
58
64
  ```
59
65
 
60
66
  ## Tuning the scan
@@ -191,7 +191,6 @@ exclude = [
191
191
  "/codecov.yml",
192
192
  "/.coveragerc-lib",
193
193
  "/.coveragerc-mcp",
194
- "/.coveragerc-app",
195
194
  "/scripts",
196
195
  "/server.json",
197
196
  ]
@@ -242,6 +241,7 @@ skip_empty = true
242
241
  [tool.pytest.ini_options]
243
242
  addopts = "-n auto --dist=loadfile"
244
243
  markers = [
244
+ "real_self_protection: MCP tests that check the real self/ancestor protection; the others see stand-in pids (tests/mcp_server/conftest.py)",
245
245
  "slow: integration tests that read/scan the current process's whole address space (minutes, not seconds)",
246
246
  "scan_heavy: tests that scan a *live* process's entire address space. Separated from `slow` because the cost is wildly platform-dependent: a macOS process exposes ~390 GiB across ~145 regions and a Windows one is not much better, so on a CI runner each scan is minutes rather than the ~1s it takes on a developer machine. CI runs these on Linux only (see the build-mcp job); the library's own live-scan coverage on the other two platforms comes from the build/build-macos jobs.",
247
247
  ]
@@ -0,0 +1,31 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """Shared setup for the GUI app tests."""
4
+
5
+ import gc
6
+
7
+ import pytest
8
+
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def _collect_garbage_on_the_test_thread():
12
+ """Keep the cyclic collector out of the tests' QThread workers.
13
+
14
+ The app does this with ``MainThreadGarbageCollector`` (``app/_gc.py``), but
15
+ the tests build widgets and workers without going through ``main()``. An
16
+ automatic collection inside a worker can free reference cycles that hold
17
+ the GUI thread's Qt objects -- left over from this or an earlier test --
18
+ and destroy them from the wrong thread, which crashed the pytest worker on
19
+ CI (segfault on macOS, access violation on Windows). So automatic
20
+ collection is off during each test, and the garbage is collected here, on
21
+ the test's own thread, at teardown.
22
+ """
23
+ was_enabled = gc.isenabled()
24
+ gc.collect()
25
+ gc.disable()
26
+ try:
27
+ yield
28
+ finally:
29
+ gc.collect()
30
+ if was_enabled:
31
+ gc.enable()
@@ -0,0 +1,58 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ """``MainThreadGarbageCollector``: the cyclic collector runs on the GUI thread."""
4
+
5
+ import gc
6
+ import threading
7
+
8
+ import pytest
9
+
10
+ pytest.importorskip("PySide6")
11
+
12
+
13
+ class _Cycle:
14
+ def __init__(self, sink):
15
+ self.self = self
16
+ self.sink = sink
17
+
18
+ def __del__(self):
19
+ self.sink.append(threading.current_thread() is threading.main_thread())
20
+
21
+
22
+ def test_it_turns_automatic_collection_off_and_back_on(qtbot):
23
+ from PyMemoryEditor.app._gc import MainThreadGarbageCollector
24
+
25
+ gc.enable()
26
+ collector = MainThreadGarbageCollector()
27
+ try:
28
+ assert not gc.isenabled()
29
+ finally:
30
+ collector.stop()
31
+ assert gc.isenabled()
32
+
33
+
34
+ def test_it_keeps_a_caller_disabled_collector_disabled(qtbot):
35
+ from PyMemoryEditor.app._gc import MainThreadGarbageCollector
36
+
37
+ gc.disable()
38
+ try:
39
+ MainThreadGarbageCollector().stop()
40
+ assert not gc.isenabled()
41
+ finally:
42
+ gc.enable()
43
+
44
+
45
+ def test_its_timer_collects_cycles_on_the_gui_thread(qtbot, monkeypatch):
46
+ from PyMemoryEditor.app._gc import MainThreadGarbageCollector
47
+
48
+ gc.collect()
49
+ # Any allocation makes a collection due, so the first tick runs one.
50
+ monkeypatch.setattr(gc, "get_threshold", lambda: (0, 0, 0))
51
+ collector = MainThreadGarbageCollector(interval_ms=10)
52
+ try:
53
+ freed = []
54
+ _Cycle(freed)
55
+ qtbot.waitUntil(lambda: bool(freed), timeout=2000)
56
+ assert freed == [True]
57
+ finally:
58
+ collector.stop()
@@ -131,6 +131,36 @@ def test_main_cli_scopes_sig_dfl_to_the_run(monkeypatch):
131
131
  signal.signal(signal.SIGINT, original)
132
132
 
133
133
 
134
+ def test_main_runs_the_cyclic_collector_on_the_gui_thread_only(monkeypatch):
135
+ """
136
+ While the app runs, automatic collection is off (a timer on the GUI thread
137
+ collects instead), so a collection never runs inside a QThread worker and
138
+ destroys GUI-thread Qt objects from there. It is back on afterwards.
139
+ """
140
+ import gc
141
+
142
+ from PyMemoryEditor.app import application, open_process_dialog
143
+
144
+ seen = {}
145
+
146
+ class _ProbingDialog:
147
+ class DialogCode:
148
+ Accepted = 1
149
+
150
+ def exec(self):
151
+ seen["inside"] = gc.isenabled()
152
+ return 0
153
+
154
+ process = None
155
+
156
+ monkeypatch.setattr(open_process_dialog, "OpenProcessDialog", _ProbingDialog)
157
+
158
+ gc.enable()
159
+ assert application.main(["pymemoryeditor"]) is None
160
+ assert seen["inside"] is False
161
+ assert gc.isenabled()
162
+
163
+
134
164
  def test_scoped_signal_handler_is_a_noop_off_the_main_thread():
135
165
  """
136
166
  ``signal.signal`` only works on the main thread. The helper must degrade to
@@ -9,6 +9,8 @@ keep its own implementations.
9
9
 
10
10
  import os
11
11
 
12
+ import pytest
13
+
12
14
  from PyMemoryEditor import AbstractProcess
13
15
 
14
16
 
@@ -69,6 +71,22 @@ def test_a_subclass_of_the_earlier_contract_still_instantiates_and_runs_its_own_
69
71
  assert list(process.search_by_addresses(int, addresses=[7])) == [(7, "own")]
70
72
  assert list(process.get_memory_regions()) == []
71
73
  assert process.main_thread is None
74
+ # Built on the methods above only: no _require_open needed.
75
+ assert list(process.scan_pointer_paths(0x1000)) == []
76
+ with pytest.raises(ValueError, match="ptr_size"):
77
+ process.scan_pointer_paths(0x1000, ptr_size=3)
78
+
79
+
80
+ def test_scan_pointer_paths_runs_an_earlier_contract_subclass_own_scanning_check():
81
+ """A 3.3-contract subclass that adds its own _require_scanning (but no
82
+ _require_open) still has it run by scan_pointer_paths."""
83
+
84
+ class _ChecksScanning(_ContractV3Process):
85
+ def _require_scanning(self):
86
+ raise PermissionError("no read access")
87
+
88
+ with pytest.raises(PermissionError, match="read access"):
89
+ _ChecksScanning().scan_pointer_paths(0x1000)
72
90
 
73
91
 
74
92
  def test_every_backend_implements_every_raw_hook():
@@ -46,6 +46,7 @@ import pytest
46
46
 
47
47
  from PyMemoryEditor.enums import ScanTypesEnum
48
48
  from PyMemoryEditor.mcp import MemoryToolset, ServerConfig
49
+ from PyMemoryEditor.mcp import policy as policy_module
49
50
  from PyMemoryEditor.process.module_info import ModuleInfo
50
51
  from PyMemoryEditor.process.region import MemoryRegion, MemoryRegionSnapshot
51
52
  from PyMemoryEditor.process.thread_info import ThreadInfo
@@ -424,3 +425,47 @@ def make_toolset(fake_process: FakeProcess):
424
425
  def session(toolset: MemoryToolset) -> Dict[str, Any]:
425
426
  """An open session on the fake target."""
426
427
  return toolset.open_process(pid=4242)
428
+
429
+
430
+ #: The real chain, captured at import -- before the session fixture swaps it.
431
+ _REAL_SELF_AND_ANCESTOR_PIDS = policy_module._self_and_ancestor_pids
432
+
433
+ #: Stand-ins for this process's ancestors: far above any pid a test makes up,
434
+ #: and above the Linux and macOS pid ranges (Windows pids reach this high only
435
+ #: in theory), so a fake pid never collides with one.
436
+ FAKE_ANCESTOR_PIDS = frozenset({0x7FFF_FF01, 0x7FFF_FF02})
437
+
438
+
439
+ @pytest.fixture(scope="session", autouse=True)
440
+ def _fake_ancestor_chain():
441
+ """Keep made-up pids clear of the real ancestor chain.
442
+
443
+ ``ProcessPolicy`` refuses this process and every ancestor. Tests use fixed
444
+ fake pids (1000, 4242, 5000, ...), and when a real ancestor happens to own
445
+ one -- a CI runner process at pid 1001 did -- the fake is refused and the
446
+ test fails for reasons unrelated to the code. So the ancestors are swapped
447
+ for stand-ins; this process itself stays protected, as a safety net for a
448
+ test that reaches the real opener.
449
+
450
+ Session-scoped so policies built by module-scoped fixtures (the fidelity
451
+ suite's shared attach) see the stand-ins too. Tests about the protection
452
+ itself opt back into the real chain with
453
+ ``@pytest.mark.real_self_protection`` -- a new one must carry it, or it
454
+ checks the stand-ins.
455
+ """
456
+ import os
457
+
458
+ with pytest.MonkeyPatch.context() as patch:
459
+ patch.setattr(
460
+ policy_module,
461
+ "_self_and_ancestor_pids",
462
+ lambda: frozenset({os.getpid()}) | FAKE_ANCESTOR_PIDS,
463
+ )
464
+ yield
465
+
466
+
467
+ @pytest.fixture(autouse=True)
468
+ def _real_self_protection(request, monkeypatch):
469
+ """Give ``@pytest.mark.real_self_protection`` tests the real chain back."""
470
+ if request.node.get_closest_marker("real_self_protection") is not None:
471
+ monkeypatch.setattr(policy_module, "_self_and_ancestor_pids", _REAL_SELF_AND_ANCESTOR_PIDS)
@@ -864,6 +864,7 @@ class TestHttpTransportsRefuseWritesByDefault:
864
864
 
865
865
 
866
866
  class TestServerConfigProtectsItself:
867
+ @pytest.mark.real_self_protection
867
868
  def test_default_policy_refuses_the_server_pid(self):
868
869
  import os
869
870
 
@@ -228,6 +228,7 @@ class TestFilterAndDescribe:
228
228
  assert described["denied_system_process_count"] > 0
229
229
 
230
230
 
231
+ @pytest.mark.real_self_protection
231
232
  class TestSelfProtection:
232
233
  """The server never opens itself or the client that launched it."""
233
234
 
@@ -272,8 +273,10 @@ class TestSelfProtection:
272
273
  import os
273
274
 
274
275
  policy = ProcessPolicy(platform="linux", allow_any=True)
275
- kept = policy.filter([(os.getpid(), "python"), (5000, "game.exe")])
276
- assert kept == [(5000, "game.exe")]
276
+ # Any pid outside the real chain: this class runs against it.
277
+ other = next(pid for pid in range(5000, 6000) if pid not in policy.protected_pids)
278
+ kept = policy.filter([(os.getpid(), "python"), (other, "game.exe")])
279
+ assert kept == [(other, "game.exe")]
277
280
 
278
281
  def test_describe_reports_it(self):
279
282
  assert ProcessPolicy(platform="linux").describe()["self_protected"] is True
@@ -336,6 +339,7 @@ class TestTruncatedNamesMatchTheAllowlist:
336
339
  assert policy.check(5000, "my-very-long-ga").needs_approval is True
337
340
 
338
341
 
342
+ @pytest.mark.real_self_protection
339
343
  class TestTheWholeLaunchChainIsProtected:
340
344
  """Under ``uvx`` the server's parent is ``uv``; the client is further up."""
341
345
 
@@ -1809,6 +1809,7 @@ class TestAttachRechecksTheTargetsIdentity:
1809
1809
  toolset._name_for_pid = lambda pid: next(names)
1810
1810
  assert toolset.open_process(pid=4242)["opened"] is True
1811
1811
 
1812
+ @pytest.mark.real_self_protection
1812
1813
  def test_the_server_refuses_its_own_pid(self, make_toolset):
1813
1814
  import os
1814
1815
 
@@ -5,10 +5,9 @@ Regression guard: the bitness detectors must *warn* when they can't read the
5
5
  target's header and fall back to a guess, instead of mis-detecting silently.
6
6
 
7
7
  A wrong bitness silently poisons the pointer-width default used by the pointer
8
- APIs (resolve_pointer_chain / RemotePointer / scan_pointer_paths). The Windows
9
- path (``IsProcess64Bit``) already warned on the
10
- ``IsWow64Process`` failure; these tests lock the same contract on the macOS and
11
- Linux fallbacks so a refactor can't drop the warning unnoticed.
8
+ APIs (resolve_pointer_chain / RemotePointer / scan_pointer_paths). These tests
9
+ lock that contract on the Windows (``IsProcess64Bit``), macOS and Linux
10
+ fallbacks so a refactor can't drop the warning unnoticed.
12
11
 
13
12
  Each test forces the no-header fallback via monkeypatch (no special process or
14
13
  permission needed) and asserts a WARNING reaches the ``PyMemoryEditor`` logger.
@@ -74,3 +73,23 @@ def test_is_process_64bit_warns_when_elf_class_unknown(monkeypatch, warning_reco
74
73
  r.levelno == logging.WARNING and "is_process_64bit" in r.getMessage()
75
74
  for r in warning_records
76
75
  )
76
+
77
+
78
+ @pytest.mark.skipif(sys.platform != "win32", reason="Windows backend only")
79
+ @pytest.mark.parametrize("host_is_64bit", [False, True])
80
+ def test_is_process_64bit_win32_falls_back_to_host_bitness(
81
+ monkeypatch, warning_records, host_is_64bit
82
+ ):
83
+ """When IsWow64Process fails the answer is the host's bitness, as the
84
+ warning says -- not a hardcoded 64-bit, which is wrong from a 32-bit
85
+ Python."""
86
+ from PyMemoryEditor.win32 import functions as win
87
+
88
+ monkeypatch.setattr(win, "_detect_process_64bit", lambda handle: None)
89
+ monkeypatch.setattr(win, "_HOST_IS_64BIT", host_is_64bit)
90
+
91
+ assert win.IsProcess64Bit(0) is host_is_64bit
92
+ assert any(
93
+ r.levelno == logging.WARNING and "IsWow64Process" in r.getMessage()
94
+ for r in warning_records
95
+ )