diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..9e43bc0c2744789b6a6fc239244c0f89a401ffca --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,2 @@ +conda: + file: docs/src/environment.yml diff --git a/docs/src/conf.py b/docs/src/conf.py index a0212d699bbb354bf1fd64c7988e1360f9210932..5584c81bc123adf0b507651eabd1790d6ec4b9e2 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -13,19 +13,29 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. # -#import tango -#import skabase -autodoc_mock_imports = ['PyTango', 'tango', 'tango.server','run', 'DeviceMeta', 'command', - 'future', 'future.utils', 'logging', 'logging.handlers', 'ska', - 'ska.base', 'SKAMaster', 'SKASubarray','numpy' - ] -autodoc_member_order = 'bysource' import os import sys sys.path.append(os.path.abspath('../../csp-lmc-common/')) sys.path.append(os.path.abspath('../../csp-lmc-mid/')) +sys.path.append(os.path.abspath('./')) +# Import tango +try: + import tango +except ImportError: + from mock_tango_extension import tango +from tango import Release +print("Building documentation for PyTango {0}".format(Release.version_long)) +print("Using PyTango from: {0}".format(os.path.dirname(tango.__file__))) + +#import skabase +autodoc_mock_imports = ['PyTango','run', 'DeviceMeta', 'command', + 'future', 'future.utils', 'logging', 'logging.handlers', 'ska', + 'ska.base', 'SKAMaster', 'SKASubarray','numpy' + ] +autodoc_member_order = 'bysource' + import sphinx_rtd_theme def setup(app): @@ -216,3 +226,264 @@ intersphinx_mapping = {'https://docs.python.org/': None} # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = True + +def copy_spaces(origin): + r = '' + for x in range(len(origin)): + if origin[x] in (' ', '\t'): + r += origin[x] + else: + return r + return r + +def type_to_link(tipus): + if tipus[:9] == 'sequence<' and tipus[-1:] == '>': + return 'sequence<' + type_to_link(tipus[9:-1]) + '>' + #elif tipus in dir(PyTango): + else: + return ':class:`' + tipus + "`" + #else: + # return tipus + +def type_to_pytango_link(tipus): + if tipus[:9] == 'sequence<' and tipus[-1:] == '>': + return 'sequence<' + type_to_link(tipus[9:-1]) + '>' + elif tipus in dir(tango): + return ':class:`' + tipus + "`" + else: + return tipus + +def possible_type_to_link(text): + if len(text) and text[0] == '(' and text[-1] == ')': + return '(' + type_to_link(text[1:-1]) +')' + return text + +def parse_typed_line(line): + spacesSplit = line.strip().split(' ') + first = spacesSplit[0].strip() + return possible_type_to_link(first) + ' ' + ' '.join(spacesSplit[1:]) + +def parse_parameters(line): + spaces = copy_spaces(line) + miniLine = line.strip() + + if miniLine[:2] != '- ': + return line + + spl = miniLine[2:].split(':', 1) + + assert(len(spl) == 2) + + return spaces + ':' + spl[0].strip() + ': ' + parse_typed_line(spl[1]) + + +def parse_bullet_with_type(line): + spaces = copy_spaces(line) + miniLine = line.strip() + + if miniLine[:2] not in ['- ', '* ']: + return line + + spl = miniLine.split(':', 1) + + if len(spl) != 2: + return line + + return spaces + spl[0] + ': ' + parse_typed_line(spl[1]) + + +def parse_throws(line): + words = re.split('(\W+)', line) + assert(line == ''.join(words)) + return ''.join(map(type_to_pytango_link, words)) + + +# http://codedump.tumblr.com/post/94712647/handling-python-docstring-indentation +def docstring_to_lines(docstring): + if not docstring: + return [] + lines = docstring.expandtabs().splitlines() + + # Determine minimum indentation (first line doesn't count): + indent = sys.maxint + for line in lines[1:]: + stripped = line.lstrip() + if stripped: + indent = min(indent, len(line) - len(stripped)) + + # Remove indentation (first line is special): + trimmed = [lines[0].strip()] + if indent < sys.maxint: + for line in lines[1:]: + trimmed.append(line[indent:].rstrip()) + + # Strip off trailing and leading blank lines: + while trimmed and not trimmed[-1]: + trimmed.pop() + while trimmed and not trimmed[0]: + trimmed.pop(0) + return trimmed + +def search_ONLY_signature(name, text): + lines = docstring_to_lines(text) + + # There should be ONE signature and must be the FIRST text + # Signature is the ONLY starting at position 0 + + signatureLine = None + + for ln in range(len(lines)): + line = lines[ln] + + if len(line.strip()) and line[0] != ' ': + parentesis = line.split('(', 1) + fname = parentesis[0].strip() + if len(parentesis)==2 and fname == name.rsplit('.',1)[1]: + if signatureLine is not None: # More than one signature! + return None + signatureLine = ln + else: + return None # There's a text as FIRST text that's NOT the signature! + + if signatureLine is None: + return None + + return lines[signatureLine] + +def split_signature(text): + if text is None: + return None + + # split "fname(params)", "returntype" + ops = text.split('->') + if len(ops) != 2: + return None + + # get rid of "fname" + params = ops[0].strip() + ret_type = ops[1].strip() + p = params.find('(') + if p < 0: + return None + params = params[p:] + return params, ret_type + + + +_with_only_one_signature_methods = {} + +def __reformat_lines(app, what, name, obj, options, lines): + global _with_only_one_signature_methods + if what != 'method': + for ln in range(len(lines)): + lines[ln] = parse_bullet_with_type(lines[ln]) + return + + toinsert = [] + parsingParameters = False + parsingThrows = False + + toinsert.append((0, "")) + + for ln in range(len(lines)): + line = lines[ln] + + if len(line) and line[0] != ' ': + if name in _with_only_one_signature_methods: + # This method has one and only one signature. So it will + # be displayed by sphinx, there's no need for us to fake + # it here... + lines[ln] = "" + else: + parentesis = line.split('(', 1) + fname = parentesis[0].strip() + if len(parentesis)==2 and fname == name.rsplit('.',1)[1]: + sg = split_signature(line) + if sg is not None: + # Main lines are like small titles (**bold**): + lines[ln] = '**' + fname +'** *' + sg[0] + '* **->** ' + type_to_link(sg[1]) + # Add an ENTER after the title, to make a different + # paragraph. So if I have 2 signatures, there's no problem + # with it... + toinsert.append((ln+1, "")) + + ## Main lines are like small titles (**bold**): + #lines[ln]='**' + line.strip() + '**' + ## Add an ENTER after the title, to make a different + ## paragraph. So if I have 2 signatures, there's no problem + ## with it... + #toinsert.append((ln+1, "")) + + + # Mark the "New in this version" lines... + if line.strip()[:14] == "New in PyTango": + lines[ln] = copy_spaces(lines[ln]) + "*" + line.strip() + "*" + parsingParameters = False + parsingThrows = False + + # Look for special control_words + # To replace the actual syntax: "Return : something" + # with the one understood by reStructuredText ":Return: something" + spl = line.strip().split(':', 1) + control_word = spl[0].strip() + + if ((len(spl) != 2) + or (control_word not in ["Parameters", "Return", "Throws", "Example", "See Also" ]) ): + if parsingParameters: + lines[ln] = parse_parameters(line) + elif parsingThrows: + lines[ln] = parse_throws(line) + continue + + parsingParameters = False + parsingThrows = False + spaces = copy_spaces(line) + + # The Example control word is even more special. I will put + # the contents from the following line into a code tag (::) + if control_word == 'Example': + lines[ln] = spaces + ":" + control_word + ": " + spl[1] + toinsert.append((ln+1, "")) + toinsert.append((ln+1, spaces + ' ::')) + toinsert.append((ln+1, "")) + elif control_word == 'Parameters': + lines[ln] = spaces + ":Parameters:" + parse_parameters(spl[1]) + parsingParameters = True + elif control_word == 'Return': + lines[ln] = spaces + ":Return: " + parse_typed_line(spl[1]) + elif control_word == "Throws": + lines[ln] = spaces + ":Throws:" + parse_throws(spl[1]) + parsingThrows = True + else: + lines[ln] = spaces + ":" + control_word + ": " + spl[1] + + for x in range(len(toinsert)-1, -1, -1): + pos, txt = toinsert[x] + lines.insert(pos, txt) + + +def __process_signature(app, what, name, obj, options, signature, return_annotation): + global _with_only_one_signature_methods + if what != 'method': + return + sg = split_signature(search_ONLY_signature(name, obj.__doc__)) + if sg is not None: + _with_only_one_signature_methods[name] = True + return sg + return (signature, return_annotation) + +def setup(app): + # sphinx will call these methods when he finds an object to document. + # I want to edit the docstring to adapt its format to something more + # beautiful. + # I also want to edit the signature because boost methods have no + # signature. I will read the signature from the docstring. + # The order sphinx will call it is __process_signature, __reformat_lines. + # And it is important because I keep some information between the two + # processes + # Problem is __process_signature works great with python methods... + # but is not even called for methods defined by boost. So, as it is, + # is useless now. + + #app.connect('autodoc-process-signature', __process_signature) + app.connect('autodoc-process-docstring', __reformat_lines) diff --git a/docs/src/environment.yml b/docs/src/environment.yml new file mode 100644 index 0000000000000000000000000000000000000000..4598327f3c63f83399886950458c864909350c89 --- /dev/null +++ b/docs/src/environment.yml @@ -0,0 +1,10 @@ +name: py3 +dependencies: +- python=3 +- numpy +- gevent +- graphviz +- mock +- pillow +- sphinx +- sphinx_rtd_theme diff --git a/docs/src/mock_tango_extension.py b/docs/src/mock_tango_extension.py new file mode 100644 index 0000000000000000000000000000000000000000..6197d2b90c407be9c6bf54ed110dae40f9dc578e --- /dev/null +++ b/docs/src/mock_tango_extension.py @@ -0,0 +1,133 @@ +"""Mock the tango._tango extension module. + +This is useful to build the documentation without building the extension. +However this is a bit tricky since the python side relies on what the +extension exposes. Here is the list of the mocking aspects that require +special attention: + + - __doc__ should not contain the mock documentation + - __mro__ is required for autodoc + - __name__ attribute is required + - Device_6Impl class should not be accessible + - the __base__ attribute for Device_[X]Impl is required + - it shoud be possible to set __init__, __getattr__ and __setattr__ methods + - tango.base_types.__document_enum needs to be patched before it is called + - tango.base_types.__document_method needs to be patched before it is called + - the mocks should not have any public methods such as assert_[...] + - _tango.constants.TgLibVers is required (e.g. '9.2.2') + - _tango._get_tango_lib_release function is required (e.g. lambda: 922) + - tango._tango AND tango.constants modules have to be patched + - autodoc requires a proper inheritance for the device impl classes + +Patching tango._tango using sys.modules does not seem to work for python +version older than 3.5 (failed with 2.7 and 3.4) +""" + +# Imports +import sys +from unittest.mock import MagicMock + +__all__ = ('tango',) + + +# Constants +TANGO_VERSION = '9.2.2' +TANGO_VERSION_INT = int(TANGO_VERSION[::2]) + + +# Extension mock class +class ExtensionMock(MagicMock): + + # Remove the mock documentation + __doc__ = None + + # The method resolution order is required for autodoc + __mro__ = object, + + @property + def __name__(self): + # __name__ is used for some objects + if self._mock_name is None: + return '' + return self._mock_name.split('.')[-1] + + def __getattr__(self, name): + # Limit device class discovery + if name == 'Device_6Impl': + raise AttributeError + # Regular mock behavior + return MagicMock.__getattr__(self, name) + + def __setattr__(self, name, value): + # Ignore unsupported magic methods + if name in ["__init__", "__getattr__", "__setattr__", + "__str__", "__repr__"]: + return + # Hook as soon as possible and patch the documentation methods + if name == 'side_effect' and self.__name__ == 'AccessControlType': + import tango.utils + import tango.base_types + import tango.device_server + import tango.connection + tango.utils.__dict__['document_enum'] = document_enum + tango.utils.__dict__['document_method'] = document_method + tango.base_types.__dict__['__document_enum'] = document_enum + tango.device_server.__dict__['__document_method'] = document_method + tango.connection.__dict__['__document_method'] = document_method + tango.connection.__dict__['__document_static_method'] = document_method + MagicMock.__setattr__(self, name, value) + + +# Remove all public methods +for name in dir(ExtensionMock): + if not name.startswith('_') and \ + callable(getattr(ExtensionMock, name)): + setattr(ExtensionMock, name, None) + + +# Patched version of document_enum +def document_enum(klass, enum_name, desc, append=True): + getattr(klass, enum_name).__doc__ = desc + + +# Patched version of document_enum +def document_method(klass, name, doc, add=True): + method = lambda self: None + method.__doc__ = doc + method.__name__ = name + setattr(klass, name, method) + + +# Use empty classes for device impl inheritance scheme +def set_device_implementations(module): + attrs = {'__module__': module.__name__} + module.DeviceImpl = type('DeviceImpl', (object,), attrs) + module.Device_2Impl = type('Device_2Impl', (module.DeviceImpl,), attrs) + module.Device_3Impl = type('Device_3Impl', (module.Device_2Impl,), attrs) + module.Device_4Impl = type('Device_4Impl', (module.Device_3Impl,), attrs) + module.Device_5Impl = type('Device_5Impl', (module.Device_4Impl,), attrs) + + +# Use empty classes for device proxy inheritance scheme +def set_device_proxy_implementations(module): + attrs = {'__module__': module.__name__} + module.Connection = type('Connection', (object,), attrs) + module.DeviceProxy = type('DeviceProxy', (module.Connection,), attrs) + + +# Patch the extension module +_tango = ExtensionMock(name='_tango') +_tango.constants.TgLibVers = TANGO_VERSION +_tango._get_tango_lib_release.return_value = TANGO_VERSION_INT +set_device_implementations(_tango) +set_device_proxy_implementations(_tango) + + +# Patch modules +sys.modules['tango._tango'] = _tango +sys.modules['tango.constants'] = _tango.constants +print('Mocking tango._tango extension module') + + +# Try to import +import tango diff --git a/docs/src/requirements.txt b/docs/src/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..a32835cc3b4a7abd615fc697885cb44ad51e06aa --- /dev/null +++ b/docs/src/requirements.txt @@ -0,0 +1 @@ +sphinx==3.1.0 \ No newline at end of file