use alphanum sorting for jobs and views

This improves UX by using a alphanum sorting which
is more natural to humans: [a1, a2, a10] instead of
[a1, a10, a2].

Change-Id: Ice9c3de282f08e85c8f8a34a27d1b6c0502ca9cb
Signed-off-by: Sorin Sbarnea <ssbarnea@redhat.com>
This commit is contained in:
Sorin Sbarnea 2017-05-10 12:53:58 +01:00
parent 2c60aff806
commit 6a9d5736d6
3 changed files with 114 additions and 5 deletions

109
jenkins_jobs/alphanum.py Executable file
View File

@ -0,0 +1,109 @@
#!/usr/bin/env python
# Copyright (C) 2012 OpenStack, LLC.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Sorts using alphanum algorithm which can be explained as:
Normal sort: ['a', 'a1', 'a10', 'a2']
Alphanum sort: ['a', 'a1', 'a2', 'a10']
It can sortof many kinds of objects, using name attribe if possible,
otherwise it will try to use str().
How to use:
from alphanum import AlphanumSort
sorted( foo, key=AlphanumSort)
"""
import re
re_chunk = re.compile("([\D]+|[\d]+)")
re_letters = re.compile("\D+")
re_numbers = re.compile("\d+")
def getchunk(item):
itemchunk = re_chunk.match(item)
# Subtract the matched portion from the original string
# if there was a match, otherwise set it to ""
item = (item[itemchunk.end():] if itemchunk else "")
# Don't return the match object, just the text
itemchunk = (itemchunk.group() if itemchunk else "")
return (itemchunk, item)
def cmp(a, b):
return (a > b) - (a < b)
def alphanum(a, b):
a = a.name if hasattr(a, 'name') else str(a)
b = b.name if hasattr(b, 'name') else str(b)
n = 0
while (n == 0):
# Get a chunk and the original string with the chunk subtracted
(ac, a) = getchunk(a)
(bc, b) = getchunk(b)
# Both items contain only letters
if (re_letters.match(ac) and re_letters.match(bc)):
n = cmp(ac, bc)
else:
# Both items contain only numbers
if (re_numbers.match(ac) and re_numbers.match(bc)):
n = cmp(int(ac), int(bc))
# item has letters and one item has numbers, or one item is empty
else:
n = cmp(ac, bc)
# Prevent deadlocks
if (n == 0):
n = 1
return n
class AlphanumSort(object):
def __init__(self, obj, *args):
self.obj = obj
def __lt__(self, other):
return alphanum(self.obj, other.obj) < 0
def __gt__(self, other):
return alphanum(self.obj, other.obj) > 0
def __eq__(self, other):
return alphanum(self.obj, other.obj) == 0
def __le__(self, other):
return alphanum(self.obj, other.obj) <= 0
def __ge__(self, other):
return alphanum(self.obj, other.obj) >= 0
def __ne__(self, other):
return alphanum(self.obj, other.obj) != 0
if __name__ == "__main__":
mylist = ['a2', 'a1', 'a10', 'a']
assert sorted(mylist, key=AlphanumSort) == ['a', 'a1', 'a2', 'a10']

View File

@ -19,7 +19,6 @@ import errno
import hashlib import hashlib
import io import io
import logging import logging
import operator
import os import os
from pprint import pformat from pprint import pformat
import re import re
@ -28,6 +27,7 @@ import xml.etree.ElementTree as XML
import jenkins import jenkins
from jenkins_jobs.alphanum import AlphanumSort
from jenkins_jobs.cache import JobCache from jenkins_jobs.cache import JobCache
from jenkins_jobs.constants import MAGIC_MANAGE_STRING from jenkins_jobs.constants import MAGIC_MANAGE_STRING
from jenkins_jobs.parallel import concurrent from jenkins_jobs.parallel import concurrent
@ -199,7 +199,7 @@ class JenkinsManager(object):
orig = time.time() orig = time.time()
logger.info("Number of jobs generated: %d", len(xml_jobs)) logger.info("Number of jobs generated: %d", len(xml_jobs))
xml_jobs.sort(key=operator.attrgetter('name')) xml_jobs.sort(key=AlphanumSort)
if (output and not hasattr(output, 'write') and if (output and not hasattr(output, 'write') and
not os.path.isdir(output)): not os.path.isdir(output)):
@ -360,7 +360,7 @@ class JenkinsManager(object):
orig = time.time() orig = time.time()
logger.info("Number of views generated: %d", len(xml_views)) logger.info("Number of views generated: %d", len(xml_views))
xml_views.sort(key=operator.attrgetter('name')) xml_views.sort(key=AlphanumSort)
if output: if output:
# ensure only wrapped once # ensure only wrapped once

View File

@ -21,7 +21,6 @@ import doctest
import io import io
import json import json
import logging import logging
import operator
import os import os
import re import re
import xml.etree.ElementTree as XML import xml.etree.ElementTree as XML
@ -36,6 +35,7 @@ from yaml import safe_dump
from jenkins_jobs.config import JJBConfig from jenkins_jobs.config import JJBConfig
from jenkins_jobs.errors import InvalidAttributeError from jenkins_jobs.errors import InvalidAttributeError
import jenkins_jobs.local_yaml as yaml import jenkins_jobs.local_yaml as yaml
from jenkins_jobs.alphanum import AlphanumSort
from jenkins_jobs.modules import project_externaljob from jenkins_jobs.modules import project_externaljob
from jenkins_jobs.modules import project_flow from jenkins_jobs.modules import project_flow
from jenkins_jobs.modules import project_matrix from jenkins_jobs.modules import project_matrix
@ -224,7 +224,7 @@ class SingleJobTestCase(BaseScenariosTestCase):
xml_generator = XmlJobGenerator(registry) xml_generator = XmlJobGenerator(registry)
xml_jobs = xml_generator.generateXML(job_data_list) xml_jobs = xml_generator.generateXML(job_data_list)
xml_jobs.sort(key=operator.attrgetter('name')) xml_jobs.sort(key=AlphanumSort)
# Prettify generated XML # Prettify generated XML
pretty_xml = u"\n".join(job.output().decode('utf-8') pretty_xml = u"\n".join(job.output().decode('utf-8')