From 74aa977c3aaaaaa86988016e3a0ccbcf9fbce700 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Sat, 25 Apr 2020 22:06:14 +0200 Subject: [PATCH 001/110] Shifted defaults Live mode seems to not work properly (for me), so the if/else construct ist changed in a way to use the saved mode whenever possible. Forthermore adapted the regex so it works on my system (windows). --- pyls_mypy/plugin.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 530dea9..2768cd8 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -3,7 +3,7 @@ from mypy import api as mypy_api from pyls import hookimpl -line_pattern = r"([^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" +line_pattern = r"([a-z]:[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" log = logging.getLogger(__name__) @@ -56,16 +56,17 @@ def parse_line(line, document=None): def pyls_lint(config, workspace, document, is_saved): settings = config.plugin_settings('pyls_mypy') live_mode = settings.get('live_mode', True) - if live_mode: + if is_saved: args = ['--incremental', '--show-column-numbers', '--follow-imports', 'silent', - '--command', document.source] - elif is_saved: + '--config-file', document.path[:document.path.rfind("\\")+1]+"mypy.ini", + document.path] + elif live_mode: args = ['--incremental', '--show-column-numbers', '--follow-imports', 'silent', - document.path] + '--command', document.source] else: return [] From 014c32adad670d25c3b3742e83946e2c9c73d525 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Sun, 26 Apr 2020 00:45:54 +0200 Subject: [PATCH 002/110] Find config file and better live mode --- pyls_mypy/plugin.py | 29 +++++++++++++++++++++++------ 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 2768cd8..d244315 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -1,4 +1,7 @@ import re +import tempfile +import os +import os.path import logging from mypy import api as mypy_api from pyls import hookimpl @@ -38,7 +41,7 @@ def parse_line(line, document=None): # There may be a better solution, but mypy does not provide end 'end': {'line': lineno, 'character': offset + 1} }, - 'message': msg, + 'message': msg.replace("]", "]"),#Prevents spyder from messign it up 'severity': errno } if document: @@ -56,24 +59,38 @@ def parse_line(line, document=None): def pyls_lint(config, workspace, document, is_saved): settings = config.plugin_settings('pyls_mypy') live_mode = settings.get('live_mode', True) + path = document.path + while (loc:=path.rfind("\\"))>-1: + p = path[:loc+1]+"mypy.ini" + if os.path.isfile(p): + break + else: + path = path[:loc] if is_saved: args = ['--incremental', '--show-column-numbers', - '--follow-imports', 'silent', - '--config-file', document.path[:document.path.rfind("\\")+1]+"mypy.ini", - document.path] + '--follow-imports', 'silent'] elif live_mode: + tmpFile = tempfile.NamedTemporaryFile('w', delete=False) + tmpFile.write(document.source) + tmpFile.flush() args = ['--incremental', '--show-column-numbers', '--follow-imports', 'silent', - '--command', document.source] + '--shadow-file', document.path, tmpFile.name] else: return [] - + if loc != -1: + args.append('--config-file') + args.append(p) + args.append(document.path) if settings.get('strict', False): args.append('--strict') report, errors, _ = mypy_api.run(args) + if "tmpFile" in locals(): + tmpFile.close() + os.unlink(tmpFile.name) diagnostics = [] for line in report.splitlines(): From 44c34f57f17c4e0dbc646e2d5188ffab331731fb Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 14:40:48 +0200 Subject: [PATCH 003/110] Changed setup and versioning --- README.rst | 8 +- pyls_mypy/__init__.py | 8 - pyls_mypy/_version.py | 521 +----------------------------------------- requirements.txt | 6 +- setup.cfg | 31 ++- setup.py | 4 +- 6 files changed, 26 insertions(+), 552 deletions(-) diff --git a/README.rst b/README.rst index 50e6ba5..4a33868 100644 --- a/README.rst +++ b/README.rst @@ -22,11 +22,7 @@ Install into the same virtualenv as pyls itself. Configuration ------------- -``live_mode`` (default is True) provides type checking as you type. - -As mypy is unaware of what file path is being checked, there are limitations with live_mode - - Imports cannot be followed correctly - - Stub files are not validated correctly +``live_mode`` (default is True) provides type checking as you type. This writes a tempfile every time a check is done. Turning off live_mode means you must save your changes for mypy diagnostics to update correctly. @@ -41,7 +37,7 @@ Depending on your editor, the configuration should be roughly like this: "pyls_mypy": { "enabled": true, - "live_mode": false + "live_mode": true } } } diff --git a/pyls_mypy/__init__.py b/pyls_mypy/__init__.py index fbe5d98..8b13789 100644 --- a/pyls_mypy/__init__.py +++ b/pyls_mypy/__init__.py @@ -1,9 +1 @@ -from ._version import get_versions -import sys -if sys.version_info[0] < 3: - from future.standard_library import install_aliases - install_aliases() - -__version__ = get_versions()['version'] -del get_versions diff --git a/pyls_mypy/_version.py b/pyls_mypy/_version.py index 89b42fc..d3ec452 100644 --- a/pyls_mypy/_version.py +++ b/pyls_mypy/_version.py @@ -1,520 +1 @@ - -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "$Format:%d$" - git_full = "$Format:%H$" - git_date = "$Format:%ci$" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "pep440" - cfg.tag_prefix = "" - cfg.parentdir_prefix = "" - cfg.versionfile_source = "pyls/_version.py" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for i in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - date = keywords.get("date") - if date is not None: - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} +__version__ = "0.2.0" diff --git a/requirements.txt b/requirements.txt index b6a814b..d5c5f76 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -future;python_version < '3' -flake8 -configparser python-language-server -mypy;python_version >= '3.2' +mypy +python_version>=3.2 diff --git a/setup.cfg b/setup.cfg index 404f62a..086df6e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,13 +1,29 @@ [metadata] name = pyls-mypy -author = Tom van Ommeren +author = Tom van Ommeren, Richard Kellnberger description = Mypy linter for the Python Language Server -url = https://github.com/tomv564/pyls-mypy +url = https://github.com/Richardk2n/pyls-mypy long_description = file: README.rst +license='MIT' +classifiers = + Development Status :: 3 - Alpha + Intended Audience :: Developers + Topic :: Software Development + License :: OSI Approved :: MIT License + Programming Language :: Python :: 3.2 + Programming Language :: Python :: 3.3 + Programming Language :: Python :: 3.4 + Programming Language :: Python :: 3.5 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 [options] +python_requires = >= 3.2 packages = find: -install_requires = python-language-server; mypy +install_requires = + python-language-server + mypy [options.entry_points] @@ -16,19 +32,10 @@ pyls = pyls_mypy = pyls_mypy.plugin [options.extras_require] test = tox - versioneer pytest pytest-cov coverage -[versioneer] -VCS = git -style = pep440 -versionfile_source = pyls_mypy/_version.py -versionfile_build = pyls_mypy/_version.py -tag_prefix = -parentdir_prefix = - [options.packages.find] exclude = contrib diff --git a/setup.py b/setup.py index d69ef61..d35bc4a 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python from setuptools import setup -import versioneer +from pyls_mypy import _version if __name__ == "__main__": - setup(version=versioneer.get_version(), cmdclass=versioneer.get_cmdclass()) + setup(version=_version.__version__) From 169076ebb996a45108478fa7d5a175a75a63ea0b Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 14:40:53 +0200 Subject: [PATCH 004/110] Update LICENSE --- LICENSE | 1 + 1 file changed, 1 insertion(+) diff --git a/LICENSE b/LICENSE index 119161b..17af835 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ MIT License Copyright (c) 2017 Tom van Ommeren +Copyright (c) 2020 Richard Kellnberger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal From 4169d693b201d330ffe78c517c1b6182ad207fa2 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 14:41:12 +0200 Subject: [PATCH 005/110] Net necessary anymore --- pyls_mypy/plugin.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index d244315..01e6519 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -5,8 +5,9 @@ import logging from mypy import api as mypy_api from pyls import hookimpl +from sys import platform -line_pattern = r"([a-z]:[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" +line_pattern = r"([^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" log = logging.getLogger(__name__) @@ -41,7 +42,7 @@ def parse_line(line, document=None): # There may be a better solution, but mypy does not provide end 'end': {'line': lineno, 'character': offset + 1} }, - 'message': msg.replace("]", "]"),#Prevents spyder from messign it up + 'message': msg, 'severity': errno } if document: From 3f4ca482a965c182d2345520db5c3266146ff27b Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Wed, 8 Jul 2020 14:42:55 +0200 Subject: [PATCH 006/110] Create python-test.yml --- .github/workflows/python-test.yml | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 .github/workflows/python-test.yml diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml new file mode 100644 index 0000000..c7f5067 --- /dev/null +++ b/.github/workflows/python-test.yml @@ -0,0 +1,36 @@ +# This workflow will install Python dependencies, run tests and lint with a single version of Python +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python application + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v2 + with: + python-version: 3.8 + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest From f526a6d02b8a46bd9f79609f16864188a623bf38 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 14:46:46 +0200 Subject: [PATCH 007/110] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d5c5f76..7935414 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ python-language-server mypy -python_version>=3.2 +python_version>="3.2" From ce04e49fe8f665f3de3f51ef2883cccc857d7087 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 15:50:18 +0200 Subject: [PATCH 008/110] spdyder compatibility --- requirements.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 7935414..32a887e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -python-language-server +python-language-server<="0.32.0" mypy python_version>="3.2" diff --git a/setup.cfg b/setup.cfg index 086df6e..337b099 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,7 +22,7 @@ classifiers = python_requires = >= 3.2 packages = find: install_requires = - python-language-server + python-language-server = <= 0.32.0 mypy From 3a275d2233f3fee170f12da54dbb51463cab80e7 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 15:53:04 +0200 Subject: [PATCH 009/110] formatting experiment --- requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 32a887e..f93725b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ -python-language-server<="0.32.0" +python-language-server<=0.32.0 mypy -python_version>="3.2" +python_version>=3.2 From ba9e42b93a086dea8a4d9aec03db28f299534cb0 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 15:54:17 +0200 Subject: [PATCH 010/110] unnecessary --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f93725b..1f1d3b9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,2 @@ python-language-server<=0.32.0 mypy -python_version>=3.2 From ab97e0d345c4f5993a71ea4c864bcd20e385ed43 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 16:07:26 +0200 Subject: [PATCH 011/110] Update requirements.txt --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 1f1d3b9..625ebe3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -python-language-server<=0.32.0 +python-language-server<0.32.0 mypy From 236f97c0f4df415d895a85b787df30b805500e88 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Wed, 8 Jul 2020 16:35:49 +0200 Subject: [PATCH 012/110] Create python-package.yml --- .github/workflows/python-package.yml | 39 ++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 .github/workflows/python-package.yml diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml new file mode 100644 index 0000000..f1abc2f --- /dev/null +++ b/.github/workflows/python-package.yml @@ -0,0 +1,39 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: Python package + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.5, 3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + pytest From ee5ae18a596bc50f831ecb9800a1737e6ae1fccc Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Wed, 8 Jul 2020 16:36:05 +0200 Subject: [PATCH 013/110] Delete python-test.yml --- .github/workflows/python-test.yml | 36 ------------------------------- 1 file changed, 36 deletions(-) delete mode 100644 .github/workflows/python-test.yml diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml deleted file mode 100644 index c7f5067..0000000 --- a/.github/workflows/python-test.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Python application - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - pytest From b4654d471b9a3f40397c4534a9d82df35f2efd06 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Wed, 8 Jul 2020 17:38:08 +0200 Subject: [PATCH 014/110] Create python-publish.yml --- .github/workflows/python-publish.yml | 31 ++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/python-publish.yml diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..d54dbd7 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,31 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* From 22aa9c29462bd266d042b49bd8031d6da858144f Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 17:43:27 +0200 Subject: [PATCH 015/110] update --- .github/workflows/python-test.yml | 36 ------------------------------- README.rst | 8 +++---- pyls_mypy/_version.py | 2 +- pyls_mypy/plugin.py | 5 +++-- setup.cfg | 6 +++--- 5 files changed, 11 insertions(+), 46 deletions(-) delete mode 100644 .github/workflows/python-test.yml diff --git a/.github/workflows/python-test.yml b/.github/workflows/python-test.yml deleted file mode 100644 index c7f5067..0000000 --- a/.github/workflows/python-test.yml +++ /dev/null @@ -1,36 +0,0 @@ -# This workflow will install Python dependencies, run tests and lint with a single version of Python -# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions - -name: Python application - -on: - push: - branches: [ master ] - pull_request: - branches: [ master ] - -jobs: - build: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - - name: Lint with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics - - name: Test with pytest - run: | - pytest diff --git a/README.rst b/README.rst index 4a33868..d41848c 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,11 @@ Mypy plugin for PYLS ====================== -.. image:: https://badge.fury.io/py/pyls-mypy.svg - :target: https://badge.fury.io/py/pyls-mypy +.. image:: https://badge.fury.io/py/mypy-ls.svg + :target: https://badge.fury.io/py/mypy-ls -.. image:: https://travis-ci.org/tomv564/pyls-mypy.svg?branch=master - :target: https://travis-ci.org/tomv564/pyls-mypy +.. image:: https://github.com/Richardk2n/pyls-mypy/workflows/Python%20package/badge.svg?branch=master + :target: https://github.com/Richardk2n/pyls-mypy/ This is a plugin for the Palantir's Python Language Server (https://github.com/palantir/python-language-server) diff --git a/pyls_mypy/_version.py b/pyls_mypy/_version.py index d3ec452..3ced358 100644 --- a/pyls_mypy/_version.py +++ b/pyls_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.2.0" +__version__ = "0.2.1" diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 01e6519..ee5262c 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -7,7 +7,7 @@ from pyls import hookimpl from sys import platform -line_pattern = r"([^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" +line_pattern = r"((?:^[a-z]:)[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" log = logging.getLogger(__name__) @@ -61,7 +61,8 @@ def pyls_lint(config, workspace, document, is_saved): settings = config.plugin_settings('pyls_mypy') live_mode = settings.get('live_mode', True) path = document.path - while (loc:=path.rfind("\\"))>-1: + loc:=path.rfind("\\") + while (loc)>-1: p = path[:loc+1]+"mypy.ini" if os.path.isfile(p): break diff --git a/setup.cfg b/setup.cfg index 337b099..a678706 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,12 +1,12 @@ [metadata] -name = pyls-mypy +name = mypy-ls author = Tom van Ommeren, Richard Kellnberger description = Mypy linter for the Python Language Server url = https://github.com/Richardk2n/pyls-mypy long_description = file: README.rst license='MIT' classifiers = - Development Status :: 3 - Alpha + Development Status :: 4 - Beta Intended Audience :: Developers Topic :: Software Development License :: OSI Approved :: MIT License @@ -22,7 +22,7 @@ classifiers = python_requires = >= 3.2 packages = find: install_requires = - python-language-server = <= 0.32.0 + python-language-server<0.32.0 mypy From 4a34b1df8892515aacc6c39cd9674e5945900235 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 17:45:08 +0200 Subject: [PATCH 016/110] typo --- pyls_mypy/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index ee5262c..4479032 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -61,7 +61,7 @@ def pyls_lint(config, workspace, document, is_saved): settings = config.plugin_settings('pyls_mypy') live_mode = settings.get('live_mode', True) path = document.path - loc:=path.rfind("\\") + loc=path.rfind("\\") while (loc)>-1: p = path[:loc+1]+"mypy.ini" if os.path.isfile(p): From a13577d5f024c0031cfa3cf787cded2eff5fdf61 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 17:53:30 +0200 Subject: [PATCH 017/110] typo --- pyls_mypy/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index 4479032..ef49c47 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -7,7 +7,7 @@ from pyls import hookimpl from sys import platform -line_pattern = r"((?:^[a-z]:)[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" +line_pattern = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" log = logging.getLogger(__name__) From a8beefa8bb7681a222c533b058fc664ba207a6a4 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Wed, 8 Jul 2020 18:18:44 +0200 Subject: [PATCH 018/110] Failed to update variable --- pyls_mypy/_version.py | 2 +- pyls_mypy/plugin.py | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/pyls_mypy/_version.py b/pyls_mypy/_version.py index 3ced358..b5fdc75 100644 --- a/pyls_mypy/_version.py +++ b/pyls_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.2.1" +__version__ = "0.2.2" diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py index ef49c47..57f903c 100644 --- a/pyls_mypy/plugin.py +++ b/pyls_mypy/plugin.py @@ -61,13 +61,14 @@ def pyls_lint(config, workspace, document, is_saved): settings = config.plugin_settings('pyls_mypy') live_mode = settings.get('live_mode', True) path = document.path - loc=path.rfind("\\") - while (loc)>-1: + loc = path.rfind("\\") + while loc > -1: p = path[:loc+1]+"mypy.ini" if os.path.isfile(p): break else: path = path[:loc] + loc = path.rfind("\\") if is_saved: args = ['--incremental', '--show-column-numbers', From 0d3f8d8a5e6413ef2394b352008a91a5596c806e Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Fri, 10 Jul 2020 10:13:51 +0200 Subject: [PATCH 019/110] Typing --- README.rst | 18 +- {pyls_mypy => mypy_ls}/__init__.py | 0 mypy_ls/_version.py | 1 + mypy_ls/plugin.py | 238 ++++ pyls_mypy/_version.py | 1 - pyls_mypy/plugin.py | 104 -- setup.cfg | 2 +- test/test_plugin.py | 7 +- versioneer.py | 1822 ---------------------------- 9 files changed, 252 insertions(+), 1941 deletions(-) rename {pyls_mypy => mypy_ls}/__init__.py (100%) create mode 100644 mypy_ls/_version.py create mode 100644 mypy_ls/plugin.py delete mode 100644 pyls_mypy/_version.py delete mode 100644 pyls_mypy/plugin.py delete mode 100644 versioneer.py diff --git a/README.rst b/README.rst index d41848c..c7973cf 100644 --- a/README.rst +++ b/README.rst @@ -17,27 +17,21 @@ Installation Install into the same virtualenv as pyls itself. -``pip install pyls-mypy`` +``pip install mypy-ls`` Configuration ------------- -``live_mode`` (default is True) provides type checking as you type. This writes a tempfile every time a check is done. +``live_mode`` (default is True) provides type checking as you type. This writes to a tempfile every time a check is done. Turning off live_mode means you must save your changes for mypy diagnostics to update correctly. -Depending on your editor, the configuration should be roughly like this: +Depending on your editor, the configuration (found in a file called mypy-ls.cfg in your workspace or a parent directory) should be roughly like this: :: - "pyls": { - "plugins": - { - "pyls_mypy": - { - "enabled": true, - "live_mode": true - } - } + "enabled": True, + "live_mode": True, + "strict": False } diff --git a/pyls_mypy/__init__.py b/mypy_ls/__init__.py similarity index 100% rename from pyls_mypy/__init__.py rename to mypy_ls/__init__.py diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py new file mode 100644 index 0000000..493f741 --- /dev/null +++ b/mypy_ls/_version.py @@ -0,0 +1 @@ +__version__ = "0.3.0" diff --git a/mypy_ls/plugin.py b/mypy_ls/plugin.py new file mode 100644 index 0000000..03a92f1 --- /dev/null +++ b/mypy_ls/plugin.py @@ -0,0 +1,238 @@ +# -*- coding: utf-8 -*- +""" +File that contains the pyls plugin mypy-ls. + +Created on Fri Jul 10 09:53:57 2020 + +@author: Richard Kellnberger +""" +import re +import tempfile +import os +import os.path +import logging +from mypy import api as mypy_api +from pyls import hookimpl +from pyls.workspace import Document, Workspace +from pyls.config.config import Config +from typing import Optional, Dict, Any, IO, List +import atexit + +line_pattern: str = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" + +log = logging.getLogger(__name__) + +mypyConfigFile: Optional[str] = None + +tmpFile: Optional[IO[str]] = None + + +def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[str, Any]]: + """ + Return a language-server diagnostic from a line of the Mypy error report. + + optionally, use the whole document to provide more context on it. + + + Parameters + ---------- + line : str + Line of mypy output to be analysed. + document : Optional[Document], optional + Document in wich the line is found. The default is None. + + Returns + ------- + Optional[Dict[str, Any]] + The dict with the lint data. + + """ + result = re.match(line_pattern, line) + if result: + file_path, linenoStr, offsetStr, severity, msg = result.groups() + + if file_path != "": # live mode + # results from other files can be included, but we cannot return + # them. + if document and document.path and not document.path.endswith( + file_path): + log.warning("discarding result for %s against %s", file_path, + document.path) + return None + + lineno = int(linenoStr or 1) - 1 # 0-based line number + offset = int(offsetStr or 1) - 1 # 0-based offset + errno = 2 + if severity == 'error': + errno = 1 + diag: Dict[str, Any] = { + 'source': 'mypy', + 'range': { + 'start': {'line': lineno, 'character': offset}, + # There may be a better solution, but mypy does not provide end + 'end': {'line': lineno, 'character': offset + 1} + }, + 'message': msg, + 'severity': errno + } + if document: + # although mypy does not provide the end of the affected range, we + # can make a good guess by highlighting the word that Mypy flagged + word = document.word_at_position(diag['range']['start']) + if word: + diag['range']['end']['character'] = ( + diag['range']['start']['character'] + len(word)) + + return diag + return None + + +@hookimpl +def pyls_lint(config: Config, workspace: Workspace, document: Document, + is_saved: bool) -> List[Dict[str, Any]]: + """ + Lints. + + Parameters + ---------- + config : Config + The pyls config. + workspace : Workspace + The pyls workspace. + document : Document + The document to be linted. + is_saved : bool + Weather the document is saved. + + Returns + ------- + List[Dict[str, Any]] + List of the linting data. + + """ + settings = config.plugin_settings('mypy-ls') + live_mode = settings.get('live_mode', True) + args = ['--incremental', + '--show-column-numbers', + '--follow-imports', 'silent'] + + global tmpFile + if live_mode and not is_saved and tmpFile: + tmpFile = open(tmpFile.name, "w") + tmpFile.write(document.source) + tmpFile.close() + args.extend(['--shadow-file', document.path, tmpFile.name]) + elif not is_saved: + return [] + + if mypyConfigFile: + args.append('--config-file') + args.append(mypyConfigFile) + args.append(document.path) + if settings.get('strict', False): + args.append('--strict') + + report, errors, _ = mypy_api.run(args) + + diagnostics = [] + for line in report.splitlines(): + diag = parse_line(line, document) + if diag: + diagnostics.append(diag) + + return diagnostics + + +@hookimpl +def pyls_settings(config: Config) -> Dict[str, Dict[str, Dict[str, str]]]: + """ + Read the settings. + + Parameters + ---------- + config : Config + The pyls config. + + Returns + ------- + Dict[str, Dict[str, Dict[str, str]]] + The config dict. + + """ + configuration = init(config._root_path) + return {"plugins": {"mypy-ls": configuration}} + + +def init(workspace: str) -> Dict[str, str]: + """ + Find plugin and mypy config files and creates the temp file should it be used. + + Parameters + ---------- + workspace : str + The path to the current workspace. + + Returns + ------- + Dict[str, str] + The plugin config dict. + + """ + configuration = {} + path = findConfigFile(workspace, "mypy-ls.cfg") + if path: + with open(path) as file: + configuration = eval(file.read()) + global mypyConfigFile + mypyConfigFile = findConfigFile(workspace, "mypy.ini") + if (("enabled" not in configuration or configuration["enabled"]) + and ("live_mode" not in configuration or configuration["live_mode"])): + global tmpFile + tmpFile = tempfile.NamedTemporaryFile('w', delete=False) + tmpFile.close() + return configuration + + +def findConfigFile(path: str, name: str) -> Optional[str]: + """ + Search for a config file. + + Search for a file of a given name from the directory specifyed by path through all parent + directories. The first file found is selected. + + Parameters + ---------- + path : str + The path where the search starts. + name : str + The file to be found. + + Returns + ------- + Optional[str] + The path where the file has been found or None if no matching file has been found. + + """ + while True: + p = f"{path}\\{name}" + if os.path.isfile(p): + return p + else: + loc = path.rfind("\\") + if loc == -1: + return None + path = path[:loc] + + +@atexit.register +def close() -> None: + """ + Deltes the tempFile should it exist. + + Returns + ------- + None. + + """ + if tmpFile and tmpFile.name: + os.unlink(tmpFile.name) diff --git a/pyls_mypy/_version.py b/pyls_mypy/_version.py deleted file mode 100644 index b5fdc75..0000000 --- a/pyls_mypy/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.2.2" diff --git a/pyls_mypy/plugin.py b/pyls_mypy/plugin.py deleted file mode 100644 index 57f903c..0000000 --- a/pyls_mypy/plugin.py +++ /dev/null @@ -1,104 +0,0 @@ -import re -import tempfile -import os -import os.path -import logging -from mypy import api as mypy_api -from pyls import hookimpl -from sys import platform - -line_pattern = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" - -log = logging.getLogger(__name__) - - -def parse_line(line, document=None): - ''' - Return a language-server diagnostic from a line of the Mypy error report; - optionally, use the whole document to provide more context on it. - ''' - result = re.match(line_pattern, line) - if result: - file_path, lineno, offset, severity, msg = result.groups() - - if file_path != "": # live mode - # results from other files can be included, but we cannot return - # them. - if document and document.path and not document.path.endswith( - file_path): - log.warning("discarding result for %s against %s", file_path, - document.path) - return None - - lineno = int(lineno or 1) - 1 # 0-based line number - offset = int(offset or 1) - 1 # 0-based offset - errno = 2 - if severity == 'error': - errno = 1 - diag = { - 'source': 'mypy', - 'range': { - 'start': {'line': lineno, 'character': offset}, - # There may be a better solution, but mypy does not provide end - 'end': {'line': lineno, 'character': offset + 1} - }, - 'message': msg, - 'severity': errno - } - if document: - # although mypy does not provide the end of the affected range, we - # can make a good guess by highlighting the word that Mypy flagged - word = document.word_at_position(diag['range']['start']) - if word: - diag['range']['end']['character'] = ( - diag['range']['start']['character'] + len(word)) - - return diag - - -@hookimpl -def pyls_lint(config, workspace, document, is_saved): - settings = config.plugin_settings('pyls_mypy') - live_mode = settings.get('live_mode', True) - path = document.path - loc = path.rfind("\\") - while loc > -1: - p = path[:loc+1]+"mypy.ini" - if os.path.isfile(p): - break - else: - path = path[:loc] - loc = path.rfind("\\") - if is_saved: - args = ['--incremental', - '--show-column-numbers', - '--follow-imports', 'silent'] - elif live_mode: - tmpFile = tempfile.NamedTemporaryFile('w', delete=False) - tmpFile.write(document.source) - tmpFile.flush() - args = ['--incremental', - '--show-column-numbers', - '--follow-imports', 'silent', - '--shadow-file', document.path, tmpFile.name] - else: - return [] - if loc != -1: - args.append('--config-file') - args.append(p) - args.append(document.path) - if settings.get('strict', False): - args.append('--strict') - - report, errors, _ = mypy_api.run(args) - if "tmpFile" in locals(): - tmpFile.close() - os.unlink(tmpFile.name) - - diagnostics = [] - for line in report.splitlines(): - diag = parse_line(line, document) - if diag: - diagnostics.append(diag) - - return diagnostics diff --git a/setup.cfg b/setup.cfg index a678706..063dac3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,7 +27,7 @@ install_requires = [options.entry_points] -pyls = pyls_mypy = pyls_mypy.plugin +pyls = mypy_ls = mypy_ls.plugin [options.extras_require] test = diff --git a/test/test_plugin.py b/test/test_plugin.py index 6e13ae3..78a7646 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,7 +1,7 @@ import pytest from pyls.workspace import Document -from pyls_mypy import plugin +from mypy_ls import plugin DOC_URI = __file__ DOC_TYPE_ERR = """{}.append(3) @@ -16,6 +16,10 @@ class FakeConfig(object): + + def __init__(self): + self._root_path = "C:" + def plugin_settings(self, plugin, document_path=None): return {} @@ -24,6 +28,7 @@ def test_plugin(): config = FakeConfig() doc = Document(DOC_URI, DOC_TYPE_ERR) workspace = None + plugin.pyls_settings(config) diags = plugin.pyls_lint(config, workspace, doc, is_saved=False) assert len(diags) == 1 diff --git a/versioneer.py b/versioneer.py deleted file mode 100644 index 64fea1c..0000000 --- a/versioneer.py +++ /dev/null @@ -1,1822 +0,0 @@ - -# Version: 0.18 - -"""The Versioneer - like a rocketeer, but for versions. - -The Versioneer -============== - -* like a rocketeer, but for versions! -* https://github.com/warner/python-versioneer -* Brian Warner -* License: Public Domain -* Compatible With: python2.6, 2.7, 3.2, 3.3, 3.4, 3.5, 3.6, and pypy -* [![Latest Version] -(https://pypip.in/version/versioneer/badge.svg?style=flat) -](https://pypi.python.org/pypi/versioneer/) -* [![Build Status] -(https://travis-ci.org/warner/python-versioneer.png?branch=master) -](https://travis-ci.org/warner/python-versioneer) - -This is a tool for managing a recorded version number in distutils-based -python projects. The goal is to remove the tedious and error-prone "update -the embedded version string" step from your release process. Making a new -release should be as easy as recording a new tag in your version-control -system, and maybe making new tarballs. - - -## Quick Install - -* `pip install versioneer` to somewhere to your $PATH -* add a `[versioneer]` section to your setup.cfg (see below) -* run `versioneer install` in your source tree, commit the results - -## Version Identifiers - -Source trees come from a variety of places: - -* a version-control system checkout (mostly used by developers) -* a nightly tarball, produced by build automation -* a snapshot tarball, produced by a web-based VCS browser, like github's - "tarball from tag" feature -* a release tarball, produced by "setup.py sdist", distributed through PyPI - -Within each source tree, the version identifier (either a string or a number, -this tool is format-agnostic) can come from a variety of places: - -* ask the VCS tool itself, e.g. "git describe" (for checkouts), which knows - about recent "tags" and an absolute revision-id -* the name of the directory into which the tarball was unpacked -* an expanded VCS keyword ($Id$, etc) -* a `_version.py` created by some earlier build step - -For released software, the version identifier is closely related to a VCS -tag. Some projects use tag names that include more than just the version -string (e.g. "myproject-1.2" instead of just "1.2"), in which case the tool -needs to strip the tag prefix to extract the version identifier. For -unreleased software (between tags), the version identifier should provide -enough information to help developers recreate the same tree, while also -giving them an idea of roughly how old the tree is (after version 1.2, before -version 1.3). Many VCS systems can report a description that captures this, -for example `git describe --tags --dirty --always` reports things like -"0.7-1-g574ab98-dirty" to indicate that the checkout is one revision past the -0.7 tag, has a unique revision id of "574ab98", and is "dirty" (it has -uncommitted changes. - -The version identifier is used for multiple purposes: - -* to allow the module to self-identify its version: `myproject.__version__` -* to choose a name and prefix for a 'setup.py sdist' tarball - -## Theory of Operation - -Versioneer works by adding a special `_version.py` file into your source -tree, where your `__init__.py` can import it. This `_version.py` knows how to -dynamically ask the VCS tool for version information at import time. - -`_version.py` also contains `$Revision$` markers, and the installation -process marks `_version.py` to have this marker rewritten with a tag name -during the `git archive` command. As a result, generated tarballs will -contain enough information to get the proper version. - -To allow `setup.py` to compute a version too, a `versioneer.py` is added to -the top level of your source tree, next to `setup.py` and the `setup.cfg` -that configures it. This overrides several distutils/setuptools commands to -compute the version when invoked, and changes `setup.py build` and `setup.py -sdist` to replace `_version.py` with a small static file that contains just -the generated version data. - -## Installation - -See [INSTALL.md](./INSTALL.md) for detailed installation instructions. - -## Version-String Flavors - -Code which uses Versioneer can learn about its version string at runtime by -importing `_version` from your main `__init__.py` file and running the -`get_versions()` function. From the "outside" (e.g. in `setup.py`), you can -import the top-level `versioneer.py` and run `get_versions()`. - -Both functions return a dictionary with different flavors of version -information: - -* `['version']`: A condensed version string, rendered using the selected - style. This is the most commonly used value for the project's version - string. The default "pep440" style yields strings like `0.11`, - `0.11+2.g1076c97`, or `0.11+2.g1076c97.dirty`. See the "Styles" section - below for alternative styles. - -* `['full-revisionid']`: detailed revision identifier. For Git, this is the - full SHA1 commit id, e.g. "1076c978a8d3cfc70f408fe5974aa6c092c949ac". - -* `['date']`: Date and time of the latest `HEAD` commit. For Git, it is the - commit date in ISO 8601 format. This will be None if the date is not - available. - -* `['dirty']`: a boolean, True if the tree has uncommitted changes. Note that - this is only accurate if run in a VCS checkout, otherwise it is likely to - be False or None - -* `['error']`: if the version string could not be computed, this will be set - to a string describing the problem, otherwise it will be None. It may be - useful to throw an exception in setup.py if this is set, to avoid e.g. - creating tarballs with a version string of "unknown". - -Some variants are more useful than others. Including `full-revisionid` in a -bug report should allow developers to reconstruct the exact code being tested -(or indicate the presence of local changes that should be shared with the -developers). `version` is suitable for display in an "about" box or a CLI -`--version` output: it can be easily compared against release notes and lists -of bugs fixed in various releases. - -The installer adds the following text to your `__init__.py` to place a basic -version in `YOURPROJECT.__version__`: - - from ._version import get_versions - __version__ = get_versions()['version'] - del get_versions - -## Styles - -The setup.cfg `style=` configuration controls how the VCS information is -rendered into a version string. - -The default style, "pep440", produces a PEP440-compliant string, equal to the -un-prefixed tag name for actual releases, and containing an additional "local -version" section with more detail for in-between builds. For Git, this is -TAG[+DISTANCE.gHEX[.dirty]] , using information from `git describe --tags ---dirty --always`. For example "0.11+2.g1076c97.dirty" indicates that the -tree is like the "1076c97" commit but has uncommitted changes (".dirty"), and -that this commit is two revisions ("+2") beyond the "0.11" tag. For released -software (exactly equal to a known tag), the identifier will only contain the -stripped tag, e.g. "0.11". - -Other styles are available. See [details.md](details.md) in the Versioneer -source tree for descriptions. - -## Debugging - -Versioneer tries to avoid fatal errors: if something goes wrong, it will tend -to return a version of "0+unknown". To investigate the problem, run `setup.py -version`, which will run the version-lookup code in a verbose mode, and will -display the full contents of `get_versions()` (including the `error` string, -which may help identify what went wrong). - -## Known Limitations - -Some situations are known to cause problems for Versioneer. This details the -most significant ones. More can be found on Github -[issues page](https://github.com/warner/python-versioneer/issues). - -### Subprojects - -Versioneer has limited support for source trees in which `setup.py` is not in -the root directory (e.g. `setup.py` and `.git/` are *not* siblings). The are -two common reasons why `setup.py` might not be in the root: - -* Source trees which contain multiple subprojects, such as - [Buildbot](https://github.com/buildbot/buildbot), which contains both - "master" and "slave" subprojects, each with their own `setup.py`, - `setup.cfg`, and `tox.ini`. Projects like these produce multiple PyPI - distributions (and upload multiple independently-installable tarballs). -* Source trees whose main purpose is to contain a C library, but which also - provide bindings to Python (and perhaps other langauges) in subdirectories. - -Versioneer will look for `.git` in parent directories, and most operations -should get the right version string. However `pip` and `setuptools` have bugs -and implementation details which frequently cause `pip install .` from a -subproject directory to fail to find a correct version string (so it usually -defaults to `0+unknown`). - -`pip install --editable .` should work correctly. `setup.py install` might -work too. - -Pip-8.1.1 is known to have this problem, but hopefully it will get fixed in -some later version. - -[Bug #38](https://github.com/warner/python-versioneer/issues/38) is tracking -this issue. The discussion in -[PR #61](https://github.com/warner/python-versioneer/pull/61) describes the -issue from the Versioneer side in more detail. -[pip PR#3176](https://github.com/pypa/pip/pull/3176) and -[pip PR#3615](https://github.com/pypa/pip/pull/3615) contain work to improve -pip to let Versioneer work correctly. - -Versioneer-0.16 and earlier only looked for a `.git` directory next to the -`setup.cfg`, so subprojects were completely unsupported with those releases. - -### Editable installs with setuptools <= 18.5 - -`setup.py develop` and `pip install --editable .` allow you to install a -project into a virtualenv once, then continue editing the source code (and -test) without re-installing after every change. - -"Entry-point scripts" (`setup(entry_points={"console_scripts": ..})`) are a -convenient way to specify executable scripts that should be installed along -with the python package. - -These both work as expected when using modern setuptools. When using -setuptools-18.5 or earlier, however, certain operations will cause -`pkg_resources.DistributionNotFound` errors when running the entrypoint -script, which must be resolved by re-installing the package. This happens -when the install happens with one version, then the egg_info data is -regenerated while a different version is checked out. Many setup.py commands -cause egg_info to be rebuilt (including `sdist`, `wheel`, and installing into -a different virtualenv), so this can be surprising. - -[Bug #83](https://github.com/warner/python-versioneer/issues/83) describes -this one, but upgrading to a newer version of setuptools should probably -resolve it. - -### Unicode version strings - -While Versioneer works (and is continually tested) with both Python 2 and -Python 3, it is not entirely consistent with bytes-vs-unicode distinctions. -Newer releases probably generate unicode version strings on py2. It's not -clear that this is wrong, but it may be surprising for applications when then -write these strings to a network connection or include them in bytes-oriented -APIs like cryptographic checksums. - -[Bug #71](https://github.com/warner/python-versioneer/issues/71) investigates -this question. - - -## Updating Versioneer - -To upgrade your project to a new release of Versioneer, do the following: - -* install the new Versioneer (`pip install -U versioneer` or equivalent) -* edit `setup.cfg`, if necessary, to include any new configuration settings - indicated by the release notes. See [UPGRADING](./UPGRADING.md) for details. -* re-run `versioneer install` in your source tree, to replace - `SRC/_version.py` -* commit any changed files - -## Future Directions - -This tool is designed to make it easily extended to other version-control -systems: all VCS-specific components are in separate directories like -src/git/ . The top-level `versioneer.py` script is assembled from these -components by running make-versioneer.py . In the future, make-versioneer.py -will take a VCS name as an argument, and will construct a version of -`versioneer.py` that is specific to the given VCS. It might also take the -configuration arguments that are currently provided manually during -installation by editing setup.py . Alternatively, it might go the other -direction and include code from all supported VCS systems, reducing the -number of intermediate scripts. - - -## License - -To make Versioneer easier to embed, all its code is dedicated to the public -domain. The `_version.py` that it creates is also in the public domain. -Specifically, both are released under the Creative Commons "Public Domain -Dedication" license (CC0-1.0), as described in -https://creativecommons.org/publicdomain/zero/1.0/ . - -""" - -from __future__ import print_function -try: - import configparser -except ImportError: - import ConfigParser as configparser -import errno -import json -import os -import re -import subprocess -import sys - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_root(): - """Get the project root directory. - - We require that all commands are run from the project root, i.e. the - directory that contains setup.py, setup.cfg, and versioneer.py . - """ - root = os.path.realpath(os.path.abspath(os.getcwd())) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - # allow 'python path/to/setup.py COMMAND' - root = os.path.dirname(os.path.realpath(os.path.abspath(sys.argv[0]))) - setup_py = os.path.join(root, "setup.py") - versioneer_py = os.path.join(root, "versioneer.py") - if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - err = ("Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND').") - raise VersioneerBadRootError(err) - try: - # Certain runtime workflows (setup.py install/develop in a setuptools - # tree) execute all dependencies in a single python process, so - # "versioneer" may be imported multiple times, and python's shared - # module-import table will cache the first one. So we can't use - # os.path.dirname(__file__), as that will find whichever - # versioneer.py was first imported, even in later projects. - me = os.path.realpath(os.path.abspath(__file__)) - me_dir = os.path.normcase(os.path.splitext(me)[0]) - vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) - if me_dir != vsr_dir: - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py)) - except NameError: - pass - return root - - -def get_config_from_root(root): - """Read the project setup.cfg file to determine Versioneer config.""" - # This might raise EnvironmentError (if setup.cfg is missing), or - # configparser.NoSectionError (if it lacks a [versioneer] section), or - # configparser.NoOptionError (if it lacks "VCS="). See the docstring at - # the top of versioneer.py for instructions on writing your setup.cfg . - setup_cfg = os.path.join(root, "setup.cfg") - parser = configparser.SafeConfigParser() - with open(setup_cfg, "r") as f: - parser.readfp(f) - VCS = parser.get("versioneer", "VCS") # mandatory - - def get(parser, name): - if parser.has_option("versioneer", name): - return parser.get("versioneer", name) - return None - cfg = VersioneerConfig() - cfg.VCS = VCS - cfg.style = get(parser, "style") or "" - cfg.versionfile_source = get(parser, "versionfile_source") - cfg.versionfile_build = get(parser, "versionfile_build") - cfg.tag_prefix = get(parser, "tag_prefix") - if cfg.tag_prefix in ("''", '""'): - cfg.tag_prefix = "" - cfg.parentdir_prefix = get(parser, "parentdir_prefix") - cfg.verbose = get(parser, "verbose") - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -# these dictionaries contain VCS-specific tools -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %s" % dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %s" % (commands,)) - return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %s (error)" % dispcmd) - print("stdout was %s" % stdout) - return None, p.returncode - return stdout, p.returncode - - -LONG_VERSION_PY['git'] = ''' -# This file helps to compute a version number in source trees obtained from -# git-archive tarball (such as those provided by githubs download-from-tag -# feature). Distribution tarballs (built by setup.py sdist) and build -# directories (produced by setup.py build) will contain a much shorter file -# that just contains the computed version number. - -# This file is released into the public domain. Generated by -# versioneer-0.18 (https://github.com/warner/python-versioneer) - -"""Git implementation of _version.py.""" - -import errno -import os -import re -import subprocess -import sys - - -def get_keywords(): - """Get the keywords needed to look up the version information.""" - # these strings will be replaced by git during git-archive. - # setup.py/versioneer.py will grep for the variable names, so they must - # each be defined on a line of their own. _version.py will just call - # get_keywords(). - git_refnames = "%(DOLLAR)sFormat:%%d%(DOLLAR)s" - git_full = "%(DOLLAR)sFormat:%%H%(DOLLAR)s" - git_date = "%(DOLLAR)sFormat:%%ci%(DOLLAR)s" - keywords = {"refnames": git_refnames, "full": git_full, "date": git_date} - return keywords - - -class VersioneerConfig: - """Container for Versioneer configuration parameters.""" - - -def get_config(): - """Create, populate and return the VersioneerConfig() object.""" - # these strings are filled in when 'setup.py versioneer' creates - # _version.py - cfg = VersioneerConfig() - cfg.VCS = "git" - cfg.style = "%(STYLE)s" - cfg.tag_prefix = "%(TAG_PREFIX)s" - cfg.parentdir_prefix = "%(PARENTDIR_PREFIX)s" - cfg.versionfile_source = "%(VERSIONFILE_SOURCE)s" - cfg.verbose = False - return cfg - - -class NotThisMethod(Exception): - """Exception raised if a method is not valid for the current scenario.""" - - -LONG_VERSION_PY = {} -HANDLERS = {} - - -def register_vcs_handler(vcs, method): # decorator - """Decorator to mark a method as the handler for a particular VCS.""" - def decorate(f): - """Store f in HANDLERS[vcs][method].""" - if vcs not in HANDLERS: - HANDLERS[vcs] = {} - HANDLERS[vcs][method] = f - return f - return decorate - - -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): - """Call the given command(s).""" - assert isinstance(commands, list) - p = None - for c in commands: - try: - dispcmd = str([c] + args) - # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) - break - except EnvironmentError: - e = sys.exc_info()[1] - if e.errno == errno.ENOENT: - continue - if verbose: - print("unable to run %%s" %% dispcmd) - print(e) - return None, None - else: - if verbose: - print("unable to find command, tried %%s" %% (commands,)) - return None, None - stdout = p.communicate()[0].strip() - if sys.version_info[0] >= 3: - stdout = stdout.decode() - if p.returncode != 0: - if verbose: - print("unable to run %%s (error)" %% dispcmd) - print("stdout was %%s" %% stdout) - return None, p.returncode - return stdout, p.returncode - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for i in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %%s but none started with prefix %%s" %% - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - date = keywords.get("date") - if date is not None: - # git-2.2.0 added "%%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %%d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%%s', no digits" %% ",".join(refs - tags)) - if verbose: - print("likely tags: %%s" %% ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %%s" %% r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) - if rc != 0: - if verbose: - print("Directory %%s not under git control" %% root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%%s*" %% tag_prefix], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%%s'" - %% describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%%s' doesn't start with prefix '%%s'" - print(fmt %% (full_tag, tag_prefix)) - pieces["error"] = ("tag '%%s' doesn't start with prefix '%%s'" - %% (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%%ci", "HEAD"], - cwd=root)[0].strip() - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%%d.g%%s" %% (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%%d.g%%s" %% (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%%d" %% pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%%d" %% pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%%s" %% pieces["short"] - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%%s" %% pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%%d" %% pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%%d-g%%s" %% (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%%s'" %% style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -def get_versions(): - """Get version information or return default if unable to do so.""" - # I am in _version.py, which lives at ROOT/VERSIONFILE_SOURCE. If we have - # __file__, we can work backwards from there to the root. Some - # py2exe/bbfreeze/non-CPython implementations don't do __file__, in which - # case we can only use expanded keywords. - - cfg = get_config() - verbose = cfg.verbose - - try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) - except NotThisMethod: - pass - - try: - root = os.path.realpath(__file__) - # versionfile_source is the relative path from the top of the source - # tree (where the .git directory might live) to this file. Invert - # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): - root = os.path.dirname(root) - except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} - - try: - pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) - return render(pieces, cfg.style) - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - return versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - except NotThisMethod: - pass - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} -''' - - -@register_vcs_handler("git", "get_keywords") -def git_get_keywords(versionfile_abs): - """Extract version information from the given file.""" - # the code embedded in _version.py can just fetch the value of these - # keywords. When used from setup.py, we don't want to import _version.py, - # so we do it with a regexp instead. This function is not used from - # _version.py. - keywords = {} - try: - f = open(versionfile_abs, "r") - for line in f.readlines(): - if line.strip().startswith("git_refnames ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["refnames"] = mo.group(1) - if line.strip().startswith("git_full ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["full"] = mo.group(1) - if line.strip().startswith("git_date ="): - mo = re.search(r'=\s*"(.*)"', line) - if mo: - keywords["date"] = mo.group(1) - f.close() - except EnvironmentError: - pass - return keywords - - -@register_vcs_handler("git", "keywords") -def git_versions_from_keywords(keywords, tag_prefix, verbose): - """Get version information from git keywords.""" - if not keywords: - raise NotThisMethod("no keywords at all, weird") - date = keywords.get("date") - if date is not None: - # git-2.2.0 added "%cI", which expands to an ISO-8601 -compliant - # datestamp. However we prefer "%ci" (which expands to an "ISO-8601 - # -like" string, which we must then edit to make compliant), because - # it's been around since git-1.5.3, and it's too difficult to - # discover which version we're using, or to work around using an - # older one. - date = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - refnames = keywords["refnames"].strip() - if refnames.startswith("$Format"): - if verbose: - print("keywords are unexpanded, not using") - raise NotThisMethod("unexpanded keywords, not a git-archive tarball") - refs = set([r.strip() for r in refnames.strip("()").split(",")]) - # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of - # just "foo-1.0". If we see a "tag: " prefix, prefer those. - TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) - if not tags: - # Either we're using git < 1.8.3, or there really are no tags. We use - # a heuristic: assume all version tags have a digit. The old git %d - # expansion behaves like git log --decorate=short and strips out the - # refs/heads/ and refs/tags/ prefixes that would let us distinguish - # between branches and tags. By ignoring refnames without digits, we - # filter out many common branch names like "release" and - # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) - if verbose: - print("discarding '%s', no digits" % ",".join(refs - tags)) - if verbose: - print("likely tags: %s" % ",".join(sorted(tags))) - for ref in sorted(tags): - # sorting will prefer e.g. "2.0" over "2.0rc1" - if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] - if verbose: - print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} - # no suitable tags, so version is "0+unknown", but full hex is still there - if verbose: - print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} - - -@register_vcs_handler("git", "pieces_from_vcs") -def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): - """Get version from 'git describe' in the root of the source tree. - - This only gets called if the git-archive 'subst' keywords were *not* - expanded, and _version.py hasn't already been rewritten with a short - version string, meaning we're inside a checked out source tree. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) - if rc != 0: - if verbose: - print("Directory %s not under git control" % root) - raise NotThisMethod("'git rev-parse --git-dir' returned error") - - # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] - # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) - # --long was added in git-1.5.5 - if describe_out is None: - raise NotThisMethod("'git describe' failed") - describe_out = describe_out.strip() - full_out, rc = run_command(GITS, ["rev-parse", "HEAD"], cwd=root) - if full_out is None: - raise NotThisMethod("'git rev-parse' failed") - full_out = full_out.strip() - - pieces = {} - pieces["long"] = full_out - pieces["short"] = full_out[:7] # maybe improved later - pieces["error"] = None - - # parse describe_out. It will be like TAG-NUM-gHEX[-dirty] or HEX[-dirty] - # TAG might have hyphens. - git_describe = describe_out - - # look for -dirty suffix - dirty = git_describe.endswith("-dirty") - pieces["dirty"] = dirty - if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] - - # now we have TAG-NUM-gHEX or HEX - - if "-" in git_describe: - # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) - if not mo: - # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) - return pieces - - # tag - full_tag = mo.group(1) - if not full_tag.startswith(tag_prefix): - if verbose: - fmt = "tag '%s' doesn't start with prefix '%s'" - print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) - return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] - - # distance: number of commits since tag - pieces["distance"] = int(mo.group(2)) - - # commit: short hex revision ID - pieces["short"] = mo.group(3) - - else: - # HEX: no tags - pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) - pieces["distance"] = int(count_out) # total number of commits - - # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() - pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) - - return pieces - - -def do_vcs_install(manifest_in, versionfile_source, ipy): - """Git-specific installation logic for Versioneer. - - For Git, this means creating/changing .gitattributes to mark _version.py - for export-subst keyword substitution. - """ - GITS = ["git"] - if sys.platform == "win32": - GITS = ["git.cmd", "git.exe"] - files = [manifest_in, versionfile_source] - if ipy: - files.append(ipy) - try: - me = __file__ - if me.endswith(".pyc") or me.endswith(".pyo"): - me = os.path.splitext(me)[0] + ".py" - versioneer_file = os.path.relpath(me) - except NameError: - versioneer_file = "versioneer.py" - files.append(versioneer_file) - present = False - try: - f = open(".gitattributes", "r") - for line in f.readlines(): - if line.strip().startswith(versionfile_source): - if "export-subst" in line.strip().split()[1:]: - present = True - f.close() - except EnvironmentError: - pass - if not present: - f = open(".gitattributes", "a+") - f.write("%s export-subst\n" % versionfile_source) - f.close() - files.append(".gitattributes") - run_command(GITS, ["add", "--"] + files) - - -def versions_from_parentdir(parentdir_prefix, root, verbose): - """Try to determine the version from the parent directory name. - - Source tarballs conventionally unpack into a directory that includes both - the project name and a version string. We will also support searching up - two directory levels for an appropriately named parent directory - """ - rootdirs = [] - - for i in range(3): - dirname = os.path.basename(root) - if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} - else: - rootdirs.append(root) - root = os.path.dirname(root) # up a level - - if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) - raise NotThisMethod("rootdir doesn't start with parentdir_prefix") - - -SHORT_VERSION_PY = """ -# This file was generated by 'versioneer.py' (0.18) from -# revision-control system data, or from the parent directory name of an -# unpacked source archive. Distribution tarballs contain a pre-generated copy -# of this file. - -import json - -version_json = ''' -%s -''' # END VERSION_JSON - - -def get_versions(): - return json.loads(version_json) -""" - - -def versions_from_file(filename): - """Try to determine the version from _version.py if present.""" - try: - with open(filename) as f: - contents = f.read() - except EnvironmentError: - raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) - if not mo: - raise NotThisMethod("no version_json in _version.py") - return json.loads(mo.group(1)) - - -def write_to_version_file(filename, versions): - """Write the given version number to the given _version.py file.""" - os.unlink(filename) - contents = json.dumps(versions, sort_keys=True, - indent=1, separators=(",", ": ")) - with open(filename, "w") as f: - f.write(SHORT_VERSION_PY % contents) - - print("set %s to '%s'" % (filename, versions["version"])) - - -def plus_or_dot(pieces): - """Return a + if we don't already have one, else return a .""" - if "+" in pieces.get("closest-tag", ""): - return "." - return "+" - - -def render_pep440(pieces): - """Build up version string, with post-release "local version identifier". - - Our goal: TAG[+DISTANCE.gHEX[.dirty]] . Note that if you - get a tagged build and then dirty it, you'll get TAG+0.gHEX.dirty - - Exceptions: - 1: no tags. git_describe was just HEX. 0+untagged.DISTANCE.gHEX[.dirty] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += plus_or_dot(pieces) - rendered += "%d.g%s" % (pieces["distance"], pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - else: - # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) - if pieces["dirty"]: - rendered += ".dirty" - return rendered - - -def render_pep440_pre(pieces): - """TAG[.post.devDISTANCE] -- No -dirty. - - Exceptions: - 1: no tags. 0.post.devDISTANCE - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += ".post.dev%d" % pieces["distance"] - else: - # exception #1 - rendered = "0.post.dev%d" % pieces["distance"] - return rendered - - -def render_pep440_post(pieces): - """TAG[.postDISTANCE[.dev0]+gHEX] . - - The ".dev0" means dirty. Note that .dev0 sorts backwards - (a dirty tree will appear "older" than the corresponding clean one), - but you shouldn't be releasing software with -dirty anyways. - - Exceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += plus_or_dot(pieces) - rendered += "g%s" % pieces["short"] - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - rendered += "+g%s" % pieces["short"] - return rendered - - -def render_pep440_old(pieces): - """TAG[.postDISTANCE[.dev0]] . - - The ".dev0" means dirty. - - Eexceptions: - 1: no tags. 0.postDISTANCE[.dev0] - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"] or pieces["dirty"]: - rendered += ".post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - else: - # exception #1 - rendered = "0.post%d" % pieces["distance"] - if pieces["dirty"]: - rendered += ".dev0" - return rendered - - -def render_git_describe(pieces): - """TAG[-DISTANCE-gHEX][-dirty]. - - Like 'git describe --tags --dirty --always'. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - if pieces["distance"]: - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render_git_describe_long(pieces): - """TAG-DISTANCE-gHEX[-dirty]. - - Like 'git describe --tags --dirty --always -long'. - The distance/hash is unconditional. - - Exceptions: - 1: no tags. HEX[-dirty] (note: no 'g' prefix) - """ - if pieces["closest-tag"]: - rendered = pieces["closest-tag"] - rendered += "-%d-g%s" % (pieces["distance"], pieces["short"]) - else: - # exception #1 - rendered = pieces["short"] - if pieces["dirty"]: - rendered += "-dirty" - return rendered - - -def render(pieces, style): - """Render the given version pieces into the requested style.""" - if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} - - if not style or style == "default": - style = "pep440" # the default - - if style == "pep440": - rendered = render_pep440(pieces) - elif style == "pep440-pre": - rendered = render_pep440_pre(pieces) - elif style == "pep440-post": - rendered = render_pep440_post(pieces) - elif style == "pep440-old": - rendered = render_pep440_old(pieces) - elif style == "git-describe": - rendered = render_git_describe(pieces) - elif style == "git-describe-long": - rendered = render_git_describe_long(pieces) - else: - raise ValueError("unknown style '%s'" % style) - - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} - - -class VersioneerBadRootError(Exception): - """The project root directory is unknown or missing key files.""" - - -def get_versions(verbose=False): - """Get the project version from whatever source is available. - - Returns dict with two keys: 'version' and 'full'. - """ - if "versioneer" in sys.modules: - # see the discussion in cmdclass.py:get_cmdclass() - del sys.modules["versioneer"] - - root = get_root() - cfg = get_config_from_root(root) - - assert cfg.VCS is not None, "please set [versioneer]VCS= in setup.cfg" - handlers = HANDLERS.get(cfg.VCS) - assert handlers, "unrecognized VCS '%s'" % cfg.VCS - verbose = verbose or cfg.verbose - assert cfg.versionfile_source is not None, \ - "please set versioneer.versionfile_source" - assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" - - versionfile_abs = os.path.join(root, cfg.versionfile_source) - - # extract version from first of: _version.py, VCS command (e.g. 'git - # describe'), parentdir. This is meant to work for developers using a - # source checkout, for users of a tarball created by 'setup.py sdist', - # and for users of a tarball/zipball created by 'git archive' or github's - # download-from-tag feature or the equivalent in other VCSes. - - get_keywords_f = handlers.get("get_keywords") - from_keywords_f = handlers.get("keywords") - if get_keywords_f and from_keywords_f: - try: - keywords = get_keywords_f(versionfile_abs) - ver = from_keywords_f(keywords, cfg.tag_prefix, verbose) - if verbose: - print("got version from expanded keyword %s" % ver) - return ver - except NotThisMethod: - pass - - try: - ver = versions_from_file(versionfile_abs) - if verbose: - print("got version from file %s %s" % (versionfile_abs, ver)) - return ver - except NotThisMethod: - pass - - from_vcs_f = handlers.get("pieces_from_vcs") - if from_vcs_f: - try: - pieces = from_vcs_f(cfg.tag_prefix, root, verbose) - ver = render(pieces, cfg.style) - if verbose: - print("got version from VCS %s" % ver) - return ver - except NotThisMethod: - pass - - try: - if cfg.parentdir_prefix: - ver = versions_from_parentdir(cfg.parentdir_prefix, root, verbose) - if verbose: - print("got version from parentdir %s" % ver) - return ver - except NotThisMethod: - pass - - if verbose: - print("unable to compute version") - - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version", - "date": None} - - -def get_version(): - """Get the short version string for this project.""" - return get_versions()["version"] - - -def get_cmdclass(): - """Get the custom setuptools/distutils subclasses used by Versioneer.""" - if "versioneer" in sys.modules: - del sys.modules["versioneer"] - # this fixes the "python setup.py develop" case (also 'install' and - # 'easy_install .'), in which subdependencies of the main project are - # built (using setup.py bdist_egg) in the same python process. Assume - # a main project A and a dependency B, which use different versions - # of Versioneer. A's setup.py imports A's Versioneer, leaving it in - # sys.modules by the time B's setup.py is executed, causing B to run - # with the wrong versioneer. Setuptools wraps the sub-dep builds in a - # sandbox that restores sys.modules to it's pre-build state, so the - # parent is protected against the child's "import versioneer". By - # removing ourselves from sys.modules here, before the child build - # happens, we protect the child from the parent's versioneer too. - # Also see https://github.com/warner/python-versioneer/issues/52 - - cmds = {} - - # we add "version" to both distutils and setuptools - from distutils.core import Command - - class cmd_version(Command): - description = "report generated version string" - user_options = [] - boolean_options = [] - - def initialize_options(self): - pass - - def finalize_options(self): - pass - - def run(self): - vers = get_versions(verbose=True) - print("Version: %s" % vers["version"]) - print(" full-revisionid: %s" % vers.get("full-revisionid")) - print(" dirty: %s" % vers.get("dirty")) - print(" date: %s" % vers.get("date")) - if vers["error"]: - print(" error: %s" % vers["error"]) - cmds["version"] = cmd_version - - # we override "build_py" in both distutils and setuptools - # - # most invocation pathways end up running build_py: - # distutils/build -> build_py - # distutils/install -> distutils/build ->.. - # setuptools/bdist_wheel -> distutils/install ->.. - # setuptools/bdist_egg -> distutils/install_lib -> build_py - # setuptools/install -> bdist_egg ->.. - # setuptools/develop -> ? - # pip install: - # copies source tree to a tempdir before running egg_info/etc - # if .git isn't copied too, 'git describe' will fail - # then does setup.py bdist_wheel, or sometimes setup.py install - # setup.py egg_info -> ? - - # we override different "build_py" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.build_py import build_py as _build_py - else: - from distutils.command.build_py import build_py as _build_py - - class cmd_build_py(_build_py): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - _build_py.run(self) - # now locate _version.py in the new build/ directory and replace - # it with an updated value - if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - cmds["build_py"] = cmd_build_py - - if "cx_Freeze" in sys.modules: # cx_freeze enabled? - from cx_Freeze.dist import build_exe as _build_exe - # nczeczulin reports that py2exe won't like the pep440-style string - # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. - # setup(console=[{ - # "version": versioneer.get_version().split("+", 1)[0], # FILEVERSION - # "product_version": versioneer.get_version(), - # ... - - class cmd_build_exe(_build_exe): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _build_exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["build_exe"] = cmd_build_exe - del cmds["build_py"] - - if 'py2exe' in sys.modules: # py2exe enabled? - try: - from py2exe.distutils_buildexe import py2exe as _py2exe # py3 - except ImportError: - from py2exe.build_exe import py2exe as _py2exe # py2 - - class cmd_py2exe(_py2exe): - def run(self): - root = get_root() - cfg = get_config_from_root(root) - versions = get_versions() - target_versionfile = cfg.versionfile_source - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, versions) - - _py2exe.run(self) - os.unlink(target_versionfile) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - cmds["py2exe"] = cmd_py2exe - - # we override different "sdist" commands for both environments - if "setuptools" in sys.modules: - from setuptools.command.sdist import sdist as _sdist - else: - from distutils.command.sdist import sdist as _sdist - - class cmd_sdist(_sdist): - def run(self): - versions = get_versions() - self._versioneer_generated_versions = versions - # unless we update this, the command will keep using the old - # version - self.distribution.metadata.version = versions["version"] - return _sdist.run(self) - - def make_release_tree(self, base_dir, files): - root = get_root() - cfg = get_config_from_root(root) - _sdist.make_release_tree(self, base_dir, files) - # now locate _version.py in the new base_dir directory - # (remembering that it may be a hardlink) and replace it with an - # updated value - target_versionfile = os.path.join(base_dir, cfg.versionfile_source) - print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, - self._versioneer_generated_versions) - cmds["sdist"] = cmd_sdist - - return cmds - - -CONFIG_ERROR = """ -setup.cfg is missing the necessary Versioneer configuration. You need -a section like: - - [versioneer] - VCS = git - style = pep440 - versionfile_source = src/myproject/_version.py - versionfile_build = myproject/_version.py - tag_prefix = - parentdir_prefix = myproject- - -You will also need to edit your setup.py to use the results: - - import versioneer - setup(version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass(), ...) - -Please read the docstring in ./versioneer.py for configuration instructions, -edit setup.cfg, and re-run the installer or 'python versioneer.py setup'. -""" - -SAMPLE_CONFIG = """ -# See the docstring in versioneer.py for instructions. Note that you must -# re-run 'versioneer.py setup' after changing this section, and commit the -# resulting files. - -[versioneer] -#VCS = git -#style = pep440 -#versionfile_source = -#versionfile_build = -#tag_prefix = -#parentdir_prefix = - -""" - -INIT_PY_SNIPPET = """ -from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions -""" - - -def do_setup(): - """Main VCS-independent setup function for installing Versioneer.""" - root = get_root() - try: - cfg = get_config_from_root(root) - except (EnvironmentError, configparser.NoSectionError, - configparser.NoOptionError) as e: - if isinstance(e, (EnvironmentError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) - with open(os.path.join(root, "setup.cfg"), "a") as f: - f.write(SAMPLE_CONFIG) - print(CONFIG_ERROR, file=sys.stderr) - return 1 - - print(" creating %s" % cfg.versionfile_source) - with open(cfg.versionfile_source, "w") as f: - LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), - "__init__.py") - if os.path.exists(ipy): - try: - with open(ipy, "r") as f: - old = f.read() - except EnvironmentError: - old = "" - if INIT_PY_SNIPPET not in old: - print(" appending to %s" % ipy) - with open(ipy, "a") as f: - f.write(INIT_PY_SNIPPET) - else: - print(" %s unmodified" % ipy) - else: - print(" %s doesn't exist, ok" % ipy) - ipy = None - - # Make sure both the top-level "versioneer.py" and versionfile_source - # (PKG/_version.py, used by runtime code) are in MANIFEST.in, so - # they'll be copied into source distributions. Pip won't be able to - # install the package without this. - manifest_in = os.path.join(root, "MANIFEST.in") - simple_includes = set() - try: - with open(manifest_in, "r") as f: - for line in f: - if line.startswith("include "): - for include in line.split()[1:]: - simple_includes.add(include) - except EnvironmentError: - pass - # That doesn't cover everything MANIFEST.in can do - # (http://docs.python.org/2/distutils/sourcedist.html#commands), so - # it might give some false negatives. Appending redundant 'include' - # lines is safe, though. - if "versioneer.py" not in simple_includes: - print(" appending 'versioneer.py' to MANIFEST.in") - with open(manifest_in, "a") as f: - f.write("include versioneer.py\n") - else: - print(" 'versioneer.py' already in MANIFEST.in") - if cfg.versionfile_source not in simple_includes: - print(" appending versionfile_source ('%s') to MANIFEST.in" % - cfg.versionfile_source) - with open(manifest_in, "a") as f: - f.write("include %s\n" % cfg.versionfile_source) - else: - print(" versionfile_source already in MANIFEST.in") - - # Make VCS-specific changes. For git, this means creating/changing - # .gitattributes to mark _version.py for export-subst keyword - # substitution. - do_vcs_install(manifest_in, cfg.versionfile_source, ipy) - return 0 - - -def scan_setup_py(): - """Validate the contents of setup.py against Versioneer's expectations.""" - found = set() - setters = False - errors = 0 - with open("setup.py", "r") as f: - for line in f.readlines(): - if "import versioneer" in line: - found.add("import") - if "versioneer.get_cmdclass()" in line: - found.add("cmdclass") - if "versioneer.get_version()" in line: - found.add("get_version") - if "versioneer.VCS" in line: - setters = True - if "versioneer.versionfile_source" in line: - setters = True - if len(found) != 3: - print("") - print("Your setup.py appears to be missing some important items") - print("(but I might be wrong). Please make sure it has something") - print("roughly like the following:") - print("") - print(" import versioneer") - print(" setup( version=versioneer.get_version(),") - print(" cmdclass=versioneer.get_cmdclass(), ...)") - print("") - errors += 1 - if setters: - print("You should remove lines like 'versioneer.VCS = ' and") - print("'versioneer.versionfile_source = ' . This configuration") - print("now lives in setup.cfg, and should be removed from setup.py") - print("") - errors += 1 - return errors - - -if __name__ == "__main__": - cmd = sys.argv[1] - if cmd == "setup": - errors = do_setup() - errors += scan_setup_py() - if errors: - sys.exit(1) From 753bc770324b9d51ac852c9d3efecd982380e873 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Fri, 10 Jul 2020 10:17:34 +0200 Subject: [PATCH 020/110] formatting --- README.rst | 2 +- setup.cfg | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index c7973cf..0bf7971 100644 --- a/README.rst +++ b/README.rst @@ -32,6 +32,6 @@ Depending on your editor, the configuration (found in a file called mypy-ls.cfg { "enabled": True, - "live_mode": True, + "live_mode": True, "strict": False } diff --git a/setup.cfg b/setup.cfg index 063dac3..620cb07 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,16 +10,13 @@ classifiers = Intended Audience :: Developers Topic :: Software Development License :: OSI Approved :: MIT License - Programming Language :: Python :: 3.2 - Programming Language :: Python :: 3.3 - Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >= 3.2 +python_requires = >= 3.5 packages = find: install_requires = python-language-server<0.32.0 From b5deee20199f3d24f47988764e4252de09d40129 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Fri, 10 Jul 2020 10:20:37 +0200 Subject: [PATCH 021/110] new version requirement --- README.rst | 8 ++++---- setup.cfg | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/README.rst b/README.rst index 0bf7971..a0eb5fa 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,7 @@ Mypy plugin for PYLS This is a plugin for the Palantir's Python Language Server (https://github.com/palantir/python-language-server) -It, like mypy, requires Python 3.2 or newer. +It, like mypy, requires Python 3.6 or newer. Installation @@ -31,7 +31,7 @@ Depending on your editor, the configuration (found in a file called mypy-ls.cfg :: { - "enabled": True, - "live_mode": True, - "strict": False + "enabled": True, + "live_mode": True, + "strict": False } diff --git a/setup.cfg b/setup.cfg index 620cb07..a897582 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,13 +10,12 @@ classifiers = Intended Audience :: Developers Topic :: Software Development License :: OSI Approved :: MIT License - Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 [options] -python_requires = >= 3.5 +python_requires = >= 3.6 packages = find: install_requires = python-language-server<0.32.0 From b9fdb51f1a9f08be9b3ead22a38d33cd4146ac72 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Fri, 10 Jul 2020 10:21:24 +0200 Subject: [PATCH 022/110] Update python-package.yml --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f1abc2f..661fb3f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.5, 3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8] steps: - uses: actions/checkout@v2 From bf12dc95b5336f9f7379c7e2c0b95cc4c66ef0e3 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Fri, 10 Jul 2020 10:26:31 +0200 Subject: [PATCH 023/110] typo --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index d35bc4a..187a6a9 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python from setuptools import setup -from pyls_mypy import _version +from mypy_ls import _version if __name__ == "__main__": setup(version=_version.__version__) From a03716721bb97ebe31aae7078b641d2bf541613a Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Fri, 10 Jul 2020 10:27:48 +0200 Subject: [PATCH 024/110] version bump --- mypy_ls/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py index 493f741..260c070 100644 --- a/mypy_ls/_version.py +++ b/mypy_ls/_version.py @@ -1 +1 @@ -__version__ = "0.3.0" +__version__ = "0.3.1" From 7c675902e589accc9a0fd62c22606a3cc5f87a38 Mon Sep 17 00:00:00 2001 From: Richardk2n Date: Fri, 10 Jul 2020 10:44:27 +0200 Subject: [PATCH 025/110] update name. Finished for now --- .travis.yml | 11 ----------- MANIFEST.in | 3 +-- mypy_ls/_version.py | 2 +- 3 files changed, 2 insertions(+), 14 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ca2307e..0000000 --- a/.travis.yml +++ /dev/null @@ -1,11 +0,0 @@ -language: python -python: - - "3.6" -before_install: - - pip install -r requirements.txt -script: - - flake8 --exclude=./versioneer.py - - pytest - # - coverage run --source=. -m unittest discover -# after_success: - # - coveralls \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in index 7a0a016..6f22dca 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,2 @@ include README.rst -include versioneer.py -include pyls-mypy/_version.py +include mypy_ls/_version.py diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py index 260c070..f9aa3e1 100644 --- a/mypy_ls/_version.py +++ b/mypy_ls/_version.py @@ -1 +1 @@ -__version__ = "0.3.1" +__version__ = "0.3.2" From afd2135d73e6b318e6cdb7f42f317aa62265a2e2 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 12 Jul 2020 18:09:12 +0200 Subject: [PATCH 026/110] update pyls --- mypy_ls/_version.py | 2 +- requirements.txt | 2 +- setup.cfg | 4 ++-- test/test_plugin.py | 43 ++++++++++++++++++++++++++++++------------- 4 files changed, 34 insertions(+), 17 deletions(-) diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py index f9aa3e1..e19434e 100644 --- a/mypy_ls/_version.py +++ b/mypy_ls/_version.py @@ -1 +1 @@ -__version__ = "0.3.2" +__version__ = "0.3.3" diff --git a/requirements.txt b/requirements.txt index 625ebe3..a32a2de 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -python-language-server<0.32.0 +python-language-server>=0.34.0 mypy diff --git a/setup.cfg b/setup.cfg index a897582..971911a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -18,7 +18,7 @@ classifiers = python_requires = >= 3.6 packages = find: install_requires = - python-language-server<0.32.0 + python-language-server>=0.34.0 mypy @@ -36,4 +36,4 @@ test = exclude = contrib docs - test \ No newline at end of file + test diff --git a/test/test_plugin.py b/test/test_plugin.py index 78a7646..26b7043 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,6 +1,9 @@ import pytest -from pyls.workspace import Document +from pyls.workspace import Workspace, Document +from pyls.config.config import Config +from pyls import uris +from mock import Mock from mypy_ls import plugin DOC_URI = __file__ @@ -15,8 +18,16 @@ 'error: "Request" has no attribute "id"') +@pytest.fixture +def workspace(tmpdir): + """Return a workspace.""" + ws = Workspace(uris.from_fs_path(str(tmpdir)), Mock()) + ws._config = Config(ws.root_uri, {}, 0, {}) + return ws + + class FakeConfig(object): - + def __init__(self): self._root_path = "C:" @@ -24,9 +35,15 @@ def plugin_settings(self, plugin, document_path=None): return {} -def test_plugin(): +def test_settings(): config = FakeConfig() - doc = Document(DOC_URI, DOC_TYPE_ERR) + settings = plugin.pyls_settings(config) + assert settings == {"plugins": {"mypy-ls": {}}} + + +def test_plugin(workspace): + config = FakeConfig() + doc = Document(DOC_URI, workspace, DOC_TYPE_ERR) workspace = None plugin.pyls_settings(config) diags = plugin.pyls_lint(config, workspace, doc, is_saved=False) @@ -38,33 +55,33 @@ def test_plugin(): assert diag['range']['end'] == {'line': 0, 'character': 1} -def test_parse_full_line(): - doc = Document(DOC_URI, DOC_TYPE_ERR) +def test_parse_full_line(workspace): + doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE, doc) assert diag['message'] == '"Request" has no attribute "id"' assert diag['range']['start'] == {'line': 278, 'character': 7} assert diag['range']['end'] == {'line': 278, 'character': 8} -def test_parse_line_without_col(): - doc = Document(DOC_URI, DOC_TYPE_ERR) +def test_parse_line_without_col(workspace): + doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_COL, doc) assert diag['message'] == '"Request" has no attribute "id"' assert diag['range']['start'] == {'line': 278, 'character': 0} assert diag['range']['end'] == {'line': 278, 'character': 1} -def test_parse_line_without_line(): - doc = Document(DOC_URI, DOC_TYPE_ERR) +def test_parse_line_without_line(workspace): + doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE, doc) assert diag['message'] == '"Request" has no attribute "id"' assert diag['range']['start'] == {'line': 0, 'character': 0} - assert diag['range']['end'] == {'line': 0, 'character': 1} + assert diag['range']['end'] == {'line': 0, 'character': 6} @pytest.mark.parametrize('word,bounds', [('', (7, 8)), ('my_var', (7, 13))]) -def test_parse_line_with_context(monkeypatch, word, bounds): - doc = Document(DOC_URI, 'DOC_TYPE_ERR') +def test_parse_line_with_context(monkeypatch, word, bounds, workspace): + doc = Document(DOC_URI, workspace) monkeypatch.setattr(Document, 'word_at_position', lambda *args: word) diag = plugin.parse_line(TEST_LINE, doc) assert diag['message'] == '"Request" has no attribute "id"' From 088b87aab936e12f415afa66f6562ff0bb9e5612 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 12 Jul 2020 18:17:57 +0200 Subject: [PATCH 027/110] update requirements --- mypy_ls/_version.py | 2 +- setup.cfg | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py index e19434e..334b899 100644 --- a/mypy_ls/_version.py +++ b/mypy_ls/_version.py @@ -1 +1 @@ -__version__ = "0.3.3" +__version__ = "0.3.4" diff --git a/setup.cfg b/setup.cfg index 971911a..e4d82ce 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ test = pytest pytest-cov coverage + mock [options.packages.find] exclude = From bbfae72bebb9d1359dd717b96ca804f28e40ea4d Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 12 Jul 2020 18:19:36 +0200 Subject: [PATCH 028/110] Update python-package.yml --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 661fb3f..274dcb1 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest + pip install flake8 pytest mock if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | From a2db211d91a1860eb7c58999501e688664c3b413 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 12 Jul 2020 18:20:39 +0200 Subject: [PATCH 029/110] Update _version.py --- mypy_ls/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py index 334b899..a8d4557 100644 --- a/mypy_ls/_version.py +++ b/mypy_ls/_version.py @@ -1 +1 @@ -__version__ = "0.3.4" +__version__ = "0.3.5" From 6ed04c10b3135583fdbabf88e83ca81cdfd21536 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Tue, 27 Oct 2020 16:22:56 +0100 Subject: [PATCH 030/110] make configs work on linux --- mypy_ls/_version.py | 2 +- mypy_ls/plugin.py | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py index a8d4557..d7b30e1 100644 --- a/mypy_ls/_version.py +++ b/mypy_ls/_version.py @@ -1 +1 @@ -__version__ = "0.3.5" +__version__ = "0.3.6" diff --git a/mypy_ls/plugin.py b/mypy_ls/plugin.py index 03a92f1..e1e9e55 100644 --- a/mypy_ls/plugin.py +++ b/mypy_ls/plugin.py @@ -178,6 +178,8 @@ def init(workspace: str) -> Dict[str, str]: The plugin config dict. """ + # On windows the path contains \\ on linux it contains / all the code works with / + workspace = workspace.replace("\\", "/") configuration = {} path = findConfigFile(workspace, "mypy-ls.cfg") if path: @@ -214,11 +216,11 @@ def findConfigFile(path: str, name: str) -> Optional[str]: """ while True: - p = f"{path}\\{name}" + p = f"{path}/{name}" if os.path.isfile(p): return p else: - loc = path.rfind("\\") + loc = path.rfind("/") if loc == -1: return None path = path[:loc] From 1489e85d4afa6d34a12e95517cb5712acdebfaef Mon Sep 17 00:00:00 2001 From: Fidel Ramos Date: Thu, 13 May 2021 23:48:32 +0200 Subject: [PATCH 031/110] Depend on python-lsp-server Palantir's python-language-server is now unmaintained. python-lsp-server is a community-maintained fork. --- README.rst | 10 +++--- mypy_ls/plugin.py | 77 +++++++++++++++++++++++---------------------- requirements.txt | 7 +++-- setup.cfg | 8 ++--- test/test_plugin.py | 53 +++++++++++++++---------------- 5 files changed, 79 insertions(+), 76 deletions(-) diff --git a/README.rst b/README.rst index a0eb5fa..5171b65 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ Mypy plugin for PYLS .. image:: https://github.com/Richardk2n/pyls-mypy/workflows/Python%20package/badge.svg?branch=master :target: https://github.com/Richardk2n/pyls-mypy/ -This is a plugin for the Palantir's Python Language Server (https://github.com/palantir/python-language-server) +This is a plugin for the [Python LSP Server](https://github.com/python-lsp/python-lsp-server). It, like mypy, requires Python 3.6 or newer. @@ -15,7 +15,7 @@ It, like mypy, requires Python 3.6 or newer. Installation ------------ -Install into the same virtualenv as pyls itself. +Install into the same virtualenv as python-lsp-server itself. ``pip install mypy-ls`` @@ -31,7 +31,7 @@ Depending on your editor, the configuration (found in a file called mypy-ls.cfg :: { - "enabled": True, - "live_mode": True, - "strict": False + "enabled": True, + "live_mode": True, + "strict": False } diff --git a/mypy_ls/plugin.py b/mypy_ls/plugin.py index e1e9e55..dac4608 100644 --- a/mypy_ls/plugin.py +++ b/mypy_ls/plugin.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -File that contains the pyls plugin mypy-ls. +File that contains the pylsp plugin mypy-ls. Created on Fri Jul 10 09:53:57 2020 @@ -12,9 +12,9 @@ import os.path import logging from mypy import api as mypy_api -from pyls import hookimpl -from pyls.workspace import Document, Workspace -from pyls.config.config import Config +from pylsp import hookimpl +from pylsp.workspace import Document, Workspace +from pylsp.config.config import Config from typing import Optional, Dict, Any, IO, List import atexit @@ -27,7 +27,9 @@ tmpFile: Optional[IO[str]] = None -def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[str, Any]]: +def parse_line( + line: str, document: Optional[Document] = None +) -> Optional[Dict[str, Any]]: """ Return a language-server diagnostic from a line of the Mypy error report. @@ -54,51 +56,53 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[ if file_path != "": # live mode # results from other files can be included, but we cannot return # them. - if document and document.path and not document.path.endswith( - file_path): - log.warning("discarding result for %s against %s", file_path, - document.path) + if document and document.path and not document.path.endswith(file_path): + log.warning( + "discarding result for %s against %s", file_path, document.path + ) return None lineno = int(linenoStr or 1) - 1 # 0-based line number offset = int(offsetStr or 1) - 1 # 0-based offset errno = 2 - if severity == 'error': + if severity == "error": errno = 1 diag: Dict[str, Any] = { - 'source': 'mypy', - 'range': { - 'start': {'line': lineno, 'character': offset}, + "source": "mypy", + "range": { + "start": {"line": lineno, "character": offset}, # There may be a better solution, but mypy does not provide end - 'end': {'line': lineno, 'character': offset + 1} + "end": {"line": lineno, "character": offset + 1}, }, - 'message': msg, - 'severity': errno + "message": msg, + "severity": errno, } if document: # although mypy does not provide the end of the affected range, we # can make a good guess by highlighting the word that Mypy flagged - word = document.word_at_position(diag['range']['start']) + word = document.word_at_position(diag["range"]["start"]) if word: - diag['range']['end']['character'] = ( - diag['range']['start']['character'] + len(word)) + diag["range"]["end"]["character"] = diag["range"]["start"][ + "character" + ] + len(word) return diag return None @hookimpl -def pyls_lint(config: Config, workspace: Workspace, document: Document, - is_saved: bool) -> List[Dict[str, Any]]: +def pylsp_lint( + config: Config, workspace: Workspace, document: Document, is_saved: bool +) -> List[Dict[str, Any]]: """ Lints. Parameters ---------- config : Config - The pyls config. + The pylsp config. workspace : Workspace - The pyls workspace. + The pylsp workspace. document : Document The document to be linted. is_saved : bool @@ -110,27 +114,25 @@ def pyls_lint(config: Config, workspace: Workspace, document: Document, List of the linting data. """ - settings = config.plugin_settings('mypy-ls') - live_mode = settings.get('live_mode', True) - args = ['--incremental', - '--show-column-numbers', - '--follow-imports', 'silent'] + settings = config.plugin_settings("mypy-ls") + live_mode = settings.get("live_mode", True) + args = ["--incremental", "--show-column-numbers", "--follow-imports", "silent"] global tmpFile if live_mode and not is_saved and tmpFile: tmpFile = open(tmpFile.name, "w") tmpFile.write(document.source) tmpFile.close() - args.extend(['--shadow-file', document.path, tmpFile.name]) + args.extend(["--shadow-file", document.path, tmpFile.name]) elif not is_saved: return [] if mypyConfigFile: - args.append('--config-file') + args.append("--config-file") args.append(mypyConfigFile) args.append(document.path) - if settings.get('strict', False): - args.append('--strict') + if settings.get("strict", False): + args.append("--strict") report, errors, _ = mypy_api.run(args) @@ -144,14 +146,14 @@ def pyls_lint(config: Config, workspace: Workspace, document: Document, @hookimpl -def pyls_settings(config: Config) -> Dict[str, Dict[str, Dict[str, str]]]: +def pylsp_settings(config: Config) -> Dict[str, Dict[str, Dict[str, str]]]: """ Read the settings. Parameters ---------- config : Config - The pyls config. + The pylsp config. Returns ------- @@ -187,10 +189,11 @@ def init(workspace: str) -> Dict[str, str]: configuration = eval(file.read()) global mypyConfigFile mypyConfigFile = findConfigFile(workspace, "mypy.ini") - if (("enabled" not in configuration or configuration["enabled"]) - and ("live_mode" not in configuration or configuration["live_mode"])): + if ("enabled" not in configuration or configuration["enabled"]) and ( + "live_mode" not in configuration or configuration["live_mode"] + ): global tmpFile - tmpFile = tempfile.NamedTemporaryFile('w', delete=False) + tmpFile = tempfile.NamedTemporaryFile("w", delete=False) tmpFile.close() return configuration diff --git a/requirements.txt b/requirements.txt index a32a2de..f865a1d 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,5 @@ -python-language-server>=0.34.0 -mypy +future;python_version < '3' +flake8 +configparser +python-lsp-server +mypy;python_version >= '3.2' diff --git a/setup.cfg b/setup.cfg index e4d82ce..2ef0ec6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,7 @@ [metadata] name = mypy-ls author = Tom van Ommeren, Richard Kellnberger -description = Mypy linter for the Python Language Server +description = Mypy linter for the Python LSP Server url = https://github.com/Richardk2n/pyls-mypy long_description = file: README.rst license='MIT' @@ -17,13 +17,13 @@ classifiers = [options] python_requires = >= 3.6 packages = find: -install_requires = - python-language-server>=0.34.0 +install_requires = + python-lsp-server mypy [options.entry_points] -pyls = mypy_ls = mypy_ls.plugin +pylsp = mypy_ls = mypy_ls.plugin [options.extras_require] test = diff --git a/test/test_plugin.py b/test/test_plugin.py index 26b7043..48650ba 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,8 +1,8 @@ import pytest -from pyls.workspace import Workspace, Document -from pyls.config.config import Config -from pyls import uris +from pylsp.workspace import Workspace, Document +from pylsp.config.config import Config +from pylsp import uris from mock import Mock from mypy_ls import plugin @@ -12,10 +12,8 @@ TYPE_ERR_MSG = '"Dict[, ]" has no attribute "append"' TEST_LINE = 'test_plugin.py:279:8: error: "Request" has no attribute "id"' -TEST_LINE_WITHOUT_COL = ('test_plugin.py:279: ' - 'error: "Request" has no attribute "id"') -TEST_LINE_WITHOUT_LINE = ('test_plugin.py: ' - 'error: "Request" has no attribute "id"') +TEST_LINE_WITHOUT_COL = "test_plugin.py:279: " 'error: "Request" has no attribute "id"' +TEST_LINE_WITHOUT_LINE = "test_plugin.py: " 'error: "Request" has no attribute "id"' @pytest.fixture @@ -27,7 +25,6 @@ def workspace(tmpdir): class FakeConfig(object): - def __init__(self): self._root_path = "C:" @@ -37,7 +34,7 @@ def plugin_settings(self, plugin, document_path=None): def test_settings(): config = FakeConfig() - settings = plugin.pyls_settings(config) + settings = plugin.pylsp_settings(config) assert settings == {"plugins": {"mypy-ls": {}}} @@ -45,45 +42,45 @@ def test_plugin(workspace): config = FakeConfig() doc = Document(DOC_URI, workspace, DOC_TYPE_ERR) workspace = None - plugin.pyls_settings(config) - diags = plugin.pyls_lint(config, workspace, doc, is_saved=False) + plugin.pylsp_settings(config) + diags = plugin.pylsp_lint(config, workspace, doc, is_saved=False) assert len(diags) == 1 diag = diags[0] - assert diag['message'] == TYPE_ERR_MSG - assert diag['range']['start'] == {'line': 0, 'character': 0} - assert diag['range']['end'] == {'line': 0, 'character': 1} + assert diag["message"] == TYPE_ERR_MSG + assert diag["range"]["start"] == {"line": 0, "character": 0} + assert diag["range"]["end"] == {"line": 0, "character": 1} def test_parse_full_line(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE, doc) - assert diag['message'] == '"Request" has no attribute "id"' - assert diag['range']['start'] == {'line': 278, 'character': 7} - assert diag['range']['end'] == {'line': 278, 'character': 8} + assert diag["message"] == '"Request" has no attribute "id"' + assert diag["range"]["start"] == {"line": 278, "character": 7} + assert diag["range"]["end"] == {"line": 278, "character": 8} def test_parse_line_without_col(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_COL, doc) - assert diag['message'] == '"Request" has no attribute "id"' - assert diag['range']['start'] == {'line': 278, 'character': 0} - assert diag['range']['end'] == {'line': 278, 'character': 1} + assert diag["message"] == '"Request" has no attribute "id"' + assert diag["range"]["start"] == {"line": 278, "character": 0} + assert diag["range"]["end"] == {"line": 278, "character": 1} def test_parse_line_without_line(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE, doc) - assert diag['message'] == '"Request" has no attribute "id"' - assert diag['range']['start'] == {'line': 0, 'character': 0} - assert diag['range']['end'] == {'line': 0, 'character': 6} + assert diag["message"] == '"Request" has no attribute "id"' + assert diag["range"]["start"] == {"line": 0, "character": 0} + assert diag["range"]["end"] == {"line": 0, "character": 6} -@pytest.mark.parametrize('word,bounds', [('', (7, 8)), ('my_var', (7, 13))]) +@pytest.mark.parametrize("word,bounds", [("", (7, 8)), ("my_var", (7, 13))]) def test_parse_line_with_context(monkeypatch, word, bounds, workspace): doc = Document(DOC_URI, workspace) - monkeypatch.setattr(Document, 'word_at_position', lambda *args: word) + monkeypatch.setattr(Document, "word_at_position", lambda *args: word) diag = plugin.parse_line(TEST_LINE, doc) - assert diag['message'] == '"Request" has no attribute "id"' - assert diag['range']['start'] == {'line': 278, 'character': bounds[0]} - assert diag['range']['end'] == {'line': 278, 'character': bounds[1]} + assert diag["message"] == '"Request" has no attribute "id"' + assert diag["range"]["start"] == {"line": 278, "character": bounds[0]} + assert diag["range"]["end"] == {"line": 278, "character": bounds[1]} From 80ac5fad83c446b32c3fe89f6137b53569095cfb Mon Sep 17 00:00:00 2001 From: Fidel Ramos Date: Tue, 18 May 2021 18:50:48 +0200 Subject: [PATCH 032/110] Undo black formatting from commit 1489e85d4afa6d34a12 --- mypy_ls/plugin.py | 63 +++++++++++++++++++++------------------------ test/test_plugin.py | 41 +++++++++++++++-------------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/mypy_ls/plugin.py b/mypy_ls/plugin.py index dac4608..c49b1d3 100644 --- a/mypy_ls/plugin.py +++ b/mypy_ls/plugin.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -File that contains the pylsp plugin mypy-ls. +File that contains the python-lsp-server plugin mypy-ls. Created on Fri Jul 10 09:53:57 2020 @@ -27,9 +27,7 @@ tmpFile: Optional[IO[str]] = None -def parse_line( - line: str, document: Optional[Document] = None -) -> Optional[Dict[str, Any]]: +def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[str, Any]]: """ Return a language-server diagnostic from a line of the Mypy error report. @@ -56,44 +54,42 @@ def parse_line( if file_path != "": # live mode # results from other files can be included, but we cannot return # them. - if document and document.path and not document.path.endswith(file_path): - log.warning( - "discarding result for %s against %s", file_path, document.path - ) + if document and document.path and not document.path.endswith( + file_path): + log.warning("discarding result for %s against %s", file_path, + document.path) return None lineno = int(linenoStr or 1) - 1 # 0-based line number offset = int(offsetStr or 1) - 1 # 0-based offset errno = 2 - if severity == "error": + if severity == 'error': errno = 1 diag: Dict[str, Any] = { - "source": "mypy", - "range": { - "start": {"line": lineno, "character": offset}, + 'source': 'mypy', + 'range': { + 'start': {'line': lineno, 'character': offset}, # There may be a better solution, but mypy does not provide end - "end": {"line": lineno, "character": offset + 1}, + 'end': {'line': lineno, 'character': offset + 1} }, - "message": msg, - "severity": errno, + 'message': msg, + 'severity': errno } if document: # although mypy does not provide the end of the affected range, we # can make a good guess by highlighting the word that Mypy flagged - word = document.word_at_position(diag["range"]["start"]) + word = document.word_at_position(diag['range']['start']) if word: - diag["range"]["end"]["character"] = diag["range"]["start"][ - "character" - ] + len(word) + diag['range']['end']['character'] = ( + diag['range']['start']['character'] + len(word)) return diag return None @hookimpl -def pylsp_lint( - config: Config, workspace: Workspace, document: Document, is_saved: bool -) -> List[Dict[str, Any]]: +def pylsp_lint(config: Config, workspace: Workspace, document: Document, + is_saved: bool) -> List[Dict[str, Any]]: """ Lints. @@ -114,25 +110,27 @@ def pylsp_lint( List of the linting data. """ - settings = config.plugin_settings("mypy-ls") - live_mode = settings.get("live_mode", True) - args = ["--incremental", "--show-column-numbers", "--follow-imports", "silent"] + settings = config.plugin_settings('mypy-ls') + live_mode = settings.get('live_mode', True) + args = ['--incremental', + '--show-column-numbers', + '--follow-imports', 'silent'] global tmpFile if live_mode and not is_saved and tmpFile: tmpFile = open(tmpFile.name, "w") tmpFile.write(document.source) tmpFile.close() - args.extend(["--shadow-file", document.path, tmpFile.name]) + args.extend(['--shadow-file', document.path, tmpFile.name]) elif not is_saved: return [] if mypyConfigFile: - args.append("--config-file") + args.append('--config-file') args.append(mypyConfigFile) args.append(document.path) - if settings.get("strict", False): - args.append("--strict") + if settings.get('strict', False): + args.append('--strict') report, errors, _ = mypy_api.run(args) @@ -189,11 +187,10 @@ def init(workspace: str) -> Dict[str, str]: configuration = eval(file.read()) global mypyConfigFile mypyConfigFile = findConfigFile(workspace, "mypy.ini") - if ("enabled" not in configuration or configuration["enabled"]) and ( - "live_mode" not in configuration or configuration["live_mode"] - ): + if (("enabled" not in configuration or configuration["enabled"]) + and ("live_mode" not in configuration or configuration["live_mode"])): global tmpFile - tmpFile = tempfile.NamedTemporaryFile("w", delete=False) + tmpFile = tempfile.NamedTemporaryFile('w', delete=False) tmpFile.close() return configuration diff --git a/test/test_plugin.py b/test/test_plugin.py index 48650ba..44eed41 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -12,8 +12,10 @@ TYPE_ERR_MSG = '"Dict[, ]" has no attribute "append"' TEST_LINE = 'test_plugin.py:279:8: error: "Request" has no attribute "id"' -TEST_LINE_WITHOUT_COL = "test_plugin.py:279: " 'error: "Request" has no attribute "id"' -TEST_LINE_WITHOUT_LINE = "test_plugin.py: " 'error: "Request" has no attribute "id"' +TEST_LINE_WITHOUT_COL = ('test_plugin.py:279: ' + 'error: "Request" has no attribute "id"') +TEST_LINE_WITHOUT_LINE = ('test_plugin.py: ' + 'error: "Request" has no attribute "id"') @pytest.fixture @@ -25,6 +27,7 @@ def workspace(tmpdir): class FakeConfig(object): + def __init__(self): self._root_path = "C:" @@ -47,40 +50,40 @@ def test_plugin(workspace): assert len(diags) == 1 diag = diags[0] - assert diag["message"] == TYPE_ERR_MSG - assert diag["range"]["start"] == {"line": 0, "character": 0} - assert diag["range"]["end"] == {"line": 0, "character": 1} + assert diag['message'] == TYPE_ERR_MSG + assert diag['range']['start'] == {'line': 0, 'character': 0} + assert diag['range']['end'] == {'line': 0, 'character': 1} def test_parse_full_line(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE, doc) - assert diag["message"] == '"Request" has no attribute "id"' - assert diag["range"]["start"] == {"line": 278, "character": 7} - assert diag["range"]["end"] == {"line": 278, "character": 8} + assert diag['message'] == '"Request" has no attribute "id"' + assert diag['range']['start'] == {'line': 278, 'character': 7} + assert diag['range']['end'] == {'line': 278, 'character': 8} def test_parse_line_without_col(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_COL, doc) - assert diag["message"] == '"Request" has no attribute "id"' - assert diag["range"]["start"] == {"line": 278, "character": 0} - assert diag["range"]["end"] == {"line": 278, "character": 1} + assert diag['message'] == '"Request" has no attribute "id"' + assert diag['range']['start'] == {'line': 278, 'character': 0} + assert diag['range']['end'] == {'line': 278, 'character': 1} def test_parse_line_without_line(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE, doc) - assert diag["message"] == '"Request" has no attribute "id"' - assert diag["range"]["start"] == {"line": 0, "character": 0} - assert diag["range"]["end"] == {"line": 0, "character": 6} + assert diag['message'] == '"Request" has no attribute "id"' + assert diag['range']['start'] == {'line': 0, 'character': 0} + assert diag['range']['end'] == {'line': 0, 'character': 6} -@pytest.mark.parametrize("word,bounds", [("", (7, 8)), ("my_var", (7, 13))]) +@pytest.mark.parametrize('word,bounds', [('', (7, 8)), ('my_var', (7, 13))]) def test_parse_line_with_context(monkeypatch, word, bounds, workspace): doc = Document(DOC_URI, workspace) - monkeypatch.setattr(Document, "word_at_position", lambda *args: word) + monkeypatch.setattr(Document, 'word_at_position', lambda *args: word) diag = plugin.parse_line(TEST_LINE, doc) - assert diag["message"] == '"Request" has no attribute "id"' - assert diag["range"]["start"] == {"line": 278, "character": bounds[0]} - assert diag["range"]["end"] == {"line": 278, "character": bounds[1]} + assert diag['message'] == '"Request" has no attribute "id"' + assert diag['range']['start'] == {'line': 278, 'character': bounds[0]} + assert diag['range']['end'] == {'line': 278, 'character': bounds[1]} From 4399194f2bfcb5f98635d436c752c2778c8dca96 Mon Sep 17 00:00:00 2001 From: Fidel Ramos Date: Tue, 18 May 2021 18:51:08 +0200 Subject: [PATCH 033/110] Revert extra requirements from commit 1489e85d4afa6d34a --- requirements.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/requirements.txt b/requirements.txt index f865a1d..b0840b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,2 @@ -future;python_version < '3' -flake8 -configparser python-lsp-server -mypy;python_version >= '3.2' +mypy From 817c4ef88a16a6b4f7d11568c36fdcc602bcae87 Mon Sep 17 00:00:00 2001 From: Fidel Ramos Date: Tue, 18 May 2021 18:53:13 +0200 Subject: [PATCH 034/110] Revert indent on README --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 5171b65..0b6c84d 100644 --- a/README.rst +++ b/README.rst @@ -31,7 +31,7 @@ Depending on your editor, the configuration (found in a file called mypy-ls.cfg :: { - "enabled": True, - "live_mode": True, - "strict": False + "enabled": True, + "live_mode": True, + "strict": False } From 06cb3c94fcc756249f180c54e1a9e145a4288482 Mon Sep 17 00:00:00 2001 From: Fidel Ramos Date: Tue, 18 May 2021 18:54:15 +0200 Subject: [PATCH 035/110] Fix indent in README --- README.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 0b6c84d..dcb642f 100644 --- a/README.rst +++ b/README.rst @@ -31,7 +31,7 @@ Depending on your editor, the configuration (found in a file called mypy-ls.cfg :: { - "enabled": True, - "live_mode": True, - "strict": False + "enabled": True, + "live_mode": True, + "strict": False } From f3be9a725496976bb9131e220e6706a7b5207a37 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Tue, 18 May 2021 19:28:34 +0200 Subject: [PATCH 036/110] Fixing link formatting --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index dcb642f..eef9258 100644 --- a/README.rst +++ b/README.rst @@ -7,8 +7,9 @@ Mypy plugin for PYLS .. image:: https://github.com/Richardk2n/pyls-mypy/workflows/Python%20package/badge.svg?branch=master :target: https://github.com/Richardk2n/pyls-mypy/ -This is a plugin for the [Python LSP Server](https://github.com/python-lsp/python-lsp-server). +This is a plugin for the `Python LSP Server`_. +.. _Python LSP Server: https://github.com/python-lsp/python-lsp-server It, like mypy, requires Python 3.6 or newer. From 382f6af0af56119d496f6d11d2b136f01eee7efe Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Tue, 18 May 2021 19:29:02 +0200 Subject: [PATCH 037/110] test 3.9 as well --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 274dcb1..da340ed 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8] + python-version: [3.6, 3.7, 3.8, 3.9] steps: - uses: actions/checkout@v2 From 4dd9c7e51ef30046585e32611f3a6803a922282c Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Tue, 18 May 2021 20:10:39 +0200 Subject: [PATCH 038/110] version bump --- mypy_ls/_version.py | 2 +- setup.cfg | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py index d7b30e1..6a9beea 100644 --- a/mypy_ls/_version.py +++ b/mypy_ls/_version.py @@ -1 +1 @@ -__version__ = "0.3.6" +__version__ = "0.4.0" diff --git a/setup.cfg b/setup.cfg index 2ef0ec6..3bb8042 100644 --- a/setup.cfg +++ b/setup.cfg @@ -13,6 +13,7 @@ classifiers = Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 [options] python_requires = >= 3.6 From 906a3dd6f60347724bccc4a015371d74b023a513 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Tue, 18 May 2021 20:58:12 +0200 Subject: [PATCH 039/110] make pipy accept formatting --- README.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.rst b/README.rst index eef9258..9d361ec 100644 --- a/README.rst +++ b/README.rst @@ -9,7 +9,8 @@ Mypy plugin for PYLS This is a plugin for the `Python LSP Server`_. -.. _Python LSP Server: https://github.com/python-lsp/python-lsp-server +.. _`Python LSP Server`: https://github.com/python-lsp/python-lsp-server + It, like mypy, requires Python 3.6 or newer. From ca189e0cd0e77139fbc3d7cad7c85073a947152b Mon Sep 17 00:00:00 2001 From: Fidel Ramos Date: Tue, 18 May 2021 21:45:26 +0200 Subject: [PATCH 040/110] Format codebase with black Add pre-commit and black pre-commit hook. Check black formatting in PRs. --- .github/workflows/python-package.yml | 4 ++ .pre-commit-config.yaml | 7 ++++ README.rst | 25 +++++++++++- mypy_ls/plugin.py | 61 +++++++++++++++------------- pyproject.toml | 14 +++++++ requirements.txt | 2 + test/test_plugin.py | 41 +++++++++---------- 7 files changed, 102 insertions(+), 52 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 pyproject.toml diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index da340ed..845981f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -34,6 +34,10 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Check black formatting + run: | + # stop the build if black detect any changes + black --check . - name: Test with pytest run: | pytest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5beba6e --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,7 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: + - repo: https://github.com/psf/black + rev: 21.5b1 + hooks: + - id: black diff --git a/README.rst b/README.rst index 9d361ec..138c891 100644 --- a/README.rst +++ b/README.rst @@ -26,7 +26,7 @@ Configuration ``live_mode`` (default is True) provides type checking as you type. This writes to a tempfile every time a check is done. -Turning off live_mode means you must save your changes for mypy diagnostics to update correctly. +Turning off ``live_mode`` means you must save your changes for mypy diagnostics to update correctly. Depending on your editor, the configuration (found in a file called mypy-ls.cfg in your workspace or a parent directory) should be roughly like this: @@ -37,3 +37,26 @@ Depending on your editor, the configuration (found in a file called mypy-ls.cfg "live_mode": True, "strict": False } + +Developing +------------- + +Install development dependencies with (you might want to create a virtualenv first): + +:: + + pip install -r requirements.txt + +The project is formatted with `black`_. You can either configure your IDE to automatically format code with it, run it manually (``black .``) or rely on pre-commit (see below) to format files on git commit. + +This project uses `pre-commit`_ to enforce code-quality. After cloning the repository install the pre-commit hooks with: + +:: + + pre-commit install + +After that pre-commit will run `all defined hooks`_ on every ``git commit`` and keep you from committing if there are any errors. + +.. _black: https://github.com/psf/black +.. _pre-commit: https://pre-commit.com/ +.. _all defined hooks: .pre-commit-config.yaml diff --git a/mypy_ls/plugin.py b/mypy_ls/plugin.py index c49b1d3..aabdd0c 100644 --- a/mypy_ls/plugin.py +++ b/mypy_ls/plugin.py @@ -27,7 +27,9 @@ tmpFile: Optional[IO[str]] = None -def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[str, Any]]: +def parse_line( + line: str, document: Optional[Document] = None +) -> Optional[Dict[str, Any]]: """ Return a language-server diagnostic from a line of the Mypy error report. @@ -54,42 +56,44 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[ if file_path != "": # live mode # results from other files can be included, but we cannot return # them. - if document and document.path and not document.path.endswith( - file_path): - log.warning("discarding result for %s against %s", file_path, - document.path) + if document and document.path and not document.path.endswith(file_path): + log.warning( + "discarding result for %s against %s", file_path, document.path + ) return None lineno = int(linenoStr or 1) - 1 # 0-based line number offset = int(offsetStr or 1) - 1 # 0-based offset errno = 2 - if severity == 'error': + if severity == "error": errno = 1 diag: Dict[str, Any] = { - 'source': 'mypy', - 'range': { - 'start': {'line': lineno, 'character': offset}, + "source": "mypy", + "range": { + "start": {"line": lineno, "character": offset}, # There may be a better solution, but mypy does not provide end - 'end': {'line': lineno, 'character': offset + 1} + "end": {"line": lineno, "character": offset + 1}, }, - 'message': msg, - 'severity': errno + "message": msg, + "severity": errno, } if document: # although mypy does not provide the end of the affected range, we # can make a good guess by highlighting the word that Mypy flagged - word = document.word_at_position(diag['range']['start']) + word = document.word_at_position(diag["range"]["start"]) if word: - diag['range']['end']['character'] = ( - diag['range']['start']['character'] + len(word)) + diag["range"]["end"]["character"] = diag["range"]["start"][ + "character" + ] + len(word) return diag return None @hookimpl -def pylsp_lint(config: Config, workspace: Workspace, document: Document, - is_saved: bool) -> List[Dict[str, Any]]: +def pylsp_lint( + config: Config, workspace: Workspace, document: Document, is_saved: bool +) -> List[Dict[str, Any]]: """ Lints. @@ -110,27 +114,25 @@ def pylsp_lint(config: Config, workspace: Workspace, document: Document, List of the linting data. """ - settings = config.plugin_settings('mypy-ls') - live_mode = settings.get('live_mode', True) - args = ['--incremental', - '--show-column-numbers', - '--follow-imports', 'silent'] + settings = config.plugin_settings("mypy-ls") + live_mode = settings.get("live_mode", True) + args = ["--incremental", "--show-column-numbers", "--follow-imports", "silent"] global tmpFile if live_mode and not is_saved and tmpFile: tmpFile = open(tmpFile.name, "w") tmpFile.write(document.source) tmpFile.close() - args.extend(['--shadow-file', document.path, tmpFile.name]) + args.extend(["--shadow-file", document.path, tmpFile.name]) elif not is_saved: return [] if mypyConfigFile: - args.append('--config-file') + args.append("--config-file") args.append(mypyConfigFile) args.append(document.path) - if settings.get('strict', False): - args.append('--strict') + if settings.get("strict", False): + args.append("--strict") report, errors, _ = mypy_api.run(args) @@ -187,10 +189,11 @@ def init(workspace: str) -> Dict[str, str]: configuration = eval(file.read()) global mypyConfigFile mypyConfigFile = findConfigFile(workspace, "mypy.ini") - if (("enabled" not in configuration or configuration["enabled"]) - and ("live_mode" not in configuration or configuration["live_mode"])): + if ("enabled" not in configuration or configuration["enabled"]) and ( + "live_mode" not in configuration or configuration["live_mode"] + ): global tmpFile - tmpFile = tempfile.NamedTemporaryFile('w', delete=False) + tmpFile = tempfile.NamedTemporaryFile("w", delete=False) tmpFile.close() return configuration diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f5f57f5 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[tool.black] +line-length = 90 +include = '\.pyi?$' +exclude = ''' +/( + \.git + | \.mypy_cache + | \.tox + | \.venv + | _build + | build + | dist +)/ +''' diff --git a/requirements.txt b/requirements.txt index b0840b2..c0ebcd5 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,4 @@ python-lsp-server mypy +black +pre-commit diff --git a/test/test_plugin.py b/test/test_plugin.py index 44eed41..48650ba 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -12,10 +12,8 @@ TYPE_ERR_MSG = '"Dict[, ]" has no attribute "append"' TEST_LINE = 'test_plugin.py:279:8: error: "Request" has no attribute "id"' -TEST_LINE_WITHOUT_COL = ('test_plugin.py:279: ' - 'error: "Request" has no attribute "id"') -TEST_LINE_WITHOUT_LINE = ('test_plugin.py: ' - 'error: "Request" has no attribute "id"') +TEST_LINE_WITHOUT_COL = "test_plugin.py:279: " 'error: "Request" has no attribute "id"' +TEST_LINE_WITHOUT_LINE = "test_plugin.py: " 'error: "Request" has no attribute "id"' @pytest.fixture @@ -27,7 +25,6 @@ def workspace(tmpdir): class FakeConfig(object): - def __init__(self): self._root_path = "C:" @@ -50,40 +47,40 @@ def test_plugin(workspace): assert len(diags) == 1 diag = diags[0] - assert diag['message'] == TYPE_ERR_MSG - assert diag['range']['start'] == {'line': 0, 'character': 0} - assert diag['range']['end'] == {'line': 0, 'character': 1} + assert diag["message"] == TYPE_ERR_MSG + assert diag["range"]["start"] == {"line": 0, "character": 0} + assert diag["range"]["end"] == {"line": 0, "character": 1} def test_parse_full_line(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE, doc) - assert diag['message'] == '"Request" has no attribute "id"' - assert diag['range']['start'] == {'line': 278, 'character': 7} - assert diag['range']['end'] == {'line': 278, 'character': 8} + assert diag["message"] == '"Request" has no attribute "id"' + assert diag["range"]["start"] == {"line": 278, "character": 7} + assert diag["range"]["end"] == {"line": 278, "character": 8} def test_parse_line_without_col(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_COL, doc) - assert diag['message'] == '"Request" has no attribute "id"' - assert diag['range']['start'] == {'line': 278, 'character': 0} - assert diag['range']['end'] == {'line': 278, 'character': 1} + assert diag["message"] == '"Request" has no attribute "id"' + assert diag["range"]["start"] == {"line": 278, "character": 0} + assert diag["range"]["end"] == {"line": 278, "character": 1} def test_parse_line_without_line(workspace): doc = Document(DOC_URI, workspace) diag = plugin.parse_line(TEST_LINE_WITHOUT_LINE, doc) - assert diag['message'] == '"Request" has no attribute "id"' - assert diag['range']['start'] == {'line': 0, 'character': 0} - assert diag['range']['end'] == {'line': 0, 'character': 6} + assert diag["message"] == '"Request" has no attribute "id"' + assert diag["range"]["start"] == {"line": 0, "character": 0} + assert diag["range"]["end"] == {"line": 0, "character": 6} -@pytest.mark.parametrize('word,bounds', [('', (7, 8)), ('my_var', (7, 13))]) +@pytest.mark.parametrize("word,bounds", [("", (7, 8)), ("my_var", (7, 13))]) def test_parse_line_with_context(monkeypatch, word, bounds, workspace): doc = Document(DOC_URI, workspace) - monkeypatch.setattr(Document, 'word_at_position', lambda *args: word) + monkeypatch.setattr(Document, "word_at_position", lambda *args: word) diag = plugin.parse_line(TEST_LINE, doc) - assert diag['message'] == '"Request" has no attribute "id"' - assert diag['range']['start'] == {'line': 278, 'character': bounds[0]} - assert diag['range']['end'] == {'line': 278, 'character': bounds[1]} + assert diag["message"] == '"Request" has no attribute "id"' + assert diag["range"]["start"] == {"line": 278, "character": bounds[0]} + assert diag["range"]["end"] == {"line": 278, "character": bounds[1]} From 91e6ade94b24959a8c0314e975ca86d373478a23 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Wed, 19 May 2021 21:11:10 +0200 Subject: [PATCH 041/110] assure the upload will work --- .github/workflows/python-package.yml | 21 ++++++++++++++++++++- setup.py | 2 +- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 845981f..fcacce8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -10,7 +10,7 @@ on: branches: [ master ] jobs: - build: + testCode: runs-on: ubuntu-latest strategy: @@ -41,3 +41,22 @@ jobs: - name: Test with pytest run: | pytest + + testUploadability: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and check + run: | + python setup.py sdist bdist_wheel + twine check dist/* diff --git a/setup.py b/setup.py index 187a6a9..5faa7e6 100755 --- a/setup.py +++ b/setup.py @@ -3,4 +3,4 @@ from mypy_ls import _version if __name__ == "__main__": - setup(version=_version.__version__) + setup(version=_version.__version__, long_description_content_type="text/x-rst") From 97b5b6202499a47814fd61796195294b499ea51f Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Wed, 19 May 2021 15:40:48 -0700 Subject: [PATCH 042/110] Add dmypy support via mypy.api.run_dmypy https://github.com/palantir/python-language-server/issues/391 Update plugin flow for non-live-mode dmypy invocation via run_dmypy. Minor fix to update detect `.mypy.ini` as well as `mypy.ini`. --- README.rst | 24 +++++++++++++++--- mypy_ls/plugin.py | 64 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 138c891..42cd529 100644 --- a/README.rst +++ b/README.rst @@ -24,11 +24,18 @@ Install into the same virtualenv as python-lsp-server itself. Configuration ------------- -``live_mode`` (default is True) provides type checking as you type. This writes to a tempfile every time a check is done. +``live_mode`` (default is True) provides type checking as you type. + This writes to a tempfile every time a check is done. Turning off ``live_mode`` means you must save your changes for mypy diagnostics to update correctly. -Turning off ``live_mode`` means you must save your changes for mypy diagnostics to update correctly. +``dmypy`` (default is False) executes via ``dmypy run`` rather than ``mypy``. + This uses the ``dmypy`` daemon and may dramatically improve the responsiveness of the ``pylsp`` server, however this currently does not work in ``live_mode``. Enabling this disables ``live_mode``, even for conflicting configs. -Depending on your editor, the configuration (found in a file called mypy-ls.cfg in your workspace or a parent directory) should be roughly like this: +``strict`` (defualt is False) refers to the ``strict`` option of ``mypy``. + This option often is too strict to be useful. + + + +Depending on your editor, the configuration (found in a file called mypy-ls.cfg in your workspace or a parent directory) should be roughly like this for a standard configuration: :: @@ -38,6 +45,17 @@ Depending on your editor, the configuration (found in a file called mypy-ls.cfg "strict": False } +With ``dmypy`` enabled your config should look like this: + +:: + + { + "enabled": True, + "live_mode": False, + "dmypy": True, + "strict": False + } + Developing ------------- diff --git a/mypy_ls/plugin.py b/mypy_ls/plugin.py index aabdd0c..f3bd2ca 100644 --- a/mypy_ls/plugin.py +++ b/mypy_ls/plugin.py @@ -17,6 +17,7 @@ from pylsp.config.config import Config from typing import Optional, Dict, Any, IO, List import atexit +import collections line_pattern: str = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" @@ -26,6 +27,13 @@ tmpFile: Optional[IO[str]] = None +# In non-live-mode the file contents aren't updated. +# Returning an empty diagnostic clears the diagnostic result, +# so store a cache of last diagnostics for each file a-la the pylint plugin, +# so we can return some potentially-stale diagnostics. +# https://github.com/python-lsp/python-lsp-server/blob/v1.0.1/pylsp/plugins/pylint_lint.py#L55-L62 +last_diagnostics: Dict[str, List] = collections.defaultdict(list) + def parse_line( line: str, document: Optional[Document] = None @@ -115,33 +123,73 @@ def pylsp_lint( """ settings = config.plugin_settings("mypy-ls") + log.info( + "lint settings = %s document.path = %s is_saved = %s", + settings, + document.path, + is_saved, + ) + live_mode = settings.get("live_mode", True) - args = ["--incremental", "--show-column-numbers", "--follow-imports", "silent"] + dmypy = settings.get("dmypy", False) + + if dmypy and live_mode: + # dmypy can only be efficiently run on files that have been saved, see: + # https://github.com/python/mypy/issues/9309 + log.warning("live_mode is not supported with dmypy, disabling") + live_mode = False + + args = ["--show-column-numbers"] global tmpFile if live_mode and not is_saved and tmpFile: + log.info("live_mode tmpFile = %s", live_mode) tmpFile = open(tmpFile.name, "w") tmpFile.write(document.source) tmpFile.close() args.extend(["--shadow-file", document.path, tmpFile.name]) - elif not is_saved: - return [] + elif not is_saved and document.path in last_diagnostics: + # On-launch the document isn't marked as saved, so fall through and run + # the diagnostics anyway even if the file contents may be out of date. + log.info( + "non-live, returning cached diagnostics len(cached) = %s", + last_diagnostics[document.path], + ) + return last_diagnostics[document.path] if mypyConfigFile: args.append("--config-file") args.append(mypyConfigFile) + args.append(document.path) + if settings.get("strict", False): args.append("--strict") - report, errors, _ = mypy_api.run(args) + if not dmypy: + args.extend(["--incremental", "--follow-imports", "silent"]) + + log.info("executing mypy args = %s", args) + report, errors, _ = mypy_api.run(args) + else: + args = ["run", "--"] + args + + log.info("executing dmypy args = %s", args) + report, errors, _ = mypy_api.run_dmypy(args) + + log.debug("report:\n%s", report) + log.debug("errors:\n%s", errors) diagnostics = [] for line in report.splitlines(): + log.debug("parsing: line = %r", line) diag = parse_line(line, document) if diag: diagnostics.append(diag) + log.info("mypy-ls len(diagnostics) = %s", len(diagnostics)) + + last_diagnostics[document.path] = diagnostics return diagnostics @@ -181,20 +229,28 @@ def init(workspace: str) -> Dict[str, str]: """ # On windows the path contains \\ on linux it contains / all the code works with / + log.info("init workspace = %s", workspace) workspace = workspace.replace("\\", "/") + configuration = {} path = findConfigFile(workspace, "mypy-ls.cfg") if path: with open(path) as file: configuration = eval(file.read()) + global mypyConfigFile mypyConfigFile = findConfigFile(workspace, "mypy.ini") + if not mypyConfigFile: + mypyConfigFile = findConfigFile(workspace, ".mypy.ini") + if ("enabled" not in configuration or configuration["enabled"]) and ( "live_mode" not in configuration or configuration["live_mode"] ): global tmpFile tmpFile = tempfile.NamedTemporaryFile("w", delete=False) tmpFile.close() + + log.info("mypyConfigFile = %s configuration = %s", mypyConfigFile, configuration) return configuration From 4d7d342263ce4e99350b87b9792feaba3f27d819 Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Wed, 19 May 2021 15:44:47 -0700 Subject: [PATCH 043/110] Fixup README typos from review --- README.rst | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.rst b/README.rst index 42cd529..fbcfd8c 100644 --- a/README.rst +++ b/README.rst @@ -30,11 +30,9 @@ Configuration ``dmypy`` (default is False) executes via ``dmypy run`` rather than ``mypy``. This uses the ``dmypy`` daemon and may dramatically improve the responsiveness of the ``pylsp`` server, however this currently does not work in ``live_mode``. Enabling this disables ``live_mode``, even for conflicting configs. -``strict`` (defualt is False) refers to the ``strict`` option of ``mypy``. +``strict`` (default is False) refers to the ``strict`` option of ``mypy``. This option often is too strict to be useful. - - Depending on your editor, the configuration (found in a file called mypy-ls.cfg in your workspace or a parent directory) should be roughly like this for a standard configuration: :: From cbc23751c868c3e829ae8c22d15d7c2db24a435a Mon Sep 17 00:00:00 2001 From: Alex Ford Date: Sun, 13 Jun 2021 15:28:16 -0700 Subject: [PATCH 044/110] Check dmypy status and kill hung daemons before run The dmypy daemon process can end up in a hung state for many reasons, and may persist in a hung state across lsp sessions. `dmypy run` will block indefinitely the daemon process is unresponsive, blocking the lsp server and preventing diagnostics. Add `dmypy status` check before `dmypy run` and kill the daemon if status is non-zero before diagnostic request. The subsequent run will then bring up a fresh daemon. --- mypy_ls/plugin.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/mypy_ls/plugin.py b/mypy_ls/plugin.py index f3bd2ca..1e1c0e9 100644 --- a/mypy_ls/plugin.py +++ b/mypy_ls/plugin.py @@ -172,9 +172,21 @@ def pylsp_lint( log.info("executing mypy args = %s", args) report, errors, _ = mypy_api.run(args) else: + # If dmypy daemon is non-responsive calls to run will block. + # Check daemon status, if non-zero daemon is dead or hung. + # If daemon is hung, kill will reset + # If daemon is dead/absent, kill will no-op. + # In either case, reset to fresh state + _, _err, _status = mypy_api.run_dmypy(["status"]) + if _status != 0: + log.info( + "restarting dmypy from status: %s message: %s", _status, _err.strip() + ) + mypy_api.run_dmypy(["kill"]) + + # run to use existing daemon or restart if required args = ["run", "--"] + args - - log.info("executing dmypy args = %s", args) + log.info("dmypy run args = %s", args) report, errors, _ = mypy_api.run_dmypy(args) log.debug("report:\n%s", report) From 1b871389a8f538d56d1b5e34437c9e9d9aed82c5 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Mon, 21 Jun 2021 19:11:16 +0200 Subject: [PATCH 045/110] add more checks --- .github/workflows/python-package.yml | 4 ++++ .pre-commit-config.yaml | 6 +++++- README.rst | 1 + requirements.txt | 1 + 4 files changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fcacce8..add5456 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -56,6 +56,10 @@ jobs: run: | python -m pip install --upgrade pip pip install setuptools wheel twine + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Test with rstcheck + run: | + rstcheck README.rst - name: Build and check run: | python setup.py sdist bdist_wheel diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 5beba6e..e797dce 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,6 +2,10 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black - rev: 21.5b1 + rev: 21.6b0 hooks: - id: black + - repo: https://github.com/Lucas-C/pre-commit-hooks-markup + rev: v1.0.1 + hooks: + - id: rst-linter \ No newline at end of file diff --git a/README.rst b/README.rst index 138c891..0778c53 100644 --- a/README.rst +++ b/README.rst @@ -58,5 +58,6 @@ This project uses `pre-commit`_ to enforce code-quality. After cloning the repos After that pre-commit will run `all defined hooks`_ on every ``git commit`` and keep you from committing if there are any errors. .. _black: https://github.com/psf/black +.. _rst-linter: https://github.com/Lucas-C/pre-commit-hooks-markup .. _pre-commit: https://pre-commit.com/ .. _all defined hooks: .pre-commit-config.yaml diff --git a/requirements.txt b/requirements.txt index c0ebcd5..c9f670f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,4 @@ python-lsp-server mypy black pre-commit +rstcheck From 6c44c3b4c00f50b68076507b7846e9b8476d6676 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Mon, 21 Jun 2021 19:19:05 +0200 Subject: [PATCH 046/110] fix readme --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 0778c53..f96f454 100644 --- a/README.rst +++ b/README.rst @@ -49,6 +49,8 @@ Install development dependencies with (you might want to create a virtualenv fir The project is formatted with `black`_. You can either configure your IDE to automatically format code with it, run it manually (``black .``) or rely on pre-commit (see below) to format files on git commit. +The project uses two rst tests in order to assure uploadability to pipy: `rst-linter`_ as a pre-commit hook and `rstcheck`_ in a GitHub workflow. This does not catch all errors. + This project uses `pre-commit`_ to enforce code-quality. After cloning the repository install the pre-commit hooks with: :: @@ -59,5 +61,6 @@ After that pre-commit will run `all defined hooks`_ on every ``git commit`` and .. _black: https://github.com/psf/black .. _rst-linter: https://github.com/Lucas-C/pre-commit-hooks-markup +.. _rstcheck: https://github.com/myint/rstcheck .. _pre-commit: https://pre-commit.com/ .. _all defined hooks: .pre-commit-config.yaml From ace74939df049ca702f0b22d8b12b4cf45d0afb1 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Mon, 21 Jun 2021 19:31:57 +0200 Subject: [PATCH 047/110] typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index 98f11c3..be88437 100644 --- a/README.rst +++ b/README.rst @@ -65,7 +65,7 @@ Install development dependencies with (you might want to create a virtualenv fir The project is formatted with `black`_. You can either configure your IDE to automatically format code with it, run it manually (``black .``) or rely on pre-commit (see below) to format files on git commit. -The project uses two rst tests in order to assure uploadability to pipy: `rst-linter`_ as a pre-commit hook and `rstcheck`_ in a GitHub workflow. This does not catch all errors. +The project uses two rst tests in order to assure uploadability to pypi: `rst-linter`_ as a pre-commit hook and `rstcheck`_ in a GitHub workflow. This does not catch all errors. This project uses `pre-commit`_ to enforce code-quality. After cloning the repository install the pre-commit hooks with: From 0b8049571ae31a4937c74662422f6f4cc6b7607c Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Mon, 21 Jun 2021 19:36:43 +0200 Subject: [PATCH 048/110] version bump --- mypy_ls/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py index 6a9beea..3d26edf 100644 --- a/mypy_ls/_version.py +++ b/mypy_ls/_version.py @@ -1 +1 @@ -__version__ = "0.4.0" +__version__ = "0.4.1" From d765a5f6ce09f43f9980856f312321e53eb530fe Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Mon, 21 Jun 2021 20:01:15 +0200 Subject: [PATCH 049/110] rename --- MANIFEST.in | 1 - README.rst | 12 ++++++------ mypy_ls/_version.py | 1 - {mypy_ls => pylsp_mypy}/__init__.py | 0 pylsp_mypy/_version.py | 1 + {mypy_ls => pylsp_mypy}/plugin.py | 10 +++++----- setup.cfg | 4 ++-- setup.py | 2 +- test/test_plugin.py | 2 +- 9 files changed, 16 insertions(+), 17 deletions(-) delete mode 100644 mypy_ls/_version.py rename {mypy_ls => pylsp_mypy}/__init__.py (100%) create mode 100644 pylsp_mypy/_version.py rename {mypy_ls => pylsp_mypy}/plugin.py (96%) diff --git a/MANIFEST.in b/MANIFEST.in index 6f22dca..9561fb1 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,2 +1 @@ include README.rst -include mypy_ls/_version.py diff --git a/README.rst b/README.rst index be88437..b8cc91d 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +1,11 @@ Mypy plugin for PYLS ====================== -.. image:: https://badge.fury.io/py/mypy-ls.svg - :target: https://badge.fury.io/py/mypy-ls +.. image:: https://badge.fury.io/py/pylsp-mypy.svg + :target: https://badge.fury.io/py/pylsp-mypy -.. image:: https://github.com/Richardk2n/pyls-mypy/workflows/Python%20package/badge.svg?branch=master - :target: https://github.com/Richardk2n/pyls-mypy/ +.. image:: https://github.com/Richardk2n/pylsp-mypy/workflows/Python%20package/badge.svg?branch=master + :target: https://github.com/Richardk2n/pylsp-mypy/ This is a plugin for the `Python LSP Server`_. @@ -19,7 +19,7 @@ Installation Install into the same virtualenv as python-lsp-server itself. -``pip install mypy-ls`` +``pip install pylsp-mypy`` Configuration ------------- @@ -33,7 +33,7 @@ Configuration ``strict`` (default is False) refers to the ``strict`` option of ``mypy``. This option often is too strict to be useful. -Depending on your editor, the configuration (found in a file called mypy-ls.cfg in your workspace or a parent directory) should be roughly like this for a standard configuration: +Depending on your editor, the configuration (found in a file called pylsp-mypy.cfg or in your workspace or a parent directory) should be roughly like this for a standard configuration: :: diff --git a/mypy_ls/_version.py b/mypy_ls/_version.py deleted file mode 100644 index 3d26edf..0000000 --- a/mypy_ls/_version.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = "0.4.1" diff --git a/mypy_ls/__init__.py b/pylsp_mypy/__init__.py similarity index 100% rename from mypy_ls/__init__.py rename to pylsp_mypy/__init__.py diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py new file mode 100644 index 0000000..3d18726 --- /dev/null +++ b/pylsp_mypy/_version.py @@ -0,0 +1 @@ +__version__ = "0.5.0" diff --git a/mypy_ls/plugin.py b/pylsp_mypy/plugin.py similarity index 96% rename from mypy_ls/plugin.py rename to pylsp_mypy/plugin.py index 1e1c0e9..4aad182 100644 --- a/mypy_ls/plugin.py +++ b/pylsp_mypy/plugin.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -File that contains the python-lsp-server plugin mypy-ls. +File that contains the python-lsp-server plugin pylsp-mypy. Created on Fri Jul 10 09:53:57 2020 @@ -122,7 +122,7 @@ def pylsp_lint( List of the linting data. """ - settings = config.plugin_settings("mypy-ls") + settings = config.plugin_settings("pylsp_mypy") log.info( "lint settings = %s document.path = %s is_saved = %s", settings, @@ -199,7 +199,7 @@ def pylsp_lint( if diag: diagnostics.append(diag) - log.info("mypy-ls len(diagnostics) = %s", len(diagnostics)) + log.info("pylsp-mypy len(diagnostics) = %s", len(diagnostics)) last_diagnostics[document.path] = diagnostics return diagnostics @@ -222,7 +222,7 @@ def pylsp_settings(config: Config) -> Dict[str, Dict[str, Dict[str, str]]]: """ configuration = init(config._root_path) - return {"plugins": {"mypy-ls": configuration}} + return {"plugins": {"pylsp_mypy": configuration}} def init(workspace: str) -> Dict[str, str]: @@ -245,7 +245,7 @@ def init(workspace: str) -> Dict[str, str]: workspace = workspace.replace("\\", "/") configuration = {} - path = findConfigFile(workspace, "mypy-ls.cfg") + path = findConfigFile(workspace, "pylsp-mypy.cfg") if path: with open(path) as file: configuration = eval(file.read()) diff --git a/setup.cfg b/setup.cfg index 3bb8042..5058ba3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,5 @@ [metadata] -name = mypy-ls +name = pylsp-mypy author = Tom van Ommeren, Richard Kellnberger description = Mypy linter for the Python LSP Server url = https://github.com/Richardk2n/pyls-mypy @@ -24,7 +24,7 @@ install_requires = [options.entry_points] -pylsp = mypy_ls = mypy_ls.plugin +pylsp = pylsp_mypy = pylsp_mypy.plugin [options.extras_require] test = diff --git a/setup.py b/setup.py index 5faa7e6..a7df058 100755 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ #!/usr/bin/env python from setuptools import setup -from mypy_ls import _version +from pylsp_mypy import _version if __name__ == "__main__": setup(version=_version.__version__, long_description_content_type="text/x-rst") diff --git a/test/test_plugin.py b/test/test_plugin.py index 48650ba..460d843 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -35,7 +35,7 @@ def plugin_settings(self, plugin, document_path=None): def test_settings(): config = FakeConfig() settings = plugin.pylsp_settings(config) - assert settings == {"plugins": {"mypy-ls": {}}} + assert settings == {"plugins": {"pylsp_mypy": {}}} def test_plugin(workspace): From 675afa7d822f320794b77c202dcb875f3751ef64 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Mon, 21 Jun 2021 20:17:20 +0200 Subject: [PATCH 050/110] typo --- README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.rst b/README.rst index b8cc91d..13ddae1 100644 --- a/README.rst +++ b/README.rst @@ -33,7 +33,7 @@ Configuration ``strict`` (default is False) refers to the ``strict`` option of ``mypy``. This option often is too strict to be useful. -Depending on your editor, the configuration (found in a file called pylsp-mypy.cfg or in your workspace or a parent directory) should be roughly like this for a standard configuration: +Depending on your editor, the configuration (found in a file called pylsp-mypy.cfg in your workspace or a parent directory) should be roughly like this for a standard configuration: :: From 0456ad3161d1bb1ef72f137ed054d1c9c840b6a8 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Mon, 21 Jun 2021 20:21:16 +0200 Subject: [PATCH 051/110] Update test_plugin.py --- test/test_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index 460d843..c933eff 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -4,7 +4,7 @@ from pylsp.config.config import Config from pylsp import uris from mock import Mock -from mypy_ls import plugin +from pylsp_mypy import plugin DOC_URI = __file__ DOC_TYPE_ERR = """{}.append(3) From 66d9b90c6873e85b80cec1733182db9c6c48a9b6 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 1 Jul 2021 22:55:45 +0200 Subject: [PATCH 052/110] Fix wrong configuration being used with multiple workspace folders When a workspace has multiple folders then the approach of using one global variable to store the config breaks down as each workspace folders can have separate configuration and the last one that gets initialized overwrites the previous one. Store configuration in a map looked up by workspace folder path. --- mypy_ls/plugin.py | 6 ++++-- test/test_plugin.py | 38 +++++++++++++++++++++++++++++++++++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/mypy_ls/plugin.py b/mypy_ls/plugin.py index 1e1c0e9..30f0b30 100644 --- a/mypy_ls/plugin.py +++ b/mypy_ls/plugin.py @@ -23,7 +23,8 @@ log = logging.getLogger(__name__) -mypyConfigFile: Optional[str] = None +# A mapping from workspace path to config file path +mypyConfigFileMap: Dict[str, Optional[str]] = dict() tmpFile: Optional[IO[str]] = None @@ -157,6 +158,7 @@ def pylsp_lint( ) return last_diagnostics[document.path] + mypyConfigFile = mypyConfigFileMap.get(workspace.root_path) if mypyConfigFile: args.append("--config-file") args.append(mypyConfigFile) @@ -250,10 +252,10 @@ def init(workspace: str) -> Dict[str, str]: with open(path) as file: configuration = eval(file.read()) - global mypyConfigFile mypyConfigFile = findConfigFile(workspace, "mypy.ini") if not mypyConfigFile: mypyConfigFile = findConfigFile(workspace, ".mypy.ini") + mypyConfigFileMap[workspace] = mypyConfigFile if ("enabled" not in configuration or configuration["enabled"]) and ( "live_mode" not in configuration or configuration["live_mode"] diff --git a/test/test_plugin.py b/test/test_plugin.py index 48650ba..ffa23e7 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -41,7 +41,6 @@ def test_settings(): def test_plugin(workspace): config = FakeConfig() doc = Document(DOC_URI, workspace, DOC_TYPE_ERR) - workspace = None plugin.pylsp_settings(config) diags = plugin.pylsp_lint(config, workspace, doc, is_saved=False) @@ -84,3 +83,40 @@ def test_parse_line_with_context(monkeypatch, word, bounds, workspace): assert diag["message"] == '"Request" has no attribute "id"' assert diag["range"]["start"] == {"line": 278, "character": bounds[0]} assert diag["range"]["end"] == {"line": 278, "character": bounds[1]} + + +def test_multiple_workspaces(tmpdir): + DOC_SOURCE = """ +def foo(): + return + unreachable = 1 +""" + DOC_ERR_MSG = 'Statement is unreachable' + + # Initialize two workspace folders. + folder1 = tmpdir.mkdir('folder1') + ws1 = Workspace(uris.from_fs_path(str(folder1)), Mock()) + ws1._config = Config(ws1.root_uri, {}, 0, {}) + folder2 = tmpdir.mkdir('folder2') + ws2 = Workspace(uris.from_fs_path(str(folder2)), Mock()) + ws2._config = Config(ws2.root_uri, {}, 0, {}) + + # Create configuration file for workspace folder 1. + mypy_config = folder1.join('mypy.ini') + mypy_config.write('[mypy]\nwarn_unreachable = True') + + # Initialize settings for both folders. + plugin.pylsp_settings(ws1._config) + plugin.pylsp_settings(ws2._config) + + # Test document in workspace 1 (uses mypy.ini configuration). + doc1 = Document(DOC_URI, ws1, DOC_SOURCE) + diags = plugin.pylsp_lint(ws1._config, ws1, doc1, is_saved=False) + assert len(diags) == 1 + diag = diags[0] + assert diag["message"] == DOC_ERR_MSG + + # Test document in workspace 2 (without mypy.ini configuration) + doc2 = Document(DOC_URI, ws2, DOC_SOURCE) + diags = plugin.pylsp_lint(ws2._config, ws2, doc2, is_saved=False) + assert len(diags) == 0 From 0f6c6d7e34cdfa255052e673bc822f3a6bee7688 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Sat, 3 Jul 2021 12:12:09 +0200 Subject: [PATCH 053/110] run black --- test/test_plugin.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index ffa23e7..a235310 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -91,19 +91,19 @@ def foo(): return unreachable = 1 """ - DOC_ERR_MSG = 'Statement is unreachable' + DOC_ERR_MSG = "Statement is unreachable" # Initialize two workspace folders. - folder1 = tmpdir.mkdir('folder1') + folder1 = tmpdir.mkdir("folder1") ws1 = Workspace(uris.from_fs_path(str(folder1)), Mock()) ws1._config = Config(ws1.root_uri, {}, 0, {}) - folder2 = tmpdir.mkdir('folder2') + folder2 = tmpdir.mkdir("folder2") ws2 = Workspace(uris.from_fs_path(str(folder2)), Mock()) ws2._config = Config(ws2.root_uri, {}, 0, {}) # Create configuration file for workspace folder 1. - mypy_config = folder1.join('mypy.ini') - mypy_config.write('[mypy]\nwarn_unreachable = True') + mypy_config = folder1.join("mypy.ini") + mypy_config.write("[mypy]\nwarn_unreachable = True") # Initialize settings for both folders. plugin.pylsp_settings(ws1._config) From 2b46fad02a63237010c4c018fa1c10f06795c842 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 3 Jul 2021 15:38:36 +0200 Subject: [PATCH 054/110] closes #6 closes #7 Also changed the line length and made the config discovery less dependet on string manipulation. Added deprecation warnings for old namespaces --- pylsp_mypy/plugin.py | 72 ++++++++++++++++++++++++-------------------- pyproject.toml | 2 +- 2 files changed, 41 insertions(+), 33 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index bdea34b..26d14ad 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -10,6 +10,7 @@ import tempfile import os import os.path +from pathlib import Path import logging from mypy import api as mypy_api from pylsp import hookimpl @@ -24,7 +25,7 @@ log = logging.getLogger(__name__) # A mapping from workspace path to config file path -mypyConfigFileMap: Dict[str, Optional[str]] = dict() +mypyConfigFileMap: Dict[str, Optional[str]] = {} tmpFile: Optional[IO[str]] = None @@ -33,12 +34,10 @@ # so store a cache of last diagnostics for each file a-la the pylint plugin, # so we can return some potentially-stale diagnostics. # https://github.com/python-lsp/python-lsp-server/blob/v1.0.1/pylsp/plugins/pylint_lint.py#L55-L62 -last_diagnostics: Dict[str, List] = collections.defaultdict(list) +last_diagnostics: Dict[str, List[Dict[str, Any]]] = collections.defaultdict(list) -def parse_line( - line: str, document: Optional[Document] = None -) -> Optional[Dict[str, Any]]: +def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[str, Any]]: """ Return a language-server diagnostic from a line of the Mypy error report. @@ -66,9 +65,7 @@ def parse_line( # results from other files can be included, but we cannot return # them. if document and document.path and not document.path.endswith(file_path): - log.warning( - "discarding result for %s against %s", file_path, document.path - ) + log.warning("discarding result for %s against %s", file_path, document.path) return None lineno = int(linenoStr or 1) - 1 # 0-based line number @@ -91,9 +88,7 @@ def parse_line( # can make a good guess by highlighting the word that Mypy flagged word = document.word_at_position(diag["range"]["start"]) if word: - diag["range"]["end"]["character"] = diag["range"]["start"][ - "character" - ] + len(word) + diag["range"]["end"]["character"] = diag["range"]["start"]["character"] + len(word) return diag return None @@ -124,6 +119,21 @@ def pylsp_lint( """ settings = config.plugin_settings("pylsp_mypy") + oldSettings1 = config.plugin_settings("mypy-ls") + if oldSettings1 != {}: + raise DeprecationWarning( + "Your configuration uses the namespace mypy-ls, this should be changed to pylsp_mypy" + ) + oldSettings2 = config.plugin_settings("mypy_ls") + if oldSettings2 != {}: + raise DeprecationWarning( + "Your configuration uses the namespace mypy_ls, this should be changed to pylsp_mypy" + ) + if settings == {}: + settings = oldSettings1 + if settings == {}: + settings = oldSettings2 + log.info( "lint settings = %s document.path = %s is_saved = %s", settings, @@ -181,9 +191,7 @@ def pylsp_lint( # In either case, reset to fresh state _, _err, _status = mypy_api.run_dmypy(["status"]) if _status != 0: - log.info( - "restarting dmypy from status: %s message: %s", _status, _err.strip() - ) + log.info("restarting dmypy from status: %s message: %s", _status, _err.strip()) mypy_api.run_dmypy(["kill"]) # run to use existing daemon or restart if required @@ -242,19 +250,15 @@ def init(workspace: str) -> Dict[str, str]: The plugin config dict. """ - # On windows the path contains \\ on linux it contains / all the code works with / log.info("init workspace = %s", workspace) - workspace = workspace.replace("\\", "/") configuration = {} - path = findConfigFile(workspace, "pylsp-mypy.cfg") + path = findConfigFile(workspace, ["pylsp-mypy.cfg", "mypy-ls.cfg", "mypy_ls.cfg"]) if path: with open(path) as file: configuration = eval(file.read()) - mypyConfigFile = findConfigFile(workspace, "mypy.ini") - if not mypyConfigFile: - mypyConfigFile = findConfigFile(workspace, ".mypy.ini") + mypyConfigFile = findConfigFile(workspace, ["mypy.ini", ".mypy.ini"]) mypyConfigFileMap[workspace] = mypyConfigFile if ("enabled" not in configuration or configuration["enabled"]) and ( @@ -268,7 +272,7 @@ def init(workspace: str) -> Dict[str, str]: return configuration -def findConfigFile(path: str, name: str) -> Optional[str]: +def findConfigFile(path: str, names: List[str]) -> Optional[str]: """ Search for a config file. @@ -279,8 +283,8 @@ def findConfigFile(path: str, name: str) -> Optional[str]: ---------- path : str The path where the search starts. - name : str - The file to be found. + names : List[str] + The file to be found (or alternative names). Returns ------- @@ -288,15 +292,19 @@ def findConfigFile(path: str, name: str) -> Optional[str]: The path where the file has been found or None if no matching file has been found. """ - while True: - p = f"{path}/{name}" - if os.path.isfile(p): - return p - else: - loc = path.rfind("/") - if loc == -1: - return None - path = path[:loc] + start = Path(path).joinpath(names[0]) # the join causes the parents to include path + for parent in start.parents: + for name in names: + file = parent.joinpath(name) + if file.is_file(): + if file.name in ["mypy-ls.cfg", "mypy_ls.cfg"]: + raise DeprecationWarning( + f"{str(file)}: {file.name} is no longer supported, you should rename your " + "config file to pylsp-mypy.cfg" + ) + return str(file) + + return None @atexit.register diff --git a/pyproject.toml b/pyproject.toml index f5f57f5..6de812e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -line-length = 90 +line-length = 100 include = '\.pyi?$' exclude = ''' /( From bb875df1da5d280f5502bbba2a32a4c5ca4fbde9 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 3 Jul 2021 19:21:33 +0200 Subject: [PATCH 055/110] update link --- README.rst | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 13ddae1..ac9c0a8 100644 --- a/README.rst +++ b/README.rst @@ -1,4 +1,4 @@ -Mypy plugin for PYLS +Mypy plugin for PYLSP ====================== .. image:: https://badge.fury.io/py/pylsp-mypy.svg diff --git a/setup.cfg b/setup.cfg index 5058ba3..113fca1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ name = pylsp-mypy author = Tom van Ommeren, Richard Kellnberger description = Mypy linter for the Python LSP Server -url = https://github.com/Richardk2n/pyls-mypy +url = https://github.com/Richardk2n/pylsp-mypy long_description = file: README.rst license='MIT' classifiers = From 5bd1a3b1d10e5ae97461ce361b6904de07c54b72 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 3 Jul 2021 19:51:36 +0200 Subject: [PATCH 056/110] closes #8 --- pylsp_mypy/plugin.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 26d14ad..228ea1e 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -153,9 +153,12 @@ def pylsp_lint( args = ["--show-column-numbers"] global tmpFile - if live_mode and not is_saved and tmpFile: - log.info("live_mode tmpFile = %s", live_mode) - tmpFile = open(tmpFile.name, "w") + if live_mode and not is_saved: + if tmpFile: + tmpFile = open(tmpFile.name, "w") + else: + tmpFile = tempfile.NamedTemporaryFile("w", delete=False) + log.info("live_mode tmpFile = %s", tmpFile.name) tmpFile.write(document.source) tmpFile.close() args.extend(["--shadow-file", document.path, tmpFile.name]) @@ -261,13 +264,6 @@ def init(workspace: str) -> Dict[str, str]: mypyConfigFile = findConfigFile(workspace, ["mypy.ini", ".mypy.ini"]) mypyConfigFileMap[workspace] = mypyConfigFile - if ("enabled" not in configuration or configuration["enabled"]) and ( - "live_mode" not in configuration or configuration["live_mode"] - ): - global tmpFile - tmpFile = tempfile.NamedTemporaryFile("w", delete=False) - tmpFile.close() - log.info("mypyConfigFile = %s configuration = %s", mypyConfigFile, configuration) return configuration From 7d18bbf562d4efb4a13309eeb88c7258910c3570 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 3 Jul 2021 19:55:38 +0200 Subject: [PATCH 057/110] Create mypy.ini --- mypy.ini | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini new file mode 100644 index 0000000..6ef17d8 --- /dev/null +++ b/mypy.ini @@ -0,0 +1,8 @@ +[mypy] +python_version = 3.6 + +[mypy-pylsp.*] +ignore_missing_imports = True + +[mypy-pylsp_mypy.plugin] + disallow_untyped_decorators = False \ No newline at end of file From a780119e3d693995adc45b92cbf7640bdb50ea7b Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 3 Jul 2021 20:09:09 +0200 Subject: [PATCH 058/110] version bump --- pylsp_mypy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index 3d18726..dd9b22c 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.5.0" +__version__ = "0.5.1" From 12cdaa1ebcbef7f69b3d727ea5a6feef7750a1cd Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 15 Aug 2021 17:51:55 +0200 Subject: [PATCH 059/110] closes #15 --- pylsp_mypy/_version.py | 2 +- pylsp_mypy/plugin.py | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index dd9b22c..7225152 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.5.1" +__version__ = "0.5.2" diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 228ea1e..0d8f69e 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -19,6 +19,7 @@ from typing import Optional, Dict, Any, IO, List import atexit import collections +import warnings line_pattern: str = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" @@ -121,13 +122,17 @@ def pylsp_lint( settings = config.plugin_settings("pylsp_mypy") oldSettings1 = config.plugin_settings("mypy-ls") if oldSettings1 != {}: - raise DeprecationWarning( - "Your configuration uses the namespace mypy-ls, this should be changed to pylsp_mypy" + warnings.warn( + DeprecationWarning( + "Your configuration uses the namespace mypy-ls, this should be changed to pylsp_mypy" + ) ) oldSettings2 = config.plugin_settings("mypy_ls") if oldSettings2 != {}: - raise DeprecationWarning( - "Your configuration uses the namespace mypy_ls, this should be changed to pylsp_mypy" + warnings.warn( + DeprecationWarning( + "Your configuration uses the namespace mypy_ls, this should be changed to pylsp_mypy" + ) ) if settings == {}: settings = oldSettings1 @@ -294,9 +299,11 @@ def findConfigFile(path: str, names: List[str]) -> Optional[str]: file = parent.joinpath(name) if file.is_file(): if file.name in ["mypy-ls.cfg", "mypy_ls.cfg"]: - raise DeprecationWarning( - f"{str(file)}: {file.name} is no longer supported, you should rename your " - "config file to pylsp-mypy.cfg" + warnings.warn( + DeprecationWarning( + f"{str(file)}: {file.name} is no longer supported, you should rename your " + "config file to pylsp-mypy.cfg" + ) ) return str(file) From c7a4d546bc85567d77f77bf5cf726502c4e1d5a8 Mon Sep 17 00:00:00 2001 From: Jochen Sprickerhof Date: Sat, 18 Sep 2021 14:23:29 +0200 Subject: [PATCH 060/110] Switch to unittest mock --- .github/workflows/python-package.yml | 2 +- setup.cfg | 1 - test/test_plugin.py | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index add5456..93bf804 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -26,7 +26,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install flake8 pytest mock + pip install flake8 pytest if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | diff --git a/setup.cfg b/setup.cfg index 113fca1..2c74ac5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -32,7 +32,6 @@ test = pytest pytest-cov coverage - mock [options.packages.find] exclude = diff --git a/test/test_plugin.py b/test/test_plugin.py index 3b07de8..e1c6bfd 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -3,7 +3,7 @@ from pylsp.workspace import Workspace, Document from pylsp.config.config import Config from pylsp import uris -from mock import Mock +from unittest.mock import Mock from pylsp_mypy import plugin DOC_URI = __file__ From 42491d8c400870b32225b7b8d7ac75895e3b4323 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Thu, 2 Dec 2021 14:15:27 +0000 Subject: [PATCH 061/110] Use subprocess to execute mypy / dmypy Fixes #17 --- pylsp_mypy/plugin.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 0d8f69e..fe143d6 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -10,9 +10,9 @@ import tempfile import os import os.path +import subprocess from pathlib import Path import logging -from mypy import api as mypy_api from pylsp import hookimpl from pylsp.workspace import Document, Workspace from pylsp.config.config import Config @@ -190,22 +190,28 @@ def pylsp_lint( args.extend(["--incremental", "--follow-imports", "silent"]) log.info("executing mypy args = %s", args) - report, errors, _ = mypy_api.run(args) + completed_process = subprocess.run(["mypy", *args], capture_output=True) + report = completed_process.stdout.decode() + errors = completed_process.stderr.decode() else: # If dmypy daemon is non-responsive calls to run will block. # Check daemon status, if non-zero daemon is dead or hung. # If daemon is hung, kill will reset # If daemon is dead/absent, kill will no-op. # In either case, reset to fresh state - _, _err, _status = mypy_api.run_dmypy(["status"]) + completed_process = subprocess.run(["dmypy", *args], capture_output=True) + _err = completed_process.stderr.decode() + _status = completed_process.returncode if _status != 0: log.info("restarting dmypy from status: %s message: %s", _status, _err.strip()) - mypy_api.run_dmypy(["kill"]) + subprocess.run(["dmypy", "kill"]) # run to use existing daemon or restart if required args = ["run", "--"] + args log.info("dmypy run args = %s", args) - report, errors, _ = mypy_api.run_dmypy(args) + completed_process = subprocess.run(["dmypy", *args], capture_output=True) + report = completed_process.stdout.decode() + errors = completed_process.stderr.decode() log.debug("report:\n%s", report) log.debug("errors:\n%s", errors) From d0e35fe6bd2fa50c8298cb7e94a68de7f3453b01 Mon Sep 17 00:00:00 2001 From: Charlie Denton Date: Sat, 4 Dec 2021 12:46:28 +0000 Subject: [PATCH 062/110] Replace capture_output shortcut with explicit args The `capture_output` parameter is a shortcut for setting `stderr` and `stdout` to `PIPE`, but is not available on Python 3.6. To ensure compatibility, this uses the longer form. This approach also has the advantage of not capturing `stdout` when it's not required on the first call to `dmypy`. --- pylsp_mypy/plugin.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index fe143d6..4568799 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -190,7 +190,9 @@ def pylsp_lint( args.extend(["--incremental", "--follow-imports", "silent"]) log.info("executing mypy args = %s", args) - completed_process = subprocess.run(["mypy", *args], capture_output=True) + completed_process = subprocess.run( + ["mypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) report = completed_process.stdout.decode() errors = completed_process.stderr.decode() else: @@ -199,7 +201,7 @@ def pylsp_lint( # If daemon is hung, kill will reset # If daemon is dead/absent, kill will no-op. # In either case, reset to fresh state - completed_process = subprocess.run(["dmypy", *args], capture_output=True) + completed_process = subprocess.run(["dmypy", *args], stderr=subprocess.PIPE) _err = completed_process.stderr.decode() _status = completed_process.returncode if _status != 0: @@ -209,7 +211,9 @@ def pylsp_lint( # run to use existing daemon or restart if required args = ["run", "--"] + args log.info("dmypy run args = %s", args) - completed_process = subprocess.run(["dmypy", *args], capture_output=True) + completed_process = subprocess.run( + ["dmypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) report = completed_process.stdout.decode() errors = completed_process.stderr.decode() From b5c786ea4fe2da6ad7b517f3b9236f93f854e55e Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 4 Dec 2021 14:04:28 +0100 Subject: [PATCH 063/110] version bump --- pylsp_mypy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index 7225152..43a1e95 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.5.2" +__version__ = "0.5.3" From a458c4b37ad19a502183ea2700bcb2b914f1d918 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 4 Dec 2021 14:17:03 +0100 Subject: [PATCH 064/110] Version bump again Something in the CI messed up, no realease was generated -> try again --- pylsp_mypy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index 43a1e95..6b27eee 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.5.3" +__version__ = "0.5.4" From 2d7d0e8aa1a824463cabfdaea2eda08dff6ed526 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 4 Dec 2021 14:42:26 +0100 Subject: [PATCH 065/110] test 3.10 as well --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 93bf804..d0343d8 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8, 3.9, 3.10] steps: - uses: actions/checkout@v2 From 607190a8c64aaf53878e4ca5ef91ad17826c4746 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 4 Dec 2021 14:45:02 +0100 Subject: [PATCH 066/110] avoid 3.10 beeing read as 3.1 --- .github/workflows/python-package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index d0343d8..e9f8c1b 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9, 3.10] + python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 From 79057cc4b0f566a9665f3ffc2cb9e862992cedc2 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 5 Dec 2021 18:42:29 +0100 Subject: [PATCH 067/110] Update setup.cfg --- setup.cfg | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.cfg b/setup.cfg index 2c74ac5..e6815aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -14,6 +14,7 @@ classifiers = Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 [options] python_requires = >= 3.6 From 9cc759870a033e3737d3092c5d97f813a3595bad Mon Sep 17 00:00:00 2001 From: "J.P. Neverwas" Date: Fri, 5 Nov 2021 01:40:24 -0700 Subject: [PATCH 068/110] Add test fixture to mock out diagnostics cache --- test/test_plugin.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index e1c6bfd..cb7ba14 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -16,6 +16,12 @@ TEST_LINE_WITHOUT_LINE = "test_plugin.py: " 'error: "Request" has no attribute "id"' +@pytest.fixture +def diag_mp(monkeypatch): + monkeypatch.setattr(plugin, "last_diagnostics", plugin.collections.defaultdict(list)) + return monkeypatch + + @pytest.fixture def workspace(tmpdir): """Return a workspace.""" @@ -38,7 +44,7 @@ def test_settings(): assert settings == {"plugins": {"pylsp_mypy": {}}} -def test_plugin(workspace): +def test_plugin(workspace, diag_mp): config = FakeConfig() doc = Document(DOC_URI, workspace, DOC_TYPE_ERR) plugin.pylsp_settings(config) @@ -85,7 +91,7 @@ def test_parse_line_with_context(monkeypatch, word, bounds, workspace): assert diag["range"]["end"] == {"line": 278, "character": bounds[1]} -def test_multiple_workspaces(tmpdir): +def test_multiple_workspaces(tmpdir, diag_mp): DOC_SOURCE = """ def foo(): return From 8e3a59174cab712034a635ddeca830d5e90bc331 Mon Sep 17 00:00:00 2001 From: "J.P. Neverwas" Date: Wed, 3 Nov 2021 04:44:52 -0700 Subject: [PATCH 069/110] Add option to override command line Closes #17 and maybe #16. --- README.rst | 13 +++++++ pylsp_mypy/plugin.py | 17 ++++++++-- test/test_plugin.py | 81 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index ac9c0a8..4681418 100644 --- a/README.rst +++ b/README.rst @@ -33,6 +33,9 @@ Configuration ``strict`` (default is False) refers to the ``strict`` option of ``mypy``. This option often is too strict to be useful. +``overrides`` (default is ``[]``) specifies a list of alternate or supplemental command-line options. + This modifies the options passed to ``mypy`` or the mypy-specific ones passed to ``dmypy run``. When present, the special boolean member ``True`` is replaced with the command-line options that would've been passed had ``overrides`` not been specified. + Depending on your editor, the configuration (found in a file called pylsp-mypy.cfg in your workspace or a parent directory) should be roughly like this for a standard configuration: :: @@ -54,6 +57,16 @@ With ``dmypy`` enabled your config should look like this: "strict": False } +With ``overrides`` specified, your config should resemble this: + +:: + + { + ..., + "overrides": ["--python-executable", "/home/me/bin/python", True] + } + + Developing ------------- diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 4568799..66d2daf 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -16,7 +16,7 @@ from pylsp import hookimpl from pylsp.workspace import Document, Workspace from pylsp.config.config import Config -from typing import Optional, Dict, Any, IO, List +from typing import Optional, Dict, Any, IO, List, Generator import atexit import collections import warnings @@ -95,6 +95,15 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[ return None +def apply_overrides(args: List[str], overrides: List[Any]) -> Generator[str, None, None]: + """Replace or combine overrides with command-line args.""" + for v in overrides: + if v is True: + yield from iter(args) + continue + yield v + + @hookimpl def pylsp_lint( config: Config, workspace: Workspace, document: Document, is_saved: bool @@ -186,8 +195,12 @@ def pylsp_lint( if settings.get("strict", False): args.append("--strict") + overrides = settings.get("overrides") + if not dmypy: args.extend(["--incremental", "--follow-imports", "silent"]) + if overrides: + args = list(apply_overrides(args, overrides)) log.info("executing mypy args = %s", args) completed_process = subprocess.run( @@ -209,7 +222,7 @@ def pylsp_lint( subprocess.run(["dmypy", "kill"]) # run to use existing daemon or restart if required - args = ["run", "--"] + args + args = ["run", "--"] + (list(apply_overrides(args, overrides)) if overrides else args) log.info("dmypy run args = %s", args) completed_process = subprocess.run( ["dmypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE diff --git a/test/test_plugin.py b/test/test_plugin.py index cb7ba14..7bdb29c 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -126,3 +126,84 @@ def foo(): doc2 = Document(DOC_URI, ws2, DOC_SOURCE) diags = plugin.pylsp_lint(ws2._config, ws2, doc2, is_saved=False) assert len(diags) == 0 + + +def test_apply_overrides(): + assert list(plugin.apply_overrides(["1", "2"], [])) == [] + assert list(plugin.apply_overrides(["1", "2"], ["a"])) == ["a"] + assert list(plugin.apply_overrides(["1", "2"], ["a", True])) == ["a", "1", "2"] + assert list(plugin.apply_overrides(["1", "2"], [True, "a"])) == ["1", "2", "a"] + assert list(plugin.apply_overrides(["1"], ["a", True, "b"])) == ["a", "1", "b"] + + +def test_option_overrides(tmpdir, diag_mp, workspace): + import sys + from textwrap import dedent + + sentinel = tmpdir / "ran" + + source = dedent( + """\ + #!{} + import os, sys, pathlib + pathlib.Path({!r}).touch() + os.execv({!r}, sys.argv) + """ + ).format(sys.executable, str(sentinel), sys.executable) + + wrapper = tmpdir / "bin/wrapper" + wrapper.write(source, ensure=True) + wrapper.chmod(0o700) + + overrides = ["--python-executable", wrapper.strpath, True] + diag_mp.setattr( + FakeConfig, + "plugin_settings", + lambda _, p: {"overrides": overrides} if p == "pylsp_mypy" else {}, + ) + + assert not sentinel.exists() + + diags = plugin.pylsp_lint( + config=FakeConfig(), + workspace=workspace, + document=Document(DOC_URI, workspace, DOC_TYPE_ERR), + is_saved=False, + ) + assert len(diags) == 1 + assert sentinel.exists() + + +def test_option_overrides_dmypy(diag_mp, workspace): + overrides = ["--python-executable", "/tmp/fake", True] + diag_mp.setattr( + FakeConfig, + "plugin_settings", + lambda _, p: { + "overrides": overrides, + "dmypy": True, + "live_mode": False, + } + if p == "pylsp_mypy" + else {}, + ) + + m = Mock(wraps=lambda a, **_: Mock(returncode=0, **{"stdout.decode": lambda: ""})) + diag_mp.setattr(plugin.subprocess, "run", m) + + plugin.pylsp_lint( + config=FakeConfig(), + workspace=workspace, + document=Document(DOC_URI, workspace, DOC_TYPE_ERR), + is_saved=False, + ) + expected = [ + "dmypy", + "run", + "--", + "--python-executable", + "/tmp/fake", + "--show-column-numbers", + __file__, + ] + m.assert_called_with(expected, stderr=-1, stdout=-1) From 86ca6ded0ccca9cea415a7e6c86b3f7348e3ba61 Mon Sep 17 00:00:00 2001 From: "J.P. Neverwas" Date: Sun, 12 Dec 2021 16:28:21 -0800 Subject: [PATCH 070/110] fixup! Add option to override command line --- README.rst | 4 ++-- pylsp_mypy/plugin.py | 20 ++++++++++---------- test/test_plugin.py | 26 ++++++++++++-------------- 3 files changed, 24 insertions(+), 26 deletions(-) diff --git a/README.rst b/README.rst index 4681418..008e2a2 100644 --- a/README.rst +++ b/README.rst @@ -33,8 +33,8 @@ Configuration ``strict`` (default is False) refers to the ``strict`` option of ``mypy``. This option often is too strict to be useful. -``overrides`` (default is ``[]``) specifies a list of alternate or supplemental command-line options. - This modifies the options passed to ``mypy`` or the mypy-specific ones passed to ``dmypy run``. When present, the special boolean member ``True`` is replaced with the command-line options that would've been passed had ``overrides`` not been specified. +``overrides`` (default is ``[True]``) specifies a list of alternate or supplemental command-line options. + This modifies the options passed to ``mypy`` or the mypy-specific ones passed to ``dmypy run``. When present, the special boolean member ``True`` is replaced with the command-line options that would've been passed had ``overrides`` not been specified. Later options take precedence, which allows for replacing or negating individual default options (see ``mypy.main:process_options`` and ``mypy --help | grep inverse``). Depending on your editor, the configuration (found in a file called pylsp-mypy.cfg in your workspace or a parent directory) should be roughly like this for a standard configuration: diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 66d2daf..fec77ab 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -16,7 +16,7 @@ from pylsp import hookimpl from pylsp.workspace import Document, Workspace from pylsp.config.config import Config -from typing import Optional, Dict, Any, IO, List, Generator +from typing import Optional, Dict, Any, IO, List import atexit import collections import warnings @@ -95,13 +95,13 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[ return None -def apply_overrides(args: List[str], overrides: List[Any]) -> Generator[str, None, None]: - """Replace or combine overrides with command-line args.""" - for v in overrides: - if v is True: - yield from iter(args) - continue - yield v +def apply_overrides(args: List[str], overrides: List[Any]) -> List[str]: + """Replace or combine default command-line options with overrides.""" + o = iter(overrides) + if True not in o: + return overrides + rest = list(o) + return [*overrides[: -(len(rest) + 1)], *args, *rest] @hookimpl @@ -200,7 +200,7 @@ def pylsp_lint( if not dmypy: args.extend(["--incremental", "--follow-imports", "silent"]) if overrides: - args = list(apply_overrides(args, overrides)) + args = apply_overrides(args, overrides) log.info("executing mypy args = %s", args) completed_process = subprocess.run( @@ -222,7 +222,7 @@ def pylsp_lint( subprocess.run(["dmypy", "kill"]) # run to use existing daemon or restart if required - args = ["run", "--"] + (list(apply_overrides(args, overrides)) if overrides else args) + args = ["run", "--"] + (apply_overrides(args, overrides) if overrides else args) log.info("dmypy run args = %s", args) completed_process = subprocess.run( ["dmypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE diff --git a/test/test_plugin.py b/test/test_plugin.py index 7bdb29c..2e4d7d2 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -129,27 +129,25 @@ def foo(): def test_apply_overrides(): - assert list(plugin.apply_overrides(["1", "2"], [])) == [] - assert list(plugin.apply_overrides(["1", "2"], ["a"])) == ["a"] - assert list(plugin.apply_overrides(["1", "2"], ["a", True])) == ["a", "1", "2"] - assert list(plugin.apply_overrides(["1", "2"], [True, "a"])) == ["1", "2", "a"] - assert list(plugin.apply_overrides(["1"], ["a", True, "b"])) == ["a", "1", "b"] + assert plugin.apply_overrides(["1", "2"], []) == [] + assert plugin.apply_overrides(["1", "2"], ["a"]) == ["a"] + assert plugin.apply_overrides(["1", "2"], ["a", True]) == ["a", "1", "2"] + assert plugin.apply_overrides(["1", "2"], [True, "a"]) == ["1", "2", "a"] + assert plugin.apply_overrides(["1"], ["a", True, "b"]) == ["a", "1", "b"] def test_option_overrides(tmpdir, diag_mp, workspace): import sys - from textwrap import dedent sentinel = tmpdir / "ran" - source = dedent( - """\ - #!{} - import os, sys, pathlib - pathlib.Path({!r}).touch() - os.execv({!r}, sys.argv) - """ - ).format(sys.executable, str(sentinel), sys.executable) + source = """\ +#!{} +import os, sys, pathlib +pathlib.Path({!r}).touch() +os.execv({!r}, sys.argv)\n""" + + source = source.format(sys.executable, str(sentinel), sys.executable) wrapper = tmpdir / "bin/wrapper" wrapper.write(source, ensure=True) From a83609e042a7eedf534211d923799d1d8197f1ff Mon Sep 17 00:00:00 2001 From: "J.P. Neverwas" Date: Mon, 13 Dec 2021 17:26:51 -0800 Subject: [PATCH 071/110] fixup! fixup! Add option to override command line --- test/test_plugin.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index 2e4d7d2..a8f9511 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -138,20 +138,23 @@ def test_apply_overrides(): def test_option_overrides(tmpdir, diag_mp, workspace): import sys + from textwrap import dedent + from stat import S_IRWXU sentinel = tmpdir / "ran" - source = """\ -#!{} -import os, sys, pathlib -pathlib.Path({!r}).touch() -os.execv({!r}, sys.argv)\n""" - - source = source.format(sys.executable, str(sentinel), sys.executable) + source = dedent( + """\ + #!{} + import os, sys, pathlib + pathlib.Path({!r}).touch() + os.execv({!r}, sys.argv) + """ + ).format(sys.executable, str(sentinel), sys.executable) wrapper = tmpdir / "bin/wrapper" wrapper.write(source, ensure=True) - wrapper.chmod(0o700) + wrapper.chmod(S_IRWXU) overrides = ["--python-executable", wrapper.strpath, True] diag_mp.setattr( From 75e687536afc93b6ace1ec53ff7c814280e25288 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 19 Dec 2021 16:51:54 +0100 Subject: [PATCH 072/110] fix venvs --- pylsp_mypy/plugin.py | 69 ++++++++++++++++++++++++++++++++------------ 1 file changed, 51 insertions(+), 18 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 4568799..6eaf8c0 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -13,6 +13,7 @@ import subprocess from pathlib import Path import logging +from mypy import api as mypy_api from pylsp import hookimpl from pylsp.workspace import Document, Workspace from pylsp.config.config import Config @@ -20,6 +21,7 @@ import atexit import collections import warnings +import shutil line_pattern: str = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" @@ -189,33 +191,64 @@ def pylsp_lint( if not dmypy: args.extend(["--incremental", "--follow-imports", "silent"]) - log.info("executing mypy args = %s", args) - completed_process = subprocess.run( - ["mypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - report = completed_process.stdout.decode() - errors = completed_process.stderr.decode() + if shutil.which("mypy"): + # mypy exists on path + # -> use mypy on path + log.info("executing mypy args = %s on path", args) + completed_process = subprocess.run( + ["mypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + report = completed_process.stdout.decode() + errors = completed_process.stderr.decode() + else: + # mypy does not exist on path, but must exist in the env pylsp-mypy is installed in + # -> use mypy via api + log.info("executing mypy args = %s via api", args) + report, errors, _ = mypy_api.run(args) else: # If dmypy daemon is non-responsive calls to run will block. # Check daemon status, if non-zero daemon is dead or hung. # If daemon is hung, kill will reset # If daemon is dead/absent, kill will no-op. # In either case, reset to fresh state - completed_process = subprocess.run(["dmypy", *args], stderr=subprocess.PIPE) - _err = completed_process.stderr.decode() - _status = completed_process.returncode - if _status != 0: - log.info("restarting dmypy from status: %s message: %s", _status, _err.strip()) - subprocess.run(["dmypy", "kill"]) + if shutil.which("dmypy"): + # dmypy exists on path + # -> use mypy on path + completed_process = subprocess.run(["dmypy", *args], stderr=subprocess.PIPE) + _err = completed_process.stderr.decode() + _status = completed_process.returncode + if _status != 0: + log.info( + "restarting dmypy from status: %s message: %s via path", _status, _err.strip() + ) + subprocess.run(["dmypy", "kill"]) + else: + # dmypy does not exist on path, but must exist in the env pylsp-mypy is installed in + # -> use dmypy via api + _, _err, _status = mypy_api.run_dmypy(["status"]) + if _status != 0: + log.info( + "restarting dmypy from status: %s message: %s via api", _status, _err.strip() + ) + mypy_api.run_dmypy(["kill"]) # run to use existing daemon or restart if required args = ["run", "--"] + args - log.info("dmypy run args = %s", args) - completed_process = subprocess.run( - ["dmypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE - ) - report = completed_process.stdout.decode() - errors = completed_process.stderr.decode() + + if shutil.which("dmypy"): + # dmypy exists on path + # -> use mypy on path + log.info("dmypy run args = %s via path", args) + completed_process = subprocess.run( + ["dmypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + report = completed_process.stdout.decode() + errors = completed_process.stderr.decode() + else: + # dmypy does not exist on path, but must exist in the env pylsp-mypy is installed in + # -> use dmypy via api + log.info("dmypy run args = %s via api", args) + report, errors, _ = mypy_api.run_dmypy(args) log.debug("report:\n%s", report) log.debug("errors:\n%s", errors) From 934851b209e5c766bc2bc808fd83a6e16b2678d3 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 19 Dec 2021 17:48:54 +0100 Subject: [PATCH 073/110] closes #18 --- pylsp_mypy/plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 6eaf8c0..5f534d1 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -22,6 +22,7 @@ import collections import warnings import shutil +import ast line_pattern: str = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" @@ -307,7 +308,7 @@ def init(workspace: str) -> Dict[str, str]: path = findConfigFile(workspace, ["pylsp-mypy.cfg", "mypy-ls.cfg", "mypy_ls.cfg"]) if path: with open(path) as file: - configuration = eval(file.read()) + configuration = ast.literal_eval(file.read()) mypyConfigFile = findConfigFile(workspace, ["mypy.ini", ".mypy.ini"]) mypyConfigFileMap[workspace] = mypyConfigFile From 299f46b5756409fd16588a2741f59b4ccd35deac Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 19 Dec 2021 17:49:17 +0100 Subject: [PATCH 074/110] version bump --- pylsp_mypy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index 6b27eee..86716a7 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.5.4" +__version__ = "0.5.5" From 788c75af60998c922ff5c69e879a688bc14967fb Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 19 Dec 2021 19:12:35 +0100 Subject: [PATCH 075/110] Temporary fix to make tests run smoothly As far as I can tell, mypy does not handle unreachable warnings (which are used in some of the tests) correctly in version 0.920, therefore a version before that is enforced for now. --- requirements.txt | 2 +- setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index c9f670f..a2338b4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ python-lsp-server -mypy +mypy < 0.920 black pre-commit rstcheck diff --git a/setup.cfg b/setup.cfg index e6815aa..21f2e67 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ python_requires = >= 3.6 packages = find: install_requires = python-lsp-server - mypy + mypy < 0.920 [options.entry_points] From a5f340e6a13c976fbb40725062260c3a62650bf0 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 19 Dec 2021 19:28:21 +0100 Subject: [PATCH 076/110] version bump --- pylsp_mypy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index 86716a7..a779a44 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.5.5" +__version__ = "0.5.6" From 88965e78ea87189906eb885693bb22a2b45e508b Mon Sep 17 00:00:00 2001 From: Tobias Backer Dirks Date: Mon, 20 Dec 2021 11:52:56 +0200 Subject: [PATCH 077/110] chore: add missing config setting and allow mypy 0.920 --- requirements.txt | 2 +- test/test_plugin.py | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index a2338b4..c9f670f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ python-lsp-server -mypy < 0.920 +mypy black pre-commit rstcheck diff --git a/test/test_plugin.py b/test/test_plugin.py index e1c6bfd..99df218 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -103,7 +103,9 @@ def foo(): # Create configuration file for workspace folder 1. mypy_config = folder1.join("mypy.ini") - mypy_config.write("[mypy]\nwarn_unreachable = True") + mypy_config.write( + "[mypy]\nwarn_unreachable = True\ncheck_untyped_defs = True" + ) # Initialize settings for both folders. plugin.pylsp_settings(ws1._config) From 2086e84de2db70c49cb4755a8a689edf875d6081 Mon Sep 17 00:00:00 2001 From: Tobias Backer Dirks Date: Mon, 20 Dec 2021 13:40:11 +0200 Subject: [PATCH 078/110] chore: remove mypy version limit from setup.cfg --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 21f2e67..e6815aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,7 +21,7 @@ python_requires = >= 3.6 packages = find: install_requires = python-lsp-server - mypy < 0.920 + mypy [options.entry_points] From df2f52bba64ea3a226db207d166ae0fb3b66f6d6 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Mon, 20 Dec 2021 12:47:46 +0100 Subject: [PATCH 079/110] fix formatting The project has a line length of 100. The standard in black is shorter -> different formatting. We have pre-commit hooks for that --- test/test_plugin.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index 99df218..be8193d 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -103,9 +103,7 @@ def foo(): # Create configuration file for workspace folder 1. mypy_config = folder1.join("mypy.ini") - mypy_config.write( - "[mypy]\nwarn_unreachable = True\ncheck_untyped_defs = True" - ) + mypy_config.write("[mypy]\nwarn_unreachable = True\ncheck_untyped_defs = True") # Initialize settings for both folders. plugin.pylsp_settings(ws1._config) From ce3844f9278c852a7d700d94af38c3e5ca6da60c Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Tue, 21 Dec 2021 19:10:40 +0100 Subject: [PATCH 080/110] minor readability changes --- README.rst | 4 ++-- pylsp_mypy/plugin.py | 22 +++++++++++++--------- test/test_plugin.py | 3 ++- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/README.rst b/README.rst index 008e2a2..45a28f7 100644 --- a/README.rst +++ b/README.rst @@ -57,12 +57,12 @@ With ``dmypy`` enabled your config should look like this: "strict": False } -With ``overrides`` specified, your config should resemble this: +With ``overrides`` specified (for example to tell mypy to use a different python than the currently active venv), your config could look like this: :: { - ..., + "enabled": True, "overrides": ["--python-executable", "/home/me/bin/python", True] } diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index fec77ab..e799a2d 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -97,11 +97,14 @@ def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[ def apply_overrides(args: List[str], overrides: List[Any]) -> List[str]: """Replace or combine default command-line options with overrides.""" - o = iter(overrides) - if True not in o: + overrides_iterator = iter(overrides) + if True not in overrides_iterator: return overrides - rest = list(o) - return [*overrides[: -(len(rest) + 1)], *args, *rest] + # If True is in the list, the if above leaves the iterator at the element after True, + # therefore, the list below only contains the elements after the True + rest = list(overrides_iterator) + # slice of the True and the rest, add the args, add the rest + return overrides[: -(len(rest) + 1)] + args + rest @hookimpl @@ -195,12 +198,11 @@ def pylsp_lint( if settings.get("strict", False): args.append("--strict") - overrides = settings.get("overrides") + overrides = settings.get("overrides", [True]) if not dmypy: args.extend(["--incremental", "--follow-imports", "silent"]) - if overrides: - args = apply_overrides(args, overrides) + args = apply_overrides(args, overrides) log.info("executing mypy args = %s", args) completed_process = subprocess.run( @@ -214,7 +216,9 @@ def pylsp_lint( # If daemon is hung, kill will reset # If daemon is dead/absent, kill will no-op. # In either case, reset to fresh state - completed_process = subprocess.run(["dmypy", *args], stderr=subprocess.PIPE) + completed_process = subprocess.run( + ["dmypy", *apply_overrides(args, overrides)], stderr=subprocess.PIPE + ) _err = completed_process.stderr.decode() _status = completed_process.returncode if _status != 0: @@ -222,7 +226,7 @@ def pylsp_lint( subprocess.run(["dmypy", "kill"]) # run to use existing daemon or restart if required - args = ["run", "--"] + (apply_overrides(args, overrides) if overrides else args) + args = ["run", "--"] + apply_overrides(args, overrides) log.info("dmypy run args = %s", args) completed_process = subprocess.run( ["dmypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE diff --git a/test/test_plugin.py b/test/test_plugin.py index a8f9511..9eac5cc 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -5,6 +5,7 @@ from pylsp import uris from unittest.mock import Mock from pylsp_mypy import plugin +import collections DOC_URI = __file__ DOC_TYPE_ERR = """{}.append(3) @@ -18,7 +19,7 @@ @pytest.fixture def diag_mp(monkeypatch): - monkeypatch.setattr(plugin, "last_diagnostics", plugin.collections.defaultdict(list)) + monkeypatch.setattr(plugin, "last_diagnostics", collections.defaultdict(list)) return monkeypatch From 2957354f3b745937b279d68c677bae0605cc925f Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Tue, 21 Dec 2021 19:17:13 +0100 Subject: [PATCH 081/110] black format --- pylsp_mypy/plugin.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 361f378..9264ec8 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -231,7 +231,9 @@ def pylsp_lint( if shutil.which("dmypy"): # dmypy exists on path # -> use mypy on path - completed_process = subprocess.run(["dmypy", *apply_overrides(args, overrides)], stderr=subprocess.PIPE) + completed_process = subprocess.run( + ["dmypy", *apply_overrides(args, overrides)], stderr=subprocess.PIPE + ) _err = completed_process.stderr.decode() _status = completed_process.returncode if _status != 0: From d1e32d5ac64803f0bc415ce72ab9075615f2f31c Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Wed, 22 Dec 2021 11:54:41 +0100 Subject: [PATCH 082/110] more expressive name and doc --- test/test_plugin.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index 0ac366a..c9827a9 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -18,7 +18,8 @@ @pytest.fixture -def diag_mp(monkeypatch): +def last_diagnostics_monkeypatch(monkeypatch): + # gets called before every test altering last_diagnostics in order to reset it monkeypatch.setattr(plugin, "last_diagnostics", collections.defaultdict(list)) return monkeypatch @@ -45,7 +46,7 @@ def test_settings(): assert settings == {"plugins": {"pylsp_mypy": {}}} -def test_plugin(workspace, diag_mp): +def test_plugin(workspace, last_diagnostics_monkeypatch): config = FakeConfig() doc = Document(DOC_URI, workspace, DOC_TYPE_ERR) plugin.pylsp_settings(config) @@ -92,7 +93,7 @@ def test_parse_line_with_context(monkeypatch, word, bounds, workspace): assert diag["range"]["end"] == {"line": 278, "character": bounds[1]} -def test_multiple_workspaces(tmpdir, diag_mp): +def test_multiple_workspaces(tmpdir, last_diagnostics_monkeypatch): DOC_SOURCE = """ def foo(): return @@ -137,7 +138,7 @@ def test_apply_overrides(): assert plugin.apply_overrides(["1"], ["a", True, "b"]) == ["a", "1", "b"] -def test_option_overrides(tmpdir, diag_mp, workspace): +def test_option_overrides(tmpdir, last_diagnostics_monkeypatch, workspace): import sys from textwrap import dedent from stat import S_IRWXU @@ -158,7 +159,7 @@ def test_option_overrides(tmpdir, diag_mp, workspace): wrapper.chmod(S_IRWXU) overrides = ["--python-executable", wrapper.strpath, True] - diag_mp.setattr( + last_diagnostics_monkeypatch.setattr( FakeConfig, "plugin_settings", lambda _, p: {"overrides": overrides} if p == "pylsp_mypy" else {}, @@ -176,9 +177,9 @@ def test_option_overrides(tmpdir, diag_mp, workspace): assert sentinel.exists() -def test_option_overrides_dmypy(diag_mp, workspace): +def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace): overrides = ["--python-executable", "/tmp/fake", True] - diag_mp.setattr( + last_diagnostics_monkeypatch.setattr( FakeConfig, "plugin_settings", lambda _, p: { @@ -191,7 +192,7 @@ def test_option_overrides_dmypy(diag_mp, workspace): ) m = Mock(wraps=lambda a, **_: Mock(returncode=0, **{"stdout.decode": lambda: ""})) - diag_mp.setattr(plugin.subprocess, "run", m) + last_diagnostics_monkeypatch.setattr(plugin.subprocess, "run", m) plugin.pylsp_lint( config=FakeConfig(), From 0c61489c896bf5eae708f77e7ca5b48cc6f2da44 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Wed, 22 Dec 2021 22:44:09 +0100 Subject: [PATCH 083/110] version bump --- pylsp_mypy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index a779a44..1cc82e6 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.5.6" +__version__ = "0.5.7" From ab00c9f692d2b600a19ef70157f56c56b0c8c87f Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 16 Jan 2022 19:02:14 +0100 Subject: [PATCH 084/110] enforce isort --- .github/workflows/python-package.yml | 4 ++++ .pre-commit-config.yaml | 6 +++++- README.rst | 3 +++ pylsp_mypy/__init__.py | 1 - pylsp_mypy/plugin.py | 21 +++++++++++---------- pyproject.toml | 4 ++++ requirements.txt | 1 + setup.py | 1 + test/test_plugin.py | 13 +++++++------ 9 files changed, 36 insertions(+), 18 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e9f8c1b..9adddfd 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -38,6 +38,10 @@ jobs: run: | # stop the build if black detect any changes black --check . + - name: Check isort sorting + run: | + # stop the build if isort detect any changes + isort . --check --diff - name: Test with pytest run: | pytest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e797dce..b17dcbd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,4 +8,8 @@ repos: - repo: https://github.com/Lucas-C/pre-commit-hooks-markup rev: v1.0.1 hooks: - - id: rst-linter \ No newline at end of file + - id: rst-linter + - repo: https://github.com/pycqa/isort + rev: 5.10.1 + hooks: + - id: isort \ No newline at end of file diff --git a/README.rst b/README.rst index 45a28f7..ffc9e42 100644 --- a/README.rst +++ b/README.rst @@ -78,6 +78,8 @@ Install development dependencies with (you might want to create a virtualenv fir The project is formatted with `black`_. You can either configure your IDE to automatically format code with it, run it manually (``black .``) or rely on pre-commit (see below) to format files on git commit. +The project is formatted with `isort`_. You can either configure your IDE to automatically sort imports with it, run it manually (``isort .``) or rely on pre-commit (see below) to sort files on git commit. + The project uses two rst tests in order to assure uploadability to pypi: `rst-linter`_ as a pre-commit hook and `rstcheck`_ in a GitHub workflow. This does not catch all errors. This project uses `pre-commit`_ to enforce code-quality. After cloning the repository install the pre-commit hooks with: @@ -89,6 +91,7 @@ This project uses `pre-commit`_ to enforce code-quality. After cloning the repos After that pre-commit will run `all defined hooks`_ on every ``git commit`` and keep you from committing if there are any errors. .. _black: https://github.com/psf/black +.. _isort: https://github.com/PyCQA/isort .. _rst-linter: https://github.com/Lucas-C/pre-commit-hooks-markup .. _rstcheck: https://github.com/myint/rstcheck .. _pre-commit: https://pre-commit.com/ diff --git a/pylsp_mypy/__init__.py b/pylsp_mypy/__init__.py index 8b13789..e69de29 100644 --- a/pylsp_mypy/__init__.py +++ b/pylsp_mypy/__init__.py @@ -1 +0,0 @@ - diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 9264ec8..bbe0e4e 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -6,23 +6,24 @@ @author: Richard Kellnberger """ -import re -import tempfile +import ast +import atexit +import collections +import logging import os import os.path +import re +import shutil import subprocess +import tempfile +import warnings from pathlib import Path -import logging +from typing import IO, Any, Dict, List, Optional + from mypy import api as mypy_api from pylsp import hookimpl -from pylsp.workspace import Document, Workspace from pylsp.config.config import Config -from typing import Optional, Dict, Any, IO, List -import atexit -import collections -import warnings -import shutil -import ast +from pylsp.workspace import Document, Workspace line_pattern: str = r"((?:^[a-z]:)?[^:]+):(?:(\d+):)?(?:(\d+):)? (\w+): (.*)" diff --git a/pyproject.toml b/pyproject.toml index 6de812e..b341104 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,3 +12,7 @@ exclude = ''' | dist )/ ''' + +[tool.isort] +profile = "black" +line_length = 100 diff --git a/requirements.txt b/requirements.txt index c9f670f..7e2b896 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ mypy black pre-commit rstcheck +isort diff --git a/setup.py b/setup.py index a7df058..25246b1 100755 --- a/setup.py +++ b/setup.py @@ -1,5 +1,6 @@ #!/usr/bin/env python from setuptools import setup + from pylsp_mypy import _version if __name__ == "__main__": diff --git a/test/test_plugin.py b/test/test_plugin.py index c9827a9..260b27d 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,11 +1,12 @@ -import pytest +import collections +from unittest.mock import Mock -from pylsp.workspace import Workspace, Document -from pylsp.config.config import Config +import pytest from pylsp import uris -from unittest.mock import Mock +from pylsp.config.config import Config +from pylsp.workspace import Document, Workspace + from pylsp_mypy import plugin -import collections DOC_URI = __file__ DOC_TYPE_ERR = """{}.append(3) @@ -140,8 +141,8 @@ def test_apply_overrides(): def test_option_overrides(tmpdir, last_diagnostics_monkeypatch, workspace): import sys - from textwrap import dedent from stat import S_IRWXU + from textwrap import dedent sentinel = tmpdir / "ran" From 95009dd93983471d5040df3193c28e52f034993f Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 1 May 2022 15:59:30 +0200 Subject: [PATCH 085/110] update links --- LICENSE | 2 +- README.rst | 4 ++-- setup.cfg | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/LICENSE b/LICENSE index 17af835..b2d1d09 100644 --- a/LICENSE +++ b/LICENSE @@ -1,7 +1,7 @@ MIT License Copyright (c) 2017 Tom van Ommeren -Copyright (c) 2020 Richard Kellnberger +Copyright (c) 2022 Richard Kellnberger Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.rst b/README.rst index ffc9e42..f52c6f1 100644 --- a/README.rst +++ b/README.rst @@ -4,8 +4,8 @@ Mypy plugin for PYLSP .. image:: https://badge.fury.io/py/pylsp-mypy.svg :target: https://badge.fury.io/py/pylsp-mypy -.. image:: https://github.com/Richardk2n/pylsp-mypy/workflows/Python%20package/badge.svg?branch=master - :target: https://github.com/Richardk2n/pylsp-mypy/ +.. image:: https://github.com/python-lsp/pylsp-mypy/workflows/Python%20package/badge.svg?branch=master + :target: https://github.com/python-lsp/pylsp-mypy/ This is a plugin for the `Python LSP Server`_. diff --git a/setup.cfg b/setup.cfg index e6815aa..7fbb4aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,7 +2,7 @@ name = pylsp-mypy author = Tom van Ommeren, Richard Kellnberger description = Mypy linter for the Python LSP Server -url = https://github.com/Richardk2n/pylsp-mypy +url = https://github.com/python-lsp/pylsp-mypy long_description = file: README.rst license='MIT' classifiers = From 7382916f3adaaab07a60ad84ba6d5d2c62c06fe9 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 1 May 2022 18:14:30 +0200 Subject: [PATCH 086/110] toml support --- pylsp_mypy/plugin.py | 24 ++++++++++++++++++++---- pyproject.toml | 5 +++++ requirements.txt | 1 + setup.cfg | 1 + 4 files changed, 27 insertions(+), 4 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index bbe0e4e..e2062fb 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -20,6 +20,7 @@ from pathlib import Path from typing import IO, Any, Dict, List, Optional +import toml from mypy import api as mypy_api from pylsp import hookimpl from pylsp.config.config import Config @@ -324,12 +325,17 @@ def init(workspace: str) -> Dict[str, str]: log.info("init workspace = %s", workspace) configuration = {} - path = findConfigFile(workspace, ["pylsp-mypy.cfg", "mypy-ls.cfg", "mypy_ls.cfg"]) + path = findConfigFile( + workspace, ["pylsp-mypy.cfg", "mypy-ls.cfg", "mypy_ls.cfg", "pyproject.toml"] + ) if path: - with open(path) as file: - configuration = ast.literal_eval(file.read()) + if "pyproject.toml" in path: + configuration = toml.load(path).get("tool").get("pylsp-mypy") + else: + with open(path) as file: + configuration = ast.literal_eval(file.read()) - mypyConfigFile = findConfigFile(workspace, ["mypy.ini", ".mypy.ini"]) + mypyConfigFile = findConfigFile(workspace, ["mypy.ini", ".mypy.ini", "pyproject.toml"]) mypyConfigFileMap[workspace] = mypyConfigFile log.info("mypyConfigFile = %s configuration = %s", mypyConfigFile, configuration) @@ -368,6 +374,16 @@ def findConfigFile(path: str, names: List[str]) -> Optional[str]: "config file to pylsp-mypy.cfg" ) ) + if file.name == "pyproject.toml": + isPluginConfig = "pylsp-mypy.cfg" in names + configPresent = ( + toml.load(file) + .get("tool", {}) + .get("pylsp-mypy" if isPluginConfig else "mypy") + is None + ) + if not configPresent: + continue return str(file) return None diff --git a/pyproject.toml b/pyproject.toml index b341104..bbd1315 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,3 +16,8 @@ exclude = ''' [tool.isort] profile = "black" line_length = 100 + +[tool.pylsp-mypy] +enabled = true +live_mode = true +strict = true diff --git a/requirements.txt b/requirements.txt index 7e2b896..8a82e68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,6 @@ python-lsp-server mypy +toml black pre-commit rstcheck diff --git a/setup.cfg b/setup.cfg index 7fbb4aa..1db5216 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,7 @@ packages = find: install_requires = python-lsp-server mypy + toml [options.entry_points] From 886fc3d8a5e21ab81241daf9c0ae8ddcdac9b579 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 1 May 2022 19:57:34 +0200 Subject: [PATCH 087/110] minor correction --- pylsp_mypy/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index e2062fb..6ff9c3d 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -380,7 +380,7 @@ def findConfigFile(path: str, names: List[str]) -> Optional[str]: toml.load(file) .get("tool", {}) .get("pylsp-mypy" if isPluginConfig else "mypy") - is None + is not None ) if not configPresent: continue From 06f841d8393cddeda09c8fd7d92f45b0095ee4f9 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 1 May 2022 19:58:44 +0200 Subject: [PATCH 088/110] use toml --- mypy.ini | 8 -------- pyproject.toml | 11 +++++++++++ requirements.txt | 1 + 3 files changed, 12 insertions(+), 8 deletions(-) delete mode 100644 mypy.ini diff --git a/mypy.ini b/mypy.ini deleted file mode 100644 index 6ef17d8..0000000 --- a/mypy.ini +++ /dev/null @@ -1,8 +0,0 @@ -[mypy] -python_version = 3.6 - -[mypy-pylsp.*] -ignore_missing_imports = True - -[mypy-pylsp_mypy.plugin] - disallow_untyped_decorators = False \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index bbd1315..c10b6f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,3 +21,14 @@ line_length = 100 enabled = true live_mode = true strict = true + +[tool.mypy] +python_version = "3.6" + +[[tool.mypy.overrides]] +module = "pylsp.*" +ignore_missing_imports = true + +[[tool.mypy.overrides]] +module = "pylsp_mypy.plugin" +disallow_untyped_decorators = false diff --git a/requirements.txt b/requirements.txt index 8a82e68..1b9c2b2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ python-lsp-server mypy toml +types-toml black pre-commit rstcheck From 8b141d255ce3489f73f70dda2ffe33ae799b8a6c Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 1 May 2022 20:19:05 +0200 Subject: [PATCH 089/110] error in unittest With C: the code detects the parent directory to be cwd and in cwd there is a config file which therefore gets detected resulting in the unittest failing --- test/test_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index 260b27d..c8dc000 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,4 +1,5 @@ import collections +import os from unittest.mock import Mock import pytest @@ -35,7 +36,7 @@ def workspace(tmpdir): class FakeConfig(object): def __init__(self): - self._root_path = "C:" + self._root_path = "C:" if os.name == "nt" else "/" def plugin_settings(self, plugin, document_path=None): return {} From 478562ca10cc055e6a92264c6f9802c68bc47753 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 15 May 2022 10:51:12 +0200 Subject: [PATCH 090/110] version bump --- pylsp_mypy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index 1cc82e6..fc0a843 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.5.7" +__version__ = "0.5.8" From b99622bcfa5d141965a03f4241217a7ea18d16ae Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 17:40:57 +0200 Subject: [PATCH 091/110] Closes #36 --- .pre-commit-config.yaml | 2 +- pylsp_mypy/_version.py | 2 +- pylsp_mypy/plugin.py | 16 ++++++++++++---- pyproject.toml | 2 +- setup.cfg | 3 +-- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b17dcbd..220c85e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/psf/black - rev: 21.6b0 + rev: 22.3.0 hooks: - id: black - repo: https://github.com/Lucas-C/pre-commit-hooks-markup diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index fc0a843..906d362 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.5.8" +__version__ = "0.6.0" diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 6ff9c3d..c347178 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -42,6 +42,14 @@ # https://github.com/python-lsp/python-lsp-server/blob/v1.0.1/pylsp/plugins/pylint_lint.py#L55-L62 last_diagnostics: Dict[str, List[Dict[str, Any]]] = collections.defaultdict(list) +# Windows started opening opening a cmd-like window for every subprocess call +# This flag prevents that. +# This flag is new in python 3.7 +# THis flag only exists on Windows +windows_flag: Dict[str, int] = ( + {"startupinfo": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {} # type: ignore +) + def parse_line(line: str, document: Optional[Document] = None) -> Optional[Dict[str, Any]]: """ @@ -214,7 +222,7 @@ def pylsp_lint( # -> use mypy on path log.info("executing mypy args = %s on path", args) completed_process = subprocess.run( - ["mypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ["mypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE, **windows_flag ) report = completed_process.stdout.decode() errors = completed_process.stderr.decode() @@ -234,7 +242,7 @@ def pylsp_lint( # dmypy exists on path # -> use mypy on path completed_process = subprocess.run( - ["dmypy", *apply_overrides(args, overrides)], stderr=subprocess.PIPE + ["dmypy", *apply_overrides(args, overrides)], stderr=subprocess.PIPE, **windows_flag ) _err = completed_process.stderr.decode() _status = completed_process.returncode @@ -242,7 +250,7 @@ def pylsp_lint( log.info( "restarting dmypy from status: %s message: %s via path", _status, _err.strip() ) - subprocess.run(["dmypy", "kill"]) + subprocess.run(["dmypy", "kill"], **windows_flag) else: # dmypy does not exist on path, but must exist in the env pylsp-mypy is installed in # -> use dmypy via api @@ -261,7 +269,7 @@ def pylsp_lint( # -> use mypy on path log.info("dmypy run args = %s via path", args) completed_process = subprocess.run( - ["dmypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ["dmypy", *args], stdout=subprocess.PIPE, stderr=subprocess.PIPE, **windows_flag ) report = completed_process.stdout.decode() errors = completed_process.stderr.decode() diff --git a/pyproject.toml b/pyproject.toml index c10b6f2..b09d6f7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ live_mode = true strict = true [tool.mypy] -python_version = "3.6" +python_version = "3.7" [[tool.mypy.overrides]] module = "pylsp.*" diff --git a/setup.cfg b/setup.cfg index 1db5216..af119a8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -10,14 +10,13 @@ classifiers = Intended Audience :: Developers Topic :: Software Development License :: OSI Approved :: MIT License - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 [options] -python_requires = >= 3.6 +python_requires = >= 3.7 packages = find: install_requires = python-lsp-server From 89fb20d87a3d080fc683e7ef82cb6322ed79572d Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 18:52:48 +0200 Subject: [PATCH 092/110] Drop 3.6 --- .github/workflows/python-package.yml | 2 +- README.rst | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 9adddfd..f9ebda9 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -15,7 +15,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.6", "3.7", "3.8", "3.9", "3.10"] + python-version: ["3.7", "3.8", "3.9", "3.10"] steps: - uses: actions/checkout@v2 diff --git a/README.rst b/README.rst index f52c6f1..0187a48 100644 --- a/README.rst +++ b/README.rst @@ -11,7 +11,7 @@ This is a plugin for the `Python LSP Server`_. .. _`Python LSP Server`: https://github.com/python-lsp/python-lsp-server -It, like mypy, requires Python 3.6 or newer. +It, like mypy, requires Python 3.7 or newer. Installation @@ -36,6 +36,16 @@ Configuration ``overrides`` (default is ``[True]``) specifies a list of alternate or supplemental command-line options. This modifies the options passed to ``mypy`` or the mypy-specific ones passed to ``dmypy run``. When present, the special boolean member ``True`` is replaced with the command-line options that would've been passed had ``overrides`` not been specified. Later options take precedence, which allows for replacing or negating individual default options (see ``mypy.main:process_options`` and ``mypy --help | grep inverse``). +This project supports the use of ``pyproject.toml`` for configuration. It is in fact the preferred way. Using that your configuration could look like this: + +:: + + [tool.pylsp-mypy] + enabled = true + live_mode = true + strict = true + +A ``pyproject.toml`` does not conflict with the legacy config file given that it does not contain a ``pylsp-mypy`` section. The following explanation uses the syntax of the legacy config file. However, all these options also apply to the ``pyproject.toml`` configuration (note the lowercase bools). Depending on your editor, the configuration (found in a file called pylsp-mypy.cfg in your workspace or a parent directory) should be roughly like this for a standard configuration: :: From 6d52a8774d11397e6cb1e142cac03f41a0bc7b0f Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 19:01:55 +0200 Subject: [PATCH 093/110] Phase out old names --- pylsp_mypy/plugin.py | 21 +++++++-------------- 1 file changed, 7 insertions(+), 14 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index c347178..73c1130 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -16,7 +16,6 @@ import shutil import subprocess import tempfile -import warnings from pathlib import Path from typing import IO, Any, Dict, List, Optional @@ -147,17 +146,13 @@ def pylsp_lint( settings = config.plugin_settings("pylsp_mypy") oldSettings1 = config.plugin_settings("mypy-ls") if oldSettings1 != {}: - warnings.warn( - DeprecationWarning( - "Your configuration uses the namespace mypy-ls, this should be changed to pylsp_mypy" - ) + raise DeprecationWarning( + "Your configuration uses the namespace mypy-ls, this should be changed to pylsp_mypy" ) oldSettings2 = config.plugin_settings("mypy_ls") if oldSettings2 != {}: - warnings.warn( - DeprecationWarning( - "Your configuration uses the namespace mypy_ls, this should be changed to pylsp_mypy" - ) + raise DeprecationWarning( + "Your configuration uses the namespace mypy_ls, this should be changed to pylsp_mypy" ) if settings == {}: settings = oldSettings1 @@ -376,11 +371,9 @@ def findConfigFile(path: str, names: List[str]) -> Optional[str]: file = parent.joinpath(name) if file.is_file(): if file.name in ["mypy-ls.cfg", "mypy_ls.cfg"]: - warnings.warn( - DeprecationWarning( - f"{str(file)}: {file.name} is no longer supported, you should rename your " - "config file to pylsp-mypy.cfg" - ) + raise DeprecationWarning( + f"{str(file)}: {file.name} is no longer supported, you should rename your " + "config file to pylsp-mypy.cfg or preferably use a pyproject.toml instead." ) if file.name == "pyproject.toml": isPluginConfig = "pylsp-mypy.cfg" in names From 8e05a357ed3ce82e8cd1304050821e61540558b6 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 19:03:11 +0200 Subject: [PATCH 094/110] version bump --- pylsp_mypy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index 906d362..43c4ab0 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.6.0" +__version__ = "0.6.1" From 2951d143b735842efc506aca1431029f12c0b6fe Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 19:52:23 +0200 Subject: [PATCH 095/110] correct flag --- pylsp_mypy/_version.py | 2 +- pylsp_mypy/plugin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index 43c4ab0..22049ab 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.6.1" +__version__ = "0.6.2" diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 73c1130..5d037f2 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -46,7 +46,7 @@ # This flag is new in python 3.7 # THis flag only exists on Windows windows_flag: Dict[str, int] = ( - {"startupinfo": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {} # type: ignore + {"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {} # type: ignore ) From 574c28e8764bd8ae6d037c13be7efeb32263337b Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 20:02:21 +0200 Subject: [PATCH 096/110] ALso test on windows --- .github/workflows/python-package.yml | 3 ++- setup.cfg | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index f9ebda9..330505c 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -12,10 +12,11 @@ on: jobs: testCode: - runs-on: ubuntu-latest strategy: matrix: + os: [ubuntu-latest, windows-latest] python-version: ["3.7", "3.8", "3.9", "3.10"] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v2 diff --git a/setup.cfg b/setup.cfg index af119a8..b4ecef4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pylsp-mypy -author = Tom van Ommeren, Richard Kellnberger +author = Richard Kellnberger, Tom van Ommeren description = Mypy linter for the Python LSP Server url = https://github.com/python-lsp/pylsp-mypy long_description = file: README.rst From 6cdf1aaf2f5a131bf69d61292b5680845eeb6ab0 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 20:13:36 +0200 Subject: [PATCH 097/110] test action --- .github/workflows/python-package.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 330505c..654d28d 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -28,7 +28,8 @@ jobs: run: | python -m pip install --upgrade pip pip install flake8 pytest - if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - if: -f requirements.txt + run: pip install -r requirements.txt - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From c9d3b2865b107a042c1a58f48e47011c0d767f91 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 20:16:20 +0200 Subject: [PATCH 098/110] action test2 --- .github/workflows/python-package.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 654d28d..118956c 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -28,8 +28,8 @@ jobs: run: | python -m pip install --upgrade pip pip install flake8 pytest - - if: -f requirements.txt - run: pip install -r requirements.txt + - if: ${{ matrix.os == ubuntu-latest }} + run: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From 49edaebea638461acf1620f3c2fe4cf560c510fc Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 20:22:30 +0200 Subject: [PATCH 099/110] fix action --- .github/workflows/python-package.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 118956c..e56df49 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -10,6 +10,10 @@ on: branches: [ master ] jobs: + defaults: + run: + shell: bash + testCode: strategy: @@ -28,8 +32,7 @@ jobs: run: | python -m pip install --upgrade pip pip install flake8 pytest - - if: ${{ matrix.os == ubuntu-latest }} - run: if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names From 3d0cac44bbf17eb147e998a28039e12538ef1c76 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 20:27:10 +0200 Subject: [PATCH 100/110] apply defaults --- .github/workflows/python-package.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index e56df49..2bf7b1f 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -9,10 +9,11 @@ on: pull_request: branches: [ master ] +defaults: + run: + shell: bash + jobs: - defaults: - run: - shell: bash testCode: From 1a47bf3d07ace79c0c49afefaf0acbfe618cba36 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sat, 2 Jul 2022 21:44:23 +0200 Subject: [PATCH 101/110] fix tests --- test/test_plugin.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index c8dc000..ce7ccab 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -1,5 +1,8 @@ import collections import os +import subprocess +from pathlib import Path +from typing import Dict from unittest.mock import Mock import pytest @@ -9,7 +12,7 @@ from pylsp_mypy import plugin -DOC_URI = __file__ +DOC_URI = f"file:/{Path(__file__)}" DOC_TYPE_ERR = """{}.append(3) """ TYPE_ERR_MSG = '"Dict[, ]" has no attribute "append"' @@ -18,6 +21,10 @@ TEST_LINE_WITHOUT_COL = "test_plugin.py:279: " 'error: "Request" has no attribute "id"' TEST_LINE_WITHOUT_LINE = "test_plugin.py: " 'error: "Request" has no attribute "id"' +windows_flag: Dict[str, int] = ( + {"creationflags": subprocess.CREATE_NO_WINDOW} if os.name == "nt" else {} # type: ignore +) + @pytest.fixture def last_diagnostics_monkeypatch(monkeypatch): @@ -196,10 +203,12 @@ def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace): m = Mock(wraps=lambda a, **_: Mock(returncode=0, **{"stdout.decode": lambda: ""})) last_diagnostics_monkeypatch.setattr(plugin.subprocess, "run", m) + document = Document(DOC_URI, workspace, DOC_TYPE_ERR) + plugin.pylsp_lint( config=FakeConfig(), workspace=workspace, - document=Document(DOC_URI, workspace, DOC_TYPE_ERR), + document=document, is_saved=False, ) expected = [ @@ -209,6 +218,6 @@ def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace): "--python-executable", "/tmp/fake", "--show-column-numbers", - __file__, + document.path, ] - m.assert_called_with(expected, stderr=-1, stdout=-1) + m.assert_called_with(expected, stderr=-1, stdout=-1, **windows_flag) From bcb77d1cd753088c203b8426c3c66d5db72fbde8 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 3 Jul 2022 16:05:53 +0200 Subject: [PATCH 102/110] Exclude non compatible test --- test/test_plugin.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index ce7ccab..fecf95c 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -43,7 +43,7 @@ def workspace(tmpdir): class FakeConfig(object): def __init__(self): - self._root_path = "C:" if os.name == "nt" else "/" + self._root_path = "C://" if os.name == "nt" else "/" def plugin_settings(self, plugin, document_path=None): return {} @@ -147,6 +147,7 @@ def test_apply_overrides(): assert plugin.apply_overrides(["1"], ["a", True, "b"]) == ["a", "1", "b"] +@pytest.mark.skipif(os.name == "nt", reason = "Not working on Windows due to test design.") def test_option_overrides(tmpdir, last_diagnostics_monkeypatch, workspace): import sys from stat import S_IRWXU From 5c1bd61c4edaf0183a3bf242d9ad7e1be75e3a3e Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 3 Jul 2022 16:09:20 +0200 Subject: [PATCH 103/110] Format --- test/test_plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_plugin.py b/test/test_plugin.py index fecf95c..e8e197a 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -147,7 +147,7 @@ def test_apply_overrides(): assert plugin.apply_overrides(["1"], ["a", True, "b"]) == ["a", "1", "b"] -@pytest.mark.skipif(os.name == "nt", reason = "Not working on Windows due to test design.") +@pytest.mark.skipif(os.name == "nt", reason="Not working on Windows due to test design.") def test_option_overrides(tmpdir, last_diagnostics_monkeypatch, workspace): import sys from stat import S_IRWXU From 03975b7710ab8df20a3166b2a3000dafc561347b Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Fri, 16 Sep 2022 20:06:24 +0200 Subject: [PATCH 104/110] Expose generic mypy error as diagnostic --- .github/workflows/python-package.yml | 4 ++-- pylsp_mypy/plugin.py | 16 ++++++++++++++++ setup.cfg | 3 +++ 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 2bf7b1f..fe48ca1 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -38,8 +38,8 @@ jobs: run: | # stop the build if there are Python syntax errors or undefined names flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + # exit-zero treats all errors as warnings + flake8 . --count --exit-zero --statistics - name: Check black formatting run: | # stop the build if black detect any changes diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 5d037f2..7934dce 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -278,6 +278,22 @@ def pylsp_lint( log.debug("errors:\n%s", errors) diagnostics = [] + + # Expose generic mypy error on the first line. + if errors: + diagnostics.append( + { + "source": "mypy", + "range": { + "start": {"line": 0, "character": 0}, + # Client is supposed to clip end column to line length. + "end": {"line": 0, "character": 1000}, + }, + "message": errors, + "severity": 1, # Error + } + ) + for line in report.splitlines(): log.debug("parsing: line = %r", line) diag = parse_line(line, document) diff --git a/setup.cfg b/setup.cfg index b4ecef4..85b0498 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,6 +23,9 @@ install_requires = mypy toml +[flake8] +max-complexity = 10 +max-line-length = 127 [options.entry_points] pylsp = pylsp_mypy = pylsp_mypy.plugin From 2e5bfb953fdca7b2df3b3a4b6c7211f7cb0fb9f2 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 22 Sep 2022 17:26:28 +0200 Subject: [PATCH 105/110] create error or warning depending on process return code --- pylsp_mypy/plugin.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 7934dce..0e18d55 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -207,6 +207,7 @@ def pylsp_lint( args.append("--strict") overrides = settings.get("overrides", [True]) + exit_status = 0 if not dmypy: args.extend(["--incremental", "--follow-imports", "silent"]) @@ -221,11 +222,12 @@ def pylsp_lint( ) report = completed_process.stdout.decode() errors = completed_process.stderr.decode() + exit_status = completed_process.returncode else: # mypy does not exist on path, but must exist in the env pylsp-mypy is installed in # -> use mypy via api log.info("executing mypy args = %s via api", args) - report, errors, _ = mypy_api.run(args) + report, errors, exit_status = mypy_api.run(args) else: # If dmypy daemon is non-responsive calls to run will block. # Check daemon status, if non-zero daemon is dead or hung. @@ -239,20 +241,20 @@ def pylsp_lint( completed_process = subprocess.run( ["dmypy", *apply_overrides(args, overrides)], stderr=subprocess.PIPE, **windows_flag ) - _err = completed_process.stderr.decode() - _status = completed_process.returncode - if _status != 0: + errors = completed_process.stderr.decode() + exit_status = completed_process.returncode + if exit_status != 0: log.info( - "restarting dmypy from status: %s message: %s via path", _status, _err.strip() + "restarting dmypy from status: %s message: %s via path", exit_status, errors.strip() ) subprocess.run(["dmypy", "kill"], **windows_flag) else: # dmypy does not exist on path, but must exist in the env pylsp-mypy is installed in # -> use dmypy via api - _, _err, _status = mypy_api.run_dmypy(["status"]) - if _status != 0: + _, errors, exit_status = mypy_api.run_dmypy(["status"]) + if exit_status != 0: log.info( - "restarting dmypy from status: %s message: %s via api", _status, _err.strip() + "restarting dmypy from status: %s message: %s via api", exit_status, errors.strip() ) mypy_api.run_dmypy(["kill"]) @@ -268,11 +270,12 @@ def pylsp_lint( ) report = completed_process.stdout.decode() errors = completed_process.stderr.decode() + exit_status = completed_process.returncode else: # dmypy does not exist on path, but must exist in the env pylsp-mypy is installed in # -> use dmypy via api log.info("dmypy run args = %s via api", args) - report, errors, _ = mypy_api.run_dmypy(args) + report, errors, exit_status = mypy_api.run_dmypy(args) log.debug("report:\n%s", report) log.debug("errors:\n%s", errors) @@ -290,7 +293,7 @@ def pylsp_lint( "end": {"line": 0, "character": 1000}, }, "message": errors, - "severity": 1, # Error + "severity": 1 if exit_status != 0 else 2, # Error if exited with error or warning. } ) From ae88f1d3c44843e7eefab16b977b2a3c785ec560 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 22 Sep 2022 17:30:06 +0200 Subject: [PATCH 106/110] black reformat --- pylsp_mypy/plugin.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 0e18d55..7ad35f4 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -245,7 +245,9 @@ def pylsp_lint( exit_status = completed_process.returncode if exit_status != 0: log.info( - "restarting dmypy from status: %s message: %s via path", exit_status, errors.strip() + "restarting dmypy from status: %s message: %s via path", + exit_status, + errors.strip(), ) subprocess.run(["dmypy", "kill"], **windows_flag) else: @@ -254,7 +256,9 @@ def pylsp_lint( _, errors, exit_status = mypy_api.run_dmypy(["status"]) if exit_status != 0: log.info( - "restarting dmypy from status: %s message: %s via api", exit_status, errors.strip() + "restarting dmypy from status: %s message: %s via api", + exit_status, + errors.strip(), ) mypy_api.run_dmypy(["kill"]) From d3b86abddb1c76cf2baf45c914c06b5d4e75a492 Mon Sep 17 00:00:00 2001 From: Rafal Chlodnicki Date: Thu, 22 Sep 2022 17:48:20 +0200 Subject: [PATCH 107/110] 100 column limit --- setup.cfg | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 85b0498..3bbe0db 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,8 +24,8 @@ install_requires = toml [flake8] -max-complexity = 10 -max-line-length = 127 +max-complexity = 20 +max-line-length = 100 [options.entry_points] pylsp = pylsp_mypy = pylsp_mypy.plugin From 20db63a351de4dd5c519c5aff57b8c799aed2b3a Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 9 Oct 2022 20:46:28 +0200 Subject: [PATCH 108/110] Detect all errors --- .github/workflows/python-package.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index fe48ca1..0ca9e3a 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -37,9 +37,7 @@ jobs: - name: Lint with flake8 run: | # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics - # exit-zero treats all errors as warnings - flake8 . --count --exit-zero --statistics + flake8 . --count --show-source --statistics - name: Check black formatting run: | # stop the build if black detect any changes From 81cf8f4897871e5f57dd653c4ae241a34c135cb0 Mon Sep 17 00:00:00 2001 From: Richard Kellnberger Date: Sun, 9 Oct 2022 21:01:05 +0200 Subject: [PATCH 109/110] version bump --- pylsp_mypy/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pylsp_mypy/_version.py b/pylsp_mypy/_version.py index 22049ab..63af887 100644 --- a/pylsp_mypy/_version.py +++ b/pylsp_mypy/_version.py @@ -1 +1 @@ -__version__ = "0.6.2" +__version__ = "0.6.3" From cff0e1954423e3283f9a11581e7bee49bec8084d Mon Sep 17 00:00:00 2001 From: hetmankp Date: Thu, 13 Oct 2022 17:25:32 +1100 Subject: [PATCH 110/110] Create new config option 'config_names' This configuration option allows us to specify additional configuration file names under which the mypy config could be found. --- README.rst | 11 ++++++++ pylsp_mypy/plugin.py | 63 +++++++++++++++++++++++++++++++++----------- test/test_plugin.py | 31 ++++++++++++++++++++++ 3 files changed, 89 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index 0187a48..08fd19f 100644 --- a/README.rst +++ b/README.rst @@ -36,6 +36,8 @@ Configuration ``overrides`` (default is ``[True]``) specifies a list of alternate or supplemental command-line options. This modifies the options passed to ``mypy`` or the mypy-specific ones passed to ``dmypy run``. When present, the special boolean member ``True`` is replaced with the command-line options that would've been passed had ``overrides`` not been specified. Later options take precedence, which allows for replacing or negating individual default options (see ``mypy.main:process_options`` and ``mypy --help | grep inverse``). +``config_names`` (default is ``[]``) specifies alternate file names under which the mypy configuration may be found. + This project supports the use of ``pyproject.toml`` for configuration. It is in fact the preferred way. Using that your configuration could look like this: :: @@ -76,6 +78,15 @@ With ``overrides`` specified (for example to tell mypy to use a different python "overrides": ["--python-executable", "/home/me/bin/python", True] } +With ``config_files`` your config could look like this: + +:: + + { + "enabled": True, + "config_files": [".config/mypy.ini"] + } + Developing ------------- diff --git a/pylsp_mypy/plugin.py b/pylsp_mypy/plugin.py index 7ad35f4..cb4e542 100644 --- a/pylsp_mypy/plugin.py +++ b/pylsp_mypy/plugin.py @@ -119,28 +119,19 @@ def apply_overrides(args: List[str], overrides: List[Any]) -> List[str]: return overrides[: -(len(rest) + 1)] + args + rest -@hookimpl -def pylsp_lint( - config: Config, workspace: Workspace, document: Document, is_saved: bool -) -> List[Dict[str, Any]]: +def _get_settings(config: Config) -> Any: """ - Lints. + Get settings checking for deprecated setting locations. Parameters ---------- config : Config The pylsp config. - workspace : Workspace - The pylsp workspace. - document : Document - The document to be linted. - is_saved : bool - Weather the document is saved. Returns ------- - List[Dict[str, Any]] - List of the linting data. + Dict[str, Any] + Mypy settings to use. """ settings = config.plugin_settings("pylsp_mypy") @@ -159,6 +150,34 @@ def pylsp_lint( if settings == {}: settings = oldSettings2 + return settings + + +@hookimpl +def pylsp_lint( + config: Config, workspace: Workspace, document: Document, is_saved: bool +) -> List[Dict[str, Any]]: + """ + Lints. + + Parameters + ---------- + config : Config + The pylsp config. + workspace : Workspace + The pylsp workspace. + document : Document + The document to be linted. + is_saved : bool + Weather the document is saved. + + Returns + ------- + List[Dict[str, Any]] + List of the linting data. + + """ + settings = _get_settings(config) log.info( "lint settings = %s document.path = %s is_saved = %s", settings, @@ -329,11 +348,20 @@ def pylsp_settings(config: Config) -> Dict[str, Dict[str, Dict[str, str]]]: The config dict. """ - configuration = init(config._root_path) + + settings = _get_settings(config) + log.info( + "initialization settings = %s", + settings, + ) + + config_names = settings.get("config_names", []) + + configuration = init(config._root_path, config_names) return {"plugins": {"pylsp_mypy": configuration}} -def init(workspace: str) -> Dict[str, str]: +def init(workspace: str, config_names: List[str] = []) -> Dict[str, str]: """ Find plugin and mypy config files and creates the temp file should it be used. @@ -341,6 +369,8 @@ def init(workspace: str) -> Dict[str, str]: ---------- workspace : str The path to the current workspace. + config_names : List[str] + List of configuration file names that will be checked for first. Returns ------- @@ -361,7 +391,8 @@ def init(workspace: str) -> Dict[str, str]: with open(path) as file: configuration = ast.literal_eval(file.read()) - mypyConfigFile = findConfigFile(workspace, ["mypy.ini", ".mypy.ini", "pyproject.toml"]) + possibleNames = config_names + ["mypy.ini", ".mypy.ini", "pyproject.toml"] + mypyConfigFile = findConfigFile(workspace, possibleNames) mypyConfigFileMap[workspace] = mypyConfigFile log.info("mypyConfigFile = %s configuration = %s", mypyConfigFile, configuration) diff --git a/test/test_plugin.py b/test/test_plugin.py index e8e197a..2583ed0 100644 --- a/test/test_plugin.py +++ b/test/test_plugin.py @@ -222,3 +222,34 @@ def test_option_overrides_dmypy(last_diagnostics_monkeypatch, workspace): document.path, ] m.assert_called_with(expected, stderr=-1, stdout=-1, **windows_flag) + + +def test_config_names(tmpdir, last_diagnostics_monkeypatch): + DOC_SOURCE = """ +def foo(): + return + unreachable = 1 +""" + DOC_ERR_MSG = "Statement is unreachable" + + config_names = [".config/mypy.ini"] + + # Initialize workspace. + ws = Workspace(uris.from_fs_path(str(tmpdir)), Mock()) + ws._config = Config(ws.root_uri, {}, 0, {}) + ws._config.update({"plugins": {"pylsp_mypy": {"config_names": config_names}}}) + + # Create configuration file for workspace. + config_dir = tmpdir.mkdir(".config") + mypy_config = config_dir.join("mypy.ini") + mypy_config.write("[mypy]\nwarn_unreachable = True\ncheck_untyped_defs = True") + + # Initialize settings for workspace. + plugin.pylsp_settings(ws._config) + + # Test document to make sure it uses .config/mypy.ini configuration. + doc = Document(DOC_URI, ws, DOC_SOURCE) + diags = plugin.pylsp_lint(ws._config, ws, doc, is_saved=False) + assert len(diags) == 1 + diag = diags[0] + assert diag["message"] == DOC_ERR_MSG