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.1.2 #853

Merged
merged 27 commits into from
Sep 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ repos:
args:
- --autofix
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
rev: 7.1.1
hooks:
- id: flake8
args:
Expand Down
24 changes: 13 additions & 11 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1006,7 +1006,7 @@ Coconut also allows a single `?` before attribute access, function calling, part

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`.

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`.
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`.

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

Expand Down Expand Up @@ -1204,6 +1204,7 @@ base_pattern ::= (
| NAME "(" patterns ")" # classes or data types
| "data" NAME "(" patterns ")" # data types
| "class" NAME "(" patterns ")" # classes
| "(" name "=" pattern ... ")" # anonymous named tuples
| "{" pattern_pairs # dictionaries
["," "**" (NAME | "{}")] "}" # (keys must be constants or equality checks)
| ["s" | "f" | "m"] "{"
Expand Down Expand Up @@ -1269,7 +1270,8 @@ base_pattern ::= (
- 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.
- 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>)`).
- 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.
- Mapping Destructuring:
- 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).
- Dict Destructuring:
- 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.
- 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>`.
- Set Destructuring:
Expand Down Expand Up @@ -1735,7 +1737,7 @@ The syntax for a statement lambda is
```
[async|match|copyclosure] def (arguments) => statement; statement; ...
```
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.
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.

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

Expand Down Expand Up @@ -2233,7 +2235,7 @@ 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).
Such syntax is also supported in [partial application](#partial-application), [anonymous `namedtuple`s](#anonymous-namedtuples), and [`class`/`data`/anonymous `namedtuple` patterns](#match).

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

Expand Down Expand Up @@ -2262,7 +2264,7 @@ main_func(

### 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.
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.

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

#### `reduce`

**reduce**(_function_, _iterable_[, _initial_], /)
**reduce**(_function_, _iterable_[, _initial_])

Coconut re-introduces Python 2's `reduce` built-in, using the `functools.reduce` version.
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.

##### Python Docs

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

#### `takewhile`

**takewhile**(_predicate_, _iterable_, /)
**takewhile**(_predicate_, _iterable_)

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

##### Python Docs

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

#### `dropwhile`

**dropwhile**(_predicate_, _iterable_, /)
**dropwhile**(_predicate_, _iterable_)

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

##### Python Docs

Expand Down
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,27 @@ dev-py3: clean setup-py3
.PHONY: setup
setup:
-python -m ensurepip
python -m pip install --upgrade setuptools wheel pip pytest_remotedata cython
python -m pip install --upgrade setuptools wheel pip cython

.PHONY: setup-py2
setup-py2:
-python2 -m ensurepip
python2 -m pip install --upgrade "setuptools<58" wheel pip pytest_remotedata cython
python2 -m pip install --upgrade "setuptools<58" wheel pip cython

.PHONY: setup-py3
setup-py3:
-python3 -m ensurepip
python3 -m pip install --upgrade setuptools wheel pip pytest_remotedata cython
python3 -m pip install --upgrade setuptools wheel pip cython

.PHONY: setup-pypy
setup-pypy:
-pypy -m ensurepip
pypy -m pip install --upgrade "setuptools<58" wheel pip pytest_remotedata
pypy -m pip install --upgrade "setuptools<58" wheel pip

.PHONY: setup-pypy3
setup-pypy3:
-pypy3 -m ensurepip
pypy3 -m pip install --upgrade setuptools wheel pip pytest_remotedata
pypy3 -m pip install --upgrade setuptools wheel pip

.PHONY: install
install: setup
Expand Down
12 changes: 12 additions & 0 deletions __coconut__/__init__.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -1865,6 +1865,18 @@ def _coconut_mk_anon_namedtuple(
fields: _t.Tuple[_t.Text, ...],
types: _t.Optional[_t.Tuple[_t.Any, ...]] = None,
) -> _t.Callable[..., _t.Tuple[_t.Any, ...]]: ...
@_t.overload
def _coconut_mk_anon_namedtuple(
fields: _t.Tuple[_t.Text, ...],
types: _t.Optional[_t.Tuple[_t.Any, ...]],
of_args: _T,
) -> _T: ...
@_t.overload
def _coconut_mk_anon_namedtuple(
fields: _t.Tuple[_t.Text, ...],
*,
of_args: _T,
) -> _T: ...


# @_t.overload
Expand Down
4 changes: 2 additions & 2 deletions coconut/_pyparsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
never_clear_incremental_cache,
warn_on_multiline_regex,
num_displayed_timing_items,
use_cache_file,
use_pyparsing_cache_file,
use_line_by_line_parser,
incremental_use_hybrid,
)
Expand Down Expand Up @@ -254,7 +254,7 @@ def enableIncremental(*args, **kwargs):
and hasattr(MatchFirst, "setAdaptiveMode")
)

USE_CACHE = SUPPORTS_INCREMENTAL and use_cache_file
USE_CACHE = SUPPORTS_INCREMENTAL and use_pyparsing_cache_file
USE_LINE_BY_LINE = USE_COMPUTATION_GRAPH and use_line_by_line_parser

if MODERN_PYPARSING:
Expand Down
4 changes: 2 additions & 2 deletions coconut/command/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -671,7 +671,7 @@ class Prompt(object):
style = None
runner = None
lexer = None
suggester = None if prompt_use_suggester else False
suggester = True if prompt_use_suggester else None

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

def set_style(self, style):
Expand Down
104 changes: 67 additions & 37 deletions coconut/compiler/compiler.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,12 +246,17 @@ def import_stmt(imp_from, imp, imp_as, raw=False):
)


def imported_names(imports):
"""Yields all the names imported by imports = [[imp1], [imp2, as], ...]."""
def get_imported_names(imports):
"""Returns all the names imported by imports = [[imp1], [imp2, as], ...] and whether there is a star import."""
saw_names = []
saw_star = False
for imp in imports:
imp_name = imp[-1].split(".", 1)[0]
if imp_name != "*":
yield imp_name
if imp_name == "*":
saw_star = True
else:
saw_names.append(imp_name)
return saw_names, saw_star


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

def strict_warn(self, *args, **kwargs):
internal_assert("extra" not in kwargs, "cannot pass extra=... to strict_warn")
if self.strict:
self.syntax_warning(*args, extra="remove --strict to dismiss", **kwargs)

def syntax_warning(self, message, original, loc, **kwargs):
"""Show a CoconutSyntaxWarning. Usage:
self.syntax_warning(message, original, loc)
Expand Down Expand Up @@ -1319,21 +1330,30 @@ def streamline(self, grammars, inputstring=None, force=False, inner=False):
elif inputstring is not None and not inner:
logger.log("No streamlining done for input of length {length}.".format(length=input_len))

def qa_error(self, msg, original, loc):
"""Strict error or warn an error that should be disabled by a NOQA comment."""
ln = self.adjust(lineno(loc, original))
comment = self.reformat(" ".join(self.comments[ln]), ignore_errors=True)
if not self.noqa_regex.search(comment):
self.strict_err_or_warn(
msg + " (add '# NOQA' to suppress)",
original,
loc,
endpoint=False,
)

def run_final_checks(self, original, keep_state=False):
"""Run post-parsing checks to raise any necessary errors/warnings."""
# only check for unused imports if we're not keeping state accross parses
# only check for unused imports/etc. if we're not keeping state accross parses
if not keep_state:
for name, locs in self.unused_imports.items():
for loc in locs:
ln = self.adjust(lineno(loc, original))
comment = self.reformat(" ".join(self.comments[ln]), ignore_errors=True)
if not self.noqa_regex.search(comment):
self.strict_err_or_warn(
"found unused import " + repr(self.reformat(name, ignore_errors=True)) + " (add '# NOQA' to suppress)",
original,
loc,
endpoint=False,
)
for name, info in self.name_info.items():
if info["imported"] and not info["referenced"]:
for loc in info["imported"]:
self.qa_error("found unused import " + repr(self.reformat(name, ignore_errors=True)), original, loc)
if not self.star_import: # only check for undefined names when there are no * imports
if name not in all_builtins and info["referenced"] and not (info["assigned"] or info["imported"]):
for loc in info["referenced"]:
self.qa_error("found undefined name " + repr(self.reformat(name, ignore_errors=True)), original, loc)

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

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

def make_namedtuple_call(self, name, namedtuple_args, types=None):
def make_namedtuple_call(self, name, namedtuple_args, types=None, of_args=None):
"""Construct a namedtuple call."""
if types:
wrapped_types = [
self.wrap_typedef(types.get(i, "_coconut.typing.Any"), for_py_typedef=False)
for i in range(len(namedtuple_args))
]
if name is None:
return "_coconut_mk_anon_namedtuple(" + tuple_str_of(namedtuple_args, add_quotes=True) + ", " + tuple_str_of(wrapped_types) + ")"
else:
return '_coconut.typing.NamedTuple("' + name + '", [' + ", ".join(
'("' + argname + '", ' + wrapped_type + ")"
for argname, wrapped_type in zip(namedtuple_args, wrapped_types)
) + "])"
else:
if name is None:
return "_coconut_mk_anon_namedtuple(" + tuple_str_of(namedtuple_args, add_quotes=True) + ")"
else:
return '_coconut.collections.namedtuple("' + name + '", ' + tuple_str_of(namedtuple_args, add_quotes=True) + ')'
wrapped_types = None
if name is None:
return (
"_coconut_mk_anon_namedtuple("
+ tuple_str_of(namedtuple_args, add_quotes=True)
+ ("" if wrapped_types is None else ", " + tuple_str_of(wrapped_types))
+ ("" if of_args is None else ", of_args=" + tuple_str_of(of_args) + "")
+ ")"
)
elif wrapped_types is None:
return '_coconut.collections.namedtuple("' + name + '", ' + tuple_str_of(namedtuple_args, add_quotes=True) + ')' + ("" if of_args is None else tuple_str_of(of_args))
else:
return '_coconut.typing.NamedTuple("' + name + '", [' + ", ".join(
'("' + argname + '", ' + wrapped_type + ")"
for argname, wrapped_type in zip(namedtuple_args, wrapped_types)
) + "])" + ("" if of_args is None else tuple_str_of(of_args))

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

namedtuple_call = self.make_namedtuple_call(None, names, types)
return namedtuple_call + "(" + ", ".join(items) + ")"
return self.make_namedtuple_call(None, names, types, of_args=items)

def single_import(self, loc, path, imp_as, type_ignore=False):
"""Generate import statements from a fully qualified import and the name to bind it to."""
Expand Down Expand Up @@ -3731,13 +3755,17 @@ def import_handle(self, original, loc, tokens):
else:
raise CoconutInternalException("invalid import tokens", tokens)
imports = list(imports)
if imp_from == "*" or imp_from is None and "*" in imports:
imported_names, star_import = get_imported_names(imports)
self.star_import = self.star_import or star_import
if star_import:
self.strict_warn("found * import; these disable Coconut's undefined name detection", original, loc)
if imp_from == "*" or (imp_from is None and star_import):
if not (len(imports) == 1 and imports[0] == "*"):
raise self.make_err(CoconutSyntaxError, "only [from *] import * allowed, not from * import name", original, loc)
self.syntax_warning("[from *] import * is a Coconut Easter egg and should not be used in production code", original, loc)
return special_starred_import_handle(imp_all=bool(imp_from))
for imp_name in imported_names(imports):
self.unused_imports[imp_name].append(loc)
for imp_name in imported_names:
self.name_info[imp_name]["imported"].append(loc)
return self.universal_import(loc, imports, imp_from=imp_from)

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

if not assign:
self.unused_imports.pop(name, None)
if assign:
self.name_info[name]["assigned"].append(loc)
else:
self.name_info[name]["referenced"].append(loc)

if (
assign
Expand Down
Loading
Loading