Completed
Push — develop ( 0a86d2...dd3b25 )
by Jace
02:48
created

yorm/types/extended.py (1 issue)

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__)
0 ignored issues
show
Coding Style Naming introduced by
The name log does not conform to the constant naming conventions ((([A-Z_][A-Z0-9_]*)|(__.*__))$).

This check looks for invalid names for a range of different identifiers.

You can set regular expressions to which the identifiers must conform if the defaults do not match your requirements.

If your project includes a Pylint configuration file, the settings contained in that file take precedence.

To find out more about Pylint, please refer to their site.

Loading history...
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 Markdown(String):
45
    """Converter for a `str` type that contains Markdown."""
46
47 1
    REGEX_MARKDOWN_SPACES = re.compile(r"""
48
49
    ([^\n ])  # any character but a newline or space
50
51
    (\ ?\n)   # optional space + single newline
52
53
    (?!       # none of the following:
54
55
      (?:\s)       # whitespace
56
      |
57
      (?:[-+*]\s)  # unordered list separator + whitespace
58
      |
59
      (?:\d+\.\s)  # number + period + whitespace
60
61
    )
62
63
    ([^\n])  # any character but a newline
64
65
    """, re.VERBOSE | re.IGNORECASE)
66
67
    # based on: http://en.wikipedia.org/wiki/Sentence_boundary_disambiguation
68 1
    REGEX_SENTENCE_BOUNDARIES = re.compile(r"""
69
70
    (            # one of the following:
71
72
      (?<=[a-z)][.?!])      # lowercase letter + punctuation
73
      |
74
      (?<=[a-z0-9][.?!]\")  # lowercase letter/number + punctuation + quote
75
76
    )
77
78
    (\s)          # any whitespace
79
80
    (?=\"?[A-Z])  # optional quote + an upppercase letter
81
82
    """, re.VERBOSE)
83
84 1
    @classmethod
85
    def to_value(cls, obj):
86
        """Join non-meaningful line breaks."""
87 1
        value = String.to_value(obj)
88 1
        return cls._join(value)
89
90 1
    @classmethod
91
    def to_data(cls, obj):
92
        """Break a string at sentences and dump as a literal string."""
93 1
        value = String.to_value(obj)
94 1
        data = String.to_data(value)
95 1
        split = cls._split(data)
96 1
        return LiteralString(split)
97
98 1
    @classmethod
99
    def _join(cls, text):
100
        r"""Convert single newlines (ignored by Markdown) to spaces.
101
102
        >>> Markdown._join("abc\n123")
103
        'abc 123'
104
105
        >>> Markdown._join("abc\n\n123")
106
        'abc\n\n123'
107
108
        >>> Markdown._join("abc \n123")
109
        'abc 123'
110
111
        """
112 1
        return cls.REGEX_MARKDOWN_SPACES.sub(r'\1 \3', text).strip()
113
114 1
    @classmethod
115 1
    def _split(cls, text, end='\n'):
116
        r"""Replace sentence boundaries with newlines and append a newline.
117
118
        :param text: string to line break at sentences
119
        :param end: appended to the end of the update text
120
121
        >>> Markdown._split("Hello, world!", end='')
122
        'Hello, world!'
123
124
        >>> Markdown._split("Hello, world! How are you? I'm fine. Good.")
125
        "Hello, world!\nHow are you?\nI'm fine.\nGood.\n"
126
127
        """
128 1
        stripped = text.strip()
129 1
        if stripped:
130 1
            return cls.REGEX_SENTENCE_BOUNDARIES.sub('\n', stripped) + end
131
        else:
132 1
            return ''
133
134
135
# CUSTOM CONTAINERS ############################################################
136
137
138 1
class AttributeDictionary(Dictionary):
139
    """Dictionary converter with keys available as attributes."""
140
141 1
    def __init__(self, *args, **kwargs):
142 1
        super().__init__(*args, **kwargs)
143 1
        self.__dict__ = self
144
145 1
    @classmethod
146
    def create_default(cls):
147
        """Create an uninitialized object with keys as attributes."""
148 1
        if cls is AttributeDictionary:
149 1
            msg = "AttributeDictionary class must be subclassed to use"
150 1
            raise NotImplementedError(msg)
151
152 1
        try:
153 1
            obj = cls()
154 1
        except TypeError as exc:
155 1
            log.info("Default values in %s are not available when "
156
                     "positional arguments are used: %s", cls.__name__, exc)
157 1
            obj = cls.__new__(cls)
158 1
            obj.__dict__ = obj
159
160 1
        return obj
161
162
163 1
class SortedList(List):
164
    """List converter that is sorted on disk."""
165
166 1
    @classmethod
167
    def create_default(cls):
168
        """Create an uninitialized object."""
169 1
        if cls is SortedList:
170 1
            msg = "SortedList class must be subclassed to use"
171 1
            raise NotImplementedError(msg)
172 1
        if not cls.item_type:
173 1
            msg = "SortedList subclass must specify item type"
174 1
            raise NotImplementedError(msg)
175
176 1
        return cls.__new__(cls)
177
178 1
    @classmethod
179
    def to_data(cls, obj):
180
        """Convert all attribute values for optimal dumping to YAML."""
181 1
        value = cls.to_value(obj)
182
183 1
        data = []
184
185 1
        for item in sorted(value):
186 1
            data.append(cls.item_type.to_data(item))  # pylint: disable=no-member
187
188
        return data
189