|
import collections |
|
import typing |
|
from dataclasses import dataclass |
|
|
|
__all__ = ["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 recommended way |
|
is to subclass ``Config`` with docs and annotations. |
|
|
|
>>> class MyConfig(Config): |
|
... '''Breakfast!''' |
|
... |
|
... eggs: int |
|
... spam: int |
|
... |
|
... def _on_setattr(self, key, value): |
|
... assert isinstance(value, int) and value >= 0 |
|
... return value |
|
>>> cfg = MyConfig(eggs=1, spam=5) |
|
|
|
Another way is to simply pass the initial configuration as keyword arguments to |
|
the ``Config`` instance: |
|
|
|
>>> cfg1 = Config(eggs=1, spam=5) |
|
>>> cfg1 |
|
Config(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 |
|
|
|
For convenience, it can also set configs within a context with the "with" statement: |
|
|
|
>>> with cfg(spam=3): |
|
... print("spam (in context):", cfg.spam) |
|
spam (in context): 3 |
|
>>> print("spam (after context):", cfg.spam) |
|
spam (after context): 42 |
|
|
|
Subclasses may also define ``_on_setattr`` (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__": dict.fromkeys(kwargs, typing.Any)}, |
|
) |
|
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 |
|
cls._prev = None |
|
cls._context_stack = [] |
|
instance = object.__new__(cls) |
|
instance.__init__(**kwargs) |
|
return instance |
|
|
|
def _on_setattr(self, key, value): |
|
"""Process config value and check whether it is valid. Useful for subclasses.""" |
|
return value |
|
|
|
def _on_delattr(self, key): |
|
"""Callback for when a config item is being deleted. 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}") |
|
value = self._on_setattr(key, value) |
|
object.__setattr__(self, key, value) |
|
self.__class__._prev = None |
|
|
|
def __delattr__(self, key): |
|
if self._strict: |
|
raise TypeError( |
|
f"Configuration items can't be deleted (can't delete {key!r})." |
|
) |
|
self._on_delattr(key) |
|
object.__delattr__(self, key) |
|
self.__class__._prev = None |
|
|
|
|
|
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 __call__(self, **kwargs): |
|
kwargs = {key: self._on_setattr(key, val) for key, val in kwargs.items()} |
|
prev = dict(self) |
|
for key, val in kwargs.items(): |
|
setattr(self, key, val) |
|
self.__class__._prev = prev |
|
return self |
|
|
|
def __enter__(self): |
|
if self.__class__._prev is None: |
|
raise RuntimeError( |
|
"Config being used as a context manager without config items being set. " |
|
"Set config items via keyword arguments when calling the config object. " |
|
"For example, using config as a context manager should be like:\n\n" |
|
' >>> with cfg(breakfast="spam"):\n' |
|
" ... ... # Do stuff\n" |
|
) |
|
self.__class__._context_stack.append(self.__class__._prev) |
|
self.__class__._prev = None |
|
return self |
|
|
|
def __exit__(self, exc_type, exc_value, traceback): |
|
prev = self.__class__._context_stack.pop() |
|
for key, val in prev.items(): |
|
setattr(self, key, val) |
|
|
|
|
|
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 BackendPriorities(Config, strict=False): |
|
"""Configuration to control automatic conversion to and calling of backends. |
|
|
|
Priority is given to backends listed earlier. |
|
|
|
Parameters |
|
---------- |
|
algos : list of backend names |
|
This controls "algorithms" such as ``nx.pagerank`` that don't return a graph. |
|
generators : list of backend names |
|
This controls "generators" such as ``nx.from_pandas_edgelist`` that return a graph. |
|
kwargs : variadic keyword arguments of function name to list of backend names |
|
This allows each function to be configured separately and will override the config |
|
in ``algos`` or ``generators`` if present. The dispatchable function name may be |
|
gotten from the ``.name`` attribute such as ``nx.pagerank.name`` (it's typically |
|
the same as the name of the function). |
|
""" |
|
|
|
algos: list[str] |
|
generators: list[str] |
|
|
|
def _on_setattr(self, key, value): |
|
from .backends import _registered_algorithms, backend_info |
|
|
|
if key in {"algos", "generators"}: |
|
pass |
|
elif key not in _registered_algorithms: |
|
raise AttributeError( |
|
f"Invalid config name: {key!r}. Expected 'algos', 'generators', or a name " |
|
"of a dispatchable function (e.g. `.name` attribute of the function)." |
|
) |
|
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 backend_info}: |
|
missing = ", ".join(map(repr, sorted(missing))) |
|
raise ValueError(f"Unknown backend when setting {key!r}: {missing}") |
|
return value |
|
|
|
def _on_delattr(self, key): |
|
if key in {"algos", "generators"}: |
|
raise TypeError(f"{key!r} configuration item can't be deleted.") |
|
|
|
|
|
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 or dict or BackendPriorities |
|
Enable automatic conversion of graphs to backend graphs for functions |
|
implemented by the backend. Priority is given to backends listed earlier. |
|
This is a nested configuration with keys ``algos``, ``generators``, and, |
|
optionally, function names. Setting this value to a list of backend names |
|
will set ``nx.config.backend_priority.algos``. For more information, see |
|
``help(nx.config.backend_priority)``. 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 True. |
|
|
|
fallback_to_nx : bool |
|
If True, then "fall back" and run with the default "networkx" implementation |
|
for dispatchable functions not implemented by backends of input graphs. When a |
|
backend graph is passed to a dispatchable function, the default behavior is to |
|
use the implementation from that backend if possible and raise if not. Enabling |
|
``fallback_to_nx`` makes the networkx implementation the fallback to use instead |
|
of raising, and will convert the backend graph to a networkx-compatible graph. |
|
Default is False. |
|
|
|
warnings_to_ignore : set of strings |
|
Control which warnings from NetworkX are not emitted. Valid elements: |
|
|
|
- `"cache"`: when a cached value is used from ``G.__networkx_cache__``. |
|
|
|
Notes |
|
----- |
|
Environment variables may be used to control some default configurations: |
|
|
|
- ``NETWORKX_BACKEND_PRIORITY``: set ``backend_priority.algos`` from comma-separated names. |
|
- ``NETWORKX_CACHE_CONVERTED_GRAPHS``: set ``cache_converted_graphs`` to True if nonempty. |
|
- ``NETWORKX_FALLBACK_TO_NX``: set ``fallback_to_nx`` to True if nonempty. |
|
- ``NETWORKX_WARNINGS_TO_IGNORE``: set `warnings_to_ignore` from comma-separated names. |
|
|
|
and can be used for finer control of ``backend_priority`` such as: |
|
|
|
- ``NETWORKX_BACKEND_PRIORITY_ALGOS``: same as ``NETWORKX_BACKEND_PRIORITY`` |
|
to set ``backend_priority.algos``. |
|
|
|
This is a global configuration. Use with caution when using from multiple threads. |
|
""" |
|
|
|
backend_priority: BackendPriorities |
|
backends: Config |
|
cache_converted_graphs: bool |
|
fallback_to_nx: bool |
|
warnings_to_ignore: set[str] |
|
|
|
def _on_setattr(self, key, value): |
|
from .backends import backend_info |
|
|
|
if key == "backend_priority": |
|
if isinstance(value, list): |
|
|
|
value = BackendPriorities( |
|
**dict( |
|
self.backend_priority, |
|
algos=self.backend_priority._on_setattr("algos", value), |
|
) |
|
) |
|
elif isinstance(value, dict): |
|
kwargs = value |
|
value = BackendPriorities(algos=[], generators=[]) |
|
for key, val in kwargs.items(): |
|
setattr(value, key, val) |
|
elif not isinstance(value, BackendPriorities): |
|
raise TypeError( |
|
f"{key!r} config must be a dict of lists of backend names; got {value!r}" |
|
) |
|
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 backend_info}: |
|
missing = ", ".join(map(repr, sorted(missing))) |
|
raise ValueError(f"Unknown backend when setting {key!r}: {missing}") |
|
elif key in {"cache_converted_graphs", "fallback_to_nx"}: |
|
if not isinstance(value, bool): |
|
raise TypeError(f"{key!r} config must be True or False; got {value!r}") |
|
elif key == "warnings_to_ignore": |
|
if not (isinstance(value, set) and all(isinstance(x, str) for x in value)): |
|
raise TypeError( |
|
f"{key!r} config must be a set of warning names; got {value!r}" |
|
) |
|
known_warnings = {"cache"} |
|
if missing := {x for x in value if x not in known_warnings}: |
|
missing = ", ".join(map(repr, sorted(missing))) |
|
raise ValueError( |
|
f"Unknown warning when setting {key!r}: {missing}. Valid entries: " |
|
+ ", ".join(sorted(known_warnings)) |
|
) |
|
return value |
|
|