wonka 0.1.2__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.
wonka/__init__.py ADDED
@@ -0,0 +1,47 @@
1
+ """Flexible, accessible, extensible Python factories"""
2
+
3
+ from __future__ import annotations
4
+
5
+ __version__ = '0.1.2'
6
+
7
+ __author__: str = 'Corey Rayburn Yung'
8
+
9
+ __all__: list[str] = [
10
+ 'Assembler',
11
+ 'Classer',
12
+ 'Delegate',
13
+ 'Factory',
14
+ 'Flexer',
15
+ 'Instancer',
16
+ 'Manager',
17
+ 'Manufacturer',
18
+ 'Producer',
19
+ 'Registrar',
20
+ 'Scribe',
21
+ 'Sourcerer',
22
+ 'Subclasser',
23
+ 'finalize',
24
+ 'inject_attributes',
25
+ 'is_constructor',
26
+ 'set_compatibility_rule',
27
+ 'set_keyer',
28
+ 'set_method_namer',
29
+ 'set_overwrite_rule',
30
+ 'set_verbose_rule']
31
+
32
+
33
+ from .base import Factory, Manager, Producer
34
+ from .configuration import (
35
+ set_compatibility_rule,
36
+ set_keyer,
37
+ set_method_namer,
38
+ set_overwrite_rule,
39
+ set_verbose_rule,
40
+ )
41
+ from .dispatchers import Delegate, Sourcerer
42
+ from .managers import Assembler
43
+ from .producers import Classer, Flexer, Instancer
44
+ from .prototypers import Scribe
45
+ from .registries import Registrar, Subclasser
46
+ from .shared import finalize, inject_attributes, is_constructor
47
+ from .storage import Manufacturer
wonka/base.py ADDED
@@ -0,0 +1,165 @@
1
+ """Base classes for `wonka` constructors.
2
+
3
+ Contents:
4
+ Factory (`abc.ABC`): interface for basic `wonka` creation classes. A
5
+ `create` class method is required for subclasses.
6
+ Manager (`Iterable`, `abc.ABC`): iterable interface for complex construction
7
+ managers. A `manage` instance method is required for subclasses. For
8
+ compatibility as a `wonka` constructor, a `create` property is included
9
+ which automatically calls the `manage` method with all args and kwargs.
10
+ Producer (`abc.ABC`): mixin interface for classes that alter created items
11
+ before returning them. A `produce` class method is required for
12
+ subclasses.
13
+ Constructor (`TypeAlias`): type alias for a wonka-compatible constructor
14
+ type. By default, it includes a `Factory` subclass, a `Factory` subclass
15
+ instance, and a `Manager` subclass instance.
16
+
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import abc
22
+ import dataclasses
23
+ from collections.abc import Hashable, Iterable, Iterator, MutableMapping
24
+ from typing import Any, TypeAlias
25
+
26
+
27
+ @dataclasses.dataclass
28
+ class Factory(abc.ABC):
29
+ """Base for `wonka` constructors.
30
+
31
+ A `wonka` `Factory` can be subclassed into any constructer design (not just
32
+ those that fit the classical factory design pattern). So, for example, the
33
+ `wonka` package itself includes `Factory` subclasses that fit the prototype
34
+ (`Scribe`), registry (`Registar` and` Subclasser`), and traditional
35
+ (`Delegate` and `Sourcerer`) design patterns. Further, a `Manager` class
36
+ instance may act as the director in a builder design pattern.
37
+
38
+ One of the goals of `wonka`, though, is not be be wedded to or worried about
39
+ the underlying design pattern. Instead, all constructers follow the simple,
40
+ universal, and easily extensible interface of `Factory`.
41
+
42
+ If you want to add code that modifies output of a `Factory`'s `create` class
43
+ method, you can either include that in the subclass `create` method or by
44
+ mixing in a `Producer` class. Details on how to use `Producer` subclasses
45
+ are included in its documentation.
46
+
47
+ """
48
+
49
+ """ Required Subclass Methods """
50
+
51
+ @classmethod
52
+ @abc.abstractmethod
53
+ def create(cls, item: Any, *args: Any, **kwargs: Any) -> Any:
54
+ """Returns a created or modified item.
55
+
56
+ Args:
57
+ item: data for creation of an item or an item to be modified.
58
+ args: allows subclass to take args.
59
+ kwargs: allows subclass to take kwargs.
60
+
61
+ Returns:
62
+ Any: created or modified item.
63
+
64
+ """
65
+
66
+
67
+ @dataclasses.dataclass
68
+ class Manager(Iterable, abc.ABC):
69
+ """Base for manageing complex class or object construction.
70
+
71
+ Args:
72
+ contents: an iterable containing `Factory` subclasses or `Manager`
73
+ subclass instances.
74
+
75
+ """
76
+
77
+ contents: Iterable
78
+
79
+ """ Required Subclass Methods """
80
+
81
+ @abc.abstractmethod
82
+ def manage(self, item: Any, *args: Any, **kwargs: Any) -> Any:
83
+ """Manages construction and/or modification based on `item`.
84
+
85
+ Args:
86
+ item: item to be passed to factories in `contents`.
87
+ args: allows subclass to take args.
88
+ kwargs: allows subclass to take kwargs.
89
+
90
+ Returns:
91
+ Any: constructed item.
92
+
93
+ """
94
+
95
+ """ Properties """
96
+
97
+ @property
98
+ def create(self, *args: Any, **kwargs: Any) -> Any:
99
+ """Calls `manage` method with args and kwargs.
100
+
101
+ This property is included as a convenience so that an instance of a
102
+ `Manager` can be used as a drop-in for a `Factory` subclass. `Manager`
103
+ cannot easily be made a subclass for `Factory` because it will often
104
+ need to rely on instance data for construction. So, every `Manager`
105
+ subclass should be designed such that an instance of that subclass could
106
+ be substituted for a `Factory` subclass. This allows other `Manager`
107
+ subclass instances to be stored in `contents` as part of an iterable
108
+ workflow.
109
+
110
+ """
111
+ return self.manage(*args, **kwargs)
112
+
113
+ """ Dunder Methods """
114
+
115
+ def __iter__(self) -> Iterator:
116
+ """Returns iterable of `contents`.
117
+
118
+ `Manager` is agnostic as to the type of iterable that is used in order
119
+ to accomodate simple sequences, complex graphs, nested trees, or any
120
+ other workflow design. As a general practice, though, any mapping should
121
+ probably return `items()` so that the interface for iteration never
122
+ requires any appended method call. But nothing in `wonka` precludes a
123
+ different rule or practice.
124
+
125
+ """
126
+ return iter(self.contents)
127
+
128
+
129
+ @dataclasses.dataclass
130
+ class Producer(abc.ABC):
131
+ """Base mixin for modifying items.
132
+
133
+ A `Producer`'s `produce` method will automatically be called if it is
134
+ mixed-in with any of the `Factory` classes in `wonka`. If you want a custom
135
+ `Factory` subclass to similarly automatically check for a `produce` method,
136
+ the easiest way to do that is to simply call the `finalize` function as your
137
+ return value for the `Factory`'s `create` method as follows:
138
+
139
+ ```python
140
+ return wonka.finalize(item = item, parameters = parameters)
141
+ ```
142
+ """
143
+
144
+ """ Required Subclass Methods """
145
+
146
+ @classmethod
147
+ @abc.abstractmethod
148
+ def produce(
149
+ cls,
150
+ item: Any,
151
+ parameters: MutableMapping[Hashable, Any] | None = None) -> Any:
152
+ """Modifies `item` and possibly incorporates `parameters`.
153
+
154
+ Args:
155
+ item: item to be modified.
156
+ parameters: keyword arguments to pass or add to a created instance.
157
+ Defaults to `None`.
158
+
159
+ Returns:
160
+ Any: modified item.
161
+
162
+ """
163
+
164
+
165
+ Constructor: TypeAlias = Factory | type[Factory] | Manager
wonka/configuration.py ADDED
@@ -0,0 +1,132 @@
1
+ """Configuration settings and convenience functions for changing those settings.
2
+
3
+ Contents:
4
+ set_compatibility_rule: sets the global attribute compatibility rule.
5
+ set_keyer: sets the global default function used to name dict keys.
6
+ set_method_namer: sets the global default function used to name factory
7
+ creation methods.
8
+ set_overwrite_rule: sets the global attribute overwrite rule.
9
+ set_verbose_rule: sets the global attribute message verbosity rule.
10
+
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from collections.abc import Callable
15
+ from typing import Any
16
+
17
+ from . import utilities
18
+
19
+ # Default naming function for non-str objects.
20
+ _KEY_NAMER: Callable[[object | type[Any]], str] = utilities._namify
21
+ # Default naming convention for dispatcher registry creation methods.
22
+ _METHOD_NAMER: Callable[[object | type[Any]], str] = lambda x: f'from_{x}'
23
+ # Whether to overwrite existing attributes when arguments are passed to create
24
+ # an item that is already an instance or has class attributes of the same name
25
+ # as in the passed arguments.
26
+ _OVERWRITE: bool = True
27
+ # Whether to validate an object as a subclass of a `wonka`-constructor or to
28
+ # support duck typing by not validating an object before its use.
29
+ _STRICT_COMPATIBILITY: bool = True
30
+ # Whether to return more elaborate error messages and feedback.
31
+ _VERBOSE: bool = False
32
+
33
+
34
+ def set_compatibility_rule(compatibility: bool) -> None:
35
+ """Sets the global attribute compatibility rule.
36
+
37
+ Args:
38
+ compatibility: whether to require the `is_constructor` method to use
39
+ strict or relaxed validation.
40
+
41
+ Raises:
42
+ TypeError: if `compatibility` is not `bool`.
43
+
44
+ """
45
+ if isinstance(compatibility, bool):
46
+ globals()["_STRICT_COMPATIBILITY"] = compatibility
47
+ else:
48
+ raise TypeError('compatibility argument must be boolean')
49
+
50
+ def set_keyer(keyer: Callable[[object | type[Any]], str]) -> None:
51
+ """Sets the global default function used to name `dict` keys.
52
+
53
+ Args:
54
+ keyer: function that returns a `str` name of any item passed.
55
+
56
+ Raises:
57
+ TypeError: if `keyer` is not callable.
58
+
59
+ """
60
+ if isinstance(keyer, Callable):
61
+ globals()["_KEY_NAMER"] = keyer
62
+ else:
63
+ raise TypeError('keyer argument must be a callable')
64
+
65
+ def set_method_namer(namer: Callable[[object | type[Any]], str]) -> None:
66
+ """Sets the global default function used to name factory creation methods.
67
+
68
+ Args:
69
+ namer: function that returns a `str` name of any item passed.
70
+
71
+ Raises:
72
+ TypeError: if 'keyer' is not callable.
73
+
74
+ """
75
+ if isinstance(namer, Callable):
76
+ globals()["_METHOD_NAMER"] = namer
77
+ else:
78
+ raise TypeError('namer argument must be a callable')
79
+
80
+ def set_overwrite_rule(overwrite: bool) -> None:
81
+ """Sets the global attribute overwrite rule.
82
+
83
+ Args:
84
+ overwrite: whether to set the default rule to overwrite existing
85
+ attributes.
86
+
87
+ Raises:
88
+ TypeError: if `overwrite` is not bool.
89
+
90
+ """
91
+ if isinstance(overwrite, bool):
92
+ globals()["_OVERWRITE"] = overwrite
93
+ else:
94
+ raise TypeError('overwrite argument must be boolean')
95
+
96
+ def set_verbose_rule(verbose: bool) -> None:
97
+ """Sets the global attribute message verbosity rule.
98
+
99
+ Args:
100
+ verbose: whether to set the default rule to verbosity in logging and
101
+ messaging.
102
+
103
+ Raises:
104
+ TypeError: if `verbose` is not bool.
105
+
106
+ """
107
+ if isinstance(verbose, bool):
108
+ globals()["_VERBOSE"] = verbose
109
+ else:
110
+ raise TypeError('verbose argument must be boolean')
111
+
112
+
113
+ # @dataclasses.dataclass
114
+ # class _MISSING_VALUE(object):
115
+ # """Sentinel object for a missing data or parameter.
116
+
117
+ # This follows the same pattern as the '__MISSING_TYPE` class in the builtin
118
+ # dataclasses library.
119
+ # https://github.com/python/cpython/blob/3.10/Lib/dataclasses.py#L182-L186
120
+
121
+ # Because None is sometimes a valid argument or data option, this class
122
+ # provides an alternative that does not create the confusion that a default of
123
+ # None can sometimes lead to.
124
+
125
+ # """
126
+ # pass
127
+
128
+
129
+ # # _MISSING, instance of _MISSING_VALUE, should be used for missing values as an
130
+ # # alternative to None when None is a valid value for an argument. This provides
131
+ # # a fuller repr and traceback.
132
+ # _MISSING = _MISSING_VALUE()
wonka/dispatchers.py ADDED
@@ -0,0 +1,191 @@
1
+ """Dispatchers: factory classes that call other constructors.
2
+
3
+ Contents:
4
+ Delegate (`base.Factory`): builds classes and/or instances using methods
5
+ that follow a naming convention and the `str` names of the types of the
6
+ first argument passed to the `create` class method.
7
+ Sourcerer (`base.Factory`): builds classes and/or instances using methods
8
+ that follow a naming convention (set at `configuration._METHOD_NAMER`)
9
+ and a `dict` of types stored in the `sources` class attribute.
10
+
11
+ """
12
+ from __future__ import annotations
13
+
14
+ import abc
15
+ import dataclasses
16
+ import inspect
17
+ from typing import TYPE_CHECKING, Any, ClassVar
18
+
19
+ from . import base, configuration, shared
20
+
21
+ if TYPE_CHECKING:
22
+ from collections.abc import Callable, Hashable, MutableMapping
23
+
24
+
25
+ @dataclasses.dataclass
26
+ class Delegate(base.Factory):
27
+ """Builds based on the str name of the type passed.
28
+
29
+ This factory acts as a dispatcher to call creation methods based on the type
30
+ or name of the type passed in a manner identical to `Sourcerer`. However,
31
+ unlike `Sourcerer`, `Delegate` only finds a matching creation method if the
32
+ `str` name of the type of `item` matches a substring of the creation method
33
+ name using the format of `configuration._METHOD_NAMER`.
34
+
35
+ """
36
+
37
+ """ Class Methods """
38
+
39
+ @classmethod
40
+ def create(
41
+ cls,
42
+ item: Any,
43
+ parameters: MutableMapping[Hashable, Any] | None = None,
44
+ **kwargs: Any) -> Any:
45
+ """Creates an item based on `item` and possibly `parameters`.
46
+
47
+ Args:
48
+ item: data for construction of the returned item.
49
+ parameters: keyword arguments to pass or add to a created instance.
50
+ kwargs: allows subclass to take kwargs.
51
+
52
+ Raises:
53
+ AttributeError: If an appropriate method does not exist for the
54
+ data type of `item.`
55
+ KeyError: If a corresponding subclass does not exist for `item.`
56
+
57
+ Returns:
58
+ Created item.
59
+
60
+ """
61
+ builder = _get_creation_method_name(item)
62
+ item = _get_from_builder_method(
63
+ factory = cls,
64
+ method = builder,
65
+ source = item,
66
+ **kwargs)
67
+ return shared.finalize(item = item, parameters = parameters)
68
+
69
+
70
+ @dataclasses.dataclass
71
+ class Sourcerer(base.Factory, abc.ABC):
72
+ """Builds based on compatibility with keys in the `sources` class attribute.
73
+
74
+ This factory acts as a dispatcher to call other methods based on the type
75
+ passed. Unlike `Delegate`, `Sourcerer` is more forgiving by allowing the
76
+ type passed to a subtype or instance of the type listed as a key in the
77
+ `sources` class attribute.
78
+
79
+ The name for a `Sourcerer` is spelled the way it is instead of "Sorcerer"
80
+ because the `sources` attribute is used. This is inspired by the "Divinity:
81
+ Original Sin" games where the magic users are called "Sourcerers" because
82
+ they may manipulate the magical energy known as "source".
83
+ https://divinity.fandom.com/wiki/Sourcerer
84
+
85
+ Attributes:
86
+ sources: `dict` with keys that are types and values are substrings of
87
+ the names of methods to call when the key type is passed to the
88
+ `create` method. Defaults to an empty `dict`.
89
+
90
+ """
91
+
92
+ sources: ClassVar[MutableMapping[type[Any], str]] = {}
93
+
94
+ """ Class Methods """
95
+
96
+ @classmethod
97
+ def create(
98
+ cls,
99
+ item: Any,
100
+ parameters: MutableMapping[Hashable, Any] | None = None,
101
+ **kwargs: Any) -> Any:
102
+ """Creates an item based on `item` and possibly `parameters`.
103
+
104
+ Args:
105
+ item: data for construction of the returned item.
106
+ parameters: keyword arguments to pass or add to a created instance.
107
+ kwargs: allows subclass to add additional parameters.
108
+
109
+ Raises:
110
+ AttributeError: if the value matching the key `item` does not
111
+ correspond to a method in the `Sourcerer` subclass.
112
+ KeyError: if there is no key in `sources` matching the type for
113
+ `item`.
114
+
115
+ Returns:
116
+ Created item.
117
+
118
+ """
119
+ for kind, substring in cls.sources.items():
120
+ if _is_kind(item, kind):
121
+ builder = _get_creation_method_name(substring)
122
+ item = _get_from_builder_method(
123
+ factory = cls,
124
+ method = builder,
125
+ source = item,
126
+ **kwargs)
127
+ return shared.finalize(item = item, parameters = parameters)
128
+ raise KeyError(f'{item} does not match any recognized types')
129
+
130
+
131
+ def _get_creation_method_name(
132
+ source: Any,
133
+ method_namer: Callable[[object | type[Any]], str] | None = None) -> str:
134
+ """Returns the creation method name for factories that call other methods.
135
+
136
+ Args:
137
+ source: source data for creating a method name.
138
+ method_namer: callable to create the creation method name. Defaults to
139
+ `None`. If it is `None`, the global namer stored in
140
+ `configuration._METHOD_NAMER` will be used.
141
+
142
+ Returns:
143
+ Name of the creation method to use.
144
+
145
+ """
146
+ if not isinstance(source, str):
147
+ source = configuration._KEY_NAMER(source)
148
+ namer = method_namer or configuration._METHOD_NAMER
149
+ return namer(source)
150
+
151
+ def _is_kind(item: Any, kind: type[Any]) -> bool:
152
+ """Returns if `item` is an instance or subclass of `kind`.
153
+
154
+ Args:
155
+ item (Any): item to evalute.
156
+ kind (type[Any]): type to compare `item` to.
157
+
158
+ Returns:
159
+ Whether `item` is an instance or subclass of `kind`.
160
+
161
+ """
162
+ return (
163
+ isinstance(item, kind)
164
+ or (inspect.isclass(item and issubclass(item, kind))))
165
+
166
+ def _get_from_builder_method(
167
+ factory: Any,
168
+ method: str,
169
+ source: Any,
170
+ **kwargs: Any) -> Any:
171
+ """Returns constructed item from a builder method of `factory`.
172
+
173
+ Args:
174
+ factory: factory class or instance.
175
+ method : name of the method to use to construct an item.
176
+ source: the `source` data used to create item.
177
+ kwargs: allows subclass to take kwargs.
178
+
179
+ Raises:
180
+ AttributeError: if `factory` has no method named `method`.
181
+
182
+
183
+ Returns:
184
+ Constructed item.
185
+
186
+ """
187
+ try:
188
+ builder = getattr(factory, method)
189
+ return builder(source, **kwargs)
190
+ except AttributeError as e:
191
+ raise AttributeError(f'{method} does not exist in {factory}') from e