Script: pybuffer.py

Debugging tool for python scripts or test WeeChat's API functions.
Author: m4v — Version: 0.3 — License: GPL-3.0-or-later
For WeeChat ≥ 0.3.0.
Tags: debug, py2, py3
Added: 2010-10-31 — Updated: 2018-11-11

Download GitHub Repository

  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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# -*- coding: utf-8 -*-
###
# Copyright (c) 2010 by Elián Hanisch <lambdae2@gmail.com>
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
###

###
# Python interpreter for WeeChat, and module for debugging python scripts.
#
#   Commands (see detailed help with /help in WeeChat):
#   * /pybuffer: Opens a python interpreter buffer.
#
# For debug a script, insert these lines after script's register() call.
#
#   import pybuffer
#   debug = pybuffer.debugBuffer(globals(), "buffer_name")
#
# Then, after loading your script, try "dir()" in the new "buffer_name" buffer, it should
# display the script's global functions and variables.
# This module should be in your python scripts path.
#
# Session example:
#
#   >>> buffer_search('irc', 'freenode.#weechat')
#   '0x9ca4ce0'
#   >>> b = buffer_search('irc', 'freenode.#weechat')
#   >>> b
#   '0x9ca4ce0'
#   >>> buffer_get(b, 'input')
#   Traceback (most recent call last):
#     File "<console>", line 1, in <module>
#   NameError: name 'buffer_get' is not defined
#   >>> search('buffer')
#   ['buffer_clear', 'buffer_close', 'buffer_get_integer', 'buffer_get_pointer', 'buffer_get_string',
#   'buffer_merge', 'buffer_new', 'buffer_search', 'buffer_search_main', 'buffer_set',
#   'buffer_string_replace_local_var', 'buffer_unmerge', 'current_buffer', 'string_input_for_buffer']
#   >>> buffer_get_string(b, 'input')
#   ''
#   >>> buffer_get_string(b, 'input')
#   'asdasdas hello!'
#   >>> buffer_get_string(b, 'title')
#   'WeeChat, stable: 0.3.3, web: http://www.weechat.org/ | English support channel | Please read
#   doc/faq/quickstart before asking here | Old versions (0.2.6.x and earlier) are not supported any
#   more'
#
#
#   History:
#   2018-10-03
#   version 0.3:
#   * Python3 compatibility (Pol Van Aubel <dev@polvanaubel.com>)
#
#   2010-11-05
#   version 0.2:
#   * More interperter console.
#   * renamed to pybuffer.
#   * added /pybuffer command (buffer isn't created on script load).
#
#   2010-10-30
#   version 0.1: Initial release
###
from __future__ import print_function

import sys # Only required for python version check.
PY2 = sys.version_info < (3,)

SCRIPT_NAME    = "pybuffer"
SCRIPT_AUTHOR  = "Elián Hanisch <lambdae2@gmail.com>"
SCRIPT_VERSION = "0.3"
SCRIPT_LICENSE = "GPL3"
SCRIPT_DESC    = "Python interpreter for WeeChat and module for debug scripts."

try:
    import weechat
    from weechat import WEECHAT_RC_OK, prnt
    import_ok = True
except ImportError:
    print("This script must be run under WeeChat.")
    print("Get WeeChat now at: https://weechat.org/")
    import_ok = False

import code, sys, traceback
from fnmatch import fnmatch

def callback(method):
    """This function will take a bound method or function and make it a callback."""
    # try to create a descriptive and unique name.
    funcname = method.__name__
    try:
        classinst = method.__self__
        try:
            instname = classinst.__name__
        except AttributeError:
            try:
                instname = classinst.name
            except AttributeError:
                raise Exception("Instance %s has no __name__ attribute" % classinst)
        classname = type(classinst).__name__
        name = '_'.join((classname, instname, funcname))
    except AttributeError:
        # not a bound method
        name = funcname

    # set our callback
    import __main__
    setattr(__main__, name, method)
    return name


class NoArguments(Exception):
    pass


class ArgumentError(Exception):
    pass


class Command(object):
    """Class for hook WeeChat commands."""
    description, usage, help = "WeeChat command.", "[define usage template]", "detailed help here"
    command = ''
    completion = ''

    def __init__(self):
        assert self.command, "No command defined"
        self.__name__ = self.command
        self._pointer = ''
        self._callback = ''

    def __call__(self, *args):
        return self.callback(*args)

    def callback(self, data, buffer, args):
        """Called by WeeChat when /command is used."""
        self.data, self.buffer, self.args = data, buffer, args
        try:
            self.parser(args)  # argument parsing
        except ArgumentError as e:
            error('Argument error, %s' %e)
        except NoArguments:
            pass
        else:
            self.execute()
        return WEECHAT_RC_OK

    def parser(self, args):
        """Argument parsing, override if needed."""
        pass

    def execute(self):
        """This method is called when the command is run, override this."""
        pass

    def hook(self):
        assert not self._pointer, \
                "There's already a hook pointer, unhook first (%s)" %self.command
        self._callback = callback(self.callback)
        pointer = weechat.hook_command(self.command,
                                       self.description,
                                       self.usage,
                                       self.help,
                                       self.completion,
                                       self._callback, '')
        if pointer == '':
            raise Exception("hook_command failed: %s %s" %(SCRIPT_NAME, self.command))
        self._pointer = pointer

    def unhook(self):
        if self._pointer:
            weechat.unhook(self._pointer)
            self._pointer = ''
            self._callback = ''


class SimpleBuffer(object):
    """WeeChat buffer. Only for displaying lines."""
    _title = ''
    def __init__(self, name):
        assert name, "Buffer needs a name."
        self.__name__ = name
        self._pointer = ''

    def _getBuffer(self):
        # we need to always search the buffer, since there's no close callback we can't know if the
        # buffer was closed.
        buffer = weechat.buffer_search('python', self.__name__)
        if not buffer:
            buffer = self.create()
        return buffer

    def _create(self):
        return weechat.buffer_new(self.__name__, '', '', '', '')

    def create(self):
        buffer = self._create()
        if self._title:
            weechat.buffer_set(buffer, 'title', self._title)
        self._pointer = buffer
        return buffer

    def title(self, s):
        self._title = s
        weechat.buffer_set(self._getBuffer(), 'title', s)

    def clear(self):
        weechat.buffer_clear(self._getBuffer())

    def __call__(self, s, *args, **kwargs):
        self.prnt(s, *args, **kwargs)

    def display(self):
        weechat.buffer_set(self._getBuffer(), 'display', '1')

    def error(self, s, *args):
        self.prnt(s, prefix=weechat.prefix('error'))

    def prnt(self, s, *args, **kwargs):
        """Prints messages in buffer."""
        buffer = self._getBuffer()

        # Ensure s is a str (Py3) or a str/unicode (Py2) so that s % args will work.
        if PY2:
            # Don't accidentally convert unicode to utf-8 or ASCII or whatever because we want to see the object.
            if not isinstance(s, basestring):
                s = str(s)
        else:
            if not isinstance(s, str):
                s = str(s)

        if args:
            s = s % args
        try:
            s = kwargs['prefix'] + s
        except KeyError:
            pass
        prnt(buffer, s)

    def prnt_lines(self, s, *args, **kwargs):
        for line in s.splitlines():
            self.prnt(line, *args, **kwargs)


class Buffer(SimpleBuffer):
    """WeeChat buffer. With input and close methods."""
    def _create(self):
        return weechat.buffer_new(self.__name__, callback(self.input), '', callback(self.close), '')

    def _getBuffer(self):
        if self._pointer:
            return self._pointer
        return SimpleBuffer._getBuffer(self)

    def input(self, data, buffer, input):
        return WEECHAT_RC_OK

    def close(self, data, buffer):
        self._pointer = ''
        return WEECHAT_RC_OK


class StreamObject(object):
    def __init__(self, buffer):
        self._content = ''
        self._buffer = buffer

    def write(self, s):
        self._content += s

    def prnt(self, *args, **kwargs):
        if self._content:
            self._buffer.prnt_lines(self._content, *args, **kwargs)
            self._content = ''


class PythonBuffer(Buffer):
    _title = "Python Buffer: use search([pattern]) for a list of objects."
    def __init__(self, name, locals=None):
        Buffer.__init__(self, name)
        self.output = StreamObject(self)
        self.error = StreamObject(self)
        # redirect stdout and stderr
        sys.stdout = self.output
        sys.stderr = self.error
        self.console = code.InteractiveConsole(locals)
        locals = self.console.locals
        # add our 'buildin' functions
        if 'search' not in locals:
            def search(s=''):
                """List functions/objects that match 's' in their name."""
                if '*' not in s:
                    s = '*%s*' %s
                return [ name for name in locals if fnmatch(name, s) ]
            locals['search'] = search

    def _create(self):
        buffer = Buffer._create(self)
        weechat.buffer_set(buffer, 'nicklist', '0')
        weechat.buffer_set(buffer, 'time_for_each_line', '0')
        weechat.buffer_set(buffer, 'localvar_set_no_log', '1')
        self.color_input = weechat.color('green')
        self.color_exc = weechat.color('red')
        self.color_call = weechat.color('cyan')
        weechat.hook_command_run('/input return', callback(self.input_return), buffer)
        # print python and WeeChat version
        prnt(buffer, "Python %s" % sys.version.split(None, 1)[0])
        prnt(buffer, "WeeChat %s" % weechat.info_get('version', ''))
        return buffer

    def __call__(self, s, *args, **kwargs):
        kwargs['prefix'] = self.color_call
        self.prnt(s, *args, **kwargs)

    def input_return(self, data, buffer, command):
        # we need to send returns even when there's no input.
        if data == buffer and not weechat.buffer_get_string(buffer, 'input'):
            self.input(data, buffer, '\n')
        return WEECHAT_RC_OK

    def input(self, data, buffer, input):
        """Python code evaluation."""
        try:
            need_more = self.console.push(input)
            if need_more:
                prompt = '%s... ' % self.color_input
            else:
                prompt = '%s>>> ' % self.color_input
            self.prnt(input, prefix=prompt)
            self.output.prnt()
            self.error.prnt(prefix=self.color_exc)
        except:
            trace = traceback.format_exc()
            self.prnt_lines(trace, prefix=self.color_exc)
        return WEECHAT_RC_OK

def debugBuffer(globals, name='debugBuffer'):
    buffer = PythonBuffer(name, globals)
    buffer.create()
    return buffer


class PyBufferCommand(Command):
    command = SCRIPT_NAME
    description = usage = help = ''
    def execute(self):
        buffer = PythonBuffer(SCRIPT_NAME)
        buffer.title("Use \"search([pattern])\" for search WeeChat API functions.")
        # import weechat and its functions.
        buffer.input('', '', 'import weechat')
        buffer.input('', '', 'from weechat import *')
        buffer.display()


if __name__ == '__main__' and import_ok and \
        weechat.register(SCRIPT_NAME, SCRIPT_AUTHOR, SCRIPT_VERSION, SCRIPT_LICENSE,
        SCRIPT_DESC, '', ''):

        # we're being loaded as a script.
        help = \
        "Opens a session with a python interpreter-like buffer."\
        " Use function 'search([pattern])' for list objects in current session.\n"\
        "\n"\
        "For debug a script add these lines after the register call:\n"\
        "\n"\
        "  import pybuffer\n"\
        "  debug = pybuffer.debugBuffer(globals(), 'name')\n"\
        "  debug('debug message example')\n"\
        "\n"\
        "You'll be able to execute python code while your script runs.\n"\
        "\n%(b)sWARNING: %(r)s" \
        "This script isn't fool-proof, you're very capable of crashing/freezing "\
        "WeeChat if you aren't careful with the code you run, use at your own risk."\
        " Running python interactive functions such as 'help()' or 'license()' %(b)swill%(r)s hang"\
        " WeeChat." %dict(b=weechat.color('bold'), r=weechat.color('reset'))

        PyBufferCommand.help = help
        PyBufferCommand().hook()


# vim:set shiftwidth=4 tabstop=4 softtabstop=4 expandtab textwidth=100: