appmap 2.1.3__tar.gz → 2.1.5__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 (183) hide show
  1. {appmap-2.1.3 → appmap-2.1.5}/PKG-INFO +1 -1
  2. {appmap-2.1.3 → appmap-2.1.5}/_appmap/event.py +36 -4
  3. {appmap-2.1.3 → appmap-2.1.5}/_appmap/importer.py +6 -5
  4. {appmap-2.1.3 → appmap-2.1.5}/_appmap/instrument.py +1 -1
  5. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap.yml +1 -0
  6. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/example_class.py +0 -49
  7. appmap-2.1.5/_appmap/test/data/properties_class.py +71 -0
  8. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_params.py +1 -2
  9. appmap-2.1.5/_appmap/test/test_properties.py +148 -0
  10. {appmap-2.1.3 → appmap-2.1.5}/_appmap/utils.py +2 -7
  11. {appmap-2.1.3 → appmap-2.1.5}/appmap/fastapi.py +1 -1
  12. {appmap-2.1.3 → appmap-2.1.5}/pyproject.toml +1 -1
  13. appmap-2.1.3/_appmap/test/test_properties.py +0 -105
  14. {appmap-2.1.3 → appmap-2.1.5}/LICENSE +0 -0
  15. {appmap-2.1.3 → appmap-2.1.5}/README.md +0 -0
  16. {appmap-2.1.3 → appmap-2.1.5}/_appmap/__init__.py +0 -0
  17. {appmap-2.1.3 → appmap-2.1.5}/_appmap/configuration.py +0 -0
  18. {appmap-2.1.3 → appmap-2.1.5}/_appmap/django.py +0 -0
  19. {appmap-2.1.3 → appmap-2.1.5}/_appmap/env.py +0 -0
  20. {appmap-2.1.3 → appmap-2.1.5}/_appmap/fastapi.py +0 -0
  21. {appmap-2.1.3 → appmap-2.1.5}/_appmap/flask.py +0 -0
  22. {appmap-2.1.3 → appmap-2.1.5}/_appmap/generation.py +0 -0
  23. {appmap-2.1.3 → appmap-2.1.5}/_appmap/labels.py +0 -0
  24. {appmap-2.1.3 → appmap-2.1.5}/_appmap/metadata.py +0 -0
  25. {appmap-2.1.3 → appmap-2.1.5}/_appmap/noappmap.py +0 -0
  26. {appmap-2.1.3 → appmap-2.1.5}/_appmap/py_version_check.py +0 -0
  27. {appmap-2.1.3 → appmap-2.1.5}/_appmap/recorder.py +0 -0
  28. {appmap-2.1.3 → appmap-2.1.5}/_appmap/recording.py +0 -0
  29. {appmap-2.1.3 → appmap-2.1.5}/_appmap/remote_recording.py +0 -0
  30. {appmap-2.1.3 → appmap-2.1.5}/_appmap/singleton.py +0 -0
  31. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/__init__.py +0 -0
  32. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/appmap_test_base.py +0 -0
  33. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/bin/server_runner +0 -0
  34. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/conftest.py +0 -0
  35. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap-all-paths-malformed.yml +0 -0
  36. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap-broken.yml +0 -0
  37. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap-class.yml +0 -0
  38. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap-empty-path.yml +0 -0
  39. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap-exclude-fn.yml +0 -0
  40. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap-func.yml +0 -0
  41. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap-malformed-path.yml +0 -0
  42. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap-no-pyyaml.yml +0 -0
  43. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap_testing/__init__.py +0 -0
  44. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/appmap_testing/django_simplelazyobject.py +0 -0
  45. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config/src/package/__init__.py +0 -0
  46. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config/test/__init__.py +0 -0
  47. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config/test/foo.py +0 -0
  48. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config-exclude/.hide/hidden_mod/__init__.py +0 -0
  49. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config-exclude/node_modules/node_mod/__init__.py +0 -0
  50. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config-exclude/src/package/__init__.py +0 -0
  51. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config-exclude/test/__init__.py +0 -0
  52. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config-exclude/venv/venv_mod/__init__.py +0 -0
  53. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config-up/appmap.yml +0 -0
  54. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config-up/project/p1/__init__.py +0 -0
  55. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/config-up/project/p2/sub1/__init__.py +0 -0
  56. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/appmap.yml +0 -0
  57. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/djangoapp/__init__.py +0 -0
  58. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/djangoapp/djangoapp.py +0 -0
  59. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/djangoapp/hello_world.html +0 -0
  60. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/djangoapp/middleware.py +0 -0
  61. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/djangoapp/settings.py +0 -0
  62. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/djangoapp/settings_dev.py +0 -0
  63. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/djangoapp/urls.py +0 -0
  64. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/init/sitecustomize.py +0 -0
  65. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/manage.py +0 -0
  66. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/pytest.ini +0 -0
  67. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/test/__init__.py +0 -0
  68. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/test/test_app.py +0 -0
  69. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/django/test/test_unittest_setup.py +0 -0
  70. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/expected.appmap.json +0 -0
  71. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/fastapi/appmap.yml +0 -0
  72. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/fastapi/fastapiapp/__init__.py +0 -0
  73. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/fastapi/fastapiapp/main.py +0 -0
  74. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/fastapi/init/sitecustomize.py +0 -0
  75. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/fastapi/test_app.py +0 -0
  76. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/flask/appmap.yml +0 -0
  77. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/flask/flaskapp.py +0 -0
  78. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/flask/init/sitecustomize.py +0 -0
  79. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/flask/templates/test.html +0 -0
  80. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/flask/test_app.py +0 -0
  81. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/flask-instrumented/appmap.yml +0 -0
  82. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/flask-instrumented/flaskapp.py +0 -0
  83. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/flask-instrumented/init/sitecustomize.py +0 -0
  84. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/flask-instrumented/test_app.py +0 -0
  85. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/package1/__init__.py +0 -0
  86. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/package1/package2/__init__.py +0 -0
  87. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/package1/package2/__main__.py +0 -0
  88. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/package1/package2/mod1.py +0 -0
  89. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/params.py +0 -0
  90. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/appmap-no-test-cases.yml +0 -0
  91. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/appmap.yml +0 -0
  92. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/expected/pytest-numpy1-no-test-cases.appmap.json +0 -0
  93. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/expected/pytest-numpy1.appmap.json +0 -0
  94. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/expected/pytest-numpy2-no-test-cases.appmap.json +0 -0
  95. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/expected/pytest-numpy2.appmap.json +0 -0
  96. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/expected/status_errored.metadata.json +0 -0
  97. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/expected/status_failed.metadata.json +0 -0
  98. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/expected/status_xfailed.metadata.json +0 -0
  99. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/simple.py +0 -0
  100. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/tests/__init__.py +0 -0
  101. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/tests/test_noappmap.py +0 -0
  102. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest/tests/test_simple.py +0 -0
  103. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest-instrumented/appmap.yml +0 -0
  104. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest-instrumented/init/sitecustomize.py +0 -0
  105. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/pytest-instrumented/test_instrumented.py +0 -0
  106. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/remote.appmap.json +0 -0
  107. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/trial/.gitignore +0 -0
  108. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/trial/appmap-no-test-cases.yml +0 -0
  109. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/trial/appmap.yml +0 -0
  110. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/trial/expected/pytest-no-test-cases.appmap.json +0 -0
  111. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/trial/expected/pytest.appmap.json +0 -0
  112. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/trial/init/sitecustomize.py +0 -0
  113. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/trial/test/__init__.py +0 -0
  114. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/trial/test/test_deferred.py +0 -0
  115. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/appmap-no-test-cases.yml +0 -0
  116. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/appmap.yml +0 -0
  117. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/expected/pytest.appmap.json +0 -0
  118. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/expected/status_errored.metadata.json +0 -0
  119. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/expected/status_failed.metadata.json +0 -0
  120. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/expected/status_xfailed.metadata.json +0 -0
  121. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/expected/status_xsucceeded.metadata.json +0 -0
  122. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/expected/unittest-no-test-cases.appmap.json +0 -0
  123. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/expected/unittest.appmap.json +0 -0
  124. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/init/sitecustomize.py +0 -0
  125. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/simple/__init__.py +0 -0
  126. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/simple/test_noappmap.py +0 -0
  127. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/data/unittest/simple/test_simple.py +0 -0
  128. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/helpers.py +0 -0
  129. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/normalize.py +0 -0
  130. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_command.py +0 -0
  131. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_configuration.py +0 -0
  132. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_describe_value.py +0 -0
  133. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_django.py +0 -0
  134. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_django_simplelazyobject.py +0 -0
  135. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_env.py +0 -0
  136. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_events.py +0 -0
  137. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_fastapi.py +0 -0
  138. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_flask.py +0 -0
  139. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_generation.py +0 -0
  140. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_http.py +0 -0
  141. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_importer.py +0 -0
  142. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_labels.py +0 -0
  143. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_metadata.py +0 -0
  144. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_recording.py +0 -0
  145. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_runner.py +0 -0
  146. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_sqlalchemy.py +0 -0
  147. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_test_frameworks.py +0 -0
  148. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/test_util.py +0 -0
  149. {appmap-2.1.3 → appmap-2.1.5}/_appmap/test/web_framework.py +0 -0
  150. {appmap-2.1.3 → appmap-2.1.5}/_appmap/testing_framework.py +0 -0
  151. {appmap-2.1.3 → appmap-2.1.5}/_appmap/trace_logger.py +0 -0
  152. {appmap-2.1.3 → appmap-2.1.5}/_appmap/unittest.py +0 -0
  153. {appmap-2.1.3 → appmap-2.1.5}/_appmap/web_framework.py +0 -0
  154. {appmap-2.1.3 → appmap-2.1.5}/appmap/__init__.py +0 -0
  155. {appmap-2.1.3 → appmap-2.1.5}/appmap/command/__init__.py +0 -0
  156. {appmap-2.1.3 → appmap-2.1.5}/appmap/command/appmap_agent_init.py +0 -0
  157. {appmap-2.1.3 → appmap-2.1.5}/appmap/command/appmap_agent_status.py +0 -0
  158. {appmap-2.1.3 → appmap-2.1.5}/appmap/command/appmap_agent_validate.py +0 -0
  159. {appmap-2.1.3 → appmap-2.1.5}/appmap/command/runner.py +0 -0
  160. {appmap-2.1.3 → appmap-2.1.5}/appmap/django.py +0 -0
  161. {appmap-2.1.3 → appmap-2.1.5}/appmap/flask.py +0 -0
  162. {appmap-2.1.3 → appmap-2.1.5}/appmap/http.py +0 -0
  163. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/__init__.py +0 -0
  164. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/crypto.yml +0 -0
  165. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/django.yml +0 -0
  166. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/flask.yml +0 -0
  167. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/formats.yml +0 -0
  168. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/http.yml +0 -0
  169. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/jobs.yml +0 -0
  170. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/jwt.yml +0 -0
  171. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/logger.yml +0 -0
  172. {appmap-2.1.3 → appmap-2.1.5}/appmap/labeling/random.yml +0 -0
  173. {appmap-2.1.3 → appmap-2.1.5}/appmap/pytest.py +0 -0
  174. {appmap-2.1.3 → appmap-2.1.5}/appmap/sqlalchemy.py +0 -0
  175. {appmap-2.1.3 → appmap-2.1.5}/appmap/unittest.py +0 -0
  176. {appmap-2.1.3 → appmap-2.1.5}/appmap/uvicorn.py +0 -0
  177. {appmap-2.1.3 → appmap-2.1.5}/appmap.pth +0 -0
  178. {appmap-2.1.3 → appmap-2.1.5}/vendor/_appmap/wrapt/LICENSE +0 -0
  179. {appmap-2.1.3 → appmap-2.1.5}/vendor/_appmap/wrapt/__init__.py +0 -0
  180. {appmap-2.1.3 → appmap-2.1.5}/vendor/_appmap/wrapt/arguments.py +0 -0
  181. {appmap-2.1.3 → appmap-2.1.5}/vendor/_appmap/wrapt/decorators.py +0 -0
  182. {appmap-2.1.3 → appmap-2.1.5}/vendor/_appmap/wrapt/importer.py +0 -0
  183. {appmap-2.1.3 → appmap-2.1.5}/vendor/_appmap/wrapt/wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appmap
3
- Version: 2.1.3
3
+ Version: 2.1.5
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,10 +1,12 @@
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
7
8
  from itertools import chain
9
+ import types
8
10
 
9
11
  from .env import Env
10
12
  from .recorder import Recorder
@@ -174,24 +176,50 @@ class Param:
174
176
  ret.update(describe_value(self.name, value))
175
177
  return ret
176
178
 
179
+ def _get_name_parts(filterable):
180
+ """
181
+ Return the module name and qualname for filterable.obj.
182
+
183
+ If filterable.obj is an operator.attrgetter that we've determined is associated with a property,
184
+ compute the names from the fqname of the filterable. If it's anything else, try to get its
185
+ __module__ and __qualname__, falling back to default values if they're not available.
186
+ """
187
+ fn = filterable.obj
188
+ assert callable(fn), f"{filterable} doesn't have a callable obj"
189
+
190
+ if (
191
+ type(fn) is operator.attrgetter
192
+ and (parts := filterable.fqname.split("."))
193
+ and len(parts) > 2
194
+ ):
195
+ # filterable.fqname was set when we identified this filterable as a property
196
+ modname = ".".join(parts[:-2])
197
+ qualname = ".".join(parts[-2:])
198
+ else:
199
+ modname = getattr(fn, "__module__", "unknown")
200
+ qualname = getattr(fn, "__qualname__", None)
201
+ if qualname is None:
202
+ qualname = getattr(fn.__class__, "__name__", "unknown")
203
+ return modname, qualname
177
204
 
178
205
  class CallEvent(Event):
179
206
  # pylint: disable=method-cache-max-size-none
180
207
  __slots__ = ["_fn", "_fqfn", "static", "receiver", "parameters", "labels", "auxtype"]
181
208
 
182
209
  @staticmethod
183
- def make(fn, fntype):
210
+ def make(filterable):
184
211
  """
185
212
  Return a factory for creating new CallEvents based on
186
213
  introspecting the given function.
187
214
  """
188
215
 
189
216
  # Delete the labels so the app doesn't see them.
217
+ fn = filterable.obj
190
218
  labels = getattr(fn, "_appmap_labels", None)
191
219
  if labels:
192
220
  del fn._appmap_labels
193
221
 
194
- return partial(CallEvent, fn, fntype, labels=labels)
222
+ return partial(CallEvent, filterable, labels=labels)
195
223
 
196
224
  @staticmethod
197
225
  def make_params(filterable):
@@ -312,10 +340,14 @@ class CallEvent(Event):
312
340
  comment = inspect.getcomments(self._fn)
313
341
  return comment
314
342
 
315
- def __init__(self, fn, fntype, parameters, labels):
343
+ def __init__(self, filterable, parameters, labels):
316
344
  super().__init__("call")
345
+ fn = filterable.obj
317
346
  self._fn = fn
318
- self._fqfn = FqFnName(fn)
347
+ modname, qualname = _get_name_parts(filterable)
348
+ self._fqfn = FqFnName(modname, qualname)
349
+
350
+ fntype = filterable.fntype
319
351
  self.static = fntype in FnType.STATIC | FnType.CLASS | FnType.MODULE
320
352
  self.receiver = None
321
353
  if fntype in FnType.CLASS | FnType.INSTANCE:
@@ -5,7 +5,7 @@ import types
5
5
  from abc import ABC, abstractmethod
6
6
  from collections import namedtuple
7
7
  from collections.abc import MutableSequence
8
- from functools import reduce
8
+ from functools import partial, reduce
9
9
 
10
10
  from _appmap import wrapt
11
11
 
@@ -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
 
@@ -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
@@ -121,7 +121,7 @@ def instrument(filterable):
121
121
  logger.debug("hooking %s", filterable.fqname)
122
122
  fn = filterable.obj
123
123
 
124
- make_call_event = event.CallEvent.make(fn, filterable.fntype)
124
+ make_call_event = event.CallEvent.make(filterable)
125
125
  params = CallEvent.make_params(filterable)
126
126
 
127
127
  # django depends on being able to find the cache_clear attribute
@@ -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
@@ -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))
@@ -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
@@ -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)
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "appmap"
3
- version = "2.1.3"
3
+ version = "2.1.5"
4
4
  description = "Create AppMap files by recording a Python application."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,105 +0,0 @@
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 = [
9
- pytest.mark.appmap_enabled,
10
- ]
11
-
12
-
13
- @pytest.fixture(autouse=True)
14
- def setup(with_data_dir): # pylint: disable=unused-argument
15
- # with_data_dir sets up sys.path so example_class can be imported
16
- pass
17
-
18
-
19
- def test_getter_instrumented(events):
20
- from example_class import ExampleClass
21
-
22
- ec = ExampleClass()
23
-
24
- actual = ExampleClass.read_only.__doc__
25
- assert actual == "Read-only"
26
-
27
- assert ec.read_only == "read only"
28
-
29
- with pytest.raises(AttributeError, match=r".*(has no setter|can't set attribute).*"):
30
- # E AttributeError: can't set attribute
31
-
32
- ec.read_only = "not allowed"
33
-
34
- with pytest.raises(AttributeError, match=r".*(has no deleter|can't delete attribute).*"):
35
- del ec.read_only
36
-
37
- assert len(events) == 2
38
- assert events[0].to_dict() == DictIncluding(
39
- {
40
- "event": "call",
41
- "defined_class": "example_class.ExampleClass",
42
- "method_id": "read_only (get)",
43
- }
44
- )
45
-
46
-
47
- def test_accessible_instrumented(events):
48
- from example_class import ExampleClass
49
-
50
- ec = ExampleClass()
51
- assert ExampleClass.fully_accessible.__doc__ == "Fully-accessible"
52
-
53
- assert ec.fully_accessible == "fully accessible"
54
-
55
- ec.fully_accessible = "updated"
56
- # Check the value of the attribute directly, to avoid extra events
57
- assert ec._fully_accessible == "updated" # pylint: disable=protected-access
58
-
59
- del ec.fully_accessible
60
-
61
- # assert len(events) == 6
62
- assert events[0].to_dict() == DictIncluding(
63
- {
64
- "event": "call",
65
- "defined_class": "example_class.ExampleClass",
66
- "method_id": "fully_accessible (get)",
67
- }
68
- )
69
-
70
- assert events[2].to_dict() == DictIncluding(
71
- {
72
- "event": "call",
73
- "defined_class": "example_class.ExampleClass",
74
- "method_id": "fully_accessible (set)",
75
- }
76
- )
77
-
78
- assert events[4].to_dict() == DictIncluding(
79
- {
80
- "event": "call",
81
- "defined_class": "example_class.ExampleClass",
82
- "method_id": "fully_accessible (del)",
83
- }
84
- )
85
-
86
-
87
- def test_writable_instrumented(events):
88
- from example_class import ExampleClass
89
-
90
- ec = ExampleClass()
91
- assert ExampleClass.write_only.__doc__ == "Write-only"
92
-
93
- with pytest.raises(AttributeError, match=r".*(has no getter|unreadable attribute).*"):
94
- _ = ec.write_only
95
-
96
- ec.write_only = "updated example"
97
-
98
- assert len(events) == 2
99
- assert events[0].to_dict() == DictIncluding(
100
- {
101
- "event": "call",
102
- "defined_class": "example_class.ExampleClass",
103
- "method_id": "set_write_only (set)",
104
- }
105
- )
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes