Loading files
This file is pinned to the selected release. Download the original to open it in your design tool.
# Copyright 2023 Efabless Corporation
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import os
import glob
import shutil
import fnmatch
import logging
import datetime
import textwrap
from dataclasses import dataclass
from abc import abstractmethod, ABC
from concurrent.futures import Future
from functools import wraps
from typing import (
List,
Sequence,
Tuple,
Type,
ClassVar,
Optional,
Dict,
Callable,
TypeVar,
Union,
)
from rich.progress import (
Progress,
TextColumn,
BarColumn,
MofNCompleteColumn,
TimeElapsedColumn,
TaskID,
)
from deprecated.sphinx import deprecated
from openlane.common.types import Path
from ..config import Config, Variable, universal_flow_config_variables, AnyConfigs
from ..state import State, DesignFormat, DesignFormatObject
from ..steps import Step, StepNotFound
from ..logging import (
LevelFilter,
console,
info,
warn,
verbose,
register_additional_handler,
deregister_additional_handler,
options,
)
from ..common import (
get_tpe,
mkdirp,
protected,
final,
slugify,
Toolbox,
get_latest_file,
)
class FlowError(RuntimeError):
"""
A ``RuntimeError`` that occurs when a Flow, or one of its underlying Steps,
fails to finish execution properly.
"""
pass
class FlowException(FlowError):
"""
A variant of :class:`FlowError` for unexpected failures or failures due
to misconfiguration, such as:
* A :class:`StepException` raised by an underlying Step
* Invalid inputs
* Mis-use of class interfaces of the :class:`Flow`
* Other unexpected failures
"""
pass
T = TypeVar("T", bound=Callable)
def ensure_progress_started(method: T) -> Callable:
"""
If a method of :class:`FlowProgressBar`decorated with `ensure_started`
and :meth:`start` had not been called yet, a :class:`FlowException` will be
thrown.
The docstring will also be amended to reflect that fact.
:param method: The method of :class:`FlowProgressBar` in question.
"""
@wraps(method)
def _impl(obj: FlowProgressBar, *method_args, **method_kwargs):
if not obj.started:
raise FlowException(
f"Attempted to call method '{method}' before initializing progress bar"
)
return method(obj, *method_args, **method_kwargs)
if method.__doc__ is None:
method.__doc__ = ""
method.__doc__ = (
"This method may not be called before the progress bar is started.\n"
+ method.__doc__
)
return _impl
class FlowProgressBar(object):
"""
A wrapper for a flow's progress bar, rendered using Rich at the bottom of
interactive terminals.
"""
def __init__(self, flow_name: str, starting_ordinal: int = 1) -> None:
self.__flow_name: str = flow_name
self.__stages_completed: int = 0
self.__max_stage: int = 0
self.__task_id: TaskID = TaskID(-1)
self.__ordinal: int = starting_ordinal
self.__progress = Progress(
TextColumn("[progress.description]{task.description}"),
BarColumn(),
MofNCompleteColumn(),
TimeElapsedColumn(),
console=console,
disable=not options.get_show_progress_bar(),
)
def start(self):
"""
Starts rendering the progress bar.
"""
self.__progress.start()
self.__task_id = self.__progress.add_task(
f"{self.__flow_name}",
)
def end(self):
"""
Stops rendering the progress bar.
"""
self.__progress.stop()
self.__task_id = TaskID(-1)
@property
def started(self) -> bool:
"""
:returns: If the progress bar has started or not
"""
return self.__task_id != TaskID(-1)
@ensure_progress_started
def set_max_stage_count(self, count: int):
"""
A helper function, used to set the total number of stages the progress
bar is expected to keep tally of.
:param count: The total number of stages.
"""
self.__max_stage = count
self.__progress.update(self.__task_id, total=count)
@ensure_progress_started
def start_stage(self, name: str):
"""
Starts a new stage, updating the progress bar appropriately.
:param name: The name of the stage.
"""
self.__progress.update(
self.__task_id,
description=f"{self.__flow_name} - Stage {self.__stages_completed + 1} - {name}",
)
@ensure_progress_started
def end_stage(self, *, increment_ordinal: bool = True):
"""
Ends the current stage, updating the progress bar appropriately.
:param increment_ordinal: Increment the step ordinal, which is used in the creation of step directories.
You may want to set this to ``False`` if the stage is being skipped.
Please note that step ordinal is not equal to stages- a skipped step
increments the stage but not the step ordinal.
"""
self.__stages_completed += 1
if increment_ordinal:
self.__ordinal += 1
self.__progress.update(self.__task_id, completed=float(self.__stages_completed))
@ensure_progress_started
def get_ordinal_prefix(self) -> str:
"""
:returns: A string with the current step ordinal, which can be
used to create a step directory.
"""
max_stage_digits = len(str(self.__max_stage))
return f"{str(self.__ordinal).zfill(max_stage_digits)}-"
class Flow(ABC):
"""
An abstract base class for a flow.
Flows encapsulate a the running of multiple :class:`Step`\\s in any order.
The sequence (or lack thereof) of running the steps is left to the Flow
itself.
The Flow ABC offers a number of convenience functions, including handling the
progress bar at the bottom of the terminal, which shows what stage the flow
is currently in and the remaining stages.
:param config: Either a resolved :class:`openlane.config.Config` object, or an
input to :meth:`openlane.config.Config.load`.
:param name: An optional string name for the Flow itself, and not a run of it.
If not provided, there are two fallbacks:
* The value of the ``name`` property (``NotImplemented`` by default)
* The name of the concrete ``Flow`` class
:param config_override_strings: See :meth:`openlane.config.Config.load`
:param pdk: See :meth:`openlane.config.Config.load`
:param pdk_root: See :meth:`openlane.config.Config.load`
:param scl: See :meth:`openlane.config.Config.load`
:param design_dir: See :meth:`openlane.config.Config.load`
:cvar Steps:
A list of :class:`Step` **types** used by the Flow (not Step objects.)
Subclasses of :class:`Flow` are expected to override the default value
as a class member- but subclasses may allow this value to be further
overridden during construction (and only then.)
:class:`Flow` subclasses without the ``Steps`` class property declared
are considered abstract and cannot be initialized.
:cvar config_vars:
A list of **flow-specific** configuration variables. These configuration
variables are used entirely within the logic of the flow itself and
are not exposed to ``Step``\\(s).
:ivar step_objects:
A list of :class:`Step` **objects** from the last run of the flow,
if it exists.
If :meth:`start` is called again, the reference is destroyed.
:ivar run_dir:
The directory of the last run of the flow, if it exists.
If :meth:`start` is called again, the reference is destroyed.
:ivar toolbox:
The :class:`Toolbox` of the last run of the flow, if it exists.
If :meth:`start` is called again, the reference is destroyed.
:ivar config_resolved_path:
The path to the serialization of the resolved configuration for the
last run of the flow.
If :meth:`start` is called again, the reference is destroyed.
"""
class _StepWarningHandler(logging.Handler):
@dataclass
class Record:
message: str
step: Optional[str] = None
repeats: int = 0
similar: int = 0
def __str__(self) -> str:
prefix = ""
if self.step is not None:
prefix = f"[{self.step}] "
postfix = ""
if self.repeats + self.similar:
postfix = f"and {self.repeats + self.similar} similar warnings"
if len(postfix):
postfix = f" ({postfix})"
return f"{prefix}{self.message}{postfix}"
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
self.warnings: Dict[str, Flow._StepWarningHandler.Record] = {}
def emit(self, record: logging.LogRecord) -> None:
step = None
if hasattr(record, "step"):
step = record.step
key = record.key if hasattr(record, "key") else record.msg
if key in self.warnings:
existing = self.warnings[key]
if record.msg == existing.message:
existing.repeats += 1
else:
existing.similar += 1
else:
self.warnings[key] = Flow._StepWarningHandler.Record(record.msg, step)
name: str = NotImplemented
Steps: List[Type[Step]] = NotImplemented # Override
config_vars: List[Variable] = []
step_objects: Optional[List[Step]] = None
run_dir: Optional[str] = None
toolbox: Optional[Toolbox] = None
config_resolved_path: Optional[str] = None
def __init__(
self,
config: AnyConfigs,
*,
name: Optional[str] = None,
pdk: Optional[str] = None,
pdk_root: Optional[str] = None,
scl: Optional[str] = None,
design_dir: Optional[str] = None,
config_override_strings: Optional[Sequence[str]] = None,
):
if self.__class__.Steps == NotImplemented:
raise NotImplementedError(
f"Abstract flow {self.__class__.__qualname__} does not implement the .Steps property and cannot be initialized."
)
for step in self.Steps:
step.assert_concrete("used in a Flow")
self.name = (
self.__class__.__name__ if self.name == NotImplemented else self.name
)
if name is not None:
self.name = name
self.Steps = self.Steps.copy() # Break global reference
if not isinstance(config, Config):
config, design_dir = Config.load(
config_in=config,
flow_config_vars=self.get_all_config_variables(),
config_override_strings=config_override_strings,
pdk=pdk,
pdk_root=pdk_root,
scl=scl,
design_dir=design_dir,
)
self.config: Config = config
self.design_dir: str = str(self.config["DESIGN_DIR"])
self.progress_bar = FlowProgressBar(self.name)
@classmethod
def get_help_md(Self) -> str: # pragma: no cover
"""
:returns: rendered Markdown help for this Flow
"""
doc_string = ""
if Self.__doc__:
doc_string = textwrap.dedent(Self.__doc__)
result = (
textwrap.dedent(
f"""\
(flow-{slugify(Self.__name__, lower=True)})=
### {Self.__name__}
```{{eval-rst}}
%s
```
#### Using from the CLI
```sh
openlane --flow {Self.__name__} [...]
```
#### Importing
```python
from openlane.flows import Flow
{Self.__name__} = Flow.factory.get("{Self.__name__}")
```
"""
)
% doc_string
)
flow_config_vars = Self.config_vars
if len(flow_config_vars):
result += textwrap.dedent(
f"""
({slugify(Self.__name__, lower=True)}-config-vars)=
#### Flow-specific Configuration Variables
| Variable Name | Type | Description | Default | Units |
| - | - | - | - | - |
"""
)
for var in flow_config_vars:
units = var.units or ""
pdk_superscript = "<sup>PDK</sup>" if var.pdk else ""
result += f"| `{var.name}`{{#{var._get_docs_identifier(Self.__name__)}}}{pdk_superscript} | {var.type_repr_md()} | {var.desc_repr_md()} | `{var.default}` | {units} |\n"
result += "\n"
if len(Self.Steps):
result += "#### Included Steps\n"
for step in Self.Steps:
if hasattr(step, "long_name"):
name = step.long_name
elif hasattr(step, "name"):
name = step.name
else:
name = step.id
result += f"* [`{step.id}`](./step_config_vars.md#{slugify(name)})\n"
return result
def get_all_config_variables(self) -> List[Variable]:
"""
:returns: All configuration variables for this Flow, including
universal configuration variables, flow-specific configuration
variables and step-specific configuration variables.
"""
flow_variables_by_name: Dict[str, Tuple[Variable, str]] = {
variable.name: (variable, "universal flow variables")
for variable in universal_flow_config_variables
}
for variable in self.config_vars:
if flow_variables_by_name.get(variable.name) is not None:
existing_variable, source = flow_variables_by_name[variable.name]
if variable != existing_variable:
raise FlowException(
f"Misconfigured flow: Unrelated variables in {source} and flow-specific variables share a name: {variable.name}"
)
flow_variables_by_name[variable.name] = (
variable,
"flow-specific variables",
)
for step_cls in self.Steps:
for variable in step_cls.config_vars:
if flow_variables_by_name.get(variable.name) is not None:
existing_variable, existing_step = flow_variables_by_name[
variable.name
]
if variable != existing_variable:
raise FlowException(
f"Misconfigured flow: Unrelated variables in {existing_step} and {step_cls.__name__} share a name: {variable.name}"
)
flow_variables_by_name[variable.name] = (variable, step_cls.__name__)
return [variable for variable, _ in flow_variables_by_name.values()]
@classmethod
@deprecated(
version="2.0.0a29",
reason="Use the constructor for the class instead",
action="once",
)
def init_with_config(
Self,
config_in: Union[Config, str, os.PathLike, Dict],
**kwargs,
): # pragma: no cover
kwargs["config"] = config_in
return Self(**kwargs)
@final
def start(
self,
with_initial_state: Optional[State] = None,
tag: Optional[str] = None,
last_run: bool = False,
_force_run_dir: Optional[str] = None,
_no_load_previous_steps: bool = False,
*,
overwrite: bool = False,
**kwargs,
) -> State:
"""
The entry point for a flow.
:param with_initial_state: An optional initial state object to use.
If not provided:
* If resuming a previous run, the latest ``state_out.json`` (by filesystem modification date)
* If not, an empty state object is created.
:param tag: A name for this invocation of the flow. If not provided,
one based on a date string will be created.
This tag is used to create the "run directory", which will be placed
under the directory ``runs/`` in the design directory.
:param last_run: Use the latest run (by modification time) as the tag.
If no runs exist, a :class:`FlowException` will be raised.
If ``last_run`` and ``tag`` are both set, a :class:`FlowException` will
also be raised.
:param overwrite: If true and a run with the desired tag was found, the
contents will be deleted instead of appended.
:returns: ``(success, state_list)``
"""
handlers: List[logging.Handler] = []
warning_handler = Flow._StepWarningHandler()
warning_handler.addFilter(LevelFilter("WARNING"))
handlers.append(warning_handler)
register_additional_handler(warning_handler)
if last_run and tag is not None:
raise FlowException("tag and last_run cannot be used simultaneously.")
tag = tag or datetime.datetime.now().astimezone().strftime(
"RUN_%Y-%m-%d_%H-%M-%S"
)
if last_run:
runs = sorted(glob.glob(os.path.join(self.design_dir, "runs", "*")))
latest_time: float = 0
latest_run: Optional[str] = None
for run in runs:
time = os.path.getmtime(run)
if time > latest_time:
latest_time = time
latest_run = run
if latest_run is not None:
tag = os.path.basename(latest_run)
else:
raise FlowException("last_run used without any existing runs")
# Stored until next start()
self.run_dir = os.path.abspath(
_force_run_dir or os.path.join(self.design_dir, "runs", tag)
)
initial_state = with_initial_state or State()
self.step_objects = []
starting_ordinal = 1
try:
entries = os.listdir(self.run_dir)
if len(entries) == 0:
raise FileNotFoundError(self.run_dir) # Treat as non-existent directory
elif overwrite:
shutil.rmtree(self.run_dir)
raise FileNotFoundError(self.run_dir) # Treat as non-existent directory
info(f"Using existing run at '{tag}' with the '{self.name}' flow.")
# Extract maximum step ordinal + load finished steps
entries_sorted = sorted(
filter(
lambda x: "-" in x and x.split("-", maxsplit=1)[0].isdigit(),
entries,
),
key=lambda x: int(x.split("-", maxsplit=1)[0]),
)
for entry in entries_sorted:
components = entry.split("-", maxsplit=1)
try:
extracted_ordinal = int(components[0])
except ValueError:
continue
if not _no_load_previous_steps:
try:
self.step_objects.append(
Step.load_finished(
os.path.join(self.run_dir, entry),
self.config["PDK_ROOT"],
self.Steps,
)
)
except StepNotFound as e:
raise FlowException(
f"Error while loading concluded step in {entry}: {e}"
)
except FileNotFoundError:
pass
starting_ordinal = max(starting_ordinal, extracted_ordinal + 1)
# Extract Maximum State
if with_initial_state is None:
if latest_json := get_latest_file(self.run_dir, "state_out.json"):
verbose(f"Using state at '{latest_json}'.")
initial_state = State.loads(
open(latest_json, encoding="utf8").read()
)
except NotADirectoryError:
raise FlowException(
f"Run directory for '{tag}' already exists as a file and not a directory."
)
except FileNotFoundError:
info(f"Starting a new run of the '{self.name}' flow with the tag '{tag}'.")
mkdirp(self.run_dir)
# Stored until next start()
self.toolbox = Toolbox(os.path.join(self.run_dir, "tmp"))
for level in ["WARNING", "ERROR"]:
path = os.path.join(self.run_dir, f"{level.lower()}.log")
handler = logging.FileHandler(path, mode="a+")
handler.setLevel(level)
handler.addFilter(LevelFilter([level]))
handlers.append(handler)
register_additional_handler(handler)
path = os.path.join(self.run_dir, "flow.log")
handler = logging.FileHandler(path, mode="a+")
handler.setLevel("VERBOSE")
handlers.append(handler)
register_additional_handler(handler)
try:
self.config_resolved_path = os.path.join(self.run_dir, "resolved.json")
with open(self.config_resolved_path, "w") as f:
f.write(self.config.dumps())
self.progress_bar = FlowProgressBar(
self.name, starting_ordinal=starting_ordinal
)
self.progress_bar.start()
final_state, step_objects = self.run(
initial_state=initial_state,
starting_ordinal=starting_ordinal,
**kwargs,
)
self.progress_bar.end()
# Stored until next start()
self.step_objects += step_objects
return final_state
finally:
self.progress_bar.end()
for registered_handlers in handlers:
deregister_additional_handler(registered_handlers)
if len(warning_handler.warnings):
warn("The following warnings were generated by the flow:")
for record in warning_handler.warnings.values():
warn(f"{record}")
@protected
@abstractmethod
def run(
self,
initial_state: State,
**kwargs,
) -> Tuple[State, List[Step]]:
"""
The core of the Flow. Subclasses of flow are expected to override this
method.
:param initial_state: An initial state object to use.
:returns: A tuple of states and instantiated step objects for inspection.
"""
pass
@protected
def dir_for_step(self, step: Step) -> str:
"""
May only be called while :attr:`run_dir` is not None, i.e., the flow
has started. Otherwise, a :class:`FlowException` is raised.
:returns: A directory within the run directory for a specific step,
prefixed with the current progress bar stage number.
"""
if self.run_dir is None:
raise FlowException(
"Attempted to call dir_for_step on a flow that has not been started."
)
return os.path.join(
self.run_dir,
f"{self.progress_bar.get_ordinal_prefix()}{slugify(step.id)}",
)
@protected
def start_step(
self,
step: Step,
*args,
**kwargs,
) -> State:
"""
A helper function that handles passing parameters to :mod:`Step.start`.'
It is essentially equivalent to:
.. code-block:: python
step.start(
toolbox=self.toolbox,
step_dir=self.dir_for_step(step),
)
See :meth:`Step.start` for more info.
:param step: The step object to run
:param args: Arguments to `step.start`
:param kwargs: Keyword arguments to `step.start`
"""
kwargs["toolbox"] = self.toolbox
kwargs["step_dir"] = self.dir_for_step(step)
return step.start(*args, **kwargs)
@protected
def start_step_async(
self,
step: Step,
*args,
**kwargs,
) -> Future[State]:
"""
An asynchronous equivalent to :meth:`start_step`.
:param step: The step object to run
:param args: Arguments to `step.start`
:param kwargs: Keyword arguments to `step.start`
:returns: A ``Future`` encapsulating a State object, which can be used
as an input to the next step (where the next step will wait for the
``Future`` to be realized before calling :meth:`Step.run`)
"""
kwargs["toolbox"] = self.toolbox
kwargs["step_dir"] = self.dir_for_step(step)
return get_tpe().submit(step.start, *args, **kwargs)
def _save_snapshot_ef(self, path: Union[str, os.PathLike]):
if (
self.step_objects is None
or self.toolbox is None
or self.config_resolved_path is None
):
raise RuntimeError(
"Flow was not run before attempting to save views in the Efabless format."
)
if len(self.step_objects) == 0:
# No steps, no data
return
last_step = self.step_objects[-1]
last_state = last_step.state_out
if last_state is None:
raise FlowException(
f"Misconfigured flow: Step {last_step.id} was appended to step objects without having been run first."
)
# 1. Copy Files
last_state.validate()
info(
f"Saving views in the Efabless/Caravel User Project format to '{os.path.abspath(path)}'…"
)
mkdirp(path)
supported_formats = {
DesignFormat.POWERED_NETLIST: (os.path.join("verilog", "gl"), "v"),
DesignFormat.DEF: ("def", "def"),
DesignFormat.LEF: ("lef", "lef"),
DesignFormat.SDF: (os.path.join("sdf", "multicorner"), "sdf"),
DesignFormat.SPEF: (os.path.join("spef", "multicorner"), "spef"),
DesignFormat.LIB: (os.path.join("lib", "multicorner"), "lib"),
DesignFormat.GDS: ("gds", "gds"),
DesignFormat.MAG: ("mag", "mag"),
}
def visitor(key, value, top_key, _, __):
df = DesignFormat.by_id(top_key)
assert df is not None
if df not in supported_formats:
return
dfo = df.value
assert isinstance(dfo, DesignFormatObject)
subdirectory, extension = supported_formats[df]
target_dir = os.path.join(path, subdirectory)
if not isinstance(value, Path):
if isinstance(value, dict):
assert (
self.toolbox is not None
), "toolbox check was not executed properly"
default_corner_view = self.toolbox.filter_views(self.config, value)
default_corner_target_dir = os.path.dirname(target_dir)
mkdirp(default_corner_target_dir)
if len(default_corner_view) == 1:
target_basename = f"{self.config['DESIGN_NAME']}.{extension}"
target_path = os.path.join(
default_corner_target_dir, target_basename
)
shutil.copyfile(
default_corner_view[0], target_path, follow_symlinks=True
)
else:
for file in default_corner_view:
shutil.copyfile(file, target_dir, follow_symlinks=True)
return
target_basename = os.path.basename(str(value))
target_basename = target_basename[: -len(dfo.extension)] + extension
target_path = os.path.join(target_dir, target_basename)
mkdirp(target_dir)
shutil.copyfile(value, target_path, follow_symlinks=True)
last_state._walk(last_state.to_raw_dict(metrics=False), path, visit=visitor)
# 2. Copy Logs, Reports, & Signoff Information
def copy_dir_contents(from_dir, to_dir, filter="*"):
for file in os.listdir(from_dir):
file_path = os.path.join(from_dir, file)
if os.path.isdir(file_path):
continue
if fnmatch.fnmatch(file, filter):
shutil.copyfile(
file_path, os.path.join(to_dir, file), follow_symlinks=True
)
signoff_folder = os.path.join(
path, "signoff", self.config["DESIGN_NAME"], "openlane-signoff"
)
mkdirp(signoff_folder)
# resolved.json
shutil.copyfile(
self.config_resolved_path,
os.path.join(signoff_folder, "resolved.json"),
follow_symlinks=True,
)
# Logs
mkdirp(signoff_folder)
copy_dir_contents(self.run_dir, signoff_folder, "*.log")
# Step-specific
for step in self.step_objects:
reports_dir = os.path.join(step.step_dir, "reports")
step_imp_id = step.get_implementation_id()
if step_imp_id.endswith("DRC") or step_imp_id.endswith("LVS"):
if os.path.exists(reports_dir):
copy_dir_contents(reports_dir, signoff_folder)
if step_imp_id.endswith("LVS"):
copy_dir_contents(step.step_dir, signoff_folder, "*.log")
if step_imp_id.endswith("CheckAntennas"):
if os.path.exists(reports_dir):
copy_dir_contents(
reports_dir, signoff_folder, "antenna_summary.rpt"
)
if step_imp_id.endswith("STAPostPNR"):
timing_report_folder = os.path.join(signoff_folder, "timing-reports")
mkdirp(timing_report_folder)
copy_dir_contents(step.step_dir, timing_report_folder, "*summary.rpt")
for dir in os.listdir(step.step_dir):
dir_path = os.path.join(step.step_dir, dir)
if not os.path.isdir(dir_path):
continue
target = os.path.join(timing_report_folder, dir)
mkdirp(target)
copy_dir_contents(dir_path, target, "*.rpt")
@deprecated(
version="2.0.0a46",
reason="Use .progress_bar.set_max_stage_count",
action="once",
)
@protected
def set_max_stage_count(self, count: int): # pragma: no cover
"""
Alias for ``self.progress_bar``'s :py:meth:`FlowProgressBar.set_max_stage_count`.
"""
self.progress_bar.set_max_stage_count(count)
@deprecated(
version="2.0.0a46",
reason="Use .progress_bar.start_stage",
action="once",
)
@protected
def start_stage(self, name: str): # pragma: no cover
"""
Alias for ``self.progress_bar``'s :py:meth:`FlowProgressBar.start_stage`.
"""
self.progress_bar.start_stage(name)
@deprecated(
version="2.0.0a46",
reason="Use .progress_bar.end_stage",
action="once",
)
@protected
def end_stage(self, increment_ordinal: bool = True): # pragma: no cover
"""
Alias for ``self.progress_bar``'s :py:meth:`FlowProgressBar.end_stage`.
"""
self.progress_bar.end_stage(increment_ordinal=increment_ordinal)
class FlowFactory(object):
"""
A factory singleton for Flows, allowing Flow types to be registered and then
retrieved by name.
See https://en.wikipedia.org/wiki/Factory_(object-oriented_programming) for
a primer.
"""
__registry: ClassVar[Dict[str, Type[Flow]]] = {}
@classmethod
def register(
Self, registered_name: Optional[str] = None
) -> Callable[[Type[Flow]], Type[Flow]]:
"""
A decorator that adds a flow type to the registry.
:param registered_name: An optional registered name for the flow.
If not specified, the flow will be referred to by its Python
class name.
"""
def decorator(cls: Type[Flow]) -> Type[Flow]:
name = cls.__name__
if registered_name is not None:
name = registered_name
Self.__registry[name] = cls
return cls
return decorator
@classmethod
def get(Self, name: str) -> Optional[Type[Flow]]:
"""
Retrieves a Flow type from the registry using a lookup string.
:param name: The registered name of the Flow. Case-sensitive.
"""
return Self.__registry.get(name)
@classmethod
def list(Self) -> List[str]:
"""
:returns: A list of strings representing all registered flows.
"""
return list(Self.__registry.keys())
factory = FlowFactory
b5a743922This file is pinned to the selected release. Download the original to open it in your design tool.