From 1235084c20f1efabbca680c03f9f4dc023b44c5d Mon Sep 17 00:00:00 2001 From: Harmen Stoppels Date: Mon, 6 Nov 2023 19:22:29 +0100 Subject: [PATCH] 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. --- lib/spack/docs/packaging_guide.rst | 50 +++++++++++++++++++ lib/spack/spack/directives.py | 19 ++++++- lib/spack/spack/multimethod.py | 8 +++ lib/spack/spack/package.py | 2 +- .../builtin/packages/py-black/package.py | 33 ++++++------ 5 files changed, 94 insertions(+), 18 deletions(-) diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst index 3b05ce8932..3dd1c7952d 100644 --- a/lib/spack/docs/packaging_guide.rst +++ b/lib/spack/docs/packaging_guide.rst @@ -3503,6 +3503,56 @@ is equivalent to: Constraints from nested context managers are also combined together, but they are rarely 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: ------------------ diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py index bfd57fc6f9..fcd72d5bfc 100644 --- a/lib/spack/spack/directives.py +++ b/lib/spack/spack/directives.py @@ -137,6 +137,7 @@ class DirectiveMeta(type): _directive_dict_names: Set[str] = set() _directives_to_be_executed: List[str] = [] _when_constraints_from_context: List[str] = [] + _default_args: List[dict] = [] def __new__(cls, name, bases, attr_dict): # Initialize the attribute containing the list of directives @@ -199,6 +200,16 @@ def pop_from_context(): """Pop the last constraint from the context""" 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 def directive(dicts=None): """Decorator for Spack directives. @@ -259,7 +270,13 @@ def _decorator(decorated_function): directive_names.append(decorated_function.__name__) @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 if DirectiveMeta._when_constraints_from_context: # Check that directives not yet supporting the when= argument diff --git a/lib/spack/spack/multimethod.py b/lib/spack/spack/multimethod.py index d3453beb79..0c66117242 100644 --- a/lib/spack/spack/multimethod.py +++ b/lib/spack/spack/multimethod.py @@ -26,6 +26,7 @@ """ import functools import inspect +from contextlib import contextmanager 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() +@contextmanager +def default_args(**kwargs): + spack.directives.DirectiveMeta.push_default_args(kwargs) + yield + spack.directives.DirectiveMeta.pop_default_args() + + class MultiMethodError(spack.error.SpackError): """Superclass for multimethod dispatch errors""" diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py index 9bf01be5d4..c537a7103a 100644 --- a/lib/spack/spack/package.py +++ b/lib/spack/spack/package.py @@ -85,7 +85,7 @@ UpstreamPackageError, ) from spack.mixins import filter_compiler_wrappers -from spack.multimethod import when +from spack.multimethod import default_args, when from spack.package_base import ( DependencyConflictError, build_system_flags, diff --git a/var/spack/repos/builtin/packages/py-black/package.py b/var/spack/repos/builtin/packages/py-black/package.py index bb6539d715..825d37a446 100644 --- a/var/spack/repos/builtin/packages/py-black/package.py +++ b/var/spack/repos/builtin/packages/py-black/package.py @@ -37,23 +37,24 @@ class PyBlack(PythonPackage): 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-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")) - depends_on("py-uvloop@0.15.2:", when="+uvloop", type=("build", "run")) - depends_on("py-aiohttp@3.7.4:", when="+d", type=("build", "run")) - depends_on("py-ipython@7.8:", when="+jupyter", type=("build", "run")) - depends_on("py-tokenize-rt@3.2:", when="+jupyter", type=("build", "run")) + with default_args(type=("build", "run")): + depends_on("python@3.8:", when="@23.7:") + depends_on("python@3.7:", when="@22.10:") + depends_on("py-click@8:") + 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 depends_on("py-setuptools@45:", when="@:22.8", type=("build", "run"))