useblocks / sphinx-needs

Adds needs/requirements to sphinx
https://sphinx-needs.readthedocs.io/en/latest/index.html
MIT License
210 stars 66 forks source link

How-to use needs-render-context with a function without getting an unpickable warning #1206

Open PhilipPartsch opened 2 months ago

PhilipPartsch commented 2 months ago

I try to use needs-render-context and get

cannot cache unpickable configuration value: 'needs_render_context' (because it contains a function, class, or module object)

Is there an option, to configure it without getting the warning?

I have set: suppress_warnings = ["config.cache"] in conf.py, but this is only a work around.

AlexanderLanin commented 1 month ago

<deprecated, nevermind> :D

The trouble is sphinx-needs takes a str() of the parameter. Which is a dynamic address for functions. So what I did so far was wrap the functions in a class, but this has stopped working (not sure which version). Now we run into pickle trouble when we inject a class in that way.

class puml_participant:  # noqa: N801
    def __repr__(self):
        """
        Encapsulating the function in a class, forces cache compatibility as
        sphinx-needs will internally call str(obj) to determine the cache key.
        """
        return "[my puml_participant class]"

    def __call__(self, need: dict | str):
        """Create a PlantUML participant string for a need."""
        ...

needs_render_context = {
    "puml_participant": puml_participant(),
}
AlexanderLanin commented 1 month ago

I tried injecting into jinja2uml instead of render_context. It's not the best API, but very first attempts look promising....


def add_custom_uml_functions(functions: dict[str, Callable]):
    orig_jinja2uml = sphinx_needs.directives.needuml.jinja2uml

    def wrapper(  # noqa: PLR0913
        app: sphinx.application.Sphinx,
        fromdocname: None | str,
        uml_content: str,
        parent_need_id: str,
        key: str,
        processed_need_ids: dict,
        kwargs: dict[str, Any],
    ) -> tuple[str, dict]:
        # inject functions into kwargs, which will be appended to data
        kwargs.update(functions)
        return orig_jinja2uml(
            app,
            fromdocname,
            uml_content,
            parent_need_id,
            key,
            processed_need_ids,
            kwargs,
        )

    frame = inspect.currentframe()
    assert frame
    name_of_this_function = frame.f_code.co_name
    is_replaced = str(orig_jinja2uml).find(name_of_this_function) != -1
    if not is_replaced:
        # print("custom uml functions added")
        sphinx_needs.directives.needuml.jinja2uml = wrapper

And in setup() or somewhere:

    add_custom_uml_functions(
        {
            "puml_participant": puml_participant,
        }
    )

edit 10 hours later: now it's failing :(

[esbonio.lsp] Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/esbonio/lsp/sphinx/__init__.py", line 235, in build
    self.app.build(force_all, filenames)
  File "/usr/local/lib/python3.12/site-packages/sphinx/application.py", line 378, in build
    self.builder.build_update()
  File "/usr/local/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 297, in build_update
    self.build(to_build,
  File "/usr/local/lib/python3.12/site-packages/sphinx/builders/__init__.py", line 334, in build
    pickle.dump(self.env, f, pickle.HIGHEST_PROTOCOL)
_pickle.PicklingError: Can't pickle <function puml_participant at 0x7f04f32c2de0>: attribute lookup puml_participant on __main__ failed