Source code for astropy.utils.parsing

# Licensed under a 3-clause BSD style license - see LICENSE.rst
"""
Wrappers for PLY to provide thread safety.
"""

import contextlib
import functools
import re
import threading
from collections.abc import Generator
from pathlib import Path
from types import ModuleType

from ply.lex import Lexer
from ply.yacc import LRParser

__all__ = ["ThreadSafeParser", "lex", "yacc"]


_TAB_HEADER = """# Licensed under a 3-clause BSD style license - see LICENSE.rst

# This file was automatically generated from ply. To re-generate this file,
# remove it from this folder, then build astropy and run the tests in-place:
#
#   python setup.py build_ext --inplace
#   pytest {package}
#
# You can then commit the changes to this file.

"""
_LOCK = threading.RLock()


@contextlib.contextmanager
def _patch_ply_module(
    module: ModuleType, file: Path, package: str
) -> Generator[None, None, None]:
    """Temporarily replace the module's get_caller_module_dict.

    This is a function inside ``ply.lex`` and ``ply.yacc`` (each has a copy)
    that is used to retrieve the caller's local symbols. Here, we patch the
    function to instead retrieve the grandparent's local symbols to account
    for a wrapper layer.

    Additionally, a custom header is inserted into any files ``ply`` writes.
    """
    original = module.get_caller_module_dict

    @functools.wraps(original)
    def wrapper(levels):
        # Add 2, not 1, because the wrapper itself adds another level
        return original(levels + 2)

    file_exists = file.exists() or file.with_suffix(".pyc").exists()
    module.get_caller_module_dict = wrapper
    yield
    module.get_caller_module_dict = original
    if not file_exists:
        file.write_text(_TAB_HEADER.format(package=package) + file.read_text())


[docs] def lex(lextab: str, package: str, reflags: int = int(re.VERBOSE)) -> Lexer: """Create a lexer from local variables. It automatically compiles the lexer in optimized mode, writing to ``lextab`` in the same directory as the calling file. This function is thread-safe. The returned lexer is *not* thread-safe, but if it is used exclusively with a single parser returned by :func:`yacc` then it will be safe. It is only intended to work with lexers defined within the calling function, rather than at class or module scope. Parameters ---------- lextab : str Name for the file to write with the generated tables, if it does not already exist (without ``.py`` suffix). package : str Name of a test package which should be run with pytest to regenerate the output file. This is inserted into a comment in the generated file. reflags : int Passed to ``ply.lex``. """ from ply import lex caller_dir = Path(lex.get_caller_module_dict(2)["__file__"]).parent with _LOCK, _patch_ply_module(lex, caller_dir / (lextab + ".py"), package): return lex.lex( optimize=True, lextab=lextab, outputdir=caller_dir, reflags=reflags )
[docs] class ThreadSafeParser: """Wrap a parser produced by ``ply.yacc.yacc``. It provides a :meth:`parse` method that is thread-safe. """ def __init__(self, parser: LRParser) -> None: self.parser = parser self._lock = threading.RLock()
[docs] def parse(self, *args, **kwargs): """Run the wrapped parser, with a lock to ensure serialization.""" with self._lock: return self.parser.parse(*args, **kwargs)
[docs] def yacc(tabmodule: str, package: str) -> ThreadSafeParser: """Create a parser from local variables. It automatically compiles the parser in optimized mode, writing to ``tabmodule`` in the same directory as the calling file. This function is thread-safe, and the returned parser is also thread-safe, provided that it does not share a lexer with any other parser. It is only intended to work with parsers defined within the calling function, rather than at class or module scope. Parameters ---------- tabmodule : str Name for the file to write with the generated tables, if it does not already exist (without ``.py`` suffix). package : str Name of a test package which should be run with pytest to regenerate the output file. This is inserted into a comment in the generated file. """ from ply import yacc caller_dir = Path(yacc.get_caller_module_dict(2)["__file__"]).parent with _LOCK, _patch_ply_module(yacc, caller_dir / (tabmodule + ".py"), package): parser = yacc.yacc( tabmodule=tabmodule, outputdir=caller_dir, debug=False, optimize=True, write_tables=True, ) return ThreadSafeParser(parser)