File: elements.py

package info (click to toggle)
python-pyout 0.8.1-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 328 kB
  • sloc: python: 3,453; makefile: 3
file content (372 lines) | stat: -rw-r--r-- 15,011 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
"""Style elements and schema validation.
"""

from collections.abc import Mapping
import functools
import jsonschema
import hashlib


schema = {
    "$schema": "http://json-schema.org/draft-07/schema#",
    "definitions": {
        # Plain style elements
        "align": {
            "description": "Alignment of text",
            "type": "string",
            "enum": ["left", "right", "center"],
            "default": "left",
            "scope": "column"},
        "bold": {
            "description": "Whether text is bold",
            "oneOf": [{"type": "boolean"},
                      {"$ref": "#/definitions/lookup"},
                      {"$ref": "#/definitions/re_lookup"},
                      {"$ref": "#/definitions/interval"}],
            "default": False,
            "scope": "field"},
        "color": {
            "description": "Foreground color of text",
            "oneOf": [{"type": "string",
                       "enum": ["black", "red", "green", "yellow",
                                "blue", "magenta", "cyan", "white"]},
                      {"$ref": "#/definitions/lookup"},
                      {"$ref": "#/definitions/re_lookup"},
                      {"$ref": "#/definitions/interval"}],
            "default": "black",
            "scope": "field"},
        "hide": {
            "description": """Whether to hide column.  A value of True
            unconditionally hides the column.  'if_missing' hides the column
            until the first non-missing value is encountered.""",
            "oneOf": [{"type": "boolean"},
                      {"type": "string", "enum": ["if_missing"]}],
            "default": False,
            "scope": "column"},
        "underline": {
            "description": "Whether text is underlined",
            "oneOf": [{"type": "boolean"},
                      {"$ref": "#/definitions/lookup"},
                      {"$ref": "#/definitions/re_lookup"},
                      {"$ref": "#/definitions/interval"}],
            "default": False,
            "scope": "field"},
        "width_type": {
            "description": "Type for numeric values in 'width'",
            "oneOf": [{"type": "integer", "minimum": 1},
                      {"type": "number",
                       "exclusiveMinimum": 0,
                       "exclusiveMaximum": 1}]},
        "width": {
            "description": """Width of field.  With the default value, 'auto',
            the column width is automatically adjusted to fit the content and
            may be truncated to ensure that the entire row fits within the
            available output width.  An integer value forces all fields in a
            column to have a width of the specified value.

            In addition, an object can be specified.  Its 'min' and 'max' keys
            specify the minimum and maximum widths allowed, whereas the 'width'
            key specifies a fixed width.  The values can be given as an integer
            (representing the number of characters) or as a fraction, which
            indicates the proportion of the total table width (typically the
            width of your terminal).

            The 'marker' key specifies the marker used for truncation ('...' by
            default).  Where the field is truncated can be configured with
            'truncate': 'right' (default), 'left', or 'center'.

            The object can also include a 'weight' key.  Conceptually,
            assigning widths to each column can be (roughly) viewed as each
            column claiming _one_ character of available width at a time until
            a column is at its maximum width or there is no available width
            left.  Setting a column's weight to an integer N makes it claim N
            characters each iteration.""",
            "oneOf": [{"$ref": "#/definitions/width_type"},
                      {"type": "string",
                       "enum": ["auto"]},
                      {"type": "object",
                       "properties": {
                           "max": {"$ref": "#/definitions/width_type"},
                           "min": {"$ref": "#/definitions/width_type"},
                           "width": {"$ref": "#/definitions/width_type"},
                           "weight": {"type": "integer", "minimum": 1},
                           "marker": {"type": ["string", "boolean"]},
                           "truncate": {"type": "string",
                                        "enum": ["left",
                                                 "right",
                                                 "center"]}},
                       "additionalProperties": False}],
            "default": "auto",
            "scope": "column"},
        # Other style elements
        "aggregate": {
            "description": """A function that produces a summary value.  This
            function will be called with all of the column's (unprocessed)
            field values and should return a single value to be displayed.""",
            "scope": "column"},
        "delayed": {
            "description": """Don't wait for this column's value.
            The accessor will be wrapped in a function and called
            asynchronously.  This can be set to a string to mark columns as
            part of a "group".  All columns within a group will be accessed
            within the same callable.  True means to access the column's value
            in its own callable (i.e. independently of other columns).""",
            "type": ["boolean", "string"],
            "scope": "field"},
        "missing": {
            "description": "Text to display for missing values",
            "type": "string",
            "default": "",
            "scope": "column"
        },
        "re_flags": {
            "description": """Flags passed to re.search when using re_lookup.
            See the documentation of the re module for a description of
            possible values.  'I' (ignore case) is the most likely value of
            interest.""",
            "type": "array",
            "items": [{"type": "string",
                       "enum": ["A", "I", "L", "M", "S", "U", "X"]}],
            "scope": "field"},
        "transform": {
            "description": """An arbitrary function.
            This function will be called with the (unprocessed) field value as
            the single argument and should return a transformed value.  Note:
            This function should not have side-effects because it may be called
            multiple times.""",
            "scope": "field"},
        # Complete list of column style elements
        "styles": {
            "type": "object",
            "properties": {"aggregate": {"$ref": "#/definitions/aggregate"},
                           "align": {"$ref": "#/definitions/align"},
                           "bold": {"$ref": "#/definitions/bold"},
                           "color": {"$ref": "#/definitions/color"},
                           "delayed": {"$ref": "#/definitions/delayed"},
                           "hide": {"$ref": "#/definitions/hide"},
                           "missing": {"$ref": "#/definitions/missing"},
                           "re_flags": {"$ref": "#/definitions/re_flags"},
                           "transform": {"$ref": "#/definitions/transform"},
                           "underline": {"$ref": "#/definitions/underline"},
                           "width": {"$ref": "#/definitions/width"}},
            "additionalProperties": False},
        # Mapping elements
        "interval": {
            "description": "Map a value within an interval to a style",
            "type": "object",
            "properties": {"interval":
                           {"type": "array",
                            "items": [
                                {"type": "array",
                                 "items": [{"type": ["number", "null"]},
                                           {"type": ["number", "null"]},
                                           {"type": ["string", "boolean"]}],
                                 "additionalItems": False}]}},
            "additionalProperties": False},
        "lookup": {
            "description": "Map a value to a style",
            "type": "object",
            "properties": {"lookup": {"type": "object"}},
            "additionalProperties": False},
        "re_lookup": {
            "description": """Apply a style to values that match a regular
            expression.  The regular expressions are matched with re.search and
            tried in order, stopping after the first match.  Flags for
            re.search can be specified via the re_flags style attribute.""",
            "type": "object",
            "properties": {"re_lookup":
                           {"type": "array",
                            "items": [
                                {"type": "array",
                                 "items": [{"type": "string"},
                                           {"type": ["string", "boolean"]}],
                                 "additionalItems": False}]}},
            "additionalProperties": False},
    },
    "type": "object",
    "properties": {
        "aggregate_": {
            "description": "Shared attributes for the summary rows",
            "oneOf": [{"type": "object",
                       "properties":
                       {"color": {"$ref": "#/definitions/color"},
                        "bold": {"$ref": "#/definitions/bold"},
                        "underline": {"$ref": "#/definitions/underline"}}},
                      {"type": "null"}],
            "default": {},
            "scope": "table"},
        "default_": {
            "description": "Default style of columns",
            "oneOf": [{"$ref": "#/definitions/styles"},
                      {"type": "null"}],
            "default": {"align": "left",
                        "hide": False,
                        "width": "auto"},
            "scope": "table"},
        "header_": {
            "description": "Attributes for the header row",
            "oneOf": [{"type": "object",
                       "properties":
                       {"color": {"$ref": "#/definitions/color"},
                        "bold": {"$ref": "#/definitions/bold"},
                        "underline": {"$ref": "#/definitions/underline"}}},
                      {"type": "null"}],
            "default": None,
            "scope": "table"},
        "separator_": {
            "description": "Separator used between fields",
            "type": "string",
            "default": " ",
            "scope": "table"},
        "width_": {
            "description": """Total width of table.
            This is typically not set directly by the user.  With the default
            null value, the width is set to the stream's width for interactive
            streams and as wide as needed to fit the content for
            non-interactive streams.""",
            "default": None,
            "oneOf": [{"type": "integer"},
                      {"type": "null"}],
            "scope": "table"}
    },
    # All other keys are column names.
    "additionalProperties": {"$ref": "#/definitions/styles"}
}


def default(prop):
    """Return the default value schema property.

    Parameters
    ----------
    prop : str
        A key for schema["properties"]
    """
    return schema["properties"][prop]["default"]


def adopt(style, new_style):
    if new_style is None:
        return style

    combined = {}
    for key, value in style.items():
        if isinstance(value, Mapping):
            combined[key] = dict(value, **new_style.get(key, {}))
        else:
            combined[key] = new_style.get(key, value)
    return combined


class StyleError(Exception):
    """Style is invalid or mispecified in some way.
    """
    pass


class StyleValidationError(StyleError):
    """Exception raised if the style schema does not validate.
    """
    def __init__(self, original_exception):
        msg = ("Invalid style\n\n{}\n\n\n"
               "See pyout.schema for style definition."
               .format(original_exception))
        super(StyleValidationError, self).__init__(msg)


class StrHasher():
    """A wrapper for objects that hashes based on the string representation.

    Primarily its purpose to provide a customized @StrHasher.lru_cache
    decorator which allows to cache based on the string representation of the
    argument of unhashable objects.
    """
    def __init__(self, obj):
        self.value = obj
        self.hash = hashlib.md5(str(obj).encode()).hexdigest()

    def __hash__(self):
        return hash(self.hash)

    def __eq__(self, other):
        return self.hash == other.hash

    @staticmethod
    def _to(func):
        def cached_func(*args, **kwargs):
            wrapper = StrHasher((args, kwargs))
            return func(wrapper)
        return cached_func

    @staticmethod
    def _from(func):
        def cached_func(wrapper):
            return func(*wrapper.value[0], **wrapper.value[1])
        return cached_func

    @classmethod
    def lru_cache(cls, maxsize=128):
        """
        Custom LRU cache decorator that allows for a custom hasher function.
        """
        def decorator(func):
            @functools.lru_cache(maxsize=maxsize)
            @cls._from
            def lru_cached_func(*args, **kwargs):
                return func(*args, **kwargs)

            # Interface cache operations
            cached_func = functools.wraps(func)(
                cls._to(
                    lru_cached_func
                )
            )
            for op in dir(lru_cached_func):
                if op.startswith('cache_'):
                   setattr(cached_func, op,
                    getattr(lru_cached_func, op))
            return cached_func
        return decorator


@StrHasher.lru_cache()  # the same styles could be checked over again
def validate(style):
    """Check `style` against pyout.styling.schema.

    Parameters
    ----------
    style : dict
        Style object to validate.

    Raises
    ------
    StyleValidationError if `style` is not valid.
    """
    try:
        jsonschema.validate(style, schema)
    except jsonschema.ValidationError as exc:
        new_exc = StyleValidationError(exc)
        # Don't dump the original jsonschema exception because it is already
        # included in the StyleValidationError's message.
        new_exc.__cause__ = None
        raise new_exc


def value_type(value):
    """Classify `value` of bold, color, and underline keys.

    Parameters
    ----------
    value : style value

    Returns
    -------
    str, {"simple", "lookup", "re_lookup", "interval"}
    """
    try:
        keys = list(value.keys())
    except AttributeError:
        return "simple"
    if keys in [["lookup"], ["re_lookup"], ["interval"]]:
        return keys[0]
    raise ValueError("Type of `value` could not be determined")