Skip to content

Commit 96a7693

Browse files
Merge pull request #1709 from dapomeroy/report_format_lines
Allow namespaced specs
2 parents 051fe4c + e8fcc69 commit 96a7693

15 files changed

Lines changed: 620 additions & 91 deletions

File tree

lib/ramble/docs/dev_guides/shared/repository_create.rst

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,3 +82,30 @@ when multiple exist with the same name. Each repository has a namespace, and
8282
these namespaces can be used to refer to specific instances of each object
8383
definition.
8484

85+
Referencing Objects with Namespaces
86+
-----------------------------------
87+
88+
When multiple repositories contain objects with the same name, or when you want
89+
to be explicit about which repository an object comes from, you can use fully-qualified
90+
namespaced specs.
91+
92+
Namespaced specs take the form:
93+
94+
* ``<namespace>.<object_name>`` (e.g., ``tutorial-repo.hostname``, ``builtin.wrf``)
95+
* ``<namespace>.<type_abbrev>.<object_name>`` (e.g., ``tutorial-repo.app.hostname``, ``builtin.mod.my_modifier``)
96+
* ``<namespace>.<type_abbrev>.<object_name>@<version>`` (e.g., ``builtin.app.wrf@4.2``, ``builtin.app.wrf@{version}``)
97+
98+
Common object type abbreviations include:
99+
100+
* ``app`` or ``application`` for applications
101+
* ``mod`` or ``modifier`` for modifiers
102+
* ``pkg_man`` or ``package_manager`` for package managers
103+
* ``wm`` or ``workflow_manager`` for workflow managers
104+
* ``sys`` or ``system`` for systems
105+
* ``plat`` or ``platform`` for platforms
106+
107+
Base objects can be referenced with the ``base_<type_abbrev>`` prefix.
108+
109+
These namespaced specs can be used in CLI commands (e.g., ``ramble info <spec>``,
110+
``ramble edit <spec>``, ``ramble create <spec>``) and in workspace configuration files
111+
(``ramble.yaml``) under the ``applications:`` and ``environments:`` sections.

lib/ramble/docs/workspace_config.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,9 @@ In the above example, the experiment name would be: ``test_1_1`` when it is crea
9393

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

98100
.. _variable-dictionaries:
99101

lib/ramble/ramble/cmd/common/info.py

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from llnl.util.tty.colify import colified
1414

1515
import ramble.repository
16+
import ramble.spec
1617
import ramble.util.colors as color
1718
from ramble.cmd.common import arguments
1819
from ramble.definitions.variables import Variable
@@ -78,7 +79,13 @@ def _map_attr_name(attr):
7879
def setup_info_parser(subparser):
7980
"""Create the info parser"""
8081

81-
subparser.add_argument("object", help="Name of object to print info for")
82+
subparser.add_argument(
83+
"object",
84+
help=(
85+
"name of object or namespaced spec to print info for "
86+
"(e.g., my-app or builtin.app.my-app)"
87+
),
88+
)
8289

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

@@ -473,9 +480,13 @@ def print_info(args):
473480
format_type = getattr(supported_formats, args.format)
474481
args.format = format_type
475482

476-
object_type = ramble.repository.ObjectTypes[args.type]
477-
obj_name = args.object
478-
obj = ramble.repository.get(obj_name, object_type=object_type)
483+
spec = ramble.spec.Spec(args.object)
484+
if spec.object_type:
485+
object_type = spec.object_type
486+
else:
487+
object_type = ramble.repository.ObjectTypes[args.type]
488+
489+
obj = ramble.repository.get(spec, object_type=object_type)
479490

480491
print_object_header(object_type, obj)
481492

lib/ramble/ramble/cmd/create.py

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
import ramble.creator
1111
import ramble.repository
12+
import ramble.spec
1213
import ramble.util.naming as nm
1314
from ramble.util.logger import logger
1415

@@ -33,8 +34,10 @@ def setup_parser(subparser):
3334
subparser.add_argument(
3435
"object_type",
3536
nargs="?",
36-
choices=list(type_mapping.keys()),
37-
help="the type of object definition to create",
37+
help=(
38+
"the type of object definition to create or a namespaced spec "
39+
"(e.g., application, or builtin.app.foo)"
40+
),
3841
)
3942
subparser.add_argument(
4043
"name",
@@ -196,11 +199,33 @@ def run_interactive_wizard():
196199
def create(parser, args):
197200
"""Main command runner logic."""
198201

202+
obj_type = None
203+
name = None
204+
repo = args.repo
205+
base = args.base
206+
maintainers = [m.strip() for m in args.maintainers.split(",")] if args.maintainers else []
207+
tags = [t.strip() for t in args.tags.split(",")] if args.tags else []
208+
209+
if args.object_type:
210+
spec = ramble.spec.Spec(args.object_type)
211+
if spec.object_type and spec.name:
212+
obj_type = spec.object_type
213+
name = spec.name
214+
if spec.namespace:
215+
repo = spec.namespace
216+
else:
217+
type_map = ramble.repository.get_object_type_map()
218+
if args.object_type in type_map:
219+
obj_type = type_map[args.object_type]
220+
name = args.name
221+
199222
# Check if interactive wizard is requested or needed
200-
if args.interactive or not args.object_type or not args.name:
223+
if args.interactive or not obj_type or not name:
201224
import sys
202225

203226
if not sys.stdin.isatty():
227+
if args.object_type and not name and obj_type:
228+
logger.die(f"Missing name for {args.object_type}.")
204229
logger.die(
205230
"Interactive wizard cannot be run in a non-interactive terminal. "
206231
"Please provide 'object_type' and 'name' arguments."
@@ -210,13 +235,6 @@ def create(parser, args):
210235
except KeyboardInterrupt:
211236
print("\n\n[ABORTED] Object creation cancelled.")
212237
return 1
213-
else:
214-
obj_type = type_mapping[args.object_type]
215-
name = args.name
216-
repo = args.repo
217-
base = args.base
218-
maintainers = [m.strip() for m in args.maintainers.split(",")] if args.maintainers else []
219-
tags = [t.strip() for t in args.tags.split(",")] if args.tags else []
220238

221239
try:
222240
file_path, repo_namespace = ramble.creator.create_object(

lib/ramble/ramble/cmd/edit.py

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import ramble.cmd
1313
import ramble.paths
1414
import ramble.repository
15+
import ramble.spec
1516
from ramble.util.logger import logger
1617

1718
from spack.util.editor import editor
@@ -25,8 +26,8 @@ def normalize_type_name(type_name):
2526
if not type_name:
2627
return None
2728

28-
# Map aliases
29-
aliases = {
29+
# Map non-object type aliases
30+
extra_type_aliases = {
3031
"test": "test",
3132
"tests": "test",
3233
"command": "command",
@@ -36,18 +37,14 @@ def normalize_type_name(type_name):
3637
"module": "module",
3738
"modules": "module",
3839
}
39-
if type_name in aliases:
40-
return aliases[type_name]
41-
42-
# Map singular to plural for ObjectTypes
43-
norm_type = type_name.lower().replace("-", " ").replace("_", " ")
44-
for obj_type in ramble.repository.ObjectTypes:
45-
if norm_type == obj_type.name.lower().replace("_", " "):
46-
return obj_type.name
47-
if obj_type in ramble.repository.type_definitions:
48-
singular = ramble.repository.type_definitions[obj_type]["singular"]
49-
if norm_type == singular.lower().replace("_", " ").replace("-", " "):
50-
return obj_type.name
40+
norm_type = type_name.lower().strip().replace(" ", "_")
41+
if norm_type in extra_type_aliases:
42+
return extra_type_aliases[norm_type]
43+
44+
# Map object types using repository's get_object_type_map()
45+
type_map = ramble.repository.get_object_type_map()
46+
if norm_type in type_map:
47+
return type_map[norm_type].name
5148

5249
return type_name
5350

@@ -65,27 +62,36 @@ def find_all_matches(name, repo_path=None, namespace=None, obj_type=None):
6562
extra_types = ["test", "command", "docs", "module"]
6663
allowed_types = ramble.repository.OBJECT_NAMES + extra_types
6764

68-
types_to_check = [obj_type] if obj_type else allowed_types
65+
# Parse spec if name might be a namespaced spec
66+
spec = ramble.spec.Spec(name)
67+
spec_obj_type = spec.object_type.name if spec.object_type else None
68+
spec_namespace = spec.namespace
69+
spec_name = spec.name if spec.name else name
70+
71+
effective_obj_type = obj_type or spec_obj_type
72+
types_to_check = [effective_obj_type] if effective_obj_type else allowed_types
73+
74+
effective_namespace = namespace or spec_namespace
6975

7076
for t in types_to_check:
7177
if t in ramble.repository.OBJECT_NAMES:
7278
# Check object type
73-
obj_type = ramble.repository.ObjectTypes[t]
79+
repo_obj_type = ramble.repository.ObjectTypes[t]
7480
if repo_path:
7581
try:
76-
repos = [ramble.repository.Repo(repo_path, object_type=obj_type)]
82+
repos = [ramble.repository.Repo(repo_path, object_type=repo_obj_type)]
7783
except Exception:
7884
repos = []
79-
elif namespace:
85+
elif effective_namespace:
8086
try:
81-
repos = [ramble.repository.paths[obj_type].get_repo(namespace)]
87+
repos = [ramble.repository.paths[repo_obj_type].get_repo(effective_namespace)]
8288
except Exception:
8389
repos = []
8490
else:
85-
repos = ramble.repository.paths[obj_type].repos
91+
repos = ramble.repository.paths[repo_obj_type].repos
8692

8793
for repo in repos:
88-
path = repo.filename_for_object_name(name)
94+
path = repo.filename_for_object_name(spec_name)
8995
if os.path.isfile(path):
9096
matches.append({"type": t, "path": path, "repo_namespace": repo.namespace})
9197
elif t in extra_types:
@@ -142,7 +148,12 @@ def setup_parser(subparser):
142148
excl_args.add_argument("-r", "--repo", default=None, help="path to repo to edit object in")
143149
excl_args.add_argument("-N", "--namespace", default=None, help="namespace of object to edit")
144150

145-
subparser.add_argument("object_name", nargs="?", default=None, help="object name")
151+
subparser.add_argument(
152+
"object_name",
153+
nargs="?",
154+
default=None,
155+
help=("object name or namespaced spec to edit " "(e.g., my-app or builtin.app.my-app)"),
156+
)
146157

147158

148159
def edit(parser, args):
@@ -206,7 +217,12 @@ def edit(parser, args):
206217

207218
# If no matches found, reproduce the original behavior/messages:
208219
# 1. If type is specified/defaulted, we show type-specific "not found" messages.
209-
type_name = args.type or ramble.repository.default_type.name
220+
spec = ramble.spec.Spec(name)
221+
spec_obj_type = spec.object_type.name if spec.object_type else None
222+
type_name = args.type or spec_obj_type or ramble.repository.default_type.name
223+
effective_namespace = args.namespace or spec.namespace
224+
spec_name = spec.name if spec.name else name
225+
210226
type_to_path = {
211227
"test": ramble.paths.test_path,
212228
"command": ramble.paths.command_path,
@@ -226,11 +242,11 @@ def edit(parser, args):
226242
obj_type = ramble.repository.ObjectTypes[type_name]
227243
if args.repo:
228244
repo = ramble.repository.Repo(args.repo, object_type=obj_type)
229-
elif args.namespace:
230-
repo = ramble.repository.paths[obj_type].get_repo(args.namespace)
245+
elif effective_namespace:
246+
repo = ramble.repository.paths[obj_type].get_repo(effective_namespace)
231247
else:
232248
repo = ramble.repository.paths[obj_type]
233-
path = repo.filename_for_object_name(name)
249+
path = repo.filename_for_object_name(spec_name)
234250
except Exception:
235251
path = None
236252

lib/ramble/ramble/cmd/workspace.py

Lines changed: 31 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
import ramble.expander
2626
import ramble.filters
2727
import ramble.pipeline
28+
import ramble.repository
2829
import ramble.software_environments
30+
import ramble.spec
2931
import ramble.util.colors as color
3032
import ramble.workspace
3133
import ramble.workspace.shell
@@ -901,19 +903,42 @@ def workspace_info(args):
901903

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

911-
key = (
912-
app_inst.variables[app_inst.keywords.application_name],
913-
app_inst.variables[app_inst.keywords.workload_template_name],
914-
experiment_template_name,
915-
)
916-
experiment_index_map[key].append(exp_name)
914+
app_ns_raw = app_inst.variables[app_inst.keywords.application_namespace]
915+
if app_ns_raw not in app_names_cache:
916+
app_ns_spec = ramble.spec.Spec(app_ns_raw)
917+
app_name = app_inst.variables[app_inst.keywords.application_name]
918+
try:
919+
repo_ns = (
920+
app_ns_spec.namespace
921+
or ramble.repository.paths[ramble.repository.ObjectTypes.applications]
922+
.repo_for_obj(app_name)
923+
.namespace
924+
)
925+
except Exception:
926+
repo_ns = app_ns_spec.namespace
927+
928+
names = {app_name, app_ns_spec.fullname, f"app.{app_name}"}
929+
if repo_ns:
930+
names.add(f"{repo_ns}.{app_name}")
931+
names.add(f"{repo_ns}.app.{app_name}")
932+
933+
app_names_cache[app_ns_raw] = tuple(names)
934+
935+
for a_name in app_names_cache[app_ns_raw]:
936+
key = (
937+
a_name,
938+
app_inst.variables[app_inst.keywords.workload_template_name],
939+
experiment_template_name,
940+
)
941+
experiment_index_map[key].append(exp_name)
917942

918943
# Construct filters here...
919944
filters = ramble.filters.Filters(

lib/ramble/ramble/repository.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,24 @@
205205
}
206206

207207

208+
@functools.lru_cache(maxsize=1)
209+
def get_object_type_map():
210+
"""Returns a mapping from string representations of object types (singular,
211+
plural, abbrev, hyphens/underscores) to their corresponding ObjectType enum."""
212+
mapping = {}
213+
for obj_type, type_def in type_definitions.items():
214+
candidates = set()
215+
for key in ("abbrev", "dir_name", "singular"):
216+
val = type_def.get(key)
217+
if val:
218+
val = val.replace(" ", "_")
219+
candidates.update([val, val.replace("_", "-"), val.replace("-", "_")])
220+
221+
for cand in candidates:
222+
mapping[cand] = obj_type
223+
return mapping
224+
225+
208226
def _gen_path(repo_dirs=None, obj_type=default_type):
209227
"""Create a RepoPath for a specific object, add it to sys.meta_path, and return it."""
210228
section_name = type_definitions[obj_type]["config_section"]
@@ -1223,12 +1241,12 @@ def filename_for_object_name(self, obj_name):
12231241
obj_dir = self.dirname_for_object_name(obj_name)
12241242
return os.path.join(obj_dir, self.object_file_name)
12251243

1226-
@autospec
12271244
def object_path(self, spec):
1245+
spec_name = spec.name if isinstance(spec, ramble.spec.Spec) else spec
12281246
return os.path.join(
12291247
self.objects_path,
1230-
self.dirname_for_object_name(spec.name),
1231-
self.filename_for_object_name(spec.name),
1248+
self.dirname_for_object_name(spec_name),
1249+
self.filename_for_object_name(spec_name),
12321250
)
12331251

12341252
@property

0 commit comments

Comments
 (0)