-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathutils.py
More file actions
246 lines (182 loc) · 7.24 KB
/
utils.py
File metadata and controls
246 lines (182 loc) · 7.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
"""
botogram.utils
Utilities used by the rest of the code
Copyright (c) 2015-2016 Pietro Albini <pietro@pietroalbini.io>
Released under the MIT license
"""
import re
import os
import sys
import gettext
import traceback
import inspect
import pkg_resources
import logbook
import functools
# URLs regex created by http://twitter.com/imme_emosol
_username_re = re.compile(r"\@([a-zA-Z0-9_]{5}[a-zA-Z0-9_]*)")
_command_re = re.compile(r"^\/[a-zA-Z0-9_]+(\@[a-zA-Z0-9_]{5}[a-zA-Z0-9_]*)?$")
_email_re = re.compile(r"[a-zA-Z0-9_\.\+\-]+\@[a-zA-Z0-9_\.\-]+\.[a-zA-Z]+")
_url_re = re.compile(r"https?://(-\.)?([^\s/?\.#]+\.?)+(/[^\s]*)?")
# This small piece of global state will track if logbook was configured
_logger_configured = False
warn_logger = logbook.Logger("botogram's code warnings")
def _deprecated_message(name, removed_on, fix, back):
before = "%s will be removed in botogram %s." % (name, removed_on)
after = "Fix: %s" % fix
warn(back - 1, before, after)
def deprecated(name, removed_on, fix):
"""Mark a function as deprecated"""
def decorator(func):
def wrapper(*args, **kwargs):
_deprecated_message(name, removed_on, fix, -2)
return func(*args, **kwargs)
return wrapper
return decorator
class DeprecatedAttributes:
"""Mark a class attribute as deprecated"""
_deprecated_ = {}
def __getattribute__(self, key):
def get(k):
return object.__getattribute__(self, k)
deprecated = get("_deprecated_")
if key in deprecated:
_deprecated_message(
get("__class__").__name__ + "." + key,
deprecated[key]["removed_on"],
deprecated[key]["fix"],
-2,
)
if "callback" in deprecated[key]:
return deprecated[key]["callback"]()
return object.__getattribute__(self, key)
def warn(stack_pos, before_message, after_message=None):
"""Issue a warning caused by user code"""
# This is a workaround for http://bugs.python.org/issue25108
# In Python 3.5.0, traceback.extract_stack returns an additional internal
# stack frame, which causes a lot of trouble around there.
if sys.version_info[:3] == (3, 5, 0):
stack_pos -= 1
frame = traceback.extract_stack()[stack_pos - 1]
at_message = "At: %s (line %s)" % (frame[0], frame[1])
warn_logger.warn(before_message)
if after_message is not None:
warn_logger.warn(at_message)
warn_logger.warn(after_message + "\n")
else:
warn_logger.warn(at_message + "\n")
def wraps(func):
"""Update a wrapper function to looks like the wrapped one"""
# A custom implementation of functools.wraps is needed because we need some
# more metadata on the returned function
def updater(original):
# Here the original signature is needed in order to call the function
# with the right set of arguments in Bot._call
original_signature = inspect.signature(original)
updated = functools.update_wrapper(original, func)
updated._botogram_original_signature = original_signature
return updated
return updater
def format_docstr(docstring):
"""Prepare a docstring for /help"""
result = []
for line in docstring.split("\n"):
stripped = line.strip()
# Allow only a blank line
if stripped == "" and len(result) and result[-1] == "":
continue
result.append(line.strip())
# Remove empty lines at the end or at the start of the docstring
for pos in 0, -1:
if result[pos] == "":
result.pop(pos)
return "\n".join(result)
def docstring_of(func, bot=None, component_id=None, format=False):
"""Get the docstring of a function"""
# Get the correct function from the hook
if hasattr(func, "_botogram_hook"):
func = func.func
if hasattr(func, "_botogram_help_message"):
if bot is not None:
docstring = bot._call(func._botogram_help_message, component_id)
else:
docstring = func._botogram_help_message()
elif func.__doc__:
docstring = func.__doc__
# Put a default message
else:
if bot is not None:
docstring = bot._("No description available.")
else:
docstring = "No description available."
if format:
docstring = "<i>%s</i>" % docstring
return format_docstr(docstring)
def strip_urls(string):
"""Strip URLs and emails from a string"""
string = _url_re.sub("", string)
string = _email_re.sub("", string)
return string
def usernames_in(message):
"""Return all the matched usernames in the message"""
# Don't parse usernames in the commands
if _command_re.match(message.split(" ", 1)[0]):
message = message.split(" ", 1)[1]
# Strip email addresses from the message, in order to avoid matching the
# user's domain. Also strip URLs, in order to avoid usernames in them.
message = strip_urls(message)
results = []
for result in _username_re.finditer(message):
if result.group(1):
results.append(result.group(1))
return results
def get_language(lang):
"""Get the GNUTranslations instance of a specific language"""
path = pkg_resources.resource_filename("botogram", "i18n/%s.mo" % lang)
if not os.path.exists(path):
raise ValueError('Language "%s" is not supported by botogram' % lang)
with open(path, "rb") as f:
gt = gettext.GNUTranslations(f)
return gt
def configure_logger():
"""Configure a logger object"""
global _logger_configured
# Don't configure the logger multiple times
if _logger_configured:
return
# The StreamHandler will log everything to stdout
min_level = 'DEBUG' if 'BOTOGRAM_DEBUG' in os.environ else 'INFO'
handler = logbook.StreamHandler(sys.stdout, level=min_level)
handler.format_string = '{record.time.hour:0>2}:{record.time.minute:0>2}' \
'.{record.time.second:0>2} - ' \
'{record.level_name:^9} - {record.message}'
handler.push_application()
# Don't reconfigure the logger, thanks
_logger_configured = True
class CallLazyArgument:
"""A special argument which is loaded lazily"""
_botogram_call_lazy_argument = True
def __init__(self, loader):
self.loader = loader
def load(self):
return self.loader()
def call(func, **available):
"""Call a function with a dynamic set of arguments"""
# Get the correct function signature
# _botogram_original_signature contains the signature used before wrapping
# a function with @utils.wraps, so the arguments gets resolved correctly
if hasattr(func, "_botogram_original_signature"):
signature = func._botogram_original_signature
else:
signature = inspect.signature(func)
kwargs = {}
for name in signature.parameters:
if name not in available:
raise TypeError("botogram doesn't know what to provide for %s"
% name)
# If the argument is lazily loaded wake him up
arg = available[name]
if hasattr(arg, "_botogram_call_lazy_argument"):
arg = arg.load()
kwargs[name] = arg
return func(**kwargs)