Skip to content

Commit 1bbe71d

Browse files
committed
abstract key normlization and support type plurals
1 parent 96a7693 commit 1bbe71d

10 files changed

Lines changed: 217 additions & 22 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: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,10 @@ 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+
try:
45+
return ramble.repository.simplify_object_type(type_name).name
46+
except ramble.repository.UnknownObjectTypeError:
47+
return type_name
5048

5149

5250
def find_all_matches(name, repo_path=None, namespace=None, obj_type=None):

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: 69 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -205,26 +205,65 @@
205205
}
206206

207207

208-
@functools.lru_cache(maxsize=1)
208+
def _normalize_type_key(key):
209+
return str(key).lower().replace("-", "_").replace(" ", "_")
210+
211+
212+
_TYPE_ALIASES = {
213+
"pkg": ObjectTypes.package_managers,
214+
"package": ObjectTypes.package_managers,
215+
"packages": ObjectTypes.package_managers,
216+
"base_pkg": ObjectTypes.base_package_managers,
217+
"workflow": ObjectTypes.workflow_managers,
218+
"workflows": ObjectTypes.workflow_managers,
219+
"base": ObjectTypes.base_classes,
220+
}
221+
222+
for _obj in ObjectTypes:
223+
_tdef = type_definitions.get(_obj, {})
224+
for _val in (_obj.name, _tdef.get("singular"), _tdef.get("abbrev"), _tdef.get("dir_name")):
225+
if isinstance(_val, str):
226+
for _v in (_val, f"{_val}s"):
227+
_norm = _normalize_type_key(_v)
228+
_TYPE_ALIASES[_norm] = _obj
229+
_TYPE_ALIASES[_norm.replace("_", "-")] = _obj
230+
231+
209232
def get_object_type_map():
210233
"""Returns a mapping from string representations of object types (singular,
211234
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("-", "_")])
235+
return _TYPE_ALIASES
236+
237+
238+
def simplify_object_type(type_name):
239+
"""Convert a type string or ObjectTypes member to an ObjectTypes enum member.
240+
241+
Args:
242+
type_name (ObjectTypes | str): Object type to simplify / normalize.
243+
244+
Returns:
245+
(ObjectTypes): The matching ObjectTypes enum member.
220246
221-
for cand in candidates:
222-
mapping[cand] = obj_type
223-
return mapping
247+
Raises:
248+
UnknownObjectTypeError: If type_name does not match any valid object type.
249+
"""
250+
if isinstance(type_name, ObjectTypes):
251+
return type_name
252+
253+
if isinstance(type_name, str):
254+
key = _normalize_type_key(type_name)
255+
if key in _TYPE_ALIASES:
256+
return _TYPE_ALIASES[key]
257+
258+
raise UnknownObjectTypeError(type_name)
259+
260+
261+
get_object_type = simplify_object_type
224262

225263

226264
def _gen_path(repo_dirs=None, obj_type=default_type):
227265
"""Create a RepoPath for a specific object, add it to sys.meta_path, and return it."""
266+
obj_type = simplify_object_type(obj_type)
228267
section_name = type_definitions[obj_type]["config_section"]
229268
singular_name = type_definitions[obj_type]["singular"]
230269
repo_dirs = repo_dirs or ramble.config.get(section_name)
@@ -259,6 +298,7 @@ def list_object_files(obj_inst, object_type):
259298
This is currently used by `ramble deployment` to copy relevant files
260299
to create a self-contained repo.
261300
"""
301+
object_type = simplify_object_type(object_type)
262302
type_def = type_definitions[object_type]
263303
base_type = ObjectTypes[f"base_{type_def['dir_name']}"]
264304
base_type_def = type_definitions[base_type]
@@ -299,11 +339,13 @@ def list_object_files(obj_inst, object_type):
299339

300340
def all_object_names(object_type=default_type):
301341
"""Convenience wrapper around ``ramble.repository.all_object_names()``."""
342+
object_type = simplify_object_type(object_type)
302343
return paths[object_type].all_object_names()
303344

304345

305346
def get(spec, object_type=default_type):
306347
"""Convenience wrapper around ``ramble.repository.get()``."""
348+
object_type = simplify_object_type(object_type)
307349
return paths[object_type].get(spec)
308350

309351

@@ -314,6 +356,7 @@ def get_base_class(spec):
314356

315357
def get_obj_class(spec, object_type=default_type):
316358
"""Convenience wrapper around ``ramble.repository.get_obj_class()``."""
359+
object_type = simplify_object_type(object_type)
317360
return paths[object_type].get_obj_class(spec)
318361

319362

@@ -324,6 +367,7 @@ def set_path(repo, object_type=default_type):
324367
``sys.meta_path`` if it is a ``Repo`` or ``RepoPath``.
325368
"""
326369
global paths # noqa: F824
370+
object_type = simplify_object_type(object_type)
327371
paths[object_type] = repo
328372

329373
# make the new repo_path an importer if needed
@@ -345,6 +389,7 @@ def use_repositories(*paths_and_repos, object_type=default_type):
345389
RepoPath: Corresponding RepoPath object
346390
"""
347391
global paths # noqa: F824
392+
object_type = simplify_object_type(object_type)
348393

349394
# Construct a temporary RepoPath object from
350395
temporary_repositories = RepoPath(*paths_and_repos, object_type=object_type)
@@ -1572,6 +1617,18 @@ class RepoError(ramble.error.RambleError):
15721617
"""Superclass for repository-related errors."""
15731618

15741619

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

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,10 @@ def test_edit_no_name_with_type_editor(mock_editor):
265265
assert len(mock_editor) == 2
266266
assert "var/ramble/repos/builtin" in mock_editor[1]
267267

268+
edit("-t", "application")
269+
assert len(mock_editor) == 3
270+
assert "var/ramble/repos/builtin" in mock_editor[2]
271+
268272

269273
def test_edit_no_name_with_custom_type_repo_editor(mock_editor):
270274
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

lib/ramble/ramble/test/repository.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,94 @@ def test_list_object_files(
115115
def test_invalid_namespace(tmpdir, bad_ns):
116116
with pytest.raises(ramble.repository.InvalidNamespaceError):
117117
ramble.repository.create_repo(str(tmpdir.join("bad_repo")), namespace=bad_ns)
118+
119+
120+
@pytest.mark.parametrize(
121+
"input_type,expected_enum",
122+
[
123+
("applications", ramble.repository.ObjectTypes.applications),
124+
("application", ramble.repository.ObjectTypes.applications),
125+
("app", ramble.repository.ObjectTypes.applications),
126+
("apps", ramble.repository.ObjectTypes.applications),
127+
("modifiers", ramble.repository.ObjectTypes.modifiers),
128+
("modifier", ramble.repository.ObjectTypes.modifiers),
129+
("mod", ramble.repository.ObjectTypes.modifiers),
130+
("package_managers", ramble.repository.ObjectTypes.package_managers),
131+
("package_manager", ramble.repository.ObjectTypes.package_managers),
132+
("package-manager", ramble.repository.ObjectTypes.package_managers),
133+
("package manager", ramble.repository.ObjectTypes.package_managers),
134+
("pkg_man", ramble.repository.ObjectTypes.package_managers),
135+
("pkg", ramble.repository.ObjectTypes.package_managers),
136+
("package", ramble.repository.ObjectTypes.package_managers),
137+
("workflow_managers", ramble.repository.ObjectTypes.workflow_managers),
138+
("workflow_manager", ramble.repository.ObjectTypes.workflow_managers),
139+
("workflow-manager", ramble.repository.ObjectTypes.workflow_managers),
140+
("workflow manager", ramble.repository.ObjectTypes.workflow_managers),
141+
("wm", ramble.repository.ObjectTypes.workflow_managers),
142+
("workflow", ramble.repository.ObjectTypes.workflow_managers),
143+
("systems", ramble.repository.ObjectTypes.systems),
144+
("system", ramble.repository.ObjectTypes.systems),
145+
("sys", ramble.repository.ObjectTypes.systems),
146+
("platforms", ramble.repository.ObjectTypes.platforms),
147+
("platform", ramble.repository.ObjectTypes.platforms),
148+
("plat", ramble.repository.ObjectTypes.platforms),
149+
("base_classes", ramble.repository.ObjectTypes.base_classes),
150+
("base_class", ramble.repository.ObjectTypes.base_classes),
151+
("base-class", ramble.repository.ObjectTypes.base_classes),
152+
("base class", ramble.repository.ObjectTypes.base_classes),
153+
("base_cls", ramble.repository.ObjectTypes.base_classes),
154+
("base", ramble.repository.ObjectTypes.base_classes),
155+
("base_applications", ramble.repository.ObjectTypes.base_applications),
156+
("base_application", ramble.repository.ObjectTypes.base_applications),
157+
("base-application", ramble.repository.ObjectTypes.base_applications),
158+
("base application", ramble.repository.ObjectTypes.base_applications),
159+
("base_app", ramble.repository.ObjectTypes.base_applications),
160+
("base_modifiers", ramble.repository.ObjectTypes.base_modifiers),
161+
("base_modifier", ramble.repository.ObjectTypes.base_modifiers),
162+
("base-modifier", ramble.repository.ObjectTypes.base_modifiers),
163+
("base modifier", ramble.repository.ObjectTypes.base_modifiers),
164+
("base_mod", ramble.repository.ObjectTypes.base_modifiers),
165+
("base_package_managers", ramble.repository.ObjectTypes.base_package_managers),
166+
("base_package_manager", ramble.repository.ObjectTypes.base_package_managers),
167+
("base-package-manager", ramble.repository.ObjectTypes.base_package_managers),
168+
("base package manager", ramble.repository.ObjectTypes.base_package_managers),
169+
("base_pkg_man", ramble.repository.ObjectTypes.base_package_managers),
170+
("base_pkg", ramble.repository.ObjectTypes.base_package_managers),
171+
("base_workflow_managers", ramble.repository.ObjectTypes.base_workflow_managers),
172+
("base_workflow_manager", ramble.repository.ObjectTypes.base_workflow_managers),
173+
("base-workflow-manager", ramble.repository.ObjectTypes.base_workflow_managers),
174+
("base workflow manager", ramble.repository.ObjectTypes.base_workflow_managers),
175+
("base_wm", ramble.repository.ObjectTypes.base_workflow_managers),
176+
("base_systems", ramble.repository.ObjectTypes.base_systems),
177+
("base_system", ramble.repository.ObjectTypes.base_systems),
178+
("base-system", ramble.repository.ObjectTypes.base_systems),
179+
("base system", ramble.repository.ObjectTypes.base_systems),
180+
("base_sys", ramble.repository.ObjectTypes.base_systems),
181+
("base_platforms", ramble.repository.ObjectTypes.base_platforms),
182+
("base_platform", ramble.repository.ObjectTypes.base_platforms),
183+
("base-platform", ramble.repository.ObjectTypes.base_platforms),
184+
("base platform", ramble.repository.ObjectTypes.base_platforms),
185+
("base_plat", ramble.repository.ObjectTypes.base_platforms),
186+
("utilities", ramble.repository.ObjectTypes.utilities),
187+
("utility", ramble.repository.ObjectTypes.utilities),
188+
("base_utilities", ramble.repository.ObjectTypes.base_utilities),
189+
("base_utility", ramble.repository.ObjectTypes.base_utilities),
190+
("base-utility", ramble.repository.ObjectTypes.base_utilities),
191+
("base utility", ramble.repository.ObjectTypes.base_utilities),
192+
],
193+
)
194+
def test_simplify_object_type(input_type, expected_enum):
195+
assert ramble.repository.simplify_object_type(input_type) == expected_enum
196+
assert ramble.repository.simplify_object_type(input_type.upper()) == expected_enum
197+
assert ramble.repository.get_object_type(input_type) == expected_enum
198+
199+
200+
def test_simplify_object_type_enum_passthrough():
201+
for obj_type in ramble.repository.ObjectTypes:
202+
assert ramble.repository.simplify_object_type(obj_type) == obj_type
203+
204+
205+
@pytest.mark.parametrize("invalid_type", ["not_a_type", "foo_bar", "123", "", None])
206+
def test_simplify_object_type_invalid(invalid_type):
207+
with pytest.raises(ramble.repository.UnknownObjectTypeError):
208+
ramble.repository.simplify_object_type(invalid_type)

0 commit comments

Comments
 (0)