meerschaum.actions

Default actions available to the mrsm CLI.

  1#! /usr/bin/env python
  2# -*- coding: utf-8 -*-
  3# vim:fenc=utf-8
  4
  5"""
  6Default actions available to the mrsm CLI.
  7"""
  8
  9from __future__ import annotations
 10from meerschaum.utils.typing import Callable, Any, Optional, Union, List, Dict, SuccessTuple
 11from meerschaum.utils.packages import get_modules_from_package
 12_custom_actions = []
 13
 14def get_subactions(
 15        action: Union[str, List[str]],
 16        _actions: Optional[Dict[str, Callable[[Any], Any]]] = None,
 17    ) -> Dict[str, Callable[[Any], Any]]:
 18    """
 19    Return a dictionary of an action's sub-action functions.
 20
 21    Examples
 22    --------
 23    >>> get_subactions('install').keys()
 24    dict_keys(['packages', 'plugins', 'required'])
 25    >>> 
 26    """
 27    import importlib, inspect
 28    subactions = {}
 29    if isinstance(action, str):
 30        action = [action]
 31    action_function = get_action(action[0], _actions=_actions)
 32    if action_function is None:
 33        return subactions
 34    try:
 35        action_module = importlib.import_module(action_function.__module__)
 36    except ImportError:
 37        action_module = None
 38    if action_module is None:
 39        return subactions
 40    for name, f in inspect.getmembers(action_module):
 41        if not inspect.isfunction(f):
 42            continue
 43        if action_function.__name__ + '_' in name and not name.lstrip('_').startswith('complete'):
 44            _name = name.replace(action_function.__name__, '')
 45            _name = _name.lstrip('_')
 46            subactions[_name] = f
 47    return subactions
 48
 49
 50def get_action(
 51        action: Union[str, List[str]],
 52        _actions: Optional[Dict[str, Callable[[Any], Any]]] = None,
 53    ) -> Union[Callable[[Any], Any], None]:
 54    """
 55    Return a function corresponding to the given action list.
 56    This may be a custom action with an underscore, in which case, allow for underscores.
 57    This may also be a subactions, which is handled by `get_subactions()`
 58    """
 59    if _actions is None:
 60        _actions = actions
 61    if isinstance(action, str):
 62        action = [action]
 63
 64    if not any(action):
 65        return None
 66
 67    ### Simple case, e.g. ['show']
 68    if len(action) == 1:
 69        if action[0] in _actions:
 70            return _actions[action[0]]
 71
 72        ### e.g. ['foo'] (and no custom action available)
 73        return None
 74
 75    ### Last case: it could be a custom action with an underscore in the name.
 76    action_name_with_underscores = '_'.join(action)
 77    candidates = []
 78    for action_key, action_function in _actions.items():
 79        if not '_' in action_key:
 80            continue
 81        if action_name_with_underscores.startswith(action_key):
 82            leftovers = action_name_with_underscores.replace(action_key, '')
 83            candidates.append((len(leftovers), action_function))
 84    if len(candidates) > 0:
 85        return sorted(candidates)[0][1]
 86
 87    ### Might be dealing with a subaction.
 88    if action[0] in _actions:
 89        subactions = get_subactions([action[0]], _actions=_actions)
 90        if action[1] not in subactions:
 91            return _actions[action[0]]
 92        return subactions[action[1]]
 93
 94    return None
 95
 96
 97def get_main_action_name(
 98        action: Union[List[str], str],
 99        _actions: Optional[Dict[str, Callable[[Any], SuccessTuple]]] = None,
100    ) -> Union[str, None]:
101    """
102    Given an action list, return the name of the main function.
103    For subactions, this will return the root function.
104    If no action function can be found, return `None`.
105
106    Parameters
107    ----------
108    action: Union[List[str], str]
109        The standard action list.
110
111    Examples
112    --------
113    >>> get_main_action_name(['show', 'pipes'])
114    'show'
115    >>> 
116    """
117    if _actions is None:
118        _actions = actions
119    action_function = get_action(action, _actions=_actions)
120    if action_function is None:
121        return None
122    clean_name = action_function.__name__.lstrip('_')
123    if clean_name in _actions:
124        return clean_name
125    words = clean_name.split('_')
126    first_word = words[0]
127    if first_word in _actions:
128        return first_word
129    ### We might be dealing with shell `do_` functions.
130    second_word = words[1] if len(words) > 1 else None
131    if second_word and second_word in _actions:
132        return second_word
133    return None
134
135
136def get_completer(
137        action: Union[List[str], str],
138        _actions: Optional[Dict[str, Callable[[Any], SuccessTuple]]] = None,
139    ) -> Union[
140        Callable[['meerschaum._internal.shell.Shell', str, str, int, int], List[str]], None
141    ]:
142    """Search for a custom completer function for an action."""
143    import importlib, inspect
144    if isinstance(action, str):
145        action = [action]
146    action_function = get_action(action)
147    if action_function is None:
148        return None
149    try:
150        action_module = importlib.import_module(action_function.__module__)
151    except ImportError:
152        action_module = None
153    if action_module is None:
154        return None
155    candidates = []
156    for name, f in inspect.getmembers(action_module):
157        if not inspect.isfunction(f):
158            continue
159        name_lstrip = name.lstrip('_')
160        if not name_lstrip.startswith('complete_'):
161            continue
162        if name_lstrip == 'complete_' + action_function.__name__:
163            candidates.append((name_lstrip, f))
164            continue
165        if name_lstrip == 'complete_' + action_function.__name__.split('_')[0]:
166            candidates.append((name_lstrip, f))
167    if len(candidates) == 1:
168        return candidates[0][1]
169    if len(candidates) > 1:
170        return sorted(candidates)[-1][1]
171    
172    return None
173
174
175def choose_subaction(
176        action: Optional[List[str]] = None,
177        options: Optional[Dict[str, Any]] = None,
178        **kw
179    ) -> SuccessTuple:
180    """
181    Given a dictionary of options and the standard Meerschaum actions list,
182    check if choice is valid and execute chosen function, else show available
183    options and return False
184
185    Parameters
186    ----------
187    action: Optional[List[str]], default None
188        A list of subactions (e.g. `show pipes` -> ['pipes']).
189
190    options: Optional[Dict[str, Any]], default None
191        Available options to execute.
192        option (key) -> function (value)
193        Functions must accept **kw keyword arguments
194        and return a tuple of success bool and message.
195
196    Returns
197    -------
198    The return value of the chosen subaction (assumed to be a `SuccessTuple`).
199
200    """
201    from meerschaum.utils.warnings import warn, info
202    import inspect
203    if action is None:
204        action = []
205    if options is None:
206        options = {}
207    parent_action = inspect.stack()[1][3]
208    if len(action) == 0:
209        action = ['']
210    choice = action[0]
211
212    def valid_choice(_choice : str, _options : dict):
213        if _choice in _options:
214            return _choice
215        if (_choice + 's') in options:
216            return _choice + 's'
217        return None
218
219    parsed_choice = valid_choice(choice, options)
220    if parsed_choice is None:
221        warn(f"Cannot {parent_action} '{choice}'. Choose one:", stack=False)
222        for option in sorted(options):
223            print(f"  - {parent_action} {option}")
224        return (False, f"Invalid choice '{choice}'.")
225    ### remove parent sub-action
226    kw['action'] = list(action)
227    del kw['action'][0]
228    return options[parsed_choice](**kw)
229
230
231def _get_subaction_names(action: str, globs: dict = None) -> List[str]:
232    """Use `meerschaum.actions.get_subactions()` instead."""
233    if globs is None:
234        import importlib
235        module = importlib.import_module(f'meerschaum.actions.{action}')
236        globs = vars(module)
237    subactions = []
238    for item in globs:
239        if f'_{action}' in item and 'complete' not in item.lstrip('_'):
240            subactions.append(globs[item])
241    return subactions
242
243
244def choices_docstring(action: str, globs : Optional[Dict[str, Any]] = None) -> str:
245    """
246    Append the an action's available options to the module docstring.
247    This function is to be placed at the bottom of each action module.
248
249    Parameters
250    ----------
251    action: str
252        The name of the action module (e.g. 'install').
253        
254    globs: Optional[Dict[str, Any]], default None
255        An optional dictionary of global variables.
256
257    Returns
258    -------
259    The generated docstring for the module.
260
261    Examples
262    --------
263    >>> from meerschaum.utils.misc import choices_docstring as _choices_docstring
264    >>> install.__doc__ += _choices_docstring('install')
265
266    """
267    options_str = f"\n    Options:\n        `{action} "
268    subactions = _get_subaction_names(action, globs=globs)
269    options_str += "["
270    sa_names = []
271    for sa in subactions:
272        try:
273            sa_names.append(sa.__name__[len(f"_{action}") + 1:])
274        except Exception as e:
275            print(e)
276            return ""
277    for sa_name in sorted(sa_names):
278        options_str += f"{sa_name}, "
279    options_str = options_str[:-2] + "]`"
280    return options_str
281
282### build __all__ from other .py files in this package
283import sys
284modules = get_modules_from_package(
285    sys.modules[__name__],
286    names = False,
287)
288__all__ = ['actions', 'get_subactions', 'get_action', 'get_main_action_name', 'get_completer']
289
290### Build the actions dictionary by importing all
291### functions that do not begin with '_' from all submodules.
292from inspect import getmembers, isfunction
293actions = {}
294
295for module in modules:
296    ### A couple important things happening here:
297    ### 1. Find all functions in all modules in `actions` package
298    ###     (skip functions that begin with '_')
299    ### 2. Add them as members to the Shell class
300    ###     - Original definition : meerschaum._internal.shell.Shell
301    ###     - New definition      : meerschaum._internal.Shell
302    ### 3. Populate the actions dictionary with function names and functions
303    ###
304    ### UPDATE:
305    ### Shell modifications have been deferred to get_shell in order to improve lazy loading.
306
307    actions.update(
308        dict(
309            [
310                ### __name__ and new function pointer
311                (ob[0], ob[1])
312                    for ob in getmembers(module)
313                        if isfunction(ob[1])
314                            ### check that the function belongs to the module
315                            and ob[0] == module.__name__.replace('_', '').split('.')[-1]
316                            ### skip functions that start with '_'
317                            and ob[0][0] != '_'
318            ]
319        )
320    )
321
322original_actions = actions.copy()
323from meerschaum._internal.entry import entry, get_shell
324import meerschaum.plugins
325make_action = meerschaum.plugins.make_action
326pre_sync_hook = meerschaum.plugins.pre_sync_hook
327post_sync_hook = meerschaum.plugins.post_sync_hook
328
329### Instruct pdoc to skip the `meerschaum.actions.plugins` subdirectory.
330__pdoc__ = {
331    'plugins': False,
332    'arguments': True,
333    'shell': False,
334    'actions': (
335        "Access functions of the standard Meerschaum actions.\n\n"
336        + "Visit the [actions reference page](https://meerschaum.io/reference/actions/) "
337        + "for documentation about each action.\n\n"
338        + """\n\nExamples
339--------
340>>> actions['show'](['pipes'])
341
342"""
343        + "Keys\n----\n"
344        + ', '.join(sorted([f'`{a}`' for a in actions]))
345    )
346}
347for a in actions:
348    __pdoc__[a] = False
349meerschaum.plugins.load_plugins()
actions = {'delete': <function delete>, 'login': <function login>, 'copy': <function copy>, 'deduplicate': <function deduplicate>, 'reload': <function reload>, 'sql': <function sql>, 'edit': <function edit>, 'stack': <function stack>, 'start': <function start>, 'verify': <function verify>, 'drop': <function drop>, 'pause': <function pause>, 'uninstall': <function uninstall>, 'os': <function os>, 'register': <function register>, 'clear': <function clear>, 'install': <function install>, 'stop': <function stop>, 'python': <function python>, 'show': <function show>, 'api': <function api>, 'sync': <function sync>, 'bootstrap': <function bootstrap>, 'tag': <function tag>, 'setup': <function setup>, 'sh': <function sh>, 'upgrade': <function upgrade>, 'lup': <function lup>, 'compose': <function compose>}
def get_subactions( action: Union[str, List[str]], _actions: Optional[Dict[str, Callable[[Any], Any]]] = None) -> Dict[str, Callable[[Any], Any]]:
15def get_subactions(
16        action: Union[str, List[str]],
17        _actions: Optional[Dict[str, Callable[[Any], Any]]] = None,
18    ) -> Dict[str, Callable[[Any], Any]]:
19    """
20    Return a dictionary of an action's sub-action functions.
21
22    Examples
23    --------
24    >>> get_subactions('install').keys()
25    dict_keys(['packages', 'plugins', 'required'])
26    >>> 
27    """
28    import importlib, inspect
29    subactions = {}
30    if isinstance(action, str):
31        action = [action]
32    action_function = get_action(action[0], _actions=_actions)
33    if action_function is None:
34        return subactions
35    try:
36        action_module = importlib.import_module(action_function.__module__)
37    except ImportError:
38        action_module = None
39    if action_module is None:
40        return subactions
41    for name, f in inspect.getmembers(action_module):
42        if not inspect.isfunction(f):
43            continue
44        if action_function.__name__ + '_' in name and not name.lstrip('_').startswith('complete'):
45            _name = name.replace(action_function.__name__, '')
46            _name = _name.lstrip('_')
47            subactions[_name] = f
48    return subactions

Return a dictionary of an action's sub-action functions.

Examples
>>> get_subactions('install').keys()
dict_keys(['packages', 'plugins', 'required'])
>>>
def get_action( action: Union[str, List[str]], _actions: Optional[Dict[str, Callable[[Any], Any]]] = None) -> Optional[Callable[[Any], Any]]:
51def get_action(
52        action: Union[str, List[str]],
53        _actions: Optional[Dict[str, Callable[[Any], Any]]] = None,
54    ) -> Union[Callable[[Any], Any], None]:
55    """
56    Return a function corresponding to the given action list.
57    This may be a custom action with an underscore, in which case, allow for underscores.
58    This may also be a subactions, which is handled by `get_subactions()`
59    """
60    if _actions is None:
61        _actions = actions
62    if isinstance(action, str):
63        action = [action]
64
65    if not any(action):
66        return None
67
68    ### Simple case, e.g. ['show']
69    if len(action) == 1:
70        if action[0] in _actions:
71            return _actions[action[0]]
72
73        ### e.g. ['foo'] (and no custom action available)
74        return None
75
76    ### Last case: it could be a custom action with an underscore in the name.
77    action_name_with_underscores = '_'.join(action)
78    candidates = []
79    for action_key, action_function in _actions.items():
80        if not '_' in action_key:
81            continue
82        if action_name_with_underscores.startswith(action_key):
83            leftovers = action_name_with_underscores.replace(action_key, '')
84            candidates.append((len(leftovers), action_function))
85    if len(candidates) > 0:
86        return sorted(candidates)[0][1]
87
88    ### Might be dealing with a subaction.
89    if action[0] in _actions:
90        subactions = get_subactions([action[0]], _actions=_actions)
91        if action[1] not in subactions:
92            return _actions[action[0]]
93        return subactions[action[1]]
94
95    return None

Return a function corresponding to the given action list. This may be a custom action with an underscore, in which case, allow for underscores. This may also be a subactions, which is handled by get_subactions()

def get_main_action_name( action: Union[List[str], str], _actions: Optional[Dict[str, Callable[[Any], Tuple[bool, str]]]] = None) -> Optional[str]:
 98def get_main_action_name(
 99        action: Union[List[str], str],
100        _actions: Optional[Dict[str, Callable[[Any], SuccessTuple]]] = None,
101    ) -> Union[str, None]:
102    """
103    Given an action list, return the name of the main function.
104    For subactions, this will return the root function.
105    If no action function can be found, return `None`.
106
107    Parameters
108    ----------
109    action: Union[List[str], str]
110        The standard action list.
111
112    Examples
113    --------
114    >>> get_main_action_name(['show', 'pipes'])
115    'show'
116    >>> 
117    """
118    if _actions is None:
119        _actions = actions
120    action_function = get_action(action, _actions=_actions)
121    if action_function is None:
122        return None
123    clean_name = action_function.__name__.lstrip('_')
124    if clean_name in _actions:
125        return clean_name
126    words = clean_name.split('_')
127    first_word = words[0]
128    if first_word in _actions:
129        return first_word
130    ### We might be dealing with shell `do_` functions.
131    second_word = words[1] if len(words) > 1 else None
132    if second_word and second_word in _actions:
133        return second_word
134    return None

Given an action list, return the name of the main function. For subactions, this will return the root function. If no action function can be found, return None.

Parameters
  • action (Union[List[str], str]): The standard action list.
Examples
>>> get_main_action_name(['show', 'pipes'])
'show'
>>>
def get_completer( action: Union[List[str], str], _actions: Optional[Dict[str, Callable[[Any], Tuple[bool, str]]]] = None) -> Optional[Callable[[meerschaum._internal.shell.Shell.Shell, str, str, int, int], List[str]]]:
137def get_completer(
138        action: Union[List[str], str],
139        _actions: Optional[Dict[str, Callable[[Any], SuccessTuple]]] = None,
140    ) -> Union[
141        Callable[['meerschaum._internal.shell.Shell', str, str, int, int], List[str]], None
142    ]:
143    """Search for a custom completer function for an action."""
144    import importlib, inspect
145    if isinstance(action, str):
146        action = [action]
147    action_function = get_action(action)
148    if action_function is None:
149        return None
150    try:
151        action_module = importlib.import_module(action_function.__module__)
152    except ImportError:
153        action_module = None
154    if action_module is None:
155        return None
156    candidates = []
157    for name, f in inspect.getmembers(action_module):
158        if not inspect.isfunction(f):
159            continue
160        name_lstrip = name.lstrip('_')
161        if not name_lstrip.startswith('complete_'):
162            continue
163        if name_lstrip == 'complete_' + action_function.__name__:
164            candidates.append((name_lstrip, f))
165            continue
166        if name_lstrip == 'complete_' + action_function.__name__.split('_')[0]:
167            candidates.append((name_lstrip, f))
168    if len(candidates) == 1:
169        return candidates[0][1]
170    if len(candidates) > 1:
171        return sorted(candidates)[-1][1]
172    
173    return None

Search for a custom completer function for an action.