-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement the diff command (raw term with colors)
- Loading branch information
NyanKiyoshi
committed
May 20, 2019
1 parent
d231310
commit 8d1821a
Showing
6 changed files
with
275 additions
and
47 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
# coding=utf-8 | ||
from collections import namedtuple | ||
from pytest_django_queries.entry import Entry | ||
from pytest_django_queries.filters import format_underscore_name_to_human | ||
|
||
_ROW_FIELD = namedtuple('_RowField', ('comp_field', 'align_char', 'length_field')) | ||
_ROW_FIELDS = ( | ||
_ROW_FIELD('test_name', '<', 'test_name'), | ||
_ROW_FIELD('left_count', '>', 'query_count'), | ||
_ROW_FIELD('right_count', '>', 'query_count'), | ||
) | ||
_ROW_PREFIX = ' ' | ||
_NA_CHAR = '-' | ||
|
||
|
||
def entry_row(entry_comp, lengths): | ||
cols = [] | ||
|
||
for field, align, length_key in _ROW_FIELDS: | ||
fmt = '{cmp.%s: %s{lengths[%s]}}' % (field, align, length_key) | ||
cols.append(fmt.format(cmp=entry_comp, lengths=lengths)) | ||
|
||
return '%(diff_char)s %(results)s' % ({ | ||
'diff_char': entry_comp.diff, | ||
'results': '\t'.join(cols)}) | ||
|
||
|
||
def get_header_row(lengths): | ||
sep_row = [] | ||
head_row = [] | ||
|
||
for field, _, length_key in _ROW_FIELDS: | ||
length = lengths[length_key] | ||
sep_row.append('%s' % ('-' * length)) | ||
head_row.append('{field: <{length}}'.format( | ||
field=field.replace('_', ' '), length=length)) | ||
|
||
return '%(prefix)s%(head)s\n%(prefix)s%(sep)s' % ({ | ||
'prefix': _ROW_PREFIX, | ||
'head': '\t'.join(head_row), | ||
'sep': '\t'.join(sep_row)}) | ||
|
||
|
||
class DiffChars(object): | ||
NEGATIVE = '-' | ||
NEUTRAL = ' ' | ||
POSITIVE = '+' | ||
|
||
@classmethod | ||
def convert(cls, diff): | ||
if diff < 0: | ||
return DiffChars.POSITIVE | ||
if diff > 0: | ||
return DiffChars.NEGATIVE | ||
return DiffChars.NEUTRAL | ||
|
||
|
||
class SingleEntryComparison(object): | ||
__slots__ = ["left", "right", "diff"] | ||
|
||
def __init__(self, left=None, right=None): | ||
""" | ||
:param left: Previous version. | ||
:type left: Entry | ||
:param right: Newest version. | ||
:type right: Entry | ||
""" | ||
|
||
self.left = left | ||
self.right = right | ||
self.diff = None | ||
|
||
def _diff_from_newest(self): | ||
""" | ||
Returns the query count difference from the previous version. | ||
If there is no older version, we assume it's an "improvement" (positive output) | ||
If there is no new version, we assume it's not an improvement (negative output) | ||
""" | ||
if self.left is None: | ||
return DiffChars.POSITIVE | ||
if self.right is None: | ||
return DiffChars.NEGATIVE | ||
return DiffChars.convert(self.right.query_count - self.left.query_count) | ||
|
||
@property | ||
def test(self): | ||
return self.left or self.right | ||
|
||
@property | ||
def test_name(self): | ||
return format_underscore_name_to_human(self.test.test_name) | ||
|
||
@property | ||
def left_count(self): | ||
return str(self.left.query_count) if self.left else _NA_CHAR | ||
|
||
@property | ||
def right_count(self): | ||
return str(self.right.query_count) if self.right else _NA_CHAR | ||
|
||
def to_string(self, lengths): | ||
if self.diff is None: | ||
self.diff = self._diff_from_newest() | ||
|
||
return entry_row(self, lengths=lengths) | ||
|
||
|
||
class DiffGenerator(object): | ||
def __init__(self, entries_left, entries_right): | ||
""" | ||
Generates the diffs from two files. | ||
:param entries_left: | ||
:type entries_left: List[Entry] | ||
:param entries_right: | ||
:type entries_right: List[Entry] | ||
""" | ||
|
||
self.entries_left = entries_left | ||
self.entries_right = entries_right | ||
|
||
self._mapping = {} | ||
self._generate_mapping() | ||
self.longest_props = self._get_longest_per_prop({'query_count', 'test_name'}) | ||
self.header_rows = get_header_row(lengths=self.longest_props) | ||
|
||
def _get_longest_per_prop(self, props): | ||
""" | ||
:param props: | ||
:type props: set | ||
:return: | ||
""" | ||
|
||
longest = {prop: 0 for prop in props} | ||
entries = ( | ||
self.entries_left + self.entries_right + [field for field, _, _ in _ROW_FIELDS] | ||
) | ||
|
||
for entry in entries: | ||
for prop in props: | ||
if isinstance(entry, Entry): | ||
current_length = len(str(getattr(entry, prop, None))) | ||
else: | ||
current_length = len(entry) | ||
if current_length > longest[prop]: | ||
longest[prop] = current_length | ||
|
||
return longest | ||
|
||
def _map_side(self, entries, side_name): | ||
for entry in entries: | ||
module_map = self._mapping.setdefault(entry.module_name, {}) | ||
|
||
if entry.test_name not in module_map: | ||
module_map[entry.test_name] = SingleEntryComparison() | ||
|
||
setattr(module_map[entry.test_name], side_name, entry) | ||
|
||
def _generate_mapping(self): | ||
self._map_side(self.entries_left, 'left') | ||
self._map_side(self.entries_right, 'right') | ||
|
||
def iter_module(self, module_entries): | ||
yield self.header_rows | ||
for test_comparison in module_entries.values(): # type: SingleEntryComparison | ||
yield test_comparison.to_string(lengths=self.longest_props) | ||
|
||
def __iter__(self): | ||
for module_name, module_entries in self._mapping.items(): | ||
yield ( | ||
format_underscore_name_to_human(module_name), | ||
self.iter_module(module_entries)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
from pytest_django_queries.utils import assert_type, raise_error | ||
|
||
|
||
class Entry(object): | ||
BASE_FIELDS = [ | ||
('test_name', 'Test Name') | ||
] | ||
REQUIRED_FIELDS = [ | ||
('query-count', 'Queries'), | ||
] | ||
FIELDS = BASE_FIELDS + REQUIRED_FIELDS | ||
|
||
def __init__(self, test_name, module_name, data): | ||
""" | ||
:param data: The test entry's data. | ||
:type data: dict | ||
""" | ||
|
||
assert_type(data, dict) | ||
|
||
self._raw_data = data | ||
self.test_name = test_name | ||
self.module_name = module_name | ||
|
||
for field, _ in self.REQUIRED_FIELDS: | ||
setattr(self, field, self._get_required_key(field)) | ||
|
||
def __getitem__(self, item): | ||
return getattr(self, item) | ||
|
||
@property | ||
def query_count(self): | ||
return self['query-count'] | ||
|
||
def _get_required_key(self, key): | ||
if key in self._raw_data: | ||
return self._raw_data.get(key) | ||
raise_error('Got invalid data. It is missing a required key: %s' % key) | ||
|
||
|
||
def iter_entries(entries): | ||
for module_name, module_data in sorted(entries.items()): | ||
assert_type(module_data, dict) | ||
|
||
yield module_name, ( | ||
Entry(test_name, module_name, test_data) | ||
for test_name, test_data in sorted(module_data.items())) | ||
|
||
|
||
def flatten_entries(file_content): | ||
entries = [] | ||
for _, data in iter_entries(file_content): | ||
entries += list(data) | ||
return entries |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
def format_underscore_name_to_human(name): | ||
if name.startswith('test'): | ||
_, name = name.split('test', 1) | ||
return name.replace('_', ' ').strip() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,3 +6,4 @@ django ; python_version >= '3.0' | |
Click | ||
beautifultable==0.7.0 | ||
jinja2 | ||
colorama |