appmap 2.0.10__tar.gz → 2.1.1__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 (172) hide show
  1. {appmap-2.0.10 → appmap-2.1.1}/PKG-INFO +1 -1
  2. {appmap-2.0.10 → appmap-2.1.1}/_appmap/env.py +2 -0
  3. {appmap-2.0.10 → appmap-2.1.1}/_appmap/event.py +24 -12
  4. {appmap-2.0.10 → appmap-2.1.1}/_appmap/importer.py +44 -13
  5. {appmap-2.0.10 → appmap-2.1.1}/_appmap/recorder.py +2 -1
  6. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/conftest.py +46 -5
  7. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/example_class.py +47 -0
  8. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/flask/flaskapp.py +12 -1
  9. appmap-2.1.1/_appmap/test/data/flask/test_app.py +26 -0
  10. appmap-2.1.1/_appmap/test/data/flask-instrumented/appmap.yml +4 -0
  11. appmap-2.1.1/_appmap/test/data/flask-instrumented/flaskapp.py +30 -0
  12. appmap-2.1.1/_appmap/test/data/flask-instrumented/test_app.py +31 -0
  13. appmap-2.0.10/_appmap/test/data/pytest/expected/pytest.appmap.json → appmap-2.1.1/_appmap/test/data/pytest/expected/pytest-numpy1.appmap.json +113 -51
  14. appmap-2.1.1/_appmap/test/data/pytest/expected/pytest-numpy2.appmap.json +242 -0
  15. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/expected/status_errored.metadata.json +1 -1
  16. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/expected/status_failed.metadata.json +1 -1
  17. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/expected/status_xfailed.metadata.json +1 -1
  18. appmap-2.1.1/_appmap/test/data/pytest/simple.py +19 -0
  19. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/test_simple.py +4 -2
  20. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/helpers.py +22 -0
  21. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_command.py +2 -2
  22. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_configuration.py +11 -11
  23. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_django.py +0 -3
  24. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_django_simplelazyobject.py +2 -1
  25. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_events.py +4 -4
  26. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_fastapi.py +5 -1
  27. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_flask.py +107 -9
  28. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_params.py +2 -2
  29. appmap-2.1.1/_appmap/test/test_properties.py +105 -0
  30. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_recording.py +4 -5
  31. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_test_frameworks.py +13 -42
  32. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/web_framework.py +11 -15
  33. {appmap-2.0.10 → appmap-2.1.1}/_appmap/utils.py +4 -0
  34. {appmap-2.0.10 → appmap-2.1.1}/_appmap/web_framework.py +24 -13
  35. {appmap-2.0.10 → appmap-2.1.1}/appmap/django.py +11 -3
  36. {appmap-2.0.10 → appmap-2.1.1}/appmap/fastapi.py +11 -3
  37. {appmap-2.0.10 → appmap-2.1.1}/appmap/flask.py +51 -22
  38. appmap-2.1.1/appmap.pth +1 -0
  39. {appmap-2.0.10 → appmap-2.1.1}/pyproject.toml +3 -1
  40. appmap-2.0.10/_appmap/test/data/flask/test_app.py +0 -14
  41. appmap-2.0.10/_appmap/test/data/pytest/simple.py +0 -18
  42. {appmap-2.0.10 → appmap-2.1.1}/LICENSE +0 -0
  43. {appmap-2.0.10 → appmap-2.1.1}/README.md +0 -0
  44. {appmap-2.0.10 → appmap-2.1.1}/_appmap/__init__.py +0 -0
  45. {appmap-2.0.10 → appmap-2.1.1}/_appmap/configuration.py +0 -0
  46. {appmap-2.0.10 → appmap-2.1.1}/_appmap/django.py +0 -0
  47. {appmap-2.0.10 → appmap-2.1.1}/_appmap/fastapi.py +0 -0
  48. {appmap-2.0.10 → appmap-2.1.1}/_appmap/flask.py +0 -0
  49. {appmap-2.0.10 → appmap-2.1.1}/_appmap/generation.py +0 -0
  50. {appmap-2.0.10 → appmap-2.1.1}/_appmap/instrument.py +0 -0
  51. {appmap-2.0.10 → appmap-2.1.1}/_appmap/labels.py +0 -0
  52. {appmap-2.0.10 → appmap-2.1.1}/_appmap/metadata.py +0 -0
  53. {appmap-2.0.10 → appmap-2.1.1}/_appmap/noappmap.py +0 -0
  54. {appmap-2.0.10 → appmap-2.1.1}/_appmap/py_version_check.py +0 -0
  55. {appmap-2.0.10 → appmap-2.1.1}/_appmap/recording.py +0 -0
  56. {appmap-2.0.10 → appmap-2.1.1}/_appmap/remote_recording.py +0 -0
  57. {appmap-2.0.10 → appmap-2.1.1}/_appmap/singleton.py +0 -0
  58. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/__init__.py +0 -0
  59. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/appmap_test_base.py +0 -0
  60. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/bin/server_runner +0 -0
  61. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-all-paths-malformed.yml +0 -0
  62. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-broken.yml +0 -0
  63. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-class.yml +0 -0
  64. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-empty-path.yml +0 -0
  65. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-exclude-fn.yml +0 -0
  66. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-func.yml +0 -0
  67. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-malformed-path.yml +0 -0
  68. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-no-pyyaml.yml +0 -0
  69. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap.yml +0 -0
  70. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap_testing/__init__.py +0 -0
  71. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap_testing/django_simplelazyobject.py +0 -0
  72. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config/src/package/__init__.py +0 -0
  73. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config/test/__init__.py +0 -0
  74. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config/test/foo.py +0 -0
  75. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/.hide/hidden_mod/__init__.py +0 -0
  76. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/node_modules/node_mod/__init__.py +0 -0
  77. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/src/package/__init__.py +0 -0
  78. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/test/__init__.py +0 -0
  79. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/venv/venv_mod/__init__.py +0 -0
  80. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-up/appmap.yml +0 -0
  81. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-up/project/p1/__init__.py +0 -0
  82. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-up/project/p2/sub1/__init__.py +0 -0
  83. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/appmap.yml +0 -0
  84. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/__init__.py +0 -0
  85. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/djangoapp.py +0 -0
  86. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/hello_world.html +0 -0
  87. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/middleware.py +0 -0
  88. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/settings.py +0 -0
  89. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/settings_dev.py +0 -0
  90. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/urls.py +0 -0
  91. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/init/sitecustomize.py +0 -0
  92. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/manage.py +0 -0
  93. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/pytest.ini +0 -0
  94. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/test/__init__.py +0 -0
  95. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/test/test_app.py +0 -0
  96. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/test/test_unittest_setup.py +0 -0
  97. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/expected.appmap.json +0 -0
  98. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/appmap.yml +0 -0
  99. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/fastapiapp/__init__.py +0 -0
  100. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/fastapiapp/main.py +0 -0
  101. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/init/sitecustomize.py +0 -0
  102. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/test_app.py +0 -0
  103. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/flask/appmap.yml +0 -0
  104. {appmap-2.0.10/_appmap/test/data/trial → appmap-2.1.1/_appmap/test/data/flask}/init/sitecustomize.py +0 -0
  105. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/flask/templates/test.html +0 -0
  106. {appmap-2.0.10/_appmap/test/data/flask → appmap-2.1.1/_appmap/test/data/flask-instrumented}/init/sitecustomize.py +0 -0
  107. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/package1/__init__.py +0 -0
  108. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/package1/package2/__init__.py +0 -0
  109. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/package1/package2/__main__.py +0 -0
  110. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/package1/package2/mod1.py +0 -0
  111. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/params.py +0 -0
  112. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/appmap.yml +0 -0
  113. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/test_noappmap.py +0 -0
  114. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/remote.appmap.json +0 -0
  115. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/.gitignore +0 -0
  116. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/appmap.yml +0 -0
  117. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/expected/pytest.appmap.json +0 -0
  118. {appmap-2.0.10/_appmap/test/data/unittest → appmap-2.1.1/_appmap/test/data/trial}/init/sitecustomize.py +0 -0
  119. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/test/__init__.py +0 -0
  120. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/test/test_deferred.py +0 -0
  121. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/appmap.yml +0 -0
  122. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/pytest.appmap.json +0 -0
  123. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/status_errored.metadata.json +0 -0
  124. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/status_failed.metadata.json +0 -0
  125. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/status_xfailed.metadata.json +0 -0
  126. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/status_xsucceeded.metadata.json +0 -0
  127. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/unittest.appmap.json +0 -0
  128. /appmap-2.0.10/appmap.pth → /appmap-2.1.1/_appmap/test/data/unittest/init/sitecustomize.py +0 -0
  129. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/simple/__init__.py +0 -0
  130. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/simple/test_noappmap.py +0 -0
  131. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/simple/test_simple.py +0 -0
  132. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/normalize.py +0 -0
  133. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_describe_value.py +0 -0
  134. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_env.py +0 -0
  135. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_generation.py +0 -0
  136. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_http.py +0 -0
  137. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_importer.py +0 -0
  138. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_labels.py +0 -0
  139. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_metadata.py +0 -0
  140. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_runner.py +0 -0
  141. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_sqlalchemy.py +0 -0
  142. {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_util.py +0 -0
  143. {appmap-2.0.10 → appmap-2.1.1}/_appmap/testing_framework.py +0 -0
  144. {appmap-2.0.10 → appmap-2.1.1}/_appmap/trace_logger.py +0 -0
  145. {appmap-2.0.10 → appmap-2.1.1}/_appmap/unittest.py +0 -0
  146. {appmap-2.0.10 → appmap-2.1.1}/appmap/__init__.py +0 -0
  147. {appmap-2.0.10 → appmap-2.1.1}/appmap/command/__init__.py +0 -0
  148. {appmap-2.0.10 → appmap-2.1.1}/appmap/command/appmap_agent_init.py +0 -0
  149. {appmap-2.0.10 → appmap-2.1.1}/appmap/command/appmap_agent_status.py +0 -0
  150. {appmap-2.0.10 → appmap-2.1.1}/appmap/command/appmap_agent_validate.py +0 -0
  151. {appmap-2.0.10 → appmap-2.1.1}/appmap/command/runner.py +0 -0
  152. {appmap-2.0.10 → appmap-2.1.1}/appmap/http.py +0 -0
  153. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/__init__.py +0 -0
  154. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/crypto.yml +0 -0
  155. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/django.yml +0 -0
  156. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/flask.yml +0 -0
  157. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/formats.yml +0 -0
  158. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/http.yml +0 -0
  159. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/jobs.yml +0 -0
  160. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/jwt.yml +0 -0
  161. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/logger.yml +0 -0
  162. {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/random.yml +0 -0
  163. {appmap-2.0.10 → appmap-2.1.1}/appmap/pytest.py +0 -0
  164. {appmap-2.0.10 → appmap-2.1.1}/appmap/sqlalchemy.py +0 -0
  165. {appmap-2.0.10 → appmap-2.1.1}/appmap/unittest.py +0 -0
  166. {appmap-2.0.10 → appmap-2.1.1}/appmap/uvicorn.py +0 -0
  167. {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/LICENSE +0 -0
  168. {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/__init__.py +0 -0
  169. {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/arguments.py +0 -0
  170. {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/decorators.py +0 -0
  171. {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/importer.py +0 -0
  172. {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/wrappers.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: appmap
3
- Version: 2.0.10
3
+ Version: 2.1.1
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
@@ -39,6 +39,8 @@ class Env(metaclass=SingletonMeta):
39
39
 
40
40
  self.log_file_creation_failed = False
41
41
  self._configure_logging()
42
+ # This uses _APPMAP, rather than APPMAP, to control whether instrumentation is enabled. The
43
+ # tests use this split to make it easier to control recording.
42
44
  enabled = self._env.get("_APPMAP", "false")
43
45
  self._enabled = enabled is None or enabled.lower() != "false"
44
46
 
@@ -94,7 +94,8 @@ def _describe_schema(name, val, depth, max_depth):
94
94
  if islist:
95
95
  elts = [(None, v) for v in val]
96
96
  schema_key = "items"
97
- elif isdict:
97
+ else:
98
+ assert isdict
98
99
  elts = val.items()
99
100
  schema_key = "properties"
100
101
 
@@ -176,7 +177,7 @@ class Param:
176
177
 
177
178
  class CallEvent(Event):
178
179
  # pylint: disable=method-cache-max-size-none
179
- __slots__ = ["_fn", "_fqfn", "static", "receiver", "parameters", "labels"]
180
+ __slots__ = ["_fn", "_fqfn", "static", "receiver", "parameters", "labels", "auxtype"]
180
181
 
181
182
  @staticmethod
182
183
  def make(fn, fntype):
@@ -283,7 +284,10 @@ class CallEvent(Event):
283
284
  @property
284
285
  @lru_cache(maxsize=None)
285
286
  def method_id(self):
286
- return self._fqfn.fqfn[1]
287
+ ret = self._fqfn.fqfn[1]
288
+ if self.auxtype is not None:
289
+ ret = f"{ret} ({self.auxtype})"
290
+ return ret
287
291
 
288
292
  @property
289
293
  @lru_cache(maxsize=None)
@@ -319,6 +323,13 @@ class CallEvent(Event):
319
323
  parameters = parameters[1:]
320
324
  self.parameters = parameters
321
325
  self.labels = labels
326
+ self.auxtype = None
327
+ if fntype & FnType.GET:
328
+ self.auxtype = "get"
329
+ elif fntype & FnType.SET:
330
+ self.auxtype = "set"
331
+ elif fntype & FnType.DEL:
332
+ self.auxtype = "del"
322
333
 
323
334
  def to_dict(self, attrs=None):
324
335
  ret = super().to_dict() # get the attrs defined in __slots__
@@ -469,18 +480,19 @@ class HttpResponseEvent(ReturnEvent):
469
480
 
470
481
  def __init__(self, status_code, headers=None, **kwargs):
471
482
  super().__init__(**kwargs)
483
+ self.response = {}
484
+ self.update(status_code, headers)
472
485
 
473
- response = {"status_code": status_code}
486
+ def update(self, status_code, headers):
487
+ if status_code is not None:
488
+ self.response.update({"status_code": status_code})
474
489
 
475
490
  if headers is not None:
476
- response.update(
477
- {
478
- "mime_type": headers.get("Content-Type"),
479
- "headers": none_if_empty(dict(headers)),
480
- }
481
- )
482
-
483
- self.response = compact_dict(response)
491
+ if "Content-Type" in headers:
492
+ self.response.update({"mime_type": headers.get("Content-Type")})
493
+ updated_headers = dict(headers)
494
+ if len(updated_headers) > 0:
495
+ self.response.update({"headers": updated_headers})
484
496
 
485
497
 
486
498
  # pylint: disable=too-few-public-methods
@@ -37,14 +37,14 @@ class FilterableCls(Filterable):
37
37
  class FilterableFn(
38
38
  namedtuple(
39
39
  "FilterableFn",
40
- Filterable._fields + ("static_fn",),
40
+ Filterable._fields + ("static_fn", "auxtype"),
41
41
  )
42
42
  ):
43
43
  __slots__ = ()
44
44
 
45
- def __new__(cls, scope, fn, static_fn):
45
+ def __new__(cls, scope, fn, static_fn, auxtype=None):
46
46
  fqname = "%s.%s" % (scope.fqname, fn.__name__)
47
- self = super(FilterableFn, cls).__new__(cls, scope.scope, fqname, fn, static_fn)
47
+ self = super(FilterableFn, cls).__new__(cls, scope.scope, fqname, fn, static_fn, auxtype)
48
48
  return self
49
49
 
50
50
  @property
@@ -52,7 +52,10 @@ class FilterableFn(
52
52
  if self.scope == Scope.MODULE:
53
53
  return FnType.MODULE
54
54
 
55
- return FnType.classify(self.static_fn)
55
+ ret = FnType.classify(self.static_fn)
56
+ if self.auxtype is not None:
57
+ ret |= self.auxtype
58
+ return ret
56
59
 
57
60
 
58
61
  class Filter(ABC): # pylint: disable=too-few-public-methods
@@ -122,19 +125,31 @@ def get_members(cls):
122
125
  # instead iterate over dir(cls), we would see functions from
123
126
  # superclasses, too. Those don't need to be instrumented here,
124
127
  # they'll get taken care of when the superclass is imported.
125
- ret = []
128
+ functions = []
129
+ properties = {}
126
130
  modname = cls.__module__ if hasattr(cls, "__module__") else cls.__name__
127
131
  for key in cls.__dict__:
128
132
  if key.startswith("__"):
129
133
  continue
130
134
  static_value = inspect.getattr_static(cls, key)
131
- if not is_member_func(static_value):
132
- continue
133
- value = getattr(cls, key)
134
- if value.__module__ != modname:
135
- continue
136
- ret.append((key, static_value, value))
137
- return ret
135
+ if isinstance(static_value, property):
136
+ properties[key] = (
137
+ static_value,
138
+ {
139
+ "fget": (static_value.fget, FnType.GET),
140
+ "fset": (static_value.fset, FnType.SET),
141
+ "fdel": (static_value.fdel, FnType.DEL),
142
+ },
143
+ )
144
+ else:
145
+ if not is_member_func(static_value):
146
+ continue
147
+ value = getattr(cls, key)
148
+ if value.__module__ != modname:
149
+ continue
150
+ functions.append((key, static_value, value))
151
+
152
+ return (functions, properties)
138
153
 
139
154
 
140
155
  class Importer:
@@ -177,7 +192,7 @@ class Importer:
177
192
 
178
193
  def instrument_functions(filterable, selected_functions=None):
179
194
  logger.trace(" looking for members of %s", filterable.obj)
180
- functions = get_members(filterable.obj)
195
+ functions, properties = get_members(filterable.obj)
181
196
  logger.trace(" functions %s", functions)
182
197
 
183
198
  for fn_name, static_fn, fn in functions:
@@ -185,6 +200,22 @@ class Importer:
185
200
  new_fn = cls.instrument_function(fn_name, filterableFn, selected_functions)
186
201
  if new_fn != fn:
187
202
  wrapt.wrap_function_wrapper(filterable.obj, fn_name, new_fn)
203
+ # Now that we've instrumented all the functions, go through the properties and update
204
+ # them
205
+ for prop_name, (prop, prop_fns) in properties.items():
206
+ instrumented_fns = {}
207
+ for k, (fn, auxtype) in prop_fns.items():
208
+ if fn is None or getattr(fn, "_appmap_wrapped", None):
209
+ continue
210
+ filterableFn = FilterableFn(filterable, fn, fn, auxtype)
211
+ new_fn = cls.instrument_function(fn.__name__, filterableFn, selected_functions)
212
+ if new_fn != fn:
213
+ new_fn = wrapt.FunctionWrapper(fn, new_fn)
214
+ setattr(new_fn, "_appmap_wrapped", True)
215
+ instrumented_fns[k] = new_fn
216
+ if len(instrumented_fns) > 0:
217
+ instrumented_fns["doc"] = prop.__doc__
218
+ setattr(filterable.obj, prop_name, property(**instrumented_fns))
188
219
 
189
220
  # Import Config here, to avoid circular top-level imports.
190
221
  from .configuration import Config # pylint: disable=import-outside-toplevel
@@ -114,7 +114,8 @@ class Recorder(ABC):
114
114
  def check_time(cls, event_time):
115
115
  if _MAX_TIME is None:
116
116
  return
117
- if event_time - cls.get_current()._start_time > _MAX_TIME:
117
+ delta = event_time - cls.get_current()._start_time # pylint: disable=protected-access
118
+ if delta > _MAX_TIME:
118
119
  raise AppMapSessionTooLong(f"Session exceeded {_MAX_TIME} seconds")
119
120
 
120
121
  @classmethod
@@ -2,7 +2,7 @@ import importlib
2
2
  import os
3
3
  import socket
4
4
  import sys
5
- from distutils.dir_util import copy_tree
5
+ from shutil import copytree
6
6
  from functools import partial, partialmethod
7
7
  from pathlib import Path
8
8
  from typing import Any
@@ -14,7 +14,7 @@ from xprocess import ProcessStarter
14
14
 
15
15
  import _appmap
16
16
  import appmap
17
- from _appmap.test.web_framework import TEST_HOST, TEST_PORT
17
+ from _appmap.test.web_framework import TEST_HOST
18
18
  from appmap import generation
19
19
 
20
20
  from .. import utils
@@ -98,7 +98,7 @@ def git_directory_fixture(tmp_path_factory):
98
98
 
99
99
  @pytest.fixture(name="git")
100
100
  def tmp_git(git_directory, tmp_path):
101
- copy_tree(git_directory, str(tmp_path))
101
+ copytree(git_directory, str(tmp_path), dirs_exist_ok=True)
102
102
  return utils.git(cwd=tmp_path)
103
103
 
104
104
 
@@ -198,14 +198,55 @@ def server_starter(info, name, cmd, pattern, env=None):
198
198
 
199
199
  return _starter
200
200
 
201
+ @pytest.fixture(name="server_port")
202
+ def server_port_fixture(worker_id):
203
+ if worker_id == "master":
204
+ offset = "0"
205
+ else:
206
+ offset = worker_id[2:]
207
+ return 8000 + int(offset)
208
+
201
209
 
202
210
  @pytest.fixture(name="server_base")
203
- def server_base_fixture(request):
211
+ def server_base_fixture(request, server_port):
204
212
  marker = request.node.get_closest_marker("server")
205
213
  debug = marker.kwargs.get("debug", False)
206
214
  server_env = os.environ.copy()
207
215
  server_env.update(marker.kwargs.get("env", {}))
208
216
 
209
- info = ServerInfo(debug=debug, host=TEST_HOST, port=TEST_PORT, env=server_env)
217
+ info = ServerInfo(debug=debug, host=TEST_HOST, port=server_port, env=server_env)
210
218
  info.factory = partial(server_starter, info)
211
219
  return info
220
+
221
+ @pytest.fixture(name="testdir")
222
+ def testdir_fixture(request, data_dir, pytester, monkeypatch):
223
+ # We need to set environment variables to control how tests are run. This will only work
224
+ # properly if pytester runs pytest in a subprocess.
225
+ assert (
226
+ pytester._method == "subprocess" # pylint:disable=protected-access
227
+ ), "must run pytest in a subprocess"
228
+
229
+ # The init subdirectory contains a sitecustomize.py file that
230
+ # imports the appmap module. This simulates the way a real
231
+ # installation works, performing the same function as the the
232
+ # appmap.pth file that gets put in site-packages.
233
+ monkeypatch.setenv("PYTHONPATH", "init")
234
+
235
+ # Make sure _APPMAP isn't in the environment, to test that recording-by-default is working as
236
+ # expected. Individual test cases may set it as necessary.
237
+ monkeypatch.delenv("_APPMAP", raising=False)
238
+
239
+ marker = request.node.get_closest_marker("example_dir")
240
+ test_type = "unittest" if marker is None else marker.args[0]
241
+ pytester.copy_example(test_type)
242
+
243
+ pytester.expected = data_dir / test_type / "expected"
244
+ pytester.test_type = test_type
245
+
246
+ # this is so test_type can be overriden in test cases
247
+ def output_dir():
248
+ return pytester.path / "tmp" / "appmap" / pytester.test_type
249
+
250
+ pytester.output = output_dir
251
+
252
+ return pytester
@@ -113,6 +113,53 @@ class ExampleClass(Super, ClassMethodMixin):
113
113
  def return_self(self):
114
114
  return self
115
115
 
116
+ def __init__(self):
117
+ self._read_only = "read only"
118
+ self._fully_accessible = "fully accessible"
119
+ self._undecorated = "undecorated"
120
+
121
+ @property
122
+ def read_only(self):
123
+ """Read-only"""
124
+ return self._read_only
125
+
126
+ @property
127
+ def fully_accessible(self):
128
+ """Fully-accessible"""
129
+ return self._fully_accessible
130
+
131
+ @fully_accessible.setter
132
+ def fully_accessible(self, v):
133
+ self._fully_accessible = v
134
+
135
+ @fully_accessible.deleter
136
+ def fully_accessible(self):
137
+ del self._fully_accessible
138
+
139
+ def get_undecorated(self):
140
+ return self._undecorated
141
+
142
+ def set_undecorated(self, value):
143
+ self._undecorated = value
144
+
145
+ def delete_undecorated(self):
146
+ del self._undecorated
147
+
148
+ undecorated_property = property(get_undecorated, set_undecorated, delete_undecorated)
149
+
150
+ def set_write_only(self, v):
151
+ self._write_only = v
152
+
153
+ def del_write_only(self):
154
+ del self._write_only
155
+
156
+ write_only = property(None, set_write_only, del_write_only, "Write-only")
157
+
116
158
 
117
159
  def modfunc():
118
160
  return "Hello world!"
161
+
162
+ if __name__ == "__main__":
163
+ ec = ExampleClass()
164
+ ec.fully_accessible = "updated"
165
+ print(ec.fully_accessible)
@@ -6,8 +6,9 @@ testing of record-by-default.
6
6
  """
7
7
  # pylint: disable=missing-function-docstring
8
8
 
9
- from flask import Flask, make_response
9
+ from flask import Flask, make_response, request
10
10
  from markupsafe import escape
11
+ import werkzeug
11
12
 
12
13
  app = Flask(__name__)
13
14
 
@@ -50,3 +51,13 @@ def show_org_user_posts(org, username):
50
51
  @app.route("/exception")
51
52
  def raise_exception():
52
53
  raise Exception("An exception")
54
+
55
+ @app.post("/do_post")
56
+ def do_post():
57
+ _ = request.get_json()
58
+ return "Got post request"
59
+
60
+
61
+ @app.errorhandler(werkzeug.exceptions.BadRequest)
62
+ def handle_bad_request(e):
63
+ return "That's a bad request!", 400
@@ -0,0 +1,26 @@
1
+ import pytest
2
+ from flaskapp import app
3
+
4
+
5
+ @pytest.fixture(name="client")
6
+ def test_client():
7
+ with app.test_client() as c: # pylint: disable=no-member
8
+ yield c
9
+
10
+
11
+ def test_request(client):
12
+ response = client.get("/")
13
+
14
+ assert response.status_code == 200
15
+
16
+ def test_not_found(client):
17
+ response = client.get("/not_found")
18
+
19
+ assert response.status_code == 404
20
+
21
+
22
+ def test_errorhandler(client):
23
+ response = client.post("/do_post", content_type="application/json")
24
+
25
+ assert response.status_code == 400
26
+ assert response.text == "That's a bad request!"
@@ -0,0 +1,4 @@
1
+ name: FlaskTest
2
+ packages:
3
+ - path: flaskapp
4
+ - path: flask
@@ -0,0 +1,30 @@
1
+ """
2
+ Rudimentary Flask application for testing.
3
+ """
4
+ # pylint: disable=missing-function-docstring
5
+
6
+ import werkzeug
7
+ from appmap.flask import AppmapFlask
8
+ from flask import Flask, request
9
+
10
+ app = Flask(__name__)
11
+ AppmapFlask(app).init_app()
12
+
13
+
14
+ @app.route("/")
15
+ def hello_world():
16
+ return "Hello, World!"
17
+
18
+ @app.route("/exception")
19
+ def raise_exception():
20
+ raise Exception("An exception")
21
+
22
+ @app.post("/do_post")
23
+ def do_post():
24
+ _ = request.get_json()
25
+ return "Got post request"
26
+
27
+
28
+ @app.errorhandler(werkzeug.exceptions.BadRequest)
29
+ def handle_bad_request(e):
30
+ return "That's a bad request!", 400
@@ -0,0 +1,31 @@
1
+ import pytest
2
+ from flaskapp import app
3
+
4
+
5
+ @pytest.fixture(name="client")
6
+ def test_client():
7
+ with app.test_client() as c: # pylint: disable=no-member
8
+ yield c
9
+
10
+
11
+ def test_request(client):
12
+ response = client.get("/")
13
+
14
+ assert response.status_code == 200
15
+
16
+ def test_exception(client):
17
+ response = client.get("/exception")
18
+
19
+ assert response.status_code == 500
20
+
21
+ def test_not_found(client):
22
+ response = client.get("/not_found")
23
+
24
+ assert response.status_code == 404
25
+
26
+
27
+ def test_errorhandler(client):
28
+ response = client.post("/do_post", content_type="application/json")
29
+
30
+ assert response.status_code == 400
31
+ assert response.text == "That's a bad request!"