Skip to content

Commit 5c3f7e4

Browse files
committed
Make Container, DerivedContainer & Pod inherit from ParameterSet
1 parent a872ff9 commit 5c3f7e4

File tree

5 files changed

+231
-10
lines changed

5 files changed

+231
-10
lines changed

pyproject.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,12 @@ strict = true
6565
[[tool.mypy.overrides]]
6666
module = "testinfra,deprecation"
6767
ignore_missing_imports = true
68+
69+
[tool.pytest.ini_options]
70+
xfail_strict = true
71+
addopts = "--strict-markers"
72+
markers = [
73+
'secretleapmark',
74+
'othersecretmark',
75+
'secretpodmark',
76+
]

pytest_container/container.py

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import time
1616
import warnings
1717
from abc import ABC
18+
from abc import ABCMeta
1819
from abc import abstractmethod
1920
from dataclasses import dataclass
2021
from dataclasses import field
@@ -34,6 +35,11 @@
3435
from typing import List
3536
from typing import Optional
3637
from typing import overload
38+
39+
try:
40+
from typing import Self
41+
except ImportError:
42+
from typing_extensions import Self
3743
from typing import Tuple
3844
from typing import Type
3945
from typing import Union
@@ -44,6 +50,8 @@
4450
import testinfra
4551
from filelock import BaseFileLock
4652
from filelock import FileLock
53+
from pytest import Mark
54+
from pytest import MarkDecorator
4755
from pytest import param
4856
from pytest_container.helpers import get_always_pull_option
4957
from pytest_container.inspect import ContainerHealth
@@ -493,6 +501,11 @@ class ContainerBase:
493501
default_factory=list
494502
)
495503

504+
#: optional list of marks applied to this container image under test
505+
_marks: Collection[Union[MarkDecorator, Mark]] = field(
506+
default_factory=list
507+
)
508+
496509
_is_local: bool = False
497510

498511
def __post_init__(self) -> None:
@@ -503,6 +516,9 @@ def __post_init__(self) -> None:
503516
def __str__(self) -> str:
504517
return self.url or self.container_id
505518

519+
def __bool__(self) -> bool:
520+
return True
521+
506522
@property
507523
def _build_tag(self) -> str:
508524
"""Internal build tag assigned to each immage, either the image url or
@@ -519,6 +535,18 @@ def local_image(self) -> bool:
519535
"""
520536
return self._is_local
521537

538+
@property
539+
def marks(self) -> Collection[Union[MarkDecorator, Mark]]:
540+
return self._marks
541+
542+
@property
543+
def values(self) -> Tuple[Self, ...]:
544+
return (self,)
545+
546+
@property
547+
def id(self) -> str:
548+
return str(self)
549+
522550
def get_launch_cmd(
523551
self,
524552
container_runtime: OciRuntimeBase,
@@ -656,8 +684,25 @@ def baseurl(self) -> Optional[str]:
656684
"""
657685

658686

687+
class _HackMROMeta(ABCMeta):
688+
def mro(cls):
689+
return (
690+
cls,
691+
ContainerBase,
692+
ContainerBaseABC,
693+
tuple,
694+
_pytest.mark.ParameterSet,
695+
object,
696+
)
697+
698+
659699
@dataclass(unsafe_hash=True)
660-
class Container(ContainerBase, ContainerBaseABC):
700+
class Container(
701+
ContainerBase,
702+
ContainerBaseABC,
703+
_pytest.mark.ParameterSet,
704+
metaclass=_HackMROMeta,
705+
):
661706
"""This class stores information about the Container Image under test."""
662707

663708
def pull_container(self, container_runtime: OciRuntimeBase) -> None:
@@ -696,7 +741,12 @@ def baseurl(self) -> Optional[str]:
696741

697742

698743
@dataclass(unsafe_hash=True)
699-
class DerivedContainer(ContainerBase, ContainerBaseABC):
744+
class DerivedContainer(
745+
ContainerBase,
746+
ContainerBaseABC,
747+
_pytest.mark.ParameterSet,
748+
metaclass=_HackMROMeta,
749+
):
700750
"""Class for storing information about the Container Image under test, that
701751
is build from a :file:`Containerfile`/:file:`Dockerfile` from a different
702752
image (can be any image from a registry or an instance of
@@ -723,6 +773,23 @@ class DerivedContainer(ContainerBase, ContainerBaseABC):
723773
#: has been built
724774
add_build_tags: List[str] = field(default_factory=list)
725775

776+
@staticmethod
777+
def _get_recursive_marks(
778+
ctr: Union[Container, "DerivedContainer", str]
779+
) -> Collection[Union[MarkDecorator, Mark]]:
780+
if isinstance(ctr, str):
781+
return []
782+
if isinstance(ctr, Container):
783+
return ctr._marks
784+
785+
return tuple(ctr._marks) + tuple(
786+
DerivedContainer._get_recursive_marks(ctr.base)
787+
)
788+
789+
@property
790+
def marks(self) -> Collection[Union[MarkDecorator, Mark]]:
791+
return DerivedContainer._get_recursive_marks(self)
792+
726793
def __post_init__(self) -> None:
727794
super().__post_init__()
728795
if not self.base:

pytest_container/plugin.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,13 @@
77
from subprocess import run
88
from typing import Callable
99
from typing import Generator
10+
from typing import Union
1011

12+
from pytest_container.container import Container
1113
from pytest_container.container import container_and_marks_from_pytest_param
1214
from pytest_container.container import ContainerData
1315
from pytest_container.container import ContainerLauncher
16+
from pytest_container.container import DerivedContainer
1417
from pytest_container.helpers import get_extra_build_args
1518
from pytest_container.helpers import get_extra_pod_create_args
1619
from pytest_container.helpers import get_extra_run_args
@@ -77,13 +80,12 @@ def fixture_funct(
7780
pytest_generate_tests.
7881
"""
7982

80-
try:
81-
container, _ = container_and_marks_from_pytest_param(request.param)
82-
except AttributeError as attr_err:
83-
raise RuntimeError(
84-
"This fixture was not parametrized correctly, "
85-
"did you forget to call `auto_container_parametrize` in `pytest_generate_tests`?"
86-
) from attr_err
83+
container: Union[DerivedContainer, Container] = (
84+
request.param
85+
if isinstance(request.param, (DerivedContainer, Container))
86+
else request.param[0]
87+
)
88+
assert isinstance(container, (DerivedContainer, Container))
8789
_logger.debug("Requesting the container %s", str(container))
8890

8991
if scope == "session" and container.singleton:

pytest_container/pod.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
"""Module for managing podman pods."""
22
import contextlib
33
import json
4+
from abc import ABCMeta
45
from dataclasses import dataclass
56
from dataclasses import field
67
from pathlib import Path
78
from subprocess import check_output
89
from types import TracebackType
10+
from typing import Collection
911
from typing import List
1012
from typing import Optional
13+
14+
try:
15+
from typing import Self
16+
except ImportError:
17+
from typing_extensions import Self
18+
from typing import Tuple
1119
from typing import Type
1220
from typing import Union
1321

1422
from _pytest.mark import ParameterSet
23+
from pytest import Mark
24+
from pytest import MarkDecorator
1525
from pytest_container.container import Container
1626
from pytest_container.container import ContainerData
1727
from pytest_container.container import ContainerLauncher
@@ -24,8 +34,18 @@
2434
from pytest_container.runtime import PodmanRuntime
2535

2636

37+
class _HackMROMeta(ABCMeta):
38+
def mro(cls):
39+
return (
40+
cls,
41+
tuple,
42+
ParameterSet,
43+
object,
44+
)
45+
46+
2747
@dataclass
28-
class Pod:
48+
class Pod(ParameterSet, metaclass=_HackMROMeta):
2949
"""A pod is a collection of containers that share the same network and port
3050
forwards. Currently only :command:`podman` supports creating pods.
3151
@@ -40,6 +60,30 @@ class Pod:
4060
#: ports exposed by the pod
4161
forwarded_ports: List[PortForwarding] = field(default_factory=list)
4262

63+
_marks: Collection[Union[MarkDecorator, Mark]] = field(
64+
default_factory=list
65+
)
66+
67+
@property
68+
def values(self) -> Tuple[Self]:
69+
return (self,)
70+
71+
@property
72+
def marks(self) -> Collection[Union[MarkDecorator, Mark]]:
73+
marks = tuple(self._marks)
74+
for ctr in self.containers:
75+
marks += tuple(ctr.marks)
76+
return marks
77+
78+
@property
79+
def id(self) -> str:
80+
return "Pod with containers: " + ",".join(
81+
str(c) for c in self.containers
82+
)
83+
84+
def __bool__(self) -> bool:
85+
return True
86+
4387

4488
@dataclass(frozen=True)
4589
class PodData:

tests/test_marks.py

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import pytest
2+
from _pytest.mark import ParameterSet
3+
from pytest_container.container import Container
4+
from pytest_container.container import ContainerBase
5+
from pytest_container.container import DerivedContainer
6+
from pytest_container.pod import Pod
7+
8+
from tests.images import LEAP_URL
9+
10+
LEAP_WITH_MARK = Container(url=LEAP_URL, _marks=[pytest.mark.secretleapmark])
11+
12+
DERIVED_ON_LEAP_WITH_MARK = DerivedContainer(base=LEAP_WITH_MARK)
13+
14+
SECOND_DERIVED_ON_LEAP = DerivedContainer(
15+
base=DERIVED_ON_LEAP_WITH_MARK, _marks=[pytest.mark.othersecretmark]
16+
)
17+
18+
INDEPENDENT_OTHER_LEAP = Container(
19+
url=LEAP_URL, _marks=[pytest.mark.othersecretmark]
20+
)
21+
22+
UNMARKED_POD = Pod(containers=[LEAP_WITH_MARK, INDEPENDENT_OTHER_LEAP])
23+
24+
MARKED_POD = Pod(
25+
containers=[LEAP_WITH_MARK, INDEPENDENT_OTHER_LEAP],
26+
_marks=[pytest.mark.secretpodmark],
27+
)
28+
29+
30+
def test_marks() -> None:
31+
assert list(LEAP_WITH_MARK.marks) == [pytest.mark.secretleapmark]
32+
assert list(DERIVED_ON_LEAP_WITH_MARK.marks) == [
33+
pytest.mark.secretleapmark
34+
]
35+
assert list(SECOND_DERIVED_ON_LEAP.marks) == [
36+
pytest.mark.othersecretmark,
37+
pytest.mark.secretleapmark,
38+
]
39+
assert not DerivedContainer(
40+
base=LEAP_URL, containerfile="ENV HOME=/root"
41+
).marks
42+
43+
pod_marks = UNMARKED_POD.marks
44+
assert (
45+
len(pod_marks) == 2
46+
and pytest.mark.othersecretmark in pod_marks
47+
and pytest.mark.secretleapmark in pod_marks
48+
)
49+
50+
pod_marks = MARKED_POD.marks
51+
assert (
52+
len(pod_marks) == 3
53+
and pytest.mark.othersecretmark in pod_marks
54+
and pytest.mark.secretleapmark in pod_marks
55+
and pytest.mark.secretpodmark in pod_marks
56+
)
57+
58+
59+
@pytest.mark.parametrize(
60+
"ctr",
61+
[
62+
LEAP_WITH_MARK,
63+
DERIVED_ON_LEAP_WITH_MARK,
64+
SECOND_DERIVED_ON_LEAP,
65+
INDEPENDENT_OTHER_LEAP,
66+
],
67+
)
68+
def test_container_is_pytest_param(ctr) -> None:
69+
70+
assert isinstance(ctr, ParameterSet)
71+
assert isinstance(ctr, (Container, DerivedContainer))
72+
73+
74+
@pytest.mark.parametrize(
75+
"ctr",
76+
[
77+
LEAP_WITH_MARK,
78+
DERIVED_ON_LEAP_WITH_MARK,
79+
SECOND_DERIVED_ON_LEAP,
80+
INDEPENDENT_OTHER_LEAP,
81+
],
82+
)
83+
def test_container_is_truthy(ctr: ContainerBase) -> None:
84+
"""Regression test that we don't accidentally inherit __bool__ from tuple
85+
and the container is False by default.
86+
87+
"""
88+
assert ctr
89+
90+
91+
@pytest.mark.parametrize("pd", [MARKED_POD, UNMARKED_POD])
92+
def test_pod_is_pytest_param(pd: Pod) -> None:
93+
assert isinstance(pd, ParameterSet)
94+
assert isinstance(pd, Pod)
95+
96+
97+
@pytest.mark.parametrize("pd", [MARKED_POD, UNMARKED_POD])
98+
def test_pod_is_truthy(pd: Pod) -> None:
99+
assert pd

0 commit comments

Comments
 (0)