Merge pull request #1414 from ynput/enhancement/abstract-host-for-interfaces

Chore: Abstract host for interfaces
This commit is contained in:
Jakub Trllo 2025-08-19 13:37:11 +02:00 committed by GitHub
commit e4e8e7d520
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 165 additions and 74 deletions

View file

@ -1,6 +1,8 @@
from .constants import ContextChangeReason
from .abstract import AbstractHost
from .host import (
HostBase,
ContextChangeData,
)
from .interfaces import (
@ -18,7 +20,10 @@ from .dirmap import HostDirmap
__all__ = (
"ContextChangeReason",
"AbstractHost",
"HostBase",
"ContextChangeData",
"IWorkfileHost",
"WorkfileInfo",

View file

@ -0,0 +1,96 @@
from __future__ import annotations
import logging
from abc import ABC, abstractmethod
import typing
from typing import Optional, Any
from .constants import ContextChangeReason
if typing.TYPE_CHECKING:
from ayon_core.pipeline import Anatomy
from .typing import HostContextData
class AbstractHost(ABC):
"""Abstract definition of host implementation."""
@property
@abstractmethod
def log(self) -> logging.Logger:
pass
@property
@abstractmethod
def name(self) -> str:
"""Host name."""
pass
@abstractmethod
def get_current_context(self) -> HostContextData:
"""Get the current context of the host.
Current context is defined by project name, folder path and task name.
Returns:
HostContextData: The current context of the host.
"""
pass
@abstractmethod
def set_current_context(
self,
folder_entity: dict[str, Any],
task_entity: dict[str, Any],
*,
reason: ContextChangeReason = ContextChangeReason.undefined,
project_entity: Optional[dict[str, Any]] = None,
anatomy: Optional[Anatomy] = None,
) -> HostContextData:
"""Change context of the host.
Args:
folder_entity (dict[str, Any]): Folder entity.
task_entity (dict[str, Any]): Task entity.
reason (ContextChangeReason): Reason for change.
project_entity (dict[str, Any]): Project entity.
anatomy (Anatomy): Anatomy entity.
"""
pass
@abstractmethod
def get_current_project_name(self) -> str:
"""Get the current project name.
Returns:
Optional[str]: The current project name.
"""
pass
@abstractmethod
def get_current_folder_path(self) -> Optional[str]:
"""Get the current folder path.
Returns:
Optional[str]: The current folder path.
"""
pass
@abstractmethod
def get_current_task_name(self) -> Optional[str]:
"""Get the current task name.
Returns:
Optional[str]: The current task name.
"""
pass
@abstractmethod
def get_context_title(self) -> str:
"""Get the context title used in UIs."""
pass

View file

@ -3,26 +3,21 @@ from __future__ import annotations
import os
import logging
import contextlib
from abc import ABC, abstractmethod
from dataclasses import dataclass
import typing
from typing import Optional, Any
from dataclasses import dataclass
import ayon_api
from ayon_core.lib import emit_event
from .constants import ContextChangeReason
from .abstract import AbstractHost
if typing.TYPE_CHECKING:
from ayon_core.pipeline import Anatomy
from typing import TypedDict
class HostContextData(TypedDict):
project_name: str
folder_path: Optional[str]
task_name: Optional[str]
from .typing import HostContextData
@dataclass
@ -34,7 +29,7 @@ class ContextChangeData:
anatomy: Anatomy
class HostBase(ABC):
class HostBase(AbstractHost):
"""Base of host implementation class.
Host is pipeline implementation of DCC application. This class should help
@ -109,48 +104,41 @@ class HostBase(ABC):
It is called automatically when 'ayon_core.pipeline.install_host' is
triggered.
"""
"""
pass
@property
def log(self):
def log(self) -> logging.Logger:
if self._log is None:
self._log = logging.getLogger(self.__class__.__name__)
return self._log
@property
@abstractmethod
def name(self) -> str:
"""Host name."""
pass
def get_current_project_name(self):
def get_current_project_name(self) -> str:
"""
Returns:
Union[str, None]: Current project name.
"""
str: Current project name.
return os.environ.get("AYON_PROJECT_NAME")
"""
return os.environ["AYON_PROJECT_NAME"]
def get_current_folder_path(self) -> Optional[str]:
"""
Returns:
Union[str, None]: Current asset name.
"""
Optional[str]: Current asset name.
"""
return os.environ.get("AYON_FOLDER_PATH")
def get_current_task_name(self) -> Optional[str]:
"""
Returns:
Union[str, None]: Current task name.
"""
Optional[str]: Current task name.
"""
return os.environ.get("AYON_TASK_NAME")
def get_current_context(self) -> "HostContextData":
def get_current_context(self) -> HostContextData:
"""Get current context information.
This method should be used to get current context of host. Usage of
@ -159,10 +147,10 @@ class HostBase(ABC):
can't be caught properly.
Returns:
Dict[str, Union[str, None]]: Context with 3 keys 'project_name',
'folder_path' and 'task_name'. All of them can be 'None'.
"""
HostContextData: Current context with 'project_name',
'folder_path' and 'task_name'.
"""
return {
"project_name": self.get_current_project_name(),
"folder_path": self.get_current_folder_path(),
@ -177,7 +165,7 @@ class HostBase(ABC):
reason: ContextChangeReason = ContextChangeReason.undefined,
project_entity: Optional[dict[str, Any]] = None,
anatomy: Optional[Anatomy] = None,
) -> "HostContextData":
) -> HostContextData:
"""Set current context information.
This method should be used to set current context of host. Usage of
@ -290,7 +278,7 @@ class HostBase(ABC):
project_name: str,
folder_path: Optional[str],
task_name: Optional[str],
) -> "HostContextData":
) -> HostContextData:
"""Emit context change event.
Args:
@ -302,7 +290,7 @@ class HostBase(ABC):
HostContextData: Data send to context change event.
"""
data = {
data: HostContextData = {
"project_name": project_name,
"folder_path": folder_path,
"task_name": task_name,

View file

@ -1,9 +1,11 @@
from abc import abstractmethod
from ayon_core.host.abstract import AbstractHost
from .exceptions import MissingMethodsError
class ILoadHost:
class ILoadHost(AbstractHost):
"""Implementation requirements to be able use reference of representations.
The load plugins can do referencing even without implementation of methods
@ -24,7 +26,7 @@ class ILoadHost:
loading. Checks only existence of methods.
Args:
Union[ModuleType, HostBase]: Object of host where to look for
Union[ModuleType, AbstractHost]: Object of host where to look for
required methods.
Returns:
@ -46,7 +48,7 @@ class ILoadHost:
"""Validate implemented methods of "old type" host for load workflow.
Args:
Union[ModuleType, HostBase]: Object of host to validate.
Union[ModuleType, AbstractHost]: Object of host to validate.
Raises:
MissingMethodsError: If there are missing methods on host
@ -83,7 +85,7 @@ class ILoadHost:
return self.get_containers()
class IPublishHost:
class IPublishHost(AbstractHost):
"""Functions related to new creation system in new publisher.
New publisher is not storing information only about each created instance
@ -99,7 +101,7 @@ class IPublishHost:
new publish creation. Checks only existence of methods.
Args:
Union[ModuleType, HostBase]: Host module where to look for
Union[ModuleType, AbstractHost]: Host module where to look for
required methods.
Returns:
@ -127,7 +129,7 @@ class IPublishHost:
"""Validate implemented methods of "old type" host.
Args:
Union[ModuleType, HostBase]: Host module to validate.
Union[ModuleType, AbstractHost]: Host module to validate.
Raises:
MissingMethodsError: If there are missing methods on host

View file

@ -15,6 +15,7 @@ import arrow
from ayon_core.lib import emit_event
from ayon_core.settings import get_project_settings
from ayon_core.host.abstract import AbstractHost
from ayon_core.host.constants import ContextChangeReason
if typing.TYPE_CHECKING:
@ -821,7 +822,7 @@ class PublishedWorkfileInfo:
return PublishedWorkfileInfo(**data)
class IWorkfileHost:
class IWorkfileHost(AbstractHost):
"""Implementation requirements to be able to use workfiles utils and tool.
Some of the methods are pre-implemented as they generally do the same in

View file

@ -0,0 +1,7 @@
from typing import Optional, TypedDict
class HostContextData(TypedDict):
project_name: str
folder_path: Optional[str]
task_name: Optional[str]

View file

@ -13,7 +13,7 @@ import pyblish.api
from pyblish.lib import MessageHandler
from ayon_core import AYON_CORE_ROOT
from ayon_core.host import HostBase
from ayon_core.host import AbstractHost
from ayon_core.lib import (
is_in_tests,
initialize_ayon_connection,
@ -100,16 +100,16 @@ def registered_root():
return _registered_root["_"]
def install_host(host: HostBase) -> None:
def install_host(host: AbstractHost) -> None:
"""Install `host` into the running Python session.
Args:
host (HostBase): A host interface object.
host (AbstractHost): A host interface object.
"""
if not isinstance(host, HostBase):
if not isinstance(host, AbstractHost):
log.error(
f"Host must be a subclass of 'HostBase', got '{type(host)}'."
f"Host must be a subclass of 'AbstractHost', got '{type(host)}'."
)
global _is_installed
@ -310,7 +310,7 @@ def get_current_host_name():
"""
host = registered_host()
if isinstance(host, HostBase):
if isinstance(host, AbstractHost):
return host.name
return os.environ.get("AYON_HOST_NAME")
@ -346,28 +346,28 @@ def get_global_context():
def get_current_context():
host = registered_host()
if isinstance(host, HostBase):
if isinstance(host, AbstractHost):
return host.get_current_context()
return get_global_context()
def get_current_project_name():
host = registered_host()
if isinstance(host, HostBase):
if isinstance(host, AbstractHost):
return host.get_current_project_name()
return get_global_context()["project_name"]
def get_current_folder_path():
host = registered_host()
if isinstance(host, HostBase):
if isinstance(host, AbstractHost):
return host.get_current_folder_path()
return get_global_context()["folder_path"]
def get_current_task_name():
host = registered_host()
if isinstance(host, HostBase):
if isinstance(host, AbstractHost):
return host.get_current_task_name()
return get_global_context()["task_name"]

View file

@ -54,15 +54,12 @@ from .creator_plugins import (
discover_convertor_plugins,
)
if typing.TYPE_CHECKING:
from ayon_core.host import HostBase
from ayon_core.lib import AbstractAttrDef
from ayon_core.lib.events import EventCallback, Event
from .structures import CreatedInstance
from .creator_plugins import BaseCreator
class PublishHost(HostBase, IPublishHost):
pass
# Import of functions and classes that were moved to different file
# TODO Should be removed in future release - Added 24/08/28, 0.4.3-dev.1
@ -169,7 +166,7 @@ class CreateContext:
context which should be handled by host.
Args:
host (PublishHost): Host implementation which handles implementation
host (IPublishHost): Host implementation which handles implementation
and global metadata.
headless (bool): Context is created out of UI (Current not used).
reset (bool): Reset context on initialization.
@ -179,7 +176,7 @@ class CreateContext:
def __init__(
self,
host: "PublishHost",
host: IPublishHost,
headless: bool = False,
reset: bool = True,
discover_publish_plugins: bool = True,

View file

@ -30,7 +30,7 @@ from ayon_api import (
)
from ayon_core.settings import get_project_settings
from ayon_core.host import IWorkfileHost, HostBase
from ayon_core.host import IWorkfileHost, AbstractHost
from ayon_core.lib import (
Logger,
StringTemplate,
@ -126,14 +126,14 @@ class AbstractTemplateBuilder(ABC):
placeholder population.
Args:
host (Union[HostBase, ModuleType]): Implementation of host.
host (Union[AbstractHost, ModuleType]): Implementation of host.
"""
_log = None
def __init__(self, host):
# Get host name
if isinstance(host, HostBase):
if isinstance(host, AbstractHost):
host_name = host.name
else:
host_name = os.environ.get("AYON_HOST_NAME")
@ -161,24 +161,24 @@ class AbstractTemplateBuilder(ABC):
@property
def project_name(self):
if isinstance(self._host, HostBase):
if isinstance(self._host, AbstractHost):
return self._host.get_current_project_name()
return os.getenv("AYON_PROJECT_NAME")
@property
def current_folder_path(self):
if isinstance(self._host, HostBase):
if isinstance(self._host, AbstractHost):
return self._host.get_current_folder_path()
return os.getenv("AYON_FOLDER_PATH")
@property
def current_task_name(self):
if isinstance(self._host, HostBase):
if isinstance(self._host, AbstractHost):
return self._host.get_current_task_name()
return os.getenv("AYON_TASK_NAME")
def get_current_context(self):
if isinstance(self._host, HostBase):
if isinstance(self._host, AbstractHost):
return self._host.get_current_context()
return {
"project_name": self.project_name,
@ -254,7 +254,7 @@ class AbstractTemplateBuilder(ABC):
"""Access to host implementation.
Returns:
Union[HostBase, ModuleType]: Implementation of host.
Union[AbstractHost, ModuleType]: Implementation of host.
"""
return self._host

View file

@ -13,7 +13,7 @@ from typing import (
)
from ayon_core.lib import AbstractAttrDef
from ayon_core.host import HostBase
from ayon_core.host import AbstractHost
from ayon_core.pipeline.create import (
CreateContext,
ConvertorItem,
@ -176,7 +176,7 @@ class AbstractPublisherBackend(AbstractPublisherCommon):
pass
@abstractmethod
def get_host(self) -> HostBase:
def get_host(self) -> AbstractHost:
pass
@abstractmethod

View file

@ -1,7 +1,7 @@
import ayon_api
from ayon_core.lib.events import QueuedEventSystem
from ayon_core.host import HostBase
from ayon_core.host import ILoadHost
from ayon_core.pipeline import (
registered_host,
get_current_context,
@ -35,7 +35,7 @@ class SceneInventoryController:
self._projects_model = ProjectsModel(self)
self._event_system = self._create_event_system()
def get_host(self) -> HostBase:
def get_host(self) -> ILoadHost:
return self._host
def emit_event(self, topic, data=None, source=None):

View file

@ -14,7 +14,6 @@ from ayon_core.lib import (
Logger,
)
from ayon_core.host import (
HostBase,
IWorkfileHost,
WorkfileInfo,
PublishedWorkfileInfo,
@ -49,19 +48,15 @@ if typing.TYPE_CHECKING:
_NOT_SET = object()
class HostType(HostBase, IWorkfileHost):
pass
class WorkfilesModel:
"""Workfiles model."""
def __init__(
self,
host: HostType,
host: IWorkfileHost,
controller: AbstractWorkfilesBackend
):
self._host: HostType = host
self._host: IWorkfileHost = host
self._controller: AbstractWorkfilesBackend = controller
self._log = Logger.get_logger("WorkfilesModel")