Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Repeated Capabilites documentation improvements #1686

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e9b6f72
Repeated Capabilites documentation improvements
ni-jfitzger Jan 21, 2022
7b3a1a5
codegen with rep_caps.rst changes
ni-jfitzger Jan 24, 2022
8bc69bc
Remove comment containing tips we decided weren't useful
ni-jfitzger Jan 28, 2022
93d62f1
Update general description of repeated capabilities.
ni-jfitzger Jan 28, 2022
e4ba124
Merge branch 'master' of https://github.com/ni-jfitzger/nimi-python i…
ni-jfitzger Apr 3, 2023
4fda89a
indexing is for repeated capability **instance**
ni-jfitzger Apr 3, 2023
948db7a
Remove commented section for non-0-length prefixes
ni-jfitzger Apr 3, 2023
f2dc5ea
Merge branch 'master' of https://github.com/ni-jfitzger/nimi-python i…
ni-jfitzger May 6, 2023
f04652c
Merge branch 'master' of https://github.com/ni-jfitzger/nimi-python i…
ni-jfitzger Jul 11, 2023
d48cfc9
generate correct snippets for nidigital
ni-jfitzger Jul 11, 2023
1cc9cf4
generate correct rep cap documentation for nidcpower
ni-jfitzger Jul 11, 2023
ce93c56
generate correct rep cap documentation for nifgen
ni-jfitzger Jul 11, 2023
8e8833c
merge indices, string_indices fields
ni-jfitzger Jul 11, 2023
cf13613
genereate correct rep cap documentation for niscope
ni-jfitzger Jul 11, 2023
76fede8
generate correct rep_cap documentation for niswitch
ni-jfitzger Jul 11, 2023
fb8c3a0
parameterize the new tests
ni-jfitzger Jul 11, 2023
ad59a96
reduce code duplication in new helper functions
ni-jfitzger Jul 11, 2023
30efefd
eliminate most rep cap example defaults
ni-jfitzger Jul 11, 2023
e461d65
Omit examples of elements and iterables at the top. Specify element t…
ni-jfitzger Jul 15, 2023
19b2208
Remove the "or errors if the value is not the same for all." statement
ni-jfitzger Jul 15, 2023
d2c61ae
like -> such as
ni-jfitzger Jul 15, 2023
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
2 changes: 2 additions & 0 deletions build/helper/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
from build.helper.documentation_helper import get_function_docstring # noqa: F401
from build.helper.documentation_helper import get_function_rst # noqa: F401
from build.helper.documentation_helper import get_indented_docstring_snippet # noqa: F401
from build.helper.documentation_helper import get_repeated_capability_single_index_python_example # noqa: F401
from build.helper.documentation_helper import get_repeated_capability_tuple_index_python_example # noqa: F401
from build.helper.documentation_helper import get_rst_header_snippet # noqa: F401
from build.helper.documentation_helper import get_rst_picture_reference # noqa: F401
from build.helper.documentation_helper import module_supports_repeated_caps # noqa: F401
Expand Down
104 changes: 104 additions & 0 deletions build/helper/documentation_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,110 @@ def get_indented_docstring_snippet(d, indent=4):
return ret_val


def _get_repeated_capability_example_info(rep_cap_config):
'''Returns values needed for building a rep cap doc snippet and explanation.'''
index = 0
indices = ["0", "2"] # use strings so that we can call join
value_type = None # we only set this for enum values

attr_for_example = rep_cap_config['attr_for_docs_example']
attr_type_for_example = rep_cap_config['attr_type_for_docs_example']
if attr_type_for_example == 'property':
class_attr_ref = f':py:attr:`{attr_for_example}`'
elif attr_type_for_example == 'method':
class_attr_ref = f':py:meth:`{attr_for_example}`'

if 'indices_for_docs_example' in rep_cap_config:
index = rep_cap_config["indices_for_docs_example"][0]
if isinstance(index, str):
index = repr(index)
indices = [repr(index) for index in rep_cap_config["indices_for_docs_example"]]

value = rep_cap_config['value_for_docs_example']
value_type = type(value)
if 'value_type_for_docs_example' in rep_cap_config:
value_type = rep_cap_config['value_type_for_docs_example']
if not value_type == 'enum' and isinstance(value, str):
value = repr(value)

explanation_value = f':python:`{value}`'
if value_type == 'enum':
explanation_value = f':py:data:`~{value}`'

ret_val = {
'attr_for_example': attr_for_example,
'attr_type_for_example': attr_type_for_example,
'class_attr_ref': class_attr_ref,
'explanation_value': explanation_value,
'index': index,
'indices': indices,
'value': value,
}
return ret_val


def get_repeated_capability_single_index_python_example(rep_cap_config):
'''Returns a python code snippet and explanation for example usage of a repeated capability.'''
rep_cap_name = rep_cap_config['python_name']

rep_cap_info = _get_repeated_capability_example_info(rep_cap_config)
attr_for_example = rep_cap_info['attr_for_example']
attr_type_for_example = rep_cap_info['attr_type_for_example']
class_attr_ref = rep_cap_info['class_attr_ref']
explanation_value = rep_cap_info['explanation_value']
index = rep_cap_info['index']
value = rep_cap_info['value']

if attr_type_for_example == "property":
if value is None:
snippet = f'print(session.{rep_cap_name}[{index}].{attr_for_example})'
explanation = f"prints {class_attr_ref} for {rep_cap_name} {index}."
else:
snippet = f'session.{rep_cap_name}[{index}].{attr_for_example} = {value}'
explanation = f"sets {class_attr_ref} to {explanation_value} for {rep_cap_name} {index}."
elif attr_type_for_example == "method":
if value is None:
snippet = f'session.{rep_cap_name}[{index}].{attr_for_example}()'
explanation = f"calls {class_attr_ref} for {rep_cap_name} {index}."
else:
snippet = f'session.{rep_cap_name}[{index}].{attr_for_example}({value})'
explanation = f"calls {class_attr_ref} with {explanation_value} for {rep_cap_name} {index}."
else:
raise ValueError(f"Ilegal value {attr_type_for_example} in {repr(rep_cap_config)}.")
return snippet, explanation


def get_repeated_capability_tuple_index_python_example(rep_cap_config):
'''Returns a python code snippet and explanation for example usage of a repeated capability.'''
rep_cap_name = rep_cap_config['python_name']

rep_cap_info = _get_repeated_capability_example_info(rep_cap_config)
attr_for_example = rep_cap_info['attr_for_example']
attr_type_for_example = rep_cap_info['attr_type_for_example']
class_attr_ref = rep_cap_info['class_attr_ref']
explanation_value = rep_cap_info['explanation_value']
indices = rep_cap_info['indices']
value = rep_cap_info['value']

if attr_type_for_example == "property":
if value is None:
snippet = f'print(session.{rep_cap_name}[{", ".join(indices)}].{attr_for_example})'
explanation = f"prints {class_attr_ref} for {rep_cap_name} {', '.join(indices)} or errors if the value is not the same for all."
else:
snippet = f'session.{rep_cap_name}[{", ".join(indices)}].{attr_for_example} = {value}'
explanation = f"sets {class_attr_ref} to {explanation_value} for {rep_cap_name} {', '.join(indices)}."
elif attr_type_for_example == "method":
if value is None:
snippet = f'session.{rep_cap_name}[{", ".join(indices)}].{attr_for_example}()'
explanation = f"calls {class_attr_ref} for {rep_cap_name} {', '.join(indices)}."
else:
snippet = f'session.{rep_cap_name}[{", ".join(indices)}].{attr_for_example}({value})'
explanation = f"calls {class_attr_ref} with {explanation_value} for {rep_cap_name} {', '.join(indices)}."
else:
raise ValueError(f"Ilegal value {attr_type_for_example} in {repr(rep_cap_config)}.")
return snippet, explanation


def get_rst_header_snippet(t, header_level='='):
'''Get rst formatted heading'''
ret_val = t + '\n'
Expand Down
47 changes: 23 additions & 24 deletions build/templates/rep_caps.rst.mako
Original file line number Diff line number Diff line change
Expand Up @@ -19,48 +19,47 @@

${helper.get_rst_header_snippet('Repeated Capabilities', '=')}

Repeated capabilities attributes are used to set the `channel_string` parameter to the
underlying driver function call. This can be the actual function based on the :py:class:`Session`
method being called, or it can be the appropriate Get/Set Attribute function, such as :c:`${config['c_function_prefix']}SetAttributeViInt32()`.
:py:class:`${module_name}.Session` supports "Repeated Capabilities", which are multiple instances of the same type of
functionality. The repeated capabilities supported by :py:class:`${module_name}.Session` are:

Repeated capabilities attributes use the indexing operator :python:`[]` to indicate the repeated capabilities.
The parameter can be a string, list, tuple, or slice (range). Each element of those can be a string or
an integer. If it is a string, you can indicate a range using the same format as the driver: :python:`'0-2'` or
:python:`'0:2'`
% for rep_cap in config['repeated_capabilities']:
<%
name = rep_cap['python_name']
%>\
#. ${name}_
% endfor

Use the indexing operator :python:`[]` to indicate which repeated capability instance you are trying to access.
The parameter can be a single element or an iterable that implements sequence semantics, like list, tuple, range and slice.

The recommended way of accessing a single repeated capability is with an integer :python:`[0]` for capabilities that support it and a string :python:`['Dev1']`
for those that don't support integers.

Some repeated capabilities use a prefix before the number and this is optional
The recommended way of accessing multiple repeated capabilites at once is with a tuple (:python:`[0, 1]` or :python:`['Dev1', 'Dev2']`) or slice :python:`[0:2]`.
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not currently providing slice usage examples. Let me know if you want me to add them.

Copy link
Member

@marcoskirsch marcoskirsch Jul 12, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that would be nice.
I wonder if we should change wording to say that it should be an iterable rather than provide examples of iterables. Or at least it should be :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, we already mention supported values above:

Use the indexing operator [] to indicate which repeated capability instance you are trying to access. The parameter can be a single element or an iterable that implements sequence semantics, like list, tuple, range and slice.

I guess it wouldn't hurt, to keep this part more generic, though, especially given our examples below.


% for rep_cap in config['repeated_capabilities']:
<%
name = rep_cap['python_name']
prefix = rep_cap['prefix']

single_index_snippet, single_index_explanation = helper.get_repeated_capability_single_index_python_example(rep_cap)
tuple_index_snippet, tuple_index_explanation = helper.get_repeated_capability_tuple_index_python_example(rep_cap)

%>\
${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.

.. code:: python

session.${name}['0-2'].channel_enabled = True
${single_index_snippet}

passes a string of :python:`'${prefix}0, ${prefix}1, ${prefix}2'` to the set attribute function.
${single_index_explanation}

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
.. code:: python

session.${name}['${prefix}0-${prefix}2'].channel_enabled = True

passes a string of :python:`'${prefix}0, ${prefix}1, ${prefix}2'` to the set attribute function.
${tuple_index_snippet}

${tuple_index_explanation}

% endfor

196 changes: 196 additions & 0 deletions build/unit_tests/test_documentation_helper.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from build.helper.documentation_helper import *


Expand Down Expand Up @@ -727,3 +729,197 @@ def test_add_notes_re_links():
assert attr_note_text in local_config['functions']['MakeAFoo']['parameters'][1]['documentation']['note']
assert enum_note_text in local_config['functions']['MakeAFoo']['parameters'][1]['documentation']['note']


@pytest.mark.parametrize(
"rep_cap,snippet,explanation",
[
pytest.param(
{
'attr_for_docs_example': 'channel_enabled',
'attr_type_for_docs_example': 'property',
'prefix': '',
'python_name': 'channels',
'value_for_docs_example': True,
},
'session.channels[0].channel_enabled = True',
'sets :py:attr:`channel_enabled` to :python:`True` for channels 0.',
id="defaults",
),
pytest.param(
{
'attr_for_docs_example': 'exported_pattern_opcode_event_output_terminal',
'attr_type_for_docs_example': 'property',
'prefix': 'patternOpcodeEvent',
'python_name': 'pattern_opcode_events',
'value_for_docs_example': '/Dev1/PXI_Trig0',
},
"session.pattern_opcode_events[0].exported_pattern_opcode_event_output_terminal = '/Dev1/PXI_Trig0'",
"sets :py:attr:`exported_pattern_opcode_event_output_terminal` to :python:`'/Dev1/PXI_Trig0'` for pattern_opcode_events 0.",
id="property_with_string_val",
),
pytest.param(
{
'attr_for_docs_example': 'channel_enabled',
'attr_type_for_docs_example': 'property',
'indices_for_docs_example': [0, 1],
'prefix': '',
'python_name': 'channels',
'value_for_docs_example': True,
},
'session.channels[0].channel_enabled = True',
'sets :py:attr:`channel_enabled` to :python:`True` for channels 0.',
id="custom_indices",
),
pytest.param(
{
'attr_for_docs_example': 'vil',
'attr_type_for_docs_example': 'property',
'indices_for_docs_example': ["PinA", "PinB", "CPin"],
'prefix': '',
'python_name': 'pins',
'value_for_docs_example': 2,
},
"session.pins['PinA'].vil = 2",
"sets :py:attr:`vil` to :python:`2` for pins 'PinA'.",
id="string_indices_with_numerical_val",
),
pytest.param(
{
'attr_for_docs_example': 'disable_sites',
'attr_type_for_docs_example': 'method',
'prefix': 'site',
'python_name': 'sites',
'value_for_docs_example': None,
},
"session.sites[0].disable_sites()",
"calls :py:meth:`disable_sites` for sites 0.",
id="method_no_val",
),
pytest.param(
{
'attr_for_docs_example': 'serial_number',
'attr_type_for_docs_example': 'property',
'indices_for_docs_example': ["Dev1", "Dev2", "3rdDevice"],
'prefix': '',
'python_name': 'instruments',
'value_for_docs_example': None,
},
"print(session.instruments['Dev1'].serial_number)",
"prints :py:attr:`serial_number` for instruments 'Dev1'.",
id="property_no_val",
),
pytest.param(
{
'attr_for_docs_example': 'conditional_jump_trigger_type',
'attr_type_for_docs_example': 'property',
'prefix': 'conditionalJumpTrigger',
'python_name': 'conditional_jump_triggers',
'value_for_docs_example': 'nidigital.TriggerType.DIGITAL_EDGE',
'value_type_for_docs_example': 'enum',
},
"session.conditional_jump_triggers[0].conditional_jump_trigger_type = nidigital.TriggerType.DIGITAL_EDGE",
"sets :py:attr:`conditional_jump_trigger_type` to :py:data:`~nidigital.TriggerType.DIGITAL_EDGE` for conditional_jump_triggers 0.",
id="enum_val",
),
],
)
def test_get_repeated_capability_single_index_python_example(rep_cap, snippet, explanation):
assert (snippet, explanation) == get_repeated_capability_single_index_python_example(rep_cap)


@pytest.mark.parametrize(
"rep_cap,snippet,explanation",
[
pytest.param(
{
'attr_for_docs_example': 'channel_enabled',
'attr_type_for_docs_example': 'property',
'prefix': '',
'python_name': 'channels',
'value_for_docs_example': True,
},
'session.channels[0, 2].channel_enabled = True',
'sets :py:attr:`channel_enabled` to :python:`True` for channels 0, 2.',
id="defaults",
),
pytest.param(
{
'attr_for_docs_example': 'exported_pattern_opcode_event_output_terminal',
'attr_type_for_docs_example': 'property',
'prefix': 'patternOpcodeEvent',
'python_name': 'pattern_opcode_events',
'value_for_docs_example': '/Dev1/PXI_Trig0',
},
"session.pattern_opcode_events[0, 2].exported_pattern_opcode_event_output_terminal = '/Dev1/PXI_Trig0'",
"sets :py:attr:`exported_pattern_opcode_event_output_terminal` to :python:`'/Dev1/PXI_Trig0'` for pattern_opcode_events 0, 2.",
id="property_with_string_val",
),
pytest.param(
{
'attr_for_docs_example': 'channel_enabled',
'attr_type_for_docs_example': 'property',
'indices_for_docs_example': [0, 1],
'prefix': '',
'python_name': 'channels',
'value_for_docs_example': True,
},
'session.channels[0, 1].channel_enabled = True',
'sets :py:attr:`channel_enabled` to :python:`True` for channels 0, 1.',
id="custom_indices",
),
pytest.param(
{
'attr_for_docs_example': 'vil',
'attr_type_for_docs_example': 'property',
'indices_for_docs_example': ["PinA", "PinB", "CPin"],
'prefix': '',
'python_name': 'pins',
'value_for_docs_example': 2,
},
"session.pins['PinA', 'PinB', 'CPin'].vil = 2",
"sets :py:attr:`vil` to :python:`2` for pins 'PinA', 'PinB', 'CPin'.",
id="string_indices_with_numerical_val",
),
pytest.param(
{
'attr_for_docs_example': 'disable_sites',
'attr_type_for_docs_example': 'method',
'prefix': 'site',
'python_name': 'sites',
'value_for_docs_example': None,
},
"session.sites[0, 2].disable_sites()",
"calls :py:meth:`disable_sites` for sites 0, 2.",
id="method_no_val",
),
pytest.param(
{
'attr_for_docs_example': 'serial_number',
'attr_type_for_docs_example': 'property',
'indices_for_docs_example': ["Dev1", "Dev2", "3rdDevice"],
'prefix': '',
'python_name': 'instruments',
'value_for_docs_example': None,
},
"print(session.instruments['Dev1', 'Dev2', '3rdDevice'].serial_number)",
"prints :py:attr:`serial_number` for instruments 'Dev1', 'Dev2', '3rdDevice' or errors if the value is not the same for all.",
id="property_no_val",
),
pytest.param(
{
'attr_for_docs_example': 'conditional_jump_trigger_type',
'attr_type_for_docs_example': 'property',
'prefix': 'conditionalJumpTrigger',
'python_name': 'conditional_jump_triggers',
'value_for_docs_example': 'nidigital.TriggerType.DIGITAL_EDGE',
'value_type_for_docs_example': 'enum',
},
"session.conditional_jump_triggers[0, 2].conditional_jump_trigger_type = nidigital.TriggerType.DIGITAL_EDGE",
"sets :py:attr:`conditional_jump_trigger_type` to :py:data:`~nidigital.TriggerType.DIGITAL_EDGE` for conditional_jump_triggers 0, 2.",
id="enum_val",
),
],
)
def test_get_repeated_capability_tuple_index_python_example(rep_cap, snippet, explanation):
assert (snippet, explanation) == get_repeated_capability_tuple_index_python_example(rep_cap)

Loading