~ubuntu-branches/debian/jessie/bibus/jessie

« back to all changes in this revision

Viewing changes to LyX/lyxclient.py

  • Committer: Bazaar Package Importer
  • Author(s): Jan Beyer
  • Date: 2009-10-12 22:44:05 UTC
  • mfrom: (4.1.2 sid)
  • Revision ID: james.westby@ubuntu.com-20091012224405-edpoan0andy2kpmb
Tags: 1.5.0-1
* New upstream release
  - patch fix-BibTeXImport.patch dropped, as it is incorporated upstream
  - patch fix-finalizing-issue.patch dropped, as it is incorporated upstream
  - man-page debian/bibus.1 dropped, as it is incorporated upstream

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# lyxclient.py         -*- coding: iso-8859-1 -*-
 
2
# Copyright (c) 2005 G�nter Milde
 
3
# Released under the terms of the GNU General Public License (ver. 2 or later)
 
4
"""
 
5
Classes and functions to implement a generic client talking to LyX via the 
 
6
lyxserver pipes
 
7
   
 
8
   LyXClient     LyX client using the serverpipes
 
9
                 More highlevel than the lyxserver.LyXServer class
 
10
                 
 
11
   LyXMessage    Message from/for the lyxserver
 
12
                 
 
13
   LyXError      Exception class for errors returned by LyX
 
14
                      
 
15
   lyx_remote()  open files in a running lyx session (or start a new one)
 
16
 
 
17
 
 
18
Needs the lyxserver running 
 
19
     Set Edit>Preferences>Paths>Server-pipe to "~/.lyx/lyxpipe"
 
20
     
 
21
Copyright (c) 2005 G�nter Milde
 
22
Released under the terms of the GNU General Public License (ver. 2 or later)
 
23
 
 
24
Notes
 
25
-----
 
26
   
 
27
   * Server documentation (slightly outdated) is available at
 
28
      LYXDIR/doc/Customization.lyx (but not in the German translation)
 
29
   
 
30
   * A list of all LFUNS is available at 
 
31
         http://wiki.lyx.org/pmwiki.php/LyX/LyxFunctions
 
32
"""
 
33
 
 
34
import sys, os, logging
 
35
from lyxserver import LyXServer, start_lyx
 
36
from constants import LOG_LEVEL, LYX_OPTIONS, LYXCMD, \
 
37
                      LYXSERVER_POLL_TIMEOUT, LYXSERVER_SETUP_RETRIES
 
38
 
 
39
# set up the logger instance
 
40
logger = logging.getLogger("lyxclient")
 
41
# logging.basicConfig()  # done in lyxserver.py
 
42
# set verbosity to show all messages of severity >= LOG_LEVEL
 
43
logger.setLevel(LOG_LEVEL) 
 
44
 
 
45
class LyXMessage(dict):
 
46
    """Message from/for the lyxserver pipes
 
47
    
 
48
    Stores the parts of the message in the attributes
 
49
        msg_type   -- type of the message
 
50
        client     -- name that the client can choose arbitrarily
 
51
        function   -- function you want LyX to perform (lyx-function, LFUN)
 
52
                      (The same as the commands you'd use in the minibuffer)
 
53
        data       -- optional data (function argument, return value, 
 
54
                      error message, or notifying key)
 
55
       
 
56
    Initializing
 
57
        positional argument -- parsed as message string, or
 
58
        keyword arguments   -- set matching attributes
 
59
    
 
60
    Converting the object to a message string (suitable for writing to the 
 
61
    serverpipe, with newline at the end) 
 
62
        str(LyXMessage_instance)
 
63
        
 
64
    Examples:
 
65
        str(LyXMessage("LYXCMD:name:fun:arg"))
 
66
        str(LyXMessage(msg_type='LYXCMD', client='name', function='fun', 
 
67
                       data='arg'))
 
68
        str(LyXMessage("LYXCMD:name:fun:arg", client='another-name'))
 
69
    all give
 
70
    >>> 'LYXCMD:name:fun:arg\n'
 
71
    while
 
72
        str(LyXMessage("NOTIFY:key-sequence"))
 
73
        str(LyXMessage("NOTIFY:key-sequence:unread junk"))
 
74
        str(LyXMessage(type="NOTIFY", data="key-sequence"))
 
75
    give
 
76
    >>> 'NOTIFY:key-sequence\n'
 
77
    and
 
78
        str(LyXMessage(type="NOTIFY", function="fun", data="key"))
 
79
    would result in the malformed message string.
 
80
    >>> 'NOTIFY:fun:key\n'
 
81
    """
 
82
    # Valid message types and recognized fields (as of LyX 1.3.4)
 
83
    # ordered list of all message fields
 
84
    fieldnames = ("msg_type", "client", "function", "data")
 
85
    msg_types = {"LYXCMD": fieldnames,
 
86
                  "INFO": fieldnames, 
 
87
                  "ERROR": fieldnames, 
 
88
                  "NOTIFY":("msg_type", "data"), 
 
89
                  "LYXSRV":("msg_type", "client", "data")
 
90
                 }
 
91
    # 
 
92
    def __init__(self, msg_string='', **keywords):
 
93
        """Parse `msg_string` or set matching attributes.
 
94
        """
 
95
        self.__dict__ = self  # dictionary elements are also attributes
 
96
        if msg_string:
 
97
            self.parse(msg_string)
 
98
        else:
 
99
            dict.__init__(self, keywords)
 
100
    # 
 
101
    def parse(self, msg_string):
 
102
        """Parse a message string and set attributes
 
103
        """
 
104
        self.clear() # delete old attributes
 
105
        # strip trailing newline and split (into <= 4 parts)
 
106
        values = msg_string.rstrip().split(':', 3)
 
107
        # get field names for this message type
 
108
        names = self.msg_types[values[0]] # values[0] holds msg_type
 
109
        fields = dict(zip(names, values))
 
110
        dict.__init__(self, fields)
 
111
 
 
112
    #
 
113
    def list_fields(self):
 
114
        """Return ordered list of field values
 
115
        
 
116
        The order is %s
 
117
        Empty fields get the value None
 
118
        """%str(self.fieldnames)
 
119
        return [self.get(name) for name in self.fieldnames]
 
120
    #
 
121
    def __str__(self):
 
122
        """Return a string representation suitable to pass to the lyxserver
 
123
        """
 
124
        values = filter(bool, self.list_fields()) # filter empty fields
 
125
        return ':'.join(values)+'\n'
 
126
    #
 
127
    def __repr__(self):
 
128
        """Return an evaluable representation"""
 
129
        args = ["%s=%s"%(name, value) for (name, value) in self.iteritems()]
 
130
        return "LyXMessage(%s)"%", ".join(args)
 
131
    #
 
132
    def __eq__(self, other):
 
133
        """Test for equality. 
 
134
        
 
135
        Two Messages are equal, if all fields match. A field value of True 
 
136
        serves as wildcard
 
137
        
 
138
        Examples:
 
139
          LyXMessage("NOTIFY:F12") == LyXMessage("NOTIFY:F12:")
 
140
          >>> True
 
141
          LyXMessage("NOTIFY:F12") == LyXMessage("NOTIFY:F11")
 
142
          >>> False
 
143
          LyXMessage("NOTIFY:F12") == LyXMessage(msg_type="NOTIFY", data=True)
 
144
          >>> True
 
145
        """
 
146
        # TODO: test if filtering True values from self.list_fields() is better
 
147
        for name in self.fieldnames:
 
148
            if self.get(name) is True or other.get(name) is True:
 
149
                continue
 
150
            if self.get(name) != other.get(name):
 
151
                return False
 
152
        return True
 
153
    #
 
154
    def __ne__(self, other):
 
155
        # needs to be defined separately, see Python documentation
 
156
        return not self.__eq__(other)
 
157
 
 
158
class LyXError(Exception):
 
159
    """Exception class for errors returned by LyX"""
 
160
    def __init__(self, strerror):
 
161
        self.strerror = strerror
 
162
    def __str__(self):
 
163
        return self.strerror
 
164
 
 
165
 
 
166
class LyXClient(LyXServer):
 
167
    """A client that connects to a LyX session via the serverpipes
 
168
    
 
169
    Adds lyx-function calls, message parsing and listening to the LyXServer
 
170
        
 
171
    Calling an instance, sends `function` as a function call to LyX 
 
172
    and returns the reply data (an ERROR reply raises a LyXError)
 
173
    """
 
174
    #
 
175
    name = "pyClient"
 
176
    bindings = {} # bindings of "notify" keys to actions
 
177
    #
 
178
    def open(self):
 
179
        """Open the server pipes and register at LyX"""
 
180
        LyXServer.open(self)
 
181
        self.write('LYXSRV:%s:hello\n'%self.name)
 
182
        logger.info(self.readmessage(msg_type='LYXSRV'))
 
183
    #
 
184
    def close(self):
 
185
        """Unregister at LyX and close the server pipes"""
 
186
        # Unregister at LyX (no response will be sent)
 
187
        try:
 
188
            self.inpipe.write('LYXSRV:%s:bye\n'%self.name)
 
189
        except AttributeError:
 
190
            pass
 
191
        LyXServer.close(self)
 
192
    #
 
193
    def readmessage(self, timeout=None,
 
194
                    msg_type=True, client=True, function=True, data=True,
 
195
                    writeback=0):
 
196
        """Read one line of outpipe, return as LyXMessage.
 
197
        (optionally, check for matching fields)
 
198
        from the outpipe, 
 
199
        
 
200
        timeout              -- polling timeout (in ms)
 
201
                                not-set or None -> use self.timeout
 
202
        msg_type, client,    -- if one of these is not None, the next message
 
203
        function, data          with matching field value is picked           
 
204
        writeback            -- write back last n filtered messages
 
205
                                (allows several clients to work in parallel)
 
206
        """
 
207
        # template to check the messages against (default: all messages match)
 
208
        pattern = LyXMessage(msg_type=msg_type, client=client, 
 
209
                             function=function, data=data)
 
210
        junkmessages = []  # leftovers from message picking
 
211
        # logger.debug("readmessage: pattern " + repr(pattern))
 
212
        for msg in self.__iter__(timeout):
 
213
            # logger.debug("readmessage: testing " + repr(msg))
 
214
            if msg == pattern:
 
215
                # logger.debug("readmessage: match " + str(msg).strip())
 
216
                break
 
217
            elif msg: # junk message
 
218
                logger.debug("readmessage: junk message " + str(msg).strip())
 
219
                junkmessages.append(msg)
 
220
            else:
 
221
                logger.warning("readmessage: no match found")
 
222
                msg = LyXMessage() # empty message
 
223
        else: # empty outpipe
 
224
            logger.warning("readmessage: timed out")
 
225
            msg = LyXMessage() # empty message
 
226
        # write back junk messages
 
227
        if writeback and junkmessages:
 
228
            logger.debug("readmessage: write back last (<=%d) messages"%writeback)
 
229
            # logger.debug(str(junkmessages[-writeback:]))
 
230
            self._writeback(junkmessages[-writeback:])
 
231
        return msg
 
232
    # 
 
233
    def readmessages(self, timeout=None):
 
234
        """Read waiting messages. Return list of `LyXMessage` objects.
 
235
        """
 
236
        return map(LyXMessage, self.readlines(timeout=None))
 
237
    #
 
238
    def _writeback(self, messages):
 
239
        """Write sequence of messages back to the outpipe"""
 
240
        lines = map(str, messages)
 
241
        # read waiting messages, to preserve the order
 
242
        # in an ideal world, the next 2 lines should be an atomic action
 
243
        lines.extend(self.readlines(timeout=0))
 
244
        self.outpipe.writelines(lines)
 
245
    #
 
246
    def write_lfun(self, function, *args):
 
247
        """send a LFUN to the inpipe"""
 
248
        msg = LyXMessage(msg_type='LYXCMD', client = self.name,
 
249
                         function = function, data = ' '.join(map(str, args)))
 
250
        self.write(msg)
 
251
    #
 
252
    # iterator protocoll
 
253
    def __iter__(self, timeout=None):
 
254
        """Return iterator (generator) yielding `LyXMessage(self.readline)`
 
255
        
 
256
        See `LyXServer.readline` for discussion of the `timeout`
 
257
        argument.
 
258
              
 
259
        Example: 
 
260
            1. simple call with default timout (self.timeout):
 
261
                for msg in instance:
 
262
                    print msg
 
263
            2. call with custom timeout:
 
264
                for msg in instance.__iter__(timeout=20):
 
265
                    print msg
 
266
        """
 
267
        while self.poll(timeout):
 
268
            msg = LyXMessage(self.outpipe.readline())
 
269
            # stop iterating if lyx died (saves waiting for `timeout` ms)
 
270
            if msg.msg_type == "LYXSRV" and msg.data == "bye":
 
271
                logger.info("LyX said bye, closing serverpipes")
 
272
                raise LyXError("LyX closed down")
 
273
            yield msg
 
274
    #
 
275
    # direct call of instance 
 
276
    def __call__(self, function, *args):
 
277
        """send a LFUN to the inpipe and return the reply data"""
 
278
        self.write_lfun(function, *args)
 
279
        # read reply
 
280
        logger.debug("LyXClient.__call__:%r sent, waiting for reply"%function)
 
281
        reply = self.readmessage(timeout=self.timeout,  client=self.name, 
 
282
                                 function=function, writeback=5)
 
283
        if not reply:
 
284
            logger.warning(function + ": no reply")
 
285
            return None
 
286
        # logger.debug("__call__: reply string: %r"%(reply))
 
287
        if reply.msg_type == 'ERROR':
 
288
            raise LyXError, ':'.join((reply.function, reply.data))
 
289
        else:
 
290
            return reply.data
 
291
    #
 
292
    def listen(self, timeout=60000):
 
293
        """wait for a NOTIFY from LyX, run function bound to key
 
294
        """
 
295
        self.timeout = timeout
 
296
        for msg in self:
 
297
            logger.debug("listen: new message '%s'"%(str(msg)))
 
298
            if msg.msg_type == 'NOTIFY':
 
299
                logger.info("listen: notice from key '%s'"%msg.data)
 
300
                # call the key binding
 
301
                try:
 
302
                    fun = self.bindings[msg.data]
 
303
                except KeyError:
 
304
                    logger.warning("key %s not bound"%msg.data)
 
305
                    self("message key %s not bound"%msg.data)
 
306
                    continue
 
307
                try:
 
308
                    if type(fun) is str:
 
309
                        exec fun
 
310
                    else:
 
311
                        fun()
 
312
                except LyXError, exception:
 
313
                    logger.warning(exception.strerror)
 
314
                    self("message", exception.strerror)
 
315
            else:
 
316
                logger.debug("listen: junk message '%s'"%(line))
 
317
                # TODO: write back, wait a bit (for others to pick up)
 
318
                # and continue (discarding msg the second time)
 
319
        else:
 
320
            logger.critical("listen timed out")
 
321
    #
 
322
    def __del__ (self):
 
323
        """Unregister at LyX and close the server pipes"""
 
324
        logger.info("deleting "+self.name)
 
325
        self.close()
 
326
 
 
327
 
 
328
# lyx-remote: Open files in a lyx session 
 
329
# ---------------------------------------
 
330
 
 
331
def filter_options(args, valid_options):
 
332
    """parse a list of arguments and filter the options
 
333
                                                                         
 
334
    args          -- list of command parameters (e.g. sys.argv[1:])
 
335
    valid options -- dictionary of accepted options with number of 
 
336
                     option-parameters as value
 
337
    
 
338
    Note: using getopt doesnot work due to 'one-dash long-options' :-(
 
339
    (opts, files) = getopt.getopt(['-help'], LYX_OPTIONS, lyx_long_options)
 
340
    Also the new 'optparse' module doesnot support these.
 
341
    """
 
342
    option_parameters = 0
 
343
    options = []
 
344
    filenames = []
 
345
    for arg in args:
 
346
        if option_parameters:
 
347
            options.append(arg)
 
348
            option_parameters -= 1
 
349
        elif arg in valid_options:
 
350
            options.append(arg)
 
351
            option_parameters = valid_options[arg]
 
352
        else:
 
353
            filenames.append(arg)
 
354
    return options, filenames
 
355
 
 
356
 
 
357
def lyx_remote(cmd=LYXCMD, args=sys.argv[1:]):
 
358
    """Open all files in `args` in a lyx session.
 
359
    
 
360
    Check for a running LyX session and let it open the files
 
361
    or start LyX in a separate process.
 
362
    
 
363
    cmd  -- lyx binary command name (searched on PATH)
 
364
    args -- list of command parameters excluding the command name
 
365
            (default sys.argv[1:])
 
366
            
 
367
    Return LyXServer instance 
 
368
    """
 
369
    # separate command line options (+ arguments) and filenames
 
370
    options, filenames = filter_options(args, LYX_OPTIONS)
 
371
    # logger.debug("lyx_remote:options:%r,filename:%r"%(options, filenames))
 
372
    # start a new lyx if there are command line options but no files to open
 
373
    if options and not filenames:
 
374
        start_lyx(cmd, options)
 
375
    
 
376
    # move the options (+ option args) to the cmd
 
377
    cmd = " ".join([cmd]+ options)
 
378
    
 
379
    client = LyXClient(lyxcmd=cmd)
 
380
    print cmd
 
381
    # Send a LYXCMD for every filename argument
 
382
    for filename in filenames:
 
383
        logger.debug("lyx-remote: opening %s"%filename)
 
384
        client("file-open", os.path.abspath(filename))
 
385
    return client