diff --git a/DOCS.md b/DOCS.md index cf7f5ac6e..e72cd01bf 100644 --- a/DOCS.md +++ b/DOCS.md @@ -90,6 +90,7 @@ The full list of optional dependencies is: - `watch`: enables use of the `--watch` flag. - `mypy`: enables use of the `--mypy` flag. - `backports`: installs libraries that backport newer Python features to older versions, which Coconut will automatically use instead of the standard library if the standard library is not available. Specifically: + - Installs [`exceptiongroup`](https://pypi.org/project/exceptiongroup/) to backport [`ExceptionGroup` and `BaseExceptionGroup`](https://docs.python.org/3/library/exceptions.html#ExceptionGroup). - Installs [`dataclasses`](https://pypi.org/project/dataclasses/) to backport [`dataclasses`](https://docs.python.org/3/library/dataclasses.html). - Installs [`typing`](https://pypi.org/project/typing/) to backport [`typing`](https://docs.python.org/3/library/typing.html) ([`typing_extensions`](https://pypi.org/project/typing-extensions/) is always installed for backporting individual `typing` objects). - Installs [`aenum`](https://pypi.org/project/aenum) to backport [`enum`](https://docs.python.org/3/library/enum.html). diff --git a/Makefile b/Makefile index bf2008bc1..5ff886a84 100644 --- a/Makefile +++ b/Makefile @@ -52,6 +52,10 @@ setup-pypy3: install: setup python -m pip install -e .[tests] +.PHONY: install-purepy +install-purepy: setup + python -m pip install --no-deps --upgrade -e . "pyparsing<3" + .PHONY: install-py2 install-py2: setup-py2 python2 -m pip install -e .[tests] diff --git a/coconut/compiler/header.py b/coconut/compiler/header.py index 409929e50..b4b68ee78 100644 --- a/coconut/compiler/header.py +++ b/coconut/compiler/header.py @@ -934,7 +934,9 @@ def getheader(which, use_hash, target, no_tco, strict, no_wrap): newline=True, ).format(**format_dict) - if target_info >= (3, 9): + if target_info >= (3, 11): + header += _get_root_header("311") + elif target_info >= (3, 9): header += _get_root_header("39") if target_info >= (3, 7): header += _get_root_header("37") diff --git a/coconut/compiler/util.py b/coconut/compiler/util.py index 5efb771d8..9db8bf782 100644 --- a/coconut/compiler/util.py +++ b/coconut/compiler/util.py @@ -214,7 +214,7 @@ def evaluate_tokens(tokens, **kwargs): class ComputationNode(object): """A single node in the computation graph.""" - __slots__ = ("action", "original", "loc", "tokens") + (("been_called",) if DEVELOP else ()) + __slots__ = ("action", "original", "loc", "tokens") pprinting = False def __new__(cls, action, original, loc, tokens, ignore_no_tokens=False, ignore_one_token=False, greedy=False, trim_arity=True): @@ -236,8 +236,6 @@ def __new__(cls, action, original, loc, tokens, ignore_no_tokens=False, ignore_o self.original = original self.loc = loc self.tokens = tokens - if DEVELOP: - self.been_called = False if greedy: return self.evaluate() else: @@ -253,9 +251,9 @@ def name(self): def evaluate(self): """Get the result of evaluating the computation graph at this node. Very performance sensitive.""" - if DEVELOP: # avoid the overhead of the call if not develop - internal_assert(not self.been_called, "inefficient reevaluation of action " + self.name + " with tokens", self.tokens) - self.been_called = True + # note that this should never cache, since if a greedy Wrap that doesn't add to the packrat context + # hits the cache, it'll get the same ComputationNode object, but since it's greedy that object needs + # to actually be reevaluated evaluated_toks = evaluate_tokens(self.tokens) if logger.tracing: # avoid the overhead of the call if not tracing logger.log_trace(self.name, self.original, self.loc, evaluated_toks, self.tokens) diff --git a/coconut/constants.py b/coconut/constants.py index af3f4ce1b..1965516a2 100644 --- a/coconut/constants.py +++ b/coconut/constants.py @@ -583,7 +583,9 @@ def get_path_env_var(env_var, default): '__file__', '__annotations__', '__debug__', - # # don't include builtins that aren't always made available by Coconut: + # we treat these as coconut_exceptions so the highlighter will always know about them: + # 'ExceptionGroup', 'BaseExceptionGroup', + # don't include builtins that aren't always made available by Coconut: # 'BlockingIOError', 'ChildProcessError', 'ConnectionError', # 'BrokenPipeError', 'ConnectionAbortedError', 'ConnectionRefusedError', # 'ConnectionResetError', 'FileExistsError', 'FileNotFoundError', @@ -807,8 +809,11 @@ def get_path_env_var(env_var, default): coconut_exceptions = ( "MatchError", + "ExceptionGroup", + "BaseExceptionGroup", ) +highlight_builtins = coconut_specific_builtins + interp_only_builtins all_builtins = frozenset(python_builtins + coconut_specific_builtins + coconut_exceptions) magic_methods = ( @@ -938,6 +943,7 @@ def get_path_env_var(env_var, default): ("dataclasses", "py==36"), ("typing", "py<35"), ("async_generator", "py35"), + ("exceptiongroup", "py37"), ), "dev": ( ("pre-commit", "py3"), @@ -994,6 +1000,7 @@ def get_path_env_var(env_var, default): ("xonsh", "py38"): (0, 14), ("pytest", "py36"): (7,), ("async_generator", "py35"): (1, 10), + ("exceptiongroup", "py37"): (1,), # pinned reqs: (must be added to pinned_reqs below) diff --git a/coconut/highlighter.py b/coconut/highlighter.py index aef74f588..a12686a06 100644 --- a/coconut/highlighter.py +++ b/coconut/highlighter.py @@ -25,8 +25,7 @@ from pygments.util import shebang_matches from coconut.constants import ( - coconut_specific_builtins, - interp_only_builtins, + highlight_builtins, new_operators, tabideal, default_encoding, @@ -94,7 +93,7 @@ class CoconutLexer(Python3Lexer): (words(reserved_vars, prefix=r"(?= 1 for develop -DEVELOP = 8 +DEVELOP = 9 ALPHA = False # for pre releases rather than post releases assert DEVELOP is False or DEVELOP >= 1, "DEVELOP must be False or an int >= 1" @@ -45,6 +45,16 @@ def _indent(code, by=1, tabsize=4, strip=False, newline=False, initial_newline=F ) + ("\n" if newline else "") +def _get_target_info(target): + """Return target information as a version tuple.""" + if not target or target == "universal": + return () + elif len(target) == 1: + return (int(target),) + else: + return (int(target[0]), int(target[1:])) + + # ----------------------------------------------------------------------------------------------------------------------- # HEADER: # ----------------------------------------------------------------------------------------------------------------------- @@ -264,15 +274,24 @@ def _coconut_reduce_partial(self): _coconut_copy_reg.pickle(_coconut_functools.partial, _coconut_reduce_partial) ''' +_py3_before_py311_extras = '''try: + from exceptiongroup import ExceptionGroup, BaseExceptionGroup +except ImportError: + class you_need_to_install_exceptiongroup(object): + __slots__ = () + ExceptionGroup = BaseExceptionGroup = you_need_to_install_exceptiongroup() +''' + # whenever new versions are added here, header.py must be updated to use them ROOT_HEADER_VERSIONS = ( "universal", "2", - "3", "27", + "3", "37", "39", + "311", ) @@ -284,6 +303,7 @@ def _get_root_header(version="universal"): ''' + _indent(_get_root_header("2")) + '''else: ''' + _indent(_get_root_header("3")) + version_info = _get_target_info(version) header = "" if version.startswith("3"): @@ -293,7 +313,7 @@ def _get_root_header(version="universal"): # if a new assignment is added below, a new builtins import should be added alongside it header += _base_py2_header - if version in ("37", "39"): + if version_info >= (3, 7): header += r'''py_breakpoint = breakpoint ''' elif version == "3": @@ -311,7 +331,7 @@ def _get_root_header(version="universal"): header += r'''if _coconut_sys.version_info < (3, 7): ''' + _indent(_below_py37_extras) + r'''elif _coconut_sys.version_info < (3, 9): ''' + _indent(_py37_py38_extras) - elif version == "37": + elif (3, 7) <= version_info < (3, 9): header += r'''if _coconut_sys.version_info < (3, 9): ''' + _indent(_py37_py38_extras) elif version.startswith("2"): @@ -320,7 +340,11 @@ def _get_root_header(version="universal"): dict.items = _coconut_OrderedDict.viewitems ''' else: - assert version == "39", version + assert version_info >= (3, 9), version + + if (3,) <= version_info < (3, 11): + header += r'''if _coconut_sys.version_info < (3, 11): +''' + _indent(_py3_before_py311_extras) return header diff --git a/coconut/tests/src/cocotest/agnostic/specific.coco b/coconut/tests/src/cocotest/agnostic/specific.coco index 1a3b8ba6f..cbb1eefbe 100644 --- a/coconut/tests/src/cocotest/agnostic/specific.coco +++ b/coconut/tests/src/cocotest/agnostic/specific.coco @@ -2,7 +2,7 @@ from io import StringIO if TYPE_CHECKING: from typing import Any -from .util import mod # NOQA +from .util import mod, assert_raises # NOQA def non_py26_test() -> bool: @@ -181,6 +181,8 @@ def py37_spec_test() -> bool: class HasVarGen[*Ts] # type: ignore assert HasVarGen `issubclass` object assert typing.Protocol.__module__ == "typing_extensions" + assert_raises((def -> raise ExceptionGroup("derp", [Exception("herp")])), ExceptionGroup) + assert_raises((def -> raise BaseExceptionGroup("derp", [BaseException("herp")])), BaseExceptionGroup) return True diff --git a/coconut/util.py b/coconut/util.py index 62e2fcfa0..128c4afd3 100644 --- a/coconut/util.py +++ b/coconut/util.py @@ -39,6 +39,7 @@ except ImportError: lru_cache = None +from coconut.root import _get_target_info from coconut.constants import ( fixpath, default_encoding, @@ -265,6 +266,9 @@ def ensure_dir(dirpath): # ----------------------------------------------------------------------------------------------------------------------- +get_target_info = _get_target_info + + def ver_tuple_to_str(req_ver): """Converts a requirement version tuple into a version string.""" return ".".join(str(x) for x in req_ver) @@ -287,16 +291,6 @@ def get_next_version(req_ver, point_to_increment=-1): return req_ver[:point_to_increment] + (req_ver[point_to_increment] + 1,) -def get_target_info(target): - """Return target information as a version tuple.""" - if not target: - return () - elif len(target) == 1: - return (int(target),) - else: - return (int(target[0]), int(target[1:])) - - def get_displayable_target(target): """Get a displayable version of the target.""" try: