PyMemoryEditor 2.0.1__tar.gz → 2.2.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.
Files changed (170) hide show
  1. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PKG-INFO +2 -28
  2. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/__init__.py +1 -1
  3. pymemoryeditor-2.2.0/PyMemoryEditor/__main__.py +4 -0
  4. pymemoryeditor-2.2.0/PyMemoryEditor/app/__init__.py +11 -0
  5. pymemoryeditor-2.2.0/PyMemoryEditor/app/_auto_refresh_dialog.py +244 -0
  6. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/_widgets.py +101 -9
  7. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/application.py +61 -3
  8. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/cheat_poll_worker.py +12 -0
  9. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/cheat_table.py +136 -14
  10. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/log_console_dialog.py +4 -3
  11. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/main_window.py +49 -33
  12. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/memory_map_dialog.py +17 -4
  13. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/memory_viewer_dialog.py +59 -19
  14. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/modules_dialog.py +17 -4
  15. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/open_process_dialog.py +70 -21
  16. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/pointer_chain_dialog.py +13 -1
  17. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/pointer_scan_dialog.py +27 -9
  18. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/scan_worker.py +58 -20
  19. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/scanner_panel.py +243 -44
  20. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/threads_dialog.py +23 -1
  21. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/value_types.py +172 -1
  22. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/functions.py +11 -0
  23. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/types.py +12 -0
  24. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/abstract.py +10 -0
  25. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/pattern.py +36 -11
  26. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/README.md +0 -26
  27. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/pyproject.toml +4 -1
  28. pymemoryeditor-2.2.0/tests/app/test_app_cheat_entry.py +490 -0
  29. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/app/test_app_scan_request.py +113 -3
  30. pymemoryeditor-2.2.0/tests/app/test_app_smoke.py +654 -0
  31. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/app/test_app_value_types.py +35 -0
  32. pymemoryeditor-2.2.0/tests/app/test_app_widgets.py +87 -0
  33. pymemoryeditor-2.2.0/tests/app/test_auto_refresh_dialog.py +475 -0
  34. pymemoryeditor-2.2.0/tests/app/test_open_process_dialog.py +246 -0
  35. pymemoryeditor-2.2.0/tests/platforms/test_macos_transient_reads.py +81 -0
  36. pymemoryeditor-2.0.1/PyMemoryEditor/__main__.py +0 -4
  37. pymemoryeditor-2.0.1/PyMemoryEditor/app/__init__.py +0 -9
  38. pymemoryeditor-2.0.1/PyMemoryEditor/app/_auto_refresh_dialog.py +0 -131
  39. pymemoryeditor-2.0.1/tests/app/test_app_cheat_entry.py +0 -85
  40. pymemoryeditor-2.0.1/tests/app/test_app_smoke.py +0 -164
  41. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/.flake8 +0 -0
  42. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/.gitignore +0 -0
  43. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/.readthedocs.yaml +0 -0
  44. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/CODE_OF_CONDUCT.md +0 -0
  45. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/CONTRIBUTING.md +0 -0
  46. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/LICENSE +0 -0
  47. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/Makefile +0 -0
  48. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/_icon.py +0 -0
  49. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/assets/icon.svg +0 -0
  50. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/cheat_entry.py +0 -0
  51. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/results_view.py +0 -0
  52. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/app/scan_types.py +0 -0
  53. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/enums.py +0 -0
  54. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/functions.py +0 -0
  55. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/libc.py +0 -0
  56. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/process.py +0 -0
  57. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/linux/types.py +0 -0
  58. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/__init__.py +0 -0
  59. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/libsystem.py +0 -0
  60. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/macos/process.py +0 -0
  61. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/__init__.py +0 -0
  62. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/errors.py +0 -0
  63. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/info.py +0 -0
  64. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/module_info.py +0 -0
  65. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/pointer_scan.py +0 -0
  66. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/region.py +0 -0
  67. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/remote_pointer.py +0 -0
  68. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/scanning.py +0 -0
  69. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/thread_info.py +0 -0
  70. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/process/util.py +0 -0
  71. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/py.typed +0 -0
  72. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/__init__.py +0 -0
  73. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/convert.py +0 -0
  74. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/scan.py +0 -0
  75. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/util/scan_numpy.py +0 -0
  76. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/__init__.py +0 -0
  77. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/memory_allocation_states.py +0 -0
  78. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/memory_protections.py +0 -0
  79. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/memory_types.py +0 -0
  80. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/process_operations.py +0 -0
  81. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/enums/standard_access_rights.py +0 -0
  82. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/functions.py +0 -0
  83. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/process.py +0 -0
  84. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/PyMemoryEditor/win32/types.py +0 -0
  85. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/SECURITY.md +0 -0
  86. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/assets/screenshots/app.png +0 -0
  87. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/assets/social-preview.html +0 -0
  88. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/assets/social-preview.png +0 -0
  89. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/_static/.gitkeep +0 -0
  90. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/_static/custom.css +0 -0
  91. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/_templates/layout.html +0 -0
  92. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/enums.md +0 -0
  93. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/errors.md +0 -0
  94. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/index.md +0 -0
  95. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/memory-region.md +0 -0
  96. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/module-info.md +0 -0
  97. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/openprocess.md +0 -0
  98. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/pointer-path.md +0 -0
  99. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/remote-pointer.md +0 -0
  100. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/thread-info.md +0 -0
  101. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/api/utilities.md +0 -0
  102. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/app.md +0 -0
  103. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/conf.py +0 -0
  104. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/contributing.md +0 -0
  105. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/funding.md +0 -0
  106. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/glossary.md +0 -0
  107. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/allocate-free.md +0 -0
  108. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/index.md +0 -0
  109. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/logging.md +0 -0
  110. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/memory-regions.md +0 -0
  111. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/modules-threads.md +0 -0
  112. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/opening-process.md +0 -0
  113. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/pattern-scan.md +0 -0
  114. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/pointer-scan.md +0 -0
  115. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/pointers.md +0 -0
  116. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/read-write.md +0 -0
  117. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/guide/searching.md +0 -0
  118. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/index.md +0 -0
  119. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/installation.md +0 -0
  120. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/license.md +0 -0
  121. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/platform-notes.md +0 -0
  122. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/quickstart.md +0 -0
  123. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/requirements.txt +0 -0
  124. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/troubleshooting.md +0 -0
  125. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/docs/why.md +0 -0
  126. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/app/__init__.py +0 -0
  127. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/app/test_app_pointer_parsing.py +0 -0
  128. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/app/test_cheat_poll_worker.py +0 -0
  129. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/app/test_refine_scan_worker.py +0 -0
  130. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/conftest.py +0 -0
  131. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/core/__init__.py +0 -0
  132. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/core/test_errors.py +0 -0
  133. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/core/test_logger.py +0 -0
  134. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/__init__.py +0 -0
  135. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_allocate_free.py +0 -0
  136. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_bufflength_inference.py +0 -0
  137. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_editor.py +0 -0
  138. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_partial_io.py +0 -0
  139. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_read_into.py +0 -0
  140. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_str_boundary.py +0 -0
  141. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_str_decode_consistency.py +0 -0
  142. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_typed_accessors.py +0 -0
  143. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_write_safety.py +0 -0
  144. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/memory/test_write_str_bytes_width.py +0 -0
  145. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/platforms/__init__.py +0 -0
  146. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/platforms/test_bitness.py +0 -0
  147. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/platforms/test_bitness_fallback_warns.py +0 -0
  148. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/platforms/test_linux_types.py +0 -0
  149. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/platforms/test_macos_protect.py +0 -0
  150. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/platforms/test_win32_permissions.py +0 -0
  151. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/pointer/__init__.py +0 -0
  152. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/pointer/test_pointer_chain.py +0 -0
  153. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/pointer/test_pointer_scan.py +0 -0
  154. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/pointer/test_remote_pointer.py +0 -0
  155. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/process/__init__.py +0 -0
  156. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/process/test_memory_region.py +0 -0
  157. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/process/test_module_enumeration.py +0 -0
  158. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/process/test_partial_name_match.py +0 -0
  159. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/process/test_process_enumeration.py +0 -0
  160. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/process/test_process_lookup.py +0 -0
  161. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/process/test_thread_enumeration.py +0 -0
  162. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/scan/__init__.py +0 -0
  163. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/scan/test_chunking_integration.py +0 -0
  164. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/scan/test_pattern_compile.py +0 -0
  165. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/scan/test_pattern_scan.py +0 -0
  166. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/scan/test_region_snapshot.py +0 -0
  167. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/scan/test_scan.py +0 -0
  168. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/scan/test_scan_numpy.py +0 -0
  169. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/scan/test_scan_properties.py +0 -0
  170. {pymemoryeditor-2.0.1 → pymemoryeditor-2.2.0}/tests/scan/test_scanning_helper.py +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: PyMemoryEditor
3
- Version: 2.0.1
3
+ Version: 2.2.0
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
@@ -138,32 +138,6 @@ That's it — read, write or scan another process in three lines, the same way o
138
138
 
139
139
  ---
140
140
 
141
- ## What's inside
142
-
143
- ### 🐍 The Python library
144
-
145
- Full control over another process's memory — in a few lines of Python:
146
-
147
- - ✅ **Read & write** values (`int`, `float`, `bool`, `str`, `bytes`)
148
- - 🔍 **Value scan** with eight comparison modes
149
- - 🎯 **Pattern scan** (IDA-style AOB & regex)
150
- - 🔗 **Pointer chains** + a live `RemotePointer` handle
151
- - 🧭 **Pointer scan** — find static pointers that survive ASLR
152
- - 🗺️ **Memory map**, **modules**, **threads**
153
- - 🧱 **Allocate & free** remote memory (Windows / macOS)
154
-
155
- ### 🖥️ The bundled GUI app
156
-
157
- All the library's power — no code required:
158
- - ⚡ **Zero setup** — attach to a process and start scanning in seconds
159
- - 🧲 **Refine workflow** — First Scan → Next Scan with live visual feedback
160
- - 📋 **Cheat table** — freeze / write values on the fly, JSON import/export
161
- - 🔬 **Hex viewer** — browse raw memory and write back inline
162
- - 🧩 **Pointer scan UI** — scan, export & rescan across sessions with a few clicks
163
- - 🎨 **One-click access** — every feature at your fingertips, no code needed
164
-
165
- ---
166
-
167
141
  ## 📖 Documentation
168
142
 
169
143
  Full documentation lives at **[pymemoryeditor.readthedocs.io](https://pymemoryeditor.readthedocs.io)** — installation, the Cheat Engine workflow, every method and parameter, the GUI app guide, platform notes and troubleshooting.
@@ -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__ = "2.0.1"
11
+ __version__ = "2.2.0"
12
12
 
13
13
  import logging
14
14
  import sys
@@ -0,0 +1,4 @@
1
+ from PyMemoryEditor.app.application import main_cli
2
+
3
+ if __name__ == "__main__":
4
+ main_cli()
@@ -0,0 +1,11 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ PyMemoryEditor App.
4
+
5
+ A Cheat-Engine-inspired memory editor built on PySide6 (Qt for Python).
6
+ Cross-platform: works on Windows, Linux and macOS.
7
+
8
+ Entry points: PyMemoryEditor.app.application:main_cli for the console script
9
+ (adds Ctrl+C handling), :main to run the app in-process without touching the
10
+ caller's signal handlers.
11
+ """
@@ -0,0 +1,244 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Shared base for the read-only, auto-refreshing table dialogs (Memory Map,
4
+ Modules, Threads).
5
+
6
+ All three did the same dance — run a one-shot read off the UI thread, repopulate
7
+ a sortable table when it returns, poll on a timer, and tear the worker down
8
+ safely on close — each with its own near-identical ``_*Worker(QThread)`` and
9
+ lifecycle boilerplate. That triplicated the fiddly bits (the in-flight guard
10
+ that self-throttles the poll, the detach-on-shutdown safety) where a fix to one
11
+ copy wouldn't reach the others. This owns the lifecycle once; subclasses provide
12
+ only what differs: how to fetch the data, and how to render it.
13
+
14
+ It also owns *when* a failure is worth interrupting the user — see
15
+ :meth:`AutoRefreshTableDialog._handle_failed`.
16
+ """
17
+ from typing import Callable, Optional
18
+
19
+ from PySide6.QtCore import QElapsedTimer, QThread, QTimer, Signal
20
+ from PySide6.QtWidgets import QDialog
21
+
22
+ from ._widgets import TearsDownOnClose, shutdown_worker_thread
23
+
24
+
25
+ # How long a target may keep failing before the auto-refresh gives up. Not
26
+ # every failure is terminal (``CreateToolhelp32Snapshot`` is documented to fail
27
+ # transiently while a target loads images), so a blip must not freeze a window
28
+ # that would recover next tick. In wall-clock, not ticks: the intervals differ
29
+ # by more than 10x across the windows that share this budget.
30
+ _FAILURE_GRACE_MS = 3000
31
+
32
+
33
+ class _DataWorker(QThread):
34
+ """Runs a no-arg ``fetch`` callable off the UI thread, once."""
35
+
36
+ ready = Signal(object)
37
+ failed = Signal(str)
38
+
39
+ def __init__(self, fetch: Callable[[], object], parent=None):
40
+ super().__init__(parent)
41
+ self._fetch = fetch
42
+
43
+ def run(self) -> None:
44
+ try:
45
+ data = self._fetch()
46
+ except Exception as exc: # noqa: BLE001 — surfaced to the UI as a string
47
+ self.failed.emit(str(exc))
48
+ return
49
+ self.ready.emit(data)
50
+
51
+
52
+ class AutoRefreshTableDialog(TearsDownOnClose, QDialog):
53
+ """Owns the fetch-worker lifecycle + auto-refresh timer for a table dialog.
54
+
55
+ Subclasses must:
56
+
57
+ * call ``super().__init__(process, refresh_interval_ms=..., parent=...)``,
58
+ then build their UI, then ``refresh()``, then ``_start_auto_refresh()``;
59
+ * implement :meth:`_fetch_data` (runs in the worker thread) and
60
+ :meth:`_on_data_ready` (runs on the UI thread);
61
+ * implement :meth:`_on_data_failed` and (optionally) :meth:`_set_loading_hint`.
62
+ """
63
+
64
+ def __init__(self, process, *, refresh_interval_ms: int, parent=None):
65
+ super().__init__(parent)
66
+ self._process = process
67
+ self._worker: Optional[_DataWorker] = None
68
+ self._has_data = False
69
+ self._refresh_interval_ms = refresh_interval_ms
70
+ self._auto_timer: Optional[QTimer] = None
71
+ # Failure state, all reset by a success or an explicit refresh(). See
72
+ # _handle_failed for how they decide what reaches the user.
73
+ self._last_error: Optional[str] = None # doubles as "already reported"
74
+ self._failing_since: Optional[QElapsedTimer] = None # streak clock
75
+ # Read by subclasses that rebuild their status line on user input, so
76
+ # the "stopped" note isn't replaced by a count that looks live.
77
+ self._polling_gave_up = False
78
+
79
+ def _start_auto_refresh(self) -> None:
80
+ """Begin polling. Call once, after the first ``refresh()``.
81
+
82
+ The ``refresh()`` in-flight guard self-throttles the cadence to however
83
+ long a fetch actually takes, so the timer can't stack workers on a slow
84
+ target.
85
+ """
86
+ self._auto_timer = QTimer(self)
87
+ self._auto_timer.setInterval(self._refresh_interval_ms)
88
+ self._auto_timer.timeout.connect(self._auto_refresh_tick)
89
+ self._auto_timer.start()
90
+
91
+ # ------------------------------------------------------------------ #
92
+ # Hooks for subclasses
93
+ # ------------------------------------------------------------------ #
94
+
95
+ def _fetch_data(self):
96
+ """Read the data to display. Runs in the worker thread; may raise."""
97
+ raise NotImplementedError
98
+
99
+ def _on_data_ready(self, data) -> None:
100
+ """Render ``data`` into the table. Runs on the UI thread."""
101
+ raise NotImplementedError
102
+
103
+ def _on_data_failed(self, message: str) -> None:
104
+ """Report a failed fetch. Runs on the UI thread.
105
+
106
+ Called only for a failure the user needs to see — the first fetch, or
107
+ the one that makes the poll give up — and never twice for the same
108
+ error (see :meth:`_handle_failed`), so a modal is safe here.
109
+ """
110
+ raise NotImplementedError
111
+
112
+ def _set_loading_hint(self) -> None:
113
+ """Optionally show a one-time loading message before the first fetch."""
114
+
115
+ def _on_polling_stopped(self) -> None:
116
+ """Note that the auto-refresh gave up. Runs on the UI thread.
117
+
118
+ Fires once, when a target has been failing for ``_FAILURE_GRACE_MS``.
119
+ The table is frozen on its last good data from here on, so subclasses
120
+ should say so. Optional.
121
+ """
122
+
123
+ # ------------------------------------------------------------------ #
124
+ # Lifecycle (shared)
125
+ # ------------------------------------------------------------------ #
126
+
127
+ def refresh(self) -> None:
128
+ """Fetch now, on the user's behalf.
129
+
130
+ Clears the error latch and restarts a poll that repeated failures
131
+ stopped. The timer goes through :meth:`_auto_refresh_tick` instead,
132
+ which must *not* re-arm — and neither may a dismissed dialog, whose
133
+ teardown latch is one-way.
134
+ """
135
+ if self._is_dismissed():
136
+ return
137
+ self._last_error = None
138
+ self._failing_since = None
139
+ self._polling_gave_up = False
140
+ if self._auto_timer is not None and not self._auto_timer.isActive():
141
+ self._auto_timer.start()
142
+ self._start_fetch()
143
+
144
+ def _auto_refresh_tick(self) -> None:
145
+ """Timer-driven fetch — never re-arms after a failure."""
146
+ self._start_fetch()
147
+
148
+ def _start_fetch(self) -> None:
149
+ # A worker started after the teardown could never be joined again.
150
+ if self._is_dismissed():
151
+ return
152
+
153
+ # Skip if a fetch is already in flight — the timer would otherwise stack
154
+ # workers on a slow target. This self-throttles to the real fetch time.
155
+ if self._worker is not None and self._worker.isRunning():
156
+ return
157
+
158
+ # Loading hint only before the first successful fetch; the periodic
159
+ # refresh updates silently to avoid flicker.
160
+ if not self._has_data:
161
+ self._set_loading_hint()
162
+
163
+ worker = _DataWorker(self._fetch_data, self)
164
+ worker.ready.connect(self._handle_ready)
165
+ worker.failed.connect(self._handle_failed)
166
+ worker.finished.connect(lambda w=worker: self._on_worker_finished(w))
167
+ self._worker = worker
168
+ worker.start()
169
+
170
+ def _handle_ready(self, data) -> None:
171
+ if self._is_dismissed():
172
+ return
173
+ self._has_data = True
174
+ self._last_error = None
175
+ self._failing_since = None
176
+ self._on_data_ready(data)
177
+
178
+ def _handle_failed(self, message: str) -> None:
179
+ """Handle a failed fetch: report it only when the user needs to know.
180
+
181
+ Once the target is gone (or its handle was closed by File → Change
182
+ Process) *every* tick fails, and ``_on_data_failed`` pops a **modal**
183
+ dialog. A modal runs a nested event loop, so the timer fires again while
184
+ it is up and the failures stack — hundreds of focus-stealing dialogs the
185
+ user could only escape by killing the app from another TTY (issue #74).
186
+
187
+ Reporting is therefore tied to the two moments where a failure changes
188
+ what the user sees: the **first** fetch (empty window) and the one that
189
+ makes the poll **give up** (frozen table). Everything in between heals
190
+ quietly — and that silence is load-bearing: reporting every failure
191
+ re-opens the spam, since a target failing on alternating ticks stacks a
192
+ modal per failure until the stack runs out.
193
+
194
+ A dismissed dialog reports nothing. The teardown disconnects the worker,
195
+ but a delivery already queued then must not raise a modal over a window
196
+ that is gone — during ``MainWindow.closeEvent`` that would be a nested
197
+ event loop in the middle of teardown.
198
+ """
199
+ if self._is_dismissed():
200
+ return
201
+
202
+ if self._failing_since is None:
203
+ self._failing_since = QElapsedTimer()
204
+ self._failing_since.start()
205
+ gave_up = False
206
+ else:
207
+ gave_up = (
208
+ self._failing_since.elapsed() >= _FAILURE_GRACE_MS
209
+ and self._auto_timer is not None
210
+ and self._auto_timer.isActive()
211
+ )
212
+
213
+ if gave_up and self._auto_timer is not None:
214
+ self._auto_timer.stop()
215
+ self._polling_gave_up = True
216
+
217
+ never_reported_yet = not self._has_data and self._last_error is None
218
+ if (gave_up or never_reported_yet) and message != self._last_error:
219
+ self._last_error = message
220
+ self._on_data_failed(message)
221
+
222
+ # After the report, so the "stopped" note is what stays on screen.
223
+ if gave_up:
224
+ self._on_polling_stopped()
225
+
226
+ def _on_worker_finished(self, worker) -> None:
227
+ """Retire the worker that just finished — and only that one.
228
+
229
+ ``finished`` is queued, so it can arrive after the next tick already
230
+ started a replacement. Clearing ``self._worker`` blindly would drop the
231
+ reference to that *running* worker and ``deleteLater()`` it — destroying
232
+ a live QThread aborts the process.
233
+ """
234
+ if worker is self._worker:
235
+ self._worker = None
236
+ worker.deleteLater()
237
+
238
+ def _teardown(self) -> None:
239
+ if self._auto_timer is not None:
240
+ self._auto_timer.stop()
241
+ # Unhook + join the worker; if it can't stop in time it's detached
242
+ # rather than destroyed under us.
243
+ shutdown_worker_thread(self._worker, wait_ms=1000)
244
+ self._worker = None
@@ -6,12 +6,17 @@ Centralises tiny helpers (numeric sort items, hex address parsing) that
6
6
  previously appeared duplicated across several dialog modules.
7
7
  """
8
8
 
9
- from typing import Callable, Iterable, List, Optional, Tuple
9
+ from typing import Any, Callable, Iterable, List, Optional, Tuple
10
10
 
11
- from PySide6.QtCore import Qt, QThread
11
+ from PySide6.QtCore import QObject, Qt, QThread
12
12
  from PySide6.QtGui import QStandardItem
13
13
 
14
14
 
15
+ # Monospace stack used across the app for address/value text. An explicit
16
+ # family list rather than the platform's default fixed font, so every table
17
+ # renders addresses at the same family and size on every OS.
18
+ MONOSPACE_FAMILY = "Menlo, Consolas, Courier New"
19
+
15
20
  # Workers that wouldn't stop in time on close are parked here so they are never
16
21
  # destroyed while still running (that aborts the whole process with
17
22
  # "QThread: Destroyed while thread is still running"). The list is module-level
@@ -39,7 +44,11 @@ def shutdown_worker_thread(worker: Optional[QThread], wait_ms: int = 2000) -> No
39
44
  if callable(cancel):
40
45
  cancel()
41
46
  try:
42
- worker.disconnect() # drop every outgoing connection at once
47
+ # The four-argument static form on purpose: bare `worker.disconnect()`
48
+ # raises TypeError in PySide6 (so this except swallowed it and nothing
49
+ # was disconnected), and `worker.disconnect(worker)` only drops
50
+ # connections whose *receiver* is the worker — ours belong to the dialog.
51
+ QObject.disconnect(worker, None, None, None)
43
52
  except (RuntimeError, TypeError):
44
53
  pass
45
54
 
@@ -47,31 +56,114 @@ def shutdown_worker_thread(worker: Optional[QThread], wait_ms: int = 2000) -> No
47
56
  worker.wait(wait_ms)
48
57
 
49
58
  if worker.isRunning():
50
- worker.setParent(None)
51
- _DETACHED_WORKERS.append(worker)
52
- worker.finished.connect(lambda: _reap_detached_worker(worker))
59
+ detach_worker(worker)
53
60
  else:
54
61
  worker.deleteLater()
55
62
 
56
63
 
64
+ def detach_worker(worker: QThread) -> None:
65
+ """Park a worker that wouldn't stop, so its owner can be destroyed.
66
+
67
+ Reparents it away from the dying widget and holds it in the module-level
68
+ registry until it finishes on its own. Callers that stop workers their own
69
+ way park them here too, so threads that outlived their owner are all in one
70
+ place.
71
+ """
72
+ worker.setParent(None)
73
+ _DETACHED_WORKERS.append(worker)
74
+ worker.finished.connect(lambda: _reap_detached_worker(worker))
75
+ # A worker that finished before this connect already emitted, so the reaper
76
+ # would never run and it would sit here for good.
77
+ if worker.isFinished():
78
+ _reap_detached_worker(worker)
79
+
80
+
57
81
  def _reap_detached_worker(worker: QThread) -> None:
58
82
  if worker in _DETACHED_WORKERS:
59
83
  _DETACHED_WORKERS.remove(worker)
60
84
  worker.deleteLater()
61
85
 
62
86
 
87
+ class TearsDownOnClose:
88
+ """Mixin that runs a dialog's teardown on *every* way out, not just close.
89
+
90
+ ``QDialog`` delivers a ``QCloseEvent`` only for a real close. The app's own
91
+ "Close" buttons call ``accept()`` and Esc reaches ``reject()``; both land in
92
+ ``done()``, which hides the dialog and emits ``finished`` *without* any
93
+ close event — so a teardown living in ``closeEvent`` never ran, and the
94
+ dialog kept polling, hidden and unreachable.
95
+
96
+ Subclasses implement :meth:`_teardown`; the mixin runs it exactly once,
97
+ whichever way the dialog is dismissed, and must precede ``QDialog`` in the
98
+ bases so its overrides win. Running once makes instances single-use, which
99
+ is what every caller here assumes.
100
+ """
101
+
102
+ def _teardown(self) -> None:
103
+ """Release timers and background threads. Runs exactly once."""
104
+ raise NotImplementedError
105
+
106
+ def _is_dismissed(self) -> bool:
107
+ """Whether the teardown has run. Guards live on this, not on a repeated
108
+ attribute lookup a typo could silently disable."""
109
+ return getattr(self, "_teardown_done", False)
110
+
111
+ def _run_teardown_once(self) -> None:
112
+ if self._is_dismissed():
113
+ return
114
+ self._teardown_done = True
115
+ self._teardown()
116
+
117
+ def done(self, result: int) -> None: # noqa: N802 — Qt naming
118
+ self._run_teardown_once()
119
+ super().done(result)
120
+
121
+ def closeEvent(self, event) -> None: # noqa: N802 — Qt naming
122
+ self._run_teardown_once()
123
+ super().closeEvent(event)
124
+
125
+
63
126
  class NumericItem(QStandardItem):
64
127
  """A QStandardItem that compares by its Qt.UserRole int payload.
65
128
 
66
129
  Used by columns showing formatted numbers (sizes, addresses, PIDs) so the
67
130
  table sorts by the underlying value rather than the lexical label.
131
+
132
+ The data storage interface is overridden because Qt keeps item data in a
133
+ QVariant, whose integers cap at qint64. Values past 2**63 can't make that
134
+ conversion — Linux x86-64 maps [vsyscall] at 0xffffffffff600000, so the
135
+ memory map hands one such address to the C++ side and gets "OverflowError:
136
+ int too big to convert", leaving the table half-populated. The workaround
137
+ is to keep user-role payloads on the Python side, where an int is an int.
138
+
139
+ The flip side: those payloads never reach the C++ model, so read them off
140
+ the item (``item.data(role)``) and never through ``model.data(index,
141
+ role)`` — that path converts the value back into a QVariant and overflows
142
+ all over again.
68
143
  """
69
144
 
70
- def __lt__(self, other):
145
+ def __init__(self, *args, **kwargs):
146
+ super().__init__(*args, **kwargs)
147
+ self._user_data: dict[int, Any] = {}
148
+
149
+ def setData(self, value: Any, role: int = Qt.UserRole):
150
+ if role >= Qt.UserRole:
151
+ self._user_data[int(role)] = value
152
+ self.emitDataChanged()
153
+ else:
154
+ super().setData(value, role)
155
+
156
+ def data(self, role: int = Qt.UserRole) -> Any:
157
+ if role >= Qt.UserRole:
158
+ return self._user_data.get(int(role))
159
+ else:
160
+ return super().data(role)
161
+
162
+ def __lt__(self, other: QStandardItem) -> bool:
71
163
  try:
72
- return int(self.data(Qt.UserRole)) < int(other.data(Qt.UserRole))
164
+ return int(self.data()) < int(other.data())
73
165
  except (TypeError, ValueError):
74
- return super().__lt__(other)
166
+ return self.text() < other.text()
75
167
 
76
168
 
77
169
  def parse_hex_address(text: str) -> Optional[int]:
@@ -6,6 +6,8 @@ A Cheat-Engine-inspired memory scanner built on PySide6 (Qt for Python),
6
6
  working on Windows, Linux and macOS.
7
7
  """
8
8
  import sys
9
+ import signal
10
+ import contextlib
9
11
  from dataclasses import dataclass
10
12
 
11
13
  from PyMemoryEditor import __version__
@@ -451,14 +453,50 @@ QLabel#processBadge {
451
453
  """
452
454
 
453
455
 
456
+ @contextlib.contextmanager
457
+ def _scoped_signal_handler(signalnum, handler):
458
+ """
459
+ Temporarily change a signal handler in the scope of a with block.
460
+
461
+ Becomes a no-op off the main thread, where `signal.signal` raises
462
+ ValueError: an embedder driving the app from a worker thread shouldn't be
463
+ hard-crashed just because the Ctrl+C handler can't be installed there.
464
+ """
465
+ try:
466
+ previous_handler = signal.signal(signalnum, handler)
467
+ except ValueError:
468
+ # Off the main thread. (`signal.signal` also raises ValueError for a
469
+ # signal number the platform rejects, but the only call site passes
470
+ # SIGINT, which every supported platform has.)
471
+ yield
472
+ return
473
+ try:
474
+ yield
475
+ finally:
476
+ # `signal.signal` reports None when "an unknown handler is in effect",
477
+ # i.e. one installed outside Python — plausible when the app is
478
+ # embedded in a host that set SIGINT up in C before the signal module
479
+ # initialized. Passing that None back raises TypeError, which would
480
+ # blow up on the way out of a run that otherwise succeeded, so leave
481
+ # the handler alone instead: SIG_DFL is closer to the host's intent
482
+ # than a crash.
483
+ if previous_handler is not None:
484
+ signal.signal(signalnum, previous_handler)
485
+
486
+
454
487
  def main(argv=None):
455
488
  """
456
- Entry point for the ``pymemoryeditor`` console script.
489
+ Run the app. Safe to call in-process: it installs no signal handler.
457
490
 
458
491
  ``argv`` defaults to ``sys.argv`` so packaging tools (which call
459
492
  ``main()`` with no arguments) keep working. Tests and embedders can pass
460
493
  an explicit list — previously a positional ``*args`` was accepted but
461
494
  ignored, which made the parameter meaningless.
495
+
496
+ Terminal users want Ctrl+C to kill the app, which takes a process-wide
497
+ signal change — see :func:`main_cli`, the console-script entry point.
498
+ Library callers get this function untouched so embedding the app can't
499
+ disturb the host's own SIGINT handling.
462
500
  """
463
501
  if argv is None:
464
502
  argv = sys.argv
@@ -501,10 +539,30 @@ def main(argv=None):
501
539
  app.exec()
502
540
  finally:
503
541
  try:
504
- process.close()
542
+ # `process` is only the handle the picker opened; File → Change
543
+ # Process… released that one and attached the window to another.
544
+ window.process.close()
505
545
  except Exception:
506
546
  pass
507
547
 
508
548
 
549
+ def main_cli(argv=None):
550
+ """
551
+ Entry point for the ``pymemoryeditor`` console script and ``python -m``.
552
+
553
+ Qt's event loop blocks inside C++, so Python's default SIGINT handler only
554
+ runs once the interpreter next regains control. In practice it raised
555
+ KeyboardInterrupt inside our own _PointerCursorFilter.eventFilter override,
556
+ where PySide6 swallows it and merely prints a traceback (#76). Hand SIGINT
557
+ back to the OS for the duration of the run so Ctrl+C from a terminal
558
+ terminates the app immediately.
559
+
560
+ Kept separate from :func:`main` so that process-wide change only happens
561
+ when the app *is* the process, never when it is embedded in someone else's.
562
+ """
563
+ with _scoped_signal_handler(signal.SIGINT, signal.SIG_DFL):
564
+ return main(argv)
565
+
566
+
509
567
  if __name__ == "__main__":
510
- main()
568
+ main_cli()
@@ -106,6 +106,11 @@ class _CheatPollWorker(QThread):
106
106
  with QMutexLocker(self._mutex):
107
107
  self._pending_writes.append((address, pytype, length, value))
108
108
 
109
+ def _stopping(self) -> bool:
110
+ """Whether ``stop`` was asked for. Cheap enough to check per entry."""
111
+ with QMutexLocker(self._mutex):
112
+ return self._stop
113
+
109
114
  def stop(self) -> None:
110
115
  with QMutexLocker(self._mutex):
111
116
  self._stop = True
@@ -126,6 +131,8 @@ class _CheatPollWorker(QThread):
126
131
 
127
132
  failures: List[Tuple[int, type, int, str]] = []
128
133
  for address, pytype, length, value in pending:
134
+ if self._stopping():
135
+ break # the handle is about to go — see _poll_once
129
136
  try:
130
137
  self._process.write_process_memory(address, pytype, length, value)
131
138
  except Exception as exc: # noqa: BLE001
@@ -206,6 +213,11 @@ class _CheatPollWorker(QThread):
206
213
  values_by_address = None
207
214
 
208
215
  for address in addresses:
216
+ # Per entry, not per tick: shutdown() closes the handle right
217
+ # after stop(), and a write on a recycled one hits another
218
+ # process.
219
+ if self._stopping():
220
+ return results
209
221
  frozen_value, is_frozen = freeze_by_addr[(pytype, length, address)]
210
222
  if values_by_address is not None:
211
223
  current = values_by_address.get(address)