Passed
Push — develop ( 1aefc8...c061ca )
by Jace
01:20
created

NullableNumber   A

Complexity

Total Complexity 0

Size/Duplication

Total Lines 3
Duplicated Lines 0 %

Test Coverage

Coverage 0%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 0
c 1
b 0
f 0
dl 0
loc 3
ccs 0
cts 0
cp 0
rs 10
1
"""Converter classes for extensions to builtin types."""
2
3 1
import re
4 1
import logging
5
6 1
from .standard import String, Integer, Float, Boolean
7 1
from .containers import Dictionary, List
8 1
from ._representers import LiteralString
9
10
11 1
log = logging.getLogger(__name__)
12
13
14
# NULLABLE BUILTINS ###########################################################
15
16
17 1
class NullableString(String):
18
    """Converter for the `str` type with `None` as default."""
19
20 1
    DEFAULT = None
21
22
23 1
class NullableInteger(Integer):
24
    """Converter for the `int` type with `None` as default."""
25
26 1
    DEFAULT = None
27
28
29 1
class NullableFloat(Float):
30
    """Converter for the `float` type with `None` as default."""
31
32 1
    DEFAULT = None
33
34
35 1
class NullableBoolean(Boolean):
36
    """Converter for the `bool` type with `None` as default."""
37
38 1
    DEFAULT = None
39
40
41
# CUSTOM TYPES ################################################################
42
43
44 1
class Number(Float):
45
46
    DEFAULT = 0
47 1
48
    @classmethod
49
    def to_value(cls, obj):
50
        value = super().to_value(obj)
51
        if value and int(value) == value:
52
            return int(value)
53
        return value
54
55
56
class NullableNumber(Number):
57
58
    DEFAULT = None
59
60
61
class Markdown(String):
62
    """Converter for a `str` type that contains Markdown."""
63
64
    REGEX_MARKDOWN_SPACES = re.compile(r"""
65
66
    ([^\n ])  # any character but a newline or space
67
68 1
    (\ ?\n)   # optional space + single newline
69
70
    (?!       # none of the following:
71
72
      (?:\s)       # whitespace
73
      |
74
      (?:[-+*]\s)  # unordered list separator + whitespace
75
      |
76
      (?:\d+\.\s)  # number + period + whitespace
77
78
    )
79
80
    ([^\n])  # any character but a newline
81
82
    """, re.VERBOSE | re.IGNORECASE)
83
84 1
    # based on: http://en.wikipedia.org/wiki/Sentence_boundary_disambiguation
85
    REGEX_SENTENCE_BOUNDARIES = re.compile(r"""
86
87 1
    (            # one of the following:
88 1
89
      (?<=[a-z)][.?!])      # lowercase letter + punctuation
90 1
      |
91
      (?<=[a-z0-9][.?!]\")  # lowercase letter/number + punctuation + quote
92
93 1
    )
94 1
95 1
    (\s)          # any whitespace
96 1
97
    (?=\"?[A-Z])  # optional quote + an upppercase letter
98 1
99
    """, re.VERBOSE)
100
101
    @classmethod
102
    def to_value(cls, obj):
103
        """Join non-meaningful line breaks."""
104
        value = String.to_value(obj)
105
        return cls._join(value)
106
107
    @classmethod
108
    def to_data(cls, obj):
109
        """Break a string at sentences and dump as a literal string."""
110
        value = String.to_value(obj)
111
        data = String.to_data(value)
112 1
        split = cls._split(data)
113
        return LiteralString(split)
114 1
115 1
    @classmethod
116
    def _join(cls, text):
117
        r"""Convert single newlines (ignored by Markdown) to spaces.
118
119
        >>> Markdown._join("abc\n123")
120
        'abc 123'
121
122
        >>> Markdown._join("abc\n\n123")
123
        'abc\n\n123'
124
125
        >>> Markdown._join("abc \n123")
126
        'abc 123'
127
128 1
        """
129 1
        return cls.REGEX_MARKDOWN_SPACES.sub(r'\1 \3', text).strip()
130 1
131
    @classmethod
132 1
    def _split(cls, text, end='\n'):
133
        r"""Replace sentence boundaries with newlines and append a newline.
134
135
        :param text: string to line break at sentences
136
        :param end: appended to the end of the update text
137
138 1
        >>> Markdown._split("Hello, world!", end='')
139
        'Hello, world!'
140
141 1
        >>> Markdown._split("Hello, world! How are you? I'm fine. Good.")
142 1
        "Hello, world!\nHow are you?\nI'm fine.\nGood.\n"
143 1
144
        """
145 1
        stripped = text.strip()
146
        if stripped:
147
            return cls.REGEX_SENTENCE_BOUNDARIES.sub('\n', stripped) + end
148 1
        else:
149 1
            return ''
150 1
151
152 1
# CUSTOM CONTAINERS ###########################################################
153 1
154 1
155 1
class AttributeDictionary(Dictionary):
156
    """Dictionary converter with keys available as attributes."""
157 1
158 1
    def __init__(self, *args, **kwargs):
159
        super().__init__(*args, **kwargs)
160 1
        self.__dict__ = self
161
162
    @classmethod
163 1
    def create_default(cls):
164
        """Create an uninitialized object with keys as attributes."""
165
        if cls is AttributeDictionary:
166 1
            msg = "AttributeDictionary class must be subclassed to use"
167
            raise NotImplementedError(msg)
168
169 1
        try:
170 1
            obj = cls()
171 1
        except TypeError as exc:
172 1
            log.info("Default values in %s are not available when "
173 1
                     "positional arguments are used: %s", cls.__name__, exc)
174 1
            obj = cls.__new__(cls)
175
            obj.__dict__ = obj
176 1
177
        return obj
178 1
179
180
class SortedList(List):
181 1
    """List converter that is sorted on disk."""
182
183 1
    @classmethod
184
    def create_default(cls):
185 1
        """Create an uninitialized object."""
186 1
        if cls is SortedList:
187
            msg = "SortedList class must be subclassed to use"
188 1
            raise NotImplementedError(msg)
189
        if not cls.item_type:
190
            msg = "SortedList subclass must specify item type"
191
            raise NotImplementedError(msg)
192
193
        return cls.__new__(cls)
194
195
    @classmethod
196
    def to_data(cls, obj):
197
        """Convert all attribute values for optimal dumping to YAML."""
198
        value = cls.to_value(obj)
199
200
        data = []
201
202
        for item in sorted(value):
203
            data.append(cls.item_type.to_data(item))  # pylint: disable=no-member
204
205
        return data
206