Skip to content

Commit f140865

Browse files
authored
utils.py cleanup. (errbotio#991)
* utils.py cleanup. I just applied a couple principles: - This should not be an API of Errbot - If not used in Errbot, it is removed - If is it used only once in Errbot, it is moved to the spot where it is used - If it is used a couple times in Errbot and doesn't have a natural spot within the source tree, it stays. * linting
1 parent e0124b8 commit f140865

File tree

13 files changed

+159
-197
lines changed

13 files changed

+159
-197
lines changed

docs/user_guide/plugin_development/exceptions.rst

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,15 +43,15 @@ When handling exceptions, follow these steps:
4343
not re-raising it, we prevent that automatic "Computer says nooo.
4444
..." message from being sent
4545

46-
Also, note that there is a ``utils.ValidationException`` class which you
46+
Also, note that there is a ``errbot.ValidationException`` class which you
4747
can use inside your helper methods to raise meaningful errors and handle
4848
them accordingly.
4949

5050
Here's an example:
5151

5252
.. code-block:: python
5353
54-
from errbot import BotPlugin, arg_botcmd, utils
54+
from errbot import BotPlugin, arg_botcmd, ValidationException
5555
5656
class FooBot(BotPlugin):
5757
"""An example bot"""
@@ -61,7 +61,7 @@ Here's an example:
6161
"""Add your first name if it doesn't contain any digits"""
6262
try:
6363
FooBot.validate_first_name(first_name)
64-
except utils.ValidationException as exc:
64+
except ValidationException as exc:
6565
self.log.exception(
6666
'first_name=%s contained a digit' % first_name
6767
)
@@ -74,7 +74,7 @@ Here's an example:
7474
@staticmethod
7575
def validate_first_name(first_name):
7676
if any(char.isdigit() for char in first_name):
77-
raise utils.ValidationException(
77+
raise ValidationException(
7878
"first_name=%s contained a digit" % first_name
7979
)
8080

errbot/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from .core_plugins.wsview import bottle_app, WebView
1313
from .backends.base import Message, ONLINE, OFFLINE, AWAY, DND # noqa
14-
from .botplugin import BotPlugin, SeparatorArgParser, ShlexArgParser, CommandError, Command # noqa
14+
from .botplugin import BotPlugin, SeparatorArgParser, ShlexArgParser, CommandError, Command, ValidationException # noqa
1515
from .flow import FlowRoot, BotFlow, Flow, FLOW_END
1616
from .core_plugins.wsview import route, view # noqa
1717
from . import core

errbot/botplugin.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,45 @@
55
from typing import Tuple, Callable, Mapping, Sequence
66
from io import IOBase
77

8-
from .utils import recurse_check_structure
98
from .storage import StoreMixin, StoreNotOpenError
109
from errbot.backends.base import Message, Presence, Stream, Room, Identifier, ONLINE, Card
1110

1211
log = logging.getLogger(__name__)
1312

1413

14+
class ValidationException(Exception):
15+
pass
16+
17+
18+
def _recurse_check_plugin_configuration(sample, to_check):
19+
sample_type = type(sample)
20+
to_check_type = type(to_check)
21+
22+
# Skip this check if the sample is None because it will always be something
23+
# other than NoneType when changed from the default. Raising ValidationException
24+
# would make no sense then because it would defeat the whole purpose of having
25+
# that key in the sample when it could only ever be None.
26+
if sample is not None and sample_type != to_check_type:
27+
raise ValidationException(
28+
'%s [%s] is not the same type as %s [%s]' % (sample, sample_type, to_check, to_check_type))
29+
30+
if sample_type in (list, tuple):
31+
for element in to_check:
32+
_recurse_check_plugin_configuration(sample[0], element)
33+
return
34+
35+
if sample_type == dict:
36+
for key in sample:
37+
if key not in to_check:
38+
raise ValidationException("%s doesn't contain the key %s" % (to_check, key))
39+
for key in to_check:
40+
if key not in sample:
41+
raise ValidationException("%s contains an unknown key %s" % (to_check, key))
42+
for key in sample:
43+
_recurse_check_plugin_configuration(sample[key], to_check[key])
44+
return
45+
46+
1547
class CommandError(Exception):
1648
"""
1749
Use this class to report an error condition from your commands, the command
@@ -312,7 +344,7 @@ def check_configuration(self, configuration: Mapping) -> None:
312344
313345
:param configuration: the configuration to be checked.
314346
"""
315-
recurse_check_structure(self.get_configuration_template(), configuration) # default behavior
347+
_recurse_check_plugin_configuration(self.get_configuration_template(), configuration) # default behavior
316348

317349
def configure(self, configuration: Mapping) -> None:
318350
"""

errbot/core.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
from .storage import StoreMixin
3131
from .streaming import Tee
3232
from .templating import tenv
33-
from .utils import split_string_after, get_class_that_defined_method
33+
from .utils import split_string_after
3434

3535
log = logging.getLogger(__name__)
3636

@@ -653,8 +653,15 @@ def get_doc(self, command):
653653
pat = re.compile(r'!({})'.format('|'.join(ununderscore_keys)))
654654
return re.sub(pat, self.prefix + '\1', command.__doc__)
655655

656+
@staticmethod
657+
def get_plugin_class_from_method(meth):
658+
for cls in inspect.getmro(type(meth.__self__)):
659+
if meth.__name__ in cls.__dict__:
660+
return cls
661+
return None
662+
656663
def get_command_classes(self):
657-
return (get_class_that_defined_method(command)
664+
return (self.get_plugin_class_from_method(command)
658665
for command in self.all_commands.values())
659666

660667
def shutdown(self):

errbot/core_plugins/help.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
from errbot import BotPlugin, botcmd
44
from errbot.version import VERSION
5-
from errbot.utils import get_class_that_defined_method
65

76

87
class Help(BotPlugin):
@@ -29,7 +28,7 @@ def apropos(self, mess, args):
2928

3029
cls_commands = {}
3130
for (name, command) in self._bot.all_commands.items():
32-
cls = get_class_that_defined_method(command)
31+
cls = self._bot.get_plugin_class_from_method(command)
3332
cls = str.__module__ + '.' + cls.__name__ # makes the fuul qualified name
3433
commands = cls_commands.get(cls, [])
3534
if not self.bot_config.HIDE_RESTRICTED_COMMANDS or self._bot.check_command_access(mess, name)[0]:
@@ -79,7 +78,7 @@ def get_name(named):
7978
if not args:
8079
cls_commands = {}
8180
for (name, command) in self._bot.all_commands.items():
82-
cls = get_class_that_defined_method(command)
81+
cls = self._bot.get_plugin_class_from_method(command)
8382
commands = cls_commands.get(cls, [])
8483
if not self.bot_config.HIDE_RESTRICTED_COMMANDS or may_access_command(mess, name):
8584
commands.append((name, command))
@@ -107,7 +106,7 @@ def get_name(named):
107106
commands = [
108107
(name, command) for (name, command)
109108
in self._bot.all_commands.items() if
110-
get_name(get_class_that_defined_method(command)) == args]
109+
get_name(self._bot.get_plugin_class_from_method(command)) == args]
111110

112111
description = '\n**{name}**\n\n*{doc}*\n\n'.format(
113112
name=cls.__name__,

errbot/core_plugins/utils.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
from os import path
22

33
from errbot import BotPlugin, botcmd
4-
from errbot.utils import tail
4+
5+
6+
def tail(f, window=20):
7+
return ''.join(f.readlines()[-window:])
58

69

710
class Utils(BotPlugin):

errbot/plugin_manager.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
from errbot.flow import BotFlow
1414
from .botplugin import BotPlugin
15-
from .utils import version2array, collect_roots, ensure_sys_path_contains
15+
from .utils import version2array, collect_roots
1616
from .templating import remove_plugin_templates_path, add_plugin_templates_path
1717
from .version import VERSION
1818
from yapsy.PluginManager import PluginManager
@@ -46,6 +46,19 @@ class PluginConfigurationException(PluginActivationException):
4646
pass
4747

4848

49+
def _ensure_sys_path_contains(paths):
50+
""" Ensure that os.path contains paths
51+
:param base_paths:
52+
a list of base paths to walk from
53+
elements can be a string or a list/tuple of strings
54+
"""
55+
for entry in paths:
56+
if isinstance(entry, (list, tuple)):
57+
_ensure_sys_path_contains(entry)
58+
elif entry is not None and entry not in sys.path:
59+
sys.path.append(entry)
60+
61+
4962
def populate_doc(plugin):
5063
plugin_type = type(plugin.plugin_object)
5164
plugin_type.__errdoc__ = plugin_type.__doc__ if plugin_type.__doc__ else plugin.description
@@ -370,7 +383,7 @@ def update_plugin_places(self, path_list, extra_plugin_dir, autoinstall_deps=Tru
370383
log.debug("Add %s to sys.path", entry)
371384
sys.path.append(entry)
372385
# so plugins can relatively import their repos
373-
ensure_sys_path_contains(repo_roots)
386+
_ensure_sys_path_contains(repo_roots)
374387

375388
errors = {}
376389
if autoinstall_deps:

errbot/repo_manager.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from errbot.plugin_manager import check_dependencies
1818
from errbot.storage import StoreMixin
19-
from .utils import which
19+
from .utils import ON_WINDOWS
2020

2121
log = logging.getLogger(__name__)
2222

@@ -61,6 +61,26 @@ def tokenizeJsonEntry(json_dict):
6161
return set(find_words.findall(' '.join((word.lower() for word in json_dict.values()))))
6262

6363

64+
def which(program):
65+
if ON_WINDOWS:
66+
program += '.exe'
67+
68+
def is_exe(file_path):
69+
return os.path.isfile(file_path) and os.access(file_path, os.X_OK)
70+
71+
fpath, fname = os.path.split(program)
72+
if fpath:
73+
if is_exe(program):
74+
return program
75+
else:
76+
for path in os.environ["PATH"].split(os.pathsep):
77+
exe_file = os.path.join(path, program)
78+
if is_exe(exe_file):
79+
return exe_file
80+
81+
return None
82+
83+
6484
class BotRepoManager(StoreMixin):
6585
"""
6686
Manages the repo list, git clones/updates or the repos.

errbot/streaming.py

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11

22
import os
33
import io
4+
from itertools import starmap, repeat
45
from threading import Thread
56
from .backends.base import STREAM_WAITING_TO_START, STREAM_TRANSFER_IN_PROGRESS
67
import logging
7-
from .utils import repeatfunc
88

99
CHUNK_SIZE = 4096
1010

1111
log = logging.getLogger(__name__)
1212

1313

14+
def repeatfunc(func, times=None, *args): # from the itertools receipes
15+
"""Repeat calls to func with specified arguments.
16+
17+
Example: repeatfunc(random.random)
18+
19+
:param args: params to the function to call.
20+
:param times: number of times to repeat.
21+
:param func: the function to repeatedly call.
22+
"""
23+
if times is None:
24+
return starmap(func, repeat(args))
25+
return starmap(func, repeat(args, times))
26+
27+
1428
class Tee(object):
1529
""" Tee implements a multi reader / single writer """
1630
def __init__(self, incoming_stream, clients):

0 commit comments

Comments
 (0)