Tuesday, March 28, 2017

Unit-testing and Code-coverage in Python [4]

Long Post

There's a fair length of code in today's post, though it's mostly pretty simple stuff.

Implementation time!

With a set of requirements for the UnitTestValuePolicy and TestValues classes (mostly?) solidified, there's not much more to discuss before diving in to the implementation-details. I expect this post will be code-heavy, and possibly quite long. If it gets too long, I'll spend my next post in actually writing a unit-test module for the serialization module — which is how this all got started.

The filtering-action properties will make frequent use of Python's list comprehensions, rather than the filter() function. In my experience, list comprehensions tend to be faster in most cases, and I've found, after making extensive use of them over the past several years, that they are often easier for me to write and maintain, rather than generating a separate free-standing function to perform evaluations of filtering criteria (which would also then live somewhere else in the code), while still allowing a certain degree of complexity that is often difficult to create with a lambda expression.

By way of example, consider this code:

import time

source = range( -1000000, 2000001 )

def isEven( num ):
    if num % 2 == 0:
        return True
    return False

for trialNumber in range( 1,4 ):
    filterFuncStart = time.time()
    results = filter( isEven, source )
    filterFuncRun = time.time() - filterFuncStart

    isEven2 = lambda n: n % 2 == 0
    filterLambdaStart = time.time()
    results = filter( isEven2, source )
    filterLambdaRun = time.time() - filterLambdaStart

    listCompStart = time.time()
    results = [ n for n in source if n % 2 == 0 ]
    listCompRun = time.time() - listCompStart

    print 'trial-run #%d' % ( trialNumber )
    print 'filter w/ function ............. %0.3f sec.' % ( filterFuncRun )
    print 'filter w/ lambda ............... %0.3f sec.' % ( filterLambdaRun )
    print 'list comp. ..................... %0.3f sec.' % ( listCompRun )
    print 'list-comp ÷ filter w/ function ... %0.2f%%' % ( 
        float( int( listCompRun *10000 / filterFuncRun ) / 100.0 ) )
    print 'list-comp ÷ filter w/ lambda ..... %0.2f%%' % ( 
        float( int( listCompRun *10000 / filterLambdaRun ) / 100.0 ) )
    print
When this is run on my main development laptop, the results are pretty close to this:
trial-run #1
filter w/ function ................ 0.790 sec.
filter w/ lambda .................. 0.652 sec.
list comp. ........................ 0.565 sec.
list-comp ÷ filter w/ function ... 71.53%
list-comp ÷ filter w/ lambda ..... 86.73%

trial-run #2
filter w/ function ................ 0.729 sec.
filter w/ lambda .................. 0.616 sec.
list comp. ........................ 0.538 sec.
list-comp ÷ filter w/ function ... 73.79%
list-comp ÷ filter w/ lambda ..... 87.39%

trial-run #3
filter w/ function ................ 0.725 sec.
filter w/ lambda .................. 0.625 sec.
list comp. ........................ 0.551 sec.
list-comp ÷ filter w/ function ... 76.04%
list-comp ÷ filter w/ lambda ..... 88.16%
If you're interested in seeing what the timing-difference looks like on your own machine, here's the script-file:

The list-comprehension approach clocks in at somewhere between 70 and 75% of the run-time for an equivalent filter() call with a dedicated function, and 85 to 90% of the run-time of a lambda-based filter() equivalent.

The _UnitTestValuePolicy Class

As a class, _UnitTestValuePolicy isn't too complex. It's really just a collection of lists of test-values, organized internally into groups by (roughly) formal value-types or by purposes served by the members of those lists of values as applied to unit-testing test-methods. There are a fair few values in the defaults of an instance of the object, so the code perhaps looks long, but it's still pretty simple:

@describe.InitClass()
class _UnitTestValuePolicy( object ):
    """
Represents a collection of standard unit-testing test-method values to be 
tested."""
    #-----------------------------------#
    # Class attributes (and instance-   #
    # attribute default values)         #
    #-----------------------------------#

    _genericObject = object()
    _defaults = {
        'bools':[ True, False ],
        'falseish':[ 0.0, 0, 0L, None, '', u'', False ],
        'floats':[ -1.0, 0.0, 1.0, 2.0 ],
        'ints':[ -1, 0, 1, 2 ],
        'longs':[ -sys.maxint *2 - 1, -1L, 0L, 1L, 2L, sys.maxint *2 ],
        'none':[ None ],
        'objects':[ _genericObject ],
        'strings':[
            '',
            ' ',
            '\t',
            '\r',
            '\n',
            'word',
            'multiple words',
            'A complete sentence,',
            'Multiple sentences. Separated with punctuation.',
            'String\tcontaining a tab',
            'Multiline\nstring',
        ],
        'trueish':[ 1.0, 0.5, 1, 1L, 'a', u'a', _genericObject, True ],
        'unicodes':[
            u'',
            u' ',
            u'\t',
            u'\r',
            u'\n',
            u'word',
            u'multiple words',
            u'A complete sentence,',
            u'Multiple sentences. Separated with punctuation.',
            u'String\tcontaining a tab',
            u'Multiline\nstring',
        ],
    }

    #-----------------------------------#
    # Instance property-getter methods  #
    #-----------------------------------#

    def _GetAll( self ):
        try:
            return self._all
        except AttributeError:
            self._all = []
            for key in self._defaults:
                self._all += self._defaults[ key ]
            return TestValues( self )

    #-----------------------------------#
    # Instance Properties               #
    #-----------------------------------#

    All = describe.makeProperty( _GetAll, None, None, 
        'the complete collection of all test-values', 
        list
    )

    #-----------------------------------#
    # Instance Initializer              #
    #-----------------------------------#
    @describe.AttachDocumentation()
    @describe.keywordargs( 'The collection of values to be populated '
        'in the instance and made available for unit-testing "good" and '
        '"bad" values' )
    @describe.keyword( 'bools', 
        'the values to store as boolean test-values in the instance',
        bool,
        default=_defaults[ 'bools' ]
    )
    @describe.keyword( 'falseish', 
        'the values to store as "false-ish" test-values in '
        'the instance',
        object,
        default=_defaults[ 'falseish' ]
    )
    @describe.keyword( 'floats', 
        'the values to store as floating-point-number test-values in '
        'the instance',
        float,
        default=_defaults[ 'floats' ]
    )
    @describe.keyword( 'ints', 
        'the values to store as integer test-values in the instance',
        int,
        default=_defaults[ 'ints' ]
    )
    @describe.keyword( 'longs', 
        'the values to store as long-integer test-values in the instance '
        '-- by default includes the minimum negative number value '
        'available to the system, and the maximum positive value '
        'available to the system',
        long,
        default=_defaults[ 'longs' ]
    )
    @describe.keyword( 'none', 
        'the values to store as "null" test-values in the instance',
        type( None ),
        default=_defaults[ 'none' ]
    )
    @describe.keyword( 'objects', 
        'the values to store as generic object test-values in the instance',
        object,
        default=_defaults[ 'objects' ]
    )
    @describe.keyword( 'strings', 
        'the values to store as string test-values in the instance',
        str,
        default=_defaults[ 'strings' ]
    )
    @describe.keyword( 'trueish', 
        'the values to store as "true-ish" test-values in '
        'the instance',
        object,
        default=_defaults[ 'trueish' ]
    )
    @describe.keyword( 'unicodes', 
        'the values to store as unicode test-values in the instance',
        str,
        default=_defaults[ 'unicodes' ]
    )
    def __init__( self, **values ):
        """
Instance initializer"""
        # Call parent initializers, if applicable.
        # Set default instance property-values with _Del... methods as needed.
        # Set instance property values from arguments if applicable.
        # Set _defaults values from **values members, if they are provided
        # - bools
        bools = values.get( 'bools' )
        if bools:
            self._defaults[ 'bools' ] = bools

        # - falseish
        falseish = values.get( 'falseish' )
        if falseish:
            self._defaults[ 'falseish' ] = falseish

        # - floats
        floats = values.get( 'floats' )
        if floats:
            self._defaults[ 'floats' ] = floats

        # - ints
        ints = values.get( 'ints' )
        if ints:
            self._defaults[ 'ints' ] = ints

        # - longs
        longs = values.get( 'longs' )
        if longs:
            self._defaults[ 'longs' ] = longs

        # - none
        none = values.get( 'none' )
        if none:
            self._defaults[ 'none' ] = none

        # - objects
        objects = values.get( 'objects' )
        if objects:
            self._defaults[ 'objects' ] = objects

        # - strings
        strings = values.get( 'strings' )
        if strings:
            self._defaults[ 'strings' ] = strings

        # - trueish
        trueish = values.get( 'trueish' )
        if trueish:
            self._defaults[ 'trueish' ] = trueish

        # - unicodes
        unicodes = values.get( 'unicodes' )
        if unicodes:
            self._defaults[ 'unicodes' ] = unicodes

        # Other set-up

The actual _UnitTestValuePolicy class is not explicitly added to the __all__ list of the unit_testing module. Instead, a default instance, UnitTestValuePolicy is created by the module that uses the default values of the class for its test-values. A non-default instance could still be created, it would just require an explicit import of the _UnitTestValuePolicy class, and creation of a new instance with whatever test-values need to be overridden from the default. For example:

from unit_testing import _UnitTestValuePolicy

myTestingValues = {
    # Populate this accordingly, using the keywords expected
    }
myTestValues = _UnitTestValuePolicy( **myTestingValues )

myOtherTestValues = _UnitTestValuePolicy( 
    ints=[ -12, -6, -2, -1, 
        0, 1, 2, 6, 12 ],
    longs=[ -12L, -6L, -2L, -1L, 
        0L, 1L, 2L, 6L, 12L ],
    floats=[ -12.0, -6.0, -2.0, -1.0, 
        0.0, 1.0, 2.0, 6.0, 12.0 ],
)
The intention is to allow both a standard, common set of test-values with minimal required set-up, that can be applied to the majority of test-method values, and the ability to customize some or all of the standard values for specific test-method needs and implementations, without having to do too much customization of test-method structures and logic. It's inevitable that some customization of values will be needed, especially as the structures being tested start having more properties and methods that utilize collections of values or custom classes. But with less customization needed for the simple values, that can be put off for a while.

The other advantage to having a single, standard collection of test-values available and in use as widely as possible is that if (when) a need for a new test-value arises, it can simply be added to the master collection of values, without having to alter each and every test-method. The larger the body of code being tested, the greater an impact this will have from a time-saving standpoint.

_UnitTestValuePolicy has a single property, All, that will gather up and return all of the test-values available to the instance, collected into a TestValues instance.

The default UnitTestValuePolicy Instance

The All value/TestValues-instance of the default _UnitTestValuePolicy instance is what's actually exposed by the module as its UnitTestValuePolicy constant:

# Define a standard UnitTestValuePolicy constant
UnitTestValuePolicy = _UnitTestValuePolicy().All

__all__.append( 'UnitTestValuePolicy' )
That allows the UnitTestValuePolicy constant to be immediately usable for generating lists of test-values with all of the filtering actions provided by any instance of TestValues.

The TestValues Class

The important aspects of TestValues are the various filtering-action properties. Their set-up as properties is pretty typical of any class that uses the describe.makeProperty process from several weeks ago — I've already listed all of the properties themselves several times, but here's a few of them as they were expressed in the code:

    # -- Boolean properties------------------#
    Boolean = describe.makeProperty( _GetBoolean, None, None, 
        'the current test-values that evaluate to True or False when used '
        'in comparison logic',
        list
    )
    Strict = describe.makeProperty( _GetStrict, None, None, 
        'the current test-values that are True or False',
        list
    )

    # ...

    # -- Numeric properties------------------#
    Numeric = describe.makeProperty( _GetNumeric, None, None, 
        'the current test-values that are numbers',
        list
    )
    Floats = describe.makeProperty( _GetFloats, None, None, 
        'the current test-values that are float-type numbers',
        list
    )
    Integers = describe.makeProperty( _GetIntegers, None, None, 
        'the current test-values that are int-type numbers',
        list
    )
    Longs = describe.makeProperty( _GetLongs, None, None, 
        'the current test-values that are long-type numbers',
        list
    )
    Even = describe.makeProperty( _GetEven, None, None, 
        'the current test-values that are even numbers (int and '
        'long-int types only)',
        list
    )
    Odd = describe.makeProperty( _GetOdd, None, None, 
        'the current test-values that are odd numbers (int and '
        'long-int types only)',
        list
    )

    # ...

    # -- Text properties---------------------#
    Text = describe.makeProperty( _GetText, None, None, 
        'the current test-values that are str- or unicode-type text-values',
        list
    )
    Strings = describe.makeProperty( _GetStrings, None, None, 
        'the current test-values that are str-type text-values',
        list
    )

    # ...
The real magic of these properties, if there is any, is all contained in their getter-methods...

There are a couple of properties that aren't directly related to filtering of test-values: All and ValueSource. The ValueSource property is a reference to the _UnitTestValuePolicy instance that contains the complete collection of all available test-values, allowing the individual instances to be able to look at that object and its _defaults items if necessary. The All property, then, allows the instances to acquire the All value from the original _UnitTestValuePolicy instance.

@describe.AttachDocumentation()
def _GetAll( self ):
    """
Gets the complete collection of all available test-values"""
    return self._valueSource.All

@describe.AttachDocumentation()
def _GetValueSource( self ):
    """
Gets the source of all available standard test-values"""
    return self._valueSource

The various Boolean filter-properties are pretty straightforward:

# -- Boolean property-getters------------#
@describe.AttachDocumentation()
def _GetBoolean( self ):
    """
Gets the current test-values that evaluate to True or False when used 
in comparison logic"""
    checkValues = ( self.ValueSource._defaults[ 'bools' ] + 
        self.ValueSource._defaults[ 'trueish' ] + 
        self.ValueSource._defaults[ 'falseish' ]
    )
    newValues = [ v for v in self if v in checkValues ]
    self._checkValues( newValues, 'Boolean' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetStrict( self ):
    """
Gets the current test-values that are True or False"""
    newValues = [ v for v in self if v in ( True, False ) ]
    self._checkValues( newValues, 'Strict' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetStrictAndNone( self ):
    """
Gets the current test-values that are True, False, or None"""
    newValues = [ v for v in self if v in ( True, False, None ) ]
    self._checkValues( newValues, 'StrictAndNone' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetStrictAndNumeric( self ):
    """
Gets the current test-values that are True, False, or any numeric 
value that is equivalent to True or False"""
    newValues = [
        v for v in self 
        if v in ( True, False, 1, 0, 1L, 0L, 1.0, 0.0 )
    ]
    self._checkValues( newValues, 'StrictAndNumeric' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetStrictNumericNone( self ):
    """
Gets the current test-values that are True, False, None, or any numeric 
value that is equivalent to True or False"""
    newValues = [
        v for v in self 
        if v in ( True, False, None, 1, 0, 1L, 0L, 1.0, 0.0 )
    ]
    self._checkValues( newValues, 'StrictNumericNone' )
    return self.__class__( self.ValueSource, newValues )

As noted earlier, these make extensive use of list comprehensions, so as long as those are understood, there's nothing too spectacular about the actual generation of values being returned. All of these getter-methods make use of a protected helper-method to check that the results being returned are going to actually be useful, though:

@describe.AttachDocumentation()
@describe.argument( 'newValues', 
    'the sequence of values to test', 
    list )
@describe.argument( 'name', 
    'the name of the property being tested', 
    str, unicode )
@describe.raises( TypeError,
    'if newValues is not a list'
)
@describe.raises( ValueError,
    'if newValues is an empty list'
)
def _checkValues( self, newValues, name ):
    if not isinstance( newValues, list ):
        raise TypeError( '%s.%s yielded a non-list value' % ( 
            self.__class__.__name__, name ) )
    if len( newValues ) == 0:
        raise ValueError( '%s.%s yielded an empty list' % ( 
            self.__class__.__name__, name ) )
The thought here is that any filtering-process that yields an empty list of test-values is going to be invalid in the context of writing a unit-test. As an example, consider what would happen if a list were generated by using UnitTestValuePolicy.Even.Odd — There are, by definition, no numbers that are both even and odd. Trying to write a test-case that relied on that is... nonsensical at best, really. The same concern would arise from mixing filtering of different types, say like UnitTestValuePolicy.Strings.Negative.

The boolean test-value filter-properties are very value-oriented — boolean values are simple, though, so that's perhaps no great surprise. There's not a whole lot of variation between True and False (and True-ish and False-ish) values, so doing direct filtering based entirely on whether values being filtered are or are not members of fixed, magic value-sets makes sense.

The numeric filter-getters are a bit more process-oriented than their boolean brethren. Numbers and numeric values are more easily processed by examining their mathematical properties, and that makes the list-comprehensions that generate the final filtered output both easy to implement and flexible enough to handle any values that might get added in on the fly, or as overrides to the defaults in the parent _UnitTestValuePolicy instance.

# -- Numeric property-getters------------#
@describe.AttachDocumentation()
def _GetNumeric( self ):
    """
Gets the current test-values that are numeric values (float, int or long types)"""
    newValues = [
        v for v in self 
        if type( v ) in ( int, float, long )
    ]
    self._checkValues( newValues, 'Numeric' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
@describe.todo( 'Document _GetFloats' )
def _GetFloats( self ):
    """
Gets the current test-values that are float-type numeric values"""
    newValues = [
        v for v in self 
        if type( v ) == float
    ]
    self._checkValues( newValues, 'Floats' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetIntegers( self ):
    """
Gets the current test-values that are int-type numeric values"""
    newValues = [
        v for v in self 
        if type( v ) == int
    ]
    self._checkValues( newValues, 'Integers' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetLongs( self ):
    """
Gets the current test-values that are long-type numeric values"""
    newValues = [
        v for v in self 
        if type( v ) == long
    ]
    self._checkValues( newValues, 'Longs' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetEven( self ):
    """
Gets the current test-values that are even numbers (int and long-int types only)"""
    newValues = [
        v for v in self 
        if type( v ) in ( int, long ) 
        and v % 2 == 0
    ]
    self._checkValues( newValues, 'Even' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetNegative( self ):
    """
Gets the current test-values that are negative numeric values"""
    newValues = [
        v for v in self 
        if v < 0
    ]
    self._checkValues( newValues, 'Negative' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetNonNegative( self ):
    """
Gets the current test-values that are non-negative numeric values"""
    newValues = [
        v for v in self 
        if v >= 0
    ]
    self._checkValues( newValues, 'NonNegative' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetNonPositive( self ):
    """
Gets the current test-values that are non-positive numeric values"""
    newValues = [
        v for v in self 
        if v <= 0
    ]
    self._checkValues( newValues, 'NonPositive' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetNonZero( self ):
    """
Gets the current test-values that are non-zero numeric values"""
    newValues = [
        v for v in self 
        if v != 0
    ]
    self._checkValues( newValues, 'NonZero' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetOdd( self ):
    """
Gets the current test-values that are odd numbers (int and long-int types only)"""
    newValues = [
        v for v in self 
        if type( v ) in ( int, long ) 
        and v % 2 == 1
    ]
    self._checkValues( newValues, 'Odd' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetPositive( self ):
    """
Gets the current test-values that are negative numeric values"""
    newValues = [
        v for v in self 
        if v > 0
    ]
    self._checkValues( newValues, 'Positive' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
@describe.todo( 'Document _GetZero' )
def _GetZero( self ):
    """
Gets the current test-values that are numeric values equal to zero"""
    newValues = [
        v for v in self 
        if v == 0
    ]
    self._checkValues( newValues, 'Zero' )
    return self.__class__( self.ValueSource, newValues )
A couple of mathematical factoids to bear in mind:
  • Zero is an even number — If divided by two, there is no remainder;
  • Zero is neither positive nor negative;

The various text-type filters have a lost of qualitative testing involved once the basic type-filters are taken care of. Those type-based filters, though are pretty much wht you might expect given the examples of the Floats, Integers and Longs propeties for numeric values:

# -- Text property-getters---------------#
    @describe.AttachDocumentation()
    def _GetText( self ):
        """
Gets the current test-values that are str- or unicode-type text-values"""
        newValues = [
            v for v in self 
            if type( v ) in ( str, unicode )
        ]
        self._checkValues( newValues, 'Text' )
        return self.__class__( self.ValueSource, newValues )

    @describe.AttachDocumentation()
    def _GetStrings( self ):
        """
Gets the current test-values that are str-type text-values"""
        newValues = [
            v for v in self 
            if type( v ) == str
        ]
        self._checkValues( newValues, 'Strings' )
        return self.__class__( self.ValueSource, newValues )

    @describe.AttachDocumentation()
    def _GetUnicodes( self ):
        """
Gets the current test-values that are unicode-type text-values"""
        newValues = [
            v for v in self 
            if type( v ) == unicode
        ]
        self._checkValues( newValues, 'Unicodes' )
        return self.__class__( self.ValueSource, newValues )

Empty- and non-empty text-values are pretty easy:

@describe.AttachDocumentation()
def _GetEmpty( self ):
    """
Gets the current test-values that are empty str- or unicode-values"""
    newValues = [
        v for v in self 
        if v == ''
    ]
    self._checkValues( newValues, 'Empty' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetNotEmpty( self ):
    """
Gets the current test-values that are non-empty str- or unicode-values"""
    newValues = [
        v for v in self 
        if v != ''
    ]
    self._checkValues( newValues, 'NotEmpty' )
    return self.__class__( self.ValueSource, newValues )

Single- and multi-line text-values are also pretty straightforward, though they rely on an aspect of the split method of str and unicode values that may not be obvious:

@describe.AttachDocumentation()
def _GetMultiline( self ):
    """
Gets the current test-values that are str- or unicode-type values that have at 
least one line-break or carriage-return in them"""
    newValues = [
        v for v in self 
        if type( v ) in  (str, unicode )
        and (
            len( v.split( '\n' ) ) > 1
            or
            len( v.split( '\r' ) ) > 1
        )
    ]
    self._checkValues( newValues, 'Multiline' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetSingleLine( self ):
    """
Gets the current test-values that are str- or unicode-type values that have 
no line-breaks or carriage-returns in them"""
    newValues = [
        v for v in self 
        if type( v ) in ( str, unicode )
        and len( v.split( '\n' ) ) == 1
        and len( v.split( '\r' ) ) == 1
        )
    ]
    self._checkValues( newValues, 'SingleLine' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetNoTabs( self ):
    """
Gets the current test-values that are CRITERIA"""
    newValues = [
        v for v in self 
        if type( v ) in  (str, unicode )
        and len( v.split( '\t' ) ) == 1
    ]
    self._checkValues( newValues, 'NoTabs' )
    return self.__class__( self.ValueSource, newValues )

@describe.AttachDocumentation()
def _GetSingleWords( self ):
    """
Gets the current test-values that have no spaces in them"""
    newValues = [
        v for v in self 
        if type( v ) in  (str, unicode )
        and len( v.split( ' ' ) ) == 1
    ]
    self._checkValues( newValues, 'SingleWords' )
    return self.__class__( self.ValueSource, newValues )
The same mechanism used for determining whether a text-value has line-breaking characters can also be used to determine if that value has tabs ('\t'), or spaces so I also included the _GetNoTabs and _GetSingleWords filter-getter methods, since they use the same basic mechanism...

The perhaps-not-obvious aspect of split() is that when a text-value is split on a character that does not exist in the original text, the result is still a list, with one member containing the original string. That is:

print 'This is a single-line string'.split( '\n' )
['This is a single-line string']

print 'This is a \nmulti-line string'.split( '\n' )
['This is a ', 'multi-line string']
The inclusion of both '\n' and '\r' is because I expect that I'll need to be able to check against values that have either or both new-lines and carriage-returns in them, particularly once I start delving into HTTP request-response functionality, where the presence of both in a response has been part of the protocol-standards for quite some time.

At present, that leaves the HasText and TagName filter-properties unimplemented:

@describe.AttachDocumentation()
@describe.todo( 'Document _GetHasText' )
@describe.todo( 'Implement _GetHasText' )
def _GetHasText( self ):
    """
Gets the current test-values that are CRITERIA"""
    # TODO: Implement me
    raise NotImplementedError( '%s.HasText has not been implemented '
        'yet' % ( self.__class__.__name__ ) )

@describe.AttachDocumentation()
@describe.todo( 'Document _GetTagName' )
@describe.todo( 'Implement _GetTagName' )
def _GetTagName( self ):
    """
Gets the current test-values that are CRITERIA"""
    # TODO: Implement me
    raise NotImplementedError( '%s.TagName has not been implemented '
        'yet' % ( self.__class__.__name__ ) )
In the time that it's taken to work out all of the other filter-getters, I've started to question whether _GetHasText (and its corresponding HasText property) are necessary. _GetTagName and TagName I'm going to leave for later (once I start in on markup handling), and I expect I'll want/need a _GetAttributeName/AttributeName filter-getter as well at that point, but I don't see a need for them now.

That covers most of the functionality of both of these classes, though, so even with the length of the post as it stands here, I'm pretty happy. There is one aspect to the TestValues object that I haven't covered, though — since it's an extension of the built-in list type, I'll need to take a look at what needs to be implemented so that it will still behave like a list while still being a TestValues.

No comments:

Post a Comment