Skip to content

Commit

Permalink
Merge pull request #136 from eoyilmaz/86-create-traversaldirection-en…
Browse files Browse the repository at this point in the history
…um-class

86 create traversaldirection enum class
  • Loading branch information
eoyilmaz authored Dec 13, 2024
2 parents 3578999 + c0f5697 commit b04ddaf
Show file tree
Hide file tree
Showing 14 changed files with 271 additions and 86 deletions.
14 changes: 7 additions & 7 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Stalker Changes
class for this).
* `Task.depends` renamed to `Task.depends_on`.
* `TaskDependency.task_depends_to` renamed to `TaskDependency.task_depends_on`.
* Modernised Stalker as a Python project. It is now fully PEP 517 compliant.
* Modernized Stalker as a Python project. It is now fully PEP 517 compliant.
* Stalker now supports Python versions from 3.8 to 3.13.
* Stalker is now SQLAlchemy 2.x compliant.
* Stalker is now fully type hinted.
Expand All @@ -20,9 +20,9 @@ Stalker Changes
* Added Makefile workflow to help creating a virtualenv, building, installing,
releasing etc. actions much more easier.
* Added `tox` config to run the test with Python 3.8 to 3.13.
* Increased test coverage to 99.71%.
* Increased test coverage to 99.76%.
* Updated documentation theme to `furo`.
* Renamed OSX to macOS anywhere it is mentioned.
* Renamed OSX to macOS where ever it is mentioned.
* `Scene` is now deriving from `Task`.
* `Shot.sequences` is now `Shot.sequences` and it is many-to-one.
* `Shot.scenes` is now `Shot.scene` and it is many-to-one.
Expand All @@ -35,10 +35,10 @@ Stalker Changes
container `Task` to hold the information.
* `Version._template_variables()` now finds the related `Asset`, `Shot` and
`Sequence` values and passes them in the returned dictionary.
* All the enum values handled with arbitrary string lists are now enum classes.
As a result we now have `ScheduleConstraint`, `TimeUnit`, `ScheduleModel`,
`DependencyTarget` enum classes which are removing the need of using fiddly
strings as enum values.
* All the enum values handled with arbitrary string lists or integer values are
now enum classes. As a result we now have `ScheduleConstraint`, `TimeUnit`,
`ScheduleModel`, `DependencyTarget`, `TraversalDirection` enum classes which
are removing the need of using fiddly strings as enum values.
* `StatusList`s that are created for super classes can now be used with the
derived classes, i.e. a status list created specifically for Task can now be
used with Asset, Shot Sequence and Scenes and any future Task derivatives.
Expand Down
1 change: 1 addition & 0 deletions src/stalker/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"""
from stalker.version import __version__ # noqa: F401
from stalker import config, log # noqa: I100

if True:
defaults: config.Config = config.Config()
from stalker.models.asset import Asset
Expand Down
65 changes: 63 additions & 2 deletions src/stalker/models/enum.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ def to_model(cls, model: Union[str, "ScheduleModel"]) -> "ScheduleModel":
raise TypeError(
"model should be a ScheduleModel enum value or one of {}, "
"not {}: '{}'".format(
[u.name.title() for u in cls] + [u.value for u in cls],
[m.name.title() for m in cls] + [m.value for m in cls],
model.__class__.__name__,
model,
)
Expand Down Expand Up @@ -322,7 +322,7 @@ def to_target(cls, target: Union[str, "DependencyTarget"]) -> "DependencyTarget"
raise ValueError(
"target should be a DependencyTarget enum value or one of {}, "
"not '{}'".format(
[m.name for m in cls] + [t.value for t in cls], target
[t.name for t in cls] + [t.value for t in cls], target
)
)

Expand Down Expand Up @@ -357,3 +357,64 @@ def process_result_value(self, value: str, dialect: str) -> DependencyTarget:
dialect (str): The name of the dialect.
"""
return DependencyTarget.to_target(value)


class TraversalDirection(IntEnum):
"""The traversal direction enum."""

DepthFirst = 0
BreadthFirst = 1

def __repr__(self) -> str:
"""Return the enum name for str().
Returns:
str: The name as the string representation of this
ScheduleConstraint.
"""
return self.name if self.name != "NONE" else "None"

__str__ = __repr__

@classmethod
def to_direction(
cls, direction: Union[int, str, "TraversalDirection"]
) -> "TraversalDirection":
"""Convert the given direction value to a TraversalDirection enum.
Args:
direction (Union[int, str, TraversalDirection]): The value to
convert to a TraversalDirection.
Raises:
TypeError: Input value type is invalid.
ValueError: Input value is invalid.
Returns:
TraversalDirection: The enum.
"""
if not isinstance(direction, (int, str, TraversalDirection)):
raise TypeError(
"direction should be a TraversalDirection enum value "
"or one of {}, not {}: '{}'".format(
[d.name for d in cls] + [d.value for d in cls],
direction.__class__.__name__,
direction,
)
)
if isinstance(direction, str):
direction_name_lut = dict([(d.name.lower(), d.name) for d in cls])
direction_name_lut.update(dict([(d.value, d.name) for d in cls]))
direction_lower_case = direction.lower()
if direction_lower_case not in direction_name_lut:
raise ValueError(
"direction should be a TraversalDirection enum value or "
"one of {}, not '{}'".format(
[d.name for d in cls] + [d.value for d in cls],
direction,
)
)

return cls.__members__[direction_name_lut[direction_lower_case]]

return direction
10 changes: 8 additions & 2 deletions src/stalker/models/mixins.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
ScheduleModelDecorator,
TimeUnit,
TimeUnitDecorator,
TraversalDirection,
)
from stalker.utils import check_circular_dependency, make_plural, walk_hierarchy

Expand Down Expand Up @@ -1983,11 +1984,16 @@ def parents(self) -> List[Self]:
parents.reverse()
return parents

def walk_hierarchy(self, method=0) -> Generator[None, Self, None]:
def walk_hierarchy(
self,
method: Union[int, str, TraversalDirection] = TraversalDirection.DepthFirst,
) -> Generator[None, Self, None]:
"""Walk the hierarchy of this task.
Args:
method (int): The walk method, 0: Depth First, 1: Breadth First.
method (Union[int, str, TraversalDirection]): The walk method
defined by the :class:`.TraversalDirection` enum value. The
default is :attr:`.TraversalDirection.DepthFirst`.
Yields:
Task: The child Task.
Expand Down
6 changes: 4 additions & 2 deletions src/stalker/models/review.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from stalker.db.session import DBSession
from stalker.log import get_logger
from stalker.models.entity import Entity, SimpleEntity
from stalker.models.enum import DependencyTarget, TimeUnit
from stalker.models.enum import DependencyTarget, TimeUnit, TraversalDirection
from stalker.models.link import Link
from stalker.models.mixins import (
ProjectMixin,
Expand Down Expand Up @@ -382,7 +382,9 @@ def finalize_review_set(self) -> None:
from stalker import TaskDependency

# update dependent task statuses
for dependency in walk_hierarchy(self.task, "dependent_of", method=1):
for dependency in walk_hierarchy(
self.task, "dependent_of", method=TraversalDirection.BreadthFirst
):
logger.debug(f"current TaskDependency object: {dependency}")
dependency.update_status_with_dependent_statuses()
if dependency.status.code in ["HREV", "PREV", "DREV", "OH", "STOP"]:
Expand Down
10 changes: 8 additions & 2 deletions src/stalker/models/task.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
ScheduleModel,
TimeUnit,
TimeUnitDecorator,
TraversalDirection,
)
from stalker.models.mixins import (
DAGMixin,
Expand Down Expand Up @@ -2528,11 +2529,16 @@ def open_tickets(self) -> List[Ticket]:
.all()
)

def walk_dependencies(self, method: int = 1) -> Generator[None, "Task", None]:
def walk_dependencies(
self,
method: Union[int, str, TraversalDirection] = TraversalDirection.BreadthFirst,
) -> Generator[None, "Task", None]:
"""Walk the dependencies of this task.
Args:
method (int): The walk method, 0: Depth First, 1: Breadth First.
method (Union[int, str, TraversalDirection]): The walk method
defined by the :class:`.TraversalDirection` enum value. Default
is :attr:`.TraversalDirection.BreadthFirst`.
Yields:
Task: Yields Task instances.
Expand Down
9 changes: 7 additions & 2 deletions src/stalker/models/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from stalker.db.declarative import Base
from stalker.db.session import DBSession
from stalker.log import get_logger
from stalker.models.enum import TraversalDirection
from stalker.models.link import Link
from stalker.models.mixins import DAGMixin
from stalker.models.review import Review
Expand Down Expand Up @@ -640,11 +641,15 @@ def nice_name(self) -> str:
"_".join(map(lambda x: x.nice_name, self.naming_parents))
)

def walk_inputs(self, method: int = 0) -> Generator[None, "Version", None]:
def walk_inputs(
self,
method: Union[int, str, TraversalDirection] = TraversalDirection.DepthFirst,
) -> Generator[None, "Version", None]:
"""Walk the inputs of this version instance.
Args:
method (int): The walk method, 0=Depth First, 1=Breadth First.
method (Union[int, str, TraversalDirection]): The walk method defined by
the :class:`.TraversalDirection` enum.
Yields:
Version: Yield the Version instances.
Expand Down
25 changes: 17 additions & 8 deletions src/stalker/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@
"""Utilities are situated here."""
import calendar
from datetime import datetime, timedelta
from typing import Any, Generator
from typing import Any, Generator, Union

import pytz

from stalker.exceptions import CircularDependencyError
from stalker.models.enum import TraversalDirection


def make_plural(name: str) -> str:
Expand All @@ -32,31 +33,39 @@ def make_plural(name: str) -> str:
return plural_name


def walk_hierarchy(entity: Any, attr: str, method: int = 0) -> Generator[Any, Any, Any]:
def walk_hierarchy(
entity: Any,
attr: str,
method: Union[int, str, TraversalDirection] = TraversalDirection.DepthFirst,
) -> Generator[Any, Any, Any]:
"""Walk the entity hierarchy over the given attribute and yield the entities found.
It doesn't check for cycle, so if the attribute is not acyclic then this
function will not find an exit point.
The default mode is Depth First Search (DFS), to walk with Breadth First
Search (BFS) set the direction to 1.
The default method is Depth First Search (DFS), to walk with Breadth First
Search (BFS) set the direction to :attr:`.TraversalDirection.BreadthFirst`.
Args:
entity (Any): Starting Entity.
attr (str): The attribute name to walk over.
method (int): 0:Depth first or 1:Breadth First
method (Union[int, str, TraversalDirection]): Use TraversalDirection
enum values, or one of the values listed here ["DepthFirst",
"BreadthFirst", 0, 1]. The default is
:attr:`.TraversalDirection.DepthFirst`.
Yields:
Any: List any entities found while traversing the hierarchy.
"""
entity_to_visit = [entity]
if not method: # Depth First Search (DFS)
method = TraversalDirection.to_direction(method)
if method == TraversalDirection.DepthFirst:
while len(entity_to_visit):
current_entity = entity_to_visit.pop(0)
for child in reversed(getattr(current_entity, attr)):
entity_to_visit.insert(0, child)
yield current_entity
else: # Breadth First Search (BFS)
else: # TraversalDirection.BreadthFirst
while len(entity_to_visit):
current_entity = entity_to_visit.pop(0)
entity_to_visit.extend(getattr(current_entity, attr))
Expand All @@ -82,7 +91,7 @@ def check_circular_dependency(entity: Any, other_entity: Any, attr_name: str) ->
if e is other_entity:
raise CircularDependencyError(
"{entity_name} ({entity_class}) and "
"{other_entity_name} ({other_entity_class}) creates a "
"{other_entity_name} ({other_entity_class}) are in a "
'circular dependency in their "{attr_name}" attribute'.format(
entity_name=entity,
entity_class=entity.__class__.__name__,
Expand Down
30 changes: 15 additions & 15 deletions tests/db/test_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -463,49 +463,49 @@ def test_task_status_list_initialization(setup_postgresql_db):

def test_asset_status_list_initialization(setup_postgresql_db):
"""Asset statuses are correctly created."""
asset_status_list = (
StatusList.query.filter(StatusList.target_entity_type == "Asset").first()
)
asset_status_list = StatusList.query.filter(
StatusList.target_entity_type == "Asset"
).first()
# we do not generate a specific StatusList for Assets anymore
# as Task specific StatusLists can be used.
assert asset_status_list is None


def test_shot_status_list_initialization(setup_postgresql_db):
"""Shot statuses are correctly created."""
shot_status_list = (
StatusList.query.filter(StatusList.target_entity_type == "Shot").first()
)
shot_status_list = StatusList.query.filter(
StatusList.target_entity_type == "Shot"
).first()
# we do not generate a specific StatusList for Shots anymore
# as Task specific StatusLists can be used.
assert shot_status_list is None


def test_sequence_status_list_initialization(setup_postgresql_db):
"""Sequence statuses are correctly created."""
sequence_status_list = (
StatusList.query.filter(StatusList.target_entity_type == "Sequence").first()
)
sequence_status_list = StatusList.query.filter(
StatusList.target_entity_type == "Sequence"
).first()
# we do not generate a specific StatusList for Sequences anymore
# as Task specific StatusLists can be used.
assert sequence_status_list is None


def test_scene_status_list_initialization(setup_postgresql_db):
"""Scene statuses are correctly created."""
scene_status_list = (
StatusList.query.filter(StatusList.target_entity_type == "Scene").first()
)
scene_status_list = StatusList.query.filter(
StatusList.target_entity_type == "Scene"
).first()
# we do not generate a specific StatusList for Scenes anymore
# as Task specific StatusLists can be used.
assert scene_status_list is None


def test_variant_status_list_initialization(setup_postgresql_db):
"""Variant statuses are correctly created."""
variant_status_list = (
StatusList.query.filter(StatusList.target_entity_type == "Variant").first()
)
variant_status_list = StatusList.query.filter(
StatusList.target_entity_type == "Variant"
).first()
# we do not generate a specific StatusList for Variant anymore
# as Task specific StatusLists can be used.
assert variant_status_list is None
Expand Down
2 changes: 1 addition & 1 deletion tests/mixins/test_dag_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def test_parent_attribute_creates_a_cycle(dag_mixin_test_case):
str(cm.value) == "<Test DAG Mixin (DAGMixinFooMixedInClass)> "
"(DAGMixinFooMixedInClass) and "
"<Test DAG Mixin (DAGMixinFooMixedInClass)> "
"(DAGMixinFooMixedInClass) creates a circular dependency in "
"(DAGMixinFooMixedInClass) are in a circular dependency in "
'their "children" attribute'
)

Expand Down
Loading

0 comments on commit b04ddaf

Please sign in to comment.