Repository: holoviz/holoviews
Pull request #6413 (2024-10-15 14:16:48)

refactor: Align version check
This PR's main point is to align the version check of upstream packages. The following things have been done:

1) Uppercase the name as they are constants.
2) Use the following naming `PACKAGE_OPERATOR_MAJOR_MINOR_MICRO`. 
3) Switch the comparison to a tuple to avoid the overhead of creating `Version` just for checks and avoiding something like `Version(Version(bokeh.__version__).base_version)` to get around pre-release versions. 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) Report
Attention: Patch coverage is `95.30201%` with `7 lines` in your changes missing coverage. Please review.
> Project coverage is 88.50%. Comparing base [(`75888ac`)](https://app.codecov.io/gh/holoviz/holoviews/commit/75888ac6f8a36711965cd0397702b8b1f2790b05?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) to head [(`9bbe5f3`)](https://app.codecov.io/gh/holoviz/holoviews/commit/9bbe5f33e8c34ab467974d4eb5361ed7b4f80ba1?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).

| [Files with missing lines](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) | Patch % | Lines |
|---|---|---|
| [holoviews/core/data/ibis.py](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&filepath=holoviews%2Fcore%2Fdata%2Fibis.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL2NvcmUvZGF0YS9pYmlzLnB5) | 95.00% | [1 Missing :warning: ](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) |
| [holoviews/operation/element.py](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&filepath=holoviews%2Foperation%2Felement.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL29wZXJhdGlvbi9lbGVtZW50LnB5) | 80.00% | [1 Missing :warning: ](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) |
| [holoviews/plotting/mpl/\_\_init\_\_.py](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&filepath=holoviews%2Fplotting%2Fmpl%2F__init__.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL21wbC9fX2luaXRfXy5weQ==) | 50.00% | [1 Missing :warning: ](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) |
| [holoviews/plotting/mpl/chart.py](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&filepath=holoviews%2Fplotting%2Fmpl%2Fchart.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL21wbC9jaGFydC5weQ==) | 75.00% | [1 Missing :warning: ](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) |
| [holoviews/plotting/renderer.py](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&filepath=holoviews%2Fplotting%2Frenderer.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Bsb3R0aW5nL3JlbmRlcmVyLnB5) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) |
| [...oviews/tests/plotting/bokeh/test\_annotationplot.py](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&filepath=holoviews%2Ftests%2Fplotting%2Fbokeh%2Ftest_annotationplot.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Rlc3RzL3Bsb3R0aW5nL2Jva2VoL3Rlc3RfYW5ub3RhdGlvbnBsb3QucHk=) | 90.00% | [1 Missing :warning: ](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) |
| [holoviews/tests/ui/bokeh/test\_callback.py](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&filepath=holoviews%2Ftests%2Fui%2Fbokeh%2Ftest_callback.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz#diff-aG9sb3ZpZXdzL3Rlc3RzL3VpL2Jva2VoL3Rlc3RfY2FsbGJhY2sucHk=) | 66.66% | [1 Missing :warning: ](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main    #6413      +/-   ##
==========================================
- Coverage   88.50%   88.50%   -0.01%     
==========================================
  Files         323      323              
  Lines       68563    68554       -9     
==========================================
- Hits        60681    60672       -9     
  Misses       7882     7882              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/holoviz/holoviews/pull/6413?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=holoviz).


Patch:
diff --git a/holoviews/core/data/dask.py b/holoviews/core/data/dask.py
index 4d0034381..2b021f5a5 100644
--- a/holoviews/core/data/dask.py
+++ b/holoviews/core/data/dask.py
@@ -218,7 +218,7 @@ class DaskInterface(PandasInterface):
         for coord in indices:
             if any(isinstance(c, float) and np.isnan(c) for c in coord):
                 continue
-            if len(coord) == 1 and not util.PANDAS_GE_220:
+            if len(coord) == 1 and not util.PANDAS_GE_2_2_0:
                 coord = coord[0]
             group = group_type(groupby.get_group(coord), **group_kwargs)
             data.append((coord, group))
diff --git a/holoviews/core/data/ibis.py b/holoviews/core/data/ibis.py
index 1023067a7..06011025b 100644
--- a/holoviews/core/data/ibis.py
+++ b/holoviews/core/data/ibis.py
@@ -1,9 +1,7 @@
 import sys
 from collections.abc import Iterable
-from functools import lru_cache
 
 import numpy as np
-from packaging.version import Version
 
 from .. import util
 from ..element import Element
@@ -11,25 +9,10 @@ from ..ndmapping import NdMapping, item_check, sorted_context
 from .interface import DataError, Interface
 from .util import cached
 
-
-@lru_cache
-def ibis_version():
-    import ibis
-    return Version(ibis.__version__)
-
-
-@lru_cache
-def ibis4():
-    return ibis_version() >= Version("4.0")
-
-
-@lru_cache
-def ibis5():
-    return ibis_version() >= Version("5.0")
-
-@lru_cache
-def ibis9_5():
-    return ibis_version() >= Version("9.5")
+IBIS_VERSION = util._no_import_version("ibis-framework")
+IBIS_GE_4_0_0 = IBIS_VERSION >= (4, 0, 0)
+IBIS_GE_5_0_0 = IBIS_VERSION >= (5, 0, 0)
+IBIS_GE_9_5_0 = IBIS_VERSION >= (9, 5, 0)
 
 
 class IbisInterface(Interface):
@@ -120,7 +103,7 @@ class IbisInterface(Interface):
     @cached
     def length(self, dataset):
         # Get the length by counting the length of an empty query.
-        if ibis4():
+        if IBIS_GE_4_0_0:
             return dataset.data.count().execute()
         else:
             return dataset.data[[]].count().execute()
@@ -129,7 +112,7 @@ class IbisInterface(Interface):
     @cached
     def nonzero(cls, dataset):
         # Make an empty query to see if a row is returned.
-        if ibis4():
+        if IBIS_GE_4_0_0:
             return bool(len(dataset.data.head(1).execute()))
         else:
             return bool(len(dataset.data[[]].head(1).execute()))
@@ -163,7 +146,7 @@ class IbisInterface(Interface):
         dimension = dataset.get_dimension(dimension, strict=True)
         data = dataset.data[dimension.name]
         if (
-            ibis_version() > Version("3")
+            IBIS_VERSION > (3, 0, 0)
             and isinstance(data, ibis.expr.types.AnyColumn)
             and not expanded
         ):
@@ -178,12 +161,12 @@ class IbisInterface(Interface):
         bins = [int(v) if bins.dtype.kind in 'iu' else float(v) for v in bins]
         binned = expr.bucket(bins).name('bucket')
         hist = np.zeros(len(bins)-1)
-        if ibis4():
+        if IBIS_GE_4_0_0:
             hist_bins = binned.value_counts().order_by('bucket').execute()
         else:
             # sort_by will be removed in Ibis 5.0
             hist_bins = binned.value_counts().sort_by('bucket').execute()
-        metric_name = 'bucket_count' if ibis5() else 'count'
+        metric_name = 'bucket_count' if IBIS_GE_5_0_0 else 'count'
         for b, v in zip(hist_bins['bucket'], hist_bins[metric_name]):
             if np.isnan(b):
                 continue
@@ -212,11 +195,11 @@ class IbisInterface(Interface):
     def sort(cls, dataset, by=None, reverse=False):
         if by is None:
             by = []
-        if ibis_version() >= Version("6.0"):
+        if IBIS_VERSION >= (6, 0, 0):
             import ibis
             order = ibis.desc if reverse else ibis.asc
             return dataset.data.order_by([order(dataset.get_dimension(x).name) for x in by])
-        elif ibis4():
+        elif IBIS_GE_4_0_0:
             # Tuple syntax will be removed in Ibis 7.0:
             # https://github.com/ibis-project/ibis/pull/6082
             return dataset.data.order_by([(dataset.get_dimension(x).name, not reverse) for x in by])
@@ -245,7 +228,7 @@ class IbisInterface(Interface):
         if "hv_row_id__" in data.columns:
             return data
 
-        if ibis4():
+        if IBIS_GE_4_0_0:
             return data.mutate(hv_row_id__=ibis.row_number())
         elif cls.is_rowid_zero_indexed(data):
             return data.mutate(hv_row_id__=data.rowid())
@@ -290,7 +273,7 @@ class IbisInterface(Interface):
                 rows = [rows]
             data = data.filter([data.hv_row_id__.isin(rows)])
 
-        if ibis4():
+        if IBIS_GE_4_0_0:
             return data.drop("hv_row_id__")
         else:
             # Passing a sequence of fields to `drop` will be removed in Ibis 5.0
@@ -302,7 +285,7 @@ class IbisInterface(Interface):
         Given a dataset object and data in the appropriate format for
         the interface, return a simple scalar.
         """
-        if ibis4():
+        if IBIS_GE_4_0_0:
             count = data.count().execute()
         else:
             count = data[[]].count().execute()
@@ -326,7 +309,7 @@ class IbisInterface(Interface):
         group_by = [d.name for d in index_dims]
 
         # execute a query against the table to find the unique groups.
-        if ibis4():
+        if IBIS_GE_4_0_0:
             groups = dataset.data.group_by(group_by).aggregate().execute()
         else:
             # groupby will be removed in Ibis 5.0
@@ -392,7 +375,7 @@ class IbisInterface(Interface):
                 data["hv_row_id__"].isin(list(map(int, selection_mask)))
             )
 
-            if ibis4():
+            if IBIS_GE_4_0_0:
                 data = data.drop("hv_row_id__")
             else:
                 # Passing a sequence of fields to `drop` will be removed in Ibis 5.0
@@ -462,7 +445,7 @@ class IbisInterface(Interface):
         if all(util.isscalar(s) or len(s) == 1 for s in samples):
             items = [s[0] if isinstance(s, tuple) else s for s in samples]
             subset = data[dims[0].name].isin(items)
-            return data.filter(subset) if ibis9_5() else data[subset]
+            return data.filter(subset) if IBIS_GE_9_5_0 else data[subset]
 
         predicates = None
         for sample in samples:
@@ -508,7 +491,7 @@ class IbisInterface(Interface):
         }.get(function, function)
 
         if len(dimensions):
-            if ibis4():
+            if IBIS_GE_4_0_0:
                  selection = new.group_by(columns)
             else:
                 # groupby will be removed in Ibis 5.0
diff --git a/holoviews/core/data/pandas.py b/holoviews/core/data/pandas.py
index c09b0348e..9db9ca488 100644
--- a/holoviews/core/data/pandas.py
+++ b/holoviews/core/data/pandas.py
@@ -1,13 +1,12 @@
 import numpy as np
 import pandas as pd
-from packaging.version import Version
 from pandas.api.types import is_numeric_dtype
 
 from .. import util
 from ..dimension import Dimension, dimension_name
 from ..element import Element
 from ..ndmapping import NdMapping, item_check, sorted_context
-from ..util import PANDAS_GE_210
+from ..util import PANDAS_GE_2_1_0
 from .interface import DataError, Interface
 from .util import finite_range
 
@@ -251,14 +250,14 @@ class PandasInterface(Interface, PandasAPI):
         group_kwargs['dataset'] = dataset.dataset
 
         group_by = [d.name for d in index_dims]
-        if len(group_by) == 1 and util.pandas_version >= Version("1.5.0"):
+        if len(group_by) == 1 and util.PANDAS_VERSION >= (1, 5, 0):
             # Because of this deprecation warning from pandas 1.5.0:
             # In a future version of pandas, a length 1 tuple will be returned
             # when iterating over a groupby with a grouper equal to a list of length 1.
             # Don't supply a list with a single grouper to avoid this warning.
             group_by = group_by[0]
         groupby_kwargs = {"sort": False}
-        if PANDAS_GE_210:
+        if PANDAS_GE_2_1_0:
             groupby_kwargs["observed"] = False
         data = [(k, group_type(v, **group_kwargs)) for k, v in
                 dataset.data.groupby(group_by, **groupby_kwargs)]
@@ -296,7 +295,7 @@ class PandasInterface(Interface, PandasAPI):
                     if is_numeric_dtype(d) and c not in cols
                 ]
             groupby_kwargs = {"sort": False}
-            if PANDAS_GE_210:
+            if PANDAS_GE_2_1_0:
                 groupby_kwargs["observed"] = False
             grouped = reindexed.groupby(cols, **groupby_kwargs)
             df = grouped[numeric_cols].aggregate(fn, **kwargs).reset_index()
diff --git a/holoviews/core/util.py b/holoviews/core/util.py
index cd25fdd6a..279d6dbbd 100644
--- a/holoviews/core/util.py
+++ b/holoviews/core/util.py
@@ -18,6 +18,7 @@ import warnings
 from collections import defaultdict, namedtuple
 from contextlib import contextmanager
 from functools import partial
+from importlib.metadata import PackageNotFoundError, version
 from threading import Event, Thread
 from types import FunctionType
 
@@ -38,13 +39,13 @@ cmp = lambda a, b: (a>b)-(a<b)
 get_keywords = operator.attrgetter('varkw')
 
 # Versions
-numpy_version = Version(Version(np.__version__).base_version)
-param_version = Version(param.__version__)
-pandas_version = Version(pd.__version__)
+NUMPY_VERSION = Version(np.__version__).release
+PARAM_VERSION = Version(param.__version__).release
+PANDAS_VERSION = Version(pd.__version__).release
 
-NUMPY_GE_200 = numpy_version >= Version("2")
-PANDAS_GE_210 = pandas_version >= Version("2.1")
-PANDAS_GE_220 = pandas_version >= Version("2.2")
+NUMPY_GE_2_0_0 = NUMPY_VERSION >= (2, 0, 0)
+PANDAS_GE_2_1_0 = PANDAS_VERSION >= (2, 1, 0)
+PANDAS_GE_2_2_0 = PANDAS_VERSION >= (2, 2, 0)
 
 # Types
 generator_types = (zip, range, types.GeneratorType)
@@ -115,6 +116,14 @@ class VersionError(Exception):
         super().__init__(msg, **kwargs)
 
 
+def _no_import_version(name) -> tuple[int, int, int]:
+    """ Get version number without importing the library """
+    try:
+        return Version(version(name)).release
+    except PackageNotFoundError:
+        return (0, 0, 0)
+
+
 class Config(param.ParameterizedFunction):
     """
     Set of boolean configuration values to change HoloViews' global
diff --git a/holoviews/element/selection.py b/holoviews/element/selection.py
index a6c8ff43a..349c3ee66 100644
--- a/holoviews/element/selection.py
+++ b/holoviews/element/selection.py
@@ -32,10 +32,12 @@ class SelectionIndexExpr:
         self._index_skip = True
         if not index:
             return None, None, None
-        ds = self.clone(kdims=index_cols, new_type=Dataset)
+        clone_vdims = [vdim.name for vdim in self.vdims if vdim.name not in index_cols]
+        cols = clone_vdims + index_cols
+        ds = self.clone(kdims=index_cols, vdims=clone_vdims, new_type=Dataset)
         if len(index_cols) == 1:
             index_dim = index_cols[0]
-            vals = dim(index_dim).apply(ds.iloc[index], expanded=False)
+            vals = dim(index_dim).apply(ds.iloc[index, cols], expanded=False)
             if vals.dtype.kind == 'O' and all(isinstance(v, np.ndarray) for v in vals):
                 vals = [v for arr in vals for v in util.unique_iterator(arr)]
             expr = dim(index_dim).isin(list(util.unique_iterator(vals)))
@@ -43,7 +45,7 @@ class SelectionIndexExpr:
             get_shape = dim(self.dataset.get_dimension(index_cols[0]), np.shape)
             index_cols = [dim(self.dataset.get_dimension(c), np.ravel) for c in index_cols]
             vals = dim(index_cols[0], util.unique_zip, *index_cols[1:]).apply(
-                ds.iloc[index], expanded=True, flat=True
+                ds.iloc[index, cols], expanded=True, flat=True
             )
             contains = dim(index_cols[0], util.lzip, *index_cols[1:]).isin(vals, object=True)
             expr = dim(contains, np.reshape, get_shape)
diff --git a/holoviews/element/util.py b/holoviews/element/util.py
index 6e6f9edce..166aeb5f3 100644
--- a/holoviews/element/util.py
+++ b/holoviews/element/util.py
@@ -10,7 +10,7 @@ from ..core.data import PandasInterface, default_datatype
 from ..core.operation import Operation
 from ..core.sheetcoords import Slice
 from ..core.util import (
-    PANDAS_GE_210,
+    PANDAS_GE_2_1_0,
     cartesian_product,
     datetime_types,
     is_cyclic,
@@ -202,7 +202,7 @@ class categorical_aggregate2d(Operation):
     def _aggregate_dataset_pandas(self, obj):
         index_cols = [d.name for d in obj.kdims]
         groupby_kwargs = {"sort": False}
-        if PANDAS_GE_210:
+        if PANDAS_GE_2_1_0:
             groupby_kwargs["observed"] = False
         df = obj.data.set_index(index_cols).groupby(index_cols, **groupby_kwargs).first()
         label = 'unique' if len(df) == len(obj) else 'non-unique'
diff --git a/holoviews/operation/element.py b/holoviews/operation/element.py
index 7d302f6dd..d7f6852cd 100644
--- a/holoviews/operation/element.py
+++ b/holoviews/operation/element.py
@@ -630,7 +630,7 @@ class contours(Operation):
         if self.p.filled:
             vdims = [vdims[0].clone(range=crange)]
 
-        if Version(contourpy_version) >= Version('1.2'):
+        if Version(contourpy_version).release >= (1, 2, 0):
             line_type = LineType.ChunkCombinedNan
         else:
             line_type = LineType.ChunkCombinedOffset
@@ -821,7 +821,7 @@ class histogram(Operation):
         is_cupy = is_cupy_array(data)
         if is_cupy:
             import cupy
-            full_cupy_support = Version(cupy.__version__) > Version('8.0')
+            full_cupy_support = Version(cupy.__version__).release > (8, 0, 0)
             if not full_cupy_support and (normed or self.p.weight_dimension):
                 data = cupy.asnumpy(data)
                 is_cupy = False
@@ -830,17 +830,17 @@ class histogram(Operation):
 
         # Mask data
         if is_ibis_expr(data):
-            from ..core.data.ibis import ibis5, ibis9_5
+            from ..core.data.ibis import IBIS_GE_5_0_0, IBIS_GE_9_5_0
 
             mask = data.notnull()
             if self.p.nonzero:
                 mask = mask & (data != 0)
-            if ibis5():
+            if IBIS_GE_5_0_0:
                 data = data.as_table()
             else:
                 # to_projection removed in ibis 5.0.0
                 data = data.to_projection()
-            data = data.filter(mask) if ibis9_5() else data[mask]
+            data = data.filter(mask) if IBIS_GE_9_5_0 else data[mask]
             no_data = not len(data.head(1).execute())
             data = data[dim.name]
         else:
diff --git a/holoviews/plotting/bokeh/__init__.py b/holoviews/plotting/bokeh/__init__.py
index d3667b271..ee9c4136c 100644
--- a/holoviews/plotting/bokeh/__init__.py
+++ b/holoviews/plotting/bokeh/__init__.py
@@ -115,7 +115,7 @@ from .sankey import SankeyPlot
 from .stats import BivariatePlot, BoxWhiskerPlot, DistributionPlot, ViolinPlot
 from .tabular import TablePlot
 from .tiles import TilePlot
-from .util import bokeh_version  # noqa (API import)
+from .util import BOKEH_VERSION  # noqa (API import)
 
 Store.renderers['bokeh'] = BokehRenderer.instance()
 
diff --git a/holoviews/plotting/bokeh/annotation.py b/holoviews/plotting/bokeh/annotation.py
index 64f9fbdc4..e242434a8 100644
--- a/holoviews/plotting/bokeh/annotation.py
+++ b/holoviews/plotting/bokeh/annotation.py
@@ -23,7 +23,7 @@ from .styles import (
     line_properties,
     text_properties,
 )
-from .util import bokeh32, date_to_integer
+from .util import BOKEH_GE_3_2_0, date_to_integer
 
 arrow_start = {'<->': NormalHead, '<|-|>': NormalHead}
 arrow_end = {'->': NormalHead, '-[': TeeHead, '-|>': NormalHead,
@@ -39,7 +39,7 @@ class _SyntheticAnnotationPlot(ColorbarPlot):
     _allow_implicit_categories = False
 
     def __init__(self, element, **kwargs):
-        if not bokeh32:
+        if not BOKEH_GE_3_2_0:
             name = type(getattr(element, "last", element)).__name__
             msg = f'{name} element requires Bokeh >=3.2'
             raise ImportError(msg)
diff --git a/holoviews/plotting/bokeh/callbacks.py b/holoviews/plotting/bokeh/callbacks.py
index 0386a20c1..0ad851356 100644
--- a/holoviews/plotting/bokeh/callbacks.py
+++ b/holoviews/plotting/bokeh/callbacks.py
@@ -72,7 +72,7 @@ from ...streams import (
     Tap,
 )
 from ...util.warnings import warn
-from .util import bokeh33, convert_timestamp
+from .util import BOKEH_GE_3_3_0, convert_timestamp
 
 
 class Callback:
@@ -1569,7 +1569,7 @@ class BoxEditCallback(GlyphDrawCallback):
             renderer = self._path_initialize()
         if stream.styles:
             self._create_style_callback(cds, renderer.glyph)
-        if bokeh33:
+        if BOKEH_GE_3_3_0:
             # First version with Quad support
             box_tool = BoxEditTool(renderers=[renderer], **kwargs)
             self.plot.state.tools.append(box_tool)
diff --git a/holoviews/plotting/bokeh/element.py b/holoviews/plotting/bokeh/element.py
index b17662554..04f0a910f 100644
--- a/holoviews/plotting/bokeh/element.py
+++ b/holoviews/plotting/bokeh/element.py
@@ -70,11 +70,11 @@ from .styles import (
 )
 from .tabular import TablePlot
 from .util import (
+    BOKEH_GE_3_2_0,
+    BOKEH_GE_3_4_0,
+    BOKEH_GE_3_5_0,
+    BOKEH_GE_3_6_0,
     TOOL_TYPES,
-    bokeh32,
-    bokeh34,
-    bokeh35,
-    bokeh36,
     cds_column_replace,
     compute_layout_properties,
     date_to_integer,
@@ -553,7 +553,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
                 tool = tools.HoverTool(
                     tooltips=tooltips, tags=['hv_created'], **tool_opts
                 )
-            elif bokeh32 and isinstance(tool, str) and tool.endswith(
+            elif BOKEH_GE_3_2_0 and isinstance(tool, str) and tool.endswith(
                 ('wheel_zoom', 'zoom_in', 'zoom_out')
             ):
                 zoom_kwargs = {}
@@ -919,7 +919,7 @@ class ElementPlot(BokehPlot, GenericElementPlot):
         axis_specs = {'x': {}, 'y': {}}
         axis_specs['x']['x'] = self._axis_props(plots, subplots, element, ranges, pos=0) + (self.xaxis, {})
         if self.multi_y:
-            if not bokeh32:
+            if not BOKEH_GE_3_2_0:
                 self.param.warning('Independent axis zooming for multi_y=True only supported for Bokeh >=3.2')
             yaxes, extra_axis_specs = self._create_extra_axes(plots, subplots, element, ranges)
             axis_specs['y'].update(extra_axis_specs)
@@ -2417,9 +2417,9 @@ class ElementPlot(BokehPlot, GenericElementPlot):
         For scalebar on a subcoordinate_y plot Bokeh 3.6 is needed.
         """
 
-        if not bokeh34:
+        if not BOKEH_GE_3_4_0:
             raise RuntimeError("Scalebar requires Bokeh >= 3.4.0")
-        elif not bokeh36 and self._subcoord_overlaid:
+        elif not BOKEH_GE_3_6_0 and self._subcoord_overlaid:
             warn("Scalebar with subcoordinate_y requires Bokeh >= 3.6.0", RuntimeWarning)
 
         from bokeh.models import Metric, ScaleBar
@@ -3329,7 +3329,7 @@ class OverlayPlot(GenericOverlayPlot, LegendPlot):
             if zoom_tool.renderers:
                 raise RuntimeError(f'Found unexpected zoom renderers {zoom_tool.renderers}')
 
-            if zoom_tool_name == 'wheel_zoom' and bokeh35:
+            if zoom_tool_name == 'wheel_zoom' and BOKEH_GE_3_5_0:
                 zoom_tool.update(
                     hit_test=True,
                     hit_test_mode='hline',
diff --git a/holoviews/plotting/bokeh/links.py b/holoviews/plotting/bokeh/links.py
index 36ac59b92..02b49ff4c 100644
--- a/holoviews/plotting/bokeh/links.py
+++ b/holoviews/plotting/bokeh/links.py
@@ -13,7 +13,7 @@ from ..links import (
     VertexTableLink,
 )
 from ..plot import GenericElementPlot, GenericOverlayPlot
-from .util import bokeh34, bokeh35
+from .util import BOKEH_GE_3_4_0, BOKEH_GE_3_5_0
 
 
 class LinkCallback:
@@ -163,7 +163,7 @@ class RangeToolLinkCallback(LinkCallback):
                 target_range_name = range_name
             axes[range_name] = ax = target_plot.handles[target_range_name]
             interval = getattr(link, f'intervals{axis}', None)
-            if interval is not None and bokeh34:
+            if interval is not None and BOKEH_GE_3_4_0:
                 min, max = interval
                 if min is not None:
                     ax.min_interval = min
@@ -183,7 +183,7 @@ class RangeToolLinkCallback(LinkCallback):
 
         tool = RangeTool(**axes)
 
-        if bokeh35:
+        if BOKEH_GE_3_5_0:
             use_handles = getattr(link, 'use_handles', True)
             start_gesture = getattr(link, 'start_gesture', 'tap')
             inverted = getattr(link, 'inverted', True)
diff --git a/holoviews/plotting/bokeh/raster.py b/holoviews/plotting/bokeh/raster.py
index 8f281cc12..ff52dd974 100644
--- a/holoviews/plotting/bokeh/raster.py
+++ b/holoviews/plotting/bokeh/raster.py
@@ -11,7 +11,7 @@ from .chart import PointPlot
 from .element import ColorbarPlot, LegendPlot
 from .selection import BokehOverlaySelectionDisplay
 from .styles import base_properties, fill_properties, line_properties, mpl_to_bokeh
-from .util import bokeh33, bokeh34, colormesh
+from .util import BOKEH_GE_3_3_0, BOKEH_GE_3_4_0, colormesh
 
 
 class RasterPlot(ColorbarPlot):
@@ -72,7 +72,7 @@ class RasterPlot(ColorbarPlot):
                 formatter += '{custom}'
             tooltips.append((name, formatter))
 
-        if not bokeh34:  # https://github.com/bokeh/bokeh/issues/13598
+        if not BOKEH_GE_3_4_0:  # https://github.com/bokeh/bokeh/issues/13598
             datetime_code = """
             if (value === -9223372036854776) {
                 return "NaN"
@@ -321,7 +321,7 @@ class ImageStackPlot(RasterPlot):
         # Bokeh 3.3 has simple support for multi hover in a tuple.
         # https://github.com/bokeh/bokeh/pull/13193
         # https://github.com/bokeh/bokeh/pull/13366
-        if bokeh33:
+        if BOKEH_GE_3_3_0:
             xdim, ydim = element.kdims
             vdim = ", ".join([d.pprint_label for d in element.vdims])
             return [(xdim.pprint_label, '$x'), (ydim.pprint_label, '$y'), (vdim, '@image')], {}
diff --git a/holoviews/plotting/bokeh/util.py b/holoviews/plotting/bokeh/util.py
index 02d8cba1a..a767beb1f 100644
--- a/holoviews/plotting/bokeh/util.py
+++ b/holoviews/plotting/bokeh/util.py
@@ -62,12 +62,12 @@ from ...core.util import (
 from ...util.warnings import warn
 from ..util import dim_axis_label
 
-bokeh_version = Version(Version(bokeh.__version__).base_version)
-bokeh32 = bokeh_version >= Version("3.2")
-bokeh33 = bokeh_version >= Version("3.3")
-bokeh34 = bokeh_version >= Version("3.4")
-bokeh35 = bokeh_version >= Version("3.5")
-bokeh36 = bokeh_version >= Version("3.6")
+BOKEH_VERSION = Version(bokeh.__version__).release
+BOKEH_GE_3_2_0 = BOKEH_VERSION >= (3, 2, 0)
+BOKEH_GE_3_3_0 = BOKEH_VERSION >= (3, 3, 0)
+BOKEH_GE_3_4_0 = BOKEH_VERSION >= (3, 4, 0)
+BOKEH_GE_3_5_0 = BOKEH_VERSION >= (3, 5, 0)
+BOKEH_GE_3_6_0 = BOKEH_VERSION >= (3, 6, 0)
 
 TOOL_TYPES = {
     'pan': tools.PanTool,
diff --git a/holoviews/plotting/mpl/__init__.py b/holoviews/plotting/mpl/__init__.py
index 675f9d255..d9a386023 100644
--- a/holoviews/plotting/mpl/__init__.py
+++ b/holoviews/plotting/mpl/__init__.py
@@ -3,7 +3,6 @@ import os
 from colorcet import kbc, register_cmap
 from matplotlib import rc_params_from_file
 from matplotlib.colors import LinearSegmentedColormap, ListedColormap
-from packaging.version import Version
 from param import concrete_descendents
 
 from ...core import Collator, GridMatrix, Layout, config
@@ -27,8 +26,7 @@ from .renderer import MPLRenderer
 from .sankey import *
 from .stats import *
 from .tabular import *
-
-mpl_ge_150 = Version(mpl.__version__) >= Version('1.5.0')
+from .util import MPL_VERSION
 
 try:
     from pandas.plotting import register_matplotlib_converters
@@ -62,7 +60,7 @@ def set_style(key):
 
 # Define matplotlib based style cycles and Palettes
 def get_color_cycle():
-    if mpl_ge_150:
+    if MPL_VERSION >= (1, 5, 0):
         cyl = mpl.rcParams['axes.prop_cycle']
         # matplotlib 1.5 verifies that axes.prop_cycle *is* a cycler
         # but no guarantee that there's a `color` key.
diff --git a/holoviews/plotting/mpl/chart.py b/holoviews/plotting/mpl/chart.py
index 917b4a631..e054c0700 100644
--- a/holoviews/plotting/mpl/chart.py
+++ b/holoviews/plotting/mpl/chart.py
@@ -3,7 +3,6 @@ import numpy as np
 import param
 from matplotlib.collections import LineCollection
 from matplotlib.dates import DateFormatter, date2num
-from packaging.version import Version
 
 from ...core.dimension import Dimension
 from ...core.options import Store, abbreviated_exception
@@ -26,7 +25,7 @@ from ..util import compute_sizes, dim_range_key, get_min_distance, get_sideplot_
 from .element import ColorbarPlot, ElementPlot, LegendPlot
 from .path import PathPlot
 from .plot import AdjoinedPlot, mpl_rc_context
-from .util import MPL_GE_3_7, MPL_GE_3_9, mpl_version
+from .util import MPL_GE_3_7_0, MPL_GE_3_9_0, MPL_VERSION
 
 
 class ChartPlot(ElementPlot):
@@ -94,7 +93,7 @@ class CurvePlot(ChartPlot):
     def init_artists(self, ax, plot_args, plot_kwargs):
         xs, ys = plot_args
         if isdatetime(xs):
-            if MPL_GE_3_9:
+            if MPL_GE_3_9_0:
                 artist = ax.plot(xs, ys, '-', **plot_kwargs)[0]
             else:
                 artist = ax.plot_date(xs, ys, '-', **plot_kwargs)[0]
@@ -130,7 +129,7 @@ class ErrorPlot(ColorbarPlot):
     def init_artists(self, ax, plot_data, plot_kwargs):
         handles = ax.errorbar(*plot_data, **plot_kwargs)
         bottoms, tops = None, None
-        if mpl_version >= Version('2.0'):
+        if MPL_VERSION >= (2, 0, 0):
             _, caps, verts = handles
             if caps:
                 bottoms, tops = caps
@@ -503,7 +502,7 @@ class SideHistogramPlot(AdjoinedPlot, HistogramPlot):
         # Get colormapping options
         if isinstance(range_item, (HeatMap, Raster)) or (cdim and cdim in element):
             style = self.lookup_options(range_item, 'style')[self.cyclic_index]
-            if MPL_GE_3_7:
+            if MPL_GE_3_7_0:
                 # https://github.com/matplotlib/matplotlib/pull/28355
                 cmap = mpl.colormaps.get_cmap(style.get('cmap'))
             else:
diff --git a/holoviews/plotting/mpl/chart3d.py b/holoviews/plotting/mpl/chart3d.py
index 215efc489..4d6da7583 100644
--- a/holoviews/plotting/mpl/chart3d.py
+++ b/holoviews/plotting/mpl/chart3d.py
@@ -2,7 +2,6 @@ import numpy as np
 import param
 from matplotlib import cm
 from mpl_toolkits.mplot3d.art3d import Line3DCollection
-from packaging.version import Version
 
 from ...core import Dimension
 from ...core.options import abbreviated_exception
@@ -11,7 +10,7 @@ from ..util import map_colors
 from .chart import PointPlot
 from .element import ColorbarPlot
 from .path import PathPlot
-from .util import mpl_version
+from .util import MPL_VERSION
 
 
 class Plot3D(ColorbarPlot):
@@ -84,7 +83,7 @@ class Plot3D(ColorbarPlot):
         if self.disable_axes:
             axis.set_axis_off()
 
-        if mpl_version <= Version('1.5.9'):
+        if MPL_VERSION <= (1, 5, 9):
             axis.set_axis_bgcolor(self.bgcolor)
         else:
             axis.set_facecolor(self.bgcolor)
diff --git a/holoviews/plotting/mpl/element.py b/holoviews/plotting/mpl/element.py
index a3cd12340..79aba31af 100644
--- a/holoviews/plotting/mpl/element.py
+++ b/holoviews/plotting/mpl/element.py
@@ -9,7 +9,6 @@ import param
 from matplotlib import ticker
 from matplotlib.dates import date2num
 from matplotlib.image import AxesImage
-from packaging.version import Version
 
 from ...core import (
     CompositeOverlay,
@@ -27,7 +26,7 @@ from ...util.transform import dim
 from ..plot import GenericElementPlot, GenericOverlayPlot
 from ..util import color_intervals, dim_range_key, process_cmap
 from .plot import MPLPlot, mpl_rc_context
-from .util import EqHistNormalize, mpl_version, validate, wrap_formatter
+from .util import MPL_VERSION, EqHistNormalize, validate, wrap_formatter
 
 
 class ElementPlot(GenericElementPlot, MPLPlot):
@@ -136,7 +135,7 @@ class ElementPlot(GenericElementPlot, MPLPlot):
         subplots = list(self.subplots.values()) if self.subplots else []
         if self.zorder == 0 and key is not None:
             if self.bgcolor:
-                if mpl_version <= Version('1.5.9'):
+                if MPL_VERSION <= (1, 5, 9):
                     axis.set_axis_bgcolor(self.bgcolor)
                 else:
                     axis.set_facecolor(self.bgcolor)
diff --git a/holoviews/plotting/mpl/raster.py b/holoviews/plotting/mpl/raster.py
index 6c3527990..d7d10d2e9 100644
--- a/holoviews/plotting/mpl/raster.py
+++ b/holoviews/plotting/mpl/raster.py
@@ -2,7 +2,6 @@ import sys
 
 import numpy as np
 import param
-from packaging.version import Version
 
 from ...core import CompositeOverlay, Element, traversal
 from ...core.util import isfinite, match_spec, max_range, unique_iterator
@@ -11,7 +10,7 @@ from ..util import categorical_legend
 from .chart import PointPlot
 from .element import ColorbarPlot, ElementPlot, LegendPlot, OverlayPlot
 from .plot import GridPlot, MPLPlot, mpl_rc_context
-from .util import get_raster_array, mpl_version
+from .util import MPL_VERSION, get_raster_array
 
 
 class RasterBasePlot(ElementPlot):
@@ -193,7 +192,7 @@ class QuadMeshPlot(ColorbarPlot):
         if 'norm' in plot_kwargs: # vmin/vmax should now be exclusively in norm
             plot_kwargs.pop('vmin', None)
             plot_kwargs.pop('vmax', None)
-        if colorbar and mpl_version < Version('3.1'):
+        if colorbar and MPL_VERSION < (3, 1, 0):
             colorbar.set_norm(artist.norm)
             if hasattr(colorbar, 'set_array'):
                 # Compatibility with mpl < 3
diff --git a/holoviews/plotting/mpl/stats.py b/holoviews/plotting/mpl/stats.py
index 9764de4e1..324e1bf87 100644
--- a/holoviews/plotting/mpl/stats.py
+++ b/holoviews/plotting/mpl/stats.py
@@ -6,7 +6,7 @@ from ..mixins import MultiDistributionMixin
 from .chart import AreaPlot, ChartPlot
 from .path import PolygonPlot
 from .plot import AdjoinedPlot
-from .util import MPL_GE_3_9
+from .util import MPL_GE_3_9_0
 
 
 class DistributionPlot(AreaPlot):
@@ -78,7 +78,7 @@ class BoxPlot(MultiDistributionMixin, ChartPlot):
             d = group[group.vdims[0]]
             data.append(d[np.isfinite(d)])
             labels.append(label)
-        if MPL_GE_3_9:
+        if MPL_GE_3_9_0:
             style['tick_labels'] = labels
         else:
             style['labels'] = labels
@@ -161,7 +161,7 @@ class ViolinPlot(BoxPlot):
         stats_color = plot_kwargs.pop('stats_color', 'black')
         facecolors = plot_kwargs.pop('facecolors', [])
         edgecolors = plot_kwargs.pop('edgecolors', 'black')
-        if MPL_GE_3_9:
+        if MPL_GE_3_9_0:
             labels = {'tick_labels': plot_kwargs.pop('tick_labels')}
         else:
             labels = {'labels': plot_kwargs.pop('labels')}
@@ -206,7 +206,7 @@ class ViolinPlot(BoxPlot):
             labels.append(label)
             colors.append(elstyle[i].get('facecolors', 'blue'))
         style['positions'] = list(range(len(data)))
-        if MPL_GE_3_9:
+        if MPL_GE_3_9_0:
             style['tick_labels'] = labels
         else:
             style['labels'] = labels
diff --git a/holoviews/plotting/mpl/util.py b/holoviews/plotting/mpl/util.py
index 99c5a9b37..d72d13304 100644
--- a/holoviews/plotting/mpl/util.py
+++ b/holoviews/plotting/mpl/util.py
@@ -36,9 +36,9 @@ from ...core.util import arraylike_types, cftime_types, is_number
 from ...element import RGB, Polygons, Raster
 from ..util import COLOR_ALIASES, RGB_HEX_REGEX
 
-mpl_version = Version(mpl.__version__)
-MPL_GE_3_7 = mpl_version >= Version('3.7')
-MPL_GE_3_9 = mpl_version >= Version('3.9')
+MPL_VERSION = Version(mpl.__version__).release
+MPL_GE_3_7_0 = MPL_VERSION >= (3, 7, 0)
+MPL_GE_3_9_0 = MPL_VERSION >= (3, 9, 0)
 
 
 def is_color(color):
@@ -88,7 +88,7 @@ def get_old_rcparams():
     ]
     old_rcparams = {
         k: v for k, v in mpl.rcParams.items()
-        if mpl_version < Version('3.0') or k not in deprecated_rcparams
+        if MPL_VERSION < (3, 0, 0) or k not in deprecated_rcparams
     }
     return old_rcparams
 
diff --git a/holoviews/plotting/plotly/__init__.py b/holoviews/plotting/plotly/__init__.py
index 58a676160..4b4c20c6a 100644
--- a/holoviews/plotting/plotly/__init__.py
+++ b/holoviews/plotting/plotly/__init__.py
@@ -21,7 +21,7 @@ from .stats import *
 from .tabular import *
 from .tiles import *
 
-if Version(plotly.__version__) < Version('4.0.0'):
+if Version(plotly.__version__).release < (4, 0, 0):
     raise VersionError(
         "The plotly extension requires a plotly version >=4.0.0, "
         f"please upgrade from plotly {plotly.__version__} to a more recent version.", plotly.__version__, '4.0.0')
diff --git a/holoviews/plotting/renderer.py b/holoviews/plotting/renderer.py
index 361de8193..e448d580f 100644
--- a/holoviews/plotting/renderer.py
+++ b/holoviews/plotting/renderer.py
@@ -40,7 +40,7 @@ from ..streams import Stream
 from . import Plot
 from .util import collate, displayable, initialize_dynamic
 
-panel_version = Version(pn.__version__)
+PANEL_VERSION = Version(pn.__version__).release
 
 # Tags used when visual output is to be embedded in HTML
 IMAGE_TAG = "<img src='{src}' style='max-width:100%; margin: auto; display: block; {css}'/>"
@@ -649,9 +649,9 @@ class Renderer(Exporter):
         Loads any resources required for display of plots
         in the Jupyter notebook
         """
-        if panel_version >= Version('1.0.2'):
+        if PANEL_VERSION >= (1, 0, 2):
             load_notebook(inline, reloading=reloading, enable_mathjax=enable_mathjax)
-        elif panel_version >= Version('1.0.0'):
+        elif PANEL_VERSION >= (1, 0, 0):
             load_notebook(inline, reloading=reloading)
         elif reloading:
             return
diff --git a/holoviews/plotting/util.py b/holoviews/plotting/util.py
index 27b035ab8..94f086645 100644
--- a/holoviews/plotting/util.py
+++ b/holoviews/plotting/util.py
@@ -570,7 +570,7 @@ def mplcmap_to_palette(cmap, ncolors=None, categorical=False):
         if cmap.startswith('Category'):
             cmap = cmap.replace('Category', 'tab')
 
-        if Version(mpl.__version__) < Version("3.5"):
+        if Version(mpl.__version__).release < (3, 5, 0):
             from matplotlib import cm
             try:
                 cmap = cm.get_cmap(cmap)
diff --git a/scripts/download_data.py b/scripts/download_data.py
index 00c900eab..21ac8a612 100644
--- a/scripts/download_data.py
+++ b/scripts/download_data.py
@@ -3,7 +3,7 @@ from contextlib import suppress
 import bokeh
 from packaging.version import Version
 
-if Version(Version(bokeh.__version__).base_version) < Version("3.5"):
+if Version(bokeh.__version__).release < (3, 5, 0):
     import bokeh.sampledata
 
     bokeh.sampledata.download()


Test patch:
diff --git a/examples/conftest.py b/examples/conftest.py
index b8af528b5..9341fbe42 100644
--- a/examples/conftest.py
+++ b/examples/conftest.py
@@ -9,7 +9,7 @@ from packaging.version import Version
 
 system = platform.system()
 py_version = sys.version_info[:2]
-PD2 = Version(pd.__version__) >= Version("2.0")
+PANDAS_GE_2_0_0 = Version(pd.__version__).release >= (2, 0, 0)
 
 # Having "OMP_NUM_THREADS"=1, set as an environment variable, can be needed
 # to avoid crashing when running tests with pytest-xdist on Windows.
@@ -30,7 +30,7 @@ collect_ignore_glob = [
 
 # 2023-07-14 with following error:
 # ValueError: Buffer dtype mismatch, expected 'const int64_t' but got 'int'
-if PD2 and system == "Windows":
+if PANDAS_GE_2_0_0 and system == "Windows":
     collect_ignore_glob += [
         "gallery/demos/bokeh/point_draw_triangulate.ipynb",
         "reference/elements/*/TriMesh.ipynb",
@@ -45,7 +45,7 @@ if system == "Windows":
     ]
 
 # First available in Bokeh 3.2.0
-if Version(bokeh.__version__) < Version("3.2.0"):
+if Version(bokeh.__version__).release < (3, 2, 0):
     collect_ignore_glob += [
         "reference/elements/bokeh/HLines.ipynb",
         "reference/elements/bokeh/HSpans.ipynb",
diff --git a/holoviews/tests/core/data/test_daskinterface.py b/holoviews/tests/core/data/test_daskinterface.py
index 05aa2661d..4f01120a1 100644
--- a/holoviews/tests/core/data/test_daskinterface.py
+++ b/holoviews/tests/core/data/test_daskinterface.py
@@ -3,7 +3,6 @@ from unittest import SkipTest
 
 import numpy as np
 import pandas as pd
-from packaging.version import Version
 
 try:
     import dask.dataframe as dd
@@ -11,7 +10,7 @@ except ImportError:
     raise SkipTest("Could not import dask, skipping DaskInterface tests.")
 
 from holoviews.core.data import Dataset
-from holoviews.core.util import pandas_version
+from holoviews.core.util import PANDAS_VERSION
 from holoviews.util.transform import dim
 
 from ...utils import dask_switcher
@@ -82,14 +81,14 @@ class _DaskDatasetTest(BasePandasInterfaceTests):
         raise SkipTest("Temporarily skipped")
 
     @unittest.skipIf(
-        pandas_version >= Version("2.0"),
+        PANDAS_VERSION >= (2, 0, 0),
         reason="Not supported yet, https://github.com/dask/dask/issues/9913"
     )
     def test_dataset_aggregate_ht(self):
         super().test_dataset_aggregate_ht()
 
     @unittest.skipIf(
-        pandas_version >= Version("2.0"),
+        PANDAS_VERSION >= (2, 0, 0),
         reason="Not supported yet, https://github.com/dask/dask/issues/9913"
     )
     def test_dataset_aggregate_ht_alias(self):
diff --git a/holoviews/tests/core/data/test_ibisinterface.py b/holoviews/tests/core/data/test_ibisinterface.py
index 907b038f3..65f66648a 100644
--- a/holoviews/tests/core/data/test_ibisinterface.py
+++ b/holoviews/tests/core/data/test_ibisinterface.py
@@ -10,10 +10,9 @@ except ImportError:
 
 import numpy as np
 import pandas as pd
-from packaging.version import Version
 
 from holoviews.core.data import Dataset
-from holoviews.core.data.ibis import IbisInterface, ibis_version
+from holoviews.core.data.ibis import IBIS_VERSION, IbisInterface
 from holoviews.core.spaces import HoloMap
 
 from .base import HeterogeneousColumnTests, InterfaceTests, ScalarColumnTests
@@ -158,7 +157,7 @@ class IbisDatasetTest(HeterogeneousColumnTests, ScalarColumnTests, InterfaceTest
         raise SkipTest("Not supported")
 
     def test_dataset_dataset_ht_dtypes(self):
-        int_dtype = "int64" if ibis_version() >= Version("9.0") else "int32"
+        int_dtype = "int64" if IBIS_VERSION >= (9, 0, 0) else "int32"
         ds = self.table
         self.assertEqual(ds.interface.dtype(ds, "Gender"), np.dtype("object"))
         self.assertEqual(ds.interface.dtype(ds, "Age"), np.dtype(int_dtype))
@@ -166,7 +165,7 @@ class IbisDatasetTest(HeterogeneousColumnTests, ScalarColumnTests, InterfaceTest
         self.assertEqual(ds.interface.dtype(ds, "Height"), np.dtype("float64"))
 
     def test_dataset_dtypes(self):
-        int_dtype = "int64" if ibis_version() >= Version("9.0") else "int32"
+        int_dtype = "int64" if IBIS_VERSION >= (9, 0, 0) else "int32"
         self.assertEqual(
             self.dataset_hm.interface.dtype(self.dataset_hm, "x"), np.dtype(int_dtype)
         )
diff --git a/holoviews/tests/core/test_dimensions.py b/holoviews/tests/core/test_dimensions.py
index 7b79e1bce..273af05a0 100644
--- a/holoviews/tests/core/test_dimensions.py
+++ b/holoviews/tests/core/test_dimensions.py
@@ -5,7 +5,7 @@ import numpy as np
 import pandas as pd
 
 from holoviews.core import Dimension, Dimensioned
-from holoviews.core.util import NUMPY_GE_200
+from holoviews.core.util import NUMPY_GE_2_0_0
 from holoviews.element.comparison import ComparisonTestCase
 
 from ..utils import LoggingComparisonTestCase
@@ -244,7 +244,7 @@ class DimensionCloneTest(ComparisonTestCase):
 class DimensionDefaultTest(ComparisonTestCase):
 
     def test_validate_default_against_values(self):
-        if NUMPY_GE_200:
+        if NUMPY_GE_2_0_0:
             msg = r"Dimension\('A'\) default 1\.1 not found in declared values: \[np\.int64\(0\), np\.int64\(1\)\]"
         else:
             msg = r"Dimension\('A'\) default 1\.1 not found in declared values: \[0, 1\]"
diff --git a/holoviews/tests/element/test_annotations.py b/holoviews/tests/element/test_annotations.py
index 9731459ee..ec25f2511 100644
--- a/holoviews/tests/element/test_annotations.py
+++ b/holoviews/tests/element/test_annotations.py
@@ -15,7 +15,7 @@ class AnnotationTests(ComparisonTestCase):
     """
 
     def test_hline_invalid_constructor(self):
-        if Version(param.__version__) > Version('2.0.0a2'):
+        if Version(param.__version__).release >= (2, 0, 0):
             err = "ClassSelector parameter 'HLine.y' value must be an instance of"
         else:
             err = "ClassSelector parameter 'y' value must be an instance of"
diff --git a/holoviews/tests/element/test_comparisondimension.py b/holoviews/tests/element/test_comparisondimension.py
index 3bdfb04c4..876cd0ef0 100644
--- a/holoviews/tests/element/test_comparisondimension.py
+++ b/holoviews/tests/element/test_comparisondimension.py
@@ -2,7 +2,7 @@
 Test cases for Dimension and Dimensioned object comparison.
 """
 from holoviews.core import Dimension, Dimensioned
-from holoviews.core.util import NUMPY_GE_200
+from holoviews.core.util import NUMPY_GE_2_0_0
 from holoviews.element.comparison import ComparisonTestCase
 
 
@@ -75,7 +75,7 @@ class DimensionsComparisonTestCase(ComparisonTestCase):
         try:
             self.assertEqual(self.dimension4, self.dimension8)
         except AssertionError as e:
-            if NUMPY_GE_200:
+            if NUMPY_GE_2_0_0:
                 msg = "Dimension parameter 'values' mismatched: [] != [np.str_('a'), np.str_('b')]"
             else:
                 msg = "Dimension parameter 'values' mismatched: [] != ['a', 'b']"
diff --git a/holoviews/tests/element/test_selection.py b/holoviews/tests/element/test_selection.py
index 6c2e89101..05f8ff341 100644
--- a/holoviews/tests/element/test_selection.py
+++ b/holoviews/tests/element/test_selection.py
@@ -30,6 +30,7 @@ from holoviews.element import (
 )
 from holoviews.element.comparison import ComparisonTestCase
 from holoviews.element.selection import spatial_select_columnar
+from holoviews.util.transform import dim
 
 from ..utils import dask_switcher
 
@@ -61,6 +62,29 @@ ds_available = skipIf(ds is None, 'datashader not available')
 dd_available = pytest.mark.skipif(dd is None, reason='dask.dataframe not available')
 
 
+class TestIndexExpr(ComparisonTestCase):
+
+    def setUp(self):
+        import holoviews.plotting.bokeh # noqa
+        super().setUp()
+        self._backend = Store.current_backend
+        Store.set_current_backend('bokeh')
+
+    def tearDown(self):
+        Store.current_backend = self._backend
+
+    def test_index_selection_on_id_column(self):
+        # tests issue in https://github.com/holoviz/holoviews/pull/6336
+        x, y = np.random.randn(2, 100)
+        idx = np.arange(100)
+
+        points = Points(
+            {'x': x, 'y': y, 'id': idx}, kdims=['x', 'y'], vdims=['id'], datatype=['dataframe']
+        )
+        sel, _, _ = points._get_index_selection([3, 7], ['id'])
+        assert sel == dim('id').isin([3, 7])
+
+
 class TestSelection1DExpr(ComparisonTestCase):
 
     def setUp(self):
diff --git a/holoviews/tests/plotting/bokeh/test_annotationplot.py b/holoviews/tests/plotting/bokeh/test_annotationplot.py
index 7ce298693..b52809763 100644
--- a/holoviews/tests/plotting/bokeh/test_annotationplot.py
+++ b/holoviews/tests/plotting/bokeh/test_annotationplot.py
@@ -17,11 +17,11 @@ from holoviews.element import (
     VSpan,
     VSpans,
 )
-from holoviews.plotting.bokeh.util import bokeh32, bokeh33, bokeh34
+from holoviews.plotting.bokeh.util import BOKEH_GE_3_2_0, BOKEH_GE_3_3_0, BOKEH_GE_3_4_0
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 
-if bokeh32:
+if BOKEH_GE_3_2_0:
     from bokeh.models import (
         HSpan as BkHSpan,
         HStrip as BkHStrip,
@@ -29,9 +29,9 @@ if bokeh32:
         VStrip as BkVStrip,
     )
 
-if bokeh34:
+if BOKEH_GE_3_4_0:
     from bokeh.models import Node
-elif bokeh33:
+elif BOKEH_GE_3_3_0:
     from bokeh.models.coordinates import Node
 
 
@@ -75,7 +75,7 @@ class TestHVSpanPlot(TestBokehPlot):
 
         assert span.left == 1.1
         assert span.right == 1.5
-        if bokeh33:
+        if BOKEH_GE_3_3_0:
             assert isinstance(span.bottom, Node)
             assert isinstance(span.top, Node)
         else:
@@ -87,7 +87,7 @@ class TestHVSpanPlot(TestBokehPlot):
         hspan = HSpan(1.1, 1.5)
         plot = bokeh_renderer.get_plot(hspan)
         span = plot.handles['glyph']
-        if bokeh33:
+        if BOKEH_GE_3_3_0:
             assert isinstance(span.left, Node)
             assert isinstance(span.right, Node)
         else:
@@ -107,7 +107,7 @@ class TestHVSpanPlot(TestBokehPlot):
         vspan = VSpan(1.1, 1.5).opts(invert_axes=True)
         plot = bokeh_renderer.get_plot(vspan)
         span = plot.handles['glyph']
-        if bokeh33:
+        if BOKEH_GE_3_3_0:
             assert isinstance(span.left, Node)
             assert isinstance(span.right, Node)
         else:
@@ -123,7 +123,7 @@ class TestHVSpanPlot(TestBokehPlot):
         span = plot.handles['glyph']
         assert span.left == 1.1
         assert span.right == 1.5
-        if bokeh33:
+        if BOKEH_GE_3_3_0:
             assert isinstance(span.bottom, Node)
             assert isinstance(span.top, Node)
         else:
@@ -245,7 +245,7 @@ class TestLabelsPlot(TestBokehPlot):
 class TestHVLinesPlot(TestBokehPlot):
 
     def setUp(self):
-        if not bokeh32:
+        if not BOKEH_GE_3_2_0:
             raise unittest.SkipTest("Bokeh 3.2 added H/VLines")
         super().setUp()
 
@@ -444,7 +444,7 @@ class TestHVLinesPlot(TestBokehPlot):
 class TestHVSpansPlot(TestBokehPlot):
 
     def setUp(self):
-        if not bokeh32:
+        if not BOKEH_GE_3_2_0:
             raise unittest.SkipTest("Bokeh 3.2 added H/VSpans")
         super().setUp()
 
diff --git a/holoviews/tests/plotting/bokeh/test_elementplot.py b/holoviews/tests/plotting/bokeh/test_elementplot.py
index 50c34c507..a2687442a 100644
--- a/holoviews/tests/plotting/bokeh/test_elementplot.py
+++ b/holoviews/tests/plotting/bokeh/test_elementplot.py
@@ -21,7 +21,7 @@ from holoviews import opts
 from holoviews.core import Dimension, DynamicMap, HoloMap, NdOverlay, Overlay
 from holoviews.core.util import dt_to_int
 from holoviews.element import Curve, HeatMap, Image, Labels, Scatter
-from holoviews.plotting.bokeh.util import bokeh34, bokeh36
+from holoviews.plotting.bokeh.util import BOKEH_GE_3_4_0, BOKEH_GE_3_6_0
 from holoviews.plotting.util import process_cmap
 from holoviews.streams import PointDraw, Stream
 from holoviews.util import render
@@ -818,7 +818,7 @@ class TestElementPlot(LoggingComparisonTestCase, TestBokehPlot):
 
 
 @pytest.mark.usefixtures("bokeh_backend")
-@pytest.mark.skipif(not bokeh34, reason="requires Bokeh >= 3.4")
+@pytest.mark.skipif(not BOKEH_GE_3_4_0, reason="requires Bokeh >= 3.4")
 class TestScalebarPlot:
 
     def get_scalebar(self, element):
@@ -885,7 +885,7 @@ class TestScalebarPlot:
         scalebar_icon = [tool for tool in toolbar.tools if tool.description == "Toggle ScaleBar"]
         assert len(scalebar_icon) == 1
 
-    @pytest.mark.skipif(not bokeh36, reason="requires Bokeh >= 3.6")
+    @pytest.mark.skipif(not BOKEH_GE_3_6_0, reason="requires Bokeh >= 3.6")
     @pytest.mark.parametrize("enabled1", [True, False])
     @pytest.mark.parametrize("enabled2", [True, False])
     @pytest.mark.parametrize("enabled3", [True, False])
diff --git a/holoviews/tests/plotting/bokeh/test_rasterplot.py b/holoviews/tests/plotting/bokeh/test_rasterplot.py
index c5c404f6f..d743e0c97 100644
--- a/holoviews/tests/plotting/bokeh/test_rasterplot.py
+++ b/holoviews/tests/plotting/bokeh/test_rasterplot.py
@@ -7,7 +7,7 @@ from bokeh.models import CustomJSHover, HoverTool
 
 from holoviews.element import RGB, Image, ImageStack, Raster
 from holoviews.plotting.bokeh.raster import ImageStackPlot
-from holoviews.plotting.bokeh.util import bokeh34
+from holoviews.plotting.bokeh.util import BOKEH_GE_3_4_0
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 
@@ -150,7 +150,7 @@ class TestRasterPlot(TestBokehPlot):
         assert hover.tooltips[-1] == ("Timestamp", "@{Timestamp}{%F %T}")
         assert "@{Timestamp}" in hover.formatters
 
-        if bokeh34:  # https://github.com/bokeh/bokeh/issues/13598
+        if BOKEH_GE_3_4_0:  # https://github.com/bokeh/bokeh/issues/13598
             assert hover.formatters["@{Timestamp}"] == "datetime"
         else:
             assert isinstance(hover.formatters["@{Timestamp}"], CustomJSHover)
diff --git a/holoviews/tests/plotting/bokeh/test_subcoordy.py b/holoviews/tests/plotting/bokeh/test_subcoordy.py
index 041cb8059..408b90246 100644
--- a/holoviews/tests/plotting/bokeh/test_subcoordy.py
+++ b/holoviews/tests/plotting/bokeh/test_subcoordy.py
@@ -6,7 +6,7 @@ from holoviews.core import NdOverlay, Overlay
 from holoviews.element import Curve
 from holoviews.element.annotation import VSpan
 from holoviews.operation.normalization import subcoordinate_group_ranges
-from holoviews.plotting.bokeh.util import bokeh35
+from holoviews.plotting.bokeh.util import BOKEH_GE_3_5_0
 
 from .test_plot import TestBokehPlot, bokeh_renderer
 
@@ -368,7 +368,7 @@ class TestSubcoordinateY(TestBokehPlot):
             2: 'B / 0', 3: 'B / 1',
         }
 
-    @pytest.mark.skipif(bokeh35, reason="test Bokeh < 3.5")
+    @pytest.mark.skipif(BOKEH_GE_3_5_0, reason="test Bokeh < 3.5")
     def test_multiple_groups_wheel_zoom_configured(self):
         # Same as test_tools_default_wheel_zoom_configured
 
@@ -389,7 +389,7 @@ class TestSubcoordinateY(TestBokehPlot):
             assert zoom_tool.level == 1
             assert zoom_tool.description == f'Wheel Zoom ({group})'
 
-    @pytest.mark.skipif(not bokeh35, reason="requires Bokeh >= 3.5")
+    @pytest.mark.skipif(not BOKEH_GE_3_5_0, reason="requires Bokeh >= 3.5")
     def test_multiple_groups_wheel_zoom_configured_35(self):
         # Same as test_tools_default_wheel_zoom_configured
 
diff --git a/holoviews/tests/plotting/matplotlib/test_annotationplot.py b/holoviews/tests/plotting/matplotlib/test_annotationplot.py
index ed10b9500..57a297f3c 100644
--- a/holoviews/tests/plotting/matplotlib/test_annotationplot.py
+++ b/holoviews/tests/plotting/matplotlib/test_annotationplot.py
@@ -2,7 +2,7 @@ import numpy as np
 
 import holoviews as hv
 from holoviews.element import HLines, HSpans, VLines, VSpans
-from holoviews.plotting.mpl.util import MPL_GE_3_9
+from holoviews.plotting.mpl.util import MPL_GE_3_9_0
 
 from .test_plot import TestMPLPlot, mpl_renderer
 
@@ -157,7 +157,7 @@ class TestHVSpansPlot(TestMPLPlot):
 
     def _hspans_check(self, source, v0, v1):
         # Matplotlib 3.9+ uses a rectangle instead of polygon
-        if MPL_GE_3_9:
+        if MPL_GE_3_9_0:
             rect = [source.get_x(), source.get_y(), source.get_width(), source.get_height()]
             assert np.allclose(rect, [0, v0, 1, v1 - v0])
         else:
@@ -166,7 +166,7 @@ class TestHVSpansPlot(TestMPLPlot):
 
     def _vspans_check(self, source, v0, v1):
         # Matplotlib 3.9+ uses a rectangle instead of polygon
-        if MPL_GE_3_9:
+        if MPL_GE_3_9_0:
             rect = [source.get_x(), source.get_y(), source.get_width(), source.get_height()]
             assert np.allclose(rect, [v0, 0, v1 - v0, 1])
         else:
diff --git a/holoviews/tests/plotting/matplotlib/test_boxwhisker.py b/holoviews/tests/plotting/matplotlib/test_boxwhisker.py
index 2c2ec926b..7dba8a96a 100644
--- a/holoviews/tests/plotting/matplotlib/test_boxwhisker.py
+++ b/holoviews/tests/plotting/matplotlib/test_boxwhisker.py
@@ -1,7 +1,7 @@
 import numpy as np
 
 from holoviews.element import BoxWhisker
-from holoviews.plotting.mpl.util import MPL_GE_3_9
+from holoviews.plotting.mpl.util import MPL_GE_3_9_0
 
 from .test_plot import TestMPLPlot, mpl_renderer
 
@@ -14,7 +14,7 @@ class TestMPLBoxWhiskerPlot(TestMPLPlot):
         plot = mpl_renderer.get_plot(boxwhisker)
         data, style, axis_opts = plot.get_data(boxwhisker, {}, {})
         self.assertEqual(data[0][0], values)
-        if MPL_GE_3_9:
+        if MPL_GE_3_9_0:
             self.assertEqual(style['tick_labels'], [''])
         else:
             self.assertEqual(style['labels'], [''])
diff --git a/holoviews/tests/plotting/matplotlib/test_graphplot.py b/holoviews/tests/plotting/matplotlib/test_graphplot.py
index 5a6ad1b2b..266443e46 100644
--- a/holoviews/tests/plotting/matplotlib/test_graphplot.py
+++ b/holoviews/tests/plotting/matplotlib/test_graphplot.py
@@ -1,7 +1,6 @@
 import numpy as np
 import pytest
 from matplotlib.collections import LineCollection, PolyCollection
-from packaging.version import Version
 
 from holoviews.core.data import Dataset
 from holoviews.core.options import AbbreviatedException, Cycle
@@ -9,7 +8,7 @@ from holoviews.core.spaces import HoloMap
 from holoviews.element import Chord, Graph, Nodes, TriMesh, circular_layout
 from holoviews.util.transform import dim
 
-from .test_plot import TestMPLPlot, mpl_renderer
+from .test_plot import MPL_GE_3_4_0, TestMPLPlot, mpl_renderer
 
 
 class TestMplGraphPlot(TestMPLPlot):
@@ -198,20 +197,19 @@ class TestMplGraphPlot(TestMPLPlot):
         self.assertEqual(artist.get_linewidths(), [12, 3, 5])
 
     def test_graph_op_node_alpha(self):
-        import matplotlib as mpl
         edges = [(0, 1), (0, 2)]
         nodes = Nodes([(0, 0, 0, 0.2), (0, 1, 1, 0.6), (1, 1, 2, 1)], vdims='alpha')
         graph = Graph((edges, nodes)).opts(node_alpha='alpha')
 
-        if Version(mpl.__version__) < Version("3.4.0"):
+        if MPL_GE_3_4_0:
+            plot = mpl_renderer.get_plot(graph)
+            artist = plot.handles['nodes']
+            self.assertEqual(artist.get_alpha(), np.array([0.2, 0.6, 1]))
+        else:
             # Python 3.6 only support up to matplotlib 3.3
             msg = 'TypeError: alpha must be a float or None'
             with pytest.raises(AbbreviatedException, match=msg):
                 mpl_renderer.get_plot(graph)
-        else:
-            plot = mpl_renderer.get_plot(graph)
-            artist = plot.handles['nodes']
-            self.assertEqual(artist.get_alpha(), np.array([0.2, 0.6, 1]))
 
     def test_graph_op_edge_color(self):
         edges = [(0, 1, 'red'), (0, 2, 'green'), (1, 3, 'blue')]
@@ -392,21 +390,19 @@ class TestMplTriMeshPlot(TestMPLPlot):
         self.assertEqual(artist.get_sizes(), np.array([9, 4, 64, 16]))
 
     def test_trimesh_op_node_alpha(self):
-        import matplotlib as mpl
-
         edges = [(0, 1, 2), (1, 2, 3)]
         nodes = [(-1, -1, 0, 0.2), (0, 0, 1, 0.6), (0, 1, 2, 1), (1, 0, 3, 0.3)]
         trimesh = TriMesh((edges, Nodes(nodes, vdims='alpha'))).opts(node_alpha='alpha')
 
-        if Version(mpl.__version__) < Version("3.4.0"):
+        if MPL_GE_3_4_0:
+            plot = mpl_renderer.get_plot(trimesh)
+            artist = plot.handles['nodes']
+            self.assertEqual(artist.get_alpha(), np.array([0.2, 0.6, 1, 0.3]))
+        else:
             # Python 3.6 only support up to matplotlib 3.3
             msg = "TypeError: alpha must be a float or None"
             with pytest.raises(AbbreviatedException, match=msg):
                 mpl_renderer.get_plot(trimesh)
-        else:
-            plot = mpl_renderer.get_plot(trimesh)
-            artist = plot.handles['nodes']
-            self.assertEqual(artist.get_alpha(), np.array([0.2, 0.6, 1, 0.3]))
 
     def test_trimesh_op_node_line_width(self):
         edges = [(0, 1, 2), (1, 2, 3)]
diff --git a/holoviews/tests/plotting/matplotlib/test_heatmapplot.py b/holoviews/tests/plotting/matplotlib/test_heatmapplot.py
index f33bc90f3..74b945ebb 100644
--- a/holoviews/tests/plotting/matplotlib/test_heatmapplot.py
+++ b/holoviews/tests/plotting/matplotlib/test_heatmapplot.py
@@ -2,7 +2,7 @@ import numpy as np
 
 from holoviews.element import HeatMap, Image
 
-from .test_plot import TestMPLPlot, mpl38, mpl_renderer
+from .test_plot import MPL_GE_3_8_0, TestMPLPlot, mpl_renderer
 
 
 class TestHeatMapPlot(TestMPLPlot):
@@ -12,7 +12,7 @@ class TestHeatMapPlot(TestMPLPlot):
         hm = HeatMap(Image(arr)).opts(invert_axes=True)
         plot = mpl_renderer.get_plot(hm)
         artist = plot.handles['artist']
-        if mpl38:
+        if MPL_GE_3_8_0:
             np.testing.assert_equal(artist.get_array().data, arr.T[::-1])
         else:
             np.testing.assert_equal(artist.get_array().data, arr.T[::-1].flatten())
@@ -26,7 +26,7 @@ class TestHeatMapPlot(TestMPLPlot):
         hmap = HeatMap([('A',1, 1), ('B', 2, 2)]).opts(invert_xaxis=True)
         plot = mpl_renderer.get_plot(hmap)
         array = plot.handles['artist'].get_array()
-        if mpl38:
+        if MPL_GE_3_8_0:
             expected = np.array([[1, np.inf], [np.inf, 2]])
         else:
             expected = np.array([1, np.inf, np.inf, 2])
@@ -38,7 +38,7 @@ class TestHeatMapPlot(TestMPLPlot):
         plot = mpl_renderer.get_plot(hmap)
         array = plot.handles['artist'].get_array()
         expected = np.array([1, np.inf, np.inf, 2])
-        if mpl38:
+        if MPL_GE_3_8_0:
             expected = np.array([[1, np.inf], [np.inf, 2]])
         else:
             expected = np.array([1, np.inf, np.inf, 2])
diff --git a/holoviews/tests/plotting/matplotlib/test_plot.py b/holoviews/tests/plotting/matplotlib/test_plot.py
index bd0039346..765924cc2 100644
--- a/holoviews/tests/plotting/matplotlib/test_plot.py
+++ b/holoviews/tests/plotting/matplotlib/test_plot.py
@@ -1,17 +1,17 @@
 import matplotlib.pyplot as plt
 import pyviz_comms as comms
-from packaging.version import Version
 from param import concrete_descendents
 
 from holoviews.core.options import Store
 from holoviews.element.comparison import ComparisonTestCase
-from holoviews.plotting.mpl import mpl_version
+from holoviews.plotting.mpl import MPL_VERSION
 from holoviews.plotting.mpl.element import ElementPlot
 
 from .. import option_intersections
 
 mpl_renderer = Store.renderers['matplotlib']
-mpl38 = mpl_version >= Version("3.8")
+MPL_GE_3_4_0 = MPL_VERSION >= (3, 4, 0)
+MPL_GE_3_8_0 = MPL_VERSION >= (3, 8, 0)
 
 
 class TestPlotDefinitions(ComparisonTestCase):
diff --git a/holoviews/tests/plotting/matplotlib/test_quadmeshplot.py b/holoviews/tests/plotting/matplotlib/test_quadmeshplot.py
index 1d3096b65..fc0e0c201 100644
--- a/holoviews/tests/plotting/matplotlib/test_quadmeshplot.py
+++ b/holoviews/tests/plotting/matplotlib/test_quadmeshplot.py
@@ -2,7 +2,7 @@ import numpy as np
 
 from holoviews.element import Dataset, Image, QuadMesh
 
-from .test_plot import TestMPLPlot, mpl38, mpl_renderer
+from .test_plot import MPL_GE_3_8_0, TestMPLPlot, mpl_renderer
 
 
 class TestQuadMeshPlot(TestMPLPlot):
@@ -12,7 +12,7 @@ class TestQuadMeshPlot(TestMPLPlot):
         qmesh = QuadMesh(Image(arr)).opts(invert_axes=True)
         plot = mpl_renderer.get_plot(qmesh)
         artist = plot.handles['artist']
-        if mpl38:
+        if MPL_GE_3_8_0:
             np.testing.assert_equal(artist.get_array().data, arr[::-1].T)
         else:
             np.testing.assert_equal(artist.get_array().data, arr.T[::-1].flatten())
@@ -22,7 +22,7 @@ class TestQuadMeshPlot(TestMPLPlot):
         qmesh = QuadMesh(Image(arr)).opts(nodata=0)
         plot = mpl_renderer.get_plot(qmesh)
         artist = plot.handles['artist']
-        if mpl38:
+        if MPL_GE_3_8_0:
             expected = np.array([[3, 4, 5], [np.nan, 1, 2]])
         else:
             expected = np.array([3, 4, 5, np.nan, 1, 2])
@@ -34,7 +34,7 @@ class TestQuadMeshPlot(TestMPLPlot):
         qmesh = QuadMesh(Image(arr)).opts(nodata=0)
         plot = mpl_renderer.get_plot(qmesh)
         artist = plot.handles['artist']
-        if mpl38:
+        if MPL_GE_3_8_0:
             expected = np.array([[3, 4, 5], [np.nan, 1, 2]])
         else:
             expected = np.array([3, 4, 5, np.nan, 1, 2])
diff --git a/holoviews/tests/plotting/matplotlib/test_violinplot.py b/holoviews/tests/plotting/matplotlib/test_violinplot.py
index e87fdb539..efd15f054 100644
--- a/holoviews/tests/plotting/matplotlib/test_violinplot.py
+++ b/holoviews/tests/plotting/matplotlib/test_violinplot.py
@@ -1,7 +1,7 @@
 import numpy as np
 
 from holoviews.element import Violin
-from holoviews.plotting.mpl.util import MPL_GE_3_9
+from holoviews.plotting.mpl.util import MPL_GE_3_9_0
 
 from .test_plot import TestMPLPlot, mpl_renderer
 
@@ -15,7 +15,7 @@ class TestMPLViolinPlot(TestMPLPlot):
         data, style, axis_opts = plot.get_data(violin, {}, {})
         self.assertEqual(data[0][0], values)
         self.assertEqual(style['positions'], [0])
-        if MPL_GE_3_9:
+        if MPL_GE_3_9_0:
             self.assertEqual(style['tick_labels'], [''])
         else:
             self.assertEqual(style['labels'], [''])
@@ -38,7 +38,7 @@ class TestMPLViolinPlot(TestMPLPlot):
         self.assertEqual(data[0][0], violin.select(A=0).dimension_values(1))
         self.assertEqual(data[0][1], violin.select(A=1).dimension_values(1))
         self.assertEqual(style['positions'], [0, 1])
-        if MPL_GE_3_9:
+        if MPL_GE_3_9_0:
             self.assertEqual(style['tick_labels'], ['0', '1'])
         else:
             self.assertEqual(style['labels'], ['0', '1'])
diff --git a/holoviews/tests/test_streams.py b/holoviews/tests/test_streams.py
index 3f7124a85..50bcb1c62 100644
--- a/holoviews/tests/test_streams.py
+++ b/holoviews/tests/test_streams.py
@@ -11,7 +11,7 @@ from panel.widgets import IntSlider
 
 import holoviews as hv
 from holoviews.core.spaces import DynamicMap
-from holoviews.core.util import NUMPY_GE_200, Version
+from holoviews.core.util import NUMPY_GE_2_0_0, PARAM_VERSION
 from holoviews.element import Curve, Histogram, Points, Polygons, Scatter
 from holoviews.element.comparison import ComparisonTestCase
 from holoviews.streams import *  # noqa (Test all available streams)
@@ -20,6 +20,7 @@ from holoviews.util.transform import dim
 
 from .utils import LoggingComparisonTestCase
 
+PARAM_GE_2_0_0 = PARAM_VERSION >= (2, 0, 0)
 
 def test_all_stream_parameters_constant():
     all_stream_cls = [v for v in globals().values() if
@@ -73,7 +74,7 @@ class TestStreamsDefine(ComparisonTestCase):
         self.assertEqual(xy.y, 2)
 
     def test_XY_set_invalid_class_x(self):
-        if Version(param.__version__) > Version('2.0.0a2'):
+        if PARAM_GE_2_0_0:
             regexp = "Number parameter 'XY.x' only takes numeric values"
         else:
             regexp = "Parameter 'x' only takes numeric values"
@@ -81,7 +82,7 @@ class TestStreamsDefine(ComparisonTestCase):
             self.XY.x = 'string'
 
     def test_XY_set_invalid_class_y(self):
-        if Version(param.__version__) > Version('2.0.0a2'):
+        if PARAM_GE_2_0_0:
             regexp = "Number parameter 'XY.y' only takes numeric values"
         else:
             regexp = "Parameter 'y' only takes numeric values"
@@ -90,7 +91,7 @@ class TestStreamsDefine(ComparisonTestCase):
 
     def test_XY_set_invalid_instance_x(self):
         xy = self.XY(x=1,y=2)
-        if Version(param.__version__) > Version('2.0.0a2'):
+        if PARAM_GE_2_0_0:
             regexp = "Number parameter 'XY.x' only takes numeric values"
         else:
             regexp = "Parameter 'x' only takes numeric values"
@@ -99,7 +100,7 @@ class TestStreamsDefine(ComparisonTestCase):
 
     def test_XY_set_invalid_instance_y(self):
         xy = self.XY(x=1,y=2)
-        if Version(param.__version__) > Version('2.0.0a2'):
+        if PARAM_GE_2_0_0:
             regexp = "Number parameter 'XY.y' only takes numeric values"
         else:
             regexp = "Parameter 'y' only takes numeric values"
@@ -343,7 +344,7 @@ class TestParamsStream(LoggingComparisonTestCase):
 class TestParamMethodStream(ComparisonTestCase):
 
     def setUp(self):
-        if Version(param.__version__) < Version('1.8.0'):
+        if PARAM_VERSION < (1, 8, 0):
             raise SkipTest('Params stream requires param >= 1.8.0')
 
         class Inner(param.Parameterized):
@@ -1445,7 +1446,7 @@ class TestExprSelectionStream(ComparisonTestCase):
         self.assertIsNone(expr_stream.bbox)
         self.assertIsNone(expr_stream.selection_expr)
 
-        fmt = lambda x: list(map(np.str_, x)) if NUMPY_GE_200 else x
+        fmt = lambda x: list(map(np.str_, x)) if NUMPY_GE_2_0_0 else x
 
         expr_stream.input_streams[2].event(index=[0, 1])
         self.assertEqual(
diff --git a/holoviews/tests/ui/bokeh/test_callback.py b/holoviews/tests/ui/bokeh/test_callback.py
index 1fe6ba082..e2cb70a00 100644
--- a/holoviews/tests/ui/bokeh/test_callback.py
+++ b/holoviews/tests/ui/bokeh/test_callback.py
@@ -5,14 +5,14 @@ import pytest
 
 import holoviews as hv
 from holoviews import Curve, DynamicMap, Scatter
-from holoviews.plotting.bokeh.util import bokeh34
+from holoviews.plotting.bokeh.util import BOKEH_GE_3_4_0
 from holoviews.streams import BoundsX, BoundsXY, BoundsY, Lasso, MultiAxisTap, RangeXY
 
 from .. import expect, wait_until
 
 pytestmark = pytest.mark.ui
 
-skip_popup = pytest.mark.skipif(not bokeh34, reason="Pop ups needs Bokeh 3.4")
+skip_popup = pytest.mark.skipif(not BOKEH_GE_3_4_0, reason="Pop ups needs Bokeh 3.4")
 
 @pytest.fixture
 def points():
@@ -74,7 +74,7 @@ def test_lasso_select(serve_hv):
     page.mouse.move(bbox['x']+50, bbox['y']+150, steps=5)
     page.mouse.up()
 
-    if bokeh34:
+    if BOKEH_GE_3_4_0:
         expected_array = np.array([
             [3.28440367e-01, 2.31836735e00],
             [4.38532110e-01, 2.22040816e00],