1010from dataclasses import MISSING , dataclass , fields
1111from dataclasses import field as dataclass_field
1212from enum import Enum , IntEnum
13- from functools import cached_property
13+ from functools import cached_property , lru_cache
1414from typing import Any
1515
1616import toml
@@ -48,14 +48,14 @@ class ConfigSource(IntEnum):
4848 # e.g. halmos.toml
4949 config_file = 2
5050
51- # from command line
52- command_line = 3
53-
5451 # contract-level annotation (e.g. @custom:halmos --some-option)
55- contract_annotation = 4
52+ contract_annotation = 3
5653
5754 # function-level annotation (e.g. @custom:halmos --some-option)
58- function_annotation = 5
55+ function_annotation = 4
56+
57+ # from command line, highest precedence
58+ command_line = 5
5959
6060
6161# helper to define config fields
@@ -639,25 +639,33 @@ class Config:
639639
640640 ### Methods
641641
642- def __getattribute__ (self , name ):
643- """Look up values in parent object if they are not set in the current object.
644-
645- This is because we consider the current object to override its parent.
642+ def __hash__ (self ):
643+ return id (self )
646644
647- Because of this, printing a Config object will show a "flattened/resolved" view of the configuration.
645+ # cachable because each layer is immutable
646+ @lru_cache (maxsize = 64 ) # noqa: B019
647+ def __getattribute__ (self , name ):
648+ """
649+ Look up values based on precedence, where higher ConfigSource values override lower ones.
648650 """
651+ # Handle internal attributes normally
652+ if name [0 ] == "_" or name in (
653+ "value_with_source" ,
654+ "values" ,
655+ "values_by_layer" ,
656+ "formatted_layers" ,
657+ "resolved_solver_command" ,
658+ "with_overrides" ,
659+ ):
660+ return object .__getattribute__ (self , name )
649661
650- # look up value in current object
651- value = object . __getattribute__ ( self , name )
652- if value is not None :
662+ # For config fields, use precedence-based lookup
663+ try :
664+ value , _ = self . value_with_source ( name )
653665 return value
654-
655- # look up value in parent object
656- parent = object .__getattribute__ (self , "_parent" )
657- if parent is not None :
658- return getattr (parent , name )
659-
660- return value
666+ except AttributeError :
667+ # Fall back to normal attribute lookup for non-config fields
668+ return object .__getattribute__ (self , name )
661669
662670 def with_overrides (self , source : ConfigSource , ** overrides ):
663671 """Create a new configuration object with some fields overridden.
@@ -673,26 +681,16 @@ def with_overrides(self, source: ConfigSource, **overrides):
673681 sys .exit (2 )
674682
675683 def value_with_source (self , name : str ) -> tuple [Any , ConfigSource ]:
676- # look up value in current object
677- value = object .__getattribute__ (self , name )
678- if value is not None :
679- return (value , self ._source )
684+ best_value , best_source = None , ConfigSource .void
680685
681- # look up value in parent object
682- parent = self ._parent
683- if parent is not None :
684- return parent .value_with_source (name )
686+ current = self
687+ while current is not None :
688+ value = object .__getattribute__ (current , name )
689+ if value is not None and (current_source := current ._source ) > best_source :
690+ best_value , best_source = value , current_source
691+ current = current ._parent
685692
686- return (value , self ._source )
687-
688- def values_with_sources (self ) -> dict [str , tuple [Any , ConfigSource ]]:
689- # field -> (value, source)
690- values = {}
691- for field in fields (self ):
692- if field .metadata .get (internal ):
693- continue
694- values [field .name ] = self .value_with_source (field .name )
695- return values
693+ return (best_value , best_source )
696694
697695 def values (self ):
698696 skip_empty = self ._parent is not None
0 commit comments