Skip to content

Commit 4659175

Browse files
authored
Release v3.1.2 (#853)
See Coconut's [documentation](http://coconut.readthedocs.io/en/develop/DOCS.html) for more information on all of the features listed below. Bugfixes: * #851, #852: Fixed comments inside of parentheses in the Jupyter kernel. Language features: * #846: `reduce`, `takewhile`, and `dropwhile` now support keyword arguments. * #848: Class and data patterns now support keyword argument name elision. * #847: New pattern-matching syntax for matching anonymous named tuples. Compiler features: * #843: Added compiler warnings for (some cases of) undefined variables.
2 parents ac70b14 + 120c273 commit 4659175

File tree

24 files changed

+304
-127
lines changed

24 files changed

+304
-127
lines changed

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ repos:
3434
args:
3535
- --autofix
3636
- repo: https://github.com/pycqa/flake8
37-
rev: 7.0.0
37+
rev: 7.1.1
3838
hooks:
3939
- id: flake8
4040
args:

DOCS.md

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ Coconut also allows a single `?` before attribute access, function calling, part
10061006

10071007
When using a `None`-aware operator for member access, either for a method or an attribute, the syntax is `obj?.method()` or `obj?.attr` respectively. `obj?.attr` is equivalent to `obj.attr if obj is not None else obj`. This does not prevent an `AttributeError` if `attr` is not an attribute or method of `obj`.
10081008

1009-
The `None`-aware indexing operator is used identically to normal indexing, using `?[]` instead of `[]`. `seq?[index]` is equivalent to the expression `seq[index] is seq is not None else seq`. Using this operator will not prevent an `IndexError` if `index` is outside the bounds of `seq`.
1009+
The `None`-aware indexing operator is used identically to normal indexing, using `?[]` instead of `[]`. `seq?[index]` is equivalent to the expression `seq[index] if seq is not None else seq`. Using this operator will not prevent an `IndexError` if `index` is outside the bounds of `seq`.
10101010

10111011
Coconut also supports None-aware [pipe operators](#pipes) and [function composition pipes](#function-composition).
10121012

@@ -1204,6 +1204,7 @@ base_pattern ::= (
12041204
| NAME "(" patterns ")" # classes or data types
12051205
| "data" NAME "(" patterns ")" # data types
12061206
| "class" NAME "(" patterns ")" # classes
1207+
| "(" name "=" pattern ... ")" # anonymous named tuples
12071208
| "{" pattern_pairs # dictionaries
12081209
["," "**" (NAME | "{}")] "}" # (keys must be constants or equality checks)
12091210
| ["s" | "f" | "m"] "{"
@@ -1269,7 +1270,8 @@ base_pattern ::= (
12691270
- Classes or Data Types (`<name>(<args>)`): will match as a data type if given [a Coconut `data` type](#data) (or a tuple of Coconut data types) and a class otherwise.
12701271
- Data Types (`data <name>(<args>)`): will check that whatever is in that position is of data type `<name>` and will match the attributes to `<args>`. Generally, `data <name>(<args>)` will match any data type that could have been constructed with `makedata(<name>, <args>)`. Includes support for positional arguments, named arguments, default arguments, and starred arguments. Also supports strict attributes by prepending a dot to the attribute name that raises `AttributError` if the attribute is not present rather than failing the match (e.g. `data MyData(.my_attr=<some_pattern>)`).
12711272
- Classes (`class <name>(<args>)`): does [PEP-634-style class matching](https://www.python.org/dev/peps/pep-0634/#class-patterns). Also supports strict attribute matching as above.
1272-
- Mapping Destructuring:
1273+
- Anonymous Named Tuples (`(<name>=<pattern>, ...)`): checks that the object is a `tuple` of the given length with the given attributes. For matching [anonymous `namedtuple`s](#anonymous-namedtuples).
1274+
- Dict Destructuring:
12731275
- Dicts (`{<key>: <value>, ...}`): will match any mapping (`collections.abc.Mapping`) with the given keys and values that match the value patterns. Keys must be constants or equality checks.
12741276
- Dicts With Rest (`{<pairs>, **<rest>}`): will match a mapping (`collections.abc.Mapping`) containing all the `<pairs>`, and will put a `dict` of everything else into `<rest>`. If `<rest>` is `{}`, will enforce that the mapping is exactly the same length as `<pairs>`.
12751277
- Set Destructuring:
@@ -1735,7 +1737,7 @@ The syntax for a statement lambda is
17351737
```
17361738
[async|match|copyclosure] def (arguments) => statement; statement; ...
17371739
```
1738-
where `arguments` can be standard function arguments or [pattern-matching function definition](#pattern-matching-functions) arguments and `statement` can be an assignment statement or a keyword statement. Note that the `async`, `match`, and [`copyclosure`](#copyclosure-functions) keywords can be combined and can be in any order.
1740+
where `arguments` can be standard function arguments or [pattern-matching function definition](#pattern-matching-functions) arguments and `statement` can be any non-compound statement—that is, any statement that doesn't open a code block below it (so `def x => assert x` is fine but `def x => if x: True` is not). Note that the `async`, `match`, and [`copyclosure`](#copyclosure-functions) keywords can be combined and can be in any order.
17391741

17401742
If the last `statement` (not followed by a semicolon) in a statement lambda is an `expression`, it will automatically be returned.
17411743

@@ -2233,7 +2235,7 @@ as a shorthand for
22332235
f(long_variable_name=long_variable_name)
22342236
```
22352237

2236-
Such syntax is also supported in [partial application](#partial-application) and [anonymous `namedtuple`s](#anonymous-namedtuples).
2238+
Such syntax is also supported in [partial application](#partial-application), [anonymous `namedtuple`s](#anonymous-namedtuples), and [`class`/`data`/anonymous `namedtuple` patterns](#match).
22372239

22382240
_Deprecated: Coconut also supports `f(...=long_variable_name)` as an alternative shorthand syntax._
22392241

@@ -2262,7 +2264,7 @@ main_func(
22622264

22632265
### Anonymous Namedtuples
22642266

2265-
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.
2267+
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 and support [`__match_args__`](https://peps.python.org/pep-0622/) on all Python versions.
22662268

22672269
The syntax for anonymous namedtuple literals is:
22682270
```coconut
@@ -3803,9 +3805,9 @@ _Can’t be done quickly without Coconut’s iterable indexing, which requires m
38033805

38043806
#### `reduce`
38053807

3806-
**reduce**(_function_, _iterable_[, _initial_], /)
3808+
**reduce**(_function_, _iterable_[, _initial_])
38073809

3808-
Coconut re-introduces Python 2's `reduce` built-in, using the `functools.reduce` version.
3810+
Coconut re-introduces Python 2's `reduce` built-in, using the `functools.reduce` version. Additionally, unlike `functools.reduce`, Coconut's `reduce` always supports keyword arguments.
38093811

38103812
##### Python Docs
38113813

@@ -3935,9 +3937,9 @@ result = itertools.zip_longest(range(5), range(10))
39353937

39363938
#### `takewhile`
39373939

3938-
**takewhile**(_predicate_, _iterable_, /)
3940+
**takewhile**(_predicate_, _iterable_)
39393941

3940-
Coconut provides `itertools.takewhile` as a built-in under the name `takewhile`.
3942+
Coconut provides `itertools.takewhile` as a built-in under the name `takewhile`. Additionally, unlike `itertools.takewhile`, Coconut's `takewhile` always supports keyword arguments.
39413943

39423944
##### Python Docs
39433945

@@ -3969,9 +3971,9 @@ negatives = itertools.takewhile(lambda x: x < 0, numiter)
39693971

39703972
#### `dropwhile`
39713973

3972-
**dropwhile**(_predicate_, _iterable_, /)
3974+
**dropwhile**(_predicate_, _iterable_)
39733975

3974-
Coconut provides `itertools.dropwhile` as a built-in under the name `dropwhile`.
3976+
Coconut provides `itertools.dropwhile` as a built-in under the name `dropwhile`. Additionally, unlike `itertools.dropwhile`, Coconut's `dropwhile` always supports keyword arguments.
39753977

39763978
##### Python Docs
39773979

Makefile

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,27 +26,27 @@ dev-py3: clean setup-py3
2626
.PHONY: setup
2727
setup:
2828
-python -m ensurepip
29-
python -m pip install --upgrade setuptools wheel pip pytest_remotedata cython
29+
python -m pip install --upgrade setuptools wheel pip cython
3030

3131
.PHONY: setup-py2
3232
setup-py2:
3333
-python2 -m ensurepip
34-
python2 -m pip install --upgrade "setuptools<58" wheel pip pytest_remotedata cython
34+
python2 -m pip install --upgrade "setuptools<58" wheel pip cython
3535

3636
.PHONY: setup-py3
3737
setup-py3:
3838
-python3 -m ensurepip
39-
python3 -m pip install --upgrade setuptools wheel pip pytest_remotedata cython
39+
python3 -m pip install --upgrade setuptools wheel pip cython
4040

4141
.PHONY: setup-pypy
4242
setup-pypy:
4343
-pypy -m ensurepip
44-
pypy -m pip install --upgrade "setuptools<58" wheel pip pytest_remotedata
44+
pypy -m pip install --upgrade "setuptools<58" wheel pip
4545

4646
.PHONY: setup-pypy3
4747
setup-pypy3:
4848
-pypy3 -m ensurepip
49-
pypy3 -m pip install --upgrade setuptools wheel pip pytest_remotedata
49+
pypy3 -m pip install --upgrade setuptools wheel pip
5050

5151
.PHONY: install
5252
install: setup

__coconut__/__init__.pyi

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1865,6 +1865,18 @@ def _coconut_mk_anon_namedtuple(
18651865
fields: _t.Tuple[_t.Text, ...],
18661866
types: _t.Optional[_t.Tuple[_t.Any, ...]] = None,
18671867
) -> _t.Callable[..., _t.Tuple[_t.Any, ...]]: ...
1868+
@_t.overload
1869+
def _coconut_mk_anon_namedtuple(
1870+
fields: _t.Tuple[_t.Text, ...],
1871+
types: _t.Optional[_t.Tuple[_t.Any, ...]],
1872+
of_args: _T,
1873+
) -> _T: ...
1874+
@_t.overload
1875+
def _coconut_mk_anon_namedtuple(
1876+
fields: _t.Tuple[_t.Text, ...],
1877+
*,
1878+
of_args: _T,
1879+
) -> _T: ...
18681880

18691881

18701882
# @_t.overload

coconut/_pyparsing.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
never_clear_incremental_cache,
4747
warn_on_multiline_regex,
4848
num_displayed_timing_items,
49-
use_cache_file,
49+
use_pyparsing_cache_file,
5050
use_line_by_line_parser,
5151
incremental_use_hybrid,
5252
)
@@ -254,7 +254,7 @@ def enableIncremental(*args, **kwargs):
254254
and hasattr(MatchFirst, "setAdaptiveMode")
255255
)
256256

257-
USE_CACHE = SUPPORTS_INCREMENTAL and use_cache_file
257+
USE_CACHE = SUPPORTS_INCREMENTAL and use_pyparsing_cache_file
258258
USE_LINE_BY_LINE = USE_COMPUTATION_GRAPH and use_line_by_line_parser
259259

260260
if MODERN_PYPARSING:

coconut/command/util.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -671,7 +671,7 @@ class Prompt(object):
671671
style = None
672672
runner = None
673673
lexer = None
674-
suggester = None if prompt_use_suggester else False
674+
suggester = True if prompt_use_suggester else None
675675

676676
def __init__(self, setup_now=False):
677677
"""Set up the prompt."""
@@ -686,7 +686,7 @@ def setup(self):
686686
We do this lazily since it's expensive."""
687687
if self.lexer is None:
688688
self.lexer = PygmentsLexer(CoconutLexer)
689-
if self.suggester is None:
689+
if self.suggester is True:
690690
self.suggester = AutoSuggestFromHistory()
691691

692692
def set_style(self, style):

coconut/compiler/compiler.py

Lines changed: 67 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -246,12 +246,17 @@ def import_stmt(imp_from, imp, imp_as, raw=False):
246246
)
247247

248248

249-
def imported_names(imports):
250-
"""Yields all the names imported by imports = [[imp1], [imp2, as], ...]."""
249+
def get_imported_names(imports):
250+
"""Returns all the names imported by imports = [[imp1], [imp2, as], ...] and whether there is a star import."""
251+
saw_names = []
252+
saw_star = False
251253
for imp in imports:
252254
imp_name = imp[-1].split(".", 1)[0]
253-
if imp_name != "*":
254-
yield imp_name
255+
if imp_name == "*":
256+
saw_star = True
257+
else:
258+
saw_names.append(imp_name)
259+
return saw_names, saw_star
255260

256261

257262
def special_starred_import_handle(imp_all=False):
@@ -529,7 +534,8 @@ def reset(self, keep_state=False, filename=None):
529534
# but always overwrite temp_vars_by_key since they store locs that will be invalidated
530535
self.temp_vars_by_key = {}
531536
self.parsing_context = defaultdict(list)
532-
self.unused_imports = defaultdict(list)
537+
self.name_info = defaultdict(lambda: {"imported": [], "referenced": [], "assigned": []})
538+
self.star_import = False
533539
self.kept_lines = []
534540
self.num_lines = 0
535541
self.disable_name_check = False
@@ -942,6 +948,11 @@ def strict_err(self, *args, **kwargs):
942948
if self.strict:
943949
raise self.make_err(CoconutStyleError, *args, **kwargs)
944950

951+
def strict_warn(self, *args, **kwargs):
952+
internal_assert("extra" not in kwargs, "cannot pass extra=... to strict_warn")
953+
if self.strict:
954+
self.syntax_warning(*args, extra="remove --strict to dismiss", **kwargs)
955+
945956
def syntax_warning(self, message, original, loc, **kwargs):
946957
"""Show a CoconutSyntaxWarning. Usage:
947958
self.syntax_warning(message, original, loc)
@@ -1319,21 +1330,30 @@ def streamline(self, grammars, inputstring=None, force=False, inner=False):
13191330
elif inputstring is not None and not inner:
13201331
logger.log("No streamlining done for input of length {length}.".format(length=input_len))
13211332

1333+
def qa_error(self, msg, original, loc):
1334+
"""Strict error or warn an error that should be disabled by a NOQA comment."""
1335+
ln = self.adjust(lineno(loc, original))
1336+
comment = self.reformat(" ".join(self.comments[ln]), ignore_errors=True)
1337+
if not self.noqa_regex.search(comment):
1338+
self.strict_err_or_warn(
1339+
msg + " (add '# NOQA' to suppress)",
1340+
original,
1341+
loc,
1342+
endpoint=False,
1343+
)
1344+
13221345
def run_final_checks(self, original, keep_state=False):
13231346
"""Run post-parsing checks to raise any necessary errors/warnings."""
1324-
# only check for unused imports if we're not keeping state accross parses
1347+
# only check for unused imports/etc. if we're not keeping state accross parses
13251348
if not keep_state:
1326-
for name, locs in self.unused_imports.items():
1327-
for loc in locs:
1328-
ln = self.adjust(lineno(loc, original))
1329-
comment = self.reformat(" ".join(self.comments[ln]), ignore_errors=True)
1330-
if not self.noqa_regex.search(comment):
1331-
self.strict_err_or_warn(
1332-
"found unused import " + repr(self.reformat(name, ignore_errors=True)) + " (add '# NOQA' to suppress)",
1333-
original,
1334-
loc,
1335-
endpoint=False,
1336-
)
1349+
for name, info in self.name_info.items():
1350+
if info["imported"] and not info["referenced"]:
1351+
for loc in info["imported"]:
1352+
self.qa_error("found unused import " + repr(self.reformat(name, ignore_errors=True)), original, loc)
1353+
if not self.star_import: # only check for undefined names when there are no * imports
1354+
if name not in all_builtins and info["referenced"] and not (info["assigned"] or info["imported"]):
1355+
for loc in info["referenced"]:
1356+
self.qa_error("found undefined name " + repr(self.reformat(name, ignore_errors=True)), original, loc)
13371357

13381358
def parse_line_by_line(self, init_parser, line_parser, original):
13391359
"""Apply init_parser then line_parser repeatedly."""
@@ -3473,25 +3493,30 @@ def __new__(_coconut_cls, {all_args}):
34733493

34743494
return self.assemble_data(decorators, name, namedtuple_call, inherit, extra_stmts, stmts, base_args, paramdefs)
34753495

3476-
def make_namedtuple_call(self, name, namedtuple_args, types=None):
3496+
def make_namedtuple_call(self, name, namedtuple_args, types=None, of_args=None):
34773497
"""Construct a namedtuple call."""
34783498
if types:
34793499
wrapped_types = [
34803500
self.wrap_typedef(types.get(i, "_coconut.typing.Any"), for_py_typedef=False)
34813501
for i in range(len(namedtuple_args))
34823502
]
3483-
if name is None:
3484-
return "_coconut_mk_anon_namedtuple(" + tuple_str_of(namedtuple_args, add_quotes=True) + ", " + tuple_str_of(wrapped_types) + ")"
3485-
else:
3486-
return '_coconut.typing.NamedTuple("' + name + '", [' + ", ".join(
3487-
'("' + argname + '", ' + wrapped_type + ")"
3488-
for argname, wrapped_type in zip(namedtuple_args, wrapped_types)
3489-
) + "])"
34903503
else:
3491-
if name is None:
3492-
return "_coconut_mk_anon_namedtuple(" + tuple_str_of(namedtuple_args, add_quotes=True) + ")"
3493-
else:
3494-
return '_coconut.collections.namedtuple("' + name + '", ' + tuple_str_of(namedtuple_args, add_quotes=True) + ')'
3504+
wrapped_types = None
3505+
if name is None:
3506+
return (
3507+
"_coconut_mk_anon_namedtuple("
3508+
+ tuple_str_of(namedtuple_args, add_quotes=True)
3509+
+ ("" if wrapped_types is None else ", " + tuple_str_of(wrapped_types))
3510+
+ ("" if of_args is None else ", of_args=" + tuple_str_of(of_args) + "")
3511+
+ ")"
3512+
)
3513+
elif wrapped_types is None:
3514+
return '_coconut.collections.namedtuple("' + name + '", ' + tuple_str_of(namedtuple_args, add_quotes=True) + ')' + ("" if of_args is None else tuple_str_of(of_args))
3515+
else:
3516+
return '_coconut.typing.NamedTuple("' + name + '", [' + ", ".join(
3517+
'("' + argname + '", ' + wrapped_type + ")"
3518+
for argname, wrapped_type in zip(namedtuple_args, wrapped_types)
3519+
) + "])" + ("" if of_args is None else tuple_str_of(of_args))
34953520

34963521
def assemble_data(self, decorators, name, namedtuple_call, inherit, extra_stmts, stmts, match_args, paramdefs=()):
34973522
"""Create a data class definition from the given components.
@@ -3597,8 +3622,7 @@ def anon_namedtuple_handle(self, original, loc, tokens):
35973622
names.append(name)
35983623
items.append(item)
35993624

3600-
namedtuple_call = self.make_namedtuple_call(None, names, types)
3601-
return namedtuple_call + "(" + ", ".join(items) + ")"
3625+
return self.make_namedtuple_call(None, names, types, of_args=items)
36023626

36033627
def single_import(self, loc, path, imp_as, type_ignore=False):
36043628
"""Generate import statements from a fully qualified import and the name to bind it to."""
@@ -3731,13 +3755,17 @@ def import_handle(self, original, loc, tokens):
37313755
else:
37323756
raise CoconutInternalException("invalid import tokens", tokens)
37333757
imports = list(imports)
3734-
if imp_from == "*" or imp_from is None and "*" in imports:
3758+
imported_names, star_import = get_imported_names(imports)
3759+
self.star_import = self.star_import or star_import
3760+
if star_import:
3761+
self.strict_warn("found * import; these disable Coconut's undefined name detection", original, loc)
3762+
if imp_from == "*" or (imp_from is None and star_import):
37353763
if not (len(imports) == 1 and imports[0] == "*"):
37363764
raise self.make_err(CoconutSyntaxError, "only [from *] import * allowed, not from * import name", original, loc)
37373765
self.syntax_warning("[from *] import * is a Coconut Easter egg and should not be used in production code", original, loc)
37383766
return special_starred_import_handle(imp_all=bool(imp_from))
3739-
for imp_name in imported_names(imports):
3740-
self.unused_imports[imp_name].append(loc)
3767+
for imp_name in imported_names:
3768+
self.name_info[imp_name]["imported"].append(loc)
37413769
return self.universal_import(loc, imports, imp_from=imp_from)
37423770

37433771
def complex_raise_stmt_handle(self, loc, tokens):
@@ -4575,7 +4603,7 @@ def string_atom_handle(self, original, loc, tokens, allow_silent_concat=False):
45754603
return tokens[0]
45764604
else:
45774605
if not allow_silent_concat:
4578-
self.strict_err_or_warn("found Python-style implicit string concatenation (use explicit '+' instead)", original, loc)
4606+
self.strict_err_or_warn("found implicit string concatenation (use explicit '+' instead)", original, loc)
45794607
if any(s.endswith(")") for s in tokens): # has .format() calls
45804608
# parens are necessary for string_atom_handle
45814609
return "(" + " + ".join(tokens) + ")"
@@ -4989,8 +5017,10 @@ def name_handle(self, original, loc, tokens, assign=False, classname=False, expr
49895017
)
49905018
return typevars[name]
49915019

4992-
if not assign:
4993-
self.unused_imports.pop(name, None)
5020+
if assign:
5021+
self.name_info[name]["assigned"].append(loc)
5022+
else:
5023+
self.name_info[name]["referenced"].append(loc)
49945024

49955025
if (
49965026
assign

0 commit comments

Comments
 (0)