Skip to content

Commit

Permalink
Add initial pyright support
Browse files Browse the repository at this point in the history
Refs   #785.
  • Loading branch information
evhub committed Jun 7, 2024
1 parent 03eade4 commit 01e6b34
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 50 deletions.
20 changes: 13 additions & 7 deletions DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ The full list of optional dependencies is:
- `kernel`: lightweight subset of `jupyter` that only includes the dependencies that are strictly necessary for Coconut's [Jupyter kernel](#kernel).
- `watch`: enables use of the `--watch` flag.
- `mypy`: enables use of the `--mypy` flag.
- `pyright`: enables use of the `--pyright` flag.
- `xonsh`: enables use of Coconut's [`xonsh` support](#xonsh-support).
- `numpy`: installs everything necessary for making use of Coconut's [`numpy` integration](#numpy-integration).
- `jupyterlab`: installs everything necessary to use [JupyterLab](https://github.com/jupyterlab/jupyterlab) with Coconut.
Expand Down Expand Up @@ -121,11 +122,11 @@ depth: 1

```
coconut [-h] [--and source [dest ...]] [-v] [-t version] [-i] [-p] [-a] [-l]
[--no-line-numbers] [-k] [-w] [-r] [-n] [-d] [-q] [-s] [--no-tco]
[--no-wrap-types] [-c code] [--incremental] [-j processes] [-f] [--minify]
[--jupyter ...] [--mypy ...] [--argv ...] [--tutorial] [--docs] [--style name]
[--vi-mode] [--recursion-limit limit] [--stack-size kbs] [--site-install]
[--site-uninstall] [--verbose] [--trace] [--profile]
[--no-line-numbers] [-k] [-w] [-r] [-n] [-d] [-q] [-s] [--no-tco] [--no-wrap-types]
[-c code] [-j processes] [-f] [--minify] [--jupyter ...] [--mypy ...] [--pyright]
[--argv ...] [--tutorial] [--docs] [--style name] [--vi-mode]
[--recursion-limit limit] [--stack-size kbs] [--fail-fast] [--no-cache]
[--site-install] [--site-uninstall] [--verbose] [--trace] [--profile]
[source] [dest]
```

Expand Down Expand Up @@ -184,6 +185,7 @@ dest destination directory for compiled files (defaults to
Jupyter)
--mypy ... run MyPy on compiled Python (remaining args passed to MyPy) (implies
--package --line-numbers)
--pyright run Pyright on compiled Python (implies --package)
--argv ..., --args ...
set sys.argv to source plus remaining args for use in the Coconut script
being run
Expand Down Expand Up @@ -452,6 +454,10 @@ You can also run `mypy`—or any other static type checker—directly on the com

To distribute your code with checkable type annotations, you'll need to include `coconut` as a dependency (though a `--no-deps` install should be fine), as installing it is necessary to make the requisite stub files available. You'll also probably want to include a [`py.typed`](https://peps.python.org/pep-0561/) file.

##### Pyright Integration

Though not as well-supported as MyPy, Coconut also has built-in [Pyright](https://github.com/microsoft/pyright) support. Simply pass `--pyright` to automatically run Pyright on all compiled code. To adjust Pyright options, rather than pass them at the command-line, add your settings to the file `~/.coconut_pyrightconfig.json` (automatically generated the first time `coconut --pyright` is run).

##### Syntax

To explicitly annotate your code with types to be checked, Coconut supports (on all Python versions):
Expand All @@ -467,7 +473,7 @@ Sometimes, MyPy will not know how to handle certain Coconut constructs, such as

##### Interpreter

Coconut even supports `--mypy` in the interpreter, which will intelligently scan each new line of code, in the context of previous lines, for newly-introduced MyPy errors. For example:
Coconut even supports `--mypy` (though not `--pyright`) in the interpreter, which will intelligently scan each new line of code, in the context of previous lines, for newly-introduced MyPy errors. For example:
```coconut_pycon
>>> a: str = count()[0]
<string>:14: error: Incompatible types in assignment (expression has type "int", variable has type "str")
Expand Down Expand Up @@ -4655,7 +4661,7 @@ else:

#### `reveal_type` and `reveal_locals`

When using MyPy, `reveal_type(<expr>)` will cause MyPy to print the type of `<expr>` and `reveal_locals()` will cause MyPy to print the types of the current `locals()`. At runtime, `reveal_type(x)` is always the identity function and `reveal_locals()` always returns `None`. See [the MyPy documentation](https://mypy.readthedocs.io/en/stable/common_issues.html#reveal-type) for more information.
When using static type analysis tools integrated with Coconut such as [MyPy](#mypy-integration), `reveal_type(<expr>)` will cause MyPy to print the type of `<expr>` and `reveal_locals()` will cause MyPy to print the types of the current `locals()`. At runtime, `reveal_type(x)` is always the identity function and `reveal_locals()` always returns `None`. See [the MyPy documentation](https://mypy.readthedocs.io/en/stable/common_issues.html#reveal-type) for more information.

##### Example

Expand Down
8 changes: 8 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,14 @@ test-mypy-tests: clean-no-tests
python ./coconut/tests/dest/runner.py
python ./coconut/tests/dest/extras.py

# same as test-mypy but uses pyright instead
.PHONY: test-pyright
test-pyright: export COCONUT_USE_COLOR=TRUE
test-pyright: clean
python ./coconut/tests --strict --keep-lines --force --target sys --no-cache --pyright
python ./coconut/tests/dest/runner.py
python ./coconut/tests/dest/extras.py

# same as test-univ but includes verbose output for better debugging
# regex for getting non-timing lines: ^(?!'|\s*(Time|Packrat|Loaded|Saving|Adaptive|Errorless|Grammar|Failed|Incremental|Pruned|Compiled)\s)[^\n]*\n*
.PHONY: test-verbose
Expand Down
6 changes: 6 additions & 0 deletions coconut/command/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,12 @@
help="run MyPy on compiled Python (remaining args passed to MyPy) (implies --package --line-numbers)",
)

arguments.add_argument(
"--pyright",
action="store_true",
help="run Pyright on compiled Python (implies --package)",
)

arguments.add_argument(
"--argv", "--args",
type=str,
Expand Down
68 changes: 42 additions & 26 deletions coconut/command/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,6 @@
first_import_time,
)
from coconut.command.util import (
writefile,
readfile,
showpath,
rem_encoding,
Runner,
Expand All @@ -104,6 +102,7 @@
run_with_stack_size,
proc_run_args,
get_python_lib,
update_pyright_config,
)
from coconut.compiler.util import (
should_indent,
Expand All @@ -128,6 +127,7 @@ class Command(object):
display = False # corresponds to --display flag
jobs = 0 # corresponds to --jobs flag
mypy_args = None # corresponds to --mypy flag
pyright = False # corresponds to --pyright flag
argv_args = None # corresponds to --argv flag
stack_size = 0 # corresponds to --stack-size flag
use_cache = USE_CACHE # corresponds to --no-cache flag
Expand Down Expand Up @@ -252,15 +252,17 @@ def execute_args(self, args, interact=True, original_args=None):
logger.log("Directly passed args:", original_args)
logger.log("Parsed args:", args)

type_checking_arg = "--mypy" if args.mypy else "--pyright" if args.pyright else None

# validate args and show warnings
if args.stack_size and args.stack_size % 4 != 0:
logger.warn("--stack-size should generally be a multiple of 4, not {stack_size} (to support 4 KB pages)".format(stack_size=args.stack_size))
if args.mypy is not None and args.no_line_numbers:
logger.warn("using --mypy running with --no-line-numbers is not recommended; mypy error messages won't include Coconut line numbers")
if args.interact and args.run:
logger.warn("extraneous --run argument passed; --interact implies --run")
if args.package and self.mypy:
logger.warn("extraneous --package argument passed; --mypy implies --package")
if args.package and type_checking_arg:
logger.warn("extraneous --package argument passed; --{type_checking_arg} implies --package".format(type_checking_arg=type_checking_arg))

# validate args and raise errors
if args.line_numbers and args.no_line_numbers:
Expand All @@ -269,10 +271,10 @@ def execute_args(self, args, interact=True, original_args=None):
raise CoconutException("cannot --site-install and --site-uninstall simultaneously")
if args.standalone and args.package:
raise CoconutException("cannot compile as both --package and --standalone")
if args.standalone and self.mypy:
raise CoconutException("cannot compile as both --package (implied by --mypy) and --standalone")
if args.no_write and self.mypy:
raise CoconutException("cannot compile with --no-write when using --mypy")
if args.standalone and type_checking_arg:
raise CoconutException("cannot compile as both --package (implied by --{type_checking_arg}) and --standalone".format(type_checking_arg=type_checking_arg))
if args.no_write and type_checking_arg:
raise CoconutException("cannot compile with --no-write when using --{type_checking_arg}".format(type_checking_arg=type_checking_arg))
for and_args in getattr(args, "and") or []:
if len(and_args) > 2:
raise CoconutException(
Expand All @@ -291,6 +293,7 @@ def execute_args(self, args, interact=True, original_args=None):
set_recursion_limit(args.recursion_limit)
self.fail_fast = args.fail_fast
self.display = args.display
self.pyright = args.pyright
self.prompt.vi_mode = args.vi_mode
if args.style is not None:
self.prompt.set_style(args.style)
Expand Down Expand Up @@ -375,8 +378,8 @@ def execute_args(self, args, interact=True, original_args=None):
for kwargs in all_compile_path_kwargs:
filepaths += self.compile_path(**kwargs)

# run mypy on compiled files
self.run_mypy(filepaths)
# run type checking on compiled files
self.run_type_checking(filepaths)

# do extra compilation if there is any
if extra_compile_path_kwargs:
Expand Down Expand Up @@ -456,7 +459,7 @@ def process_source_dest(self, source, dest, args):
processed_dest = dest

# determine package mode
if args.package or self.mypy:
if args.package or self.type_checking:
package = True
elif args.standalone:
package = False
Expand Down Expand Up @@ -576,7 +579,7 @@ def compile_file(self, filepath, write=True, package=False, force=False, **kwarg
def compile(self, codepath, destpath=None, package=False, run=False, force=False, show_unchanged=True, handling_exceptions_kwargs={}, callback=None):
"""Compile a source Coconut file to a destination Python file."""
with univ_open(codepath, "r") as opened:
code = readfile(opened)
code = opened.read()

package_level = -1
if destpath is not None:
Expand Down Expand Up @@ -607,7 +610,7 @@ def inner_callback(compiled):
logger.show_tabulated("Compiled", showpath(codepath), "without writing to file.")
else:
with univ_open(destpath, "w") as opened:
writefile(opened, compiled)
opened.write(compiled)
logger.show_tabulated("Compiled to", showpath(destpath), ".")
if self.display:
logger.print(compiled)
Expand Down Expand Up @@ -657,7 +660,7 @@ def create_package(self, dirpath, retries_left=create_package_retries):
filepath = os.path.join(dirpath, "__coconut__.py")
try:
with univ_open(filepath, "w") as opened:
writefile(opened, self.comp.getheader("__coconut__"))
opened.write(self.comp.getheader("__coconut__"))
except OSError:
logger.log_exc()
if retries_left <= 0:
Expand Down Expand Up @@ -792,7 +795,7 @@ def has_hash_of(self, destpath, code, package_level):
"""Determine if a file has the hash of the code."""
if destpath is not None and os.path.isfile(destpath):
with univ_open(destpath, "r") as opened:
compiled = readfile(opened)
compiled = opened.read()
hashash = gethash(compiled)
if hashash is not None:
newhash = self.comp.genhash(code, package_level)
Expand Down Expand Up @@ -880,7 +883,7 @@ def execute(self, compiled=None, path=None, use_eval=False, allow_show=True):
logger.print(compiled)

if path is None: # header is not included
if not self.mypy:
if not self.type_checking:
no_str_code = self.comp.remove_strs(compiled)
if no_str_code is not None:
result = mypy_builtin_regex.search(no_str_code)
Expand All @@ -892,7 +895,7 @@ def execute(self, compiled=None, path=None, use_eval=False, allow_show=True):

self.runner.run(compiled, use_eval=use_eval, path=path, all_errors_exit=path is not None)

self.run_mypy(code=self.runner.was_run_code())
self.run_type_checking(code=self.runner.was_run_code())

def execute_file(self, destpath, **kwargs):
"""Execute compiled file."""
Expand All @@ -912,15 +915,20 @@ def check_runner(self, set_sys_vars=True, argv_source_path=""):

# set up runner
if self.runner is None:
self.runner = Runner(self.comp, exit=self.exit_runner, store=self.mypy)
self.runner = Runner(self.comp, exit=self.exit_runner, store=self.type_checking)

# pass runner to prompt
self.prompt.set_runner(self.runner)

@property
def mypy(self):
"""Whether using MyPy or not."""
return self.mypy_args is not None
def type_checking(self):
"""Whether using a static type-checker or not."""
return self.mypy_args is not None or self.pyright

@property
def type_checking_version(self):
"""What version of Python to type check against."""
return ver_tuple_to_str(get_target_info_smart(self.comp.target, mode="highest"))

def set_mypy_args(self, mypy_args=None):
"""Set MyPy arguments."""
Expand All @@ -940,7 +948,7 @@ def set_mypy_args(self, mypy_args=None):
if not any(arg.startswith("--python-version") for arg in self.mypy_args):
self.mypy_args += [
"--python-version",
ver_tuple_to_str(get_target_info_smart(self.comp.target, mode="highest")),
self.type_checking_version,
]

if not any(arg.startswith("--python-executable") for arg in self.mypy_args):
Expand All @@ -960,9 +968,9 @@ def set_mypy_args(self, mypy_args=None):
logger.log("MyPy args:", self.mypy_args)
self.mypy_errs = []

def run_mypy(self, paths=(), code=None):
"""Run MyPy with arguments."""
if self.mypy:
def run_type_checking(self, paths=(), code=None):
"""Run type-checking on the given paths / code."""
if self.mypy_args is not None:
set_mypy_path()
from coconut.command.mypy import mypy_run
args = list(paths) + self.mypy_args
Expand All @@ -987,6 +995,14 @@ def run_mypy(self, paths=(), code=None):
if code is not None: # interpreter
logger.printerr(line)
self.mypy_errs.append(line)
if self.pyright:
config_file = update_pyright_config()
if code is not None:
logger.warn("--pyright only works on files, not code snippets or at the interpreter")
if paths:
from pyright import main
args = ["--project", config_file, "--pythonversion", self.type_checking_version] + list(paths)
main(args)

def run_silent_cmd(self, *args):
"""Same as run_cmd$(show_output=logger.verbose)."""
Expand Down Expand Up @@ -1157,7 +1173,7 @@ def error_callback(err):
writedir = os.path.join(dest, os.path.relpath(dirpath, src))

def inner_callback(path):
self.run_mypy([path])
self.run_type_checking([path])
callback()
self.compile_path(
path,
Expand Down
7 changes: 0 additions & 7 deletions coconut/command/resources/pyrightconfig.json

This file was deleted.

Loading

0 comments on commit 01e6b34

Please sign in to comment.