| Index: commit-queue/buildbot_json.py
|
| ===================================================================
|
| --- commit-queue/buildbot_json.py (revision 249146)
|
| +++ commit-queue/buildbot_json.py (working copy)
|
| @@ -1,1455 +0,0 @@
|
| -#!/usr/bin/env python
|
| -# Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file at
|
| -# http://src.chromium.org/viewvc/chrome/trunk/src/LICENSE
|
| -# This file is NOT under GPL.
|
| -
|
| -"""Queries buildbot through the json interface.
|
| -"""
|
| -
|
| -__author__ = 'maruel@chromium.org'
|
| -__version__ = '1.2'
|
| -
|
| -import code
|
| -import datetime
|
| -import functools
|
| -import json
|
| -import logging
|
| -import optparse
|
| -import time
|
| -import urllib
|
| -import urllib2
|
| -import sys
|
| -
|
| -try:
|
| - from natsort import natsorted
|
| -except ImportError:
|
| - # natsorted is a simple helper to sort "naturally", e.g. "vm40" is sorted
|
| - # after "vm7". Defaults to normal sorting.
|
| - natsorted = sorted
|
| -
|
| -
|
| -# These values are buildbot constants used for Build and BuildStep.
|
| -# This line was copied from master/buildbot/status/builder.py.
|
| -SUCCESS, WARNINGS, FAILURE, SKIPPED, EXCEPTION, RETRY = range(6)
|
| -
|
| -
|
| -## Generic node caching code.
|
| -
|
| -
|
| -class Node(object):
|
| - """Root class for all nodes in the graph.
|
| -
|
| - Provides base functionality for any node in the graph, independent if it has
|
| - children or not or if its content can be addressed through an url or needs to
|
| - be fetched as part of another node.
|
| -
|
| - self.printable_attributes is only used for self documentation and for str()
|
| - implementation.
|
| - """
|
| - printable_attributes = []
|
| -
|
| - def __init__(self, parent, url):
|
| - self.printable_attributes = self.printable_attributes[:]
|
| - if url:
|
| - self.printable_attributes.append('url')
|
| - url = url.rstrip('/')
|
| - if parent is not None:
|
| - self.printable_attributes.append('parent')
|
| - self.url = url
|
| - self.parent = parent
|
| -
|
| - def __str__(self):
|
| - return self.to_string()
|
| -
|
| - def __repr__(self):
|
| - """Embeds key if present."""
|
| - key = getattr(self, 'key', None)
|
| - if key is not None:
|
| - return '<%s key=%s>' % (self.__class__.__name__, key)
|
| - cached_keys = getattr(self, 'cached_keys', None)
|
| - if cached_keys is not None:
|
| - return '<%s keys=%s>' % (self.__class__.__name__, cached_keys)
|
| - return super(Node, self).__repr__()
|
| -
|
| - def to_string(self, maximum=100):
|
| - out = ['%s:' % self.__class__.__name__]
|
| - assert not 'printable_attributes' in self.printable_attributes
|
| -
|
| - def limit(txt):
|
| - txt = str(txt)
|
| - if maximum > 0:
|
| - if len(txt) > maximum + 2:
|
| - txt = txt[:maximum] + '...'
|
| - return txt
|
| -
|
| - for k in sorted(self.printable_attributes):
|
| - if k == 'parent':
|
| - # Avoid infinite recursion.
|
| - continue
|
| - out.append(limit(' %s: %r' % (k, getattr(self, k))))
|
| - return '\n'.join(out)
|
| -
|
| - def refresh(self):
|
| - """Refreshes the data."""
|
| - self.discard()
|
| - return self.cache()
|
| -
|
| - def cache(self): # pragma: no cover
|
| - """Caches the data."""
|
| - raise NotImplementedError()
|
| -
|
| - def discard(self): # pragma: no cover
|
| - """Discards cached data.
|
| -
|
| - Pretty much everything is temporary except completed Build.
|
| - """
|
| - raise NotImplementedError()
|
| -
|
| - def read_non_json(self, suburl):
|
| - """Returns raw data for a suburl.
|
| -
|
| - Contrary to self.read(), self.read_non_json() is always available since
|
| - suburl is rooted at the base url. read() is only accessible for resources
|
| - that have an URI.
|
| - """
|
| - return self.parent.read_non_json(suburl)
|
| -
|
| -
|
| -class AddressableBaseDataNode(Node): # pylint: disable=W0223
|
| - """A node that contains a dictionary of data that can be fetched with an url.
|
| -
|
| - The node is directly addressable. It also often can be fetched by the parent.
|
| - """
|
| - printable_attributes = Node.printable_attributes + ['data']
|
| -
|
| - def __init__(self, parent, url, data):
|
| - super(AddressableBaseDataNode, self).__init__(parent, url)
|
| - self._data = data
|
| -
|
| - @property
|
| - def cached_data(self):
|
| - return self._data
|
| -
|
| - @property
|
| - def data(self):
|
| - self.cache()
|
| - return self._data
|
| -
|
| - def cache(self):
|
| - if self._data is None:
|
| - self._data = self._readall()
|
| - return True
|
| - return False
|
| -
|
| - def discard(self):
|
| - self._data = None
|
| -
|
| - def read(self, suburl):
|
| - assert self.url, self.__class__.__name__
|
| - url = self.url
|
| - if suburl:
|
| - url = '%s/%s' % (self.url, suburl)
|
| - return self.parent.read(url)
|
| -
|
| - def _readall(self):
|
| - return self.read('')
|
| -
|
| -
|
| -class AddressableDataNode(AddressableBaseDataNode): # pylint: disable=W0223
|
| - """Automatically encodes the url."""
|
| -
|
| - def __init__(self, parent, url, data):
|
| - super(AddressableDataNode, self).__init__(parent, urllib.quote(url), data)
|
| -
|
| -
|
| -class NonAddressableDataNode(Node): # pylint: disable=W0223
|
| - """A node that cannot be addressed by an unique url.
|
| -
|
| - The data comes directly from the parent.
|
| - """
|
| - def __init__(self, parent, subkey):
|
| - super(NonAddressableDataNode, self).__init__(parent, None)
|
| - self.subkey = subkey
|
| -
|
| - @property
|
| - def cached_data(self):
|
| - if self.parent.cached_data is None:
|
| - return None
|
| - return self.parent.cached_data[self.subkey]
|
| -
|
| - @property
|
| - def data(self):
|
| - return self.parent.data[self.subkey]
|
| -
|
| - def cache(self):
|
| - self.parent.cache()
|
| -
|
| - def discard(self): # pragma: no cover
|
| - """Avoid invalid state when parent recreate the object."""
|
| - raise AttributeError('Call parent discard() instead')
|
| -
|
| -
|
| -class VirtualNodeList(Node):
|
| - """Base class for every node that has children.
|
| -
|
| - Adds partial supports for keys and iterator functionality. 'key' can be a
|
| - string or a int. Not to be used directly.
|
| - """
|
| - printable_attributes = Node.printable_attributes + ['keys']
|
| -
|
| - def __init__(self, parent, url):
|
| - super(VirtualNodeList, self).__init__(parent, url)
|
| - # Keeps the keys independently when ordering is needed.
|
| - self._is_cached = False
|
| - self._has_keys_cached = False
|
| -
|
| - def __contains__(self, key):
|
| - """Enables 'if i in obj:'."""
|
| - return key in self.keys
|
| -
|
| - def __iter__(self):
|
| - """Enables 'for i in obj:'. It returns children."""
|
| - self.cache_keys()
|
| - for key in self.keys:
|
| - yield self[key]
|
| -
|
| - def __len__(self):
|
| - """Enables 'len(obj)' to get the number of childs."""
|
| - return len(self.keys)
|
| -
|
| - def discard(self):
|
| - """Discards data.
|
| -
|
| - The default behavior is to not invalidate cached keys. The only place where
|
| - keys need to be invalidated is with Builds.
|
| - """
|
| - self._is_cached = False
|
| - self._has_keys_cached = False
|
| -
|
| - @property
|
| - def cached_children(self): # pragma: no cover
|
| - """Returns an iterator over the children that are cached."""
|
| - raise NotImplementedError()
|
| -
|
| - @property
|
| - def cached_keys(self): # pragma: no cover
|
| - raise NotImplementedError()
|
| -
|
| - @property
|
| - def keys(self): # pragma: no cover
|
| - """Returns the keys for every children."""
|
| - raise NotImplementedError()
|
| -
|
| - def __getitem__(self, key): # pragma: no cover
|
| - """Returns a child, without fetching its data.
|
| -
|
| - The children could be invalid since no verification is done.
|
| - """
|
| - raise NotImplementedError()
|
| -
|
| - def cache(self): # pragma: no cover
|
| - """Cache all the children."""
|
| - raise NotImplementedError()
|
| -
|
| - def cache_keys(self): # pragma: no cover
|
| - """Cache all children's keys."""
|
| - raise NotImplementedError()
|
| -
|
| -
|
| -class NodeList(VirtualNodeList): # pylint: disable=W0223
|
| - """Adds a cache of the keys."""
|
| - def __init__(self, parent, url):
|
| - super(NodeList, self).__init__(parent, url)
|
| - self._keys = []
|
| -
|
| - @property
|
| - def cached_keys(self):
|
| - return self._keys
|
| -
|
| - @property
|
| - def keys(self):
|
| - self.cache_keys()
|
| - return self._keys
|
| -
|
| -
|
| -class NonAddressableNodeList(VirtualNodeList): # pylint: disable=W0223
|
| - """A node that contains children but retrieves all its data from its parent.
|
| -
|
| - I.e. there's no url to get directly this data.
|
| - """
|
| - # Child class object for children of this instance. For example, BuildSteps
|
| - # has BuildStep children.
|
| - _child_cls = None
|
| -
|
| - def __init__(self, parent, subkey):
|
| - super(NonAddressableNodeList, self).__init__(parent, None)
|
| - self.subkey = subkey
|
| - assert (
|
| - not isinstance(self._child_cls, NonAddressableDataNode) and
|
| - issubclass(self._child_cls, NonAddressableDataNode)), (
|
| - self._child_cls.__name__)
|
| -
|
| - @property
|
| - def cached_children(self):
|
| - if self.parent.cached_data is not None:
|
| - for i in xrange(len(self.parent.cached_data[self.subkey])):
|
| - yield self[i]
|
| -
|
| - @property
|
| - def cached_data(self):
|
| - if self.parent.cached_data is None:
|
| - return None
|
| - return self.parent.data.get(self.subkey, None)
|
| -
|
| - @property
|
| - def cached_keys(self):
|
| - if self.parent.cached_data is None:
|
| - return None
|
| - return range(len(self.parent.data.get(self.subkey, [])))
|
| -
|
| - @property
|
| - def data(self):
|
| - return self.parent.data[self.subkey]
|
| -
|
| - def cache(self):
|
| - self.parent.cache()
|
| -
|
| - def cache_keys(self):
|
| - self.parent.cache()
|
| -
|
| - def discard(self): # pragma: no cover
|
| - """Avoid infinite recursion by having the caller calls the parent's
|
| - discard() explicitely.
|
| - """
|
| - raise AttributeError('Call parent discard() instead')
|
| -
|
| - def __iter__(self):
|
| - """Enables 'for i in obj:'. It returns children."""
|
| - if self.data:
|
| - for i in xrange(len(self.data)):
|
| - yield self[i]
|
| -
|
| - def __getitem__(self, key):
|
| - """Doesn't cache the value, it's not needed.
|
| -
|
| - TODO(maruel): Cache?
|
| - """
|
| - if isinstance(key, int) and key < 0:
|
| - key = len(self.data) + key
|
| - # pylint: disable=E1102
|
| - return self._child_cls(self, key)
|
| -
|
| -
|
| -class AddressableNodeList(NodeList):
|
| - """A node that has children that can be addressed with an url."""
|
| -
|
| - # Child class object for children of this instance. For example, Builders has
|
| - # Builder children and Builds has Build children.
|
| - _child_cls = None
|
| -
|
| - def __init__(self, parent, url):
|
| - super(AddressableNodeList, self).__init__(parent, url)
|
| - self._cache = {}
|
| - assert (
|
| - not isinstance(self._child_cls, AddressableDataNode) and
|
| - issubclass(self._child_cls, AddressableDataNode)), (
|
| - self._child_cls.__name__)
|
| -
|
| - @property
|
| - def cached_children(self):
|
| - for item in self._cache.itervalues():
|
| - if item.cached_data is not None:
|
| - yield item
|
| -
|
| - @property
|
| - def cached_keys(self):
|
| - return self._cache.keys()
|
| -
|
| - def __getitem__(self, key):
|
| - """Enables 'obj[i]'."""
|
| - if self._has_keys_cached and not key in self._keys:
|
| - raise KeyError(key)
|
| -
|
| - if not key in self._cache:
|
| - # Create an empty object.
|
| - self._create_obj(key, None)
|
| - return self._cache[key]
|
| -
|
| - def cache(self):
|
| - if not self._is_cached:
|
| - data = self._readall()
|
| - for key in sorted(data):
|
| - self._create_obj(key, data[key])
|
| - self._is_cached = True
|
| - self._has_keys_cached = True
|
| -
|
| - def cache_partial(self, children):
|
| - """Caches a partial number of children.
|
| -
|
| - This method is more efficient since it does a single request for all the
|
| - children instead of one request per children.
|
| -
|
| - It only grab objects not already cached.
|
| - """
|
| - # pylint: disable=W0212
|
| - if not self._is_cached:
|
| - to_fetch = [
|
| - child for child in children
|
| - if not (child in self._cache and self._cache[child].cached_data)
|
| - ]
|
| - if to_fetch:
|
| - # Similar to cache(). The only reason to sort is to simplify testing.
|
| - params = '&'.join(
|
| - 'select=%s' % urllib.quote(str(v)) for v in sorted(to_fetch))
|
| - data = self.read('?' + params)
|
| - for key in sorted(data):
|
| - self._create_obj(key, data[key])
|
| -
|
| - def cache_keys(self):
|
| - """Implement to speed up enumeration. Defaults to call cache()."""
|
| - if not self._has_keys_cached:
|
| - self.cache()
|
| - assert self._has_keys_cached
|
| -
|
| - def discard(self):
|
| - """Discards temporary children."""
|
| - super(AddressableNodeList, self).discard()
|
| - for v in self._cache.itervalues():
|
| - v.discard()
|
| -
|
| - def read(self, suburl):
|
| - assert self.url, self.__class__.__name__
|
| - url = self.url
|
| - if suburl:
|
| - url = '%s/%s' % (self.url, suburl)
|
| - return self.parent.read(url)
|
| -
|
| - def _create_obj(self, key, data):
|
| - """Creates an object of type self._child_cls."""
|
| - # pylint: disable=E1102
|
| - obj = self._child_cls(self, key, data)
|
| - # obj.key and key may be different.
|
| - # No need to overide cached data with None.
|
| - if data is not None or obj.key not in self._cache:
|
| - self._cache[obj.key] = obj
|
| - if obj.key not in self._keys:
|
| - self._keys.append(obj.key)
|
| -
|
| - def _readall(self):
|
| - return self.read('')
|
| -
|
| -
|
| -class SubViewNodeList(VirtualNodeList): # pylint: disable=W0223
|
| - """A node that shows a subset of children that comes from another structure.
|
| -
|
| - The node is not addressable.
|
| -
|
| - E.g. the keys are retrieved from parent but the actual data comes from
|
| - virtual_parent.
|
| - """
|
| -
|
| - def __init__(self, parent, virtual_parent, subkey):
|
| - super(SubViewNodeList, self).__init__(parent, None)
|
| - self.subkey = subkey
|
| - self.virtual_parent = virtual_parent
|
| - assert isinstance(self.parent, AddressableDataNode)
|
| - assert isinstance(self.virtual_parent, NodeList)
|
| -
|
| - @property
|
| - def cached_children(self):
|
| - if self.parent.cached_data is not None:
|
| - for item in self.keys:
|
| - if item in self.virtual_parent.keys:
|
| - child = self[item]
|
| - if child.cached_data is not None:
|
| - yield child
|
| -
|
| - @property
|
| - def cached_keys(self):
|
| - return (self.parent.cached_data or {}).get(self.subkey, [])
|
| -
|
| - @property
|
| - def keys(self):
|
| - self.cache_keys()
|
| - return self.parent.data.get(self.subkey, [])
|
| -
|
| - def cache(self):
|
| - """Batch request for each child in a single read request."""
|
| - if not self._is_cached:
|
| - self.virtual_parent.cache_partial(self.keys)
|
| - self._is_cached = True
|
| -
|
| - def cache_keys(self):
|
| - if not self._has_keys_cached:
|
| - self.parent.cache()
|
| - self._has_keys_cached = True
|
| -
|
| - def discard(self):
|
| - if self.parent.cached_data is not None:
|
| - for child in self.virtual_parent.cached_children:
|
| - if child.key in self.keys:
|
| - child.discard()
|
| - self.parent.discard()
|
| - super(SubViewNodeList, self).discard()
|
| -
|
| - def __getitem__(self, key):
|
| - """Makes sure the key is in our key but grab it from the virtual parent."""
|
| - return self.virtual_parent[key]
|
| -
|
| - def __iter__(self):
|
| - self.cache()
|
| - return super(SubViewNodeList, self).__iter__()
|
| -
|
| -
|
| -###############################################################################
|
| -## Buildbot-specific code
|
| -
|
| -
|
| -class Slave(AddressableDataNode):
|
| - printable_attributes = AddressableDataNode.printable_attributes + [
|
| - 'name', 'key', 'connected', 'version',
|
| - ]
|
| -
|
| - def __init__(self, parent, name, data):
|
| - super(Slave, self).__init__(parent, name, data)
|
| - self.name = name
|
| - self.key = self.name
|
| - # TODO(maruel): Add SlaveBuilders and a 'builders' property.
|
| - # TODO(maruel): Add a 'running_builds' property.
|
| -
|
| - @property
|
| - def connected(self):
|
| - return self.data.get('connected', False)
|
| -
|
| - @property
|
| - def version(self):
|
| - return self.data.get('version')
|
| -
|
| -
|
| -class Slaves(AddressableNodeList):
|
| - _child_cls = Slave
|
| - printable_attributes = AddressableNodeList.printable_attributes + ['names']
|
| -
|
| - def __init__(self, parent):
|
| - super(Slaves, self).__init__(parent, 'slaves')
|
| -
|
| - @property
|
| - def names(self):
|
| - return self.keys
|
| -
|
| -
|
| -class BuilderSlaves(SubViewNodeList):
|
| - """Similar to Slaves but only list slaves connected to a specific builder.
|
| - """
|
| - printable_attributes = SubViewNodeList.printable_attributes + ['names']
|
| -
|
| - def __init__(self, parent):
|
| - super(BuilderSlaves, self).__init__(
|
| - parent, parent.parent.parent.slaves, 'slaves')
|
| -
|
| - @property
|
| - def names(self):
|
| - return self.keys
|
| -
|
| -
|
| -class BuildStep(NonAddressableDataNode):
|
| - printable_attributes = NonAddressableDataNode.printable_attributes + [
|
| - 'name', 'number', 'start_time', 'end_time', 'duration', 'is_started',
|
| - 'is_finished', 'is_running',
|
| - 'result', 'simplified_result',
|
| - ]
|
| -
|
| - def __init__(self, parent, number):
|
| - """It's already pre-loaded by definition since the data is retrieve via the
|
| - Build object.
|
| - """
|
| - assert isinstance(number, int)
|
| - super(BuildStep, self).__init__(parent, number)
|
| - self.number = number
|
| -
|
| - @property
|
| - def build(self):
|
| - """Returns the Build object for this BuildStep."""
|
| - # Build.BuildSteps.BuildStep
|
| - return self.parent.parent
|
| -
|
| - @property
|
| - def start_time(self):
|
| - if self.data.get('times'):
|
| - return int(round(self.data['times'][0]))
|
| -
|
| - @property
|
| - def end_time(self):
|
| - times = self.data.get('times')
|
| - if times and len(times) == 2 and times[1]:
|
| - return int(round(times[1]))
|
| -
|
| - @property
|
| - def duration(self):
|
| - if self.start_time:
|
| - return (self.end_time or int(round(time.time()))) - self.start_time
|
| -
|
| - @property
|
| - def name(self):
|
| - return self.data['name']
|
| -
|
| - @property
|
| - def is_started(self):
|
| - return self.data.get('isStarted', False)
|
| -
|
| - @property
|
| - def is_finished(self):
|
| - return self.data.get('isFinished', False)
|
| -
|
| - @property
|
| - def is_running(self):
|
| - return self.is_started and not self.is_finished
|
| -
|
| - @property
|
| - def result(self):
|
| - result = self.data.get('results')
|
| - if result is None:
|
| - # results may be 0, in that case with filter=1, the value won't be
|
| - # present.
|
| - if self.data.get('isFinished'):
|
| - result = self.data.get('results', 0)
|
| - while isinstance(result, list):
|
| - result = result[0]
|
| - return result
|
| -
|
| - @property
|
| - def simplified_result(self):
|
| - """Returns a simplified 3 state value, True, False or None."""
|
| - result = self.result
|
| - if result in (SUCCESS, WARNINGS):
|
| - return True
|
| - elif result in (FAILURE, EXCEPTION, RETRY):
|
| - return False
|
| - assert result in (None, SKIPPED), (result, self.data)
|
| - return None
|
| -
|
| - @property
|
| - def stdio(self):
|
| - """Returns the stdio for this step or None if not available."""
|
| - # Parents ordering is BuildSteps / Build / Builds / Builders
|
| - # A bit hackish but works.
|
| - build = self.build
|
| - builder = build.builder
|
| - suburl = 'builders/%s/builds/%d/steps/%s/logs/stdio/text' % (
|
| - builder.name, build.number, self.name)
|
| - return self.read_non_json(suburl)
|
| -
|
| -
|
| -class BuildSteps(NonAddressableNodeList):
|
| - """Duplicates keys to support lookup by both step number and step name."""
|
| - printable_attributes = NonAddressableNodeList.printable_attributes + [
|
| - 'failed',
|
| - ]
|
| - _child_cls = BuildStep
|
| -
|
| - def __init__(self, parent):
|
| - """It's already pre-loaded by definition since the data is retrieve via the
|
| - Build object.
|
| - """
|
| - super(BuildSteps, self).__init__(parent, 'steps')
|
| -
|
| - @property
|
| - def keys(self):
|
| - """Returns the steps name in order."""
|
| - return [i['name'] for i in (self.data or [])]
|
| -
|
| - @property
|
| - def failed(self):
|
| - """Shortcuts that lists the step names of steps that failed."""
|
| - return [step.name for step in self if step.simplified_result is False]
|
| -
|
| - def __getitem__(self, key):
|
| - """Accept step name in addition to index number."""
|
| - if isinstance(key, basestring):
|
| - # It's a string, try to find the corresponding index.
|
| - for i, step in enumerate(self.data):
|
| - if step['name'] == key:
|
| - key = i
|
| - break
|
| - else:
|
| - raise KeyError(key)
|
| - return super(BuildSteps, self).__getitem__(key)
|
| -
|
| -
|
| -class Build(AddressableDataNode):
|
| - printable_attributes = AddressableDataNode.printable_attributes + [
|
| - 'key', 'number', 'steps', 'blame', 'reason', 'revision', 'result',
|
| - 'simplified_result', 'start_time', 'end_time', 'duration', 'slave',
|
| - 'properties', 'completed',
|
| - ]
|
| -
|
| - def __init__(self, parent, key, data):
|
| - super(Build, self).__init__(parent, str(key), data)
|
| - self.number = int(key)
|
| - self.key = self.number
|
| - self.steps = BuildSteps(self)
|
| -
|
| - @property
|
| - def blame(self):
|
| - return self.data.get('blame', [])
|
| -
|
| - @property
|
| - def builder(self):
|
| - """Returns the Builder object.
|
| -
|
| - Goes up the hierarchy to find the Buildbot.builders[builder] instance.
|
| - """
|
| - return self.parent.parent.parent.parent.builders[self.data['builderName']]
|
| -
|
| - @property
|
| - def start_time(self):
|
| - if self.data.get('times'):
|
| - return int(round(self.data['times'][0]))
|
| -
|
| - @property
|
| - def end_time(self):
|
| - times = self.data.get('times')
|
| - if times and len(times) == 2 and times[1]:
|
| - return int(round(times[1]))
|
| -
|
| - @property
|
| - def duration(self):
|
| - if self.start_time:
|
| - return (self.end_time or int(round(time.time()))) - self.start_time
|
| -
|
| - @property
|
| - def eta(self):
|
| - return self.data.get('eta', 0)
|
| -
|
| - @property
|
| - def completed(self):
|
| - return self.data.get('currentStep') is None
|
| -
|
| - @property
|
| - def properties(self):
|
| - return self.data.get('properties', [])
|
| -
|
| - @property
|
| - def properties_as_dict(self):
|
| - """Converts the 3-tuple properties into a dict(p[0]: p[1]) and ignores the
|
| - property's source.
|
| - """
|
| - return dict((p[0], p[1]) for p in self.properties)
|
| -
|
| - @property
|
| - def reason(self):
|
| - return self.data.get('reason')
|
| -
|
| - @property
|
| - def result(self):
|
| - result = self.data.get('results')
|
| - while isinstance(result, list):
|
| - result = result[0]
|
| - if result is None and self.steps:
|
| - # results may be 0, in that case with filter=1, the value won't be
|
| - # present.
|
| - result = self.steps[-1].result
|
| - return result
|
| -
|
| - @property
|
| - def revision(self):
|
| - return self.data.get('sourceStamp', {}).get('revision')
|
| -
|
| - @property
|
| - def simplified_result(self):
|
| - """Returns a simplified 3 state value, True, False or None."""
|
| - result = self.result
|
| - if result in (SUCCESS, WARNINGS, SKIPPED):
|
| - return True
|
| - elif result in (FAILURE, EXCEPTION, RETRY):
|
| - return False
|
| - assert result is None, (result, self.data)
|
| - return None
|
| -
|
| - @property
|
| - def slave(self):
|
| - """Returns the Slave object.
|
| -
|
| - Goes up the hierarchy to find the Buildbot.slaves[slave] instance.
|
| - """
|
| - return self.parent.parent.parent.parent.slaves[self.data['slave']]
|
| -
|
| - def discard(self):
|
| - """Completed Build isn't discarded."""
|
| - if self._data and self.result is None:
|
| - assert not self.steps or not self.steps[-1].data.get('isFinished')
|
| - self._data = None
|
| -
|
| -
|
| -class CurrentBuilds(SubViewNodeList):
|
| - """Lists of the current builds."""
|
| - def __init__(self, parent):
|
| - super(CurrentBuilds, self).__init__(
|
| - parent, parent.builds, 'currentBuilds')
|
| -
|
| -
|
| -class PendingBuilds(AddressableDataNode):
|
| - def __init__(self, parent):
|
| - super(PendingBuilds, self).__init__(parent, 'pendingBuilds', None)
|
| -
|
| -
|
| -class Builds(AddressableNodeList):
|
| - """Supports iteration.
|
| -
|
| - Recommends using .cache() to speed up if a significant number of builds are
|
| - iterated over.
|
| - """
|
| - _child_cls = Build
|
| -
|
| - def __init__(self, parent):
|
| - super(Builds, self).__init__(parent, 'builds')
|
| -
|
| - def __getitem__(self, key):
|
| - """Adds supports for negative reference and enables retrieving non-cached
|
| - builds.
|
| -
|
| - e.g. -1 is the last build, -2 is the previous build before the last one.
|
| - """
|
| - key = int(key)
|
| - if key < 0:
|
| - # Convert negative to positive build number.
|
| - self.cache_keys()
|
| - # Since the negative value can be outside of the cache keys range, use the
|
| - # highest key value and calculate from it.
|
| - key = max(self._keys) + key + 1
|
| -
|
| - if not key in self._cache:
|
| - # Create an empty object.
|
| - self._create_obj(key, None)
|
| - return self._cache[key]
|
| -
|
| - def __iter__(self):
|
| - """Returns cached Build objects in reversed order.
|
| -
|
| - The most recent build is returned first and then in reverse chronological
|
| - order, up to the oldest cached build by the server. Older builds can be
|
| - accessed but will trigger significantly more I/O so they are not included by
|
| - default in the iteration.
|
| -
|
| - To access the older builds, use self.iterall() instead.
|
| - """
|
| - self.cache()
|
| - return reversed(self._cache.values())
|
| -
|
| - def iterall(self):
|
| - """Returns Build objects in decreasing order unbounded up to build 0.
|
| -
|
| - The most recent build is returned first and then in reverse chronological
|
| - order. Older builds can be accessed and will trigger significantly more I/O
|
| - so use this carefully.
|
| - """
|
| - # Only cache keys here.
|
| - self.cache_keys()
|
| - if self._keys:
|
| - for i in xrange(max(self._keys), -1, -1):
|
| - yield self[i]
|
| -
|
| - def cache_keys(self):
|
| - """Grabs the keys (build numbers) from the builder."""
|
| - if not self._has_keys_cached:
|
| - for i in self.parent.data.get('cachedBuilds', []):
|
| - i = int(i)
|
| - self._cache.setdefault(i, Build(self, i, None))
|
| - if i not in self._keys:
|
| - self._keys.append(i)
|
| - self._has_keys_cached = True
|
| -
|
| - def discard(self):
|
| - super(Builds, self).discard()
|
| - # Can't keep keys.
|
| - self._has_keys_cached = False
|
| -
|
| - def _readall(self):
|
| - return self.read('_all')
|
| -
|
| -
|
| -class Builder(AddressableDataNode):
|
| - printable_attributes = AddressableDataNode.printable_attributes + [
|
| - 'name', 'key', 'builds', 'slaves', 'pending_builds', 'current_builds',
|
| - ]
|
| -
|
| - def __init__(self, parent, name, data):
|
| - super(Builder, self).__init__(parent, name, data)
|
| - self.name = name
|
| - self.key = name
|
| - self.builds = Builds(self)
|
| - self.slaves = BuilderSlaves(self)
|
| - self.current_builds = CurrentBuilds(self)
|
| - self.pending_builds = PendingBuilds(self)
|
| -
|
| - def discard(self):
|
| - super(Builder, self).discard()
|
| - self.builds.discard()
|
| - self.slaves.discard()
|
| - self.current_builds.discard()
|
| -
|
| -
|
| -class Builders(AddressableNodeList):
|
| - """Root list of builders."""
|
| - _child_cls = Builder
|
| -
|
| - def __init__(self, parent):
|
| - super(Builders, self).__init__(parent, 'builders')
|
| -
|
| -
|
| -class Buildbot(AddressableBaseDataNode):
|
| - """If a master restart occurs, this object should be recreated as it caches
|
| - data.
|
| - """
|
| - # Throttle fetches to not kill the server.
|
| - auto_throttle = None
|
| - printable_attributes = AddressableDataNode.printable_attributes + [
|
| - 'slaves', 'builders', 'last_fetch',
|
| - ]
|
| -
|
| - def __init__(self, url):
|
| - super(Buildbot, self).__init__(None, url, None)
|
| - self._builders = Builders(self)
|
| - self._slaves = Slaves(self)
|
| - self.last_fetch = None
|
| -
|
| - @property
|
| - def builders(self):
|
| - return self._builders
|
| -
|
| - @property
|
| - def slaves(self):
|
| - return self._slaves
|
| -
|
| - def discard(self):
|
| - """Discards information about Builders and Slaves."""
|
| - super(Buildbot, self).discard()
|
| - self._builders.discard()
|
| - self._slaves.discard()
|
| -
|
| - def read(self, suburl):
|
| - """Returns json decoded data for the suburl."""
|
| - if self.auto_throttle:
|
| - if self.last_fetch:
|
| - delta = datetime.datetime.utcnow() - self.last_fetch
|
| - remaining = (datetime.timedelta(seconds=self.auto_throttle) -
|
| - delta)
|
| - if remaining > datetime.timedelta(seconds=0):
|
| - logging.debug('Sleeping for %ss' % remaining)
|
| - time.sleep(remaining.seconds)
|
| - self.last_fetch = datetime.datetime.utcnow()
|
| - url = '%s/json/%s' % (self.url, suburl)
|
| - if '?' in url:
|
| - url += '&filter=1'
|
| - else:
|
| - url += '?filter=1'
|
| - logging.debug('read(%s)' % suburl)
|
| - try:
|
| - channel = urllib.urlopen(url)
|
| - data = channel.read()
|
| - except IOError as e:
|
| - logging.warning('caught %s while fetching "%s"; re-throwing' % (
|
| - str(e), url))
|
| - raise
|
| - try:
|
| - return json.loads(data)
|
| - except ValueError:
|
| - if channel.getcode() >= 400:
|
| - # Convert it into an HTTPError for easier processing.
|
| - raise urllib2.HTTPError(
|
| - url, channel.getcode(), '%s:\n%s' % (url, data), channel.headers,
|
| - None)
|
| - raise
|
| -
|
| - def read_non_json(self, suburl):
|
| - """Returns data for an arbitrary suburl outside of the /json/ path."""
|
| - logging.debug('read_non_json(%s)' % suburl)
|
| - return urllib.urlopen('%s/%s' % (self.url, suburl)).read()
|
| -
|
| - def _readall(self):
|
| - return self.read('project')
|
| -
|
| -
|
| -###############################################################################
|
| -## Controller code
|
| -
|
| -
|
| -def usage(more):
|
| - def hook(fn):
|
| - fn.func_usage_more = more
|
| - return fn
|
| - return hook
|
| -
|
| -
|
| -def need_buildbot(fn):
|
| - """Post-parse args to create a buildbot object."""
|
| - @functools.wraps(fn)
|
| - def hook(parser, args, *extra_args, **kwargs):
|
| - old_parse_args = parser.parse_args
|
| - def new_parse_args(args):
|
| - options, args = old_parse_args(args)
|
| - if len(args) < 1:
|
| - parser.error('Need to pass the root url of the buildbot')
|
| - url = args.pop(0)
|
| - if not url.startswith('http'):
|
| - url = 'http://' + url
|
| - buildbot = Buildbot(url)
|
| - buildbot.auto_throttle = options.throttle
|
| - return options, args, buildbot
|
| - parser.parse_args = new_parse_args
|
| - # Call the original function with the modified parser.
|
| - return fn(parser, args, *extra_args, **kwargs)
|
| -
|
| - hook.func_usage_more = '[options] <url>'
|
| - return hook
|
| -
|
| -
|
| -@need_buildbot
|
| -def CMDpending(parser, args):
|
| - """Lists pending jobs."""
|
| - parser.add_option(
|
| - '-b', '--builder', dest='builders', action='append', default=[],
|
| - help='Builders to filter on')
|
| - options, args, buildbot = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unrecognized parameters: %s' % ' '.join(args))
|
| - if not options.builders:
|
| - options.builders = buildbot.builders.keys
|
| - for builder in options.builders:
|
| - builder = buildbot.builders[builder]
|
| - pending_builds = builder.data.get('pendingBuilds', 0)
|
| - if not pending_builds:
|
| - continue
|
| - print 'Builder %s: %d' % (builder.name, pending_builds)
|
| - if not options.quiet:
|
| - for pending in builder.pending_builds.data:
|
| - if 'revision' in pending['source']:
|
| - print ' revision: %s' % pending['source']['revision']
|
| - for change in pending['source']['changes']:
|
| - print ' change:'
|
| - print ' comment: %r' % unicode(change['comments'][:50])
|
| - print ' who: %s' % change['who']
|
| - return 0
|
| -
|
| -
|
| -@usage('[options] <url> [commands] ...')
|
| -@need_buildbot
|
| -def CMDrun(parser, args):
|
| - """Runs commands passed as parameters.
|
| -
|
| - When passing commands on the command line, each command will be run as if it
|
| - was on its own line.
|
| - """
|
| - parser.add_option('-f', '--file', help='Read script from file')
|
| - parser.add_option(
|
| - '-i', dest='use_stdin', action='store_true', help='Read script on stdin')
|
| - # Variable 'buildbot' is not used directly.
|
| - # pylint: disable=W0612
|
| - options, args, buildbot = parser.parse_args(args)
|
| - if (bool(args) + bool(options.use_stdin) + bool(options.file)) != 1:
|
| - parser.error('Need to pass only one of: <commands>, -f <file> or -i')
|
| - if options.use_stdin:
|
| - cmds = sys.stdin.read()
|
| - elif options.file:
|
| - cmds = open(options.file).read()
|
| - else:
|
| - cmds = '\n'.join(args)
|
| - compiled = compile(cmds, '<cmd line>', 'exec')
|
| - eval(compiled, globals(), locals())
|
| - return 0
|
| -
|
| -
|
| -@need_buildbot
|
| -def CMDinteractive(parser, args):
|
| - """Runs an interactive shell to run queries."""
|
| - _, args, buildbot = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unrecognized parameters: %s' % ' '.join(args))
|
| - prompt = (
|
| - 'Buildbot interactive console for "%s".\n'
|
| - 'Hint: Start with typing: \'buildbot.printable_attributes\' or '
|
| - '\'print str(buildbot)\' to explore.') % buildbot.url
|
| - local_vars = {
|
| - 'buildbot': buildbot,
|
| - 'b': buildbot,
|
| - }
|
| - code.interact(prompt, None, local_vars)
|
| -
|
| -
|
| -@need_buildbot
|
| -def CMDidle(parser, args):
|
| - """Lists idle slaves."""
|
| - return find_idle_busy_slaves(parser, args, True)
|
| -
|
| -
|
| -@need_buildbot
|
| -def CMDbusy(parser, args):
|
| - """Lists idle slaves."""
|
| - return find_idle_busy_slaves(parser, args, False)
|
| -
|
| -
|
| -@need_buildbot
|
| -def CMDdisconnected(parser, args):
|
| - """Lists disconnected slaves."""
|
| - _, args, buildbot = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unrecognized parameters: %s' % ' '.join(args))
|
| - for slave in buildbot.slaves:
|
| - if not slave.connected:
|
| - print slave.name
|
| - return 0
|
| -
|
| -
|
| -def find_idle_busy_slaves(parser, args, show_idle):
|
| - parser.add_option(
|
| - '-b', '--builder', dest='builders', action='append', default=[],
|
| - help='Builders to filter on')
|
| - parser.add_option(
|
| - '-s', '--slave', dest='slaves', action='append', default=[],
|
| - help='Slaves to filter on')
|
| - options, args, buildbot = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unrecognized parameters: %s' % ' '.join(args))
|
| - if not options.builders:
|
| - options.builders = buildbot.builders.keys
|
| - for builder in options.builders:
|
| - builder = buildbot.builders[builder]
|
| - if options.slaves:
|
| - # Only the subset of slaves connected to the builder.
|
| - slaves = list(set(options.slaves).intersection(set(builder.slaves.names)))
|
| - if not slaves:
|
| - continue
|
| - else:
|
| - slaves = builder.slaves.names
|
| - busy_slaves = [build.slave.name for build in builder.current_builds]
|
| - if show_idle:
|
| - slaves = natsorted(set(slaves) - set(busy_slaves))
|
| - else:
|
| - slaves = natsorted(set(slaves) & set(busy_slaves))
|
| - if options.quiet:
|
| - for slave in slaves:
|
| - print slave
|
| - else:
|
| - if slaves:
|
| - print 'Builder %s: %s' % (builder.name, ', '.join(slaves))
|
| - return 0
|
| -
|
| -
|
| -def last_failure(
|
| - buildbot, builders=None, slaves=None, steps=None, no_cache=False):
|
| - """Generator returning Build object that were the last failure with the
|
| - specific filters.
|
| - """
|
| - builders = builders or buildbot.builders.keys
|
| - for builder in builders:
|
| - builder = buildbot.builders[builder]
|
| - if slaves:
|
| - # Only the subset of slaves connected to the builder.
|
| - builder_slaves = list(set(slaves).intersection(set(builder.slaves.names)))
|
| - if not builder_slaves:
|
| - continue
|
| - else:
|
| - builder_slaves = builder.slaves.names
|
| -
|
| - if not no_cache and len(builder.slaves) > 2:
|
| - # Unless you just want the last few builds, it's often faster to
|
| - # fetch the whole thing at once, at the cost of a small hickup on
|
| - # the buildbot.
|
| - # TODO(maruel): Cache only N last builds or all builds since
|
| - # datetime.
|
| - builder.builds.cache()
|
| -
|
| - found = []
|
| - for build in builder.builds:
|
| - if build.slave.name not in builder_slaves or build.slave.name in found:
|
| - continue
|
| - # Only add the slave for the first completed build but still look for
|
| - # incomplete builds.
|
| - if build.completed:
|
| - found.append(build.slave.name)
|
| -
|
| - if steps:
|
| - if any(build.steps[step].simplified_result is False for step in steps):
|
| - yield build
|
| - elif build.simplified_result is False:
|
| - yield build
|
| -
|
| - if len(found) == len(builder_slaves):
|
| - # Found all the slaves, quit.
|
| - break
|
| -
|
| -
|
| -@need_buildbot
|
| -def CMDlast_failure(parser, args):
|
| - """Lists all slaves that failed on that step on their last build.
|
| -
|
| - Example: to find all slaves where their last build was a compile failure,
|
| - run with --step compile"""
|
| - parser.add_option(
|
| - '-S', '--step', dest='steps', action='append', default=[],
|
| - help='List all slaves that failed on that step on their last build')
|
| - parser.add_option(
|
| - '-b', '--builder', dest='builders', action='append', default=[],
|
| - help='Builders to filter on')
|
| - parser.add_option(
|
| - '-s', '--slave', dest='slaves', action='append', default=[],
|
| - help='Slaves to filter on')
|
| - parser.add_option(
|
| - '-n', '--no_cache', action='store_true',
|
| - help='Don\'t load all builds at once')
|
| - options, args, buildbot = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unrecognized parameters: %s' % ' '.join(args))
|
| - print_builders = not options.quiet and len(options.builders) != 1
|
| - last_builder = None
|
| - for build in last_failure(
|
| - buildbot, builders=options.builders,
|
| - slaves=options.slaves, steps=options.steps,
|
| - no_cache=options.no_cache):
|
| -
|
| - if print_builders and last_builder != build.builder:
|
| - print build.builder.name
|
| - last_builder = build.builder
|
| -
|
| - if options.quiet:
|
| - if options.slaves:
|
| - print '%s: %s' % (build.builder.name, build.slave.name)
|
| - else:
|
| - print build.slave.name
|
| - else:
|
| - out = '%d on %s: blame:%s' % (
|
| - build.number, build.slave.name, ', '.join(build.blame))
|
| - if print_builders:
|
| - out = ' ' + out
|
| - print out
|
| -
|
| - if len(options.steps) != 1:
|
| - for step in build.steps:
|
| - if step.simplified_result is False:
|
| - # Assume the first line is the text name anyway.
|
| - summary = ', '.join(step.data['text'][1:])[:40]
|
| - out = ' %s: "%s"' % (step.data['name'], summary)
|
| - if print_builders:
|
| - out = ' ' + out
|
| - print out
|
| - return 0
|
| -
|
| -
|
| -@need_buildbot
|
| -def CMDcurrent(parser, args):
|
| - """Lists current jobs."""
|
| - parser.add_option(
|
| - '-b', '--builder', dest='builders', action='append', default=[],
|
| - help='Builders to filter on')
|
| - parser.add_option(
|
| - '--blame', action='store_true', help='Only print the blame list')
|
| - options, args, buildbot = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unrecognized parameters: %s' % ' '.join(args))
|
| - if not options.builders:
|
| - options.builders = buildbot.builders.keys
|
| -
|
| - if options.blame:
|
| - blame = set()
|
| - for builder in options.builders:
|
| - for build in buildbot.builders[builder].current_builds:
|
| - if build.blame:
|
| - for blamed in build.blame:
|
| - blame.add(blamed)
|
| - print '\n'.join(blame)
|
| - return 0
|
| -
|
| - for builder in options.builders:
|
| - builder = buildbot.builders[builder]
|
| - if not options.quiet and builder.current_builds:
|
| - print builder.name
|
| - for build in builder.current_builds:
|
| - if options.quiet:
|
| - print build.slave.name
|
| - else:
|
| - out = '%4d: slave=%10s' % (build.number, build.slave.name)
|
| - out += ' duration=%5d' % (build.duration or 0)
|
| - if build.eta:
|
| - out += ' eta=%5.0f' % build.eta
|
| - else:
|
| - out += ' '
|
| - if build.blame:
|
| - out += ' blame=' + ', '.join(build.blame)
|
| - print out
|
| -
|
| - return 0
|
| -
|
| -
|
| -@need_buildbot
|
| -def CMDbuilds(parser, args):
|
| - """Lists all builds.
|
| -
|
| - Example: to find all builds on a single slave, run with -b bar -s foo
|
| - """
|
| - parser.add_option(
|
| - '-r', '--result', type='int', help='Build result to filter on')
|
| - parser.add_option(
|
| - '-b', '--builder', dest='builders', action='append', default=[],
|
| - help='Builders to filter on')
|
| - parser.add_option(
|
| - '-s', '--slave', dest='slaves', action='append', default=[],
|
| - help='Slaves to filter on')
|
| - parser.add_option(
|
| - '-n', '--no_cache', action='store_true',
|
| - help='Don\'t load all builds at once')
|
| - options, args, buildbot = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unrecognized parameters: %s' % ' '.join(args))
|
| - builders = options.builders or buildbot.builders.keys
|
| - for builder in builders:
|
| - builder = buildbot.builders[builder]
|
| - for build in builder.builds:
|
| - if not options.slaves or build.slave.name in options.slaves:
|
| - if options.quiet:
|
| - out = ''
|
| - if options.builders:
|
| - out += '%s/' % builder.name
|
| - if len(options.slaves) != 1:
|
| - out += '%s/' % build.slave.name
|
| - out += '%d revision:%s result:%s blame:%s' % (
|
| - build.number, build.revision, build.result, ','.join(build.blame))
|
| - print out
|
| - else:
|
| - print build
|
| - return 0
|
| -
|
| -
|
| -@need_buildbot
|
| -def CMDcount(parser, args):
|
| - """Count the number of builds that occured during a specific period.
|
| - """
|
| - parser.add_option(
|
| - '-o', '--over', type='int', help='Number of seconds to look for')
|
| - parser.add_option(
|
| - '-b', '--builder', dest='builders', action='append', default=[],
|
| - help='Builders to filter on')
|
| - options, args, buildbot = parser.parse_args(args)
|
| - if args:
|
| - parser.error('Unrecognized parameters: %s' % ' '.join(args))
|
| - if not options.over:
|
| - parser.error(
|
| - 'Specify the number of seconds, e.g. --over 86400 for the last 24 '
|
| - 'hours')
|
| - builders = options.builders or buildbot.builders.keys
|
| - counts = {}
|
| - since = time.time() - options.over
|
| - for builder in builders:
|
| - builder = buildbot.builders[builder]
|
| - counts[builder.name] = 0
|
| - if not options.quiet:
|
| - print builder.name
|
| - for build in builder.builds.iterall():
|
| - try:
|
| - start_time = build.start_time
|
| - except urllib2.HTTPError:
|
| - # The build was probably trimmed.
|
| - print >> sys.stderr, (
|
| - 'Failed to fetch build %s/%d' % (builder.name, build.number))
|
| - continue
|
| - if start_time >= since:
|
| - counts[builder.name] += 1
|
| - else:
|
| - break
|
| - if not options.quiet:
|
| - print '.. %d' % counts[builder.name]
|
| -
|
| - align_name = max(len(b) for b in counts)
|
| - align_number = max(len(str(c)) for c in counts.itervalues())
|
| - for builder in sorted(counts):
|
| - print '%*s: %*d' % (align_name, builder, align_number, counts[builder])
|
| - print 'Total: %d' % sum(counts.itervalues())
|
| - return 0
|
| -
|
| -
|
| -class OptionParser(optparse.OptionParser):
|
| - def parse_args(self, args=None, values=None):
|
| - """Adds common parsing."""
|
| - options, args = optparse.OptionParser.parse_args(self, args, values)
|
| - levels = (logging.WARNING, logging.INFO, logging.DEBUG)
|
| - logging.basicConfig(level=levels[min(options.verbose, len(levels)-1)])
|
| - return options, args
|
| -
|
| - def format_description(self, _):
|
| - """Removes description formatting."""
|
| - return self.description
|
| -
|
| -
|
| -def gen_parser():
|
| - """Returns an OptionParser instance with default options.
|
| -
|
| - It should be then processed with gen_usage() before being used.
|
| - """
|
| - parser = OptionParser(version=__version__)
|
| - parser.add_option(
|
| - '-v', '--verbose', action='count', default=0,
|
| - help='Use multiple times to increase logging leve')
|
| - parser.add_option(
|
| - '-q', '--quiet', action='store_true',
|
| - help='Reduces the output to be parsed by scripts, independent of -v')
|
| - parser.add_option(
|
| - '--throttle', type='float',
|
| - help='Minimum delay to sleep between requests')
|
| - return parser
|
| -
|
| -
|
| -###############################################################################
|
| -## Generic subcommand handling code
|
| -
|
| -
|
| -def Command(name):
|
| - return getattr(sys.modules[__name__], 'CMD' + name, None)
|
| -
|
| -
|
| -@usage('<command>')
|
| -def CMDhelp(parser, args):
|
| - """Print list of commands or use 'help <command>'."""
|
| - _, args = parser.parse_args(args)
|
| - if len(args) == 1:
|
| - return main(args + ['--help'])
|
| - parser.print_help()
|
| - return 0
|
| -
|
| -
|
| -def gen_usage(parser, command):
|
| - """Modifies an OptionParser object with the command's documentation.
|
| -
|
| - The documentation is taken from the function's docstring.
|
| - """
|
| - obj = Command(command)
|
| - more = getattr(obj, 'func_usage_more')
|
| - # OptParser.description prefer nicely non-formatted strings.
|
| - parser.description = obj.__doc__ + '\n'
|
| - parser.set_usage('usage: %%prog %s %s' % (command, more))
|
| -
|
| -
|
| -def main(args=None):
|
| - # Do it late so all commands are listed.
|
| - # pylint: disable=E1101
|
| - CMDhelp.__doc__ += '\n\nCommands are:\n' + '\n'.join(
|
| - ' %-12s %s' % (fn[3:], Command(fn[3:]).__doc__.split('\n', 1)[0])
|
| - for fn in dir(sys.modules[__name__]) if fn.startswith('CMD'))
|
| -
|
| - parser = gen_parser()
|
| - if args is None:
|
| - args = sys.argv[1:]
|
| - if args:
|
| - command = Command(args[0])
|
| - if command:
|
| - # "fix" the usage and the description now that we know the subcommand.
|
| - gen_usage(parser, args[0])
|
| - return command(parser, args[1:])
|
| -
|
| - # Not a known command. Default to help.
|
| - gen_usage(parser, 'help')
|
| - return CMDhelp(parser, args)
|
| -
|
| -
|
| -if __name__ == '__main__':
|
| - sys.exit(main())
|
|
|