Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release v3.0.1 #747

Merged
merged 29 commits into from
May 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 52 additions & 4 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ pip install coconut-develop
```
which will install the most recent working version from Coconut's [`develop` branch](https://github.com/evhub/coconut/tree/develop). Optional dependency installation is supported in the same manner as above. For more information on the current development build, check out the [development version of this documentation](http://coconut.readthedocs.io/en/develop/DOCS.html). Be warned: `coconut-develop` is likely to be unstable—if you find a bug, please report it by [creating a new issue](https://github.com/evhub/coconut/issues/new).

_Note: if you have an existing release version of `coconut` installed, you'll need to `pip uninstall coconut` before installing `coconut-develop`._

## Compilation

```{contents}
Expand Down Expand Up @@ -142,7 +144,7 @@ dest destination directory for compiled files (defaults to
```
-h, --help show this help message and exit
--and source [dest ...]
add an additional source/dest pair to compile
add an additional source/dest pair to compile (dest is optional)
-v, -V, --version print Coconut and Python version information
-t version, --target version
specify target Python version (defaults to universal)
Expand Down Expand Up @@ -726,6 +728,8 @@ The `..` operator has lower precedence than `::` but higher precedence than infi

All function composition operators also have in-place versions (e.g. `..=`).

Since all forms of function composition always call the first function in the composition (`f` in `f ..> g` and `g` in `f <.. g`) with exactly the arguments passed into the composition, all forms of function composition will preserve all metadata attached to the first function in the composition, including the function's [signature](https://docs.python.org/3/library/inspect.html#inspect.signature) and any of that function's attributes.

##### Example

**Coconut:**
Expand All @@ -747,7 +751,7 @@ Coconut uses a `$` sign right after an iterator before a slice to perform iterat

Iterator slicing works just like sequence slicing, including support for negative indices and slices, and support for `slice` objects in the same way as can be done with normal slicing. Iterator slicing makes no guarantee, however, that the original iterator passed to it be preserved (to preserve the iterator, use Coconut's [`reiterable`](#reiterable) built-in).

Coconut's iterator slicing is very similar to Python's `itertools.islice`, but unlike `itertools.islice`, Coconut's iterator slicing supports negative indices, and will preferentially call an object's `__iter_getitem__` (always used if available) or `__getitem__` (only used if the object is a collections.abc.Sequence). Coconut's iterator slicing is also optimized to work well with all of Coconut's built-in objects, only computing the elements of each that are actually necessary to extract the desired slice.
Coconut's iterator slicing is very similar to Python's `itertools.islice`, but unlike `itertools.islice`, Coconut's iterator slicing supports negative indices, and will preferentially call an object's `__iter_getitem__` (always used if available) or `__getitem__` (only used if the object is a `collections.abc.Sequence`). Coconut's iterator slicing is also optimized to work well with all of Coconut's built-in objects, only computing the elements of each that are actually necessary to extract the desired slice.

##### Example

Expand Down Expand Up @@ -874,6 +878,8 @@ Custom operators will often need to be surrounded by whitespace (or parentheses

If a custom operator that is also a valid name is desired, you can use a backslash before the name to get back the name instead using Coconut's [keyword/variable disambiguation syntax](#handling-keywordvariable-name-overlap).

_Note: redefining existing Coconut operators using custom operator definition syntax is forbidden, including Coconut's built-in [Unicode operator alternatives](#unicode-alternatives)._

##### Examples

**Coconut:**
Expand Down Expand Up @@ -1030,6 +1036,8 @@ class CanAddAndSub(Protocol, Generic[T, U, V]):

Coconut supports Unicode alternatives to many different operator symbols. The Unicode alternatives are relatively straightforward, and chosen to reflect the look and/or meaning of the original symbol.

_Note: these are only the default, built-in unicode operators. Coconut supports [custom operator definition](#custom-operators) to define your own._

##### Full List

```
Expand Down Expand Up @@ -2057,6 +2065,41 @@ print(p1(5))
quad = 5 * x**2 + 3 * x + 1
```

### Keyword Argument Name Elision

When passing in long variable names as keyword arguments of the same name, Coconut supports the syntax
```
f(...=long_variable_name)
```
as a shorthand for
```
f(long_variable_name=long_variable_name)
```

Such syntax is also supported in [partial application](#partial-application) and [anonymous `namedtuple`s](#anonymous-namedtuples).

##### Example

**Coconut:**
```coconut
really_long_variable_name_1 = get_1()
really_long_variable_name_2 = get_2()
main_func(
...=really_long_variable_name_1,
...=really_long_variable_name_2,
)
```

**Python:**
```coconut_python
really_long_variable_name_1 = get_1()
really_long_variable_name_2 = get_2()
main_func(
really_long_variable_name_1=really_long_variable_name_1,
really_long_variable_name_2=really_long_variable_name_2,
)
```

### Anonymous Namedtuples

Coconut supports anonymous [`namedtuple`](https://docs.python.org/3/library/collections.html#collections.namedtuple) literals, such that `(a=1, b=2)` can be used just as `(1, 2)`, but with added names. Anonymous `namedtuple`s are always pickleable.
Expand All @@ -2067,6 +2110,8 @@ The syntax for anonymous namedtuple literals is:
```
where, if `<type>` is given for any field, [`typing.NamedTuple`](https://docs.python.org/3/library/typing.html#typing.NamedTuple) is used instead of `collections.namedtuple`.

Anonymous `namedtuple`s also support [keyword argument name elision](#keyword-argument-name-elision).

##### `_namedtuple_of`

On Python versions `>=3.6`, `_namedtuple_of` is provided as a built-in that can mimic the behavior of anonymous namedtuple literals such that `_namedtuple_of(a=1, b=2)` is equivalent to `(a=1, b=2)`. Since `_namedtuple_of` is only available on Python 3.6 and above, however, it is generally recommended to use anonymous namedtuple literals instead, as they work on any Python version.
Expand Down Expand Up @@ -3013,6 +3058,7 @@ The new methods provided by `multiset` on top of `collections.Counter` are:
- multiset.**isdisjoint**(_other_): Return True if two multisets have a null intersection.
- multiset.**\_\_xor\_\_**(_other_): Return the symmetric difference of two multisets as a new multiset. Specifically: `a ^ b = (a - b) | (b - a)`
- multiset.**count**(_item_): Return the number of times an element occurs in a multiset. Equivalent to `multiset[item]`, but additionally verifies the count is non-negative.
- multiset.**\_\_fmap\_\_**(_func_): Apply a function to the contents of the multiset, preserving counts; magic method for [`fmap`](#fmap).

Coconut also ensures that `multiset` supports [rich comparisons and `Counter.total()`](https://docs.python.org/3/library/collections.html#collections.Counter) on all Python versions.

Expand Down Expand Up @@ -3158,9 +3204,9 @@ _Can't be done without a series of method definitions for each data type. See th

#### `fmap`

**fmap**(_func_, _obj_, *, _starmap\_over\_mappings_=`False`)
**fmap**(_func_, _obj_)

In Haskell, `fmap(func, obj)` takes a data type `obj` and returns a new data type with `func` mapped over the contents. Coconut's `fmap` function does the exact same thing in Coconut.
In Haskell, `fmap(func, obj)` takes a data type `obj` and returns a new data type with `func` mapped over the contents. Coconut's `fmap` function does the exact same thing for Coconut's [data types](#data).

`fmap` can also be used on built-ins such as `str`, `list`, `set`, and `dict` as a variant of `map` that returns back an object of the same type. The behavior of `fmap` for a given object can be overridden by defining an `__fmap__(self, func)` magic method that will be called whenever `fmap` is invoked on that object. Note that `__fmap__` implementations should always satisfy the [Functor Laws](https://wiki.haskell.org/Functor).

Expand All @@ -3178,6 +3224,8 @@ async def fmap_over_async_iters(func, async_iter):
```
such that `fmap` can effectively be used as an async map.

_DEPRECATED: `fmap(func, obj, fallback_to_init=True)` will fall back to `obj.__class__(map(func, obj))` if no `fmap` implementation is available rather than raise `TypeError`._

##### Example

**Coconut:**
Expand Down
23 changes: 17 additions & 6 deletions __coconut__/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -169,23 +169,29 @@ enumerate = enumerate

_coconut_py_str = py_str
_coconut_super = super
_coconut_enumerate = enumerate
_coconut_filter = filter
_coconut_range = range
_coconut_reversed = reversed
_coconut_zip = zip


zip_longest = _coconut.zip_longest
memoize = _lru_cache


reduce = _coconut.functools.reduce
takewhile = _coconut.itertools.takewhile
dropwhile = _coconut.itertools.dropwhile
tee = _coconut_tee = _coconut.itertools.tee
starmap = _coconut_starmap = _coconut.itertools.starmap
tee = _coconut.itertools.tee
starmap = _coconut.itertools.starmap
cartesian_product = _coconut.itertools.product
multiset = _coconut_multiset = _coconut.collections.Counter

multiset = _coconut.collections.Counter

_coconut_tee = tee
_coconut_starmap = starmap
_coconut_cartesian_product = cartesian_product
_coconut_multiset = multiset


parallel_map = concurrent_map = _coconut_map = map


Expand All @@ -200,6 +206,7 @@ def scan(
iterable: _t.Iterable[_U],
initial: _T = ...,
) -> _t.Iterable[_T]: ...
_coconut_scan = scan


class MatchError(Exception):
Expand Down Expand Up @@ -968,6 +975,7 @@ class cycle(_t.Iterable[_T]):
def __fmap__(self, func: _t.Callable[[_T], _U]) -> _t.Iterable[_U]: ...
def __copy__(self) -> cycle[_T]: ...
def __len__(self) -> int: ...
_coconut_cycle = cycle


class groupsof(_t.Generic[_T]):
Expand All @@ -981,6 +989,7 @@ class groupsof(_t.Generic[_T]):
def __copy__(self) -> groupsof[_T]: ...
def __len__(self) -> int: ...
def __fmap__(self, func: _t.Callable[[_t.Tuple[_T, ...]], _U]) -> _t.Iterable[_U]: ...
_coconut_groupsof = groupsof


class windowsof(_t.Generic[_T]):
Expand All @@ -996,6 +1005,7 @@ class windowsof(_t.Generic[_T]):
def __copy__(self) -> windowsof[_T]: ...
def __len__(self) -> int: ...
def __fmap__(self, func: _t.Callable[[_t.Tuple[_T, ...]], _U]) -> _t.Iterable[_U]: ...
_coconut_windowsof = windowsof


class flatten(_t.Iterable[_T]):
Expand Down Expand Up @@ -1228,6 +1238,7 @@ def lift(func: _t.Callable[[_T, _U], _W]) -> _coconut_lifted_2[_T, _U, _W]: ...
def lift(func: _t.Callable[[_T, _U, _V], _W]) -> _coconut_lifted_3[_T, _U, _V, _W]: ...
@_t.overload
def lift(func: _t.Callable[..., _W]) -> _t.Callable[..., _t.Callable[..., _W]]: ...
_coconut_lift = lift


def all_equal(iterable: _Iterable) -> bool: ...
Expand Down
1 change: 1 addition & 0 deletions _coconut/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ pandas_numpy_modules: _t.Any = ...
jax_numpy_modules: _t.Any = ...
tee_type: _t.Any = ...
reiterables: _t.Any = ...
fmappables: _t.Any = ...

Ellipsis = Ellipsis
NotImplemented = NotImplemented
Expand Down
2 changes: 1 addition & 1 deletion coconut/command/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
type=str,
nargs="+",
action="append",
help="add an additional source/dest pair to compile",
help="add an additional source/dest pair to compile (dest is optional)",
)

arguments.add_argument(
Expand Down
62 changes: 40 additions & 22 deletions coconut/command/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import os
import time
import shutil
import random
from contextlib import contextmanager
from subprocess import CalledProcessError

Expand Down Expand Up @@ -68,6 +69,7 @@
error_color_code,
jupyter_console_commands,
default_jobs,
create_package_retries,
)
from coconut.util import (
univ_open,
Expand Down Expand Up @@ -96,6 +98,8 @@
can_parse,
invert_mypy_arg,
run_with_stack_size,
memoized_isdir,
memoized_isfile,
)
from coconut.compiler.util import (
should_indent,
Expand Down Expand Up @@ -293,16 +297,17 @@ def execute_args(self, args, interact=True, original_args=None):
raise CoconutException("cannot compile with --no-write when using --mypy")

# process all source, dest pairs
src_dest_package_triples = [
self.process_source_dest(src, dst, args)
for src, dst in (
[(args.source, args.dest)]
+ (getattr(args, "and") or [])
)
]
src_dest_package_triples = []
for and_args in [(args.source, args.dest)] + (getattr(args, "and") or []):
if len(and_args) == 1:
src, = and_args
dest = None
else:
src, dest = and_args
src_dest_package_triples.append(self.process_source_dest(src, dest, args))

# disable jobs if we know we're only compiling one file
if len(src_dest_package_triples) <= 1 and not any(package for _, _, package in src_dest_package_triples):
if len(src_dest_package_triples) <= 1 and not any(memoized_isdir(source) for source, dest, package in src_dest_package_triples):
self.disable_jobs()

# do compilation
Expand Down Expand Up @@ -363,12 +368,12 @@ def process_source_dest(self, source, dest, args):
processed_source = fixpath(source)

# validate args
if (args.run or args.interact) and os.path.isdir(processed_source):
if (args.run or args.interact) and memoized_isdir(processed_source):
if args.run:
raise CoconutException("source path %r must point to file not directory when --run is enabled" % (source,))
if args.interact:
raise CoconutException("source path %r must point to file not directory when --run (implied by --interact) is enabled" % (source,))
if args.watch and os.path.isfile(processed_source):
if args.watch and memoized_isfile(processed_source):
raise CoconutException("source path %r must point to directory not file when --watch is enabled" % (source,))

# determine dest
Expand All @@ -389,9 +394,9 @@ def process_source_dest(self, source, dest, args):
package = False
else:
# auto-decide package
if os.path.isfile(source):
if memoized_isfile(processed_source):
package = False
elif os.path.isdir(source):
elif memoized_isdir(processed_source):
package = True
else:
raise CoconutException("could not find source path", source)
Expand Down Expand Up @@ -442,17 +447,17 @@ def compile_path(self, path, write=True, package=True, **kwargs):
"""Compile a path and returns paths to compiled files."""
if not isinstance(write, bool):
write = fixpath(write)
if os.path.isfile(path):
if memoized_isfile(path):
destpath = self.compile_file(path, write, package, **kwargs)
return [destpath] if destpath is not None else []
elif os.path.isdir(path):
elif memoized_isdir(path):
return self.compile_folder(path, write, package, **kwargs)
else:
raise CoconutException("could not find source path", path)

def compile_folder(self, directory, write=True, package=True, **kwargs):
"""Compile a directory and returns paths to compiled files."""
if not isinstance(write, bool) and os.path.isfile(write):
if not isinstance(write, bool) and memoized_isfile(write):
raise CoconutException("destination path cannot point to a file when compiling a directory")
filepaths = []
for dirpath, dirnames, filenames in os.walk(directory):
Expand Down Expand Up @@ -581,11 +586,21 @@ def get_package_level(self, codepath):
return package_level
return 0

def create_package(self, dirpath):
def create_package(self, dirpath, retries_left=create_package_retries):
"""Set up a package directory."""
filepath = os.path.join(dirpath, "__coconut__.py")
with univ_open(filepath, "w") as opened:
writefile(opened, self.comp.getheader("__coconut__"))
try:
with univ_open(filepath, "w") as opened:
writefile(opened, self.comp.getheader("__coconut__"))
except OSError:
logger.log_exc()
if retries_left <= 0:
logger.warn("Failed to write header file at", filepath)
else:
# sleep a random amount of time from 0 to 0.1 seconds to
# stagger calls across processes
time.sleep(random.random() / 10)
self.create_package(dirpath, retries_left - 1)

def submit_comp_job(self, path, callback, method, *args, **kwargs):
"""Submits a job on self.comp to be run in parallel."""
Expand Down Expand Up @@ -660,7 +675,7 @@ def running_jobs(self, exit_on_error=True):

def has_hash_of(self, destpath, code, package_level):
"""Determine if a file has the hash of the code."""
if destpath is not None and os.path.isfile(destpath):
if destpath is not None and memoized_isfile(destpath):
with univ_open(destpath, "r") as opened:
compiled = readfile(opened)
hashash = gethash(compiled)
Expand Down Expand Up @@ -969,7 +984,10 @@ def start_jupyter(self, args):

# pass the kernel to the console or otherwise just launch Jupyter now that we know our kernel is available
if args[0] in jupyter_console_commands:
args += ["--kernel", kernel]
if any(a.startswith("--kernel") for a in args):
logger.warn("unable to specify Coconut kernel in 'jupyter " + args[0] + "' command as --kernel was already specified in the given arguments")
else:
args += ["--kernel", kernel]
run_args = jupyter + args

if newly_installed_kernels:
Expand All @@ -989,7 +1007,7 @@ def watch(self, src_dest_package_triples, run=False, force=False):

def recompile(path, src, dest, package):
path = fixpath(path)
if os.path.isfile(path) and os.path.splitext(path)[1] in code_exts:
if memoized_isfile(path) and os.path.splitext(path)[1] in code_exts:
with self.handling_exceptions():
if dest is True or dest is None:
writedir = dest
Expand Down Expand Up @@ -1043,7 +1061,7 @@ def site_uninstall(self):
python_lib = self.get_python_lib()
pth_file = os.path.join(python_lib, os.path.basename(coconut_pth_file))

if os.path.isfile(pth_file):
if memoized_isfile(pth_file):
os.remove(pth_file)
logger.show_sig("Removed %s from %s" % (os.path.basename(coconut_pth_file), python_lib))
else:
Expand Down
Loading