diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 23092417..1848a00e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.9.1-dev0 +current_version = 0.9.1-dev2 commit = True tag = False parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? diff --git a/.travis.yml b/.travis.yml index 6f53af32..469361f1 100755 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,7 @@ language: python python: - "3.6" + - "3.7" # install dependencies install: diff --git a/CHANGELOG.md b/CHANGELOG.md index 594bda54..f579b377 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,17 @@ # Changelog -## 0.9.0 +## 0.9 + +### 0.9.1 (work in progress) + +- updated documentation +- removed conversion function register, because the functions were not used and made the code unnecessarily complicated + - might be replaced by a graph-based conversion path-finder in the future, if necessary +- Extended support for loading circuits from and saving to files + - supported formats: `yaml`, `pickle` + - supported classes: templates + +### 0.9.0 - Added experimental support for multiple source variables per edge - edges can either have multiple input variable from the same input node, or diff --git a/pyrates/__init__.py b/pyrates/__init__.py index da80b197..29756a0e 100755 --- a/pyrates/__init__.py +++ b/pyrates/__init__.py @@ -32,7 +32,7 @@ __author__ = "Richard Gast, Daniel Rose" __status__ = "Development" -__version__ = "0.9.1-dev0" +__version__ = "0.9.1-dev2" class PyRatesException(Exception): diff --git a/pyrates/frontend/__init__.py b/pyrates/frontend/__init__.py index 2070c7df..26af6c34 100755 --- a/pyrates/frontend/__init__.py +++ b/pyrates/frontend/__init__.py @@ -35,28 +35,11 @@ # template-based interface from pyrates.frontend import template -# from pyrates.frontend import dict as dict_ -from pyrates.frontend import yaml -# from pyrates.frontend import nxgraph +from pyrates.frontend.fileio import yaml from pyrates.frontend.template import CircuitTemplate, NodeTemplate, EdgeTemplate, OperatorTemplate -# By importing the above, all transformation functions (starting with `to_` or `from_`) are registered -# Below these functions are collected and made available from pyrates.frontend following the naming convention -# `{target}_from_{source}` with target and source being respective valid representations in the frontend - -from pyrates.frontend._registry import REGISTERED_INTERFACES -import sys - -# add all registered functions to main frontend module -this_module = sys.modules[__name__] - -for new_name, func in REGISTERED_INTERFACES.items(): - # set new name on current module - setattr(this_module, new_name, func) - # The following function are shorthands that bridge multiple interface steps - def circuit_from_yaml(path: str): """Directly return CircuitIR instance from a yaml file.""" return CircuitTemplate.from_yaml(path).apply() diff --git a/pyrates/frontend/_registry.py b/pyrates/frontend/_registry.py deleted file mode 100755 index 173c4a19..00000000 --- a/pyrates/frontend/_registry.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -# -# -# PyRates software framework for flexible implementation of neural -# network models and simulations. See also: -# https://github.com/pyrates-neuroscience/PyRates -# -# Copyright (C) 2017-2018 the original authors (Richard Gast and -# Daniel Rose), the Max-Planck-Institute for Human Cognitive Brain -# Sciences ("MPI CBS") and contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see -# -# CITATION: -# -# Richard Gast and Daniel Rose et. al. in preparation -"""Functionality to register functions that are used to transform between different data types in the frontend. -""" - -__author__ = "Daniel Rose" -__status__ = "Development" - -REGISTERED_INTERFACES = dict() - - -def register_interface(func, name=""): - """Register a transformation function (interface) between two representations of models. - - Parameters - ---------- - func - Function to be registered. Needs to start with "from_" or "to_" to signify the direction of transformation - name - (Optional) String that defines the name under which the function should be registered. If left empty, - the name will be formatted in the form {target}_from_{source}, where target and source are representations to - transform from or to.""" - if name is "": - - # get interface name from module name - module_name = func.__module__.split(".")[-1] - # parse to_ and from_ functions - func_name = func.__name__ - if func_name.startswith("from_"): - target = module_name - source = func_name[5:] # crop 'from_' - elif func_name.startswith("to_"): - source = module_name - target = func_name[3:] # crop 'to_' - else: - raise ValueError(f"Function name {func_name} does not adhere to convention to start " - f"with either `to_` or `from_`.") # ignore any other functions - new_name = f"{target}_from_{source}" - else: - new_name = name - - if new_name in REGISTERED_INTERFACES: - raise ValueError(f"Interface {new_name} already exist. Cannot add {func}.") - else: - REGISTERED_INTERFACES[new_name] = func - - return func diff --git a/pyrates/frontend/dict.py b/pyrates/frontend/dict.py index 7ef28c7f..b71ee1c1 100755 --- a/pyrates/frontend/dict.py +++ b/pyrates/frontend/dict.py @@ -35,13 +35,13 @@ from pyrates.ir.edge import EdgeIR from pyrates.ir.operator import OperatorIR -from pyrates.frontend._registry import register_interface + __author__ = "Daniel Rose" __status__ = "Development" -# @register_interface +# # def to_node(node_dict: dict): # # order = node_dict["operator_order"] @@ -70,7 +70,7 @@ # return NodeIR(operators=operators) # # -# @register_interface +# # def to_operator(op_dict: dict): # from pyrates.frontend import OperatorTemplate # template = OperatorTemplate(**op_dict) @@ -78,7 +78,7 @@ # return template.apply() # -@register_interface + def from_circuit(circuit: CircuitIR): """Reformat graph structure into a dictionary that can be saved as YAML template. The current implementation assumes that nodes and edges are given by as templates.""" diff --git a/pyrates/frontend/file.py b/pyrates/frontend/file.py index a8dc3270..174a7622 100755 --- a/pyrates/frontend/file.py +++ b/pyrates/frontend/file.py @@ -31,7 +31,7 @@ import importlib from pyrates import PyRatesException -from pyrates.frontend import yaml as _yaml +from pyrates.frontend.fileio import yaml as _yaml __author__ = "Daniel Rose" __status__ = "Development" @@ -53,7 +53,8 @@ def parse_path(path: str): - """Parse a path of form path.to.template, returning a tuple of (name, file, abspath).""" + """Parse a path of form path.to.template_file.template_name or path/to/template_file/template_name, + returning a tuple of (name, file, abspath).""" if "/" in path or "\\" in path: import os diff --git a/pyrates/frontend/fileio/__init__.py b/pyrates/frontend/fileio/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pyrates/frontend/fileio/pickle.py b/pyrates/frontend/fileio/pickle.py new file mode 100644 index 00000000..d48145c4 --- /dev/null +++ b/pyrates/frontend/fileio/pickle.py @@ -0,0 +1,19 @@ +"""Interface to load from and save to python pickles.""" + + +def dump(data, filename): + import pickle + + from pyrates.utility.filestorage import create_directory + create_directory(filename) + + pickle.dump(data, open(filename, 'wb'), protocol=pickle.HIGHEST_PROTOCOL) + + + +def load(filename): + import pickle + + data = pickle.load(open(filename, 'rb')) + + return data diff --git a/pyrates/frontend/yaml.py b/pyrates/frontend/fileio/yaml.py old mode 100755 new mode 100644 similarity index 76% rename from pyrates/frontend/yaml.py rename to pyrates/frontend/fileio/yaml.py index 731bbea0..77517ec9 --- a/pyrates/frontend/yaml.py +++ b/pyrates/frontend/fileio/yaml.py @@ -27,13 +27,12 @@ # Richard Gast and Daniel Rose et. al. in preparation """ Some utility functions for parsing YAML-based definitions of circuits and components. """ -from pyrates.frontend._registry import register_interface + __author__ = "Daniel Rose" __status__ = "Development" -@register_interface def to_dict(path: str): """Load a template from YAML and return the resulting dictionary. @@ -41,9 +40,12 @@ def to_dict(path: str): ---------- path - string containing path of YAML template of the form path.to.template or path/to/template.file.TemplateName. - The dot notation refers to a path that can be found using python's import functionality. The slash notation - refers to a file in an absolute or relative path from the current working directory. + (str) path to YAML template of the form `path.to.template_file.template_name` or + path/to/template_file/template_name.TemplateName. The dot notation refers to a path that can be found + using python's import functionality. That means it needs to be a module (a folder containing an `__init__.py`) + located in the Python path (e.g. the current working directory). The slash notation refers to a file in an + absolute or relative path from the current working directory. In either case the second-to-last part refers to + the filename without file extension and the last part refers to the template name. """ from pyrates.frontend.file import parse_path @@ -81,10 +83,10 @@ def to_dict(path: str): return template_dict -@register_interface def from_circuit(circuit, path: str, name: str): - from pyrates.frontend.dict import from_circuit - dict_repr = {name: from_circuit(circuit)} + """Interface to dump a CircuitIR instance to YAML.""" + from pyrates.frontend.dict import from_circuit as dict_from_circuit + dict_repr = {name: dict_from_circuit(circuit)} from ruamel.yaml import YAML yaml = YAML() @@ -94,8 +96,3 @@ def from_circuit(circuit, path: str, name: str): from pathlib import Path path = Path(path) yaml.dump(dict_repr, path) - - -@register_interface -def to_template(path: str, template_cls): - return template_cls.from_yaml(path) diff --git a/pyrates/frontend/nxgraph.py b/pyrates/frontend/nxgraph.py deleted file mode 100755 index ec1d832b..00000000 --- a/pyrates/frontend/nxgraph.py +++ /dev/null @@ -1,271 +0,0 @@ - -# -*- coding: utf-8 -*- -# -# -# PyRates software framework for flexible implementation of neural -# network models and simulations. See also: -# https://github.com/pyrates-neuroscience/PyRates -# -# Copyright (C) 2017-2018 the original authors (Richard Gast and -# Daniel Rose), the Max-Planck-Institute for Human Cognitive Brain -# Sciences ("MPI CBS") and contributors -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see -# -# CITATION: -# -# Richard Gast and Daniel Rose et. al. in preparation -""" -""" -from copy import deepcopy - -import networkx as nx -from networkx import MultiDiGraph, DiGraph, find_cycle, NetworkXNoCycle - -from pyrates import PyRatesException -from pyrates.ir.edge import EdgeIR -from pyrates.ir.circuit import CircuitIR -from pyrates.frontend.dict import to_node -from pyrates.frontend._registry import register_interface - - -__author__ = "Daniel Rose" -__status__ = "Development" - - -# @register_interface -# def to_circuit(graph: nx.MultiDiGraph, label="circuit", -# node_creator=to_node): -# """Create a CircuitIR instance out of a networkx.MultiDiGraph""" -# -# circuit = CircuitIR(label) -# -# for name, data in graph.nodes(data=True): -# -# circuit.add_node(name, node=node_creator(data)) -# -# required_keys = ["source_var", "target_var", "weight", "delay"] -# for source, target, data in graph.edges(data=True): -# -# if all([key in data for key in required_keys]): -# if "edge_ir" not in data: -# data["edge_ir"] = EdgeIR() -# source_var = data.pop("source_var") -# target_var = data.pop("target_var") -# circuit.add_edge(f"{source}/{source_var}", f"{target}/{target_var}", **data) -# else: -# raise KeyError(f"Missing a key out of {required_keys} in an edge with source `{source}` and target" -# f"`{target}`") -# -# return circuit -# -# -# def from_circuit(circuit, revert_node_names=False): -# """Old implementation that transforms all information in a circuit to a networkx.MultiDiGraph with a few additional -# transformations that the old backend needed.""" -# return Circuit2NetDef.network_def(circuit, revert_node_names) -# -# -# class Circuit2NetDef: -# # label_counter = {} # type: Dict[str, int] -# -# @classmethod -# def network_def(cls, circuit: CircuitIR, revert_node_names=False): -# """A bit of a workaround to connect interfaces of frontend and backend. -# TODO: Remove BackendIRFormatter and adapt corresponding tests""" -# # import re -# -# network_def = MultiDiGraph() -# -# edge_list = [] -# node_dict = {} -# -# # reorganize node to conform with backend API -# ############################################# -# for node_key, data in circuit.graph.nodes(data=True): -# node = data["node"] -# # reformat all node internals into operators + operator_args -# if revert_node_names: -# names = node_key.split("/") -# node_key = ".".join(reversed(names)) -# node_dict[node_key] = {} # type: Dict[str, Union[list, dict]] -# node_dict[node_key] = dict(cls._nd_reformat_operators(node.op_graph)) -# op_order = cls._nd_get_operator_order(node.op_graph) # type: list -# # noinspection PyTypeChecker -# node_dict[node_key]["operator_order"] = op_order -# -# # reorganize edge to conform with backend API -# ############################################# -# for source, target, data in circuit.graph.edges(data=True): -# # move edge operators to node -# if revert_node_names: -# source = ".".join(reversed(source.split("/"))) -# target = ".".join(reversed(target.split("/"))) -# node_dict[target], edge = cls._move_edge_ops_to_node(target, node_dict[target], data) -# -# edge_list.append((source, target, dict(**edge))) -# -# # network_def.add_nodes_from(node_dict) -# for key, node in node_dict.items(): -# network_def.add_node(key, **node) -# network_def.add_edges_from(edge_list) -# -# return network_def # return MultiDiGraph as needed by ComputeGraph class -# -# @staticmethod -# def _nd_reformat_operators(op_graph: DiGraph): -# operator_args = dict() -# operators = dict() -# -# for op_key, op_dict in op_graph.nodes(data=True): -# op_cp = deepcopy(op_dict) # duplicate operator info -# var_dict = op_cp["operator"].variables -# for var_key, var_props in var_dict.items(): -# operator_args[f"{op_key}/{var_key}"] = var_props -# -# op_cp["equations"] = op_cp["operator"].equations -# op_cp["inputs"] = op_cp["operator"].inputs -# op_cp["output"] = op_cp["operator"].output -# # op_cp.pop("values", None) -# op_cp.pop("operator", None) -# operators[op_key] = op_cp -# -# reformatted = dict(operator_args=operator_args, -# operators=operators, -# inputs={}) -# return reformatted -# -# @staticmethod -# def _nd_get_operator_order(op_graph: DiGraph) -> list: -# """ -# -# Parameters -# ---------- -# op_graph -# -# Returns -# ------- -# op_order -# """ -# # check, if cycles are present in operator graph (which would be problematic -# try: -# find_cycle(op_graph) -# except NetworkXNoCycle: -# pass -# else: -# raise PyRatesException("Found cyclic operator graph. Cycles are not allowed for operators within one node.") -# -# op_order = [] -# graph = op_graph.copy() # type: DiGraph -# while graph.nodes: -# # noinspection PyTypeChecker -# primary_nodes = [node for node, in_degree in graph.in_degree if in_degree == 0] -# op_order.extend(primary_nodes) -# graph.remove_nodes_from(primary_nodes) -# -# return op_order -# -# @classmethod -# def _move_edge_ops_to_node(cls, target, node_dict: dict, edge_dict: dict) -> (dict, dict): -# """ -# -# Parameters -# ---------- -# target -# Key identifying target node in backend graph -# node_dict -# Dictionary of target node (to move operators into) -# edge_dict -# Dictionary with edge properties (to move operators from) -# Returns -# ------- -# node_dict -# Updated dictionary of target node -# edge_dict -# Dictionary of reformatted edge -# """ -# # grab all edge variables -# edge = edge_dict["edge_ir"] # type: EdgeIR -# source_var = edge_dict["source_var"] -# target_var = edge_dict["target_var"] -# weight = edge_dict["weight"] -# delay = edge_dict["delay"] -# input_var = edge.input -# output_var = edge.output -# -# if len(edge.op_graph) > 0: -# # reformat all edge internals into operators + operator_args -# op_data = cls._nd_reformat_operators(edge.op_graph) # type: dict -# op_order = cls._nd_get_operator_order(edge.op_graph) # type: List[str] -# operators = op_data["operators"] -# operator_args = op_data["operator_args"] -# -# # operator keys refer to a unique combination of template names and changed values -# -# # add operators to target node in reverse order, so they can be safely prepended -# added_ops = False -# for op_name in reversed(op_order): -# # check if operator name is already known in target node -# if op_name in node_dict["operators"]: -# pass -# else: -# added_ops = True -# # this should all go smoothly, because operator should not be known yet -# # add operator dict to target node operators -# node_dict["operators"][op_name] = operators[op_name] -# # prepend operator to op_order -# node_dict["operator_order"].insert(0, op_name) -# # ToDo: consider using collections.deque instead -# # add operator args to target node -# node_dict["operator_args"].update(operator_args) -# -# out_op = op_order[-1] -# out_var = operators[out_op]['output'] -# if added_ops: -# # append operator output to target operator sources -# # assume that only last operator in edge operator_order gives the output -# # for op_name in node_dict["operators"]: -# # if out_var in node_dict["operators"][op_name]["inputs"]: -# # if out_var_long not in node_dict["operators"][op_name]["inputs"][out_var]: -# # # add reference to source operator that was previously in an edge -# # node_dict["operators"][op_name]["inputs"][out_var].append(output_var) -# -# # shortcut, since target_var and output_var are known: -# target_op, target_vname = target_var.split("/") -# if output_var not in node_dict["operators"][target_op]["inputs"][target_vname]["sources"]: -# node_dict["operators"][target_op]["inputs"][target_vname]["sources"].append(out_op) -# -# # simplify edges and save into edge_list -# # op_graph = edge.op_graph -# # in_ops = [op for op, in_degree in op_graph.in_degree if in_degree == 0] -# # if len(in_ops) == 1: -# # # simple case: only one input operator? then it's the first in the operator order. -# # target_op = op_order[0] -# # target_inputs = operators[target_op]["inputs"] -# # if len(target_var) != 1: -# # raise PyRatesException("Either too many or too few input variables detected. " -# # "Needs to be exactly one.") -# # target_var = list(target_inputs.keys())[0] -# # target_var = f"{target_op}/{target_var}" -# # else: -# # raise NotImplementedError("Transforming an edge with multiple input operators is not yet handled.") -# -# # shortcut to new target war: -# target_var = input_var -# edge_dict = {"source_var": source_var, -# "target_var": target_var, -# "weight": weight, -# "delay": delay} -# # set target_var to singular input of last operator added -# return node_dict, edge_dict \ No newline at end of file diff --git a/pyrates/frontend/template/__init__.py b/pyrates/frontend/template/__init__.py index 0f9b07c8..5fb99a86 100755 --- a/pyrates/frontend/template/__init__.py +++ b/pyrates/frontend/template/__init__.py @@ -25,35 +25,127 @@ # CITATION: # # Richard Gast and Daniel Rose et. al. in preparation - +from ._io import _complete_template_path from .node import NodeTemplate from .operator import OperatorTemplate from .edge import EdgeTemplate from .circuit import CircuitTemplate -from pyrates.frontend._registry import register_interface + +known_template_classes = dict() + +template_cache = dict() + + +def register_template_class(name, cls): + """Register a given template class to the module attribute `_known_template_classes`. This way new template classes + can be registered by users. Could also be used to overwrite existing template classes.""" + + if name in known_template_classes: + raise UserWarning(f"Overwriting existing map from name `{name}` to template class `{cls}`.") + + known_template_classes[name] = cls + + +register_template_class("OperatorTemplate", OperatorTemplate) +register_template_class("NodeTemplate", NodeTemplate) +register_template_class("EdgeTemplate", EdgeTemplate) +register_template_class("CircuitTemplate", CircuitTemplate) + + +def from_file(path: str, mode: str = "yaml"): + """Generic file loader function that looks for correct template class""" + + if mode == "yaml": + loader = from_yaml + + else: + raise ValueError(f"Unknown file loading mode '{mode}'.") + + return loader(path) + + +def from_yaml(path): + """Load template from yaml file. Templates are cached by path. Depending on the 'base' key of the yaml template, + either a template class is instantiated or the function recursively loads base templates until it hits a known + template class. + + Parameters: + ----------- + path + (str) path to YAML template of the form `path.to.template_file.template_name` or + path/to/template_file/template_name.TemplateName. The dot notation refers to a path that can be found + using python's import functionality. That means it needs to be a module (a folder containing an `__init__.py`) + located in the Python path (e.g. the current working directory). The slash notation refers to a file in an + absolute or relative path from the current working directory. In either case the second-to-last part refers to + the filename without file extension and the last part refers to the template name. + """ + + if path in template_cache: + # if we have loaded this template in the past, return what has been cached + template = template_cache[path] + else: + # if it has not been cached yet, load the file and parse into dict + from pyrates.frontend.fileio.yaml import to_dict + template_dict = to_dict(path) + + try: + base = template_dict.pop("base") + except KeyError: + raise KeyError(f"No 'base' defined for template {path}. Please define a " + f"base to derive the template from.") + + # figure out which type of template this is by analysing the "base" key + try: + # If the base key coincides with any known template class name, fetch the class + cls = known_template_classes[base] + + except KeyError: + # class not known, so the base must refer to a parent template. Then let's recursively load that one until + # we hit a known template class. + base = _complete_template_path(base, path) + + base_template = from_yaml(base) + template = base_template.update_template(**template_dict) + # may fail if "base" is present but empty + else: + # instantiate template class + template = cls(**template_dict) + + template_cache[path] = template + + return template + + +def clear_cache(): + """Shorthand to clear template cache for whatever reason.""" + template_cache.clear() + + +def _select_template_class(): + pass # module-lvl functions for template conversion # writing them out explicitly -@register_interface + def to_circuit(template: CircuitTemplate): """Takes a circuit template and returns a CircuitIR instance from it.""" return template.apply() -@register_interface + def to_node(template: NodeTemplate): """Takes a node template and returns a NodeIR instance from it.""" return template.apply() -@register_interface + def to_edge(template: EdgeTemplate): """Takes a edge template and returns a EdgeIR instance from it.""" return template.apply() -@register_interface + def to_operator(template: OperatorTemplate): """Takes a operator template and returns a OperatorIR instance from it.""" return template.apply() diff --git a/pyrates/frontend/template/_io.py b/pyrates/frontend/template/_io.py new file mode 100644 index 00000000..4a50948b --- /dev/null +++ b/pyrates/frontend/template/_io.py @@ -0,0 +1,14 @@ +"""Some utility functions for template loading/writing""" + + +def _complete_template_path(target_path: str, source_path: str) -> str: + """Check if path contains a folder structure and prepend own path, if it doesn't""" + + if "." not in target_path: + if "/" in source_path or "\\" in source_path: + import os + basedir, _ = os.path.split(source_path) + target_path = os.path.normpath(os.path.join(basedir, target_path)) + else: + target_path = ".".join((*source_path.split('.')[:-1], target_path)) + return target_path \ No newline at end of file diff --git a/pyrates/frontend/template/abc.py b/pyrates/frontend/template/abc.py index 217951b9..64a413b8 100755 --- a/pyrates/frontend/template/abc.py +++ b/pyrates/frontend/template/abc.py @@ -29,8 +29,6 @@ """ Abstract base classes """ -from pyrates.frontend.yaml import to_dict as dict_from_yaml - __author__ = "Daniel Rose" __status__ = "Development" @@ -39,63 +37,49 @@ class AbstractBaseTemplate: """Abstract base class for templates""" target_ir = None # placeholder for template-specific intermediate representation (IR) target class - cache = {} # dictionary that keeps track of already loaded templates def __init__(self, name: str, path: str, description: str = "A template."): + """Basic initialiser for template classes, requires template name and path that it is loaded from. For custom + templates that are not loaded from a file, the path can be set arbitrarily.""" self.name = name self.path = path self.__doc__ = description # overwrite class-specific doc with user-defined description def __repr__(self): + """Defines how an instance identifies itself when called with `str()` or `repr()`, e.g. when shown in an + interactive terminal. Shows Class name and path that was used to construct the class.""" return f"<{self.__class__.__name__} '{self.path}'>" - @staticmethod - def _complete_template_path(target_path: str, source_path: str) -> str: - """Check if path contains a folder structure and prepend own path, if it doesn't""" - - if "." not in target_path: - if "/" in source_path or "\\" in source_path: - import os - basedir, _ = os.path.split(source_path) - target_path = os.path.normpath(os.path.join(basedir, target_path)) - else: - target_path = ".".join((*source_path.split('.')[:-1], target_path)) - return target_path - @classmethod def from_yaml(cls, path): - """Convenience method that looks for a loader class for the template type and applies it, assuming - the class naming convention '