Completed
Push — develop ( 86b58f...0a86d2 )
by Jace
03:19
created

yorm/types/extended.py (1 issue)

Labels
Severity
1
"""Converter classes for extensions to builtin types."""
0 ignored issues
show
There seems to be a cyclic import (yorm.bases -> yorm.bases.converter).

Cyclic imports may cause partly loaded modules to be returned. This might lead to unexpected runtime behavior which is hard to debug.

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