From 26677a1a908b64263a021514856c20e2b86a03b6 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 12:49:12 -0500 Subject: [PATCH 01/36] meson.options: add the defer_runtime_checks option Following the discussion on, https://github.com/sagemath/sage/discussions/41067 we add an option to defer (forthcoming) build-time feature checks to runtime. --- meson.options | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/meson.options b/meson.options index 294e1171b03..5c31cf4c95d 100644 --- a/meson.options +++ b/meson.options @@ -17,6 +17,16 @@ option( description: 'Build the HTML / PDF documentation' ) +# Useful on binary distros, for example, to allow features to flip on as soon +# as the package that provides it is installed. If this is disabled, a rebuild +# of sagelib is required to toggle features on or off. +option( + 'defer_feature_checks', + type: 'boolean', + value: false, + description: 'Defer feature checks to runtime' +) + # # Features # From 629023db254f096392ea10433bfd0da3e9a3fc64 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 12:50:29 -0500 Subject: [PATCH 02/36] src/sage/meson.build,src/sage/config.py.in: record defer_feature_checks Record the value of the "defer_feature_checks" meson option in the sage config file, for use by the sage/features subsystem. --- src/sage/config.py.in | 2 ++ src/sage/meson.build | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/src/sage/config.py.in b/src/sage/config.py.in index 3a95538d313..06e42030e51 100644 --- a/src/sage/config.py.in +++ b/src/sage/config.py.in @@ -60,6 +60,8 @@ THREEJS_DIR = SAGE_LOCAL + "/share/threejs-sage" OPENMP_CFLAGS = "@OPENMP_CFLAGS@" OPENMP_CXXFLAGS = "@OPENMP_CXXFLAGS@" +# build-time feature flags +defer_feature_checks = @DEFER_FEATURE_CHECKS@ def is_editable_install() -> bool: """ diff --git a/src/sage/meson.build b/src/sage/meson.build index d43e636a4e8..1705995be10 100644 --- a/src/sage/meson.build +++ b/src/sage/meson.build @@ -173,6 +173,11 @@ configure_file( configuration: kernel_data, ) +# Record what build-time features (e.g. external package support) were +# enabled or disabled, and whether or not we should defer detection to +# runtime. +conf_data.set10('DEFER_FEATURE_CHECKS', get_option('defer_feature_checks')) + # Write config file # Should be last so that subdir calls can modify the config data config_file = configure_file( From d448425631c2f1df61b016a5c09a66fbaafc0eff Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 12:52:45 -0500 Subject: [PATCH 03/36] src/sage/features/build_feature.py: new class for build-time features Implement Option 3 from the dissussion at https://github.com/sagemath/sage/discussions/41067 with a new BuildFeature class. Features that can be detected at build-time should 1. Subclass this 2. Set self._enabled_in_build to a value from sage.config that says whether or not the feature was enabled at build time. 3. Implement is_present_at_runtime() if it makes sense to do so. (Without this, deferring the check to run-time won't help.) --- src/sage/features/build_feature.py | 137 +++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 src/sage/features/build_feature.py diff --git a/src/sage/features/build_feature.py b/src/sage/features/build_feature.py new file mode 100644 index 00000000000..2a41b30b1b7 --- /dev/null +++ b/src/sage/features/build_feature.py @@ -0,0 +1,137 @@ +r""" +Features that can be explicitly enabled or disabled at build time + +These features are unique in that, if they are enabled or disabled at +build-time, then we usually do not want to detect them on-the-fly. +Instead we trust the value supplied (or detected) at build-time. There +is however an overrride to defer these checks to run-time (the classic +behavior) for use on binary distros or anywhere it is desirable to +enable/disable features without rebuilding sage. + +This is an implementation of Option 3 in `Github discussion 41067 +`__. +""" + +from sage.features import Feature, FeatureTestResult + +class BuildFeature(Feature): + r""" + A class for features that can be enabled or disabled at + build-time. + + The current implementation refers to build features that are + configurable in meson. For example:: + + option( + 'foo', + type: 'feature', + value: 'auto', + description: 'support for foo' + ) + + At build time, support for this "foo" will be automatically + detected, and either enabled or disabled depending on whether or + not its requirements are met. Alternatively, users may pass either + ``-Dfoo=enabled`` or ``-Dfoo=disabled`` to explicitly enable or + disable the feature. Features may be disabled regardless of + whether or not they are installed, but usually features may only + be enabled if their dependencies are present and usable. + + In any event, after ``meson setup``, support for "foo" is either + enabled or disabled, and a boolean variable called something like + ``foo_enabled`` is written to ``sage.config``. In your subclass, + you should set the member variable ``_enabled_in_build`` to the + value of that config variable. + + The :meth:`_is_present` method for this class will return the + value of that config variable unless ``defer_feature_checks`` is + set to ``True`` in ``sage.config``. If checks are deferred, the + :meth:`_is_present` method will try to return the value of + :meth:`is_present_at_runtime` instead. If your feature can be + detected at run-time, you should implement that check in + :meth:`is_present_at_runtime`. Otherwise, leave it unimplemented; + and :meth:`_is_present` will return ``False``. + + EXAMPLES:: + + sage: from sage.features.build_feature import BuildFeature + sage: BuildFeature("foo") + Feature('foo') + + """ + + # Set this in subclasses. + _enabled_in_build = None + + # Implement this method if your feature is detectable at run-time. + # Your test should only return True if the feature meets Sage's + # requirements; for example, if there are doctests for gzipped foo + # data files hidden behind "needs foo", then you should ensure + # that foo was compiled with (say) --enable-zlib in your check. + # + # def is_present_at_runtime(self): + # pass + + def is_runtime_detectable(self): + r""" + Return whether or not this feature can (and should) be + detected at runtime. + + A feature is runtime detectable if both of the following hold: + + - Deferred feature checks have been enabled globally by + passing ``-Ddefer_feature_checks=true`` to ``meson setup``. + + - An ``is_present_at_runtime`` method has been implemented for + the feature. + + EXAMPLES: + + The method returns ``False`` if you have not implemented + ``is_present_at_runtime``:: + + sage: from sage.features.build_feature import BuildFeature + sage: bf = BuildFeature("example") + sage: bf.is_runtime_detectable() + False + + """ + from sage.config import defer_feature_checks + if not defer_feature_checks: + return False + elif hasattr(self, "is_present_at_runtime"): + return True + else: + return False + + def _is_present(self): + r""" + Default presence check for build features. + + If this feature :meth:`is_runtime_detectable`, we return the + result of that method. Otherwise, we use the value of + ``self._enabled_in_build``. + + EXAMPLES: + + When feature checks are deferred, runtime-detectable features + can be detected without ``self._enabled_in_build`` being set, + but this will fail by surprise when they are un-deferred:: + + sage: from sage.config import defer_feature_checks + sage: from sage.features.build_feature import BuildFeature + sage: bf = BuildFeature("example") + sage: bf.is_present_at_runtime = lambda s: True + sage: (not defer_feature_checks) or bf.is_present() + True + + """ + if self.is_runtime_detectable(): + return self.is_present_at_runtime() + else: + import sage.config + # Wrap with bool() so that we can be lazy and use meson's + # set10() rather than painstakingly writing "True" and + # "False" to the config file. + result = bool(self._enabled_in_build) + return FeatureTestResult(self, result) From 1cdabf64bee9d614aef5b1f4ad2797d4ea02b26b Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 12:57:39 -0500 Subject: [PATCH 04/36] src/sage/config.py.in: add placeholder vars for build features Add foo_enabled variables for the existing features defined in meson.options that are detectable at build-time. --- src/sage/config.py.in | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/sage/config.py.in b/src/sage/config.py.in index 06e42030e51..aca1247cc11 100644 --- a/src/sage/config.py.in +++ b/src/sage/config.py.in @@ -62,6 +62,15 @@ OPENMP_CXXFLAGS = "@OPENMP_CXXFLAGS@" # build-time feature flags defer_feature_checks = @DEFER_FEATURE_CHECKS@ +bliss_enabled = @BLISS_ENABLED@ +brial_enabled = @BRIAL_ENABLED@ +coxeter3_enabled = @COXETER3_ENABLED@ +eclib_enabled = @ECLIB_ENABLED@ +mcqd_enabled = @MCQD_ENABLED@ +meataxe_enabled = @MEATAXE_ENABLED@ +rankwidth_enabled = @RANKWIDTH_ENABLED@ +sirocco_enabled = @SIROCCO_ENABLED@ +tdlib_enabled = @TDLIB_ENABLED@ def is_editable_install() -> bool: """ From 9302d2ac16300c01c618c90dd9ab02bcd56d477d Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 12:59:10 -0500 Subject: [PATCH 05/36] src/**/meson.build: record build-time feature information in conf_data For all of the build-time features defined in meson.options, record whether or not the feature (and its dependencies) were found by meson. The associated config variables were added to src/sage/config.py.in in an earlier commit; here we just substitute the correct values. --- src/sage/graphs/graph_decompositions/meson.build | 4 ++++ src/sage/graphs/meson.build | 5 +++++ src/sage/libs/coxeter3/meson.build | 4 ++++ src/sage/libs/meson.build | 5 +++++ src/sage/meson.build | 1 + src/sage/rings/polynomial/pbori/meson.build | 3 +++ 6 files changed, 22 insertions(+) diff --git a/src/sage/graphs/graph_decompositions/meson.build b/src/sage/graphs/graph_decompositions/meson.build index 06763ab1771..bfe106dc0d0 100644 --- a/src/sage/graphs/graph_decompositions/meson.build +++ b/src/sage/graphs/graph_decompositions/meson.build @@ -83,3 +83,7 @@ py.extension_module( dependencies: [py_dep, cysignals, tdlib], ) +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('RANKWIDTH_ENABLED', rw.found()) +conf_data.set10('TDLIB_ENABLED', tdlib.found()) diff --git a/src/sage/graphs/meson.build b/src/sage/graphs/meson.build index fce249794c7..6ab180db024 100644 --- a/src/sage/graphs/meson.build +++ b/src/sage/graphs/meson.build @@ -177,6 +177,11 @@ py.extension_module( dependencies: [py_dep, cysignals, mcqd], ) +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('BLISS_ENABLED', bliss.found()) +conf_data.set10('MCQD_ENABLED', mcqd.found()) + subdir('base') subdir('generators') subdir('graph_decompositions') diff --git a/src/sage/libs/coxeter3/meson.build b/src/sage/libs/coxeter3/meson.build index caf5f30b535..af484dc98a2 100644 --- a/src/sage/libs/coxeter3/meson.build +++ b/src/sage/libs/coxeter3/meson.build @@ -25,3 +25,7 @@ foreach name, pyx : extension_data_cpp dependencies: [py_dep, cysignals, coxeter3], ) endforeach + +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('COXETER3_ENABLED', coxeter3.found()) diff --git a/src/sage/libs/meson.build b/src/sage/libs/meson.build index 923fd5416b2..5710553c74b 100644 --- a/src/sage/libs/meson.build +++ b/src/sage/libs/meson.build @@ -169,6 +169,11 @@ foreach name, pyx : extension_data_cpp ) endforeach +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('MEATAXE_ENABLED', mtx.found()) +conf_data.set10('SIROCCO_ENABLED', sirocco.found()) + subdir('arb') subdir('coxeter3') install_subdir('cremona', install_dir: sage_install_dir / 'libs') diff --git a/src/sage/meson.build b/src/sage/meson.build index 1705995be10..8682482bb58 100644 --- a/src/sage/meson.build +++ b/src/sage/meson.build @@ -177,6 +177,7 @@ configure_file( # enabled or disabled, and whether or not we should defer detection to # runtime. conf_data.set10('DEFER_FEATURE_CHECKS', get_option('defer_feature_checks')) +conf_data.set10('ECLIB_ENABLED', ec.found()) # Write config file # Should be last so that subdir calls can modify the config data diff --git a/src/sage/rings/polynomial/pbori/meson.build b/src/sage/rings/polynomial/pbori/meson.build index 3456379c864..f12a5a70273 100644 --- a/src/sage/rings/polynomial/pbori/meson.build +++ b/src/sage/rings/polynomial/pbori/meson.build @@ -62,3 +62,6 @@ foreach name, pyx : extension_data_cpp ) endforeach +# Record in the config file whether or not optional packages were +# found, so that we don't have to look for them at runtime. +conf_data.set10('BRIAL_ENABLED', brial.found() and brial_groebner.found()) From 21754627b5e4932669f50d0811331ede16395a58 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 12:45:48 -0500 Subject: [PATCH 06/36] src/sage/features/sagemath.py: make BRiAl a BuildFeature --- src/sage/features/sagemath.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/sage/features/sagemath.py b/src/sage/features/sagemath.py index 6313f1c715a..fdacf93c735 100644 --- a/src/sage/features/sagemath.py +++ b/src/sage/features/sagemath.py @@ -42,6 +42,8 @@ from . import PythonModule, StaticFile from .join_feature import JoinFeature +from sage.config import brial_enabled +from sage.features.build_feature import BuildFeature class sagemath_doc_html(StaticFile): r""" @@ -941,7 +943,7 @@ def __init__(self): type='standard') -class sage__rings__polynomial__pbori(JoinFeature): +class sage__rings__polynomial__pbori(BuildFeature): r""" A :class:`sage.features.Feature` describing the presence of :mod:`sage.rings.polynomial.pbori`. @@ -951,6 +953,8 @@ class sage__rings__polynomial__pbori(JoinFeature): sage: sage__rings__polynomial__pbori().is_present() # needs sage.rings.polynomial.pbori FeatureTestResult('sage.rings.polynomial.pbori', True) """ + _enabled_in_build = brial_enabled + def __init__(self): r""" TESTS:: @@ -959,9 +963,9 @@ def __init__(self): sage: isinstance(sage__rings__polynomial__pbori(), sage__rings__polynomial__pbori) True """ - JoinFeature.__init__(self, 'sage.rings.polynomial.pbori', - [PythonModule('sage.rings.polynomial.pbori.pbori')], - spkg='sagemath_brial', type='standard') + super().__init__('sage.rings.polynomial.pbori', + spkg='sagemath_brial', + type='standard') class sage__rings__real_double(PythonModule): From 46d3c4d968559f71463d6ca12f794cde6bdd585b Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:01:49 -0500 Subject: [PATCH 07/36] src/sage/features/bliss.py: convert to a BuildFeature --- src/sage/features/bliss.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/sage/features/bliss.py b/src/sage/features/bliss.py index f14893b5a89..0fa1d865b79 100644 --- a/src/sage/features/bliss.py +++ b/src/sage/features/bliss.py @@ -14,8 +14,9 @@ # ***************************************************************************** from . import CythonFeature, PythonModule -from .join_feature import JoinFeature +from sage.config import bliss_enabled +from sage.features.build_feature import BuildFeature TEST_CODE = """ # distutils: language=c++ @@ -57,7 +58,7 @@ def __init__(self): url='http://www.tcs.hut.fi/Software/bliss/') -class Bliss(JoinFeature): +class Bliss(BuildFeature): r""" A :class:`~sage.features.Feature` which describes whether the :mod:`sage.graphs.bliss` module is available in this installation of Sage. @@ -67,6 +68,8 @@ class Bliss(JoinFeature): sage: from sage.features.bliss import Bliss sage: Bliss().require() # optional - bliss """ + _enabled_in_build = bliss_enabled + def __init__(self): r""" TESTS:: @@ -75,9 +78,8 @@ def __init__(self): sage: Bliss() Feature('bliss') """ - JoinFeature.__init__(self, "bliss", - [PythonModule("sage.graphs.bliss", spkg='bliss', - url='http://www.tcs.hut.fi/Software/bliss/')]) + super().__init__("bliss", + url='http://www.tcs.hut.fi/Software/bliss/') def all_features(): From 0608eebd0d14763c3ab296e3263579dd9b70dede Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:02:07 -0500 Subject: [PATCH 08/36] src/sage/features/coxeter3.py: convert to a BuildFeature --- src/sage/features/coxeter3.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sage/features/coxeter3.py b/src/sage/features/coxeter3.py index 3fdc3c88e03..f20fbc21771 100644 --- a/src/sage/features/coxeter3.py +++ b/src/sage/features/coxeter3.py @@ -13,11 +13,10 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule -from .join_feature import JoinFeature +from sage.config import coxeter3_enabled +from sage.features.build_feature import BuildFeature - -class Coxeter3(JoinFeature): +class Coxeter3(BuildFeature): r""" A :class:`~sage.features.Feature` which describes whether the :mod:`sage.libs.coxeter3` module is available in this installation of Sage. @@ -27,6 +26,8 @@ class Coxeter3(JoinFeature): sage: from sage.features.coxeter3 import Coxeter3 sage: Coxeter3().require() # optional - coxeter3 """ + _enabled_in_build = coxeter3_enabled + def __init__(self): r""" TESTS:: @@ -35,9 +36,7 @@ def __init__(self): sage: Coxeter3() Feature('coxeter3') """ - JoinFeature.__init__(self, "coxeter3", - [PythonModule("sage.libs.coxeter3.coxeter", - spkg='coxeter3')]) + super().__init__("coxeter3", spkg='coxeter3') def all_features(): From e62d5c668c8978eb309082e3329ad9e3cc3ec71c Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:02:19 -0500 Subject: [PATCH 09/36] src/sage/features/mcqd.py: convert to a BuildFeature --- src/sage/features/mcqd.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/sage/features/mcqd.py b/src/sage/features/mcqd.py index 7f0df8eedda..18685da4ac6 100644 --- a/src/sage/features/mcqd.py +++ b/src/sage/features/mcqd.py @@ -11,11 +11,10 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule -from .join_feature import JoinFeature +from sage.config import mcqd_enabled +from sage.features.build_feature import BuildFeature - -class Mcqd(JoinFeature): +class Mcqd(BuildFeature): r""" A :class:`~sage.features.Feature` describing the presence of the :mod:`~sage.graphs.mcqd` module, which is the SageMath interface to the :ref:`mcqd ` library @@ -26,6 +25,7 @@ class Mcqd(JoinFeature): sage: Mcqd().is_present() # optional - mcqd FeatureTestResult('mcqd', True) """ + _enabled_in_build = mcqd_enabled def __init__(self): """ @@ -35,9 +35,7 @@ def __init__(self): sage: isinstance(Mcqd(), Mcqd) True """ - JoinFeature.__init__(self, 'mcqd', - [PythonModule('sage.graphs.mcqd', - spkg='mcqd')]) + super().__init__('mcqd', spkg='mcqd') def all_features(): From bf38da22b941d1a422e7b7bf0a8eb65e1b2ef656 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:02:30 -0500 Subject: [PATCH 10/36] src/sage/features/meataxe.py: convert to a BuildFeature --- src/sage/features/meataxe.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/sage/features/meataxe.py b/src/sage/features/meataxe.py index 3276b3cdba7..2f5103af226 100644 --- a/src/sage/features/meataxe.py +++ b/src/sage/features/meataxe.py @@ -12,12 +12,10 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** +from sage.config import meataxe_enabled +from sage.features.build_feature import BuildFeature -from . import PythonModule -from .join_feature import JoinFeature - - -class Meataxe(JoinFeature): +class Meataxe(BuildFeature): r""" A :class:`~sage.features.Feature` describing the presence of the Sage modules that depend on the :ref:`meataxe ` library. @@ -28,6 +26,8 @@ class Meataxe(JoinFeature): sage: Meataxe().is_present() # optional - meataxe FeatureTestResult('meataxe', True) """ + _enabled_in_build = meataxe_enabled + def __init__(self): r""" TESTS:: @@ -36,9 +36,7 @@ def __init__(self): sage: isinstance(Meataxe(), Meataxe) True """ - JoinFeature.__init__(self, 'meataxe', - [PythonModule('sage.matrix.matrix_gfpn_dense', - spkg='meataxe')]) + super().__init__('meataxe', spkg='meataxe') def all_features(): From 25f3844e2114ee10f67ec0ae4660ec60fc3fa9f6 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:02:40 -0500 Subject: [PATCH 11/36] src/sage/features/sirocco.py: convert to a BuildFeature --- src/sage/features/sirocco.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sage/features/sirocco.py b/src/sage/features/sirocco.py index 727513f940f..86abde7a1e0 100644 --- a/src/sage/features/sirocco.py +++ b/src/sage/features/sirocco.py @@ -13,11 +13,10 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule -from .join_feature import JoinFeature +from sage.config import sirocco_enabled +from sage.features.build_feature import BuildFeature - -class Sirocco(JoinFeature): +class Sirocco(BuildFeature): r""" A :class:`~sage.features.Feature` which describes whether the :mod:`sage.libs.sirocco` module is available in this installation of Sage. @@ -27,6 +26,8 @@ class Sirocco(JoinFeature): sage: from sage.features.sirocco import Sirocco sage: Sirocco().require() # optional - sirocco """ + _enabled_in_build = sirocco_enabled + def __init__(self): r""" TESTS:: @@ -35,9 +36,7 @@ def __init__(self): sage: Sirocco() Feature('sirocco') """ - JoinFeature.__init__(self, "sirocco", - [PythonModule("sage.libs.sirocco", - spkg='sirocco')]) + super().__init__("sirocco", spkg='sirocco') def all_features(): From 580453cc4cf6878e6f29d3868cbccfc64ae164f5 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:02:50 -0500 Subject: [PATCH 12/36] src/sage/features/tdlib.py: convert to a BuildFeature --- src/sage/features/tdlib.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/sage/features/tdlib.py b/src/sage/features/tdlib.py index b47f9d8db9d..5d47bd41561 100644 --- a/src/sage/features/tdlib.py +++ b/src/sage/features/tdlib.py @@ -12,14 +12,15 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule -from .join_feature import JoinFeature +from sage.config import tdlib_enabled +from sage.features.build_feature import BuildFeature - -class Tdlib(JoinFeature): +class Tdlib(BuildFeature): r""" A :class:`~sage.features.Feature` describing the presence of the SageMath interface to the :ref:`tdlib ` library. """ + _enabled_in_build = tdlib_enabled + def __init__(self): r""" TESTS:: @@ -28,9 +29,7 @@ def __init__(self): sage: isinstance(Tdlib(), Tdlib) True """ - JoinFeature.__init__(self, 'tdlib', - [PythonModule('sage.graphs.graph_decompositions.tdlib', - spkg='tdlib')]) + super().__init__('tdlib', spkg='tdlib') def all_features(): From 260a5869faad0c2959461b7997a97325388c1b74 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:03:05 -0500 Subject: [PATCH 13/36] src/sage/features/rankwidth.py: new BuildFeature --- src/sage/features/rankwidth.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 src/sage/features/rankwidth.py diff --git a/src/sage/features/rankwidth.py b/src/sage/features/rankwidth.py new file mode 100644 index 00000000000..d133d97965b --- /dev/null +++ b/src/sage/features/rankwidth.py @@ -0,0 +1,26 @@ +r""" +Feature for testing the availability of the ``rw`` library +""" + +from sage.config import rankwidth_enabled +from sage.features.build_feature import BuildFeature + +class RankWidth(BuildFeature): + r""" + A :class:`~sage.features.Feature` for the availability of + the ``rankwidth`` library as determined at build-time. + + EXAMPLES:: + + sage: from sage.features.rankwidth import RankWidth + sage: RankWidth().is_present() # needs rankwidth + FeatureTestResult('rankwidth', True) + + """ + _enabled_in_build = rankwidth_enabled + + def __init__(self): + super().__init__("rankwidth", spkg="rw", type='standard') + +def all_features(): + return [RankWidth()] From 85865b61036db476afba3d42ab32efa7c4926b4b Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Mon, 4 Aug 2025 20:18:58 -0400 Subject: [PATCH 14/36] src/sage/features/mwrank.py: new feature for the mwrank program The meson build system is now capable of building sagelib without linking to libec, which means that the mwrank program may not be installed. Here we add a new feature to represent it. In particular this allows us to use "needs mwrank" in tests. --- src/sage/features/mwrank.py | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/sage/features/mwrank.py diff --git a/src/sage/features/mwrank.py b/src/sage/features/mwrank.py new file mode 100644 index 00000000000..0a6d746de76 --- /dev/null +++ b/src/sage/features/mwrank.py @@ -0,0 +1,42 @@ +r""" +Feature for testing the presence of the ``mwrank`` program + +This is part of the eclib package. +""" + +from sage.config import eclib_enabled +from sage.features import Executable +from sage.features.build_feature import BuildFeature + + +class Mwrank(BuildFeature, Executable): + r""" + A :class:`~sage.features.Feature` describing the presence of + the ``mwrank`` program. + + EXAMPLES:: + + sage: from sage.features.mwrank import Mwrank + sage: Mwrank().is_present() # needs mwrank + FeatureTestResult('mwrank', True) + + """ + _enabled_in_build = eclib_enabled + + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.mwrank import Mwrank + sage: isinstance(Mwrank(), Mwrank) + True + + """ + Executable.__init__(self, 'mwrank', executable='mwrank', + spkg='eclib', type='standard') + + def is_present_at_runtime(self): + return Executable.is_present(self) + +def all_features(): + return [Mwrank()] From a2e3826ffa621f6ac8798308408f70644b259718 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Mon, 4 Aug 2025 20:16:24 -0400 Subject: [PATCH 15/36] src/sage/features/sagemath.py: new feature for sage.libs.eclib The meson build system is now capable of building sagelib without sage.libs.eclib. Here we add a new feature to represent it. In particular this allows us to use "needs sage.libs.eclib" in tests. --- src/sage/features/sagemath.py | 37 ++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/sage/features/sagemath.py b/src/sage/features/sagemath.py index fdacf93c735..b524c238bb4 100644 --- a/src/sage/features/sagemath.py +++ b/src/sage/features/sagemath.py @@ -42,7 +42,7 @@ from . import PythonModule, StaticFile from .join_feature import JoinFeature -from sage.config import brial_enabled +from sage.config import brial_enabled, eclib_enabled from sage.features.build_feature import BuildFeature class sagemath_doc_html(StaticFile): @@ -514,6 +514,41 @@ def __init__(self): spkg='sagemath_ntl', type='standard') +class sage__libs__eclib(BuildFeature): + r""" + A :class:`sage.features.Feature` describing the presence of + :mod:`sage.libs.eclib`. + + EXAMPLES: + + This library is linked in, and so is not runtime detectable:: + + sage: from sage.features.sagemath import sage__libs__eclib + sage: e = sage__libs__eclib() + sage: e.is_runtime_detectable() + False + + TESTS:: + + sage: from sage.features.sagemath import sage__libs__eclib + sage: sage__libs__eclib().is_present() # needs sage.libs.eclib + FeatureTestResult('sage.libs.eclib', True) + + """ + _enabled_in_build = eclib_enabled + + def __init__(self): + r""" + TESTS:: + + sage: from sage.features.sagemath import sage__libs__eclib + sage: isinstance(sage__libs__eclib(), sage__libs__eclib) + True + + """ + super().__init__('sage.libs.eclib', spkg='eclib', type='standard') + + class sage__libs__giac(JoinFeature): r""" A :class:`sage.features.Feature` describing the presence of :mod:`sage.libs.giac`. From 6c5618514cceadb68e25710ed05afc75bc0798ef Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Sun, 7 Sep 2025 14:05:25 -0400 Subject: [PATCH 16/36] src/doc/en/reference/spkg/index.rst: add sage.features.mwrank --- src/doc/en/reference/spkg/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/src/doc/en/reference/spkg/index.rst b/src/doc/en/reference/spkg/index.rst index e9ba934a540..cc5f4ad00cd 100644 --- a/src/doc/en/reference/spkg/index.rst +++ b/src/doc/en/reference/spkg/index.rst @@ -52,6 +52,7 @@ Features sage/features/mcqd sage/features/meataxe sage/features/mip_backends + sage/features/mwrank sage/features/normaliz sage/features/pandoc sage/features/pdf2svg From b6d3eeafbbea6dfcba65562b5fbdd6c000ba7b0b Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:05:54 -0500 Subject: [PATCH 17/36] src/doc/en/reference/spkg/index.rst: add sage.features.build_feature --- src/doc/en/reference/spkg/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/src/doc/en/reference/spkg/index.rst b/src/doc/en/reference/spkg/index.rst index cc5f4ad00cd..efeb172267f 100644 --- a/src/doc/en/reference/spkg/index.rst +++ b/src/doc/en/reference/spkg/index.rst @@ -31,6 +31,7 @@ Features sage/features sage/features/join_feature sage/features/all + sage/features/build_feature sage/features/sagemath sage/features/pkg_systems sage/features/bliss From a5aed67707d7d8ee3ffa5306439b27cd980710c1 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:06:15 -0500 Subject: [PATCH 18/36] src/doc/en/reference/spkg/index.rst: add sage.features.rankwidth --- src/doc/en/reference/spkg/index.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/src/doc/en/reference/spkg/index.rst b/src/doc/en/reference/spkg/index.rst index efeb172267f..bffd8afb9b7 100644 --- a/src/doc/en/reference/spkg/index.rst +++ b/src/doc/en/reference/spkg/index.rst @@ -58,6 +58,7 @@ Features sage/features/pandoc sage/features/pdf2svg sage/features/polymake + sage/features/rankwidth sage/features/rubiks sage/features/tdlib sage/features/topcom From c7cf7521f00bf9491fe3e7ca9227818391c747b1 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:03:14 -0500 Subject: [PATCH 19/36] src/sage/doctest/external.py: no runtime detection for build features When a feature is not runtime-detectable, don't include it in the list of features to be detected. This keeps the list, $ sage -t src ... Features to be detected: 32_bit,4ti2,benzene,buckygen, ... ... Features detected for doctesting: database_ellcurves, ... free of misleading information and clutter. --- src/sage/doctest/external.py | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/sage/doctest/external.py b/src/sage/doctest/external.py index 026ca7f08cd..e8afae17bbd 100644 --- a/src/sage/doctest/external.py +++ b/src/sage/doctest/external.py @@ -513,9 +513,20 @@ def detectable(self): """ Return the list of names of those features for which testing their presence is allowed. """ + # Exclude build features that aren't runtime detectable from + # the list. Note that when defer_feature_checks is not set, + # *no* BuildFeatures are runtime-detectable. + from sage.features.build_feature import BuildFeature + def build_time_only(f): + return ( isinstance(f, BuildFeature) + and + not f.is_runtime_detectable() ) + return [feature.name for feature, seen in zip(self._features, self._seen) - if seen >= 0 and (self._allow_external or feature not in self._external_features)] + if seen >= 0 + and (self._allow_external or feature not in self._external_features) + and not build_time_only(feature)] def seen(self): """ @@ -527,9 +538,18 @@ def seen(self): sage: available_software.seen() # random ['internet', 'latex', 'magma'] """ + # Exclude build features that aren't runtime detectable from + # the list. Note that when defer_feature_checks is not set, + # *no* BuildFeatures are runtime-detectable. + from sage.features.build_feature import BuildFeature + def build_time_only(f): + return ( isinstance(f, BuildFeature) + and + not f.is_runtime_detectable() ) return [feature.name for feature, seen in zip(self._features, self._seen) - if seen > 0] + if seen > 0 + and not build_time_only(feature)] def hidden(self): """ From 576e2a692c2bc56c4543a4f050c2106c13124041 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Mon, 13 Oct 2025 16:42:39 -0400 Subject: [PATCH 20/36] conftest.py: only ignore ImportErrors for disabled features If the feature for sage.libs.foo is enabled, we shouldn't skip over files that fail to collect with an ImportError for sage.libs.foo. --- conftest.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/conftest.py b/conftest.py index 1ccd5f48d68..7e9b36a4267 100644 --- a/conftest.py +++ b/conftest.py @@ -117,14 +117,33 @@ def _find( pytest.skip("unable to import module %r" % self.path) else: if isinstance(exception, ModuleNotFoundError): - # Ignore some missing features/modules for now - # TODO: Remove this once all optional things are using Features - if exception.name in ( - "valgrind", - "rpy2", - "sage.libs.coxeter3.coxeter", - "sagemath_giac", - ): + # Ignore some missing features/modules for + # now. Many of these are using custom + # "sage.doctest" headers that only our doctest + # runner (i.e. not pytest) can understand. + # + # TODO: we should remove this once all + # optional things are using Features. It + # wouldn't be too hard to move the + # "sage.doctest" header into pytest (as + # explicit ignore lists based on feature + # tests), but that would require duplication + # for as long as `sage -t` is still used. + from sage.features.coxeter3 import Coxeter3 + from sage.features.sagemath import sage__libs__giac + from sage.features.standard import PythonModule + + exc_list = ["valgrind"] + if not PythonModule("rpy2").is_present(): + exc_list.append("rpy2") + if not Coxeter3().is_present(): + exc_list.append("sage.libs.coxeter3.coxeter") + if not sage__libs__giac().is_present(): + exc_list.append("sagemath_giac") + + # Ignore import errors, but only when the associated + # feature is actually disabled. + if exception.name in exc_list: pytest.skip( f"unable to import module {self.path} due to missing feature {exception.name}" ) From 0e8efbe1163fab51140e1a32721ecb41de7be33d Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Mon, 13 Oct 2025 16:44:41 -0400 Subject: [PATCH 21/36] conftest.py: ignore ImportErrors for sage.libs.eclib if disabled When sage.libs.eclib doesn't exist, pytest will still try to collect *.py files that import it, leading to an ImportError. We add eclib to the list of feature-backed modules with workarounds for this issue. --- conftest.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/conftest.py b/conftest.py index 7e9b36a4267..74f0747b50b 100644 --- a/conftest.py +++ b/conftest.py @@ -130,10 +130,13 @@ def _find( # tests), but that would require duplication # for as long as `sage -t` is still used. from sage.features.coxeter3 import Coxeter3 - from sage.features.sagemath import sage__libs__giac + from sage.features.sagemath import (sage__libs__eclib, + sage__libs__giac) from sage.features.standard import PythonModule exc_list = ["valgrind"] + if not sage__libs__eclib().is_present(): + exc_list.append("sage.libs.eclib") if not PythonModule("rpy2").is_present(): exc_list.append("rpy2") if not Coxeter3().is_present(): @@ -142,8 +145,9 @@ def _find( exc_list.append("sagemath_giac") # Ignore import errors, but only when the associated - # feature is actually disabled. - if exception.name in exc_list: + # feature is actually disabled. Use startswith() so + # that sage.libs.foo matches all of sage.libs.foo.* + if any(exception.name.startswith(e) for e in exc_list): pytest.skip( f"unable to import module {self.path} due to missing feature {exception.name}" ) From afe3b437bcd87a64a2ae4d4b1334641c99994cb8 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Wed, 28 Jan 2026 13:21:23 -0500 Subject: [PATCH 22/36] build/pkgs/sagelib: defer feature checks to runtime For backwards compatibility, defer all possible build-time feature checks to run-time. (For now this only affects the detection of the mwrank program.) --- build/pkgs/sagelib/spkg-install.in | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/build/pkgs/sagelib/spkg-install.in b/build/pkgs/sagelib/spkg-install.in index 2a638df17f1..e223e0d236c 100644 --- a/build/pkgs/sagelib/spkg-install.in +++ b/build/pkgs/sagelib/spkg-install.in @@ -55,7 +55,8 @@ if [ "$SAGE_EDITABLE" = yes ]; then --config-settings=build-dir="build/sage-distro" \ --config-settings=setup-args="--native-file=$SAGE_PKGS/../platform/meson/sage-configure-native-file.ini" \ --config-settings=setup-args="-DSAGE_LOCAL=$SAGE_LOCAL" \ - --config-settings=setup-args="-Dbuild-docs=$SAGE_BUILD_DOCS" + --config-settings=setup-args="-Dbuild-docs=$SAGE_BUILD_DOCS" \ + --config-settings=setup-args="-Ddefer_feature_checks=true" if [ "$SAGE_WHEELS" = yes ]; then # Additionally build a wheel (for use in other venvs) @@ -73,7 +74,8 @@ else # Compiling sage/interfaces/sagespawn.pyx because it depends on /private/var/folders/38/wnh4gf1552g_crsjnv2vmmww0000gp/T/pip-build-env-609n5985/overlay/lib/python3.10/site-packages/Cython/Includes/posix/unistd.pxd sdh_pip_install --no-build-isolation . --config-setting=build-dir="build/sage-distro" \ --config-setting=setup-args="-DSAGE_LOCAL=$SAGE_LOCAL" \ - --config-setting=setup-args="-Dbuild-docs=$SAGE_BUILD_DOCS" + --config-setting=setup-args="-Dbuild-docs=$SAGE_BUILD_DOCS" \ + --config-settings=setup-args="-Ddefer_feature_checks=true" fi # Remove (potentially invalid) star import caches. From ca6bc319e8c7621d8a622370d11f68b5deee9221 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 23:37:00 -0500 Subject: [PATCH 23/36] src/sage/features/build_feature.py: fix is_present() doctest A trick is needed to dynamically add a method to an object. --- src/sage/features/build_feature.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sage/features/build_feature.py b/src/sage/features/build_feature.py index 2a41b30b1b7..9c9bb0cd463 100644 --- a/src/sage/features/build_feature.py +++ b/src/sage/features/build_feature.py @@ -121,8 +121,9 @@ def _is_present(self): sage: from sage.config import defer_feature_checks sage: from sage.features.build_feature import BuildFeature sage: bf = BuildFeature("example") - sage: bf.is_present_at_runtime = lambda s: True - sage: (not defer_feature_checks) or bf.is_present() + sage: const_True = lambda s: True + sage: bf.is_present_at_runtime = const_True.__get__(bf) + sage: (not defer_feature_checks) or bf.is_present().is_present True """ From 375203691ff7a0c5fe6731ca5494c8179f2956b7 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 21:22:22 -0500 Subject: [PATCH 24/36] src/sage/features/mwrank.py: fix is_present_at_runtime() Call Executable._is_present() instead of Executable.is_present() to avoid infinite recursion. Somehow this worked before I replaced the relative import of Executable with an absolute one. --- src/sage/features/mwrank.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/sage/features/mwrank.py b/src/sage/features/mwrank.py index 0a6d746de76..05182073961 100644 --- a/src/sage/features/mwrank.py +++ b/src/sage/features/mwrank.py @@ -36,7 +36,19 @@ def __init__(self): spkg='eclib', type='standard') def is_present_at_runtime(self): - return Executable.is_present(self) + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.mwrank import Mwrank + sage: result = Mwrank().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs mwrank + FeatureTestResult('mwrank', True) + + """ + return Executable._is_present(self) def all_features(): return [Mwrank()] From 86d20c37c838dbb4160554b82f72bed266107ac7 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 21:58:46 -0500 Subject: [PATCH 25/36] src/sage/features/sagemath.py: make eclib runtime-detectable The result of _is_present() is cached and we want to check for a module with a different name than that of the feature, so we create a new PythonModule on the fly for this. --- src/sage/features/sagemath.py | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/sage/features/sagemath.py b/src/sage/features/sagemath.py index b524c238bb4..48a24996342 100644 --- a/src/sage/features/sagemath.py +++ b/src/sage/features/sagemath.py @@ -521,12 +521,13 @@ class sage__libs__eclib(BuildFeature): EXAMPLES: - This library is linked in, and so is not runtime detectable:: + This module is runtime detectable if feature checks are deferred:: sage: from sage.features.sagemath import sage__libs__eclib + sage: from sage.config import defer_feature_checks sage: e = sage__libs__eclib() - sage: e.is_runtime_detectable() - False + sage: (not defer_feature_checks) or e.is_runtime_detectable() + True TESTS:: @@ -546,7 +547,28 @@ def __init__(self): True """ - super().__init__('sage.libs.eclib', spkg='eclib', type='standard') + super().__init__("sage.libs.eclib", spkg="eclib", type="standard") + + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.sagemath import sage__libs__eclib + sage: result = sage__libs__eclib().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs sage.libs.eclib + FeatureTestResult('sage.libs.eclib', True) + + """ + # The build system installs the sage/libs/eclib source code + # even when eclib support was disabled, so a naive import of + # that module will actually succeed. We check for a + # conditionally-compiled cython module instead. + result = PythonModule("sage.libs.eclib.mwrank")._is_present() + result.feature = self + return result class sage__libs__giac(JoinFeature): From 1bb60a88347abf3e98f458b6f444fa896054ec64 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 22:03:25 -0500 Subject: [PATCH 26/36] src/sage/features/sirocco.py: make sirocco runtime-detectable The result of _is_present() is cached and we want to check for a module with a different name than that of the feature, so we create a new PythonModule on the fly for this. --- src/sage/features/sirocco.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/sage/features/sirocco.py b/src/sage/features/sirocco.py index 86abde7a1e0..587cbc65304 100644 --- a/src/sage/features/sirocco.py +++ b/src/sage/features/sirocco.py @@ -14,17 +14,20 @@ # ***************************************************************************** from sage.config import sirocco_enabled +from sage.features import PythonModule from sage.features.build_feature import BuildFeature class Sirocco(BuildFeature): r""" - A :class:`~sage.features.Feature` which describes whether the :mod:`sage.libs.sirocco` - module is available in this installation of Sage. + A :class:`~sage.features.Feature` which describes whether the + :mod:`sage.libs.sirocco` module is available in this installation + of Sage. EXAMPLES:: sage: from sage.features.sirocco import Sirocco - sage: Sirocco().require() # optional - sirocco + sage: Sirocco().require() # needs sirocco + """ _enabled_in_build = sirocco_enabled @@ -35,9 +38,26 @@ def __init__(self): sage: from sage.features.sirocco import Sirocco sage: Sirocco() Feature('sirocco') + """ super().__init__("sirocco", spkg='sirocco') + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.sirocco import Sirocco + sage: result = Sirocco().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs sirocco + FeatureTestResult('sirocco', True) + + """ + result = PythonModule("sage.libs.sirocco")._is_present() + result.feature = self + return result def all_features(): return [Sirocco()] From c9a099eae950b799a478f2d0d31ae454282ce04f Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 22:16:15 -0500 Subject: [PATCH 27/36] src/sage/features/mcqd.py: make mcqd runtime-detectable The result of _is_present() is cached and we want to check for a module with a different name than that of the feature, so we create a new PythonModule on the fly for this. --- src/sage/features/mcqd.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/sage/features/mcqd.py b/src/sage/features/mcqd.py index 18685da4ac6..95211ca4e69 100644 --- a/src/sage/features/mcqd.py +++ b/src/sage/features/mcqd.py @@ -12,18 +12,21 @@ # ***************************************************************************** from sage.config import mcqd_enabled +from sage.features import PythonModule from sage.features.build_feature import BuildFeature class Mcqd(BuildFeature): r""" - A :class:`~sage.features.Feature` describing the presence of the :mod:`~sage.graphs.mcqd` module, - which is the SageMath interface to the :ref:`mcqd ` library + A :class:`~sage.features.Feature` describing the presence of + the :mod:`~sage.graphs.mcqd` module, which is the SageMath + interface to the :ref:`mcqd ` library EXAMPLES:: sage: from sage.features.mcqd import Mcqd - sage: Mcqd().is_present() # optional - mcqd + sage: Mcqd().is_present() # needs mcqd FeatureTestResult('mcqd', True) + """ _enabled_in_build = mcqd_enabled @@ -34,9 +37,26 @@ def __init__(self): sage: from sage.features.mcqd import Mcqd sage: isinstance(Mcqd(), Mcqd) True + """ super().__init__('mcqd', spkg='mcqd') + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.mcqd import Mcqd + sage: result = Mcqd().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs mcqd + FeatureTestResult('mcqd', True) + + """ + result = PythonModule("sage.graphs.mcqd")._is_present() + result.feature = self + return result def all_features(): return [Mcqd()] From 1d22a0dd4bcd173e61741edf814f0e41900b9a46 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 22:16:31 -0500 Subject: [PATCH 28/36] src/sage/features/meataxe.py: make meataxe runtime-detectable The result of _is_present() is cached and we want to check for a module with a different name than that of the feature, so we create a new PythonModule on the fly for this. --- src/sage/features/meataxe.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/sage/features/meataxe.py b/src/sage/features/meataxe.py index 2f5103af226..a8126661f0f 100644 --- a/src/sage/features/meataxe.py +++ b/src/sage/features/meataxe.py @@ -13,18 +13,21 @@ # ***************************************************************************** from sage.config import meataxe_enabled +from sage.features import PythonModule from sage.features.build_feature import BuildFeature class Meataxe(BuildFeature): r""" - A :class:`~sage.features.Feature` describing the presence of the Sage modules - that depend on the :ref:`meataxe ` library. + A :class:`~sage.features.Feature` describing the presence of + the Sage modules that depend on the :ref:`meataxe ` + library. EXAMPLES:: sage: from sage.features.meataxe import Meataxe - sage: Meataxe().is_present() # optional - meataxe + sage: Meataxe().is_present() # needs meataxe FeatureTestResult('meataxe', True) + """ _enabled_in_build = meataxe_enabled @@ -35,9 +38,27 @@ def __init__(self): sage: from sage.features.meataxe import Meataxe sage: isinstance(Meataxe(), Meataxe) True + """ super().__init__('meataxe', spkg='meataxe') + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.meataxe import Meataxe + sage: result = Meataxe().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs meataxe + FeatureTestResult('meataxe', True) + + """ + modname = "sage.matrix.matrix_gfpn_dense" + result = PythonModule(modname)._is_present() + result.feature = self + return result def all_features(): return [Meataxe()] From 9eb9e0640e75e9f4f99cf43ba763725fb5ea16b2 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 22:19:23 -0500 Subject: [PATCH 29/36] src/sage/features/bliss.py: make bliss runtime-detectable The result of _is_present() is cached and we want to check for a module with a different name than that of the feature, so we create a new PythonModule on the fly for this. --- src/sage/features/bliss.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/sage/features/bliss.py b/src/sage/features/bliss.py index 0fa1d865b79..4c0f59f3fee 100644 --- a/src/sage/features/bliss.py +++ b/src/sage/features/bliss.py @@ -16,6 +16,7 @@ from . import CythonFeature, PythonModule from sage.config import bliss_enabled +from sage.features import PythonModule from sage.features.build_feature import BuildFeature TEST_CODE = """ @@ -60,13 +61,14 @@ def __init__(self): class Bliss(BuildFeature): r""" - A :class:`~sage.features.Feature` which describes whether the :mod:`sage.graphs.bliss` - module is available in this installation of Sage. + A :class:`~sage.features.Feature` which describes whether the + :mod:`sage.graphs.bliss` module is available in this installation + of Sage. EXAMPLES:: sage: from sage.features.bliss import Bliss - sage: Bliss().require() # optional - bliss + sage: Bliss().require() # needs bliss """ _enabled_in_build = bliss_enabled @@ -77,10 +79,27 @@ def __init__(self): sage: from sage.features.bliss import Bliss sage: Bliss() Feature('bliss') + """ super().__init__("bliss", url='http://www.tcs.hut.fi/Software/bliss/') + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.bliss import Bliss + sage: result = Bliss().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs bliss + FeatureTestResult('bliss', True) + + """ + result = PythonModule("sage.graphs.bliss")._is_present() + result.feature = self + return result def all_features(): return [Bliss()] From cc2da29f1074f10b8ed3828c7cf5b9e0a4f34fe8 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 22:45:31 -0500 Subject: [PATCH 30/36] src/sage/features/tdlib.py: make tdlib runtime-detectable The result of _is_present() is cached and we want to check for a module with a different name than that of the feature, so we create a new PythonModule on the fly for this. --- src/sage/features/tdlib.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/sage/features/tdlib.py b/src/sage/features/tdlib.py index 5d47bd41561..e11848dc41f 100644 --- a/src/sage/features/tdlib.py +++ b/src/sage/features/tdlib.py @@ -13,11 +13,13 @@ # ***************************************************************************** from sage.config import tdlib_enabled +from sage.features import PythonModule from sage.features.build_feature import BuildFeature class Tdlib(BuildFeature): r""" - A :class:`~sage.features.Feature` describing the presence of the SageMath interface to the :ref:`tdlib ` library. + A :class:`~sage.features.Feature` describing the presence of + the SageMath interface to the :ref:`tdlib ` library. """ _enabled_in_build = tdlib_enabled @@ -28,9 +30,27 @@ def __init__(self): sage: from sage.features.tdlib import Tdlib sage: isinstance(Tdlib(), Tdlib) True + """ super().__init__('tdlib', spkg='tdlib') + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.tdlib import Tdlib + sage: result = Tdlib().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs tdlib + FeatureTestResult('tdlib', True) + + """ + modname = "sage.graphs.graph_decompositions.tdlib" + result = PythonModule(modname)._is_present() + result.feature = self + return result def all_features(): return [Tdlib()] From 86bb563e2fc7203194d06863f64252cde46f1e47 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 22:45:50 -0500 Subject: [PATCH 31/36] src/sage/features/coxeter3.py: make coxeter3 runtime-detectable The result of _is_present() is cached and we want to check for a module with a different name than that of the feature, so we create a new PythonModule on the fly for this. --- src/sage/features/coxeter3.py | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/sage/features/coxeter3.py b/src/sage/features/coxeter3.py index f20fbc21771..aa23a4eb721 100644 --- a/src/sage/features/coxeter3.py +++ b/src/sage/features/coxeter3.py @@ -14,17 +14,20 @@ # ***************************************************************************** from sage.config import coxeter3_enabled +from sage.features import PythonModule from sage.features.build_feature import BuildFeature class Coxeter3(BuildFeature): r""" - A :class:`~sage.features.Feature` which describes whether the :mod:`sage.libs.coxeter3` - module is available in this installation of Sage. + A :class:`~sage.features.Feature` which describes whether the + :mod:`sage.libs.coxeter3` module is available in this installation + of Sage. EXAMPLES:: sage: from sage.features.coxeter3 import Coxeter3 - sage: Coxeter3().require() # optional - coxeter3 + sage: Coxeter3().require() # needs coxeter3 + """ _enabled_in_build = coxeter3_enabled @@ -35,9 +38,26 @@ def __init__(self): sage: from sage.features.coxeter3 import Coxeter3 sage: Coxeter3() Feature('coxeter3') + """ super().__init__("coxeter3", spkg='coxeter3') + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.coxeter3 import Coxeter3 + sage: result = Coxeter3().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs coxeter3 + FeatureTestResult('coxeter3', True) + + """ + result = PythonModule("sage.libs.coxeter3")._is_present() + result.feature = self + return result def all_features(): return [Coxeter3()] From 24d5e0eb470677bb050278029d0ba1cd9cb0f1f1 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 22:46:09 -0500 Subject: [PATCH 32/36] src/sage/features/rankwidth.py: make rankwidth runtime-detectable The result of _is_present() is cached and we want to check for a module with a different name than that of the feature, so we create a new PythonModule on the fly for this. --- src/sage/features/rankwidth.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/src/sage/features/rankwidth.py b/src/sage/features/rankwidth.py index d133d97965b..bfea87d457d 100644 --- a/src/sage/features/rankwidth.py +++ b/src/sage/features/rankwidth.py @@ -3,6 +3,7 @@ """ from sage.config import rankwidth_enabled +from sage.features import PythonModule from sage.features.build_feature import BuildFeature class RankWidth(BuildFeature): @@ -10,6 +11,9 @@ class RankWidth(BuildFeature): A :class:`~sage.features.Feature` for the availability of the ``rankwidth`` library as determined at build-time. + This library is required for the rank-width graph decomposition + found in :mod:`sage.graphs.graph_decompositions.rankwidth`. + EXAMPLES:: sage: from sage.features.rankwidth import RankWidth @@ -20,7 +24,26 @@ class RankWidth(BuildFeature): _enabled_in_build = rankwidth_enabled def __init__(self): - super().__init__("rankwidth", spkg="rw", type='standard') + super().__init__("rankwidth", spkg="rw", type="standard") + + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.rankwidth import RankWidth + sage: result = RankWidth().is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs rankwidth + FeatureTestResult('rankwidth', True) + + """ + modname = "sage.graphs.graph_decompositions.rankwidth" + result = PythonModule(modname)._is_present() + result.feature = self + return result + def all_features(): return [RankWidth()] From 2f1486d2d4e683194d3128e064ad1a3ded6cb246 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Fri, 30 Jan 2026 22:46:22 -0500 Subject: [PATCH 33/36] src/sage/features/sagemath.py: make brial runtime-detectable The module name that we check for in this case is the same as the feature name, so we can subclass the feature from PythonModule and use PythonModule's _is_present() in is_present_at_runtime(). --- src/sage/features/sagemath.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/src/sage/features/sagemath.py b/src/sage/features/sagemath.py index 48a24996342..987e489474b 100644 --- a/src/sage/features/sagemath.py +++ b/src/sage/features/sagemath.py @@ -39,11 +39,10 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import PythonModule, StaticFile -from .join_feature import JoinFeature - from sage.config import brial_enabled, eclib_enabled +from sage.features import PythonModule, StaticFile from sage.features.build_feature import BuildFeature +from sage.features.join_feature import JoinFeature class sagemath_doc_html(StaticFile): r""" @@ -1000,15 +999,16 @@ def __init__(self): type='standard') -class sage__rings__polynomial__pbori(BuildFeature): +class sage__rings__polynomial__pbori(BuildFeature, PythonModule): r""" A :class:`sage.features.Feature` describing the presence of :mod:`sage.rings.polynomial.pbori`. TESTS:: sage: from sage.features.sagemath import sage__rings__polynomial__pbori - sage: sage__rings__polynomial__pbori().is_present() # needs sage.rings.polynomial.pbori + sage: sage__rings__polynomial__pbori().is_present() # needs sage.rings.polynomial.pbori FeatureTestResult('sage.rings.polynomial.pbori', True) + """ _enabled_in_build = brial_enabled @@ -1017,13 +1017,30 @@ def __init__(self): TESTS:: sage: from sage.features.sagemath import sage__rings__polynomial__pbori - sage: isinstance(sage__rings__polynomial__pbori(), sage__rings__polynomial__pbori) + sage: isinstance(sage__rings__polynomial__pbori(), + ....: sage__rings__polynomial__pbori) True + """ super().__init__('sage.rings.polynomial.pbori', spkg='sagemath_brial', type='standard') + def is_present_at_runtime(self): + r""" + TESTS:: + + sage: from sage.features import FeatureTestResult + sage: from sage.features.sagemath import sage__rings__polynomial__pbori + sage: pbori = sage__rings__polynomial__pbori() + sage: result = pbori.is_present_at_runtime() + sage: isinstance(result, FeatureTestResult) + True + sage: result # needs sage.rings.polynomial.pbori + FeatureTestResult('sage.rings.polynomial.pbori', True) + + """ + return PythonModule._is_present(self) class sage__rings__real_double(PythonModule): r""" From 927a70f3b17cf108af163b4da157992b7a0d8331 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Sat, 31 Jan 2026 09:52:53 -0500 Subject: [PATCH 34/36] src/sage/features/bliss.py: don't import PythonModule twice --- src/sage/features/bliss.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/features/bliss.py b/src/sage/features/bliss.py index 4c0f59f3fee..450010f83a0 100644 --- a/src/sage/features/bliss.py +++ b/src/sage/features/bliss.py @@ -13,7 +13,7 @@ # https://www.gnu.org/licenses/ # ***************************************************************************** -from . import CythonFeature, PythonModule +from . import CythonFeature from sage.config import bliss_enabled from sage.features import PythonModule From c5360d5a31ce15db10ac96038e1cf0806953c219 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Sat, 31 Jan 2026 16:50:02 -0500 Subject: [PATCH 35/36] src/sage/features/coxeter3.py: fix module name To detect coxeter3 at runtime, we should check for a module that won't be present when coxeter3 support is disabled. This feature now checks for a conditionally-compiled cython module beneath sage.libs.coxeter3, rather than for sage.libs.coxeter3 itself. --- src/sage/features/coxeter3.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sage/features/coxeter3.py b/src/sage/features/coxeter3.py index aa23a4eb721..eed4d844e10 100644 --- a/src/sage/features/coxeter3.py +++ b/src/sage/features/coxeter3.py @@ -55,7 +55,12 @@ def is_present_at_runtime(self): FeatureTestResult('coxeter3', True) """ - result = PythonModule("sage.libs.coxeter3")._is_present() + # The build system installs the sage/libs/coxeter3 source code + # even when coxeter3 support is disabled, so a naive import of + # that module will actually succeed. We check for a + # conditionally-compiled cython module instead. + cython_modname = "sage.libs.coxeter3.coxeter" + result = PythonModule(cython_modname)._is_present() result.feature = self return result From bead86a2d908c8bf49893dbd8fd21ca2076ea909 Mon Sep 17 00:00:00 2001 From: Michael Orlitzky Date: Mon, 2 Feb 2026 11:34:48 -0500 Subject: [PATCH 36/36] src/sage/graphs/graph_decompositions/rankwidth.pyx: add "needs" Add the "needs rankwidth" tags for the doctests in this module, all of which need rankwidth. At first a one-line magic sage.doctest header appears to make more sense, but pytest does not understand these and needs workarounds (see conftest.py). Given the small number of tests in this module, individual annotations seem like less of a hassle. --- src/sage/graphs/graph_decompositions/rankwidth.pyx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/sage/graphs/graph_decompositions/rankwidth.pyx b/src/sage/graphs/graph_decompositions/rankwidth.pyx index f5714cd9387..e1092a200ee 100644 --- a/src/sage/graphs/graph_decompositions/rankwidth.pyx +++ b/src/sage/graphs/graph_decompositions/rankwidth.pyx @@ -46,6 +46,7 @@ i.e. singletons. :: + sage: # needs rankwidth sage: g = graphs.PetersenGraph() sage: rw, tree = g.rank_decomposition() sage: all(len(v)==1 for v in tree if tree.degree(v) == 1) @@ -58,6 +59,7 @@ from the smaller of the two and its complement. :: + sage: # needs rankwidth sage: g = graphs.PetersenGraph() sage: rw, tree = g.rank_decomposition() sage: u = Set([8, 9, 3, 7]) @@ -80,6 +82,7 @@ from the smaller of the two and its complement. EXAMPLES:: + sage: # needs rankwidth sage: g = graphs.PetersenGraph() sage: g.rank_decomposition() (3, Graph on 19 vertices) @@ -137,6 +140,7 @@ def rank_decomposition(G, verbose=False, immutable=None): EXAMPLES:: + sage: # needs rankwidth sage: from sage.graphs.graph_decompositions.rankwidth import rank_decomposition sage: g = graphs.PetersenGraph() sage: rank_decomposition(g) @@ -144,6 +148,7 @@ def rank_decomposition(G, verbose=False, immutable=None): On more than 32 vertices:: + sage: # needs rankwidth sage: g = graphs.RandomGNP(40, .5) sage: rank_decomposition(g) Traceback (most recent call last): @@ -152,6 +157,7 @@ def rank_decomposition(G, verbose=False, immutable=None): The empty graph:: + sage: # needs rankwidth sage: g = Graph() sage: rank_decomposition(g) (0, Graph on 0 vertices) @@ -302,6 +308,7 @@ def mkgraph(int num_vertices): EXAMPLES:: + sage: # needs rankwidth sage: from sage.graphs.graph_decompositions.rankwidth import rank_decomposition sage: g = graphs.PetersenGraph() sage: rank_decomposition(g)