2020
2121import logging
2222from collections .abc import Iterable
23- from typing import Any , Dict , List , Optional , Set , Union
23+ from typing import Any , Dict , List , Optional , Sequence , Set , Union , cast
2424
2525from climada .engine .option_appraisal .MCDM .constants import (
2626 DEFAULT_CATEGORY_WEIGHT ,
2727 IMPORTANCE_MATCH ,
2828)
2929from climada .engine .option_appraisal .MCDM .weights import WeightedItem
3030
31- # Define type aliases for cleaner type hints
3231CategoryName = str
3332CategoryLike = Union [CategoryName , "CriteriaCategory" ]
3433
3837class CategorySpace :
3938 """Manages a dedicated, isolated registry of CriteriaCategory objects."""
4039
41- # Class attribute to hold the singleton default space
4240 _default_space : Optional ["CategorySpace" ] = None
4341
4442 def __init__ (self ):
4543 self ._registry : Dict [str , "CriteriaCategory" ] = {}
46- self .category_weights = None
4744
48- # --- New Class Method for Default Access ---
4945 @classmethod
5046 def get_default_space (cls ) -> "CategorySpace" :
5147 """Returns the default CategorySpace instance, creating it if necessary."""
@@ -107,22 +103,32 @@ def remove(self, category: CategoryLike) -> None:
107103
108104 def register (self , category : "CriteriaCategory" ) -> None :
109105 if category .name in self ._registry :
110- raise ValueError (
111- f"Category '{ category .name } ' already exists in space '{ self .name } '."
112- )
106+ raise ValueError (f"Category '{ category .name } ' already exists in space." )
113107 self ._registry [category .name ] = category
114108
115109 def add_category (
116110 self ,
117111 name : CategoryName ,
118- parent_cats : Optional [Union [CategoryLike , List [CategoryLike ]]] = None ,
112+ parent_cats : Optional [Union [CategoryLike , Sequence [CategoryLike ]]] = None ,
119113 category_type : Optional [str ] = None ,
114+ weight : Optional [float ] = None ,
120115 overwrite : bool = False ,
121116 ) -> "CriteriaCategory" :
117+ """Create and register a new category in this space.
118+
119+ Parameters
120+ ----------
121+ name : str
122+ parent_cats : str or CriteriaCategory or list, optional
123+ category_type : str, optional
124+ weight : float or str, optional
125+ overwrite : bool, optional
126+ """
122127 return CriteriaCategory (
123128 name ,
124129 parents = parent_cats ,
125130 category_type = category_type ,
131+ weight = weight ,
126132 overwrite = overwrite ,
127133 space = self ,
128134 )
@@ -153,46 +159,74 @@ def create_subset_by_type(
153159 return subspace
154160
155161 def create_subspace (
156- self , selection : Union [CategoryLike , List [CategoryLike ]]
162+ self , selection : Union [CategoryLike , Sequence [CategoryLike ]]
157163 ) -> "CategorySpace" :
158164 if not isinstance (selection , Iterable ):
159165 selection = [selection ]
160166
161167 subspace = CategorySpace ()
162168 for category in selection :
163169 if isinstance (category , str ):
164- category = self .get (category )
170+ category = cast ( CriteriaCategory , self .get (category ) )
165171
166172 subspace .register (category )
167173
168174 return subspace
169175
170176 @property
171- def category_weights (self ):
172- return {k : self . _registry [ k ]. item_weight for k in self ._registry .keys ()}
177+ def effective_weights (self ):
178+ return {name : cat . effective_weight for name , cat in self ._registry .items ()}
173179
174- @category_weights .setter
175- def category_weights (self , value , / ):
176- if value is None :
177- for k , v in {
178- cat_name : DEFAULT_CATEGORY_WEIGHT for cat_name in self ._registry .keys ()
179- }.items ():
180- self ._registry [k ].item_weight = v
181- else :
182- no_match = [k for k in value .keys () if k not in self ._registry .keys ()]
183- no_weight = [k for k in self ._registry .keys () if k not in value .keys ()]
184- if len (no_match ) > 0 :
185- LOGGER .warning (
186- f"Some weights do not correspond to any category: { no_match } "
187- )
180+ @property
181+ def weights (self ):
182+ return {name : cat .weight for name , cat in self ._registry .items ()}
188183
189- if len (no_weight ) > 0 :
190- LOGGER .warning (
191- f"No weight given for one or more categories: { no_weight } \n (will use existing, { DEFAULT_CATEGORY_WEIGHT } by default)"
192- )
184+ @weights .setter
185+ def weights (self , value : Dict [str , float ]):
186+ """Set weights from a dict. Missing keys keep their current value.
193187
194- for k , v in value .items ():
195- self ._registry [k ].item_weight = v
188+ Parameters
189+ ----------
190+ value : dict[str, float]
191+ Mapping of category name to weight. Values can be float or
192+ importance strings (e.g. ``"high"``).
193+ """
194+ if not isinstance (value , dict ):
195+ raise TypeError (f"weights must be a dict, got { type (value ).__name__ } ." )
196+
197+ unrecognized = [k for k in value if k not in self ._registry ]
198+ unset = [k for k in self ._registry if k not in value ]
199+
200+ if unrecognized :
201+ LOGGER .warning (
202+ "Weights given for unknown categories (ignored): %s" , unrecognized
203+ )
204+ if unset :
205+ LOGGER .warning ("No weight given for categories (unchanged): %s" , unset )
206+
207+ for name , w in value .items ():
208+ if name in self ._registry :
209+ self ._registry [name ].weight = w # WeightedItem setter validates
210+
211+ def reset_weights (self , weight = None ) -> None :
212+ """Reset all category weights to ``DEFAULT_WEIGHT``."""
213+ for cat in self ._registry .values ():
214+ cat .weight = weight if weight else DEFAULT_CATEGORY_WEIGHT
215+
216+ def set_weight (self , category : CategoryLike , weight ) -> None :
217+ """Set the weight of a single category.
218+
219+ Parameters
220+ ----------
221+ category : str or CriteriaCategory
222+ Target category name or object.
223+ weight : float or str
224+ New weight value.
225+ """
226+ name = category if isinstance (category , str ) else category .name
227+ if name not in self ._registry :
228+ raise KeyError (f"Category '{ name } ' not in this space." )
229+ self ._registry [name ].weight = weight
196230
197231 @property
198232 def all_categories (self ) -> List ["CriteriaCategory" ]:
@@ -241,11 +275,11 @@ def print_node_recursive(
241275 print (
242276 prefix
243277 + connector
244- + node .category_type
278+ + str ( node .category_type )
245279 + ": "
246280 + node .name
247281 + " category weight: "
248- + str (self .category_weights [node .name ])
282+ + str (self .weights [node .name ])
249283 )
250284
251285 # Determine the prefix for the children
@@ -297,9 +331,9 @@ class CriteriaCategory(WeightedItem):
297331 def __init__ (
298332 self ,
299333 name : CategoryName ,
300- parents : Optional [Union [CategoryLike , List [CategoryLike ]]] = None ,
334+ parents : Optional [Union [CategoryLike , Sequence [CategoryLike ]]] = None ,
301335 category_type : Optional [str ] = None ,
302- category_weight : Optional [float ] = None ,
336+ weight : Optional [float ] = None ,
303337 space : Optional [CategorySpace ] = None ,
304338 overwrite : Optional [bool ] = False ,
305339 ) -> None :
@@ -310,7 +344,7 @@ def __init__(
310344 ----------
311345 name : CategoryName
312346 The unique name of the category.
313- parents : Optional[Union[CategoryLike, List [CategoryLike]]], optional
347+ parents : Optional[Union[CategoryLike, Sequence [CategoryLike]]], optional
314348 The parent criteria(s) this category inherits from. Can be a single
315349 name/object or a list of names/objects. By default, None.
316350
@@ -319,31 +353,38 @@ def __init__(
319353 ValueError
320354 If a category with the given name already exists in the registry.
321355 """
322- WeightedItem .__init__ (self , category_weight )
356+ WeightedItem .__init__ (self , weight )
323357 self ._space = space if space is not None else CategorySpace .get_default_space ()
324358 self .name : CategoryName = name
325359 self .category_type : str | None = category_type
326360 self ._parents : Set [CriteriaCategory ] = set ()
327361 self ._children : Set [CriteriaCategory ] = set ()
328362
329363 if self in self .space :
364+ existing = cast (CriteriaCategory , self .space .get (name ))
330365 if not overwrite :
331- if not self . space . get ( name ) .has_parents_exactly (parents ):
366+ if not existing .has_parents_exactly (parents ):
332367 raise ValueError (
333- f"CriteriaCategory '{ name } ' with different parents ({ self .space .get (name )._parents_name } != { parents } ) already exists in current category space ({ self .space } ). You can overwrite with `overwrite=True`."
368+ f"CriteriaCategory '{ name } ' with different parents ({ existing .parents } != { parents } )"
369+ f" already exists in current category space ({ self .space } )."
370+ " You can overwrite with `overwrite=True`."
334371 )
335372 return
336373
337374 self .space .register (self )
338375 if parents :
339- self ._add_parents (parents )
376+ self .add_parents (parents )
340377
341378 @property
342- def weight (self ):
379+ def effective_weight (self ):
380+ """Hierarchical weight: max of own weight and all ancestors' weights.
381+
382+ Distinct from ``weight``, which is this category's direct assignment.
383+ """
343384 if len (self .parents ) > 0 :
344- return max (self .item_weight , max ([p .weight for p in self .parents ]))
385+ return max (self .weight , max ([p .effective_weight for p in self .parents ]))
345386 else :
346- return self .item_weight
387+ return self .weight
347388
348389 @property
349390 def space (self ):
@@ -380,7 +421,7 @@ def _parents_names(self):
380421 return [p .name for p in self .parents ]
381422
382423 def has_parents_exactly (
383- self , check_parents : Union [CategoryLike , List [CategoryLike ], None ]
424+ self , check_parents : Union [CategoryLike , Sequence [CategoryLike ], None ]
384425 ) -> bool :
385426 """
386427 Checks if the criteria's set of parents is exactly equal to the provided set of parents.
@@ -389,7 +430,7 @@ def has_parents_exactly(
389430
390431 Parameters
391432 ----------
392- check_parents : Union[CategoryLike, List [CategoryLike]]
433+ check_parents : Union[CategoryLike, Sequence [CategoryLike]]
393434 A single parent or a list of parent names or CriteriaCategory objects
394435 to compare against the criteria's actual parents.
395436
@@ -408,19 +449,19 @@ def has_parents_exactly(
408449 return True
409450
410451 if not isinstance (check_parents , list ):
411- check_parents = [check_parents ]
452+ check_parents = [cast ( CategoryLike , check_parents ) ]
412453
413454 return check_parents == self ._parents_names
414455
415- def _add_parents (
416- self , parent_names : Union [CategoryLike , List [CategoryLike ]]
456+ def add_parents (
457+ self , parent_names : Union [CategoryLike , Sequence [CategoryLike ]]
417458 ) -> None :
418459 """
419460 Internal helper to resolve and establish parent links.
420461
421462 Parameters
422463 ----------
423- parent_names : Union[CategoryLike, List [CategoryLike]]
464+ parent_names : Union[CategoryLike, Sequence [CategoryLike]]
424465 The parent criteria(s) to link.
425466
426467 Raises
@@ -430,7 +471,7 @@ def _add_parents(
430471 TypeError
431472 If an item in the parent list is not a string or CriteriaCategory object.
432473 """
433- if not isinstance (parent_names , list ):
474+ if not isinstance (parent_names , Sequence ):
434475 parent_names = [parent_names ]
435476
436477 for p_name in parent_names :
@@ -502,12 +543,12 @@ def __repr__(self, indent=0) -> str:
502543 parent_names = sorted ([p .name for p in self .parents ])
503544 parent_str = f" Parents: { ', ' .join (parent_names )} " if parent_names else "none"
504545 indent_space = " " * indent
505- return f"""{ indent_space } name: { self .name } weight: { self .item_weight } type: { self .category_type } \n { indent_space } parents: { parent_str } """
546+ return f"""{ indent_space } name: { self .name } weight: { self .weight } type: { self .category_type } \n { indent_space } parents: { parent_str } """
506547
507548
508549def create_criteria_category (
509550 name : CategoryName ,
510- parent_cats : Optional [Union [CategoryLike , List [CategoryLike ]]] = None ,
551+ parent_cats : Optional [Union [CategoryLike , Sequence [CategoryLike ]]] = None ,
511552 category_type : Optional [str ] = None ,
512553 space : Optional [CategorySpace ] = None ,
513554 overwrite : bool = False ,
@@ -569,7 +610,7 @@ def update_categories_from_dict(
569610def __categories_hierarchy_recursion (
570611 hierarchy_dict : Dict [str , Any ],
571612 space : CategorySpace ,
572- current_parents : Optional [Union [CategoryLike , List [CategoryLike ]]] = None ,
613+ current_parents : Optional [Union [CategoryLike , Sequence [CategoryLike ]]] = None ,
573614) -> None :
574615 """
575616 Recursively creates a hierarchy of CriteriaCategory objects from a nested dictionary.
@@ -613,7 +654,7 @@ def __categories_hierarchy_recursion(
613654 )
614655 except ValueError as e :
615656 if "different parents" in str (e ):
616- space .get (category_name )._add_parents (parent_list )
657+ space .get (category_name ).add_parents (parent_list ) # type: ignore
617658 else :
618659 print (
619660 f"Warning: Category '{ category_name } ' skipped (likely duplicate). Error: { e } "
@@ -646,15 +687,15 @@ class CategorizedObject:
646687 ----------
647688 name : str
648689 The name or identifier of the object.
649- categories : set [CriteriaCategory]
690+ categories : Sequence [CriteriaCategory]
650691 The set of CriteriaCategory objects this instance is directly assigned to.
651692 """
652693
653694 def __init__ (
654695 self ,
655696 name : str ,
656697 categories : Optional [
657- Union [CategoryLike , List [CategoryLike ], Set [CriteriaCategory ]]
698+ Union [CategoryLike , Sequence [CategoryLike ], Sequence [CriteriaCategory ]]
658699 ] = None ,
659700 space : Optional [CategorySpace ] = None ,
660701 ) -> None :
@@ -687,7 +728,10 @@ def category_space(self):
687728 return self ._space
688729
689730 def add_categories (
690- self , categories : Union [CategoryLike , List [CategoryLike ], Set [CriteriaCategory ]]
731+ self ,
732+ categories : Union [
733+ CategoryLike , Sequence [CategoryLike ], Sequence [CriteriaCategory ]
734+ ],
691735 ) -> None :
692736 """
693737 Adds one or more criteria categories to the object by name.
@@ -702,7 +746,7 @@ def add_categories(
702746 ValueError
703747 If a category name does not exist in the CriteriaCategory registry.
704748 """
705- if not isinstance (categories , ( list , set ) ):
749+ if not isinstance (categories , Sequence ):
706750 categories = [categories ]
707751
708752 for cat_to_add in categories :
0 commit comments