From d1b1cde1dd6736d206514e0dc1ddb0e29d6c5df2 Mon Sep 17 00:00:00 2001 From: Evan Hubinger Date: Sun, 5 Nov 2023 00:51:20 -0700 Subject: [PATCH] Add find_packages, improve override Resolves #798, #800. --- DOCS.md | 55 ++++++++++++++----- coconut/api.py | 47 ++++++++++++++-- coconut/api.pyi | 7 +++ coconut/command/command.py | 10 +++- coconut/command/command.pyi | 5 +- coconut/compiler/templates/header.py_template | 6 ++ coconut/constants.py | 4 +- coconut/integrations.py | 3 +- coconut/root.py | 2 +- coconut/tests/main_test.py | 28 ++++++++++ .../tests/src/cocotest/agnostic/suite.coco | 2 + coconut/tests/src/cocotest/agnostic/util.coco | 14 +++++ coconut/util.py | 5 ++ 13 files changed, 164 insertions(+), 24 deletions(-) diff --git a/DOCS.md b/DOCS.md index a745d5f50..b3fa538cf 100644 --- a/DOCS.md +++ b/DOCS.md @@ -3106,7 +3106,7 @@ def fib(n): **override**(_func_) -Coconut provides the `@override` decorator to allow declaring a method definition in a subclass as an override of some parent class method. When `@override` is used on a method, if a method of the same name does not exist on some parent class, the class definition will raise a `RuntimeError`. +Coconut provides the `@override` decorator to allow declaring a method definition in a subclass as an override of some parent class method. When `@override` is used on a method, if a method of the same name does not exist on some parent class, the class definition will raise a `RuntimeError`. `@override` works with other decorators such as `@classmethod` and `@staticmethod`, but only if `@override` is the outer-most decorator. Additionally, `override` will present to type checkers as [`typing_extensions.override`](https://pypi.org/project/typing-extensions/). @@ -4672,6 +4672,12 @@ Executes the given _args_ as if they were fed to `coconut` on the command-line, Has the same effect of setting the command-line flags on the given _state_ object as `setup` (with the global `state` object used when _state_ is `False`). +#### `cmd_sys` + +**coconut.api.cmd_sys**(_args_=`None`, *, _argv_=`None`, _interact_=`False`, _default\_target_=`"sys"`, _state_=`False`) + +Same as `coconut.api.cmd` but _default\_target_ is `"sys"` rather than `None` (universal). + #### `coconut_exec` **coconut.api.coconut_exec**(_expression_, _globals_=`None`, _locals_=`None`, _state_=`False`, _keep\_internal\_state_=`None`) @@ -4684,18 +4690,6 @@ Version of [`exec`](https://docs.python.org/3/library/functions.html#exec) which Version of [`eval`](https://docs.python.org/3/library/functions.html#eval) which can evaluate Coconut code. -#### `version` - -**coconut.api.version**(**[**_which_**]**) - -Retrieves a string containing information about the Coconut version. The optional argument _which_ is the type of version information desired. Possible values of _which_ are: - -- `"num"`: the numerical version (the default) -- `"name"`: the version codename -- `"spec"`: the numerical version with the codename attached -- `"tag"`: the version tag used in GitHub and documentation URLs -- `"-v"`: the full string printed by `coconut -v` - #### `auto_compilation` **coconut.api.auto_compilation**(_on_=`True`, _args_=`None`, _use\_cache\_dir_=`None`) @@ -4712,6 +4706,41 @@ If _use\_cache\_dir_ is passed, it will turn on or off the usage of a `__coconut Switches the [`breakpoint` built-in](https://www.python.org/dev/peps/pep-0553/) which Coconut makes universally available to use [`coconut.embed`](#coconut-embed) instead of [`pdb.set_trace`](https://docs.python.org/3/library/pdb.html#pdb.set_trace) (or undoes that switch if `on=False`). This function is called automatically when `coconut.api` is imported. +#### `find_and_compile_packages` + +**coconut.api.find_and_compile_packages**(_where_=`"."`, _exclude_=`()`, _include_=`("*",)`) + +Behaves similarly to [`setuptools.find_packages`](https://setuptools.pypa.io/en/latest/userguide/quickstart.html#package-discovery) except that it finds Coconut packages rather than Python packages, and compiles any Coconut packages that it finds in-place. + +Note that if you want to use `find_and_compile_packages` in your `setup.py`, you'll need to include `coconut` as a [build-time dependency in your `pyproject.toml`](https://pip.pypa.io/en/stable/reference/build-system/pyproject-toml/#build-time-dependencies). + +##### Example + +```coconut_python +# if you put this in your setup.py, your Coconut package will be compiled in-place whenever it is installed + +from setuptools import setup +from coconut.api import find_and_compile_packages + +setup( + name=..., + version=..., + packages=find_and_compile_packages(), +) +``` + +#### `version` + +**coconut.api.version**(**[**_which_**]**) + +Retrieves a string containing information about the Coconut version. The optional argument _which_ is the type of version information desired. Possible values of _which_ are: + +- `"num"`: the numerical version (the default) +- `"name"`: the version codename +- `"spec"`: the numerical version with the codename attached +- `"tag"`: the version tag used in GitHub and documentation URLs +- `"-v"`: the full string printed by `coconut -v` + #### `CoconutException` If an error is encountered in a api function, a `CoconutException` instance may be raised. `coconut.api.CoconutException` is provided to allow catching such errors. diff --git a/coconut/api.py b/coconut/api.py index c8a8bb995..71d74773e 100644 --- a/coconut/api.py +++ b/coconut/api.py @@ -23,15 +23,17 @@ import os.path import codecs from functools import partial +from setuptools import PackageFinder try: from encodings import utf_8 except ImportError: utf_8 = None from coconut.root import _coconut_exec +from coconut.util import override from coconut.integrations import embed from coconut.exceptions import CoconutException -from coconut.command import Command +from coconut.command.command import Command from coconut.command.cli import cli_version from coconut.command.util import proc_run_args from coconut.compiler import Compiler @@ -42,7 +44,6 @@ coconut_kernel_kwargs, default_use_cache_dir, coconut_cache_dir, - coconut_run_kwargs, ) # ----------------------------------------------------------------------------------------------------------------------- @@ -68,9 +69,16 @@ def get_state(state=None): def cmd(cmd_args, **kwargs): """Process command-line arguments.""" state = kwargs.pop("state", False) + cmd_func = kwargs.pop("_cmd_func", "cmd") if isinstance(cmd_args, (str, bytes)): cmd_args = cmd_args.split() - return get_state(state).cmd(cmd_args, **kwargs) + return getattr(get_state(state), cmd_func)(cmd_args, **kwargs) + + +def cmd_sys(*args, **kwargs): + """Same as api.cmd() but defaults to --target sys.""" + kwargs["_cmd_func"] = "cmd_sys" + return cmd(*args, **kwargs) VERSIONS = { @@ -214,7 +222,7 @@ def cmd(self, *args): """Run the Coconut compiler with the given args.""" if self.command is None: self.command = Command() - return self.command.cmd(list(args) + self.args, interact=False, **coconut_run_kwargs) + return self.command.cmd_sys(list(args) + self.args, interact=False) def compile(self, path, package): """Compile a path to a file or package.""" @@ -315,6 +323,7 @@ def compile_coconut(cls, source): cls.coconut_compiler = Compiler(**coconut_kernel_kwargs) return cls.coconut_compiler.parse_sys(source) + @override @classmethod def decode(cls, input_bytes, errors="strict"): """Decode and compile the given Coconut source bytes.""" @@ -347,3 +356,33 @@ def get_coconut_encoding(encoding="coconut"): codecs.register(get_coconut_encoding) + + +# ----------------------------------------------------------------------------------------------------------------------- +# SETUPTOOLS: +# ----------------------------------------------------------------------------------------------------------------------- + +class CoconutPackageFinder(PackageFinder, object): + + _coconut_command = None + + @classmethod + def _coconut_compile(cls, path): + """Run the Coconut compiler with the given args.""" + if cls._coconut_command is None: + cls._coconut_command = Command() + return cls._coconut_command.cmd_sys([path], interact=False) + + @override + @classmethod + def _looks_like_package(cls, path, _package_name): + is_coconut_package = any( + os.path.isfile(os.path.join(path, "__init__" + ext)) + for ext in code_exts + ) + if is_coconut_package: + cls._coconut_compile(path) + return is_coconut_package + + +find_and_compile_packages = CoconutPackageFinder.find diff --git a/coconut/api.pyi b/coconut/api.pyi index 97f6fbf80..5078d8206 100644 --- a/coconut/api.pyi +++ b/coconut/api.pyi @@ -21,6 +21,8 @@ from typing import ( Text, ) +from setuptools import find_packages as _find_packages + from coconut.command.command import Command class CoconutException(Exception): @@ -50,6 +52,8 @@ def cmd( """Process command-line arguments.""" ... +cmd_sys = cmd + VERSIONS: Dict[Text, Text] = ... @@ -150,3 +154,6 @@ def auto_compilation( def get_coconut_encoding(encoding: Text = ...) -> Any: """Get a CodecInfo for the given Coconut encoding.""" ... + + +find_and_compile_packages = _find_packages diff --git a/coconut/command/command.py b/coconut/command/command.py index 9848c7fc1..d427fc79e 100644 --- a/coconut/command/command.py +++ b/coconut/command/command.py @@ -72,7 +72,7 @@ create_package_retries, default_use_cache_dir, coconut_cache_dir, - coconut_run_kwargs, + coconut_sys_kwargs, interpreter_uses_incremental, disable_incremental_for_len, ) @@ -165,10 +165,16 @@ def start(self, run=False): dest = os.path.join(os.path.dirname(source), coconut_cache_dir) else: dest = os.path.join(source, coconut_cache_dir) - self.cmd(args, argv=argv, use_dest=dest, **coconut_run_kwargs) + self.cmd_sys(args, argv=argv, use_dest=dest) else: self.cmd() + def cmd_sys(self, *args, **in_kwargs): + """Same as .cmd(), but uses defaults from coconut_sys_kwargs.""" + out_kwargs = coconut_sys_kwargs.copy() + out_kwargs.update(in_kwargs) + return self.cmd(*args, **out_kwargs) + # new external parameters should be updated in api.pyi and DOCS def cmd(self, args=None, argv=None, interact=True, default_target=None, use_dest=None): """Process command-line arguments.""" diff --git a/coconut/command/command.pyi b/coconut/command/command.pyi index 3f1d4ba40..f69b9ec2b 100644 --- a/coconut/command/command.pyi +++ b/coconut/command/command.pyi @@ -15,7 +15,10 @@ Description: MyPy stub file for command.py. # MAIN: # ----------------------------------------------------------------------------------------------------------------------- +from typing import Callable + class Command: """Coconut command-line interface.""" - ... + cmd: Callable + cmd_sys: Callable diff --git a/coconut/compiler/templates/header.py_template b/coconut/compiler/templates/header.py_template index dc3ffff00..3a742eee9 100644 --- a/coconut/compiler/templates/header.py_template +++ b/coconut/compiler/templates/header.py_template @@ -1649,6 +1649,12 @@ class override(_coconut_baseclass): def __init__(self, func): self.func = func def __get__(self, obj, objtype=None): + self_func_get = _coconut.getattr(self.func, "__get__", None) + if self_func_get is not None: + if objtype is None: + return self_func_get(obj) + else: + return self_func_get(obj, objtype) if obj is None: return self.func {return_method_of_self_func} diff --git a/coconut/constants.py b/coconut/constants.py index 66717a961..8b7399e8d 100644 --- a/coconut/constants.py +++ b/coconut/constants.py @@ -669,7 +669,7 @@ def get_path_env_var(env_var, default): # always use atomic --xxx=yyy rather than --xxx yyy # and don't include --run, --quiet, or --target as they're added separately coconut_base_run_args = ("--keep-lines",) -coconut_run_kwargs = dict(default_target="sys") # passed to Command.cmd +coconut_sys_kwargs = dict(default_target="sys") # passed to Command.cmd default_mypy_args = ( "--pretty", @@ -902,6 +902,7 @@ def get_path_env_var(env_var, default): ("async_generator", "py35"), ("exceptiongroup", "py37;py<311"), ("anyio", "py36"), + "setuptools", ), "cpython": ( "cPyparsing", @@ -1043,6 +1044,7 @@ def get_path_env_var(env_var, default): # don't upgrade this; it breaks on Python 3.4 ("pygments", "py<39"): (2, 3), # don't upgrade these; they break on Python 2 + "setuptools": (44,), ("jupyter-client", "py<35"): (5, 3), ("pywinpty", "py<3;windows"): (0, 5), ("jupyter-console", "py<35"): (5, 2), diff --git a/coconut/integrations.py b/coconut/integrations.py index f2a3537ee..6ca1c377c 100644 --- a/coconut/integrations.py +++ b/coconut/integrations.py @@ -23,7 +23,6 @@ from coconut.constants import ( coconut_kernel_kwargs, - coconut_run_kwargs, enabled_xonsh_modes, interpreter_uses_incremental, ) @@ -77,7 +76,7 @@ def magic(line, cell=None): # first line in block is cmd, rest is code line = line.strip() if line: - api.cmd(line, state=magic_state, **coconut_run_kwargs) + api.cmd_sys(line, state=magic_state) code = cell compiled = api.parse(code, state=magic_state) except CoconutException: diff --git a/coconut/root.py b/coconut/root.py index 1e9792141..e6538d3ed 100644 --- a/coconut/root.py +++ b/coconut/root.py @@ -26,7 +26,7 @@ VERSION = "3.0.3" VERSION_NAME = None # False for release, int >= 1 for develop -DEVELOP = 19 +DEVELOP = 20 ALPHA = False # for pre releases rather than post releases assert DEVELOP is False or DEVELOP >= 1, "DEVELOP must be False or an int >= 1" diff --git a/coconut/tests/main_test.py b/coconut/tests/main_test.py index 6c5b44701..5f6ec7b30 100644 --- a/coconut/tests/main_test.py +++ b/coconut/tests/main_test.py @@ -105,6 +105,8 @@ additional_dest = os.path.join(base, "dest", "additional_dest") src_cache_dir = os.path.join(src, coconut_cache_dir) +cocotest_dir = os.path.join(src, "cocotest") +agnostic_dir = os.path.join(cocotest_dir, "agnostic") runnable_coco = os.path.join(src, "runnable.coco") runnable_py = os.path.join(src, "runnable.py") @@ -472,6 +474,26 @@ def using_coconut(fresh_logger=True, fresh_api=False): logger.copy_from(saved_logger) +def remove_pys_in(dirpath): + removed_pys = 0 + for fname in os.listdir(dirpath): + if fname.endswith(".py"): + rm_path(os.path.join(dirpath, fname)) + removed_pys += 1 + return removed_pys + + +@contextmanager +def using_pys_in(dirpath): + """Remove *.py in dirpath at start and finish.""" + remove_pys_in(dirpath) + try: + yield + finally: + removed_pys = remove_pys_in(dirpath) + assert removed_pys > 0, os.listdir(dirpath) + + @contextmanager def using_sys_path(path, prepend=False): """Adds a path to sys.path.""" @@ -797,6 +819,12 @@ def test_import_hook(self): reload(runnable) assert runnable.success == "" + def test_find_packages(self): + with using_pys_in(agnostic_dir): + with using_coconut(): + from coconut.api import find_and_compile_packages + assert find_and_compile_packages(cocotest_dir) == ["agnostic"] + def test_runnable(self): run_runnable() diff --git a/coconut/tests/src/cocotest/agnostic/suite.coco b/coconut/tests/src/cocotest/agnostic/suite.coco index 569418ee2..beba7d22d 100644 --- a/coconut/tests/src/cocotest/agnostic/suite.coco +++ b/coconut/tests/src/cocotest/agnostic/suite.coco @@ -1066,6 +1066,8 @@ forward 2""") == 900 assert safe_raise_exc(IOError).handle(IOError, const 10).unwrap() == 10 == safe_raise_exc(IOError).expect_error(IOError).result_or(10) assert pickle_round_trip(ident$(1))() == 1 == pickle_round_trip(ident$(x=?))(1) assert x_or_y(x=1) == (1, 1) == x_or_y(y=1) + assert DerivedWithMeths().cls_meth() + assert DerivedWithMeths().static_meth() with process_map.multiple_sequential_calls(): # type: ignore assert process_map(tuple <.. (|>)$(to_sort), qsorts) |> list == [to_sort |> sorted |> tuple] * len(qsorts) diff --git a/coconut/tests/src/cocotest/agnostic/util.coco b/coconut/tests/src/cocotest/agnostic/util.coco index ae0a9bfef..517168152 100644 --- a/coconut/tests/src/cocotest/agnostic/util.coco +++ b/coconut/tests/src/cocotest/agnostic/util.coco @@ -881,6 +881,20 @@ class inh_inh_A(inh_A): @override def true(self) = False +class BaseWithMeths: + @classmethod + def cls_meth(cls) = False + @staticmethod + def static_meth() = False + +class DerivedWithMeths(BaseWithMeths): + @override + @classmethod + def cls_meth(cls) = True + @override + @staticmethod + def static_meth() = True + class MyExc(Exception): def __init__(self, m): super().__init__(m) diff --git a/coconut/util.py b/coconut/util.py index 739645214..a5d68f39c 100644 --- a/coconut/util.py +++ b/coconut/util.py @@ -107,6 +107,11 @@ def __init__(self, func): self.func = func def __get__(self, obj, objtype=None): + if hasattr(self.func, "__get__"): + if objtype is None: + return self.func.__get__(obj) + else: + return self.func.__get__(obj, objtype) if obj is None: return self.func if PY2: