Skip to content

Commit

Permalink
Add find_packages, improve override
Browse files Browse the repository at this point in the history
Resolves   #798, #800.
  • Loading branch information
evhub committed Nov 5, 2023
1 parent 827c06c commit d1b1cde
Show file tree
Hide file tree
Showing 13 changed files with 164 additions and 24 deletions.
55 changes: 42 additions & 13 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/).

Expand Down Expand Up @@ -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`)
Expand All @@ -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`)
Expand All @@ -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.
Expand Down
47 changes: 43 additions & 4 deletions coconut/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -42,7 +44,6 @@
coconut_kernel_kwargs,
default_use_cache_dir,
coconut_cache_dir,
coconut_run_kwargs,
)

# -----------------------------------------------------------------------------------------------------------------------
Expand All @@ -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 = {
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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
7 changes: 7 additions & 0 deletions coconut/api.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -50,6 +52,8 @@ def cmd(
"""Process command-line arguments."""
...

cmd_sys = cmd


VERSIONS: Dict[Text, Text] = ...

Expand Down Expand Up @@ -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
10 changes: 8 additions & 2 deletions coconut/command/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
)
Expand Down Expand Up @@ -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."""
Expand Down
5 changes: 4 additions & 1 deletion coconut/command/command.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions coconut/compiler/templates/header.py_template
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
4 changes: 3 additions & 1 deletion coconut/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -902,6 +902,7 @@ def get_path_env_var(env_var, default):
("async_generator", "py35"),
("exceptiongroup", "py37;py<311"),
("anyio", "py36"),
"setuptools",
),
"cpython": (
"cPyparsing",
Expand Down Expand Up @@ -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),
Expand Down
3 changes: 1 addition & 2 deletions coconut/integrations.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

from coconut.constants import (
coconut_kernel_kwargs,
coconut_run_kwargs,
enabled_xonsh_modes,
interpreter_uses_incremental,
)
Expand Down Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion coconut/root.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
28 changes: 28 additions & 0 deletions coconut/tests/main_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -797,6 +819,12 @@ def test_import_hook(self):
reload(runnable)
assert runnable.success == "<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()

Expand Down
2 changes: 2 additions & 0 deletions coconut/tests/src/cocotest/agnostic/suite.coco
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 14 additions & 0 deletions coconut/tests/src/cocotest/agnostic/util.coco
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions coconut/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down

0 comments on commit d1b1cde

Please sign in to comment.