Loading .readthedocs.yml +2 −4 Original line number Diff line number Diff line Loading @@ -17,7 +17,5 @@ sphinx: # formats: all # Optionally set the version of Python and requirements required to build your docs python: version: 3.7 install: - requirements: docs/src/requirements.txt conda: file: docs/src/environment.yml docs/src/conf.py +270 −2 Original line number Diff line number Diff line Loading @@ -12,10 +12,17 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # 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 tango #import skabase autodoc_mock_imports = ['PyTango', 'tango', 'tango.server','run', 'DeviceMeta', 'command', autodoc_mock_imports = ['PyTango','run', 'DeviceMeta', 'command', 'future', 'future.utils', 'logging', 'logging.handlers', 'ska', 'ska.base', 'SKAMaster', 'SKASubarray','numpy' ] Loading Loading @@ -216,3 +223,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) docs/src/environment.yml 0 → 100644 +10 −0 Original line number Diff line number Diff line name: py3 dependencies: - python=3 - numpy - gevent - graphviz - mock - pillow - sphinx - sphinx_rtd_theme docs/src/mock_tango_extension.py 0 → 100644 +133 −0 Original line number Diff line number Diff line """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 Loading
.readthedocs.yml +2 −4 Original line number Diff line number Diff line Loading @@ -17,7 +17,5 @@ sphinx: # formats: all # Optionally set the version of Python and requirements required to build your docs python: version: 3.7 install: - requirements: docs/src/requirements.txt conda: file: docs/src/environment.yml
docs/src/conf.py +270 −2 Original line number Diff line number Diff line Loading @@ -12,10 +12,17 @@ # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # # 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 tango #import skabase autodoc_mock_imports = ['PyTango', 'tango', 'tango.server','run', 'DeviceMeta', 'command', autodoc_mock_imports = ['PyTango','run', 'DeviceMeta', 'command', 'future', 'future.utils', 'logging', 'logging.handlers', 'ska', 'ska.base', 'SKAMaster', 'SKASubarray','numpy' ] Loading Loading @@ -216,3 +223,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)
docs/src/environment.yml 0 → 100644 +10 −0 Original line number Diff line number Diff line name: py3 dependencies: - python=3 - numpy - gevent - graphviz - mock - pillow - sphinx - sphinx_rtd_theme
docs/src/mock_tango_extension.py 0 → 100644 +133 −0 Original line number Diff line number Diff line """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