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

Search for a custom completer function for an action.