lycantropos / hypothesis_sqlalchemy

hypothesis strategies for generating SQLAlchemy objects
MIT License
28 stars 8 forks source link

Ability to change module's context #1

Closed lycantropos closed 6 years ago

lycantropos commented 7 years ago

To make package more flexible we need to provide simple and comfortable way of changing modules context (like global variables and functions).

lycantropos commented 7 years ago

(valid for version 0.0.2)

For now if someone wants (for example) change strategy for generating values for String columns: it can be manually done by modification of factories_by_sql_types object in records module like

from hypothesis_sqlalchemy import records
from sqlalchemy.sql.sqltypes import String
...
# defining `custom_factory`
...
records.factories_by_sql_types[String] = custom_factory
...
# generating values with `custom_factory` for `String` columns
...

or more specific: let's say we want to generate IP addresses using regular expression from here

('\A(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
 '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
 '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
 '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\Z')

(more about \A, \Z at docs)

We can use hypothesis.strategies.from_regex factory like

import re
import textwrap
from functools import partial
from typing import Pattern

from hypothesis import strategies
from sqlalchemy.sql.sqltypes import String

from hypothesis_sqlalchemy import records
from hypothesis_sqlalchemy.types import Strategy

def string_type_values_factory_by_pattern(string_type: String,
                                          *,
                                          pattern: Pattern) -> Strategy:
    result = strategies.from_regex(pattern)
    max_length = string_type.length
    if max_length is not None:
        cropper = partial(textwrap.shorten,
                          width=max_length,
                          placeholder='')
        result = result.map(cropper)
    return result

...
ip_pattern = re.compile('\A(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
                        '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
                        '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
                        '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\Z')
records.factories_by_sql_types[String] = partial(
        string_type_values_factory_by_pattern,
        pattern=ip_pattern)
...
# generating values with `custom_factory` for `String` columns
...

Same goes to changing strategy for generating values by python types.

lycantropos commented 6 years ago

In 0.0.3 I've replaced factories_by_sql_types mapping with functools.singledispatch decorated records.from_column_type function, so changing behavior for all String column types can be done like

import re
import textwrap
from functools import partial
from typing import Pattern

from hypothesis import strategies
from sqlalchemy import String

from hypothesis_sqlalchemy import records
from hypothesis_sqlalchemy.types import Strategy

def string_type_values_factory_by_pattern(string_type: String,
                                          *,
                                          pattern: Pattern) -> Strategy:
    result = strategies.from_regex(pattern)
    max_length = string_type.length
    if max_length is not None:
        cropper = partial(textwrap.shorten,
                          width=max_length,
                          placeholder='')
        result = result.map(cropper)
    return result

...
ip_pattern = re.compile('\A(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
                        '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
                        '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.'
                        '(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\Z')
records.from_column_type.register(String)(partial(
    string_type_values_factory_by_pattern,
    pattern=ip_pattern))