appmap 3.0.0__tar.gz → 4.0.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 (193) hide show
  1. {appmap-3.0.0 → appmap-4.0.0}/PKG-INFO +16 -8
  2. {appmap-3.0.0 → appmap-4.0.0}/README.md +1 -1
  3. {appmap-3.0.0 → appmap-4.0.0}/_appmap/__init__.py +1 -0
  4. {appmap-3.0.0 → appmap-4.0.0}/_appmap/configuration.py +12 -7
  5. {appmap-3.0.0 → appmap-4.0.0}/_appmap/env.py +4 -1
  6. {appmap-3.0.0 → appmap-4.0.0}/_appmap/event.py +20 -13
  7. {appmap-3.0.0 → appmap-4.0.0}/_appmap/importer.py +31 -21
  8. {appmap-3.0.0 → appmap-4.0.0}/_appmap/py_version_check.py +1 -1
  9. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/params.py +7 -0
  10. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_command.py +3 -3
  11. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_params.py +158 -4
  12. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_properties.py +1 -2
  13. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_runner.py +31 -0
  14. {appmap-3.0.0 → appmap-4.0.0}/appmap/command/runner.py +18 -25
  15. {appmap-3.0.0 → appmap-4.0.0}/pyproject.toml +13 -4
  16. {appmap-3.0.0 → appmap-4.0.0}/.gitignore +0 -0
  17. {appmap-3.0.0 → appmap-4.0.0}/LICENSE +0 -0
  18. {appmap-3.0.0 → appmap-4.0.0}/_appmap/django.py +0 -0
  19. {appmap-3.0.0 → appmap-4.0.0}/_appmap/fastapi.py +0 -0
  20. {appmap-3.0.0 → appmap-4.0.0}/_appmap/flask.py +0 -0
  21. {appmap-3.0.0 → appmap-4.0.0}/_appmap/generation.py +0 -0
  22. {appmap-3.0.0 → appmap-4.0.0}/_appmap/instrument.py +0 -0
  23. {appmap-3.0.0 → appmap-4.0.0}/_appmap/labels.py +0 -0
  24. {appmap-3.0.0 → appmap-4.0.0}/_appmap/metadata.py +0 -0
  25. {appmap-3.0.0 → appmap-4.0.0}/_appmap/noappmap.py +0 -0
  26. {appmap-3.0.0 → appmap-4.0.0}/_appmap/recorder.py +0 -0
  27. {appmap-3.0.0 → appmap-4.0.0}/_appmap/recording.py +0 -0
  28. {appmap-3.0.0 → appmap-4.0.0}/_appmap/remote_recording.py +0 -0
  29. {appmap-3.0.0 → appmap-4.0.0}/_appmap/singleton.py +0 -0
  30. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/__init__.py +0 -0
  31. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/appmap_test_base.py +0 -0
  32. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/bin/server_runner +0 -0
  33. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/conftest.py +0 -0
  34. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap-all-paths-malformed.yml +0 -0
  35. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap-broken.yml +0 -0
  36. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap-class.yml +0 -0
  37. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap-empty-path.yml +0 -0
  38. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap-exclude-fn.yml +0 -0
  39. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap-func.yml +0 -0
  40. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap-malformed-path.yml +0 -0
  41. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap-no-pyyaml.yml +0 -0
  42. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap.yml +0 -0
  43. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap_testing/__init__.py +0 -0
  44. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/appmap_testing/django_simplelazyobject.py +0 -0
  45. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config/src/package/__init__.py +0 -0
  46. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config/test/__init__.py +0 -0
  47. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config/test/foo.py +0 -0
  48. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config-exclude/.hide/hidden_mod/__init__.py +0 -0
  49. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config-exclude/node_modules/node_mod/__init__.py +0 -0
  50. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config-exclude/src/package/__init__.py +0 -0
  51. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config-exclude/test/__init__.py +0 -0
  52. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config-exclude/venv/venv_mod/__init__.py +0 -0
  53. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config-up/appmap.yml +0 -0
  54. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config-up/project/p1/__init__.py +0 -0
  55. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/config-up/project/p2/sub1/__init__.py +0 -0
  56. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/appmap.yml +0 -0
  57. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/djangoapp/__init__.py +0 -0
  58. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/djangoapp/djangoapp.py +0 -0
  59. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/djangoapp/hello_world.html +0 -0
  60. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/djangoapp/middleware.py +0 -0
  61. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/djangoapp/settings.py +0 -0
  62. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/djangoapp/settings_dev.py +0 -0
  63. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/djangoapp/urls.py +0 -0
  64. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/init/sitecustomize.py +0 -0
  65. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/manage.py +0 -0
  66. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/pytest.ini +0 -0
  67. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/test/__init__.py +0 -0
  68. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/test/test_app.py +0 -0
  69. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/test/test_request.py +0 -0
  70. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/django/test/test_unittest_setup.py +0 -0
  71. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/example_class.py +0 -0
  72. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/expected.appmap.json +0 -0
  73. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/fastapi/appmap.yml +0 -0
  74. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/fastapi/fastapiapp/__init__.py +0 -0
  75. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/fastapi/fastapiapp/main.py +0 -0
  76. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/fastapi/init/sitecustomize.py +0 -0
  77. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/fastapi/test_app.py +0 -0
  78. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/flask/appmap.yml +0 -0
  79. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/flask/flaskapp.py +0 -0
  80. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/flask/init/sitecustomize.py +0 -0
  81. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/flask/templates/test.html +0 -0
  82. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/flask/test_app.py +0 -0
  83. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/flask-instrumented/appmap.yml +0 -0
  84. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/flask-instrumented/flaskapp.py +0 -0
  85. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/flask-instrumented/init/sitecustomize.py +0 -0
  86. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/flask-instrumented/test_app.py +0 -0
  87. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/package1/__init__.py +0 -0
  88. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/package1/package2/__init__.py +0 -0
  89. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/package1/package2/__main__.py +0 -0
  90. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/package1/package2/mod1.py +0 -0
  91. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/properties_class.py +0 -0
  92. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/appmap-no-test-cases.yml +0 -0
  93. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/appmap.yml +0 -0
  94. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/expected/pytest-numpy1-no-test-cases.appmap.json +0 -0
  95. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/expected/pytest-numpy1.appmap.json +0 -0
  96. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/expected/pytest-numpy2-no-test-cases.appmap.json +0 -0
  97. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/expected/pytest-numpy2.appmap.json +0 -0
  98. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/expected/status_errored.metadata.json +0 -0
  99. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/expected/status_failed.metadata.json +0 -0
  100. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/expected/status_xfailed.metadata.json +0 -0
  101. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/expected/status_xsucceeded.metadata.json +0 -0
  102. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/simple.py +0 -0
  103. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/tests/__init__.py +0 -0
  104. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/tests/test_noappmap.py +0 -0
  105. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest/tests/test_simple.py +0 -0
  106. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest-instrumented/appmap.yml +0 -0
  107. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest-instrumented/init/sitecustomize.py +0 -0
  108. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/pytest-instrumented/test_instrumented.py +0 -0
  109. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/remote.appmap.json +0 -0
  110. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/trial/.gitignore +0 -0
  111. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/trial/appmap-no-test-cases.yml +0 -0
  112. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/trial/appmap.yml +0 -0
  113. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/trial/expected/pytest-no-test-cases.appmap.json +0 -0
  114. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/trial/expected/pytest.appmap.json +0 -0
  115. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/trial/init/sitecustomize.py +0 -0
  116. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/trial/test/__init__.py +0 -0
  117. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/trial/test/test_deferred.py +0 -0
  118. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/appmap-no-test-cases.yml +0 -0
  119. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/appmap.yml +0 -0
  120. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/expected/pytest.appmap.json +0 -0
  121. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/expected/status_errored.metadata.json +0 -0
  122. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/expected/status_failed.metadata.json +0 -0
  123. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/expected/status_xfailed.metadata.json +0 -0
  124. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/expected/status_xsucceeded.metadata.json +0 -0
  125. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/expected/unittest-no-test-cases.appmap.json +0 -0
  126. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/expected/unittest.appmap.json +0 -0
  127. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/init/sitecustomize.py +0 -0
  128. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/simple/__init__.py +0 -0
  129. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/simple/test_noappmap.py +0 -0
  130. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/data/unittest/simple/test_simple.py +0 -0
  131. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/helpers.py +0 -0
  132. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/normalize.py +0 -0
  133. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_configuration.py +0 -0
  134. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_describe_value.py +0 -0
  135. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_django.py +0 -0
  136. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_django_simplelazyobject.py +0 -0
  137. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_env.py +0 -0
  138. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_events.py +0 -0
  139. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_fastapi.py +0 -0
  140. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_flask.py +0 -0
  141. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_generation.py +0 -0
  142. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_http.py +0 -0
  143. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_importer.py +0 -0
  144. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_labels.py +0 -0
  145. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_metadata.py +0 -0
  146. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_recording.py +0 -0
  147. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_sqlalchemy.py +0 -0
  148. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_test_frameworks.py +0 -0
  149. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/test_util.py +0 -0
  150. {appmap-3.0.0 → appmap-4.0.0}/_appmap/test/web_framework.py +0 -0
  151. {appmap-3.0.0 → appmap-4.0.0}/_appmap/testing_framework.py +0 -0
  152. {appmap-3.0.0 → appmap-4.0.0}/_appmap/trace_logger.py +0 -0
  153. {appmap-3.0.0 → appmap-4.0.0}/_appmap/unittest.py +0 -0
  154. {appmap-3.0.0 → appmap-4.0.0}/_appmap/utils.py +0 -0
  155. {appmap-3.0.0 → appmap-4.0.0}/_appmap/web_framework.py +0 -0
  156. {appmap-3.0.0 → appmap-4.0.0}/_appmap/wrapt/LICENSE +0 -0
  157. {appmap-3.0.0 → appmap-4.0.0}/_appmap/wrapt/__init__.py +0 -0
  158. {appmap-3.0.0 → appmap-4.0.0}/_appmap/wrapt/arguments.py +0 -0
  159. {appmap-3.0.0 → appmap-4.0.0}/_appmap/wrapt/decorators.py +0 -0
  160. {appmap-3.0.0 → appmap-4.0.0}/_appmap/wrapt/importer.py +0 -0
  161. {appmap-3.0.0 → appmap-4.0.0}/_appmap/wrapt/wrappers.py +0 -0
  162. {appmap-3.0.0 → appmap-4.0.0}/appmap/__init__.py +0 -0
  163. {appmap-3.0.0 → appmap-4.0.0}/appmap/command/__init__.py +0 -0
  164. {appmap-3.0.0 → appmap-4.0.0}/appmap/command/appmap_agent_init.py +0 -0
  165. {appmap-3.0.0 → appmap-4.0.0}/appmap/command/appmap_agent_status.py +0 -0
  166. {appmap-3.0.0 → appmap-4.0.0}/appmap/command/appmap_agent_validate.py +0 -0
  167. {appmap-3.0.0 → appmap-4.0.0}/appmap/django.py +0 -0
  168. {appmap-3.0.0 → appmap-4.0.0}/appmap/fastapi.py +0 -0
  169. {appmap-3.0.0 → appmap-4.0.0}/appmap/flask.py +0 -0
  170. {appmap-3.0.0 → appmap-4.0.0}/appmap/http.py +0 -0
  171. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/__init__.py +0 -0
  172. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/crypto.yml +0 -0
  173. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/django.yml +0 -0
  174. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/flask.yml +0 -0
  175. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/formats.yml +0 -0
  176. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/http.yml +0 -0
  177. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/jobs.yml +0 -0
  178. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/jwt.yml +0 -0
  179. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/logger.yml +0 -0
  180. {appmap-3.0.0 → appmap-4.0.0}/appmap/labeling/random.yml +0 -0
  181. {appmap-3.0.0 → appmap-4.0.0}/appmap/pytest.py +0 -0
  182. {appmap-3.0.0 → appmap-4.0.0}/appmap/sqlalchemy.py +0 -0
  183. {appmap-3.0.0 → appmap-4.0.0}/appmap/unittest.py +0 -0
  184. {appmap-3.0.0 → appmap-4.0.0}/appmap/uvicorn.py +0 -0
  185. {appmap-3.0.0 → appmap-4.0.0}/appmap.pth +0 -0
  186. {appmap-3.0.0 → appmap-4.0.0}/vendor/_appmap/wrapt/LICENSE +0 -0
  187. {appmap-3.0.0 → appmap-4.0.0}/vendor/_appmap/wrapt/__init__.py +0 -0
  188. {appmap-3.0.0 → appmap-4.0.0}/vendor/_appmap/wrapt/arguments.py +0 -0
  189. {appmap-3.0.0 → appmap-4.0.0}/vendor/_appmap/wrapt/decorators.py +0 -0
  190. {appmap-3.0.0 → appmap-4.0.0}/vendor/_appmap/wrapt/importer.py +0 -0
  191. {appmap-3.0.0 → appmap-4.0.0}/vendor/_appmap/wrapt/wrappers.py +0 -0
  192. {appmap-3.0.0 → appmap-4.0.0}/vendor/patches/wrapt.patch +0 -0
  193. {appmap-3.0.0 → appmap-4.0.0}/vendor/vendor.txt +0 -0
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: appmap
3
- Version: 3.0.0
3
+ Version: 4.0.0
4
4
  Summary: Create AppMap files by recording a Python application.
5
5
  Project-URL: Homepage, https://github.com/applandinc/appmap-python
6
6
  Author-email: Alan Potter <alan@app.land>, Viraj Kanwade <viraj.kanwade@forgeahead.io>, Rafał Rzepecki <rafal@app.land>
@@ -9,14 +9,24 @@ License-File: LICENSE
9
9
  Classifier: Development Status :: 4 - Beta
10
10
  Classifier: Framework :: Django
11
11
  Classifier: Framework :: Django :: 3.2
12
+ Classifier: Framework :: Django :: 4.2
13
+ Classifier: Framework :: Django :: 5.2
14
+ Classifier: Framework :: Django :: 6.0
12
15
  Classifier: Framework :: Flask
13
16
  Classifier: Framework :: Pytest
14
17
  Classifier: Intended Audience :: Developers
15
18
  Classifier: License :: OSI Approved :: MIT License
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3 :: Only
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
24
+ Classifier: Programming Language :: Python :: 3.14
25
+ Classifier: Programming Language :: Python :: 3.15
16
26
  Classifier: Topic :: Software Development
17
27
  Classifier: Topic :: Software Development :: Debuggers
18
28
  Classifier: Topic :: Software Development :: Documentation
19
- Requires-Python: >=3.8
29
+ Requires-Python: >=3.11
20
30
  Requires-Dist: importlib-resources>=5.4.0
21
31
  Requires-Dist: inflection>=0.3.0
22
32
  Requires-Dist: packaging>=19.0
@@ -33,8 +43,7 @@ Requires-Dist: httpretty<2.0,>=1.0.5; extra == 'dev'
33
43
  Requires-Dist: httpx<1.0,>=0.27.0; extra == 'dev'
34
44
  Requires-Dist: incremental<24.7.0; extra == 'dev'
35
45
  Requires-Dist: isort<6.0,>=5.10.1; extra == 'dev'
36
- Requires-Dist: numpy<2.0,>=1.24.4; (python_version < '3.9') and extra == 'dev'
37
- Requires-Dist: numpy>=2.0; (python_version >= '3.9') and extra == 'dev'
46
+ Requires-Dist: numpy>=2.0; extra == 'dev'
38
47
  Requires-Dist: pprintpp<1.0,>=0.4.0; extra == 'dev'
39
48
  Requires-Dist: psutil<7.0,>=6.0.0; extra == 'dev'
40
49
  Requires-Dist: pyfakefs<6.0,>=5.3.5; extra == 'dev'
@@ -66,8 +75,7 @@ Requires-Dist: flask; extra == 'test'
66
75
  Requires-Dist: httpretty<2.0,>=1.0.5; extra == 'test'
67
76
  Requires-Dist: httpx<1.0,>=0.27.0; extra == 'test'
68
77
  Requires-Dist: incremental<24.7.0; extra == 'test'
69
- Requires-Dist: numpy<2.0,>=1.24.4; (python_version < '3.9') and extra == 'test'
70
- Requires-Dist: numpy>=2.0; (python_version >= '3.9') and extra == 'test'
78
+ Requires-Dist: numpy>=2.0; extra == 'test'
71
79
  Requires-Dist: psutil<7.0,>=6.0.0; extra == 'test'
72
80
  Requires-Dist: pyfakefs<6.0,>=5.3.5; extra == 'test'
73
81
  Requires-Dist: pytest-console-scripts<2.0,>=1.4.1; extra == 'test'
@@ -185,7 +193,7 @@ cd appmap-python
185
193
  APPMAP_DISPLAY_PARAMS=true uv run appmap-python pytest
186
194
 
187
195
  # Run tests with a specific Python version
188
- APPMAP_DISPLAY_PARAMS=true uv run --python 3.9 appmap-python pytest
196
+ APPMAP_DISPLAY_PARAMS=true uv run --python 3.11 appmap-python pytest
189
197
 
190
198
  # Run tests in parallel
191
199
  APPMAP_DISPLAY_PARAMS=true uv run appmap-python pytest -n auto
@@ -96,7 +96,7 @@ cd appmap-python
96
96
  APPMAP_DISPLAY_PARAMS=true uv run appmap-python pytest
97
97
 
98
98
  # Run tests with a specific Python version
99
- APPMAP_DISPLAY_PARAMS=true uv run --python 3.9 appmap-python pytest
99
+ APPMAP_DISPLAY_PARAMS=true uv run --python 3.11 appmap-python pytest
100
100
 
101
101
  # Run tests in parallel
102
102
  APPMAP_DISPLAY_PARAMS=true uv run appmap-python pytest -n auto
@@ -12,6 +12,7 @@ def initialize(**kwargs):
12
12
  importer.initialize()
13
13
  recorder.initialize()
14
14
  configuration.initialize() # needs to be initialized after recorder
15
+ importer.install() # needs the filters registered by configuration
15
16
  metadata.initialize()
16
17
  web_framework.initialize()
17
18
  recording.initialize()
@@ -185,13 +185,10 @@ class Config(metaclass=SingletonMeta):
185
185
  env = Env.current
186
186
  output_dir = env.output_dir
187
187
  root_dir = env.root_dir
188
- try:
188
+ # If output_dir isn't relative to root_dir, APPMAP_OUTPUT_DIR is set, and we've already
189
+ # logged a warning.
190
+ if output_dir.is_relative_to(root_dir):
189
191
  ret["appmap_dir"] = str(output_dir.relative_to(root_dir))
190
- except ValueError:
191
- # The only way we can get here is if APPMAP_OUTPUT_DIR is set, and we've already logged
192
- # a warning. (Note that PurePath.is_relative_to wasn't added till 3.9, so it can't be
193
- # used here.)
194
- pass
195
192
 
196
193
  return ret
197
194
 
@@ -508,5 +505,13 @@ if _startup_messages_shown is None:
508
505
  logger.info("file: %s", c._file if c.file_present else "[no appmap.yml]")
509
506
  logger.info("config: %r", c)
510
507
  logger.debug("package_functions: %s", c.package_functions)
511
- logger.info("env: %r", os.environ)
508
+ # Only log AppMap's own settings, never the full environment: arbitrary
509
+ # environment variables (API keys, credentials, tokens, etc.) must never
510
+ # end up in application logs.
511
+ appmap_env = {
512
+ k: v
513
+ for k, v in os.environ.items()
514
+ if k in ("APPMAP", "_APPMAP") or k.startswith(("APPMAP_", "_APPMAP_"))
515
+ }
516
+ logger.info("env: %r", appmap_env)
512
517
  os.environ["_APPMAP_MESSAGES_SHOWN"] = "true"
@@ -168,7 +168,10 @@ class Env(metaclass=SingletonMeta):
168
168
  trace_logger.install()
169
169
 
170
170
  log_level = self.get("APPMAP_LOG_LEVEL", "warn").upper()
171
- disable_log = os.environ.get("APPMAP_DISABLE_LOG_FILE", "false").upper() != "FALSE"
171
+ # No log file unless the user opts in: it can contain data (e.g.
172
+ # rendered parameter values) that shouldn't be written to disk or
173
+ # committed to source control by default.
174
+ disable_log = os.environ.get("APPMAP_DISABLE_LOG_FILE", "true").upper() != "FALSE"
172
175
  log_config = self.get("APPMAP_LOG_CONFIG")
173
176
  config_dict = {
174
177
  "version": 1,
@@ -156,10 +156,11 @@ class Event:
156
156
 
157
157
 
158
158
  class Param:
159
- __slots__ = ["name", "kind", "default", "default_class"]
159
+ __slots__ = ["name", "kind", "positional_only", "default", "default_class"]
160
160
 
161
161
  def __init__(self, sigp):
162
162
  self.name = sigp.name
163
+ self.positional_only = sigp.kind == Parameter.POSITIONAL_ONLY
163
164
  has_default = sigp.default is not Signature.empty
164
165
  if sigp.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD):
165
166
  self.kind = "opt" if has_default else "req"
@@ -275,28 +276,34 @@ class CallEvent(Event):
275
276
  # we're popping the front repeatedly, lists are better at this
276
277
  args = list(args)
277
278
 
279
+ # Names of keyword arguments bound to named parameters. Python
280
+ # doesn't include them in **kwargs, so neither do we.
281
+ bound = set()
278
282
  for p in params:
279
- if p.kind == "req":
280
- # A 'req' argument can be either keyword or
281
- # positional.
282
- if p.name in kwargs:
283
+ if p.kind in ("req", "opt"):
284
+ # These can be either keyword or positional, unless
285
+ # they're positional-only.
286
+ if not p.positional_only and p.name in kwargs:
283
287
  value = kwargs[p.name]
288
+ bound.add(p.name)
289
+ elif args:
290
+ value = args.pop(0)
291
+ elif p.kind == "opt":
292
+ value = p.default
284
293
  else:
285
- if args:
286
- value = args.pop(0)
287
- else:
288
- continue # required argument missing
289
- elif p.kind == "keyreq":
294
+ continue # required argument missing
295
+ elif p.kind in ("keyreq", "key"):
290
296
  if p.name in kwargs:
291
297
  value = kwargs[p.name]
298
+ bound.add(p.name)
299
+ elif p.kind == "key":
300
+ value = p.default
292
301
  else:
293
302
  continue # required argument missing
294
- elif p.kind in ("opt", "key"):
295
- value = kwargs.get(p.name, p.default)
296
303
  elif p.kind == "rest":
297
304
  value = tuple(args)
298
305
  elif p.kind == "keyrest":
299
- value = kwargs
306
+ value = {k: v for k, v in kwargs.items() if k not in bound} if bound else kwargs
300
307
  else:
301
308
  # If all the parameter types are handled, this
302
309
  # shouldn't ever happen...
@@ -166,7 +166,7 @@ class Importer:
166
166
  @classmethod
167
167
  def initialize(cls):
168
168
  cls.filter_stack = []
169
- cls.filter_chain = []
169
+ cls.filter_chain = None
170
170
  cls._skip_instrumenting = ("appmap", "_appmap")
171
171
  cls.instrument_properties = (
172
172
  Env.current.get("APPMAP_INSTRUMENT_PROPERTIES", "true").lower() == "true"
@@ -176,6 +176,10 @@ class Importer:
176
176
  def use_filter(cls, filter_class):
177
177
  cls.filter_stack.append(filter_class)
178
178
 
179
+ @classmethod
180
+ def build_filter_chain(cls):
181
+ cls.filter_chain = reduce(lambda acc, e: e(acc), cls.filter_stack, NullFilter(None))
182
+
179
183
  @classmethod
180
184
  def instrument_function(cls, fn_name, filterableFn: FilterableFn, selected_functions=None):
181
185
  # Only instrument the function if it was specifically called out for the package
@@ -194,8 +198,10 @@ class Importer:
194
198
  return
195
199
 
196
200
  logger.trace("do_import, mod %s args %s kwargs %s", mod, args, kwargs)
197
- if not cls.filter_chain:
198
- cls.filter_chain = reduce(lambda acc, e: e(acc), cls.filter_stack, NullFilter(None))
201
+ if cls.filter_chain is None:
202
+ # Only possible while _appmap is being (re)initialized, before install() has run.
203
+ logger.trace("no filter chain yet, not instrumenting %s", mod.__name__)
204
+ return
199
205
 
200
206
  def instrument_functions(filterable, selected_functions=None):
201
207
  # pylint: disable=too-many-locals
@@ -314,26 +320,18 @@ def wrapped_find_spec(find_spec, _, args, kwargs):
314
320
 
315
321
 
316
322
  def wrap_finder_find_spec(finder):
317
- # Prior to 3.11, it worked fine to just grab find_spec from the finder and wrap it. The
318
- # implementation of builtin finders must have changed with 3.11, because we now need the same
319
- # kind of workaround we use above for exec_module.
320
- if sys.version_info[1] < 11:
321
- find_spec = getattr(finder, "find_spec", None)
322
- if find_spec is None:
323
- logger.trace("no find_spec for finder %r", finder)
324
- return
325
-
323
+ # Builtin finders define find_spec as a classmethod or staticmethod, which wrapt has trouble
324
+ # identifying (see the comment in wrapped_find_spec above), so wrap the descriptor found in
325
+ # __dict__ instead.
326
+ find_spec = inspect.getattr_static(finder, "find_spec", None)
327
+ if find_spec is None:
328
+ logger.trace("no find_spec for finder %r", finder)
329
+ return
330
+
331
+ if isinstance(find_spec, (classmethod, staticmethod)):
326
332
  finder.find_spec = wrap_finder_function(find_spec, wrapped_find_spec)
327
333
  else:
328
- find_spec = inspect.getattr_static(finder, "find_spec", None)
329
- if find_spec is None:
330
- logger.trace("no find_spec for finder %r", finder)
331
- return
332
-
333
- if isinstance(find_spec, (classmethod, staticmethod)):
334
- finder.find_spec = wrap_finder_function(find_spec, wrapped_find_spec)
335
- else:
336
- finder.find_spec = wrap_finder_function(finder.find_spec, wrapped_find_spec)
334
+ finder.find_spec = wrap_finder_function(finder.find_spec, wrapped_find_spec)
337
335
 
338
336
 
339
337
  class MetapathObserver(MutableSequence):
@@ -362,6 +360,18 @@ class MetapathObserver(MutableSequence):
362
360
 
363
361
  def initialize():
364
362
  Importer.initialize()
363
+
364
+
365
+ def install():
366
+ """
367
+ Build the filter chain and hook the finders. Must run after the filters have been registered
368
+ (i.e. after configuration.initialize()).
369
+
370
+ The chain is built before the finders are hooked because building it can import modules (e.g.
371
+ since 3.13, importlib.metadata.files imports csv lazily). If the hooks were already in place,
372
+ those imports would need the chain that's still being built.
373
+ """
374
+ Importer.build_filter_chain()
365
375
  # If we're not enabled, there's no reason to hook the finders.
366
376
  if Env.current.enabled:
367
377
  logger.trace("sys.metapath: %s", sys.meta_path)
@@ -17,7 +17,7 @@ def _get_platform_version():
17
17
 
18
18
 
19
19
  def check_py_version():
20
- req = (3, 8)
20
+ req = (3, 11)
21
21
  actual = _get_platform_version()
22
22
  if _get_py_version() < req:
23
23
  raise AppMapPyVerException(
@@ -22,6 +22,9 @@ class C:
22
22
  def with_defaults(self, p1=1, p2=2):
23
23
  return "%s %s" % (p1, p2)
24
24
 
25
+ def mixed(self, a, b=None, c="dflt", *args, d, e="e", **kwargs):
26
+ return "%s %s %s %s %s %s %s" % (a, b, c, args, d, e, kwargs)
27
+
25
28
 
26
29
  if sys.version_info >= (3, 8):
27
30
  exec(
@@ -33,5 +36,9 @@ C.positional_only = positional_only
33
36
  def keyword_only(self, *, p1, p2):
34
37
  return '%s %s' % (p1, p2)
35
38
  C.keyword_only = keyword_only
39
+
40
+ def positional_only_kwargs(self, a, b="dflt", /, **kwargs):
41
+ return '%s %s %s' % (a, b, kwargs)
42
+ C.positional_only_kwargs = positional_only_kwargs
36
43
  """
37
44
  )
@@ -115,14 +115,14 @@ class TestAgentValidate:
115
115
  def test_python_version(self, capsys, mocker):
116
116
  mocker.patch(
117
117
  "_appmap.py_version_check._get_py_version",
118
- return_value=(3, 5),
118
+ return_value=(3, 10),
119
119
  )
120
120
  mocker.patch(
121
121
  "_appmap.py_version_check._get_platform_version",
122
- return_value="3.5",
122
+ return_value="3.10",
123
123
  )
124
124
 
125
- self.check_errors(capsys, 1, 1, r"Minimum Python version supported is \d\.\d, found")
125
+ self.check_errors(capsys, 1, 1, r"Minimum Python version supported is \d+\.\d+, found")
126
126
 
127
127
  def test_django_version(self, capsys, mocker):
128
128
  mocker.patch(
@@ -266,9 +266,6 @@ class TestInstanceMethods(TestMethodBase):
266
266
  },
267
267
  )
268
268
 
269
- @pytest.mark.skipif(
270
- sys.version_info < (3, 8), reason="positional-only added in 3.8"
271
- )
272
269
  @pytest.mark.parametrize("params", ["positional_only"], indirect=True)
273
270
  def test_positional_only(self, params):
274
271
  evt = params.C().positional_only(1, 2)
@@ -282,7 +279,6 @@ class TestInstanceMethods(TestMethodBase):
282
279
  evt, 1, {"class": "builtins.int", "kind": "req", "name": "p2", "value": "2"}
283
280
  )
284
281
 
285
- @pytest.mark.skipif(sys.version_info < (3, 8), reason="keyword-only added in 3.8")
286
282
  @pytest.mark.parametrize("params", ["keyword_only"], indirect=True)
287
283
  def test_keyword_only(self, params):
288
284
  evt = params.C().keyword_only(p2=2, p1=1)
@@ -325,3 +321,161 @@ class TestInstanceMethods(TestMethodBase):
325
321
  self.assert_parameter(
326
322
  evt, 1, {"class": "builtins.int", "kind": "opt", "name": "p2", "value": "4"}
327
323
  )
324
+
325
+ @pytest.mark.parametrize("params", ["with_defaults"], indirect=True)
326
+ def test_defaults_positional(self, params):
327
+ evt = params.C().with_defaults(3, 4)
328
+ assert len(evt.parameters) == 2
329
+
330
+ self.assert_parameter(
331
+ evt, 0, {"class": "builtins.int", "kind": "opt", "name": "p1", "value": "3"}
332
+ )
333
+
334
+ self.assert_parameter(
335
+ evt, 1, {"class": "builtins.int", "kind": "opt", "name": "p2", "value": "4"}
336
+ )
337
+
338
+ @pytest.mark.parametrize("params", ["with_defaults"], indirect=True)
339
+ def test_defaults_positional_and_keyword(self, params):
340
+ evt = params.C().with_defaults(3, p2=4)
341
+ assert len(evt.parameters) == 2
342
+
343
+ self.assert_parameter(
344
+ evt, 0, {"class": "builtins.int", "kind": "opt", "name": "p1", "value": "3"}
345
+ )
346
+
347
+ self.assert_parameter(
348
+ evt, 1, {"class": "builtins.int", "kind": "opt", "name": "p2", "value": "4"}
349
+ )
350
+
351
+
352
+ class TestMixedParams(TestMethodBase):
353
+ """
354
+ C.mixed has the signature (self, a, b=None, c="dflt", *args, d, e="e", **kwargs),
355
+ covering every parameter kind.
356
+ """
357
+
358
+ def assert_values(self, evt, expected):
359
+ actual = {p["name"]: (p["kind"], p["value"]) for p in evt.parameters}
360
+ assert actual == expected
361
+
362
+ @pytest.mark.parametrize("params", ["mixed"], indirect=True)
363
+ def test_positional_opt(self, params):
364
+ evt = params.C().mixed("A", "B", "C", d="D")
365
+ self.assert_values(
366
+ evt,
367
+ {
368
+ "a": ("req", "A"),
369
+ "b": ("opt", "B"),
370
+ "c": ("opt", "C"),
371
+ "args": ("rest", "()"),
372
+ "d": ("keyreq", "D"),
373
+ "e": ("key", "e"),
374
+ "kwargs": ("keyrest", "{}"),
375
+ },
376
+ )
377
+
378
+ @pytest.mark.parametrize("params", ["mixed"], indirect=True)
379
+ def test_partial_positional_opt(self, params):
380
+ evt = params.C().mixed("A", "B", d="D")
381
+ self.assert_values(
382
+ evt,
383
+ {
384
+ "a": ("req", "A"),
385
+ "b": ("opt", "B"),
386
+ "c": ("opt", "dflt"),
387
+ "args": ("rest", "()"),
388
+ "d": ("keyreq", "D"),
389
+ "e": ("key", "e"),
390
+ "kwargs": ("keyrest", "{}"),
391
+ },
392
+ )
393
+
394
+ @pytest.mark.parametrize("params", ["mixed"], indirect=True)
395
+ def test_keyword_opt(self, params):
396
+ evt = params.C().mixed("A", c="C", d="D")
397
+ self.assert_values(
398
+ evt,
399
+ {
400
+ "a": ("req", "A"),
401
+ "b": ("opt", "None"),
402
+ "c": ("opt", "C"),
403
+ "args": ("rest", "()"),
404
+ "d": ("keyreq", "D"),
405
+ "e": ("key", "e"),
406
+ "kwargs": ("keyrest", "{}"),
407
+ },
408
+ )
409
+
410
+ @pytest.mark.parametrize("params", ["mixed"], indirect=True)
411
+ def test_rest_after_opt(self, params):
412
+ evt = params.C().mixed("A", "B", "C", "X", "Y", d="D")
413
+ self.assert_values(
414
+ evt,
415
+ {
416
+ "a": ("req", "A"),
417
+ "b": ("opt", "B"),
418
+ "c": ("opt", "C"),
419
+ "args": ("rest", "('X', 'Y')"),
420
+ "d": ("keyreq", "D"),
421
+ "e": ("key", "e"),
422
+ "kwargs": ("keyrest", "{}"),
423
+ },
424
+ )
425
+
426
+ @pytest.mark.parametrize("params", ["mixed"], indirect=True)
427
+ def test_keyword_only(self, params):
428
+ evt = params.C().mixed("A", d="D", e="E", f="F")
429
+ self.assert_values(
430
+ evt,
431
+ {
432
+ "a": ("req", "A"),
433
+ "b": ("opt", "None"),
434
+ "c": ("opt", "dflt"),
435
+ "args": ("rest", "()"),
436
+ "d": ("keyreq", "D"),
437
+ "e": ("key", "E"),
438
+ "kwargs": ("keyrest", "{'f': 'F'}"),
439
+ },
440
+ )
441
+
442
+ @pytest.mark.parametrize("params", ["mixed"], indirect=True)
443
+ def test_keyword_req(self, params):
444
+ evt = params.C().mixed(a="A", d="D", f="F")
445
+ self.assert_values(
446
+ evt,
447
+ {
448
+ "a": ("req", "A"),
449
+ "b": ("opt", "None"),
450
+ "c": ("opt", "dflt"),
451
+ "args": ("rest", "()"),
452
+ "d": ("keyreq", "D"),
453
+ "e": ("key", "e"),
454
+ "kwargs": ("keyrest", "{'f': 'F'}"),
455
+ },
456
+ )
457
+
458
+ @pytest.mark.parametrize("params", ["positional_only_kwargs"], indirect=True)
459
+ def test_positional_only_name_in_kwargs(self, params):
460
+ # Keyword arguments named like positional-only parameters go to **kwargs
461
+ evt = params.C().positional_only_kwargs("A", "B", a="X", b="Y")
462
+ self.assert_values(
463
+ evt,
464
+ {
465
+ "a": ("req", "A"),
466
+ "b": ("opt", "B"),
467
+ "kwargs": ("keyrest", "{'a': 'X', 'b': 'Y'}"),
468
+ },
469
+ )
470
+
471
+ @pytest.mark.parametrize("params", ["positional_only_kwargs"], indirect=True)
472
+ def test_positional_only_default_name_in_kwargs(self, params):
473
+ evt = params.C().positional_only_kwargs("A", b="Y")
474
+ self.assert_values(
475
+ evt,
476
+ {
477
+ "a": ("req", "A"),
478
+ "b": ("opt", "dflt"),
479
+ "kwargs": ("keyrest", "{'b': 'Y'}"),
480
+ },
481
+ )
@@ -118,8 +118,7 @@ def test_operator_itemgetter(events):
118
118
  assert len(events) == 2
119
119
  assert events[0].to_dict() == DictIncluding({
120
120
  "event": "call",
121
- # operator.itemgetter.__module__ isn't available before 3.10
122
- # "defined_class": "operator",
121
+ "defined_class": "operator",
123
122
  "method_id": "itemgetter (get)",
124
123
  })
125
124
 
@@ -1,3 +1,4 @@
1
+ import os
1
2
  import re
2
3
 
3
4
  import pytest
@@ -37,6 +38,20 @@ def test_runner_multi_recording_type(script_runner, flag, expected):
37
38
  assert len(re.findall("(?m)^APPMAP_RECORD_PYTEST=true$", result.stdout)) == expected
38
39
 
39
40
 
41
+ @pytest.mark.parametrize(
42
+ "flags,expected",
43
+ [
44
+ ([], "true"),
45
+ (["--no-enable-log"], "true"),
46
+ (["--enable-log"], "false"),
47
+ ],
48
+ )
49
+ def test_runner_log_file_disabled_by_default(script_runner, flags, expected):
50
+ result = script_runner.run(["appmap-python", *flags, "--record", "process"])
51
+ assert result.returncode == 0
52
+ assert re.search(f"(?m)^APPMAP_DISABLE_LOG_FILE={expected}$", result.stdout) is not None
53
+
54
+
40
55
  @pytest.mark.script_launch_mode("subprocess")
41
56
  class TestEnv:
42
57
  def test_appmap_present(self, script_runner):
@@ -50,3 +65,19 @@ class TestEnv:
50
65
  )
51
66
  assert result.returncode == 0
52
67
  assert re.match(r"true", result.stdout) is not None
68
+
69
+ def test_internal_state_not_leaked_to_child(self, script_runner):
70
+ # appmap-python is itself instrumented at interpreter startup (via
71
+ # appmap.pth), which can set internal, process-scoped _APPMAP*
72
+ # markers using whatever it inherited, before this script has
73
+ # computed the environment the child command should actually run
74
+ # with. Simulate that by pre-setting one such marker (the
75
+ # once-per-process "startup messages already shown" guard) and
76
+ # confirm it doesn't leak into the child's environment, which would
77
+ # otherwise silently suppress the child's own startup logging.
78
+ env = {**os.environ, "_APPMAP_MESSAGES_SHOWN": "true"}
79
+ result = script_runner.run(
80
+ ["appmap-python", "printenv", "_APPMAP_MESSAGES_SHOWN"], env=env
81
+ )
82
+ assert result.returncode != 0
83
+ assert result.stdout == ""
@@ -53,29 +53,12 @@ _parser.add_argument(
53
53
  default=argparse.SUPPRESS,
54
54
  )
55
55
 
56
- if sys.version_info >= (3, 9):
57
- _parser.add_argument(
58
- "--enable-log",
59
- help="create a log file",
60
- action=argparse.BooleanOptionalAction,
61
- default=False,
62
- )
63
- else:
64
- # You can see why BooleanOptionalAction was added. This is close, though not
65
- # really as good....
66
- _enable_log_group = _parser.add_mutually_exclusive_group()
67
- _enable_log_group.add_argument(
68
- "--enable-log",
69
- help="create a log file",
70
- dest="enable_log",
71
- action="store_true",
72
- )
73
- _enable_log_group.add_argument(
74
- "--no-enable-log",
75
- help="don't create a log file",
76
- dest="enable_log",
77
- action="store_false",
78
- )
56
+ _parser.add_argument(
57
+ "--enable-log",
58
+ help="create a log file",
59
+ action=argparse.BooleanOptionalAction,
60
+ default=False,
61
+ )
79
62
 
80
63
  _parser.add_argument(
81
64
  "command",
@@ -122,7 +105,7 @@ def run():
122
105
  envvars[f"APPMAP_RECORD_{disabled.upper()}"] = "false"
123
106
 
124
107
  envvars["APPMAP_DISABLE_LOG_FILE"] = (
125
- "true" if parsed_args.get("no_enable_log", set()) else "false"
108
+ "false" if parsed_args.get("enable_log", False) else "true"
126
109
  )
127
110
 
128
111
  if len(cmd) == 0:
@@ -130,7 +113,17 @@ def run():
130
113
  print(f"{k}={v}")
131
114
  sys.exit(0)
132
115
 
133
- os.execvpe(cmd[0], cmd, {**os.environ, **envvars})
116
+ # appmap-python is itself instrumented on interpreter startup (via
117
+ # appmap.pth), before this point, using whatever environment it inherited
118
+ # rather than the envvars computed above. That incidental self-init can
119
+ # set internal, process-scoped state under _APPMAP*-prefixed names (e.g.
120
+ # the once-per-process "startup messages already shown" guard); left in
121
+ # place, it would carry over into the child's environment and suppress
122
+ # or corrupt the child's own startup behavior. Drop all of it and let
123
+ # envvars below re-set whatever the child actually needs.
124
+ child_env = {k: v for k, v in os.environ.items() if not k.startswith("_APPMAP")}
125
+ child_env.update(envvars)
126
+ os.execvpe(cmd[0], cmd, child_env)
134
127
 
135
128
 
136
129
  if __name__ == "__main__":