Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -1,3 +0,0 @@
[submodule "extern/wrapt"]
path = vendor/wrapt
url = https://github.com/applandinc/wrapt.git
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ os: linux
dist: jammy
language: python
python:
- "3.11"
- "3.10"
- "3.9.14"
- "3.8"
Expand Down
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- [Getting the code](#getting-the-code)
- [Python version support](#python-version-support)
- [Dependency management](#dependency-management)
- [wrapt](#wrapt)
- [Linting](#linting)
- [Testing](#testing)
- [pytest](#pytest)
Expand Down Expand Up @@ -71,6 +72,14 @@ oldest version currently supported (see the
% poetry install
```

### wrapt
The one dependency that is not managed using `poetry` is `wrapt`. Because it's possible that
projects that use `appmap` may also need an unmodified version of `wrapt` (e.g. `pylint` depends on
`astroid`, which in turn depends on `wrapt`), we use
[vendoring](https://github.com/pradyunsg/vendoring) to vendor `wrapt`.

To update `wrapt`, use `tox` (described below) to run the `vendoring` environment.

## Linting
[pylint](https://www.pylint.org/) for linting:

Expand All @@ -83,9 +92,9 @@ Your code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)

```

[Note that the current configuration requires a 10.0 for the Travis build to pass. To make
this easier to achieve, convention and refactoring checks have both been disabled. They
should be reenabled as soon as possible.]
[Note that the current configuration has a threshold set which must be met for the Travis build to
pass. To make this easier to achieve, a number of checks have both been disabled. They should be
reenabled as soon as possible.]


## Testing
Expand Down
30 changes: 21 additions & 9 deletions _appmap/importer.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from collections.abc import MutableSequence
from functools import reduce

from appmap import wrapt
from _appmap import wrapt

from .env import Env
from .utils import FnType
Expand Down Expand Up @@ -165,9 +165,7 @@ def do_import(cls, *args, **kwargs):

logger.debug("do_import, mod %s args %s kwargs %s", mod, args, kwargs)
if not cls.filter_chain:
cls.filter_chain = reduce(
lambda acc, e: e(acc), cls.filter_stack, NullFilter(None)
)
cls.filter_chain = reduce(lambda acc, e: e(acc), cls.filter_stack, NullFilter(None))

def instrument_functions(filterable, selected_functions=None):
logger.debug(" looking for members of %s", filterable.obj)
Expand Down Expand Up @@ -264,12 +262,26 @@ def wrapped_find_spec(find_spec, _, args, kwargs):


def wrap_finder_find_spec(finder):
find_spec = getattr(finder, "find_spec", None)
if find_spec is None:
logger.debug("no find_spec for finder %r", finder)
return
# Prior to 3.11, it worked fine to just grab find_spec from the finder and wrap it. The
# implementation of builtin finders must have changed with 3.11, because we now need the same
# kind of workaround we use above for exec_module.
if sys.version_info[1] < 11:
find_spec = getattr(finder, "find_spec", None)
if find_spec is None:
logger.debug("no find_spec for finder %r", finder)
return

finder.find_spec = wrap_finder_function(find_spec, wrapped_find_spec)
finder.find_spec = wrap_finder_function(find_spec, wrapped_find_spec)
else:
find_spec = inspect.getattr_static(finder, "find_spec", None)
if find_spec is None:
logger.debug("no find_spec for finder %r", finder)
return

if isinstance(find_spec, (classmethod, staticmethod)):
finder.find_spec = wrap_finder_function(find_spec, wrapped_find_spec)
else:
finder.find_spec = wrap_finder_function(finder.find_spec, wrapped_find_spec)


class MetapathObserver(MutableSequence):
Expand Down
6 changes: 3 additions & 3 deletions _appmap/test/data/unittest/expected/pytest.appmap.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"recording": {
"defined_class": "simple.test_simple.UnitTestTest",
"method_id": "test_hello_world",
"source_location": "simple/test_simple.py:13"
"source_location": "simple/test_simple.py:12"
},
"name": "Unit test test hello world",
"feature": "Hello world",
Expand All @@ -28,7 +28,7 @@
"defined_class": "simple.test_simple.UnitTestTest",
"method_id": "test_hello_world",
"path": "simple/test_simple.py",
"lineno": 14,
"lineno": 13,
"static": false,
"receiver": {
"name": "self",
Expand Down Expand Up @@ -178,7 +178,7 @@
{
"name": "test_hello_world",
"type": "function",
"location": "simple/test_simple.py:14",
"location": "simple/test_simple.py:13",
"static": false
}
]
Expand Down
6 changes: 3 additions & 3 deletions _appmap/test/data/unittest/expected/unittest.appmap.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"recording": {
"defined_class": "simple.test_simple.UnitTestTest",
"method_id": "test_hello_world",
"source_location": "simple/test_simple.py:14"
"source_location": "simple/test_simple.py:13"
},
"name": "Unit test test hello world",
"feature": "Hello world",
Expand All @@ -28,7 +28,7 @@
"defined_class": "simple.test_simple.UnitTestTest",
"method_id": "test_hello_world",
"path": "simple/test_simple.py",
"lineno": 14,
"lineno": 13,
"static": false,
"receiver": {
"name": "self",
Expand Down Expand Up @@ -178,7 +178,7 @@
{
"name": "test_hello_world",
"type": "function",
"location": "simple/test_simple.py:14",
"location": "simple/test_simple.py:13",
"static": false
}
]
Expand Down
1 change: 0 additions & 1 deletion _appmap/test/data/unittest/simple/test_simple.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
# finders correctly.
from decouple import config

import appmap.unittest


class UnitTestTest(unittest.TestCase):
Expand Down
2 changes: 1 addition & 1 deletion _appmap/test/test_labels.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import pytest

from appmap.wrapt import BoundFunctionWrapper, FunctionWrapper
from _appmap.wrapt import BoundFunctionWrapper, FunctionWrapper


@pytest.mark.appmap_enabled
Expand Down
2 changes: 1 addition & 1 deletion _appmap/test/test_params.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import pytest

from _appmap import wrapt
from _appmap.event import CallEvent
from _appmap.importer import FilterableCls, FilterableFn
from appmap import wrapt

empty_args = {"name": "args", "class": "builtins.tuple", "kind": "rest", "value": "()"}

Expand Down
2 changes: 1 addition & 1 deletion _appmap/test/test_recording.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import appmap
from _appmap.event import Event
from _appmap.recorder import Recorder, ThreadRecorder
from appmap.wrapt import FunctionWrapper
from _appmap.wrapt import FunctionWrapper

from .normalize import normalize_appmap, remove_line_numbers

Expand Down
6 changes: 6 additions & 0 deletions _appmap/test/test_test_frameworks.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@ def test_write_appmap(tmp_path):

@pytest.fixture(name="testdir")
def fixture_runner_testdir(request, data_dir, pytester, monkeypatch):
# We need to set environment variables to control how tests are run. This will only work
# properly if pytester runs pytest in a subprocess.
assert (
pytester._method == "subprocess" # pylint:disable=protected-access
), "must run pytest in a subprocess"

# The init subdirectory contains a sitecustomize.py file that
# imports the appmap module. This simulates the way a real
# installation works, performing the same function as the the
Expand Down
69 changes: 69 additions & 0 deletions _appmap/unittest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import sys
import unittest
from contextlib import contextmanager

from _appmap import testing_framework, wrapt
from _appmap.utils import get_function_location

_session = testing_framework.session("unittest", "tests")


def _get_test_location(cls, method_name):
fn = getattr(cls, method_name)
return get_function_location(fn)


if sys.version_info[1] < 8:
# Prior to 3.8, unittest called the test case's test method directly, which left us without an
# opportunity to hook it. So, instead, instrument unittest.case._Outcome.testPartExecutor, a
# method used to run test cases. `isTest` will be True when the part is the actual test method,
# False when it's setUp or teardown.
@wrapt.patch_function_wrapper("unittest.case", "_Outcome.testPartExecutor")
@contextmanager
def testPartExecutor(wrapped, _, args, kwargs):
def _args(test_case, *_, isTest=False, **__):
return (test_case, isTest)

test_case, is_test = _args(*args, **kwargs)
already_recording = getattr(test_case, "_appmap_pytest_recording", None)
# fmt: off
if (
(not is_test)
or isinstance(test_case, unittest.case._SubTest) # pylint: disable=protected-access
or already_recording
):
# fmt: on
with wrapped(*args, **kwargs):
yield
return

method_name = test_case.id().split(".")[-1]
location = _get_test_location(test_case.__class__, method_name)
with _session.record(
test_case.__class__, method_name, location=location
) as metadata:
if metadata:
with wrapped(
*args, **kwargs
), testing_framework.collect_result_metadata(metadata):
yield
else:
# session.record may return None
yield

else:
# As of 3.8, unittest.case.TestCase now calls the test's method indirectly, through
# TestCase._callTestMethod. Hook that to manage a recording session.
@wrapt.patch_function_wrapper("unittest.case", "TestCase._callTestMethod")
def callTestMethod(wrapped, test_case, args, kwargs):
already_recording = getattr(test_case, "_appmap_pytest_recording", None)
if already_recording:
wrapped(*args, **kwargs)
return

method_name = test_case.id().split(".")[-1]
location = _get_test_location(test_case.__class__, method_name)
with _session.record(test_case.__class__, method_name, location=location) as metadata:
if metadata:
with testing_framework.collect_result_metadata(metadata):
wrapped(*args, **kwargs)
1 change: 1 addition & 0 deletions _appmap/wrapt
2 changes: 1 addition & 1 deletion appmap/flask.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@
from werkzeug.exceptions import BadRequest, UnsupportedMediaType
from werkzeug.middleware.dispatcher import DispatcherMiddleware

from _appmap import wrapt
from _appmap.env import Env
from _appmap.event import HttpServerRequestEvent, HttpServerResponseEvent
from _appmap.flask import app as remote_recording_app
from _appmap.metadata import Metadata
from _appmap.utils import patch_class, values_dict
from _appmap.web_framework import JSON_ERRORS, AppmapMiddleware, MiddlewareInserter
from _appmap.web_framework import TemplateHandler as BaseTemplateHandler
from appmap import wrapt

try:
# pylint: disable=unused-import
Expand Down
3 changes: 1 addition & 2 deletions appmap/pytest.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import pytest

from _appmap import testing_framework
from _appmap import testing_framework, wrapt
from _appmap.env import Env
from appmap import wrapt

logger = Env.current.getLogger(__name__)

Expand Down
55 changes: 1 addition & 54 deletions appmap/unittest.py
Original file line number Diff line number Diff line change
@@ -1,60 +1,7 @@
import unittest
from contextlib import contextmanager

from _appmap import testing_framework
from _appmap.env import Env
from _appmap.utils import get_function_location
from appmap import wrapt

logger = Env.current.getLogger(__name__)


def setup_unittest():
session = testing_framework.session("unittest", "tests")

def get_test_location(cls, method_name):

fn = getattr(cls, method_name)
return get_function_location(fn)

# unittest.case._Outcome.testPartExecutor is used by all supported
# versions of unittest to run test cases. `isTest` will be True when
# the part is the actual test method, False when it's setUp or
# teardown.
@wrapt.patch_function_wrapper("unittest.case", "_Outcome.testPartExecutor")
@contextmanager
def testPartExecutor(wrapped, _, args, kwargs):
def _args(test_case, *_, isTest=False, **__):
return (test_case, isTest)

test_case, is_test = _args(*args, **kwargs)
already_recording = getattr(test_case, "_appmap_pytest_recording", None)
# fmt: off
if (
(not is_test)
or isinstance(test_case, unittest.case._SubTest) # pylint: disable=protected-access
or already_recording
):
# fmt: on
with wrapped(*args, **kwargs):
yield
return

method_name = test_case.id().split(".")[-1]
location = get_test_location(test_case.__class__, method_name)
with session.record(
test_case.__class__, method_name, location=location
) as metadata:
if metadata:
with wrapped(
*args, **kwargs
), testing_framework.collect_result_metadata(metadata):
yield
else:
# session.record may return None
yield


if not Env.current.is_appmap_repo and Env.current.enables("unittest"):
logger.debug("Test recording is enabled (unittest)")
setup_unittest()
import _appmap.unittest # pyright: ignore pylint: disable=unused-import
1 change: 0 additions & 1 deletion appmap/wrapt

This file was deleted.

2 changes: 1 addition & 1 deletion ci/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ docker run -i${t} --rm\
-v $PWD/dist:/dist -v $PWD/_appmap/test/data/unittest:/_appmap/test/data/unittest\
-v $PWD/ci:/ci\
-w /tmp\
python:3.9 bash -ce "${@:-/ci/smoketest.sh; /ci/test_pipenv.sh; /ci/test_poetry.sh}"
python:3.11 bash -ce "${@:-/ci/smoketest.sh; /ci/test_pipenv.sh; /ci/test_poetry.sh}"
2 changes: 1 addition & 1 deletion ci/smoketest.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env bash

set -e
set -ex
pip install -U pip pytest "flask<2" python-decouple
pip install /dist/appmap-*-py3-none-any.whl

Expand Down
Loading