3434from typing import List
3535from typing import Optional
3636from typing import overload
37+
38+ try :
39+ from typing import Self
40+ except ImportError :
41+ from typing_extensions import Self
3742from typing import Tuple
3843from typing import Type
3944from typing import Union
4550import testinfra
4651from filelock import BaseFileLock
4752from filelock import FileLock
53+ from pytest import Mark
54+ from pytest import MarkDecorator
4855from pytest_container .helpers import get_always_pull_option
4956from pytest_container .helpers import get_extra_build_args
5057from pytest_container .helpers import get_extra_run_args
@@ -431,37 +438,46 @@ class EntrypointSelection(enum.Enum):
431438 IMAGE = enum .auto ()
432439
433440
434- @dataclass
435- class ContainerBase :
441+ class ContainerBase (ABC , _pytest .mark .ParameterSet ):
436442 """Base class for defining containers to be tested. Not to be used directly,
437443 instead use :py:class:`Container` or :py:class:`DerivedContainer`.
438444
439445 """
440446
447+ def __new__ (cls , * args , ** kwargs ):
448+ # Filter out all fields of ParameterSet and invoke object.__new__ only for the
449+ # fields that it supports
450+ parameter_set_fields = _pytest .mark .ParameterSet ._fields
451+ filtered_kwargs = {}
452+ for f in parameter_set_fields :
453+ filtered_kwargs [f ] = kwargs .get (f , None )
454+
455+ return super ().__new__ (cls , * args , ** filtered_kwargs )
456+
441457 #: Full url to this container via which it can be pulled
442458 #:
443459 #: If your container image is not available via a registry and only locally,
444460 #: then you can use the following syntax: ``containers-storage:$local_name``
445- url : str = ""
461+ # url: str = ""
446462
447463 #: id of the container if it is not available via a registry URL
448- container_id : str = ""
464+ # container_id: str = ""
449465
450466 #: Defines which entrypoint of the container is used.
451467 #: By default either :py:attr:`custom_entry_point` will be used (if defined)
452468 #: or the container's entrypoint or cmd. If neither of the two is set, then
453469 #: :file:`/bin/bash` will be used.
454- entry_point : EntrypointSelection = EntrypointSelection .AUTO
470+ # entry_point: EntrypointSelection = EntrypointSelection.AUTO
455471
456472 #: custom entry point for this container (i.e. neither its default, nor
457473 #: :file:`/bin/bash`)
458- custom_entry_point : Optional [str ] = None
474+ # custom_entry_point: Optional[str] = None
459475
460476 #: List of additional flags that will be inserted after
461477 #: `docker/podman run -d` and before the image name (i.e. these arguments
462478 #: are not passed to the entrypoint or ``CMD``). The list must be properly
463479 #: escaped, e.g. as created by ``shlex.split``.
464- extra_launch_args : List [str ] = field (default_factory = list )
480+ # extra_launch_args: List[str] = field(default_factory=list)
465481
466482 #: List of additional arguments that are passed to the ``CMD`` or
467483 #: entrypoint. These arguments are inserted after the :command:`docker/podman
@@ -471,44 +487,96 @@ class ContainerBase:
471487 #: The arguments must not cause the container to exit early. It must remain
472488 #: active in the background, otherwise this library will not function
473489 #: properly.
474- extra_entrypoint_args : List [str ] = field (default_factory = list )
490+ # extra_entrypoint_args: List[str] = field(default_factory=list)
475491
476492 #: Time for the container to become healthy (the timeout is ignored
477493 #: when the container image defines no ``HEALTHCHECK`` or when the timeout
478494 #: is below zero).
479495 #: When the value is ``None``, then the timeout will be inferred from the
480496 #: container image's ``HEALTHCHECK`` directive.
481- healthcheck_timeout : Optional [timedelta ] = None
497+ # healthcheck_timeout: Optional[timedelta] = None
482498
483499 #: additional environment variables that should be injected into the
484500 #: container
485- extra_environment_variables : Optional [Dict [str , str ]] = None
501+ # extra_environment_variables: Optional[Dict[str, str]] = None
486502
487503 #: Indicate whether there must never be more than one running container of
488504 #: this type at all times (e.g. because it opens a shared port).
489- singleton : bool = False
505+ # singleton: bool = False
490506
491507 #: forwarded ports of this container
492- forwarded_ports : List [PortForwarding ] = field (default_factory = list )
508+ # forwarded_ports: List[PortForwarding] = field(default_factory=list)
493509
494510 #: optional list of volumes that should be mounted in this container
495- volume_mounts : List [Union [ContainerVolume , BindMount ]] = field (
496- default_factory = list
497- )
511+ # volume_mounts: List[Union[ContainerVolume, BindMount]] = field(
512+ # default_factory=list
513+ # )
498514
499- _is_local : bool = False
515+ #: optional list of marks applied to this container image under test
516+ # _marks: Collection[Union[MarkDecorator, Mark]] = field(
517+ # default_factory=list
518+ # )
519+
520+ # _is_local: bool = False
521+
522+ def __init__ (
523+ self ,
524+ url : str = "" ,
525+ container_id : str = "" ,
526+ entry_point : EntrypointSelection = EntrypointSelection .AUTO ,
527+ custom_entry_point : Optional [str ] = None ,
528+ extra_launch_args : Optional [List [str ]] = None ,
529+ extra_entrypoint_args : Optional [List [str ]] = None ,
530+ healthcheck_timeout : Optional [timedelta ] = None ,
531+ extra_environment_variables : Optional [Dict [str , str ]] = None ,
532+ singleton : bool = False ,
533+ forwarded_ports : Optional [List [PortForwarding ]] = None ,
534+ volume_mounts : Optional [
535+ List [Union [ContainerVolume , BindMount ]]
536+ ] = None ,
537+ _marks : Optional [Collection [Union [MarkDecorator , Mark ]]] = None ,
538+ ) -> None :
539+ self .url = url
540+ self .container_id = container_id
541+ self .entry_point = entry_point
542+ self .custom_entry_point = custom_entry_point
543+ self .extra_launch_args = extra_launch_args or []
544+ self .extra_entrypoint_args = extra_entrypoint_args or []
545+ self .healthcheck_timeout = healthcheck_timeout
546+ self .extra_environment_variables = extra_environment_variables
547+ self .singleton = singleton
548+ self .forwarded_ports = forwarded_ports or []
549+ self .volume_mounts = volume_mounts or []
550+ self ._marks = _marks or []
500551
501- def __post_init__ (self ) -> None :
502552 local_prefix = "containers-storage:"
503553 if self .url .startswith (local_prefix ):
504554 self ._is_local = True
505555 # returns before_separator, separator, after_separator
506556 before , sep , self .url = self .url .partition (local_prefix )
507557 assert before == "" and sep == local_prefix
558+ else :
559+ self ._is_local = False
560+
561+ def __eq__ (self , value : object ) -> bool :
562+ if not isinstance (value , ContainerBase ):
563+ return False
564+
565+ if set (self .__dict__ .keys ()) != set (value .__dict__ .keys ()):
566+ return False
567+
568+ for k , v in self .__dict__ .items ():
569+ if v != value .__dict__ [k ]:
570+ return False
571+
572+ return True
508573
509574 def __str__ (self ) -> str :
510575 return self .url or self .container_id
511576
577+ def __bool__ (self ) -> bool :
578+ return True
579+
512580 @property
513581 def _build_tag (self ) -> str :
514582 """Internal build tag assigned to each immage, either the image url or
@@ -525,6 +593,18 @@ def local_image(self) -> bool:
525593 """
526594 return self ._is_local
527595
596+ @property
597+ def marks (self ) -> Collection [Union [MarkDecorator , Mark ]]:
598+ return self ._marks
599+
600+ @property
601+ def values (self ) -> Tuple [Self , ...]:
602+ return (self ,)
603+
604+ @property
605+ def id (self ) -> str :
606+ return str (self )
607+
528608 def get_launch_cmd (
529609 self ,
530610 container_runtime : OciRuntimeBase ,
@@ -630,13 +710,6 @@ def filelock_filename(self) -> str:
630710 # that is not available on old python versions that we still support
631711 return f"{ sha3_256 (('' .join (all_elements )).encode ()).hexdigest ()} .lock"
632712
633-
634- class ContainerBaseABC (ABC ):
635- """Abstract base class defining the methods that must be implemented by the
636- classes fed to the ``*container*`` fixtures.
637-
638- """
639-
640713 @abstractmethod
641714 def prepare_container (
642715 self ,
@@ -662,8 +735,7 @@ def baseurl(self) -> Optional[str]:
662735 """
663736
664737
665- @dataclass (unsafe_hash = True )
666- class Container (ContainerBase , ContainerBaseABC ):
738+ class Container (ContainerBase ):
667739 """This class stores information about the Container Image under test."""
668740
669741 def pull_container (self , container_runtime : OciRuntimeBase ) -> None :
@@ -702,19 +774,34 @@ def baseurl(self) -> Optional[str]:
702774
703775
704776@dataclass (unsafe_hash = True )
705- class DerivedContainer (ContainerBase , ContainerBaseABC ):
777+ class DerivedContainer (ContainerBase ):
706778 """Class for storing information about the Container Image under test, that
707779 is build from a :file:`Containerfile`/:file:`Dockerfile` from a different
708780 image (can be any image from a registry or an instance of
709781 :py:class:`Container` or :py:class:`DerivedContainer`).
710782
711783 """
712784
713- base : Union [Container , "DerivedContainer" , str ] = ""
785+ def __init__ (
786+ self ,
787+ base : Union [Container , "DerivedContainer" , str ],
788+ containerfile : str = "" ,
789+ image_format : Optional [ImageFormat ] = None ,
790+ add_build_tags : Optional [List [str ]] = None ,
791+ * args ,
792+ ** kwargs ,
793+ ) -> None :
794+ super ().__init__ (* args , ** kwargs )
795+ self .base = base
796+ self .containerfile = containerfile
797+ self .image_format = image_format
798+ self .add_build_tags = add_build_tags or []
799+
800+ # base: Union[Container, "DerivedContainer", str] = ""
714801
715802 #: The :file:`Containerfile` that is used to build this container derived
716803 #: from :py:attr:`base`.
717- containerfile : str = ""
804+ # containerfile: str = ""
718805
719806 #: An optional image format when building images with :command:`buildah`. It
720807 #: is ignored when the container runtime is :command:`docker`.
@@ -723,16 +810,28 @@ class DerivedContainer(ContainerBase, ContainerBaseABC):
723810 #: ``docker`` image format will be used instead.
724811 #: Specifying an image format disables the auto-detection and uses the
725812 #: supplied value.
726- image_format : Optional [ImageFormat ] = None
813+ # image_format: Optional[ImageFormat] = None
727814
728815 #: Additional build tags/names that should be added to the container once it
729816 #: has been built
730- add_build_tags : List [str ] = field (default_factory = list )
817+ # add_build_tags: List[str] = field(default_factory=list)
731818
732- def __post_init__ (self ) -> None :
733- super ().__post_init__ ()
734- if not self .base :
735- raise ValueError ("A base container must be provided" )
819+ @staticmethod
820+ def _get_recursive_marks (
821+ ctr : Union [Container , "DerivedContainer" , str ]
822+ ) -> Collection [Union [MarkDecorator , Mark ]]:
823+ if isinstance (ctr , str ):
824+ return []
825+ if isinstance (ctr , Container ):
826+ return ctr ._marks
827+
828+ return tuple (ctr ._marks ) + tuple (
829+ DerivedContainer ._get_recursive_marks (ctr .base )
830+ )
831+
832+ @property
833+ def marks (self ) -> Collection [Union [MarkDecorator , Mark ]]:
834+ return DerivedContainer ._get_recursive_marks (self )
736835
737836 @property
738837 def baseurl (self ) -> Optional [str ]:
0 commit comments