chide 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
chide/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ from .collection import Collection
2
+ from .set import Set
3
+
4
+
5
+ __all__ = ['Collection', 'Set']
chide/collection.py ADDED
@@ -0,0 +1,80 @@
1
+ from typing import Type, Any, TypeVar, Callable
2
+
3
+ from .factory import Factory
4
+ from .simplifiers import ObjectSimplifier, Simplifier
5
+ from .typing import Attrs
6
+
7
+ T = TypeVar('T')
8
+
9
+
10
+ class Collection:
11
+ """
12
+ A collection of attributes to use to make sample objects.
13
+
14
+ :param mapping:
15
+ A dictionary mapping object types to a dictionary
16
+ of attributes to make a sample object of that type.
17
+
18
+ """
19
+
20
+ def __init__(self, mapping: dict[Type[Any], Attrs] | None = None) -> None:
21
+ self.mapping = mapping or {}
22
+
23
+ def _attrs(
24
+ self,
25
+ type_: Type[Any],
26
+ attrs: Attrs,
27
+ nest: Callable[[Type[T]], T]
28
+ ) -> Attrs:
29
+ computed_attrs = dict(self.mapping[type_])
30
+ for key, value in computed_attrs.items():
31
+ try:
32
+ value_in_mapping = value in self.mapping
33
+ except TypeError:
34
+ value_in_mapping = False
35
+ if value_in_mapping and key not in attrs:
36
+ computed_attrs[key] = nest(value)
37
+ computed_attrs.update(attrs)
38
+ return computed_attrs
39
+
40
+ def add(self, obj: T, simplifier: Simplifier[T] = ObjectSimplifier()) -> None:
41
+ """
42
+ Add the attributes from the supplied object to this collection and
43
+ use them when samples of the type of that object are required.
44
+
45
+ :param obj: The sample object from which to extract attributes.
46
+
47
+ :param simplifier:
48
+ The :class:`~chide.simplifiers.Simplifier` to use to extract attributes
49
+ from ``obj``.
50
+ """
51
+ self.mapping[type(obj)] = simplifier.one(obj)
52
+
53
+ def attributes(self, type_: Type[T], **attrs: Any) -> Attrs:
54
+ """
55
+ Make the attributes for a sample object of the specified ``type_``
56
+ using the default attributes for that type in this :class:`Collection`.
57
+
58
+ The ``attrs`` mapping will be overlaid onto the sample attributes
59
+ and returned as a :class:`dict`.
60
+ """
61
+ return self._attrs(type_, attrs, self.make)
62
+
63
+ def make(self, type_: Type[T], **attrs: Any) -> T:
64
+ """
65
+ Make a sample object of the specified ``type_`` using the default
66
+ attributes for that type in this :class:`Collection`.
67
+
68
+ The ``attrs`` mapping will be overlaid onto the sample attributes
69
+ before being used with ``type_`` to instantiate and return a new
70
+ sample object.
71
+ """
72
+ return type_(**self.attributes(type_, **attrs))
73
+
74
+ def bind(self, type_: Type[T], **attrs: Any) -> Factory[T]:
75
+ """
76
+ Bind the supplied attributes into a :class:`~chide.factory.Factory` for the
77
+ requested ``type_`` by overlaying them onto the sample attributes
78
+ for that ``type_`` in this :class:`Collection`.
79
+ """
80
+ return Factory[T](self, type_, attrs)
chide/factory.py ADDED
@@ -0,0 +1,54 @@
1
+ from typing import TypeVar, Generic, Any, Self, Type, TYPE_CHECKING, Callable
2
+
3
+ from .typing import Attrs
4
+
5
+ if TYPE_CHECKING:
6
+ from .collection import Collection
7
+
8
+ T = TypeVar("T")
9
+
10
+
11
+ class Factory(Generic[T]):
12
+ """
13
+ A factory for objects of a particular type.
14
+ These are created by either :meth:`chide.Collection.bind` or :meth:`chide.factory.Factory.bind`.
15
+ """
16
+ def __init__(self, collection: 'Collection', type_: Type[T], attrs: dict[str, Any]) -> None:
17
+ self.collection = collection
18
+ self.type_ = type_
19
+ self.attrs = attrs
20
+
21
+ def _combine(self, attrs: Attrs) -> Attrs:
22
+ attrs_ = self.attrs.copy()
23
+ attrs_.update(attrs)
24
+ return attrs_
25
+
26
+ def attributes(self, **attrs: Any) -> Attrs:
27
+ """
28
+ Make the attributes for a sample object of this factory's type
29
+ using the default attributes for the type in this factory's :class:`Collection`
30
+ overlaid with any attributes bound into the factory.
31
+
32
+ The ``attrs`` mapping will be overlaid onto those attributes
33
+ and returned as a :class:`dict`.
34
+ """
35
+ return self.collection.attributes(self.type_, **self._combine(attrs))
36
+
37
+ def make(self, **attrs: Any) -> T:
38
+ """
39
+ Make a sample object of this factory's type using the default
40
+ attributes for the type in this factory's :class:`Collection`
41
+ overlaid with any attributes bound into the factory.
42
+
43
+ The ``attrs`` mapping will be overlaid onto the sample attributes
44
+ before being used to instantiate and return a new
45
+ sample object of this factory's type.
46
+ """
47
+ return self.collection.make(self.type_, **self._combine(attrs))
48
+
49
+ def bind(self, **attrs: Any) -> Self:
50
+ """
51
+ Bind the supplied attributes into a new :class:`~chide.factory.Factory`
52
+ by overlaying them onto the sample attributes this factory would use.
53
+ """
54
+ return type(self)(self.collection, self.type_, self._combine(attrs))
chide/formats.py ADDED
@@ -0,0 +1,475 @@
1
+ import builtins
2
+ import csv
3
+ import re
4
+ from ast import literal_eval
5
+ from enum import Enum, auto
6
+ from io import StringIO
7
+ from itertools import zip_longest
8
+ from textwrap import dedent
9
+ from typing import Protocol, Iterable, Type, Callable, Any, TypeVar, TypeAlias, Mapping
10
+
11
+ from .typing import Attrs
12
+
13
+ T = TypeVar('T')
14
+
15
+
16
+ class Format(Protocol):
17
+ """
18
+ Protocol for :doc:`formats <formats>`.
19
+ """
20
+
21
+ def parse(self, text: str) -> list[Attrs]:
22
+ """
23
+ Parse the supplied ``text`` into a list of :class:`~chide.typing.Attrs`.
24
+ """
25
+
26
+ def render(self, attrs: Iterable[Attrs]) -> str:
27
+ """
28
+ Render the supplied :class:`~chide.typing.Attrs` into a :class:`str`.
29
+ """
30
+
31
+
32
+ #: Type of a callable to parse a value from a :class:`str`.
33
+ ValueParse = Callable[[str], Any]
34
+ #: Type of a callable to render a value into a :class:`str`.
35
+ ValueRender = Callable[[Any], str]
36
+
37
+ #: A mapping of column or type names to the :class:`ValueParse` to use.
38
+ ParseMapping: TypeAlias = dict[str, ValueParse]
39
+ #: A mapping of data types to the :class:`ValueRender` to use.
40
+ TypeRenderMapping: TypeAlias = dict[Type[Any], ValueRender]
41
+ #: A mapping of data types to the name to use for them when rendering a table.
42
+ TypeNameMapping: TypeAlias = dict[Type[Any], str | None]
43
+ #: A mapping of column name to the :class:`ValueRender` to use.
44
+ ColumnRenderMapping: TypeAlias = dict[str, ValueRender]
45
+
46
+
47
+ def default_parse(text: str) -> Any:
48
+ """
49
+ The default :class:`ValueParse`.
50
+ """
51
+ try:
52
+ return literal_eval(text)
53
+ except SyntaxError:
54
+ return text
55
+
56
+
57
+ NEEDS_REPR = re.compile(r'^(\s.+|.+\s)$')
58
+
59
+
60
+ def default_render(value: Any) -> str:
61
+ """
62
+ The default :class:`ValueRender`.
63
+ """
64
+ rendered = str(value)
65
+ if NEEDS_REPR.match(rendered):
66
+ rendered = repr(rendered)
67
+ return rendered
68
+
69
+
70
+ class TypeLocation(Enum):
71
+ #: The types are located in parentheses, after the column name, in the
72
+ #: row containing the column names.
73
+ HEADER = auto()
74
+ #: The types are located in their own row.
75
+ ROW = auto()
76
+
77
+
78
+ #: Shortcut for :any:`TypeLocation.HEADER`.
79
+ HEADER = TypeLocation.HEADER
80
+ #: Shortcut for :any:`TypeLocation.ROW`.
81
+ ROW = TypeLocation.ROW
82
+
83
+
84
+ class TabularFormat(Format):
85
+ """
86
+ A base class for tabular formats.
87
+ """
88
+
89
+ header_type_pattern = re.compile(r'([^ (]+) *\((.+)\) *')
90
+
91
+ def __init__(
92
+ self,
93
+ type_parse: ParseMapping | None = None,
94
+ default_type_parse: ValueParse = default_parse,
95
+ column_parse: ParseMapping | None = None,
96
+ type_render: TypeRenderMapping | None = None,
97
+ default_type_render: ValueRender = default_render,
98
+ type_names: TypeNameMapping | None = None,
99
+ column_render: ColumnRenderMapping | None = None,
100
+ types_location: TypeLocation | None = None,
101
+ ) -> None:
102
+ self.type_parse: ParseMapping = type_parse or {}
103
+ self.column_parse: ParseMapping = column_parse or {}
104
+ self.default_type_parse = default_type_parse
105
+ self.type_render: TypeRenderMapping = type_render or {}
106
+ self.type_names: TypeNameMapping = type_names or {}
107
+ self.column_render: ColumnRenderMapping = column_render or {}
108
+ self.default_type_render = default_type_render
109
+ self.types_location = types_location
110
+
111
+ def _resolve_type_names(self, type_names: dict[str, str]) -> None:
112
+ for column, name in type_names.items():
113
+ if name:
114
+ if name not in self.column_parse:
115
+ handler = self.type_parse.get(name)
116
+ if handler is None:
117
+ handler = getattr(builtins, name)
118
+ self.column_parse[column] = handler
119
+
120
+ def _parse(self, text: str, lexer: Callable[[str], Iterable[Iterable[str]]]) -> list[Attrs]:
121
+ parsed = []
122
+ columns: list[str] | None = None
123
+ types_row_handled = self.types_location is not ROW
124
+ types_row_next = False
125
+ for parts in lexer(text):
126
+
127
+ if columns is not None and not types_row_handled:
128
+ types_row_next = True
129
+
130
+ if columns is None:
131
+ columns = []
132
+ type_names = {}
133
+ for c in parts:
134
+ if (
135
+ self.types_location is HEADER
136
+ and (match := self.header_type_pattern.match(c))
137
+ ):
138
+ column, t = match.groups()
139
+ type_names[column] = t
140
+ else:
141
+ column = c
142
+ columns.append(column)
143
+ self._resolve_type_names(type_names)
144
+ elif types_row_next:
145
+ self._resolve_type_names(type_names={c: t for c, t in zip(columns, parts)})
146
+ types_row_handled = True
147
+ types_row_next = False
148
+ else:
149
+ row = {}
150
+ for column, value in zip(columns, parts):
151
+ try:
152
+ handler = self.column_parse.get(column, self.default_type_parse)
153
+ value = handler(value)
154
+ except ValueError:
155
+ pass
156
+ row[column] = value
157
+ parsed.append(row)
158
+ return parsed
159
+
160
+
161
+ class Widths(dict[str, int]):
162
+
163
+ def handle(self, item: Mapping[str, str | int]) -> None:
164
+ for column, text_or_width in item.items():
165
+ width = text_or_width if isinstance(text_or_width, int) else len(text_or_width)
166
+ self[column] = max(width, self.get(column, 0))
167
+
168
+
169
+ class RenderedRows(list[dict[str, str]]):
170
+ columns: list[str] | None = None
171
+ types: dict[str, str] | None = None
172
+ header: dict[str, str]
173
+
174
+ def __init__(
175
+ self, attrs: Iterable[Attrs], format_: 'TabularFormat', columns: list[str] | None = None
176
+ ) -> None:
177
+ super().__init__()
178
+ for attrs_ in attrs:
179
+ if self.columns is None:
180
+ attr_columns = list(attrs_.keys())
181
+ if columns is None:
182
+ self.columns = attr_columns
183
+ else:
184
+ self.columns = columns + [c for c in attr_columns if c not in columns]
185
+ if self.types is None:
186
+ self.types = {}
187
+ for column, value in attrs_.items():
188
+ type_ = type(value)
189
+ type_name = format_.type_names.get(type_, type(value).__name__)
190
+ self.types[column] = type_name or ''
191
+ row = {}
192
+ for column in self.columns:
193
+ value = attrs_.get(column)
194
+ handler = format_.column_render.get(column)
195
+ if handler is None:
196
+ handler = format_.type_render.get(type(value), format_.default_type_render)
197
+ text = handler(value)
198
+ row[column] = text
199
+ self.append(row)
200
+
201
+ self.header = {}
202
+ if self.columns is not None:
203
+ for column in self.columns:
204
+ text = column
205
+ if self.types is not None and (type_name := self.types.get(column)) is not None:
206
+ if format_.types_location is HEADER and type_name:
207
+ text = f'{text} ({type_name})'
208
+ self.header[column] = text
209
+
210
+ def update(self, widths: Widths, types_location: TypeLocation | None) -> None:
211
+ if self.header:
212
+ widths.handle(self.header)
213
+ if self.types is not None and types_location is ROW:
214
+ widths.handle(self.types)
215
+ for row in self:
216
+ widths.handle(row)
217
+
218
+
219
+ class PrettyLexer:
220
+
221
+ def __init__(self, padding: int):
222
+ self.widths: list[int] = []
223
+ self.padding = padding
224
+
225
+ def __call__(self, text: str) -> Iterable[list[str]]:
226
+ padding_text = ' ' * self.padding
227
+ padding_size = self.padding * 2
228
+ for line in dedent(text).splitlines():
229
+ line = line.strip()
230
+ if not line or line.startswith('+'):
231
+ continue
232
+ parts = line.split('|')[1:-1]
233
+ widths = []
234
+ for part in parts:
235
+ width = len(part)
236
+ if (
237
+ width >= padding_size and
238
+ part[:self.padding] == padding_text and
239
+ part[-self.padding:] == padding_text
240
+ ):
241
+ width -= padding_size
242
+ widths.append(width)
243
+ if self.widths:
244
+ self.widths = [max(w, w_) for w, w_ in zip_longest(self.widths, widths)]
245
+ else:
246
+ self.widths = widths
247
+ yield [p.strip() for p in parts]
248
+
249
+
250
+ class PrettyParsed(list[Attrs]):
251
+ """
252
+ A list of :class:`~chide.typing.Attrs` that also keeps track of the :attr:`widths`
253
+ required to render the columns, if they are known.
254
+ """
255
+ def __init__(self, attrs: list[Attrs], widths: list[int]) -> None:
256
+ super().__init__(attrs)
257
+ #: The widths required for the columns needed by these `~chide.typing.Attrs`.
258
+ self.widths: dict[str, int] = {}
259
+ if attrs:
260
+ for columns, width in zip(attrs[0].keys(), widths):
261
+ self.widths[columns] = width
262
+
263
+
264
+ class PrettyRenderedRows(list[str]):
265
+
266
+ def __init__(self, widths: Widths, padding: int) -> None:
267
+ super().__init__()
268
+ self.divider = ''.join('+'+'-'*(widths[column]+padding*2) for column in widths)+'+\n'
269
+ pad = padding*' '
270
+ self.templates = {c: f'|{pad}{{:{w}}}{pad}' for c, w in widths.items()}
271
+ self.widths = widths
272
+
273
+ def add_divider(self) -> None:
274
+ self.append(self.divider)
275
+
276
+ def add_row(self, row: dict[str, str]) -> None:
277
+ parts = (self.templates[column].format(value) for column, value in row.items())
278
+ self.append(''.join(parts) + '|\n')
279
+
280
+ def text(self) -> str:
281
+ return ''.join(self)
282
+
283
+
284
+ class PrettyFormat(TabularFormat):
285
+ """
286
+ A pretty :class:`Format` for tabular data, where tables look something like::
287
+
288
+ +-----+------+
289
+ | x | y |
290
+ +-----+------+
291
+ |float| str |
292
+ +-----+------+
293
+ | 1 | foo |
294
+ | 2 | bar |
295
+ +-----+------+
296
+
297
+ :param type_parse:
298
+ A mapping of type name, as found in either a row or column heading, dependent
299
+ on the ``types_location``, to a function that parses the text of a cell into
300
+ a value.
301
+
302
+ :param default_type_parse:
303
+ The default function to use when parsing the text of a cell into a value.
304
+
305
+ :param column_parse:
306
+ A mapping of column names to functions that will be used to parse the text of cells
307
+ in that column into values.
308
+
309
+ :param type_render:
310
+ A mapping of type objects to functions that will be used to render values of that
311
+ type to text for cells.
312
+
313
+ :param default_type_render:
314
+ The default function to use when rendingering values to text for cells.
315
+
316
+ :param type_names:
317
+ A mapping of type objects to names to use for those types when including types
318
+ in either column headings or their own own. If a type is mapped to ``None``,
319
+ then no type name will be rendered for columns containing date of that type.
320
+
321
+ :param column_render:
322
+ A mapping of column names to functions that will be used to render values in that
323
+ column to text for cells.
324
+
325
+ :param types_location:
326
+ An optional location from which type information will be parsed or to which it
327
+ will be rendered. Must be :any:`HEADER`, :any:`ROW` or ``None``.
328
+
329
+ :param minimum_column_widths:
330
+ An optional mapping of column name to the minimum width to use for a column.
331
+
332
+ :param padding:
333
+ The number of space to put to the left and right of values of cells.
334
+ """
335
+
336
+ def __init__(
337
+ self,
338
+ type_parse: ParseMapping | None = None,
339
+ default_type_parse: ValueParse = default_parse,
340
+ column_parse: ParseMapping | None = None,
341
+ type_render: TypeRenderMapping | None = None,
342
+ default_type_render: ValueRender = default_render,
343
+ type_names: TypeNameMapping | None = None,
344
+ column_render: ColumnRenderMapping | None = None,
345
+ types_location: TypeLocation | None = None,
346
+ minimum_column_widths: dict[str, int] | None = None,
347
+ padding: int = 1,
348
+ ) -> None:
349
+ super().__init__(
350
+ type_parse,
351
+ default_type_parse,
352
+ column_parse,
353
+ type_render,
354
+ default_type_render,
355
+ type_names,
356
+ column_render,
357
+ types_location,
358
+ )
359
+ self.minimum_column_widths: dict[str, int] = minimum_column_widths or {}
360
+ self.padding = padding
361
+
362
+ def parse(self, text: str) -> PrettyParsed:
363
+ """
364
+ Parse the supplied ``text`` into a :class:`PrettyParsed`.
365
+ """
366
+ lexer = PrettyLexer(self.padding)
367
+ rows = self._parse(text, lexer)
368
+ return PrettyParsed(rows, lexer.widths)
369
+
370
+ def render(self, attrs: Iterable[Attrs], ref: list[Attrs] | PrettyParsed | None = None) -> str:
371
+ """
372
+ Render the supplied :class:`~chide.typing.Attrs` into a :class:`str`.
373
+
374
+ If supplied, ``ref`` is used for reference to make sure:
375
+
376
+ - the reference columns are always present.
377
+ - columns are rendered in the order specified in the reference.
378
+ - columns widths will be at least as wide as those in the reference.
379
+ """
380
+ columns = None
381
+ widths = Widths(self.minimum_column_widths)
382
+
383
+ if ref is not None:
384
+ ref_widths = getattr(ref, 'widths', None)
385
+ if ref_widths is None:
386
+ ref_rows = RenderedRows(ref, self)
387
+ ref_rows.update(widths, self.types_location)
388
+ columns = ref_rows.columns
389
+ else:
390
+ widths.handle(ref_widths)
391
+ columns = list(ref_widths)
392
+
393
+ rows = RenderedRows(attrs, self, columns)
394
+ rows.update(widths, self.types_location)
395
+
396
+ rendered = PrettyRenderedRows(widths, self.padding)
397
+ rendered.add_divider()
398
+ if rows.header:
399
+ rendered.add_row(rows.header)
400
+ rendered.add_divider()
401
+ if rows.types is not None and self.types_location is ROW:
402
+ rendered.add_row(rows.types)
403
+ rendered.add_divider()
404
+ for row in rows:
405
+ rendered.add_row(row)
406
+ rendered.add_divider()
407
+
408
+ return rendered.text()
409
+
410
+
411
+ class CSVFormat(TabularFormat):
412
+ """
413
+ A :class:`Format` that parses and renders comma separated values.
414
+
415
+ :param type_parse:
416
+ A mapping of type name, as found in either a row or column heading, dependent
417
+ on the ``types_location``, to a function that parses the text of a cell into
418
+ a value.
419
+
420
+ :param default_type_parse:
421
+ The default function to use when parsing the text of a cell into a value.
422
+
423
+ :param column_parse:
424
+ A mapping of column names to functions that will be used to parse the text of cells
425
+ in that column into values.
426
+
427
+ :param type_render:
428
+ A mapping of type objects to functions that will be used to render values of that
429
+ type to text for cells.
430
+
431
+ :param default_type_render:
432
+ The default function to use when rendingering values to text for cells.
433
+
434
+ :param type_names:
435
+ A mapping of type objects to names to use for those types when including types
436
+ in either column headings or their own own. If a type is mapped to ``None``,
437
+ then no type name will be rendered for columns containing date of that type.
438
+
439
+ :param column_render:
440
+ A mapping of column names to functions that will be used to render values in that
441
+ column to text for cells.
442
+
443
+ :param types_location:
444
+ An optional location from which type information will be parsed or to which it
445
+ will be rendered. Must be :any:`HEADER`, :any:`ROW` or ``None``.
446
+ """
447
+
448
+ def parse(self, text: str) -> list[Attrs]:
449
+ return self._parse(text, lambda text: [row for row in csv.reader(StringIO(text))])
450
+
451
+ def render(self, attrs: Iterable[Attrs], ref: list[Attrs] | None = None) -> str:
452
+ """
453
+ Render the supplied :class:`~chide.typing.Attrs` into a :class:`str`.
454
+
455
+ If supplied, ``ref`` is used for reference to make sure:
456
+
457
+ - the reference columns are always present.
458
+ - columns are rendered in the order specified in the reference.
459
+ """
460
+ columns = None
461
+ if ref:
462
+ columns = list(ref[0])
463
+
464
+ rows = RenderedRows(attrs, self, columns)
465
+ text = StringIO()
466
+ writer = csv.writer(text)
467
+
468
+ if rows.header:
469
+ writer.writerow(rows.header.values())
470
+ if rows.types is not None and self.types_location is ROW:
471
+ writer.writerow(rows.types.values())
472
+ for row in rows:
473
+ writer.writerow(row.values())
474
+
475
+ return text.getvalue()
chide/set.py ADDED
@@ -0,0 +1,71 @@
1
+ from typing import Any, TypeVar, Type, Hashable
2
+
3
+ from chide import Collection
4
+ from .typing import Identifier
5
+
6
+ T = TypeVar('T')
7
+
8
+
9
+ class Set:
10
+ """
11
+ A collection of sample objects where only one object with
12
+ a given identity may exist at one time.
13
+
14
+ :param collection:
15
+ The :class:`~chide.Collection` instance used to create sample objects
16
+ when necessary.
17
+
18
+ :param identify:
19
+ An :class:`~chide.typing.Identifier` callable that takes
20
+ `type_` and `attrs` parameters.
21
+
22
+ `type_`, usually a class, is the type of the
23
+ sample object being requested.
24
+
25
+ `attrs` is a :class:`dict` of the attributes being requested for the
26
+ sample object to have.
27
+
28
+ The callable should return a hashable value that indicates the identity
29
+ to use for the requested sample object. For each unique hashable value,
30
+ only one sample object will be instantiated and returned each time
31
+ a sample is requested where this callable returns the given identity.
32
+
33
+ ``None`` may be returned to indicate that a new object should always
34
+ be returned for the provided parameters.
35
+
36
+ """
37
+
38
+ #: You may also want to subclass :class:`Set` and implement
39
+ #: an :meth:`identify` method, see :class:`chide.sqlalchemy.Set`
40
+ #: for an example.
41
+ identify: Identifier
42
+
43
+ def __init__(self, collection: Collection, identify: Identifier | None = None) -> None:
44
+ self.collection = collection
45
+ identify = identify or getattr(self, 'identify', None)
46
+ if identify is None:
47
+ raise TypeError('No identify callable supplied')
48
+ self.identify = identify
49
+ self.objects: dict[Hashable, Any] = {}
50
+
51
+ def get(self, type_: Type[T], **attrs: Any) -> T:
52
+ """
53
+ Return an appropriate sample object of the specified ``type_``.
54
+
55
+ The ``attrs`` mapping will be overlaid onto the sample attributes
56
+ found in this set's :class:`~chide.Collection` before checking if
57
+ an appropriate sample object already exists in the set.
58
+
59
+ If one exists, it is returned. If not, one is created using
60
+ this set's :class:`~chide.Collection`, added to the set and then
61
+ returned.
62
+ """
63
+ attrs = self.collection._attrs(type_, attrs, self.get)
64
+ key = self.identify(type_, attrs)
65
+ if key is None:
66
+ return type_(**attrs)
67
+ obj = self.objects.get(key)
68
+ if obj is None:
69
+ obj = type_(**attrs)
70
+ self.objects[key] = obj
71
+ return obj
chide/simplifiers.py ADDED
@@ -0,0 +1,49 @@
1
+ from typing import Protocol, TypeVar, Iterable
2
+
3
+ from chide.typing import Attrs
4
+
5
+ T = TypeVar('T', contravariant=True)
6
+
7
+
8
+ class Simplifier(Protocol[T]):
9
+ """
10
+ Protocol for :doc:`simplifiers <simplifiers>`.
11
+ """
12
+
13
+ def one(self, obj: T) -> Attrs:
14
+ """
15
+ Simplify one object into its :class:`~chide.typing.Attrs`.
16
+ """
17
+
18
+ def many(self, objs: Iterable[T]) -> list[Attrs]:
19
+ """
20
+ Simplify many objects into a list of their :class:`~chide.typing.Attrs`.
21
+ """
22
+ return [self.one(obj) for obj in objs]
23
+
24
+
25
+ _MARKER = object()
26
+
27
+
28
+ class ObjectSimplifier(Simplifier[object]):
29
+ """
30
+ A simplifier that can extract attributes from :class:`object`-based
31
+ classes that have either a ``__dict__`` or ``__slots__``.
32
+ """
33
+
34
+ def one(self, obj: object) -> Attrs:
35
+ attrs = {}
36
+ slots = set()
37
+ for class_ in type(obj).__mro__:
38
+ class_slots = getattr(class_, '__slots__', None)
39
+ if class_slots is not None:
40
+ slots.update(class_slots)
41
+ for attr in sorted(slots):
42
+ value = getattr(obj, attr, _MARKER)
43
+ if value is not _MARKER:
44
+ attrs[attr] = value
45
+ try:
46
+ attrs.update(vars(obj))
47
+ except TypeError:
48
+ pass
49
+ return attrs
chide/sqlalchemy.py ADDED
@@ -0,0 +1,57 @@
1
+ from typing import Any, Type
2
+
3
+ from sqlalchemy import inspect, Row
4
+ from sqlalchemy.orm import DeclarativeBase
5
+
6
+ from .simplifiers import Simplifier, T, ObjectSimplifier
7
+ from .set import Set as BaseSet
8
+ from .typing import Attrs
9
+
10
+
11
+ class Set(BaseSet):
12
+ """
13
+ A specialised :class:`chide.Set` for getting sample declaratively
14
+ mapped objects when using SQLAlchemy.
15
+ """
16
+
17
+ @staticmethod
18
+ def identify(type_: Type[Any], attrs: Attrs) -> tuple[Any, ...] | None:
19
+ """
20
+ This method returns the primary key that will be used for the
21
+ returned object, meaning that only one sample object will exist
22
+ and be returned for each primary key in a table.
23
+
24
+ If any element of the primary key is ``None``, a new object
25
+ is always returned.
26
+ """
27
+ mapper = inspect(type_)
28
+ key = [type_]
29
+ for prop in mapper._identity_key_props:
30
+ value = attrs.get(prop.key)
31
+ if value is None:
32
+ # no primary key, so we always get a new object...
33
+ return None
34
+ key.append(value)
35
+ return tuple(key)
36
+
37
+
38
+ class RowSimplifier(Simplifier[Row[Any]]):
39
+ """
40
+ A simplifier for SQLAlchemy :class:`~sqlalchemy.engine.Row` objects.
41
+ """
42
+
43
+ def one(self, row: Row[Any]) -> Attrs:
44
+ return row._asdict()
45
+
46
+
47
+ class MappedSimplifier(Simplifier[DeclarativeBase]):
48
+ """
49
+ A simplifier for SQLAlchemy ORM-mapped objects.
50
+ """
51
+
52
+ def __init__(self) -> None:
53
+ self._obj_simplifier = ObjectSimplifier()
54
+
55
+ def one(self, obj: DeclarativeBase) -> Attrs:
56
+ state = inspect(obj)
57
+ return {a.key: a.value for a in state.attrs}
chide/typing.py ADDED
@@ -0,0 +1,7 @@
1
+ from typing import TypeAlias, Any, Callable, Type, Hashable
2
+
3
+ #: A dictionary of attributes that can be used to create a sample object
4
+ Attrs: TypeAlias = dict[str, Any]
5
+
6
+ #: A callable for uniquely identifying a sample object in a :class:`~chide.Set`
7
+ Identifier: TypeAlias = Callable[[Type[Any], Attrs], Hashable | None]
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2016 onwards Chris Withers
2
+
3
+ Permission is hereby granted, free of charge, to any person
4
+ obtaining a copy of this software and associated documentation
5
+ files (the "Software"), to deal in the Software without restriction,
6
+ including without limitation the rights to use, copy, modify, merge,
7
+ publish, distribute, sublicense, and/or sell copies of the Software,
8
+ and to permit persons to whom the Software is furnished to do so,
9
+ subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
16
+ OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
18
+ BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
19
+ ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,94 @@
1
+ Metadata-Version: 2.1
2
+ Name: chide
3
+ Version: 3.0.0
4
+ Summary: Quickly create sample objects from data.
5
+ Home-page: https://github.com/cjw296/chide
6
+ Author: Chris Withers
7
+ Author-email: chris@withers.org
8
+ License: MIT
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.11
14
+ License-File: LICENSE.txt
15
+ Provides-Extra: build
16
+ Requires-Dist: setuptools-git ; extra == 'build'
17
+ Requires-Dist: twine ; extra == 'build'
18
+ Requires-Dist: wheel ; extra == 'build'
19
+ Provides-Extra: docs
20
+ Requires-Dist: furo ; extra == 'docs'
21
+ Requires-Dist: sphinx ; extra == 'docs'
22
+ Requires-Dist: sqlalchemy ; extra == 'docs'
23
+ Provides-Extra: test
24
+ Requires-Dist: mypy ; extra == 'test'
25
+ Requires-Dist: pytest ; extra == 'test'
26
+ Requires-Dist: pytest-cov ; extra == 'test'
27
+ Requires-Dist: sybil ; extra == 'test'
28
+ Requires-Dist: testfixtures ; extra == 'test'
29
+ Requires-Dist: sqlalchemy ; extra == 'test'
30
+
31
+ Chide
32
+ =====
33
+
34
+ |CircleCI|_ |Docs|_
35
+
36
+ .. |CircleCI| image:: https://circleci.com/gh/cjw296/chide/tree/master.svg?style=shield
37
+ .. _CircleCI: https://circleci.com/gh/cjw296/tree/chide
38
+
39
+ .. |Docs| image:: https://readthedocs.org/projects/chide/badge/?version=latest
40
+ .. _Docs: http://chide.readthedocs.org/en/latest/
41
+
42
+ Quickly create and compare sample objects.
43
+
44
+ Chide's philosophy is to give you a simple registry of parameters
45
+ needed to instantiate objects for your tests.
46
+ There's also support for simplifying objects down to mappings of their attributes
47
+ for easier comparison and rendering, along with parsing and rendering of formats
48
+ for inserting or asserting about multiple objects that are naturally tabular.
49
+
50
+ Quickstart
51
+ ~~~~~~~~~~
52
+
53
+ Say we have two classes that each require two parameters in order to
54
+ be instantiated:
55
+
56
+ .. code-block:: python
57
+
58
+ from dataclasses import dataclass
59
+
60
+ @dataclass
61
+ class ClassOne:
62
+ x: int
63
+ y: int
64
+
65
+ @dataclass
66
+ class ClassTwo:
67
+ a: int
68
+ b: ClassOne
69
+
70
+ We can set up a registry of sample values as follows:
71
+
72
+ .. code-block:: python
73
+
74
+ from chide import Collection
75
+
76
+ samples = Collection({
77
+ ClassOne: {'x': 1, 'y': 2},
78
+ ClassTwo: {'a': 1, 'b': ClassOne},
79
+ })
80
+
81
+ Now we can quickly make sample objects:
82
+
83
+ >>> samples.make(ClassOne)
84
+ ClassOne(x=1, y=2)
85
+
86
+ We can provide our own overrides if we want:
87
+
88
+ >>> samples.make(ClassOne, y=3)
89
+ ClassOne(x=1, y=3)
90
+
91
+ We can also create nested trees of objects:
92
+
93
+ >>> samples.make(ClassTwo)
94
+ ClassTwo(a=1, b=ClassOne(x=1, y=2))
@@ -0,0 +1,13 @@
1
+ chide/__init__.py,sha256=tzfn3LuNeHzaEOQNam4RGtiGYHkNy0oJ195390MGof0,90
2
+ chide/collection.py,sha256=-JxfjClKy8kpjtUOv38H560mBUv4DT9vfwBKu5Bjq5w,2816
3
+ chide/factory.py,sha256=DvGHoKJgMSUnZderYKvoretdxwksyjJw39Bna3AcELM,1998
4
+ chide/formats.py,sha256=PKm9Rq1p-QQz97f_JCQeCvcyHF3B4PGC_fCj3jpUdJU,17031
5
+ chide/set.py,sha256=WWFJJU9TpqxUJZymDXyUG3MIYChwj3dq04u3w2DGYx0,2539
6
+ chide/simplifiers.py,sha256=-LRvTeyRHfSxP60mzozbIHT00MCPhh95AVNqbQC2MaA,1305
7
+ chide/sqlalchemy.py,sha256=QqPyAFx_0yLZXAJCJ0CdJOE79L3du3HNa4nyMd6Jduo,1659
8
+ chide/typing.py,sha256=HLRLFSQL975FByveENpeRoskLAQIDiNeCZI5dyaXfiE,319
9
+ chide-3.0.0.dist-info/LICENSE.txt,sha256=s86hR15Wsne7Yq8TE5oXlQKejwdUh4LxUKiXHtMsB78,1079
10
+ chide-3.0.0.dist-info/METADATA,sha256=VS3LZtgpsQbKynHfuNcfqPE73A3uBzwLmmnEdJIW67U,2527
11
+ chide-3.0.0.dist-info/WHEEL,sha256=GJ7t_kWBFywbagK5eo9IoUwLW6oyOeTKmQ-9iHFVNxQ,92
12
+ chide-3.0.0.dist-info/top_level.txt,sha256=59D5gEv53fg1VYjdlnkbkQymGB8xea5-Z56Y_7t5wyk,6
13
+ chide-3.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: bdist_wheel (0.43.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ chide