latexify_py icon indicating copy to clipboard operation
latexify_py copied to clipboard

Config class

Open odashi opened this issue 2 years ago • 9 comments

Follows #65

It may be good if we provide a config class integrating every setting, which are currently passed directly to with_latex:

import latexify

config = latexify.Config.defaults()
config.use_math_symbols()
config.expand_function("expit")

@latexify.with_latex(config)
def f(x):
    return expit(x)

# Will generate: \mathrm{f}(x) \triangleq \frac{1}{1+\exp{(-x)}}

odashi avatar Oct 25 '22 17:10 odashi

possible logic:

inner_config = copy(config if config is not None else Config.defaults())
for each kwarg:
    inner_config.update(kwarg)

inner_process(fn, inner_config)

odashi avatar Oct 25 '22 17:10 odashi

working on this

chunibyo-wly avatar Nov 08 '22 13:11 chunibyo-wly

@chunibyo-wly Thanks for working on this. Since this feature should be carefully designed, could you provide your idea about the implementation before throwing PRs?

odashi avatar Nov 08 '22 18:11 odashi

hello, thanks for your reply, could you please give me some time to do some tests? I will give you answer later

chunibyo-wly avatar Nov 09 '22 15:11 chunibyo-wly

@chunibyo-wly Sure. I targeted this feature on the release after the next (0.3) so we don't need to rush at this point.

odashi avatar Nov 10 '22 01:11 odashi

  1. I think we could use bitwise and dynamic method creation to do this
from __future__ import annotations

from latexify.constants import CONFIGURATION
from typing import Callable

class Config:
    def __init__(self,
        identifiers: dict[str, str] | None = None,
        reduce_assignments: bool = False,
        use_math_symbols: bool = False,
        use_raw_function_name: bool = False,
        use_signature: bool = True,) -> None:

        self.flag: int = reduce_assignments * CONFIGURATION['reduce_assignments'] \
                    + use_math_symbols * CONFIGURATION["use_math_symbols"] \
                    + use_raw_function_name * CONFIGURATION["use_raw_function_name"] \
                    + use_signature * CONFIGURATION["use_signature"]
        self.identifiers: dict[str, str] = identifiers
        self.expanded_function: set[str] = set()

        for _config in CONFIGURATION:
            setattr(self, _config, self.__make_method_change_flag(_config))
            setattr(self, f"is_{_config}", self.__make_method_use_flag(_config))

    @staticmethod
    def defaults() -> Config:
        return Config()

    def expand_function(self, function: str | list[str]) -> None:
        if type(function) == str:
            self.expanded_function.add(function)
        elif type(function) == list:
            self.expand_function.update(function)

    def __make_method_change_flag(self, config_name: str) -> Callable:
        def _method(enabled: bool | None = True) -> None:
            if enabled:
                self.flag += CONFIGURATION[config_name]
            else:
                self.flag -= CONFIGURATION[config_name]
        return _method

    def __make_method_use_flag(self, config_name: str) -> Callable:
        @property
        def _method() -> bool:
            return (self.flag & CONFIGURATION[config_name]) != 0
        return _method

and constants is look like this:

CONFIGURATION = {
    "reduce_assignments": 0,
    "use_math_symbols": 2,
    "use_raw_function_name": 4,
    "use_signature": 8
}
  1. change frontend get_latex and reserved original parameter for user convenience
def get_latex(
    fn: Callable[..., Any],
    *,
    identifiers: dict[str, str] | None = None,
    reduce_assignments: bool = False,
    use_math_symbols: bool = False,
    use_raw_function_name: bool = False,
    use_signature: bool = True,
    configuration: Config | None = None,
) -> str:

chunibyo-wly avatar Nov 12 '22 05:11 chunibyo-wly

@chunibyo-wly Thanks! I think we don't need bitset here and it is enough to store individual members for all boolean configs. We basically don't need to care about the efficiency of this class because parsing and codegen are much (maybe a hundred of times) slower.

odashi avatar Nov 12 '22 12:11 odashi

I think we can simply define the config class as a dataclass with only nullable members, with the merge method that generates a new Config while maintaining the original config unchanged:

@dataclasses.dataclass
class Config(frozen=True):
    some_flag: SomeType | None
    ...

    def merge(self, *, config: Config | None = None, **kwargs) -> Config:
        def merge_field(name: str) -> Any:
            # Precedence: kwargs -> config -> self
            arg = kwargs.get(name)
            if arg is None and config is not None:
                arg = getattr(config, name)
            if arg is None:
                arg = getattr(self, name)
            return arg

        return Config(
            **{f.name: merge_field(f.name) for f in dataclasses.fields(self)}
        )

then define the defaults with non-null values:

_DEFAULT_CONFIG = Config(
    some_flag=some_value,
    ...
)

In get_latex, we can construct the final config as follows:

def get_latex(fn: Callable[..., Any], *, config: Config | None, **kwargs):
    merged_config = _DEFAULT_CONFIG.merge(config=config, **kwargs)

odashi avatar Nov 12 '22 13:11 odashi

thanks for your advice, I will write code based on your suggestions later

chunibyo-wly avatar Nov 12 '22 13:11 chunibyo-wly