appmap 2.1.4__tar.gz → 2.1.6__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 (184) hide show
  1. {appmap-2.1.4 → appmap-2.1.6}/PKG-INFO +1 -1
  2. {appmap-2.1.4 → appmap-2.1.6}/_appmap/event.py +35 -4
  3. {appmap-2.1.4 → appmap-2.1.6}/_appmap/importer.py +6 -5
  4. {appmap-2.1.4 → appmap-2.1.6}/_appmap/instrument.py +1 -2
  5. {appmap-2.1.4 → appmap-2.1.6}/_appmap/recording.py +1 -0
  6. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap.yml +1 -0
  7. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/djangoapp/settings.py +13 -0
  8. appmap-2.1.6/_appmap/test/data/django/test/test_request.py +8 -0
  9. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/example_class.py +0 -49
  10. appmap-2.1.6/_appmap/test/data/properties_class.py +71 -0
  11. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_django.py +3 -3
  12. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_params.py +1 -2
  13. appmap-2.1.6/_appmap/test/test_properties.py +148 -0
  14. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_recording.py +38 -23
  15. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_test_frameworks.py +14 -0
  16. {appmap-2.1.4 → appmap-2.1.6}/_appmap/utils.py +2 -7
  17. {appmap-2.1.4 → appmap-2.1.6}/appmap/fastapi.py +1 -1
  18. {appmap-2.1.4 → appmap-2.1.6}/appmap/pytest.py +8 -1
  19. {appmap-2.1.4 → appmap-2.1.6}/pyproject.toml +1 -1
  20. {appmap-2.1.4 → appmap-2.1.6}/vendor/_appmap/wrapt/wrappers.py +40 -21
  21. appmap-2.1.4/_appmap/test/test_properties.py +0 -102
  22. {appmap-2.1.4 → appmap-2.1.6}/LICENSE +0 -0
  23. {appmap-2.1.4 → appmap-2.1.6}/README.md +0 -0
  24. {appmap-2.1.4 → appmap-2.1.6}/_appmap/__init__.py +0 -0
  25. {appmap-2.1.4 → appmap-2.1.6}/_appmap/configuration.py +0 -0
  26. {appmap-2.1.4 → appmap-2.1.6}/_appmap/django.py +0 -0
  27. {appmap-2.1.4 → appmap-2.1.6}/_appmap/env.py +0 -0
  28. {appmap-2.1.4 → appmap-2.1.6}/_appmap/fastapi.py +0 -0
  29. {appmap-2.1.4 → appmap-2.1.6}/_appmap/flask.py +0 -0
  30. {appmap-2.1.4 → appmap-2.1.6}/_appmap/generation.py +0 -0
  31. {appmap-2.1.4 → appmap-2.1.6}/_appmap/labels.py +0 -0
  32. {appmap-2.1.4 → appmap-2.1.6}/_appmap/metadata.py +0 -0
  33. {appmap-2.1.4 → appmap-2.1.6}/_appmap/noappmap.py +0 -0
  34. {appmap-2.1.4 → appmap-2.1.6}/_appmap/py_version_check.py +0 -0
  35. {appmap-2.1.4 → appmap-2.1.6}/_appmap/recorder.py +0 -0
  36. {appmap-2.1.4 → appmap-2.1.6}/_appmap/remote_recording.py +0 -0
  37. {appmap-2.1.4 → appmap-2.1.6}/_appmap/singleton.py +0 -0
  38. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/__init__.py +0 -0
  39. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/appmap_test_base.py +0 -0
  40. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/bin/server_runner +0 -0
  41. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/conftest.py +0 -0
  42. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap-all-paths-malformed.yml +0 -0
  43. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap-broken.yml +0 -0
  44. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap-class.yml +0 -0
  45. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap-empty-path.yml +0 -0
  46. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap-exclude-fn.yml +0 -0
  47. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap-func.yml +0 -0
  48. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap-malformed-path.yml +0 -0
  49. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap-no-pyyaml.yml +0 -0
  50. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap_testing/__init__.py +0 -0
  51. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/appmap_testing/django_simplelazyobject.py +0 -0
  52. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config/src/package/__init__.py +0 -0
  53. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config/test/__init__.py +0 -0
  54. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config/test/foo.py +0 -0
  55. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config-exclude/.hide/hidden_mod/__init__.py +0 -0
  56. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config-exclude/node_modules/node_mod/__init__.py +0 -0
  57. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config-exclude/src/package/__init__.py +0 -0
  58. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config-exclude/test/__init__.py +0 -0
  59. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config-exclude/venv/venv_mod/__init__.py +0 -0
  60. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config-up/appmap.yml +0 -0
  61. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config-up/project/p1/__init__.py +0 -0
  62. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/config-up/project/p2/sub1/__init__.py +0 -0
  63. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/appmap.yml +0 -0
  64. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/djangoapp/__init__.py +0 -0
  65. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/djangoapp/djangoapp.py +0 -0
  66. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/djangoapp/hello_world.html +0 -0
  67. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/djangoapp/middleware.py +0 -0
  68. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/djangoapp/settings_dev.py +0 -0
  69. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/djangoapp/urls.py +0 -0
  70. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/init/sitecustomize.py +0 -0
  71. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/manage.py +0 -0
  72. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/pytest.ini +0 -0
  73. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/test/__init__.py +0 -0
  74. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/test/test_app.py +0 -0
  75. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/django/test/test_unittest_setup.py +0 -0
  76. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/expected.appmap.json +0 -0
  77. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/fastapi/appmap.yml +0 -0
  78. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/fastapi/fastapiapp/__init__.py +0 -0
  79. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/fastapi/fastapiapp/main.py +0 -0
  80. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/fastapi/init/sitecustomize.py +0 -0
  81. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/fastapi/test_app.py +0 -0
  82. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/flask/appmap.yml +0 -0
  83. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/flask/flaskapp.py +0 -0
  84. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/flask/init/sitecustomize.py +0 -0
  85. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/flask/templates/test.html +0 -0
  86. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/flask/test_app.py +0 -0
  87. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/flask-instrumented/appmap.yml +0 -0
  88. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/flask-instrumented/flaskapp.py +0 -0
  89. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/flask-instrumented/init/sitecustomize.py +0 -0
  90. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/flask-instrumented/test_app.py +0 -0
  91. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/package1/__init__.py +0 -0
  92. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/package1/package2/__init__.py +0 -0
  93. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/package1/package2/__main__.py +0 -0
  94. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/package1/package2/mod1.py +0 -0
  95. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/params.py +0 -0
  96. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/appmap-no-test-cases.yml +0 -0
  97. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/appmap.yml +0 -0
  98. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/expected/pytest-numpy1-no-test-cases.appmap.json +0 -0
  99. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/expected/pytest-numpy1.appmap.json +0 -0
  100. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/expected/pytest-numpy2-no-test-cases.appmap.json +0 -0
  101. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/expected/pytest-numpy2.appmap.json +0 -0
  102. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/expected/status_errored.metadata.json +0 -0
  103. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/expected/status_failed.metadata.json +0 -0
  104. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/expected/status_xfailed.metadata.json +0 -0
  105. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/simple.py +0 -0
  106. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/tests/__init__.py +0 -0
  107. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/tests/test_noappmap.py +0 -0
  108. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest/tests/test_simple.py +0 -0
  109. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest-instrumented/appmap.yml +0 -0
  110. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest-instrumented/init/sitecustomize.py +0 -0
  111. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/pytest-instrumented/test_instrumented.py +0 -0
  112. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/remote.appmap.json +0 -0
  113. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/trial/.gitignore +0 -0
  114. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/trial/appmap-no-test-cases.yml +0 -0
  115. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/trial/appmap.yml +0 -0
  116. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/trial/expected/pytest-no-test-cases.appmap.json +0 -0
  117. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/trial/expected/pytest.appmap.json +0 -0
  118. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/trial/init/sitecustomize.py +0 -0
  119. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/trial/test/__init__.py +0 -0
  120. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/trial/test/test_deferred.py +0 -0
  121. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/appmap-no-test-cases.yml +0 -0
  122. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/appmap.yml +0 -0
  123. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/expected/pytest.appmap.json +0 -0
  124. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/expected/status_errored.metadata.json +0 -0
  125. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/expected/status_failed.metadata.json +0 -0
  126. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/expected/status_xfailed.metadata.json +0 -0
  127. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/expected/status_xsucceeded.metadata.json +0 -0
  128. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/expected/unittest-no-test-cases.appmap.json +0 -0
  129. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/expected/unittest.appmap.json +0 -0
  130. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/init/sitecustomize.py +0 -0
  131. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/simple/__init__.py +0 -0
  132. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/simple/test_noappmap.py +0 -0
  133. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/data/unittest/simple/test_simple.py +0 -0
  134. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/helpers.py +0 -0
  135. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/normalize.py +0 -0
  136. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_command.py +0 -0
  137. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_configuration.py +0 -0
  138. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_describe_value.py +0 -0
  139. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_django_simplelazyobject.py +0 -0
  140. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_env.py +0 -0
  141. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_events.py +0 -0
  142. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_fastapi.py +0 -0
  143. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_flask.py +0 -0
  144. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_generation.py +0 -0
  145. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_http.py +0 -0
  146. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_importer.py +0 -0
  147. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_labels.py +0 -0
  148. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_metadata.py +0 -0
  149. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_runner.py +0 -0
  150. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_sqlalchemy.py +0 -0
  151. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/test_util.py +0 -0
  152. {appmap-2.1.4 → appmap-2.1.6}/_appmap/test/web_framework.py +0 -0
  153. {appmap-2.1.4 → appmap-2.1.6}/_appmap/testing_framework.py +0 -0
  154. {appmap-2.1.4 → appmap-2.1.6}/_appmap/trace_logger.py +0 -0
  155. {appmap-2.1.4 → appmap-2.1.6}/_appmap/unittest.py +0 -0
  156. {appmap-2.1.4 → appmap-2.1.6}/_appmap/web_framework.py +0 -0
  157. {appmap-2.1.4 → appmap-2.1.6}/appmap/__init__.py +0 -0
  158. {appmap-2.1.4 → appmap-2.1.6}/appmap/command/__init__.py +0 -0
  159. {appmap-2.1.4 → appmap-2.1.6}/appmap/command/appmap_agent_init.py +0 -0
  160. {appmap-2.1.4 → appmap-2.1.6}/appmap/command/appmap_agent_status.py +0 -0
  161. {appmap-2.1.4 → appmap-2.1.6}/appmap/command/appmap_agent_validate.py +0 -0
  162. {appmap-2.1.4 → appmap-2.1.6}/appmap/command/runner.py +0 -0
  163. {appmap-2.1.4 → appmap-2.1.6}/appmap/django.py +0 -0
  164. {appmap-2.1.4 → appmap-2.1.6}/appmap/flask.py +0 -0
  165. {appmap-2.1.4 → appmap-2.1.6}/appmap/http.py +0 -0
  166. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/__init__.py +0 -0
  167. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/crypto.yml +0 -0
  168. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/django.yml +0 -0
  169. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/flask.yml +0 -0
  170. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/formats.yml +0 -0
  171. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/http.yml +0 -0
  172. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/jobs.yml +0 -0
  173. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/jwt.yml +0 -0
  174. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/logger.yml +0 -0
  175. {appmap-2.1.4 → appmap-2.1.6}/appmap/labeling/random.yml +0 -0
  176. {appmap-2.1.4 → appmap-2.1.6}/appmap/sqlalchemy.py +0 -0
  177. {appmap-2.1.4 → appmap-2.1.6}/appmap/unittest.py +0 -0
  178. {appmap-2.1.4 → appmap-2.1.6}/appmap/uvicorn.py +0 -0
  179. {appmap-2.1.4 → appmap-2.1.6}/appmap.pth +0 -0
  180. {appmap-2.1.4 → appmap-2.1.6}/vendor/_appmap/wrapt/LICENSE +0 -0
  181. {appmap-2.1.4 → appmap-2.1.6}/vendor/_appmap/wrapt/__init__.py +0 -0
  182. {appmap-2.1.4 → appmap-2.1.6}/vendor/_appmap/wrapt/arguments.py +0 -0
  183. {appmap-2.1.4 → appmap-2.1.6}/vendor/_appmap/wrapt/decorators.py +0 -0
  184. {appmap-2.1.4 → appmap-2.1.6}/vendor/_appmap/wrapt/importer.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appmap
3
- Version: 2.1.4
3
+ Version: 2.1.6
4
4
  Summary: Create AppMap files by recording a Python application.
5
5
  Home-page: https://github.com/applandinc/appmap-python
6
6
  License: MIT
@@ -1,6 +1,7 @@
1
1
  # pylint: disable=missing-module-docstring,missing-class-docstring,missing-function-docstring
2
2
  import inspect
3
3
  import logging
4
+ import operator
4
5
  import threading
5
6
  from functools import lru_cache, partial
6
7
  from inspect import Parameter, Signature
@@ -174,24 +175,50 @@ class Param:
174
175
  ret.update(describe_value(self.name, value))
175
176
  return ret
176
177
 
178
+ def _get_name_parts(filterable):
179
+ """
180
+ Return the module name and qualname for filterable.obj.
181
+
182
+ If filterable.obj is an operator.attrgetter that we've determined is associated with a property,
183
+ compute the names from the fqname of the filterable. If it's anything else, try to get its
184
+ __module__ and __qualname__, falling back to default values if they're not available.
185
+ """
186
+ fn = filterable.obj
187
+ assert callable(fn), f"{filterable} doesn't have a callable obj"
188
+
189
+ if (
190
+ type(fn) is operator.attrgetter
191
+ and (parts := filterable.fqname.split("."))
192
+ and len(parts) > 2
193
+ ):
194
+ # filterable.fqname was set when we identified this filterable as a property
195
+ modname = ".".join(parts[:-2])
196
+ qualname = ".".join(parts[-2:])
197
+ else:
198
+ modname = getattr(fn, "__module__", "unknown")
199
+ qualname = getattr(fn, "__qualname__", None)
200
+ if qualname is None:
201
+ qualname = getattr(fn.__class__, "__name__", "unknown")
202
+ return modname, qualname
177
203
 
178
204
  class CallEvent(Event):
179
205
  # pylint: disable=method-cache-max-size-none
180
206
  __slots__ = ["_fn", "_fqfn", "static", "receiver", "parameters", "labels", "auxtype"]
181
207
 
182
208
  @staticmethod
183
- def make(fn, fntype):
209
+ def make(filterable):
184
210
  """
185
211
  Return a factory for creating new CallEvents based on
186
212
  introspecting the given function.
187
213
  """
188
214
 
189
215
  # Delete the labels so the app doesn't see them.
216
+ fn = filterable.obj
190
217
  labels = getattr(fn, "_appmap_labels", None)
191
218
  if labels:
192
219
  del fn._appmap_labels
193
220
 
194
- return partial(CallEvent, fn, fntype, labels=labels)
221
+ return partial(CallEvent, filterable, labels=labels)
195
222
 
196
223
  @staticmethod
197
224
  def make_params(filterable):
@@ -312,10 +339,14 @@ class CallEvent(Event):
312
339
  comment = inspect.getcomments(self._fn)
313
340
  return comment
314
341
 
315
- def __init__(self, fn, fntype, parameters, labels):
342
+ def __init__(self, filterable, parameters, labels):
316
343
  super().__init__("call")
344
+ fn = filterable.obj
317
345
  self._fn = fn
318
- self._fqfn = FqFnName(fn)
346
+ modname, qualname = _get_name_parts(filterable)
347
+ self._fqfn = FqFnName(modname, qualname)
348
+
349
+ fntype = filterable.fntype
319
350
  self.static = fntype in FnType.STATIC | FnType.CLASS | FnType.MODULE
320
351
  self.receiver = None
321
352
  if fntype in FnType.CLASS | FnType.INSTANCE:
@@ -134,7 +134,7 @@ def get_members(cls):
134
134
  static_value = inspect.getattr_static(cls, key)
135
135
  # Don't use isinstance to check the type of static_value -- we don't want to invoke the
136
136
  # descriptor protocol.
137
- if Importer.instrument_properties and type(static_value) is property: # pylint: disable=unidiomatic-typecheck
137
+ if Importer.instrument_properties and type(static_value) is property:
138
138
  properties[key] = (
139
139
  static_value,
140
140
  {
@@ -147,7 +147,7 @@ def get_members(cls):
147
147
  if not is_member_func(static_value):
148
148
  continue
149
149
  value = getattr(cls, key)
150
- if value.__module__ != modname:
150
+ if (m := getattr(value, "__module__", None)) and (m is None or m != modname):
151
151
  continue
152
152
  functions.append((key, static_value, value))
153
153
 
@@ -167,7 +167,7 @@ class Importer:
167
167
  cls.filter_chain = []
168
168
  cls._skip_instrumenting = ("appmap", "_appmap")
169
169
  cls.instrument_properties = (
170
- Env.current.get("APPMAP_INSTRUMENT_PROPERTIES", "false").lower() == "true"
170
+ Env.current.get("APPMAP_INSTRUMENT_PROPERTIES", "true").lower() == "true"
171
171
  )
172
172
 
173
173
  @classmethod
@@ -202,7 +202,7 @@ class Importer:
202
202
  logger.trace(" functions %s", functions)
203
203
 
204
204
  for fn_name, static_fn, fn in functions:
205
- filterableFn = FilterableFn(filterable, fn.__name__, fn, static_fn)
205
+ filterableFn = FilterableFn(filterable, fn_name, fn, static_fn)
206
206
  new_fn = cls.instrument_function(fn_name, filterableFn, selected_functions)
207
207
  if new_fn != fn:
208
208
  fw = wrapt.wrap_function_wrapper(filterable.obj, fn_name, new_fn)
@@ -221,7 +221,8 @@ class Importer:
221
221
  new_fn = cls.instrument_function(prop_name, filterableFn, selected_functions)
222
222
  if new_fn != fn:
223
223
  new_fn = wrapt.FunctionWrapper(fn, new_fn)
224
- # Set _appmap_instrumented on the FunctionWrapper, not on the wrapped function
224
+ # Set _appmap_instrumented on the FunctionWrapper, not on the wrapped
225
+ # function.
225
226
  new_fn._appmap_instrumented = True # pylint: disable=protected-access
226
227
 
227
228
  instrumented_fns[k] = new_fn
@@ -119,9 +119,8 @@ def call_instrumented(f, instance, args, kwargs):
119
119
  def instrument(filterable):
120
120
  """return an instrumented function"""
121
121
  logger.debug("hooking %s", filterable.fqname)
122
- fn = filterable.obj
123
122
 
124
- make_call_event = event.CallEvent.make(fn, filterable.fntype)
123
+ make_call_event = event.CallEvent.make(filterable)
125
124
  params = CallEvent.make_params(filterable)
126
125
 
127
126
  # django depends on being able to find the cache_clear attribute
@@ -62,6 +62,7 @@ class NoopRecording:
62
62
 
63
63
  def __init__(self, exit_hook=None):
64
64
  self.exit_hook = exit_hook
65
+ self.events = []
65
66
 
66
67
  def start(self):
67
68
  pass
@@ -4,6 +4,7 @@ packages:
4
4
  - path: example_class.Super
5
5
  shallow: true
6
6
  - path: example_class
7
+ - path: properties_class
7
8
  - path: appmap_testing
8
9
  - path: package1
9
10
  - dist: PyYAML
@@ -1,3 +1,9 @@
1
+ from pathlib import Path
2
+
3
+
4
+ # Build paths inside the project like this: BASE_DIR / 'subdir'.
5
+ BASE_DIR = Path(__file__).resolve().parent.parent
6
+
1
7
  # If the SECRET_KEY isn't defined we get the misleading error message
2
8
  # CommandError: You must set settings.ALLOWED_HOSTS if DEBUG is False.
3
9
  SECRET_KEY = "3*+d^_kjnr2gz)4q2m(&&^%$p4fj5dk3%lz4pl3g4m-%6!gf&)"
@@ -10,3 +16,10 @@ ROOT_URLCONF = "djangoapp.urls"
10
16
 
11
17
  # Turn off deprecation warning
12
18
  USE_TZ = True
19
+
20
+ DATABASES = {
21
+ "default": {
22
+ "ENGINE": "django.db.backends.sqlite3",
23
+ "NAME": BASE_DIR / "db.sqlite3",
24
+ }
25
+ }
@@ -0,0 +1,8 @@
1
+ from django.test import TestCase
2
+ from django.test import Client
3
+
4
+
5
+ class TestRequest(TestCase):
6
+ def test_request_test(self):
7
+ resp = self.client.get("/test")
8
+ assert resp.status_code == 200
@@ -114,55 +114,6 @@ class ExampleClass(Super, ClassMethodMixin):
114
114
  def return_self(self):
115
115
  return self
116
116
 
117
- def __init__(self):
118
- self._read_only = "read only"
119
- self._fully_accessible = "fully accessible"
120
- self._undecorated = "undecorated"
121
-
122
- @property
123
- def read_only(self):
124
- """Read-only"""
125
- return self._read_only
126
-
127
- @property
128
- def fully_accessible(self):
129
- """Fully-accessible"""
130
- return self._fully_accessible
131
-
132
- @fully_accessible.setter
133
- def fully_accessible(self, v):
134
- self._fully_accessible = v
135
-
136
- @fully_accessible.deleter
137
- def fully_accessible(self):
138
- del self._fully_accessible
139
-
140
- def get_undecorated(self):
141
- return self._undecorated
142
-
143
- def set_undecorated(self, value):
144
- self._undecorated = value
145
-
146
- def delete_undecorated(self):
147
- del self._undecorated
148
-
149
- undecorated_property = property(get_undecorated, set_undecorated, delete_undecorated)
150
-
151
- def set_write_only(self, v):
152
- self._write_only = v
153
-
154
- def del_write_only(self):
155
- del self._write_only
156
-
157
- write_only = property(None, set_write_only, del_write_only, "Write-only")
158
-
159
- def raise_base_exception(self) -> NoReturn:
160
- raise BaseException("not derived from Exception") # pylint: disable=broad-exception-raised
161
117
 
162
118
  def modfunc():
163
119
  return "Hello world!"
164
-
165
- if __name__ == "__main__":
166
- ec = ExampleClass()
167
- ec.fully_accessible = "updated"
168
- print(ec.fully_accessible)
@@ -0,0 +1,71 @@
1
+ from functools import cached_property, partial
2
+ import operator
3
+ from typing import NoReturn
4
+
5
+ def free_read_only(self):
6
+ return self._read_only
7
+
8
+ def free_func():
9
+ return "hello world"
10
+ class PropertiesClass:
11
+ def __init__(self):
12
+ self._read_only = "read only"
13
+ self._fully_accessible = "fully accessible"
14
+ self._undecorated = "undecorated"
15
+
16
+ @property
17
+ def read_only(self):
18
+ """Read-only"""
19
+ return self._read_only
20
+
21
+ @property
22
+ def fully_accessible(self):
23
+ """Fully-accessible"""
24
+ return self._fully_accessible
25
+
26
+ @fully_accessible.setter
27
+ def fully_accessible(self, v):
28
+ self._fully_accessible = v
29
+
30
+ @fully_accessible.deleter
31
+ def fully_accessible(self):
32
+ del self._fully_accessible
33
+
34
+ def get_undecorated(self):
35
+ return self._undecorated
36
+
37
+ def set_undecorated(self, value):
38
+ self._undecorated = value
39
+
40
+ def delete_undecorated(self):
41
+ del self._undecorated
42
+
43
+ undecorated_property = property(get_undecorated, set_undecorated, delete_undecorated)
44
+
45
+ def set_write_only(self, v):
46
+ self._write_only = v
47
+
48
+ def del_write_only(self):
49
+ del self._write_only
50
+
51
+ write_only = property(None, set_write_only, del_write_only, "Write-only")
52
+
53
+ def raise_base_exception(self) -> NoReturn:
54
+ raise BaseException("not derived from Exception") # pylint: disable=broad-exception-raised
55
+
56
+ @cached_property
57
+ def cached_read_only(self):
58
+ return self._read_only
59
+
60
+ operator_read_only = property(operator.attrgetter("cached_read_only"))
61
+
62
+ tastes = {"bacon": "yum"}
63
+
64
+ def __getitem__(self, key):
65
+ return self.tastes[key]
66
+
67
+ taste = property(operator.itemgetter("bacon"))
68
+
69
+ free_read_only_prop = property(free_read_only)
70
+
71
+ static_partial_method = staticmethod(partial(free_func))
@@ -198,7 +198,7 @@ class TestDjangoApp:
198
198
  # To really check middleware reset, the tests must run in order,
199
199
  # so disable randomly.
200
200
  result = pytester.runpytest("-svv", "-p", "no:randomly")
201
- result.assert_outcomes(passed=5, failed=0, errors=0)
201
+ result.assert_outcomes(passed=6, failed=0, errors=0)
202
202
  # Look for the http_server_request event in test_app's appmap. If
203
203
  # middleware reset is broken, it won't be there.
204
204
  appmap_file = pytester.path / "tmp" / "appmap" / "pytest" / "test_request.appmap.json"
@@ -212,7 +212,7 @@ class TestDjangoApp:
212
212
  def test_disabled(self, pytester, monkeypatch):
213
213
  monkeypatch.setenv("_APPMAP", "false")
214
214
  result = pytester.runpytest("-svv", "-p", "no:randomly", "-k", "test_request")
215
- result.assert_outcomes(passed=2, failed=0, errors=0)
215
+ result.assert_outcomes(passed=3, failed=0, errors=0)
216
216
  assert not (pytester.path / "tmp").exists()
217
217
 
218
218
  def test_disabled_for_process(self, pytester, monkeypatch):
@@ -222,7 +222,7 @@ class TestDjangoApp:
222
222
 
223
223
  # There are two tests for remote recording. They should both fail,
224
224
  # because process recording should disable remote recording.
225
- result.assert_outcomes(passed=3, failed=2, errors=0)
225
+ result.assert_outcomes(passed=4, failed=2, errors=0)
226
226
 
227
227
  assert (pytester.path / "tmp" / "appmap" / "process").exists()
228
228
  assert not (pytester.path / "tmp" / "appmap" / "requests").exists()
@@ -28,8 +28,7 @@ class _params:
28
28
 
29
29
  @classmethod
30
30
  def prepare(cls, ffn):
31
- fn = ffn.obj
32
- make_call_event = CallEvent.make(fn, ffn.fntype)
31
+ make_call_event = CallEvent.make(ffn)
33
32
  params = CallEvent.make_params(ffn)
34
33
 
35
34
  def wrapped_fn(_, instance, args, kwargs):
@@ -0,0 +1,148 @@
1
+ """Tests for methods decorated with @property"""
2
+
3
+ # pyright: reportMissingImports=false
4
+ # pylint: disable=import-error,import-outside-toplevel
5
+ import pytest
6
+ from _appmap.test.helpers import DictIncluding
7
+
8
+ pytestmark = pytest.mark.appmap_enabled
9
+
10
+ @pytest.fixture(autouse=True)
11
+ def setup(with_data_dir): # pylint: disable=unused-argument
12
+ # with_data_dir sets up sys.path so properties_class can be imported
13
+ pass
14
+
15
+
16
+ def test_getter_instrumented(events):
17
+ from properties_class import PropertiesClass
18
+
19
+ ec = PropertiesClass()
20
+
21
+ actual = PropertiesClass.read_only.__doc__
22
+ assert actual == "Read-only"
23
+
24
+ assert ec.read_only == "read only"
25
+
26
+ with pytest.raises(AttributeError, match=r".*(has no setter|can't set attribute).*"):
27
+ ec.read_only = "not allowed"
28
+
29
+ with pytest.raises(AttributeError, match=r".*(has no deleter|can't delete attribute).*"):
30
+ del ec.read_only
31
+
32
+ assert len(events) == 2
33
+ assert events[0].to_dict() == DictIncluding({
34
+ "event": "call",
35
+ "defined_class": "properties_class.PropertiesClass",
36
+ "method_id": "read_only (get)",
37
+ })
38
+
39
+
40
+ def test_accessible_instrumented(events):
41
+ from properties_class import PropertiesClass
42
+
43
+ ec = PropertiesClass()
44
+ assert PropertiesClass.fully_accessible.__doc__ == "Fully-accessible"
45
+
46
+ assert ec.fully_accessible == "fully accessible"
47
+
48
+ ec.fully_accessible = "updated"
49
+ # Check the value of the attribute directly, to avoid extra events
50
+ assert ec._fully_accessible == "updated" # pylint: disable=protected-access
51
+
52
+ del ec.fully_accessible
53
+
54
+ assert len(events) == 6
55
+ assert events[0].to_dict() == DictIncluding({
56
+ "event": "call",
57
+ "defined_class": "properties_class.PropertiesClass",
58
+ "method_id": "fully_accessible (get)",
59
+ })
60
+
61
+ assert events[2].to_dict() == DictIncluding({
62
+ "event": "call",
63
+ "defined_class": "properties_class.PropertiesClass",
64
+ "method_id": "fully_accessible (set)",
65
+ })
66
+
67
+ assert events[4].to_dict() == DictIncluding({
68
+ "event": "call",
69
+ "defined_class": "properties_class.PropertiesClass",
70
+ "method_id": "fully_accessible (del)",
71
+ })
72
+
73
+
74
+ def test_writable_instrumented(events):
75
+ from properties_class import PropertiesClass
76
+
77
+ ec = PropertiesClass()
78
+ assert PropertiesClass.write_only.__doc__ == "Write-only"
79
+
80
+ with pytest.raises(AttributeError, match=r".*(has no getter|unreadable attribute).*"):
81
+ _ = ec.write_only
82
+
83
+ ec.write_only = "updated example"
84
+
85
+ assert len(events) == 2
86
+ assert events[0].to_dict() == DictIncluding({
87
+ "event": "call",
88
+ "defined_class": "properties_class.PropertiesClass",
89
+ "method_id": "set_write_only (set)",
90
+ })
91
+
92
+
93
+ def test_operator_attrgetter(events):
94
+ from properties_class import PropertiesClass
95
+
96
+ ec = PropertiesClass()
97
+
98
+ assert ec.operator_read_only == "read only"
99
+
100
+ with pytest.raises(AttributeError, match=r".*(has no setter|can't set attribute).*"):
101
+ ec.operator_read_only = "not allowed"
102
+
103
+ with pytest.raises(AttributeError, match=r".*(has no deleter|can't delete attribute).*"):
104
+ del ec.operator_read_only
105
+
106
+ assert len(events) == 2
107
+ assert events[0].to_dict() == DictIncluding({
108
+ "event": "call",
109
+ "defined_class": "properties_class.PropertiesClass",
110
+ "method_id": "operator_read_only (get)",
111
+ })
112
+
113
+ def test_operator_itemgetter(events):
114
+ from properties_class import PropertiesClass
115
+
116
+ ec = PropertiesClass()
117
+ assert ec.taste == "yum"
118
+ assert len(events) == 2
119
+ assert events[0].to_dict() == DictIncluding({
120
+ "event": "call",
121
+ # operator.itemgetter.__module__ isn't available before 3.10
122
+ # "defined_class": "operator",
123
+ "method_id": "itemgetter (get)",
124
+ })
125
+
126
+
127
+ def test_free_function(events):
128
+ from properties_class import PropertiesClass
129
+
130
+ ec = PropertiesClass()
131
+ assert ec.free_read_only_prop == "read only"
132
+ assert len(events) == 2
133
+ assert events[0].to_dict() == DictIncluding({
134
+ "event": "call",
135
+ "defined_class": "properties_class",
136
+ "method_id": "free_read_only (get)",
137
+ })
138
+
139
+
140
+ @pytest.mark.xfail(
141
+ raises=AssertionError,
142
+ reason="needs fix for https://github.com/getappmap/appmap-python/issues/365",
143
+ )
144
+ def test_functools_partial(events):
145
+ from properties_class import PropertiesClass
146
+
147
+ PropertiesClass.static_partial_method()
148
+ assert len(events) > 0
@@ -9,6 +9,7 @@ from threading import Thread
9
9
  import pytest
10
10
 
11
11
  import appmap
12
+ from _appmap.configuration import Config
12
13
  from _appmap.event import Event
13
14
  from _appmap.recorder import Recorder, ThreadRecorder
14
15
  from _appmap.wrapt import FunctionWrapper
@@ -16,6 +17,17 @@ from _appmap.wrapt import FunctionWrapper
16
17
  from .normalize import normalize_appmap, remove_line_numbers
17
18
 
18
19
 
20
+ def _call_modfunc(q):
21
+ r = appmap.Recording()
22
+ with r:
23
+ f = q.get()
24
+ f()
25
+ events = r.events
26
+ assert len(events) == 2
27
+ assert events[0].event == "call"
28
+ assert events[0].method_id == "modfunc"
29
+
30
+
19
31
  @pytest.mark.appmap_enabled
20
32
  @pytest.mark.usefixtures("with_data_dir")
21
33
  class TestRecordingWhenEnabled:
@@ -47,11 +59,12 @@ class TestRecordingWhenEnabled:
47
59
  ), f"expected path {expected_path}"
48
60
 
49
61
  def test_recording_clears(self):
50
- from example_class import ( # pyright: ignore[reportMissingImports] pylint: disable=import-error
51
- ExampleClass,
52
- )
62
+ # pylint: disable=import-error
63
+ from example_class import ExampleClass # pyright: ignore[reportMissingImports]
64
+ # pylint: enable=import-error
53
65
 
54
- with appmap.Recording():
66
+ rec = appmap.Recording()
67
+ with rec:
55
68
  ExampleClass.static_method()
56
69
 
57
70
  # fresh recording shouldn't contain previous traces
@@ -68,9 +81,9 @@ class TestRecordingWhenEnabled:
68
81
  assert rec.events[2].method_id == "instance_method"
69
82
 
70
83
  def test_recording_shallow(self):
71
- from example_class import ( # pyright: ignore[reportMissingImports] pylint: disable=import-error
72
- ExampleClass,
73
- )
84
+ # pylint: disable=import-error
85
+ from example_class import ExampleClass # pyright: ignore[reportMissingImports]
86
+ # pylint: enable=import-error
74
87
 
75
88
  rec = appmap.Recording()
76
89
  with rec:
@@ -82,9 +95,9 @@ class TestRecordingWhenEnabled:
82
95
  assert len(rec.events) == 8
83
96
 
84
97
  def test_recording_wrapped(self):
85
- from example_class import ( # pyright: ignore[reportMissingImports] pylint: disable=import-error
86
- ExampleClass,
87
- )
98
+ # pylint: disable=import-error
99
+ from example_class import ExampleClass # pyright: ignore[reportMissingImports]
100
+ # pylint: enable=import-error
88
101
 
89
102
  rec = appmap.Recording()
90
103
  with rec:
@@ -117,21 +130,23 @@ class TestRecordingWhenEnabled:
117
130
  f1 = deepcopy(modfunc)
118
131
  f1()
119
132
 
120
- def test_can_pickle(self):
121
- import pickle
133
+ def test_can_pickle(self, monkeypatch):
134
+ # Make sure subprocesses see whatever config is set for us.
135
+ monkeypatch.setenv("APPMAP_CONFIG", str(Config.current._file)) # pylint: disable=protected-access
122
136
 
123
- from example_class import ( # pyright: ignore[reportMissingImports] pylint: disable=import-error
124
- modfunc,
125
- )
137
+ from multiprocessing import Process, Queue
126
138
 
127
- rec = appmap.Recording()
128
- with rec:
129
- assert isinstance(modfunc, FunctionWrapper)
130
- f = pickle.loads(pickle.dumps(modfunc))
131
- f()
132
- evt = rec.events[-2]
133
- assert evt.event == "call"
134
- assert evt.method_id == "modfunc"
139
+ # pylint: disable=import-error
140
+ from example_class import modfunc # pyright: ignore[reportMissingImports]
141
+ # pylint: enable=import-error
142
+
143
+ assert isinstance(modfunc, FunctionWrapper), "modfunc isn't instrumented?"
144
+
145
+ q = Queue()
146
+ q.put(modfunc)
147
+ p = Process(target=_call_modfunc, args=(q,))
148
+ p.start()
149
+ p.join()
135
150
 
136
151
 
137
152
  @pytest.mark.appmap_enabled
@@ -122,6 +122,20 @@ class TestPytestRunnerPytest(_TestTestRunner):
122
122
  verify_expected_appmap(testdir, f"-numpy{numpy_version.major}-no-test-cases")
123
123
  verify_expected_metadata(testdir)
124
124
 
125
+ @pytest.mark.example_dir("django")
126
+ def test_pytest_django(testdir):
127
+ result = testdir.runpytest("-svv", "-k", "test_request_test")
128
+ result.assert_outcomes(passed=1)
129
+ # django.test.TestCase is a subclass of unittest.TestCase, so recorder type is unittest
130
+ assert (
131
+ testdir.path
132
+ / "tmp"
133
+ / "appmap"
134
+ / "unittest"
135
+ / "test_test_request_TestRequest_test_request_test.appmap.json"
136
+ ).exists()
137
+ assert not (testdir.path / "tmp" / "appmap" / "requests").exists()
138
+
125
139
 
126
140
  @pytest.mark.example_dir("trial")
127
141
  class TestPytestRunnerTrial(_TestTestRunner):
@@ -8,7 +8,6 @@ from contextlib import contextmanager
8
8
  from contextvars import ContextVar
9
9
  from enum import Enum, IntFlag, auto
10
10
  from pathlib import Path
11
- from typing import Any, Callable
12
11
 
13
12
  from .env import Env
14
13
 
@@ -79,10 +78,8 @@ class FqFnName:
79
78
  FqFnName makes it easy to reference the parts of the fully-qualified name of a callable.
80
79
  """
81
80
 
82
- def __init__(self, fn: Callable[..., Any]):
83
-
84
- self._modname = fn.__module__
85
- qualname = fn.__qualname__
81
+ def __init__(self, modname, qualname):
82
+ self._modname = modname
86
83
  if "." in qualname:
87
84
  self._scope = Scope.CLASS
88
85
  self._class_name, self._fn_name = qualname.rsplit(".", 1)
@@ -112,8 +109,6 @@ class FqFnName:
112
109
  def fn_name(self):
113
110
  return self._fn_name
114
111
 
115
- FqFnName(fqname)
116
-
117
112
  def root_relative_path(path):
118
113
  """Returns the path relative to the current root_dir.
119
114
 
@@ -31,7 +31,7 @@ def _add_api_route(wrapped, _, args, kwargs):
31
31
 
32
32
  fn = args[1]
33
33
 
34
- fqn = utils.FqFnName(fn)
34
+ fqn = utils.FqFnName(fn.__module__, fn.__qualname__)
35
35
  scope = Filterable(fqn.scope, fqn.fqclass, None)
36
36
 
37
37
  filterable_fn = FilterableFn(scope, fn.__name__, fn, fn)