-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
/
_craft.py
176 lines (135 loc) · 5.34 KB
/
_craft.py
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
# copyright: sktime developers, BSD-3-Clause License (see LICENSE file)
"""Quick crafting methods to build an object from string and registry.
craft(spec)
craft an object or estimator from string
deps(spec)
retrieves all dependencies required to craft str, in PEP440 format
The ``craft`` function is a pair to ``str`` coercion, the two can be seen as
deserialization/serialization counterparts to each other.
That is,
spec = str(my_est)
new_est = craft(spec)
will have the same effect as new_est = spec.clone()
"""
__author__ = ["fkiraly"]
import re
from sktime.registry._lookup import all_estimators
def _extract_class_names(spec):
"""Get all maximal alphanumeric substrings that start with a capital letter.
Parameters
----------
spec : str (any)
Returns
-------
cls_name_list : list of str
list of all maximal alphanumeric substrings starting with a capital in ``spec``
excluding reserved expressions True, False (if they occur)
"""
# class names are all UpperCamelCase alphanumeric strings
# which is the same as maximal substrings starting with a capital
pattern = r"\b([A-Z][A-Za-z0-9_]*)\b"
cls_name_list = re.findall(pattern, spec)
# we need to exclude expressions that look like classes per the regex
# but aren't
EXCLUDE_LIST = ["True", "False"]
cls_name_list = [x for x in cls_name_list if x not in EXCLUDE_LIST]
return cls_name_list
def craft(spec):
"""Instantiate an object from the specification string.
Parameters
----------
spec : str, sktime/skbase compatible object specification
i.e., a string that executes to construct an object if all imports were present
imports inferred are of any classes in the scope of ``all_estimators``
option 1: a string that evaluates to an estimator
option 2: a sequence of assignments in valid python code,
with the object to be defined preceded by a "return"
assignments can use names of classes as if all imports were present
Returns
-------
obj : skbase BaseObject descendant, constructed from ``spec``
this will have the property that ``spec == str(obj)`` (up to formatting)
"""
register = dict(all_estimators()) # noqa: F841
for x in _extract_class_names(spec):
exec(f"{x} = register['{x}']")
try:
obj = eval(spec)
except Exception:
from textwrap import indent
spec_fun = indent(spec, " ")
spec_fun = (
"""
def build_obj():
"""
+ spec_fun
)
exec(spec_fun, locals())
obj = eval("build_obj()")
return obj
def deps(spec):
"""Get PEP 440 dependency requirements for a craft spec.
Parameters
----------
spec : str, sktime/skbase compatible object specification
i.e., a string that executes to construct an object if all imports were present
imports inferred are of any classes in the scope of ``all_estimators``
option 1: a string that evaluates to an estimator
option 2: a sequence of assignments in valid python code,
with the object to be defined preceded by a "return"
assignments can use names of classes as if all imports were present
Returns
-------
reqs : list of str
each str is PEP 440 compatible requirement string for craft(spec)
if spec has no requirements, return is [], the length 0 list
"""
register = dict(all_estimators())
dep_strs = []
for x in _extract_class_names(spec):
if x not in register.keys():
raise RuntimeError(
f"class {x} is required to build spec, but was not found "
"in all_estimators scope"
)
cls = register[x]
new_deps = cls.get_class_tag("python_dependencies")
if isinstance(new_deps, list):
dep_strs += new_deps
elif isinstance(new_deps, str) and len(new_deps) > 0:
dep_strs += [new_deps]
reqs = list(set(dep_strs))
return reqs
def imports(spec):
"""Get import code block for a craft spec.
Parameters
----------
spec : str, sktime/skbase compatible object specification
i.e., a string that executes to construct an object if all imports were present
imports inferred are of any classes in the scope of ``all_estimators``
option 1: a string that evaluates to an estimator
option 2: a sequence of assignments in valid python code,
with the object to be defined preceded by a "return"
assignments can use names of classes as if all imports were present
Returns
-------
import_str : str
python code consisting of all import statements required for spec
imports cover object/estimator classes found as sub-strings of spec
"""
register = dict(all_estimators())
import_strs = []
for x in _extract_class_names(spec):
if x not in register.keys():
raise RuntimeError(
f"class {x} is required to build spec, but was not found "
"in all_estimators scope"
)
cls = register[x]
import_str = f"from {cls.__module__} import {x}"
import_strs += [import_str]
if len(import_strs) == 0:
imports_str = ""
else:
imports_str = "\n".join(sorted(import_strs))
return imports_str