[Git][debian-gis-team/pytest-recording][upstream] New upstream version 0.13.4
Antonio Valentino (@antonio.valentino)
gitlab at salsa.debian.org
Sun Aug 10 17:57:34 BST 2025
Antonio Valentino pushed to branch upstream at Debian GIS Project / pytest-recording
Commits:
2b52d032 by Antonio Valentino at 2025-08-09T10:07:37+00:00
New upstream version 0.13.4
- - - - -
22 changed files:
- .coveragerc
- .github/workflows/build.yml
- .github/workflows/release.yml
- .pre-commit-config.yaml
- .relint.yml
- CONTRIBUTING.rst
- README.rst
- docs/changelog.rst
- mypy.ini
- pyproject.toml
- src/pytest_recording/_vcr.py
- src/pytest_recording/hooks.py
- src/pytest_recording/plugin.py
- + src/pytest_recording/py.typed
- src/pytest_recording/utils.py
- + tests/__init__.py
- tests/test_blocking_network.py
- tests/test_plugin.py
- tests/test_recording.py
- tests/test_replaying.py
- tests/test_utils.py
- tox.ini
Changes:
=====================================
.coveragerc
=====================================
@@ -11,5 +11,5 @@ source =
[report]
show_missing = true
precision = 2
-exclude_lines =
- pragma: no cover
+exclude_also =
+ if TYPE_CHECKING:
=====================================
.github/workflows/build.yml
=====================================
@@ -16,7 +16,7 @@ on:
jobs:
pre-commit:
name: Generic pre-commit checks
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout at v4
with:
@@ -24,14 +24,14 @@ jobs:
- uses: actions/setup-python at v5
with:
- python-version: 3.11
+ python-version: 3.13
- run: pip install pre-commit
- run: SKIP=mypy pre-commit run --all-files
mypy:
name: Mypy
- runs-on: ubuntu-latest
+ runs-on: ubuntu-24.04
steps:
- uses: actions/checkout at v4
with:
@@ -39,41 +39,66 @@ jobs:
- uses: actions/setup-python at v5
with:
- python-version: 3.11
+ python-version: 3.13
- run: pip install pre-commit
- run: pre-commit run mypy --all-files
tests:
- name: tests_${{ matrix.tox_job }}
+ name: Test ${{ matrix.tox_job }} on ${{ matrix.os_version }}
runs-on: ${{ matrix.os_version }}
strategy:
matrix:
include:
- - tox_job: py37
- python: "3.7"
- os_version: "ubuntu-latest"
- - tox_job: py38
- python: "3.8"
- os_version: "ubuntu-latest"
- tox_job: py39
python: "3.9"
- os_version: "ubuntu-latest"
- - tox_job: no_pycurl
- python: "3.8"
- os_version: "ubuntu-latest"
+ os_version: "ubuntu-24.04"
+ - tox_job: py39-no_pycurl
+ python: "3.9"
+ os_version: "ubuntu-24.04"
+ - tox_job: pypy3-no_pycurl
+ python: "pypy-3.10"
+ os_version: "ubuntu-24.04"
+ - tox_job: vcr_431
+ python: "3.9"
+ os_version: "ubuntu-24.04"
+ - tox_job: py310
+ python: "3.10"
+ os_version: "ubuntu-24.04"
+ - tox_job: py311
+ python: "3.11"
+ os_version: "ubuntu-24.04"
+ - tox_job: py312
+ python: "3.12"
+ os_version: "ubuntu-24.04"
+ - tox_job: py313
+ python: "3.13"
+ os_version: "ubuntu-24.04"
+
+ - tox_job: py39
+ python: "3.9"
+ os_version: "windows-2022"
+ - tox_job: py39-no_pycurl
+ python: "3.9"
+ os_version: "windows-2022"
+ - tox_job: pypy3-no_pycurl
+ python: "pypy-3.10"
+ os_version: "windows-2022"
- tox_job: vcr_431
- python: "3.8"
- os_version: "ubuntu-latest"
+ python: "3.9"
+ os_version: "windows-2022"
- tox_job: py310
python: "3.10"
- os_version: "ubuntu-latest"
+ os_version: "windows-2022"
- tox_job: py311
python: "3.11"
- os_version: "ubuntu-latest"
+ os_version: "windows-2022"
- tox_job: py312
python: "3.12"
- os_version: "ubuntu-latest"
+ os_version: "windows-2022"
+ - tox_job: py313
+ python: "3.13"
+ os_version: "windows-2022"
steps:
- uses: actions/checkout at v4
@@ -86,7 +111,9 @@ jobs:
- run: pip install tox coverage
- - run: sudo apt update && sudo apt install libcurl4-openssl-dev libssl-dev
+ - name: Install system deps on Linux
+ if: runner.os == 'Linux'
+ run: sudo apt update && sudo apt install -y libcurl4-openssl-dev libssl-dev
- name: Run ${{ matrix.tox_job }} tox job
run: tox -e ${{ matrix.tox_job }}
@@ -96,6 +123,6 @@ jobs:
- run: coverage xml -i
- name: Upload coverage to Codecov
- uses: codecov/codecov-action at v4.5.0
+ uses: codecov/codecov-action at v5.4.2
with:
file: ./coverage.xml
=====================================
.github/workflows/release.yml
=====================================
@@ -10,10 +10,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout at master
- - name: Set up Python 3.11
+ - name: Set up Python 3.13
uses: actions/setup-python at v5
with:
- python-version: 3.11
+ python-version: 3.13
- run: pip install hatch
=====================================
.pre-commit-config.yaml
=====================================
@@ -1,9 +1,9 @@
default_language_version:
- python: python3.11
+ python: python3.13
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
- rev: v4.5.0
+ rev: v5.0.0
hooks:
- id: check-yaml
- id: end-of-file-fixer
@@ -20,34 +20,33 @@ repos:
- id: gitlint
- repo: https://github.com/adrienverge/yamllint
- rev: v1.33.0
+ rev: v1.35.1
hooks:
- id: yamllint
- repo: https://github.com/igorshubovych/markdownlint-cli
- rev: v0.37.0
+ rev: v0.44.0
hooks:
- id: markdownlint
language_version: system
- repo: https://github.com/codingjoe/relint
- rev: 3.1.0
+ rev: 3.3.1
hooks:
- id: relint
- repo: https://github.com/pre-commit/mirrors-mypy
- rev: v1.7.1
+ rev: v1.15.0
hooks:
- id: mypy
- exclude: ^(docs/|tests/|setup.py).*$
- additional_dependencies: [ "types-pycurl" ]
+ additional_dependencies: [ "types-pycurl", "types-PyYAML", "types-requests" ]
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.7
+ rev: v0.9.5
hooks:
- id: ruff-format
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.1.7
+ rev: v0.9.5
hooks:
- id: ruff
=====================================
.relint.yml
=====================================
@@ -1,24 +1,11 @@
-- name: Fix it now
- pattern: "[fF][iI][xX][mM][eE]"
- filename:
- - "*.py"
-
- name: No sys.path changes
pattern: "sys\\.path\\.append|sys\\.path\\.insert"
- filename:
- - "src/**.py"
+ filePattern: src/.*\.py
- name: IPython debug leftover
pattern: "IPython\\.embed()"
- filename:
- - "*.py"
-
-- name: Leftover print
- pattern: "print\\("
- filename:
- - ^(?!.*conftest).*\.py$
+ filePattern: .*\.py
- name: Use relative imports
pattern: "import pytest_recording|from pytest_recording"
- filename:
- - "src/**.py"
+ filePattern: src/.*\.py
=====================================
CONTRIBUTING.rst
=====================================
@@ -50,7 +50,7 @@ Submitting Pull Requests
#. Follow **PEP-8** for naming and `ruff <https://github.com/astral-sh/ruff>`_ for formatting.
#. Tests are run using ``tox``::
- tox -e py37
+ tox -e py39
The test environments above are usually enough to cover most cases locally.
=====================================
README.rst
=====================================
@@ -3,7 +3,7 @@ pytest-recording
|codecov| |Build| |Version| |Python versions| |License|
-A pytest plugin that records network interactions in your tests via VCR.py.
+A pytest plugin powered by VCR.py to record and replay HTTP traffic.
Features
--------
@@ -22,8 +22,9 @@ This project can be installed via pip:
pip install pytest-recording
-⚠️This project is not compatible with `pytest-vcr`, make sure to uninstall before ⚠️
+⚠️ Incompatibility Warning
+ If you have ``pytest-vcr`` installed, please uninstall it before using ``pytest-recording``, as the two plugins are incompatible.
Usage
-----
@@ -242,13 +243,37 @@ To run the tests:
For more information, take a look at `our contributing guide <https://github.com/kiwicom/pytest-recording/blob/master/CONTRIBUTING.rst>`_
+Test Isolation for Package Maintainers
+--------------------------------------
+
+When running pytest-based tests in a packaging environment, globally installed plugins can break test suites by
+injecting unexpected hooks or fixtures (e.g. ``pytest-pretty``) that your code isn’t designed for.
+
+To guarantee a clean, reproducible test run:
+
+.. code-block:: console
+
+ export PYTEST_DISABLE_PLUGIN_AUTOLOAD=1
+ export PYTEST_PLUGINS=pytest_httpbin.plugin,pytest_mock,pytest_recording.plugin
+
+- **PYTEST_DISABLE_PLUGIN_AUTOLOAD**
+ Disables loading of any plugins via setuptools entry-points; only those you explicitly list
+ will be activated.
+
+- **PYTEST_PLUGINS**
+ Comma-separated list of plugin modules pytest should load (the core plugin manager still
+ discovers builtin plugins and `conftest.py`).
+
+Include these exports in your package’s build or CI script so that system-wide pytest plugins
+(e.g. linting, formatting, or unrelated test helpers) cannot interfere with your tests.
+
Python support
--------------
Pytest-recording supports:
-- CPython 3.7, 3.8, 3.9, 3.10, 3.11, and 3.12
-- PyPy 7 (3.6)
+- CPython 3.9, 3.10, 3.11, 3.12, and 3.13
+- PyPy 7.3 (3.10)
License
-------
=====================================
docs/changelog.rst
=====================================
@@ -6,6 +6,16 @@ Changelog
`Unreleased`_
-------------
+`0.13.4`_ - 2025-04-24
+----------------------
+
+- ``AttributeError`` on Windows. `#172`_
+
+`0.13.3`_ - 2025-04-24
+----------------------
+
+- Limit generated cassette names to prevent ``OSError``. `#172`_
+
`0.13.2`_ - 2024-07-09
----------------------
@@ -204,7 +214,9 @@ Added
- Initial public release
-.. _Unreleased: https://github.com/kiwicom/pytest-recording/compare/v0.13.2...HEAD
+.. _Unreleased: https://github.com/kiwicom/pytest-recording/compare/v0.13.4...HEAD
+.. _0.13.4: https://github.com/kiwicom/pytest-recording/compare/v0.13.3...v0.13.4
+.. _0.13.3: https://github.com/kiwicom/pytest-recording/compare/v0.13.2...v0.13.3
.. _0.13.2: https://github.com/kiwicom/pytest-recording/compare/v0.13.1...v0.13.2
.. _0.13.1: https://github.com/kiwicom/pytest-recording/compare/v0.13.0...v0.13.1
.. _0.13.0: https://github.com/kiwicom/pytest-recording/compare/v0.12.2...v0.13.0
@@ -229,6 +241,8 @@ Added
.. _0.3.0: https://github.com/kiwicom/pytest-recording/compare/v0.2.0...v0.3.0
.. _0.2.0: https://github.com/kiwicom/pytest-recording/compare/v0.1.0...v0.2.0
+.. _#174: https://github.com/kiwicom/pytest-recording/issues/174
+.. _#172: https://github.com/kiwicom/pytest-recording/issues/172
.. _#145: https://github.com/kiwicom/pytest-recording/issues/145
.. _#118: https://github.com/kiwicom/pytest-recording/pull/118
.. _#99: https://github.com/kiwicom/pytest-recording/pull/99
=====================================
mypy.ini
=====================================
@@ -1,5 +1,5 @@
[mypy]
-python_version = 3.11
+python_version = 3.13
show_error_context = true
verbosity = 0
ignore_missing_imports = false
@@ -14,3 +14,10 @@ disallow_untyped_calls = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
disallow_untyped_decorators = true
+
+[mypy-tests.*]
+disallow_untyped_calls = false
+disallow_untyped_defs = false
+
+[mypy-vcr.*]
+ignore_missing_imports = True
=====================================
pyproject.toml
=====================================
@@ -4,12 +4,12 @@ build-backend = "hatchling.build"
[project]
name = "pytest-recording"
-version = "0.13.2"
-description = "A pytest plugin that allows you recording of network interactions via VCR.py"
+version = "0.13.4"
+description = "A pytest plugin powered by VCR.py to record and replay HTTP traffic"
keywords = ["pytest", "vcr", "network", "mock"]
authors = [{ name = "Dmitry Dygalo", email = "dmitry at dygalo.dev" }]
maintainers = [{ name = "Dmitry Dygalo", email = "dmitry at dygalo.dev" }]
-requires-python = ">=3.7"
+requires-python = ">=3.9"
license = "MIT"
readme = "README.rst"
classifiers = [
@@ -18,17 +18,15 @@ classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Topic :: Software Development :: Testing",
+ "Typing :: Typed",
"Programming Language :: Python",
"Programming Language :: Python :: 3 :: Only",
"Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.5",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
- "Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
+ "Programming Language :: Python :: 3.13",
"Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
"Operating System :: OS Independent",
@@ -42,7 +40,7 @@ tests = [
"pytest-httpbin",
"pytest-mock",
"requests",
- "Werkzeug==3.0.3"
+ "Werkzeug==3.1.3"
]
dev = ["pytest_recording[tests]"]
@@ -57,6 +55,9 @@ recording = "pytest_recording.plugin"
[tool.ruff]
line-length = 120
+target-version = "py39"
+
+[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
@@ -64,6 +65,9 @@ select = [
"C", # flake8-comprehensions
"B", # flake8-bugbear
"D", # pydocstyle
+ "I", # isort
+ "T", # prints
+ "FIX", # fixme comments
]
ignore = [
"E501", # Line too long, handled by ruff
@@ -80,11 +84,10 @@ ignore = [
"D213", # Multiline summary second line
"D401", # Imperative mood
]
-target-version = "py37"
[tool.ruff.format]
skip-magic-trailing-comma = false
-[tool.ruff.isort]
+[tool.ruff.lint.isort]
known-first-party = ["pytest_recording"]
known-third-party = ["_pytest", "packaging", "pytest", "vcr", "yaml"]
=====================================
src/pytest_recording/_vcr.py
=====================================
@@ -1,3 +1,4 @@
+import hashlib
import os
from dataclasses import dataclass
from itertools import chain, starmap
@@ -14,12 +15,19 @@ from vcr.serialize import deserialize
try:
# VCR.py >=5
from vcr.cassette import CassetteNotFoundError
-except ImportError:
+except ImportError: # pragma: no cover
# VCR.py <5
CassetteNotFoundError = ValueError
from .utils import ConfigType, merge_kwargs, unique, unpack
+try:
+ # Try to get max filename length on Unix-like systems
+ MAX_FILENAME_LEN = os.pathconf(".", "PC_NAME_MAX")
+except (AttributeError, ValueError, OSError):
+ # Fallback for Windows or unsupported systems
+ MAX_FILENAME_LEN = 255
+
def load_cassette(cassette_path: str, serializer: ModuleType) -> Tuple[List, List]:
try:
@@ -63,6 +71,14 @@ def use_cassette(
) -> CassetteContextDecorator:
"""Create a VCR instance and return an appropriate context manager for the given cassette configuration."""
merged_config = merge_kwargs(config, markers)
+
+ # Check `default_cassette` to prevent it from being too long.
+ suffix = merged_config.get("serializer", ".yaml")
+ if len(default_cassette) + len(suffix) > MAX_FILENAME_LEN:
+ hash_part = hashlib.md5(default_cassette.encode()).hexdigest()
+ prefix = default_cassette[: MAX_FILENAME_LEN - len(suffix) - len(hash_part) - 3]
+ default_cassette = f"{prefix}...{hash_part}"
+
if "record_mode" in merged_config:
record_mode = merged_config["record_mode"]
path_transformer = get_path_transformer(merged_config)
=====================================
src/pytest_recording/hooks.py
=====================================
@@ -1,6 +1,7 @@
-from _pytest.config import Config
from typing import TYPE_CHECKING
+from _pytest.config import Config
+
if TYPE_CHECKING:
from vcr import VCR
=====================================
src/pytest_recording/plugin.py
=====================================
@@ -1,5 +1,5 @@
import os
-from typing import Any, Dict, Iterator, List, Optional, TYPE_CHECKING
+from typing import TYPE_CHECKING, Any, Dict, Iterator, List, Optional
import pytest
from _pytest.config import Config, PytestPluginManager
@@ -185,7 +185,7 @@ def default_cassette_name(request: SubRequest) -> str:
marker = request.node.get_closest_marker("default_cassette")
if marker is not None:
assert marker.args, (
- "You should pass the cassette name as an argument " "to the `pytest.mark.default_cassette` marker"
+ "You should pass the cassette name as an argument to the `pytest.mark.default_cassette` marker"
)
return marker.args[0]
return get_default_cassette_name(request.cls, request.node.name)
=====================================
src/pytest_recording/py.typed
=====================================
=====================================
src/pytest_recording/utils.py
=====================================
@@ -1,6 +1,7 @@
from copy import deepcopy
from itertools import chain
from typing import Any, Dict, Iterable, Iterator, List
+
from _pytest.mark.structures import Mark
ConfigType = Dict[str, Any]
=====================================
tests/__init__.py
=====================================
@@ -0,0 +1 @@
+# this file is only here so mypy can see it as a module and allow custom rules
=====================================
tests/test_blocking_network.py
=====================================
@@ -1,6 +1,23 @@
+import json
+import sys
+from io import BytesIO
+from socket import AF_INET, SOCK_RAW, SOCK_STREAM, socket
+
import pytest
+import requests
+import vcr.errors
from packaging import version
+from pytest_recording.network import blocking_context
+
+# Windows doesn’t have AF_NETLINK & AF_UNIX
+try:
+ from socket import AF_NETLINK, AF_UNIX
+except ImportError:
+ AF_NETLINK = None # type: ignore[assignment]
+ AF_UNIX = None # type: ignore[assignment]
+
+
try:
import pycurl
except ImportError as exc:
@@ -8,7 +25,11 @@ except ImportError as exc:
# Case with different SSL backends should be loud and visible
# Could happen with development when environment is recreated (e.g. locally)
raise
- pycurl = None
+ pycurl = None # type: ignore[assignment]
+
+
+skip_netlink = pytest.mark.skipif(AF_NETLINK is None, reason="AF_NETLINK not available on this platform")
+skip_unix = pytest.mark.skipif(AF_UNIX is None, reason="AF_UNIX not available on this platform")
def assert_network_blocking(testdir, dirname):
@@ -130,77 +151,49 @@ def test_no_blocking(httpbin):
result.assert_outcomes(passed=1)
-def test_unix_socket(testdir):
- testdir.makepyfile(
- """
-from socket import socket, AF_UNIX, SOCK_STREAM
-import pytest
-
-def call(socket_name):
- s = socket(AF_UNIX, SOCK_STREAM)
+def call(socket_name, family, type):
+ s = socket(family, type)
try:
return s.connect(socket_name)
finally:
s.close()
+
+ at skip_unix
@pytest.mark.block_network(allowed_hosts=["./allowed_socket"])
-def test_allowed():
+def test_block_network_allowed_socket():
# Error from actual socket call, that means it was not blocked
with pytest.raises(IOError):
- call("./allowed_socket")
+ call("./allowed_socket", AF_UNIX, SOCK_STREAM)
+
+ at skip_unix
@pytest.mark.block_network(allowed_hosts=["./allowed_socket"])
-def test_blocked():
+def test_block_network_blocked_socket():
with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
- call("./blocked_socket")
- """
- )
-
- result = testdir.runpytest()
- result.assert_outcomes(passed=2)
+ call("./blocked_socket", AF_UNIX, SOCK_STREAM)
-def test_other_socket(testdir):
- # When not AF_UNIX, AF_INET or AF_INET6 socket is used
- testdir.makepyfile(
- """
-from socket import socket, AF_NETLINK, SOCK_RAW
-import pytest
-
-def call():
- s = socket(AF_NETLINK, SOCK_RAW)
- try:
- return s.connect((0, 0))
- finally:
- s.close()
-
+# When not AF_UNIX, AF_INET or AF_INET6 socket is used
+# Then socket.socket.connect call is blocked, even if resource name is in the allowed list
+ at skip_netlink
@pytest.mark.block_network(allowed_hosts=["./allowed_socket", "127.0.0.1", "0"])
def test_blocked():
with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
- call()
- """
- )
- # Then socket.socket.connect call is blocked, even if resource name is in the allowed list
- result = testdir.runpytest()
- result.assert_outcomes(passed=1)
+ call((0, 0), AF_NETLINK, SOCK_RAW)
-def test_block_network(testdir):
- # When record is disabled
- testdir.makepyfile(
- """
-import socket
-import pytest
-import requests
-import vcr.errors
+# When record is disabled
+
@pytest.mark.block_network
@pytest.mark.vcr
def test_with_vcr_mark(httpbin):
with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException, match=r"overwrite existing cassette"):
requests.get(httpbin.url + "/ip")
- assert socket.socket.connect.__name__ == "network_guard"
- assert socket.socket.connect_ex.__name__ == "network_guard"
+ assert socket.connect.__name__ == "network_guard"
+ assert socket.connect_ex.__name__ == "network_guard"
+
@pytest.mark.block_network
def test_no_vcr_mark(httpbin):
@@ -211,19 +204,15 @@ def test_no_vcr_mark(httpbin):
@pytest.mark.block_network(allowed_hosts=["127.0.0.2"])
def test_no_vcr_mark_bytes():
with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+ with socket(AF_INET, SOCK_STREAM) as sock:
sock.connect((b"127.0.0.1", 80))
+
@pytest.mark.block_network(allowed_hosts=["127.0.0.2"])
def test_no_vcr_mark_bytearray():
with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
- with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
+ with socket(AF_INET, SOCK_STREAM) as sock:
sock.connect((bytearray(b"127.0.0.1"), 80))
- """
- )
-
- result = testdir.runpytest("-s")
- result.assert_outcomes(passed=4)
@pytest.mark.parametrize(
@@ -337,81 +326,57 @@ def test_no_vcr_mark(httpbin):
assert cassette_path.exists()
+# When pycurl is used for network access
+# It should be blocked as well
@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
-def test_pycurl(testdir):
- # When pycurl is used for network access
- testdir.makepyfile(
- r"""
-import json
-import sys
-import pytest
-import pycurl
-from io import BytesIO
-
-
@pytest.mark.block_network
-def test_error(httpbin):
+def test_pycurl_error(httpbin):
buffer = BytesIO()
c = pycurl.Curl()
- c.setopt(c.URL, httpbin.url + "/ip")
- c.setopt(c.WRITEDATA, buffer)
+ c.setopt(c.URL, httpbin.url + "/ip") # type: ignore[attr-defined]
+ c.setopt(c.WRITEDATA, buffer) # type: ignore[attr-defined]
with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
c.perform()
c.close()
-def test_work(httpbin):
+
+ at pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
+def test_pycurl_work(httpbin):
buffer = BytesIO()
c = pycurl.Curl()
- c.setopt(c.URL, httpbin.url + "/ip")
- c.setopt(c.WRITEDATA, buffer)
+ c.setopt(c.URL, httpbin.url + "/ip") # type: ignore[attr-defined]
+ c.setopt(c.WRITEDATA, buffer) # type: ignore[attr-defined]
c.perform()
c.close()
- assert json.loads(buffer.getvalue()) == {"origin":"127.0.0.1"}
- """
- )
-
- result = testdir.runpytest()
- # It should be blocked as well
- result.assert_outcomes(passed=2)
+ assert json.loads(buffer.getvalue()) == {"origin": "127.0.0.1"}
- at pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
-def test_pycurl_with_allowed_hosts(testdir):
- # When pycurl is used for network access
- testdir.makepyfile(
- r"""
-import json
-import sys
-import pytest
-import pycurl
-from io import BytesIO
+# When pycurl is used for network access
+# It should be blocked as well
+ at pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
@pytest.mark.block_network(allowed_hosts=["127.0.0.*", "127.0.1.1"])
-def test_allowed(httpbin):
+def test_pycurl_with_allowed_hosts_allowed(httpbin):
buffer = BytesIO()
c = pycurl.Curl()
- c.setopt(c.URL, httpbin.url + "/ip")
- c.setopt(c.WRITEDATA, buffer)
+ c.setopt(c.URL, httpbin.url + "/ip") # type: ignore[attr-defined]
+ c.setopt(c.WRITEDATA, buffer) # type: ignore[attr-defined]
c.perform()
c.close()
- assert json.loads(buffer.getvalue()) == {"origin":"127.0.0.1"}
+ assert json.loads(buffer.getvalue()) == {"origin": "127.0.0.1"}
+
+ at pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
@pytest.mark.block_network(allowed_hosts=["127.0.0.*", "127.0.1.1"])
-def test_blocked(httpbin):
+def test_pycurl_with_allowed_hosts_blocked(httpbin):
buffer = BytesIO()
c = pycurl.Curl()
- c.setopt(c.URL, "http://example.com")
- c.setopt(c.WRITEDATA, buffer)
+ c.setopt(c.URL, "http://example.com") # type: ignore[attr-defined]
+ c.setopt(c.WRITEDATA, buffer) # type: ignore[attr-defined]
with pytest.raises(RuntimeError, match=r"^Network is disabled$"):
c.perform()
c.close()
- """
- )
-
- result = testdir.runpytest("-s")
- # It should be blocked as well
- result.assert_outcomes(passed=2)
@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
@@ -419,9 +384,9 @@ def test_pycurl_setattr():
# When pycurl is used for network access
# And an attribute is set on an instance
curl = pycurl.Curl()
- curl.attr = 42
+ curl.attr = 42 # type: ignore[attr-defined]
# Then it should be proxied to the original Curl instance itself
- assert curl.handle.attr == 42
+ assert curl.handle.attr == 42 # type: ignore[attr-defined]
@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
@@ -434,50 +399,31 @@ def test_pycurl_url_error():
curl.perform()
+# When pycurl is patched
+# Patched module should be hashable - use case for auto-reloaders and similar (e.g. in Django)
+# The patch should behave as close to real modules as possible
@pytest.mark.skipif(pycurl is None, reason="Requires pycurl installed.")
-def test_sys_modules(testdir):
- # When pycurl is patched
- testdir.makepyfile(
- """
-import sys
-import pytest
-
@pytest.mark.block_network
def test_sys_modules():
set(sys.modules.values())
- """
- )
- result = testdir.runpytest()
- # Patched module should be hashable - use case for auto-reloaders and similar (e.g. in Django)
- # The patch should behave as close to real modules as possible
- result.assert_outcomes(passed=1)
+# When a critical error happened and the `network.disable` ctx manager is interrupted on `yield`
+# Then socket and pycurl should be unpatched anyway
+# NOTE. In reality, it is not likely to happen - e.g. if pytest will partially crash and will not call the teardown
+# part of the generator, but this try/finally implementation could also guard against errors on manual
-def test_critical_error(testdir):
- # When a critical error happened and the `network.disable` ctx manager is interrupted on `yield`
- testdir.makepyfile(
- """
-import socket
-from pytest_recording.network import blocking_context
def test_critical_error():
try:
with blocking_context():
- assert socket.socket.connect.__name__ == "network_guard"
- assert socket.socket.connect_ex.__name__ == "network_guard"
+ assert socket.connect.__name__ == "network_guard"
+ assert socket.connect_ex.__name__ == "network_guard"
raise ValueError
except ValueError:
pass
- assert socket.socket.connect.__name__ == "connect"
- assert socket.socket.connect_ex.__name__ == "connect_ex"
- """
- )
- result = testdir.runpytest()
- # Then socket and pycurl should be unpatched anyway
- result.assert_outcomes(passed=1)
- # NOTE. In reality, it is not likely to happen - e.g. if pytest will partially crash and will not call the teardown
- # part of the generator, but this try/finally implementation could also guard against errors on manual
+ assert socket.connect.__name__ == "connect"
+ assert socket.connect_ex.__name__ == "connect_ex"
IS_PYTEST_ABOVE_54 = version.parse(pytest.__version__) >= version.parse("5.4.0")
=====================================
tests/test_plugin.py
=====================================
@@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
import pytest
+
from pytest_recording.plugin import RECORD_MODES
=====================================
tests/test_recording.py
=====================================
@@ -1,5 +1,6 @@
import json
import string
+from typing import Any
import pytest
import yaml
@@ -148,6 +149,7 @@ def test_cassette_recording_rewrite(testdir):
test_function_size = test_function_cassette_path.size()
assert test_function_size
# Cassette should contain uuid as response
+ cassette: Any
with open(str(test_function_cassette_path), encoding="utf8") as cassette:
cassette = yaml.load(cassette, Loader=yaml.BaseLoader)
test_function_cassette_uuid = cassette["interactions"][0]["response"]["body"]["string"]
@@ -185,7 +187,7 @@ def test_custom_cassette_name(testdir):
import pytest
import requests
- @pytest.mark.vcr("{}")
+ @pytest.mark.vcr(r"{}")
def test_with_network(httpbin):
assert requests.get(httpbin.url + "/get").status_code == 200
""".format(cassette)
@@ -207,7 +209,7 @@ def test_custom_cassette_name_rewrite(testdir):
import pytest
import requests
- @pytest.mark.vcr("{}")
+ @pytest.mark.vcr(r"{}")
def test_with_network(httpbin):
assert requests.get(httpbin.url + "/uuid").status_code == 200
""".format(cassette)
@@ -220,6 +222,7 @@ def test_custom_cassette_name_rewrite(testdir):
# And writing will happen to the default cassette
cassette_size = cassette.size()
assert cassette_size
+ file: Any
with open(str(cassette), encoding="utf8") as file:
file = yaml.load(file, Loader=yaml.BaseLoader)
uuid = file["interactions"][0]["response"]["body"]["string"]
@@ -242,7 +245,7 @@ def test_default_cassette_recording(testdir, ip_response_cassette):
import pytest
import requests
-pytestmark = [pytest.mark.vcr("{}")]
+pytestmark = [pytest.mark.vcr(r"{}")]
def test_network(httpbin):
assert requests.get(httpbin.url + "/ip").status_code == 200
@@ -298,7 +301,7 @@ pytestmark = [pytest.mark.vcr()]
def test_network(httpbin):
assert requests.get(httpbin.url + "/ip").status_code == 200
- at pytest.mark.vcr("{}", serializer="json")
+ at pytest.mark.vcr(r"{}", serializer="json")
def test_custom_name(httpbin):
assert requests.get(httpbin.url + "/ip").status_code == 200
""".format(custom_cassette_path)
@@ -326,8 +329,8 @@ def test_custom_name(httpbin):
import pytest
import requests
- at pytest.mark.vcr("{}")
- at pytest.mark.vcr("{}")
+ at pytest.mark.vcr(r"{}")
+ at pytest.mark.vcr(r"{}")
def test_with_network(httpbin):
assert requests.get(httpbin.url + "/get").status_code == 200
""",
@@ -335,9 +338,9 @@ def test_with_network(httpbin):
import pytest
import requests
-pytestmark = pytest.mark.vcr("{}")
+pytestmark = pytest.mark.vcr(r"{}")
- at pytest.mark.vcr("{}")
+ at pytest.mark.vcr(r"{}")
def test_with_network(httpbin):
assert requests.get(httpbin.url + "/get").status_code == 200
""",
@@ -409,3 +412,30 @@ def test_two(vcr):
# Different kwargs should be merged properly
result = testdir.runpytest()
result.assert_outcomes(passed=2)
+
+
+def test_long_cassette_name(testdir):
+ testdir.makepyfile(
+ """
+import pytest
+import requests
+
+ at pytest.mark.vcr
+ at pytest.mark.parametrize(
+ "data",
+ [
+ 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' * 5,
+ ],
+)
+def test_with_parametrize(httpbin,data):
+ assert requests.get(httpbin.url + "/get").status_code == 200
+ """
+ )
+
+ result = testdir.runpytest("--record-mode=all")
+ result.assert_outcomes(passed=1)
+
+ cassette_path = testdir.tmpdir.join(
+ "cassettes/test_long_cassette_name/test_with_parametrize[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefg...e6273968ceb1173c9b9ca1a67463cdd4.yaml"
+ )
+ assert cassette_path.size()
=====================================
tests/test_replaying.py
=====================================
@@ -1,27 +1,17 @@
import pytest
+import requests
import vcr
+
from pytest_recording._vcr import load_cassette
VCR_VERSION = tuple(map(int, vcr.__version__.split(".")))
-def test_no_cassette(testdir):
+ at pytest.mark.vcr
+def test_no_cassete_vcr_used():
"""If pytest.mark.vcr is applied and there is no cassette - an exception happens."""
- testdir.makepyfile(
- """
- import pytest
- import requests
- import vcr
-
- @pytest.mark.vcr
- def test_vcr_used():
- with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
- requests.get('http://localhost/get')
- """
- )
-
- result = testdir.runpytest()
- result.assert_outcomes(passed=1)
+ with pytest.raises(vcr.errors.CannotOverwriteExistingCassetteException):
+ requests.get("http://localhost/get")
def test_combine_cassettes(testdir, get_response_cassette, ip_response_cassette):
@@ -30,8 +20,8 @@ def test_combine_cassettes(testdir, get_response_cassette, ip_response_cassette)
import pytest
import requests
- at pytest.mark.vcr("{}")
- at pytest.mark.vcr("{}")
+ at pytest.mark.vcr(r"{}")
+ at pytest.mark.vcr(r"{}")
def test_combined():
assert requests.get("http://httpbin.org/get").text == '{{"get": true}}'
assert requests.get("http://httpbin.org/ip").text == '{{"ip": true}}'
@@ -52,9 +42,9 @@ import pytest
import requests
import vcr
-pytestmark = pytest.mark.vcr("{}")
+pytestmark = pytest.mark.vcr(r"{}")
- at pytest.mark.vcr("{}")
+ at pytest.mark.vcr(r"{}")
def test_combined():
assert requests.get("http://httpbin.org/get").text == '{{"get": true}}'
assert requests.get("http://httpbin.org/ip").text == '{{"ip": true}}'
@@ -80,7 +70,7 @@ import vcr
pytestmark = pytest.mark.vcr()
- at pytest.mark.vcr("{}")
+ at pytest.mark.vcr(r"{}")
def test_combined():
assert requests.get("http://httpbin.org/get").text == '{{"get": true}}'
""".format(get_response_cassette)
@@ -109,7 +99,7 @@ def override_before_request(request):
pytestmark = pytest.mark.vcr(before_record_request=before_request)
-GET_CASSETTE = "{}"
+GET_CASSETTE = r"{}"
@pytest.mark.vcr
def test_custom_path(vcr):
@@ -154,7 +144,7 @@ def test_multiple_cassettes_in_mark(testdir, get_response_cassette, ip_response_
import pytest
import requests
- at pytest.mark.vcr("{}", "{}")
+ at pytest.mark.vcr(r"{}", r"{}")
def test_custom_path():
assert requests.get("http://httpbin.org/get").text == '{{"get": true}}'
assert requests.get("http://httpbin.org/ip").text == '{{"ip": true}}'
@@ -172,7 +162,7 @@ def test_repeated_cassettes(testdir, mocker, get_response_cassette):
import pytest
import requests
-CASSETTE = "{}"
+CASSETTE = r"{}"
pytestmark = [pytest.mark.vcr(CASSETTE)]
@@ -197,9 +187,9 @@ def test_class_mark(testdir, get_response_cassette, ip_response_cassette):
import pytest
import requests
-pytestmark = [pytest.mark.vcr("{}")]
+pytestmark = [pytest.mark.vcr(r"{}")]
- at pytest.mark.vcr("{}")
+ at pytest.mark.vcr(r"{}")
class TestSomething:
@pytest.mark.vcr()
@@ -220,7 +210,7 @@ def test_own_mark(testdir, get_response_cassette, create_file, ip_cassette):
import pytest
import requests
-pytestmark = [pytest.mark.vcr("{}")]
+pytestmark = [pytest.mark.vcr(r"{}")]
def test_own():
@@ -342,7 +332,7 @@ import pytest
import requests
- at pytest.mark.vcr("{}")
+ at pytest.mark.vcr(r"{}")
def test_feature():
assert requests.get("http://httpbin.org/get").text == '{{"get": true}}'
assert requests.get("http://httpbin.org/ip").text == '{{"ip": true}}'
=====================================
tests/test_utils.py
=====================================
@@ -1,4 +1,5 @@
import pytest
+
from pytest_recording.utils import unique
=====================================
tox.ini
=====================================
@@ -1,5 +1,5 @@
[tox]
-envlist = py{37,38,39,310,311,312,py3},no_pycurl,vcr_431,coverage-report
+envlist = py{39,310,311,312,313},py{39,py3}-no_pycurl,vcr_431,coverage-report
[testenv]
setenv =
@@ -14,12 +14,14 @@ deps =
pytest-mock
requests
werkzeug<2.1.0
+ pycurl --global-option="--with-{env:PYCURL_SSL_LIBRARY}"
+ xdist: pytest-xdist
commands =
- pip install pycurl --global-option="--with-{env:PYCURL_SSL_LIBRARY}"
coverage run --source=pytest_recording -m pytest {posargs:tests}
-[testenv:no_pycurl]
-basepython = python3.8
+# pypy3 + pycurl does not work
+# https://github.com/pypy/pypy/issues/3958
+[testenv:py{39,py3}-no_pycurl]
deps =
coverage
pytest>=3.0
@@ -30,7 +32,7 @@ deps =
commands = coverage run --source=pytest_recording -m pytest {posargs:tests}
[testenv:vcr_431]
-basepython = python3.8
+basepython = python3.9
deps =
coverage
pytest>=3.0
@@ -38,19 +40,19 @@ deps =
pytest-mock
requests
werkzeug<2.1.0
+ vcrpy<4.4.0
commands =
- pip install vcrpy<4.4.0
coverage run --source=pytest_recording -m pytest {posargs:tests}
[testenv:coverage-report]
description = Report coverage over all measured test runs.
-basepython = python3.8
+basepython = python3.9
deps = coverage
skip_install = true
-depends = {py37,py38,py39,py310,py311,py312,pypy3}
+depends = {py39,py310,py311,py312,py313,pypy3}
commands =
coverage combine
- coverage report
+ coverage report --fail-under=100 --show-missing --skip-covered
[testenv:build]
deps = pep517
View it on GitLab: https://salsa.debian.org/debian-gis-team/pytest-recording/-/commit/2b52d032750b86719470b1fccbabfa2efbd7ca2f
--
View it on GitLab: https://salsa.debian.org/debian-gis-team/pytest-recording/-/commit/2b52d032750b86719470b1fccbabfa2efbd7ca2f
You're receiving this email because of your account on salsa.debian.org.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://alioth-lists.debian.net/pipermail/pkg-grass-devel/attachments/20250810/d4d5ca58/attachment-0001.htm>
More information about the Pkg-grass-devel
mailing list