From 662319ca4cc254455bba2278b2ef443dc80ba098 Mon Sep 17 00:00:00 2001 From: Chris Sewell Date: Wed, 16 Oct 2024 15:23:08 +0200 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20Improve=20access=20to=20dy?= =?UTF-8?q?namic=20configs:=20extra=5Foptions,=20functions,=20warnings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit These three configurations are complicated by the fact that they can be set both in the `conf.py` configuration, but also via functions from `sphinx_needs.api`. This has lead to confusion, when to use `NEEDS_CONFIG` and when to use `NeedsSphinxConfig` (which I have already had to fix numerous bugs for) In this PR we make split access to these configs into: - `NeedsSphinxConfig._extra_options`, `NeedsSphinxConfig._functions`, `NeedsSphinxConfig._warnings`, which access the "raw" sphinx configuration - `NeedsSphinxConfig.extra_options`, `NeedsSphinxConfig.functions`, `NeedsSphinxConfig.warnings`, which access the "combined" sphinx config + API added values --- sphinx_needs/api/configuration.py | 12 +- sphinx_needs/api/need.py | 6 +- sphinx_needs/config.py | 194 +++++++++++++++++++------- sphinx_needs/data.py | 4 +- sphinx_needs/directives/need.py | 4 +- sphinx_needs/directives/needimport.py | 4 +- sphinx_needs/directives/needreport.py | 4 +- sphinx_needs/external_needs.py | 4 +- sphinx_needs/functions/__init__.py | 1 - sphinx_needs/functions/functions.py | 37 +---- sphinx_needs/needs.py | 43 +++--- sphinx_needs/needsfile.py | 12 +- sphinx_needs/services/manager.py | 8 +- sphinx_needs/utils.py | 14 -- sphinx_needs/warnings.py | 8 +- 15 files changed, 205 insertions(+), 150 deletions(-) diff --git a/sphinx_needs/api/configuration.py b/sphinx_needs/api/configuration.py index 578fcb7b7..0592a9088 100644 --- a/sphinx_needs/api/configuration.py +++ b/sphinx_needs/api/configuration.py @@ -12,9 +12,9 @@ from sphinx.util.logging import SphinxLoggerAdapter from sphinx_needs.api.exceptions import NeedsApiConfigException -from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig +from sphinx_needs.config import _NEEDS_CONFIG, NeedsSphinxConfig from sphinx_needs.data import NeedsInfoType -from sphinx_needs.functions.functions import DynamicFunction, register_func +from sphinx_needs.functions.functions import DynamicFunction def get_need_types(app: Sphinx) -> list[str]: @@ -101,7 +101,7 @@ def add_extra_option( :param name: Name as string of the extra option :return: None """ - NEEDS_CONFIG.add_extra_option(name, description) + _NEEDS_CONFIG.add_extra_option(name, description) def add_dynamic_function( @@ -130,7 +130,7 @@ def my_function(app, need, needs, *args, **kwargs): :param name: Name of the dynamic function as string :return: None """ - register_func(function, name) + _NEEDS_CONFIG.add_function(function, name) # 'Need' is untyped, so we temporarily use 'Any' here @@ -170,7 +170,7 @@ def add_warning( if warning_check is None: raise NeedsApiConfigException("either function or filter_string must be given") - if name in NEEDS_CONFIG.warnings: + if name in _NEEDS_CONFIG.warnings: raise NeedsApiConfigException(f"Warning {name} already registered.") - NEEDS_CONFIG.warnings[name] = warning_check + _NEEDS_CONFIG.add_warning(name, warning_check) diff --git a/sphinx_needs/api/need.py b/sphinx_needs/api/need.py index d720edda6..61ca1556e 100644 --- a/sphinx_needs/api/need.py +++ b/sphinx_needs/api/need.py @@ -16,7 +16,7 @@ from sphinx.environment import BuildEnvironment from sphinx_needs.api.exceptions import InvalidNeedException -from sphinx_needs.config import NEEDS_CONFIG, GlobalOptionsType, NeedsSphinxConfig +from sphinx_needs.config import GlobalOptionsType, NeedsSphinxConfig from sphinx_needs.data import NeedsInfoType, NeedsPartType, SphinxNeedsData from sphinx_needs.directives.needuml import Needuml, NeedumlException from sphinx_needs.filter_common import filter_single_need @@ -118,7 +118,7 @@ def generate_need( # validate kwargs allowed_kwargs = {x["option"] for x in needs_config.extra_links} | set( - NEEDS_CONFIG.extra_options + needs_config.extra_options ) unknown_kwargs = set(kwargs) - allowed_kwargs if unknown_kwargs: @@ -235,7 +235,7 @@ def generate_need( } # add dynamic keys to needs_info - _merge_extra_options(needs_info, kwargs, NEEDS_CONFIG.extra_options) + _merge_extra_options(needs_info, kwargs, needs_config.extra_options) _merge_global_options(needs_config, needs_info, needs_config.global_options) # Merge links diff --git a/sphinx_needs/config.py b/sphinx_needs/config.py index 2cde4a08c..5d4f055b9 100644 --- a/sphinx_needs/config.py +++ b/sphinx_needs/config.py @@ -9,6 +9,7 @@ from sphinx_needs.data import GraphvizStyleType, NeedsCoreFields from sphinx_needs.defaults import DEFAULT_DIAGRAM_TEMPLATE +from sphinx_needs.logging import get_logger, log_warning if TYPE_CHECKING: from sphinx.util.logging import SphinxLoggerAdapter @@ -18,6 +19,9 @@ from sphinx_needs.functions.functions import DynamicFunction +LOGGER = get_logger(__name__) + + @dataclass class ExtraOptionParams: """Defines a single extra option for needs""" @@ -28,34 +32,37 @@ class ExtraOptionParams: """A function to validate the directive option value.""" -class Config: - """ - Stores sphinx-needs specific configuration values. +class NeedFunctionsType(TypedDict): + name: str + function: DynamicFunction - This is used to avoid the usage of the sphinx internal config option, as these can be reset or cleaned in - unspecific order during different events. - So this Config class somehow collects possible configurations and stores it in a save way. +class _Config: + """Stores sphinx-needs configuration values that can be set both via the sphinx configuration, + and also via the API functions. """ def __init__(self) -> None: self._extra_options: dict[str, ExtraOptionParams] = {} + self._functions: dict[str, NeedFunctionsType] = {} self._warnings: dict[ str, str | Callable[[NeedsInfoType, SphinxLoggerAdapter], bool] ] = {} def clear(self) -> None: self._extra_options = {} + self._functions = {} self._warnings = {} @property def extra_options(self) -> Mapping[str, ExtraOptionParams]: - """Options that are dynamically added to `NeedDirective` & `NeedserviceDirective`, - after the config is initialized. + """Custom need fields. - These fields are also added to the each needs data item. + These fields can be added via sphinx configuration, + and also via the `add_extra_option` API function. - :returns: Mapping of name to validation function + They are added to the each needs data item, + and as directive options on `NeedDirective` and `NeedserviceDirective`. """ return self._extra_options @@ -68,27 +75,60 @@ def add_extra_option( override: bool = False, ) -> None: """Adds an extra option to the configuration.""" - if not override and name in self._extra_options: - from sphinx_needs.api.exceptions import ( - NeedsApiConfigWarning, # avoid circular import - ) + if name in self._extra_options: + if override: + log_warning( + LOGGER, + f'extra_option "{name}" already registered.', + "config", + None, + ) + else: + from sphinx_needs.api.exceptions import ( + NeedsApiConfigWarning, # avoid circular import + ) - raise NeedsApiConfigWarning(f"Option {name} already registered.") + raise NeedsApiConfigWarning(f"Option {name} already registered.") self._extra_options[name] = ExtraOptionParams( description, directives.unchanged if validator is None else validator ) + @property + def functions(self) -> Mapping[str, NeedFunctionsType]: + """Dynamic functions that are added by the user.""" + return self._functions + + def add_function(self, function: DynamicFunction, name: str | None = None) -> None: + """Adds a dynamic function to the configuration.""" + func_name = function.__name__ if name is None else name + if func_name in self._functions: + log_warning( + LOGGER, + f"Dynamic function {func_name} already registered.", + "config", + None, + ) + self._functions[func_name] = {"name": func_name, "function": function} + @property def warnings( self, - ) -> dict[str, str | Callable[[NeedsInfoType, SphinxLoggerAdapter], bool]]: + ) -> Mapping[str, str | Callable[[NeedsInfoType, SphinxLoggerAdapter], bool]]: """Warning handlers that are added by the user, then called at the end of the build. """ return self._warnings + def add_warning( + self, + name: str, + filter: str | Callable[[NeedsInfoType, SphinxLoggerAdapter], bool], + ) -> None: + """Adds a warning handler to the configuration.""" + self._warnings[name] = filter + -NEEDS_CONFIG = Config() +_NEEDS_CONFIG = _Config() class ConstraintFailedType(TypedDict): @@ -199,18 +239,68 @@ class NeedsSphinxConfig: # such that we simply redirect all attribute access to the # Sphinx config object, but in a manner where type annotations will work # for static type analysis. + # Note also that we treat `extra_options`, `functions` and `warnings` as special-cases, + # since these configurations can also be added to dynamically via the API def __init__(self, config: _SphinxConfig) -> None: super().__setattr__("_config", config) def __getattribute__(self, name: str) -> Any: - if name.startswith("__"): + if name.startswith("__") or name in ( + "_config", + "extra_options", + "functions", + "warnings", + ): return super().__getattribute__(name) + if name.startswith("_"): + name = name[1:] return getattr(super().__getattribute__("_config"), f"needs_{name}") def __setattr__(self, name: str, value: Any) -> None: + if name.startswith("__") or name in ( + "_config", + "extra_options", + "functions", + "warnings", + ): + return super().__setattr__(name, value) + if name.startswith("_"): + name = name[1:] return setattr(super().__getattribute__("_config"), f"needs_{name}", value) + @classmethod + def add_config_values(cls, app: Sphinx) -> None: + """Add all config values to the Sphinx application.""" + for item in fields(cls): + if item.default_factory is not MISSING: + default = item.default_factory() + elif item.default is not MISSING: + default = item.default + else: + raise Exception( + f"Config item {item.name} has no default value or factory." + ) + name = item.name + if name.startswith("_"): + name = name[1:] + app.add_config_value( + f"needs_{name}", + default, + item.metadata["rebuild"], + types=item.metadata["types"], + ) + + @classmethod + def get_default(cls, name: str) -> Any: + """Get the default value for a config item.""" + _field = next( + field for field in fields(cls) if field.name in (name, f"_{name}") + ) + if _field.default_factory is not MISSING: + return _field.default_factory() + return _field.default + types: list[NeedType] = field( default_factory=lambda: [ { @@ -301,10 +391,23 @@ def __setattr__(self, name: str, value: Any) -> None: default=30, metadata={"rebuild": "html", "types": (int,)} ) """Maximum length of the title in the need role output.""" - extra_options: list[str] = field( + _extra_options: list[str] = field( default_factory=list, metadata={"rebuild": "html", "types": (list,)} ) """List of extra options for needs, that get added as directive options and need fields.""" + + @property + def extra_options(self) -> Mapping[str, ExtraOptionParams]: + """Custom need fields. + + These fields can be added via sphinx configuration, + but also via the `add_extra_option` API function. + + They are added to the each needs data item, + and as directive options on `NeedDirective` and `NeedserviceDirective`. + """ + return _NEEDS_CONFIG.extra_options + title_optional: bool = field( default=False, metadata={"rebuild": "html", "types": (bool,)} ) @@ -322,10 +425,20 @@ def __setattr__(self, name: str, value: Any) -> None: metadata={"rebuild": "html", "types": (str,)}, ) """Template for node content in needflow diagrams (with plantuml engine).""" - functions: list[DynamicFunction] = field( + _functions: list[DynamicFunction] = field( default_factory=list, metadata={"rebuild": "html", "types": (list,)} ) """List of dynamic functions.""" + + @property + def functions(self) -> Mapping[str, NeedFunctionsType]: + """Dynamic functions that are added by the user. + + These functions can be added via sphinx configuration, + but also via the `add_dynamic_function` API function. + """ + return _NEEDS_CONFIG.functions + global_options: GlobalOptionsType = field( default_factory=dict, metadata={"rebuild": "html", "types": (dict,)} ) @@ -404,10 +517,22 @@ def __setattr__(self, name: str, value: Any) -> None: default_factory=lambda: ["links"], metadata={"rebuild": "html", "types": ()} ) """Defines the link_types to show in a needflow diagram.""" - warnings: dict[str, Any] = field( + _warnings: dict[str, Any] = field( default_factory=dict, metadata={"rebuild": "html", "types": ()} ) """Defines warnings to be checked at the end of the build (name -> string filter / filter function).""" + + @property + def warnings( + self, + ) -> Mapping[str, str | Callable[[NeedsInfoType, SphinxLoggerAdapter], bool]]: + """Defines warnings to be checked at the end of the build (name -> string filter / filter function). + + These handlers can be added via sphinx configuration, + but also via the `add_warning` API function. + """ + return _NEEDS_CONFIG.warnings + warnings_always_warn: bool = field( default=False, metadata={"rebuild": "html", "types": (bool,)} ) @@ -549,30 +674,3 @@ def __setattr__(self, name: str, value: Any) -> None: default=False, metadata={"rebuild": "html", "types": (bool,)} ) """If True, log filter processing runtime information.""" - - @classmethod - def add_config_values(cls, app: Sphinx) -> None: - """Add all config values to the Sphinx application.""" - for item in fields(cls): - if item.default_factory is not MISSING: - default = item.default_factory() - elif item.default is not MISSING: - default = item.default - else: - raise Exception( - f"Config item {item.name} has no default value or factory." - ) - app.add_config_value( - f"needs_{item.name}", - default, - item.metadata["rebuild"], - types=item.metadata["types"], - ) - - @classmethod - def get_default(cls, name: str) -> Any: - """Get the default value for a config item.""" - _field = next(field for field in fields(cls) if field.name == name) - if _field.default_factory is not MISSING: - return _field.default_factory() - return _field.default diff --git a/sphinx_needs/data.py b/sphinx_needs/data.py index 76fa7b659..15714111c 100644 --- a/sphinx_needs/data.py +++ b/sphinx_needs/data.py @@ -478,7 +478,7 @@ class NeedsInfoType(TypedDict, total=False): """ # Fields added dynamically by services: - # options from ``BaseService.options`` get added to ``NEEDS_CONFIG.extra_options``, + # options from ``BaseService.options`` get added to ``extra_options``, # via `ServiceManager.register`, # which in turn means they are added to every need via ``add_need`` # ``GithubService.options`` @@ -503,7 +503,7 @@ class NeedsInfoType(TypedDict, total=False): # Note there are also these dynamic keys: # - items in ``needs_extra_options`` + ``needs_duration_option`` + ``needs_completion_option``, - # which get added to ``NEEDS_CONFIG.extra_options``, + # which get added to ``extra_options``, # and in turn means they are added to every need via ``add_need`` (as strings) # - keys in ``needs_global_options`` config are added to every need via ``add_need`` diff --git a/sphinx_needs/directives/need.py b/sphinx_needs/directives/need.py index 1e2ad5d86..346637ea8 100644 --- a/sphinx_needs/directives/need.py +++ b/sphinx_needs/directives/need.py @@ -10,7 +10,7 @@ from sphinx.util.docutils import SphinxDirective from sphinx_needs.api import InvalidNeedException, add_need -from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import NeedsMutable, SphinxNeedsData from sphinx_needs.debug import measure_time from sphinx_needs.defaults import NEED_DEFAULT_OPTIONS @@ -80,7 +80,7 @@ def run(self) -> Sequence[nodes.Node]: extra_link["option"], "" ) - for extra_option in NEEDS_CONFIG.extra_options: + for extra_option in needs_config.extra_options: need_extra_options[extra_option] = self.options.get(extra_option, "") try: diff --git a/sphinx_needs/directives/needimport.py b/sphinx_needs/directives/needimport.py index 0451c6f02..d05fd5473 100644 --- a/sphinx_needs/directives/needimport.py +++ b/sphinx_needs/directives/needimport.py @@ -13,7 +13,7 @@ from sphinx.util.docutils import SphinxDirective from sphinx_needs.api import InvalidNeedException, add_need -from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import NeedsCoreFields, NeedsInfoType from sphinx_needs.debug import measure_time from sphinx_needs.defaults import string_to_boolean @@ -203,7 +203,7 @@ def run(self) -> Sequence[nodes.Node]: *NeedsCoreFields, *(x["option"] for x in needs_config.extra_links), *(x["option"] + "_back" for x in needs_config.extra_links), - *NEEDS_CONFIG.extra_options, + *needs_config.extra_options, } # all keys that should not be imported from external needs omitted_keys = { diff --git a/sphinx_needs/directives/needreport.py b/sphinx_needs/directives/needreport.py index 270c5dfae..f67ceb831 100644 --- a/sphinx_needs/directives/needreport.py +++ b/sphinx_needs/directives/needreport.py @@ -41,7 +41,9 @@ def run(self) -> Sequence[nodes.raw]: report_info = { "types": needs_config.types if "types" in self.options else [], - "options": needs_config.extra_options if "options" in self.options else [], + "options": list(needs_config.extra_options) + if "options" in self.options + else [], "links": needs_config.extra_links if "links" in self.options else [], # note the usage dict format here is just to keep backwards compatibility, # but actually this is now post-processed so we only really need the need types diff --git a/sphinx_needs/external_needs.py b/sphinx_needs/external_needs.py index f56d301ba..08b131e9a 100644 --- a/sphinx_needs/external_needs.py +++ b/sphinx_needs/external_needs.py @@ -11,7 +11,7 @@ from sphinx.environment import BuildEnvironment from sphinx_needs.api import InvalidNeedException, add_external_need, del_need -from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import NeedsCoreFields, SphinxNeedsData from sphinx_needs.logging import get_logger, log_warning from sphinx_needs.utils import clean_log, import_prefix_link_edit @@ -124,7 +124,7 @@ def load_external_needs( *NeedsCoreFields, *(x["option"] for x in needs_config.extra_links), *(x["option"] + "_back" for x in needs_config.extra_links), - *NEEDS_CONFIG.extra_options, + *needs_config.extra_options, } # all keys that should not be imported from external needs omitted_keys = { diff --git a/sphinx_needs/functions/__init__.py b/sphinx_needs/functions/__init__.py index 16b57a1c4..a067537f9 100644 --- a/sphinx_needs/functions/__init__.py +++ b/sphinx_needs/functions/__init__.py @@ -13,7 +13,6 @@ FunctionParsingException, execute_func, find_and_replace_node_content, - register_func, resolve_dynamic_values, resolve_variants_options, ) diff --git a/sphinx_needs/functions/functions.py b/sphinx_needs/functions/functions.py index 482d9c20e..46ef090e5 100644 --- a/sphinx_needs/functions/functions.py +++ b/sphinx_needs/functions/functions.py @@ -24,7 +24,7 @@ from sphinx_needs.logging import get_logger, log_warning from sphinx_needs.nodes import Need from sphinx_needs.roles.need_func import NeedFunc -from sphinx_needs.utils import NEEDS_FUNCTIONS, match_variants +from sphinx_needs.utils import match_variants from sphinx_needs.views import NeedsView logger = get_logger(__name__) @@ -47,32 +47,6 @@ def __call__( ) -> str | int | float | list[str] | list[int] | list[float] | None: ... -def register_func(need_function: DynamicFunction, name: str | None = None) -> None: - """ - Registers a new sphinx-needs function for the given sphinx environment. - :param env: Sphinx environment - :param need_function: Python method - :param name: Name of the function as string - :return: None - """ - - global NEEDS_FUNCTIONS - if NEEDS_FUNCTIONS is None: - NEEDS_FUNCTIONS = {} - - func_name = need_function.__name__ if name is None else name - - if func_name in NEEDS_FUNCTIONS: - # We can not throw an exception here, as using sphinx-needs in different sphinx-projects with the - # same python interpreter session does not clean NEEDS_FUNCTIONS. - # This is mostly the case during tet runs. - logger.info( - f"sphinx-needs: Function name {func_name} already registered. Ignoring the new one!" - ) - - NEEDS_FUNCTIONS[func_name] = {"name": func_name, "function": need_function} - - def execute_func( app: Sphinx, need: NeedsInfoType | None, @@ -88,7 +62,6 @@ def execute_func( :param location: source location of the function call :return: return value of executed function """ - global NEEDS_FUNCTIONS try: func_name, func_args, func_kwargs = _analyze_func_string(func_string, need) except FunctionParsingException as err: @@ -100,7 +73,9 @@ def execute_func( ) return "??" - if func_name not in NEEDS_FUNCTIONS: + needs_config = NeedsSphinxConfig(app.config) + + if func_name not in needs_config.functions: log_warning( logger, f"Unknown function {func_name!r}", @@ -110,7 +85,9 @@ def execute_func( return "??" func = measure_time_func( - NEEDS_FUNCTIONS[func_name]["function"], category="dyn_func", source="user" + needs_config.functions[func_name]["function"], + category="dyn_func", + source="user", ) try: diff --git a/sphinx_needs/needs.py b/sphinx_needs/needs.py index 97804ff8b..d56659649 100644 --- a/sphinx_needs/needs.py +++ b/sphinx_needs/needs.py @@ -21,7 +21,7 @@ build_needs_json, build_needumls_pumls, ) -from sphinx_needs.config import NEEDS_CONFIG, LinkOptionsType, NeedsSphinxConfig +from sphinx_needs.config import _NEEDS_CONFIG, LinkOptionsType, NeedsSphinxConfig from sphinx_needs.data import ( ENV_DATA_VERSION, NeedsCoreFields, @@ -98,7 +98,6 @@ ) from sphinx_needs.external_needs import load_external_needs from sphinx_needs.functions import NEEDS_COMMON_FUNCTIONS -from sphinx_needs.functions.functions import register_func from sphinx_needs.logging import get_logger, log_warning from sphinx_needs.nodes import Need from sphinx_needs.roles import NeedsXRefRole @@ -110,11 +109,10 @@ from sphinx_needs.roles.need_ref import NeedRef, process_need_ref from sphinx_needs.services.github import GithubService from sphinx_needs.services.open_needs import OpenNeedsService -from sphinx_needs.utils import NEEDS_FUNCTIONS, node_match +from sphinx_needs.utils import node_match from sphinx_needs.warnings import process_warnings __version__ = VERSION = "4.0.0" -NEEDS_FUNCTIONS.clear() _NODE_TYPES_T = Dict[ Type[nodes.Element], @@ -302,7 +300,7 @@ def setup(app: Sphinx) -> dict[str, Any]: # Be sure Sphinx-Needs config gets erased before any events or external API calls get executed. # So never but this inside an event. - NEEDS_CONFIG.clear() + _NEEDS_CONFIG.clear() return { "version": VERSION, @@ -364,28 +362,21 @@ def load_config(app: Sphinx, *_args: Any) -> None: """ needs_config = NeedsSphinxConfig(app.config) - if isinstance(needs_config.extra_options, dict): + if isinstance(needs_config._extra_options, dict): LOGGER.info( 'Config option "needs_extra_options" supports list and dict. However new default type since ' "Sphinx-Needs 0.7.2 is list. Please see docs for details." ) - for option in needs_config.extra_options: - if option in NEEDS_CONFIG.extra_options: - log_warning( - LOGGER, - f'extra_option "{option}" already registered.', - "config", - None, - ) - NEEDS_CONFIG.add_extra_option( + for option in needs_config._extra_options: + _NEEDS_CONFIG.add_extra_option( option, "Added by needs_extra_options config", override=True ) # ensure options for ``needgantt`` functionality are added to the extra options for option in (needs_config.duration_option, needs_config.completion_option): - if option not in NEEDS_CONFIG.extra_options: - NEEDS_CONFIG.add_extra_option( + if option not in _NEEDS_CONFIG.extra_options: + _NEEDS_CONFIG.add_extra_option( option, "Added for needgantt functionality", validator=directives.unchanged_required, @@ -402,10 +393,10 @@ def load_config(app: Sphinx, *_args: Any) -> None: # Update NeedDirective to use customized options NeedDirective.option_spec.update( - {k: v.validator for k, v in NEEDS_CONFIG.extra_options.items()} + {k: v.validator for k, v in _NEEDS_CONFIG.extra_options.items()} ) NeedserviceDirective.option_spec.update( - {k: v.validator for k, v in NEEDS_CONFIG.extra_options.items()} + {k: v.validator for k, v in _NEEDS_CONFIG.extra_options.items()} ) # Update NeedDirective to use customized links @@ -447,7 +438,7 @@ def load_config(app: Sphinx, *_args: Any) -> None: "-links_back": directives.flag, } ) - for key, value in NEEDS_CONFIG.extra_options.items(): + for key, value in _NEEDS_CONFIG.extra_options.items(): NeedextendDirective.option_spec.update( { key: value.validator, @@ -464,9 +455,9 @@ def load_config(app: Sphinx, *_args: Any) -> None: # Register requested types of needs app.add_directive(t["directive"], NeedDirective) - for name, check in needs_config.warnings.items(): - if name not in NEEDS_CONFIG.warnings: - NEEDS_CONFIG.warnings[name] = check + for name, check in needs_config._warnings.items(): + if name not in _NEEDS_CONFIG.warnings: + _NEEDS_CONFIG.add_warning(name, check) else: log_warning( LOGGER, @@ -527,11 +518,11 @@ def prepare_env(app: Sphinx, env: BuildEnvironment, _docnames: list[str]) -> Non # Register built-in functions for need_common_func in NEEDS_COMMON_FUNCTIONS: - register_func(need_common_func) + _NEEDS_CONFIG.add_function(need_common_func) # Register functions configured by user - for needs_func in needs_config.functions: - register_func(needs_func) + for needs_func in needs_config._functions: + _NEEDS_CONFIG.add_function(needs_func) # The default link name. Must exist in all configurations. Therefore we set it here # for the user. diff --git a/sphinx_needs/needsfile.py b/sphinx_needs/needsfile.py index ff7a4561f..88cbb9cc1 100644 --- a/sphinx_needs/needsfile.py +++ b/sphinx_needs/needsfile.py @@ -17,7 +17,7 @@ from jsonschema import Draft7Validator from sphinx.config import Config -from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import NeedsCoreFields, NeedsInfoType from sphinx_needs.logging import get_logger, log_warning @@ -25,7 +25,7 @@ def generate_needs_schema( - config: Config, exclude_properties: Iterable[str] = () + needs_config: NeedsSphinxConfig, exclude_properties: Iterable[str] = () ) -> dict[str, Any]: """Generate a JSON schema for all fields in each need item. @@ -37,7 +37,7 @@ def generate_needs_schema( """ properties: dict[str, Any] = {} - for name, extra_params in NEEDS_CONFIG.extra_options.items(): + for name, extra_params in needs_config.extra_options.items(): properties[name] = { "type": "string", "description": extra_params.description, @@ -54,8 +54,6 @@ def generate_needs_schema( properties[name]["description"] = f"{core_params['description']}" properties[name]["field_type"] = "core" - needs_config = NeedsSphinxConfig(config) - for link in needs_config.extra_links: properties[link["option"]] = { "type": "array", @@ -114,7 +112,9 @@ def __init__( self._exclude_need_keys = set(self.needs_config.json_exclude_fields) self._schema = ( - generate_needs_schema(config, exclude_properties=self._exclude_need_keys) + generate_needs_schema( + self.needs_config, exclude_properties=self._exclude_need_keys + ) if add_schema else None ) diff --git a/sphinx_needs/services/manager.py b/sphinx_needs/services/manager.py index 1a87649c7..698861f95 100644 --- a/sphinx_needs/services/manager.py +++ b/sphinx_needs/services/manager.py @@ -4,7 +4,7 @@ from sphinx.application import Sphinx -from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig +from sphinx_needs.config import _NEEDS_CONFIG, NeedsSphinxConfig from sphinx_needs.directives.needservice import NeedserviceDirective from sphinx_needs.logging import get_logger from sphinx_needs.services.base import BaseService @@ -28,12 +28,12 @@ def register(self, name: str, klass: type[BaseService], **kwargs: Any) -> None: # Register options from service class for option in klass.options: - if option not in NEEDS_CONFIG.extra_options: + if option not in _NEEDS_CONFIG.extra_options: self.log.debug(f'Register option "{option}" for service "{name}"') - NEEDS_CONFIG.add_extra_option(option, f"Added by service {name}") + _NEEDS_CONFIG.add_extra_option(option, f"Added by service {name}") # Register new option directly in Service directive, as its class options got already # calculated - NeedserviceDirective.option_spec[option] = NEEDS_CONFIG.extra_options[ + NeedserviceDirective.option_spec[option] = _NEEDS_CONFIG.extra_options[ option ].validator diff --git a/sphinx_needs/utils.py b/sphinx_needs/utils.py index 06c44c139..ab5d9ab07 100644 --- a/sphinx_needs/utils.py +++ b/sphinx_needs/utils.py @@ -22,28 +22,14 @@ from sphinx_needs.logging import get_logger, log_warning from sphinx_needs.views import NeedsAndPartsListView, NeedsView -try: - from typing import TypedDict -except ImportError: - from typing_extensions import TypedDict - if TYPE_CHECKING: import matplotlib from matplotlib.figure import FigureBase - from sphinx_needs.functions.functions import DynamicFunction logger = get_logger(__name__) -class NeedFunctionsType(TypedDict): - name: str - function: DynamicFunction - - -NEEDS_FUNCTIONS: dict[str, NeedFunctionsType] = {} - - MONTH_NAMES = [ "January", "February", diff --git a/sphinx_needs/warnings.py b/sphinx_needs/warnings.py index 5c0e78601..f7ea2910a 100644 --- a/sphinx_needs/warnings.py +++ b/sphinx_needs/warnings.py @@ -8,7 +8,7 @@ from sphinx.application import Sphinx from sphinx.util import logging -from sphinx_needs.config import NEEDS_CONFIG, NeedsSphinxConfig +from sphinx_needs.config import NeedsSphinxConfig from sphinx_needs.data import SphinxNeedsData from sphinx_needs.filter_common import filter_needs_view from sphinx_needs.logging import get_logger, log_warning @@ -32,8 +32,10 @@ def process_warnings(app: Sphinx, exception: Exception | None) -> None: if exception: return + needs_config = NeedsSphinxConfig(app.config) + # If no warnings were defined, we do not need to do anything - if not NEEDS_CONFIG.warnings: + if not needs_config.warnings: return env = app.env @@ -59,7 +61,7 @@ def process_warnings(app: Sphinx, exception: Exception | None) -> None: with logging.pending_logging(): logger.info("\nChecking sphinx-needs warnings") warning_raised = False - for warning_name, warning_filter in NEEDS_CONFIG.warnings.items(): + for warning_name, warning_filter in needs_config.warnings.items(): if isinstance(warning_filter, str): # filter string used result = filter_needs_view(