Add 'project finders' to the add-on

The Flamenco add-on can now find the top-level directory of your Blender
project. It can be configured to find any of these directories:

- `.blender_project`
- `.git`
- `.subversion`
- None, which is the default and the old behaviour of just using the
  directory of the current blend file as the 'project directory'.
This commit is contained in:
Sybren A. Stüvel 2023-06-01 16:07:11 +02:00
parent fbc7c0cfd9
commit f4f61ea593
5 changed files with 151 additions and 5 deletions

@ -10,6 +10,7 @@ bugs in actually-released versions.
- Improve logging of job deletion.
- Add Worker Cluster support. Workers can be members of any number of clusters. Workers will only work on jobs that are assigned to that cluster. Jobs that do not have a cluster will be available to all workers, regardless of their cluster assignment. As a result, clusterless workers will only work on clusterless jobs.
- Fix limitation where a job could have no more than 1000 tasks ([#104201](https://projects.blender.org/studio/flamenco/issues/104201))
- Add support for finding the top-level 'project' directory. When submitting files to Flamenco, the add-on will try to retain the directory structure of your Blender project as precisely as possible. This new feature allows the add-on to find the top-level directory of your project by finding a `.blender_project`, `.git`, or `.subversion` directory. This can be configured in the add-on preferences.
## 3.2 - released 2023-02-21

@ -19,7 +19,15 @@ from pathlib import Path
__is_first_load = "operators" not in locals()
if __is_first_load:
from . import operators, gui, job_types, comms, preferences, worker_clusters
from . import (
operators,
gui,
job_types,
comms,
preferences,
projects,
worker_clusters,
)
else:
import importlib
@ -28,6 +36,7 @@ else:
job_types = importlib.reload(job_types)
comms = importlib.reload(comms)
preferences = importlib.reload(preferences)
projects = importlib.reload(projects)
worker_clusters = importlib.reload(worker_clusters)
import bpy

@ -381,8 +381,9 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
"""
from .bat import interface as bat_interface
# TODO: get project path from addon preferences / project definition on Manager.
project_path = blendfile.parent
# Get project path from addon preferences.
prefs = preferences.get(context)
project_path: Path = prefs.project_root()
try:
project_path = Path(bpy.path.abspath(str(project_path))).resolve()
except FileNotFoundError:
@ -395,7 +396,6 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
datetime.datetime.now().isoformat("-").replace(":", ""),
self.job_name,
)
prefs = preferences.get(context)
pack_target_dir = Path(prefs.job_storage) / unique_dir
# TODO: this should take the blendfile location relative to the project path into account.
@ -438,9 +438,12 @@ class FLAMENCO_OT_submit_job(FlamencoOpMixin, bpy.types.Operator):
assert self.job is not None
self.log.info("Sending BAT pack to Shaman")
prefs = preferences.get(context)
project_path: Path = prefs.project_root()
self.packthread = bat_interface.copy(
base_blendfile=blendfile,
project=blendfile.parent, # TODO: get from preferences/GUI.
project=project_path,
target="/", # Target directory irrelevant for Shaman transfers.
exclusion_filter="", # TODO: get from GUI.
relative_only=True, # TODO: get from GUI.

@ -1,8 +1,12 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# <pep8 compliant>
from pathlib import Path
import bpy
from . import projects
def discard_flamenco_client(context):
"""Discard any cached Flamenco client after the Manager URL changes."""
@ -34,6 +38,11 @@ def _manager_url_updated(prefs, context):
comms.ping_manager_with_report(context.window_manager, api_client, prefs)
_project_finder_enum_items = [
(key, info.label, info.description) for key, info in projects.finders.items()
]
class WorkerCluster(bpy.types.PropertyGroup):
id: bpy.props.StringProperty(name="id") # type: ignore
name: bpy.props.StringProperty(name="Name") # type: ignore
@ -50,6 +59,13 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
update=_manager_url_updated,
)
project_finder: bpy.props.EnumProperty( # type: ignore
name="Project Finder",
description="Strategy for Flamenco to find the top level directory of your project",
default=_project_finder_enum_items[0][0],
items=_project_finder_enum_items,
)
is_shaman_enabled: bpy.props.BoolProperty( # type: ignore
name="Shaman Enabled",
description="Whether this Manager has the Shaman protocol enabled",
@ -114,6 +130,23 @@ class FlamencoPreferences(bpy.types.AddonPreferences):
text_row(col, "Shaman enabled")
col.prop(self, "job_storage_for_gui", text="Job Storage")
# Project Root
col = layout.column(align=True)
col.prop(self, "project_finder")
try:
project_root = self.project_root()
except ValueError:
pass
else:
text_row(col, str(project_root))
def project_root(self) -> Path:
"""Use the configured project finder to find the project root directory."""
# It is assumed that the blendfile is saved.
blendfile = Path(bpy.data.filepath)
return projects.for_blendfile(blendfile, self.project_finder)
def get(context: bpy.types.Context) -> FlamencoPreferences:
"""Return the add-on preferences."""

100
addon/flamenco/projects.py Normal file

@ -0,0 +1,100 @@
# SPDX-License-Identifier: GPL-3.0-or-later
# <pep8 compliant>
from pathlib import Path
from typing import Callable, TypeAlias
import dataclasses
def for_blendfile(blendfile: Path, strategy: str) -> Path:
"""Return what is considered to be the project directory containing the given file.
If none can be found, the directory containing the current blend file is returned.
If the current blend file has no path (because it was not saved), a ValueError is raised.
:param blendfile: the path of the blend file for which to find the project.
:param strategy: the name of the finder to use, see `finders`.
"""
if blendfile.is_dir():
msg = f"{blendfile} is not a blend file, cannot find project directory"
raise ValueError(msg)
try:
finder_info = finders[strategy]
except KeyError:
msg = f"Unknown strategy {strategy!r}, cannot find project directory"
raise ValueError(msg) from None
return finder_info.finder(blendfile)
def _finder_none(blendfile: Path) -> Path:
return blendfile.absolute().parent
def _finder_blender_project(blendfile: Path) -> Path:
return _search_path_marker(blendfile, ".blender_project")
def _finder_git(blendfile: Path) -> Path:
return _search_path_marker(blendfile, ".git")
def _finder_subversion(blendfile: Path) -> Path:
return _search_path_marker(blendfile, ".svn")
def _search_path_marker(blendfile: Path, marker_path: str) -> Path:
"""Go up the directory hierarchy until a file or directory 'marker_path' is found."""
blendfile_dir = blendfile.absolute().parent
directory = blendfile_dir
while True:
marker: Path = directory / marker_path
if marker.exists():
return directory
parent = directory.parent
if directory == parent:
# If a directory is its own parent, we're at the root and cannot go
# up further.
break
directory = parent
# Could not find the marker, so use the directory containing the blend file.
return blendfile_dir
Finder: TypeAlias = Callable[[Path], Path]
@dataclasses.dataclass
class FinderInfo:
label: str
description: str
finder: Finder
finders: dict[str, FinderInfo] = {
"NONE": FinderInfo(
"None",
"Just use the directory containing the current blend file as the project directory.",
_finder_none,
),
"BLENDER_PROJECT": FinderInfo(
"Blender Project",
"Find a .blend_project directory and use that as indicator for the top level project directory.",
_finder_blender_project,
),
"GIT": FinderInfo(
"Git",
"Find a .git directory and use that as indicator for the top level project directory.",
_finder_git,
),
"SUBVERSION": FinderInfo(
"Subversion",
"Find a .svn directory and use that as indicator for the top level project directory.",
_finder_subversion,
),
}