Skip to content

Toolbox

MODULE DESCRIPTION
p
CLASS DESCRIPTION
Progressor

A more configurable Progressor class that supports nesting.

Tool
FUNCTION DESCRIPTION
progressor

Initialize a progress bar for an iterable

safe_load

Safely load in tools to a toolbox placing all failed imports in a Broken Tools category

toolify

Convert a typed function into a tool for the specified Toolbox class

Progressor

Progressor(
    it: Iterable[T],
    message: str = '...',
    *,
    mode: ProgressorMode
    | Iterable[ProgressorMode] = 'auto',
    lazy: bool = False,
    length_hint: int | None = None,
    bar_width: int = 10,
)

A more configurable Progressor class that supports nesting.

Note

To allow for progress position, the input iterable is consumed into a list. If this is not what you want, it's best to handroll a progressor flow.

Example
import time
a = Progressor(range(10), 'A')
b = Progressor(range(10), 'B')
c = Progressor(range(10), 'C')
for i in a:
    time.sleep(0.1)
    for j in b:
        time.sleep(0.1)
        for k in c:
            time.sleep(0.1)

Each loop will initialize a new progressor and maintain its position.

PARAMETER DESCRIPTION

it

Iterable to create a Progressor for.

TYPE: Iterable[T]

message

The message to display in the progressor message box. (default: '...')

TYPE: str DEFAULT: '...'

mode

Display a count/percent in the progressor message box 'hide' shows message only. (default: 'count')

TYPE: ProgressorMode | Iterable[ProgressorMode] DEFAULT: 'auto'

lazy

Don't immediately consume the iterable and instead use __length_hint__ or length_hint param. (default: False)

TYPE: bool DEFAULT: False

length_hint

If the iterable has not __len__ or __length_hint__, use this as the iterable length. (default: None)

TYPE: int | None DEFAULT: None

Note

If you set lazy or length_hint, the progressor will lazily iterate the iterable. Excluding length_hint when lazy is set with an iterable with no __length_hint__ attribute will cause the progress bar to set the max position to 1. Alternatively, setting length_hint and not setting lazy will automatically make the progressor lazy.

METHOD DESCRIPTION
global_position

Get the position of the innermost (most recent) progressor on the stack

reset_progressor

Alias for ResetProgressor

root

Get the outermost progressor (this one drives the window progress bar)

set_label

Alias for SetProgressorLabel

set_position

Alias for SetProgressorPosition

set_progressor

Alias for SetProgressor

ATTRIBUTE DESCRIPTION
position

Get the position of this progressor

TYPE: int

Source code in src/arcpie/toolbox.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
def __init__(
    self,
    it: Iterable[T],
    message: str = '...',
    *,
    mode: ProgressorMode | Iterable[ProgressorMode] = 'auto',
    lazy: bool = False,
    length_hint: int | None = None,
    bar_width: int = 10,
) -> None:
    """
    Args:
        it: Iterable to create a Progressor for.
        message: The message to display in the progressor message box. (default: ``'...'``)
        mode: Display a count/percent in the progressor message box ``'hide'`` shows message only. (default: ``'count'``)
        lazy: Don't immediately consume the iterable and instead use `__length_hint__` *or* `length_hint` param. (default: ``False``)
        length_hint: If the iterable has not ``__len__`` or ``__length_hint__``, use this as the iterable length. (default: ``None``)

    Note:
        If you set ``lazy`` *or* ``length_hint``, the progressor will lazily iterate the iterable.
        Excluding ``length_hint`` when ``lazy`` is set with an iterable with no ``__length_hint__``
        attribute will cause the progress bar to set the max position to 1.
        Alternatively, setting ``length_hint`` and not setting ``lazy`` will automatically make the progressor lazy.
    """
    if not lazy and length_hint is None:
        self.it = list(it)
        self.it_len = len(self.it)
    else:
        self.it = it
        self.it_len = length_hint or operator.length_hint(it, 1)
    self._message = self.__message = message
    self.mode = [mode] if isinstance(mode, str) else list(mode)
    if mode == 'auto':
        mode = type(self).default_mode
    self._bar_width = bar_width
    self._pos = 0

position property writable

position: int

Get the position of this progressor

global_position classmethod

global_position() -> int

Get the position of the innermost (most recent) progressor on the stack

Source code in src/arcpie/toolbox.py
230
231
232
233
234
235
@classmethod
def global_position(cls: type[Progressor[Any]]) -> int:
    """Get the position of the innermost (most recent) progressor on the stack"""
    if not cls.stack:
        return 0
    return cls.stack[-1].position

reset_progressor classmethod

reset_progressor() -> None

Alias for ResetProgressor

Source code in src/arcpie/toolbox.py
251
252
253
254
@classmethod
def reset_progressor(cls: type[Progressor[Any]]) -> None:
    """Alias for ResetProgressor"""
    ResetProgressor()

root classmethod

root() -> Progressor[Any]

Get the outermost progressor (this one drives the window progress bar)

Source code in src/arcpie/toolbox.py
241
242
243
244
@classmethod
def root(cls: type[Progressor[Any]]) -> Progressor[Any]:
    """Get the outermost progressor (this one drives the window progress bar)"""
    return cls('') if not cls.stack else cls.stack[0]

set_label classmethod

set_label(label: str) -> None

Alias for SetProgressorLabel

Source code in src/arcpie/toolbox.py
261
262
263
264
@classmethod
def set_label(cls: type[Progressor[Any]], label: str) -> None:
    """Alias for SetProgressorLabel"""
    SetProgressorLabel(label)

set_position classmethod

set_position(pos: int) -> None

Alias for SetProgressorPosition

Source code in src/arcpie/toolbox.py
256
257
258
259
@classmethod
def set_position(cls: type[Progressor[Any]], pos: int) -> None:
    """Alias for SetProgressorPosition"""
    SetProgressorPosition(pos)

set_progressor classmethod

set_progressor(msg: str, rng: range) -> None

Alias for SetProgressor

Source code in src/arcpie/toolbox.py
246
247
248
249
@classmethod
def set_progressor(cls, msg: str, rng: range) -> None:
    """Alias for SetProgressor"""
    SetProgressor('step', msg, rng.start, rng.stop, rng.step)

Tool

Tool()

Bases: ToolABC

ATTRIBUTE DESCRIPTION
project

Get the current project that the tool is running in if it exists (otherwise: None)

TYPE: Project

Source code in src/arcpie/toolbox.py
45
46
47
48
def __init__(self) -> None:
    self.label: str = self.__class__.__name__
    self.description: str = self.__doc__ or 'No Descrption Provided'
    self.category: str | None = None

project property

project: Project

Get the current project that the tool is running in if it exists (otherwise: None)

progressor

progressor(
    it: Iterable[T], msg: str = '...'
) -> Iterable[T]

Initialize a progress bar for an iterable

PARAMETER DESCRIPTION

it

The iterable to initialize a progressor for

TYPE: Iterable[T]

msg

The message to show in the progress bar label area

TYPE: str DEFAULT: '...'

Example
for parcel in progressor(parcels, 'Gathering'):
    ...
Note

The iterable is consumed on progressor initialization to allow a size check. Each iteration of the progressor will update the label area with a count in the format: {msg}: [{idx}/{total}]

Source code in src/arcpie/toolbox.py
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
def progressor[T](it: Iterable[T], msg: str = '...') -> Iterable[T]:
    """Initialize a progress bar for an iterable

    Args:
        it: The iterable to initialize a progressor for
        msg: The message to show in the progress bar label area

    Example:
        ```python
        for parcel in progressor(parcels, 'Gathering'):
            ...
        ```

    Note:
        The iterable is consumed on progressor initialization to allow a size check.
         Each iteration of the progressor will update the label area with a count in the
         format: `{msg}: [{idx}/{total}]`
    """
    ResetProgressor()
    it = list(it)
    length = len(it)
    SetProgressor('step', msg, 0, length, 1)
    for idx, item in enumerate(it, start=1):
        SetProgressorLabel(f'{msg}: [{idx}/{length}]')
        yield item
        SetProgressorPosition()
    ResetProgressor()

safe_load

safe_load(
    tools: dict[str, list[str]],
    *,
    scope: dict[str, Any] | None = None,
    reload_module: bool = False,
) -> list[type[ToolABC]]

Safely load in tools to a toolbox placing all failed imports in a Broken Tools category

PARAMETER DESCRIPTION

tools

A mapping of tool modules to tool files or tool classes in a module

TYPE: dict[str, list[str]]

scope

The globals() dict for the Toolbox scope (required so loading happend in Toolbox scope)

TYPE: dict[str, Any] DEFAULT: None

reload_module

Reload the module after importing (default: False)

TYPE: bool DEFAULT: False

RETURNS DESCRIPTION
list[type[ToolABC]]

A list of tool classes

Note

To isolate bugs in a large toolbox, it is reccomended that you use file/module level importing with matching toolclass names (e.g. Tool.py -> class Tool) where the Tool mapping is: Tools = {'tools': ['Tool.py']} instead of: Tools = {'tools.Tool': ['Tool']} This allows the import to fail softly on a single tool instead of breaking imports for all other toolclasses in that Tool.py module.

Any tools with errors will be placed in a Broken Tools category in the toolbox with the full stack trace placed in its description field. The final component of the exception will be placed at the end of the tool label for easy debugging.

Example
>>> # File import
>>> Tools = {
...     # import matching class from named file in a submodule (tools)
...     # where ToolFileA is ToolFileA.py with a class ToolFileA
...     'tools': ['ToolFileA', 'ToolFileB'],
...     # Import explicit ToolClass from toolfile (MyTools.py)
...     # NOT RECOMMENDED
...     'tools.MyTools': ['ToolClassA', 'ToolClassB'],
... }
>>> tools = safe_import(globals(), Tools)
Source code in src/arcpie/toolbox.py
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
def safe_load(tools: dict[str, list[str]],
              *,
              scope: dict[str, Any] | None = None,
              reload_module: bool = False) -> list[type[ToolABC]]:
    """Safely load in tools to a toolbox placing all failed imports in a `Broken Tools` category

    Args:
        tools (dict[str, list[str]]): A mapping of tool modules to tool files or tool classes in a module
        scope (dict[str, Any]): The `globals()` dict for the Toolbox scope (required so loading happend in Toolbox scope)
        reload_module (bool): Reload the module after importing (default: False)

    Returns:
        ( list[type[ToolABC]] ): A list of tool classes

    Note:
        To isolate bugs in a large toolbox, it is reccomended that you use file/module level importing
        with matching toolclass names (e.g. `Tool.py -> class Tool`) where the Tool mapping is:
            `Tools = {'tools': ['Tool.py']}`
        instead of:
            `Tools = {'tools.Tool': ['Tool']}`
        This allows the import to fail softly on a single tool instead of breaking imports for all other
        toolclasses in that `Tool.py` module.

        Any tools with errors will be placed in a `Broken Tools` category in the toolbox with the full
        stack trace placed in its description field. The final component of the exception will be placed
        at the end of the tool label for easy debugging.

    Example:
        ```python
        >>> # File import
        >>> Tools = {
        ...     # import matching class from named file in a submodule (tools)
        ...     # where ToolFileA is ToolFileA.py with a class ToolFileA
        ...     'tools': ['ToolFileA', 'ToolFileB'],
        ...     # Import explicit ToolClass from toolfile (MyTools.py)
        ...     # NOT RECOMMENDED
        ...     'tools.MyTools': ['ToolClassA', 'ToolClassB'],
        ... }
        >>> tools = safe_import(globals(), Tools)
        ```
    """
    tb_tools = [
        _get_tool(module, tool_name, reload_module)
        for module in tools
        for tool_name in tools[module]
    ]
    if scope:
        scope.update({tool.__name__: tool for tool in tb_tools})
    return tb_tools

toolify

toolify(
    *tool_registries: list[type[ToolABC]],
    name: str | None = None,
    params: ParameterTypeMap | None = None,
    debug: bool = False,
    logger: Logger | None = None,
)

Convert a typed function into a tool for the specified Toolbox class

PARAMETER DESCRIPTION

*tool_registries

The tool registry lists to add this tool to

TYPE: list[type[ToolABC]] DEFAULT: ()

name

The name of the tool

TYPE: str DEFAULT: None

params

A mapping of parameter names to Parameter types and a callable constructor that converts the parameter to the expected value for the function parameter. You can also pass a fully formed arcpy.Parameter object as the first item in the tuple instead of a simple type

TYPE: ParameterTypeMap DEFAULT: None

debug

Print the converted arguments to the ArcGIS Pro message console (default: False)

TYPE: bool DEFAULT: False

logger

An optional logger to use for logging all runs of the toolified tool

TYPE: Logger | None DEFAULT: None

Usage
>>> @toolify(
>>>     TOOL_REGISTRY,
>>>     name='PDF Exporter',
>>>     params={
>>>         'project': ('DEFile', lambda p: Project(p.valueAsText)),
>>>         'outfile': ('DEFile', lambda p: Path(p.valueAsText))
>>>     }
>>> )
>>> def export_pdf(project: Project|str='CURRENT', outfile: Path|str='out.pdf') -> None:
>>>     ...
Source code in src/arcpie/toolbox.py
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
def toolify(*tool_registries: list[type[ToolABC]],
            name: str | None = None,
            params: ParameterTypeMap | None = None,
            debug: bool = False,
            logger: Logger | None = None,
):
    """Convert a typed function into a tool for the specified Toolbox class

    Args:
        *tool_registries (list[type[ToolABC]]): The tool registry lists to add this tool to
        name (str): The name of the tool
        params (ParameterTypeMap): A mapping of parameter names to Parameter types and a callable constructor
            that converts the parameter to the expected value for the function parameter. You can also pass
            a fully formed arcpy.Parameter object as the first item in the tuple instead of a simple type
        debug (bool): Print the converted arguments to the ArcGIS Pro message console (default: False)
        logger (Logger|None): An optional logger to use for logging all runs of the toolified tool

    Usage:
        ```python
        >>> @toolify(
        >>>     TOOL_REGISTRY,
        >>>     name='PDF Exporter',
        >>>     params={
        >>>         'project': ('DEFile', lambda p: Project(p.valueAsText)),
        >>>         'outfile': ('DEFile', lambda p: Path(p.valueAsText))
        >>>     }
        >>> )
        >>> def export_pdf(project: Project|str='CURRENT', outfile: Path|str='out.pdf') -> None:
        >>>     ...
        ```
    """
    def _builder(func: Callable[..., Any]):

        @wraps(func)
        def _execute(*args: Any, **kwargs: Any):
            return func(*args, **kwargs)

        # Build the tool class
        label = func.__name__.replace('_', ' ').title()
        description = func.__doc__ or 'No Description Provided'
        class_name = label.replace(' ', '')
        sig = inspect.signature(func)
        sig_params = sig.parameters

        # Handle parameter converison
        def _passthrough_execution(self: ToolABC, parameters: Parameters | list[Parameter], messages: Any) -> None:
            if debug:
                print(f'Executing toolified {func.__name__} via {self.label}')
            args, kwargs = _read_params(parameters, sig_params, params or {})
            start = time.time()
            try:
                if debug:
                    print(f"Using *{args}, **{kwargs}")
                res = _execute(*args, **kwargs)
                end = time.time()
                if logger:
                    logger.info(f'[{datetime.isoformat(datetime.now(tz=dt.UTC))}] PASS "{self.label}" ({end - start:0.2f} seconds) [{res}]')
            except Exception as e:
                print(f'Something went wrong!:\n\t{traceback.format_exc()}', severity='ERROR')
                end = time.time()
                if logger:
                    logger.info(f'[{datetime.isoformat(datetime.now(tz=dt.UTC))}] FAIL "{self.label}" ({end - start:0.2f} seconds) [{e}] ')

        def _local_build_params(self: ToolABC) -> Parameters | list[Parameter]:
            return _build_params(sig_params, params or {})

        def _local_init(self: ToolABC) -> None:
            self.label = name or label
            self.description = description

        tool_class = type(
            class_name,
            (ToolABC, ),
            {
                '__init__': _local_init,
                'getParameterInfo': _local_build_params,
                'execute': _passthrough_execution
            }
        )

        for registry in tool_registries:
            if tool_class.__name__ not in (c.__name__ for c in registry):
                registry.append(tool_class)
                globals()[class_name] = tool_class
        return _execute
    return _builder