|
import collections |
|
import os |
|
import typing |
|
from dataclasses import dataclass |
|
|
|
__all__ = ["Config", "config"] |
|
|
|
|
|
@dataclass(init=False, eq=False, slots=True, kw_only=True, match_args=False) |
|
class Config: |
|
"""The base class for NetworkX configuration. |
|
|
|
There are two ways to use this to create configurations. The first is to |
|
simply pass the initial configuration as keyword arguments to ``Config``: |
|
|
|
>>> cfg = Config(eggs=1, spam=5) |
|
>>> cfg |
|
Config(eggs=1, spam=5) |
|
|
|
The second--and preferred--way is to subclass ``Config`` with docs and annotations. |
|
|
|
>>> class MyConfig(Config): |
|
... '''Breakfast!''' |
|
... |
|
... eggs: int |
|
... spam: int |
|
... |
|
... def _check_config(self, key, value): |
|
... assert isinstance(value, int) and value >= 0 |
|
>>> cfg = MyConfig(eggs=1, spam=5) |
|
|
|
Once defined, config items may be modified, but can't be added or deleted by default. |
|
``Config`` is a ``Mapping``, and can get and set configs via attributes or brackets: |
|
|
|
>>> cfg.eggs = 2 |
|
>>> cfg.eggs |
|
2 |
|
>>> cfg["spam"] = 42 |
|
>>> cfg["spam"] |
|
42 |
|
|
|
Subclasses may also define ``_check_config`` (as done in the example above) |
|
to ensure the value being assigned is valid: |
|
|
|
>>> cfg.spam = -1 |
|
Traceback (most recent call last): |
|
... |
|
AssertionError |
|
|
|
If a more flexible configuration object is needed that allows adding and deleting |
|
configurations, then pass ``strict=False`` when defining the subclass: |
|
|
|
>>> class FlexibleConfig(Config, strict=False): |
|
... default_greeting: str = "Hello" |
|
>>> flexcfg = FlexibleConfig() |
|
>>> flexcfg.name = "Mr. Anderson" |
|
>>> flexcfg |
|
FlexibleConfig(default_greeting='Hello', name='Mr. Anderson') |
|
""" |
|
|
|
def __init_subclass__(cls, strict=True): |
|
cls._strict = strict |
|
|
|
def __new__(cls, **kwargs): |
|
orig_class = cls |
|
if cls is Config: |
|
|
|
cls = type( |
|
cls.__name__, |
|
(cls,), |
|
{"__annotations__": {key: typing.Any for key in kwargs}}, |
|
) |
|
cls = dataclass( |
|
eq=False, |
|
repr=cls._strict, |
|
slots=cls._strict, |
|
kw_only=True, |
|
match_args=False, |
|
)(cls) |
|
if not cls._strict: |
|
cls.__repr__ = _flexible_repr |
|
cls._orig_class = orig_class |
|
instance = object.__new__(cls) |
|
instance.__init__(**kwargs) |
|
return instance |
|
|
|
def _check_config(self, key, value): |
|
"""Check whether config value is valid. This is useful for subclasses.""" |
|
|
|
|
|
def __dir__(self): |
|
return self.__dataclass_fields__.keys() |
|
|
|
def __setattr__(self, key, value): |
|
if self._strict and key not in self.__dataclass_fields__: |
|
raise AttributeError(f"Invalid config name: {key!r}") |
|
self._check_config(key, value) |
|
object.__setattr__(self, key, value) |
|
|
|
def __delattr__(self, key): |
|
if self._strict: |
|
raise TypeError( |
|
f"Configuration items can't be deleted (can't delete {key!r})." |
|
) |
|
object.__delattr__(self, key) |
|
|
|
|
|
def __contains__(self, key): |
|
return ( |
|
key in self.__dataclass_fields__ if self._strict else key in self.__dict__ |
|
) |
|
|
|
def __iter__(self): |
|
return iter(self.__dataclass_fields__ if self._strict else self.__dict__) |
|
|
|
def __len__(self): |
|
return len(self.__dataclass_fields__ if self._strict else self.__dict__) |
|
|
|
def __reversed__(self): |
|
return reversed(self.__dataclass_fields__ if self._strict else self.__dict__) |
|
|
|
|
|
def __getitem__(self, key): |
|
try: |
|
return getattr(self, key) |
|
except AttributeError as err: |
|
raise KeyError(*err.args) from None |
|
|
|
def __setitem__(self, key, value): |
|
try: |
|
self.__setattr__(key, value) |
|
except AttributeError as err: |
|
raise KeyError(*err.args) from None |
|
|
|
def __delitem__(self, key): |
|
try: |
|
self.__delattr__(key) |
|
except AttributeError as err: |
|
raise KeyError(*err.args) from None |
|
|
|
_ipython_key_completions_ = __dir__ |
|
|
|
|
|
def get(self, key, default=None): |
|
return getattr(self, key, default) |
|
|
|
def items(self): |
|
return collections.abc.ItemsView(self) |
|
|
|
def keys(self): |
|
return collections.abc.KeysView(self) |
|
|
|
def values(self): |
|
return collections.abc.ValuesView(self) |
|
|
|
|
|
def __eq__(self, other): |
|
if not isinstance(other, Config): |
|
return NotImplemented |
|
return self._orig_class == other._orig_class and self.items() == other.items() |
|
|
|
|
|
def __reduce__(self): |
|
return self._deserialize, (self._orig_class, dict(self)) |
|
|
|
@staticmethod |
|
def _deserialize(cls, kwargs): |
|
return cls(**kwargs) |
|
|
|
|
|
def _flexible_repr(self): |
|
return ( |
|
f"{self.__class__.__qualname__}(" |
|
+ ", ".join(f"{key}={val!r}" for key, val in self.__dict__.items()) |
|
+ ")" |
|
) |
|
|
|
|
|
|
|
collections.abc.Mapping.register(Config) |
|
|
|
|
|
class NetworkXConfig(Config): |
|
"""Configuration for NetworkX that controls behaviors such as how to use backends. |
|
|
|
Attribute and bracket notation are supported for getting and setting configurations: |
|
|
|
>>> nx.config.backend_priority == nx.config["backend_priority"] |
|
True |
|
|
|
Parameters |
|
---------- |
|
backend_priority : list of backend names |
|
Enable automatic conversion of graphs to backend graphs for algorithms |
|
implemented by the backend. Priority is given to backends listed earlier. |
|
Default is empty list. |
|
|
|
backends : Config mapping of backend names to backend Config |
|
The keys of the Config mapping are names of all installed NetworkX backends, |
|
and the values are their configurations as Config mappings. |
|
|
|
cache_converted_graphs : bool |
|
If True, then save converted graphs to the cache of the input graph. Graph |
|
conversion may occur when automatically using a backend from `backend_priority` |
|
or when using the `backend=` keyword argument to a function call. Caching can |
|
improve performance by avoiding repeated conversions, but it uses more memory. |
|
Care should be taken to not manually mutate a graph that has cached graphs; for |
|
example, ``G[u][v][k] = val`` changes the graph, but does not clear the cache. |
|
Using methods such as ``G.add_edge(u, v, weight=val)`` will clear the cache to |
|
keep it consistent. ``G.__networkx_cache__.clear()`` manually clears the cache. |
|
Default is False. |
|
|
|
Notes |
|
----- |
|
Environment variables may be used to control some default configurations: |
|
|
|
- NETWORKX_BACKEND_PRIORITY: set `backend_priority` from comma-separated names. |
|
- NETWORKX_CACHE_CONVERTED_GRAPHS: set `cache_converted_graphs` to True if nonempty. |
|
|
|
This is a global configuration. Use with caution when using from multiple threads. |
|
""" |
|
|
|
backend_priority: list[str] |
|
backends: Config |
|
cache_converted_graphs: bool |
|
|
|
def _check_config(self, key, value): |
|
from .backends import backends |
|
|
|
if key == "backend_priority": |
|
if not (isinstance(value, list) and all(isinstance(x, str) for x in value)): |
|
raise TypeError( |
|
f"{key!r} config must be a list of backend names; got {value!r}" |
|
) |
|
if missing := {x for x in value if x not in backends}: |
|
missing = ", ".join(map(repr, sorted(missing))) |
|
raise ValueError(f"Unknown backend when setting {key!r}: {missing}") |
|
elif key == "backends": |
|
if not ( |
|
isinstance(value, Config) |
|
and all(isinstance(key, str) for key in value) |
|
and all(isinstance(val, Config) for val in value.values()) |
|
): |
|
raise TypeError( |
|
f"{key!r} config must be a Config of backend configs; got {value!r}" |
|
) |
|
if missing := {x for x in value if x not in backends}: |
|
missing = ", ".join(map(repr, sorted(missing))) |
|
raise ValueError(f"Unknown backend when setting {key!r}: {missing}") |
|
elif key == "cache_converted_graphs": |
|
if not isinstance(value, bool): |
|
raise TypeError(f"{key!r} config must be True or False; got {value!r}") |
|
|
|
|
|
|
|
config = NetworkXConfig( |
|
backend_priority=[], |
|
backends=Config(), |
|
cache_converted_graphs=bool(os.environ.get("NETWORKX_CACHE_CONVERTED_GRAPHS", "")), |
|
) |
|
|