| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396 |
- 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:
- # Enable the "simple" case of accepting config definition as keywords
- 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 # Save original class so we can pickle
- cls._prev = None # Stage previous configs to enable use as context manager
- cls._context_stack = [] # Stack of previous configs when used as context
- 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."""
- # Control behavior of attributes
- 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
- # Be a `collection.abc.Collection`
- 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__)
- # Add dunder methods for `collections.abc.Mapping`
- 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__ # config["<TAB>
- # Go ahead and make it a `collections.abc.Mapping`
- 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)
- # dataclass can define __eq__ for us, but do it here so it works after pickling
- def __eq__(self, other):
- if not isinstance(other, Config):
- return NotImplemented
- return self._orig_class == other._orig_class and self.items() == other.items()
- # Make pickle work
- def __reduce__(self):
- return self._deserialize, (self._orig_class, dict(self))
- @staticmethod
- def _deserialize(cls, kwargs):
- return cls(**kwargs)
- # Allow to be used as context manager
- 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())
- + ")"
- )
- # Register, b/c `Mapping.__subclasshook__` returns `NotImplemented`
- 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.
- classes : list of backend names
- This controls graph classes such as ``nx.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]
- classes: list[str]
- def _on_setattr(self, key, value):
- from .backends import _registered_algorithms, backend_info
- if key in {"algos", "generators", "classes"}:
- pass
- elif key not in _registered_algorithms:
- raise AttributeError(
- f"Invalid config name: {key!r}. Expected 'algos', 'generators', "
- "'classes', 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", "classes"}:
- 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``,
- ``classes``, 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):
- # `config.backend_priority = [backend]` sets `backend_priority.algos`
- value = BackendPriorities(
- **dict(
- self.backend_priority,
- algos=self.backend_priority._on_setattr("algos", value),
- )
- )
- elif isinstance(value, dict):
- kwargs = value
- value = BackendPriorities(algos=[], generators=[], classes=[])
- 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
|