Skip to content

Commit 51a43db

Browse files
authored
Misc improvements (#47)
2 parents d20d035 + ec8e1b1 commit 51a43db

File tree

3 files changed

+111
-31
lines changed

3 files changed

+111
-31
lines changed

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,9 @@ Following is a list of *additional* settings specific to this linter:
4242

4343
|Setting|Description|
4444
|:------|:----------|
45-
|cache-dir|The directory to store the cache in. Creates a sub-folder in your temporary directory if not specified.|
46-
|follow-imports|Whether imports should be followed and linted. The default is `"silent"`, but `"skip"` may also be used. The other options are not interesting.|
47-
|incremental|By default, we use incremental mode to speed up lint passes. Set this to `false` to disable.|
45+
|cache-dir|The directory to store the cache in. Creates a sub-folder in your temporary directory if not specified. Set it to `false` to disable this automatic behavior, for example if the cache location is set in your mypy.ini file.|
46+
|follow-imports|Whether imports should be followed and linted. The default is `"silent"` for speed, but `"normal"` or `"skip"` may also be used.|
47+
|show-error-codes|Set to `false` for older mypy versions, or better yet update mypy.|
4848

4949
All other args to mypy should be specified in the `args` list directly.
5050

linter.py

Lines changed: 98 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,27 @@
1111

1212
"""This module exports the Mypy plugin class."""
1313

14+
from collections import defaultdict
15+
import hashlib
1416
import logging
1517
import os
1618
import shutil
1719
import tempfile
20+
import time
21+
import threading
1822
import getpass
1923

20-
from SublimeLinter.lint import const
2124
from SublimeLinter.lint import PythonLinter
25+
from SublimeLinter.lint.linter import PermanentError
26+
27+
28+
MYPY = False
29+
if MYPY:
30+
from typing import Dict, DefaultDict, Optional, Protocol
31+
32+
class TemporaryDirectory(Protocol):
33+
name = None # type: str
34+
2235

2336
USER = getpass.getuser()
2437
TMPDIR_PREFIX = "SublimeLinter-contrib-mypy-%s" % USER
@@ -28,28 +41,33 @@
2841
# Mapping for our created temporary directories.
2942
# For smarter caching purposes,
3043
# we index different cache folders based on the working dir.
31-
tmpdirs = {}
44+
try:
45+
tmpdirs
46+
except NameError:
47+
tmpdirs = {} # type: Dict[str, TemporaryDirectory]
48+
locks = defaultdict(lambda: threading.Lock()) # type: DefaultDict[Optional[str], threading.Lock]
3249

3350

3451
class Mypy(PythonLinter):
3552
"""Provides an interface to mypy."""
3653

37-
regex = r'^(\w:)?[^:]+:(?P<line>\d+):((?P<col>\d+):)?\s*(?P<error_type>[^:]+):\s*(?P<message>.+)'
54+
regex = (
55+
r'^(?P<filename>.+?):(?P<line>\d+):((?P<col>\d+):)?\s*'
56+
r'(?P<error_type>[^:]+):\s*(?P<message>.+?)(\s\s\[(?P<code>.+)\])?$'
57+
)
3858
line_col_base = (1, 1)
3959
tempfile_suffix = 'py'
40-
default_type = const.WARNING
4160

4261
# Pretty much all interesting options don't expect a value,
4362
# so you'll have to specify those in "args" anyway.
4463
# This dict only contains settings for which we have special handling.
4564
defaults = {
4665
'selector': "source.python",
4766
# Will default to tempfile.TemporaryDirectory if empty.
48-
"--cache-dir:": "",
49-
# Allow users to disable this
50-
"--incremental": True,
67+
"--cache-dir": "",
68+
"--show-error-codes": True,
5169
# Need this to silent lints for other files. Alternatively: 'skip'
52-
"--follow-imports:": "silent",
70+
"--follow-imports": "silent",
5371
}
5472

5573
def cmd(self):
@@ -59,7 +77,6 @@ def cmd(self):
5977
'${args}',
6078
'--show-column-numbers',
6179
'--hide-error-context',
62-
# '--incremental',
6380
]
6481
if self.filename:
6582
cmd.extend([
@@ -75,42 +92,95 @@ def cmd(self):
7592
else:
7693
cmd.append('${temp_file}')
7794

78-
# Add a temporary cache dir to the command if none was specified.
79-
# Helps keep the environment clean
80-
# by not littering everything with `.mypy_cache` folders.
81-
if not self.settings.get('cache-dir'):
95+
# Compare against `''` so the user can set just `False`,
96+
# for example if the cache is configured in "mypy.ini".
97+
if self.settings.get('cache-dir') == '':
8298
cwd = self.get_working_dir()
83-
if cwd in tmpdirs:
84-
cache_dir = tmpdirs[cwd].name
99+
if not cwd: # abort silently
100+
self.notify_unassign()
101+
raise PermanentError()
102+
103+
if os.path.exists(os.path.join(cwd, '.mypy_cache')):
104+
self.settings.set('cache-dir', False) # do not set it as arg
85105
else:
86-
tmp_dir = tempfile.TemporaryDirectory(prefix=TMPDIR_PREFIX)
87-
tmpdirs[cwd] = tmp_dir
88-
cache_dir = tmp_dir.name
89-
logger.info("Created temporary cache dir at: %s", cache_dir)
90-
cmd[1:1] = ["--cache-dir", cache_dir]
106+
# Add a temporary cache dir to the command if none was specified.
107+
# Helps keep the environment clean by not littering everything
108+
# with `.mypy_cache` folders.
109+
try:
110+
cache_dir = tmpdirs[cwd].name
111+
except KeyError:
112+
tmpdirs[cwd] = tmp_dir = _get_tmpdir(cwd)
113+
cache_dir = tmp_dir.name
114+
115+
self.settings.set('cache-dir', cache_dir)
91116

92117
return cmd
93118

119+
def run(self, cmd, code):
120+
with locks[self.get_working_dir()]:
121+
return super().run(cmd, code)
122+
123+
124+
class FakeTemporaryDirectory:
125+
def __init__(self, name):
126+
# type: (str) -> None
127+
self.name = name
128+
129+
130+
def _get_tmpdir(folder):
131+
# type: (str) -> TemporaryDirectory
132+
folder_hash = hashlib.sha256(folder.encode('utf-8')).hexdigest()[:7]
133+
tmpdir = tempfile.gettempdir()
134+
for dirname in os.listdir(tmpdir):
135+
if dirname.startswith(TMPDIR_PREFIX) and dirname.endswith(folder_hash):
136+
path = os.path.join(tmpdir, dirname)
137+
tmp_dir = FakeTemporaryDirectory(path) # type: TemporaryDirectory
138+
try: # touch it so `_cleanup_tmpdirs` doesn't catch it
139+
os.utime(path)
140+
except OSError:
141+
pass
142+
logger.info("Reuse temporary cache dir at: %s", path)
143+
return tmp_dir
144+
else:
145+
tmp_dir = tempfile.TemporaryDirectory(prefix=TMPDIR_PREFIX, suffix=folder_hash)
146+
logger.info("Created temporary cache dir at: %s", tmp_dir.name)
147+
return tmp_dir
148+
149+
150+
def _cleanup_tmpdirs(keep_recent=False):
94151

95-
def _cleanup_tmpdirs():
96152
def _onerror(function, path, exc_info):
97153
logger.exception("Unable to delete '%s' while cleaning up temporary directory", path,
98154
exc_info=exc_info)
155+
99156
tmpdir = tempfile.gettempdir()
100157
for dirname in os.listdir(tmpdir):
101158
if dirname.startswith(TMPDIR_PREFIX):
102-
shutil.rmtree(os.path.join(tmpdir, dirname), onerror=_onerror)
159+
full_path = os.path.join(tmpdir, dirname)
160+
if keep_recent:
161+
try:
162+
atime = os.stat(full_path).st_atime
163+
except OSError:
164+
pass
165+
else:
166+
if (time.time() - atime) / 60 / 60 / 24 < 14:
167+
continue
168+
169+
shutil.rmtree(full_path, onerror=_onerror)
103170

104171

105172
def plugin_loaded():
106173
"""Attempt to clean up temporary directories from previous runs."""
107-
_cleanup_tmpdirs()
174+
_cleanup_tmpdirs(keep_recent=True)
108175

109176

110177
def plugin_unloaded():
111-
"""Clear references to TemporaryDirectory instances.
178+
try:
179+
from package_control import events
180+
181+
if events.remove('SublimeLinter-contrib-mypy'):
182+
logger.info("Cleanup temporary directories.")
183+
_cleanup_tmpdirs()
112184

113-
They should then be removed automatically.
114-
"""
115-
# (Actually, do we even need to do this?)
116-
tmpdirs.clear()
185+
except ImportError:
186+
pass

mypy.ini

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
[mypy]
2+
check_untyped_defs = True
3+
warn_redundant_casts = True
4+
warn_unused_ignores = True
5+
mypy_path =
6+
../
7+
sqlite_cache = True
8+
9+
[mypy-package_control]
10+
ignore_missing_imports = True

0 commit comments

Comments
 (0)