Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions lib/ramble/docs/dev_guides/shared/repository_create.rst
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,29 @@ when multiple exist with the same name. Each repository has a namespace, and
these namespaces can be used to refer to specific instances of each object
definition.

Referencing Objects with Namespaces
-----------------------------------

When multiple repositories contain objects with the same name, or when you want
to be explicit about which repository an object comes from, you can use fully-qualified
namespaced specs.

Namespaced specs take the form:

* ``<namespace>.<object_name>`` (e.g., ``tutorial-repo.hostname``, ``builtin.wrf``)
* ``<namespace>.<type_abbrev>.<object_name>`` (e.g., ``tutorial-repo.app.hostname``, ``builtin.mod.my_modifier``)
* ``<namespace>.<type_abbrev>.<object_name>@<version>`` (e.g., ``builtin.app.wrf@4.2``, ``builtin.app.wrf@{version}``)

Common object type abbreviations include:

* ``app`` or ``application`` for applications
* ``mod`` or ``modifier`` for modifiers
* ``pkg_man`` or ``package_manager`` for package managers
* ``wm`` or ``workflow_manager`` for workflow managers
* ``sys`` or ``system`` for systems
* ``plat`` or ``platform`` for platforms

These namespaced specs can be used in CLI commands (e.g., ``ramble info <spec>``,
``ramble edit <spec>``, ``ramble create <spec>``) and in workspace configuration files
(``ramble.yaml``) under the ``applications:`` and ``environments:`` sections.

4 changes: 3 additions & 1 deletion lib/ramble/docs/workspace_config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ In the above example, the experiment name would be: ``test_1_1`` when it is crea

**NOTE:** Each experiment has a namespace that follows this pattern:
``application.workload.experiment``. Every experiment needs a unique namespace,
or ramble will throw an error.
or ramble will throw an error. Application entries can be specified with short names
(e.g., ``hostname``), fully qualified namespaced specs (e.g., ``builtin.app.hostname``),
or with version suffixes (e.g., ``builtin.app.wrf@{version}`` or ``wrf@4.2``).

.. _variable-dictionaries:

Expand Down
19 changes: 15 additions & 4 deletions lib/ramble/ramble/cmd/common/info.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from llnl.util.tty.colify import colified

import ramble.repository
import ramble.spec
import ramble.util.colors as color
from ramble.cmd.common import arguments
from ramble.definitions.variables import Variable
Expand Down Expand Up @@ -78,7 +79,13 @@ def _map_attr_name(attr):
def setup_info_parser(subparser):
"""Create the info parser"""

subparser.add_argument("object", help="Name of object to print info for")
subparser.add_argument(
"object",
help=(
"name of object or namespaced spec to print info for "
"(e.g., my-app or builtin.app.my-app)"
),
)

arguments.add_common_arguments(subparser, ["obj_type"])

Expand Down Expand Up @@ -473,9 +480,13 @@ def print_info(args):
format_type = getattr(supported_formats, args.format)
args.format = format_type

object_type = ramble.repository.ObjectTypes[args.type]
obj_name = args.object
obj = ramble.repository.get(obj_name, object_type=object_type)
spec = ramble.spec.Spec(args.object)
if spec.object_type:
object_type = spec.object_type
else:
object_type = ramble.repository.ObjectTypes[args.type]

obj = ramble.repository.get(spec, object_type=object_type)

print_object_header(object_type, obj)

Expand Down
37 changes: 27 additions & 10 deletions lib/ramble/ramble/cmd/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,10 @@ def setup_parser(subparser):
subparser.add_argument(
"object_type",
nargs="?",
choices=list(type_mapping.keys()),
help="the type of object definition to create",
help=(
"the type of object definition to create or a namespaced spec "
"(e.g., application, or builtin.app.foo)"
),
)
subparser.add_argument(
"name",
Expand Down Expand Up @@ -196,11 +198,33 @@ def run_interactive_wizard():
def create(parser, args):
"""Main command runner logic."""

obj_type = None
name = None
repo = args.repo
base = args.base
maintainers = [m.strip() for m in args.maintainers.split(",")] if args.maintainers else []
tags = [t.strip() for t in args.tags.split(",")] if args.tags else []

if args.object_type:
spec = ramble.spec.Spec(args.object_type)
if spec.object_type and spec.name:
obj_type = spec.object_type
name = spec.name
if spec.namespace:
repo = spec.namespace
else:
type_map = ramble.repository.get_object_type_map()
if args.object_type in type_map:
obj_type = type_map[args.object_type]
name = args.name

# Check if interactive wizard is requested or needed
if args.interactive or not args.object_type or not args.name:
if args.interactive or not obj_type or not name:
import sys

if not sys.stdin.isatty():
if args.object_type and not name and obj_type:
logger.die(f"Missing name for {args.object_type}.")
logger.die(
"Interactive wizard cannot be run in a non-interactive terminal. "
"Please provide 'object_type' and 'name' arguments."
Expand All @@ -210,13 +234,6 @@ def create(parser, args):
except KeyboardInterrupt:
print("\n\n[ABORTED] Object creation cancelled.")
return 1
else:
obj_type = type_mapping[args.object_type]
name = args.name
repo = args.repo
base = args.base
maintainers = [m.strip() for m in args.maintainers.split(",")] if args.maintainers else []
tags = [t.strip() for t in args.tags.split(",")] if args.tags else []

try:
file_path, repo_namespace = ramble.creator.create_object(
Expand Down
22 changes: 19 additions & 3 deletions lib/ramble/ramble/cmd/edit.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import ramble.cmd
import ramble.paths
import ramble.repository
import ramble.spec
from ramble.util.logger import logger

from spack.util.editor import editor
Expand All @@ -25,12 +26,22 @@ def edit_object(name, obj_type_name, repo_path, namespace):
"""Opens the requested application file in your favorite $EDITOR.

Args:
name (str): The name of the application
name (str): The name of the object or a namespaced spec
obj_type_name (str): Name of the object type to edit
repo_path (str): The path to the repository containing this application
namespace (str): A valid namespace registered with Ramble
"""
obj_type = ramble.repository.ObjectTypes[obj_type_name]
spec = ramble.spec.Spec(name)
if spec.object_type:
obj_type = spec.object_type
else:
obj_type = ramble.repository.ObjectTypes[obj_type_name]

if spec.namespace and not namespace:
namespace = spec.namespace
if spec.name:
name = spec.name

# Find the location of the package
if repo_path:
repo = ramble.repository.Repo(repo_path, object_type=obj_type)
Expand Down Expand Up @@ -107,7 +118,12 @@ def setup_parser(subparser):
excl_args.add_argument("-r", "--repo", default=None, help="path to repo to edit object in")
excl_args.add_argument("-N", "--namespace", default=None, help="namespace of object to edit")

subparser.add_argument("object_name", nargs="?", default=None, help="object name")
subparser.add_argument(
"object_name",
nargs="?",
default=None,
help=("object name or namespaced spec to edit " "(e.g., my-app or builtin.app.my-app)"),
)


def edit(parser, args):
Expand Down
26 changes: 20 additions & 6 deletions lib/ramble/ramble/cmd/workspace.py
Original file line number Diff line number Diff line change
Expand Up @@ -901,19 +901,33 @@ def workspace_info(args):

# Build an index of experiments to avoid re-rendering them in the loops below
experiment_index_map = defaultdict(list)
app_names_cache = {}
for exp_name, app_inst, _ in experiment_set.all_experiments():
experiment_template_name = app_inst.variables[app_inst.keywords.experiment_template_name]
if app_inst.repeats.repeat_index:
suffix = f".{app_inst.repeats.repeat_index}"
if experiment_template_name.endswith(suffix):
experiment_template_name = experiment_template_name[: -len(suffix)]

key = (
app_inst.variables[app_inst.keywords.application_name],
app_inst.variables[app_inst.keywords.workload_template_name],
experiment_template_name,
)
experiment_index_map[key].append(exp_name)
app_ns_raw = app_inst.variables[app_inst.keywords.application_namespace]
if app_ns_raw not in app_names_cache:
app_ns_spec = ramble.spec.Spec(app_ns_raw)
app_ns = app_ns_spec.fullname
app_name = app_inst.variables[app_inst.keywords.application_name]
app_ns_no_type = (
f"{app_ns_spec.namespace}.{app_ns_spec.name}"
if app_ns_spec.namespace
else app_name
)
app_names_cache[app_ns_raw] = tuple({app_ns, app_name, app_ns_no_type})

for a_name in app_names_cache[app_ns_raw]:
key = (
a_name,
app_inst.variables[app_inst.keywords.workload_template_name],
experiment_template_name,
)
experiment_index_map[key].append(exp_name)

# Construct filters here...
filters = ramble.filters.Filters(
Expand Down
24 changes: 21 additions & 3 deletions lib/ramble/ramble/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,24 @@
}


@functools.lru_cache(maxsize=1)
def get_object_type_map():
"""Returns a mapping from string representations of object types (singular,
plural, abbrev, hyphens/underscores) to their corresponding ObjectType enum."""
mapping = {}
for obj_type, type_def in type_definitions.items():
candidates = set()
for key in ("abbrev", "dir_name", "singular"):
val = type_def.get(key)
if val:
val = val.replace(" ", "_")
candidates.update([val, val.replace("_", "-"), val.replace("-", "_")])

for cand in candidates:
mapping[cand] = obj_type
return mapping


def _gen_path(repo_dirs=None, obj_type=default_type):
"""Create a RepoPath for a specific object, add it to sys.meta_path, and return it."""
section_name = type_definitions[obj_type]["config_section"]
Expand Down Expand Up @@ -1217,12 +1235,12 @@ def filename_for_object_name(self, obj_name):
obj_dir = self.dirname_for_object_name(obj_name)
return os.path.join(obj_dir, self.object_file_name)

@autospec
def object_path(self, spec):
spec_name = spec.name if isinstance(spec, ramble.spec.Spec) else spec
return os.path.join(
self.objects_path,
self.dirname_for_object_name(spec.name),
self.filename_for_object_name(spec.name),
self.dirname_for_object_name(spec_name),
self.filename_for_object_name(spec_name),
)

@property
Expand Down
77 changes: 64 additions & 13 deletions lib/ramble/ramble/spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# option. This file may not be copied, modified, or distributed
# except according to those terms.

import functools
import io
from typing import Mapping

Expand All @@ -16,31 +17,71 @@
default_format = "{name}"


@functools.lru_cache(maxsize=None)
def _parse_spec_string(spec_like):
if not spec_like:
return "", None, None

# Strip any version suffix if present (e.g., app@1.0)
spec_like = spec_like.partition("@")[0]

parts = spec_like.split(".")

import ramble.repository

type_map = ramble.repository.get_object_type_map()
Comment thread
dapomeroy marked this conversation as resolved.

if len(parts) >= 3 and parts[-2] in type_map:
object_type = type_map[parts[-2]]
name = parts[-1]
namespace = ".".join(parts[:-2])
elif len(parts) >= 2:
if len(parts) == 2 and parts[0] in type_map:
object_type = type_map[parts[0]]
name = parts[1]
namespace = None
else:
object_type = None
name = parts[-1]
namespace = ".".join(parts[:-1])
else:
object_type = None
name = parts[0]
namespace = None

return name, namespace, object_type


class Spec:
def __init__(self, spec_like=None):
def __init__(self, spec_like=None, object_type=None):
"""Create a new Spec.

Arguments:
spec_like (optional string): If not provided we initialize an
spec_like (optional string or Spec): If not provided we initialize an
anonymous Spec that matches any Spec object; if provided we parse
this as a Spec string.
object_type (optional ObjectTypes): Optional object type enum.
"""

# Copy if spec_like is a Spec.
if isinstance(spec_like, Spec):
self._dup(spec_like)
if object_type is not None:
self.object_type = object_type
return

# init an empty spec that matches anything.
self.name = None
self.namespace = None
self.object_type = object_type

if isinstance(spec_like, str):
namespace, _, spec_name = spec_like.rpartition(".")
if not namespace:
namespace = None
self.name = spec_name
self.namespace = namespace
self._parse_spec_string(spec_like)

def _parse_spec_string(self, spec_like):
self.name, self.namespace, parsed_type = _parse_spec_string(spec_like)
if self.object_type is None:
self.object_type = parsed_type

def copy(self):
new_spec = Spec()
Expand All @@ -50,6 +91,7 @@ def copy(self):
def _dup(self, other):
self.name = other.name
self.namespace = other.namespace
self.object_type = getattr(other, "object_type", None)

def format(self, format_string=default_format, **kwargs):
r"""Prints out particular pieces of a spec, depending on what is
Expand Down Expand Up @@ -160,15 +202,24 @@ def cformat(self, *args, **kwargs):
return self.format(*args, **kwargs)

def __str__(self):
return self.name
return self.name if self.name is not None else ""

@property
def fullname(self):
return (
(f"{self.namespace}.{self.name}")
if self.namespace
else (self.name if self.name else "")
)
if not self.name:
return ""
import ramble.repository

if self.namespace:
if self.object_type:
abbrev = ramble.repository.type_definitions[self.object_type]["abbrev"]
return f"{self.namespace}.{abbrev}.{self.name}"
return f"{self.namespace}.{self.name}"
else:
if self.object_type:
abbrev = ramble.repository.type_definitions[self.object_type]["abbrev"]
return f"{abbrev}.{self.name}"
return self.name


class SpecFormatStringError(ramble.error.SpecError):
Expand Down
Loading
Loading