Skip to content

Commit

Permalink
Merge pull request #219 from praw-dev/upgrade_tests
Browse files Browse the repository at this point in the history
Fully migrate from nose to pytest and general code clean up
  • Loading branch information
LilSpazJoekp committed Dec 8, 2022
2 parents 16fabf8 + b701a95 commit 8ec364a
Show file tree
Hide file tree
Showing 63 changed files with 5,870 additions and 7,460 deletions.
1 change: 0 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ repos:
- repo: https://github.com/LilSpazJoekp/docstrfmt
hooks:
- id: docstrfmt
require_serial: true
rev: v1.5.1

- repo: https://github.com/pycqa/flake8
Expand Down
2 changes: 1 addition & 1 deletion .readthedocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,5 @@ python:
extra_requirements:
- readthedocs
path: .
version: 3.8
version: '3.8'
version: 2
4 changes: 0 additions & 4 deletions MANIFEST.in
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
include CHANGES.rst LICENSE.txt README.rst praw_license.txt
include asyncpraw/praw.ini
include "asyncpraw/images/PRAW logo.png"
include docs/Makefile
recursive-include docs *.png *.py *.rst
recursive-include tests *.json *.py
recursive-include tests/files *
5 changes: 5 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,8 @@ extend_exclude = ['./docs/examples/']
[tool.isort]
profile = 'black'
skip_glob = '.venv*'

[tool.pytest.ini_options]
asyncio_mode = "auto"
filterwarnings = "ignore::DeprecationWarning"
testpaths = "tests"
5 changes: 0 additions & 5 deletions pytest.ini

This file was deleted.

2 changes: 0 additions & 2 deletions setup.cfg

This file was deleted.

19 changes: 8 additions & 11 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""asyncpraw setup.py"""
import os
import re
from codecs import open
from os import path
Expand All @@ -11,7 +10,7 @@
with open(path.join(HERE, "README.rst"), encoding="utf-8") as fp:
README = fp.read()
with open(path.join(HERE, PACKAGE_NAME, "const.py"), encoding="utf-8") as fp:
VERSION = re.search('__version__ = "([^"]+)"', fp.read()).group(1)
VERSION = re.search(r'__version__ = "([^"]+)"', fp.read()).group(1)

extras = {
"ci": ["coveralls"],
Expand All @@ -24,15 +23,13 @@
"sphinxcontrib-trio",
],
"test": [
"asynctest >=0.13.0 ; python_version < '3.8'",
"mock >=0.8",
"pytest ==7.2.*",
"pytest-asyncio",
"pytest-vcr",
"testfixtures >4.13.2, <7",
"vcrpy >=4.1.1"
if os.getenv("PYPI_UPLOAD", False)
else "vcrpy@git+https://github.com/kevin1024/vcrpy.git@b1bc5c3a02db0447c28ab9a4cee314aeb6cdf1a7",
"asynctest ==0.13.* ; python_version < '3.8'", # TODO: Remove me when support for 3.7 is dropped
"mock ==4.*",
"pytest ==7.*",
"pytest-asyncio ==0.18.*",
"pytest-vcr ==1.*",
"testfixtures ==6.*",
"vcrpy ==4.*",
],
}
extras["lint"] += extras["readthedocs"]
Expand Down
12 changes: 0 additions & 12 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,4 @@
"""Async PRAW Test Suite."""
import sys

if sys.version_info < (3, 8):
from asynctest import TestCase

class BaseTest(TestCase):
"""Base class for Async PRAW tests."""

else:

class BaseTest:
"""Base class for Async PRAW tests."""


class HelperMethodMixin:
Expand Down
202 changes: 35 additions & 167 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,190 +1,58 @@
"""Prepare py.test."""
"""Prepare pytest."""
import asyncio
import json
import os
from base64 import b64encode
from datetime import datetime
from functools import wraps

import pytest
from _pytest.tmpdir import _mk_tmp
from vcr import VCR
from vcr.cassette import Cassette
from vcr.persisters.filesystem import FilesystemPersister
from vcr.serialize import deserialize, serialize


# Prevent calls to sleep
async def _sleep(*args):
raise Exception("Call to sleep")
class Placeholders:
def __init__(self, _dict):
self.__dict__ = _dict


asyncio.sleep = _sleep
@pytest.fixture
def image_path():
"""Return path to image."""

def _get_path(name):
"""Return path to image."""
return os.path.join(os.path.dirname(__file__), "integration", "files", name)

return _get_path

def b64_string(input_string):
"""Return a base64 encoded string (not bytes) from input_string."""
return b64encode(input_string.encode("utf-8")).decode("utf-8")

def pytest_configure(config):
pytest.placeholders = Placeholders(placeholders)
config.addinivalue_line(
"markers", "cassette_name: Name of cassette to use for test."
)
config.addinivalue_line(
"markers", "recorder_kwargs: Arguments to pass to the recorder."
)

def env_default(key):
"""Return environment variable or placeholder string."""
return os.environ.get(f"prawtest_{key}", f"placeholder_{key}")

@pytest.fixture(autouse=True)
def patch_sleep(monkeypatch):
"""Auto patch sleep to speed up tests."""

def filter_access_token(response):
"""Add VCR callback to filter access token."""
request_uri = response["url"]
if "api/v1/access_token" not in request_uri or response["status"]["code"] != 200:
return response
body = response["body"]["string"].decode()
try:
token = json.loads(body)["access_token"]
response["body"]["string"] = response["body"]["string"].replace(
token.encode("utf-8"), b"<ACCESS_TOKEN>"
)
placeholders["access_token"] = token
except (KeyError, TypeError, ValueError):
async def _sleep(*_, **__):
"""Dud sleep function."""
pass
return response


def serialize_dict(data: dict):
"""This is to filter out buffered readers."""
new_dict = {}
for key, value in data.items():
if key == "file":
new_dict[key] = serialize_file(value.name)
elif isinstance(value, dict):
new_dict[key] = serialize_dict(value)
elif isinstance(value, list):
new_dict[key] = serialize_list(value)
else:
new_dict[key] = value
return new_dict


def serialize_file(file_name):
with open(file_name, "rb") as f:
return f.read().decode("utf-8", "replace")


def serialize_list(data: list):
new_list = []
for item in data:
if isinstance(item, dict):
new_list.append(serialize_dict(item))
elif isinstance(item, list):
new_list.append(serialize_list(item))
elif isinstance(item, tuple):
if item[0] == "file":
item = (item[0], serialize_file(item[1].name))
new_list.append(item)
else:
new_list.append(item)
return new_list

monkeypatch.setattr(asyncio, "sleep", value=_sleep)


os.environ["praw_check_for_updates"] = "False"

placeholders = {
x: env_default(x)
x: os.environ.get(f"prawtest_{x}", f"placeholder_{x}")
for x in (
"auth_code client_id client_secret password redirect_uri test_subreddit"
" user_agent username refresh_token"
"auth_code client_id client_secret password redirect_uri refresh_token"
" test_subreddit user_agent username"
).split()
}

placeholders["basic_auth"] = b64_string(
f"{placeholders['client_id']}:{placeholders['client_secret']}"
)


class CustomPersister(FilesystemPersister):
@classmethod
def load_cassette(cls, cassette_path, serializer):
try:
with open(cassette_path) as f:
cassette_content = f.read()
except OSError:
raise ValueError("Cassette not found.")
for replacement, value in [
(v, f"<{k.upper()}>") for k, v in placeholders.items()
]:
cassette_content = cassette_content.replace(value, replacement)
cassette = deserialize(cassette_content, serializer)
return cassette

@staticmethod
def save_cassette(cassette_path, cassette_dict, serializer):
data = serialize(cassette_dict, serializer)
for replacement, value in [
(f"<{k.upper()}>", v) for k, v in placeholders.items()
]:
data = data.replace(value, replacement)
dirname, filename = os.path.split(cassette_path)
if dirname and not os.path.exists(dirname):
os.makedirs(dirname)
with open(cassette_path, "w") as f:
f.write(data)


class CustomSerializer(object):
@staticmethod
def serialize(cassette_dict):
cassette_dict["recorded_at"] = datetime.now().isoformat()[:-7]
return (
f"{json.dumps(serialize_dict(cassette_dict), sort_keys=True, indent=2)}\n"
)

@staticmethod
def deserialize(cassette_string):
return json.loads(cassette_string)


vcr = VCR(
before_record_response=filter_access_token,
cassette_library_dir="tests/integration/cassettes",
match_on=["uri", "method"],
path_transformer=VCR.ensure_suffix(".json"),
serializer="custom_serializer",
)
vcr.register_serializer("custom_serializer", CustomSerializer)
vcr.register_persister(CustomPersister)


def after_init(func, *args):
func(*args)


def add_init_hook(original_init):
"""Wrap an __init__ method to also call some hooks."""

@wraps(original_init)
def wrapper(self, *args, **kwargs):
original_init(self, *args, **kwargs)
after_init(init_hook, self)

return wrapper


Cassette.__init__ = add_init_hook(Cassette.__init__)


def init_hook(cassette):
if not cassette.requests:
pytest.set_up_record() # dynamically defined in __init__.py


class Placeholders:
def __init__(self, _dict):
self.__dict__ = _dict


def pytest_configure():
pytest.placeholders = Placeholders(placeholders)


@pytest.fixture
def tmp_path(request, tmp_path_factory):
# Manually create tmp_path fixture since asynctest does not play nicely with
# fixtures as args
request.cls.tmp_path = _mk_tmp(request, tmp_path_factory)
placeholders["basic_auth"] = b64encode(
f"{placeholders['client_id']}:{placeholders['client_secret']}".encode("utf-8")
).decode("utf-8")

0 comments on commit 8ec364a

Please sign in to comment.