diff --git a/docs/examples.rst b/docs/examples.rst index c5977bd..faaca4c 100644 --- a/docs/examples.rst +++ b/docs/examples.rst @@ -2,6 +2,9 @@ Examples of JSON input files ============================================================================== +Netbotz +-------------------- + NetBotz II Example:: { @@ -42,6 +45,10 @@ NetBotz II Example:: }] } + +Genbotz Generalization of Netbotz +---------------------------------- + GenBotz Example with Organizers (DeviceClasses):: @@ -101,6 +108,9 @@ GenBotz Example with Organizers (DeviceClasses):: }] } +NetScalar +-------------------- + NetScaler Example:: { diff --git a/docs/index.rst b/docs/index.rst index 7ae0156..38032c1 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -15,6 +15,7 @@ This is the documentation for the ZenPack Generator tool. usage jsonformat examples + use_cases Indices and tables diff --git a/docs/jsonformat.rst b/docs/jsonformat.rst index 9f27133..5541535 100644 --- a/docs/jsonformat.rst +++ b/docs/jsonformat.rst @@ -210,12 +210,12 @@ Json Options for a ComponentType:: .. _deviceType: -deviceType [] +deviceType {} ------------- Json Options for a DeviceType:: - "deviceType": [{ + "deviceType": { "name": "DeviceName", "names": "DeviceNames", "klasses": ['DeviceComponent'], @@ -228,7 +228,7 @@ Json Options for a DeviceType:: "componentTypes": [], "impacts": [], "impactedBy": [] - }] + } * name: The Name of the Device Component, Used to define the Module and Class of a Component. [required] * names: The Plural Form of the Device Component Name. [optional] @@ -271,6 +271,10 @@ Json Options for a Property:: "mode": 'w', "value": 10, "detailDisplay": True, + readonly=True, + detail_group=None, + detail_order=None, + addl_detail_args=None, "gridDisplay": True, "sortable": True, "width": 10, @@ -291,6 +295,19 @@ Json Options for a Property:: * True * False * Defaults to True +* readonly: Allow the property to be modified from the detail pane. + * Valid Inputs: + * True + * False + * Defaults to True +* detail_group: Group the property in the details pane. [Currently unsupported in Zenoss] + * Valid Inputs: + * string +* detail_order: Order the property in the details pane. [Currently unsupported in Zenoss] + * Valid Inputs: + * positive or negative integer +* addl_detail_args: A free form string to pass the details pane. + * A string * gridDisplay: Display the property in the Grid Component Panel section. * Valid Inputs: * True diff --git a/docs/use_cases.rst b/docs/use_cases.rst new file mode 100644 index 0000000..982255a --- /dev/null +++ b/docs/use_cases.rst @@ -0,0 +1,64 @@ +============================================================================== +Specific Examples of ZPG Use Cases +============================================================================== + This document will provide specific use cases and fragments that will +hopefully guide the users onto a higher plane of ZPG conciousness. + +Devices without a Root +============================================================================== + +OracleDB:: + +In this example OracleDB is a device that inherits its /Device base from the +parent server, be it Linux, AIX, Solaris, or some "other" operating system. +This means that it needs to be able to patch itself underneath the device tree +of that server target type and not have a stand-alone device root. + +Thus inside of ZenPacks.zenoss.OracleDB/ZenPacks/zenoss/OracleDB/__init__.py +we should see: + +__init__.py:: + + # Define new device relations. + NEW_DEVICE_RELATIONS = ( + ('instance', 'OracleInstance'), + ('storage', 'OracleStorage'), + ('rac', 'OracleRAC'), + ('schema', 'OracleSchema'), + ) + +In order to achieve this you must remove the entire "deviceType" structure out of +the ZPG json file; + +OracleDB.json Example:: + + { + "id": "ZenPacks.zenoss.OracleDB", + "author": "Zenoss", + "version": "0.0.1", + "compat_zenoss_vers": ">=4.2", + + "deviceClasses": [{ + "path": "", + + "componentTypes": [ + { + "name": "Instance", + "meta_type": "OracleInstance", + "properties": [ + {"name": "sid"}, + {"name": "name_label"}, + {"name": "connectionString", "Type": "string"}, + {"name": "hostname"}, + {"name": "oracleVersion"}, + {"name": "databaseName"}, + {"name": "databaseDBID"}, + {"name": "databasePlatformName"}, + {"name": "RACnetworkPeers"}, + {"name": "instanceRole"}, + {"name": "publicIP"}, + {"name": "interconnectIP"} + ] + },... + }].... + } diff --git a/docs/zenpackgenerator.rst b/docs/zenpackgenerator.rst index e2e3b74..f8dac75 100644 --- a/docs/zenpackgenerator.rst +++ b/docs/zenpackgenerator.rst @@ -48,6 +48,7 @@ Prerequisites * Linux or Mac * Python 2.7 +* Python setuptools This tool does NOT require a zenoss server. diff --git a/zpg/Component.py b/zpg/Component.py index acf499f..ddc9fad 100644 --- a/zpg/Component.py +++ b/zpg/Component.py @@ -8,8 +8,11 @@ # # +import logging + import inflect +from .colors import error, warn, debug, info, green, red, yellow from ._defaults import Defaults from ._zenoss_utils import KlassExpand, zpDir from .Property import Property @@ -41,6 +44,8 @@ def __init__(self, componentTypes=None, impacts=None, impactedBy=None, + *args, + **kwargs ): """Args: name: Component Name @@ -65,6 +70,15 @@ def __init__(self, """ super(Component, self).__init__(zenpack) + self.logger = logger = logging.getLogger('ZenPack Generator') + for key in kwargs: + do_not_warn = False + clsname = self.__class__.__name__ + layer = "%s:%s" % (clsname, name) + msg = "WARNING: [%s] unknown keyword ignored in file: '%s'" + margs = (layer, key) + if not do_not_warn: + warn(self.logger, yellow(msg) % margs) self.source_template = 'component.tmpl' self.name = name.split('.')[-1] self.names = names diff --git a/zpg/DeviceClass.py b/zpg/DeviceClass.py index dfcd919..7dbccb9 100644 --- a/zpg/DeviceClass.py +++ b/zpg/DeviceClass.py @@ -7,7 +7,9 @@ # file at the top-level directory of this package. # # +import logging +from .colors import error, warn, debug, info, green, red, yellow from ._zenoss_utils import KlassExpand from .Relationship import Relationship find = Relationship.find @@ -24,14 +26,17 @@ def __init__(self, prefix='/zport/dmd', zPythonClass='Products.ZenModel.Device.Device', componentTypes=None, - deviceType=None): + deviceType=None, + *args, + **kwargs + ): '''Args: path: Destination device class path (the prefix is automatically prepended) ZenPack: ZenPack Class Instance prefix: Destination device class prefix [/zport/dmd] zPythonClass: The zPythonClass this Device Class references. - [Products.ZenModel.Device.Device] + [Products.ZenModel.Device] componentTypes: an array of dictionaries used to create components. deviceType: a dictionary used to create a device component. @@ -42,6 +47,14 @@ def __init__(self, self.id = self.path self.subClasses = {} self.zPythonClass = KlassExpand(self.zenpack, zPythonClass) + self.logger = logger = logging.getLogger('ZenPack Generator') + for key in kwargs: + do_not_warn = False + layer = self.__class__.__name__ + msg = "WARNING: JSON keyword ignored in layer '%s': '%s'" + margs = (layer, key) + if not do_not_warn: + warn(self.logger, yellow(msg) % margs) if deviceType: self.DeviceType(**deviceType) else: diff --git a/zpg/License.py b/zpg/License.py index eb9d5d0..1e550cc 100644 --- a/zpg/License.py +++ b/zpg/License.py @@ -88,16 +88,16 @@ def __init__(self, zenpack, id_): elif id_ in dflt_licenses: self.id = id_ - self.header = load_header('Licenses', self.id) - self.license = load_license('Licenses', self.id) + self.header = load_header(tpath, self.id) + self.license = load_license(tpath, self.id) else: # Default GPlv2 info(log, red('Specified License [%s] not known. Defaulting to GPLv2.' % id_)) self.id = 'GPLv2' - self.header = load_header('Licenses', self.id) - self.license = load_license('Licenses', self.id) + self.header = load_header(tpath, self.id) + self.license = load_license(tpath, self.id) def header(self): return self.header diff --git a/zpg/Organizer.py b/zpg/Organizer.py index 4fd59c1..035aeb0 100644 --- a/zpg/Organizer.py +++ b/zpg/Organizer.py @@ -8,10 +8,12 @@ # # +import logging import sys from lxml import etree +from .colors import error, warn, debug, info, green, red, yellow from .Property import Property from ._zenoss_utils import KlassExpand @@ -23,8 +25,10 @@ class Organizer(object): def __init__(self, zenpack, name, - type_, - properties=None + type_=None, + properties=None, + *args, + **kwargs ): """Args: name: Organizer Name in the form of a Slash separated path. @@ -36,6 +40,27 @@ def __init__(self, self.id = name self.type_ = type_ self.properties = {} + self.logger = logger = logging.getLogger('ZenPack Generator') + for key in kwargs: + do_not_warn = False + layer = self.__class__.__name__ + msg = "WARNING: JSON keyword ignored in layer '%s': '%s'" + margs = (layer, key) + if key == "Type": + msg = "WARNING: JSON keyword deprecated in '%s' layer. "\ + "'%s' is now '%s'." + margs = (layer, key, key.lower()) + self.type_ = kwargs[key] + elif key == "type": + self.type_ = type_ = kwargs[key] + do_not_warn = True + elif key == "Contained": + msg = "WARNING: JSON keyword deprecated in '%s' layer. "\ + "'%s' is now '%s'." + margs = (layer, key, key.lower()) + self.contained = kwargs[key] + if not do_not_warn: + warn(self.logger, yellow(msg) % margs) # Dict loading if properties: for p in properties: diff --git a/zpg/Property.py b/zpg/Property.py index 58b2699..5245f38 100644 --- a/zpg/Property.py +++ b/zpg/Property.py @@ -8,9 +8,13 @@ # # +import logging +import re + import inflect from lxml import etree -import re + +from .colors import error, warn, debug, info, green, red, yellow plural = inflect.engine().plural @@ -27,7 +31,14 @@ def __init__(self, detailDisplay=True, gridDisplay=True, sortable=True, - panelRenderer=None): + panelRenderer=None, + readonly=True, + detail_group=None, + detail_order=None, + addl_detail_args=None, + *args, + **kwargs + ): """Args: name: Property name value: default value [None] @@ -56,6 +67,27 @@ def __init__(self, self.panelRenderer = panelRenderer self.type_ = type_ if type_ else value self.value = value + self.readonly = readonly + self.detail_group = detail_group + self.detail_order = detail_order + self.addl_detail_args = addl_detail_args + self.logger = logger = logging.getLogger('ZenPack Generator') + for key in kwargs: + do_not_warn = False + clsname = self.__class__.__name__ + layer = "%s:%s" % (clsname, self.name) + msg = "WARNING: [%s] unknown keyword ignored in file: '%s'" + margs = (layer, key) + if key == "Type": + msg = "WARNING: [%s] keyword deprecated: "\ + "'%s' is now '%s'." + margs = (layer, key, key.lower()) + self.type_ = type_ = kwargs[key] + elif key == "type": + self.type_ = type_ = kwargs[key] + do_not_warn = True + if not do_not_warn: + warn(self.logger, yellow(msg) % margs) def Schema(self): """Given the type return the correct Schema.""" @@ -68,6 +100,23 @@ def Schema(self): else: return 'TextLine' + @property + def detail_args(self): + """Return additional detail arguements""" + + detail_args = ", readonly=%s" % self.readonly + + if self.detail_group: + detail_args = ", group=u'%s'" % self.detail_group + + if self.detail_order: + detail_args = ", order=%s" % self.detail_order + + if self.addl_detail_args: + detail_args = ", %s" % self.addl_detail_args + + return detail_args + @property def type_(self): """Return the type""" diff --git a/zpg/Relationship.py b/zpg/Relationship.py index d4ee911..0695fac 100644 --- a/zpg/Relationship.py +++ b/zpg/Relationship.py @@ -8,28 +8,72 @@ # # +import logging + +from .colors import error, warn, debug, info, green, red, yellow -class Relationship(object): +class Relationship(object): '''ZenPack Relationship Management''' + valid_relationship_types = ['1-M', 'M-M', '1-1'] relationships = {} def __init__(self, ZenPack, componentA, componentB, - type_='1-M', - contained=True): + type_=None, + contained=True, + *args, + **kwargs + ): """Args: ZenPack: A ZenPack Class instance componentA: Parent component string id componentB: Child component string id - type_: Relationship type_. Valid inputs [1-1,1-M,M-M] + type_: Relationship type_. Valid inputs [1-1, 1-M, M-M] contained: ComponentA contains ComponentB True or False """ self.ZenPack = ZenPack - from Component import Component + from .Component import Component + + self.logger = logger = logging.getLogger('ZenPack Generator') + name = "-".join([componentA, componentB]) + layer = "%s:%s" % (self.__class__.__name__, name) + if type_ not in self.valid_relationship_types: + msg = "WARNING: [%s] unknown type: '%s'. Defaulted to '%s'. " + layer = "%s:%s" % (self.__class__.__name__, name) + margs = (layer, type, '1-M') + if type_ == 'M-1': + a_b = (componentA, componentB) + msg += "Reversed '%s' and '%s'." % a_b + swap = componentB + componentB = componentA + componentA = swap + if type_ is not None: + warn(self.logger, yellow(msg) % margs) + type_ = '1-M' + for key in kwargs: + do_not_warn = False + msg = "WARNING: [%s] unknown keyword ignored in file: '%s'" + margs = (layer, key) + if key == "Type": + msg = "WARNING: [%s] keyword deprecated: "\ + "'%s' is now '%s'." + margs = (layer, key, key.lower()) + self.type_ = kwargs[key] + elif key == "type": + self.type_ = type_ = kwargs[key] + do_not_warn = True + elif key == "Contained": + msg = "WARNING: [%s] keyword deprecated: "\ + "'%s' is now '%s'." + margs = (layer, key, key.lower()) + self.contained = kwargs[key] + if not do_not_warn: + warn(self.logger, yellow(msg) % margs) + lookup = Component.lookup self.components = lookup( ZenPack, componentA), lookup(ZenPack, componentB) diff --git a/zpg/Templates/component.tmpl b/zpg/Templates/component.tmpl index 7328f73..f549d47 100644 --- a/zpg/Templates/component.tmpl +++ b/zpg/Templates/component.tmpl @@ -53,12 +53,6 @@ class ${shortklass}(#echo ', '.join($klassNames)#): },), },) -## # Query for events by id instead of name. -## event_key = "ComponentId" -## -## def getIconPath(self): -## return '/++resource++cloudstack/img/cloudstack.png' -## def device(self): ''' Return device under which this component/device is contained. @@ -76,6 +70,24 @@ class ${shortklass}(#echo ', '.join($klassNames)#): 'Unable to determine parent at %s (%s) ' 'while getting device for %s' % ( obj, exc, self)) + +#if not $device + def manage_deleteComponent(self, REQUEST=None): + """ + Delete Component + """ + try: + # Default to using built-in method in Zenoss >= 4.2.4. + return super(${shortklass}, self).manage_deleteComponent(REQUEST) + except AttributeError: + # Fall back to copying the Zenoss 4.2.4 implementation. + url = None + if REQUEST is not None: + url = self.device().absolute_url() + self.getPrimaryParent()._delObject(self.id) + if REQUEST is not None: + REQUEST['RESPONSE'].redirect(url) +#end if #for $key in $updateComponents #if $key == '1' #for $c in $updateComponents['1'] @@ -142,7 +154,7 @@ class I${shortklass}Info(I${type_}Info): #if $properties.values() #for $prop in $properties.values() - $prop.id = schema.${prop.Schema}(title=_t(u'$prop.names')) + $prop.id = schema.${prop.Schema}(title=_t(u'$prop.names')$prop.detail_args) #end for #end if #end if diff --git a/zpg/Templates/configure.zcml.tmpl b/zpg/Templates/configure.zcml.tmpl index 816a2c5..6954e59 100644 --- a/zpg/Templates/configure.zcml.tmpl +++ b/zpg/Templates/configure.zcml.tmpl @@ -8,12 +8,14 @@ #for $c in sorted($components.values()) +#if $c.displayInfo +#end if #end for #end if diff --git a/zpg/Templates/utils.tmpl b/zpg/Templates/utils.tmpl index 251c645..df931dd 100644 --- a/zpg/Templates/utils.tmpl +++ b/zpg/Templates/utils.tmpl @@ -10,6 +10,9 @@ from Products.Zuul.interfaces import ICatalogTool from zope.event import notify from Products.Zuul.catalog.events import IndexingEvent +from Products.ZenUtils.guid.interfaces import IGlobalIdentifier +import functools +import importlib import logging LOG = logging.getLogger('${zenpack.id}.utils') @@ -157,3 +160,43 @@ def string_to_int(value): i = value return i + +def guid(obj): + ''' + Return GUID for obj. + ''' + + return IGlobalIdentifier(obj).getGUID() + +def require_zenpack(zenpack_name, default=None): + ''' + Decorator with mandatory zenpack_name argument. + + If zenpack_name can't be imported, the decorated function or method + will return default. Otherwise it will execute and return as + written. + + Usage looks like the following: + + @require_zenpack('ZenPacks.zenoss.Impact') + @require_zenpack('ZenPacks.zenoss.vCloud') + def dothatthingyoudo(args): + return "OK" + + @require_zenpack('ZenPacks.zenoss.Impact', []) + def returnalistofthings(args): + return [1, 2, 3] + ''' + def wrap(f): + @functools.wraps(f) + def wrapper(*args, **kwargs): + try: + importlib.import_module(zenpack_name) + except ImportError: + return + + return f(*args, **kwargs) + + return wrapper + + return wrap diff --git a/zpg/ZenPack.py b/zpg/ZenPack.py index f910b43..fe16c79 100644 --- a/zpg/ZenPack.py +++ b/zpg/ZenPack.py @@ -8,12 +8,15 @@ # ############################################################################## +import logging import os from git import Repo from .memoize import memoize +from .colors import error, warn, debug, info, green, red, yellow +from ._defaults import Defaults from ._zenoss_utils import prepId from .Component import Component from .ComponentJS import ComponentJS @@ -31,7 +34,6 @@ from .ImpactPy import ImpactPy from .ImpactZcml import ImpactZcml -from ._defaults import Defaults defaults = Defaults() @@ -57,6 +59,8 @@ def __init__(self, deviceClasses=None, relationships=None, opts=None, + *args, + **kwargs ): self.id = id @@ -73,6 +77,14 @@ def __init__(self, self.version = version self.license = License(self, license) self.prepname = prepId(id).replace('.', '_') + self.logger = logger = logging.getLogger('ZenPack Generator') + for key in kwargs: + do_not_warn = False + layer = self.__class__.__name__ + msg = "WARNING: JSON keyword ignored in layer '%s': '%s'" + margs = (layer, key) + if not do_not_warn: + warn(self.logger, yellow(msg) % margs) if install_requires: if isinstance(install_requires, basestring): self.install_requires = [install_requires] diff --git a/zpg/ZenPackUI.py b/zpg/ZenPackUI.py index 1a546e1..6a176db 100644 --- a/zpg/ZenPackUI.py +++ b/zpg/ZenPackUI.py @@ -21,7 +21,7 @@ def __init__(self, ZenPack): self.zenpack = ZenPack self.source_template = 'zenpackjs.tmpl' self.dest_file = "%s/resources/js/%s.js" % ( - zpDir(self.zenpack), self.zenpack.id) + zpDir(self.zenpack), self.zenpack.prepname) def write(self): # Update the components just before we need them. diff --git a/zpg/_defaults.py b/zpg/_defaults.py index fd5bf70..6b48e25 100644 --- a/zpg/_defaults.py +++ b/zpg/_defaults.py @@ -26,7 +26,7 @@ 'author': 'ZenossLabs ', 'author_email': 'labs@zenoss.com', 'description': 'A tool to assist building zenpacks.', - 'version': '1.0.11', + 'version': '1.0.12', 'license': 'GPLv2', 'component_classes': [ 'Products.ZenModel.DeviceComponent.DeviceComponent', diff --git a/zpg/api.py b/zpg/api.py index efde59d..96d9dcc 100644 --- a/zpg/api.py +++ b/zpg/api.py @@ -45,7 +45,7 @@ def __init__(self): group1 = self.add_argument_group('standard arguments') group2 = self.add_argument_group('special arguments') group1.add_argument("input", type=str, # FileType('rt'), - default=sys.stdin, nargs="?", + default='', nargs="?", help="input file") group1.add_argument("dest", type=str, nargs="?", default=prefix, @@ -65,19 +65,52 @@ def __init__(self): group1.add_argument("-v", "--verbose", action="count", dest="verbose", default=0, help="Increase output verbosity") + group1.add_argument("-c", "--clean", action="store_true", + dest="clean", default=False, + help="Cleans destination path before generating") group2.add_argument('-V', "--version", action="store_true", dest="version", default=False, help="Display version of %(prog)s") +def replacer(match): + new_string = matched = match.group(0) + if matched.startswith('/') or matched.startswith("#"): + new_string = '' + return new_string + + +def remove_comments(text): + """Removes the comments from a JSON, YAML or Python file + """ + pattern = r'#.*?$|//.*?$|/\*.*?\*/|\'(?:\\.|[^\\\'])*\'|"(?:\\.|[^\\"])*"' + flags = re.DOTALL | re.MULTILINE + engine = re.compile(pattern, flags) + lines = re.sub(engine, replacer, text).split("\n") + return "\n".join(line for line in lines if line.strip()) + + +def remove_folder(fpath): + removed = [] + if os.path.exists(fpath): + for root, folders, files in os.walk(fpath, topdown=False): + for file in files: + filepath = os.path.join(root, file) + os.remove(filepath) + removed.append(filepath) + for folder in folders: + folderpath = os.path.join(root, folder) + if os.path.exists(folderpath): + os.removedirs(folderpath) + removed.append(folderpath) + + class TemplateJSONDecoder(json.JSONDecoder): def decode(self, json_string): """ json_string is basically string that you give to json.loads method """ - json_string = re.sub('"Contained"', '"contained"', json_string) - json_string = re.sub('"Type"', '"type_"', json_string) - json_string = re.sub('"type"', '"type_"', json_string) + json_string = remove_comments(json_string) default_obj = super(TemplateJSONDecoder, self).decode(json_string) return default_obj @@ -130,11 +163,16 @@ def generate(filename=None): sys.exit(0) if not filename or not os.path.exists(filename): - if not opts.input or not os.path.exists(str(opts.input)): + filename = opts.input + # Make sure filename is an expanded, absolute path + if filename.startswith('~'): + filename = os.path.expanduser(filename) + elif filename.startswith('.'): + filename = os.path.abspath(filename) + if not filename or not os.path.exists(filename): err_msg = "Required input file missing. Exiting...\n" error(logger, err_msg) sys.exit(1) - filename = opts.input if not os.path.exists(filename): err_msg = "Input file does not exist! %s" % filename @@ -160,8 +198,12 @@ def generate(filename=None): debug(logger, ' Loaded.') debug(logger, 'Populating ZenPack...') zp_json = ZenPack(**jsi) + fpath = os.path.join(opts.dest, zp_json.id) debug(logger, 'Done ZenPack populating.') debug(logger, 'Writing output...') + if opts.clean: + info(logger, 'Cleaning: %s' % fpath) + remove_folder(fpath) zp_json.write() debug(logger, 'Done writing.') diff --git a/zpg/tests/test_Configure.py b/zpg/tests/test_Configure.py index 6588283..d79a604 100644 --- a/zpg/tests/test_Configure.py +++ b/zpg/tests/test_Configure.py @@ -58,7 +58,8 @@ def test_findCustomPathsTrue(self): dc.addComponentType('Blade') dc.addComponentType('Fan') - Relationship(self.zp, 'Enclosure', 'Fan', type_='1-M', contained=False) + Relationship(self.zp, 'Enclosure', + 'Fan', type_='1-M', contained=False) Relationship(self.zp, 'Enclosure', 'Blade', type_='1-M', contained=False) self.assertTrue(self.zp.configure_zcml.customPathReporters()) diff --git a/zpg/tests/test_pep8_conformance.py b/zpg/tests/test_pep8_conformance.py index 65b5233..83aaa2b 100644 --- a/zpg/tests/test_pep8_conformance.py +++ b/zpg/tests/test_pep8_conformance.py @@ -20,7 +20,7 @@ class WriteTemplatesBase(unittest.TestCase): def test_pep8_conformance(self): """Test that we conform to PEP8.""" - pep8style = pep8.StyleGuide(quiet=True) + pep8style = pep8.StyleGuide(show_source=True) # assume this file lives in git_repo/zpg/tests current_file = os.path.abspath(__file__) current_folder = os.path.dirname(current_file) @@ -32,7 +32,7 @@ def test_pep8_conformance(self): for f in files if f[-3:] == ".py" if f[:5] != "test_")) - results = pep8style.check_files(filepaths) + results = pep8style.check_files(sorted(filepaths)) errors = str(results.total_errors) msgs = [ "Found %s code style errors (and warnings)." % errors,