Skip to content

Commit

Permalink
Merge pull request #356 from melexis/natsort
Browse files Browse the repository at this point in the history
Support natural sorting on attributes
  • Loading branch information
Letme authored Oct 6, 2023
2 parents d95f84c + 80a22e4 commit 7706ed2
Show file tree
Hide file tree
Showing 6 changed files with 47 additions and 20 deletions.
5 changes: 4 additions & 1 deletion doc/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Example of attribute stringification:
Valid relationships
-------------------

Python variable *traceability_relationsips* can be defined in order to override the
Python variable *traceability_relationships* can be defined in order to override the
default configuration of the traceability plugin.
It is a *dictionary* of relationship pairs: the *key* is the name of the forward relationship, while the *value* holds
the name of the corresponding reverse relationship. Both can only be lowercase.
Expand Down Expand Up @@ -416,6 +416,9 @@ The plugin itself holds a default config that can be used for any traceability d
'assignee': 'Assignee',
'effort': 'Effort estimation',
}
traceability_attributes_sort = {
'effort': natsort.natsorted,
}
traceability_relationships = {
'fulfills': 'fulfilled_by',
'depends_on': 'impacts_on',
Expand Down
3 changes: 2 additions & 1 deletion doc/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -326,7 +326,8 @@ to hide the item IDs, set the *onlycaptions* flag instead.

By default, items are sorted naturally based on their name. With the *sort* argument it is possible to sort on one
or more attribute values alphabetically. When providing multiple attributes to sort on, the attribute keys are
space-separated. With the *reverse* argument, the sorting is reversed.
space-separated. With the *reverse* argument, the sorting is reversed. Instead of the default sort order (alphabetical),
you can configure a custom sort order per attribute, (see :ref:`traceability_default_config`).

By default, the attribute names are listed the header row and every item takes up a row. Depending on the number of
items and attributes it could be better to transpose the generated matrix (swap columns for row) by providing the
Expand Down
19 changes: 15 additions & 4 deletions mlx/traceability.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,17 @@
See readme for more details.
"""
from collections import namedtuple
from re import fullmatch, match
from os import path
from re import fullmatch, match

import natsort
from docutils import nodes
from docutils.parsers.rst import directives
from requests import Session
from sphinx import version_info as sphinx_version
from sphinx.errors import NoUri
from sphinx.roles import XRefRole
from sphinx.util.nodes import make_refnode
from sphinx.errors import NoUri
from docutils import nodes
from docutils.parsers.rst import directives

from mlx.__traceability_version__ import version
from mlx.traceable_attribute import TraceableAttribute
Expand Down Expand Up @@ -315,6 +316,7 @@ def initialize_environment(app):
# generates placeholders when parsing the reverse relationships, the
# database of items needs to be empty on every re-build.
env.traceability_collection = TraceableCollection()
env.traceability_collection.attributes_sort = app.config.traceability_attributes_sort
env.traceability_ref_nodes = {}

all_relationships = set(app.config.traceability_relationships).union(app.config.traceability_relationships.values())
Expand Down Expand Up @@ -533,6 +535,15 @@ def setup(app):
'env',
)

# Configuration for custom sort orders for sorting on attribute in item-attributes-matrix (default is alphabetical)
app.add_config_value(
'traceability_attributes_sort',
{
'effort': natsort.natsorted,
},
'env',
)

# Create default relationships dictionary. Can be customized in conf.py
app.add_config_value(
'traceability_relationships',
Expand Down
14 changes: 11 additions & 3 deletions mlx/traceable_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ def __init__(self):
self.items = {}
self.relations_sorted = {}
self._intermediate_nodes = []
self.attributes_sort = {}

def add_relation_pair(self, forward, reverse=NO_RELATION_STR):
'''
Expand Down Expand Up @@ -298,7 +299,8 @@ def get_items(self, regex, attributes=None, sortattributes=None, reverse=False,
Args:
regex (str/re.Pattern): Regex pattern or object to match the items in this collection against
attributes (dict): Dictionary with attribute-regex pairs to match the items in this collection against
sortattributes (list): List of attributes on which to alphabetically sort the items
sortattributes (list): List of attributes on which to sort the items alphabetically, or using a custom
sort order if at least one attribute is in ``attributes_sort``
reverse (bool): True for reverse sorting
sort (bool): When sortattributes is falsy: True to enable natural sorting, False to disable sorting
Expand All @@ -313,8 +315,14 @@ def get_items(self, regex, attributes=None, sortattributes=None, reverse=False,
if item.is_match(regex) and (not attributes or item.attributes_match(attributes)):
matches.append(itemid)
if sortattributes:
return sorted(matches, key=lambda itemid: self.get_item(itemid).get_attributes(sortattributes),
reverse=reverse)
for attr in sortattributes:
if attr in self.attributes_sort:
sorted_func = self.attributes_sort[attr]
break
else:
sorted_func = sorted
return sorted_func(matches, key=lambda itemid: self.get_item(itemid).get_attributes(sortattributes),
reverse=reverse)
elif sort:
return natsorted(matches, reverse=reverse)
return matches
Expand Down
15 changes: 9 additions & 6 deletions tests/test_traceable_collection.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
from unittest import TestCase
from unittest.mock import patch, mock_open

from natsort import natsorted

import mlx.traceable_item as item
import mlx.traceable_attribute as attribute
import mlx.traceability_exception as exception
Expand Down Expand Up @@ -322,12 +324,13 @@ def test_get_items_sortattributes(self):
dut.TraceableItem.define_attribute(attr)
item1.add_attribute(self.attribute_key, '0x0029')
item2.add_attribute(self.attribute_key, '0x003A')
# Alphabetical sorting: 0x0029 before 0x003A
self.assertEqual(item1.get_id(), coll.get_items('', sortattributes=[self.attribute_key])[0])
self.assertEqual(item2.get_id(), coll.get_items('', sortattributes=[self.attribute_key])[1])
# Natural sorting: z2 before z11
self.assertEqual(item2.get_id(), coll.get_items('')[0])
self.assertEqual(item1.get_id(), coll.get_items('')[1])
# Alphabetical sorting on attributes: 0x0029 before 0x003A
self.assertEqual([name1, name2], coll.get_items('', sortattributes=[self.attribute_key]))
# Natural sorting: 0x003A before 0x0029
coll.attributes_sort.update({self.attribute_key: natsorted})
self.assertEqual([name2, name1], coll.get_items('', sortattributes=[self.attribute_key]))
# Natural sorting on items: z2 before z11
self.assertEqual([name2, name1], coll.get_items(''))

def test_related(self):
coll = dut.TraceableCollection()
Expand Down
11 changes: 6 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,8 @@ deps=
pytest
pytest-cov
coverage
reportlab
natsort
matplotlib
sphinx-testing >= 0.5.2
sphinx_selective_exclude>=1.0.3
sphinx_rtd_theme<2.0.0
parameterized
Expand All @@ -58,7 +56,10 @@ commands =
python setup.py sdist
twine check dist/*
check-manifest {toxinidir} -u
flake8 mlx tests setup.py --per-file-ignores mlx/directives/item_pie_chart_directive.py:E402
flake8 mlx tests setup.py --per-file-ignores '\
mlx/directives/item_pie_chart_directive.py:E402 \
mlx/__traceability_version__.py:F401 \
'

[testenv:sphinx2.4.5]
deps=
Expand All @@ -67,7 +68,7 @@ deps=
markupsafe == 1.1.0
sphinx == 2.4.5 # rq.filter: ==2.4.5
sphinxcontrib-plantuml
mlx.warnings >= 1.3.0
mlx.warnings >= 4.3.2
whitelist_externals =
make
tee
Expand All @@ -81,7 +82,7 @@ deps=
{[testenv]deps}
sphinx<7.0
sphinxcontrib-plantuml
mlx.warnings >= 1.3.0
mlx.warnings >= 4.3.2
whitelist_externals =
make
tee
Expand Down

0 comments on commit 7706ed2

Please sign in to comment.