As noted in the question and the following discussion attached to it, the usage of property
does not work on the class definition directly, and that forcing subclasses to also define the property to maintain the definition "protocol" for every attribute may become cumbersome. A usage of format string was also suggested, but we will still have the same issue with the assignment done at class definition for the standard type
metaclass is that it will not be recalculated. Consider the alternative approach:
class Config:
URL = "http://127.0.0.1"
ANOTHER_URL = f"{URL}/home"
class ProdConfig(Config):
URL = "http://test.abc.com"
Running the following will fail to produce the desired result:
>>> conf = ProdConfig()
>>> print(conf.URL)
http://test.abc.com
>>> print(conf.ANOTHER_URL)
http://127.0.0.1/home
Simply due to the fact that ANOTHER_URL
was not reassigned in the scope of ProdConfig
. However, this problem can be solved using the following metaclass:
class ConfigFormatMeta(type):
def __init__(cls, name, bases, attrs):
# create and store a "private" mapping of original definitions,
# for reuse by subclasses
cls._config_map = config_map = {}
# merge all config_maps of base classes.
for base_cls in bases:
if hasattr(base_cls, '_config_map'):
config_map.update(base_cls._config_map)
# update the config_map with original definitions in the newly
# constructed class, filter out all values beginning with '_'
config_map.update({
k: v for k, v in vars(cls).items() if not k.startswith('_')})
# Now assign the formatted attributes to the class
for k in config_map:
# Only apply to str attributes; other types of attributes
# on the class will need additional work.
if isinstance(config_map[k], str):
setattr(cls, k, config_map[k].format(**config_map))
super().__init__(name, bases, attrs)
With that, try basing the Config
class using the new metaclass:
class Config(metaclass=ConfigFormatMeta):
URL = 'http://example.com'
ANOTHER_URL = '{URL}/home'
class ProdConfig(Config):
URL = 'http://abc.example.com'
Now try this again:
>>> conf = ProdConfig()
>>> print(conf.URL)
http://abc.example.com
>>> print(conf.ANOTHER_URL)
http://abc.example.com/home
Note that ANOTHER_URL
was not redefined in the scope of ProdConfig
, yet the desired behavior of having only the URL
be redefined to have the expected value of 'http://abc.example.com/home'
was achieved.
Also it is worth noting that using metaclasses will interfere with multiple inheritance with other classes that have a different base metaclass, and that there is a bit of duplication with the original mapping that is slightly hidden via the _config_map
attribute on the class itself, so please treat this mostly as a proof of concept.