Skip to content

Commit 185471f

Browse files
Merge pull request #1724 from rfbgo/plural_type
abstract key normlization and support type plurals
2 parents 68681d4 + a65ed83 commit 185471f

10 files changed

Lines changed: 222 additions & 30 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ def print_info(args):
484484
if spec.object_type:
485485
object_type = spec.object_type
486486
else:
487-
object_type = ramble.repository.ObjectTypes[args.type]
487+
object_type = ramble.repository.simplify_object_type(args.type)
488488

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

lib/ramble/ramble/cmd/common/list.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,7 @@ def perform_list(args):
162162
# retrieve the formatter to use from args
163163
formatter = formatters[args.format]
164164

165-
object_type = ramble.repository.ObjectTypes[args.type]
165+
object_type = ramble.repository.simplify_object_type(args.type)
166166

167167
sorted_objects = object_utils.filter_by_name(args.filter, args.search_description, object_type)
168168

lib/ramble/ramble/cmd/edit.py

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,7 @@ def normalize_type_name(type_name):
4141
if norm_type in extra_type_aliases:
4242
return extra_type_aliases[norm_type]
4343

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
48-
49-
return type_name
44+
return ramble.repository.simplify_object_type(type_name).name
5045

5146

5247
def find_all_matches(name, repo_path=None, namespace=None, obj_type=None):
@@ -162,11 +157,6 @@ def edit(parser, args):
162157
# Normalize input type if specified
163158
if args.type:
164159
args.type = normalize_type_name(args.type)
165-
extra_types = ["test", "command", "docs", "module"]
166-
allowed_types = ramble.repository.OBJECT_NAMES + extra_types
167-
if args.type not in allowed_types:
168-
# Trigger KeyError like the original code did
169-
_ = ramble.repository.ObjectTypes[args.type]
170160

171161
if name:
172162
matches = find_all_matches(name, args.repo, args.namespace, args.type)

lib/ramble/ramble/cmd/repo.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -360,7 +360,7 @@ def repo(parser, args):
360360
"rm": repo_remove,
361361
}
362362

363-
if args.type != "any" and args.type not in ramble.repository.OBJECT_NAMES:
364-
logger.die(f"Repository type '{args.type}' is not valid.")
363+
if args.type != "any":
364+
args.type = ramble.repository.simplify_object_type(args.type).name
365365

366366
action[args.repo_command](args)

lib/ramble/ramble/repository.py

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@
101101
"config_section": "package_manager_repos",
102102
"accepted_configs": ["package_manager_repo.yaml", unified_config],
103103
"singular": "package manager",
104+
"aliases": ["pkg", "package"],
104105
},
105106
ObjectTypes.workflow_managers: {
106107
"file_name": "workflow_manager.py",
@@ -109,6 +110,7 @@
109110
"config_section": "workflow_manager_repos",
110111
"accepted_configs": ["workflow_manager_repo.yaml", unified_config],
111112
"singular": "workflow manager",
113+
"aliases": ["workflow"],
112114
},
113115
ObjectTypes.systems: {
114116
"file_name": "system.py",
@@ -133,6 +135,7 @@
133135
"config_section": "base_class_repos",
134136
"accepted_configs": ["base_class_repo.yaml", unified_config],
135137
"singular": "base class",
138+
"aliases": ["base"],
136139
},
137140
ObjectTypes.base_applications: {
138141
"file_name": "base_application.py",
@@ -157,6 +160,7 @@
157160
"config_section": "base_package_manager_repos",
158161
"accepted_configs": ["base_package_manager_repo.yaml", unified_config],
159162
"singular": "base package manager",
163+
"aliases": ["base_pkg"],
160164
},
161165
ObjectTypes.base_workflow_managers: {
162166
"file_name": "base_workflow_manager.py",
@@ -165,6 +169,7 @@
165169
"config_section": "base_workflow_manager_repos",
166170
"accepted_configs": ["base_workflow_manager_repo.yaml", unified_config],
167171
"singular": "base workflow manager",
172+
"aliases": ["base_workflow"],
168173
},
169174
ObjectTypes.base_systems: {
170175
"file_name": "base_system.py",
@@ -205,26 +210,64 @@
205210
}
206211

207212

208-
@functools.lru_cache(maxsize=1)
213+
def _normalize_type_key(key):
214+
return str(key).lower().replace("-", "_").replace(" ", "_")
215+
216+
217+
_TYPE_ALIASES = {}
218+
219+
for _obj in ObjectTypes:
220+
_tdef = type_definitions.get(_obj, {})
221+
_candidates = [
222+
_obj.name,
223+
_tdef.get("singular"),
224+
_tdef.get("abbrev"),
225+
_tdef.get("dir_name"),
226+
*_tdef.get("aliases", []),
227+
]
228+
for _val in _candidates:
229+
if isinstance(_val, str):
230+
for _v in (_val, f"{_val}s"):
231+
_norm = _normalize_type_key(_v)
232+
_TYPE_ALIASES[_norm] = _obj
233+
_TYPE_ALIASES[_norm.replace("_", "-")] = _obj
234+
235+
209236
def get_object_type_map():
210237
"""Returns a mapping from string representations of object types (singular,
211238
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("-", "_")])
239+
return _TYPE_ALIASES
240+
220241

221-
for cand in candidates:
222-
mapping[cand] = obj_type
223-
return mapping
242+
def simplify_object_type(type_name):
243+
"""Convert a type string or ObjectTypes member to an ObjectTypes enum member.
244+
245+
Args:
246+
type_name (ObjectTypes | str): Object type to simplify / normalize.
247+
248+
Returns:
249+
(ObjectTypes): The matching ObjectTypes enum member.
250+
251+
Raises:
252+
UnknownObjectTypeError: If type_name does not match any valid object type.
253+
"""
254+
if isinstance(type_name, ObjectTypes):
255+
return type_name
256+
257+
if isinstance(type_name, str):
258+
key = _normalize_type_key(type_name)
259+
if key in _TYPE_ALIASES:
260+
return _TYPE_ALIASES[key]
261+
262+
raise UnknownObjectTypeError(type_name)
263+
264+
265+
get_object_type = simplify_object_type
224266

225267

226268
def _gen_path(repo_dirs=None, obj_type=default_type):
227269
"""Create a RepoPath for a specific object, add it to sys.meta_path, and return it."""
270+
obj_type = simplify_object_type(obj_type)
228271
section_name = type_definitions[obj_type]["config_section"]
229272
singular_name = type_definitions[obj_type]["singular"]
230273
repo_dirs = repo_dirs or ramble.config.get(section_name)
@@ -259,6 +302,7 @@ def list_object_files(obj_inst, object_type):
259302
This is currently used by `ramble deployment` to copy relevant files
260303
to create a self-contained repo.
261304
"""
305+
object_type = simplify_object_type(object_type)
262306
type_def = type_definitions[object_type]
263307
base_type = ObjectTypes[f"base_{type_def['dir_name']}"]
264308
base_type_def = type_definitions[base_type]
@@ -299,11 +343,13 @@ def list_object_files(obj_inst, object_type):
299343

300344
def all_object_names(object_type=default_type):
301345
"""Convenience wrapper around ``ramble.repository.all_object_names()``."""
346+
object_type = simplify_object_type(object_type)
302347
return paths[object_type].all_object_names()
303348

304349

305350
def get(spec, object_type=default_type):
306351
"""Convenience wrapper around ``ramble.repository.get()``."""
352+
object_type = simplify_object_type(object_type)
307353
return paths[object_type].get(spec)
308354

309355

@@ -314,6 +360,7 @@ def get_base_class(spec):
314360

315361
def get_obj_class(spec, object_type=default_type):
316362
"""Convenience wrapper around ``ramble.repository.get_obj_class()``."""
363+
object_type = simplify_object_type(object_type)
317364
return paths[object_type].get_obj_class(spec)
318365

319366

@@ -324,6 +371,7 @@ def set_path(repo, object_type=default_type):
324371
``sys.meta_path`` if it is a ``Repo`` or ``RepoPath``.
325372
"""
326373
global paths # noqa: F824
374+
object_type = simplify_object_type(object_type)
327375
paths[object_type] = repo
328376

329377
# make the new repo_path an importer if needed
@@ -345,6 +393,7 @@ def use_repositories(*paths_and_repos, object_type=default_type):
345393
RepoPath: Corresponding RepoPath object
346394
"""
347395
global paths # noqa: F824
396+
object_type = simplify_object_type(object_type)
348397

349398
# Construct a temporary RepoPath object from
350399
temporary_repositories = RepoPath(*paths_and_repos, object_type=object_type)
@@ -1572,6 +1621,18 @@ class RepoError(ramble.error.RambleError):
15721621
"""Superclass for repository-related errors."""
15731622

15741623

1624+
class UnknownObjectTypeError(RepoError):
1625+
"""Raised when an unknown or invalid object type is specified."""
1626+
1627+
def __init__(self, type_name):
1628+
valid_types = ", ".join(OBJECT_NAMES)
1629+
super().__init__(
1630+
f"Unknown object type '{type_name}'.",
1631+
f"Allowed types are: {valid_types} "
1632+
"(singular, plural, and abbreviation forms are accepted).",
1633+
)
1634+
1635+
15751636
class NoRepoConfiguredError(RepoError):
15761637
"""Raised when there are no repositories configured."""
15771638

lib/ramble/ramble/test/cmd/edit.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
import pytest
1010

11+
import ramble.repository
1112
from ramble.main import RambleCommand
1213

1314
edit = RambleCommand("edit")
@@ -118,7 +119,7 @@ def test_edit_singular_type_with_spaces_hyphens(mock_editor):
118119

119120

120121
def test_edit_unknown_type():
121-
with pytest.raises(KeyError):
122+
with pytest.raises(ramble.repository.UnknownObjectTypeError):
122123
edit("-t", "unknown_type", "spack")
123124

124125

@@ -174,8 +175,8 @@ def test_normalize_type_name():
174175
assert normalize_type_name("TEST") == "test"
175176
assert normalize_type_name("Command") == "command"
176177
assert normalize_type_name("APP") == "applications"
177-
assert normalize_type_name("MODIFIER") == "modifiers"
178-
assert normalize_type_name("unknown_type") == "unknown_type"
178+
with pytest.raises(ramble.repository.UnknownObjectTypeError):
179+
normalize_type_name("unknown_type")
179180

180181

181182
def test_edit_abbreviated_type(mock_modifiers, mock_editor):
@@ -272,6 +273,10 @@ def test_edit_no_name_with_type_editor(mock_editor):
272273
assert len(mock_editor) == 2
273274
assert "var/ramble/repos/builtin" in mock_editor[1]
274275

276+
edit("-t", "application")
277+
assert len(mock_editor) == 3
278+
assert "var/ramble/repos/builtin" in mock_editor[2]
279+
275280

276281
def test_edit_no_name_with_custom_type_repo_editor(mock_editor):
277282
edit("-t", "applications", "--repo", "/non-existent-path")

lib/ramble/ramble/test/cmd/info.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,9 +110,14 @@ def test_mock_spack_info_software(mock_applications, app_query):
110110
"info_query",
111111
[
112112
["--type", "modifiers", "apptainer"],
113+
["--type", "modifier", "apptainer"],
113114
["--type", "modifiers", "apptainer", "-vv"],
114115
["--type", "package_managers", "spack"],
116+
["--type", "package_manager", "spack"],
117+
["--type", "package-manager", "spack"],
115118
["--type", "workflow_managers", "slurm"],
119+
["--type", "workflow_manager", "slurm"],
120+
["--type", "workflow-manager", "slurm"],
116121
],
117122
)
118123
def test_non_app_object_info_common_fields(info_query):

lib/ramble/ramble/test/cmd/list.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,30 @@ def test_list_base_html():
7575
assert '<div class="section" id="hpl">' in output
7676

7777

78+
@pytest.mark.parametrize(
79+
"type_arg",
80+
[
81+
"applications",
82+
"application",
83+
"app",
84+
"base_applications",
85+
"base_application",
86+
"base-application",
87+
"modifiers",
88+
"modifier",
89+
"package_managers",
90+
"package_manager",
91+
"package-manager",
92+
"workflow_managers",
93+
"workflow_manager",
94+
],
95+
)
96+
def test_list_types(type_arg):
97+
output = list("--type", type_arg)
98+
assert output is not None
99+
assert len(output) > 0
100+
101+
78102
def test_list_update(tmpdir):
79103
update_file = tmpdir.join("output")
80104

lib/ramble/ramble/test/cmd/repo.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,3 +181,19 @@ def test_add_repo_missing_config_file(mutable_config, tmpdir):
181181

182182
expected_error_message = f"No valid config file found in '{repo_path}'"
183183
assert expected_error_message in str(e.value)
184+
185+
186+
@pytest.mark.parametrize("type_arg", ["application", "app"])
187+
def test_singular_type_repo_commands(mutable_config, tmpdir, type_arg):
188+
repo_path = str(tmpdir.join(f"test_repo_{type_arg}"))
189+
repo("create", repo_path, f"mockrepo_{type_arg}", "-t", type_arg)
190+
assert os.path.exists(os.path.join(repo_path, "application_repo.yaml"))
191+
assert os.path.exists(os.path.join(repo_path, "applications"))
192+
193+
repo("add", "-t", type_arg, "--scope=site", repo_path)
194+
output = repo("list", "-t", type_arg, "--scope=site", output=str)
195+
assert f"mockrepo_{type_arg}" in output
196+
197+
repo("remove", "-t", type_arg, "--scope=site", repo_path)
198+
output = repo("list", "-t", type_arg, "--scope=site", output=str)
199+
assert f"mockrepo_{type_arg}" not in output

0 commit comments

Comments
 (0)