问题的出现与分析

有一阵子没仔细琢磨Emacs的Python配置,最近几天在开发一个项目的时候遇到一个报错。

Suspicious state from syntax checker python-flake8: Flycheck checker python-flake8 returned non-zero exit code 1, but its output contained no errors: Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/lib64/python3.6/runpy.py", line 14, in <module>
    import importlib.machinery # importlib first so we can test #15386 via -m
  File "/usr/lib64/python3.6/importlib/__init__.py", line 57, in <module>
    import types
  File "/usr/lib64/python3.6/types.py", line 171, in <module>
    import functools as _functools
  File "/usr/lib64/python3.6/functools.py", line 21, in <module>
    from collections import namedtuple
  File "/usr/lib64/python3.6/collections/__init__.py", line 27, in <module>
    from keyword import iskeyword as _iskeyword
  File "/home/momoka/projects/qianka/hebe/hebe/services/subtask/keyword.py", line 3, in <module>
    import logging
  File "/usr/lib64/python3.6/logging/__init__.py", line 26, in <module>
    import sys, os, time, io, traceback, warnings, weakref, collections
  File "/usr/lib64/python3.6/traceback.py", line 5, in <module>
    import linecache
  File "/usr/lib64/python3.6/linecache.py", line 11, in <module>
    import tokenize
  File "/usr/lib64/python3.6/tokenize.py", line 33, in <module>
    import re
  File "/usr/lib64/python3.6/re.py", line 314, in <module>
    @functools.lru_cache(_MAXCACHE)
AttributeError: module 'functools' has no attribute 'lru_cache'

Try installing a more recent version of python-flake8, and please open a bug report if the issue persists in the latest release.  Thanks!

由于报错message的buffer frame太小,一开始没注意到是Flycheck报错(主要是对Emacs还不够精通),花精力去研究是否是jedi报错,找偏了方向。

当回过神来仔细阅读错误信息,发现报错内容是指 functools里缺少 lru_cache属性,这个属性是从3.2开始加入,并且本地项目开发都用的3.6,就比较奇怪。

再多花点时间仔细阅读错误栈后发现,是因为 collections包里会用到 keyword1。这个标准库从未见过,看说明只是用来判断某字符串内容是否为Python关键字的。

经过一番查询后,想调试出Flycheck到底是跑了哪个命令才出现的报错,能够重现那就能办法修复。这时需要按下 C-c ! C-c,选择checker并运行。

-*- mode: compilation; default-directory: "~/projects/qianka/hebe/hebe/services/subtask/" -*-
Compilation started at Wed Jul 25 09:52:59

python -c import\ sys\,runpy\;sys.path.pop\(0\)\;runpy.run_module\(\"flake8\"\) --format\=default - < /home/momoka/projects/qianka/hebe/hebe/services/subtask/subtask_cache.py
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/lib64/python3.6/runpy.py", line 14, in <module>
    import importlib.machinery # importlib first so we can test #15386 via -m
  File "/usr/lib64/python3.6/importlib/__init__.py", line 57, in <module>
    import types
  File "/usr/lib64/python3.6/types.py", line 171, in <module>
    import functools as _functools
  File "/usr/lib64/python3.6/functools.py", line 21, in <module>
    from collections import namedtuple
  File "/usr/lib64/python3.6/collections/__init__.py", line 27, in <module>
    from keyword import iskeyword as _iskeyword
  File "/home/momoka/projects/qianka/hebe/hebe/services/subtask/keyword.py", line 3, in <module>
    import logging
  File "/usr/lib64/python3.6/logging/__init__.py", line 26, in <module>
    import sys, os, time, io, traceback, warnings, weakref, collections
  File "/usr/lib64/python3.6/traceback.py", line 5, in <module>
    import linecache
  File "/usr/lib64/python3.6/linecache.py", line 11, in <module>
    import tokenize
  File "/usr/lib64/python3.6/tokenize.py", line 33, in <module>
    import re
  File "/usr/lib64/python3.6/re.py", line 314, in <module>
    @functools.lru_cache(_MAXCACHE)
AttributeError: module 'functools' has no attribute 'lru_cache'

Compilation exited abnormally with code 1 at Wed Jul 25 09:52:59

在项目根目录下运行 python -c ...那句命令时并未报错,但请留意第一行的运行路径,似乎Flycheck运行时会将工作路径设置为源代码文件所在目录。

而恰巧该项目的同一个目录下便有一个源代码文件 keyword.py,恐怕是由于Python的引入机制2引起的问题,导致循环依赖的发生。

既然定位症结,那么将Flycheck运行checker的路径改到项目根目录即可。Flycheck在某个版本中加入了当前工作路径的支持34,这里可以参考haskell-stack-ghc的配置。

解决方案

直接对 flycheck.el 源代码进行修改不合适,由于需要跟着package升级,一旦升级后即会失效,基于同样的理由,自己fork一个版本保持维护也相对麻烦。所以考虑直接定制一个新的checker5。从 flycheck.el 中将原本 python-flake8 抄出来一份,放在 ~/.emacs.d/my-flycheck.el 中,并命名为 my-python-flake8

;; package -- Summary
;; my-flycheck.el
;;

;;; Commentary:

;;; Code:

(defun flycheck-python--find-default-directory (checker)
  "... `CHECKER`."
     (or
      (when (buffer-file-name)
        (flycheck--locate-dominating-file-matching
         (file-name-directory (buffer-file-name))
         "local\\'"))
      (when (buffer-file-name)
        (flycheck--locate-dominating-file-matching
         (file-name-directory (buffer-file-name))
         "requirements.txt\\'"))))

(flycheck-define-checker my-python-flake8
  "A Python syntax and style checker using Flake8.

Requires Flake8 3.0 or newer. See URL
`https://flake8.readthedocs.io/'."
  ;; Not calling flake8 directly makes it easier to switch between different
  ;; Python versions; see https://github.com/flycheck/flycheck/issues/1055.
  :command ("python"
            (eval (flycheck-python-module-args 'python-flake8 "flake8"))
            "--format=default"
            (config-file "--config" flycheck-flake8rc)
            (option "--max-complexity" flycheck-flake8-maximum-complexity nil
                    flycheck-option-int)
            (option "--max-line-length" flycheck-flake8-maximum-line-length nil
                    flycheck-option-int)
            "-")
  :standard-input t
  :error-filter (lambda (errors)
                  (let ((errors (flycheck-sanitize-errors errors)))
                    (seq-map #'flycheck-flake8-fix-error-level errors)))
  :error-patterns
  ((warning line-start
            "stdin:" line ":" (optional column ":") " "
            (id (one-or-more (any alpha)) (one-or-more digit)) " "
            (message (one-or-more not-newline))
            line-end))
  :enabled (lambda ()
             (or (not (flycheck-python-needs-module-p 'python-flake8))
                 (flycheck-python-find-module 'python-flake8 "flake8")))
  :verify (lambda (_) (flycheck-python-verify-module 'python-flake8 "flake8"))
  :modes python-mode
  :working-directory flycheck-python--find-default-directory)

(provide 'my-flycheck)
;;; my-flycheck.el ends here

请留意最后设置的 :working-directory 属性,调用了自定义的方法,该方法的含义是指依次向上寻找当前编辑源代码所在目录的父级,是否有含有 local (个人习惯用这个名字建立virtualenv)或者 requirements.txt 的,即表示为项目根目录,作为运行checker的目录。同时flychecker很智能的会将后续的参数自动修改为基于这个目录起始的相对路径。

Flycheck提供了一个变量设置 flycheck-checker 来指定当前使用哪个 checker 。 接下来的问题是何时启用该 checker 呢,所有项目均启用显然也不合适。这时就想到了项目级或者目录级的变量设置6

在需要启用定制checker的项目根目录下写入新文件 .dir-locals.el ,内容如下

((python-mode . ((flycheck-checker . my-python-flake8))))

该文件的含义是,表达式第一个参数表示需要在哪些模式下开启,这里仅需要 python-mode 即可,全部启用可以用 nil ,后面参数为设置变量内容。

当打开该目录下的文件时,Emacs会提示检测到目录级本地的变量设置unsafe,会提示是否接受,说明已经起效。

这里可以按以下步骤验证配置已经功能是否已经正常。

运行命令( M-xdescribe-variable 或者按 C-h v 再输入 flycheck-checker ,查看设置的checker。

再次打开源代码文件后按下 C-c ! C-c 查看输出。

-*- mode: compilation; default-directory: "~/projects/qianka/hebe/" -*-
Compilation started at Wed Jul 25 10:29:05

python -c import\ sys\,runpy\;sys.path.pop\(0\)\;runpy.run_module\(\"flake8\"\) --format\=default - < /home/momoka/projects/qianka/hebe/hebe/services/subtask/subtask_cache.py
stdin:43:9: F841 local variable '_' is assigned to but never used
stdin:135:1: E302 expected 2 blank lines, found 1
stdin:256:5: F841 local variable '_' is assigned to but never used
stdin:346:5: F841 local variable '_' is assigned to but never used

Compilation exited abnormally with code 1 at Wed Jul 25 10:29:05

说明配置已经吃上啦,可以继续愉快地使用Emacs写Python啦。

至于jedi的问题,还有待进一步研究,由于目前jedi配置的环境与项目均使用Python3.6,暂时不会有问题。但其实还是需要类似的配置,在对应项目下启动时,jedi启用的Python环境应是项目对应的virtualenv。

注释与参考

__END__


  1. https://docs.python.org/3/library/keyword.html

  2. Python3虽然默认是绝对路径引入,但sys.path中仍然保留了第一位的 '',也即当前工作路径。

  3. https://github.com/flycheck/flycheck/issues/312

  4. http://www.flycheck.org/en/latest/changes.html#aug-28-2016

  5. http://www.flycheck.org/en/27/_downloads/flycheck.html#Syntax-checker-definitions

  6. https://www.gnu.org/software/emacs/manual/html_node/emacs/Directory-Variables.html