1import inspect 2import re 3import sys 4 5from docutils.parsers.rst import Directive 6from docutils.parsers.rst import directives 7from sphinx import addnodes 8from sphinx.directives import ObjectDescription 9from sphinx.domains import Domain 10from sphinx.domains import ObjType 11from sphinx.domains.python import PyAttribute 12from sphinx.domains.python import PyClasslike 13from sphinx.domains.python import PyMethod 14from sphinx.ext import autodoc 15from sphinx.locale import _ 16from sphinx.roles import XRefRole 17from sphinx.util.docfields import Field 18from sphinx.util.nodes import make_refnode 19 20import wsme 21import wsme.rest.json 22import wsme.rest.xml 23import wsme.types 24 25field_re = re.compile(r':(?P<field>\w+)(\s+(?P<name>\w+))?:') 26 27 28def datatypename(datatype): 29 if isinstance(datatype, wsme.types.UserType): 30 return datatype.name 31 if isinstance(datatype, wsme.types.DictType): 32 return 'dict(%s: %s)' % (datatypename(datatype.key_type), 33 datatypename(datatype.value_type)) 34 if isinstance(datatype, wsme.types.ArrayType): 35 return 'list(%s)' % datatypename(datatype.item_type) 36 return datatype.__name__ 37 38 39def make_sample_object(datatype): 40 if datatype is wsme.types.bytes: 41 return b'samplestring' 42 if datatype is wsme.types.text: 43 return 'sample unicode' 44 if datatype is int: 45 return 5 46 sample_obj = getattr(datatype, 'sample', datatype)() 47 return sample_obj 48 49 50def get_protocols(names): 51 names = list(names) 52 protocols = [] 53 if 'rest' in names: 54 names.remove('rest') 55 protocols.extend('restjson', 'restxml') 56 if 'restjson' in names: 57 names.remove('restjson') 58 protocols.append(('Json', wsme.rest.json)) 59 if 'restxml' in names: 60 names.remove('restxml') 61 protocols.append(('XML', wsme.rest.xml)) 62 for name in names: 63 p = wsme.protocol.getprotocol(name) 64 protocols.append((p.displayname or p.name, p)) 65 return protocols 66 67 68class SampleType(object): 69 """A Sample Type""" 70 71 #: A Int 72 aint = int 73 74 def __init__(self, aint=None): 75 if aint: 76 self.aint = aint 77 78 @classmethod 79 def sample(cls): 80 return cls(10) 81 82 83class SampleService(wsme.WSRoot): 84 @wsme.expose(SampleType) 85 @wsme.validate(SampleType, int, str) 86 def change_aint(data, aint, dummy='useless'): 87 """ 88 :param aint: The new value 89 90 :return: The data object with its aint field value changed. 91 """ 92 data.aint = aint 93 return data 94 95 96def getroot(env, force=False): 97 root = env.temp_data.get('wsme:root') 98 if not force and root: 99 return root 100 rootpath = env.temp_data.get('wsme:rootpath', env.app.config.wsme_root) 101 102 if rootpath is None: 103 return None 104 105 modname, classname = rootpath.rsplit('.', 1) 106 __import__(modname) 107 module = sys.modules[modname] 108 root = getattr(module, classname) 109 env.temp_data['wsme:root'] = root 110 return root 111 112 113def scan_services(service, path=[]): 114 has_functions = False 115 for name in dir(service): 116 if name.startswith('_'): 117 continue 118 a = getattr(service, name) 119 if inspect.ismethod(a): 120 if hasattr(a, '_wsme_definition'): 121 has_functions = True 122 if inspect.isclass(a): 123 continue 124 if len(path) > wsme.rest.APIPATH_MAXLEN: 125 raise ValueError("Path is too long: " + str(path)) 126 for value in scan_services(a, path + [name]): 127 yield value 128 if has_functions: 129 yield service, path 130 131 132def find_service_path(env, service): 133 root = getroot(env) 134 if service == root: 135 return [] 136 for s, path in scan_services(root): 137 if s == service: 138 return path 139 return None 140 141 142class TypeDirective(PyClasslike): 143 def get_index_text(self, modname, name_cls): 144 return _('%s (webservice type)') % name_cls[0] 145 146 def add_target_and_index(self, name_cls, sig, signode): 147 ret = super(TypeDirective, self).add_target_and_index( 148 name_cls, sig, signode 149 ) 150 name = name_cls[0] 151 types = self.env.domaindata['wsme']['types'] 152 if name in types: 153 self.state_machine.reporter.warning( 154 'duplicate type description of %s ' % name) 155 types[name] = self.env.docname 156 return ret 157 158 159class AttributeDirective(PyAttribute): 160 doc_field_types = [ 161 Field('datatype', label=_('Type'), has_arg=False, 162 names=('type', 'datatype')) 163 ] 164 165 166def check_samples_slot(value): 167 """Validate the samples_slot option to the TypeDocumenter. 168 169 Valid positions are 'before-docstring' and 170 'after-docstring'. Using the explicit 'none' disables sample 171 output. The default is after-docstring. 172 """ 173 if not value: 174 return 'after-docstring' 175 val = directives.choice( 176 value, 177 ('none', # do not include 178 'before-docstring', # show samples then docstring 179 'after-docstring', # show docstring then samples 180 )) 181 return val 182 183 184class TypeDocumenter(autodoc.ClassDocumenter): 185 objtype = 'type' 186 directivetype = 'type' 187 domain = 'wsme' 188 189 required_arguments = 1 190 default_samples_slot = 'after-docstring' 191 192 option_spec = dict( 193 autodoc.ClassDocumenter.option_spec, 194 **{'protocols': lambda l: [v.strip() for v in l.split(',')], 195 'samples-slot': check_samples_slot, 196 }) 197 198 @staticmethod 199 def can_document_member(member, membername, isattr, parent): 200 # we don't want to be automaticaly used 201 # TODO check if the member is registered an an exposed type 202 return False 203 204 def format_name(self): 205 return self.object.__name__ 206 207 def format_signature(self): 208 return u'' 209 210 def add_directive_header(self, sig): 211 super(TypeDocumenter, self).add_directive_header(sig) 212 # remove the :module: option that was added by ClassDocumenter 213 result_len = len(self.directive.result) 214 for index, item in zip(reversed(range(result_len)), 215 reversed(self.directive.result)): 216 if ':module:' in item: 217 self.directive.result.pop(index) 218 219 def import_object(self): 220 if super(TypeDocumenter, self).import_object(): 221 wsme.types.register_type(self.object) 222 return True 223 else: 224 return False 225 226 def add_content(self, more_content, no_docstring=False): 227 # Check where to include the samples 228 samples_slot = self.options.samples_slot or self.default_samples_slot 229 230 def add_docstring(): 231 super(TypeDocumenter, self).add_content( 232 more_content, no_docstring) 233 234 def add_samples(): 235 protocols = get_protocols( 236 self.options.protocols or self.env.app.config.wsme_protocols 237 ) 238 content = [] 239 if protocols: 240 sample_obj = make_sample_object(self.object) 241 content.extend([ 242 _(u'Data samples:'), 243 u'', 244 u'.. cssclass:: toggle', 245 u'' 246 ]) 247 for name, protocol in protocols: 248 language, sample = protocol.encode_sample_value( 249 self.object, sample_obj, format=True) 250 content.extend([ 251 name, 252 u' .. code-block:: ' + language, 253 u'', 254 ]) 255 content.extend( 256 u' ' * 8 + line 257 for line in str(sample).split('\n')) 258 for line in content: 259 self.add_line(line, u'<wsmeext.sphinxext') 260 261 self.add_line(u'', '<wsmeext.sphinxext>') 262 263 if samples_slot == 'after-docstring': 264 add_docstring() 265 add_samples() 266 elif samples_slot == 'before-docstring': 267 add_samples() 268 add_docstring() 269 else: 270 add_docstring() 271 272 273class AttributeDocumenter(autodoc.AttributeDocumenter): 274 datatype = None 275 domain = 'wsme' 276 277 @staticmethod 278 def can_document_member(member, membername, isattr, parent): 279 return isinstance(parent, TypeDocumenter) 280 281 def import_object(self): 282 success = super(AttributeDocumenter, self).import_object() 283 if success: 284 self.datatype = self.object.datatype 285 return success 286 287 def add_content(self, more_content, no_docstring=False): 288 self.add_line( 289 u':type: %s' % datatypename(self.datatype), 290 '<wsmeext.sphinxext>' 291 ) 292 self.add_line(u'', '<wsmeext.sphinxext>') 293 super(AttributeDocumenter, self).add_content( 294 more_content, no_docstring) 295 296 def add_directive_header(self, sig): 297 super(AttributeDocumenter, self).add_directive_header(sig) 298 299 300class RootDirective(Directive): 301 """ 302 This directive is to tell what class is the Webservice root 303 """ 304 has_content = False 305 required_arguments = 1 306 optional_arguments = 0 307 final_argument_whitespace = False 308 option_spec = { 309 'webpath': directives.unchanged 310 } 311 312 def run(self): 313 env = self.state.document.settings.env 314 rootpath = self.arguments[0].strip() 315 env.temp_data['wsme:rootpath'] = rootpath 316 if 'wsme:root' in env.temp_data: 317 del env.temp_data['wsme:root'] 318 if 'webpath' in self.options: 319 env.temp_data['wsme:webpath'] = self.options['webpath'] 320 return [] 321 322 323class ServiceDirective(ObjectDescription): 324 name = 'service' 325 326 optional_arguments = 1 327 328 def handle_signature(self, sig, signode): 329 path = sig.split('/') 330 331 namespace = '/'.join(path[:-1]) 332 if namespace and not namespace.endswith('/'): 333 namespace += '/' 334 335 servicename = path[-1] 336 337 if not namespace and not servicename: 338 servicename = '/' 339 340 signode += addnodes.desc_annotation('service ', 'service ') 341 342 if namespace: 343 signode += addnodes.desc_addname(namespace, namespace) 344 345 signode += addnodes.desc_name(servicename, servicename) 346 347 return sig 348 349 350class ServiceDocumenter(autodoc.ClassDocumenter): 351 domain = 'wsme' 352 objtype = 'service' 353 directivetype = 'service' 354 355 def add_directive_header(self, sig): 356 super(ServiceDocumenter, self).add_directive_header(sig) 357 # remove the :module: option that was added by ClassDocumenter 358 result_len = len(self.directive.result) 359 for index, item in zip(reversed(range(result_len)), 360 reversed(self.directive.result)): 361 if ':module:' in item: 362 self.directive.result.pop(index) 363 364 def format_signature(self): 365 return u'' 366 367 def format_name(self): 368 path = find_service_path(self.env, self.object) 369 if path is None: 370 return 371 return '/' + '/'.join(path) 372 373 374class FunctionDirective(PyMethod): 375 name = 'function' 376 objtype = 'function' 377 378 def get_signature_prefix(self, sig): 379 return 'function ' 380 381 382def document_function(funcdef, docstrings=None, protocols=['restjson']): 383 """A helper function to complete a function documentation with return and 384 parameter types""" 385 # If the function doesn't have a docstring, add an empty list 386 # so the default behaviors below work correctly. 387 if not docstrings: 388 docstrings = [[]] 389 found_params = set() 390 391 for si, docstring in enumerate(docstrings): 392 for i, line in enumerate(docstring): 393 m = field_re.match(line) 394 if m and m.group('field') == 'param': 395 found_params.add(m.group('name')) 396 397 next_param_pos = (0, 0) 398 399 for arg in funcdef.arguments: 400 content = [ 401 u':type %s: :wsme:type:`%s`' % ( 402 arg.name, datatypename(arg.datatype)) 403 ] 404 if arg.name not in found_params: 405 content.insert(0, u':param %s: ' % (arg.name)) 406 pos = next_param_pos 407 else: 408 for si, docstring in enumerate(docstrings): 409 for i, line in enumerate(docstring): 410 m = field_re.match(line) 411 if m and m.group('field') == 'param' \ 412 and m.group('name') == arg.name: 413 pos = (si, i + 1) 414 break 415 docstring = docstrings[pos[0]] 416 docstring[pos[1]:pos[1]] = content 417 next_param_pos = (pos[0], pos[1] + len(content)) 418 419 if funcdef.return_type: 420 content = [ 421 u':rtype: %s' % datatypename(funcdef.return_type) 422 ] 423 pos = None 424 for si, docstring in enumerate(docstrings): 425 for i, line in enumerate(docstring): 426 m = field_re.match(line) 427 if m and m.group('field') == 'return': 428 pos = (si, i + 1) 429 break 430 else: 431 pos = next_param_pos 432 docstring = docstrings[pos[0]] 433 docstring[pos[1]:pos[1]] = content 434 435 codesamples = [] 436 437 if protocols: 438 params = [] 439 for arg in funcdef.arguments: 440 params.append(( 441 arg.name, 442 arg.datatype, 443 make_sample_object(arg.datatype) 444 )) 445 codesamples.extend([ 446 u':%s:' % _(u'Parameters samples'), 447 u' .. cssclass:: toggle', 448 u'' 449 ]) 450 for name, protocol in protocols: 451 language, sample = protocol.encode_sample_params( 452 params, format=True) 453 codesamples.extend([ 454 u' ' * 4 + name, 455 u' .. code-block:: ' + language, 456 u'', 457 ]) 458 codesamples.extend(( 459 u' ' * 12 + line 460 for line in str(sample).split('\n') 461 )) 462 463 if funcdef.return_type: 464 codesamples.extend([ 465 u':%s:' % _(u'Return samples'), 466 u' .. cssclass:: toggle', 467 u'' 468 ]) 469 sample_obj = make_sample_object(funcdef.return_type) 470 for name, protocol in protocols: 471 language, sample = protocol.encode_sample_result( 472 funcdef.return_type, sample_obj, format=True) 473 codesamples.extend([ 474 u' ' * 4 + name, 475 u' .. code-block:: ' + language, 476 u'', 477 ]) 478 codesamples.extend(( 479 u' ' * 12 + line 480 for line in str(sample).split('\n') 481 )) 482 483 docstrings[0:0] = [codesamples] 484 return docstrings 485 486 487class FunctionDocumenter(autodoc.MethodDocumenter): 488 domain = 'wsme' 489 directivetype = 'function' 490 objtype = 'function' 491 priority = 1 492 493 option_spec = { 494 'path': directives.unchanged, 495 'method': directives.unchanged 496 } 497 498 @staticmethod 499 def can_document_member(member, membername, isattr, parent): 500 return (isinstance(parent, ServiceDocumenter) and 501 wsme.api.iswsmefunction(member)) 502 503 def import_object(self): 504 ret = super(FunctionDocumenter, self).import_object() 505 self.directivetype = 'function' 506 self.wsme_fd = wsme.api.FunctionDefinition.get(self.object) 507 self.retann = datatypename(self.wsme_fd.return_type) 508 return ret 509 510 def format_args(self): 511 args = [arg.name for arg in self.wsme_fd.arguments] 512 defaults = [ 513 arg.default 514 for arg in self.wsme_fd.arguments if not arg.mandatory 515 ] 516 return inspect.formatargspec(args, defaults=defaults) 517 518 def get_doc(self, encoding=None): 519 """Inject the type and param fields into the docstrings so that the 520 user can add its own param fields to document the parameters""" 521 docstrings = super(FunctionDocumenter, self).get_doc(encoding) 522 523 protocols = get_protocols( 524 self.options.protocols or self.env.app.config.wsme_protocols 525 ) 526 527 return document_function( 528 self.wsme_fd, docstrings, protocols 529 ) 530 531 def add_content(self, more_content, no_docstring=False): 532 super(FunctionDocumenter, self).add_content(more_content, no_docstring) 533 534 def format_name(self): 535 return self.wsme_fd.name 536 537 def add_directive_header(self, sig): 538 super(FunctionDocumenter, self).add_directive_header(sig) 539 # remove the :module: option that was added by ClassDocumenter 540 result_len = len(self.directive.result) 541 for index, item in zip(reversed(range(result_len)), 542 reversed(self.directive.result)): 543 if ':module:' in item: 544 self.directive.result.pop(index) 545 546 547class WSMEDomain(Domain): 548 name = 'wsme' 549 label = 'WSME' 550 551 object_types = { 552 'type': ObjType(_('type'), 'type', 'obj'), 553 'service': ObjType(_('service'), 'service', 'obj') 554 } 555 556 directives = { 557 'type': TypeDirective, 558 'attribute': AttributeDirective, 559 'service': ServiceDirective, 560 'root': RootDirective, 561 'function': FunctionDirective, 562 } 563 564 roles = { 565 'type': XRefRole() 566 } 567 568 initial_data = { 569 'types': {}, # fullname -> docname 570 } 571 572 def clear_doc(self, docname): 573 keys = list(self.data['types'].keys()) 574 for key in keys: 575 value = self.data['types'][key] 576 if value == docname: 577 del self.data['types'][key] 578 579 def resolve_xref(self, env, fromdocname, builder, 580 type, target, node, contnode): 581 if target not in self.data['types']: 582 return None 583 todocname = self.data['types'][target] 584 return make_refnode( 585 builder, fromdocname, todocname, target, contnode, target) 586 587 588def setup(app): 589 app.add_domain(WSMEDomain) 590 app.add_autodocumenter(TypeDocumenter) 591 app.add_autodocumenter(AttributeDocumenter) 592 app.add_autodocumenter(ServiceDocumenter) 593 app.add_autodocumenter(FunctionDocumenter) 594 595 app.add_config_value('wsme_root', None, 'env') 596 app.add_config_value('wsme_webpath', '/', 'env') 597 app.add_config_value('wsme_protocols', ['restjson', 'restxml'], 'env') 598 app.add_js_file('toggle.js') 599 app.add_css_file('toggle.css') 600