[py-dev] Advanced assert equal

holger krekel holger at merlinux.eu
Thu Sep 2 11:27:22 CEST 2010


Hi Floris,

sidenote: the original implementation was done by Armin Rigo 
and i am not much more accustomed to the assert-interpretation
code than you are.  The other guy having done bits there is Benjamin -
still haven't got hold of him on this issue and your mail. 
But I discussed with Benjamin introducing a more general API that
would have the AST-transformation call back into a generic
pytest-hook if an assertion fails.  So one could customize
"hasattr(x, y)" or "x in y" or "x == y == z" etc. and all
such representation code would move to the py/_plugin/pytest_assertion.py 
plugin. That being said going for just comparisons right now
is fine, let's not have more general thoughts hold us up.

On Mon, Aug 16, 2010 at 14:51 +0100, Floris Bruynooghe wrote:
> On Mon, Aug 16, 2010 at 01:29:13PM +0200, Ronny Pfannschmidt wrote:
> > On Mon, 2010-08-16 at 01:25 +0100, Floris Bruynooghe wrote: 
> > > The attached patch makes compare equal a special case and checks if
> > > the two arguments to it are both a list, text or dict and tries to
> > > generate a nicer explanation text for them.  The patch is more like a
> > > proof of concept then a final implementation, I may have done some
> > > very strange or silly things as I'm not familiar with the code.  It
> > > would be great to get feedback, both on the general concept and the
> > > actual implementation (particularly note the way I had to hack
> > > _format_explanation() in assertion.py).
> > 
> > I think it will be helpful to have some kind of hook to add more
> > explain-functions
> 
> Adding hooks should be possible, looking at all the .visit_*()
> functions it would seem only one hook is required, unless separate
> hooks for each rich compare operator are deemed useful.

no, a single one for all comparisons sound fine.

> The trickiest bit I think is how to produce multiline explanations.
> _format_explanation() concatenates all newlines.  Having only special
> cases for \n{ and \n} which is used by .visit_Call() to force nested
> and indented newlines.  In the patch I added \n== for this but a more
> general one is probably required, something like \n> or \n~ could work
> I guess.  This could be completely hidden from the hook however, by
> returning a list for each line to be printed, the caller of the hook
> would then join these up correctly so that _format_explanation() will
> add newlines and indentation correctly.  

Makes sense to have the caller deal with concatentation.  

> A possible api for the hook
> could be:
> 
> def pytest_assert_compare(op, left, right, left_repr, right_repr):
>     """Customise compare
> 
>     Return None for no custom compare, otherwise return a list of
>     strings.  The strings will be joined by newlines but any newlines
>     *in* a string will be escaped.
>     """
>     pass
> 
> I guess the reprs are not really necessary if there's an easy way to
> make them.  It's just that I used them in my original patch.

Hum, couldn't custom representations just mean that there is no 
"history" of where the object comes from? 

The hook looks otherwise good to me.  

> Another option is to encapsulate the arguments into an object that
> also knows how the builtin types and operators are compared, something
> like:
> 
> class CompareItem(object):
>     def __init__(self, op, left, right, ...):
>         self.op = op
>         self.left = left
>         self.right = right
> 	...
> 
>     def default(self):
>         if type(self.left) != type(self.right):
>             return None
>         if self.op == '==':
>             if isinstance(self.left, (list, tuple)):
>                 return self.sequence_equal()
>             elif isinstance(self.left, basestring):
>                 return self.string_equal()
>             ...
>         elif self.op == '!=':
>             ...
> 
>     def sequence_equal(self):
>         pass
> 
>     def string_equal(self):
>         pass
> 
>     ...
 
> This would allow pytest_assert_compare() to use those methods as part
> of the implementation.

I think the other style is more sensible because delegation to
"builtin representation styles" a) can happen via plugins and
the plugin mechanism b) we could pass a "testrepr" or "builtinrepr"
argument to the hook that helps to invoke helpful default machinery.
(I guess you are aware that any pytest-hook implementation can always
choose to accept less than the available arguments).

> There's also the question of who should truncate large amounts of data
> (e.g. screenfulls of diffs): the hook itself, the caller of the hook
> or _format_explanation()?  Probably one of the first two to get rid of
> the memory usage as soon as possible.

If a hook returns something we should (probably) not further do anything
with it in _format_explanation().  And making truncation the repsonsibility 
of the hook makes sense i think.  

> > In particular, cause there are many more build-in types to manage,
> > and i have at least 2 projects where custom compare-explain is helpfull
> > 
> > another wishlist item i see is the rest of rich compare
> > i.e. <, >, <=, >=, !=
> 
> Sure, all builtin types and operators should ideally be supported by
> default as best as possible.  I just started with some I wanted most.

I am not sure about the general use cases, from my side:

    x == y
    x != y
    x in y
   
are the interesting ones (with some of the objects being long
lists, long strings etc.). so some hook for a "binary" relation
makes sense, pytest_assert_binrepr(op) where op could be "==",
"in" or "is" etc.

Floris, i'd very much like to start using/having improved assertion
representations.  Happy to review patches or forks for inclusion.

best,
holger



More information about the Pytest-dev mailing list