Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance plugin system with dependency management #118

Merged
merged 14 commits into from
Jan 7, 2025
Prev Previous commit
Next Next commit
add docstrings
  • Loading branch information
tsv1 committed Jan 6, 2025
commit f9fbad688be99a243438b3eca341f9299384f3ac
29 changes: 28 additions & 1 deletion vedro/_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,31 @@


class Config(core.Config):
"""
Defines the main configuration for the Vedro testing framework.

This class contains settings for the framework's behavior, such as enabling
plugins, defining factories for core components, and specifying filters
for scenario discovery.
"""

# Validate each plugin's configuration, checking for unknown attributes to prevent errors
validate_plugins_configs: bool = True
"""
Whether to validate plugin configurations.

If set to `True`, the framework will validate plugin configurations to
ensure that no unknown attributes are defined, reducing the likelihood
of errors.
"""

class Registry(core.Config.Registry):
"""
Defines factories and singleton instances for core components.

The `Registry` class is responsible for configuring key components,
such as the scenario finder, loader, scheduler, and runner.
"""

Dispatcher = Singleton[Dispatcher](Dispatcher)

ModuleLoader = Factory[ModuleLoader](ModuleFileLoader)
Expand Down Expand Up @@ -87,6 +107,13 @@ class Registry(core.Config.Registry):
))

class Plugins(core.Config.Plugins):
"""
Configuration for enabling and disabling plugins.

This class contains nested classes for each plugin, where the `enabled`
attribute determines whether the plugin is active.
"""

class Director(director.Director):
enabled = True

Expand Down
39 changes: 24 additions & 15 deletions vedro/core/_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,29 @@ class Artifact(ABC):
"""
The base class for representing artifacts in a system.

An artifact in this context is a piece of data generated during the execution
of a scenario or a step. It can be anything from log files, screenshots, to data dumps.
This class serves as an abstract base class for different types of artifacts,
such as MemoryArtifact and FileArtifact.
An artifact is a piece of data generated during the execution of a scenario or a step.
It can be anything such as log files, screenshots, or data dumps. This class serves as
an abstract base class for other artifact types, such as `MemoryArtifact` and`FileArtifact`.
"""
pass


class MemoryArtifact(Artifact):
"""
Represents an artifact that is stored in memory.

This class is used for artifacts whose data resides entirely in memory, such as
temporary data buffers, small data blobs, or in-memory-generated files.
"""

def __init__(self, name: str, mime_type: str, data: bytes) -> None:
def __init__(self, name: str, mime_type: str, data: bytes) -> None:
"""
Initialize a MemoryArtifact with a name, MIME type, and data.

:param name: The name of the artifact.
:param mime_type: The MIME type of the data.
:param data: The actual data in bytes.
:param mime_type: The MIME type of the data (e.g., "text/plain", "application/json").
:param data: The actual data of the artifact as a byte sequence.
:raises TypeError: If `data` is not of type `bytes`.
"""
if not isinstance(data, bytes):
raise TypeError("'data' must be of type bytes")
Expand All @@ -41,7 +44,7 @@ def name(self) -> str:
"""
Get the name of the artifact.

:return: The name of the artifact.
:return: The name of the artifact as a string.
"""
return self._name

Expand All @@ -59,15 +62,16 @@ def data(self) -> bytes:
"""
Get the data stored in the artifact.

:return: The data as bytes.
:return: The data as a byte sequence.
"""
return self._data

def __repr__(self) -> str:
"""
Represent the MemoryArtifact as a string.

:return: A string representation of the MemoryArtifact.
:return: A string representation of the MemoryArtifact, including its name,
MIME type, and size of the data.
"""
size = len(self._data)
return f"{self.__class__.__name__}<{self._name!r}, {self._mime_type!r}, size={size}>"
Expand All @@ -85,15 +89,19 @@ def __eq__(self, other: Any) -> bool:
class FileArtifact(Artifact):
"""
Represents an artifact that is stored as a file on the filesystem.

This class is used for artifacts whose data is written to or read from disk,
such as large logs, reports, or exported data files.
"""

def __init__(self, name: str, mime_type: str, path: Path) -> None:
"""
Initialize a FileArtifact with a name, MIME type, and file path.

:param name: The name of the artifact.
:param mime_type: The MIME type of the file.
:param path: The path to the file.
:param mime_type: The MIME type of the file (e.g., "image/png", "text/plain").
:param path: The path to the file containing the artifact data.
:raises TypeError: If `path` is not an instance of `pathlib.Path`.
"""
if not isinstance(path, Path):
raise TypeError("'path' must be an instance of pathlib.Path")
Expand All @@ -106,7 +114,7 @@ def name(self) -> str:
"""
Get the name of the artifact.

:return: The name of the artifact.
:return: The name of the artifact as a string.
"""
return self._name

Expand All @@ -124,15 +132,16 @@ def path(self) -> Path:
"""
Get the file path of the artifact.

:return: The path of the file as a Path object.
:return: The path of the file as a `Path` object.
"""
return self._path

def __repr__(self) -> str:
"""
Represent the FileArtifact as a string.

:return: A string representation of the FileArtifact.
:return: A string representation of the FileArtifact, including its name,
MIME type, and file path.
"""
return f"{self.__class__.__name__}<{self._name!r}, {self._mime_type!r}, {self._path!r}>"

Expand Down
107 changes: 107 additions & 0 deletions vedro/core/_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,37 +12,108 @@


class ConflictError(Exception):
"""
Raised when there is a conflict during the registration of a resolver.

This exception is raised if a new resolver is attempted to be registered
when one is already registered by another plugin.
"""
pass


class Container(Generic[T], ABC):
"""
Base class for dependency injection containers.

A container is responsible for managing the creation and resolution of
objects of a specific type. Subclasses define specific behaviors for
managing resolvers and handling object creation (e.g., singleton or factory).

:param resolver: The initial resolver function or type for creating objects.
"""

def __init__(self, resolver: FactoryType[T]) -> None:
"""
Initialize the container with the given resolver.

:param resolver: A callable or type used to create objects of type `T`.
"""
self._resolver = resolver
self._initial = resolver
self._registrant: Union[Plugin, None] = None

def _make_conflict_error(self, registrant: Plugin) -> ConflictError:
"""
Create a conflict error indicating a registration conflict.

:param registrant: The plugin attempting to register the resolver.
:return: A `ConflictError` with details about the conflicting registration.
"""
type_ = self.__orig_class__.__args__[0] # type: ignore
return ConflictError(f"{registrant} is trying to register {type_.__name__}, "
f"but it is already registered by {self._registrant!r}")

@abstractmethod
def register(self, resolver: FactoryType[T], registrant: Plugin) -> None:
"""
Register a new resolver with the container.

:param resolver: A callable or type used to create objects of type `T`.
:param registrant: The plugin attempting to register the resolver.
:raises ConflictError: If another resolver is already registered.
"""
pass

@abstractmethod
def resolve(self, *args: Any, **kwargs: Any) -> T:
"""
Resolve and return an object of type `T`.

Subclasses should define how the object is created or retrieved.

:param args: Positional arguments to pass to the resolver.
:param kwargs: Keyword arguments to pass to the resolver.
:return: An instance of type `T`.
"""
pass

def __call__(self, *args: Any, **kwargs: Any) -> T:
"""
Call the container to resolve an object.

This is a shorthand for calling the `resolve` method.

:param args: Positional arguments to pass to the resolver.
:param kwargs: Keyword arguments to pass to the resolver.
:return: An instance of type `T`.
"""
return self.resolve(*args, **kwargs)

def __repr__(self) -> str:
"""
Return a string representation of the container.

:return: A string describing the container and its resolver.
"""
return f"{self.__class__.__name__}({self._resolver!r})"


class Factory(Container[T]):
"""
A container that creates a new instance of an object each time it is resolved.

The `Factory` class manages resolvers that are responsible for creating new
objects of type `T` upon each resolution.
"""

def register(self, resolver: FactoryType[T], registrant: Plugin) -> None:
"""
Register a new resolver with the factory.

:param resolver: A callable or type used to create objects of type `T`.
:param registrant: The plugin attempting to register the resolver.
:raises ConflictError: If another resolver is already registered.
"""
assert isinstance(registrant, Plugin)

if self._registrant is not None:
Expand All @@ -52,15 +123,41 @@ def register(self, resolver: FactoryType[T], registrant: Plugin) -> None:
self._registrant = registrant

def resolve(self, *args: Any, **kwargs: Any) -> T:
"""
Create and return a new instance of type `T`.

:param args: Positional arguments to pass to the resolver.
:param kwargs: Keyword arguments to pass to the resolver.
:return: A new instance of type `T`.
"""
return self._resolver(*args, **kwargs)


class Singleton(Container[T]):
"""
A container that ensures a single instance of an object is created and reused.

The `Singleton` class manages resolvers that are responsible for creating
objects of type `T`, ensuring that the same instance is returned on each resolution.
"""

def __init__(self, resolver: FactoryType[T]) -> None:
"""
Initialize the singleton container with the given resolver.

:param resolver: A callable or type used to create objects of type `T`.
"""
super().__init__(resolver)
self._singleton: Union[None, T] = None

def register(self, resolver: FactoryType[T], registrant: Plugin) -> None:
"""
Register a new resolver with the singleton container.

:param resolver: A callable or type used to create objects of type `T`.
:param registrant: The plugin attempting to register the resolver.
:raises ConflictError: If another resolver is already registered.
"""
assert isinstance(registrant, Plugin)

if self._registrant is not None:
Expand All @@ -70,6 +167,16 @@ def register(self, resolver: FactoryType[T], registrant: Plugin) -> None:
self._registrant = registrant

def resolve(self, *args: Any, **kwargs: Any) -> T:
"""
Resolve and return the singleton instance of type `T`.

If the singleton instance has not been created yet, it will be created
using the resolver. Subsequent calls will return the same instance.

:param args: Positional arguments to pass to the resolver.
:param kwargs: Keyword arguments to pass to the resolver.
:return: The singleton instance of type `T`.
"""
if self._singleton is None:
self._singleton = self._resolver(*args, **kwargs)
return self._singleton
13 changes: 7 additions & 6 deletions vedro/core/_exc_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@

class ExcInfo:
"""
Represent exception information.
Represents exception information.

This class encapsulates the details of an exception, including its type, the exception
instance itself, and the traceback associated with the exception. It provides a structured
way to store and access exception information.
way to store and access exception details.
"""

def __init__(self,
Expand All @@ -20,9 +20,10 @@ def __init__(self,
"""
Initialize an instance of ExcInfo with exception details.

:param type_: The type of the exception.
:param value: The exception instance.
:param traceback: The traceback object associated with the exception.
:param type_: The type of the exception (e.g., `ValueError`, `TypeError`).
:param value: The exception instance (i.e., the exception object raised).
:param traceback: The traceback object associated with the exception, representing
the call stack at the point where the exception occurred.
"""
self.type = type_
self.value = value
Expand All @@ -32,6 +33,6 @@ def __repr__(self) -> str:
"""
Return a string representation of the ExcInfo instance.

:return: A string representation of the ExcInfo instance.
:return: A string containing the exception type, value, and traceback.
"""
return f"{self.__class__.__name__}({self.type!r}, {self.value!r}, {self.traceback!r})"
Loading