config_ninja.settings.poe

Integrate with poethepoet for callback hooks.

  1"""Integrate with `poethepoet` for callback hooks."""
  2
  3from __future__ import annotations
  4
  5import logging
  6import os
  7import sys
  8from pathlib import Path
  9
 10# pyright: reportMissingTypeStubs=false
 11from poethepoet import context, exceptions
 12from poethepoet.config import PoeConfig
 13from poethepoet.task.base import PoeTask, TaskContext, TaskSpecFactory
 14from poethepoet.task.graph import TaskExecutionGraph
 15from poethepoet.ui import PoeUi
 16
 17from config_ninja.settings import DEFAULT_PATHS
 18
 19__all__ = ['Hook', 'HooksEngine', 'exceptions']
 20
 21logger = logging.getLogger(__name__)
 22
 23
 24class Hook:
 25    """Simple callable to execute the named hook with the given engine."""
 26
 27    engine: HooksEngine
 28    """The `HooksEngine` instance contains each of the hooks that can be executed."""
 29
 30    name: str
 31    """The name of the `poethepoet` task (`Hook`) to invoke."""
 32
 33    def __init__(self, engine: HooksEngine, name: str) -> None:
 34        """Raise a `ValueError` if the engine does not define a hook by the given name."""
 35        self.name = name
 36        self.engine = engine
 37
 38        if name not in engine:
 39            raise ValueError(f'Undefined hook {name!r} (options: {list(engine.tasks)})')
 40
 41    def __repr__(self) -> str:
 42        """The string representation of the `Hook` instance.
 43
 44        <!-- Perform doctest setup that is excluded from the docs
 45        >>> engine = ['example-hook']
 46
 47        -->
 48        >>> Hook(engine, 'example-hook')
 49        <Hook: 'example-hook'>
 50        """
 51        return f'<{self.__class__.__name__}: {self.name!r}>'
 52
 53    def __call__(self) -> None:
 54        """Invoke the `Hook` instance to execute it."""
 55        self.engine.execute(self.name)
 56
 57
 58class HooksEngine:
 59    """Encapsulate configuration for executing `poethepoet` tasks as callback hooks."""
 60
 61    config_file: Path
 62    config: PoeConfig
 63    tasks: dict[str, PoeTask]
 64    ui: PoeUi
 65
 66    def __init__(self, config: PoeConfig, ui: PoeUi, tasks: dict[str, PoeTask]) -> None:
 67        """Initialize the engine with the given configuration, UI, and tasks."""
 68        self.config = config
 69        self.tasks = tasks
 70        self.ui = ui
 71
 72    def __contains__(self, item: str) -> bool:
 73        """Convenience method for checking if a hook is defined in the engine."""
 74        return item in self.tasks
 75
 76    def get_hook(self, name: str) -> Hook:
 77        """Initialize a `Hook` instance for running the `PoeTask` of the given name."""
 78        return Hook(self, name)
 79
 80    def get_run_context(self, multistage: bool = False) -> context.RunContext:
 81        """Create a `poethepoet.context.RunContext` instance for executing tasks.
 82
 83        This method is based on `poethepoet.app.PoeThePoet.get_run_context()` (`reference`_).
 84
 85        .. _reference: https://github.com/nat-n/poethepoet/blob/3c9fd8bcffde8a95c5cd9513923d0f43c1507385/poethepoet/app.py#L210-L225
 86        """
 87        return context.RunContext(
 88            config=self.config,
 89            ui=self.ui,
 90            env=os.environ,
 91            dry=self.ui['dry_run'] or False,
 92            poe_active=os.environ.get('POE_ACTIVE'),
 93            multistage=multistage,
 94            cwd=Path.cwd(),
 95        )
 96
 97    def execute(self, hook_name: str) -> None:
 98        """Execute the `poethepoet.task.base.PoeTask` of the given name."""
 99        self.ui.parse_args([hook_name])
100
101        task = self.tasks[hook_name]
102
103        if task.has_deps():
104            self.run_task_graph(task)
105        else:
106            self.run_task(task)
107
108    @classmethod
109    def load_file(cls, path: Path) -> HooksEngine:
110        """Instantiate a `poethepoet.config.PoeConfig` object, then populate it with the given file."""
111        cfg = PoeConfig(config_name=tuple({str(p.name) for p in DEFAULT_PATHS}))
112
113        cfg.load(path)
114        logger.debug('parsed hooks from %s: %s', path, list(cfg.task_names))
115
116        ui = PoeUi(
117            output=sys.stdout,
118            program_name=f'{sys.argv[0]} hook'
119            if sys.argv[0].endswith('config-ninja')
120            else f'{sys.executable} {sys.argv[0]} hook',
121        )
122
123        tasks: dict[str, PoeTask] = {}
124        factory = TaskSpecFactory(cfg)
125
126        for task_spec in factory.load_all():
127            task_spec.validate(cfg, factory)
128            tasks[task_spec.name] = task_spec.create_task(
129                invocation=(task_spec.name,),
130                ctx=TaskContext(config=cfg, cwd=str(task_spec.source.cwd), specs=factory, ui=ui),
131            )
132
133        return cls(cfg, ui, tasks)
134
135    def run_task(self, task: PoeTask, ctx: context.RunContext | None = None) -> None:
136        """Reimplement the `poethepoet.app.PoeThePoet.run_task()` method (`reference`_).
137
138        .. _reference: https://github.com/nat-n/poethepoet/blob/3c9fd8bcffde8a95c5cd9513923d0f43c1507385/poethepoet/app.py#L169-L181
139        """
140        try:
141            task.run(context=ctx or self.get_run_context())
142        except exceptions.ExecutionError as error:
143            logger.exception('error running task %s: %s', task.name, error)
144            raise
145
146    def run_task_graph(self, task: PoeTask) -> None:
147        """Reimplement the `poethepoet.app.PoeThePoet.run_task_graph()` method (`reference`_).
148
149        .. _reference: https://github.com/nat-n/poethepoet/blob/3c9fd8bcffde8a95c5cd9513923d0f43c1507385/poethepoet/app.py#L183-L208
150        """
151        ctx = self.get_run_context(multistage=True)
152        graph = TaskExecutionGraph(task, ctx)
153        plan = graph.get_execution_plan()
154
155        for stage in plan:
156            for stage_task in stage:
157                if stage_task == task:
158                    # The final sink task gets special treatment
159                    self.run_task(stage_task, ctx)
160                    return
161
162                task_result = stage_task.run(context=ctx)
163                if task_result:
164                    raise exceptions.ExecutionError(f'Task graph aborted after failed task {stage_task.name!r}')
165
166
167logger.debug('successfully imported %s', __name__)
class Hook:
25class Hook:
26    """Simple callable to execute the named hook with the given engine."""
27
28    engine: HooksEngine
29    """The `HooksEngine` instance contains each of the hooks that can be executed."""
30
31    name: str
32    """The name of the `poethepoet` task (`Hook`) to invoke."""
33
34    def __init__(self, engine: HooksEngine, name: str) -> None:
35        """Raise a `ValueError` if the engine does not define a hook by the given name."""
36        self.name = name
37        self.engine = engine
38
39        if name not in engine:
40            raise ValueError(f'Undefined hook {name!r} (options: {list(engine.tasks)})')
41
42    def __repr__(self) -> str:
43        """The string representation of the `Hook` instance.
44
45        <!-- Perform doctest setup that is excluded from the docs
46        >>> engine = ['example-hook']
47
48        -->
49        >>> Hook(engine, 'example-hook')
50        <Hook: 'example-hook'>
51        """
52        return f'<{self.__class__.__name__}: {self.name!r}>'
53
54    def __call__(self) -> None:
55        """Invoke the `Hook` instance to execute it."""
56        self.engine.execute(self.name)

Simple callable to execute the named hook with the given engine.

Hook(engine: HooksEngine, name: str)
34    def __init__(self, engine: HooksEngine, name: str) -> None:
35        """Raise a `ValueError` if the engine does not define a hook by the given name."""
36        self.name = name
37        self.engine = engine
38
39        if name not in engine:
40            raise ValueError(f'Undefined hook {name!r} (options: {list(engine.tasks)})')

Raise a ValueError if the engine does not define a hook by the given name.

engine: HooksEngine

The HooksEngine instance contains each of the hooks that can be executed.

name: str

The name of the poethepoet task (Hook) to invoke.

class HooksEngine:
 59class HooksEngine:
 60    """Encapsulate configuration for executing `poethepoet` tasks as callback hooks."""
 61
 62    config_file: Path
 63    config: PoeConfig
 64    tasks: dict[str, PoeTask]
 65    ui: PoeUi
 66
 67    def __init__(self, config: PoeConfig, ui: PoeUi, tasks: dict[str, PoeTask]) -> None:
 68        """Initialize the engine with the given configuration, UI, and tasks."""
 69        self.config = config
 70        self.tasks = tasks
 71        self.ui = ui
 72
 73    def __contains__(self, item: str) -> bool:
 74        """Convenience method for checking if a hook is defined in the engine."""
 75        return item in self.tasks
 76
 77    def get_hook(self, name: str) -> Hook:
 78        """Initialize a `Hook` instance for running the `PoeTask` of the given name."""
 79        return Hook(self, name)
 80
 81    def get_run_context(self, multistage: bool = False) -> context.RunContext:
 82        """Create a `poethepoet.context.RunContext` instance for executing tasks.
 83
 84        This method is based on `poethepoet.app.PoeThePoet.get_run_context()` (`reference`_).
 85
 86        .. _reference: https://github.com/nat-n/poethepoet/blob/3c9fd8bcffde8a95c5cd9513923d0f43c1507385/poethepoet/app.py#L210-L225
 87        """
 88        return context.RunContext(
 89            config=self.config,
 90            ui=self.ui,
 91            env=os.environ,
 92            dry=self.ui['dry_run'] or False,
 93            poe_active=os.environ.get('POE_ACTIVE'),
 94            multistage=multistage,
 95            cwd=Path.cwd(),
 96        )
 97
 98    def execute(self, hook_name: str) -> None:
 99        """Execute the `poethepoet.task.base.PoeTask` of the given name."""
100        self.ui.parse_args([hook_name])
101
102        task = self.tasks[hook_name]
103
104        if task.has_deps():
105            self.run_task_graph(task)
106        else:
107            self.run_task(task)
108
109    @classmethod
110    def load_file(cls, path: Path) -> HooksEngine:
111        """Instantiate a `poethepoet.config.PoeConfig` object, then populate it with the given file."""
112        cfg = PoeConfig(config_name=tuple({str(p.name) for p in DEFAULT_PATHS}))
113
114        cfg.load(path)
115        logger.debug('parsed hooks from %s: %s', path, list(cfg.task_names))
116
117        ui = PoeUi(
118            output=sys.stdout,
119            program_name=f'{sys.argv[0]} hook'
120            if sys.argv[0].endswith('config-ninja')
121            else f'{sys.executable} {sys.argv[0]} hook',
122        )
123
124        tasks: dict[str, PoeTask] = {}
125        factory = TaskSpecFactory(cfg)
126
127        for task_spec in factory.load_all():
128            task_spec.validate(cfg, factory)
129            tasks[task_spec.name] = task_spec.create_task(
130                invocation=(task_spec.name,),
131                ctx=TaskContext(config=cfg, cwd=str(task_spec.source.cwd), specs=factory, ui=ui),
132            )
133
134        return cls(cfg, ui, tasks)
135
136    def run_task(self, task: PoeTask, ctx: context.RunContext | None = None) -> None:
137        """Reimplement the `poethepoet.app.PoeThePoet.run_task()` method (`reference`_).
138
139        .. _reference: https://github.com/nat-n/poethepoet/blob/3c9fd8bcffde8a95c5cd9513923d0f43c1507385/poethepoet/app.py#L169-L181
140        """
141        try:
142            task.run(context=ctx or self.get_run_context())
143        except exceptions.ExecutionError as error:
144            logger.exception('error running task %s: %s', task.name, error)
145            raise
146
147    def run_task_graph(self, task: PoeTask) -> None:
148        """Reimplement the `poethepoet.app.PoeThePoet.run_task_graph()` method (`reference`_).
149
150        .. _reference: https://github.com/nat-n/poethepoet/blob/3c9fd8bcffde8a95c5cd9513923d0f43c1507385/poethepoet/app.py#L183-L208
151        """
152        ctx = self.get_run_context(multistage=True)
153        graph = TaskExecutionGraph(task, ctx)
154        plan = graph.get_execution_plan()
155
156        for stage in plan:
157            for stage_task in stage:
158                if stage_task == task:
159                    # The final sink task gets special treatment
160                    self.run_task(stage_task, ctx)
161                    return
162
163                task_result = stage_task.run(context=ctx)
164                if task_result:
165                    raise exceptions.ExecutionError(f'Task graph aborted after failed task {stage_task.name!r}')

Encapsulate configuration for executing poethepoet tasks as callback hooks.

HooksEngine( config: poethepoet.config.PoeConfig, ui: poethepoet.ui.PoeUi, tasks: dict[str, poethepoet.task.base.PoeTask])
67    def __init__(self, config: PoeConfig, ui: PoeUi, tasks: dict[str, PoeTask]) -> None:
68        """Initialize the engine with the given configuration, UI, and tasks."""
69        self.config = config
70        self.tasks = tasks
71        self.ui = ui

Initialize the engine with the given configuration, UI, and tasks.

config_file: pathlib.Path
tasks: dict[str, poethepoet.task.base.PoeTask]
def get_hook(self, name: str) -> Hook:
77    def get_hook(self, name: str) -> Hook:
78        """Initialize a `Hook` instance for running the `PoeTask` of the given name."""
79        return Hook(self, name)

Initialize a Hook instance for running the PoeTask of the given name.

def get_run_context(self, multistage: bool = False) -> poethepoet.context.RunContext:
81    def get_run_context(self, multistage: bool = False) -> context.RunContext:
82        """Create a `poethepoet.context.RunContext` instance for executing tasks.
83
84        This method is based on `poethepoet.app.PoeThePoet.get_run_context()` (`reference`_).
85
86        .. _reference: https://github.com/nat-n/poethepoet/blob/3c9fd8bcffde8a95c5cd9513923d0f43c1507385/poethepoet/app.py#L210-L225
87        """
88        return context.RunContext(
89            config=self.config,
90            ui=self.ui,
91            env=os.environ,
92            dry=self.ui['dry_run'] or False,
93            poe_active=os.environ.get('POE_ACTIVE'),
94            multistage=multistage,
95            cwd=Path.cwd(),
96        )

Create a poethepoet.context.RunContext instance for executing tasks.

This method is based on poethepoet.app.PoeThePoet.get_run_context() (reference).

def execute(self, hook_name: str) -> None:
 98    def execute(self, hook_name: str) -> None:
 99        """Execute the `poethepoet.task.base.PoeTask` of the given name."""
100        self.ui.parse_args([hook_name])
101
102        task = self.tasks[hook_name]
103
104        if task.has_deps():
105            self.run_task_graph(task)
106        else:
107            self.run_task(task)

Execute the poethepoet.task.base.PoeTask of the given name.

@classmethod
def load_file(cls, path: pathlib.Path) -> HooksEngine:
109    @classmethod
110    def load_file(cls, path: Path) -> HooksEngine:
111        """Instantiate a `poethepoet.config.PoeConfig` object, then populate it with the given file."""
112        cfg = PoeConfig(config_name=tuple({str(p.name) for p in DEFAULT_PATHS}))
113
114        cfg.load(path)
115        logger.debug('parsed hooks from %s: %s', path, list(cfg.task_names))
116
117        ui = PoeUi(
118            output=sys.stdout,
119            program_name=f'{sys.argv[0]} hook'
120            if sys.argv[0].endswith('config-ninja')
121            else f'{sys.executable} {sys.argv[0]} hook',
122        )
123
124        tasks: dict[str, PoeTask] = {}
125        factory = TaskSpecFactory(cfg)
126
127        for task_spec in factory.load_all():
128            task_spec.validate(cfg, factory)
129            tasks[task_spec.name] = task_spec.create_task(
130                invocation=(task_spec.name,),
131                ctx=TaskContext(config=cfg, cwd=str(task_spec.source.cwd), specs=factory, ui=ui),
132            )
133
134        return cls(cfg, ui, tasks)

Instantiate a poethepoet.config.PoeConfig object, then populate it with the given file.

def run_task( self, task: poethepoet.task.base.PoeTask, ctx: poethepoet.context.RunContext | None = None) -> None:
136    def run_task(self, task: PoeTask, ctx: context.RunContext | None = None) -> None:
137        """Reimplement the `poethepoet.app.PoeThePoet.run_task()` method (`reference`_).
138
139        .. _reference: https://github.com/nat-n/poethepoet/blob/3c9fd8bcffde8a95c5cd9513923d0f43c1507385/poethepoet/app.py#L169-L181
140        """
141        try:
142            task.run(context=ctx or self.get_run_context())
143        except exceptions.ExecutionError as error:
144            logger.exception('error running task %s: %s', task.name, error)
145            raise
def run_task_graph(self, task: poethepoet.task.base.PoeTask) -> None:
147    def run_task_graph(self, task: PoeTask) -> None:
148        """Reimplement the `poethepoet.app.PoeThePoet.run_task_graph()` method (`reference`_).
149
150        .. _reference: https://github.com/nat-n/poethepoet/blob/3c9fd8bcffde8a95c5cd9513923d0f43c1507385/poethepoet/app.py#L183-L208
151        """
152        ctx = self.get_run_context(multistage=True)
153        graph = TaskExecutionGraph(task, ctx)
154        plan = graph.get_execution_plan()
155
156        for stage in plan:
157            for stage_task in stage:
158                if stage_task == task:
159                    # The final sink task gets special treatment
160                    self.run_task(stage_task, ctx)
161                    return
162
163                task_result = stage_task.run(context=ctx)
164                if task_result:
165                    raise exceptions.ExecutionError(f'Task graph aborted after failed task {stage_task.name!r}')