问题的出现与分析
有一阵子没仔细琢磨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报错,找偏了方向。
当回过神来仔细阅读错误信息,发现报错内容是指 src_shell{functools}里缺少 src_shell{lru_cache}属性,这个属性是从3.2开始加入,并且本地项目开发都用的3.6,就比较奇怪。
再多花点时间仔细阅读错误栈后发现,是因为 src_shell{collections}包里会用到 src_shell{keyword}库1。这个标准库从未见过,看说明只是用来判断某字符串内容是否为Python关键字的。
经过一番查询后,想调试出Flycheck到底是跑了哪个命令才出现的报错,能够重现那就能办法修复。这时需要按下 src_shell{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
在项目根目录下运行 src_shell{python -c …}那句命令时并未报错,但请留意第一行的运行路径,似乎Flycheck运行时会将工作路径设置为源代码文件所在目录。
而恰巧该项目的同一个目录下便有一个源代码文件 src_shell{keyword.py},恐怕是由于Python的引入机制2引起的问题,导致循环依赖的发生。
既然定位症结,那么将Flycheck运行checker的路径改到项目根目录即可。Flycheck在某个版本中加入了当前工作路径的支持34,这里可以参考haskell-stack-ghc的配置。
解决方案
直接对 src_shell{flycheck.el} 源代码进行修改不合适,由于需要跟着package升级,一旦升级后即会失效,基于同样的理由,自己fork一个版本保持维护也相对麻烦。所以考虑直接定制一个新的checker5。从 src_shell{flycheck.el} 中将原本 src_shell{python-flake8} 抄出来一份,放在 src_shell{~/.emacs.d/my-flycheck.el} 中,并命名为 src_shell{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
请留意最后设置的 src_shell{:working-directory} 属性,调用了自定义的方法,该方法的含义是指依次向上寻找当前编辑源代码所在目录的父级,是否有含有 src_shell{local} (个人习惯用这个名字建立virtualenv)或者 src_shell{requirements.txt} 的,即表示为项目根目录,作为运行checker的目录。同时flychecker很智能的会将后续的参数自动修改为基于这个目录起始的相对路径。
Flycheck提供了一个变量设置 src_shell{flycheck-checker} 来指定当前使用哪个 checker 。 接下来的问题是何时启用该 checker 呢,所有项目均启用显然也不合适。这时就想到了项目级或者目录级的变量设置6。
在需要启用定制checker的项目根目录下写入新文件 src_shell{.dir-locals.el} ,内容如下
((python-mode . ((flycheck-checker . my-python-flake8))))
该文件的含义是,表达式第一个参数表示需要在哪些模式下开启,这里仅需要 src_shel{python-mode} 即可,全部启用可以用 src_shell{nil} ,后面参数为设置变量内容。
当打开该目录下的文件时,Emacs会提示检测到目录级本地的变量设置unsafe,会提示是否接受,说明已经起效。
这里可以按以下步骤验证配置已经功能是否已经正常。
运行命令( src_shell{M-x} ) src_shell{describe-variable} 或者按 src_shellC-h v 再输入 src_shell{flycheck-checker} ,查看设置的checker。
再次打开源代码文件后按下 src_shel{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\_\_
Python3虽然默认是绝对路径引入,但sys.path中仍然保留了第一位的 src_shell{''},也即当前工作路径。