diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e1ea9b6af..2b253a6d5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -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: diff --git a/DOCS.md b/DOCS.md index a341d3a4a..a3d7441af 100644 --- a/DOCS.md +++ b/DOCS.md @@ -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). @@ -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"] "{" @@ -1269,7 +1270,8 @@ base_pattern ::= ( - Classes or Data Types (`()`): 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 ()`): will check that whatever is in that position is of data type `` and will match the attributes to ``. Generally, `data ()` will match any data type that could have been constructed with `makedata(, )`. 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=)`). - Classes (`class ()`): 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 (`(=, ...)`): 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 (`{: , ...}`): 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 (`{, **}`): will match a mapping (`collections.abc.Mapping`) containing all the ``, and will put a `dict` of everything else into ``. If `` is `{}`, will enforce that the mapping is exactly the same length as ``. - Set Destructuring: @@ -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. @@ -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._ @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/Makefile b/Makefile index e96ed3eef..f60e01a94 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/__coconut__/__init__.pyi b/__coconut__/__init__.pyi index fedb0bb90..f2bc2bdfe 100644 --- a/__coconut__/__init__.pyi +++ b/__coconut__/__init__.pyi @@ -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 diff --git a/coconut/_pyparsing.py b/coconut/_pyparsing.py index f3101d42e..c0cb69325 100644 --- a/coconut/_pyparsing.py +++ b/coconut/_pyparsing.py @@ -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, ) @@ -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: diff --git a/coconut/command/util.py b/coconut/command/util.py index fe26947d8..923b0b597 100644 --- a/coconut/command/util.py +++ b/coconut/command/util.py @@ -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.""" @@ -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): diff --git a/coconut/compiler/compiler.py b/coconut/compiler/compiler.py index 458d6c283..cae7f6d82 100644 --- a/coconut/compiler/compiler.py +++ b/coconut/compiler/compiler.py @@ -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): @@ -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 @@ -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) @@ -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.""" @@ -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. @@ -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.""" @@ -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): @@ -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) + ")" @@ -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 diff --git a/coconut/compiler/grammar.py b/coconut/compiler/grammar.py index 7e5bd8e6e..f7947e9ef 100644 --- a/coconut/compiler/grammar.py +++ b/coconut/compiler/grammar.py @@ -1995,8 +1995,17 @@ class Grammar(object): del_stmt = addspace(keyword("del") - simple_assignlist) - matchlist_data_item = Group(Optional(star | Optional(dot) + unsafe_name + equals) + match) - matchlist_data = Group(Optional(tokenlist(matchlist_data_item, comma))) + interior_name_match = labeled_group(setname, "var") + matchlist_anon_named_tuple_item = ( + Group(Optional(dot) + unsafe_name) + equals + match + | Group(Optional(dot) + interior_name_match) + equals + ) + matchlist_data_item = ( + matchlist_anon_named_tuple_item + | Optional(star) + match + ) + matchlist_data = Group(Optional(tokenlist(Group(matchlist_data_item), comma))) + matchlist_anon_named_tuple = Optional(tokenlist(Group(matchlist_anon_named_tuple_item), comma)) match_check_equals = Forward() match_check_equals_ref = equals @@ -2031,7 +2040,6 @@ class Grammar(object): match_tuple = Group(lparen + matchlist_tuple + rparen.suppress()) match_lazy = Group(lbanana + matchlist_list + rbanana.suppress()) - interior_name_match = labeled_group(setname, "var") match_string = interleaved_tokenlist( # f_string_atom must come first f_string_atom("f_string") | fixed_len_string_tokens("string"), @@ -2085,6 +2093,7 @@ class Grammar(object): | (keyword("data").suppress() + dotted_refname + lparen.suppress() + matchlist_data + rparen.suppress())("data") | (keyword("class").suppress() + dotted_refname + lparen.suppress() + matchlist_data + rparen.suppress())("class") | (dotted_refname + lparen.suppress() + matchlist_data + rparen.suppress())("data_or_class") + | (lparen.suppress() + matchlist_anon_named_tuple + rparen.suppress())("anon_named_tuple") | Optional(keyword("as").suppress()) + setname("var"), ) diff --git a/coconut/compiler/header.py b/coconut/compiler/header.py index 989e0c6a8..e96b00c70 100644 --- a/coconut/compiler/header.py +++ b/coconut/compiler/header.py @@ -458,6 +458,14 @@ def recursive_iterator(*args, **kwargs): ''', indent=1, ), + set_NT_match_args=pycondition( + (3, 10), + if_lt=r''' +NT.__match_args__ = _coconut.property(lambda self: self._fields) + ''', + indent=1, + newline=True, + ), import_copyreg=pycondition( (3,), if_lt="import copy_reg as copyreg", diff --git a/coconut/compiler/matching.py b/coconut/compiler/matching.py index 9690dc9d9..7f6c4d55f 100644 --- a/coconut/compiler/matching.py +++ b/coconut/compiler/matching.py @@ -150,6 +150,7 @@ class Matcher(object): "data": lambda self: self.match_data, "class": lambda self: self.match_class, "data_or_class": lambda self: self.match_data_or_class, + "anon_named_tuple": lambda self: self.match_anon_named_tuple, "paren": lambda self: self.match_paren, "as": lambda self: self.match_as, "and": lambda self: self.match_and, @@ -1056,10 +1057,8 @@ def match_set(self, tokens, item): for const in match: self.add_check(const + " in " + item) - def split_data_or_class_match(self, tokens): - """Split data/class match tokens into cls_name, pos_matches, name_matches, star_match.""" - cls_name, matches = tokens - + def split_data_or_class_matches(self, matches): + """Split data/class match tokens into pos_matches, name_matches, star_match.""" pos_matches = [] name_matches = {} star_match = None @@ -1073,8 +1072,7 @@ def split_data_or_class_match(self, tokens): raise CoconutDeferredSyntaxError("positional arg after keyword arg in data/class match", self.loc) pos_matches.append(match) # starred arg - elif len(match_arg) == 2: - internal_assert(match_arg[0] == "*", "invalid starred data/class match arg tokens", match_arg) + elif len(match_arg) == 2 and match_arg[0] == "*": _, match = match_arg if star_match is not None: raise CoconutDeferredSyntaxError("duplicate starred arg in data/class match", self.loc) @@ -1083,23 +1081,30 @@ def split_data_or_class_match(self, tokens): star_match = match # keyword arg else: + internal_assert(match_arg[1] == "=", "invalid keyword data/class match arg tokens", match_arg) if len(match_arg) == 3: - internal_assert(match_arg[1] == "=", "invalid keyword data/class match arg tokens", match_arg) - name, _, match = match_arg - strict = False - elif len(match_arg) == 4: - internal_assert(match_arg[0] == "." and match_arg[2] == "=", "invalid strict keyword data/class match arg tokens", match_arg) - _, name, _, match = match_arg - strict = True + name_grp, _, match = match_arg + elif len(match_arg) == 2: + match_grp, _ = match_arg + match = match_grp[-1] + name, = match + name_grp = match_grp[:-1] + [name] else: raise CoconutInternalException("invalid data/class match arg", match_arg) + if len(name_grp) == 1: + name, = name_grp + strict = False + else: + internal_assert(name_grp[0] == ".", "invalid keyword data/class match arg tokens", name_grp) + _, name = name_grp + strict = True if star_match is not None: raise CoconutDeferredSyntaxError("both keyword arg and starred arg in data/class match", self.loc) if name in name_matches: raise CoconutDeferredSyntaxError("duplicate keyword arg {name!r} in data/class match".format(name=name), self.loc) name_matches[name] = (match, strict) - return cls_name, pos_matches, name_matches, star_match + return pos_matches, name_matches, star_match def match_class_attr(self, match, attr, item): """Match an attribute for a class match where attr is an expression that evaluates to the attribute name.""" @@ -1119,7 +1124,8 @@ def match_class_names(self, name_matches, item): def match_class(self, tokens, item): """Matches a class PEP-622-style.""" - cls_name, pos_matches, name_matches, star_match = self.split_data_or_class_match(tokens) + cls_name, matches = tokens + pos_matches, name_matches, star_match = self.split_data_or_class_matches(matches) self.add_check("_coconut.isinstance(" + item + ", " + cls_name + ")") @@ -1191,7 +1197,8 @@ def match_class(self, tokens, item): def match_data(self, tokens, item): """Matches a data type.""" - cls_name, pos_matches, name_matches, star_match = self.split_data_or_class_match(tokens) + cls_name, matches = tokens + pos_matches, name_matches, star_match = self.split_data_or_class_matches(matches) self.add_check("_coconut.isinstance(" + item + ", " + cls_name + ")") @@ -1240,6 +1247,17 @@ def match_data(self, tokens, item): with self.down_a_level(): self.add_check(temp_var) + def match_anon_named_tuple(self, tokens, item): + """Matches an anonymous named tuple pattern.""" + pos_matches, name_matches, star_match = self.split_data_or_class_matches(tokens) + internal_assert(not pos_matches and not star_match, "got invalid pos/star matches in anon named tuple pattern", (pos_matches, star_match)) + self.add_check("_coconut.isinstance(" + item + ", tuple)") + self.add_check("_coconut.len({item}) == {expected_len}".format( + item=item, + expected_len=len(name_matches), + )) + self.match_class_names(name_matches, item) + def match_data_or_class(self, tokens, item): """Matches an ambiguous data or class match.""" cls_name, matches = tokens diff --git a/coconut/compiler/templates/header.py_template b/coconut/compiler/templates/header.py_template index c5cfb8f26..33fb4b4b6 100644 --- a/coconut/compiler/templates/header.py_template +++ b/coconut/compiler/templates/header.py_template @@ -62,7 +62,7 @@ class _coconut{object}:{COMMENT.EVERYTHING_HERE_MUST_BE_COPIED_TO_STUB_FILE} fmappables = list, tuple, dict, set, frozenset, bytes, bytearray abc.Sequence.register(collections.deque) Ellipsis, NotImplemented, NotImplementedError, Exception, AttributeError, ImportError, IndexError, KeyError, NameError, TypeError, ValueError, StopIteration, RuntimeError, all, any, bool, bytes, callable, chr, classmethod, complex, dict, enumerate, filter, float, frozenset, getattr, hasattr, hash, id, int, isinstance, issubclass, iter, len, list, locals, globals, map, min, max, next, object, ord, property, range, reversed, set, setattr, slice, str, sum, super, tuple, type, vars, zip, repr, print{comma_bytearray} = Ellipsis, NotImplemented, NotImplementedError, Exception, AttributeError, ImportError, IndexError, KeyError, NameError, TypeError, ValueError, StopIteration, RuntimeError, all, any, bool, bytes, callable, chr, classmethod, complex, dict, enumerate, filter, float, frozenset, getattr, hasattr, hash, id, int, isinstance, issubclass, iter, len, list, locals, globals, map, {lstatic}min{rstatic}, {lstatic}max{rstatic}, next, object, ord, property, range, reversed, set, setattr, slice, str, sum, {lstatic}super{rstatic}, tuple, type, vars, zip, {lstatic}repr{rstatic}, {lstatic}print{rstatic}{comma_bytearray} -@_coconut.functools.wraps(_coconut.functools.partial) +@_coconut_wraps(_coconut.functools.partial) def _coconut_partial(_coconut_func, *args, **kwargs): partial_func = _coconut.functools.partial(_coconut_func, *args, **kwargs) partial_func.__name__ = _coconut.getattr(_coconut_func, "__name__", None) @@ -182,7 +182,7 @@ class _coconut_tail_call(_coconut_baseclass): return (self.__class__, (self.func, self.args, self.kwargs)) _coconut_tco_func_dict = _coconut.weakref.WeakValueDictionary() def _coconut_tco(func): - @_coconut.functools.wraps(func) + @_coconut_wraps(func) def tail_call_optimized_func(*args, **kwargs): call_func = func while True:{COMMENT.weakrefs_necessary_for_ignoring_functools_wraps_decorators} @@ -209,7 +209,7 @@ def _coconut_tco(func): tail_call_optimized_func.__qualname__ = _coconut.getattr(func, "__qualname__", None) _coconut_tco_func_dict[_coconut.id(tail_call_optimized_func)] = tail_call_optimized_func return tail_call_optimized_func -@_coconut.functools.wraps(_coconut.itertools.tee) +@_coconut_wraps(_coconut.itertools.tee) def tee(iterable, n=2): if n < 0: raise _coconut.ValueError("tee: n cannot be negative") @@ -2017,15 +2017,16 @@ collectby.using_threads = _coconut_partial(_coconut_parallel_mapreduce, collectb def _namedtuple_of(**kwargs): """Construct an anonymous namedtuple of the given keyword arguments.""" {namedtuple_of_implementation} -def _coconut_mk_anon_namedtuple(fields, types=None, of_kwargs=None): +def _coconut_mk_anon_namedtuple(fields, types=None, of_kwargs={empty_dict}, of_args=()): if types is None: NT = _coconut.collections.namedtuple("_namedtuple_of", fields) else: NT = _coconut.typing.NamedTuple("_namedtuple_of", [(f, t) for f, t in _coconut.zip(fields, types)]) _coconut.copyreg.pickle(NT, lambda nt: (_coconut_mk_anon_namedtuple, (nt._fields, types, nt._asdict()))) - if of_kwargs is None: +{set_NT_match_args} if of_kwargs or of_args: + return NT(*of_args, **of_kwargs) + else: return NT - return NT(**of_kwargs) def _coconut_ndim(arr): arr_mod = _coconut_get_base_module(arr) if (arr_mod in _coconut.numpy_modules or _coconut.hasattr(arr.__class__, "__matconcat__")) and _coconut.hasattr(arr, "ndim"): @@ -2219,7 +2220,22 @@ class _coconut_SupportsInv(_coconut.typing.Protocol): """ def __invert__(self): raise _coconut.NotImplementedError("Protocol methods cannot be called at runtime ((~) in a typing context is a Protocol)") +@_coconut_wraps(_coconut.functools.reduce) +def reduce(function, iterable, initial=_coconut_sentinel): + if initial is _coconut_sentinel: + return _coconut.functools.reduce(function, iterable) + return _coconut.functools.reduce(function, iterable, initial) +class takewhile(_coconut.itertools.takewhile{comma_object}): + __slots__ = () + __doc__ = _coconut.itertools.takewhile.__doc__ + def __new__(cls, predicate, iterable): + return _coconut.itertools.takewhile.__new__(cls, predicate, iterable) +class dropwhile(_coconut.itertools.dropwhile{comma_object}): + __slots__ = () + __doc__ = _coconut.itertools.dropwhile.__doc__ + def __new__(cls, predicate, iterable): + return _coconut.itertools.dropwhile.__new__(cls, predicate, iterable) {def_async_map} {def_aliases} _coconut_self_match_types = {self_match_types} -_coconut_Expected, _coconut_MatchError, _coconut_cartesian_product, _coconut_count, _coconut_cycle, _coconut_enumerate, _coconut_flatten, _coconut_fmap, _coconut_filter, _coconut_groupsof, _coconut_ident, _coconut_lift, _coconut_map, _coconut_mapreduce, _coconut_multiset, _coconut_range, _coconut_reiterable, _coconut_reversed, _coconut_scan, _coconut_starmap, _coconut_tee, _coconut_windowsof, _coconut_zip, _coconut_zip_longest, TYPE_CHECKING, reduce, takewhile, dropwhile = Expected, MatchError, cartesian_product, count, cycle, enumerate, flatten, fmap, filter, groupsof, ident, lift, map, mapreduce, multiset, range, reiterable, reversed, scan, starmap, tee, windowsof, zip, zip_longest, False, _coconut.functools.reduce, _coconut.itertools.takewhile, _coconut.itertools.dropwhile{COMMENT.anything_added_here_should_be_copied_to_stub_file} +TYPE_CHECKING, _coconut_Expected, _coconut_MatchError, _coconut_cartesian_product, _coconut_count, _coconut_cycle, _coconut_enumerate, _coconut_flatten, _coconut_fmap, _coconut_filter, _coconut_groupsof, _coconut_ident, _coconut_lift, _coconut_map, _coconut_mapreduce, _coconut_multiset, _coconut_range, _coconut_reiterable, _coconut_reversed, _coconut_scan, _coconut_starmap, _coconut_tee, _coconut_windowsof, _coconut_zip, _coconut_zip_longest = False, Expected, MatchError, cartesian_product, count, cycle, enumerate, flatten, fmap, filter, groupsof, ident, lift, map, mapreduce, multiset, range, reiterable, reversed, scan, starmap, tee, windowsof, zip, zip_longest{COMMENT.anything_added_here_should_be_copied_to_stub_file} diff --git a/coconut/compiler/util.py b/coconut/compiler/util.py index bd434b363..d733a76bc 100644 --- a/coconut/compiler/util.py +++ b/coconut/compiler/util.py @@ -1925,6 +1925,12 @@ def rem_comment(line): return base +def get_comment(line): + """Extract a comment from a line if it has one.""" + base, comment = split_comment(line) + return comment + + def should_indent(code): """Determines whether the next line should be indented.""" last_line = rem_comment(code.splitlines()[-1]) diff --git a/coconut/constants.py b/coconut/constants.py index 4b10de3b2..243bca2ab 100644 --- a/coconut/constants.py +++ b/coconut/constants.py @@ -101,6 +101,10 @@ def get_path_env_var(env_var, default): and not (PYPY and PY39) and (PY38 or not PY36) ) +NUMPY = ( + not PYPY + and (PY2 or PY34) +) py_version_str = sys.version.split()[0] @@ -135,7 +139,7 @@ def get_path_env_var(env_var, default): streamline_grammar_for_len = 1536 -use_cache_file = True +use_pyparsing_cache_file = True adaptive_any_of_env_var = "COCONUT_ADAPTIVE_ANY_OF" use_adaptive_any_of = get_bool_env_var(adaptive_any_of_env_var, True) @@ -607,15 +611,20 @@ def get_path_env_var(env_var, default): "tuple", "type", "vars", "zip", + 'Ellipsis', "__import__", '__name__', '__file__', '__annotations__', '__debug__', + '__build_class__', + '__loader__', + '__package__', + '__spec__', ) python_exceptions = ( - "BaseException", "BaseExceptionGroup", "GeneratorExit", "KeyboardInterrupt", "SystemExit", "Exception", "ArithmeticError", "FloatingPointError", "OverflowError", "ZeroDivisionError", "AssertionError", "AttributeError", "BufferError", "EOFError", "ExceptionGroup", "BaseExceptionGroup", "ImportError", "ModuleNotFoundError", "LookupError", "IndexError", "KeyError", "MemoryError", "NameError", "UnboundLocalError", "OSError", "BlockingIOError", "ChildProcessError", "ConnectionError", "BrokenPipeError", "ConnectionAbortedError", "ConnectionRefusedError", "ConnectionResetError", "FileExistsError", "FileNotFoundError", "InterruptedError", "IsADirectoryError", "NotADirectoryError", "PermissionError", "ProcessLookupError", "TimeoutError", "ReferenceError", "RuntimeError", "NotImplementedError", "RecursionError", "StopAsyncIteration", "StopIteration", "SyntaxError", "IndentationError", "TabError", "SystemError", "TypeError", "ValueError", "UnicodeError", "UnicodeDecodeError", "UnicodeEncodeError", "UnicodeTranslateError", "Warning", "BytesWarning", "DeprecationWarning", "EncodingWarning", "FutureWarning", "ImportWarning", "PendingDeprecationWarning", "ResourceWarning", "RuntimeWarning", "SyntaxWarning", "UnicodeWarning", "UserWarning", + 'ArithmeticError', 'AssertionError', 'AttributeError', 'BaseException', 'BaseExceptionGroup', 'BlockingIOError', 'BrokenPipeError', 'BufferError', 'BytesWarning', 'ChildProcessError', 'ConnectionAbortedError', 'ConnectionError', 'ConnectionRefusedError', 'ConnectionResetError', 'DeprecationWarning', 'EOFError', 'EncodingWarning', 'EnvironmentError', 'Exception', 'ExceptionGroup', 'FileExistsError', 'FileNotFoundError', 'FloatingPointError', 'FutureWarning', 'GeneratorExit', 'IOError', 'ImportError', 'ImportWarning', 'IndentationError', 'IndexError', 'InterruptedError', 'IsADirectoryError', 'KeyError', 'KeyboardInterrupt', 'LookupError', 'MemoryError', 'ModuleNotFoundError', 'NameError', 'NotADirectoryError', 'NotImplemented', 'NotImplementedError', 'OSError', 'OverflowError', 'PendingDeprecationWarning', 'PermissionError', 'ProcessLookupError', 'RecursionError', 'ReferenceError', 'ResourceWarning', 'RuntimeError', 'RuntimeWarning', 'StopAsyncIteration', 'StopIteration', 'SyntaxError', 'SyntaxWarning', 'SystemError', 'SystemExit', 'TabError', 'TimeoutError', 'TypeError', 'UnboundLocalError', 'UnicodeDecodeError', 'UnicodeEncodeError', 'UnicodeError', 'UnicodeTranslateError', 'UnicodeWarning', 'UserWarning', 'ValueError', 'Warning', 'WindowsError', 'ZeroDivisionError' ) always_keep_parse_name_prefix = "HAS_" @@ -633,7 +642,7 @@ def get_path_env_var(env_var, default): main_prompt = ">>> " more_prompt = " " -default_use_cache_dir = PY34 +default_use_cache_dir = get_bool_env_var("COCONUT_USE_CACHE_DIR", PY34) coconut_cache_dir = "__coconut_cache__" mypy_path_env_var = "MYPYPATH" @@ -1019,6 +1028,9 @@ def get_path_env_var(env_var, default): ("pygments", "py>=39"), "myst-parser", "pydata-sphinx-theme", + # these are necessary to fix a sphinx error + "sphinxcontrib_applehelp", + "sphinxcontrib_htmlhelp", ), "numpy": ( ("numpy", "py<3;cpy"), @@ -1032,6 +1044,7 @@ def get_path_env_var(env_var, default): ("pytest", "py>=36;py<38"), ("pytest", "py38"), "pexpect", + "pytest_remotedata", # fixes a pytest error ), } @@ -1039,22 +1052,23 @@ def get_path_env_var(env_var, default): unpinned_min_versions = { "cPyparsing": (2, 4, 7, 2, 4, 0), ("pre-commit", "py3"): (3,), - ("psutil", "py>=27"): (5,), - "jupyter": (1, 0), + ("psutil", "py>=27"): (6,), + "jupyter": (1, 1), "types-backports": (0, 1), ("futures", "py<3"): (3, 4), ("argparse", "py<27"): (1, 4), "pexpect": (4,), ("trollius", "py<3;cpy"): (2, 2), "requests": (2, 32), - ("numpy", "py39"): (1, 26), ("xarray", "py39"): (2024,), ("dataclasses", "py==36"): (0, 8), ("aenum", "py<34"): (3, 1, 15), "pydata-sphinx-theme": (0, 15), - "myst-parser": (3,), - "sphinx": (7,), - "mypy[python2]": (1, 10), + "myst-parser": (4,), + "sphinx": (8,), + "sphinxcontrib_applehelp": (2,), + "sphinxcontrib_htmlhelp": (2,), + "mypy[python2]": (1, 11), "pyright": (1, 1), ("jupyter-console", "py37"): (6, 6), ("typing", "py<35"): (3, 10), @@ -1062,14 +1076,16 @@ def get_path_env_var(env_var, default): ("ipykernel", "py38"): (6,), ("jedi", "py39"): (0, 19), ("pygments", "py>=39"): (2, 18), - ("xonsh", "py39"): (0, 16), + ("xonsh", "py39"): (0, 18), ("async_generator", "py35"): (1, 10), ("exceptiongroup", "py37;py<311"): (1,), - ("ipython", "py>=310"): (8, 25), + ("ipython", "py>=310"): (8, 27), "py-spy": (0, 3), } pinned_min_versions = { + # don't upgrade this; some extensions implicitly require numpy<2 + ("numpy", "py39"): (1, 26), # don't upgrade this; it breaks xonsh ("pytest", "py38"): (8, 0), # don't upgrade these; they break on Python 3.9 @@ -1083,7 +1099,7 @@ def get_path_env_var(env_var, default): # don't upgrade these; they break on Python 3.6 ("anyio", "py36"): (3,), ("xonsh", "py>=36;py<39"): (0, 11), - ("pandas", "py36"): (1,), + ("pandas", "py36"): (1, 1), ("jupyter-client", "py36"): (7, 1, 2), ("typing_extensions", "py==36"): (4, 1), ("pytest", "py>=36;py<38"): (7,), @@ -1116,6 +1132,7 @@ def get_path_env_var(env_var, default): "papermill": (1, 2), ("numpy", "py<3;cpy"): (1, 16), ("backports.functools-lru-cache", "py<3"): (1, 6), + "pytest_remotedata": (0, 3), # don't upgrade this; it breaks with old IPython versions ("jedi", "py<39"): (0, 17), # Coconut requires pyparsing 2 diff --git a/coconut/icoconut/root.py b/coconut/icoconut/root.py index 7fd1d968c..8afe190a5 100644 --- a/coconut/icoconut/root.py +++ b/coconut/icoconut/root.py @@ -48,7 +48,7 @@ from coconut.terminal import logger from coconut.util import override, memoize_with_exceptions, replace_all from coconut.compiler import Compiler -from coconut.compiler.util import should_indent, paren_change +from coconut.compiler.util import should_indent, paren_change, get_comment from coconut.command.util import Runner try: @@ -214,7 +214,10 @@ def _coconut_assemble_logical_lines(): level += paren_change(no_strs_line) # put line in parts and break if done - if level < 0: + if get_comment(line): + parts.append(line) + break + elif level < 0: parts.append(line) elif no_strs_line.endswith("\\"): parts.append(line[:-1]) diff --git a/coconut/requirements.py b/coconut/requirements.py index c6db84597..b7f1ce2e8 100644 --- a/coconut/requirements.py +++ b/coconut/requirements.py @@ -25,6 +25,7 @@ from coconut.constants import ( CPYTHON, PY34, + NUMPY, IPY, MYPY, XONSH, @@ -249,7 +250,7 @@ def everything_in(req_dict): "docs": unique_wrt(get_reqs("docs"), requirements), "tests": uniqueify_all( get_reqs("tests"), - extras["numpy"], + extras["numpy"] if NUMPY else [], extras["jupyter"] if IPY else [], extras["mypy"] if MYPY else [], extras["xonsh"] if XONSH else [], diff --git a/coconut/root.py b/coconut/root.py index 9c5e80b58..a80341ad3 100644 --- a/coconut/root.py +++ b/coconut/root.py @@ -23,7 +23,7 @@ # VERSION: # ----------------------------------------------------------------------------------------------------------------------- -VERSION = "3.1.1" +VERSION = "3.1.2" VERSION_NAME = None # False for release, int >= 1 for develop DEVELOP = False @@ -59,11 +59,23 @@ def _get_target_info(target): # HEADER: # ----------------------------------------------------------------------------------------------------------------------- +_base_header = r''' +import functools as _coconut_functools +_coconut_getattr = getattr +def _coconut_wraps(base_func): + def wrap(new_func): + new_func_module = _coconut_getattr(new_func, "__module__") + _coconut_functools.update_wrapper(new_func, base_func) + if new_func_module is not None: + new_func.__module__ = new_func_module + return new_func + return wrap +''' + # if a new assignment is added below, a new builtins import should be added alongside it _base_py3_header = r'''from builtins import chr, dict, hex, input, int, map, object, oct, open, print, range, str, super, zip, filter, reversed, enumerate, repr py_bytes, py_chr, py_dict, py_hex, py_input, py_int, py_map, py_object, py_oct, py_open, py_print, py_range, py_str, py_super, py_zip, py_filter, py_reversed, py_enumerate, py_repr, py_min, py_max = bytes, chr, dict, hex, input, int, map, object, oct, open, print, range, str, super, zip, filter, reversed, enumerate, repr, min, max _coconut_py_str, _coconut_py_super, _coconut_py_dict, _coconut_py_min, _coconut_py_max = str, super, dict, min, max -from functools import wraps as _coconut_wraps exec("_coconut_exec = exec") ''' @@ -71,7 +83,6 @@ def _get_target_info(target): _base_py2_header = r'''from __builtin__ import chr, dict, hex, input, int, map, object, oct, open, print, range, str, super, zip, filter, reversed, enumerate, raw_input, xrange, repr, long py_bytes, py_chr, py_dict, py_hex, py_input, py_int, py_map, py_object, py_oct, py_open, py_print, py_range, py_str, py_super, py_zip, py_filter, py_reversed, py_enumerate, py_raw_input, py_xrange, py_repr, py_min, py_max = bytes, chr, dict, hex, input, int, map, object, oct, open, print, range, str, super, zip, filter, reversed, enumerate, raw_input, xrange, repr, min, max _coconut_py_raw_input, _coconut_py_xrange, _coconut_py_int, _coconut_py_long, _coconut_py_print, _coconut_py_str, _coconut_py_super, _coconut_py_unicode, _coconut_py_repr, _coconut_py_dict, _coconut_py_bytes, _coconut_py_min, _coconut_py_max = raw_input, xrange, int, long, print, str, super, unicode, repr, dict, bytes, min, max -from functools import wraps as _coconut_wraps from collections import Sequence as _coconut_Sequence from future_builtins import * chr, str = unichr, unicode @@ -353,7 +364,7 @@ def __repr__(self): __doc__ = getattr(_coconut_py_dict, "__doc__", "")''' + _finish_dict_def _py26_extras = '''if _coconut_sys.version_info < (2, 7): - import functools as _coconut_functools, copy_reg as _coconut_copy_reg + import copy_reg as _coconut_copy_reg def _coconut_new_partial(func, args, keywords): return _coconut_functools.partial(func, *(args if args is not None else ()), **(keywords if keywords is not None else {})) _coconut_copy_reg.constructor(_coconut_new_partial) @@ -392,7 +403,7 @@ def _get_root_header(version="universal"): ''' + _indent(_get_root_header("3")) version_info = _get_target_info(version) - header = "" + header = _base_header if version.startswith("3"): header += _base_py3_header diff --git a/coconut/tests/main_test.py b/coconut/tests/main_test.py index 07b3a04c2..e911da599 100644 --- a/coconut/tests/main_test.py +++ b/coconut/tests/main_test.py @@ -168,6 +168,7 @@ def pexpect(p, out): "tutorial.py", "unused 'type: ignore' comment", "site-packages/numpy", + ".py: error:" ) ignore_atexit_errors_with = ( @@ -1054,11 +1055,8 @@ def test_any_of(self): }): run() - def test_keep_lines(self): - run(["--keep-lines"]) - - def test_strict(self): - run(["--strict"]) + def test_strict_keep_lines(self): + run(["--strict", "--keep-lines"]) def test_and(self): run(["--and"]) # src and dest built by comp @@ -1095,7 +1093,7 @@ class TestExternal(unittest.TestCase): if not PYPY or PY2: def test_prelude(self): with using_paths(prelude): - comp_prelude() + comp_prelude(expect_retcode=None) if MYPY and PY38: run_prelude() @@ -1111,11 +1109,12 @@ def test_bbopt(self): # if PY38: # run_pyprover() - def test_pyston(self): - with using_paths(pyston): - comp_pyston(["--no-tco"]) - if PYPY and PY2: - run_pyston() + if PY312: # reduce test load + def test_pyston(self): + with using_paths(pyston): + comp_pyston(["--no-tco"]) + if PYPY and PY2: + run_pyston() # ----------------------------------------------------------------------------------------------------------------------- diff --git a/coconut/tests/src/cocotest/agnostic/main.coco b/coconut/tests/src/cocotest/agnostic/main.coco index 56bfad400..7e92d3a8a 100644 --- a/coconut/tests/src/cocotest/agnostic/main.coco +++ b/coconut/tests/src/cocotest/agnostic/main.coco @@ -35,7 +35,7 @@ def package_test(outer_MatchError) -> bool: assert MatchError() `isinstance` outer_MatchError, (MatchError, outer_MatchError) assert outer_MatchError() `isinstance` MatchError, (outer_MatchError, MatchError) assert_raises((raise)$(outer_MatchError), MatchError) - assert_raises((raise)$(MatchError), outer_MatchError) + assert_raises((raise)$(MatchError), outer_MatchError) # type: ignore def raises_outer_MatchError(obj=None): raise outer_MatchError("raises_outer_MatchError") match raises_outer_MatchError -> None in 10: diff --git a/coconut/tests/src/cocotest/agnostic/primary_1.coco b/coconut/tests/src/cocotest/agnostic/primary_1.coco index bfe7888cf..299e3e7e3 100644 --- a/coconut/tests/src/cocotest/agnostic/primary_1.coco +++ b/coconut/tests/src/cocotest/agnostic/primary_1.coco @@ -90,7 +90,7 @@ def primary_test_1() -> bool: \\assert data == 3 \\def backslash_test(): return (x) -> x - assert \(1) == 1 == backslash_test()(1) + assert \(1) == 1 == backslash_test()(1) # NOQA assert True is (\( "hello" ) == "hello" == \( @@ -100,7 +100,7 @@ def primary_test_1() -> bool: x, y): return x + y - assert multiline_backslash_test(1, 2) == 3 + assert multiline_backslash_test(1, 2) == 3 # noqa \\ assert True class one_line_class: pass assert isinstance(one_line_class(), one_line_class) diff --git a/coconut/tests/src/cocotest/agnostic/primary_2.coco b/coconut/tests/src/cocotest/agnostic/primary_2.coco index e95fa4c61..7d1645840 100644 --- a/coconut/tests/src/cocotest/agnostic/primary_2.coco +++ b/coconut/tests/src/cocotest/agnostic/primary_2.coco @@ -466,6 +466,31 @@ def primary_test_2() -> bool: """ == '\n"2"\n' assert f"\{1}" == "\\1" assert f''' '{1}' ''' == " '1' " + tuple(x=) = (x=4) + assert x == 4 + tuple(x=, y=) = (x=5, y=5) + assert x == 5 == y + data tuple(x=) = (x=6) + assert x == 6 + class tuple(x=) = (x=7) + assert x == 7 + data tuple(x, y=) = (x=8, y=8) + assert x == 8 == y + (x=, y=) = (x=9, y=9) + assert x == 9 == y + (x=x) = (x=10) + assert x == 10 + (y=y, x=) = (x=11, y=11) + assert x == 11 == y + tuple(x=) = (x=12, y=12) + assert x == 12 + match (x=) in (x=13, y=13): + assert False + assert x == 12 + assert (x=1).__match_args__ == ('x',) # type: ignore + assert reduce(function=(+), iterable=range(5), initial=-1) == 9 # type: ignore + assert takewhile(predicate=ident, iterable=[1, 2, 1, 0, 1]) |> list == [1, 2, 1] # type: ignore + assert dropwhile(predicate=(not), iterable=range(5)) |> list == [1, 2, 3, 4] # type: ignore with process_map.multiple_sequential_calls(): # type: ignore assert map((+), range(3), range(4)$[:-1], strict=True) |> list == [0, 2, 4] == process_map((+), range(3), range(4)$[:-1], strict=True) |> list # type: ignore diff --git a/coconut/tests/src/cocotest/agnostic/suite.coco b/coconut/tests/src/cocotest/agnostic/suite.coco index ae651af80..3589aedfb 100644 --- a/coconut/tests/src/cocotest/agnostic/suite.coco +++ b/coconut/tests/src/cocotest/agnostic/suite.coco @@ -178,7 +178,7 @@ def suite_test() -> bool: assert init_last([1,2,3]) == ([1,2], 3) assert last_two([1,2,3]) == (2, 3) == last_two_([1,2,3]) assert expl_ident(5) == 5 == ident(5) - assert mod$ <| 5 <| 3 == 2 == (%)$ <| 5 <| 3 + assert mod$ <| 5 <| 3 == 2 == (%)$ <| 5 <| 3 # type: ignore assert 5 |> dectest == 5 try: raise ValueError() diff --git a/coconut/tests/src/cocotest/agnostic/tutorial.coco b/coconut/tests/src/cocotest/agnostic/tutorial.coco index 3eeabae34..cd96b0a08 100644 --- a/coconut/tests/src/cocotest/agnostic/tutorial.coco +++ b/coconut/tests/src/cocotest/agnostic/tutorial.coco @@ -22,13 +22,15 @@ assert range(1, 5) |> product == 24 first_five_words = .split() ..> .$[:5] ..> " ".join assert first_five_words("ab cd ef gh ij kl") == "ab cd ef gh ij" -@recursive_iterator +# TODO: recursive_iterator -> recursive_generator +@recursive_iterator # noqa def fib() = (1, 1) :: map((+), fib(), fib()$[1:]) assert fib()$[:5] |> list == [1, 1, 2, 3, 5] +# TODO: parallel_map -> process_map # can't use parallel_map here otherwise each process would have to rerun all # the tutorial tests since we don't guard them behind __name__ == "__main__" -assert range(100) |> concurrent_map$(.**2) |> list |> .$[-1] == 9801 +assert range(100) |> thread_map$(.**2) |> list |> .$[-1] == 9801 def factorial(n, acc=1): match n: diff --git a/coconut/tests/src/cocotest/target_2/py2_test.coco b/coconut/tests/src/cocotest/target_2/py2_test.coco index cf8ef713e..b9711f614 100644 --- a/coconut/tests/src/cocotest/target_2/py2_test.coco +++ b/coconut/tests/src/cocotest/target_2/py2_test.coco @@ -4,5 +4,5 @@ def py2_test() -> bool: assert py_map((+)$(2), range(5)) == [2, 3, 4, 5, 6] assert py_range(5) == [0, 1, 2, 3, 4] assert not isinstance(long(1), py_int) # type: ignore - assert py_str(3) == b"3" == unicode(b"3") # type: ignore + assert py_str(3) == b"3" == unicode(b"3") # noqa # type: ignore return True diff --git a/coconut/tests/src/extras.coco b/coconut/tests/src/extras.coco index 13c69496f..d8c359f5c 100644 --- a/coconut/tests/src/extras.coco +++ b/coconut/tests/src/extras.coco @@ -7,8 +7,7 @@ os.environ["COCONUT_USE_COLOR"] = "False" from coconut.__coconut__ import consume as coc_consume from coconut.constants import ( IPY, - PY2, - PY34, + NUMPY, PY35, PY36, PY39, @@ -414,6 +413,16 @@ import abc except CoconutStyleError as err: assert str(err) == """found unused import 'abc' (add '# NOQA' to suppress) (remove --strict to downgrade to a warning) (line 1) import abc""" + try: + parse(""" +1 +2 + x +3 + """.strip()) + except CoconutStyleError as err: + assert str(err) == """found undefined name 'x' (add '# NOQA' to suppress) (remove --strict to downgrade to a warning) (line 2) + 2 + x + ^""" assert_raises(-> parse(""" class A(object): 1 @@ -602,6 +611,15 @@ def test_kernel() -> bool: assert captured_msg_content is None assert captured_msg_type["content"]["data"]["text/plain"] == "'()'" + assert k.do_execute("""[ + "hey", + # "there", # dont want this value now + "is comment" +]""", False, True, {}, True) |> unwrap_future$(loop) |> .["status"] == "ok" + captured_msg_type, captured_msg_content = fake_session.captured_messages[-1] + assert captured_msg_content is None + assert captured_msg_type["content"]["data"]["text/plain"] == "['hey', 'is comment']" + return True @@ -743,13 +761,13 @@ def test_xarray() -> bool: def test_extras() -> bool: - if not PYPY and (PY2 or PY34): + if NUMPY: assert test_numpy() is True print(".", end="") - if not PYPY and PY36: + if NUMPY and PY36: assert test_pandas() is True # . print(".", end="") - if not PYPY and PY39: + if NUMPY and PY39: assert test_xarray() is True # .. print(".") # newline bc we print stuff after this assert test_setup_none() is True # ...