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.
- {appmap-2.0.10 → appmap-2.1.1}/PKG-INFO +1 -1
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/env.py +2 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/event.py +24 -12
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/importer.py +44 -13
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/recorder.py +2 -1
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/conftest.py +46 -5
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/example_class.py +47 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/flask/flaskapp.py +12 -1
- appmap-2.1.1/_appmap/test/data/flask/test_app.py +26 -0
- appmap-2.1.1/_appmap/test/data/flask-instrumented/appmap.yml +4 -0
- appmap-2.1.1/_appmap/test/data/flask-instrumented/flaskapp.py +30 -0
- appmap-2.1.1/_appmap/test/data/flask-instrumented/test_app.py +31 -0
- 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
- appmap-2.1.1/_appmap/test/data/pytest/expected/pytest-numpy2.appmap.json +242 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/expected/status_errored.metadata.json +1 -1
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/expected/status_failed.metadata.json +1 -1
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/expected/status_xfailed.metadata.json +1 -1
- appmap-2.1.1/_appmap/test/data/pytest/simple.py +19 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/test_simple.py +4 -2
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/helpers.py +22 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_command.py +2 -2
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_configuration.py +11 -11
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_django.py +0 -3
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_django_simplelazyobject.py +2 -1
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_events.py +4 -4
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_fastapi.py +5 -1
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_flask.py +107 -9
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_params.py +2 -2
- appmap-2.1.1/_appmap/test/test_properties.py +105 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_recording.py +4 -5
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_test_frameworks.py +13 -42
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/web_framework.py +11 -15
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/utils.py +4 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/web_framework.py +24 -13
- {appmap-2.0.10 → appmap-2.1.1}/appmap/django.py +11 -3
- {appmap-2.0.10 → appmap-2.1.1}/appmap/fastapi.py +11 -3
- {appmap-2.0.10 → appmap-2.1.1}/appmap/flask.py +51 -22
- appmap-2.1.1/appmap.pth +1 -0
- {appmap-2.0.10 → appmap-2.1.1}/pyproject.toml +3 -1
- appmap-2.0.10/_appmap/test/data/flask/test_app.py +0 -14
- appmap-2.0.10/_appmap/test/data/pytest/simple.py +0 -18
- {appmap-2.0.10 → appmap-2.1.1}/LICENSE +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/README.md +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/configuration.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/django.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/fastapi.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/flask.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/generation.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/instrument.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/labels.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/metadata.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/noappmap.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/py_version_check.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/recording.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/remote_recording.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/singleton.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/appmap_test_base.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/bin/server_runner +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-all-paths-malformed.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-broken.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-class.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-empty-path.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-exclude-fn.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-func.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-malformed-path.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap-no-pyyaml.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap_testing/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/appmap_testing/django_simplelazyobject.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config/src/package/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config/test/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config/test/foo.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/.hide/hidden_mod/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/node_modules/node_mod/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/src/package/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/test/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-exclude/venv/venv_mod/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-up/appmap.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-up/project/p1/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/config-up/project/p2/sub1/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/appmap.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/djangoapp.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/hello_world.html +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/middleware.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/settings.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/settings_dev.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/djangoapp/urls.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/init/sitecustomize.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/manage.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/pytest.ini +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/test/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/test/test_app.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/django/test/test_unittest_setup.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/expected.appmap.json +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/appmap.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/fastapiapp/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/fastapiapp/main.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/init/sitecustomize.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/fastapi/test_app.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/flask/appmap.yml +0 -0
- {appmap-2.0.10/_appmap/test/data/trial → appmap-2.1.1/_appmap/test/data/flask}/init/sitecustomize.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/flask/templates/test.html +0 -0
- {appmap-2.0.10/_appmap/test/data/flask → appmap-2.1.1/_appmap/test/data/flask-instrumented}/init/sitecustomize.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/package1/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/package1/package2/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/package1/package2/__main__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/package1/package2/mod1.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/params.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/appmap.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/pytest/test_noappmap.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/remote.appmap.json +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/.gitignore +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/appmap.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/expected/pytest.appmap.json +0 -0
- {appmap-2.0.10/_appmap/test/data/unittest → appmap-2.1.1/_appmap/test/data/trial}/init/sitecustomize.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/test/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/trial/test/test_deferred.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/appmap.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/pytest.appmap.json +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/status_errored.metadata.json +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/status_failed.metadata.json +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/status_xfailed.metadata.json +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/status_xsucceeded.metadata.json +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/expected/unittest.appmap.json +0 -0
- /appmap-2.0.10/appmap.pth → /appmap-2.1.1/_appmap/test/data/unittest/init/sitecustomize.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/simple/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/simple/test_noappmap.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/data/unittest/simple/test_simple.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/normalize.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_describe_value.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_env.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_generation.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_http.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_importer.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_labels.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_metadata.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_runner.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_sqlalchemy.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/test/test_util.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/testing_framework.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/trace_logger.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/_appmap/unittest.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/command/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/command/appmap_agent_init.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/command/appmap_agent_status.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/command/appmap_agent_validate.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/command/runner.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/http.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/crypto.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/django.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/flask.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/formats.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/http.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/jobs.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/jwt.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/logger.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/labeling/random.yml +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/pytest.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/sqlalchemy.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/unittest.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/appmap/uvicorn.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/LICENSE +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/__init__.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/arguments.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/decorators.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/importer.py +0 -0
- {appmap-2.0.10 → appmap-2.1.1}/vendor/_appmap/wrapt/wrappers.py +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
477
|
-
{
|
|
478
|
-
|
|
479
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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=
|
|
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,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!"
|