feat: refactor assemblicon (2/3) context job topology
This commit is contained in:
@@ -35,3 +35,6 @@ class Address:
|
|||||||
self.__domain == other.__domain
|
self.__domain == other.__domain
|
||||||
and self.__subdirectories == other.__subdirectories
|
and self.__subdirectories == other.__subdirectories
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def __repr__(self) -> str:
|
||||||
|
return f"Address({self.__domain.name}, {'/'.join(self.__subdirectories)})"
|
||||||
|
|||||||
@@ -1,27 +1,37 @@
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Callable, Iterable, TypeVar, Generic
|
from typing import Iterable, TypeVar, Generic, Protocol, Callable, cast
|
||||||
from .environment import Environment, Prologue
|
from .environment import Environment, Prologue
|
||||||
|
from .address import Address
|
||||||
|
from .source import Source
|
||||||
|
from .sink import Sink
|
||||||
from ..logger import HIGHLEVEL_LOGGER as LOGGER
|
from ..logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|
||||||
P = TypeVar("P", bound=Prologue)
|
P = TypeVar("P", bound=Prologue)
|
||||||
|
|
||||||
|
|
||||||
|
class Executor[*Ts](Protocol):
|
||||||
|
def __call__(self, *args: *Ts) -> None: ...
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class Job(Generic[P]):
|
class Job:
|
||||||
"""The class represents a job."""
|
"""
|
||||||
|
The class represents a type-erased job.
|
||||||
|
Precise typing lives only in register's signature.
|
||||||
|
"""
|
||||||
|
|
||||||
name: str
|
name: str
|
||||||
"""The name of this job."""
|
"""The name of this job."""
|
||||||
dependencies: set[str]
|
ios: tuple[Source | Sink, ...]
|
||||||
"""The dependencies of this job. Each item is a job name. Empty for no dependencies."""
|
"""The sources and sinks of this job."""
|
||||||
executor: Callable[[Environment[P]], None]
|
executor: Callable[..., None]
|
||||||
"""The executor of this job."""
|
"""The executor of this job."""
|
||||||
|
|
||||||
|
|
||||||
class Context(Generic[P]):
|
class Context(Generic[P]):
|
||||||
__jobs: dict[str, Job[P]]
|
__jobs: dict[str, Job]
|
||||||
"""The dict storing all jobs. Key is job name and value is job payload."""
|
"""The dict storing all jobs. Key is job name and value is job payload."""
|
||||||
__env: Environment[P]
|
__env: Environment[P]
|
||||||
"""The shared environment passed to every job executor."""
|
"""The shared environment passed to every job executor."""
|
||||||
@@ -39,26 +49,76 @@ class Context(Generic[P]):
|
|||||||
executed sequentially with the shared environment. The chain is
|
executed sequentially with the shared environment. The chain is
|
||||||
aborted on the first failing executor.
|
aborted on the first failing executor.
|
||||||
|
|
||||||
|
Job dependencies are not declared explicitly. They are inferred
|
||||||
|
from the job IO declarations instead: a job depends on the
|
||||||
|
producer of every address required by one of its sources.
|
||||||
|
|
||||||
:param selected: Only build jobs matching these given names.
|
:param selected: Only build jobs matching these given names.
|
||||||
Their dependencies are always built first. None for building
|
Their dependencies are always built first. None for building
|
||||||
all jobs.
|
all jobs.
|
||||||
|
:raises ValueError: If a selected job name is unknown, some
|
||||||
|
address is produced more than once (by one or several
|
||||||
|
jobs), some required address has no producer at all, or the
|
||||||
|
inferred dependency graph contains a cycle (including
|
||||||
|
self-dependencies).
|
||||||
"""
|
"""
|
||||||
# Step 1: resolve the selection closure. Collect the selected jobs
|
# Step 0: build the producer table over all registered jobs,
|
||||||
# themselves plus all their transitive dependencies into "needed".
|
# mapping every produced address to its producing job name.
|
||||||
needed: set[str] = set()
|
# An address produced more than once is rejected right here,
|
||||||
if selected is None:
|
# even if it is never actually required: the producers may be
|
||||||
needed = set(self.__jobs.keys())
|
# several jobs, or a single job declaring several identical
|
||||||
else:
|
# sinks.
|
||||||
pending = list(selected)
|
producers: dict[Address, str] = {}
|
||||||
while pending:
|
for name, job in self.__jobs.items():
|
||||||
name = pending.pop()
|
for item in job.ios:
|
||||||
if name in needed:
|
if not isinstance(item, Sink):
|
||||||
continue
|
continue
|
||||||
job = self.__jobs.get(name)
|
addr = item.address
|
||||||
if job is None:
|
prev = producers.get(addr)
|
||||||
raise ValueError(f'Unknown job name: "{name}"')
|
if prev is not None and prev == name:
|
||||||
needed.add(name)
|
raise ValueError(
|
||||||
pending.extend(job.dependencies)
|
f'Job "{name}" declares multiple sinks '
|
||||||
|
f"producing the same address {addr!r}."
|
||||||
|
)
|
||||||
|
if prev is not None:
|
||||||
|
raise ValueError(
|
||||||
|
f"Address {addr!r} is produced by both job "
|
||||||
|
f'"{prev}" and "{name}".'
|
||||||
|
)
|
||||||
|
producers[addr] = name
|
||||||
|
|
||||||
|
# Step 1: resolve the selection closure. Collect the selected
|
||||||
|
# jobs themselves plus, transitively, every job producing an
|
||||||
|
# address one of their sources requires. A source whose address
|
||||||
|
# is ``None`` depends on nothing.
|
||||||
|
needed: set[str] = set()
|
||||||
|
pending = list(self.__jobs.keys()) if selected is None else list(selected)
|
||||||
|
while pending:
|
||||||
|
name = pending.pop()
|
||||||
|
if name in needed:
|
||||||
|
continue
|
||||||
|
job = self.__jobs.get(name)
|
||||||
|
if job is None:
|
||||||
|
raise ValueError(f'Unknown job name: "{name}"')
|
||||||
|
needed.add(name)
|
||||||
|
for item in job.ios:
|
||||||
|
if not isinstance(item, Source):
|
||||||
|
continue
|
||||||
|
addr = item.address
|
||||||
|
if addr is None:
|
||||||
|
continue
|
||||||
|
producer = producers.get(addr)
|
||||||
|
if producer is None:
|
||||||
|
raise ValueError(
|
||||||
|
f'Job "{name}" requires resource {addr!r} '
|
||||||
|
f"but no registered job produces it."
|
||||||
|
)
|
||||||
|
if producer == name:
|
||||||
|
raise ValueError(
|
||||||
|
f'Job "{name}" requires resource {addr!r} '
|
||||||
|
f"which is produced by itself."
|
||||||
|
)
|
||||||
|
pending.append(producer)
|
||||||
|
|
||||||
# Step 2: Kahn topological sort over the sub-graph induced by "needed".
|
# Step 2: Kahn topological sort over the sub-graph induced by "needed".
|
||||||
# "in_degree": key is a job name, value is how many of its (needed)
|
# "in_degree": key is a job name, value is how many of its (needed)
|
||||||
@@ -68,7 +128,17 @@ class Context(Generic[P]):
|
|||||||
in_degree: dict[str, int] = {name: 0 for name in needed}
|
in_degree: dict[str, int] = {name: 0 for name in needed}
|
||||||
dependents: dict[str, list[str]] = {name: [] for name in needed}
|
dependents: dict[str, list[str]] = {name: [] for name in needed}
|
||||||
for name in needed:
|
for name in needed:
|
||||||
for dep in self.__jobs[name].dependencies:
|
# "deps": the names of jobs producing resources this job
|
||||||
|
# requires, deduplicated
|
||||||
|
deps: set[str] = set()
|
||||||
|
for item in self.__jobs[name].ios:
|
||||||
|
if not isinstance(item, Source):
|
||||||
|
continue
|
||||||
|
addr = item.address
|
||||||
|
if addr is None:
|
||||||
|
continue
|
||||||
|
deps.add(producers[addr])
|
||||||
|
for dep in deps:
|
||||||
dependents[dep].append(name)
|
dependents[dep].append(name)
|
||||||
in_degree[name] += 1
|
in_degree[name] += 1
|
||||||
queue = deque(
|
queue = deque(
|
||||||
@@ -86,29 +156,35 @@ class Context(Generic[P]):
|
|||||||
cyclic = sorted(needed - set(order))
|
cyclic = sorted(needed - set(order))
|
||||||
raise ValueError(f"Dependency cycle detected among jobs: {cyclic}")
|
raise ValueError(f"Dependency cycle detected among jobs: {cyclic}")
|
||||||
|
|
||||||
# Step 3: execute the build chain in resolved order.
|
# Step 3: execute the build chain in resolved order, feeding
|
||||||
|
# the shared environment and the declared IO items to each
|
||||||
|
# executor.
|
||||||
for name in order:
|
for name in order:
|
||||||
LOGGER.info('Running job "%s"...', name)
|
LOGGER.info('Running job "%s"...', name)
|
||||||
self.__jobs[name].executor(self.__env)
|
self.__jobs[name].executor(self.__env, *self.__jobs[name].ios)
|
||||||
|
|
||||||
def register(
|
def register[*Ts](
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
deps: Iterable[str],
|
ios: tuple[*Ts],
|
||||||
exec: Callable[[Environment[P]], None],
|
executor: Executor[Environment[P], *Ts],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""
|
"""
|
||||||
Register a new job with given name, dependencies and executor.
|
Register a new job with given name, dependencies and executor.
|
||||||
|
|
||||||
:param name: The name of job.
|
:param name: The name of job.
|
||||||
:param deps: An iterable whose each item is the name of dependencies of this job.
|
:param ios: A tuple declares ths sources and sinks of this job.
|
||||||
|
These sources and sinks will be directly passed to executor.
|
||||||
:param exec: The executor of this job.
|
:param exec: The executor of this job.
|
||||||
"""
|
"""
|
||||||
|
# check job name collision
|
||||||
if name in self.__jobs:
|
if name in self.__jobs:
|
||||||
raise ValueError(f'Can not register 2 jobs with same name: "{name}"')
|
raise ValueError(f'Can not register 2 jobs with same name: "{name}"')
|
||||||
|
# check the type of sources and sinks
|
||||||
|
for item in ios:
|
||||||
|
if not isinstance(item, (Source, Sink)):
|
||||||
|
raise TypeError(
|
||||||
|
"passes source or sink instance must be the subclass of Source or Sink"
|
||||||
|
)
|
||||||
|
|
||||||
job_deps = set(deps)
|
self.__jobs[name] = Job(name, cast(tuple[Source | Sink, ...], ios), executor)
|
||||||
if name in job_deps:
|
|
||||||
raise ValueError(f'Self-dependency is not allowed in job: "{name}"')
|
|
||||||
|
|
||||||
self.__jobs[name] = Job(name, job_deps, exec)
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from pathlib import Path
|
|||||||
from typing import TypeVar, Generic, Callable
|
from typing import TypeVar, Generic, Callable
|
||||||
from ..lowlevel.environment import Prologue as LowPrologue, Environment as LowEnv
|
from ..lowlevel.environment import Prologue as LowPrologue, Environment as LowEnv
|
||||||
from ..lowlevel.artrdr import SvgRenderKind
|
from ..lowlevel.artrdr import SvgRenderKind
|
||||||
from .platform import FdContext, WinCategory, MacCategory, FD_THUMBNAIL_HWS
|
from .utils import FdContext, WinCategory, MacCategory, FD_THUMBNAIL_HWS
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
import enum
|
|
||||||
|
|
||||||
|
|
||||||
class FdContext(enum.StrEnum):
|
|
||||||
Actions = "actions"
|
|
||||||
Animations = "animations"
|
|
||||||
Applications = "apps"
|
|
||||||
Categories = "categories"
|
|
||||||
Devices = "devices"
|
|
||||||
Emblems = "emblems"
|
|
||||||
Emotes = "emotes"
|
|
||||||
International = "intl"
|
|
||||||
MimeTypes = "mimetypes"
|
|
||||||
Places = "places"
|
|
||||||
Status = "status"
|
|
||||||
|
|
||||||
|
|
||||||
class WinCategory(enum.StrEnum):
|
|
||||||
Application = "apps"
|
|
||||||
"""Icons for applications"""
|
|
||||||
Extension = "exts"
|
|
||||||
"""Icons for file extensions"""
|
|
||||||
|
|
||||||
|
|
||||||
class MacCategory(enum.StrEnum):
|
|
||||||
Application = "apps"
|
|
||||||
"""Icons for applications"""
|
|
||||||
Extension = "exts"
|
|
||||||
"""Icons for file extensions"""
|
|
||||||
|
|
||||||
|
|
||||||
# TODO: For easy testing, we only build 32x32 and 48x48.
|
|
||||||
# Enable full generation after testing.
|
|
||||||
FD_THUMBNAIL_HWS: tuple[int, ...] = (32, 48)
|
|
||||||
# FD_THUMBNAIL_HWS: tuple[int, ...] = (16, 32, 48, 64, 128, 256)
|
|
||||||
@@ -1,7 +1,13 @@
|
|||||||
from .common import Sink
|
from .common import Sink
|
||||||
from .tempart_sink import TempArtSink
|
from .tempart_sink import TempArtSink
|
||||||
|
from .fdart_sink import FdArtSink
|
||||||
|
from .winart_sink import WinArtSink
|
||||||
|
from .macart_sink import MacArtSink
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Sink",
|
"Sink",
|
||||||
"TempArtSink"
|
"TempArtSink",
|
||||||
|
"FdArtSink",
|
||||||
|
"WinArtSink",
|
||||||
|
"MacArtSink"
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ from dataclasses import dataclass
|
|||||||
from .common import Sink
|
from .common import Sink
|
||||||
from ..address import Address, AddressDomain
|
from ..address import Address, AddressDomain
|
||||||
from ..environment import Environment
|
from ..environment import Environment
|
||||||
from ..platform import FdContext, FD_THUMBNAIL_HWS
|
from ..utils import Artifact, FdContext, FD_THUMBNAIL_HWS
|
||||||
from ..utils import Artifact
|
|
||||||
from ...lowlevel import artio, artrdr
|
from ...lowlevel import artio, artrdr
|
||||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ from dataclasses import dataclass
|
|||||||
from .common import Sink
|
from .common import Sink
|
||||||
from ..address import Address, AddressDomain
|
from ..address import Address, AddressDomain
|
||||||
from ..environment import Environment
|
from ..environment import Environment
|
||||||
from ..platform import MacCategory
|
from ..utils import Artifact, MacCategory
|
||||||
from ..utils import Artifact
|
|
||||||
from ...lowlevel import artrdr
|
from ...lowlevel import artrdr
|
||||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ from dataclasses import dataclass
|
|||||||
from .common import Sink
|
from .common import Sink
|
||||||
from ..address import Address, AddressDomain
|
from ..address import Address, AddressDomain
|
||||||
from ..environment import Environment
|
from ..environment import Environment
|
||||||
from ..platform import WinCategory
|
from ..utils import Artifact, WinCategory
|
||||||
from ..utils import Artifact
|
|
||||||
from ...lowlevel import artrdr
|
from ...lowlevel import artrdr
|
||||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,22 @@
|
|||||||
from .common import Source
|
from .common import Source
|
||||||
|
from .newart_source import NewArtSource
|
||||||
|
from .bitmap_source import BitmapSource
|
||||||
|
from .svg_source import SvgSource
|
||||||
|
from .blender_source import BlenderSource
|
||||||
from .tempart_source import TempArtSource
|
from .tempart_source import TempArtSource
|
||||||
|
from .fdart_source import FdArtSource
|
||||||
|
from .winart_source import WinArtSource
|
||||||
|
from .macart_source import MacArtSource
|
||||||
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"Source",
|
"Source",
|
||||||
"TempArtSource"
|
"NewArtSource",
|
||||||
|
"BitmapSource",
|
||||||
|
"SvgSource",
|
||||||
|
"BlenderSource",
|
||||||
|
"TempArtSource",
|
||||||
|
"FdArtSource",
|
||||||
|
"WinArtSource",
|
||||||
|
"MacArtSource",
|
||||||
]
|
]
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ from ...lowlevel import artio
|
|||||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|
||||||
class InArtSource(Source):
|
class BitmapSource(Source):
|
||||||
"""The source to bimap artifact (PNG, JPEG or BMP) located in input directory."""
|
"""The source to bimap artifact (PNG, JPEG or BMP) located in input directory."""
|
||||||
|
|
||||||
__comps: tuple[str, ...]
|
__comps: tuple[str, ...]
|
||||||
+2
-2
@@ -2,12 +2,12 @@ from .common import Source
|
|||||||
from ..address import Address, AddressDomain
|
from ..address import Address, AddressDomain
|
||||||
from ..environment import Environment
|
from ..environment import Environment
|
||||||
from ..sink.fdart_sink import FdArtSink
|
from ..sink.fdart_sink import FdArtSink
|
||||||
from ..platform import FdContext, FD_THUMBNAIL_HWS
|
from ..utils import FdContext, FD_THUMBNAIL_HWS
|
||||||
from ...lowlevel import artio
|
from ...lowlevel import artio
|
||||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|
||||||
class FdOutArtSource(Source):
|
class FdArtSource(Source):
|
||||||
__kind: FdContext
|
__kind: FdContext
|
||||||
__name: str
|
__name: str
|
||||||
|
|
||||||
+2
-2
@@ -2,12 +2,12 @@ from .common import Source
|
|||||||
from ..address import Address, AddressDomain
|
from ..address import Address, AddressDomain
|
||||||
from ..environment import Environment
|
from ..environment import Environment
|
||||||
from ..sink.macart_sink import MacArtSink
|
from ..sink.macart_sink import MacArtSink
|
||||||
from ..platform import MacCategory
|
from ..utils import MacCategory
|
||||||
from ...lowlevel import artio
|
from ...lowlevel import artio
|
||||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|
||||||
class MacOutArtSource(Source):
|
class MacArtSource(Source):
|
||||||
__kind: MacCategory
|
__kind: MacCategory
|
||||||
__name: str
|
__name: str
|
||||||
|
|
||||||
+2
-2
@@ -2,12 +2,12 @@ from .common import Source
|
|||||||
from ..address import Address, AddressDomain
|
from ..address import Address, AddressDomain
|
||||||
from ..environment import Environment
|
from ..environment import Environment
|
||||||
from ..sink.winart_sink import WinArtSink
|
from ..sink.winart_sink import WinArtSink
|
||||||
from ..platform import WinCategory
|
from ..utils import WinCategory
|
||||||
from ...lowlevel import artio
|
from ...lowlevel import artio
|
||||||
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
from ...logger import HIGHLEVEL_LOGGER as LOGGER
|
||||||
|
|
||||||
|
|
||||||
class WinOutArtSource(Source):
|
class WinArtSource(Source):
|
||||||
__kind: WinCategory
|
__kind: WinCategory
|
||||||
__name: str
|
__name: str
|
||||||
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import enum
|
||||||
from ..lowlevel.utils import Artifact
|
from ..lowlevel.utils import Artifact
|
||||||
|
|
||||||
VART_HW: int = 1024
|
VART_HW: int = 1024
|
||||||
@@ -9,3 +10,37 @@ def check_vart(art: Artifact) -> None:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"The size of general highlevel used artifact must be {VART_HW}x{VART_HW}, got {art.width}x{art.height} instead"
|
f"The size of general highlevel used artifact must be {VART_HW}x{VART_HW}, got {art.width}x{art.height} instead"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FdContext(enum.StrEnum):
|
||||||
|
Actions = "actions"
|
||||||
|
Animations = "animations"
|
||||||
|
Applications = "apps"
|
||||||
|
Categories = "categories"
|
||||||
|
Devices = "devices"
|
||||||
|
Emblems = "emblems"
|
||||||
|
Emotes = "emotes"
|
||||||
|
International = "intl"
|
||||||
|
MimeTypes = "mimetypes"
|
||||||
|
Places = "places"
|
||||||
|
Status = "status"
|
||||||
|
|
||||||
|
|
||||||
|
class WinCategory(enum.StrEnum):
|
||||||
|
Application = "apps"
|
||||||
|
"""Icons for applications"""
|
||||||
|
Extension = "exts"
|
||||||
|
"""Icons for file extensions"""
|
||||||
|
|
||||||
|
|
||||||
|
class MacCategory(enum.StrEnum):
|
||||||
|
Application = "apps"
|
||||||
|
"""Icons for applications"""
|
||||||
|
Extension = "exts"
|
||||||
|
"""Icons for file extensions"""
|
||||||
|
|
||||||
|
|
||||||
|
# TODO: For easy testing, we only build 32x32 and 48x48.
|
||||||
|
# Enable full generation after testing.
|
||||||
|
FD_THUMBNAIL_HWS: tuple[int, ...] = (32, 48)
|
||||||
|
# FD_THUMBNAIL_HWS: tuple[int, ...] = (16, 32, 48, 64, 128, 256)
|
||||||
|
|||||||
Reference in New Issue
Block a user