Merge pull request #787 from pypeclub/3.0/anatomy_load_from_settings

Anatomy load data from settings
This commit is contained in:
Milan Kolar 2020-12-09 12:40:31 +01:00 committed by GitHub
commit 3a71ec975e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 80 additions and 314 deletions

3
.gitmodules vendored
View file

@ -11,9 +11,6 @@
[submodule "repos/maya-look-assigner"]
path = repos/maya-look-assigner
url = git@github.com:pypeclub/maya-look-assigner.git
[submodule "repos/pype-config"]
path = repos/pype-config
url = git@github.com:pypeclub/pype-config.git
[submodule "repos/acre"]
path = repos/acre
url = git@github.com:antirotor/acre.git

View file

@ -1,64 +1,27 @@
import os
import re
import json
import copy
import platform
import collections
import numbers
from pype.settings.lib import (
get_default_anatomy_settings,
get_anatomy_settings
)
from . import config
from .log import PypeLogger
log = PypeLogger().get_logger(__name__)
try:
StringType = basestring
except NameError:
StringType = str
from . import config
from .log import PypeLogger
try:
import ruamel.yaml as yaml
except ImportError:
print("yaml module wasn't found, skipping anatomy")
else:
directory = os.path.join(
os.environ.get("PYPE_ENV", ""), "Lib", "site-packages", "ruamel"
)
file_path = os.path.join(directory, "__init__.py")
if os.path.exists(directory) and not os.path.exists(file_path):
print(
"{0} found but not {1}. Patching ruamel.yaml...".format(
directory, file_path
)
)
open(file_path, "a").close()
log = PypeLogger().get_logger(__name__)
def overrides_dir_path():
value = os.environ.get("PYPE_PROJECT_CONFIGS")
if value:
value = os.path.normpath(value)
return value
def project_overrides_dir_path(project_name):
return os.path.join(
overrides_dir_path(),
project_name
)
def project_anatomy_overrides_dir_path(project_name):
return os.path.join(
project_overrides_dir_path(project_name),
"anatomy"
)
def default_anatomy_dir_path():
return os.path.join(
os.environ["PYPE_CONFIG"],
"anatomy"
)
class ProjectNotSet(Exception):
"""Exception raised when is created Anatomy without project name."""
class RootCombinationError(Exception):
@ -68,12 +31,13 @@ class RootCombinationError(Exception):
joined_roots = ", ".join(
["\"{}\"".format(_root) for _root in roots]
)
# TODO better error message
msg = (
"Combination of root with and"
" without root name in Templates. {}"
).format(joined_roots)
return super(self.__class__, self).__init__(msg)
super(RootCombinationError, self).__init__(msg)
class Anatomy:
@ -83,24 +47,58 @@ class Anatomy:
Args:
project_name (str): Project name to look on overrides.
keep_updated (bool): Project name is updated by AVALON_PROJECT environ.
"""
root_key_regex = re.compile(r"{(root?[^}]+)}")
root_name_regex = re.compile(r"root\[([^]]+)\]")
def __init__(self, project_name=None, keep_updated=False):
def __init__(self, project_name=None):
if not project_name:
project_name = os.environ.get("AVALON_PROJECT")
self.project_name = project_name
self.keep_updated = keep_updated
if not project_name:
raise ProjectNotSet((
"Implementation bug: Project name is not set. Anatomy requires"
" to load data for specific project."
))
self._templates_obj = Templates(parent=self)
self._roots_obj = Roots(parent=self)
self.project_name = project_name
self._data = get_anatomy_settings(project_name)
self._templates_obj = Templates(self)
self._roots_obj = Roots(self)
# Anatomy used as dictionary
# - implemented only getters returning copy
def __getitem__(self, key):
return copy.deepcopy(self._data[key])
def get(self, key, default=None):
return copy.deepcopy(self._data).get(key, default)
def keys(self):
return copy.deepcopy(self._data).keys()
def values(self):
return copy.deepcopy(self._data).values()
def items(self):
return copy.deepcopy(self._data).items()
@staticmethod
def default_data():
"""Default project anatomy data.
Always return fresh loaded data. May be used as data for new project.
Not used inside Anatomy itself.
"""
return get_default_anatomy_settings(clear_metadata=False)
def reset(self):
"""Reset values of cached data in templates and roots objects."""
self._data = get_anatomy_settings(self.project_name)
self.templates_obj.reset()
self.roots_obj.reset()
@ -437,21 +435,8 @@ class Templates:
inner_key_pattern = re.compile(r"(\{@.*?[^{}0]*\})")
inner_key_name_pattern = re.compile(r"\{@(.*?[^{}0]*)\}")
templates_file_name = "default.yaml"
def __init__(
self, project_name=None, keep_updated=False, roots=None, parent=None
):
self._keep_updated = keep_updated
self._project_name = project_name
self._roots = roots
self.parent = parent
if parent is None and project_name is None:
log.warning((
"It is expected to enter project_name if Templates are created"
" out of Anatomy."
))
def __init__(self, anatomy):
self.anatomy = anatomy
self.loaded_project = None
self._templates = None
@ -466,29 +451,14 @@ class Templates:
@property
def project_name(self):
if self.parent:
return self.parent.project_name
return self._project_name
@property
def keep_updated(self):
if self.parent:
return self.parent.keep_updated
return self._keep_updated
return self.anatomy.project_name
@property
def roots(self):
if self.parent:
return self.parent.roots
return self._roots
return self.anatomy.roots
@property
def templates(self):
if self.parent is None and self.keep_updated:
project = os.environ.get("AVALON_PROJECT", None)
if project is not None and project != self.project_name:
self._project_name = project
if self.project_name != self.loaded_project:
self._templates = None
@ -497,74 +467,12 @@ class Templates:
self.loaded_project = self.project_name
return self._templates
@staticmethod
def default_templates_raw():
"""Return default templates raw data."""
path = os.path.join(
default_anatomy_dir_path(),
Templates.templates_file_name
)
with open(path, "r") as stream:
# QUESTION Should we not raise exception if file is invalid?
default_templates = yaml.load(
stream, Loader=yaml.loader.Loader
)
return default_templates
@staticmethod
def default_templates():
def default_templates(self):
"""Return default templates data with solved inner keys."""
return Templates.solve_template_inner_links(
Templates.default_templates_raw()
self.anatomy["templates"]
)
@staticmethod
def project_overrides_path(project_name):
return os.path.join(
project_anatomy_overrides_dir_path(project_name),
Templates.templates_file_name
)
def _project_overrides_path(self):
"""Returns path to project's overide template file."""
return Templates.project_overrides_path(self.project_name)
@staticmethod
def save_project_overrides(project_name, templates=None, override=False):
"""Save templates values into projects overrides.
Creates or replace "default.yaml" file in project overrides.
Args:
project_name (str): Project name for which overrides will be saved.
templates (dict, optional): Templates values to save into
project's overrides. Filled with `default_templates_raw` when
set to None.
override (bool): Allows to override already existing templates
file. This option is set to False by default.
"""
if templates is None:
templates = Templates.default_templates_raw()
yaml_path = Templates.project_overrides_path(project_name)
if os.path.exists(yaml_path) and not override:
log.warning((
"Template overrides for project \"{}\" already exists."
).format(project_name))
return
yaml_dir_path = os.path.dirname(yaml_path)
if not os.path.exists(yaml_dir_path):
log.debug(
"Creating Anatomy folder: \"{}\"".format(yaml_dir_path)
)
os.makedirs(yaml_dir_path)
yaml_obj = yaml.YAML()
yaml_obj.indent(mapping=4, sequence=4, offset=4)
with open(yaml_path, "w") as yaml_file:
yaml_obj.dump(templates, yaml_file)
def _discover(self):
""" Loads anatomy templates from yaml.
Default templates are loaded if project is not set or project does
@ -576,24 +484,14 @@ class Templates:
default templates.
"""
if self.project_name is not None:
project_templates_path = self._project_overrides_path()
if os.path.exists(project_templates_path):
# QUESTION Should we not raise exception if file is invalid?
with open(project_templates_path, "r") as stream:
proj_templates = yaml.load(
stream, Loader=yaml.loader.Loader
)
return Templates.solve_template_inner_links(proj_templates)
if self.project_name is None:
# QUESTION create project specific if not found?
raise AssertionError((
"Project \"{0}\" does not have his own templates."
" Trying to use default."
).format(self.project_name))
else:
# QUESTION create project specific if not found?
log.warning((
"Project \"{0}\" does not have his own templates."
" Trying to use default."
).format(self.project_name))
return self.default_templates()
return Templates.solve_template_inner_links(self.anatomy["templates"])
@classmethod
def replace_inner_keys(cls, matches, value, key_values, key):
@ -1337,31 +1235,15 @@ class Roots:
"""Object which should be used for formatting "root" key in templates.
Args:
project_name (str, optional): Project name to look on overrides.
keep_updated (bool, optional): Project name is updated by
AVALON_PROJECT environ.
parent (object, optional): Expected that parent is Anatomy object.
When parent is set then values of attributes `project_name` and
`keep_updated` are ignored and are used parent's values.
anatomy Anatomy: Anatomy object created for a specific project.
"""
env_prefix = "PYPE_ROOT"
env_prefix = "PYPE_PROJECT_ROOT"
roots_filename = "roots.json"
def __init__(
self, project_name=None, keep_updated=False, parent=None
):
def __init__(self, anatomy):
self.anatomy = anatomy
self.loaded_project = None
self._project_name = project_name
self._keep_updated = keep_updated
if parent is None and project_name is None:
log.warning((
"It is expected to enter project_name if Roots are created"
" out of Anatomy."
))
self.parent = parent
self._roots = None
def __format__(self, *args, **kwargs):
@ -1533,33 +1415,7 @@ class Roots:
@property
def project_name(self):
"""Return project name which will be used for loading root values."""
if self.parent:
return self.parent.project_name
return self._project_name
@property
def keep_updated(self):
"""Keep updated property helps to keep roots updated.
Returns:
bool: Return True when roots should be updated for project set
in "AVALON_PROJECT" environment variable.
"""
if self.parent:
return self.parent.keep_updated
return self._keep_updated
@staticmethod
def project_overrides_path(project_name):
"""Returns path to project overrides roots file."""
project_config_items = [
project_anatomy_overrides_dir_path(project_name),
Roots.roots_filename
]
return os.path.sep.join(project_config_items)
def _project_overrides_path(self):
return Roots.project_overrides_path(self.project_name)
return self.anatomy.project_name
@property
def roots(self):
@ -1571,39 +1427,14 @@ class Roots:
roots settings. That may happend when project use multiroot
templates but default roots miss their keys.
"""
if self.parent is None and self.keep_updated:
project_name = os.environ.get("AVALON_PROJECT")
if self.project_name != project_name:
self._project_name = project_name
if self.project_name != self.loaded_project:
self._roots = None
if self._roots is None:
self._roots = self._discover()
self.loaded_project = self.project_name
# Backwards compatibility
if self._roots is None:
self._roots = Roots.default_roots(self)
return self._roots
@staticmethod
def default_roots_raw():
"""Loads raw default roots data from roots.json."""
default_roots_path = os.path.normpath(os.path.join(
default_anatomy_dir_path(),
Roots.roots_filename
))
with open(default_roots_path, "r") as default_roots_file:
raw_default_roots = json.load(default_roots_file)
return raw_default_roots
@staticmethod
def default_roots(parent=None):
"""Returns parsed default roots."""
return Roots._parse_dict(Roots.default_roots_raw())
def _discover(self):
""" Loads current project's roots or default.
@ -1614,21 +1445,7 @@ class Roots:
setting is used.
"""
# Return default roots if project is not set
if self.project_name is None:
return Roots.default_roots(self)
# Return project specific roots
project_roots_path = self._project_overrides_path()
# If path does not exist we assume it is older project without roots
if not os.path.exists(project_roots_path):
return None
with open(project_roots_path, "r") as project_roots_file:
raw_project_roots = json.load(project_roots_file)
return self._parse_dict(raw_project_roots, parent=self)
return self._parse_dict(self.anatomy["roots"], parent=self)
@staticmethod
def _parse_dict(data, key=None, parent_keys=None, parent=None):
@ -1661,51 +1478,8 @@ class Roots:
return RootItem(data, key, parent_keys, parent=parent)
output = {}
for key, value in data.items():
for _key, value in data.items():
_parent_keys = list(parent_keys)
_parent_keys.append(key)
output[key] = Roots._parse_dict(value, key, _parent_keys, parent)
_parent_keys.append(_key)
output[_key] = Roots._parse_dict(value, _key, _parent_keys, parent)
return output
@staticmethod
def save_project_overrides(project_name, roots_data=None, override=False):
"""Save root values into projects overrides.
Creates or replace "roots.json" file in project overrides.
Args:
project_name (str): Project name for which overrides will be saved.
roots_data (dict, optional): Root values to save into
project's overrides. Filled with `default_roots_raw` when
set to None.
override (bool): Allows to override already existing roots file.
This option is set to False by default.
Example:
`roots_data` should contain platform specific keys::
{
"windows": "P:/projects",
"linux": "/mnt/share/projects",
"darwin": "/Volumes/projects"
}
"""
if roots_data is None:
roots_data = Roots.default_roots_raw()
json_path = Roots.project_overrides_path(project_name)
if os.path.exists(json_path) and not override:
log.warning((
"Roots overrides for project \"{}\" already exists."
).format(project_name))
return
json_dir_path = os.path.dirname(json_path)
if not os.path.exists(json_dir_path):
log.debug(
"Creating Anatomy folder: \"{}\"".format(json_dir_path)
)
os.makedirs(json_dir_path)
with open(json_path, "w") as json_file:
json.dump(roots_data, json_file)

View file

@ -374,13 +374,9 @@ class PrepareProject(BaseAction):
for key, value in root_values.items():
root_data[key] = value
project_name = entities[0]["full_name"]
anatomy = Anatomy(project_name)
anatomy.templates_obj.save_project_overrides(project_name)
anatomy.roots_obj.save_project_overrides(
project_name, root_data, override=True
)
anatomy.reset()
# TODO implement creating of anatomy for new projects
# project_name = entities[0]["full_name"]
# anatomy = Anatomy(project_name)
# pop out info about creating project structure
create_proj_struct = in_data.pop(self.create_project_structure_key)

View file

@ -458,7 +458,7 @@ def get_anatomy_settings(project_name, clear_metadata=True):
"`get_default_anatomy_settings` to get project defaults."
)
studio_overrides = get_default_anatomy_settings()
studio_overrides = get_default_anatomy_settings(False)
project_overrides = get_project_anatomy_overrides(project_name)
result = apply_overrides(studio_overrides, project_overrides)

@ -1 +0,0 @@
Subproject commit 9589ee5f7cf279c612a4121b63724924ce0d6310