Skip to content
Open
5 changes: 5 additions & 0 deletions azure-pipelines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,11 @@ variables:
- group: deploy-secrets

jobs:
- template: pip-install-packages.yml@templates
parameters:
packages:
- mock

- template: build-pipeline.yml@templates
parameters:
# After the tests have run, run the integration tests.
Expand Down
15 changes: 13 additions & 2 deletions python/tank/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,17 @@ def root_path(self):
"""
return self._prefix

def _dirname(self):
"""
Returns the directory part of the template.

:returns: String
"""
dirname, basename = os.path.split(self._repr_def)
if "[" in basename and "]" not in basename:
return dirname.split("[")[0]
return dirname

Comment on lines +562 to +570
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The logic for handling optional keys in _dirname() is incomplete. If the basename contains a [ but no ], it splits by [ and takes only the first part, but this doesn't account for cases where the opening bracket is in the dirname portion. For example, with shots/{Sequence}/[{Shot}]-{Step}, after os.path.split, basename would be [{Shot}]-{Step} (containing both [ and ]), and the condition would be false, but the dirname shots/{Sequence} would be incorrect if it also had an unclosed bracket. Consider a more robust approach that properly handles nested or partial optional sections.

Suggested change
Returns the directory part of the template.
:returns: String
"""
dirname, basename = os.path.split(self._repr_def)
if "[" in basename and "]" not in basename:
return dirname.split("[")[0]
return dirname
Returns the directory part of the template, robustly handling optional sections.
:returns: String
"""
return self._split_path_outside_brackets(self._repr_def)
@staticmethod
def _split_path_outside_brackets(path):
"""
Splits the path at the last path separator that is outside any brackets.
Returns the directory part.
"""
bracket_depth = 0
last_sep_outside = -1
for i, c in enumerate(path):
if c == "[":
bracket_depth += 1
elif c == "]":
bracket_depth = max(0, bracket_depth - 1)
elif c == os.sep and bracket_depth == 0:
last_sep_outside = i
if last_sep_outside == -1:
return ""
return path[:last_sep_outside]

Copilot uses AI. Check for mistakes.
@property
def parent(self):
"""
Expand All @@ -566,8 +577,8 @@ def parent(self):

:returns: :class:`Template`
"""
parent_definition = os.path.dirname(self.definition)
if parent_definition:
parent_definition = self._dirname()
if parent_definition and parent_definition != "/":
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The check parent_definition != \"/\" is Unix-specific and may not work correctly on Windows where root paths use drive letters (e.g., C:\\). Consider using os.path.dirname(parent_definition) != parent_definition or a more platform-agnostic approach to detect root paths.

Suggested change
if parent_definition and parent_definition != "/":
if parent_definition and os.path.dirname(parent_definition) != parent_definition:

Copilot uses AI. Check for mistakes.
return TemplatePath(
parent_definition,
self.keys,
Expand Down
40 changes: 40 additions & 0 deletions tests/core_tests/test_templatepath.py
Original file line number Diff line number Diff line change
Expand Up @@ -1184,3 +1184,43 @@ def test_aliased_key(self):
template = TemplatePath(definition, keys, root_path=self.project_root)
result = template.parent
self.assertEqual("{new_name}", result.definition)

def test_parent_with_optional(self):
definition = "shots/{Sequence}[-{seq_num}]/{Shot}/{Step}/work"
parent_expected_definitions = [
"shots/{Sequence}-{seq_num}/{Shot}/{Step}",
"shots/{Sequence}/{Shot}/{Step}",
]
parent_expected__repr_def = os.path.dirname(definition)
Comment on lines +1189 to +1194
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The test verifies _definitions and _repr_def but doesn't explain why both are necessary or what they represent. Add a comment explaining that _definitions contains all possible expanded forms (with and without optional keys) while _repr_def is the original representation-preserving form with optional syntax intact.

Copilot uses AI. Check for mistakes.

template = TemplatePath(definition, self.keys, root_path=self.project_root)
parent = template.parent
self.assertEqual(parent_expected_definitions, parent._definitions)
self.assertEqual(parent_expected__repr_def, parent._repr_def)


class TestDirname(TestTemplatePath):
def test_dirname_with_word(self):
definition = "shots/{Sequence}/{Shot}/{Step}/work"
template = TemplatePath(definition, self.keys, root_path=self.project_root)
self.assertEqual("shots/{Sequence}/{Shot}/{Step}", template.dirname)

def test_dirname_with_key(self):
definition = "shots/{Sequence}/{Shot}/{Step}"
template = TemplatePath(definition, self.keys, root_path=self.project_root)
self.assertEqual("shots/{Sequence}/{Shot}", template.dirname)

def test_dirname_with_optional(self):
definition = "shots/{Sequence}/{Shot}[/{Step}]"
template = TemplatePath(definition, {}, root_path=self.project_root)
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The TestDirname test cases pass an empty dictionary for keys instead of self.keys. This may not fully test the interaction between the dirname property and key validation. Consider using self.keys consistently to ensure the tests validate behavior with actual key definitions, or add a comment explaining why empty keys are intentional for these specific tests.

Copilot uses AI. Check for mistakes.
self.assertEqual("shots/{Sequence}/{Shot}", template.dirname)

def test_dirname_with_optional_word_and_key(self):
definition = "shots/{Sequence}/{Shot}/[abc_{Step}]"
template = TemplatePath(definition, {}, root_path=self.project_root)
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The TestDirname test cases pass an empty dictionary for keys instead of self.keys. This may not fully test the interaction between the dirname property and key validation. Consider using self.keys consistently to ensure the tests validate behavior with actual key definitions, or add a comment explaining why empty keys are intentional for these specific tests.

Copilot uses AI. Check for mistakes.
self.assertEqual("shots/{Sequence}/{Shot}", template.dirname)

def test_dirname_with_optional_key_and_key(self):
definition = "shots/{Sequence}/[{Shot}]-{Step}"
template = TemplatePath(definition, {}, root_path=self.project_root)
Copy link

Copilot AI Dec 2, 2025

Choose a reason for hiding this comment

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

The TestDirname test cases pass an empty dictionary for keys instead of self.keys. This may not fully test the interaction between the dirname property and key validation. Consider using self.keys consistently to ensure the tests validate behavior with actual key definitions, or add a comment explaining why empty keys are intentional for these specific tests.

Copilot uses AI. Check for mistakes.
self.assertEqual("shots/{Sequence}", template.dirname)