Skip to content

Commit

Permalink
Set the working dir and shadow file flag for mypy (#9)
Browse files Browse the repository at this point in the history
* Run mypy from inferred python project root

* Remove leading dot from temp file name

This appears to break relative imports in files checked by mypy. Maybe
make the temp file pattern configurable per checker?

* Use mypy --shadow-file for buffers with a file

Fall back to the buffer name when `buffer-file-name' is nil. Necessary
for checking temp files since python module names are based on the
file name.

* Parse mypy diagnostic ids

* Make the mypy working dir configurable

* Add basic tests for mypy

* Add require for project

* Move required mypy flags out of custom variable

* Fix parsing of mypy error ids with hyphens

* Add temp-file-prefix paramter

* Update src/checkers/flymake-collection-mypy.el

Co-authored-by: dmitrig <34632025+dgarbuzov@users.noreply.github.com>
Co-authored-by: Mohsin Kaleem <mohkale@kisara.moe>
  • Loading branch information
3 people authored Dec 19, 2022
1 parent 274e5ec commit 74e2f3f
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 21 deletions.
93 changes: 80 additions & 13 deletions src/checkers/flymake-collection-mypy.el
Original file line number Diff line number Diff line change
Expand Up @@ -26,34 +26,101 @@

;;; Code:

(require 'project)
(require 'flymake)
(require 'flymake-collection)

(eval-when-compile
(require 'flymake-collection-define))

(defcustom flymake-collection-mypy-args
'("--show-error-codes")
"Command line arguments always passed to `flymake-collection-mypy'."
:type '(repeat (string :tag "Arg"))
:group 'flymake-collection)

(defcustom flymake-collection-mypy-project-root
'(("mypy.ini" "project.toml" "setup.cfg") project-root default-directory)
"Method to set project root."
:type '(repeat :tag "Run mypy from the first choice that succeeds"
(choice (const :tag "The buffer default-directory" default-directory)
(const :tag "The current project root" project-root)
(directory :tag "A specific directory")
(repeat :tag "The first ancestor directory containing"
:value ("mypy.ini" "pyproject.toml" "setup.cfg")
(string :tag "File name"))))
:group 'flymake-collection
:safe 'listp)

(defun flymake-collection-mypy--locate-dominating-files (buffer files)
"Find ancestor directory of `BUFFER' containing any of `FILES'."
(let* ((start (if-let ((file (buffer-file-name buffer)))
(file-name-directory file)
(buffer-local-value 'default-directory buffer)))
(regex (mapconcat 'regexp-quote files "\\|"))
(root (locate-dominating-file
start (lambda (dir) (directory-files dir nil regex t)))))
root))

(defun flymake-collection-mypy--default-directory (buffer)
"Find a directory from which to run mypy to check `BUFFER'.
Try each method specified in `flymake-collection-mypy-project-root' in
order and returns the first non-nil result."
(cl-dolist (spec flymake-collection-mypy-project-root)
(when-let
((res (cond
((eq spec 'default-directory)
(buffer-local-value 'default-directory buffer))
((eq spec 'project-root)
(when-let ((proj (project-current)))
(project-root proj)))
((listp spec)
(flymake-collection-mypy--locate-dominating-files
buffer spec))
((stringp spec) spec))))
(cl-return res))))


;;;###autoload (autoload 'flymake-collection-mypy "flymake-collection-mypy")
(flymake-collection-define-rx flymake-collection-mypy
"Mypy syntax and type checker. Requires mypy>=0.580.
See URL `http://mypy-lang.org/'."
:title "mypy"
:pre-let ((mypy-exec (executable-find "mypy")))
:pre-check (unless mypy-exec
(error "Cannot find mypy executable"))
:pre-let ((mypy-exec (executable-find "mypy"))
(default-directory (flymake-collection-mypy--default-directory
flymake-collection-source)))
:pre-check (progn
(flymake-log :debug "Working dir is %s" default-directory)
(unless mypy-exec
(error "Cannot find mypy executable"))
(unless default-directory
(error "Default dir is nil: check `flymake-collection-mypy-project-root'")))
:write-type 'file
:source-inplace t
:command (list mypy-exec
"--show-column-numbers"
"--no-error-summary"
"--no-color-output"
"--show-absolute-path"
"--show-error-codes"
flymake-collection-temp-file)
:command `(,mypy-exec
"--show-column-numbers"
"--show-absolute-path"
"--no-error-summary"
"--no-color-output"
,@flymake-collection-mypy-args
,@(if-let ((source-file (buffer-file-name
flymake-collection-source))
((file-exists-p source-file)))
(list
"--shadow-file" source-file flymake-collection-temp-file
source-file)
(list flymake-collection-temp-file)))
;; Temporary file cannot start with a dot for mypy, see
;; https://github.com/mohkale/flymake-collection/pull/9
:temp-file-prefix "flymake_mypy_"
:regexps
((error bol (file-name) ":" line ":" column ": error: " (message) eol)
(warning bol (file-name) ":" line ":" column ": warning: " (message) eol)
(note bol (file-name) ":" line ":" column ": note: " (message) eol)))
((error bol (file-name) ":" line ":" column ": error: "
(message) (opt " [" (id (* graph)) "]") eol)
(warning bol (file-name) ":" line ":" column ": warning: "
(message) (opt " [" (id (* graph)) "]") eol)
(note bol (file-name) ":" line ":" column ": note: "
(message) (opt " [" (id (* graph)) "]") eol)))

(provide 'flymake-collection-mypy)

Expand Down
24 changes: 16 additions & 8 deletions src/flymake-collection-define.el
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,13 @@ killed and replaced with the new check.")

(define-obsolete-function-alias 'flymake-rest-define 'flymake-collection-define "2.0.0")

(defun flymake-collection-define--temp-file (temp-dir temp-file source-inplace)
(defun flymake-collection-define--temp-file
(temp-dir temp-file source-inplace temp-file-prefix)
"Let forms for defining a temporary directory and file.
TEMP-DIR and TEMP-FILE are the symbols used for the corresponding variables.
SOURCE-INPLACE specifies whether the TEMP-DIR should be in the same working
directory as the current buffer."
directory as the current buffer. Temporary files are named by concatenating
TEMP-FILE-PREFIX with the current buffer file name."
`((,temp-dir
,@(let ((forms
(append
Expand All @@ -84,7 +86,7 @@ doesn't exist: %s" dir))
(let ((temporary-file-directory ,temp-dir)
(basename (file-name-nondirectory (or (buffer-file-name)
(buffer-name)))))
(make-temp-file ".flymake_" nil (concat "_" basename))))))
(make-temp-file ,temp-file-prefix nil (concat "_" basename))))))

(defmacro flymake-collection-define--parse-diags
(title proc-symb diags-symb current-diag-symb source-symb error-parser)
Expand Down Expand Up @@ -132,7 +134,7 @@ CURRENT-DIAGS-SYMB, SOURCE-SYMB, ERROR-PARSER are all described in
(cl-defmacro flymake-collection-define
(name docstring
&optional &key title command error-parser write-type
source-inplace pre-let pre-check)
source-inplace pre-let pre-check (temp-file-prefix ".flymake_"))
"Quickly define a backend function for use with Flymake.
Define a function NAME which is suitable for use with the variable
`flymake-diagnostic-functions'. DOCSTRING if given will become the
Expand Down Expand Up @@ -192,7 +194,11 @@ used to start the checker process. It should be suitable for use as the
ERROR-PARSER is a lisp-form that should, each time it is evaluated,
return the next diagnostic from the checker output. The result should be
a value that can be passed to the `flymake-make-diagnostic' function. Once
there are no more diagnostics to parse this form should evaluate to nil."
there are no more diagnostics to parse this form should evaluate to nil.
TEMP-FILE-PREFIX overrides the prefix of temporary file names created by
the checker. This is useful for checker programs that have issues running
on hidden files."
(declare (indent defun) (doc-string 2))
(unless lexical-binding
(error "Need lexical-binding for flymake-collection-define (%s)" name))
Expand Down Expand Up @@ -228,7 +234,7 @@ there are no more diagnostics to parse this form should evaluate to nil."
(let* ((,source-symb (current-buffer))
,@(when (eq write-type 'file)
(flymake-collection-define--temp-file
temp-dir-symb temp-file-symb source-inplace))
temp-dir-symb temp-file-symb source-inplace temp-file-prefix))
,@pre-let)
;; With vars defined, do pre-check.
,@(when pre-check
Expand Down Expand Up @@ -441,13 +447,14 @@ For an example of this macro in action, see `flymake-collection-pycodestyle'."

(cl-defmacro flymake-collection-define-rx
(name docstring
&optional &key title command write-type source-inplace pre-let pre-check regexps)
&optional &key title command write-type
source-inplace pre-let pre-check temp-file-prefix regexps)
"`flymake-collection-define' helper using `rx' syntax to parse diagnostics.
This helper macro adapts `flymake-collection-define' to use an error-parser
built from a collections of REGEXPS (see `flymake-collection-define--parse-rx').
See `flymake-collection-define' for a description of NAME, DOCSTRING, TITLE,
COMMAND,WRITE-TYPE, SOURCE-INPLACE, PRE-LET, and PRE-CHECK."
COMMAND,WRITE-TYPE, SOURCE-INPLACE, PRE-LET, PRE-CHECK, and TEMP-FILE-PREFIX."
(declare (indent defun) (doc-string 2))
`(flymake-collection-define ,name
,docstring
Expand All @@ -457,6 +464,7 @@ COMMAND,WRITE-TYPE, SOURCE-INPLACE, PRE-LET, and PRE-CHECK."
:source-inplace ,source-inplace
:pre-let ,pre-let
:pre-check ,pre-check
:temp-file-prefix ,temp-file-prefix
:error-parser
(flymake-collection-define--parse-rx ,regexps)))

Expand Down
1 change: 1 addition & 0 deletions tests/checkers/installers/mypy.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
python3.8 -m pip install mypy
47 changes: 47 additions & 0 deletions tests/checkers/test-cases/mypy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
checker: flymake-collection-mypy
tests:
- name: no-lints
file: |
"""A test case with no output from mypy."""
print("hello world")
lints: []
- name: error-operator
file: |
"""Test parsing an error."""
"x" + 1
lints:
- point: [3, 6]
level: error
message: operator Unsupported operand types for + ("str" and "int") (mypy)
- name: error-attr-defined
file: |
"""Test parsing an error with a hyphen in the id."""
x = 1
x.foo
lints:
- point: [4, 0]
level: error
message: attr-defined "int" has no attribute "foo" (mypy)
- name: note-reveal
file: |
"""Test parsing a note."""
foo = "bar"
reveal_type(foo)
lints:
- point: [4, 12]
level: note
message: Revealed type is "builtins.str" (mypy)
- name: error-syntax
file: |
"""Test syntax error."""
class Foo:
lints:
- point: [3, 0]
level: error
message: syntax unexpected EOF while parsing (mypy)

0 comments on commit 74e2f3f

Please sign in to comment.