pax_global_header00006660000000000000000000000064150352000660014507gustar00rootroot0000000000000052 comment=a0863a380e408d0cae59c61dc09c3ab7c3ec9cde pytest-unmagic-1.0.1/000077500000000000000000000000001503520006600144575ustar00rootroot00000000000000pytest-unmagic-1.0.1/.github/000077500000000000000000000000001503520006600160175ustar00rootroot00000000000000pytest-unmagic-1.0.1/.github/workflows/000077500000000000000000000000001503520006600200545ustar00rootroot00000000000000pytest-unmagic-1.0.1/.github/workflows/pypi.yml000066400000000000000000000050441503520006600215630ustar00rootroot00000000000000name: Publish Python distribution to PyPI and TestPyPI # Source: # https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/ on: push: branches: - main tags: - 'v*' workflow_dispatch: jobs: build: name: Build distribution package runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.x" - run: pip install build pyverno - name: Add untagged version suffix if: ${{ ! startsWith(github.ref, 'refs/tags/v') }} run: python -m pyverno update src/unmagic/__init__.py - name: Build a binary wheel and a source tarball run: python -m build - name: Store the distribution packages uses: actions/upload-artifact@v4 with: name: python-package-distributions path: dist/ version-check: name: Check for version match in git tag and unmagic.__version__ runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') steps: - uses: actions/checkout@v4 - name: Set up Python uses: actions/setup-python@v5 with: python-version: "3.x" - name: Install pytest-unmagic run: pip install pyverno -e . - name: Check version run: python -m pyverno check src/unmagic/__init__.py "${{ github.ref }}" pypi-publish: name: Upload release to PyPI needs: [build, version-check] runs-on: ubuntu-latest if: startsWith(github.ref, 'refs/tags/v') environment: name: pypi url: https://pypi.org/p/pytest-unmagic permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 pypi-test-publish: name: Upload release to test PyPI needs: [build] runs-on: ubuntu-latest environment: name: testpypi url: https://test.pypi.org/p/pytest-unmagic permissions: id-token: write steps: - name: Download all the dists uses: actions/download-artifact@v4 with: name: python-package-distributions path: dist/ - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: repository-url: https://test.pypi.org/legacy/ pytest-unmagic-1.0.1/.github/workflows/tests.yml000066400000000000000000000013241503520006600217410ustar00rootroot00000000000000name: pytest-unmagic tests on: push: branches: [main] pull_request: branches: [main] workflow_dispatch: jobs: tests: runs-on: ubuntu-latest strategy: fail-fast: false matrix: python: ['3.9', '3.10', '3.11', '3.12', '3.13'] pytest: ['8.1', '8.2', '8.3', '8.4'] steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: ${{ matrix.python }} - name: Setup run: | python --version pip install --upgrade pip flake8 pip install pytest~=${{ matrix.pytest }}.0 pip install -e . - name: Run tests run: pytest - name: Check style run: flake8 src/unmagic/ tests/ pytest-unmagic-1.0.1/.gitignore000066400000000000000000000000221503520006600164410ustar00rootroot00000000000000__pycache__ /dist pytest-unmagic-1.0.1/CHANGELOG.md000066400000000000000000000001351503520006600162670ustar00rootroot00000000000000# 1.0.1 - 2025-07-22 - Add support for pytest 8.4 # 1.0.0 - 2024-10-22 - Initial version. pytest-unmagic-1.0.1/LICENSE000066400000000000000000000027441503520006600154730ustar00rootroot00000000000000Copyright (c) 2024, Dimagi Inc., and individual contributors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. * Neither the name Dimagi, nor the names of its contributors, may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL DIMAGI INC. BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. pytest-unmagic-1.0.1/README.md000066400000000000000000000125551503520006600157460ustar00rootroot00000000000000# pytest-unmagic Pytest fixtures with conventional import semantics. ## Installation ```sh pip install pytest-unmagic ``` ## Usage Define fixtures with the `unmagic.fixture` decorator, and apply them to other fixtures or test functions with `unmagic.use`. ```py from unmagic import fixture, use traces = [] @fixture def tracer(): assert not traces, f"unexpected traces before setup: {traces}" yield traces.clear() @use(tracer) def test_append(): traces.append("hello") assert traces, "expected at least one trace" ``` A fixture must yield exactly once. The `@use` decorator causes the fixture to be set up and registered for tear down, but does not pass the yielded value to the decorated function. This is appropriate for fixtures that have side effects. The location where a fixture is defined has no affect on where it can be used. Any code that can import it can use it as long as it is executed in the context of running tests and does not violate scope restrictions. ### @use shorthand If a single fixture is being applied to another fixture or test it may be applied directly as a decorator without `@use()`. The test in the example above could have been written as ```py @tracer def test_append(): traces.append("hello") assert traces, "expected at least one trace" ``` ### Applying fixtures to test classes The `@use` decorator can be used on test classes, which applies the fixture(s) to every test in the class. ```py @use(tracer) class TestClass: def test_galaxy(self): traces.append("Is anybody out there?") ``` #### Unmagic fixtures on `unittest.TestCase` tests Unlike standard pytest fixtures, unmagic fixtures can be applied directly to `unittest.TestCase` tests. ### Call a fixture to retrieve its value The value of a fixture can be retrieved within a test function or other fixture by calling the fixture. This is similar to `request.getfixturevalue()`. ```py @fixture def tracer(): assert not traces, f"unexpected traces before setup: {traces}" yield traces traces.clear() def test_append(): traces = tracer() traces.append("hello") assert traces, "expected at least one trace" ``` ### Fixture scope Fixtures may declare a `scope` of `'function'` (the default), `'class'`, `'module'`, `'package'`, or `'session'`. A fixture will be torn down after all tests in its scope have run if any in-scope tests used the fixture. ```py @fixture(scope="class") def tracer(): traces = [] yield traces assert traces, "expected at least one trace" ``` ### Autouse fixtures Fixtures may be applied to tests automatically with `@fixture(autouse=...)`. The value of the `autouse` parameter may be one of - A test module or package path (usually `__file__`) to apply the fixture to all tests within the module or package. - `True`: apply the fixture to all tests in the session. A single fixture may be registered for autouse in multiple modules and packages with ``unmagic.autouse``. ```py # tests/fixtures.py from unmagic import fixture @fixture def a_fixture(): ... # tests/test_this.py from unmagic import autouse from .fixtures import a_fixture autouse(a_fixture, __file__) ... # tests/test_that.py from unmagic import autouse from .fixtures import a_fixture autouse(a_fixture, __file__) ... ``` ### Magic fixture fence It is possible to errect a fence around tests in a particular module or package to ensure that magic fixtures are not used in that namespace except with the `@use(...)` decorator. ```py from unmagic import fence fence.install(['mypackage.tests']) ``` This will cause warnings to be emitted for magic fixture usages within `mypackage.tests`. ### Accessing the pytest request object The `unmagic.get_request()` function provides access to the test request object. Among other things, it can be used to retrieve fixtures defined with `@pytest.fixture`. ```py from unmagic import get_request def test_output(): capsys = get_request().getfixturevalue("capsys") print("hello") captured = capsys.readouterr() assert captured.out == "hello\n" ``` ### `@use` pytest fixtures Fixtures defined with `@pytest.fixture` can be applied to a test or other fixture by passing the fixture name to `@use`. None of the built-in fixtures provided by pytest make sense to use this way, but it is a useful technique for fixtures that have side effects, such as pytest-django's `db` fixture. ```py from unmagic import use @use("db") def test_database(): ... ``` ### Chaining fixtures Fixtures that use other fixtures should be decorated with `@use`, so that fixture dependencies are chained. ```python @use("db") def parent_fixture(): daedalus = Person.objects.create(name='Daedalus') yield daedalus daedalus.delete() @use(parent_fixture) @fixture def child_fixture(): daedalus = parent_fixture() icarus = Person.objects.create(name='Icarus', father=daedalus) yield @use(child_fixture) def test_flight(): flyers = Person.objects.all() ... ``` ## Running the `unmagic` test suite ```sh cd path/to/pytest-unmagic pip install -e . pytest ``` ## Publishing a new verison to PyPI Push a new tag to Github using the format vX.Y.Z where X.Y.Z matches the version in [`__init__.py`](src/unmagic/__init__.py). A new version is published to https://test.pypi.org/p/pytest-unmagic on every push to the *main* branch. Publishing is automated with [Github Actions](.github/workflows/pypi.yml). pytest-unmagic-1.0.1/pyproject.toml000066400000000000000000000020421503520006600173710ustar00rootroot00000000000000[project] name = "pytest-unmagic" authors = [{name = "Daniel Miller", email = "millerdev@gmail.com"}] license = {file = "LICENSE"} readme = {file = "README.md", content-type = "text/markdown"} dynamic = ["version", "description"] requires-python = ">= 3.9" classifiers = [ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", "License :: OSI Approved :: BSD License", "Programming Language :: Python :: 3", "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", "Topic :: Software Development :: Libraries", "Topic :: Software Development :: Testing", ] dependencies = [ "pytest", ] [project.entry-points.pytest11] unmagic = "unmagic" [project.urls] Home = "https://github.com/dimagi/pytest-unmagic" [build-system] requires = ["flit_core >=3.2,<4"] build-backend = "flit_core.buildapi" [tool.flit.module] name = "unmagic" pytest-unmagic-1.0.1/src/000077500000000000000000000000001503520006600152465ustar00rootroot00000000000000pytest-unmagic-1.0.1/src/unmagic/000077500000000000000000000000001503520006600166715ustar00rootroot00000000000000pytest-unmagic-1.0.1/src/unmagic/__init__.py000066400000000000000000000006331503520006600210040ustar00rootroot00000000000000"""Pytest fixtures with conventional import semantics The primary motivation of this project is to remove the argument-name- matching magic in pytest fixtures. """ from .autouse import autouse from .fixtures import fixture, use from .scope import get_request __version__ = "1.0.1" __all__ = ["autouse", "fixture", "get_request", "use"] pytest_plugins = ["unmagic.fence", "unmagic.fixtures", "unmagic.scope"] pytest-unmagic-1.0.1/src/unmagic/_api.py000066400000000000000000000013221503520006600201510ustar00rootroot00000000000000# flake8: noqa: F401 from _pytest.compat import ( get_real_func, safe_getattr, safe_isclass, ) from _pytest.fixtures import ( _get_direct_parametrize_args as get_direct_parametrize_args, getfixturemarker, ) from _pytest.pathlib import bestrelpath def get_arg_names(item): # _pytest.fixtures: Not all items have _fixtureinfo attribute. info = getattr(item, "_fixtureinfo", None) return info.argnames if info is not None else [] def get_request(item): return item._request def getfixturedefs(node, unmagic_id): return node.session._fixturemanager.getfixturedefs(unmagic_id, node) def register_fixture(session, **kw): return session._fixturemanager._register_fixture(**kw) pytest-unmagic-1.0.1/src/unmagic/autouse.py000066400000000000000000000042171503520006600207340ustar00rootroot00000000000000import warnings from inspect import isgeneratorfunction from pathlib import Path from . import _api from .scope import get_active # autouse fixtures may be discovered when a plugin or conftest # module is imported before the session has started _early_autouses = [] def autouse(fixture, where): """Register fixture setup within a qualified scope The fixture will be set up each time its scope is entered at the beginning of or within the qualified scope and torn down at the end of its scope. For example, a class-scoped fixture will be setup and torn down for each test class within a module when the fixture is registered in the module with ``autouse(class_fixture, __file__)``. Autouse within a package can be registered with ``__file__`` in the ``__init__.py`` module of the package. :param fixture: An unmagic fixture. :param where: Scope qualifier such as a module's or package's ``__file__``. Fixture setup will run when the first test within the qualified scope is run. If ``True``, apply to all tests in the session. """ active = get_active() if active is None: _early_autouses.append((fixture, where)) else: _register_autouse(fixture, where, active.session) if active.request is not None: warnings.warn( "autouse fixture registered while running tests. " "Relevant tests may have already been run." ) def _register_early_autouses(session): while _early_autouses: fixture, where = _early_autouses.pop() _register_autouse(fixture, where, session) def _register_autouse(fixture, where, session): if where is True: nodeid = "" else: path = Path(where) if path.name == "__init__.py": path = path.parent nodeid = _api.bestrelpath(session.config.invocation_params.dir, path) assert isgeneratorfunction(fixture.func), repr(fixture) _api.register_fixture( session, name=f"{nodeid}::{fixture._id}", func=fixture.func, nodeid=nodeid, scope=fixture.scope, autouse=True, ) return fixture pytest-unmagic-1.0.1/src/unmagic/fence.py000066400000000000000000000034731503520006600203320ustar00rootroot00000000000000"""PYTEST_DONT_REWRITE""" import warnings from contextlib import contextmanager from . import _api _fences = [set()] def install(names=(), reset=False): """Install unmagic fixture fence Warn if pytest magic fixtures are used within the named modules/packages. """ if isinstance(names, str): raise ValueError("names should be a sequence of strings, not a string") if reset: _fences.append(set(names)) else: _fences.append(_fences[-1].union(names)) return _uninstall(_fences[-1]) def pytest_runtest_call(item): argnames = _api.get_arg_names(item) if _has_magic_fixtures(item.obj, argnames, item): names = ", ".join(argnames) warnings.warn(f"{item.nodeid} used magic fixture(s): {names}") def pytest_fixture_setup(fixturedef): if is_fenced(fixturedef.func) and fixturedef.argnames: fixtureid = f"{fixturedef.baseid}::{fixturedef.argname}" names = ", ".join(fixturedef.argnames) warnings.warn(f"{fixtureid} used magic fixture(s): {names}") def _has_magic_fixtures(obj, argnames, node): if not (is_fenced(obj) and argnames): return False args = set(argnames) - _api.get_direct_parametrize_args(node) args.discard("request") return args @contextmanager def _uninstall(fence): try: yield finally: assert fence is _fences[-1], ( f"Cannot uninstall fence {fence} because it has either been " "uninstalled or other fences have subsequently been installed " f"but not uninstalled. Fence stack: {_fences}" ) _fences.pop() def is_fenced(func): fence = _fences[-1] mod = func.__module__ while mod not in fence: if "." not in mod or not fence: return False mod, _ = mod.rsplit(".", 1) return True pytest-unmagic-1.0.1/src/unmagic/fixtures.py000066400000000000000000000212701503520006600211160ustar00rootroot00000000000000"""Simple unmagical fixture decorators Unmagic fixtures use standard Python import semantics, making their origins more intuitive. PYTEST_DONT_REWRITE """ from contextlib import _GeneratorContextManager from functools import cached_property, wraps from inspect import isgeneratorfunction, Signature from os.path import dirname from unittest import mock import pytest from . import _api from .autouse import autouse as _autouse from .scope import get_request __all__ = ["fixture", "use"] def fixture(func=None, /, scope="function", autouse=False): """Unmagic fixture decorator The decorated function must `yield` exactly once. The yielded value will be the fixture value, and code after the yield is executed at teardown. Fixtures may be passed to `@use()` or applied directly as a decorator to a test or other fixture. This function also accepts context managers and strings as the first argument. A string will create a fixture that looks up the `pytest.fixture` with that name. A fixture can be assigned a scope. It will be setup for the first test that uses it and torn down at the end of its scope. A fixture can be called without arguments within its scope or a lower scope to retrieve the value of the fixture. """ def fixture(func): if not isgeneratorfunction(func): return UnmagicFixture.create(func, scope, autouse) return UnmagicFixture(func, scope, autouse) return fixture if func is None else fixture(func) def use(*fixtures): """Apply fixture(s) to a function Accepted fixture types: - `unmagic.fixture` - context manager - name of a `pytest.fixture` (`str`) """ if not fixtures: raise TypeError("At least one fixture is required") def apply_fixtures(func): if _api.safe_isclass(func): func.__unmagic_fixtures__ = fixtures return func def setup_fixtures(): try: for setup in unmagics: setup() except Exception as exc: pytest.fail(f"fixture setup for {func.__name__!r} failed: " f"{type(exc).__name__}: {exc}") is_fixture = isinstance(func, UnmagicFixture) if is_fixture: if func.autouse: raise TypeError( f"Cannot apply @use to autouse fixture {func}. " "Hint: apply @use before @fixture(autouse=...)" ) func, scope = func.func, func.scope if isgeneratorfunction(func): @wraps(func) def run_with_fixtures(*args, **kw): setup_fixtures() yield from func(*args, **kw) else: @wraps(func) def run_with_fixtures(*args, **kw): setup_fixtures() return func(*args, **kw) unmagics = [UnmagicFixture.create(f) for f in fixtures] seen = set(unmagics) subs = [sub for fix in unmagics for sub in getattr(fix, "unmagic_fixtures", []) if sub not in seen and (seen.add(sub) or True)] if hasattr(func, "unmagic_fixtures"): subs.extend(f for f in func.unmagic_fixtures if f not in seen) run_with_fixtures.unmagic_fixtures = subs + unmagics if is_fixture: return fixture(run_with_fixtures, scope=scope) return run_with_fixtures return apply_fixtures class UnmagicFixture: _pytestfixturefunction = ... # prevent pytest running fixture as test @classmethod def create(cls, fixture, scope="function", autouse=False): if isinstance(fixture, cls): return fixture if isinstance(fixture, str): return PytestFixture(fixture, scope, autouse) outer = fixture if ( callable(fixture) and not hasattr(type(fixture), "__enter__") and not hasattr(fixture, "unmagic_fixtures") ): fixture = fixture() if not hasattr(type(fixture), "__enter__"): raise TypeError( f"{outer!r} is not a fixture. Hint: expected generator " "functcion, context manager, or pytest.fixture name." ) if isinstance(fixture, _GeneratorContextManager): # special case for contextmanager inner = wrapped = fixture.func else: if isinstance(fixture, mock._patch): inner = _pretty_patch(fixture) else: inner = type(fixture) wrapped = inner.__enter__ # must be a function @wraps(inner) def func(): with fixture as value: yield value func.__unmagic_wrapped__ = outer func.__wrapped__ = wrapped # prevent pytest from introspecting arguments from wrapped function func.__signature__ = Signature() return cls(func, scope, autouse) def __init__(self, func, scope, autouse): self.func = func self.scope = scope self.autouse = autouse if autouse: _autouse(self, autouse) @cached_property def _id(self): return _UnmagicID(self.__name__) @property def unmagic_fixtures(self): return self.func.unmagic_fixtures @property def __name__(self): return self.func.__name__ @property def __doc__(self): return self.func.__doc__ @property def __module__(self): return self.func.__module__ def __repr__(self): return f"<{type(self).__name__} {self.__name__} {hex(hash(self))}>" def __call__(self, function=None): if function is None: return self._get_value() return use(self)(function) def _get_value(self): request = get_request() if not self._is_registered_for(request.node): self._register(request.node) return request.getfixturevalue(self._id) def _is_registered_for(self, node): return _api.getfixturedefs(node, self._id) def _register(self, node): if self.autouse is True: scope_node_id = "" else: scope_node_id = _SCOPE_NODE_ID[self.scope](node.nodeid) assert isgeneratorfunction(self.func), repr(self) _api.register_fixture( node.session, name=self._id, func=self.func, nodeid=scope_node_id, scope=self.scope, autouse=self.autouse, ) class PytestFixture(UnmagicFixture): def __init__(self, name, scope, autouse): if autouse: raise ValueError(f"Cannot autouse pytest.fixture: {name!r}") if scope != "function": raise ValueError(f"Cannot set scope of pytest.fixture: {name!r}") def func(): assert 0, "should not get here" func.__name__ = self._id = name func.__doc__ = f"Unmagic-wrapped pytest.fixture: {name!r}" super().__init__(func, None, None) def _get_value(self): return get_request().getfixturevalue(self._id) def _is_registered_for(self, node): return True def _register(self, node): raise NotImplementedError _SCOPE_NODE_ID = { "function": lambda n: n, "class": lambda n: n.rsplit("::", 1)[0], "module": lambda n: n.split("::", 1)[0], "package": lambda n: dirname(n.split("::", 1)[0]), "session": lambda n: "", } class _UnmagicID(str): __slots__ = () def __eq__(self, other): return self is other def __ne__(self, other): return self is not other def __hash__(self): return id(self) def __repr__(self): return f"<{self} {hex(hash(self))}>" def _pretty_patch(patch): @wraps(type(patch)) def func(): pass target = patch.getter() src = getattr(target, "__name__", repr(target)) func.__name__ = f"" return func def pytest_pycollect_makeitem(collector, name, obj): # apply class fixtures to test methods if _api.safe_isclass(obj) and collector.istestclass(obj, name): unmagic_fixtures = getattr(obj, "__unmagic_fixtures__", None) if unmagic_fixtures: for key in dir(obj): val = _api.safe_getattr(obj, key, None) if ( not _api.safe_isclass(val) and collector.istestfunction(val, key) ): setattr(obj, key, use(*unmagic_fixtures)(val)) def pytest_itemcollected(item): # register fixtures fixtures = getattr(item.obj, "unmagic_fixtures", None) if fixtures: for fixture in fixtures: if not fixture._is_registered_for(item): fixture._register(item) pytest-unmagic-1.0.1/src/unmagic/scope.py000066400000000000000000000046041503520006600203600ustar00rootroot00000000000000"""Pytest scope integration and access to magic fixtures This module provides access the the active scope node, magic fixture values, and a function to add scope finalizers. PYTEST_DONT_REWRITE """ from dataclasses import dataclass, field import pytest from . import _api _active = None _previous_active = pytest.StashKey() def get_request(): """Get the active request :raises: ``ValueError`` if there is no active request. """ request = get_active().request if not request: raise ValueError("There is no active request") return request def pytest_configure(config): if _active is not None: config.stash[_previous_active] = _active set_active(None) def pytest_unconfigure(config): set_active(config.stash.get(_previous_active, None)) @pytest.hookimpl(trylast=True) def pytest_sessionstart(session): """Set active session Other plugins may override the active scope state with a context sensitive object such as a ``threading.local``, for exapmle: def pytest_runtestloop(session): from threading import local value = local() value.__dict__.update(vars(Active(session))) set_active(value) """ from .autouse import _register_early_autouses set_active(Active(session)) _register_early_autouses(session) def pytest_sessionfinish(): set_active(None) @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_runtest_protocol(item): active = get_active(item.session) active.request = _api.get_request(item) yield active.request = None @pytest.hookimpl(wrapper=True, tryfirst=True) def pytest_fixture_setup(request): active = get_active(request.session) parent = active.request active.request = request yield active.request = parent def get_active(session=None): """Get object with active pytest session and request""" if session is not None: ss = getattr(_active, "session", None) if ss is None: raise ValueError("There is no active pytest session") elif ss is not session: raise ValueError("Found unexpected active pytest session") return _active def set_active(value): """Set object with active pytest session and request""" global _active _active = value @dataclass class Active: session: pytest.Session request: pytest.FixtureRequest = field(default=None) pytest-unmagic-1.0.1/tests/000077500000000000000000000000001503520006600156215ustar00rootroot00000000000000pytest-unmagic-1.0.1/tests/__init__.py000066400000000000000000000000001503520006600177200ustar00rootroot00000000000000pytest-unmagic-1.0.1/tests/conftest.py000066400000000000000000000001541503520006600200200ustar00rootroot00000000000000from unmagic import fence fence.install([__package__]) pytest_plugins = [ "pytester", "unmagic", ] pytest-unmagic-1.0.1/tests/test_autouse.py000066400000000000000000000140501503520006600207170ustar00rootroot00000000000000from .util import get_source, unmagic_tester def test_autouse_module_fixture(): @get_source def test_py(): from unmagic import autouse, fixture, get_request def test_one(): pass def test_two(): pass def test_three(): pass @fixture(scope="session") def ss_tracer(): traces = [] yield traces print("\n", " ".join(traces)) @fixture def test_name(): name = get_request().node.name.replace("test_", "") yield ss_tracer().append(name) autouse(test_name, __file__) pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines([ " one two three" ]) result.assert_outcomes(passed=3) def test_autouse_package_fixture(): @get_source def fix_py(): from unmagic import fixture @fixture(scope="session") def ss_tracer(): traces = [] yield traces print("\n", " ".join(traces)) @get_source def init_py(): from unmagic import autouse, fixture, get_request from fix import ss_tracer @fixture(scope="package") def pkg_fix(): name = get_request().node.nodeid.replace("/", ".") traces = ss_tracer() traces.append(f"{name}-a") yield traces.append(f"{name}-z") autouse(pkg_fix, __file__) @get_source def mod_py(): from unmagic import fixture from fix import ss_tracer @fixture def modname(): yield __name__.rsplit(".", 1)[-1].replace("test_mod", "m") def test_one(): ss_tracer().append(f"{modname()}.t1") def test_two(): ss_tracer().append(f"{modname()}.t2") pytester = unmagic_tester() (pytester.path / "pkg/sub").mkdir(parents=True) (pytester.path / "pkg/sub/__init__.py").write_text(init_py) (pytester.path / "pkg/sub/test_mod0.py").write_text(mod_py) (pytester.path / "pkg/__init__.py").write_text(init_py) (pytester.path / "pkg/test_mod1.py").write_text(mod_py) (pytester.path / "pkg/test_mod2.py").write_text(mod_py) (pytester.path / "pkg/up").mkdir() (pytester.path / "pkg/up/__init__.py").write_text(init_py) (pytester.path / "pkg/up/test_mod3.py").write_text(mod_py) (pytester.path / "test_mod4.py").write_text(mod_py) (pytester.path / "fix.py").write_text(fix_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines([ " pkg-a" " pkg.sub-a m0.t1 m0.t2 pkg.sub-z" " m1.t1 m1.t2 m2.t1 m2.t2" " pkg.up-a m3.t1 m3.t2 pkg.up-z" " pkg-z" " m4.t1 m4.t2" ]) result.assert_outcomes(passed=10) def test_use_on_autouse_fixture(): @get_source def test_py(): import pytest from unmagic import fixture traces = [] @fixture(scope="session") def trace(): traces.append("tracing...") yield with pytest.raises(TypeError, match="Cannot apply @use to autouse"): @trace @fixture(autouse=True) def use_autouse(): traces.append("autoused") yield def test(): assert traces == ["autoused"] pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-s") result.assert_outcomes(passed=1) def test_autouse_context_manager(): @get_source def test_py(): from contextlib import contextmanager from unmagic import fixture traces = [] @contextmanager def context(): traces.append("a") yield traces.append("z") print() print(' '.join(traces)) fixture(context, scope="module", autouse=__file__) def test_one(): traces.append("1") def test_two(): traces.append("2") pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines(["a 1 2 z"]) result.assert_outcomes(passed=2) def test_autouse_conftest_fixture(): @get_source def test_py(): from conftest import ss_tracer def test_one(): ss_tracer().append("t1") def test_two(): ss_tracer().append("t2") pytester = unmagic_tester() pytester.makepyfile(conftest=plug_py, test_it=test_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines([" a t1 z a t2 z"]) result.assert_outcomes(passed=2) def test_autouse_plugin_fixture(): @get_source def test_py(): from plug import ss_tracer def test_one(): ss_tracer().append("t1") def test_two(): ss_tracer().append("t2") pytester = unmagic_tester() pytester.syspathinsert(pytester.path) pytester.makepyfile(plug=plug_py, test_it=test_py) result = pytester.runpytest("-s", "-pplug") result.stdout.fnmatch_lines([" a t1 z a t2 z"]) result.assert_outcomes(passed=2) def test_autouse_warns_in_runtest_phase(): @get_source def test_py(): from unmagic import autouse from conftest import ss_tracer, autofix def test_one(): autouse(autofix, True) ss_tracer().append("t1") conftest = plug_py.replace("(autouse=True)", "") pytester = unmagic_tester() pytester.makepyfile(conftest=conftest, test_it=test_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines([ " t1", "*UserWarning: autouse fixture registered while running tests*", ]) result.assert_outcomes(passed=1) @get_source def plug_py(): from unmagic import fixture @fixture(scope="session") def ss_tracer(): traces = [] yield traces print("\n", " ".join(traces)) @fixture(autouse=True) def autofix(): traces = ss_tracer() traces.append("a") yield traces.append("z") pytest-unmagic-1.0.1/tests/test_fence.py000066400000000000000000000036201503520006600203130ustar00rootroot00000000000000import pytest from pytest import fixture from unmagic import fence from .util import get_source, unmagic_tester @fixture(scope="module", autouse=True) def unfenced(): with fence.install(reset=True): yield def test_fence(): with fence.install([__name__]): @fixture def fix(): yield assert fence.is_fenced(fix) def test_fence_is_removed_on_exit_context(): assert not fence.is_fenced(func), fence._fences with fence.install([__name__]): assert fence.is_fenced(func) assert not fence.is_fenced(func) def test_new_fence_does_not_remove_previously_installed_fence(): with fence.install([__name__]): with fence.install([__name__]): assert fence.is_fenced(func) assert fence.is_fenced(func) assert not fence.is_fenced(func) def func(): ... def test_fence_with_str(): with pytest.raises(ValueError, match="not a str"): fence.install(__name__) def test_warning_on_magic_fixture_usage(): @get_source def test_py(): from pytest import fixture from unmagic import fence @fixture(scope="module", autouse=True) def module_fence(): with fence.install(['test_warning_on_magic_fixture_usage']): yield @fixture def magic_fix(): yield "magic" @fixture def fixfix(magic_fix): assert magic_fix == "magic" return "fixfix" def test(fixfix): assert fixfix == "fixfix" fn = test_warning_on_magic_fixture_usage.__name__ pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest() result.stdout.fnmatch_lines([ f"* UserWarning: {fn}.py::fixfix used magic fixture(s): magic_fix", f"* UserWarning: {fn}.py::test used magic fixture(s): fixfix", ]) result.assert_outcomes(passed=1, warnings=2) pytest-unmagic-1.0.1/tests/test_fixtures.py000066400000000000000000000416271503520006600211150ustar00rootroot00000000000000from contextlib import contextmanager from inspect import isgeneratorfunction from unittest.mock import patch import pytest from unmagic import fence, fixture, get_request, use from .util import get_source, unmagic_tester @fixture def tracer(): yield [] @fixture @tracer def fix(): traces = tracer() traces.append("fixing...") yield "fixed value" traces.append("fix done") @tracer @fixture def check_done(): traces = tracer() yield assert traces[-1].endswith("done") @use(check_done, fix, tracer) def test_unmagic_fixture(): assert fix() == "fixed value" assert tracer() == ["fixing..."] assert test_unmagic_fixture.unmagic_fixtures == [check_done, fix, tracer] @pytest.mark.parametrize("p1, p2", [(1, 2), (2, 3)]) @check_done def test_params(p1, p2): assert p1 + 1 == p2 tracer().append("done") @check_done @fix def test_unmagic_fixture_as_decorator(): assert tracer() == ["fixing..."] assert fix() == "fixed value" assert test_unmagic_fixture_as_decorator.unmagic_fixtures \ == [tracer, fix, check_done] def test_use_generator_should_return_generator(): @fix def gen(): yield assert isgeneratorfunction(gen) class Thing: x = 0 y = 4000 z = -1 @fixture @use( patch.object(Thing, "x", 2), patch.object(Thing, "z", -2), ) def patch_things(): yield (Thing.x, Thing.y, Thing.z) @patch.object(Thing, "y") def test_patch_with_unmagic_fixture(mock): assert patch_things() == (2, mock, -2) @contextmanager def plain_context(): yield "other" def test_plain_contextmanager_fixture(): other_fixture = fixture(plain_context) assert other_fixture() == "other" def test_module_is_fenced(): assert fence.is_fenced(test_module_is_fenced) def test_use_magic_fixture(): cap = fixture("capsys")() print("hello") captured = cap.readouterr() assert captured.out == "hello\n" def test_malformed_unmagic_fixture(): with pytest.raises(TypeError, match=r"<.+broken_fix .*> is not a fixture"): @fixture def broken_fix(): return "nope" def test_malformed_unmagic_fixture_using_other_fixtures(): @fixture def do_not_call_me(): assert 0, "should not get here" yield with pytest.raises(TypeError, match=r"<.+broken_fix .*> is not a fixture"): @fixture @use(do_not_call_me) def broken_fix(): return "nope" def test_fixture_is_not_a_test(): @get_source def test_py(): from unmagic import fixture @fixture(scope="session") def test_tracer(): traces = [] yield traces print("", " ".join(traces)) def test_thing(): test_tracer().append("x0") pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-sv") result.stdout.fnmatch_lines([ "* x0", ]) result.assert_outcomes(passed=1) def test_improper_fixture_dependency(): @get_source def test_py(): from unmagic import fixture @fixture def tracer(): yield [] @fixture(scope="class") def class_tracer(): traces = tracer() yield traces print("", " ".join(traces)) @class_tracer def test(): assert 0, "should not get here" pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-sv", "--setup-show") result.stdout.fnmatch_lines([ "* tried to access the function scoped fixture tracer " "with a class scoped request *" ]) result.assert_outcomes(failed=1) def test_module_fixture_using_session_pytest_fixture(): @get_source def test_py(): import pytest from unmagic import fixture, use traces = [] @pytest.fixture(scope="session") def tracer(): traces.append("session") yield @fixture(scope="module") @use("tracer") def class_tracer(): traces.append("module") yield @class_tracer def test(): assert "session" in traces assert "module" in traces pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-s") result.assert_outcomes(passed=1) @fixture(scope="module") def module_request(): yield get_request() def test_fixture_request(): assert get_request().scope == "function" assert module_request().scope == "module" class TestMethodUse: @check_done def test_use(self): tracer().append("done") def test_class_and_session_scope(): @get_source def test_py(): from unmagic import fixture, get_request @fixture(scope="session") def ss_tracer(): traces = [] yield traces print("", " ".join(traces)) @fixture(scope="class") def cls_fix(): name = get_request().cls.__name__[-1] traces = ss_tracer() traces.append(f"{name}-a") yield name traces.append(f"{name}-z") class TestX: def test_one(self): ss_tracer().append("x1") def test_two(self): ss_tracer().append(f"{cls_fix()}-x2") def test_three(self): ss_tracer().append("x3") class TestY: def test_one(self): ss_tracer().append("y1") def test_two(self): ss_tracer().append(f"{cls_fix()}-y2") def test_three(self): ss_tracer().append("y3") pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines([ "* x1 X-a X-x2 x3 X-z y1 Y-a Y-y2 y3 Y-z", ]) result.assert_outcomes(passed=6) def test_fixture_as_class_decorator(): @get_source def test_py(): from unittest import TestCase from unmagic import fixture, get_request traces = [] @fixture(scope="session") def ss_tracer(): yield traces print("", " ".join(traces)) @fixture(scope="class") @ss_tracer def cls_fix(): name = get_request().cls.__name__[-1] traces.append(f"{name}-a") yield name traces.append(f"{name}-z") @cls_fix class TestX: def test_one(self): traces.append("X1") def test_two(self): traces.append("X2") def test_three(self): traces.append("X3") @cls_fix class TestY(TestCase): def test_one(self): traces.append("Y1") def test_two(self): traces.append("Y2") def test_three(self): traces.append("Y3") pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines([ "* X-a X1 X2 X3 X-z Y-a Y1 Y3 Y2 Y-z", ]) result.assert_outcomes(passed=6) def test_non_function_scoped_contextmanager_fixture(): @get_source def test_py(): from contextlib import contextmanager from unmagic import fixture traces = [] @contextmanager def context(): traces.append("setup") yield traces.append("teardown") print("", " ".join(traces)) @fixture(context, scope="class") class Tests: def test_one(self): traces.append("1") def test_two(self): traces.append("2") def test_three(self): traces.append("3") pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines(["* setup 1 2 3 teardown"]) result.assert_outcomes(passed=3) def test_module_scope(): @get_source def fix_py(): from unmagic import fixture, get_request @fixture(scope="session") def ss_tracer(): traces = [] yield traces print("", " ".join(traces)) @fixture(scope="module") def mod_fix(): name = get_request().module.__name__[-4:] traces = ss_tracer() traces.append(f"{name}-a") yield name traces.append(f"{name}-z") @get_source def mod1_py(): from fix import ss_tracer, mod_fix def test_one(): ss_tracer().append("x1") def test_two(): ss_tracer().append(f"{mod_fix()}-x2") def test_three(): ss_tracer().append("x3") @get_source def mod2_py(): from fix import ss_tracer, mod_fix def test_one(): ss_tracer().append("y1") def test_two(): ss_tracer().append(f"{mod_fix()}-y2") def test_three(): ss_tracer().append("y3") pytester = unmagic_tester() pytester.makepyfile(fix=fix_py, test_mod1=mod1_py, test_mod2=mod2_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines([ "* x1 mod1-a mod1-x2 x3 mod1-z y1 mod2-a mod2-y2 y3 mod2-z", ]) result.assert_outcomes(passed=6) def test_package_scope(): @get_source def fix_py(): from unmagic import fixture @fixture(scope="session") def ss_tracer(): traces = [] yield traces print("\n", " ".join(traces)) @get_source def init_py(): from unmagic import fixture, get_request from fix import ss_tracer @fixture(scope="package") def pkg_fix(): name = get_request().node.nodeid.replace("/", ".") traces = ss_tracer() traces.append(f"{name}-a") yield traces.append(f"{name}-z") @get_source def mod_py(): from unmagic import fixture from fix import ss_tracer from . import pkg_fix @fixture(scope="module") @pkg_fix def modname(): yield __name__.rsplit(".", 1)[-1].replace("test_mod", "m") def test_one(): ss_tracer().append(f"{modname()}.t1") def test_two(): ss_tracer().append(f"{modname()}.t2") pytester = unmagic_tester() (pytester.path / "pkg/sub").mkdir(parents=True) (pytester.path / "pkg/sub/__init__.py").write_text(init_py) (pytester.path / "pkg/sub/test_mod0.py").write_text(mod_py) (pytester.path / "pkg/__init__.py").write_text(init_py) (pytester.path / "pkg/test_mod1.py").write_text(mod_py) (pytester.path / "pkg/test_mod2.py").write_text(mod_py) (pytester.path / "pkg/up").mkdir() (pytester.path / "pkg/up/__init__.py").write_text(init_py) (pytester.path / "pkg/up/test_mod3.py").write_text(mod_py) (pytester.path / "fix.py").write_text(fix_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines([ " pkg.sub-a m0.t1 m0.t2 pkg.sub-z" " pkg-a m1.t1 m1.t2 m2.t1 m2.t2 pkg.up-a m3.t1 m3.t2 pkg.up-z pkg-z" ]) result.assert_outcomes(passed=8) def test_fixture_autouse(): @get_source def fix_py(): from unmagic import fixture, get_request @fixture(scope="session") def ss_tracer(): traces = [] yield traces print("", " ".join(traces)) @fixture(scope="module", autouse=True) def mod_fix(): name = get_request().module.__name__[-4:] traces = ss_tracer() traces.append(f"{name}-a") yield name traces.append(f"{name}-z") @get_source def mod_py(): from fix import ss_tracer def test_one(): ss_tracer().append("x1") def test_two(): ss_tracer().append("x2") def test_three(): ss_tracer().append("x3") pytester = unmagic_tester() pytester.makepyfile(fix=fix_py, test_mod1=mod_py, test_mod2=mod_py) result = pytester.runpytest("-s") result.stdout.fnmatch_lines([ "* mod1-a x1 x2 x3 mod1-z mod2-a x1 x2 x3 mod2-z", ]) result.assert_outcomes(passed=6) def test_setup_function(): @get_source def test_py(): from unmagic import fixture, get_request @fixture(scope="session") def ss_tracer(): traces = [] yield traces print("", " ".join(traces)) @fixture def fun_fix(): name = f"t{get_request().function.__name__[-1]}" traces = ss_tracer() traces.append(f"{name}-a") yield name traces.append(f"{name}-z") @fun_fix def setup_function(): ss_tracer().append("sf") def test_x0(): ss_tracer().append("x0") def test_x1(): ss_tracer().append("x1") def test_x2(): ss_tracer().append(f"x2-{fun_fix()}") pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("-sl", "--tb=long") result.stdout.fnmatch_lines([ "* t0-a sf x0 t0-z t1-a sf x1 t1-z t2-a sf x2-t2 t2-z", ]) result.assert_outcomes(passed=3) class TestFixturesOption: def test_basic(self): @get_source def test_py(): from unmagic import fixture @fixture def fix(): "A fixture" yield @fix def test(): pass pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("--fixtures") result.stdout.re_match_lines([ r"-* fixtures defined from test_basic -*", r"fix -- test_basic.py:4", r" +A fixture", ]) result.assert_outcomes() def test_fixture_uses_fixture(self): @get_source def test_py(): from unmagic import fixture @fixture def first(): "First fixture" yield @first # important: @use applied after @fixture @fixture def second(): "Second fixture" yield @second def test(): pass pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("--fixtures") result.stdout.re_match_lines([ r"-* fixtures defined from test_fixture_uses_fixture -*", r"second -- test_fixture_uses_fixture.py:9", r" +Second fixture", ]) result.assert_outcomes() def test_use_magic_fixture(self): @get_source def test_py(): from unmagic import use @use("capsys") def test(): pass pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("--fixtures") result.stdout.re_match_lines([ r"capsys -- \.{3}/_pytest/capture.py:\d+", r" +Enable text capturing .+", ]) result.assert_outcomes() def test_use_contextmanager(self): @get_source def test_py(): from contextlib import contextmanager from unmagic import use @contextmanager def plain_jane(): """Plain Jane""" yield @use(plain_jane) def test(): pass pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("--fixtures") result.stdout.re_match_lines([ r"-* fixtures defined from test_use_contextmanager -*", r"plain_jane -- test_use_contextmanager.py:\d+", r" +Plain Jane", ]) result.assert_outcomes() def test_use_patch(self): @get_source def test_py(): from unittest.mock import patch from unmagic import use @use(patch.object(use, "x", 2)) def test(): pass pytester = unmagic_tester() pytester.makepyfile(test_py) result = pytester.runpytest("--fixtures") result.stdout.re_match_lines([ r"-* fixtures defined from unittest.mock -*", r" -- .+/unittest/mock\.py:\d+", ]) result.assert_outcomes() class TestUnmagicFixtureId: @fixture def one(): yield @fixture def two(): yield def test_str(self): assert str(self.one._id) == "one" def test_repr(self): assert repr(self.one._id).startswith("