From 0dfc38c355e20255e25045f3f01a130eb30d51f7 Mon Sep 17 00:00:00 2001 From: Mike Cousins Date: Thu, 22 Dec 2022 12:02:32 -0600 Subject: [PATCH] fix(spy): use classmethod `__func__` source for async detection (#148) Closes #146 --- .github/workflows/ci.yml | 56 ++++++++++++++++++++-------------------- decoy/spy_core.py | 3 +++ tests/fixtures.py | 8 ++++++ tests/test_spy_core.py | 12 +++++++++ 4 files changed, 51 insertions(+), 28 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edcc16f..4628e46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,30 +1,30 @@ -name: "Continuous integration" +name: 'Continuous integration' on: [push, pull_request] env: PIP_CACHE_DIR: ${{ github.workspace }}/.cache/pip POETRY_CACHE_DIR: ${{ github.workspace }}/.cache/pypoetry - DEFAULT_PYTHON: "3.8" + DEFAULT_PYTHON: '3.8' jobs: test: - name: "Test Python ${{ matrix.python-version }} on ${{ matrix.os }}" + name: 'Test Python ${{ matrix.python-version }} on ${{ matrix.os }}' runs-on: ${{ matrix.os }}-latest strategy: matrix: os: [Ubuntu, Windows, macOS] - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ['3.7', '3.8', '3.9', '3.10'] steps: - - name: "Check out repository" + - name: 'Check out repository' uses: actions/checkout@v3 - - name: "Set up Python" + - name: 'Set up Python' uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - - name: "Set up dependency cache" + - name: 'Set up dependency cache' uses: actions/cache@v3 # poetry venv restore is buggy on windows # https://github.com/python-poetry/poetry/issues/2629 @@ -35,34 +35,34 @@ jobs: ${{ env.PIP_CACHE_DIR }} ${{ env.POETRY_CACHE_DIR }} - - name: "Install poetry" + - name: 'Install poetry' run: pip install "poetry==1.1.11" - - name: "Install dependencies" + - name: 'Install dependencies' run: poetry install - - name: "Run tests" + - name: 'Run tests' run: poetry run coverage run --branch --source=decoy -m pytest --mypy-same-process - - name: "Generate coverage report" + - name: 'Generate coverage report' run: poetry run coverage xml - - name: "Upload coverage report" + - name: 'Upload coverage report' uses: codecov/codecov-action@v3 check: - name: "Lint and type checks" + name: 'Lint and type checks' runs-on: ubuntu-latest steps: - - name: "Check out repository" + - name: 'Check out repository' uses: actions/checkout@v3 - - name: "Set up Python" + - name: 'Set up Python' uses: actions/setup-python@v4 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: "Set up dependency cache" + - name: 'Set up dependency cache' uses: actions/cache@v3 with: key: deps-${{ secrets.GH_CACHE }}-${{ runner.os }}-python${{ env.DEFAULT_PYTHON }}-${{ hashFiles('**/*.lock') }} @@ -70,19 +70,19 @@ jobs: ${{ env.PIP_CACHE_DIR }} ${{ env.POETRY_CACHE_DIR }} - - name: "Install poetry" + - name: 'Install poetry' run: pip install "poetry==1.1.11" - - name: "Install dependencies" + - name: 'Install dependencies' run: poetry install - - name: "Check formatting" + - name: 'Check formatting' run: poetry run black --check . - - name: "Check linter" + - name: 'Check linter' run: poetry run flake8 - - name: "Checks types" + - name: 'Checks types' run: poetry run mypy build: @@ -90,15 +90,15 @@ jobs: runs-on: ubuntu-latest needs: [test, check] steps: - - name: "Check out repository" + - name: 'Check out repository' uses: actions/checkout@v3 - - name: "Set up Python" + - name: 'Set up Python' uses: actions/setup-python@v4 with: python-version: ${{ env.DEFAULT_PYTHON }} - - name: "Set up dependency cache" + - name: 'Set up dependency cache' uses: actions/cache@v3 with: key: deps-${{ secrets.GH_CACHE }}-${{ runner.os }}-python${{ env.DEFAULT_PYTHON }}-${{ hashFiles('**/*.lock') }} @@ -106,18 +106,18 @@ jobs: ${{ env.PIP_CACHE_DIR }} ${{ env.POETRY_CACHE_DIR }} - - name: "Install poetry" + - name: 'Install poetry' run: pip install "poetry==1.1.11" - - name: "Install dependencies" + - name: 'Install dependencies' run: poetry install - - name: "Build artifacts" + - name: 'Build artifacts' run: | poetry build poetry run mkdocs build - - name: "Deploy to PyPI and GitHub Pages" + - name: 'Deploy to PyPI and GitHub Pages' if: startsWith(github.ref, 'refs/tags/v') env: USER_NAME: ${{ github.actor }} diff --git a/decoy/spy_core.py b/decoy/spy_core.py index 3465b3a..c683b9a 100644 --- a/decoy/spy_core.py +++ b/decoy/spy_core.py @@ -126,6 +126,9 @@ def create_child_core(self, name: str, is_async: bool) -> "SpyCore": child_source = child_source.__func__ else: + if isinstance(child_source, classmethod): + child_source = child_source.__func__ + child_source = inspect.unwrap(child_source) if inspect.isfunction(child_source): diff --git a/tests/fixtures.py b/tests/fixtures.py index 951d469..3a4f87c 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -53,6 +53,14 @@ async def bar(self, a: int, b: float, c: str) -> bool: async def do_the_thing(self, *, flag: bool) -> None: """Perform a side-effect without a return value.""" + @classmethod + async def async_class_method(cls) -> int: + """An async class method.""" + + @staticmethod + async def async_static_method() -> int: + """An async static method.""" + class SomeAsyncCallableClass: """Async callable class.""" diff --git a/tests/test_spy_core.py b/tests/test_spy_core.py index 8a1df48..14ed30b 100644 --- a/tests/test_spy_core.py +++ b/tests/test_spy_core.py @@ -365,6 +365,18 @@ class GetIsAsyncSpec(NamedTuple): ), expected_is_async=True, ), + GetIsAsyncSpec( + subject=SpyCore(source=SomeAsyncClass, name=None).create_child_core( + "async_class_method", is_async=False + ), + expected_is_async=True, + ), + GetIsAsyncSpec( + subject=SpyCore(source=SomeAsyncClass, name=None).create_child_core( + "async_static_method", is_async=False + ), + expected_is_async=True, + ), ], ) def test_get_is_async(subject: SpyCore, expected_is_async: bool) -> None: