Introduce default_args context manager (#39964)

This adds a rather trivial context manager that lets you deduplicate repeated
arguments in directives, e.g.

```python
depends_on("py-x@1", when="@1", type=("build", "run"))
depends_on("py-x@2", when="@2", type=("build", "run"))
depends_on("py-x@3", when="@3", type=("build", "run"))
depends_on("py-x@4", when="@4", type=("build", "run"))
```

can be condensed to

```python
with default_args(type=("build", "run")):
    depends_on("py-x@1", when="@1")
    depends_on("py-x@2", when="@2")
    depends_on("py-x@3", when="@3")
    depends_on("py-x@4", when="@4")
```

The advantage is it's clear for humans, the downside it's less clear for type checkers due to type erasure.
This commit is contained in:
Harmen Stoppels 2023-11-06 19:22:29 +01:00 committed by GitHub
parent b5538960c3
commit 1235084c20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 94 additions and 18 deletions

View file

@ -3503,6 +3503,56 @@ is equivalent to:
Constraints from nested context managers are also combined together, but they are rarely Constraints from nested context managers are also combined together, but they are rarely
needed or recommended. needed or recommended.
.. _default_args:
------------------------
Common default arguments
------------------------
Similarly, if directives have a common set of default arguments, you can
group them together in a ``with default_args()`` block:
.. code-block:: python
class PyExample(PythonPackage):
with default_args(type=("build", "run")):
depends_on("py-foo")
depends_on("py-foo@2:", when="@2:")
depends_on("py-bar")
depends_on("py-bz")
The above is short for:
.. code-block:: python
class PyExample(PythonPackage):
depends_on("py-foo", type=("build", "run"))
depends_on("py-foo@2:", when="@2:", type=("build", "run"))
depends_on("py-bar", type=("build", "run"))
depends_on("py-bz", type=("build", "run"))
.. note::
The ``with when()`` context manager is composable, while ``with default_args()``
merely overrides the default. For example:
.. code-block:: python
with default_args(when="+feature"):
depends_on("foo")
depends_on("bar")
depends_on("baz", when="+baz")
is equivalent to:
.. code-block:: python
depends_on("foo", when="+feature")
depends_on("bar", when="+feature")
depends_on("baz", when="+baz") # Note: not when="+feature+baz"
.. _install-method: .. _install-method:
------------------ ------------------

View file

@ -137,6 +137,7 @@ class DirectiveMeta(type):
_directive_dict_names: Set[str] = set() _directive_dict_names: Set[str] = set()
_directives_to_be_executed: List[str] = [] _directives_to_be_executed: List[str] = []
_when_constraints_from_context: List[str] = [] _when_constraints_from_context: List[str] = []
_default_args: List[dict] = []
def __new__(cls, name, bases, attr_dict): def __new__(cls, name, bases, attr_dict):
# Initialize the attribute containing the list of directives # Initialize the attribute containing the list of directives
@ -199,6 +200,16 @@ def pop_from_context():
"""Pop the last constraint from the context""" """Pop the last constraint from the context"""
return DirectiveMeta._when_constraints_from_context.pop() return DirectiveMeta._when_constraints_from_context.pop()
@staticmethod
def push_default_args(default_args):
"""Push default arguments"""
DirectiveMeta._default_args.append(default_args)
@staticmethod
def pop_default_args():
"""Pop default arguments"""
return DirectiveMeta._default_args.pop()
@staticmethod @staticmethod
def directive(dicts=None): def directive(dicts=None):
"""Decorator for Spack directives. """Decorator for Spack directives.
@ -259,7 +270,13 @@ def _decorator(decorated_function):
directive_names.append(decorated_function.__name__) directive_names.append(decorated_function.__name__)
@functools.wraps(decorated_function) @functools.wraps(decorated_function)
def _wrapper(*args, **kwargs): def _wrapper(*args, **_kwargs):
# First merge default args with kwargs
kwargs = dict()
for default_args in DirectiveMeta._default_args:
kwargs.update(default_args)
kwargs.update(_kwargs)
# Inject when arguments from the context # Inject when arguments from the context
if DirectiveMeta._when_constraints_from_context: if DirectiveMeta._when_constraints_from_context:
# Check that directives not yet supporting the when= argument # Check that directives not yet supporting the when= argument

View file

@ -26,6 +26,7 @@
""" """
import functools import functools
import inspect import inspect
from contextlib import contextmanager
from llnl.util.lang import caller_locals from llnl.util.lang import caller_locals
@ -271,6 +272,13 @@ def __exit__(self, exc_type, exc_val, exc_tb):
spack.directives.DirectiveMeta.pop_from_context() spack.directives.DirectiveMeta.pop_from_context()
@contextmanager
def default_args(**kwargs):
spack.directives.DirectiveMeta.push_default_args(kwargs)
yield
spack.directives.DirectiveMeta.pop_default_args()
class MultiMethodError(spack.error.SpackError): class MultiMethodError(spack.error.SpackError):
"""Superclass for multimethod dispatch errors""" """Superclass for multimethod dispatch errors"""

View file

@ -85,7 +85,7 @@
UpstreamPackageError, UpstreamPackageError,
) )
from spack.mixins import filter_compiler_wrappers from spack.mixins import filter_compiler_wrappers
from spack.multimethod import when from spack.multimethod import default_args, when
from spack.package_base import ( from spack.package_base import (
DependencyConflictError, DependencyConflictError,
build_system_flags, build_system_flags,

View file

@ -37,23 +37,24 @@ class PyBlack(PythonPackage):
depends_on("py-hatchling@1.8:", when="@22.10:", type="build") depends_on("py-hatchling@1.8:", when="@22.10:", type="build")
depends_on("py-hatch-vcs", when="@22.10:", type="build") depends_on("py-hatch-vcs", when="@22.10:", type="build")
depends_on("py-hatch-fancy-pypi-readme", when="@22.10:", type="build") depends_on("py-hatch-fancy-pypi-readme", when="@22.10:", type="build")
depends_on("python@3.8:", when="@23.7:", type=("build", "run"))
# Needed to ensure that Spack can bootstrap black with Python 3.6
depends_on("python@3.7:", when="@22.10:", type=("build", "run"))
depends_on("py-click@8:", type=("build", "run"))
depends_on("py-mypy-extensions@0.4.3:", type=("build", "run"))
depends_on("py-packaging@22:", when="@23.1:", type=("build", "run"))
depends_on("py-pathspec@0.9:", type=("build", "run"))
depends_on("py-platformdirs@2:", type=("build", "run"))
depends_on("py-tomli@1.1:", when="@22.8: ^python@:3.10", type=("build", "run"))
depends_on("py-tomli@1.1:", when="@21.7:22.6", type=("build", "run"))
depends_on("py-typing-extensions@3.10:", when="^python@:3.9", type=("build", "run"))
depends_on("py-colorama@0.4.3:", when="+colorama", type=("build", "run")) with default_args(type=("build", "run")):
depends_on("py-uvloop@0.15.2:", when="+uvloop", type=("build", "run")) depends_on("python@3.8:", when="@23.7:")
depends_on("py-aiohttp@3.7.4:", when="+d", type=("build", "run")) depends_on("python@3.7:", when="@22.10:")
depends_on("py-ipython@7.8:", when="+jupyter", type=("build", "run")) depends_on("py-click@8:")
depends_on("py-tokenize-rt@3.2:", when="+jupyter", type=("build", "run")) depends_on("py-mypy-extensions@0.4.3:")
depends_on("py-packaging@22:", when="@23.1:")
depends_on("py-pathspec@0.9:")
depends_on("py-platformdirs@2:")
depends_on("py-tomli@1.1:", when="@22.8: ^python@:3.10")
depends_on("py-tomli@1.1:", when="@21.7:22.6")
depends_on("py-typing-extensions@3.10:", when="^python@:3.9")
depends_on("py-colorama@0.4.3:", when="+colorama")
depends_on("py-uvloop@0.15.2:", when="+uvloop")
depends_on("py-aiohttp@3.7.4:", when="+d")
depends_on("py-ipython@7.8:", when="+jupyter")
depends_on("py-tokenize-rt@3.2:", when="+jupyter")
# Historical dependencies # Historical dependencies
depends_on("py-setuptools@45:", when="@:22.8", type=("build", "run")) depends_on("py-setuptools@45:", when="@:22.8", type=("build", "run"))