Skip to content

Commit

Permalink
Misc improvements (#47)
Browse files Browse the repository at this point in the history
  • Loading branch information
FichteFoll authored Mar 22, 2020
2 parents d20d035 + ec8e1b1 commit 51a43db
Show file tree
Hide file tree
Showing 3 changed files with 111 additions and 31 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@ Following is a list of *additional* settings specific to this linter:

|Setting|Description|
|:------|:----------|
|cache-dir|The directory to store the cache in. Creates a sub-folder in your temporary directory if not specified.|
|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.|
|incremental|By default, we use incremental mode to speed up lint passes. Set this to `false` to disable.|
|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.|
|follow-imports|Whether imports should be followed and linted. The default is `"silent"` for speed, but `"normal"` or `"skip"` may also be used.|
|show-error-codes|Set to `false` for older mypy versions, or better yet update mypy.|

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

Expand Down
126 changes: 98 additions & 28 deletions linter.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,27 @@

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

from collections import defaultdict
import hashlib
import logging
import os
import shutil
import tempfile
import time
import threading
import getpass

from SublimeLinter.lint import const
from SublimeLinter.lint import PythonLinter
from SublimeLinter.lint.linter import PermanentError


MYPY = False
if MYPY:
from typing import Dict, DefaultDict, Optional, Protocol

class TemporaryDirectory(Protocol):
name = None # type: str


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


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

regex = r'^(\w:)?[^:]+:(?P<line>\d+):((?P<col>\d+):)?\s*(?P<error_type>[^:]+):\s*(?P<message>.+)'
regex = (
r'^(?P<filename>.+?):(?P<line>\d+):((?P<col>\d+):)?\s*'
r'(?P<error_type>[^:]+):\s*(?P<message>.+?)(\s\s\[(?P<code>.+)\])?$'
)
line_col_base = (1, 1)
tempfile_suffix = 'py'
default_type = const.WARNING

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

def cmd(self):
Expand All @@ -59,7 +77,6 @@ def cmd(self):
'${args}',
'--show-column-numbers',
'--hide-error-context',
# '--incremental',
]
if self.filename:
cmd.extend([
Expand All @@ -75,42 +92,95 @@ def cmd(self):
else:
cmd.append('${temp_file}')

# Add a temporary cache dir to the command if none was specified.
# Helps keep the environment clean
# by not littering everything with `.mypy_cache` folders.
if not self.settings.get('cache-dir'):
# Compare against `''` so the user can set just `False`,
# for example if the cache is configured in "mypy.ini".
if self.settings.get('cache-dir') == '':
cwd = self.get_working_dir()
if cwd in tmpdirs:
cache_dir = tmpdirs[cwd].name
if not cwd: # abort silently
self.notify_unassign()
raise PermanentError()

if os.path.exists(os.path.join(cwd, '.mypy_cache')):
self.settings.set('cache-dir', False) # do not set it as arg
else:
tmp_dir = tempfile.TemporaryDirectory(prefix=TMPDIR_PREFIX)
tmpdirs[cwd] = tmp_dir
cache_dir = tmp_dir.name
logger.info("Created temporary cache dir at: %s", cache_dir)
cmd[1:1] = ["--cache-dir", cache_dir]
# Add a temporary cache dir to the command if none was specified.
# Helps keep the environment clean by not littering everything
# with `.mypy_cache` folders.
try:
cache_dir = tmpdirs[cwd].name
except KeyError:
tmpdirs[cwd] = tmp_dir = _get_tmpdir(cwd)
cache_dir = tmp_dir.name

self.settings.set('cache-dir', cache_dir)

return cmd

def run(self, cmd, code):
with locks[self.get_working_dir()]:
return super().run(cmd, code)


class FakeTemporaryDirectory:
def __init__(self, name):
# type: (str) -> None
self.name = name


def _get_tmpdir(folder):
# type: (str) -> TemporaryDirectory
folder_hash = hashlib.sha256(folder.encode('utf-8')).hexdigest()[:7]
tmpdir = tempfile.gettempdir()
for dirname in os.listdir(tmpdir):
if dirname.startswith(TMPDIR_PREFIX) and dirname.endswith(folder_hash):
path = os.path.join(tmpdir, dirname)
tmp_dir = FakeTemporaryDirectory(path) # type: TemporaryDirectory
try: # touch it so `_cleanup_tmpdirs` doesn't catch it
os.utime(path)
except OSError:
pass
logger.info("Reuse temporary cache dir at: %s", path)
return tmp_dir
else:
tmp_dir = tempfile.TemporaryDirectory(prefix=TMPDIR_PREFIX, suffix=folder_hash)
logger.info("Created temporary cache dir at: %s", tmp_dir.name)
return tmp_dir


def _cleanup_tmpdirs(keep_recent=False):

def _cleanup_tmpdirs():
def _onerror(function, path, exc_info):
logger.exception("Unable to delete '%s' while cleaning up temporary directory", path,
exc_info=exc_info)

tmpdir = tempfile.gettempdir()
for dirname in os.listdir(tmpdir):
if dirname.startswith(TMPDIR_PREFIX):
shutil.rmtree(os.path.join(tmpdir, dirname), onerror=_onerror)
full_path = os.path.join(tmpdir, dirname)
if keep_recent:
try:
atime = os.stat(full_path).st_atime
except OSError:
pass
else:
if (time.time() - atime) / 60 / 60 / 24 < 14:
continue

shutil.rmtree(full_path, onerror=_onerror)


def plugin_loaded():
"""Attempt to clean up temporary directories from previous runs."""
_cleanup_tmpdirs()
_cleanup_tmpdirs(keep_recent=True)


def plugin_unloaded():
"""Clear references to TemporaryDirectory instances.
try:
from package_control import events

if events.remove('SublimeLinter-contrib-mypy'):
logger.info("Cleanup temporary directories.")
_cleanup_tmpdirs()

They should then be removed automatically.
"""
# (Actually, do we even need to do this?)
tmpdirs.clear()
except ImportError:
pass
10 changes: 10 additions & 0 deletions mypy.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[mypy]
check_untyped_defs = True
warn_redundant_casts = True
warn_unused_ignores = True
mypy_path =
../
sqlite_cache = True

[mypy-package_control]
ignore_missing_imports = True

0 comments on commit 51a43db

Please sign in to comment.