diff --git a/build/helper/metadata_add_all.py b/build/helper/metadata_add_all.py index 69132a934f..f28de7d9d8 100644 --- a/build/helper/metadata_add_all.py +++ b/build/helper/metadata_add_all.py @@ -729,6 +729,40 @@ def add_all_config_metadata(config): ''' config = merge_helper(config, 'config', config, use_re=False) + for repeated_capability in config['repeated_capabilities']: + documentation = repeated_capability.setdefault('documentation', {}) + prefix = repeated_capability['prefix'] + name = repeated_capability['python_name'] + + if prefix: + documentation.setdefault( + 'description', + ( + 'If no prefix is added to the items in the parameter, the correct prefix will be added when\n' + 'the driver function call is made.\n\n' + ".. code:: python\n\n session.{}['0-2'].channel_enabled = True\n\n" + "passes a string of :python:`'{}0, {}1, {}2'` to the set attribute function.\n\n" + 'If an invalid repeated capability is passed to the driver, the driver will return an error.\n\n' + 'You can also explicitly use the prefix as part of the parameter, but it must be the correct prefix\n' + 'for the specific repeated capability.' + ).format(name, prefix, prefix, prefix) + ) + else: + documentation.setdefault('description', '') + + documentation.setdefault( + 'examples', + [ + { + 'code': "session.{}['{}0-{}2'].channel_enabled = True".format(name, prefix, prefix), + 'description': ( + "passes a string of :python:`'{}0, {}1, {}2'` to the set attribute function." + ).format(prefix, prefix, prefix), + } + ] + ) + documentation.setdefault('valid_indices', []) + if 'use_locking' not in config: config['use_locking'] = True diff --git a/build/templates/rep_caps.rst.mako b/build/templates/rep_caps.rst.mako index 179d56e1fc..d95948fae6 100644 --- a/build/templates/rep_caps.rst.mako +++ b/build/templates/rep_caps.rst.mako @@ -1,5 +1,6 @@ <% import build.helper as helper + import textwrap config = template_parameters['metadata'].config module_name = config['module_name'] @@ -33,34 +34,30 @@ ${helper.get_rst_header_snippet('Repeated Capabilities', '=')} % for rep_cap in config['repeated_capabilities']: <% name = rep_cap['python_name'] -prefix = rep_cap['prefix'] +rep_cap_doc = rep_cap['documentation'] %>\ ${helper.get_rst_header_snippet(name, '-')} .. py:attribute:: ${module_name}.Session.${name}[] -% if len(prefix) > 0: - If no prefix is added to the items in the parameter, the correct prefix will be added when - the driver function call is made. +% if rep_cap_doc['description']: +${textwrap.indent(rep_cap_doc['description'], ' ')} - .. code:: python - - session.${name}['0-2'].channel_enabled = True - - passes a string of :python:`'${prefix}0, ${prefix}1, ${prefix}2'` to the set attribute function. - - If an invalid repeated capability is passed to the driver, the driver will return an error. - - You can also explicitly use the prefix as part of the parameter, but it must be the correct prefix - for the specific repeated capability. +% endif +% if rep_cap_doc['valid_indices']: + Valid Indices: :python:`'${", ".join(rep_cap_doc["valid_indices"])}'`. % endif +% for example in rep_cap_doc['examples']: .. code:: python - session.${name}['${prefix}0-${prefix}2'].channel_enabled = True +${textwrap.indent(example['code'], ' ')} - passes a string of :python:`'${prefix}0, ${prefix}1, ${prefix}2'` to the set attribute function. +% if example['description']: +${textwrap.indent(example['description'], ' ')} +% endif +% endfor % endfor diff --git a/build/unit_tests/test_metadata_add_all.py b/build/unit_tests/test_metadata_add_all.py index fdbf8e1285..252155c66f 100644 --- a/build/unit_tests/test_metadata_add_all.py +++ b/build/unit_tests/test_metadata_add_all.py @@ -961,9 +961,7 @@ def _compare_dicts(actual, expected): }, ], 'enum_whitelist_suffix': ['_POINT_FIVE'], - 'repeated_capabilities': [ - {'python_name': 'channels', 'prefix': '', }, - ], + 'repeated_capabilities': [], # These are added here strictly for testing. 'functions': {}, 'attributes': {}, @@ -1007,9 +1005,7 @@ def _compare_dicts(actual, expected): }, ], 'enum_whitelist_suffix': ['_POINT_FIVE'], - 'repeated_capabilities': [ - {'python_name': 'channels', 'prefix': '', }, - ], + 'repeated_capabilities': [], 'use_locking': True, 'functions': functions_expected, 'attributes': attributes_expected, @@ -1021,6 +1017,31 @@ def _compare_dicts(actual, expected): } +config_with_custom_rep_cap_documentation = copy.deepcopy(config_input) +config_with_custom_rep_cap_documentation['repeated_capabilities'] = [ + { + 'python_name': 'resources', + 'prefix': 'res', + 'documentation': { + 'description': 'Resources use fully-qualified identifiers.', + 'examples': [ + { + 'code': "session.resources['dev0/res0'].channel_enabled = True", + 'description': 'Enables the first resource.', + } + ], + 'valid_indices': ['dev0/res0', 'dev0/res1'], + }, + }, +] + + +config_with_custom_rep_cap_documentation_expected = copy.deepcopy(config_expected) +config_with_custom_rep_cap_documentation_expected['repeated_capabilities'] = copy.deepcopy( + config_with_custom_rep_cap_documentation['repeated_capabilities'] +) + + def _do_the_test_add_functions_metadata(functions, expected): actual = copy.deepcopy(functions) actual = add_all_function_metadata(actual, config_input) @@ -1073,9 +1094,9 @@ def test_add_all_metadata(): actual_functions = copy.deepcopy(functions_input) actual_attributes = copy.deepcopy(attributes_input) actual_enums = copy.deepcopy(enums_input) - actual_config = copy.deepcopy(config_input) + actual_config = copy.deepcopy(config_with_custom_rep_cap_documentation) actual_config['use_locking'] = False - expected = copy.deepcopy(config_expected) + expected = copy.deepcopy(config_with_custom_rep_cap_documentation_expected) expected['use_locking'] = False _do_the_test_add_all_metadata( functions=actual_functions, diff --git a/build/unit_tests/test_rep_caps_template.py b/build/unit_tests/test_rep_caps_template.py new file mode 100644 index 0000000000..b7c1a8c960 --- /dev/null +++ b/build/unit_tests/test_rep_caps_template.py @@ -0,0 +1,117 @@ +from pathlib import Path +from tempfile import TemporaryDirectory +from types import SimpleNamespace + +from build.generate_template import generate_template +from build.helper.metadata_add_all import add_all_config_metadata + + +def _render_rep_caps(config): + repo_root = Path(__file__).resolve().parents[2] + template_path = repo_root / 'build' / 'templates' / 'rep_caps.rst.mako' + metadata = SimpleNamespace(config=add_all_config_metadata(config)) + with TemporaryDirectory() as temp_dir: + output_path = Path(temp_dir) / 'rep_caps.rst' + generate_template(str(template_path), {'metadata': metadata}, str(output_path)) + return output_path.read_text() + + +def test_custom_documentation_overwrites_rep_caps_template_defaults(): + config = { + 'module_name': 'nifake', + 'c_function_prefix': 'niFake_', + 'repeated_capabilities': [ + { + 'prefix': 'res', + 'python_name': 'resources', + 'documentation': { + 'description': 'Resource repeated capabilities use fully-qualified identifiers.', + 'valid_indices': ['dev0/res0', 'dev0/res1'], + 'examples': [ + { + 'code': ( + "session.resources['dev0/res0'].channel_enabled = True\n" + "session.resources['dev0/res1'].channel_enabled = True" + ), + 'description': ( + 'The first line enables resource 0.\n' + 'The second line enables resource 1.' + ), + }, + { + 'code': "session.resources['dev0/res2'].channel_enabled = True", + 'description': '', + }, + ], + }, + } + ], + } + + rendered = _render_rep_caps(config) + + assert 'Resource repeated capabilities use fully-qualified identifiers.' in rendered + assert "Valid Indices: :python:`'dev0/res0, dev0/res1'`." in rendered + assert "session.resources['dev0/res0'].channel_enabled = True" in rendered + assert "session.resources['dev0/res1'].channel_enabled = True" in rendered + assert "session.resources['dev0/res2'].channel_enabled = True" in rendered + assert 'The first line enables resource 0.' in rendered + assert 'The second line enables resource 1.' in rendered + + # Custom documentation should override the generic auto-prefix guidance. + assert 'If no prefix is added to the items in the parameter' not in rendered + assert "session.resources['0-2'].channel_enabled = True" not in rendered + assert "'res0, res1, res2'" not in rendered + + +def test_rep_caps_template_preserves_default_prefixed_behavior(): + config = { + 'module_name': 'nifake', + 'c_function_prefix': 'niFake_', + 'repeated_capabilities': [ + { + 'prefix': 'channel', + 'python_name': 'channels', + }, + { + 'prefix': '', + 'python_name': 'instruments', + } + ], + } + + rendered = _render_rep_caps(config) + + assert 'If no prefix is added to the items in the parameter' in rendered + assert "session.channels['0-2'].channel_enabled = True" in rendered + assert "'channel0, channel1, channel2'" in rendered + example_description = ( + " passes a string of :python:`'channel0, channel1, channel2'` to the set attribute function." + ) + assert rendered.count(example_description) == 2 + assert '\n passes a string' not in rendered + assert "set attribute function.\n\n\ninstruments\n" in rendered + assert rendered.endswith('\n\n\n\n') + assert not any(line.isspace() for line in rendered.splitlines()) + + +def test_rep_caps_template_expands_default_documentation_fields(): + config = { + 'module_name': 'nifake', + 'c_function_prefix': 'niFake_', + 'repeated_capabilities': [ + { + 'prefix': 'channel', + 'python_name': 'channels', + 'documentation': { + 'description': 'Custom channel documentation.', + }, + } + ], + } + + rendered = _render_rep_caps(config) + + assert 'Custom channel documentation.' in rendered + assert "session.channels['channel0-channel2'].channel_enabled = True" in rendered + assert "'channel0, channel1, channel2'" in rendered diff --git a/generated/nifake/nifake/unit_tests/test_session.py b/generated/nifake/nifake/unit_tests/test_session.py index 36e45fb06f..94d233ec03 100644 --- a/generated/nifake/nifake/unit_tests/test_session.py +++ b/generated/nifake/nifake/unit_tests/test_session.py @@ -350,6 +350,10 @@ def test_chained_repeated_capabilities_list(self): with nifake.Session('dev1') as session: assert session.sites[0, 1].channels[2, 3]._repeated_capability_list == ['site0/2', 'site0/3', 'site1/2', 'site1/3'] + def test_multi_instrument_chained_repeated_capabilities_list(self): + with nifake.Session('dev1,dev2') as session: + assert session.instruments['dev1', 'dev2'].sites[0, 1]._repeated_capability_list == ['dev1/site0', 'dev1/site1', 'dev2/site0', 'dev2/site1'] + def test_chained_repeated_capability_method_on_specific_channel(self): test_maximum_time_ms = 10 # milliseconds test_maximum_time = hightime.timedelta(milliseconds=test_maximum_time_ms) diff --git a/src/nifake/unit_tests/test_session.py b/src/nifake/unit_tests/test_session.py index 36e45fb06f..94d233ec03 100644 --- a/src/nifake/unit_tests/test_session.py +++ b/src/nifake/unit_tests/test_session.py @@ -350,6 +350,10 @@ def test_chained_repeated_capabilities_list(self): with nifake.Session('dev1') as session: assert session.sites[0, 1].channels[2, 3]._repeated_capability_list == ['site0/2', 'site0/3', 'site1/2', 'site1/3'] + def test_multi_instrument_chained_repeated_capabilities_list(self): + with nifake.Session('dev1,dev2') as session: + assert session.instruments['dev1', 'dev2'].sites[0, 1]._repeated_capability_list == ['dev1/site0', 'dev1/site1', 'dev2/site0', 'dev2/site1'] + def test_chained_repeated_capability_method_on_specific_channel(self): test_maximum_time_ms = 10 # milliseconds test_maximum_time = hightime.timedelta(milliseconds=test_maximum_time_ms)