Skip to content

[Editorial P2.3]: Shared sequence path helpers - #69

Draft
jakubjezek001 wants to merge 5 commits into
feature/14-otio-timefrom
feature/15-otio-paths
Draft

jakubjezek001 wants to merge 5 commits into
feature/14-otio-timefrom
feature/15-otio-paths

Conversation

@jakubjezek001

@jakubjezek001 jakubjezek001 commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

Changelog Description

New shared module ayon_editorial.otio.paths: one parser and normaliser for image sequence paths in the notations %04d, [0001-1008], #### and a concrete frame (plate.1001.exr).

Additional info

Replaces the copies in Hiero, Resolve and Flame. A concrete frame number is only read when asked (allow_frame_numbers=True, reformat_frame_filename), so a movie clip_0010.mov is never taken as frame 10. get_frame takes the accepted extensions as an argument instead of importing ayon-core's IMAGE_EXTENSIONS.

Deliberate differences from the host copies (each has a test):

  • Hiero reads the last number in the whole path as the padding, so plate.%04d.v02 became %02d. The shared code reads it from the token.
  • Flame rewrites the first .<digits>. but measures padding on the last; the shared code uses the number before the extension for both.
  • A malformed [...] is left alone where Resolve raised IndexError.

Testing notes:

  1. ./tools/manage.sh run-tests (tests/test_otio_paths.py)

Dependency

Close #15

Checklist

  • ruff check and pytest pass (Python 3.9 with OTIO 0.15, 0.16, 0.17 and 0.18)
  • python create_package.py --skip-zip builds
  • No frozen contract changed
  • Settings key renamed only with a conversion function (none renamed)
  • Layer rules kept (tests/test_dcc_agnostic.py)

@jakubjezek001
jakubjezek001 added this pull request to stack #72 October 29, 2026 15:32
@jakubjezek001 jakubjezek001 changed the title Add the shared sequence path helpers [Editorial P2.3]: Shared sequence path helpers Oct 29, 2026
@jakubjezek001 jakubjezek001 added the type: feature Adding something new and exciting to the product label Oct 29, 2026
@jakubjezek001
jakubjezek001 marked this pull request as draft October 30, 2026 13:40
@jakubjezek001

jakubjezek001 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

Cross-review finding 69-1: docstring says the notation closest to the end wins; the code picks by fixed priority, so a folder token hijacks the path

client/ayon_editorial/otio/paths.py:96-110 (docstring of parse_sequence_path) vs :112-157 (body).

The docstring: "The notation closest to the end of the path wins, so folders with brackets or hashes do not matter." The body only takes the last match inside one notation and then tries printf, bracket, hash, frame in that order. A folder with a higher-priority token beats the file's own notation, and the result is a corrupted path:

reformat_path("/shots/[010-020]/plate.####.exr")      -> '/shots/%03d/plate.####.exr'   (expected '/shots/[010-020]/plate.%04d.exr')
reformat_path("/shots/%04d/plate.[0001-1008].exr")    -> '/shots/%04d/plate.[0001-1008].exr' (file bracket untouched)
get_padding("/shots/[010-020]/plate.####.exr")        -> 3  (expected 4)

test_sequence_in_last_path_part_wins only covers bracket-vs-bracket, which is the one mixed case the priority order does not break. Fix: collect the last match of every notation and pick the one with the greatest start() (or only search the basename), and add mixed-notation tests.

Severity: should-fix.

@jakubjezek001

jakubjezek001 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

Cross-review finding 69-2: bare # and %…d are read as sequence tokens in ordinary file names (regression vs Hiero/Resolve)

client/ayon_editorial/otio/paths.py:47-49 (_PRINTF_RE = %(\d*)d, _HASH_RE = #+) used by reformat_path / get_padding.

The Resolve copies only look at [, the Hiero copy only at %; neither touches #. The shared functions treat any # as a one-digit sequence token, so a plain movie with a # in its name is rewritten and reported as a sequence:

reformat_path("/shots/Shot #1 final.mov")  -> '/shots/Shot %01d1 final.mov'
get_padding("/shots/Shot #1 final.mov")    -> 1
get_padding("/shots/#12/plate.mov")        -> 1      (hash in a folder)
get_padding("/shots/My%20data.mov")        -> 20     (URL-encoded space followed by 'd')
reformat_path("/shots/My%20data.mov")      -> '/shots/My%020data.mov'

davinci_export.create_otio_reference does if padding: metadata.update({"isSequence": True, "padding": padding}) on get_padding_from_path(path), so a Resolve movie called Shot #1.mov would be flagged as an image sequence once Resolve adopts this module. The %20d case was already wrong in Hiero, but # is new and is not in the PR's list of deliberate differences. Require a token that ends the stem (#+ directly before the extension, %\d*d between ./_ and the extension) or take the notation only from the basename, and add negative tests (Shot #1.mov, My%20data.mov).

Severity: should-fix.

@jakubjezek001

jakubjezek001 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

Cross-review finding 69-3: first=True is silently ignored with the default padded=True; the ayon_resolve/api/lib.py variant (default padded=False) is not characterized

client/ayon_editorial/otio/paths.py:208-238 (reformat_path), module docstring line 3 claims to replace the ayon_resolve/api/lib.py copy.

ayon_resolve/api/lib.py:1276 is get_reformated_path(path, padded=False, first=False) and its only caller, lib.py:335, relies on that default: get_reformated_path(_mpi_name, first=True). The shared function defaults to padded=True and applies first only when not padded:

reformat_path("plate.[0001-1008].exr", first=True)               -> 'plate.%04d.exr'   (lib.py variant: 'plate.0001.exr')
reformat_path("plate.[0001-1008].exr", padded=False, first=True) -> 'plate.0001.exr'

test_reformat_path_resolve_variant parametrizes padded and first explicitly and only against the otio/utils.py copy, so the differing default and the lib.py copy are never compared; a straight swap of the import at lib.py:335 makes fname in _mpi_name stop matching. Either make first=True win over padded (that is what a caller that passes first=True means), or state in the docstring and the PR body that the lib.py default differs, and add a characterization test of the lib.py copy.

Severity: should-fix.

@jakubjezek001

jakubjezek001 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

Cross-review finding 69-4: Hiero's frame-number delimiters (-, space, any non-alphanumeric) are dropped without being listed as a difference

client/ayon_editorial/otio/paths.py:51 (_FRAME_RE = [._](\d+)(?=\.[^./\\]*$)) vs ayon_hiero/api/lib.py:1304 get_sequence_pattern_and_padding, whose regex is (?<=[^a-zA-Z0-9])(\d+)(?=\.\w+$).

Hiero accepts any non-alphanumeric before the digits; the shared module accepts only . and _:

hiero_sequence_pattern_and_padding("plate-0001.exr")  -> ('0001', 4)
hiero_sequence_pattern_and_padding("plate 0001.exr")  -> ('0001', 4)
get_padding("plate-0001.exr", allow_frame_numbers=True) -> None
get_padding("plate 0001.exr", allow_frame_numbers=True) -> None

test_pattern_and_padding_hiero_variant only has . and _ inputs, so the characterization test passes while the behaviour differs, and the PR body's list of deliberate differences does not mention it (it also drops the found pattern string Hiero returns, which has no counterpart in the new API; no caller of it exists in ayon-hiero today). Either widen the delimiter class ([^A-Za-z0-9] before the digits) or list this as a deliberate difference with a test.

Severity: minor.

@jakubjezek001

jakubjezek001 commented Oct 30, 2026 •

Copy link
Copy Markdown
Member Author

Cross-review finding 69-5: get_frame(extensions=".exr") silently returns None for every file

client/ayon_editorial/otio/paths.py:182-205.

The annotation is Optional[Iterable[str]], and a str is an Iterable[str], so set(".exr") becomes {'.', 'e', 'x', 'r'}:

get_frame("plate.1001.exr", ".exr")   -> None     (expected '1001')
get_frame("plate.1001.exr", {".EXR"}) -> None     (docs say lower case; an upper-case entry never matches)

ayon-core's IMAGE_EXTENSIONS is a set, so the documented use works, but a single extension string is the obvious call from a host and fails silently with no error. Accept str (wrap it in a tuple) and lower-case the entries, or reject str with a TypeError. Also set(extensions) is rebuilt on every call.

Severity: minor.

One parser and normaliser for printf, bracket, hash and concrete-frame
notations, replacing the Hiero, Resolve and Flame copies. Host copies are
reproduced in the tests; the deliberate differences are tested and noted.
Cross-review findings 69-1 to 69-5: the nearest notation wins as
documented, a # inside a name is not a sequence, reformat_path says that
first needs padded=False and has a test for the Resolve api/lib call,
get_frame reads Hiero's separators and takes a str or any-case extensions.
@jakubjezek001

Copy link
Copy Markdown
Member Author

Author answers to 69-1 to 69-5: all fixed in effe2c1, each with tests in tests/test_otio_paths.py.

  • 69-1: the notation whose match is closest to the end now wins, as documented: /shots/[010-020]/plate.####.exr -> /shots/[010-020]/plate.%04d.exr (and the reverse case).
  • 69-2: a # run counts only when it is not touching letters or digits (plate.####.exr, plate_####.exr); Shot #1 final.mov, take#3.wav stay plain. Hiero's own get_sequence_pattern_and_padding reads such a # too, but the Resolve copies never did and Resolve would flag a movie as a sequence, so the stricter rule is the safe union. I left %d/%20d as is: % followed by digits and d is a printf token in every host copy.
  • 69-3: confirmed. first only applies when padded=False, like ayon_resolve/otio/utils.py. The docstring now says the ayon_resolve/api/lib.py:335 caller must pass padded=False, first=True, and that copy is now in the tests verbatim (resolve_api_reformated_path) and compared.
  • 69-4: get_frame / frame-number reading now accepts any non-alphanumeric separator before the digits like Hiero (plate-0001.exr, plate 0001.exr), which is a superset of Flame's . and _. It stays opt-in.
  • 69-5: get_frame takes a single str extension and compares both sides case-insensitively.

@jakubjezek001

Copy link
Copy Markdown
Member Author

Cross-review round 2 on 69-2: the new hash rule is stricter than both hosts that read #

After effe2c1, _HASH_RE (paths.py, (?<![A-Za-z0-9#])#+(?![A-Za-z0-9#])) requires the run of # to touch no letter or digit. The #1 false positives are fixed (Shot #1 final.mov, /shots/#12/plate.mov stay plain, verified). But the answer calls this "the safe union", and it is the intersection for names where the hashes touch a letter:

reformat_path("plate####.exr")  -> 'plate####.exr'   get_padding -> None

Traypublisher's CSV ingest treats any # run in the basename as padding (ayon_traypublisher/.../create_csv_ingest.py:1301-1304, "#" in basename then replace("#" * padding, ...)), and Hiero's (#+) also matches plate####.exr. So a CSV or Hiero path written without a separator before the hashes no longer normalises, and the class docstring still lists Traypublisher CSV as a user. Either allow #{2,} to touch letters (a single # stays the ambiguous case) or list the narrower rule in the PR body as a deliberate difference with a test for plate####.exr.

%d / %20d left as is: accepted, the Hiero copy behaves the same.

Severity: minor.

Cross-review round 2 on 69-2: Hiero and the Traypublisher CSV ingest write
it; a # followed by a letter, digit or space is still a plain name.
@jakubjezek001

Copy link
Copy Markdown
Member Author

Author answer to the round 2 remark on 69-2 (plate####.exr): fixed in the new commit on this PR (head of feature/15-otio-paths). Agreed that the "safe union" wording was wrong. The rule is now a # run that ends the token (followed by ., _, -, a path separator or the end): plate.####.exr, plate_####.exr and plate####.exr are sequences; Shot #1 final.mov, take#3.wav, #hashtag.mov and Shot # final.mov stay plain. Tests added for both groups.

Comment thread tests/test_otio_paths.py
Comment on lines 383 to +386
("plate.####.exr", "plate.%04d.exr"),
("plate_####.exr", "plate_%04d.exr"),
("/a/b/####.exr", "/a/b/%04d.exr"),
("plate####.exr", "plate%04d.exr"),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ideally also include shot0010_v0010.0010.exr or shot0010_v0010_0010.exr into the checked names

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in fad81d5. shot0010_v0010.0010.exr and shot0010_v0010_0010.exr are now checked names: the frame reads as 0010 with padding 4, reformat_frame_filename gives shot0010_v0010.%04d.exr and shot0010_v0010_%04d.exr, and reformat_path leaves a concrete frame alone. Their hash forms (shot0010_v0010.####.exr, shot0010_v0010_####.exr) are in the hash test, and shot0010_v0010.exr / .mov are checked to have no frame (the v0010 is a version, not a frame). The code already handled all of them; no behaviour change.

Review comment on PR 69: shot0010_v0010.0010.exr, shot0010_v0010_0010.exr
and their hash forms are checked names.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

type: feature Adding something new and exciting to the product

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Editorial P2.3]: Sequence path helpers (otio/paths.py)

1 participant