engforge 0.3.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.
Files changed (69) hide show
  1. engforge/__init__.py +26 -0
  2. engforge/_testing_components.py +864 -0
  3. engforge/analysis.py +257 -0
  4. engforge/attr_dynamics.py +222 -0
  5. engforge/attr_plotting.py +811 -0
  6. engforge/attr_signals.py +116 -0
  7. engforge/attr_slots.py +211 -0
  8. engforge/attr_solver.py +443 -0
  9. engforge/attributes.py +326 -0
  10. engforge/common.py +76 -0
  11. engforge/component_collections.py +276 -0
  12. engforge/components.py +55 -0
  13. engforge/configuration.py +673 -0
  14. engforge/dataframe.py +185 -0
  15. engforge/datastores/__init__.py +116 -0
  16. engforge/datastores/data.py +519 -0
  17. engforge/datastores/gdocs.py +2650 -0
  18. engforge/datastores/reporting.py +935 -0
  19. engforge/datastores/secrets.py +549 -0
  20. engforge/dynamics.py +662 -0
  21. engforge/eng/__init__.py +0 -0
  22. engforge/eng/costs.py +1459 -0
  23. engforge/eng/fluid_material.py +279 -0
  24. engforge/eng/geometry.py +1060 -0
  25. engforge/eng/pipes.py +1211 -0
  26. engforge/eng/prediction.py +366 -0
  27. engforge/eng/solid_materials.py +445 -0
  28. engforge/eng/structure.py +2228 -0
  29. engforge/eng/structure_beams.py +821 -0
  30. engforge/eng/thermodynamics.py +271 -0
  31. engforge/engforge_attributes.py +380 -0
  32. engforge/env_var.py +214 -0
  33. engforge/locations.py +18 -0
  34. engforge/logging.py +290 -0
  35. engforge/patterns.py +449 -0
  36. engforge/problem_context.py +2251 -0
  37. engforge/properties.py +501 -0
  38. engforge/reporting.py +595 -0
  39. engforge/solveable.py +964 -0
  40. engforge/solver.py +316 -0
  41. engforge/solver_utils.py +726 -0
  42. engforge/system.py +143 -0
  43. engforge/system_reference.py +299 -0
  44. engforge/tabulation.py +288 -0
  45. engforge/test/__init__.py +2 -0
  46. engforge/test/_pre_test_structures.py +221 -0
  47. engforge/test/report_testing.py +158 -0
  48. engforge/test/test_airfilter.py +91 -0
  49. engforge/test/test_analysis.py +3 -0
  50. engforge/test/test_comp_iter.py +196 -0
  51. engforge/test/test_composition.py +139 -0
  52. engforge/test/test_costs.py +338 -0
  53. engforge/test/test_dynamics.py +109 -0
  54. engforge/test/test_dynamics_spaces.py +39 -0
  55. engforge/test/test_four_bar.py +97 -0
  56. engforge/test/test_modules.py +72 -0
  57. engforge/test/test_performance.py +93 -0
  58. engforge/test/test_pipes.py +49 -0
  59. engforge/test/test_problem.py +411 -0
  60. engforge/test/test_problem_deepscoping.py +68 -0
  61. engforge/test/test_slider_crank.py +51 -0
  62. engforge/test/test_solver.py +343 -0
  63. engforge/test/test_tabulation.py +233 -0
  64. engforge/typing.py +92 -0
  65. engforge-0.3.0.dist-info/METADATA +392 -0
  66. engforge-0.3.0.dist-info/RECORD +69 -0
  67. engforge-0.3.0.dist-info/WHEEL +5 -0
  68. engforge-0.3.0.dist-info/licenses/LICENSE +7 -0
  69. engforge-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,673 @@
1
+ import attr, attrs
2
+
3
+ from engforge.engforge_attributes import AttributedBaseMixin
4
+ from engforge.logging import LoggingMixin, log, change_all_log_levels
5
+ from engforge.properties import *
6
+ from engforge.env_var import EnvVariable
7
+ import randomname
8
+ import datetime
9
+ import copy
10
+
11
+
12
+ # make a module logger
13
+ class ConfigLog(LoggingMixin):
14
+ pass
15
+
16
+
17
+ log = ConfigLog()
18
+
19
+ conv_nms = lambda v: v.split(",") if isinstance(v, str) else v
20
+ NAME_ADJ = EnvVariable(
21
+ "FORGE_NAME_ADJ",
22
+ default=(
23
+ "geometry",
24
+ "size",
25
+ "algorithms",
26
+ "complexity",
27
+ "colors",
28
+ "materials",
29
+ ),
30
+ type_conv=conv_nms,
31
+ )
32
+ NAME_NOUN = EnvVariable(
33
+ "FORGE_NAME_NOUN",
34
+ default=(
35
+ "chemistry",
36
+ "astronomy",
37
+ "linear_algebra",
38
+ "geometry",
39
+ "coding",
40
+ "corporate_job",
41
+ "design",
42
+ "car_parts",
43
+ "machine_learning",
44
+ "physics_units",
45
+ ),
46
+ type_conv=conv_nms,
47
+ )
48
+ FORGE_DEBUG = EnvVariable(
49
+ "FORGE_LOG_LEVEL",
50
+ default=20,
51
+ type_conv=int,
52
+ )
53
+
54
+
55
+ def name_generator(instance):
56
+ """a name generator for the instance"""
57
+ base = str(instance.__class__.__name__).lower() + "-"
58
+ if instance.__class__._use_random_name:
59
+ out = base + randomname.get_name(adj=NAME_ADJ.secret, noun=NAME_NOUN.secret)
60
+ else:
61
+ out = base
62
+ log.debug(f"generated name: {out}")
63
+ return out
64
+
65
+
66
+ PROTECTED_NAMES = [
67
+ "solver",
68
+ "dataframe",
69
+ "_anything_changed",
70
+ "time",
71
+ "run_id",
72
+ "converged",
73
+ ]
74
+
75
+
76
+ # Wraps Configuration with a decorator, similar to @attrs.define(**options)
77
+ def forge(cls=None, **kwargs):
78
+ """Wrap all Configurations with this decorator with the following behavior
79
+ 1) we use the callback when any property changes
80
+ 2) repr is default
81
+ 3) hash is by object identity"""
82
+
83
+ # Define defaults and handle conflicts
84
+ dflts = dict(repr=False, eq=False, slots=False, kw_only=True, hash=False)
85
+ for k, v in kwargs.items():
86
+ if k in dflts:
87
+ dflts.pop(k)
88
+
89
+ if cls is not None:
90
+ # we can't import system here since cls might be system, so look for any system subclasses
91
+ if "System" in [c.__name__ for c in cls.mro()]:
92
+ log.info(f"Configuring System: {cls.__name__}")
93
+ acls = attr.s(
94
+ cls,
95
+ on_setattr=property_changed,
96
+ field_transformer=signals_slots_handler,
97
+ **dflts,
98
+ **kwargs,
99
+ )
100
+
101
+ # must be here since can't inspect till after fields corrected
102
+ acls.pre_compile() # custom class compiler
103
+ acls.validate_class()
104
+ if acls.__name__ != "Configuration": # prevent configuration lookup
105
+ acls.compile_classes() # compile subclasses
106
+ return acls
107
+
108
+ # Component/Config Flow
109
+ log.msg(f"Configuring: {cls.__name__}")
110
+ acls = attr.s(
111
+ cls,
112
+ on_setattr=property_changed,
113
+ field_transformer=comp_transform,
114
+ **dflts,
115
+ **kwargs,
116
+ )
117
+ if FORGE_DEBUG.in_env:
118
+ change_all_log_levels(inst=acls, new_log_level=FORGE_DEBUG.secret)
119
+ # must be here since can't inspect till after fields corrected
120
+ acls.pre_compile() # custom class compiler
121
+ acls.validate_class()
122
+ if acls.__name__ != "Configuration": # prevent configuration lookup
123
+ acls.compile_classes() # compile subclasses
124
+ return acls
125
+
126
+ else:
127
+
128
+ def f(cls, *args):
129
+ return forge(cls, **kwargs)
130
+
131
+ f.__name__ == "forge_wrapper"
132
+
133
+ return f
134
+
135
+
136
+ def meta(title, desc=None, **kwargs):
137
+ """a convienience wrapper to add metadata to attr.ib
138
+ :param title: a title that gets formatted for column headers
139
+ :param desc: a description of the property"""
140
+ out = {
141
+ "label": title.replace(".", "_").replace("-", "_").title(),
142
+ "desc": None,
143
+ **kwargs,
144
+ }
145
+ return out
146
+
147
+
148
+ # Class Definition Wrapper Methods
149
+ def property_changed(instance, variable, value):
150
+ """a callback for when a property changes, this will set the _anything_changed flag to True, and change value when appropriate"""
151
+ from engforge.tabulation import TabulationMixin
152
+ from engforge.problem_context import ProblemExec
153
+
154
+ # TODO: determine when, in an active problem if components change, and update refs accordingly!
155
+
156
+ if not isinstance(instance, (TabulationMixin)):
157
+ return value
158
+
159
+ # Establish Active Session
160
+ session = None
161
+ if hasattr(ProblemExec.class_cache, "session"):
162
+ session = ProblemExec.class_cache.session
163
+
164
+ if not session and instance._anything_changed:
165
+ # Bypass Check since we've already flagged for an update
166
+ if log.log_level <= 2:
167
+ log.debug(f"already property changed {instance}{variable.name} {value}")
168
+ return value
169
+
170
+ # elif session:
171
+ # notify session that this variable has changed
172
+ # log.info(f'property changed {variable.name} {value}')
173
+
174
+ # TODO: notify change input system here, perhaps with & without a session
175
+ # session.change_sys_var(variable,value,doset=False)
176
+ attrs = attr.fields(instance.__class__) # check identity of variable
177
+ cur = getattr(instance, variable.name)
178
+ is_different = value != cur if isinstance(value, (int, float, str)) else True
179
+ is_var = variable in attrs
180
+ chgnw = instance._anything_changed
181
+
182
+ if log.log_level <= 6:
183
+ log.debug(
184
+ f"checking property changed {instance}{variable.name} {value}|invar: {is_var}| nteqval: {is_different}"
185
+ )
186
+ print(
187
+ f"checking property changed {instance}{variable.name} {value}|invar: {is_var}| nteqval: {is_different}"
188
+ )
189
+
190
+ # Check if should be updated
191
+ if not chgnw and is_var and is_different:
192
+ if log.log_level < 5:
193
+ log.debug(f"changing variables: {variable.name} {value}")
194
+ instance._anything_changed = True
195
+
196
+ elif log.log_level < 4 and is_var and not is_different:
197
+ log.warning(f"variables same {variable.name}| {value} == {cur}")
198
+
199
+ # If active session in dynamic mode and the component is dynamic, flag for matrix update
200
+ # TODO: determine if dynamic matricies affected by this.
201
+ if session and session.dynamic_solve and instance.is_dynamic:
202
+ # log.info(f'dynamics changed')
203
+ session.dynamics_updated = True # flag for update
204
+
205
+ elif session:
206
+ # log.info(f'static change')
207
+ session.dynamics_updated = False
208
+
209
+ return value
210
+
211
+
212
+ # Class Definition Wrapper Methods
213
+ def signals_slots_handler(
214
+ cls, fields, slots=True, signals=True, solvers=True, sys=True, plots=True
215
+ ):
216
+ """
217
+ creates attributes as per the attrs.define field_transformer use case.
218
+
219
+ Customize initalization with slots,signals,solvers and sys flags.
220
+ """
221
+ from engforge.problem_context import ProblemExec
222
+
223
+ log.debug(f"transforming signals and slots for {cls.__name__}")
224
+
225
+ # add attrs attributes from mro parent class
226
+ # TODO: work on this to allow attrs from non attrs classes
227
+ # fd = {f.name:f for f in fields}
228
+ # has_fields = set(list(fd.keys()))
229
+ #
230
+ # if cls._inherit_parent_attributres:
231
+ # non_dec_attrs = {} #get all attrs variables from non-decorated mixins
232
+ # for icls in cls.mro():
233
+ # if attrs.has(icls):
234
+ # print(icls,str(list(attrs.fields_dict(icls).keys()))[:100])
235
+ # atr_dict = {k:v.evolve(icls) for k,v in icls.__dict__.items() if isinstance(v,attrs.Attribute)}
236
+ # non_dec_attrs.update({k:v for k,v in atr_dict.items() if k not in non_dec_attrs and k not in has_fields})
237
+ # if non_dec_attrs:
238
+ # log.info(f'adding non decorated attrs: {list(non_dec_attrs.keys())}')
239
+ # fields = fields + list(non_dec_attrs.values())
240
+
241
+ # Fields
242
+ for t in fields:
243
+ if t.name in PROTECTED_NAMES:
244
+ raise Exception(f"cannot use {t.name} as a field name, its protected")
245
+ if t.type is None:
246
+ log.warning(f"{cls.__name__}.{t.name} has no type")
247
+
248
+ out = []
249
+ field_names = set([o.name for o in fields])
250
+ log.debug(f"fields: {field_names}")
251
+
252
+ # Add Fields (no underscored fields)
253
+ in_fields = {f.name: f for f in fields if not f.name.startswith("_")}
254
+ if "name" in in_fields:
255
+ name = in_fields.pop("name")
256
+ out.append(name)
257
+
258
+ else:
259
+ log.warning(f"{cls.__name__} does not have a name!")
260
+ name = attrs.Attribute(
261
+ name="name",
262
+ default=attrs.Factory(name_generator, True),
263
+ validator=None,
264
+ repr=True,
265
+ cmp=None,
266
+ eq=True,
267
+ eq_key=None,
268
+ order=True,
269
+ order_key=None,
270
+ hash=None,
271
+ init=True,
272
+ metadata=None,
273
+ type=str,
274
+ converter=None,
275
+ kw_only=True,
276
+ inherited=True,
277
+ on_setattr=None,
278
+ alias="name",
279
+ )
280
+ out.append(name)
281
+
282
+ # Add Slots
283
+ if slots:
284
+ for slot_name, slot in cls.slots_attributes().items():
285
+ at = slot.make_attribute(slot_name, cls)
286
+ out.append(at)
287
+
288
+ # Add Signals
289
+ if signals:
290
+ for signal_name, signal in cls.signals_attributes().items():
291
+ at = signal.make_attribute(signal_name, cls)
292
+ out.append(at)
293
+
294
+ # Add SOLVERS
295
+ if solvers:
296
+ for solver_name, solver in cls.solvers_attributes().items():
297
+ at = solver.make_attribute(solver_name, cls)
298
+ out.append(at)
299
+
300
+ # Add Time
301
+ for solver_name, solver in cls.transients_attributes().items():
302
+ # add from cls since not accessible from attrs
303
+ at = solver.make_attribute(solver_name, cls)
304
+ out.append(at)
305
+
306
+ if plots:
307
+ for pltname, plot in cls.plot_attributes().items():
308
+ at = plot.make_attribute(pltname, cls)
309
+ out.append(at)
310
+
311
+ for pltname, plot in cls.trace_attributes().items():
312
+ at = plot.make_attribute(pltname, cls)
313
+ out.append(at)
314
+
315
+ created_fields = set([o.name for o in out])
316
+
317
+ # Merge Fields Checking if we are overriding an attribute with system_property
318
+ # hack since TabulationMixin isn't available yet
319
+ if "TabulationMixin" in str(cls.mro()):
320
+ cls_properties = cls.system_properties_classdef(True)
321
+ else:
322
+ cls_properties = {}
323
+
324
+ cls_dict = cls.__dict__.copy()
325
+ cls.__anony_store = {}
326
+ # print(f'tab found!! {cls_properties.keys()}')
327
+ for k, o in in_fields.items():
328
+ if k not in created_fields:
329
+ if k in cls_properties and o.inherited:
330
+ log.warning(
331
+ f"{cls.__name__} overriding inherited attr: {o.name} as a system property overriding it"
332
+ )
333
+ elif o.inherited and k in cls_dict:
334
+ log.warning(
335
+ f"{cls.__name__} overriding inherited attr: {o.name} as {cls_dict[k]} in cls"
336
+ )
337
+ # FIXME: should we deepcopy?
338
+ val = copy.copy(cls_dict[k])
339
+ cls.__anony_store[k] = sscb = lambda: val
340
+ out.append(o.evolve(default=attrs.Factory(sscb)))
341
+ else:
342
+ log.msg(f"{cls.__name__} adding attr: {o.name}")
343
+ out.append(o)
344
+ else:
345
+ log.warning(
346
+ f"{cls.__name__} skipping inherited attr: {o.name} as a custom type overriding it"
347
+ )
348
+
349
+ # Enforce Property Changing
350
+ # FIXME: is this more reliable than a class wide callback?
351
+ # real_out = []
352
+ # for fld in out:
353
+ # if fld.type in (int,float,str):
354
+ # #log.warning(f"setting property changed on {fld}")
355
+ # fld = fld.evolve(on_setattr = property_changed)
356
+ # real_out.append(fld)
357
+ # else:
358
+ # real_out.append(fld)
359
+ # #return real_out
360
+ return out
361
+
362
+
363
+ # alternate initalisers
364
+ comp_transform = lambda c, f: signals_slots_handler(
365
+ c, f, slots=True, signals=True, solvers=True, sys=False, plots=False
366
+ )
367
+
368
+
369
+ # TODO: Make A MetaClass for Configuration, and provide forge interface there. Problem with replaceing metaclass later, as in the case of a singleton.
370
+ @forge
371
+ class Configuration(AttributedBaseMixin):
372
+ """Configuration is a pattern for storing attributes that might change frequently, and proivdes the core functionality for a host of different applications.
373
+
374
+ Configuration is able to go through itself and its objects and map all included Configurations, just to a specific level.
375
+
376
+ Common functionality includes an __on_init__ wrapper for attrs post-init method
377
+ """
378
+
379
+ _temp_vars = None
380
+
381
+ _use_random_name: bool = True
382
+ name: str = attr.ib(
383
+ default=attrs.Factory(name_generator, True),
384
+ validator=attr.validators.instance_of(str),
385
+ kw_only=True,
386
+ )
387
+
388
+ _inherit_parent_attributres = True
389
+ log_fmt = "[%(name)-24s]%(message)s"
390
+ log_silo = True
391
+
392
+ _created_datetime = None
393
+ _subclass_init: bool = True
394
+
395
+ # Configuration Information
396
+ def internal_configurations(
397
+ self, check_config=True, use_dict=True, none_ok=False
398
+ ) -> dict:
399
+ """go through all attributes determining which are configuration objects
400
+ additionally this skip any configuration that start with an underscore (private variable)
401
+ """
402
+ from engforge.configuration import Configuration
403
+
404
+ slots = self.slots_attributes()
405
+
406
+ if check_config:
407
+ chk = lambda k, v: isinstance(v, Configuration)
408
+ else:
409
+ chk = lambda k, v: k in slots and (none_ok or v is not None)
410
+
411
+ obj = self.__dict__
412
+ if not use_dict: # slots
413
+ obj = {k: obj.get(k, None) for k in slots}
414
+
415
+ return {k: v for k, v in obj.items() if chk(k, v) and not k.startswith("_")}
416
+
417
+ def copy_config_at_state(
418
+ self, level=None, levels_deep: int = -1, changed: dict = None, **kw
419
+ ):
420
+ """copy the system at the current state recrusively to a certain level, by default copying everything
421
+ :param levels_deep: how many levels deep to copy, -1 is all
422
+ :param level: the current level, defaults to 0 if not set
423
+ """
424
+ from engforge.configuration import Configuration
425
+ from engforge.components import SolveableInterface
426
+
427
+ if changed is None:
428
+ # top!
429
+ changed = {}
430
+
431
+ if self in changed:
432
+ self.debug(f"already changed {self}")
433
+ return changed[self]
434
+
435
+ if level is None:
436
+ level = 0
437
+ # at top level add parent to changed to prevent infinte parent recursion
438
+ changed[self] = None
439
+
440
+ # exit early if below the level
441
+ if level >= levels_deep and levels_deep > 0:
442
+ self.debug(f"below level {level} {levels_deep} {self}")
443
+ new_sys = attrs.evolve(self) # copy as is
444
+ changed[self] = new_sys
445
+ return new_sys
446
+
447
+ # copy the internal configurations
448
+ kwcomps = {}
449
+ for key, config in self.internal_configurations().items():
450
+ self.debug(f"copying {key} {config} {level} {changed}")
451
+ # copy the system
452
+ if config in changed:
453
+ ccomp = changed[config]
454
+ else:
455
+ ccomp = config.copy_config_at_state(level + 1, levels_deep, changed)
456
+ kwcomps[key] = ccomp
457
+
458
+ # Finally make the new system with changed internals
459
+ self.debug(f"changing with changes {self} {kwcomps} {kw}")
460
+ new_sys = attrs.evolve(self, **kwcomps, **kw)
461
+ changed[self] = new_sys
462
+
463
+ # Finally change references!
464
+ if isinstance(new_sys, SolveableInterface):
465
+ new_sys.system_references(recache=True)
466
+
467
+ # update the parents
468
+ # TODO: use pyee to broadcast change
469
+ if hasattr(self, "parent"):
470
+ if self.parent in changed:
471
+ new_sys.parent = changed[self.parent]
472
+
473
+ return new_sys
474
+
475
+ def go_through_configurations(
476
+ self,
477
+ level=0,
478
+ levels_to_descend=-1,
479
+ parent_level=0,
480
+ only_inst=True,
481
+ **kw,
482
+ ):
483
+ """A generator that will go through all internal configurations up to a certain level
484
+ if levels_to_descend is less than 0 ie(-1) it will go down, if it 0, None, or False it will
485
+ only go through this configuration
486
+
487
+ :return: level,config"""
488
+ from engforge.configuration import Configuration
489
+
490
+ # TODO: instead of a recursive loop a global map per problem context should be used, with a static map of slots, updating with every change per note in system_references. This function could be a part of that but each system shouldn't be responsible for it.
491
+
492
+ should_yield_level = lambda level: all(
493
+ [
494
+ level >= parent_level,
495
+ any([levels_to_descend < 0, level <= levels_to_descend]),
496
+ ]
497
+ )
498
+
499
+ if should_yield_level(level):
500
+ yield "", level, self
501
+
502
+ level += 1
503
+ if "check_config" not in kw:
504
+ kw["check_config"] = False
505
+
506
+ for key, config in self.internal_configurations(**kw).items():
507
+ if isinstance(config, Configuration):
508
+ for skey, level, iconf in config.go_through_configurations(
509
+ level, levels_to_descend, parent_level
510
+ ):
511
+ if not only_inst or iconf is not None:
512
+ yield f"{key}.{skey}" if skey else key, level, iconf
513
+ else:
514
+ if not only_inst or config is not None:
515
+ yield key, level, config
516
+
517
+ # Our Special Init Methodology
518
+ def __on_init__(self):
519
+ """Override this when creating your special init functionality, you must use attrs for input variables, this is called after parents are assigned. subclasses are always called after"""
520
+ pass
521
+
522
+ def __pre_init__(self):
523
+ """Override this when creating your special init functionality, you must use attrs for input variables, this is called before parents are assigned"""
524
+ pass
525
+
526
+ def __attrs_post_init__(self):
527
+ """This is called after __init__ by attr's functionality, we expose __oninit__ for you to use!"""
528
+ # Store abs path On Creation, in case we change
529
+
530
+ from engforge.components import Component
531
+
532
+ self._log = None
533
+ self._anything_changed = True # save by default first go!
534
+ self._created_datetime = datetime.datetime.utcnow()
535
+
536
+ # subclass instance instance init causes conflicts in structures ect
537
+ self.__pre_init__()
538
+ if self._subclass_init:
539
+ try:
540
+ for comp in self.__class__.mro():
541
+ if (
542
+ hasattr(comp, "__pre_init__")
543
+ and comp.__pre_init__ != Configuration.__pre_init__
544
+ ):
545
+ comp.__pre_init__(self)
546
+ except Exception as e:
547
+ self.error(e, f"error in __pre_init__ {e}")
548
+
549
+ # Assign Parents, ensure single componsition
550
+ # TODO: allow multi-parent, w/wo keeping state, state swap on update()?
551
+ # TODO: replace parent concept with problem context
552
+ for compnm, comp in self.internal_configurations(False).items():
553
+ if isinstance(comp, Component):
554
+ # TODO: allow multiple parents
555
+ if (hasattr(comp, "parent")) and (comp.parent is not None):
556
+ self.warning(
557
+ f"Component {compnm} already has a parent {comp.parent} #copying, and assigning to {self}"
558
+ )
559
+ # setattr(self, compnm, attrs.evolve(comp, parent=self))
560
+ else:
561
+ comp.parent = self
562
+
563
+ self.debug(f"created {self.identity}")
564
+
565
+ # subclass instance instance init causes conflicts in structures
566
+ self.__on_init__()
567
+ runs = set((self.__class__.__on_init__,)) # keep track of unique functions
568
+ if self._subclass_init:
569
+ try:
570
+ for comp in self.__class__.mro():
571
+ if (
572
+ hasattr(comp, "__on_init__")
573
+ and comp.__on_init__ != Configuration.__on_init__
574
+ and comp.__on_init__ not in runs
575
+ ):
576
+ comp.__on_init__(self)
577
+ runs.add(comp.__on_init__)
578
+
579
+ except Exception as e:
580
+ self.error(e, f"error in __on_init__")
581
+
582
+ @classmethod
583
+ def validate_class(cls):
584
+ """A customizeable validator at the end of class creation in forge"""
585
+ return
586
+
587
+ @classmethod
588
+ def pre_compile(cls):
589
+ """an overrideable classmethod that executes when compiled, however will not execute as a subclass"""
590
+ pass
591
+
592
+ @classmethod
593
+ def compile_classes(cls):
594
+ """compiles all subclass functionality"""
595
+ cls.cls_compile()
596
+ for subcls in cls.parent_configurations_cls():
597
+ if subcls.subcls_compile is not Configuration:
598
+ log.debug(f"{cls.__name__} compiling {subcls.__name__}")
599
+ subcls.subcls_compile()
600
+
601
+ @classmethod
602
+ def cls_compile(cls):
603
+ """compiles this class, override this to compile functionality for this class"""
604
+ pass
605
+
606
+ @classmethod
607
+ def subcls_compile(cls):
608
+ """reliably compiles this method even for subclasses, override this to compile functionality for subclass interfaces & mixins"""
609
+ pass
610
+
611
+ @classmethod
612
+ def parent_configurations_cls(cls) -> list:
613
+ """returns all subclasses that are a Configuration"""
614
+ return [c for c in cls.mro() if issubclass(c, Configuration)]
615
+
616
+ # Identity & location Methods
617
+ @property
618
+ def filename(self):
619
+ """A nice to have, good to override"""
620
+ fil = (
621
+ self.identity.replace(" ", "_")
622
+ .replace("-", "_")
623
+ .replace(":", "")
624
+ .replace("|", "_")
625
+ .title()
626
+ )
627
+ filename = "".join(
628
+ [c for c in fil if c.isalpha() or c.isdigit() or c == "_" or c == "-"]
629
+ ).rstrip()
630
+ return filename
631
+
632
+ @property
633
+ def displayname(self):
634
+ dn = (
635
+ self.identity.replace("_", " ")
636
+ .replace("|", " ")
637
+ .replace("-", " ")
638
+ .replace(" ", " ")
639
+ .title()
640
+ )
641
+ # dispname = "".join([c for c in dn if c.isalpha() or c.isdigit() or c=='_' or c=='-']).rstrip()
642
+ return dn
643
+
644
+ @property
645
+ def identity(self):
646
+ """A customizeable property that will be in the log by default"""
647
+ if not self.name or self.name == "default":
648
+ return self.classname.lower()
649
+ if self.classname in self.name:
650
+ return self.name.lower()
651
+ return f"{self.classname}-{self.name}".lower()
652
+
653
+ @property
654
+ def classname(self):
655
+ """Shorthand for the classname"""
656
+ return str(type(self).__name__).lower()
657
+
658
+ # Structural Orchestration Through Subclassing
659
+ @classmethod
660
+ def subclasses(cls, out=None):
661
+ """return all subclasses of components, including their subclasses
662
+ :param out: out is to pass when the middle of a recursive operation, do not use it!
663
+ """
664
+
665
+ # init the set by default
666
+ if out is None:
667
+ out = set()
668
+
669
+ for cls in cls.__subclasses__():
670
+ out.add(cls)
671
+ cls.subclasses(out)
672
+
673
+ return out