Revision a7cb58ce ncclient/operations/rpc.py

b/ncclient/operations/rpc.py
17 17
from weakref import WeakValueDictionary
18 18

  
19 19
from ncclient import content
20
from ncclient.capabilities import check
20 21
from ncclient.transport import SessionListener
21 22

  
22 23
from errors import OperationError
......
27 28

  
28 29
class RPCReply:
29 30

  
31
    """Represents an *<rpc-reply>*. Only concerns itself with whether the
32
    operation was successful. Note that if the reply has not yet been parsed
33
    there is a one-time parsing overhead to accessing the :attr:`ok` and
34
    :attr:`error`/:attr:`errors` attributes."""
35

  
30 36
    def __init__(self, raw):
31 37
        self._raw = raw
32 38
        self._parsed = False
......
36 42
    def __repr__(self):
37 43
        return self._raw
38 44

  
39
    def _parsing_hook(self, root): pass
45
    def _parsing_hook(self, root):
46
        """Subclass can implement.
47

  
48
        :type root: :class:`~xml.etree.ElementTree.Element`
49
        """
50
        pass
40 51

  
41 52
    def parse(self):
53
        """Parse the *<rpc-reply>*"""
42 54
        if self._parsed:
43 55
            return
44 56
        root = self._root = content.xml2ele(self._raw) # <rpc-reply> element
......
65 77

  
66 78
    @property
67 79
    def xml(self):
68
        '<rpc-reply> as returned'
80
        "*<rpc-reply>* as returned"
69 81
        return self._raw
70 82

  
71 83
    @property
72 84
    def ok(self):
85
        "Boolean value indicating if there were no errors."
73 86
        if not self._parsed:
74 87
            self.parse()
75 88
        return not self._errors # empty list => false
76 89

  
77 90
    @property
78 91
    def error(self):
92
        "Short for :attr:`errors`[0], returning :const:`None` if there were no errors."
79 93
        if not self._parsed:
80 94
            self.parse()
81 95
        if self._errors:
......
85 99

  
86 100
    @property
87 101
    def errors(self):
88
        'List of RPCError objects. Will be empty if no <rpc-error> elements in reply.'
102
        "List of :class:`RPCError` objects. Will be empty if there were no :class:`<rpc-error>` elements in reply."
89 103
        if not self._parsed:
90 104
            self.parse()
91 105
        return self._errors
......
93 107

  
94 108
class RPCError(OperationError): # raise it if you like
95 109

  
110
    """Represents an *<rpc-error>*. It is an instance of :exc:`OperationError`
111
    so it can be raised like any other exception."""
112

  
96 113
    def __init__(self, err_dict):
97 114
        self._dict = err_dict
98 115
        if self.message is not None:
......
102 119

  
103 120
    @property
104 121
    def type(self):
122
        "`string` represeting *error-type* element"
105 123
        return self.get('error-type', None)
106 124

  
107 125
    @property
108 126
    def severity(self):
127
        "`string` represeting *error-severity* element"
109 128
        return self.get('error-severity', None)
110 129

  
111 130
    @property
112 131
    def tag(self):
132
        "`string` represeting *error-tag* element"
113 133
        return self.get('error-tag', None)
114 134

  
115 135
    @property
116 136
    def path(self):
137
        "`string` or :const:`None`; represeting *error-path* element"
117 138
        return self.get('error-path', None)
118 139

  
119 140
    @property
120 141
    def message(self):
142
        "`string` or :const:`None`; represeting *error-message* element"
121 143
        return self.get('error-message', None)
122 144

  
123 145
    @property
124 146
    def info(self):
147
        "`string` or :const:`None`, represeting *error-info* element"
125 148
        return self.get('error-info', None)
126 149

  
127 150
    ## dictionary interface
......
151 174

  
152 175
class RPCReplyListener(SessionListener):
153 176

  
177
    # internal use
178

  
154 179
    # one instance per session
155 180
    def __new__(cls, session):
156 181
        instance = session.get_listener_instance(cls)
......
191 216
            else:
192 217
                logger.warning('<rpc-reply> without message-id received: %s' % raw)
193 218
        logger.debug('delivering to %r' % rpc)
194
        rpc.deliver(raw)
219
        rpc.deliver_reply(raw)
195 220

  
196 221
    def errback(self, err):
197 222
        for rpc in self._id2rpc.values():
198
            rpc.error(err)
223
            rpc.deliver_error(err)
199 224

  
200 225

  
201 226
class RPC(object):
202 227

  
228
    "Directly corresponds to *<rpc>* requests. Handles making the request, and taking delivery of the reply."
229

  
230
    # : Subclasses can specify their dependencies on capabilities. List of URI's
231
    # or abbreviated names, e.g. ':writable-running'. These are verified at the
232
    # time of object creation. If the capability is not available, a
233
    # :exc:`MissingCapabilityError` is raised.
203 234
    DEPENDS = []
235

  
236
    # : Subclasses can specify a different reply class, but it must be a
237
    # subclass of :class:`RPCReply`.
204 238
    REPLY_CLS = RPCReply
205 239

  
206 240
    def __init__(self, session, async=False, timeout=None):
207
        if not session.can_pipeline:
208
            raise UserWarning('Asynchronous mode not supported for this device/session')
209 241
        self._session = session
210 242
        try:
211 243
            for cap in self.DEPENDS:
......
221 253
        self._listener.register(self._id, self)
222 254
        self._reply = None
223 255
        self._error = None
224
        self._reply_event = Event()
256
        self._event = Event()
225 257

  
226 258
    def _build(self, opspec):
227
        "TODO: docstring"
259
        # internal
228 260
        spec = {
229 261
            'tag': content.qualify('rpc'),
230 262
            'attrib': {'message-id': self._id},
231
            'subtree': opspec
263
            'subtree': [ opspec ]
232 264
            }
233 265
        return content.dtree2xml(spec)
234 266

  
235 267
    def _request(self, op):
268
        """Subclasses call this method to make the RPC request.
269

  
270
        In asynchronous mode, returns an :class:`~threading.Event` which is set
271
        when the reply has been received or an error occured. It is prudent,
272
        therefore, to check the :attr:`error` attribute before accesing
273
        :attr:`reply`.
274

  
275
        Otherwise, waits until the reply is received and returns
276
        :class:`RPCReply`.
277

  
278
        :arg opspec: :ref:`dtree` for the operation
279
        :type opspec: :obj:`dict` or :obj:`string` or :class:`~xml.etree.ElementTree.Element`
280
        :rtype: :class:`~threading.Event` or :class:`RPCReply`
281
        """
236 282
        req = self._build(op)
237 283
        self._session.send(req)
238 284
        if self._async:
239
            return self._reply_event
285
            return self._event
240 286
        else:
241
            self._reply_event.wait(self._timeout)
242
            if self._reply_event.isSet():
287
            self._event.wait(self._timeout)
288
            if self._event.isSet():
243 289
                if self._error:
244 290
                    raise self._error
245 291
                self._reply.parse()
......
247 293
            else:
248 294
                raise ReplyTimeoutError
249 295

  
250
    def request(self):
251
        return self._request(self.SPEC)
296
    def request(self, *args, **kwds):
297
        "Subclasses implement this method. Here, the operation is to be constructed as a :ref:`dtree`, and the result of :meth:`_request` returned."
298
        return self._request(self.SPEC, *args, **kwds)
252 299

  
253 300
    def _delivery_hook(self):
254
        'For subclasses'
301
        """Subclasses can implement this method. Will be called after
302
        initialising the :attr:`reply` or :attr:`error` attribute and before
303
        setting the :attr:`event`"""
255 304
        pass
256 305

  
257 306
    def _assert(self, capability):
307
        """Subclasses can use this method to verify that a capability is available
308
        with the NETCONF server, before making a request that requires it. A
309
        :class:`MissingCapabilityError` will be raised if the capability is not
310
        available."""
258 311
        if capability not in self._session.server_capabilities:
259 312
            raise MissingCapabilityError('Server does not support [%s]' % cap)
260 313

  
261
    def deliver(self, raw):
314
    def deliver_reply(self, raw):
315
        # internal use
262 316
        self._reply = self.REPLY_CLS(raw)
263 317
        self._delivery_hook()
264
        self._reply_event.set()
318
        self._event.set()
265 319

  
266
    def error(self, err):
320
    def deliver_error(self, err):
321
        # internal use
267 322
        self._error = err
268
        self._reply_event.set()
269

  
270
    @property
271
    def has_reply(self):
272
        return self._reply_event.is_set()
323
        self._delivery_hook()
324
        self._event.set()
273 325

  
274 326
    @property
275 327
    def reply(self):
276
        if self.error:
277
            raise self._error
328
        ":class:`RPCReply` element if reply has been received or :const:`None`"
278 329
        return self._reply
279 330

  
280 331
    @property
332
    def error(self):
333
        """:exc:`Exception` type if an error occured or :const:`None`.
334

  
335
        This attribute should be checked if the request was made asynchronously,
336
        so that it can be determined if :attr:`event` being set is because of a
337
        reply or error.
338

  
339
        .. note::
340
            This represents an error which prevented a reply from being
341
            received. An *<rpc-error>* does not fall in that category -- see
342
            :class:`RPCReply` for that.
343
        """
344
        return self._error
345

  
346
    @property
281 347
    def id(self):
348
        "The *message-id* for this RPC"
282 349
        return self._id
283 350

  
284 351
    @property
285 352
    def session(self):
353
        """The :class:`~ncclient.transport.Session` object associated with this
354
        RPC"""
286 355
        return self._session
287 356

  
288 357
    @property
289
    def reply_event(self):
290
        return self._reply_event
358
    def event(self):
359
        """:class:`~threading.Event` that is set when reply has been received or
360
        error occured."""
361
        return self._event
362

  
363
    def set_async(self, async=True):
364
        """Set asynchronous mode for this RPC."""
365
        self._async = async
366
        if async and not session.can_pipeline:
367
            raise UserWarning('Asynchronous mode not supported for this device/session')
368

  
369
    def set_timeout(self, timeout):
370
        """Set the timeout for synchronous waiting defining how long the RPC
371
        request will block on a reply before raising an error."""
372
        self._timeout = timeout
291 373

  
292
    def set_async(self, bool): self._async = bool
374
    #: Whether this RPC is asynchronous
293 375
    async = property(fget=lambda self: self._async, fset=set_async)
294 376

  
295
    def set_timeout(self, timeout): self._timeout = timeout
377
    #: Timeout for synchronous waiting
296 378
    timeout = property(fget=lambda self: self._timeout, fset=set_timeout)

Also available in: Unified diff