Skip to content

Utilities

Utility functions and classes which are not specific to anything else in the codebase.

RefreshedProperty

Bases: property

Custom property that calls .refresh() before getting the actual value.

Refreshable()

Bases: abc.ABC

Abstract class that denotes an object that have data that can be refreshed (a TVPaint project for example).

Source code in pytvpaint/utils.py
51
52
def __init__(self) -> None:
    self.refresh_on_call = True

refresh() -> None abstractmethod

Refreshes the object data.

Source code in pytvpaint/utils.py
54
55
56
57
@abstractmethod
def refresh(self) -> None:
    """Refreshes the object data."""
    raise NotImplementedError("Function refresh() needs to be implemented")

Removable()

Bases: pytvpaint.utils.Refreshable

Abstract class that denotes an object that can be removed from TVPaint (a Layer for example).

Source code in pytvpaint/utils.py
63
64
65
def __init__(self) -> None:
    super().__init__()
    self._is_removed: bool = False

is_removed: bool property

Checks if the object is removed by trying to refresh its data.

Returns:

Name Type Description
bool bool

whether if it was removed or not

refresh() -> None

Does a refresh of the object data.

Raises:

Type Description
ValueError

if the object has been mark removed

Source code in pytvpaint/utils.py
74
75
76
77
78
79
80
81
def refresh(self) -> None:
    """Does a refresh of the object data.

    Raises:
        ValueError: if the object has been mark removed
    """
    if self._is_removed:
        raise ValueError(f"{self.__class__.__name__} has been removed!")

remove() -> None abstractmethod

Removes the object in TVPaint.

Source code in pytvpaint/utils.py
96
97
98
99
@abstractmethod
def remove(self) -> None:
    """Removes the object in TVPaint."""
    raise NotImplementedError("Function refresh() needs to be implemented")

mark_removed() -> None

Marks the object as removed and is therefor not usable.

Source code in pytvpaint/utils.py
101
102
103
def mark_removed(self) -> None:
    """Marks the object as removed and is therefor not usable."""
    self._is_removed = True

Renderable()

Bases: abc.ABC

Abstract class that denotes an object that can be removed from TVPaint (a Layer for example).

Source code in pytvpaint/utils.py
109
110
def __init__(self) -> None:
    super().__init__()

current_frame: int abstractmethod property writable

Gives the current frame.

CanMakeCurrent

Bases: typing_extensions.Protocol

Describes an object that can do make_current and has an id.

HasCurrentFrame

Bases: typing_extensions.Protocol

Class that has a current frame property.

current_frame: int property writable

The current frame, clip or project.

get_unique_name(names: Iterable[str], stub: str) -> str

Get a unique name from a list of names and a stub prefix. It does auto increment it.

Parameters:

Name Type Description Default
names collections.abc.Iterable[str]

existing names

required
stub str

the base name

required

Raises:

Type Description
ValueError

if the stub is empty

Returns:

Name Type Description
str str

a unique name with the stub prefix

Source code in pytvpaint/utils.py
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
def get_unique_name(names: Iterable[str], stub: str) -> str:
    """Get a unique name from a list of names and a stub prefix. It does auto increment it.

    Args:
        names (Iterable[str]): existing names
        stub (str): the base name

    Raises:
        ValueError: if the stub is empty

    Returns:
        str: a unique name with the stub prefix
    """
    if not stub:
        raise ValueError("Stub is empty")

    number_re = re.compile(r"(?P<number>\d+)$", re.I)

    stub_without_number = number_re.sub("", stub)
    max_number = 0
    padding_length = 1

    for name in names:
        without_number = number_re.sub("", name)

        if without_number != stub_without_number:
            continue

        res = number_re.search(name)
        number = res.group("number") if res else "1"

        padding_length = max(padding_length, len(number))
        max_number = max(max_number, int(number))

    if max_number == 0:
        return stub

    next_number = max_number + 1
    return f"{stub_without_number}{next_number:0{padding_length}}"

position_generator(fn: Callable[[int], T], stop_when: type[GeorgeError] = GeorgeError) -> Iterator[T]

Utility generator that yields the result of a function according to a position.

Parameters:

Name Type Description Default
fn typing.Callable[[int], pytvpaint.utils.T]

the function to run at each iteration

required
stop_when Type[pytvpaint.george.exceptions.GeorgeError]

exception at which we stop. Defaults to GeorgeError.

pytvpaint.george.exceptions.GeorgeError

Yields:

Type Description
pytvpaint.utils.T

Iterator[T]: an generator of the resulting values

Source code in pytvpaint/utils.py
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
def position_generator(
    fn: Callable[[int], T],
    stop_when: type[GeorgeError] = GeorgeError,
) -> Iterator[T]:
    """Utility generator that yields the result of a function according to a position.

    Args:
        fn (Callable[[int], T]): the function to run at each iteration
        stop_when (Type[GeorgeError], optional): exception at which we stop. Defaults to GeorgeError.

    Yields:
        Iterator[T]: an generator of the resulting values
    """
    pos = 0

    while True:
        try:
            yield fn(pos)
        except stop_when:
            break
        pos += 1

set_as_current(func: Callable[Params, ReturnType]) -> Callable[Params, ReturnType]

Decorator to apply on object methods.

Sets the current TVPaint object as 'current'. Useful when George functions only apply on the current project, clip, layer or scene.

Parameters:

Name Type Description Default
func typing.Callable[pytvpaint.utils.Params, pytvpaint.utils.ReturnType]

the method apply on

required

Returns:

Type Description
typing.Callable[pytvpaint.utils.Params, pytvpaint.utils.ReturnType]

Callable[Params, ReturnType]: the wrapped method

Source code in pytvpaint/utils.py
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
def set_as_current(func: Callable[Params, ReturnType]) -> Callable[Params, ReturnType]:
    """Decorator to apply on object methods.

    Sets the current TVPaint object as 'current'.
    Useful when George functions only apply on the current project, clip, layer or scene.

    Args:
        func (Callable[Params, ReturnType]): the method apply on

    Returns:
        Callable[Params, ReturnType]: the wrapped method
    """

    def wrapper(*args: Params.args, **kwargs: Params.kwargs) -> ReturnType:
        self = cast(CanMakeCurrent, args[0])
        self.make_current()
        return func(*args, **kwargs)

    return wrapper

render_context(alpha_mode: george.AlphaSaveMode | None = None, background_mode: george.BackgroundMode | None = None, save_format: george.SaveFormat | None = None, format_opts: list[str] | None = None, layer_selection: list[Layer] | None = None) -> Generator[None, None, None]

Context used to do renders in TVPaint.

It does the following things:

  • Set the alpha mode and save format (with custom options)
  • Hide / Show the given layers (some render functions only render by visibility)
  • Restore the previous values after rendering

Parameters:

Name Type Description Default
alpha_mode pytvpaint.george.AlphaSaveMode | None

the render alpha save mode

None
save_format pytvpaint.george.SaveFormat | None

the render format to use. Defaults to None.

None
background_mode pytvpaint.george.BackgroundMode | None

the render background mode

None
format_opts list[str] | None

the custom format options as strings. Defaults to None.

None
layer_selection list[pytvpaint.layer.Layer] | None

the layers to render. Defaults to None.

None
Source code in pytvpaint/utils.py
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
@contextlib.contextmanager
def render_context(
    alpha_mode: george.AlphaSaveMode | None = None,
    background_mode: george.BackgroundMode | None = None,
    save_format: george.SaveFormat | None = None,
    format_opts: list[str] | None = None,
    layer_selection: list[Layer] | None = None,
) -> Generator[None, None, None]:
    """Context used to do renders in TVPaint.

    It does the following things:

    - Set the alpha mode and save format (with custom options)
    - Hide / Show the given layers (some render functions only render by visibility)
    - Restore the previous values after rendering

    Args:
        alpha_mode: the render alpha save mode
        save_format: the render format to use. Defaults to None.
        background_mode: the render background mode
        format_opts: the custom format options as strings. Defaults to None.
        layer_selection: the layers to render. Defaults to None.
    """
    from pytvpaint.clip import Clip

    # Save the current state
    pre_alpha_save_mode = george.tv_alpha_save_mode_get()
    pre_save_format, pre_save_args = george.tv_save_mode_get()
    pre_background_mode, pre_background_colors = george.tv_background_get()

    # Set the save mode values
    if alpha_mode:
        george.tv_alpha_save_mode_set(alpha_mode)
    if background_mode:
        george.tv_background_set(background_mode)
    if save_format:
        george.tv_save_mode_set(save_format, *(format_opts or []))

    layers_visibility = []
    if layer_selection:
        clip = Clip.current_clip()
        layers_visibility = [(layer, layer.is_visible) for layer in clip.layers]
        # Show and hide the clip layers to render
        for layer, _ in layers_visibility:
            should_be_visible = not layer_selection or layer in layer_selection
            layer.is_visible = should_be_visible

    # Do the render
    yield

    # Restore the previous values
    if alpha_mode:
        george.tv_alpha_save_mode_set(pre_alpha_save_mode)
    if save_format:
        george.tv_save_mode_set(pre_save_format, *pre_save_args)
    if background_mode:
        george.tv_background_set(pre_background_mode, pre_background_colors)

    # Restore the layer visibility
    if layers_visibility:
        for layer, was_visible in layers_visibility:
            layer.is_visible = was_visible

restore_current_frame(tvp_element: HasCurrentFrame, frame: int) -> Generator[None, None, None]

Context that temporarily changes the current frame to the one provided and restores it when done.

Parameters:

Name Type Description Default
tvp_element pytvpaint.utils.HasCurrentFrame

clip to change

required
frame int

frame to set. Defaults to None.

required
Source code in pytvpaint/utils.py
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
@contextlib.contextmanager
def restore_current_frame(
    tvp_element: HasCurrentFrame, frame: int
) -> Generator[None, None, None]:
    """Context that temporarily changes the current frame to the one provided and restores it when done.

    Args:
        tvp_element: clip to change
        frame: frame to set. Defaults to None.
    """
    previous_frame = tvp_element.current_frame

    if frame != previous_frame:
        tvp_element.current_frame = frame

    yield

    if tvp_element.current_frame != previous_frame:
        tvp_element.current_frame = previous_frame

get_tvp_element(tvp_elements: Iterator[TVPElementType], by_id: int | str | None = None, by_name: str | None = None, by_path: str | Path | None = None) -> TVPElementType | None

Search for a TVPaint element by attributes.

Parameters:

Name Type Description Default
tvp_elements collections.abc.Iterator[pytvpaint.utils.TVPElementType]

a collection of TVPaint objects

required
by_id int | str | None

search by id. Defaults to None.

None
by_name str | None

search by name, search is case-insensitive. Defaults to None.

None
by_path str | pathlib.Path | None

search by path. Defaults to None.

None

Raises:

Type Description
ValueError

if bad arguments were given

Returns:

Type Description
pytvpaint.utils.TVPElementType | None

TVPElementType | None: the found element

Source code in pytvpaint/utils.py
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
def get_tvp_element(
    tvp_elements: Iterator[TVPElementType],
    by_id: int | str | None = None,
    by_name: str | None = None,
    by_path: str | Path | None = None,
) -> TVPElementType | None:
    """Search for a TVPaint element by attributes.

    Args:
        tvp_elements: a collection of TVPaint objects
        by_id: search by id. Defaults to None.
        by_name: search by name, search is case-insensitive. Defaults to None.
        by_path: search by path. Defaults to None.

    Raises:
        ValueError: if bad arguments were given

    Returns:
        TVPElementType | None: the found element
    """
    if by_id is None and by_name is None:
        raise ValueError(
            "At least one of the values (id or name) must be provided, none found !"
        )

    for element in tvp_elements:
        if by_id is not None and element.id != by_id:
            continue
        if by_name is not None and element.name.lower() != by_name.lower():
            continue
        if by_path is not None and getattr(element, "path") != Path(by_path):
            continue
        return element

    return None

handle_output_range(output_path: Path | str | FileSequence, default_start: int, default_end: int, start: int | None = None, end: int | None = None) -> tuple[FileSequence, int, int, bool, bool]

Handle the different options for output paths and range.

Whether the user provides a range (start-end) or a filesequence with a range or not, this functions ensures we always end up with a valid range to render

Parameters:

Name Type Description Default
output_path pathlib.Path | str | fileseq.filesequence.FileSequence

user provided output path

required
default_start int

the default start to use if none provided or found in the file sequence object

required
default_end int

the default end to use if none provided or found in the file sequence object

required
start int | None

user provided start frame or None

None
end int | None

user provided end frame or None

None

Returns:

Name Type Description
file_sequence fileseq.filesequence.FileSequence

output path as a FileSequence object

start int

computed start frame

end int

computed end frame

is_sequence bool

whether the output is a sequence or not

is_image bool

whether the output is an image or not (a movie)

Source code in pytvpaint/utils.py
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
def handle_output_range(
    output_path: Path | str | FileSequence,
    default_start: int,
    default_end: int,
    start: int | None = None,
    end: int | None = None,
) -> tuple[FileSequence, int, int, bool, bool]:
    """Handle the different options for output paths and range.

    Whether the user provides a range (start-end) or a filesequence with a range or not, this functions ensures we
    always end up with a valid range to render

    Args:
        output_path: user provided output path
        default_start: the default start to use if none provided or found in the file sequence object
        default_end: the default end to use if none provided or found in the file sequence object
        start: user provided start frame or None
        end: user provided end frame or None

    Returns:
        file_sequence: output path as a FileSequence object
        start: computed start frame
        end: computed end frame
        is_sequence: whether the output is a sequence or not
        is_image: whether the output is an image or not (a movie)
    """
    # we handle all outputs as a FileSequence, makes it a bit easier to handle ranges and padding
    if not isinstance(output_path, FileSequence):
        file_sequence = FileSequence(Path(output_path).as_posix())
    else:
        file_sequence = output_path

    frame_set = file_sequence.frameSet()
    is_image = george.SaveFormat.is_image(file_sequence.extension())

    # if the provided sequence has a range, and we don't, use the sequence range
    if frame_set and len(frame_set) >= 1 and is_image:
        start = start or file_sequence.start()
        end = end or file_sequence.end()

    # check characteristics of file sequence
    fseq_has_range = frame_set and len(frame_set) > 1
    fseq_is_single_image = frame_set and len(frame_set) == 1
    fseq_no_range_padding = not frame_set and file_sequence.padding()
    range_is_seq = start is not None and end is not None and start != end
    range_is_single_image = start is not None and end is not None and start == end

    is_single_image = bool(
        is_image and (fseq_is_single_image or not frame_set) and range_is_single_image
    )
    is_sequence = bool(
        is_image and (fseq_has_range or fseq_no_range_padding or range_is_seq)
    )

    # if no range provided, use clip mark in/out, if none, use clip start/end
    if start is None:
        start = default_start
    if is_single_image and not end:
        end = start
    else:
        if end is None:
            end = default_end

    frame_set = FrameSet(f"{start}-{end}")

    if not file_sequence.padding() and is_image and len(frame_set) > 1:
        file_sequence.setPadding("#")

    # we should have a range by now, set it in the sequence
    if (is_image and not is_single_image) or file_sequence.padding():
        file_sequence.setFrameSet(frame_set)

    return file_sequence, start, end, is_sequence, is_image